@doeixd/machine 0.0.7 → 0.0.9

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.
@@ -0,0 +1,2325 @@
1
+ /**
2
+ * @file Middleware/interception system for state machines
3
+ * @description Provides composable middleware for logging, analytics, validation, and more.
4
+ */
5
+
6
+ import type { Context, BaseMachine } from './index';
7
+
8
+ // =============================================================================
9
+ // SECTION: MIDDLEWARE TYPES
10
+ // =============================================================================
11
+
12
+ /**
13
+ * Context object passed to middleware hooks containing transition metadata.
14
+ * @template C - The context object type
15
+ */
16
+ export interface MiddlewareContext<C extends object> {
17
+ /** The name of the transition being called */
18
+ transitionName: string;
19
+ /** The current machine context before the transition */
20
+ context: Readonly<C>;
21
+ /** Arguments passed to the transition function */
22
+ args: any[];
23
+ }
24
+
25
+ /**
26
+ * Result object passed to after hooks containing transition outcome.
27
+ * @template C - The context object type
28
+ */
29
+ export interface MiddlewareResult<C extends object> {
30
+ /** The name of the transition that was called */
31
+ transitionName: string;
32
+ /** The context before the transition */
33
+ prevContext: Readonly<C>;
34
+ /** The context after the transition */
35
+ nextContext: Readonly<C>;
36
+ /** Arguments that were passed to the transition */
37
+ args: any[];
38
+ }
39
+
40
+ /**
41
+ * Error context passed to error hooks.
42
+ * @template C - The context object type
43
+ */
44
+ export interface MiddlewareError<C extends object> {
45
+ /** The name of the transition that failed */
46
+ transitionName: string;
47
+ /** The context when the error occurred */
48
+ context: Readonly<C>;
49
+ /** Arguments that were passed to the transition */
50
+ args: any[];
51
+ /** The error that was thrown */
52
+ error: Error;
53
+ }
54
+
55
+ /**
56
+ * Configuration object for middleware hooks.
57
+ * All hooks are optional - provide only the ones you need.
58
+ * @template C - The context object type
59
+ */
60
+ /**
61
+ * Strongly typed middleware hooks with precise context and return types.
62
+ * All hooks are optional - provide only the ones you need.
63
+ *
64
+ * @template C - The machine context type for precise type inference
65
+ */
66
+ export interface MiddlewareHooks<C extends object> {
67
+ /**
68
+ * Called before a transition executes.
69
+ * Can be used for validation, logging, analytics, etc.
70
+ *
71
+ * @param ctx - Transition context with machine state and transition details
72
+ * @returns void to continue, CANCEL to abort silently, or Promise for async validation
73
+ *
74
+ * @example
75
+ * ```typescript
76
+ * before: ({ transitionName, args, context }) => {
77
+ * if (transitionName === 'withdraw' && context.balance < args[0]) {
78
+ * throw new Error('Insufficient funds');
79
+ * }
80
+ * }
81
+ * ```
82
+ */
83
+ before?: (ctx: MiddlewareContext<C>) => void | typeof CANCEL | Promise<void | typeof CANCEL>;
84
+
85
+ /**
86
+ * Called after a transition successfully executes.
87
+ * Receives both the previous and next context.
88
+ * Cannot prevent the transition (it already happened).
89
+ *
90
+ * @param result - Transition result with before/after contexts and transition details
91
+ *
92
+ * @example
93
+ * ```typescript
94
+ * after: ({ transitionName, before, after }) => {
95
+ * console.log(`${transitionName}: ${before.count} -> ${after.count}`);
96
+ * }
97
+ * ```
98
+ */
99
+ after?: (result: MiddlewareResult<C>) => void | Promise<void>;
100
+
101
+ /**
102
+ * Called if a transition throws an error.
103
+ * Can be used for error logging, Sentry reporting, fallback states, etc.
104
+ *
105
+ * @param error - Error context with transition details and the thrown error
106
+ * @returns
107
+ * - void/undefined/null: Re-throw the original error (default)
108
+ * - BaseMachine: Use this as fallback state instead of throwing
109
+ * - throw new Error: Transform the error
110
+ *
111
+ * @example
112
+ * ```typescript
113
+ * error: ({ transitionName, error, context }) => {
114
+ * // Log to error reporting service
115
+ * reportError(error, { transitionName, context });
116
+ *
117
+ * // Return fallback state for recoverable errors
118
+ * if (error.message.includes('network')) {
119
+ * return createMachine({ ...context, error: 'offline' }, transitions);
120
+ * }
121
+ * }
122
+ * ```
123
+ */
124
+ error?: (error: MiddlewareError<C>) => void | null | BaseMachine<C> | Promise<void | null | BaseMachine<C>>;
125
+ }
126
+
127
+ /**
128
+ * Options for middleware configuration.
129
+ */
130
+ export interface MiddlewareOptions {
131
+ /**
132
+ * Execution mode for middleware hooks.
133
+ * - 'sync': Hooks must be synchronous, throws if hooks return Promise
134
+ * - 'async': Always await hooks and transition
135
+ * - 'auto' (default): Adaptive mode - starts synchronously, automatically handles async results if encountered
136
+ *
137
+ * Note: 'auto' mode provides the best of both worlds - zero overhead for sync transitions
138
+ * while seamlessly handling async ones when they occur.
139
+ * @default 'auto'
140
+ */
141
+ mode?: 'sync' | 'async' | 'auto';
142
+
143
+ /**
144
+ * Properties to exclude from middleware interception.
145
+ * Useful for excluding utility methods or getters.
146
+ * @default ['context']
147
+ */
148
+ exclude?: string[];
149
+ }
150
+
151
+ // =============================================================================
152
+ // SECTION: CANCELLATION SUPPORT
153
+ // =============================================================================
154
+
155
+ /**
156
+ * Special symbol that can be returned from before hooks to cancel a transition.
157
+ * When returned, the transition will not execute and the current machine state is preserved.
158
+ *
159
+ * @example
160
+ * createMiddleware(machine, {
161
+ * before: ({ transitionName, context }) => {
162
+ * if (shouldCancel(context)) {
163
+ * return CANCEL; // Abort transition without throwing
164
+ * }
165
+ * }
166
+ * });
167
+ */
168
+ export const CANCEL = Symbol('CANCEL');
169
+
170
+ // =============================================================================
171
+ // SECTION: UTILITY TYPES FOR AUGMENTED MACHINES
172
+ // =============================================================================
173
+
174
+ /**
175
+ * Augmented machine type with history tracking capabilities.
176
+ * @template M - The base machine type
177
+ * @template C - The context type
178
+ */
179
+ export type WithHistory<M extends BaseMachine<any>> = M & {
180
+ /** Array of recorded transition history entries */
181
+ history: HistoryEntry[];
182
+ /** Clear all history entries */
183
+ clearHistory: () => void;
184
+ };
185
+
186
+ /**
187
+ * Augmented machine type with snapshot tracking capabilities.
188
+ * @template M - The base machine type
189
+ * @template C - The context type
190
+ */
191
+ export type WithSnapshot<M extends BaseMachine<any>, C extends object = Context<M>> = M & {
192
+ /** Array of recorded context snapshots */
193
+ snapshots: ContextSnapshot<C>[];
194
+ /** Clear all snapshots */
195
+ clearSnapshots: () => void;
196
+ /** Restore machine to a previous context state */
197
+ restoreSnapshot: (context: C) => M;
198
+ };
199
+
200
+ /**
201
+ * Augmented machine type with full time-travel debugging capabilities.
202
+ * Combines both history and snapshot tracking.
203
+ * @template M - The base machine type
204
+ * @template C - The context type
205
+ */
206
+ export type WithTimeTravel<M extends BaseMachine<any>, C extends object = Context<M>> = M & {
207
+ /** Array of recorded transition history entries */
208
+ history: HistoryEntry[];
209
+ /** Array of recorded context snapshots */
210
+ snapshots: ContextSnapshot<C>[];
211
+ /** Clear all history and snapshots */
212
+ clearTimeTravel: () => void;
213
+ /** Restore machine to a previous context state */
214
+ restoreSnapshot: (context: C) => M;
215
+ /** Replay all transitions from a specific snapshot */
216
+ replayFrom: (snapshotIndex: number) => M;
217
+ };
218
+
219
+ // =============================================================================
220
+ // SECTION: CORE MIDDLEWARE FUNCTION
221
+ // =============================================================================
222
+
223
+ /**
224
+ * Wraps a machine with middleware hooks that intercept all transitions.
225
+ * Uses direct property wrapping for optimal performance (3x faster than Proxy).
226
+ *
227
+ * The middleware preserves:
228
+ * - Full type safety (return type matches input machine)
229
+ * - `this` binding for transitions
230
+ * - Async and sync transitions
231
+ * - Machine immutability
232
+ *
233
+ * @template M - The machine type
234
+ * @param machine - The machine to wrap with middleware
235
+ * @param hooks - Middleware hooks (before, after, error)
236
+ * @param options - Configuration options
237
+ * @returns A new machine with middleware applied
238
+ *
239
+ * @example
240
+ * const instrumented = createMiddleware(counter, {
241
+ * before: ({ transitionName, context, args }) => {
242
+ * console.log(`→ ${transitionName}`, args);
243
+ * },
244
+ * after: ({ transitionName, prevContext, nextContext }) => {
245
+ * console.log(`✓ ${transitionName}`, nextContext);
246
+ * },
247
+ * error: ({ transitionName, error }) => {
248
+ * console.error(`✗ ${transitionName}:`, error);
249
+ * }
250
+ * });
251
+ */
252
+ export function createMiddleware<M extends BaseMachine<any>>(
253
+ machine: M,
254
+ hooks: MiddlewareHooks<Context<M>>,
255
+ options: MiddlewareOptions = {}
256
+ ): M {
257
+ const { mode = 'auto', exclude = ['context'] } = options;
258
+
259
+ // Build wrapped machine object with direct property iteration
260
+ const wrapped: any = {};
261
+
262
+ // Copy all properties and wrap functions
263
+ for (const prop in machine) {
264
+ if (!Object.prototype.hasOwnProperty.call(machine, prop)) continue;
265
+
266
+ const value = machine[prop];
267
+
268
+ // Always copy context
269
+ if (prop === 'context') {
270
+ wrapped.context = value;
271
+ continue;
272
+ }
273
+
274
+ // Skip excluded properties
275
+ if (exclude.includes(prop)) {
276
+ wrapped[prop] = value;
277
+ continue;
278
+ }
279
+
280
+ // Skip non-functions and private methods
281
+ if (typeof value !== 'function' || prop.startsWith('_')) {
282
+ wrapped[prop] = value;
283
+ continue;
284
+ }
285
+
286
+ // Wrap transition function
287
+ wrapped[prop] = createTransitionWrapper(
288
+ prop,
289
+ value,
290
+ machine,
291
+ hooks,
292
+ mode
293
+ );
294
+ }
295
+
296
+ return wrapped as M;
297
+ }
298
+
299
+ /**
300
+ * Creates a wrapped transition function with middleware hooks.
301
+ * Extracted as a separate function for clarity and reusability.
302
+ *
303
+ * @internal
304
+ */
305
+ function createTransitionWrapper<M extends BaseMachine<any>>(
306
+ transitionName: string,
307
+ originalFn: Function,
308
+ machine: M,
309
+ hooks: MiddlewareHooks<Context<M>>,
310
+ mode: 'sync' | 'async' | 'auto'
311
+ ): Function {
312
+ return function wrappedTransition(this: any, ...args: any[]) {
313
+ // Get current context (might be different from initial if machine changed)
314
+ const context = machine.context;
315
+
316
+ const middlewareCtx: MiddlewareContext<Context<M>> = {
317
+ transitionName,
318
+ context,
319
+ args
320
+ };
321
+
322
+ // Helper for sync execution
323
+ const executeSyncTransition = () => {
324
+ try {
325
+ // Call before hook (must be sync or throw)
326
+ if (hooks.before) {
327
+ const beforeResult = hooks.before(middlewareCtx);
328
+ // Check for cancellation
329
+ if (beforeResult === CANCEL) {
330
+ return machine; // Return current machine unchanged
331
+ }
332
+ // If before hook returns a promise in sync mode, throw
333
+ if (beforeResult instanceof Promise) {
334
+ throw new Error(
335
+ `Middleware mode is 'sync' but before hook returned Promise for transition: ${transitionName}`
336
+ );
337
+ }
338
+ }
339
+
340
+ // Execute the actual transition
341
+ const result = originalFn.call(this, ...args);
342
+
343
+ // If result is async, switch to async handling
344
+ if (result instanceof Promise) {
345
+ return handleAsyncResult(result, context);
346
+ }
347
+
348
+ // Call after hook (must be sync or throw)
349
+ if (hooks.after) {
350
+ const middlewareResult: MiddlewareResult<Context<M>> = {
351
+ transitionName,
352
+ prevContext: context,
353
+ nextContext: result.context,
354
+ args
355
+ };
356
+ const afterResult = hooks.after(middlewareResult);
357
+ if (afterResult instanceof Promise) {
358
+ throw new Error(
359
+ `Middleware mode is 'sync' but after hook returned Promise for transition: ${transitionName}`
360
+ );
361
+ }
362
+ }
363
+
364
+ return result;
365
+ } catch (err) {
366
+ // Call error hook and check for fallback state
367
+ if (hooks.error) {
368
+ const middlewareError: MiddlewareError<Context<M>> = {
369
+ transitionName,
370
+ context,
371
+ args,
372
+ error: err as Error
373
+ };
374
+ const errorResult = hooks.error(middlewareError);
375
+
376
+ // Handle async error hook in sync mode
377
+ if (errorResult instanceof Promise) {
378
+ // Fire-and-forget for async error hooks in sync mode
379
+ errorResult.catch(() => {});
380
+ throw err; // Re-throw original error
381
+ }
382
+
383
+ // Check if error hook returned a fallback machine
384
+ if (errorResult && typeof errorResult === 'object' && 'context' in errorResult) {
385
+ return errorResult as M; // Return fallback state
386
+ }
387
+ }
388
+
389
+ // Re-throw the error
390
+ throw err;
391
+ }
392
+ };
393
+
394
+ // Helper for handling async transition results
395
+ const handleAsyncResult = async (resultPromise: Promise<any>, ctx: any) => {
396
+ try {
397
+ const result = await resultPromise;
398
+
399
+ // Call after hook
400
+ if (hooks.after) {
401
+ const middlewareResult: MiddlewareResult<Context<M>> = {
402
+ transitionName,
403
+ prevContext: ctx,
404
+ nextContext: result.context,
405
+ args
406
+ };
407
+ await hooks.after(middlewareResult);
408
+ }
409
+
410
+ return result;
411
+ } catch (err) {
412
+ // Call error hook and check for fallback state
413
+ if (hooks.error) {
414
+ const middlewareError: MiddlewareError<Context<M>> = {
415
+ transitionName,
416
+ context: ctx,
417
+ args,
418
+ error: err as Error
419
+ };
420
+ const errorResult = await hooks.error(middlewareError);
421
+
422
+ // Check if error hook returned a fallback machine
423
+ if (errorResult && typeof errorResult === 'object' && 'context' in errorResult) {
424
+ return errorResult as M; // Return fallback state
425
+ }
426
+ }
427
+
428
+ // Re-throw the error
429
+ throw err;
430
+ }
431
+ };
432
+
433
+ // Helper for fully async execution
434
+ const executeAsyncTransition = async () => {
435
+ try {
436
+ // Call before hook
437
+ if (hooks.before) {
438
+ const beforeResult = await hooks.before(middlewareCtx);
439
+ // Check for cancellation
440
+ if (beforeResult === CANCEL) {
441
+ return machine; // Return current machine unchanged
442
+ }
443
+ }
444
+
445
+ // Execute the actual transition
446
+ const result = await originalFn.call(this, ...args);
447
+
448
+ // Call after hook
449
+ if (hooks.after) {
450
+ const middlewareResult: MiddlewareResult<Context<M>> = {
451
+ transitionName,
452
+ prevContext: context,
453
+ nextContext: result.context,
454
+ args
455
+ };
456
+ await hooks.after(middlewareResult);
457
+ }
458
+
459
+ return result;
460
+ } catch (err) {
461
+ // Call error hook and check for fallback state
462
+ if (hooks.error) {
463
+ const middlewareError: MiddlewareError<Context<M>> = {
464
+ transitionName,
465
+ context,
466
+ args,
467
+ error: err as Error
468
+ };
469
+ const errorResult = await hooks.error(middlewareError);
470
+
471
+ // Check if error hook returned a fallback machine
472
+ if (errorResult && typeof errorResult === 'object' && 'context' in errorResult) {
473
+ return errorResult as M; // Return fallback state
474
+ }
475
+ }
476
+
477
+ // Re-throw the error
478
+ throw err;
479
+ }
480
+ };
481
+
482
+ // Choose execution mode
483
+ if (mode === 'async') {
484
+ // Force async execution
485
+ return executeAsyncTransition();
486
+ } else if (mode === 'sync') {
487
+ // Force sync execution
488
+ return executeSyncTransition();
489
+ } else {
490
+ // Auto mode (adaptive): Starts synchronously for zero overhead,
491
+ // but automatically switches to async if the transition returns a Promise.
492
+ // This provides optimal performance for sync transitions while
493
+ // seamlessly handling async ones when they occur.
494
+ return executeSyncTransition();
495
+ }
496
+ };
497
+ }
498
+
499
+ // =============================================================================
500
+ // SECTION: COMPOSABLE MIDDLEWARE HELPERS
501
+ // =============================================================================
502
+
503
+ /**
504
+ * Logging middleware that logs transition calls and results to console.
505
+ * Useful for debugging and development.
506
+ *
507
+ * @template M - The machine type
508
+ * @param machine - The machine to add logging to
509
+ * @param options - Optional configuration for logging format
510
+ * @returns A new machine with logging middleware
511
+ *
512
+ * @example
513
+ * const logged = withLogging(counter);
514
+ * logged.increment(); // Console: "→ increment []" then "✓ increment { count: 1 }"
515
+ */
516
+ export function withLogging<M extends BaseMachine<any>>(
517
+ machine: M,
518
+ options: {
519
+ /** Custom logger function (default: console.log) */
520
+ logger?: (message: string, ...args: any[]) => void;
521
+ /** Include context in logs (default: true) */
522
+ includeContext?: boolean;
523
+ /** Include arguments in logs (default: true) */
524
+ includeArgs?: boolean;
525
+ } = {}
526
+ ): M {
527
+ const {
528
+ logger = console.log,
529
+ includeContext = true,
530
+ includeArgs = true
531
+ } = options;
532
+
533
+ return createMiddleware(machine, {
534
+ before: ({ transitionName, args }) => {
535
+ const argsStr = includeArgs && args.length > 0 ? ` ${JSON.stringify(args)}` : '';
536
+ logger(`→ ${transitionName}${argsStr}`);
537
+ },
538
+ after: ({ transitionName, nextContext }) => {
539
+ const contextStr = includeContext ? ` ${JSON.stringify(nextContext)}` : '';
540
+ logger(`✓ ${transitionName}${contextStr}`);
541
+ }
542
+ });
543
+ }
544
+
545
+ /**
546
+ * Analytics middleware that tracks state transitions.
547
+ * Compatible with any analytics service (Segment, Mixpanel, GA, etc.).
548
+ *
549
+ * @template M - The machine type
550
+ * @param machine - The machine to track
551
+ * @param track - Analytics tracking function
552
+ * @param options - Optional configuration for event naming
553
+ * @returns A new machine with analytics middleware
554
+ *
555
+ * @example
556
+ * const tracked = withAnalytics(machine, (event, props) => {
557
+ * analytics.track(event, props);
558
+ * });
559
+ */
560
+ export function withAnalytics<M extends BaseMachine<any>>(
561
+ machine: M,
562
+ track: (event: string, properties: Record<string, any>) => void | Promise<void>,
563
+ options: {
564
+ /** Prefix for event names (default: "state_transition") */
565
+ eventPrefix?: string;
566
+ /** Include previous context in properties (default: false) */
567
+ includePrevContext?: boolean;
568
+ /** Include arguments in properties (default: true) */
569
+ includeArgs?: boolean;
570
+ } = {}
571
+ ): M {
572
+ const {
573
+ eventPrefix = 'state_transition',
574
+ includePrevContext = false,
575
+ includeArgs = true
576
+ } = options;
577
+
578
+ return createMiddleware(machine, {
579
+ after: async ({ transitionName, prevContext, nextContext, args }) => {
580
+ const properties: Record<string, any> = {
581
+ transition: transitionName,
582
+ to: nextContext
583
+ };
584
+
585
+ if (includePrevContext) {
586
+ properties.from = prevContext;
587
+ }
588
+
589
+ if (includeArgs && args.length > 0) {
590
+ properties.args = args;
591
+ }
592
+
593
+ await track(`${eventPrefix}.${transitionName}`, properties);
594
+ }
595
+ }, { mode: 'async' });
596
+ }
597
+
598
+ /**
599
+ * Validation middleware that validates transitions before they execute.
600
+ * Throws an error if validation fails, preventing the transition.
601
+ *
602
+ * @template M - The machine type
603
+ * @param machine - The machine to validate
604
+ * @param validate - Validation function that throws or returns false on invalid transitions
605
+ * @returns A new machine with validation middleware
606
+ *
607
+ * @example
608
+ * const validated = withValidation(counter, ({ transitionName, context, args }) => {
609
+ * if (transitionName === 'decrement' && context.count === 0) {
610
+ * throw new Error('Cannot decrement below zero');
611
+ * }
612
+ * });
613
+ */
614
+ export function withValidation<M extends BaseMachine<any>>(
615
+ machine: M,
616
+ validate: (ctx: MiddlewareContext<Context<M>>) => void | boolean | Promise<void | boolean>,
617
+ options?: Pick<MiddlewareOptions, 'mode'>
618
+ ): M {
619
+ return createMiddleware(machine, {
620
+ before: (ctx) => {
621
+ const result = validate(ctx);
622
+ if (result instanceof Promise) {
623
+ return result.then(r => {
624
+ if (r === false) {
625
+ throw new Error(`Validation failed for transition: ${ctx.transitionName}`);
626
+ }
627
+ return undefined;
628
+ });
629
+ }
630
+ if (result === false) {
631
+ throw new Error(`Validation failed for transition: ${ctx.transitionName}`);
632
+ }
633
+ return undefined;
634
+ }
635
+ }, { mode: 'auto', ...options });
636
+ }
637
+
638
+ /**
639
+ * Permission/authorization middleware that checks if a transition is allowed.
640
+ * Useful for implementing role-based access control (RBAC) in state machines.
641
+ *
642
+ * @template M - The machine type
643
+ * @param machine - The machine to protect
644
+ * @param canPerform - Function that checks if the transition is allowed
645
+ * @returns A new machine with permission checks
646
+ *
647
+ * @example
648
+ * const protected = withPermissions(machine, (user) => ({ transitionName }) => {
649
+ * if (transitionName === 'delete' && user.role !== 'admin') {
650
+ * return false;
651
+ * }
652
+ * return true;
653
+ * });
654
+ */
655
+ export function withPermissions<M extends BaseMachine<any>>(
656
+ machine: M,
657
+ canPerform: (ctx: MiddlewareContext<Context<M>>) => boolean | Promise<boolean>,
658
+ options?: Pick<MiddlewareOptions, 'mode'>
659
+ ): M {
660
+ return createMiddleware(machine, {
661
+ before: (ctx) => {
662
+ const result = canPerform(ctx);
663
+ if (result instanceof Promise) {
664
+ return result.then(allowed => {
665
+ if (!allowed) {
666
+ throw new Error(`Unauthorized transition: ${ctx.transitionName}`);
667
+ }
668
+ return undefined;
669
+ });
670
+ }
671
+ if (!result) {
672
+ throw new Error(`Unauthorized transition: ${ctx.transitionName}`);
673
+ }
674
+ return undefined;
675
+ }
676
+ }, { mode: 'auto', ...options });
677
+ }
678
+
679
+ /**
680
+ * Error reporting middleware that sends errors to an error tracking service.
681
+ * Compatible with Sentry, Bugsnag, Rollbar, etc.
682
+ *
683
+ * @template M - The machine type
684
+ * @param machine - The machine to monitor
685
+ * @param captureError - Error capture function (e.g., Sentry.captureException)
686
+ * @param options - Optional configuration for error context
687
+ * @returns A new machine with error reporting
688
+ *
689
+ * @example
690
+ * const monitored = withErrorReporting(machine, (error, context) => {
691
+ * Sentry.captureException(error, { extra: context });
692
+ * });
693
+ */
694
+ export function withErrorReporting<M extends BaseMachine<any>>(
695
+ machine: M,
696
+ captureError: (error: Error, context: Record<string, any>) => void | Promise<void>,
697
+ options: {
698
+ /** Include machine context in error report (default: true) */
699
+ includeContext?: boolean;
700
+ /** Include arguments in error report (default: true) */
701
+ includeArgs?: boolean;
702
+ /** Middleware execution mode */
703
+ mode?: MiddlewareOptions['mode'];
704
+ } = {}
705
+ ): M {
706
+ const { includeContext = true, includeArgs = true, mode } = options;
707
+
708
+ return createMiddleware(machine, {
709
+ error: async ({ transitionName, context, args, error }) => {
710
+ const errorContext: Record<string, any> = {
711
+ transition: transitionName
712
+ };
713
+
714
+ if (includeContext) {
715
+ errorContext.context = context;
716
+ }
717
+
718
+ if (includeArgs && args.length > 0) {
719
+ errorContext.args = args;
720
+ }
721
+
722
+ await Promise.resolve(captureError(error, errorContext));
723
+ }
724
+ }, { mode });
725
+ }
726
+
727
+ /**
728
+ * Performance monitoring middleware that tracks transition execution time.
729
+ * Useful for identifying slow transitions and performance bottlenecks.
730
+ *
731
+ * @template M - The machine type
732
+ * @param machine - The machine to monitor
733
+ * @param onMetric - Callback to receive performance metrics
734
+ * @returns A new machine with performance monitoring
735
+ *
736
+ * @example
737
+ * const monitored = withPerformanceMonitoring(machine, ({ transition, duration }) => {
738
+ * if (duration > 100) {
739
+ * console.warn(`Slow transition: ${transition} took ${duration}ms`);
740
+ * }
741
+ * });
742
+ */
743
+ export function withPerformanceMonitoring<M extends BaseMachine<any>>(
744
+ machine: M,
745
+ onMetric: (metric: {
746
+ transitionName: string;
747
+ duration: number;
748
+ context: Readonly<Context<M>>;
749
+ }) => void | Promise<void>
750
+ ): M {
751
+ const timings = new Map<string, number>();
752
+
753
+ return createMiddleware(machine, {
754
+ before: ({ transitionName }) => {
755
+ timings.set(transitionName, performance.now());
756
+ return undefined;
757
+ },
758
+ after: ({ transitionName, nextContext }) => {
759
+ const startTime = timings.get(transitionName);
760
+ if (startTime) {
761
+ const duration = performance.now() - startTime;
762
+ timings.delete(transitionName);
763
+ const result = onMetric({ transitionName, duration, context: nextContext });
764
+ if (result instanceof Promise) {
765
+ return result;
766
+ }
767
+ }
768
+ return undefined;
769
+ }
770
+ }, { mode: 'auto' });
771
+ }
772
+
773
+ /**
774
+ * Retry middleware that automatically retries failed transitions.
775
+ * Uses direct property wrapping for optimal performance.
776
+ * Useful for handling transient failures in async operations.
777
+ *
778
+ * @template M - The machine type
779
+ * @param machine - The machine to add retry logic to
780
+ * @param options - Retry configuration
781
+ * @returns A new machine with retry logic
782
+ *
783
+ * @example
784
+ * const resilient = withRetry(machine, {
785
+ * maxRetries: 3,
786
+ * delay: 1000,
787
+ * shouldRetry: (error) => error.message.includes('network')
788
+ * });
789
+ */
790
+ export function withRetry<M extends BaseMachine<any>>(
791
+ machine: M,
792
+ options: {
793
+ /** Maximum number of retry attempts (default: 3) */
794
+ maxRetries?: number;
795
+ /** Delay between retries in milliseconds (default: 1000) */
796
+ delay?: number;
797
+ /** Exponential backoff multiplier (default: 1, no backoff) */
798
+ backoffMultiplier?: number;
799
+ /** Function to determine if error should trigger retry (default: always retry) */
800
+ shouldRetry?: (error: Error) => boolean;
801
+ /** Callback when retry occurs */
802
+ onRetry?: (attempt: number, error: Error) => void;
803
+ } = {}
804
+ ): M {
805
+ const {
806
+ maxRetries = 3,
807
+ delay = 1000,
808
+ backoffMultiplier = 1,
809
+ shouldRetry = () => true,
810
+ onRetry
811
+ } = options;
812
+
813
+ // Build wrapped machine object with direct property iteration
814
+ const wrapped: any = {};
815
+
816
+ for (const prop in machine) {
817
+ if (!Object.prototype.hasOwnProperty.call(machine, prop)) continue;
818
+
819
+ const value = machine[prop];
820
+
821
+ // Skip context and non-functions
822
+ if (prop === 'context' || typeof value !== 'function') {
823
+ wrapped[prop] = value;
824
+ continue;
825
+ }
826
+
827
+ // Wrap with retry logic
828
+ wrapped[prop] = async function retriableTransition(this: any, ...args: any[]) {
829
+ let lastError: Error | undefined;
830
+
831
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
832
+ try {
833
+ return await value.call(this, ...args);
834
+ } catch (error) {
835
+ lastError = error as Error;
836
+
837
+ // Don't retry if we've exhausted attempts
838
+ if (attempt === maxRetries) {
839
+ break;
840
+ }
841
+
842
+ // Don't retry if shouldRetry returns false
843
+ if (!shouldRetry(lastError)) {
844
+ break;
845
+ }
846
+
847
+ // Call onRetry callback
848
+ onRetry?.(attempt + 1, lastError);
849
+
850
+ // Wait before retrying
851
+ const currentDelay = delay * Math.pow(backoffMultiplier, attempt);
852
+ await new Promise(resolve => setTimeout(resolve, currentDelay));
853
+ }
854
+ }
855
+
856
+ // All retries exhausted, throw the last error
857
+ throw lastError;
858
+ };
859
+ }
860
+
861
+ return wrapped as M;
862
+ }
863
+
864
+ /**
865
+ * Guard configuration for a single transition.
866
+ */
867
+ export interface GuardConfig<C extends object> {
868
+ /** Guard predicate function that returns true if transition is allowed */
869
+ guard: (ctx: MiddlewareContext<C>, ...args: any[]) => boolean | Promise<boolean>;
870
+ /**
871
+ * Action to take when guard fails.
872
+ * - 'throw': Throw an error (default)
873
+ * - 'ignore': Silently cancel the transition
874
+ *
875
+ * Note: For custom fallback machines, use the error hook in createMiddleware:
876
+ * @example
877
+ * createMiddleware(machine, {
878
+ * error: ({ error, context }) => {
879
+ * if (error.message.includes('Guard failed')) {
880
+ * return createMachine({ ...context, error: 'Unauthorized' }, machine);
881
+ * }
882
+ * }
883
+ * });
884
+ */
885
+ onFail?: 'throw' | 'ignore';
886
+ }
887
+
888
+ /**
889
+ * Guard middleware that prevents transitions based on predicate functions.
890
+ * Implements fundamental FSM guard concept - transitions only occur when guards pass.
891
+ *
892
+ * @template M - The machine type
893
+ * @param machine - The machine to protect with guards
894
+ * @param guards - Object mapping transition names to guard configurations
895
+ * @returns A new machine with guard checks
896
+ *
897
+ * @example
898
+ * const guarded = withGuards(counter, {
899
+ * decrement: {
900
+ * guard: ({ context }) => context.count > 0,
901
+ * onFail: 'throw' // or 'ignore'
902
+ * },
903
+ * delete: {
904
+ * guard: ({ context }) => context.user?.isAdmin === true,
905
+ * onFail: 'throw'
906
+ * }
907
+ * });
908
+ *
909
+ * guarded.decrement(); // Throws if count === 0
910
+ *
911
+ * // For custom fallback machines, combine with error middleware:
912
+ * const guardedWithFallback = createMiddleware(guarded, {
913
+ * error: ({ error, context }) => {
914
+ * if (error.message.includes('Guard failed')) {
915
+ * return createMachine({ ...context, error: 'Unauthorized' }, machine);
916
+ * }
917
+ * }
918
+ * });
919
+ */
920
+ export function withGuards<M extends BaseMachine<any>>(
921
+ machine: M,
922
+ guards: Record<string, GuardConfig<Context<M>> | ((ctx: MiddlewareContext<Context<M>>, ...args: any[]) => boolean | Promise<boolean>)>,
923
+ options?: Pick<MiddlewareOptions, 'mode'>
924
+ ): M {
925
+ return createMiddleware(machine, {
926
+ before: async (ctx) => {
927
+ const guardConfig = guards[ctx.transitionName];
928
+ if (!guardConfig) {
929
+ return undefined; // No guard for this transition
930
+ }
931
+
932
+ // Handle shorthand: function directly instead of config object
933
+ const guard = typeof guardConfig === 'function' ? guardConfig : guardConfig.guard;
934
+ const onFail = typeof guardConfig === 'object' ? guardConfig.onFail : 'throw';
935
+
936
+ // Evaluate guard
937
+ const allowed = await Promise.resolve(guard(ctx, ...ctx.args));
938
+
939
+ if (!allowed) {
940
+ if (onFail === 'ignore') {
941
+ return CANCEL; // Silently cancel transition
942
+ } else {
943
+ // Default to 'throw'
944
+ throw new Error(`Guard failed for transition: ${ctx.transitionName}`);
945
+ }
946
+ }
947
+ return undefined;
948
+ }
949
+ }, { mode: 'async', ...options });
950
+ }
951
+
952
+ /**
953
+ * Creates conditional middleware that only applies to specific transitions.
954
+ * Useful for targeted instrumentation without affecting all transitions.
955
+ *
956
+ * @template M - The machine type
957
+ * @param machine - The machine to instrument
958
+ * @param config - Configuration specifying which transitions to instrument
959
+ * @returns A new machine with conditional middleware
960
+ *
961
+ * @example
962
+ * const conditional = createConditionalMiddleware(counter, {
963
+ * only: ['delete', 'update'], // Only these transitions
964
+ * hooks: {
965
+ * before: ({ transitionName }) => console.log('Sensitive operation:', transitionName),
966
+ * after: ({ transitionName }) => auditLog(transitionName)
967
+ * }
968
+ * });
969
+ *
970
+ * @example
971
+ * const excluding = createConditionalMiddleware(counter, {
972
+ * except: ['increment'], // All except these
973
+ * hooks: {
974
+ * before: ({ transitionName, args }) => validate(transitionName, args)
975
+ * }
976
+ * });
977
+ */
978
+ export function createConditionalMiddleware<M extends BaseMachine<any>>(
979
+ machine: M,
980
+ config: {
981
+ /** Only apply to these transitions (mutually exclusive with except) */
982
+ only?: string[];
983
+ /** Apply to all except these transitions (mutually exclusive with only) */
984
+ except?: string[];
985
+ /** Middleware hooks to apply */
986
+ hooks: MiddlewareHooks<Context<M>>;
987
+ /** Middleware options */
988
+ options?: MiddlewareOptions;
989
+ }
990
+ ): M {
991
+ const { only, except, hooks, options } = config;
992
+
993
+ if (only && except) {
994
+ throw new Error('Cannot specify both "only" and "except" - choose one');
995
+ }
996
+
997
+ // Create filter function
998
+ const shouldApply = (transitionName: string): boolean => {
999
+ if (only) {
1000
+ return only.includes(transitionName);
1001
+ }
1002
+ if (except) {
1003
+ return !except.includes(transitionName);
1004
+ }
1005
+ return true;
1006
+ };
1007
+
1008
+ // Wrap hooks to check filter
1009
+ const conditionalHooks: MiddlewareHooks<Context<M>> = {
1010
+ before: hooks.before
1011
+ ? async (ctx) => {
1012
+ if (shouldApply(ctx.transitionName)) {
1013
+ return await hooks.before!(ctx);
1014
+ }
1015
+ }
1016
+ : undefined,
1017
+ after: hooks.after
1018
+ ? async (result) => {
1019
+ if (shouldApply(result.transitionName)) {
1020
+ return await hooks.after!(result);
1021
+ }
1022
+ }
1023
+ : undefined,
1024
+ error: hooks.error
1025
+ ? async (error) => {
1026
+ if (shouldApply(error.transitionName)) {
1027
+ return await hooks.error!(error);
1028
+ }
1029
+ }
1030
+ : undefined
1031
+ };
1032
+
1033
+ return createMiddleware(machine, conditionalHooks, options);
1034
+ }
1035
+
1036
+ /**
1037
+ * Creates state-dependent middleware that only applies when a predicate is true.
1038
+ * Allows middleware behavior to change based on current context/state.
1039
+ *
1040
+ * @template M - The machine type
1041
+ * @param machine - The machine to instrument
1042
+ * @param config - Configuration with predicate and hooks
1043
+ * @returns A new machine with state-dependent middleware
1044
+ *
1045
+ * @example
1046
+ * const stateful = createStateMiddleware(counter, {
1047
+ * when: (ctx) => ctx.debugMode === true,
1048
+ * hooks: {
1049
+ * before: (ctx) => console.log('Debug:', ctx),
1050
+ * after: (result) => console.log('Debug result:', result)
1051
+ * }
1052
+ * });
1053
+ *
1054
+ * // Logging only happens when context.debugMode === true
1055
+ */
1056
+ export function createStateMiddleware<M extends BaseMachine<any>>(
1057
+ machine: M,
1058
+ config: {
1059
+ /** Predicate that determines if middleware should apply */
1060
+ when: (ctx: Context<M>) => boolean | Promise<boolean>;
1061
+ /** Middleware hooks to apply when predicate is true */
1062
+ hooks: MiddlewareHooks<Context<M>>;
1063
+ /** Middleware options */
1064
+ options?: MiddlewareOptions;
1065
+ }
1066
+ ): M {
1067
+ const { when, hooks, options } = config;
1068
+
1069
+ // Wrap hooks to check predicate
1070
+ const conditionalHooks: MiddlewareHooks<Context<M>> = {
1071
+ before: hooks.before
1072
+ ? async (ctx) => {
1073
+ if (await Promise.resolve(when(ctx.context))) {
1074
+ return await hooks.before!(ctx);
1075
+ }
1076
+ }
1077
+ : undefined,
1078
+ after: hooks.after
1079
+ ? async (result) => {
1080
+ if (await Promise.resolve(when(result.prevContext))) {
1081
+ return await hooks.after!(result);
1082
+ }
1083
+ }
1084
+ : undefined,
1085
+ error: hooks.error
1086
+ ? async (error) => {
1087
+ if (await Promise.resolve(when(error.context))) {
1088
+ return await hooks.error!(error);
1089
+ }
1090
+ }
1091
+ : undefined
1092
+ };
1093
+
1094
+ return createMiddleware(machine, conditionalHooks, options);
1095
+ }
1096
+
1097
+ // =============================================================================
1098
+ // SECTION: HISTORY AND SNAPSHOT TRACKING
1099
+ // =============================================================================
1100
+
1101
+ /**
1102
+ * Represents a recorded transition call in the history.
1103
+ */
1104
+ export interface HistoryEntry {
1105
+ /** Unique ID for this history entry */
1106
+ id: string;
1107
+ /** The transition that was called */
1108
+ transitionName: string;
1109
+ /** Arguments passed to the transition */
1110
+ args: any[];
1111
+ /** Timestamp when the transition was called */
1112
+ timestamp: number;
1113
+ /** Optional serialized version of args (if serializer provided) */
1114
+ serializedArgs?: string;
1115
+ }
1116
+
1117
+ /**
1118
+ * Generic serializer/deserializer interface.
1119
+ * Used for serializing history arguments, context snapshots, etc.
1120
+ * @template T - The type being serialized
1121
+ */
1122
+ export interface Serializer<T = any> {
1123
+ /** Serialize data to a string */
1124
+ serialize: (data: T) => string;
1125
+ /** Deserialize string back to data */
1126
+ deserialize: (serialized: string) => T;
1127
+ }
1128
+
1129
+ /**
1130
+ * History tracking middleware that records all transition calls.
1131
+ * Useful for debugging, replay, undo/redo, and audit logging.
1132
+ *
1133
+ * @template M - The machine type
1134
+ * @param machine - The machine to track
1135
+ * @param options - Configuration options
1136
+ * @returns A new machine with history tracking and a history array
1137
+ *
1138
+ * Note: Arguments are shallow-cloned by default. If you need deep cloning or
1139
+ * serialization for persistence, provide a serializer:
1140
+ *
1141
+ * @example
1142
+ * const tracked = withHistory(counter, {
1143
+ * maxSize: 100,
1144
+ * serializer: {
1145
+ * serialize: (args) => JSON.stringify(args), // For deep clone or persistence
1146
+ * deserialize: (str) => JSON.parse(str)
1147
+ * }
1148
+ * });
1149
+ *
1150
+ * tracked.increment();
1151
+ * tracked.add(5);
1152
+ * console.log(tracked.history); // [{ transitionName: 'increment', args: [], ... }, ...]
1153
+ * tracked.clearHistory(); // Clear history
1154
+ */
1155
+ export function withHistory<M extends BaseMachine<any>>(
1156
+ machine: M,
1157
+ options: {
1158
+ /** Maximum number of entries to keep (default: unlimited) */
1159
+ maxSize?: number;
1160
+ /** Optional serializer for arguments */
1161
+ serializer?: Serializer<any[]>;
1162
+ /** Filter function to exclude certain transitions from history */
1163
+ filter?: (transitionName: string, args: any[]) => boolean;
1164
+ /** Callback when new entry is added */
1165
+ onEntry?: (entry: HistoryEntry) => void;
1166
+ /** Internal flag to prevent rewrapping */
1167
+ _isRewrap?: boolean;
1168
+ } = {}
1169
+ ): WithHistory<M> {
1170
+ const {
1171
+ maxSize,
1172
+ serializer,
1173
+ filter,
1174
+ onEntry,
1175
+ _isRewrap = false
1176
+ } = options;
1177
+
1178
+ const history: HistoryEntry[] = [];
1179
+ let entryId = 0;
1180
+
1181
+ const instrumentedMachine = createMiddleware(machine, {
1182
+ before: ({ transitionName, args }) => {
1183
+ // Check filter
1184
+ if (filter && !filter(transitionName, args)) {
1185
+ return;
1186
+ }
1187
+
1188
+ // Create entry
1189
+ const entry: HistoryEntry = {
1190
+ id: `entry-${entryId++}`,
1191
+ transitionName,
1192
+ args: [...args], // Shallow clone args (fast, works with any type)
1193
+ timestamp: Date.now()
1194
+ };
1195
+
1196
+ // Serialize if serializer provided
1197
+ if (serializer) {
1198
+ try {
1199
+ entry.serializedArgs = serializer.serialize(args);
1200
+ } catch (err) {
1201
+ console.error('Failed to serialize history args:', err);
1202
+ }
1203
+ }
1204
+
1205
+ // Add to history
1206
+ history.push(entry);
1207
+
1208
+ // Enforce max size
1209
+ if (maxSize && history.length > maxSize) {
1210
+ history.shift();
1211
+ }
1212
+
1213
+ // Call callback
1214
+ onEntry?.(entry);
1215
+ }
1216
+ }, { exclude: ['context', 'history', 'clearHistory'] });
1217
+
1218
+ // Override transitions to propagate history to returned machines
1219
+ if (!_isRewrap) {
1220
+ for (const prop in instrumentedMachine) {
1221
+ if (!Object.prototype.hasOwnProperty.call(instrumentedMachine, prop)) continue;
1222
+ const value = instrumentedMachine[prop];
1223
+ if (typeof value === 'function' && !prop.startsWith('_') && prop !== 'context' && !['history', 'clearHistory'].includes(prop)) {
1224
+ const originalFn = value;
1225
+ (instrumentedMachine as any)[prop] = function(this: any, ...args: any[]) {
1226
+ const result = originalFn.apply(this, args);
1227
+ // If result is a machine, re-wrap it with history tracking using the shared history array
1228
+ if (result && typeof result === 'object' && 'context' in result && !('history' in result)) {
1229
+ // Create a new wrapped machine that shares the same history array
1230
+ const rewrappedResult = createMiddleware(result, {
1231
+ before: ({ transitionName, args: transArgs }) => {
1232
+ // Check filter
1233
+ if (filter && !filter(transitionName, transArgs)) {
1234
+ return;
1235
+ }
1236
+
1237
+ // Create entry
1238
+ const entry: HistoryEntry = {
1239
+ id: `entry-${entryId++}`,
1240
+ transitionName,
1241
+ args: [...transArgs],
1242
+ timestamp: Date.now()
1243
+ };
1244
+
1245
+ // Serialize if serializer provided
1246
+ if (serializer) {
1247
+ try {
1248
+ entry.serializedArgs = serializer.serialize(transArgs);
1249
+ } catch (err) {
1250
+ console.error('Failed to serialize history args:', err);
1251
+ }
1252
+ }
1253
+
1254
+ // Add to history
1255
+ history.push(entry);
1256
+
1257
+ // Enforce max size
1258
+ if (maxSize && history.length > maxSize) {
1259
+ history.shift();
1260
+ }
1261
+
1262
+ // Call callback
1263
+ onEntry?.(entry);
1264
+ }
1265
+ }, { exclude: ['context', 'history', 'clearHistory'] });
1266
+
1267
+ // Attach the shared history
1268
+ return Object.assign(rewrappedResult, {
1269
+ history,
1270
+ clearHistory: () => {
1271
+ history.length = 0;
1272
+ entryId = 0;
1273
+ }
1274
+ });
1275
+ }
1276
+ return result;
1277
+ };
1278
+ }
1279
+ }
1280
+ }
1281
+
1282
+ // Attach history tracking properties to the instrumented machine
1283
+ return Object.assign(instrumentedMachine, {
1284
+ history,
1285
+ clearHistory: () => {
1286
+ history.length = 0;
1287
+ entryId = 0;
1288
+ }
1289
+ });
1290
+ }
1291
+
1292
+ /**
1293
+ * Represents a snapshot of context at a point in time.
1294
+ * @template C - The context type
1295
+ */
1296
+ export interface ContextSnapshot<C extends object = any> {
1297
+ /** Unique ID for this snapshot */
1298
+ id: string;
1299
+ /** The transition that caused this change */
1300
+ transitionName: string;
1301
+ /** Context before the transition */
1302
+ before: C;
1303
+ /** Context after the transition */
1304
+ after: C;
1305
+ /** Timestamp of the snapshot */
1306
+ timestamp: number;
1307
+ /** Optional serialized version of contexts */
1308
+ serializedBefore?: string;
1309
+ serializedAfter?: string;
1310
+ /** Optional diff information */
1311
+ diff?: any;
1312
+ }
1313
+
1314
+ /**
1315
+ * Snapshot middleware that records context before/after each transition.
1316
+ * Useful for time-travel debugging, undo/redo, and state inspection.
1317
+ *
1318
+ * @template M - The machine type
1319
+ * @param machine - The machine to track
1320
+ * @param options - Configuration options
1321
+ * @returns A new machine with snapshot tracking and snapshots array
1322
+ *
1323
+ * @example
1324
+ * const tracked = withSnapshot(counter, {
1325
+ * maxSize: 50,
1326
+ * serializer: {
1327
+ * serialize: (ctx) => JSON.stringify(ctx),
1328
+ * deserialize: (str) => JSON.parse(str)
1329
+ * },
1330
+ * captureSnapshot: (before, after) => ({
1331
+ * changed: before.count !== after.count
1332
+ * })
1333
+ * });
1334
+ *
1335
+ * tracked.increment();
1336
+ * console.log(tracked.snapshots); // [{ before: { count: 0 }, after: { count: 1 }, ... }]
1337
+ *
1338
+ * // Time-travel: restore to previous state
1339
+ * const previousState = tracked.restoreSnapshot(tracked.snapshots[0].before);
1340
+ */
1341
+ export function withSnapshot<M extends BaseMachine<any>>(
1342
+ machine: M,
1343
+ options: {
1344
+ /** Maximum number of snapshots to keep (default: unlimited) */
1345
+ maxSize?: number;
1346
+ /** Optional serializer for context */
1347
+ serializer?: Serializer<Context<M>>;
1348
+ /** Custom function to capture additional snapshot data */
1349
+ captureSnapshot?: (before: Context<M>, after: Context<M>) => any;
1350
+ /** Only capture snapshots where context actually changed */
1351
+ onlyIfChanged?: boolean;
1352
+ /** Filter function to exclude certain transitions from snapshots */
1353
+ filter?: (transitionName: string) => boolean;
1354
+ /** Callback when new snapshot is taken */
1355
+ onSnapshot?: (snapshot: ContextSnapshot<Context<M>>) => void;
1356
+ /** Additional properties to exclude from middleware (for composition) */
1357
+ _extraExclusions?: string[];
1358
+ /** Internal flag to prevent rewrapping */
1359
+ _isRewrap?: boolean;
1360
+ } = {}
1361
+ ): WithSnapshot<M, Context<M>> {
1362
+ const {
1363
+ maxSize,
1364
+ serializer,
1365
+ captureSnapshot,
1366
+ onlyIfChanged = false,
1367
+ filter,
1368
+ onSnapshot,
1369
+ _extraExclusions = [],
1370
+ _isRewrap = false
1371
+ } = options;
1372
+
1373
+ const snapshots: ContextSnapshot<Context<M>>[] = [];
1374
+ let snapshotId = 0;
1375
+
1376
+ const instrumentedMachine = createMiddleware(machine, {
1377
+ after: ({ transitionName, prevContext, nextContext }) => {
1378
+ // Check filter
1379
+ if (filter && !filter(transitionName)) {
1380
+ return;
1381
+ }
1382
+
1383
+ // Check if changed (if required)
1384
+ if (onlyIfChanged) {
1385
+ const changed = JSON.stringify(prevContext) !== JSON.stringify(nextContext);
1386
+ if (!changed) {
1387
+ return;
1388
+ }
1389
+ }
1390
+
1391
+ // Create snapshot
1392
+ const snapshot: ContextSnapshot<Context<M>> = {
1393
+ id: `snapshot-${snapshotId++}`,
1394
+ transitionName,
1395
+ before: { ...prevContext as Context<M> }, // Clone
1396
+ after: { ...nextContext as Context<M> }, // Clone
1397
+ timestamp: Date.now()
1398
+ };
1399
+
1400
+ // Serialize if serializer provided
1401
+ if (serializer) {
1402
+ try {
1403
+ snapshot.serializedBefore = serializer.serialize(prevContext as Context<M>);
1404
+ snapshot.serializedAfter = serializer.serialize(nextContext as Context<M>);
1405
+ } catch (err) {
1406
+ console.error('Failed to serialize snapshot:', err);
1407
+ }
1408
+ }
1409
+
1410
+ // Capture custom snapshot data
1411
+ if (captureSnapshot) {
1412
+ try {
1413
+ snapshot.diff = captureSnapshot(prevContext as Context<M>, nextContext as Context<M>);
1414
+ } catch (err) {
1415
+ console.error('Failed to capture snapshot:', err);
1416
+ }
1417
+ }
1418
+
1419
+ // Add to snapshots
1420
+ snapshots.push(snapshot);
1421
+
1422
+ // Enforce max size
1423
+ if (maxSize && snapshots.length > maxSize) {
1424
+ snapshots.shift();
1425
+ }
1426
+
1427
+ // Call callback
1428
+ onSnapshot?.(snapshot);
1429
+ }
1430
+ }, { exclude: ['context', 'snapshots', 'clearSnapshots', 'restoreSnapshot', ..._extraExclusions] });
1431
+
1432
+ // Helper to restore machine to a previous context
1433
+ const restoreSnapshot = (context: Context<M>): M => {
1434
+ const { context: _, ...transitions } = machine;
1435
+ return { context, ...transitions } as M;
1436
+ };
1437
+
1438
+ // Override transitions to propagate snapshots and history to returned machines
1439
+ if (!_isRewrap) {
1440
+ for (const prop in instrumentedMachine) {
1441
+ if (!Object.prototype.hasOwnProperty.call(instrumentedMachine, prop)) continue;
1442
+ const value = instrumentedMachine[prop];
1443
+ if (typeof value === 'function' && !prop.startsWith('_') && prop !== 'context' && !['snapshots', 'clearSnapshots', 'restoreSnapshot', 'history', 'clearHistory'].includes(prop)) {
1444
+ const originalWrappedFn = value;
1445
+ (instrumentedMachine as any)[prop] = function(this: any, ...args: any[]) {
1446
+ const result = originalWrappedFn.apply(this, args);
1447
+ // If result is a machine, re-wrap it with snapshot tracking using the shared snapshots array
1448
+ if (result && typeof result === 'object' && 'context' in result && !('snapshots' in result)) {
1449
+ // Manually handle snapshot tracking without calling createMiddleware again
1450
+ // to avoid infinite recursion and complex wrapping issues
1451
+
1452
+ // Create a proxy that intercepts transition calls to record snapshots
1453
+
1454
+ // Wrap each transition to record snapshots
1455
+ for (const transProp in result) {
1456
+ if (!Object.prototype.hasOwnProperty.call(result, transProp)) continue;
1457
+ const transValue = result[transProp];
1458
+ if (typeof transValue === 'function' && !transProp.startsWith('_') && transProp !== 'context' && !['snapshots', 'clearSnapshots', 'restoreSnapshot', 'history', 'clearHistory'].includes(transProp)) {
1459
+ const origTransFn = transValue;
1460
+ (result as any)[transProp] = function(this: any, ...transArgs: any[]) {
1461
+ const prevCtx = result.context;
1462
+ const transResult = origTransFn.apply(this, transArgs);
1463
+
1464
+ // Record snapshot if we got a machine back
1465
+ if (transResult && typeof transResult === 'object' && 'context' in transResult) {
1466
+ const nextCtx = transResult.context;
1467
+
1468
+ // Check filter
1469
+ if (!(filter && !filter(transProp))) {
1470
+ // Check if changed (if required)
1471
+ let shouldRecord = true;
1472
+ if (onlyIfChanged) {
1473
+ const changed = JSON.stringify(prevCtx) !== JSON.stringify(nextCtx);
1474
+ shouldRecord = changed;
1475
+ }
1476
+
1477
+ if (shouldRecord) {
1478
+ // Create snapshot
1479
+ const snapshot: ContextSnapshot<Context<M>> = {
1480
+ id: `snapshot-${snapshotId++}`,
1481
+ transitionName: transProp,
1482
+ before: { ...prevCtx as Context<M> },
1483
+ after: { ...nextCtx as Context<M> },
1484
+ timestamp: Date.now()
1485
+ };
1486
+
1487
+ // Serialize if serializer provided
1488
+ if (serializer) {
1489
+ try {
1490
+ snapshot.serializedBefore = serializer.serialize(prevCtx as Context<M>);
1491
+ snapshot.serializedAfter = serializer.serialize(nextCtx as Context<M>);
1492
+ } catch (err) {
1493
+ console.error('Failed to serialize snapshot:', err);
1494
+ }
1495
+ }
1496
+
1497
+ // Capture custom snapshot data
1498
+ if (captureSnapshot) {
1499
+ try {
1500
+ snapshot.diff = captureSnapshot(prevCtx as Context<M>, nextCtx as Context<M>);
1501
+ } catch (err) {
1502
+ console.error('Failed to capture snapshot:', err);
1503
+ }
1504
+ }
1505
+
1506
+ // Add to snapshots
1507
+ snapshots.push(snapshot);
1508
+
1509
+ // Enforce max size
1510
+ if (maxSize && snapshots.length > maxSize) {
1511
+ snapshots.shift();
1512
+ }
1513
+
1514
+ // Call callback
1515
+ onSnapshot?.(snapshot);
1516
+ }
1517
+ }
1518
+ }
1519
+
1520
+ return transResult;
1521
+ };
1522
+ }
1523
+ }
1524
+
1525
+ // Attach the shared snapshots and history
1526
+ const resultWithTracking = Object.assign(result, {
1527
+ snapshots,
1528
+ clearSnapshots: () => {
1529
+ snapshots.length = 0;
1530
+ snapshotId = 0;
1531
+ },
1532
+ restoreSnapshot
1533
+ });
1534
+
1535
+ // Also propagate history if it exists on the input machine
1536
+ if ((machine as any).history) {
1537
+ resultWithTracking.history = (machine as any).history;
1538
+ resultWithTracking.clearHistory = (machine as any).clearHistory;
1539
+ }
1540
+
1541
+ return resultWithTracking;
1542
+ }
1543
+ return result;
1544
+ };
1545
+ }
1546
+ }
1547
+ }
1548
+
1549
+ // Attach snapshot tracking properties to the instrumented machine
1550
+ return Object.assign(instrumentedMachine, {
1551
+ snapshots,
1552
+ clearSnapshots: () => {
1553
+ snapshots.length = 0;
1554
+ snapshotId = 0;
1555
+ },
1556
+ restoreSnapshot
1557
+ });
1558
+ }
1559
+
1560
+ /**
1561
+ * Combined history and snapshot middleware for full time-travel debugging.
1562
+ * Records both transition calls and context changes.
1563
+ *
1564
+ * @template M - The machine type
1565
+ * @param machine - The machine to track
1566
+ * @param options - Configuration options
1567
+ * @returns Machine with both history and snapshot tracking
1568
+ *
1569
+ * @example
1570
+ * const tracker = withTimeTravel(counter, {
1571
+ * maxSize: 100,
1572
+ * serializer: {
1573
+ * serialize: (data) => JSON.stringify(data),
1574
+ * deserialize: (str) => JSON.parse(str)
1575
+ * }
1576
+ * });
1577
+ *
1578
+ * tracker.increment();
1579
+ * tracker.add(5);
1580
+ *
1581
+ * console.log(tracker.history); // All transitions
1582
+ * console.log(tracker.snapshots); // All state changes
1583
+ *
1584
+ * // Replay from a specific snapshot
1585
+ * const replayed = tracker.replayFrom(0);
1586
+ *
1587
+ * // Restore to specific snapshot
1588
+ * const restored = tracker.restoreSnapshot(tracker.snapshots[0].before);
1589
+ *
1590
+ * // Clear all tracking data
1591
+ * tracker.clearTimeTravel();
1592
+ */
1593
+ export function withTimeTravel<M extends BaseMachine<any>>(
1594
+ machine: M,
1595
+ options: {
1596
+ /** Maximum size for both history and snapshots */
1597
+ maxSize?: number;
1598
+ /** Serializer for both args and context */
1599
+ serializer?: Serializer<any>;
1600
+ /** Callback for each recorded action */
1601
+ onRecord?: (type: 'history' | 'snapshot', data: any) => void;
1602
+ } = {}
1603
+ ): WithTimeTravel<M, Context<M>> {
1604
+ const { maxSize, serializer, onRecord } = options;
1605
+
1606
+ const history: HistoryEntry[] = [];
1607
+ const snapshots: ContextSnapshot<Context<M>>[] = [];
1608
+ let entryId = 0;
1609
+ let snapshotId = 0;
1610
+
1611
+ // Middleware hooks that record to shared arrays
1612
+ const recordHistory = (transitionName: string, args: any[]) => {
1613
+ const entry: HistoryEntry = {
1614
+ id: `entry-${entryId++}`,
1615
+ transitionName,
1616
+ args: [...args],
1617
+ timestamp: Date.now()
1618
+ };
1619
+
1620
+ if (serializer) {
1621
+ try {
1622
+ entry.serializedArgs = serializer.serialize(args);
1623
+ } catch (err) {
1624
+ console.error('Failed to serialize history args:', err);
1625
+ }
1626
+ }
1627
+
1628
+ history.push(entry);
1629
+ if (maxSize && history.length > maxSize) {
1630
+ history.shift();
1631
+ }
1632
+
1633
+ onRecord?.('history', entry);
1634
+ };
1635
+
1636
+ const recordSnapshot = (transitionName: string, prevContext: Context<M>, nextContext: Context<M>) => {
1637
+ const snapshot: ContextSnapshot<Context<M>> = {
1638
+ id: `snapshot-${snapshotId++}`,
1639
+ transitionName,
1640
+ before: { ...prevContext },
1641
+ after: { ...nextContext },
1642
+ timestamp: Date.now()
1643
+ };
1644
+
1645
+ if (serializer) {
1646
+ try {
1647
+ snapshot.serializedBefore = serializer.serialize(prevContext);
1648
+ snapshot.serializedAfter = serializer.serialize(nextContext);
1649
+ } catch (err) {
1650
+ console.error('Failed to serialize snapshot:', err);
1651
+ }
1652
+ }
1653
+
1654
+ snapshots.push(snapshot);
1655
+ if (maxSize && snapshots.length > maxSize) {
1656
+ snapshots.shift();
1657
+ }
1658
+
1659
+ onRecord?.('snapshot', snapshot);
1660
+ };
1661
+
1662
+ // Helper to restore machine to a previous context
1663
+ const restoreSnapshot = (context: Context<M>): M => {
1664
+ const { context: _, ...transitions } = machine;
1665
+ return Object.assign({ context }, context, transitions) as M;
1666
+ };
1667
+
1668
+ // Implementation of replay functionality
1669
+ const replayFrom = (snapshotIndex: number = 0): M => {
1670
+ if (snapshotIndex < 0 || snapshotIndex >= snapshots.length) {
1671
+ throw new Error(`Invalid snapshot index: ${snapshotIndex}`);
1672
+ }
1673
+
1674
+ let current = restoreSnapshot(snapshots[snapshotIndex].before);
1675
+
1676
+ // Find the history index that corresponds to this snapshot
1677
+ const snapshot = snapshots[snapshotIndex];
1678
+ const historyStartIndex = history.findIndex(
1679
+ entry => entry.transitionName === snapshot.transitionName && entry.timestamp === snapshot.timestamp
1680
+ );
1681
+
1682
+ if (historyStartIndex === -1) {
1683
+ throw new Error('Could not find matching history entry for snapshot');
1684
+ }
1685
+
1686
+ // Replay all transitions from that point
1687
+ for (let i = historyStartIndex; i < history.length; i++) {
1688
+ const entry = history[i];
1689
+ const transition = (current as any)[entry.transitionName];
1690
+
1691
+ if (typeof transition === 'function') {
1692
+ try {
1693
+ current = transition.apply(current.context, entry.args);
1694
+ } catch (err) {
1695
+ console.error(`Replay failed at step ${i}:`, err);
1696
+ throw err;
1697
+ }
1698
+ }
1699
+ }
1700
+
1701
+ return current;
1702
+ };
1703
+
1704
+ // Helper to wrap a machine with tracking properties and wrapped transitions
1705
+ const wrapMachine = (machine: any): any => {
1706
+ const wrapped: any = { ...machine };
1707
+
1708
+ // Wrap transition functions
1709
+ for (const prop in machine) {
1710
+ if (!Object.prototype.hasOwnProperty.call(machine, prop)) continue;
1711
+ const value = machine[prop];
1712
+ if (typeof value === 'function' && !prop.startsWith('_') && prop !== 'context' &&
1713
+ !['history', 'snapshots', 'clearHistory', 'clearSnapshots', 'clearTimeTravel', 'restoreSnapshot', 'replayFrom'].includes(prop)) {
1714
+ wrapped[prop] = function(this: any, ...args: any[]) {
1715
+ // Record history before transition
1716
+ recordHistory(prop, args);
1717
+
1718
+ const prevContext = wrapped.context;
1719
+ const result = value.apply(this, args);
1720
+
1721
+ // Record snapshot after transition
1722
+ if (result && typeof result === 'object' && 'context' in result) {
1723
+ recordSnapshot(prop, prevContext, result.context);
1724
+ }
1725
+
1726
+ // Wrap returned machine
1727
+ if (result && typeof result === 'object' && 'context' in result) {
1728
+ return wrapMachine(result);
1729
+ }
1730
+ return result;
1731
+ };
1732
+ }
1733
+ }
1734
+
1735
+ // Attach tracking properties
1736
+ return Object.assign(wrapped, {
1737
+ history,
1738
+ snapshots,
1739
+ clearHistory: () => { history.length = 0; entryId = 0; },
1740
+ clearSnapshots: () => { snapshots.length = 0; snapshotId = 0; },
1741
+ clearTimeTravel: () => {
1742
+ history.length = 0;
1743
+ snapshots.length = 0;
1744
+ entryId = 0;
1745
+ snapshotId = 0;
1746
+ },
1747
+ restoreSnapshot,
1748
+ replayFrom
1749
+ });
1750
+ };
1751
+
1752
+ return wrapMachine(machine);
1753
+ }
1754
+
1755
+ /**
1756
+ * Compose multiple middleware functions into a single middleware stack.
1757
+ * Middleware is applied left-to-right (first middleware wraps outermost).
1758
+ *
1759
+ * @template M - The machine type
1760
+ * @param machine - The base machine
1761
+ * @param middlewares - Array of middleware functions
1762
+ * @returns A new machine with all middleware applied
1763
+ *
1764
+ * @example
1765
+ * const instrumented = compose(
1766
+ * counter,
1767
+ * withLogging,
1768
+ * withAnalytics(analytics.track),
1769
+ * withValidation(validator),
1770
+ * withErrorReporting(Sentry.captureException)
1771
+ * );
1772
+ */
1773
+ export function compose<M extends BaseMachine<any>>(
1774
+ machine: M,
1775
+ ...middlewares: Array<(m: M) => M>
1776
+ ): M {
1777
+ return middlewares.reduce((acc, middleware) => middleware(acc), machine);
1778
+ }
1779
+
1780
+ /**
1781
+ * Create a reusable middleware function from hooks.
1782
+ * Useful for defining custom middleware that can be applied to multiple machines.
1783
+ *
1784
+ * @template M - The machine type
1785
+ * @param hooks - Middleware hooks configuration
1786
+ * @param options - Middleware options
1787
+ * @returns A middleware function that can be applied to machines
1788
+ *
1789
+ * @example
1790
+ * const myMiddleware = createCustomMiddleware({
1791
+ * before: ({ transitionName }) => console.log('Before:', transitionName),
1792
+ * after: ({ transitionName }) => console.log('After:', transitionName)
1793
+ * });
1794
+ *
1795
+ * const machine1 = myMiddleware(counter1);
1796
+ * const machine2 = myMiddleware(counter2);
1797
+ */
1798
+ export function createCustomMiddleware<M extends BaseMachine<any>>(
1799
+ hooks: MiddlewareHooks<Context<M>>,
1800
+ options?: MiddlewareOptions
1801
+ ): (machine: M) => M {
1802
+ return (machine: M) => createMiddleware(machine, hooks, options);
1803
+ }
1804
+
1805
+ // =============================================================================
1806
+ // SECTION: TYPESAFE MIDDLEWARE COMPOSITION
1807
+ // =============================================================================
1808
+
1809
+ /**
1810
+ * A middleware function that transforms a machine.
1811
+ * @template M - The input machine type
1812
+ * @template R - The output machine type (usually extends M)
1813
+ */
1814
+ /**
1815
+ * A middleware function that transforms a machine.
1816
+ * @template M - Input machine type
1817
+ * @template R - Output machine type (defaults to same as input if no augmentation)
1818
+ */
1819
+ export type MiddlewareFn<M extends BaseMachine<any>, R extends BaseMachine<any> = M> = (machine: M) => R;
1820
+
1821
+ /**
1822
+ * A conditional middleware that may or may not be applied based on a predicate.
1823
+ * @template M - The machine type
1824
+ */
1825
+ export type ConditionalMiddleware<M extends BaseMachine<any>> = {
1826
+ /** The middleware function to apply */
1827
+ middleware: MiddlewareFn<M>;
1828
+ /** Predicate function that determines if the middleware should be applied */
1829
+ when: (machine: M) => boolean;
1830
+ };
1831
+
1832
+ /**
1833
+ * A named middleware entry for registry-based composition.
1834
+ * @template M - The machine type
1835
+ */
1836
+ export type NamedMiddleware<M extends BaseMachine<any>> = {
1837
+ /** Unique name for the middleware */
1838
+ name: string;
1839
+ /** The middleware function */
1840
+ middleware: MiddlewareFn<M>;
1841
+ /** Optional description */
1842
+ description?: string;
1843
+ /** Optional priority for ordering (higher numbers = applied later) */
1844
+ priority?: number;
1845
+ };
1846
+
1847
+ /**
1848
+ * Configuration for middleware pipeline execution.
1849
+ */
1850
+ export interface PipelineConfig {
1851
+ /** Whether to continue executing remaining middlewares if one fails */
1852
+ continueOnError?: boolean;
1853
+ /** Whether to log errors to console */
1854
+ logErrors?: boolean;
1855
+ /** Custom error handler */
1856
+ onError?: (error: Error, middlewareName?: string) => void;
1857
+ }
1858
+
1859
+ /**
1860
+ * Result of pipeline execution.
1861
+ */
1862
+ export interface PipelineResult<M extends BaseMachine<any>> {
1863
+ /** The final machine after all middlewares */
1864
+ machine: M;
1865
+ /** Any errors that occurred during execution */
1866
+ errors: Array<{ error: Error; middlewareIndex: number; middlewareName?: string }>;
1867
+ /** Whether the pipeline completed successfully */
1868
+ success: boolean;
1869
+ }
1870
+
1871
+ /**
1872
+ * Compose multiple middlewares with improved type inference.
1873
+ * This is a more typesafe version of the basic compose function.
1874
+ *
1875
+ * @template M - The initial machine type
1876
+ * @template Ms - Array of middleware functions
1877
+ * @param machine - The initial machine
1878
+ * @param middlewares - Array of middleware functions to apply
1879
+ * @returns The machine with all middlewares applied
1880
+ *
1881
+ * @example
1882
+ * const enhanced = composeTyped(
1883
+ * counter,
1884
+ * withHistory(),
1885
+ * withSnapshot(),
1886
+ * withTimeTravel()
1887
+ * );
1888
+ */
1889
+ /**
1890
+ * Recursively applies middlewares to infer the final machine type.
1891
+ * Provides precise type inference for middleware composition chains.
1892
+ */
1893
+ type ComposeResult<
1894
+ M extends BaseMachine<any>,
1895
+ Ms extends readonly MiddlewareFn<any, any>[]
1896
+ > = Ms extends readonly []
1897
+ ? M
1898
+ : Ms extends readonly [infer First, ...infer Rest]
1899
+ ? First extends MiddlewareFn<any, infer Output>
1900
+ ? Rest extends readonly MiddlewareFn<any, any>[]
1901
+ ? ComposeResult<Output, Rest>
1902
+ : Output
1903
+ : M
1904
+ : M;
1905
+
1906
+ /**
1907
+ * Type-safe middleware composition with perfect inference.
1908
+ * Composes multiple middlewares into a single transformation chain.
1909
+ *
1910
+ * @template M - The input machine type
1911
+ * @template Ms - Array of middleware functions
1912
+ * @param machine - The machine to enhance
1913
+ * @param middlewares - Middleware functions to apply in order
1914
+ * @returns The machine with all middlewares applied, with precise type inference
1915
+ *
1916
+ * @example
1917
+ * ```typescript
1918
+ * const enhanced = composeTyped(
1919
+ * counter,
1920
+ * withHistory(),
1921
+ * withSnapshot(),
1922
+ * withTimeTravel()
1923
+ * );
1924
+ * // enhanced: WithTimeTravel<WithSnapshot<WithHistory<Counter>>>
1925
+ * // Perfect IntelliSense for all methods and properties
1926
+ * ```
1927
+ */
1928
+ export function composeTyped<
1929
+ M extends BaseMachine<any>,
1930
+ Ms extends readonly MiddlewareFn<any, any>[]
1931
+ >(
1932
+ machine: M,
1933
+ ...middlewares: Ms
1934
+ ): ComposeResult<M, Ms> {
1935
+ return middlewares.reduce((acc, middleware) => middleware(acc), machine) as ComposeResult<M, Ms>;
1936
+ }
1937
+
1938
+ /**
1939
+ * Type-safe middleware composition with fluent API.
1940
+ * Allows building middleware chains with method chaining.
1941
+ *
1942
+ * @example
1943
+ * ```typescript
1944
+ * const enhanced = chain(counter)
1945
+ * .with(withHistory())
1946
+ * .with(withSnapshot())
1947
+ * .with(withTimeTravel())
1948
+ * .build();
1949
+ * ```
1950
+ */
1951
+ export function chain<M extends BaseMachine<any>>(machine: M) {
1952
+ return new MiddlewareChainBuilder(machine);
1953
+ }
1954
+
1955
+ /**
1956
+ * Fluent middleware composer for building complex middleware chains.
1957
+ * Provides excellent TypeScript inference and IntelliSense.
1958
+ */
1959
+ class MiddlewareChainBuilder<M extends BaseMachine<any>> {
1960
+ constructor(private machine: M) {}
1961
+
1962
+ /**
1963
+ * Add a middleware to the composition chain.
1964
+ * @param middleware - The middleware function to add
1965
+ * @returns A new composer with the middleware applied
1966
+ */
1967
+ with<M2 extends MiddlewareFn<any, any>>(
1968
+ middleware: M2
1969
+ ): MiddlewareChainBuilder<ReturnType<M2> extends BaseMachine<any> ? ReturnType<M2> : M> {
1970
+ const result = middleware(this.machine);
1971
+ return new MiddlewareChainBuilder(result as any);
1972
+ }
1973
+
1974
+ /**
1975
+ * Build the final machine with all middlewares applied.
1976
+ */
1977
+ build(): M {
1978
+ return this.machine;
1979
+ }
1980
+ }
1981
+
1982
+ /**
1983
+ * Common middleware combination types for better DX.
1984
+ * These types help with inference when using popular middleware combinations.
1985
+ */
1986
+ export type WithDebugging<M extends BaseMachine<any>> = WithTimeTravel<WithSnapshot<WithHistory<M>>>;
1987
+
1988
+ /**
1989
+ * Convenience function for the most common debugging middleware stack.
1990
+ * Combines history, snapshots, and time travel for full debugging capabilities.
1991
+ *
1992
+ * @example
1993
+ * ```typescript
1994
+ * const debugMachine = withDebugging(counter);
1995
+ * debugMachine.increment();
1996
+ * debugMachine.history; // Full transition history
1997
+ * debugMachine.snapshots; // Context snapshots
1998
+ * debugMachine.replayFrom(0); // Time travel
1999
+ * ```
2000
+ */
2001
+ export function withDebugging<M extends BaseMachine<any>>(machine: M): WithDebugging<M> {
2002
+ return withTimeTravel(withSnapshot(withHistory(machine)));
2003
+ }
2004
+
2005
+ /**
2006
+ * Create a middleware pipeline with error handling and conditional execution.
2007
+ *
2008
+ * @template M - The machine type
2009
+ * @param config - Pipeline configuration
2010
+ * @returns A function that executes middlewares in a pipeline
2011
+ *
2012
+ * @example
2013
+ * const pipeline = createPipeline({ continueOnError: true });
2014
+ *
2015
+ * const result = pipeline(
2016
+ * counter,
2017
+ * withHistory(),
2018
+ * withSnapshot(),
2019
+ * { middleware: withLogging(), when: (m) => m.context.debug }
2020
+ * );
2021
+ */
2022
+ export function createPipeline<M extends BaseMachine<any>>(
2023
+ config: PipelineConfig = {}
2024
+ ): {
2025
+ <Ms extends Array<MiddlewareFn<M> | ConditionalMiddleware<M>>>(
2026
+ machine: M,
2027
+ ...middlewares: Ms
2028
+ ): PipelineResult<M>;
2029
+ } {
2030
+ const {
2031
+ continueOnError = false,
2032
+ logErrors = true,
2033
+ onError
2034
+ } = config;
2035
+
2036
+ return (machine: M, ...middlewares: Array<MiddlewareFn<M> | ConditionalMiddleware<M>>): PipelineResult<M> => {
2037
+ let currentMachine = machine;
2038
+ const errors: Array<{ error: Error; middlewareIndex: number; middlewareName?: string }> = [];
2039
+
2040
+ for (let i = 0; i < middlewares.length; i++) {
2041
+ const middleware = middlewares[i];
2042
+
2043
+ try {
2044
+ // Handle conditional middleware
2045
+ if ('middleware' in middleware && 'when' in middleware) {
2046
+ if (!middleware.when(currentMachine)) {
2047
+ continue; // Skip this middleware
2048
+ }
2049
+ currentMachine = middleware.middleware(currentMachine);
2050
+ } else {
2051
+ // Regular middleware
2052
+ currentMachine = (middleware as MiddlewareFn<M>)(currentMachine);
2053
+ }
2054
+ } catch (error) {
2055
+ const err = error instanceof Error ? error : new Error(String(error));
2056
+ errors.push({ error: err, middlewareIndex: i });
2057
+
2058
+ if (logErrors) {
2059
+ console.error(`Middleware pipeline error at index ${i}:`, err);
2060
+ }
2061
+
2062
+ onError?.(err, `middleware-${i}`);
2063
+
2064
+ if (!continueOnError) {
2065
+ break;
2066
+ }
2067
+ }
2068
+ }
2069
+
2070
+ return {
2071
+ machine: currentMachine,
2072
+ errors,
2073
+ success: errors.length === 0
2074
+ };
2075
+ };
2076
+ }
2077
+
2078
+ /**
2079
+ * Create a middleware registry for named middleware composition.
2080
+ * Useful for building complex middleware stacks from reusable components.
2081
+ *
2082
+ * @template M - The machine type
2083
+ *
2084
+ * @example
2085
+ * const registry = createMiddlewareRegistry<CounterMachine>()
2086
+ * .register('history', withHistory(), 'Track state changes')
2087
+ * .register('snapshot', withSnapshot(), 'Take context snapshots', 10)
2088
+ * .register('timeTravel', withTimeTravel(), 'Enable time travel debugging', 20);
2089
+ *
2090
+ * const machine = registry.apply(counter, ['history', 'snapshot', 'timeTravel']);
2091
+ */
2092
+ export function createMiddlewareRegistry<M extends BaseMachine<any>>() {
2093
+ const registry = new Map<string, NamedMiddleware<M>>();
2094
+
2095
+ return {
2096
+ /**
2097
+ * Register a middleware with a name and optional metadata.
2098
+ */
2099
+ register(
2100
+ name: string,
2101
+ middleware: MiddlewareFn<M>,
2102
+ description?: string,
2103
+ priority?: number
2104
+ ): typeof this {
2105
+ if (registry.has(name)) {
2106
+ throw new Error(`Middleware '${name}' is already registered`);
2107
+ }
2108
+
2109
+ registry.set(name, { name, middleware, description, priority });
2110
+ return this;
2111
+ },
2112
+
2113
+ /**
2114
+ * Unregister a middleware by name.
2115
+ */
2116
+ unregister(name: string): boolean {
2117
+ return registry.delete(name);
2118
+ },
2119
+
2120
+ /**
2121
+ * Check if a middleware is registered.
2122
+ */
2123
+ has(name: string): boolean {
2124
+ return registry.has(name);
2125
+ },
2126
+
2127
+ /**
2128
+ * Get a registered middleware by name.
2129
+ */
2130
+ get(name: string): NamedMiddleware<M> | undefined {
2131
+ return registry.get(name);
2132
+ },
2133
+
2134
+ /**
2135
+ * List all registered middlewares.
2136
+ */
2137
+ list(): NamedMiddleware<M>[] {
2138
+ return Array.from(registry.values()).sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
2139
+ },
2140
+
2141
+ /**
2142
+ * Apply a selection of registered middlewares to a machine.
2143
+ * Middlewares are applied in priority order (lowest to highest).
2144
+ */
2145
+ apply(machine: M, middlewareNames: string[]): M {
2146
+ const middlewares = middlewareNames
2147
+ .map(name => {
2148
+ const entry = registry.get(name);
2149
+ if (!entry) {
2150
+ throw new Error(`Middleware '${name}' is not registered`);
2151
+ }
2152
+ return entry;
2153
+ })
2154
+ .sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
2155
+
2156
+ return composeTyped(machine, ...middlewares.map(m => m.middleware));
2157
+ },
2158
+
2159
+ /**
2160
+ * Apply all registered middlewares to a machine in priority order.
2161
+ */
2162
+ applyAll(machine: M): M {
2163
+ const middlewares = this.list();
2164
+ return composeTyped(machine, ...middlewares.map(m => m.middleware));
2165
+ }
2166
+ };
2167
+ }
2168
+
2169
+ /**
2170
+ * Create a conditional middleware that only applies when a predicate is true.
2171
+ *
2172
+ * @template M - The machine type
2173
+ * @param middleware - The middleware to conditionally apply
2174
+ * @param predicate - Function that determines when to apply the middleware
2175
+ * @returns A conditional middleware that can be called directly or used in pipelines
2176
+ *
2177
+ * @example
2178
+ * const debugMiddleware = when(
2179
+ * withTimeTravel(),
2180
+ * (machine) => machine.context.debugMode
2181
+ * );
2182
+ *
2183
+ * // Can be called directly
2184
+ * const machine = debugMiddleware(baseMachine);
2185
+ *
2186
+ * // Can also be used in pipelines
2187
+ * const pipeline = createPipeline();
2188
+ * const result = pipeline(machine, debugMiddleware);
2189
+ */
2190
+ export function when<M extends BaseMachine<any>>(
2191
+ middleware: MiddlewareFn<M>,
2192
+ predicate: (machine: M) => boolean
2193
+ ): ConditionalMiddleware<M> & MiddlewareFn<M> {
2194
+ const conditional: ConditionalMiddleware<M> & MiddlewareFn<M> = function(machine: M) {
2195
+ return predicate(machine) ? middleware(machine) : machine;
2196
+ };
2197
+
2198
+ conditional.middleware = middleware;
2199
+ conditional.when = predicate;
2200
+
2201
+ return conditional;
2202
+ }
2203
+
2204
+ /**
2205
+ * Create a middleware that only applies in development mode.
2206
+ *
2207
+ * @template M - The machine type
2208
+ * @param middleware - The middleware to apply in development
2209
+ * @returns A conditional middleware for development mode
2210
+ *
2211
+ * @example
2212
+ * const devMachine = composeTyped(
2213
+ * counter,
2214
+ * inDevelopment(withTimeTravel())
2215
+ * );
2216
+ */
2217
+ export function inDevelopment<M extends BaseMachine<any>>(
2218
+ middleware: MiddlewareFn<M>
2219
+ ): ConditionalMiddleware<M> & MiddlewareFn<M> {
2220
+ return when(middleware, () => {
2221
+ return typeof process !== 'undefined'
2222
+ ? process.env.NODE_ENV === 'development'
2223
+ : typeof window !== 'undefined'
2224
+ ? !window.location.hostname.includes('production')
2225
+ : false;
2226
+ });
2227
+ }
2228
+
2229
+ /**
2230
+ * Create a middleware that only applies when a context property matches a value.
2231
+ *
2232
+ * @template M - The machine type
2233
+ * @template K - The context key
2234
+ * @param key - The context property key
2235
+ * @param value - The value to match
2236
+ * @param middleware - The middleware to apply when the condition matches
2237
+ * @returns A conditional middleware
2238
+ *
2239
+ * @example
2240
+ * const adminMachine = composeTyped(
2241
+ * userMachine,
2242
+ * whenContext('role', 'admin', withAdminFeatures())
2243
+ * );
2244
+ */
2245
+ export function whenContext<M extends BaseMachine<any>, K extends keyof Context<M>>(
2246
+ key: K,
2247
+ value: Context<M>[K],
2248
+ middleware: MiddlewareFn<M>
2249
+ ): ConditionalMiddleware<M> & MiddlewareFn<M> {
2250
+ return when(middleware, (machine) => machine.context[key] === value);
2251
+ }
2252
+
2253
+ /**
2254
+ * Combine multiple middlewares with short-circuiting.
2255
+ * If any middleware returns a different type, the composition stops.
2256
+ *
2257
+ * @template M - The machine type
2258
+ * @param middlewares - Array of middlewares to combine
2259
+ * @returns A combined middleware function
2260
+ *
2261
+ * @example
2262
+ * const combined = combine(
2263
+ * withHistory(),
2264
+ * withSnapshot(),
2265
+ * withValidation()
2266
+ * );
2267
+ */
2268
+ export function combine<M extends BaseMachine<any>>(
2269
+ ...middlewares: Array<MiddlewareFn<M>>
2270
+ ): MiddlewareFn<M> {
2271
+ return (machine: M) => composeTyped(machine, ...middlewares);
2272
+ }
2273
+
2274
+ /**
2275
+ * Create a middleware that applies different middlewares based on context.
2276
+ *
2277
+ * @template M - The machine type
2278
+ * @param branches - Array of [predicate, middleware] pairs
2279
+ * @param fallback - Optional fallback middleware if no predicates match
2280
+ * @returns A branching middleware
2281
+ *
2282
+ * @example
2283
+ * const smartMiddleware = branch(
2284
+ * [(m) => m.context.userType === 'admin', withAdminFeatures()],
2285
+ * [(m) => m.context.debug, withTimeTravel()],
2286
+ * withBasicLogging() // fallback
2287
+ * );
2288
+ */
2289
+ export function branch<M extends BaseMachine<any>>(
2290
+ branches: Array<[predicate: (machine: M) => boolean, middleware: MiddlewareFn<M>]>,
2291
+ fallback?: MiddlewareFn<M>
2292
+ ): MiddlewareFn<M> {
2293
+ return (machine: M) => {
2294
+ for (const [predicate, middleware] of branches) {
2295
+ if (predicate(machine)) {
2296
+ return middleware(machine);
2297
+ }
2298
+ }
2299
+ return fallback ? fallback(machine) : machine;
2300
+ };
2301
+ }
2302
+
2303
+ /**
2304
+ * Type guard to check if a value is a middleware function.
2305
+ */
2306
+ export function isMiddlewareFn<M extends BaseMachine<any>>(
2307
+ value: any
2308
+ ): value is MiddlewareFn<M> {
2309
+ return typeof value === 'function' && value.length === 1;
2310
+ }
2311
+
2312
+ /**
2313
+ * Type guard to check if a value is a conditional middleware.
2314
+ */
2315
+ export function isConditionalMiddleware<M extends BaseMachine<any>>(
2316
+ value: any
2317
+ ): value is ConditionalMiddleware<M> {
2318
+ return (
2319
+ value !== null &&
2320
+ 'middleware' in value &&
2321
+ 'when' in value &&
2322
+ isMiddlewareFn(value.middleware) &&
2323
+ typeof value.when === 'function'
2324
+ );
2325
+ }