| /** |
| * @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 {importHref} from '../../../scripts/import-href'; |
| import {HookApi, PluginApi} from '../../plugins/gr-plugin-types'; |
| import {notUndefined} from '../../../types/types'; |
| |
| type Callback = (value: any) => void; |
| |
| export interface ModuleInfo { |
| moduleName: string; |
| plugin: PluginApi; |
| pluginUrl?: URL; |
| type?: string; |
| domHook?: HookApi; |
| slot?: string; |
| } |
| |
| interface Options { |
| endpoint: string; |
| dynamicEndpoint?: string; |
| slot?: string; |
| type?: string; |
| moduleName?: string; |
| domHook?: HookApi; |
| } |
| |
| export class GrPluginEndpoints { |
| private readonly _endpoints = new Map<string, ModuleInfo[]>(); |
| |
| private readonly _callbacks = new Map<string, ((value: any) => void)[]>(); |
| |
| private readonly _dynamicPlugins = new Map<string, Set<string>>(); |
| |
| private readonly _importedUrls = new Set<string>(); |
| |
| private _pluginLoaded = false; |
| |
| setPluginsReady() { |
| this._pluginLoaded = true; |
| } |
| |
| onNewEndpoint(endpoint: string, callback: Callback) { |
| if (!this._callbacks.has(endpoint)) { |
| this._callbacks.set(endpoint, []); |
| } |
| this._callbacks.get(endpoint)!.push(callback); |
| } |
| |
| onDetachedEndpoint(endpoint: string, callback: Callback) { |
| if (this._callbacks.has(endpoint)) { |
| const filteredCallbacks = this._callbacks |
| .get(endpoint)! |
| .filter((cb: Callback) => cb !== callback); |
| this._callbacks.set(endpoint, filteredCallbacks); |
| } |
| } |
| |
| _getOrCreateModuleInfo(plugin: PluginApi, opts: Options): ModuleInfo { |
| const {endpoint, slot, type, moduleName, domHook} = opts; |
| const existingModule = this._endpoints |
| .get(endpoint!)! |
| .find( |
| (info: ModuleInfo) => |
| info.plugin === plugin && |
| info.moduleName === moduleName && |
| info.domHook === domHook && |
| info.slot === slot |
| ); |
| if (existingModule) { |
| return existingModule; |
| } else { |
| const newModule: ModuleInfo = { |
| moduleName: moduleName!, |
| plugin, |
| pluginUrl: plugin._url, |
| type, |
| domHook, |
| slot, |
| }; |
| this._endpoints.get(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. |
| */ |
| registerModule(plugin: PluginApi, opts: Options) { |
| const endpoint = opts.endpoint!; |
| const dynamicEndpoint = opts.dynamicEndpoint; |
| if (dynamicEndpoint) { |
| if (!this._dynamicPlugins.has(dynamicEndpoint)) { |
| this._dynamicPlugins.set(dynamicEndpoint, new Set()); |
| } |
| this._dynamicPlugins.get(dynamicEndpoint)!.add(endpoint); |
| } |
| if (!this._endpoints.has(endpoint)) { |
| this._endpoints.set(endpoint, []); |
| } |
| const moduleInfo = this._getOrCreateModuleInfo(plugin, opts); |
| // TODO: the logic below seems wrong when: |
| // multiple plugins register to the same endpoint |
| // one register before plugins ready |
| // the other done after, then only the later one will have the callbacks |
| // invoked. |
| if (this._pluginLoaded && this._callbacks.has(endpoint)) { |
| this._callbacks.get(endpoint)!.forEach(callback => callback(moduleInfo)); |
| } |
| } |
| |
| getDynamicEndpoints(dynamicEndpoint: string): string[] { |
| const plugins = this._dynamicPlugins.get(dynamicEndpoint); |
| if (!plugins) return []; |
| return Array.from(plugins); |
| } |
| |
| /** |
| * Get detailed information about modules registered with an extension |
| * endpoint. |
| */ |
| getDetails(name: string, options?: Options): ModuleInfo[] { |
| const type = options && options.type; |
| const moduleName = options && options.moduleName; |
| if (!this._endpoints.has(name)) { |
| return []; |
| } else { |
| return this._endpoints |
| .get(name)! |
| .filter( |
| (item: ModuleInfo) => |
| (!type || item.type === type) && |
| (!moduleName || moduleName === item.moduleName) |
| ); |
| } |
| } |
| |
| /** |
| * Get detailed module names for instantiating at the endpoint. |
| */ |
| getModules(name: string, options?: Options): string[] { |
| const modulesData = this.getDetails(name, options); |
| if (!modulesData.length) { |
| return []; |
| } |
| return modulesData.map(m => m.moduleName); |
| } |
| |
| /** |
| * Get plugin URLs with element and module definitions. |
| */ |
| getPlugins(name: string, options?: Options): URL[] { |
| const modulesData = this.getDetails(name, options); |
| if (!modulesData.length) { |
| return []; |
| } |
| return Array.from(new Set(modulesData.map(m => m.pluginUrl))).filter( |
| notUndefined |
| ); |
| } |
| |
| importUrl(pluginUrl: URL) { |
| let timerId: any; |
| return Promise.race([ |
| new Promise((resolve, reject) => { |
| this._importedUrls.add(pluginUrl.href); |
| importHref(pluginUrl.href, 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. |
| */ |
| getAndImportPlugins(name: string, options?: Options) { |
| return Promise.all( |
| this.getPlugins(name, 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?.pathname.endsWith('.html')) { |
| return this.importUrl(pluginUrl); |
| } else { |
| return Promise.resolve(); |
| } |
| }) |
| ); |
| } |
| } |
| |
| // TODO(dmfilippov): Convert to service and add to appContext |
| let pluginEndpoints = new GrPluginEndpoints(); |
| |
| // To avoid mutable-exports, we don't want to export above variable directly |
| export function getPluginEndpoints() { |
| return pluginEndpoints; |
| } |
| export function _testOnly_resetEndpoints() { |
| pluginEndpoints = new GrPluginEndpoints(); |
| } |