UNPKG

@travetto/schema

Version:

Data type registry for runtime validation, reflection and binding.

156 lines (129 loc) 4.57 kB
import { EventEmitter } from 'node:events'; import { Class } from '@travetto/runtime'; import { ChangeEvent } from '@travetto/registry'; import { FieldConfig, ClassConfig } from './types'; import { AllViewSymbol } from '../internal/types'; const id = (c: Class | string): string => typeof c === 'string' ? c : c.Ⲑid; interface FieldMapping { path: FieldConfig[]; config: ClassConfig; } export interface FieldChangeEvent { cls: Class; changes: ChangeEvent<FieldConfig>[]; } interface SubSchemaChange { path: FieldConfig[]; fields: ChangeEvent<FieldConfig>[]; } export interface SchemaChange { config: ClassConfig; subs: SubSchemaChange[]; } export interface SchemaChangeEvent { cls: Class; change: SchemaChange; } /** * Schema change listener. Handles all changes that occur via the SchemaRegistry */ class $SchemaChangeListener { #emitter = new EventEmitter(); #mapping = new Map<string, Map<string, FieldMapping>>(); /** * On schema change, emit the change event for the whole schema * @param cb The function to call on schema change */ onSchemaChange(handler: (e: SchemaChangeEvent) => void): void { this.#emitter.on('schema', handler); } /** * On schema field change, emit the change event for the whole schema * @param cb The function to call on schema field change */ onFieldChange(handler: (e: FieldChangeEvent) => void): void { this.#emitter.on('field', handler); } /** * Clear dependency mappings for a given class */ clearSchemaDependency(cls: Class): void { this.#mapping.delete(id(cls)); } /** * Track a specific class for dependencies * @param src The target class * @param parent The parent class * @param path The path within the object hierarchy to arrive at the class * @param config The configuration or the class */ trackSchemaDependency(src: Class, parent: Class, path: FieldConfig[], config: ClassConfig): void { const idValue = id(src); if (!this.#mapping.has(idValue)) { this.#mapping.set(idValue, new Map()); } this.#mapping.get(idValue)!.set(id(parent), { path, config }); } /** * Emit changes to the schema * @param cls The class of the event * @param changes The changes to send */ emitSchemaChanges({ cls, changes }: FieldChangeEvent): void { const updates = new Map<string, SchemaChange>(); const clsId = id(cls); if (this.#mapping.has(clsId)) { const deps = this.#mapping.get(clsId)!; for (const depClsId of deps.keys()) { if (!updates.has(depClsId)) { updates.set(depClsId, { config: deps.get(depClsId)!.config, subs: [] }); } const c = deps.get(depClsId)!; updates.get(depClsId)!.subs.push({ path: [...c.path], fields: changes }); } } for (const key of updates.keys()) { this.#emitter.emit('schema', { cls: updates.get(key)!.config.class, change: updates.get(key)! }); } } /** * Emit field level changes in the schema * @param prev The previous class config * @param curr The current class config */ emitFieldChanges({ prev, curr }: ChangeEvent<ClassConfig>): void { const prevView = prev?.views[AllViewSymbol] || { fields: [], schema: {} }; const currView = curr!.views[AllViewSymbol]; const prevFields = new Set(prevView.fields); const currFields = new Set(currView.fields); const changes: ChangeEvent<FieldConfig>[] = []; for (const c of currFields) { if (!prevFields.has(c)) { changes.push({ curr: currView.schema[c], type: 'added' }); } } for (const c of prevFields) { if (!currFields.has(c)) { changes.push({ prev: prevView.schema[c], type: 'removing' }); } } // Handle class references changing, but keeping same id const compareTypes = (a: Class, b: Class): boolean => a.Ⲑid ? a.Ⲑid === b.Ⲑid : a === b; for (const c of currFields) { if (prevFields.has(c)) { const prevSchema = prevView.schema[c]; const currSchema = currView.schema[c]; if ( JSON.stringify(prevSchema) !== JSON.stringify(currSchema) || !compareTypes(prevSchema.type, currSchema.type) ) { changes.push({ prev: prevView.schema[c], curr: currView.schema[c], type: 'changed' }); } } } // Send field changes this.#emitter.emit('field', { cls: curr!.class, changes }); this.emitSchemaChanges({ cls: curr!.class, changes }); } } export const SchemaChangeListener = new $SchemaChangeListener();