import type { RoundaboutOptions, RoundaboutReady } from '../types/roundabout/types.js'; export class RoundaboutManager { private vm!: TProps & TActions & RoundaboutReady; private propagator: EventTarget; private options: RoundaboutOptions; private abortController: AbortController; private processingQueue: Map>; private cleanupFunctions: Array<() => void> = []; constructor( options: RoundaboutOptions, private infractions?: Array ) { this.options = options; this.abortController = new AbortController(); this.processingQueue = new Map(); this.propagator = new EventTarget(); } async initialize(): Promise<[vm: TProps & TActions & RoundaboutReady, propagator: EventTarget]> { // Step 1: Setup VM with RoundaboutReady interface await this.setupViewModel(); // Step 2: Infer properties to monitor and setup propagator with getter/setters await this.setupPropagatorAndProperties(); // Step 3: Process configuration options await this.processOptions(); // Step 4: Notify that roundabout is fully initialized if (this.vm instanceof EventTarget) { const { ROUNDABOUT_READY_EVENT } = await import('../core/Events.js'); this.vm.dispatchEvent(new Event(ROUNDABOUT_READY_EVENT)); } return [this.vm, this.propagator]; } private async setupViewModel(): Promise { const vm = this.options.vm || {} as any; // Add RoundaboutReady interface if not present if (!vm.RAController) { vm.RAController = this.abortController; } if (!vm.covertAssignment) { vm.covertAssignment = async (obj: any) => { // Import covert property setter const { covertlySetProperty } = await import('../utils/PropagatorSetup.js'); // Set properties covertly (without triggering events) for (const [key, value] of Object.entries(obj)) { covertlySetProperty(vm, key, value); } }; } if (!vm.awake) { vm.awake = async () => { // TODO: Implement sleep/awake mechanism }; } if (!vm.nudge) { vm.nudge = () => { // TODO: Implement nudge }; } if (!vm.rock) { vm.rock = () => { // TODO: Implement rock }; } this.vm = vm as TProps & TActions & RoundaboutReady; // Store assignOptions on VM so processors can access them if (this.options.assignOptions) { Object.defineProperty(vm, '__roundaboutAssignOptions', { value: this.options.assignOptions, enumerable: false, writable: false, configurable: true }); } } private async setupPropagatorAndProperties(): Promise { const { setupPropagator, inferPropertiesToMonitor } = await import('../utils/PropagatorSetup.js'); // Infer which properties need monitoring const propertiesToMonitor = inferPropertiesToMonitor(this.options); // Setup propagator and convert properties to getter/setters this.propagator = await setupPropagator(this.vm, propertiesToMonitor, this.options.weakRef); // Subscribe to propagator events to trigger reactions for (const prop of propertiesToMonitor) { this.propagator.addEventListener(prop, (event: Event) => { const propChangeEvent = event as any; // PropertyChangeEvent this.handlePropertyChange(prop, propChangeEvent.newValue).catch(err => { console.error(`Error handling property change for ${prop}:`, err); }); }, { signal: this.abortController.signal }); } } private pendingValues: Map = new Map(); private async handlePropertyChange(key: string, value: any): Promise { // If already processing this key, store the latest value so it gets // picked up after the current cycle completes (don't drop it). const existing = this.processingQueue.get(key); if (existing) { this.pendingValues.set(key, value); await existing; return; } const promise = this.processPropertyChangeInternal(key, value); this.processingQueue.set(key, promise); try { await promise; } finally { this.processingQueue.delete(key); } // If another value arrived while we were processing, handle it now. if (this.pendingValues.has(key)) { const next = this.pendingValues.get(key); this.pendingValues.delete(key); await this.handlePropertyChange(key, next); } } private async processPropertyChangeInternal(key: string, value: any): Promise { // Trigger any registered reactions for this property const vmAny = this.vm as any; const reactions = vmAny.__roundaboutReactions?.get(key); if (reactions && Array.isArray(reactions)) { for (const reaction of reactions) { try { await reaction(value); } catch (err) { console.error(`Error in reaction for ${key}:`, err); } } } } private async processOptions(): Promise { // Dynamically import and process each configuration type if (this.options.compacts) { await this.processCompacts(); } if (this.options.actions) { await this.processActions(); } if (this.options.handlers) { await this.processHandlers(); } if (this.options.hitches) { await this.processHitches(); } if (this.infractions) { await this.processInfractions(); } if (this.options.positractions) { await this.processPositractions(); } if (this.options.merges) { await this.processMerges(); } if (this.options.yields) { await this.processYields(); } } private async processCompacts(): Promise { const { processCompacts } = await import('../processors/compacts.js'); const cleanup = await processCompacts( this.vm, this.options.compacts!, (key: string) => this.handlePropertyChange(key, this.vm[key as keyof typeof this.vm]) ); this.cleanupFunctions.push(cleanup); } private async processActions(): Promise { const { processActions } = await import('../processors/actions.js'); const cleanup = await processActions( this.vm, this.options.actions!, (key: string) => this.handlePropertyChange(key, this.vm[key as keyof typeof this.vm]), this.options.internalRouting === true // Default to false (traditional) ); this.cleanupFunctions.push(cleanup); } private async processHandlers(): Promise { const { processHandlers } = await import('../processors/handlers.js'); const cleanup = await processHandlers( this.vm as any, this.options.handlers!, this.abortController.signal ); this.cleanupFunctions.push(cleanup); } private async processHitches(): Promise { const { processHitches } = await import('../processors/hitches.js'); const cleanup = await processHitches( this.vm, this.options.hitches!, (key: string) => this.handlePropertyChange(key, this.vm[key as keyof typeof this.vm]) ); this.cleanupFunctions.push(cleanup); } private async processInfractions(): Promise { const { processInfractions } = await import('../processors/infractions.js'); const cleanup = await processInfractions( this.vm, this.infractions!, (key: string) => this.handlePropertyChange(key, this.vm[key as keyof typeof this.vm]) ); this.cleanupFunctions.push(cleanup); } private async processPositractions(): Promise { const { processPositractions } = await import('../processors/positractions.js'); const cleanup = await processPositractions( this.vm, this.options.positractions!, (key: string) => this.handlePropertyChange(key, this.vm[key as keyof typeof this.vm]) ); this.cleanupFunctions.push(cleanup); } private async processMerges(): Promise { const { processMerges } = await import('../processors/merges.js'); const cleanup = await processMerges( this.vm, this.options.merges!, (key: string) => this.handlePropertyChange(key, this.vm[key as keyof typeof this.vm]) ); this.cleanupFunctions.push(cleanup); } private async processYields(): Promise { const { processYields } = await import('../processors/yields.js'); const cleanup = await processYields( this.vm, this.options.yields!, (key: string) => this.handlePropertyChange(key, this.vm[key as keyof typeof this.vm]) ); this.cleanupFunctions.push(cleanup); } async cleanup(): Promise { this.abortController.abort(); this.cleanupFunctions.forEach(fn => fn()); this.cleanupFunctions = []; } }