@travetto/registry
Version:
Patterns and utilities for handling registration of metadata and functionality for run-time use
209 lines (183 loc) • 4.9 kB
text/typescript
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));
}
}