/** * @typedef {Object} Extension An extension registered to an extension point * @property {string} name Unique name for the extension. * @property {Object|Callback} response Object to be returned or function to be called by the extension point. * @property {number} [priority] Order priority for execution used for executing in serial. */ /** * Represents a point in the consumer's code that can be extended by a plugin. * The plugin can register a callback or object to the extension point. * When the extension point is triggered, the provided function will then be called or object will be returned. */ class ExtensionPoint { /** @type {string} Name of the extension point */ name /** * @type {Array.} The list of all extensions registered with this extension point. * @private */ _extensions: any[] = [] /** * @type {Array.} A list of functions to be executed when the list of extensions changes. * @private */ changeListeners: any[] = [] constructor(name: string) { this.name = name } /** * Register new extension with this extension point. * The registered response will be executed (if callback) or returned (if object) * when the extension point is executed (see below). * @param {string} name Unique name for the extension. * @param {Object|Callback} response Object to be returned or function to be called by the extension point. * @param {number} [priority] Order priority for execution used for executing in serial. * @returns {void} */ register(name: string, response: any, priority: number = 0) { const index = this._extensions.findIndex((p) => p.priority > priority) const newExt = { name, response, priority } if (index > -1) { this._extensions.splice(index, 0, newExt) } else { this._extensions.push(newExt) } this.emitChange() } /** * Remove an extension from the registry. It will no longer be part of the extension point execution. * @param {RegExp } name Matcher for the name of the extension to remove. * @returns {void} */ unregister(name: string) { const index = this._extensions.findIndex((ext) => ext.name.match(name)) if (index > -1) this._extensions.splice(index, 1) this.emitChange() } /** * Empty the registry of all extensions. * @returns {void} */ clear() { this._extensions = [] this.emitChange() } /** * Get a specific extension registered with the extension point * @param {string} name Name of the extension to return * @returns {Object|Callback|undefined} The response of the extension. If this is a function the function is returned, not its response. */ get(name: string) { const ep = this._extensions.find((ext) => ext.name === name) return ep && ep.response } /** * Execute (if callback) and return or just return (if object) the response for each extension registered to this extension point. * Any asynchronous responses will be executed in parallel and the returned array will contain a promise for each of these responses. * @param {*} input Input to be provided as a parameter to each response if response is a callback. * @returns {Array} List of responses from the extensions. */ execute(input: any) { return this._extensions.map((p) => { if (typeof p.response === 'function') { return p.response(input) } else { return p.response } }) } /** * Execute (if callback) and return the response, or push it to the array if the previous response is an array * for each extension registered to this extension point in serial, * feeding the result from the last response as input to the next. * @param {*} input Input to be provided as a parameter to the 1st callback * @returns {Promise.<*>} Result of the last extension that was called */ async executeSerial(input: any) { return await this._extensions.reduce(async (throughput, p) => { let tp = await throughput if (typeof p.response === 'function') { tp = await p.response(tp) } else if (Array.isArray(tp)) { tp.push(p.response) } return tp }, input) } /** * Register a callback to be executed if the list of extensions changes. * @param {string} name Name of the listener needed if it is to be removed. * @param {Function} callback The callback function to trigger on a change. */ onRegister(name: string, callback: any) { if (typeof callback === 'function') this.changeListeners.push({ name, callback }) } /** * Unregister a callback from the extension list changes. * @param {string} name The name of the listener to remove. */ offRegister(name: string) { const index = this.changeListeners.findIndex((l) => l.name === name) if (index > -1) this.changeListeners.splice(index, 1) } emitChange() { for (const l of this.changeListeners) { l.callback(this) } } } export default ExtensionPoint