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
JavaScript
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