@travetto/schema
Version:
Data type registry for runtime validation, reflection and binding.
156 lines (129 loc) • 4.57 kB
text/typescript
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();