UNPKG

plugin-engine

Version:
176 lines (169 loc) 5.82 kB
/** * The PluginEngine class provides a structured way to allow extensibility * within an application by implementing a Publish-Subscribe pattern with middleware support. * This pattern is crucial for creating loosely coupled systems, * which is essential for maintaining a scalable and maintainable * codebase. * * By utilizing a middleware system with event-driven architecture, * different parts of an application can communicate with each other in a decoupled fashion. * This enables easier feature additions and modifications without * causing a ripple effect of changes throughout the codebase. * * The PluginEngine class defines a mechanism to register event listeners (subscribers) * for different named events (publishers) with `enter` and `exit` hooks. * * An event with associated data can be executed using the `exec` * method, which invokes all the registered listeners for that event in * the order they were added, allowing for potential modifications to * the event data. * * The registered listeners can either process the events synchronously, * processing one event at a time in the order they are received, or * asynchronously, processing events in parallel as they are received. * * Additionally, a listener can terminate the processing of subsequent * listeners for a particular event and immediately return the current * state of the event data by utilizing the `abortWith` method * provided in the context argument to the listener. * This provides a mechanism to short-circuit the event processing * chain when a certain condition is met, like an authorization failure. * * The PluginEngine class provides a clean and intuitive API for extending * the functionality in a systematic way, while maintaining the * decoupling and scalability of the application architecture. */ // Event handler function in the PluginEngine system. // The plugin object definition /** * Type representing an unsubscribe function. */ /** * Class representing a minimalistic Publish-Subscribe system with middleware support. * * @template Events - An object type where keys are event names and values are the types of data associated with the events. */ export class PluginEngine { /** * Object to hold the event listeners. */ listeners = Object.create(null); /** * Method to exec an event and wait for possible data modifications from subscribers. * * @template EventName - The name of the event. * @template Events - Record representing all combinations of eventName:eventData * @param {EventName} eventName - The name of the event to exec. * @param {Events[EventName]} data - The data associated with the event. * @returns {Promise<Events[EventName]>} - The potentially modified event data. */ async exec(eventName, data) { // // const set = this.listeners[eventName]; if (!set?.size) return data; const listeners = Array.from(set); const context = { abortWith: data => { throw new Exit(data); } }; async function run(listener, step) { const { error } = listener.plugin; const exec = listener.plugin[step]; try { if (!exec) return; const result = await exec(data, context); if (result !== undefined) { data = result; } } catch (e) { if (Exit.is(e)) { throw e; } if (typeof e?.stack === 'string') { e.stack = getStack(listener.plugin); } throw error ? error(e) : e; } } for (const listener of listeners) { try { await run(listener, 'enter'); } catch (e) { if (Exit.is(e)) return e.data; throw e; } } for (let i = listeners.length - 1; i >= 0; i--) { try { await run(listeners[i], 'exit'); } catch (e) { if (Exit.is(e)) return e.data; throw e; } } return data; } /** * Method to register a new event listener. * * @template EventName - The name of the event. * @template Events - Record representing all combinations of eventName:eventData * @param {EventName} eventName - The name of the event to listen for. * @param {Plugin<Events[EventName]>} plugin - The event handler. * will not be awaited to finish before going to the next middleware execution * @returns {UnsubscribeListener} - A function to unregister the listener. */ on = (eventName, plugin) => { const listeners = this.listeners[eventName] = this.listeners[eventName] || new Set(); const register = { plugin, eventName }; listeners.add(register); return () => { this.listeners[eventName]?.delete(register); }; }; } // JS accepts anything to be thrown, not only errors (throw new Error(...)) // so, when we need to stop an execution, we can throw something, and // catch on a try {..} catch(e) { } // The Exit class is used here just to identify when we throw something special class Exit { static symbol = Symbol('exit'); symbol = Exit.symbol; constructor(data) { this.data = data; } static is = value => { return value?.['symbol'] === Exit.symbol; }; } function getStack(parent) { const err = new Error(); captureStackTrace(err, parent === undefined ? getStack : parent); return err.stack || ''; } function captureStackTrace(error, parent) { if (typeof Error.captureStackTrace === 'function') { return Error.captureStackTrace(error, parent); } const container = new Error(); Object.defineProperty(error, 'stack', { configurable: true, get() { const { stack } = container; Object.defineProperty(this, 'stack', { value: stack }); return stack; } }); } //# sourceMappingURL=index.mjs.map