@travetto/registry
Version:
Patterns and utilities for handling registration of metadata and functionality for run-time use
147 lines (126 loc) • 3.92 kB
text/typescript
import { AppError, castTo, type Class, Env, flushPendingFunctions, isClass, Runtime, RuntimeIndex } from '@travetto/runtime';
import type { RegistryIndex, RegistryIndexClass } from './types.ts';
class $Registry {
#resolved = false;
#initialized?: Promise<unknown>;
#indexByClass = new Map<RegistryIndexClass, RegistryIndex>();
#indexes: RegistryIndex[] = [];
#finalizeItems(classes: Class[]): void {
for (const index of this.#indexes) {
for (const cls of classes) {
if (index.store.has(cls) && !index.store.finalized(cls)) {
if (index.finalize) {
index.finalize(cls);
} else {
index.store.finalize(cls);
}
}
}
}
}
trace = false;
validateConstructor(source: unknown): void {
if (source !== this) {
throw new AppError('constructor is private');
}
}
finalizeForIndex(indexCls: RegistryIndexClass): void {
const inst = this.instance(indexCls);
this.#finalizeItems(inst.store.getClasses());
}
/**
* Process change events
*/
process(classes: Class[]): void {
this.#finalizeItems(classes);
const byIndex = new Map<RegistryIndex, Class[]>();
for (const index of this.#indexes) {
byIndex.set(index, classes.filter(cls => index.store.has(cls)));
}
for (const index of this.#indexes) {
for (const cls of byIndex.get(index)!) {
index.onCreate?.(cls);
}
index.beforeChangeSetComplete?.(byIndex.get(index)!);
}
// Call after everything is done
for (const index of this.#indexes) {
index.onChangeSetComplete?.(byIndex.get(index)!);
}
}
/**
* Run initialization
*/
async #init(): Promise<void> {
try {
this.#resolved = false;
if (this.trace) {
console.debug('Initializing');
}
// Ensure everything is loaded
for (const entry of RuntimeIndex.find({
module: (module) => {
const role = Env.TRV_ROLE.value;
return role !== 'test' && // Skip all modules when in test
module.roles.includes('std') &&
(
!Runtime.production || module.production ||
(role === 'doc' && module.roles.includes(role))
);
},
folder: folder => folder === 'src' || folder === '$index'
})) {
await Runtime.importFrom(entry.import);
}
// Flush all load events
const added = flushPendingFunctions().filter(isClass);
this.process(added);
} finally {
this.#resolved = true;
}
}
/**
* Verify initialized state
*/
verifyInitialized(): void {
if (!this.#resolved) {
throw new AppError('Registry not initialized, call init() first');
}
}
/**
* Register a new index
*/
registerIndex<T extends RegistryIndexClass>(indexCls: T): InstanceType<T> {
if (!this.#indexByClass.has(indexCls)) {
const instance = new indexCls(this);
this.#indexByClass.set(indexCls, instance);
this.#indexes.push(instance);
}
return castTo(this.#indexByClass.get(indexCls));
}
/**
* Initialize, with a built-in latch to prevent concurrent initializations
*/
async init(): Promise<unknown> {
if (this.trace && this.#initialized) {
console.trace('Trying to re-initialize', { initialized: !!this.#initialized });
}
return this.#initialized ??= this.#init();
}
/**
* Manual init, not meant to be used directly
* @private
*/
async manualInit(files: string[]): Promise<Class[]> {
for (const file of files) {
await Runtime.importFrom(file);
}
const imported = flushPendingFunctions().filter(isClass);
this.process(imported);
return imported;
}
instance<T extends RegistryIndexClass>(indexCls: T): InstanceType<T> {
return castTo(this.#indexByClass.get(indexCls));
}
}
export const Registry = new $Registry();