UNPKG

@loopback/context

Version:

Facilities to manage artifacts and their dependencies in your Node.js applications. The module exposes TypeScript/JavaScript APIs and decorators to register artifacts, declare dependencies, and resolve artifacts by keys. It also serves as an IoC container

317 lines 12 kB
"use strict"; // Copyright IBM Corp. and LoopBack contributors 2019,2020. All Rights Reserved. // Node module: @loopback/context // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT Object.defineProperty(exports, "__esModule", { value: true }); exports.ContextSubscriptionManager = void 0; const tslib_1 = require("tslib"); const debug_1 = tslib_1.__importDefault(require("debug")); const events_1 = require("events"); const p_event_1 = require("p-event"); const debug = (0, debug_1.default)('loopback:context:subscription'); /** * An implementation of `Subscription` interface for context events */ class ContextSubscription { constructor(context, observer) { this.context = context; this.observer = observer; this._closed = false; } unsubscribe() { this.context.unsubscribe(this.observer); this._closed = true; } get closed() { return this._closed; } } /** * Manager for context observer subscriptions */ class ContextSubscriptionManager extends events_1.EventEmitter { constructor(context) { super(); this.context = context; /** * Internal counter for pending notification events which are yet to be * processed by observers. */ this.pendingNotifications = 0; this.setMaxListeners(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) { /* istanbul ignore if */ 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) { /** * Add an event listener to its parent context so that this context will * be notified of parent events, such as `bind` or `unbind`. */ this._parentContextEventListener = event => { this.handleParentEvent(event); }; // Listen on the parent context events this.context.parent.on('bind', this._parentContextEventListener); this.context.parent.on('unbind', this._parentContextEventListener); } // The following are two async functions. Returned promises are ignored as // they are long-running background tasks. 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; // Propagate the event to this context only if the binding key does not // exist in this context. The parent binding is shadowed if there is a // binding with the same key in this one. 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() { // Set up listeners on `bind` and `unbind` for notifications this.setupNotification('bind', 'unbind'); // Create an async iterator for the `notification` event as a queue this.notificationQueue = (0, p_event_1.iterator)(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) { // The loop will happen asynchronously upon events try { // The execution of observers happen in the Promise micro-task queue 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) { // Do not reduce the pending notification count so that errors // can be captured by waitUntilPendingNotificationsDone this._debug('Error caught from observers', err); // Errors caught from observers. if (this.listenerCount('error') > 0) { // waitUntilPendingNotificationsDone may be called this.emitError(err); } else { // Emit it to the current context. If no error listeners are // registered, crash the process. 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 }) => { // No need to schedule notifications if no observers are present if (!this._observers || this._observers.size === 0) return; // Track pending events this.pendingNotifications++; // Take a snapshot of current observers to ensure notifications of this // event will only be sent to current ones. Emit a new event to notify // current context observers. 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; await (0, p_event_1.multiple)(this, 'observersNotified', { count, timeout }); } /** * Add a context event observer to the context * @param observer - Context observer instance or function */ subscribe(observer) { var _a; this._observers = (_a = this._observers) !== null && _a !== void 0 ? _a : 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) { // Bubbling up the error event over the context chain // until we find an error listener let ctx = this.context; while (ctx) { if (ctx.listenerCount('error') === 0) { // No error listener found, try its parent ctx = ctx.parent; continue; } this._debug('Emitting error to context %s', ctx.name, err); ctx.emitError(err); return; } // No context with error listeners found this._debug('No error handler is configured for the context chain', err); // Let it crash now by emitting an error event 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 = undefined; if (this.notificationQueue != null) { // Cancel the notification iterator this.notificationQueue.return(undefined).catch(err => { this.handleNotificationError(err); }); this.notificationQueue = undefined; } if (this.context.parent && this._parentContextEventListener) { this.context.parent.removeListener('bind', this._parentContextEventListener); this.context.parent.removeListener('unbind', this._parentContextEventListener); this._parentContextEventListener = undefined; } } } exports.ContextSubscriptionManager = ContextSubscriptionManager; //# sourceMappingURL=context-subscription.js.map