blob: 2c97df018eed5147dd55b1f58bd1edeaf2b976fd [file] [log] [blame]
/**
* @license
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {pluginLoader} from './gr-plugin-loader.js';
import {importHref} from '../../../scripts/import-href.js';
/** @constructor */
export class GrPluginEndpoints {
constructor() {
this._endpoints = {};
this._callbacks = {};
this._dynamicPlugins = {};
this._importedUrls = new Set();
}
onNewEndpoint(endpoint, callback) {
if (!this._callbacks[endpoint]) {
this._callbacks[endpoint] = [];
}
this._callbacks[endpoint].push(callback);
}
onDetachedEndpoint(endpoint, callback) {
if (this._callbacks[endpoint]) {
this._callbacks[endpoint] = this._callbacks[endpoint].filter(
cb => cb !== callback
);
}
}
_getOrCreateModuleInfo(plugin, opts) {
const {endpoint, slot, type, moduleName, domHook} = opts;
const existingModule = this._endpoints[endpoint].find(
info =>
info.plugin === plugin &&
info.moduleName === moduleName &&
info.domHook === domHook &&
info.slot === slot
);
if (existingModule) {
return existingModule;
} else {
const newModule = {
moduleName,
plugin,
pluginUrl: plugin._url,
type,
domHook,
slot,
};
this._endpoints[endpoint].push(newModule);
return newModule;
}
}
/**
* Register a plugin to an endpoint.
*
* Dynamic plugins are registered to a specific prefix, such as
* 'change-list-header'. These plugins are then fetched by prefix to determine
* which endpoints to dynamically add to the page.
*
* @param {Object} plugin
* @param {Object} opts
*/
registerModule(plugin, opts) {
const {endpoint, dynamicEndpoint} = opts;
if (dynamicEndpoint) {
if (!this._dynamicPlugins[dynamicEndpoint]) {
this._dynamicPlugins[dynamicEndpoint] = new Set();
}
this._dynamicPlugins[dynamicEndpoint].add(endpoint);
}
if (!this._endpoints[endpoint]) {
this._endpoints[endpoint] = [];
}
const moduleInfo = this._getOrCreateModuleInfo(plugin, opts);
if (pluginLoader.arePluginsLoaded() && this._callbacks[endpoint]) {
this._callbacks[endpoint].forEach(callback => callback(moduleInfo));
}
}
getDynamicEndpoints(dynamicEndpoint) {
const plugins = this._dynamicPlugins[dynamicEndpoint];
if (!plugins) return [];
return Array.from(plugins);
}
/**
* Get detailed information about modules registered with an extension
* endpoint.
*
* @param {string} name Endpoint name.
* @param {?{
* type: (string|undefined),
* moduleName: (string|undefined)
* }} opt_options
* @return {!Array<{
* moduleName: string,
* plugin: Plugin,
* pluginUrl: String,
* type: EndpointType,
* domHook: !Object
* }>}
*/
getDetails(name, opt_options) {
const type = opt_options && opt_options.type;
const moduleName = opt_options && opt_options.moduleName;
if (!this._endpoints[name]) {
return [];
}
return this._endpoints[name].filter(
item =>
(!type || item.type === type) &&
(!moduleName || moduleName == item.moduleName)
);
}
/**
* Get detailed module names for instantiating at the endpoint.
*
* @param {string} name Endpoint name.
* @param {?{
* type: (string|undefined),
* moduleName: (string|undefined)
* }} opt_options
* @return {!Array<string>}
*/
getModules(name, opt_options) {
const modulesData = this.getDetails(name, opt_options);
if (!modulesData.length) {
return [];
}
return modulesData.map(m => m.moduleName);
}
/**
* Get plugin URLs with element and module definitions.
*
* @param {string} name Endpoint name.
* @param {?{
* type: (string|undefined),
* moduleName: (string|undefined)
* }} opt_options
* @return {!Array<!URL>}
*/
getPlugins(name, opt_options) {
const modulesData = this.getDetails(name, opt_options);
if (!modulesData.length) {
return [];
}
return Array.from(new Set(modulesData.map(m => m.pluginUrl)));
}
importUrl(pluginUrl) {
let timerId;
return Promise
.race([
new Promise((resolve, reject) => {
this._importedUrls.add(pluginUrl.href);
importHref(pluginUrl, resolve, reject);
}),
// Timeout after 3s
new Promise(r => timerId = setTimeout(r, 3000)),
])
.finally(() => {
if (timerId) clearTimeout(timerId);
});
}
/**
* Get plugin URLs with element and module definitions.
*
* @param {string} name Endpoint name.
* @param {?{
* type: (string|undefined),
* moduleName: (string|undefined)
* }} opt_options
* @return {!Array<!Promise<void>>}
*/
getAndImportPlugins(name, opt_options) {
return Promise.all(
this.getPlugins(name, opt_options).map(pluginUrl => {
if (this._importedUrls.has(pluginUrl.href)) {
return Promise.resolve();
}
// TODO: we will deprecate html plugins entirely
// for now, keep the original behavior and import
// only for html ones
if (pluginUrl && pluginUrl.pathname.endsWith('.html')) {
return this.importUrl(pluginUrl);
} else {
return Promise.resolve();
}
})
);
}
}
// TODO(dmfilippov): Convert to service and add to appContext
export let pluginEndpoints = new GrPluginEndpoints();
export function _testOnly_resetEndpoints() {
pluginEndpoints = new GrPluginEndpoints();
}