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