UNPKG

contexify

Version:

A TypeScript library providing a powerful dependency injection container with context-based IoC capabilities, inspired by LoopBack's Context system.

297 lines 9.02 kB
var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); import { EventEmitter } from "events"; import createDebugger from "../utils/debug.js"; import * as pEvent from "../utils/p-event.js"; const debug = createDebugger("contexify:subscription"); let ContextSubscription = class ContextSubscription2 { static { __name(this, "ContextSubscription"); } context; observer; constructor(context, observer) { this.context = context; this.observer = observer; } _closed = false; unsubscribe() { this.context.unsubscribe(this.observer); this._closed = true; } get closed() { return this._closed; } }; class ContextSubscriptionManager extends EventEmitter { static { __name(this, "ContextSubscriptionManager"); } context; /** * A listener to watch parent context events */ _parentContextEventListener; /** * A list of registered context observers. The Set will be created when the * first observer is added. */ _observers; /** * Internal counter for pending notification events which are yet to be * processed by observers. */ pendingNotifications = 0; /** * Queue for background notifications for observers */ notificationQueue; constructor(context) { super(), this.context = context; this.setMaxListeners(Number.POSITIVE_INFINITY); } /** * @internal */ get parentContextEventListener() { return this._parentContextEventListener; } /** * @internal */ get observers() { return this._observers; } /** * Wrap the debug statement so that it always print out the context name * as the prefix * @param args - Arguments for the debug */ _debug(...args) { if (!debug.enabled) return; const formatter = args.shift(); if (typeof formatter === "string") { debug(`[%s] ${formatter}`, this.context.name, ...args); } else { debug("[%s] ", this.context.name, formatter, ...args); } } /** * Set up an internal listener to notify registered observers asynchronously * upon `bind` and `unbind` events. This method will be called lazily when * the first observer is added. */ setupEventHandlersIfNeeded() { if (this.notificationQueue != null) return; if (this.context.parent != null) { this._parentContextEventListener = (event) => { this.handleParentEvent(event); }; this.context.parent.on("bind", this._parentContextEventListener); this.context.parent.on("unbind", this._parentContextEventListener); } this.startNotificationTask().catch((err) => { this.handleNotificationError(err); }); let ctx = this.context.parent; while (ctx) { ctx.subscriptionManager.setupEventHandlersIfNeeded(); ctx = ctx.parent; } } handleParentEvent(event) { const { binding, context, type } = event; if (this.context.contains(binding.key)) { this._debug("Event %s %s is not re-emitted from %s to %s", type, binding.key, context.name, this.context.name); return; } this._debug("Re-emitting %s %s from %s to %s", type, binding.key, context.name, this.context.name); this.context.emitEvent(type, event); } /** * A strongly-typed method to emit context events * @param type Event type * @param event Context event */ emitEvent(type, event) { this.emit(type, event); } /** * Emit an `error` event * @param err Error */ emitError(err) { this.emit("error", err); } /** * Start a background task to listen on context events and notify observers */ startNotificationTask() { this.setupNotification("bind", "unbind"); this.notificationQueue = pEvent.pEventIterator(this, "notification", { // Do not end the iterator if an error event is emitted on the // subscription manager rejectionEvents: [] }); return this.processNotifications(); } /** * Publish an event to the registered observers. Please note the * notification is queued and performed asynchronously so that we allow fluent * APIs such as `ctx.bind('key').to(...).tag(...);` and give observers the * fully populated binding. * * @param event - Context event * @param observers - Current set of context observers */ async notifyObservers(event, observers = this._observers) { if (!observers || observers.size === 0) return; const { type, binding, context } = event; for (const observer of observers) { if (typeof observer === "function") { await observer(type, binding, context); } else if (!observer.filter || observer.filter(binding)) { await observer.observe(type, binding, context); } } } /** * Process notification events as they arrive on the queue */ async processNotifications() { const events = this.notificationQueue; if (events == null) return; for await (const { type, binding, context, observers } of events) { try { await this.notifyObservers({ type, binding, context }, observers); this.pendingNotifications--; this._debug("Observers notified for %s of binding %s", type, binding.key); this.emitEvent("observersNotified", { type, binding, context }); } catch (err) { this._debug("Error caught from observers", err); if (this.listenerCount("error") > 0) { this.emitError(err); } else { this.handleNotificationError(err); } } } } /** * Listen on given event types and emit `notification` event. This method * merge multiple event types into one for notification. * @param eventTypes - Context event types */ setupNotification(...eventTypes) { for (const type of eventTypes) { this.context.on(type, ({ binding, context }) => { if (!this._observers || this._observers.size === 0) return; this.pendingNotifications++; this.emitEvent("notification", { type, binding, context, observers: new Set(this._observers) }); }); } } /** * Wait until observers are notified for all of currently pending notification * events. * * This method is for test only to perform assertions after observers are * notified for relevant events. */ async waitUntilPendingNotificationsDone(timeout) { const count = this.pendingNotifications; debug("Number of pending notifications: %d", count); if (count === 0) return; const options = { count }; if (timeout != null) { options.timeout = timeout; } await pEvent.pEventMultiple(this, "observersNotified", options); } /** * Add a context event observer to the context * @param observer - Context observer instance or function */ subscribe(observer) { this._observers = this._observers ?? /* @__PURE__ */ new Set(); this.setupEventHandlersIfNeeded(); this._observers.add(observer); return new ContextSubscription(this.context, observer); } /** * Remove the context event observer from the context * @param observer - Context event observer */ unsubscribe(observer) { if (!this._observers) return false; return this._observers.delete(observer); } /** * Check if an observer is subscribed to this context * @param observer - Context observer */ isSubscribed(observer) { if (!this._observers) return false; return this._observers.has(observer); } /** * Handle errors caught during the notification of observers * @param err - Error */ handleNotificationError(err) { let ctx = this.context; while (ctx) { if (ctx.listenerCount("error") === 0) { ctx = ctx.parent; continue; } this._debug("Emitting error to context %s", ctx.name, err); ctx.emitError(err); return; } this._debug("No error handler is configured for the context chain", err); this.context.emitError(err); } /** * Close the context: clear observers, stop notifications, and remove event * listeners from its parent context. * * @remarks * This method MUST be called to avoid memory leaks once a context object is * no longer needed and should be recycled. An example is the `RequestContext`, * which is created per request. */ close() { this._observers = void 0; if (this.notificationQueue != null) { this.notificationQueue.return(void 0).catch((err) => { this.handleNotificationError(err); }); this.notificationQueue = void 0; } if (this.context.parent && this._parentContextEventListener) { this.context.parent.removeListener("bind", this._parentContextEventListener); this.context.parent.removeListener("unbind", this._parentContextEventListener); this._parentContextEventListener = void 0; } } } export { ContextSubscriptionManager }; //# sourceMappingURL=context-subscription.js.map