UNPKG

@travetto/registry

Version:

Patterns and utilities for handling registration of metadata and functionality for run-time use

209 lines (183 loc) 4.9 kB
import { EventEmitter } from 'node:events'; import { Class, Env } from '@travetto/runtime'; import { ChangeSource, ChangeEvent, ChangeHandler } from './types'; /** * Base registry class, designed to listen to changes over time */ export abstract class Registry implements ChangeSource<Class> { /** * Has the registry been resolved */ #resolved: boolean; /** * Initializing promises */ #initialized?: Promise<unknown>; /** * Event emitter, to broadcast event changes */ #emitter = new EventEmitter(); /** * Dependent registries */ #dependents: Registry[] = []; /** * Parent registries */ #parents: ChangeSource<Class>[] = []; /** * Unique identifier */ #uid: string; /** * Are we in a mode that should have enhanced debug info */ trace = Env.DEBUG.val?.includes('@travetto/registry'); /** * Creates a new registry, with it's parents specified */ constructor(...parents: ChangeSource<Class>[]) { this.#uid = `${this.constructor.name}_${Date.now()}`; this.#parents = parents; if (this.#parents.length) { // Have the child listen to the parents for (const parent of this.#parents) { this.listen(parent); if (parent instanceof Registry) { parent.#dependents.push(this); } } } } /** * Run initialization */ async #runInit(): Promise<void> { try { this.#resolved = false; if (this.trace) { console.debug('Initializing', { id: this.constructor.Ⲑid, uid: this.#uid }); } // Handle top level when dealing with non-registry const waitFor = this.#parents.filter(x => !(x instanceof Registry)); await Promise.all(waitFor.map(x => x.init())); const classes = await this.initialInstall(); if (classes) { for (const cls of classes) { this.install(cls, { type: 'added', curr: cls }); } } await Promise.all(this.#dependents.map(x => x.init())); } finally { this.#resolved = true; } } get resolved(): boolean { return this.#resolved; } /** * Return list of classes for the initial installation */ initialInstall(): Class[] { return []; } /** * Verify initialized state */ verifyInitialized(): void { if (!this.#resolved) { throw new Error(`${this.constructor.name} has not been initialized, you probably need to call RootRegistry.init()`); } } /** * Initialize, with a built-in latch to prevent concurrent initializations */ async init(): Promise<unknown> { if (this.trace) { console.debug('Trying to initialize', { id: this.constructor.Ⲑid, uid: this.#uid, initialized: !!this.#initialized }); } if (!this.#initialized) { this.#initialized = this.#runInit(); } return this.#initialized; } parent<T extends ChangeSource<Class>>(type: Class<T>): T | undefined { return this.#parents.find((dep: unknown): dep is T => dep instanceof type); } /** * When an installation event occurs */ onInstall?(cls: Class, e: ChangeEvent<Class>): void; /** * When an un-installation event occurs */ onUninstall?(cls: Class, e: ChangeEvent<Class>): void; /** * Uninstall a class or list of classes */ uninstall(classes: Class | Class[], e: ChangeEvent<Class>): void { if (!Array.isArray(classes)) { classes = [classes]; } for (const cls of classes) { this.onUninstall?.(cls, e); } } /** * Install a class or a list of classes */ install(classes: Class | Class[], e: ChangeEvent<Class>): void { if (!Array.isArray(classes)) { classes = [classes]; } for (const cls of classes) { this.onInstall?.(cls, e); } } /** * Listen for events from the parent */ onEvent(event: ChangeEvent<Class>): void { if (this.trace) { console.debug('Received', { id: this.constructor.Ⲑid, type: event.type, targetId: (event.curr ?? event.prev)!.Ⲑid }); } switch (event.type) { case 'removing': this.uninstall(event.prev!, event); break; case 'added': this.install(event.curr!, event); break; case 'changed': this.uninstall(event.prev!, event); this.install(event.curr!, event); break; default: return; } } /** * Emit a new event */ emit(e: ChangeEvent<Class>): void { this.#emitter.emit('change', e); } /** * Register additional listeners */ on<T>(callback: ChangeHandler<Class<T>>): void { this.#emitter.on('change', callback); } /** * Remove listeners */ off<T>(callback: ChangeHandler<Class<T>>): void { this.#emitter.off('change', callback); } /** * Connect changes sources */ listen(source: ChangeSource<Class>): void { source.on(e => this.onEvent(e)); } }