UNPKG

@travetto/registry

Version:

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

160 lines (138 loc) 4.5 kB
import { EventEmitter } from 'node:events'; import { Class, Env, Runtime, RuntimeIndex, describeFunction, flushPendingFunctions } from '@travetto/runtime'; import { DynamicFileLoader } from '../internal/file-loader'; import { ChangeSource, ChangeEvent, ChangeHandler } from '../types'; 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(); /** * 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.getImport(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 }); } } /** * 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)); let prev = new Map<string, Class>(); if (this.#classes.has(importFile)) { prev = new Map(this.#classes.get(importFile)!.entries()); } const keys = new Set([...Array.from(prev.keys()), ...Array.from(next.keys())]); if (!this.#classes.has(importFile)) { this.#classes.set(importFile, 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(importFile)!.delete(k); } else { this.#classes.get(importFile)!.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) { DynamicFileLoader.onLoadEvent(ev => { this.#handleChanges(flushPendingFunctions().filter(isClass)); if (ev.action === 'create') { this.#flush(); } }); await DynamicFileLoader.init(); } // 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); } }