xcomponent-ai 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CONTRIBUTING.md +195 -0
  2. package/LICENSE +45 -0
  3. package/PERSISTENCE.md +774 -0
  4. package/README.md +594 -0
  5. package/dist/agents.d.ts +81 -0
  6. package/dist/agents.d.ts.map +1 -0
  7. package/dist/agents.js +405 -0
  8. package/dist/agents.js.map +1 -0
  9. package/dist/api.d.ts +36 -0
  10. package/dist/api.d.ts.map +1 -0
  11. package/dist/api.js +404 -0
  12. package/dist/api.js.map +1 -0
  13. package/dist/cli.d.ts +7 -0
  14. package/dist/cli.d.ts.map +1 -0
  15. package/dist/cli.js +437 -0
  16. package/dist/cli.js.map +1 -0
  17. package/dist/component-registry.d.ts +190 -0
  18. package/dist/component-registry.d.ts.map +1 -0
  19. package/dist/component-registry.js +382 -0
  20. package/dist/component-registry.js.map +1 -0
  21. package/dist/fsm-runtime.d.ts +263 -0
  22. package/dist/fsm-runtime.d.ts.map +1 -0
  23. package/dist/fsm-runtime.js +1122 -0
  24. package/dist/fsm-runtime.js.map +1 -0
  25. package/dist/index.d.ts +23 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +56 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/monitoring.d.ts +68 -0
  30. package/dist/monitoring.d.ts.map +1 -0
  31. package/dist/monitoring.js +176 -0
  32. package/dist/monitoring.js.map +1 -0
  33. package/dist/persistence.d.ts +100 -0
  34. package/dist/persistence.d.ts.map +1 -0
  35. package/dist/persistence.js +270 -0
  36. package/dist/persistence.js.map +1 -0
  37. package/dist/timer-wheel.d.ts +85 -0
  38. package/dist/timer-wheel.d.ts.map +1 -0
  39. package/dist/timer-wheel.js +181 -0
  40. package/dist/timer-wheel.js.map +1 -0
  41. package/dist/types.d.ts +404 -0
  42. package/dist/types.d.ts.map +1 -0
  43. package/dist/types.js +40 -0
  44. package/dist/types.js.map +1 -0
  45. package/dist/websockets.d.ts +32 -0
  46. package/dist/websockets.d.ts.map +1 -0
  47. package/dist/websockets.js +117 -0
  48. package/dist/websockets.js.map +1 -0
  49. package/package.json +103 -0
@@ -0,0 +1,1122 @@
1
+ "use strict";
2
+ /**
3
+ * FSM Runtime Engine
4
+ * Implements XComponent-inspired state machine execution with multi-instance support
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.FSMRuntime = void 0;
8
+ exports.loadComponent = loadComponent;
9
+ const events_1 = require("events");
10
+ const uuid_1 = require("uuid");
11
+ const types_1 = require("./types");
12
+ const timer_wheel_1 = require("./timer-wheel");
13
+ const persistence_1 = require("./persistence");
14
+ /**
15
+ * Sender implementation for triggered methods
16
+ * Provides controlled access to runtime operations
17
+ * Supports both intra-component and cross-component communication
18
+ */
19
+ class SenderImpl {
20
+ runtime;
21
+ registry;
22
+ constructor(runtime, registry) {
23
+ this.runtime = runtime;
24
+ this.registry = registry;
25
+ }
26
+ async sendTo(instanceId, event) {
27
+ return this.runtime.sendEvent(instanceId, event);
28
+ }
29
+ async sendToComponent(componentName, instanceId, event) {
30
+ if (!this.registry) {
31
+ throw new Error('Cross-component communication requires ComponentRegistry');
32
+ }
33
+ return this.registry.sendEventToComponent(componentName, instanceId, event);
34
+ }
35
+ async broadcast(machineName, currentState, event) {
36
+ return this.runtime.broadcastEvent(machineName, currentState, event);
37
+ }
38
+ async broadcastToComponent(componentName, machineName, currentState, event) {
39
+ if (!this.registry) {
40
+ throw new Error('Cross-component communication requires ComponentRegistry');
41
+ }
42
+ return this.registry.broadcastToComponent(componentName, machineName, currentState, event);
43
+ }
44
+ createInstance(machineName, initialContext) {
45
+ return this.runtime.createInstance(machineName, initialContext);
46
+ }
47
+ createInstanceInComponent(componentName, machineName, initialContext) {
48
+ if (!this.registry) {
49
+ throw new Error('Cross-component communication requires ComponentRegistry');
50
+ }
51
+ return this.registry.createInstanceInComponent(componentName, machineName, initialContext);
52
+ }
53
+ }
54
+ /**
55
+ * FSM Runtime Engine
56
+ * Manages multiple FSM instances with event-driven execution
57
+ */
58
+ class FSMRuntime extends events_1.EventEmitter {
59
+ instances;
60
+ machines;
61
+ timerWheel; // Performance: Single timer for all timeouts
62
+ timeoutTasks; // instanceId → taskIds (for cleanup)
63
+ persistence;
64
+ componentDef;
65
+ registry; // For cross-component communication
66
+ // Performance: Hash-based indexes for efficient property matching (XComponent pattern)
67
+ machineIndex; // machineName → Set<instanceId>
68
+ stateIndex; // "machineName:state" → Set<instanceId>
69
+ propertyIndex; // "machineName:propName:propValue" → Set<instanceId>
70
+ constructor(component, persistenceConfig) {
71
+ super();
72
+ this.instances = new Map();
73
+ this.machines = new Map();
74
+ // Timer wheel: 10ms ticks for high precision, 6000 buckets = 60s max
75
+ // Still O(1) with single timer - 10ms granularity is sufficient for most use cases
76
+ // For longer timeouts (>60s), tasks will wrap around (multi-lap)
77
+ this.timerWheel = new timer_wheel_1.TimerWheel(10, 6000);
78
+ this.timeoutTasks = new Map(); // Track tasks for cleanup
79
+ this.componentDef = component;
80
+ // Initialize indexes
81
+ this.machineIndex = new Map();
82
+ this.stateIndex = new Map();
83
+ this.propertyIndex = new Map();
84
+ // Start timer wheel
85
+ this.timerWheel.start();
86
+ // Setup persistence (optional)
87
+ if (persistenceConfig && (persistenceConfig.eventSourcing || persistenceConfig.snapshots)) {
88
+ const eventStore = persistenceConfig.eventStore || new persistence_1.InMemoryEventStore();
89
+ const snapshotStore = persistenceConfig.snapshotStore || new persistence_1.InMemorySnapshotStore();
90
+ this.persistence = new persistence_1.PersistenceManager(eventStore, snapshotStore, {
91
+ eventSourcing: persistenceConfig.eventSourcing,
92
+ snapshots: persistenceConfig.snapshots,
93
+ snapshotInterval: persistenceConfig.snapshotInterval,
94
+ });
95
+ }
96
+ else {
97
+ this.persistence = null;
98
+ }
99
+ // Index machines by name
100
+ component.stateMachines.forEach(machine => {
101
+ this.machines.set(machine.name, machine);
102
+ });
103
+ // Setup cascading rules engine (XComponent pattern)
104
+ this.setupCascadingEngine();
105
+ }
106
+ /**
107
+ * Create a new FSM instance
108
+ *
109
+ * If the state machine defines a publicMemberType (XComponent pattern),
110
+ * the instance will have separate publicMember and internalMember.
111
+ * Otherwise, it uses the simple context pattern.
112
+ *
113
+ * @param machineName State machine name
114
+ * @param initialContext Initial context or public member data
115
+ * @returns Instance ID
116
+ */
117
+ createInstance(machineName, initialContext = {}) {
118
+ const machine = this.machines.get(machineName);
119
+ if (!machine) {
120
+ throw new Error(`Machine ${machineName} not found`);
121
+ }
122
+ const instanceId = (0, uuid_1.v4)();
123
+ // XComponent pattern: separate publicMember and internalMember
124
+ const instance = {
125
+ id: instanceId,
126
+ machineName,
127
+ currentState: machine.initialState,
128
+ context: machine.publicMemberType ? {} : initialContext,
129
+ publicMember: machine.publicMemberType ? initialContext : undefined,
130
+ internalMember: machine.publicMemberType ? {} : undefined,
131
+ createdAt: Date.now(),
132
+ updatedAt: Date.now(),
133
+ status: 'active',
134
+ };
135
+ this.instances.set(instanceId, instance);
136
+ // Add to indexes for fast lookups
137
+ this.addToIndex(instance);
138
+ this.emit('instance_created', instance);
139
+ // Setup timeout transitions if any from initial state
140
+ this.setupTimeouts(instanceId, machine.initialState);
141
+ // Setup auto-transitions if any from initial state
142
+ this.setupAutoTransitions(instanceId, machine.initialState);
143
+ return instanceId;
144
+ }
145
+ /**
146
+ * Send event to an instance
147
+ */
148
+ async sendEvent(instanceId, event) {
149
+ const instance = this.instances.get(instanceId);
150
+ if (!instance) {
151
+ throw new Error(`Instance ${instanceId} not found`);
152
+ }
153
+ if (instance.status !== 'active') {
154
+ throw new Error(`Instance ${instanceId} is not active`);
155
+ }
156
+ const machine = this.machines.get(instance.machineName);
157
+ if (!machine) {
158
+ throw new Error(`Machine ${instance.machineName} not found`);
159
+ }
160
+ // Use publicMember if available (XComponent pattern), otherwise fallback to context
161
+ const instanceContext = instance.publicMember || instance.context;
162
+ // Find applicable transition
163
+ const transition = this.findTransition(machine, instance.currentState, event, instanceContext);
164
+ if (!transition) {
165
+ this.emit('event_ignored', { instanceId, event, currentState: instance.currentState });
166
+ return;
167
+ }
168
+ // Check guards
169
+ if (transition.guards && !this.evaluateGuards(transition.guards, event, instanceContext)) {
170
+ this.emit('guard_failed', { instanceId, event, transition });
171
+ return;
172
+ }
173
+ const previousState = instance.currentState;
174
+ try {
175
+ // Execute transition
176
+ await this.executeTransition(instance, transition, event);
177
+ // Update instance
178
+ instance.currentState = transition.to;
179
+ instance.updatedAt = Date.now();
180
+ // Update indexes
181
+ this.updateIndexOnStateChange(instance, previousState, transition.to);
182
+ // Persist event (event sourcing)
183
+ let eventId = '';
184
+ if (this.persistence) {
185
+ eventId = await this.persistence.persistEvent(instanceId, instance.machineName, this.componentDef.name, event, previousState, transition.to);
186
+ // Set as current event for causality tracking
187
+ this.persistence.setCurrentEventId(eventId);
188
+ }
189
+ // Clear old timeouts
190
+ this.clearTimeouts(instanceId);
191
+ // Emit state change
192
+ this.emit('state_change', {
193
+ instanceId,
194
+ previousState,
195
+ newState: transition.to,
196
+ event,
197
+ eventId,
198
+ timestamp: Date.now(),
199
+ });
200
+ // Save snapshot if needed
201
+ // Note: With timer wheel, we don't pass pending timeouts map
202
+ // Timeouts are resynchronized during restore() based on elapsed time
203
+ if (this.persistence) {
204
+ await this.persistence.maybeSnapshot(instance, eventId, undefined);
205
+ }
206
+ // Check if final or error state
207
+ const targetState = machine.states.find(s => s.name === transition.to);
208
+ if (targetState && (targetState.type === types_1.StateType.FINAL || targetState.type === types_1.StateType.ERROR)) {
209
+ instance.status = targetState.type === types_1.StateType.FINAL ? 'completed' : 'error';
210
+ // Remove from indexes before disposing
211
+ this.removeFromIndex(instance);
212
+ this.emit('instance_disposed', instance);
213
+ this.instances.delete(instanceId);
214
+ return;
215
+ }
216
+ // Setup new timeouts
217
+ this.setupTimeouts(instanceId, transition.to);
218
+ // Setup auto-transitions
219
+ this.setupAutoTransitions(instanceId, transition.to);
220
+ // Handle inter-machine transitions
221
+ if (transition.type === types_1.TransitionType.INTER_MACHINE && transition.targetMachine) {
222
+ const newInstanceId = this.createInstance(transition.targetMachine, { ...instance.context });
223
+ this.emit('inter_machine_transition', {
224
+ sourceInstanceId: instanceId,
225
+ targetInstanceId: newInstanceId,
226
+ targetMachine: transition.targetMachine,
227
+ });
228
+ }
229
+ }
230
+ catch (error) {
231
+ instance.status = 'error';
232
+ // Remove from indexes before deleting
233
+ this.removeFromIndex(instance);
234
+ this.emit('instance_error', { instanceId, error: error.message });
235
+ this.instances.delete(instanceId);
236
+ }
237
+ }
238
+ /**
239
+ * Broadcast event to all matching instances (XComponent-style property matching)
240
+ *
241
+ * This method implements XComponent's property-based instance routing:
242
+ * - Finds all instances of the target machine in the specified state
243
+ * - Evaluates matching rules (property equality checks)
244
+ * - Routes event to ALL instances where matching rules pass
245
+ *
246
+ * Example:
247
+ * // 100 Order instances exist
248
+ * // Event: ExecutionInput { OrderId: 42, Quantity: 500 }
249
+ * // Matching rule: ExecutionInput.OrderId = Order.Id
250
+ * // → Automatically routes to Order instance with Id=42
251
+ *
252
+ * @param machineName Target state machine name
253
+ * @param currentState Current state filter (only instances in this state)
254
+ * @param event Event to broadcast
255
+ * @returns Number of instances that received the event
256
+ */
257
+ async broadcastEvent(machineName, currentState, event) {
258
+ const machine = this.machines.get(machineName);
259
+ if (!machine) {
260
+ throw new Error(`Machine ${machineName} not found`);
261
+ }
262
+ // Find ALL transitions with matching rules for this state/event combination
263
+ const transitionsWithMatchingRules = machine.transitions.filter(t => t.from === currentState && t.event === event.type && t.matchingRules && t.matchingRules.length > 0);
264
+ if (transitionsWithMatchingRules.length === 0) {
265
+ throw new Error(`No transition with matching rules found for ${machineName}.${currentState} on event ${event.type}`);
266
+ }
267
+ let processedCount = 0;
268
+ const processedInstances = new Set();
269
+ // For each transition with matching rules, find and process matching instances
270
+ for (const transition of transitionsWithMatchingRules) {
271
+ // Find all instances that match this transition's rules
272
+ const matchingInstances = this.findMatchingInstances(machineName, currentState, event, transition.matchingRules);
273
+ // Send event to each matching instance (only once per instance)
274
+ for (const instance of matchingInstances) {
275
+ if (processedInstances.has(instance.id)) {
276
+ continue; // Already processed by another transition
277
+ }
278
+ try {
279
+ const stateBefore = instance.currentState;
280
+ await this.sendEvent(instance.id, event);
281
+ // Check if state actually changed (or instance was disposed)
282
+ const instanceAfter = this.instances.get(instance.id);
283
+ const transitioned = !instanceAfter || instanceAfter.currentState !== stateBefore;
284
+ if (transitioned) {
285
+ processedInstances.add(instance.id);
286
+ processedCount++;
287
+ }
288
+ }
289
+ catch (error) {
290
+ this.emit('broadcast_error', {
291
+ instanceId: instance.id,
292
+ event,
293
+ error: error.message,
294
+ });
295
+ }
296
+ }
297
+ }
298
+ this.emit('broadcast_completed', {
299
+ machineName,
300
+ currentState,
301
+ event,
302
+ matchedCount: processedInstances.size,
303
+ processedCount,
304
+ });
305
+ return processedCount;
306
+ }
307
+ /**
308
+ * Find instances that match the property matching rules
309
+ *
310
+ * @param machineName Target machine name
311
+ * @param currentState Current state filter
312
+ * @param event Event to match against
313
+ * @param matchingRules Property matching rules
314
+ * @returns Array of matching instances
315
+ */
316
+ findMatchingInstances(machineName, currentState, event, matchingRules) {
317
+ // Performance optimization: Use hash-based index for O(1) lookup
318
+ // Instead of iterating all instances, use state index
319
+ // Try to use property index for direct lookup (fastest path)
320
+ // If we have a single matching rule with === operator, we can use the property index
321
+ if (matchingRules.length === 1 && (!matchingRules[0].operator || matchingRules[0].operator === '===')) {
322
+ const rule = matchingRules[0];
323
+ const eventValue = this.getNestedProperty(event.payload, rule.eventProperty);
324
+ if (eventValue !== undefined) {
325
+ // Check if property is top-level (no dots) for direct index lookup
326
+ if (!rule.instanceProperty.includes('.')) {
327
+ const propKey = `${machineName}:${rule.instanceProperty}:${String(eventValue)}`;
328
+ const candidateIds = this.propertyIndex.get(propKey);
329
+ if (candidateIds) {
330
+ // Filter by state and status
331
+ return Array.from(candidateIds)
332
+ .map(id => this.instances.get(id))
333
+ .filter(instance => instance &&
334
+ instance.currentState === currentState &&
335
+ instance.status === 'active');
336
+ }
337
+ }
338
+ }
339
+ }
340
+ // Fallback: Use state index (still faster than iterating all instances)
341
+ const stateKey = `${machineName}:${currentState}`;
342
+ const candidateIds = this.stateIndex.get(stateKey);
343
+ if (!candidateIds || candidateIds.size === 0) {
344
+ return [];
345
+ }
346
+ // Get instances and filter by matching rules
347
+ const candidates = Array.from(candidateIds)
348
+ .map(id => this.instances.get(id))
349
+ .filter(instance => instance && instance.status === 'active');
350
+ // Filter by matching rules
351
+ return candidates.filter(instance => {
352
+ return this.evaluateMatchingRules(instance, event, matchingRules);
353
+ });
354
+ }
355
+ /**
356
+ * Evaluate matching rules for an instance
357
+ *
358
+ * @param instance FSM instance to check
359
+ * @param event Event to match against
360
+ * @param matchingRules Matching rules to evaluate
361
+ * @returns true if all matching rules pass
362
+ */
363
+ evaluateMatchingRules(instance, event, matchingRules) {
364
+ return matchingRules.every(rule => {
365
+ const eventValue = this.getNestedProperty(event.payload, rule.eventProperty);
366
+ // Use publicMember if available (XComponent pattern), otherwise fallback to context
367
+ const instanceData = instance.publicMember || instance.context;
368
+ const instanceValue = this.getNestedProperty(instanceData, rule.instanceProperty);
369
+ // Handle undefined values
370
+ if (eventValue === undefined || instanceValue === undefined) {
371
+ return false;
372
+ }
373
+ // Apply operator (default to strict equality)
374
+ // Semantics: instanceValue operator eventValue
375
+ // Example: balance > threshold means instanceValue (balance) > eventValue (threshold)
376
+ const operator = rule.operator || '===';
377
+ switch (operator) {
378
+ case '===':
379
+ return instanceValue === eventValue;
380
+ case '!==':
381
+ return instanceValue !== eventValue;
382
+ case '>':
383
+ return instanceValue > eventValue;
384
+ case '<':
385
+ return instanceValue < eventValue;
386
+ case '>=':
387
+ return instanceValue >= eventValue;
388
+ case '<=':
389
+ return instanceValue <= eventValue;
390
+ default:
391
+ return instanceValue === eventValue;
392
+ }
393
+ });
394
+ }
395
+ /**
396
+ * Get nested property value from object using dot notation
397
+ *
398
+ * Example: getNestedProperty({ customer: { id: 42 } }, "customer.id") → 42
399
+ *
400
+ * @param obj Object to get property from
401
+ * @param path Property path (dot notation)
402
+ * @returns Property value or undefined
403
+ */
404
+ getNestedProperty(obj, path) {
405
+ return path.split('.').reduce((current, prop) => current?.[prop], obj);
406
+ }
407
+ /**
408
+ * Find applicable transition with support for specific triggering rules
409
+ *
410
+ * When multiple transitions exist from the same state with the same event,
411
+ * specific triggering rules differentiate them (XComponent pattern).
412
+ *
413
+ * @param machine State machine
414
+ * @param currentState Current state name
415
+ * @param event Event to match
416
+ * @param instanceContext Instance context for specific triggering rule evaluation
417
+ * @returns Matching transition or null
418
+ */
419
+ findTransition(machine, currentState, event, instanceContext) {
420
+ // Find all candidate transitions
421
+ const candidates = machine.transitions.filter(t => t.from === currentState && t.event === event.type);
422
+ if (candidates.length === 0) {
423
+ return null;
424
+ }
425
+ // Single candidate - return it
426
+ if (candidates.length === 1) {
427
+ return candidates[0];
428
+ }
429
+ // Multiple candidates - try specific triggering rules first
430
+ for (const transition of candidates) {
431
+ if (transition.specificTriggeringRule) {
432
+ try {
433
+ const func = new Function('event', 'context', `return ${transition.specificTriggeringRule}`);
434
+ if (func(event, instanceContext)) {
435
+ return transition;
436
+ }
437
+ }
438
+ catch {
439
+ // Rule evaluation failed, skip this transition
440
+ continue;
441
+ }
442
+ }
443
+ }
444
+ // If no specific triggering rules matched, try matching rules
445
+ // This handles cases where multiple transitions differentiate by matching rules (e.g., different operators)
446
+ for (const transition of candidates) {
447
+ if (transition.matchingRules && transition.matchingRules.length > 0) {
448
+ // Create a mock instance to evaluate matching rules
449
+ const mockInstance = {
450
+ id: 'temp',
451
+ machineName: machine.name,
452
+ currentState,
453
+ context: instanceContext,
454
+ publicMember: instanceContext,
455
+ createdAt: Date.now(),
456
+ updatedAt: Date.now(),
457
+ status: 'active',
458
+ };
459
+ if (this.evaluateMatchingRules(mockInstance, event, transition.matchingRules)) {
460
+ return transition;
461
+ }
462
+ }
463
+ }
464
+ // No rules matched - return first candidate (backward compatibility)
465
+ return candidates[0];
466
+ }
467
+ /**
468
+ * Evaluate guards
469
+ */
470
+ evaluateGuards(guards, event, context) {
471
+ return guards.every(guard => {
472
+ // Key matching
473
+ if (guard.keys) {
474
+ return guard.keys.every(key => event.payload[key] !== undefined);
475
+ }
476
+ // Contains check
477
+ if (guard.contains) {
478
+ return JSON.stringify(event.payload).includes(guard.contains);
479
+ }
480
+ // Custom function (evaluate as string - in production, use sandboxed eval)
481
+ if (guard.customFunction) {
482
+ try {
483
+ const func = new Function('event', 'context', `return ${guard.customFunction}`);
484
+ return func(event, context);
485
+ }
486
+ catch {
487
+ return false;
488
+ }
489
+ }
490
+ return true;
491
+ });
492
+ }
493
+ /**
494
+ * Execute transition
495
+ *
496
+ * Creates a Sender instance and passes it to triggered methods,
497
+ * enabling cross-instance communication (XComponent pattern)
498
+ */
499
+ async executeTransition(instance, transition, event) {
500
+ // In production, execute triggered methods here
501
+ if (transition.triggeredMethod) {
502
+ const sender = new SenderImpl(this, this.registry);
503
+ const instanceContext = instance.publicMember || instance.context;
504
+ this.emit('triggered_method', {
505
+ instanceId: instance.id,
506
+ method: transition.triggeredMethod,
507
+ event,
508
+ context: instanceContext,
509
+ sender,
510
+ });
511
+ }
512
+ }
513
+ /**
514
+ * Setup timeout transitions
515
+ */
516
+ /**
517
+ * Setup timeout transitions using timer wheel (performance optimized)
518
+ *
519
+ * Instead of creating one setTimeout per instance (O(n) timers),
520
+ * use a single timer wheel that manages all timeouts (O(1) timer).
521
+ */
522
+ setupTimeouts(instanceId, stateName) {
523
+ const instance = this.instances.get(instanceId);
524
+ if (!instance)
525
+ return;
526
+ const machine = this.machines.get(instance.machineName);
527
+ if (!machine)
528
+ return;
529
+ const timeoutTransitions = machine.transitions.filter(t => t.from === stateName && t.type === types_1.TransitionType.TIMEOUT);
530
+ timeoutTransitions.forEach(transition => {
531
+ if (transition.timeoutMs) {
532
+ const taskId = `${instanceId}-${stateName}-${transition.event}`;
533
+ // Track task for cleanup
534
+ if (!this.timeoutTasks.has(instanceId)) {
535
+ this.timeoutTasks.set(instanceId, []);
536
+ }
537
+ this.timeoutTasks.get(instanceId).push(taskId);
538
+ // Use timer wheel instead of setTimeout
539
+ this.timerWheel.addTimeout(taskId, transition.timeoutMs, () => {
540
+ // Check if instance still exists and is in the same state
541
+ const currentInstance = this.instances.get(instanceId);
542
+ if (currentInstance && currentInstance.currentState === stateName) {
543
+ this.sendEvent(instanceId, {
544
+ type: transition.event,
545
+ payload: { reason: 'timeout' },
546
+ timestamp: Date.now(),
547
+ }).catch(error => {
548
+ console.error(`Timeout event failed for ${instanceId}:`, error);
549
+ });
550
+ }
551
+ // Remove taskId from tracking
552
+ const tasks = this.timeoutTasks.get(instanceId);
553
+ if (tasks) {
554
+ const index = tasks.indexOf(taskId);
555
+ if (index >= 0)
556
+ tasks.splice(index, 1);
557
+ }
558
+ });
559
+ }
560
+ });
561
+ }
562
+ /**
563
+ * Setup auto-transitions (XComponent-style automatic transitions)
564
+ *
565
+ * Auto-transitions are triggered automatically when entering a state,
566
+ * typically with timeoutMs: 0 for immediate execution.
567
+ *
568
+ * Example:
569
+ * - from: Validated
570
+ * to: Processing
571
+ * event: AUTO_PROCESS
572
+ * type: auto
573
+ * timeoutMs: 0
574
+ */
575
+ setupAutoTransitions(instanceId, stateName) {
576
+ const instance = this.instances.get(instanceId);
577
+ if (!instance)
578
+ return;
579
+ const machine = this.machines.get(instance.machineName);
580
+ if (!machine)
581
+ return;
582
+ const autoTransitions = machine.transitions.filter(t => t.from === stateName && t.type === types_1.TransitionType.AUTO);
583
+ autoTransitions.forEach(transition => {
584
+ // Use publicMember if available (XComponent pattern), otherwise fallback to context
585
+ const instanceContext = instance.publicMember || instance.context;
586
+ // Check guards before scheduling auto-transition
587
+ if (transition.guards && !this.evaluateGuards(transition.guards, { type: transition.event, payload: {}, timestamp: Date.now() }, instanceContext)) {
588
+ return; // Guard failed, skip this auto-transition
589
+ }
590
+ const delay = transition.timeoutMs || 0; // Default to immediate (0ms)
591
+ const taskId = `${instanceId}-${stateName}-auto-${transition.event}`;
592
+ // Track task for cleanup
593
+ if (!this.timeoutTasks.has(instanceId)) {
594
+ this.timeoutTasks.set(instanceId, []);
595
+ }
596
+ this.timeoutTasks.get(instanceId).push(taskId);
597
+ // Use timer wheel for auto-transitions
598
+ this.timerWheel.addTimeout(taskId, delay, () => {
599
+ // Check if instance still exists and is in the same state
600
+ const currentInstance = this.instances.get(instanceId);
601
+ if (currentInstance && currentInstance.currentState === stateName) {
602
+ this.sendEvent(instanceId, {
603
+ type: transition.event,
604
+ payload: { reason: 'auto-transition' },
605
+ timestamp: Date.now(),
606
+ }).catch(error => {
607
+ console.error(`Auto-transition failed for ${instanceId}:`, error);
608
+ });
609
+ }
610
+ // Remove taskId from tracking
611
+ const tasks = this.timeoutTasks.get(instanceId);
612
+ if (tasks) {
613
+ const index = tasks.indexOf(taskId);
614
+ if (index >= 0)
615
+ tasks.splice(index, 1);
616
+ }
617
+ });
618
+ });
619
+ }
620
+ /**
621
+ * Setup cascading rules engine (XComponent pattern)
622
+ *
623
+ * Listens to state_change events and automatically triggers cross-machine updates
624
+ * based on cascading rules defined in state definitions.
625
+ */
626
+ setupCascadingEngine() {
627
+ this.on('state_change', async (data) => {
628
+ const { instanceId, newState } = data;
629
+ const instance = this.instances.get(instanceId);
630
+ if (!instance)
631
+ return;
632
+ const machine = this.machines.get(instance.machineName);
633
+ if (!machine)
634
+ return;
635
+ // Find the state definition
636
+ const state = machine.states.find(s => s.name === newState);
637
+ if (!state || !state.cascadingRules || state.cascadingRules.length === 0) {
638
+ return; // No cascading rules for this state
639
+ }
640
+ // Process each cascading rule
641
+ for (const rule of state.cascadingRules) {
642
+ try {
643
+ await this.processCascadingRule(instance, rule);
644
+ }
645
+ catch (error) {
646
+ this.emit('cascade_error', {
647
+ sourceInstanceId: instanceId,
648
+ rule,
649
+ error: error.message,
650
+ });
651
+ }
652
+ }
653
+ });
654
+ }
655
+ /**
656
+ * Add instance to indexes (for O(1) lookup)
657
+ * Performance optimization: XComponent hash-based matching
658
+ */
659
+ addToIndex(instance) {
660
+ const { id, machineName, currentState, publicMember, context } = instance;
661
+ // Machine index
662
+ if (!this.machineIndex.has(machineName)) {
663
+ this.machineIndex.set(machineName, new Set());
664
+ }
665
+ this.machineIndex.get(machineName).add(id);
666
+ // State index
667
+ const stateKey = `${machineName}:${currentState}`;
668
+ if (!this.stateIndex.has(stateKey)) {
669
+ this.stateIndex.set(stateKey, new Set());
670
+ }
671
+ this.stateIndex.get(stateKey).add(id);
672
+ // Property index (for commonly matched properties)
673
+ const instanceData = publicMember || context;
674
+ if (instanceData) {
675
+ // Index all top-level properties for fast matching
676
+ for (const [propName, propValue] of Object.entries(instanceData)) {
677
+ if (propValue !== null && propValue !== undefined) {
678
+ const propKey = `${machineName}:${propName}:${String(propValue)}`;
679
+ if (!this.propertyIndex.has(propKey)) {
680
+ this.propertyIndex.set(propKey, new Set());
681
+ }
682
+ this.propertyIndex.get(propKey).add(id);
683
+ }
684
+ }
685
+ }
686
+ }
687
+ /**
688
+ * Remove instance from indexes
689
+ */
690
+ removeFromIndex(instance) {
691
+ const { id, machineName, currentState, publicMember, context } = instance;
692
+ // Machine index
693
+ this.machineIndex.get(machineName)?.delete(id);
694
+ // State index
695
+ const stateKey = `${machineName}:${currentState}`;
696
+ this.stateIndex.get(stateKey)?.delete(id);
697
+ // Property index
698
+ const instanceData = publicMember || context;
699
+ if (instanceData) {
700
+ for (const [propName, propValue] of Object.entries(instanceData)) {
701
+ if (propValue !== null && propValue !== undefined) {
702
+ const propKey = `${machineName}:${propName}:${String(propValue)}`;
703
+ this.propertyIndex.get(propKey)?.delete(id);
704
+ }
705
+ }
706
+ }
707
+ }
708
+ /**
709
+ * Update state index when state changes
710
+ */
711
+ updateIndexOnStateChange(instance, oldState, newState) {
712
+ const { id, machineName } = instance;
713
+ // Remove from old state index
714
+ const oldStateKey = `${machineName}:${oldState}`;
715
+ this.stateIndex.get(oldStateKey)?.delete(id);
716
+ // Add to new state index
717
+ const newStateKey = `${machineName}:${newState}`;
718
+ if (!this.stateIndex.has(newStateKey)) {
719
+ this.stateIndex.set(newStateKey, new Set());
720
+ }
721
+ this.stateIndex.get(newStateKey).add(id);
722
+ }
723
+ /**
724
+ * Process a single cascading rule
725
+ *
726
+ * Applies payload templating and broadcasts event to target instances
727
+ * If matchingRules exist, uses property-based routing
728
+ * Otherwise, sends to ALL instances in the target state
729
+ */
730
+ async processCascadingRule(sourceInstance, rule) {
731
+ // Get source context (publicMember or context)
732
+ const sourceContext = sourceInstance.publicMember || sourceInstance.context;
733
+ // Apply payload templating
734
+ const payload = rule.payload ? this.applyPayloadTemplate(rule.payload, sourceContext) : {};
735
+ const event = {
736
+ type: rule.event,
737
+ payload,
738
+ timestamp: Date.now(),
739
+ };
740
+ let processedCount = 0;
741
+ if (rule.matchingRules && rule.matchingRules.length > 0) {
742
+ // Use property-based routing
743
+ processedCount = await this.broadcastEvent(rule.targetMachine, rule.targetState, event);
744
+ }
745
+ else {
746
+ // No matching rules - send to ALL instances in target state
747
+ // Performance: Use state index instead of iterating all instances
748
+ const stateKey = `${rule.targetMachine}:${rule.targetState}`;
749
+ const candidateIds = this.stateIndex.get(stateKey);
750
+ if (candidateIds) {
751
+ for (const instanceId of candidateIds) {
752
+ const instance = this.instances.get(instanceId);
753
+ if (!instance || instance.status !== 'active')
754
+ continue;
755
+ try {
756
+ await this.sendEvent(instance.id, event);
757
+ processedCount++;
758
+ }
759
+ catch (error) {
760
+ // Continue processing other instances even if one fails
761
+ this.emit('cascade_error', {
762
+ sourceInstanceId: sourceInstance.id,
763
+ targetInstanceId: instance.id,
764
+ error: error.message,
765
+ });
766
+ }
767
+ }
768
+ }
769
+ }
770
+ this.emit('cascade_completed', {
771
+ sourceInstanceId: sourceInstance.id,
772
+ targetMachine: rule.targetMachine,
773
+ targetState: rule.targetState,
774
+ event: rule.event,
775
+ processedCount,
776
+ });
777
+ }
778
+ /**
779
+ * Apply payload template with {{property}} syntax
780
+ *
781
+ * Replaces {{property}} with actual values from source context
782
+ *
783
+ * Example:
784
+ * payload: { orderId: "{{Id}}", total: "{{Total}}" }
785
+ * context: { Id: 42, Total: 99.99 }
786
+ * result: { orderId: 42, total: 99.99 }
787
+ */
788
+ applyPayloadTemplate(template, context) {
789
+ const result = {};
790
+ for (const [key, value] of Object.entries(template)) {
791
+ if (typeof value === 'string' && value.startsWith('{{') && value.endsWith('}}')) {
792
+ // Extract property name: "{{Id}}" → "Id"
793
+ const propertyPath = value.slice(2, -2).trim();
794
+ result[key] = this.getNestedProperty(context, propertyPath);
795
+ }
796
+ else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
797
+ // Recursively apply template for nested objects
798
+ result[key] = this.applyPayloadTemplate(value, context);
799
+ }
800
+ else {
801
+ // Use value as-is
802
+ result[key] = value;
803
+ }
804
+ }
805
+ return result;
806
+ }
807
+ /**
808
+ * Clear timeouts for instance (using timer wheel)
809
+ */
810
+ clearTimeouts(instanceId) {
811
+ const taskIds = this.timeoutTasks.get(instanceId);
812
+ if (taskIds) {
813
+ // Remove all timeout tasks for this instance
814
+ taskIds.forEach(taskId => {
815
+ this.timerWheel.removeTimeout(taskId);
816
+ });
817
+ this.timeoutTasks.delete(instanceId);
818
+ }
819
+ }
820
+ /**
821
+ * Get instance
822
+ */
823
+ getInstance(instanceId) {
824
+ return this.instances.get(instanceId);
825
+ }
826
+ /**
827
+ * Get all instances
828
+ */
829
+ getAllInstances() {
830
+ return Array.from(this.instances.values());
831
+ }
832
+ /**
833
+ * Get instances by machine
834
+ */
835
+ getInstancesByMachine(machineName) {
836
+ return Array.from(this.instances.values()).filter(i => i.machineName === machineName);
837
+ }
838
+ /**
839
+ * Simulate FSM path
840
+ */
841
+ simulatePath(machineName, events) {
842
+ const machine = this.machines.get(machineName);
843
+ if (!machine) {
844
+ return { success: false, path: [], error: `Machine ${machineName} not found` };
845
+ }
846
+ const path = [machine.initialState];
847
+ let currentState = machine.initialState;
848
+ const context = {};
849
+ for (const event of events) {
850
+ const transition = this.findTransition(machine, currentState, event, context);
851
+ if (!transition) {
852
+ return { success: false, path, error: `No transition from ${currentState} for event ${event.type}` };
853
+ }
854
+ if (transition.guards && !this.evaluateGuards(transition.guards, event, context)) {
855
+ return { success: false, path, error: `Guard failed for transition from ${currentState}` };
856
+ }
857
+ currentState = transition.to;
858
+ path.push(currentState);
859
+ const state = machine.states.find(s => s.name === currentState);
860
+ if (state && (state.type === types_1.StateType.FINAL || state.type === types_1.StateType.ERROR)) {
861
+ break;
862
+ }
863
+ }
864
+ return { success: true, path };
865
+ }
866
+ // ============================================================
867
+ // PHASE 4: PERSISTENCE & RESTORATION
868
+ // ============================================================
869
+ /**
870
+ * Restore all instances from snapshots (for long-running workflows)
871
+ *
872
+ * Called after restart to restore system state from persistence
873
+ *
874
+ * Example:
875
+ * const runtime = new FSMRuntime(component, { snapshots: true });
876
+ * await runtime.restore();
877
+ * // System is now in same state as before restart
878
+ */
879
+ async restore() {
880
+ if (!this.persistence) {
881
+ throw new Error('Persistence is not enabled');
882
+ }
883
+ const snapshots = await this.persistence.getAllSnapshots();
884
+ let restored = 0;
885
+ let failed = 0;
886
+ for (const snapshot of snapshots) {
887
+ try {
888
+ const instance = snapshot.instance;
889
+ // Validate machine exists
890
+ if (!this.machines.has(instance.machineName)) {
891
+ failed++;
892
+ this.emit('restore_error', {
893
+ instanceId: instance.id,
894
+ error: `Machine ${instance.machineName} not found`,
895
+ });
896
+ continue;
897
+ }
898
+ // Restore instance
899
+ this.instances.set(instance.id, instance);
900
+ restored++;
901
+ this.emit('instance_restored', {
902
+ instanceId: instance.id,
903
+ machineName: instance.machineName,
904
+ currentState: instance.currentState,
905
+ });
906
+ }
907
+ catch (error) {
908
+ failed++;
909
+ this.emit('restore_error', {
910
+ instanceId: snapshot.instance.id,
911
+ error: error.message,
912
+ });
913
+ }
914
+ }
915
+ // Resynchronize timeouts after restore
916
+ if (restored > 0) {
917
+ await this.resynchronizeTimeouts();
918
+ }
919
+ return { restored, failed };
920
+ }
921
+ /**
922
+ * Resynchronize timeouts after restart
923
+ *
924
+ * Recalculates timeout transitions based on current state and elapsed time
925
+ * Handles expired timeouts by triggering them immediately
926
+ */
927
+ async resynchronizeTimeouts() {
928
+ let synced = 0;
929
+ let expired = 0;
930
+ for (const [instanceId, instance] of this.instances.entries()) {
931
+ if (instance.status !== 'active') {
932
+ continue;
933
+ }
934
+ const machine = this.machines.get(instance.machineName);
935
+ if (!machine) {
936
+ continue;
937
+ }
938
+ // Find timeout transitions from current state
939
+ const timeoutTransitions = machine.transitions.filter(t => t.from === instance.currentState && t.type === types_1.TransitionType.TIMEOUT);
940
+ for (const transition of timeoutTransitions) {
941
+ if (!transition.timeoutMs) {
942
+ continue;
943
+ }
944
+ // Calculate elapsed time since last update
945
+ const elapsedMs = Date.now() - instance.updatedAt;
946
+ const remainingMs = transition.timeoutMs - elapsedMs;
947
+ if (remainingMs <= 0) {
948
+ // Timeout already expired - trigger immediately
949
+ try {
950
+ await this.sendEvent(instanceId, {
951
+ type: transition.event,
952
+ payload: { reason: 'timeout_expired_during_restart' },
953
+ timestamp: Date.now(),
954
+ });
955
+ expired++;
956
+ }
957
+ catch (error) {
958
+ this.emit('timeout_resync_error', {
959
+ instanceId,
960
+ error: error.message,
961
+ });
962
+ }
963
+ }
964
+ else {
965
+ // Timeout still pending - reschedule with remaining time using timer wheel
966
+ const taskId = `${instanceId}-${instance.currentState}-${transition.event}`;
967
+ // Track task for cleanup
968
+ if (!this.timeoutTasks.has(instanceId)) {
969
+ this.timeoutTasks.set(instanceId, []);
970
+ }
971
+ this.timeoutTasks.get(instanceId).push(taskId);
972
+ this.timerWheel.addTimeout(taskId, remainingMs, () => {
973
+ const currentInstance = this.instances.get(instanceId);
974
+ if (currentInstance && currentInstance.currentState === instance.currentState) {
975
+ this.sendEvent(instanceId, {
976
+ type: transition.event,
977
+ payload: { reason: 'timeout' },
978
+ timestamp: Date.now(),
979
+ }).catch(error => {
980
+ console.error(`Timeout resync failed for ${instanceId}:`, error);
981
+ });
982
+ }
983
+ // Remove taskId from tracking
984
+ const tasks = this.timeoutTasks.get(instanceId);
985
+ if (tasks) {
986
+ const index = tasks.indexOf(taskId);
987
+ if (index >= 0)
988
+ tasks.splice(index, 1);
989
+ }
990
+ });
991
+ synced++;
992
+ }
993
+ }
994
+ // Resynchronize auto-transitions (should trigger immediately if not already transitioned)
995
+ const autoTransitions = machine.transitions.filter(t => t.from === instance.currentState && t.type === types_1.TransitionType.AUTO);
996
+ for (const transition of autoTransitions) {
997
+ // Use publicMember if available (XComponent pattern), otherwise fallback to context
998
+ const instanceContext = instance.publicMember || instance.context;
999
+ // Check guards before scheduling auto-transition
1000
+ if (transition.guards && !this.evaluateGuards(transition.guards, { type: transition.event, payload: {}, timestamp: Date.now() }, instanceContext)) {
1001
+ continue; // Guard failed, skip this auto-transition
1002
+ }
1003
+ const delay = transition.timeoutMs || 0;
1004
+ // Calculate elapsed time
1005
+ const elapsedMs = Date.now() - instance.updatedAt;
1006
+ const remainingMs = Math.max(0, delay - elapsedMs);
1007
+ const taskId = `${instanceId}-${instance.currentState}-auto-${transition.event}`;
1008
+ // Track task for cleanup
1009
+ if (!this.timeoutTasks.has(instanceId)) {
1010
+ this.timeoutTasks.set(instanceId, []);
1011
+ }
1012
+ this.timeoutTasks.get(instanceId).push(taskId);
1013
+ this.timerWheel.addTimeout(taskId, remainingMs, () => {
1014
+ const currentInstance = this.instances.get(instanceId);
1015
+ if (currentInstance && currentInstance.currentState === instance.currentState) {
1016
+ this.sendEvent(instanceId, {
1017
+ type: transition.event,
1018
+ payload: { reason: 'auto-transition' },
1019
+ timestamp: Date.now(),
1020
+ }).catch(error => {
1021
+ console.error(`Auto-transition resync failed for ${instanceId}:`, error);
1022
+ });
1023
+ }
1024
+ // Remove taskId from tracking
1025
+ const tasks = this.timeoutTasks.get(instanceId);
1026
+ if (tasks) {
1027
+ const index = tasks.indexOf(taskId);
1028
+ if (index >= 0)
1029
+ tasks.splice(index, 1);
1030
+ }
1031
+ });
1032
+ synced++;
1033
+ }
1034
+ }
1035
+ return { synced, expired };
1036
+ }
1037
+ /**
1038
+ * Get persistence manager (for testing/inspection)
1039
+ */
1040
+ getPersistenceManager() {
1041
+ return this.persistence;
1042
+ }
1043
+ /**
1044
+ * Get instance event history (for audit/debug)
1045
+ */
1046
+ async getInstanceHistory(instanceId) {
1047
+ if (!this.persistence) {
1048
+ return [];
1049
+ }
1050
+ return await this.persistence.getInstanceEvents(instanceId);
1051
+ }
1052
+ /**
1053
+ * Get all persisted events for this component
1054
+ */
1055
+ async getAllPersistedEvents() {
1056
+ if (!this.persistence) {
1057
+ return [];
1058
+ }
1059
+ return await this.persistence.getAllEvents();
1060
+ }
1061
+ /**
1062
+ * Trace event causality chain (for debugging cascades/sender)
1063
+ */
1064
+ async traceEventCausality(eventId) {
1065
+ if (!this.persistence) {
1066
+ return [];
1067
+ }
1068
+ return await this.persistence.traceEventCausality(eventId);
1069
+ }
1070
+ /**
1071
+ * Get component definition
1072
+ */
1073
+ getComponent() {
1074
+ return this.componentDef;
1075
+ }
1076
+ /**
1077
+ * Set component registry for cross-component communication
1078
+ */
1079
+ setRegistry(registry) {
1080
+ this.registry = registry;
1081
+ }
1082
+ /**
1083
+ * Get available transitions from current state of an instance
1084
+ */
1085
+ getAvailableTransitions(instanceId) {
1086
+ const instance = this.getInstance(instanceId);
1087
+ if (!instance) {
1088
+ return [];
1089
+ }
1090
+ const machine = this.machines.get(instance.machineName);
1091
+ if (!machine) {
1092
+ return [];
1093
+ }
1094
+ // Find all transitions from current state
1095
+ return machine.transitions.filter(t => t.from === instance.currentState);
1096
+ }
1097
+ /**
1098
+ * Stop the runtime and cleanup resources
1099
+ * Important: Call this when done to prevent memory leaks from timer wheel
1100
+ */
1101
+ dispose() {
1102
+ // Stop timer wheel
1103
+ this.timerWheel.stop();
1104
+ // Clear all instances
1105
+ this.instances.clear();
1106
+ this.timeoutTasks.clear();
1107
+ // Clear indexes
1108
+ this.machineIndex.clear();
1109
+ this.stateIndex.clear();
1110
+ this.propertyIndex.clear();
1111
+ // Remove all event listeners
1112
+ this.removeAllListeners();
1113
+ }
1114
+ }
1115
+ exports.FSMRuntime = FSMRuntime;
1116
+ /**
1117
+ * Load component from object
1118
+ */
1119
+ function loadComponent(data) {
1120
+ return data;
1121
+ }
1122
+ //# sourceMappingURL=fsm-runtime.js.map