@travetto/registry
Version:
Patterns and utilities for handling registration of metadata and functionality for run-time use
177 lines (154 loc) • 5.07 kB
text/typescript
import { EventEmitter } from 'node:events';
import { Class, Env, Runtime, RuntimeIndex, describeFunction, flushPendingFunctions } from '@travetto/runtime';
import { DynamicFileLoader } from '../internal/file-loader.ts';
import { ChangeSource, ChangeEvent, ChangeHandler } from '../types.ts';
function isClass(cls: Function): cls is Class {
return !!describeFunction(cls).class;
}
/**
* A class change source. Meant to be hooked into the
* compiler as a way to listen to changes via the compiler
* watching.
*/
export class ClassSource implements ChangeSource<Class> {
#classes = new Map<string, Map<string, Class>>();
#emitter = new EventEmitter();
#listening: Promise<void> | undefined;
/**
* Are we in a mode that should have enhanced debug info
*/
trace = Env.DEBUG.val?.includes('@travetto/registry');
/**
* Flush classes
*/
#flush(): void {
for (const cls of flushPendingFunctions().filter(isClass)) {
const src = Runtime.getSourceFile(cls);
if (!this.#classes.has(src)) {
this.#classes.set(src, new Map());
}
this.#classes.get(src)!.set(cls.Ⲑid, cls);
this.emit({ type: 'added', curr: cls });
}
}
#removeFile(file: string): void {
const data = this.#classes.get(file);
if (data) {
this.#classes.delete(file);
for (const cls of data) {
this.emit({ type: 'removing', prev: cls[1] });
}
}
}
/**
* Process changes for a single file, looking for add/remove/update of classes
*/
#handleFileChanges(importFile: string, classes: Class[] = []): number {
const next = new Map<string, Class>(classes.map(cls => [cls.Ⲑid, cls] as const));
const sourceFile = RuntimeIndex.getSourceFile(importFile);
let prev = new Map<string, Class>();
if (this.#classes.has(sourceFile)) {
prev = new Map(this.#classes.get(sourceFile)!.entries());
}
const keys = new Set([...Array.from(prev.keys()), ...Array.from(next.keys())]);
if (!this.#classes.has(sourceFile)) {
this.#classes.set(sourceFile, new Map());
}
let changes = 0;
// Determine delta based on the various classes (if being added, removed or updated)
for (const k of keys) {
if (!next.has(k)) {
changes += 1;
this.emit({ type: 'removing', prev: prev.get(k)! });
this.#classes.get(sourceFile)!.delete(k);
} else {
this.#classes.get(sourceFile)!.set(k, next.get(k)!);
if (!prev.has(k)) {
changes += 1;
this.emit({ type: 'added', curr: next.get(k)! });
} else {
const prevHash = describeFunction(prev.get(k)!)?.hash;
const nextHash = describeFunction(next.get(k)!)?.hash;
if (prevHash !== nextHash) {
changes += 1;
this.emit({ type: 'changed', curr: next.get(k)!, prev: prev.get(k) });
}
}
}
}
return changes;
}
/**
* Process all class changes
*/
#handleChanges(classes: Class[] = []): void {
const classesByFile = new Map<string, Class[]>();
for (const el of classes) {
const imp = Runtime.getImport(el);
if (!classesByFile.has(imp)) {
classesByFile.set(imp, []);
}
classesByFile.get(imp)!.push(el);
}
for (const [imp, els] of classesByFile.entries()) {
if (!this.#handleFileChanges(imp, els)) {
this.#emitter.emit('unchanged-import', imp);
}
}
}
/**
* Emit a change event
*/
emit(e: ChangeEvent<Class>): void {
if (this.trace) {
console.debug('Emitting change', { type: e.type, curr: e.curr?.Ⲑid, prev: e.prev?.Ⲑid });
}
this.#emitter.emit('change', e);
}
/**
* Initialize
*/
async init(): Promise<void> {
if (Runtime.dynamic && !this.#listening) {
this.#listening = (async (): Promise<void> => {
for await (const ev of await DynamicFileLoader.listen()) {
if (ev.action === 'delete') {
this.#removeFile(ev.file); // File no longer exists
} else {
this.#handleChanges(flushPendingFunctions().filter(isClass));
}
if (ev.action === 'create') {
this.#flush();
}
}
})();
}
// Ensure everything is loaded
for (const entry of RuntimeIndex.find({
module: (m) => {
const role = Env.TRV_ROLE.val;
return m.roles.includes('std') && (
!Runtime.production || m.prod ||
((role === 'doc' || role === 'test') && m.roles.includes(role))
);
},
folder: f => f === 'src' || f === '$index'
})) {
await Runtime.importFrom(entry.import);
}
// Flush all load events
this.#flush();
}
/**
* Add callback for change events
*/
on(callback: ChangeHandler<Class>): void {
this.#emitter.on('change', callback);
}
/**
* Add callback for when a import is changed, but emits no class changes
*/
onNonClassChanges(callback: (imp: string) => void): void {
this.#emitter.on('unchanged-import', callback);
}
}