|  | /** | 
|  | * @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(); | 
|  | } |