@doeixd/machine 0.0.13 → 0.0.18

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 (54) hide show
  1. package/README.md +77 -25
  2. package/dist/cjs/development/core.js +1852 -0
  3. package/dist/cjs/development/core.js.map +7 -0
  4. package/dist/cjs/development/index.js +1377 -1372
  5. package/dist/cjs/development/index.js.map +4 -4
  6. package/dist/cjs/production/core.js +1 -0
  7. package/dist/cjs/production/index.js +5 -5
  8. package/dist/esm/development/core.js +1829 -0
  9. package/dist/esm/development/core.js.map +7 -0
  10. package/dist/esm/development/index.js +1377 -1372
  11. package/dist/esm/development/index.js.map +4 -4
  12. package/dist/esm/production/core.js +1 -0
  13. package/dist/esm/production/index.js +5 -5
  14. package/dist/types/core.d.ts +18 -0
  15. package/dist/types/core.d.ts.map +1 -0
  16. package/dist/types/extract.d.ts +15 -1
  17. package/dist/types/extract.d.ts.map +1 -1
  18. package/dist/types/functional-combinators.d.ts +3 -5
  19. package/dist/types/functional-combinators.d.ts.map +1 -1
  20. package/dist/types/index.d.ts +254 -18
  21. package/dist/types/index.d.ts.map +1 -1
  22. package/dist/types/middleware/composition.d.ts +460 -0
  23. package/dist/types/middleware/composition.d.ts.map +1 -0
  24. package/dist/types/middleware/core.d.ts +196 -0
  25. package/dist/types/middleware/core.d.ts.map +1 -0
  26. package/dist/types/middleware/history.d.ts +54 -0
  27. package/dist/types/middleware/history.d.ts.map +1 -0
  28. package/dist/types/middleware/index.d.ts +10 -0
  29. package/dist/types/middleware/index.d.ts.map +1 -0
  30. package/dist/types/middleware/snapshot.d.ts +63 -0
  31. package/dist/types/middleware/snapshot.d.ts.map +1 -0
  32. package/dist/types/middleware/time-travel.d.ts +81 -0
  33. package/dist/types/middleware/time-travel.d.ts.map +1 -0
  34. package/package.json +19 -6
  35. package/src/core.ts +167 -0
  36. package/src/entry-react.ts +9 -0
  37. package/src/entry-solid.ts +9 -0
  38. package/src/extract.ts +61 -61
  39. package/src/functional-combinators.ts +3 -3
  40. package/src/generators.ts +6 -6
  41. package/src/index.ts +389 -101
  42. package/src/middleware/composition.ts +944 -0
  43. package/src/middleware/core.ts +573 -0
  44. package/src/middleware/history.ts +104 -0
  45. package/src/middleware/index.ts +13 -0
  46. package/src/middleware/snapshot.ts +153 -0
  47. package/src/middleware/time-travel.ts +236 -0
  48. package/src/middleware.ts +735 -1614
  49. package/src/prototype_functional.ts +46 -0
  50. package/src/reproduce_issue.ts +26 -0
  51. package/dist/types/middleware.d.ts +0 -1048
  52. package/dist/types/middleware.d.ts.map +0 -1
  53. package/dist/types/runtime-extract.d.ts +0 -53
  54. package/dist/types/runtime-extract.d.ts.map +0 -1
package/src/middleware.ts CHANGED
@@ -57,12 +57,6 @@ export interface MiddlewareError<C extends object> {
57
57
  * All hooks are optional - provide only the ones you need.
58
58
  * @template C - The context object type
59
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
60
  export interface MiddlewareHooks<C extends object> {
67
61
  /**
68
62
  * Called before a transition executes.
@@ -70,15 +64,6 @@ export interface MiddlewareHooks<C extends object> {
70
64
  *
71
65
  * @param ctx - Transition context with machine state and transition details
72
66
  * @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
67
  */
83
68
  before?: (ctx: MiddlewareContext<C>) => void | typeof CANCEL | Promise<void | typeof CANCEL>;
84
69
 
@@ -88,1112 +73,541 @@ export interface MiddlewareHooks<C extends object> {
88
73
  * Cannot prevent the transition (it already happened).
89
74
  *
90
75
  * @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
76
  */
99
77
  after?: (result: MiddlewareResult<C>) => void | Promise<void>;
100
78
 
101
79
  /**
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 });
80
+ * Called when a transition throws an error.
81
+ * Can be used for error reporting, recovery, etc.
116
82
  *
117
- * // Return fallback state for recoverable errors
118
- * if (error.message.includes('network')) {
119
- * return createMachine({ ...context, error: 'offline' }, transitions);
120
- * }
121
- * }
122
- * ```
83
+ * @param error - Error context with transition details and error information
123
84
  */
124
- error?: (error: MiddlewareError<C>) => void | null | BaseMachine<C> | Promise<void | null | BaseMachine<C>>;
85
+ error?: (error: MiddlewareError<C>) => void | Promise<void>;
125
86
  }
126
87
 
127
88
  /**
128
- * Options for middleware configuration.
89
+ * Options for configuring middleware behavior.
129
90
  */
130
91
  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[];
92
+ /** Whether to continue execution if a hook throws an error */
93
+ continueOnError?: boolean;
94
+ /** Whether to log errors from hooks */
95
+ logErrors?: boolean;
96
+ /** Custom error handler for hook errors */
97
+ onError?: (error: Error, hookName: string, ctx: any) => void;
149
98
  }
150
99
 
151
- // =============================================================================
152
- // SECTION: CANCELLATION SUPPORT
153
- // =============================================================================
154
-
155
100
  /**
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
- * });
101
+ * Symbol used to cancel a transition from a before hook.
167
102
  */
168
103
  export const CANCEL = Symbol('CANCEL');
169
104
 
170
105
  // =============================================================================
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
106
+ // SECTION: CORE MIDDLEWARE FUNCTIONS
221
107
  // =============================================================================
222
108
 
223
109
  /**
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
110
+ * Creates a middleware function that wraps machine transitions with hooks.
232
111
  *
233
112
  * @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
113
+ * @param machine - The machine to instrument
114
+ * @param hooks - Middleware hooks to execute
115
+ * @param options - Middleware configuration options
237
116
  * @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
117
  */
252
118
  export function createMiddleware<M extends BaseMachine<any>>(
253
119
  machine: M,
254
120
  hooks: MiddlewareHooks<Context<M>>,
255
121
  options: MiddlewareOptions = {}
256
122
  ): M {
257
- const { mode = 'auto', exclude = ['context'] } = options;
123
+ const { continueOnError = false, logErrors = true, onError } = options;
258
124
 
259
- // Build wrapped machine object with direct property iteration
260
- const wrapped: any = {};
125
+ // Create a wrapped machine that intercepts all transition calls
126
+ const wrappedMachine: any = { ...machine };
261
127
 
262
- // Copy all properties and wrap functions
128
+ // Copy any extra properties from the original machine (for middleware composition)
263
129
  for (const prop in machine) {
264
130
  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;
131
+ if (prop !== 'context' && typeof machine[prop] !== 'function') {
132
+ wrappedMachine[prop] = machine[prop];
278
133
  }
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
134
  }
295
135
 
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
136
+ // Wrap each transition function
137
+ for (const prop in machine) {
138
+ if (!Object.prototype.hasOwnProperty.call(machine, prop)) continue;
139
+ const value = machine[prop];
140
+ if (typeof value === 'function' && prop !== 'context') {
141
+ wrappedMachine[prop] = function (this: any, ...args: any[]) {
142
+ const transitionName = prop;
143
+ const context = wrappedMachine.context;
144
+
145
+ // Helper function to execute the transition and after hooks
146
+ const executeTransition = () => {
147
+ // 2. Execute the actual transition
148
+ let nextMachine: any;
149
+ try {
150
+ nextMachine = value.apply(this, args);
151
+ } catch (error) {
152
+ // 3. Execute error hooks if transition failed
153
+ if (hooks.error) {
154
+ try {
155
+ // Error hooks are called synchronously for now
156
+ hooks.error({
157
+ transitionName,
158
+ context,
159
+ args: [...args],
160
+ error: error as Error
161
+ });
162
+ } catch (hookError) {
163
+ if (!continueOnError) throw hookError;
164
+ if (logErrors) console.error(`Middleware error hook error for ${transitionName}:`, hookError);
165
+ onError?.(hookError as Error, 'error', { transitionName, context, args, error });
166
+ }
167
+ }
168
+ throw error; // Re-throw the original error
386
169
  }
387
- }
388
170
 
389
- // Re-throw the error
390
- throw err;
391
- }
392
- };
171
+ // Check if the transition is async (returns a Promise)
172
+ if (nextMachine && typeof nextMachine.then === 'function') {
173
+ // For async transitions, we need to handle the after hooks after the promise resolves
174
+ const asyncResult = nextMachine.then((resolvedMachine: any) => {
175
+ // Execute after hooks with the resolved machine
176
+ if (hooks.after) {
177
+ try {
178
+ const result = hooks.after({
179
+ transitionName,
180
+ prevContext: context,
181
+ nextContext: resolvedMachine.context,
182
+ args: [...args]
183
+ });
184
+
185
+ // Handle async after hooks
186
+ if (result && typeof result.then === 'function') {
187
+ return result.then(() => resolvedMachine);
188
+ }
189
+ } catch (error) {
190
+ if (!continueOnError) throw error;
191
+ if (logErrors) console.error(`Middleware after hook error for ${transitionName}:`, error);
192
+ onError?.(error as Error, 'after', {
193
+ transitionName,
194
+ prevContext: context,
195
+ nextContext: resolvedMachine.context,
196
+ args
197
+ });
198
+ }
199
+ }
200
+ return resolvedMachine;
201
+ });
393
202
 
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
- }
203
+ return asyncResult;
204
+ } else {
205
+ // Synchronous transition
206
+ // 4. Execute after hooks
207
+ if (hooks.after) {
208
+ try {
209
+ const result = hooks.after({
210
+ transitionName,
211
+ prevContext: context,
212
+ nextContext: nextMachine.context,
213
+ args: [...args]
214
+ });
215
+
216
+ // Handle async after hooks
217
+ if (result && typeof result === 'object' && result && 'then' in result) {
218
+ return result.then(() => nextMachine).catch((error: Error) => {
219
+ if (!continueOnError) throw error;
220
+ if (logErrors) console.error(`Middleware after hook error for ${transitionName}:`, error);
221
+ onError?.(error, 'after', {
222
+ transitionName,
223
+ prevContext: context,
224
+ nextContext: nextMachine.context,
225
+ args
226
+ });
227
+ return nextMachine;
228
+ });
229
+ }
230
+ } catch (error) {
231
+ if (!continueOnError) throw error;
232
+ if (logErrors) console.error(`Middleware after hook error for ${transitionName}:`, error);
233
+ onError?.(error as Error, 'after', {
234
+ transitionName,
235
+ prevContext: context,
236
+ nextContext: nextMachine.context,
237
+ args
238
+ });
239
+ }
240
+ }
409
241
 
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
242
+ // 5. Return the next machine state
243
+ return nextMachine;
425
244
  }
426
- }
427
-
428
- // Re-throw the error
429
- throw err;
430
- }
431
- };
245
+ };
432
246
 
433
- // Helper for fully async execution
434
- const executeAsyncTransition = async () => {
435
- try {
436
- // Call before hook
247
+ // 1. Execute before hooks synchronously if possible
437
248
  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
- }
249
+ try {
250
+ const result = hooks.before({
251
+ transitionName,
252
+ context,
253
+ args: [...args]
254
+ });
444
255
 
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
- }
256
+ // Handle async hooks
257
+ if (result && typeof result === 'object' && result && 'then' in result) {
258
+ // For async hooks, return a promise that executes the transition after
259
+ return result.then((hookResult: any) => {
260
+ if (hookResult === CANCEL) {
261
+ return wrappedMachine;
262
+ }
263
+ return executeTransition();
264
+ }).catch((error: Error) => {
265
+ if (!continueOnError) throw error;
266
+ if (logErrors) console.error(`Middleware before hook error for ${transitionName}:`, error);
267
+ onError?.(error, 'before', { transitionName, context, args });
268
+ return executeTransition();
269
+ });
270
+ }
458
271
 
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
272
+ // Check if transition should be cancelled
273
+ if (result === CANCEL) {
274
+ return wrappedMachine; // Return the same machine instance
275
+ }
276
+ } catch (error) {
277
+ if (!continueOnError) throw error;
278
+ if (logErrors) console.error(`Middleware before hook error for ${transitionName}:`, error);
279
+ onError?.(error as Error, 'before', { transitionName, context, args });
474
280
  }
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();
281
+ };
495
282
  }
496
- };
497
- }
283
+ }
498
284
 
499
- // =============================================================================
500
- // SECTION: COMPOSABLE MIDDLEWARE HELPERS
501
- // =============================================================================
285
+ return wrappedMachine;
286
+ }
502
287
 
503
288
  /**
504
- * Logging middleware that logs transition calls and results to console.
505
- * Useful for debugging and development.
289
+ * Creates a simple logging middleware that logs all transitions.
506
290
  *
507
291
  * @template M - The machine type
508
292
  * @param machine - The machine to add logging to
509
- * @param options - Optional configuration for logging format
293
+ * @param options - Logging configuration options
510
294
  * @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
295
  */
516
296
  export function withLogging<M extends BaseMachine<any>>(
517
297
  machine: M,
518
298
  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) */
299
+ logger?: (message: string) => void;
524
300
  includeArgs?: boolean;
301
+ includeContext?: boolean;
525
302
  } = {}
526
303
  ): M {
527
- const {
528
- logger = console.log,
529
- includeContext = true,
530
- includeArgs = true
531
- } = options;
304
+ const { logger = console.log, includeArgs = false, includeContext = true } = options;
532
305
 
533
306
  return createMiddleware(machine, {
534
307
  before: ({ transitionName, args }) => {
535
- const argsStr = includeArgs && args.length > 0 ? ` ${JSON.stringify(args)}` : '';
536
- logger(`→ ${transitionName}${argsStr}`);
308
+ const message = includeArgs ? `→ ${transitionName} [${args.join(', ')}]` : `→ ${transitionName}`;
309
+ logger(message);
537
310
  },
538
- after: ({ transitionName, nextContext }) => {
539
- const contextStr = includeContext ? ` ${JSON.stringify(nextContext)}` : '';
540
- logger(`✓ ${transitionName}${contextStr}`);
311
+ after: ({ transitionName, nextContext }) => {
312
+ const contextStr = includeContext ? ` ${JSON.stringify(nextContext)}` : '';
313
+ logger(`✓ ${transitionName}${contextStr}`);
314
+ },
315
+ error: ({ transitionName, error }) => {
316
+ console.error(`[Machine] ${transitionName} failed:`, error);
541
317
  }
542
318
  });
543
319
  }
544
320
 
545
321
  /**
546
- * Analytics middleware that tracks state transitions.
547
- * Compatible with any analytics service (Segment, Mixpanel, GA, etc.).
322
+ * Creates analytics tracking middleware.
548
323
  *
549
324
  * @template M - The machine type
550
325
  * @param machine - The machine to track
551
326
  * @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
- * });
327
+ * @param options - Configuration options
328
+ * @returns A new machine with analytics tracking
559
329
  */
560
330
  export function withAnalytics<M extends BaseMachine<any>>(
561
331
  machine: M,
562
- track: (event: string, properties: Record<string, any>) => void | Promise<void>,
332
+ track: (event: string, data?: any) => void,
563
333
  options: {
564
- /** Prefix for event names (default: "state_transition") */
565
334
  eventPrefix?: string;
566
- /** Include previous context in properties (default: false) */
567
335
  includePrevContext?: boolean;
568
- /** Include arguments in properties (default: true) */
569
336
  includeArgs?: boolean;
570
337
  } = {}
571
338
  ): M {
572
- const {
573
- eventPrefix = 'state_transition',
574
- includePrevContext = false,
575
- includeArgs = true
576
- } = options;
339
+ const { eventPrefix = 'state_transition', includePrevContext = false, includeArgs = false } = options;
577
340
 
578
341
  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);
342
+ after: ({ transitionName, prevContext, nextContext, args }) => {
343
+ const event = `${eventPrefix}.${transitionName}`;
344
+ const data: any = { transition: transitionName };
345
+ if (includePrevContext) data.from = prevContext;
346
+ data.to = nextContext;
347
+ if (includeArgs) data.args = args;
348
+ track(event, data);
594
349
  }
595
- }, { mode: 'async' });
350
+ });
596
351
  }
597
352
 
598
353
  /**
599
- * Validation middleware that validates transitions before they execute.
600
- * Throws an error if validation fails, preventing the transition.
354
+ * Creates validation middleware that runs before transitions.
601
355
  *
602
356
  * @template M - The machine type
603
357
  * @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
- * });
358
+ * @param validator - Validation function
359
+ * @returns A new machine with validation
613
360
  */
614
361
  export function withValidation<M extends BaseMachine<any>>(
615
362
  machine: M,
616
- validate: (ctx: MiddlewareContext<Context<M>>) => void | boolean | Promise<void | boolean>,
617
- options?: Pick<MiddlewareOptions, 'mode'>
363
+ validator: (ctx: MiddlewareContext<Context<M>>) => boolean | void
618
364
  ): M {
619
365
  return createMiddleware(machine, {
620
366
  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
- }
367
+ const result = validator(ctx);
630
368
  if (result === false) {
631
369
  throw new Error(`Validation failed for transition: ${ctx.transitionName}`);
632
370
  }
633
- return undefined;
634
371
  }
635
- }, { mode: 'auto', ...options });
372
+ });
636
373
  }
637
374
 
638
375
  /**
639
- * Permission/authorization middleware that checks if a transition is allowed.
640
- * Useful for implementing role-based access control (RBAC) in state machines.
376
+ * Creates permission-checking middleware.
641
377
  *
642
378
  * @template M - The machine type
643
379
  * @param machine - The machine to protect
644
- * @param canPerform - Function that checks if the transition is allowed
380
+ * @param checker - Permission checking function
645
381
  * @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
382
  */
655
383
  export function withPermissions<M extends BaseMachine<any>>(
656
384
  machine: M,
657
- canPerform: (ctx: MiddlewareContext<Context<M>>) => boolean | Promise<boolean>,
658
- options?: Pick<MiddlewareOptions, 'mode'>
385
+ checker: (ctx: MiddlewareContext<Context<M>>) => boolean
659
386
  ): M {
660
387
  return createMiddleware(machine, {
661
388
  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) {
389
+ if (!checker(ctx)) {
672
390
  throw new Error(`Unauthorized transition: ${ctx.transitionName}`);
673
391
  }
674
- return undefined;
675
392
  }
676
- }, { mode: 'auto', ...options });
393
+ });
677
394
  }
678
395
 
679
396
  /**
680
- * Error reporting middleware that sends errors to an error tracking service.
681
- * Compatible with Sentry, Bugsnag, Rollbar, etc.
397
+ * Creates error reporting middleware.
682
398
  *
683
399
  * @template M - The machine type
684
400
  * @param machine - The machine to monitor
685
- * @param captureError - Error capture function (e.g., Sentry.captureException)
686
- * @param options - Optional configuration for error context
401
+ * @param reporter - Error reporting function
402
+ * @param options - Configuration options
687
403
  * @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
404
  */
694
405
  export function withErrorReporting<M extends BaseMachine<any>>(
695
406
  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
- } = {}
407
+ reporter: (error: Error, ctx: any) => void,
408
+ options: { includeArgs?: boolean } = {}
705
409
  ): M {
706
- const { includeContext = true, includeArgs = true, mode } = options;
410
+ const { includeArgs = false } = options;
707
411
 
708
412
  return createMiddleware(machine, {
709
- error: async ({ transitionName, context, args, error }) => {
710
- const errorContext: Record<string, any> = {
711
- transition: transitionName
413
+ error: (errorCtx) => {
414
+ // Format the context to match test expectations
415
+ const formattedCtx = {
416
+ transition: errorCtx.transitionName,
417
+ context: errorCtx.context,
418
+ ...(includeArgs && { args: errorCtx.args })
712
419
  };
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));
420
+ reporter(errorCtx.error, formattedCtx);
723
421
  }
724
- }, { mode });
422
+ });
725
423
  }
726
424
 
727
425
  /**
728
- * Performance monitoring middleware that tracks transition execution time.
729
- * Useful for identifying slow transitions and performance bottlenecks.
426
+ * Creates performance monitoring middleware.
730
427
  *
731
428
  * @template M - The machine type
732
429
  * @param machine - The machine to monitor
733
- * @param onMetric - Callback to receive performance metrics
430
+ * @param tracker - Performance tracking function
734
431
  * @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
432
  */
743
433
  export function withPerformanceMonitoring<M extends BaseMachine<any>>(
744
434
  machine: M,
745
- onMetric: (metric: {
746
- transitionName: string;
747
- duration: number;
748
- context: Readonly<Context<M>>;
749
- }) => void | Promise<void>
435
+ tracker: (metric: { transitionName: string; duration: number; context: Context<M> }) => void
750
436
  ): M {
751
- const timings = new Map<string, number>();
437
+ const startTimes = new Map<string, number>();
752
438
 
753
439
  return createMiddleware(machine, {
754
- before: ({ transitionName }) => {
755
- timings.set(transitionName, performance.now());
756
- return undefined;
440
+ before: (ctx) => {
441
+ startTimes.set(ctx.transitionName, Date.now());
757
442
  },
758
- after: ({ transitionName, nextContext }) => {
759
- const startTime = timings.get(transitionName);
443
+ after: (result) => {
444
+ const startTime = startTimes.get(result.transitionName);
760
445
  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
- }
446
+ const duration = Date.now() - startTime;
447
+ startTimes.delete(result.transitionName);
448
+ // For test compatibility, pass a single object as expected
449
+ const testResult = {
450
+ transitionName: result.transitionName,
451
+ duration,
452
+ context: result.nextContext || result.prevContext
453
+ };
454
+ tracker(testResult);
767
455
  }
768
- return undefined;
769
456
  }
770
- }, { mode: 'auto' });
457
+ });
771
458
  }
772
459
 
773
460
  /**
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.
461
+ * Creates retry middleware for failed transitions.
777
462
  *
778
463
  * @template M - The machine type
779
464
  * @param machine - The machine to add retry logic to
780
465
  * @param options - Retry configuration
781
466
  * @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
467
  */
790
468
  export function withRetry<M extends BaseMachine<any>>(
791
469
  machine: M,
792
470
  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;
471
+ maxAttempts?: number;
472
+ maxRetries?: number; // alias for maxAttempts
473
+ shouldRetry?: (error: Error, attempt: number) => boolean;
474
+ backoffMs?: number | ((attempt: number) => number);
475
+ delay?: number | ((attempt: number) => number); // alias for backoffMs
476
+ backoffMultiplier?: number; // multiplier for exponential backoff
477
+ onRetry?: (error: Error, attempt: number) => void;
803
478
  } = {}
804
479
  ): M {
805
480
  const {
806
- maxRetries = 3,
807
- delay = 1000,
808
- backoffMultiplier = 1,
481
+ maxAttempts = options.maxRetries ?? 3,
809
482
  shouldRetry = () => true,
483
+ backoffMs = options.delay ?? 100,
484
+ backoffMultiplier = 2,
810
485
  onRetry
811
486
  } = options;
812
487
 
813
- // Build wrapped machine object with direct property iteration
814
- const wrapped: any = {};
488
+ // Create a wrapped machine that adds retry logic
489
+ const wrappedMachine: any = { ...machine };
815
490
 
491
+ // Wrap each transition function with retry logic
816
492
  for (const prop in machine) {
817
493
  if (!Object.prototype.hasOwnProperty.call(machine, prop)) continue;
818
-
819
494
  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;
495
+ if (typeof value === 'function' && prop !== 'context') {
496
+ wrappedMachine[prop] = async function (this: any, ...args: any[]) {
497
+ let lastError: Error;
498
+ let attempt = 0;
499
+
500
+ while (attempt < maxAttempts) {
501
+ try {
502
+ return await value.apply(this, args);
503
+ } catch (error) {
504
+ lastError = error as Error;
505
+ attempt++;
506
+
507
+ if (attempt < maxAttempts && shouldRetry(lastError, attempt)) {
508
+ onRetry?.(lastError, attempt);
509
+ const baseDelay = typeof backoffMs === 'function' ? backoffMs(attempt) : backoffMs;
510
+ const delay = baseDelay * Math.pow(backoffMultiplier, attempt - 1);
511
+ await new Promise(resolve => setTimeout(resolve, delay));
512
+ } else {
513
+ throw lastError;
514
+ }
845
515
  }
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
516
  }
854
- }
855
517
 
856
- // All retries exhausted, throw the last error
857
- throw lastError;
858
- };
859
- }
518
+ throw lastError!;
519
+ };
860
520
 
861
- return wrapped as M;
862
- }
863
521
 
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';
522
+ }
523
+ }
524
+
525
+ return wrappedMachine;
886
526
  }
887
527
 
888
528
  /**
889
- * Guard middleware that prevents transitions based on predicate functions.
890
- * Implements fundamental FSM guard concept - transitions only occur when guards pass.
529
+ * Creates custom middleware from hooks.
891
530
  *
892
531
  * @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
- * });
532
+ * @param hooks - Middleware hooks
533
+ * @param options - Middleware options
534
+ * @returns A middleware function
919
535
  */
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
- }
536
+ export function createCustomMiddleware<M extends BaseMachine<any>>(
537
+ hooks: MiddlewareHooks<Context<M>>,
538
+ options?: MiddlewareOptions
539
+ ): (machine: M) => M {
540
+ return (machine: M) => createMiddleware(machine, hooks, options);
541
+ }
931
542
 
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';
543
+ // =============================================================================
544
+ // SECTION: HISTORY TRACKING
545
+ // =============================================================================
935
546
 
936
- // Evaluate guard
937
- const allowed = await Promise.resolve(guard(ctx, ...ctx.args));
547
+ /**
548
+ * A single history entry recording a transition.
549
+ */
550
+ export interface HistoryEntry {
551
+ /** Unique ID for this history entry */
552
+ id: string;
553
+ /** Name of the transition that was called */
554
+ transitionName: string;
555
+ /** Arguments passed to the transition */
556
+ args: any[];
557
+ /** Timestamp when the transition occurred */
558
+ timestamp: number;
559
+ /** Optional serialized version of args for persistence */
560
+ serializedArgs?: string;
561
+ }
938
562
 
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 });
563
+ /**
564
+ * Serializer interface for converting context/args to/from strings.
565
+ */
566
+ export interface Serializer<T = any> {
567
+ serialize: (value: T) => string;
568
+ deserialize: (str: string) => T;
950
569
  }
951
570
 
952
571
  /**
953
- * Creates conditional middleware that only applies to specific transitions.
954
- * Useful for targeted instrumentation without affecting all transitions.
572
+ * Creates a machine with history tracking capabilities.
573
+ * Records all transitions that occur, allowing you to see the sequence of state changes.
955
574
  *
956
575
  * @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
- * });
576
+ * @param machine - The machine to track
577
+ * @param options - Configuration options
578
+ * @returns A new machine with history tracking
969
579
  *
970
580
  * @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
- *
581
+ * ```typescript
582
+ * const tracked = withHistory(counter, { maxSize: 50 });
1150
583
  * tracked.increment();
1151
- * tracked.add(5);
1152
- * console.log(tracked.history); // [{ transitionName: 'increment', args: [], ... }, ...]
1153
- * tracked.clearHistory(); // Clear history
584
+ * console.log(tracked.history); // [{ id: "entry-1", transitionName: "increment", ... }]
585
+ * ```
1154
586
  */
1155
587
  export function withHistory<M extends BaseMachine<any>>(
1156
588
  machine: M,
1157
589
  options: {
1158
- /** Maximum number of entries to keep (default: unlimited) */
590
+ /** Maximum number of history entries to keep (default: unlimited) */
1159
591
  maxSize?: number;
1160
- /** Optional serializer for arguments */
592
+ /** Optional serializer for transition arguments */
1161
593
  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 */
594
+ /** Callback when a transition occurs */
1165
595
  onEntry?: (entry: HistoryEntry) => void;
1166
- /** Internal flag to prevent rewrapping */
1167
- _isRewrap?: boolean;
1168
596
  } = {}
1169
- ): WithHistory<M> {
1170
- const {
1171
- maxSize,
1172
- serializer,
1173
- filter,
1174
- onEntry,
1175
- _isRewrap = false
1176
- } = options;
1177
-
597
+ ): M & { history: HistoryEntry[]; clearHistory: () => void } {
598
+ const { maxSize, serializer, onEntry } = options;
1178
599
  const history: HistoryEntry[] = [];
1179
600
  let entryId = 0;
1180
601
 
1181
602
  const instrumentedMachine = createMiddleware(machine, {
1182
603
  before: ({ transitionName, args }) => {
1183
- // Check filter
1184
- if (filter && !filter(transitionName, args)) {
1185
- return;
1186
- }
1187
-
1188
- // Create entry
1189
604
  const entry: HistoryEntry = {
1190
605
  id: `entry-${entryId++}`,
1191
606
  transitionName,
1192
- args: [...args], // Shallow clone args (fast, works with any type)
607
+ args: [...args],
1193
608
  timestamp: Date.now()
1194
609
  };
1195
610
 
1196
- // Serialize if serializer provided
1197
611
  if (serializer) {
1198
612
  try {
1199
613
  entry.serializedArgs = serializer.serialize(args);
@@ -1202,7 +616,6 @@ export function withHistory<M extends BaseMachine<any>>(
1202
616
  }
1203
617
  }
1204
618
 
1205
- // Add to history
1206
619
  history.push(entry);
1207
620
 
1208
621
  // Enforce max size
@@ -1210,93 +623,28 @@ export function withHistory<M extends BaseMachine<any>>(
1210
623
  history.shift();
1211
624
  }
1212
625
 
1213
- // Call callback
1214
626
  onEntry?.(entry);
1215
627
  }
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
- }
628
+ });
1281
629
 
1282
- // Attach history tracking properties to the instrumented machine
630
+ // Attach history properties to the machine
1283
631
  return Object.assign(instrumentedMachine, {
1284
632
  history,
1285
- clearHistory: () => {
1286
- history.length = 0;
1287
- entryId = 0;
1288
- }
633
+ clearHistory: () => { history.length = 0; entryId = 0; }
1289
634
  });
1290
635
  }
1291
636
 
637
+ // =============================================================================
638
+ // SECTION: SNAPSHOT TRACKING
639
+ // =============================================================================
640
+
1292
641
  /**
1293
- * Represents a snapshot of context at a point in time.
1294
- * @template C - The context type
642
+ * A snapshot of machine context before and after a transition.
1295
643
  */
1296
- export interface ContextSnapshot<C extends object = any> {
644
+ export interface ContextSnapshot<C extends object> {
1297
645
  /** Unique ID for this snapshot */
1298
646
  id: string;
1299
- /** The transition that caused this change */
647
+ /** Name of the transition that caused this snapshot */
1300
648
  transitionName: string;
1301
649
  /** Context before the transition */
1302
650
  before: C;
@@ -1304,7 +652,7 @@ export interface ContextSnapshot<C extends object = any> {
1304
652
  after: C;
1305
653
  /** Timestamp of the snapshot */
1306
654
  timestamp: number;
1307
- /** Optional serialized version of contexts */
655
+ /** Optional serialized versions of contexts */
1308
656
  serializedBefore?: string;
1309
657
  serializedAfter?: string;
1310
658
  /** Optional diff information */
@@ -1312,31 +660,27 @@ export interface ContextSnapshot<C extends object = any> {
1312
660
  }
1313
661
 
1314
662
  /**
1315
- * Snapshot middleware that records context before/after each transition.
1316
- * Useful for time-travel debugging, undo/redo, and state inspection.
663
+ * Creates a machine with snapshot tracking capabilities.
664
+ * Records context state before and after each transition for debugging and inspection.
1317
665
  *
1318
666
  * @template M - The machine type
1319
667
  * @param machine - The machine to track
1320
668
  * @param options - Configuration options
1321
- * @returns A new machine with snapshot tracking and snapshots array
669
+ * @returns A new machine with snapshot tracking
1322
670
  *
1323
671
  * @example
672
+ * ```typescript
1324
673
  * const tracked = withSnapshot(counter, {
1325
674
  * maxSize: 50,
1326
675
  * serializer: {
1327
676
  * serialize: (ctx) => JSON.stringify(ctx),
1328
677
  * deserialize: (str) => JSON.parse(str)
1329
- * },
1330
- * captureSnapshot: (before, after) => ({
1331
- * changed: before.count !== after.count
1332
- * })
678
+ * }
1333
679
  * });
1334
680
  *
1335
681
  * tracked.increment();
1336
682
  * 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);
683
+ * ```
1340
684
  */
1341
685
  export function withSnapshot<M extends BaseMachine<any>>(
1342
686
  machine: M,
@@ -1348,26 +692,18 @@ export function withSnapshot<M extends BaseMachine<any>>(
1348
692
  /** Custom function to capture additional snapshot data */
1349
693
  captureSnapshot?: (before: Context<M>, after: Context<M>) => any;
1350
694
  /** 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;
695
+ onlyOnChange?: boolean;
1360
696
  } = {}
1361
- ): WithSnapshot<M, Context<M>> {
697
+ ): M & {
698
+ snapshots: ContextSnapshot<Context<M>>[];
699
+ clearSnapshots: () => void;
700
+ restoreSnapshot: (snapshot: ContextSnapshot<Context<M>>['before']) => M;
701
+ } {
1362
702
  const {
1363
703
  maxSize,
1364
704
  serializer,
1365
705
  captureSnapshot,
1366
- onlyIfChanged = false,
1367
- filter,
1368
- onSnapshot,
1369
- _extraExclusions = [],
1370
- _isRewrap = false
706
+ onlyOnChange = false
1371
707
  } = options;
1372
708
 
1373
709
  const snapshots: ContextSnapshot<Context<M>>[] = [];
@@ -1375,33 +711,24 @@ export function withSnapshot<M extends BaseMachine<any>>(
1375
711
 
1376
712
  const instrumentedMachine = createMiddleware(machine, {
1377
713
  after: ({ transitionName, prevContext, nextContext }) => {
1378
- // Check filter
1379
- if (filter && !filter(transitionName)) {
714
+ // Skip if only capturing on change and context didn't change
715
+ if (onlyOnChange && JSON.stringify(prevContext) === JSON.stringify(nextContext)) {
1380
716
  return;
1381
717
  }
1382
718
 
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
719
  const snapshot: ContextSnapshot<Context<M>> = {
1393
720
  id: `snapshot-${snapshotId++}`,
1394
721
  transitionName,
1395
- before: { ...prevContext as Context<M> }, // Clone
1396
- after: { ...nextContext as Context<M> }, // Clone
722
+ before: { ...prevContext },
723
+ after: { ...nextContext },
1397
724
  timestamp: Date.now()
1398
725
  };
1399
726
 
1400
- // Serialize if serializer provided
727
+ // Serialize contexts if serializer provided
1401
728
  if (serializer) {
1402
729
  try {
1403
- snapshot.serializedBefore = serializer.serialize(prevContext as Context<M>);
1404
- snapshot.serializedAfter = serializer.serialize(nextContext as Context<M>);
730
+ snapshot.serializedBefore = serializer.serialize(prevContext);
731
+ snapshot.serializedAfter = serializer.serialize(nextContext);
1405
732
  } catch (err) {
1406
733
  console.error('Failed to serialize snapshot:', err);
1407
734
  }
@@ -1410,348 +737,273 @@ export function withSnapshot<M extends BaseMachine<any>>(
1410
737
  // Capture custom snapshot data
1411
738
  if (captureSnapshot) {
1412
739
  try {
1413
- snapshot.diff = captureSnapshot(prevContext as Context<M>, nextContext as Context<M>);
740
+ snapshot.diff = captureSnapshot(prevContext, nextContext);
1414
741
  } catch (err) {
1415
742
  console.error('Failed to capture snapshot:', err);
1416
743
  }
1417
744
  }
1418
745
 
1419
- // Add to snapshots
1420
746
  snapshots.push(snapshot);
1421
747
 
1422
748
  // Enforce max size
1423
749
  if (maxSize && snapshots.length > maxSize) {
1424
750
  snapshots.shift();
1425
751
  }
1426
-
1427
- // Call callback
1428
- onSnapshot?.(snapshot);
1429
752
  }
1430
- }, { exclude: ['context', 'snapshots', 'clearSnapshots', 'restoreSnapshot', ..._extraExclusions] });
753
+ });
1431
754
 
1432
- // Helper to restore machine to a previous context
755
+ // Helper to restore machine to a previous state
1433
756
  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
- }
757
+ // Find the machine's transition functions (excluding context and snapshot properties)
758
+ const transitions = Object.fromEntries(
759
+ Object.entries(machine).filter(([key]) =>
760
+ key !== 'context' &&
761
+ key !== 'snapshots' &&
762
+ key !== 'clearSnapshots' &&
763
+ key !== 'restoreSnapshot' &&
764
+ typeof machine[key as keyof M] === 'function'
765
+ )
766
+ );
1540
767
 
1541
- return resultWithTracking;
1542
- }
1543
- return result;
1544
- };
1545
- }
1546
- }
1547
- }
768
+ return Object.assign({ context }, transitions) as M;
769
+ };
1548
770
 
1549
- // Attach snapshot tracking properties to the instrumented machine
771
+ // Attach snapshot properties to the machine
1550
772
  return Object.assign(instrumentedMachine, {
1551
773
  snapshots,
1552
- clearSnapshots: () => {
1553
- snapshots.length = 0;
1554
- snapshotId = 0;
1555
- },
774
+ clearSnapshots: () => { snapshots.length = 0; snapshotId = 0; },
1556
775
  restoreSnapshot
1557
776
  });
1558
777
  }
1559
778
 
779
+ // =============================================================================
780
+ // SECTION: TIME TRAVEL DEBUGGING
781
+ // =============================================================================
782
+
783
+ /**
784
+ * A machine enhanced with history tracking capabilities.
785
+ */
786
+ export type WithHistory<M extends BaseMachine<any>> = M & {
787
+ /** History of all transitions */
788
+ history: HistoryEntry[];
789
+ /** Clear all history */
790
+ clearHistory: () => void;
791
+ };
792
+
793
+ /**
794
+ * A machine enhanced with snapshot tracking capabilities.
795
+ */
796
+ export type WithSnapshot<M extends BaseMachine<any>> = M & {
797
+ /** Snapshots of context before/after each transition */
798
+ snapshots: ContextSnapshot<Context<M>>[];
799
+ /** Clear all snapshots */
800
+ clearSnapshots: () => void;
801
+ /** Restore machine to a previous context state */
802
+ restoreSnapshot: (context: Context<M>) => M;
803
+ };
804
+
805
+ /**
806
+ * A machine enhanced with time travel capabilities.
807
+ */
808
+ export type WithTimeTravel<M extends BaseMachine<any>> = M & {
809
+ /** History of all transitions */
810
+ history: HistoryEntry[];
811
+ /** Snapshots of context before/after each transition */
812
+ snapshots: ContextSnapshot<Context<M>>[];
813
+ /** Clear all history and snapshots */
814
+ clearTimeTravel: () => void;
815
+ /** Clear just the history */
816
+ clearHistory: () => void;
817
+ /** Clear just the snapshots */
818
+ clearSnapshots: () => void;
819
+ /** Restore machine to a previous context state */
820
+ restoreSnapshot: (context: Context<M>) => M;
821
+ /** Replay transitions from a specific point in history */
822
+ replayFrom: (startIndex: number) => M;
823
+ };
824
+
1560
825
  /**
1561
- * Combined history and snapshot middleware for full time-travel debugging.
1562
- * Records both transition calls and context changes.
826
+ * Creates a machine with full time travel debugging capabilities.
827
+ * Combines history tracking, snapshots, and replay functionality.
1563
828
  *
1564
829
  * @template M - The machine type
1565
- * @param machine - The machine to track
830
+ * @param machine - The machine to enhance
1566
831
  * @param options - Configuration options
1567
- * @returns Machine with both history and snapshot tracking
832
+ * @returns A machine with time travel capabilities
1568
833
  *
1569
834
  * @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
835
+ * ```typescript
836
+ * const debugMachine = withTimeTravel(counter);
1583
837
  *
1584
- * // Replay from a specific snapshot
1585
- * const replayed = tracker.replayFrom(0);
838
+ * // Make some transitions
839
+ * debugMachine.increment();
840
+ * debugMachine.increment();
841
+ * debugMachine.decrement();
1586
842
  *
1587
- * // Restore to specific snapshot
1588
- * const restored = tracker.restoreSnapshot(tracker.snapshots[0].before);
843
+ * // Time travel to previous states
844
+ * const previousState = debugMachine.replayFrom(0); // Replay from start
845
+ * const undoLast = debugMachine.restoreSnapshot(debugMachine.snapshots[1].before);
1589
846
  *
1590
- * // Clear all tracking data
1591
- * tracker.clearTimeTravel();
847
+ * // Inspect history
848
+ * console.log(debugMachine.history);
849
+ * console.log(debugMachine.snapshots);
850
+ * ```
1592
851
  */
1593
852
  export function withTimeTravel<M extends BaseMachine<any>>(
1594
853
  machine: M,
1595
854
  options: {
1596
- /** Maximum size for both history and snapshots */
855
+ /** Maximum number of history entries/snapshots to keep */
1597
856
  maxSize?: number;
1598
- /** Serializer for both args and context */
1599
- serializer?: Serializer<any>;
1600
- /** Callback for each recorded action */
857
+ /** Optional serializer for persistence */
858
+ serializer?: Serializer;
859
+ /** Callback when history/snapshot events occur */
1601
860
  onRecord?: (type: 'history' | 'snapshot', data: any) => void;
1602
861
  } = {}
1603
- ): WithTimeTravel<M, Context<M>> {
862
+ ): WithTimeTravel<M> {
1604
863
  const { maxSize, serializer, onRecord } = options;
1605
864
 
865
+ // Create separate history and snapshot tracking
1606
866
  const history: HistoryEntry[] = [];
1607
867
  const snapshots: ContextSnapshot<Context<M>>[] = [];
1608
- let entryId = 0;
868
+ let historyId = 0;
1609
869
  let snapshotId = 0;
1610
870
 
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
- };
871
+ // Create middleware that handles both history and snapshots
872
+ const instrumentedMachine = createMiddleware(machine, {
873
+ before: ({ transitionName, args }) => {
874
+ const entry: HistoryEntry = {
875
+ id: `entry-${historyId++}`,
876
+ transitionName,
877
+ args: [...args],
878
+ timestamp: Date.now()
879
+ };
1619
880
 
1620
- if (serializer) {
1621
- try {
1622
- entry.serializedArgs = serializer.serialize(args);
1623
- } catch (err) {
1624
- console.error('Failed to serialize history args:', err);
881
+ if (serializer) {
882
+ try {
883
+ entry.serializedArgs = serializer.serialize(args);
884
+ } catch (err) {
885
+ console.error('Failed to serialize history args:', err);
886
+ }
1625
887
  }
1626
- }
1627
888
 
1628
- history.push(entry);
1629
- if (maxSize && history.length > maxSize) {
1630
- history.shift();
1631
- }
889
+ history.push(entry);
1632
890
 
1633
- onRecord?.('history', entry);
1634
- };
891
+ // Enforce max size
892
+ if (maxSize && history.length > maxSize) {
893
+ history.shift();
894
+ }
1635
895
 
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
- };
896
+ onRecord?.('history', entry);
897
+ },
898
+ after: ({ transitionName, prevContext, nextContext }) => {
899
+ const snapshot: ContextSnapshot<Context<M>> = {
900
+ id: `snapshot-${snapshotId++}`,
901
+ transitionName,
902
+ before: { ...prevContext },
903
+ after: { ...nextContext },
904
+ timestamp: Date.now()
905
+ };
1644
906
 
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);
907
+ // Serialize contexts if serializer provided
908
+ if (serializer) {
909
+ try {
910
+ snapshot.serializedBefore = serializer.serialize(prevContext);
911
+ snapshot.serializedAfter = serializer.serialize(nextContext);
912
+ } catch (err) {
913
+ console.error('Failed to serialize snapshot:', err);
914
+ }
1651
915
  }
1652
- }
1653
-
1654
- snapshots.push(snapshot);
1655
- if (maxSize && snapshots.length > maxSize) {
1656
- snapshots.shift();
1657
- }
1658
916
 
1659
- onRecord?.('snapshot', snapshot);
1660
- };
917
+ snapshots.push(snapshot);
1661
918
 
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
- };
919
+ // Enforce max size
920
+ if (maxSize && snapshots.length > maxSize) {
921
+ snapshots.shift();
922
+ }
1667
923
 
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}`);
924
+ onRecord?.('snapshot', snapshot);
1672
925
  }
926
+ });
1673
927
 
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
928
+ // Helper to restore machine to a previous state
929
+ const restoreSnapshot = (context: Context<M>): M => {
930
+ // Find the machine's transition functions (excluding context and snapshot properties)
931
+ const transitions = Object.fromEntries(
932
+ Object.entries(machine).filter(([key]) =>
933
+ key !== 'context' &&
934
+ key !== 'history' &&
935
+ key !== 'snapshots' &&
936
+ key !== 'clearHistory' &&
937
+ key !== 'clearSnapshots' &&
938
+ key !== 'restoreSnapshot' &&
939
+ key !== 'clearTimeTravel' &&
940
+ key !== 'replayFrom' &&
941
+ typeof machine[key as keyof M] === 'function'
942
+ )
1680
943
  );
1681
944
 
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];
945
+ return Object.assign({ context }, transitions) as M;
946
+ };
1690
947
 
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
- }
948
+ // Create replay functionality
949
+ const replayFrom = (startIndex: number): M => {
950
+ if (startIndex < 0 || startIndex >= history.length) {
951
+ throw new Error(`Invalid replay start index: ${startIndex}`);
1699
952
  }
1700
953
 
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
- }
954
+ // Start from the context at the specified history index
955
+ let currentContext = snapshots[startIndex]?.before;
956
+ if (!currentContext) {
957
+ throw new Error(`No snapshot available for index ${startIndex}`);
958
+ }
1725
959
 
1726
- // Wrap returned machine
1727
- if (result && typeof result === 'object' && 'context' in result) {
1728
- return wrapMachine(result);
1729
- }
1730
- return result;
1731
- };
960
+ // Get all transitions from start index to end
961
+ const transitionsToReplay = history.slice(startIndex);
962
+
963
+ // Create a fresh machine instance
964
+ const freshMachine = Object.assign(
965
+ { context: currentContext },
966
+ Object.fromEntries(
967
+ Object.entries(machine).filter(([key]) =>
968
+ key !== 'context' &&
969
+ typeof machine[key as keyof M] === 'function'
970
+ )
971
+ )
972
+ ) as M;
973
+
974
+ // Replay each transition
975
+ let replayedMachine = freshMachine;
976
+ for (const entry of transitionsToReplay) {
977
+ const transitionFn = replayedMachine[entry.transitionName as keyof M] as Function;
978
+ if (transitionFn) {
979
+ replayedMachine = transitionFn.apply(replayedMachine.context, entry.args);
1732
980
  }
1733
981
  }
1734
982
 
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
- });
983
+ return replayedMachine;
1750
984
  };
1751
985
 
1752
- return wrapMachine(machine);
986
+ // Return machine with all time travel capabilities
987
+ return Object.assign(instrumentedMachine, {
988
+ history,
989
+ snapshots,
990
+ clearHistory: () => { history.length = 0; historyId = 0; },
991
+ clearSnapshots: () => { snapshots.length = 0; snapshotId = 0; },
992
+ clearTimeTravel: () => {
993
+ history.length = 0;
994
+ snapshots.length = 0;
995
+ historyId = 0;
996
+ snapshotId = 0;
997
+ },
998
+ restoreSnapshot,
999
+ replayFrom
1000
+ }) as WithTimeTravel<M>;
1753
1001
  }
1754
1002
 
1003
+ // =============================================================================
1004
+ // SECTION: MIDDLEWARE COMPOSITION
1005
+ // =============================================================================
1006
+
1755
1007
  /**
1756
1008
  * Compose multiple middleware functions into a single middleware stack.
1757
1009
  * Middleware is applied left-to-right (first middleware wraps outermost).
@@ -1760,15 +1012,6 @@ export function withTimeTravel<M extends BaseMachine<any>>(
1760
1012
  * @param machine - The base machine
1761
1013
  * @param middlewares - Array of middleware functions
1762
1014
  * @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
1015
  */
1773
1016
  export function compose<M extends BaseMachine<any>>(
1774
1017
  machine: M,
@@ -1778,43 +1021,48 @@ export function compose<M extends BaseMachine<any>>(
1778
1021
  }
1779
1022
 
1780
1023
  /**
1781
- * Create a reusable middleware function from hooks.
1782
- * Useful for defining custom middleware that can be applied to multiple machines.
1024
+ * Type-safe middleware composition with perfect inference.
1025
+ * Composes multiple middlewares into a single transformation chain.
1783
1026
  *
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);
1027
+ * @template M - The input machine type
1028
+ * @template Ms - Array of middleware functions
1029
+ * @param machine - The machine to enhance
1030
+ * @param middlewares - Middleware functions to apply in order
1031
+ * @returns The machine with all middlewares applied, with precise type inference
1797
1032
  */
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);
1033
+ export function composeTyped<
1034
+ M extends BaseMachine<any>,
1035
+ Ms extends readonly MiddlewareFn<any, any>[]
1036
+ >(
1037
+ machine: M,
1038
+ ...middlewares: Ms
1039
+ ): ComposeResult<M, Ms> {
1040
+ return middlewares.reduce((acc, middleware) => middleware(acc), machine) as ComposeResult<M, Ms>;
1803
1041
  }
1804
1042
 
1805
1043
  // =============================================================================
1806
- // SECTION: TYPESAFE MIDDLEWARE COMPOSITION
1044
+ // SECTION: TYPE-LEVEL COMPOSITION
1807
1045
  // =============================================================================
1808
1046
 
1809
1047
  /**
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)
1048
+ * Type-level utility for composing middleware return types.
1049
+ * This enables perfect TypeScript inference when chaining middlewares.
1813
1050
  */
1051
+ export type ComposeResult<
1052
+ M extends BaseMachine<any>,
1053
+ Ms extends readonly MiddlewareFn<any, any>[]
1054
+ > = Ms extends readonly [infer First, ...infer Rest]
1055
+ ? First extends MiddlewareFn<any, infer R>
1056
+ ? Rest extends readonly MiddlewareFn<any, any>[]
1057
+ ? ComposeResult<R, Rest>
1058
+ : R
1059
+ : M
1060
+ : M;
1061
+
1814
1062
  /**
1815
1063
  * 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)
1064
+ * @template M - The input machine type
1065
+ * @template R - The output machine type (usually extends M)
1818
1066
  */
1819
1067
  export type MiddlewareFn<M extends BaseMachine<any>, R extends BaseMachine<any> = M> = (machine: M) => R;
1820
1068
 
@@ -1848,109 +1096,22 @@ export type NamedMiddleware<M extends BaseMachine<any>> = {
1848
1096
  * Configuration for middleware pipeline execution.
1849
1097
  */
1850
1098
  export interface PipelineConfig {
1851
- /** Whether to continue executing remaining middlewares if one fails */
1099
+ /** Whether to continue execution if a middleware throws an error */
1852
1100
  continueOnError?: boolean;
1853
- /** Whether to log errors to console */
1101
+ /** Whether to log errors from middlewares */
1854
1102
  logErrors?: boolean;
1855
1103
  /** Custom error handler */
1856
- onError?: (error: Error, middlewareName?: string) => void;
1104
+ onError?: (error: Error, middlewareIndex: number, middlewareName?: string) => void;
1857
1105
  }
1858
1106
 
1859
1107
  /**
1860
1108
  * Result of pipeline execution.
1861
1109
  */
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;
1110
+ export type PipelineResult<M extends BaseMachine<any>> = M;
1905
1111
 
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
- }
1112
+ // =============================================================================
1113
+ // SECTION: FLUENT API
1114
+ // =============================================================================
1954
1115
 
1955
1116
  /**
1956
1117
  * Fluent middleware composer for building complex middleware chains.
@@ -1980,121 +1141,97 @@ class MiddlewareChainBuilder<M extends BaseMachine<any>> {
1980
1141
  }
1981
1142
 
1982
1143
  /**
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.
1144
+ * Create a fluent middleware chain builder.
1991
1145
  *
1992
1146
  * @example
1993
1147
  * ```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
1148
+ * const enhanced = chain(counter)
1149
+ * .with(withHistory())
1150
+ * .with(withSnapshot())
1151
+ * .with(withTimeTravel())
1152
+ * .build();
1999
1153
  * ```
2000
1154
  */
2001
- export function withDebugging<M extends BaseMachine<any>>(machine: M): WithDebugging<M> {
2002
- return withTimeTravel(withSnapshot(withHistory(machine)));
1155
+ export function chain<M extends BaseMachine<any>>(machine: M) {
1156
+ return new MiddlewareChainBuilder(machine);
2003
1157
  }
2004
1158
 
1159
+ // =============================================================================
1160
+ // SECTION: CONDITIONAL MIDDLEWARE
1161
+ // =============================================================================
1162
+
2005
1163
  /**
2006
- * Create a middleware pipeline with error handling and conditional execution.
1164
+ * Create a conditional middleware that only applies when a predicate is true.
2007
1165
  *
2008
1166
  * @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
- * );
1167
+ * @param middleware - The middleware to conditionally apply
1168
+ * @param predicate - Function that determines when to apply the middleware
1169
+ * @returns A conditional middleware that can be called directly or used in pipelines
2021
1170
  */
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}`);
1171
+ export function when<M extends BaseMachine<any>>(
1172
+ middleware: MiddlewareFn<M>,
1173
+ predicate: (machine: M) => boolean
1174
+ ): ConditionalMiddleware<M> & MiddlewareFn<M> {
1175
+ const conditional: ConditionalMiddleware<M> & MiddlewareFn<M> = function(machine: M) {
1176
+ return predicate(machine) ? middleware(machine) : machine;
1177
+ };
2063
1178
 
2064
- if (!continueOnError) {
2065
- break;
2066
- }
2067
- }
2068
- }
1179
+ conditional.middleware = middleware;
1180
+ conditional.when = predicate;
2069
1181
 
2070
- return {
2071
- machine: currentMachine,
2072
- errors,
2073
- success: errors.length === 0
2074
- };
2075
- };
1182
+ return conditional;
2076
1183
  }
2077
1184
 
2078
1185
  /**
2079
- * Create a middleware registry for named middleware composition.
2080
- * Useful for building complex middleware stacks from reusable components.
1186
+ * Create a middleware that only applies in development mode.
2081
1187
  *
2082
1188
  * @template M - The machine type
1189
+ * @param middleware - The middleware to apply in development
1190
+ * @returns A conditional middleware for development mode
1191
+ */
1192
+ export function inDevelopment<M extends BaseMachine<any>>(
1193
+ middleware: MiddlewareFn<M>
1194
+ ): ConditionalMiddleware<M> & MiddlewareFn<M> {
1195
+ return when(middleware, () => {
1196
+ return typeof process !== 'undefined'
1197
+ ? process.env.NODE_ENV === 'development'
1198
+ : typeof window !== 'undefined'
1199
+ ? !window.location.hostname.includes('production')
1200
+ : false;
1201
+ });
1202
+ }
1203
+
1204
+ /**
1205
+ * Create a middleware that only applies when a context property matches a value.
2083
1206
  *
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']);
1207
+ * @template M - The machine type
1208
+ * @template K - The context key
1209
+ * @param key - The context property key
1210
+ * @param value - The value to match
1211
+ * @param middleware - The middleware to apply when the condition matches
1212
+ * @returns A conditional middleware
1213
+ */
1214
+ export function whenContext<M extends BaseMachine<any>, K extends keyof Context<M>>(
1215
+ key: K,
1216
+ value: Context<M>[K],
1217
+ middleware: MiddlewareFn<M>
1218
+ ): ConditionalMiddleware<M> & MiddlewareFn<M> {
1219
+ return when(middleware, (machine) => machine.context[key] === value);
1220
+ }
1221
+
1222
+ // =============================================================================
1223
+ // SECTION: MIDDLEWARE REGISTRY
1224
+ // =============================================================================
1225
+
1226
+ /**
1227
+ * Create a middleware registry for managing reusable middleware configurations.
2091
1228
  */
2092
1229
  export function createMiddlewareRegistry<M extends BaseMachine<any>>() {
2093
1230
  const registry = new Map<string, NamedMiddleware<M>>();
2094
1231
 
2095
1232
  return {
2096
1233
  /**
2097
- * Register a middleware with a name and optional metadata.
1234
+ * Register a middleware by name.
2098
1235
  */
2099
1236
  register(
2100
1237
  name: string,
@@ -2166,104 +1303,80 @@ export function createMiddlewareRegistry<M extends BaseMachine<any>>() {
2166
1303
  };
2167
1304
  }
2168
1305
 
1306
+ // =============================================================================
1307
+ // SECTION: PIPELINES
1308
+ // =============================================================================
1309
+
2169
1310
  /**
2170
- * Create a conditional middleware that only applies when a predicate is true.
1311
+ * Create a middleware pipeline with error handling and conditional execution.
2171
1312
  *
2172
1313
  * @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);
1314
+ * @param config - Pipeline configuration
1315
+ * @returns A function that executes middlewares in a pipeline
2189
1316
  */
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
- };
1317
+ export function createPipeline<M extends BaseMachine<any>>(
1318
+ config: PipelineConfig = {}
1319
+ ): {
1320
+ <Ms extends Array<MiddlewareFn<M> | ConditionalMiddleware<M>>>(
1321
+ machine: M,
1322
+ ...middlewares: Ms
1323
+ ): { machine: M; errors: Array<{ error: Error; middlewareIndex: number; middlewareName?: string }>; success: boolean };
1324
+ } {
1325
+ const {
1326
+ continueOnError = false,
1327
+ logErrors = true,
1328
+ onError
1329
+ } = config;
2197
1330
 
2198
- conditional.middleware = middleware;
2199
- conditional.when = predicate;
1331
+ return (machine: M, ...middlewares: Array<MiddlewareFn<M> | ConditionalMiddleware<M>>) => {
1332
+ let currentMachine = machine;
1333
+ const errors: Array<{ error: Error; middlewareIndex: number; middlewareName?: string }> = [];
1334
+ let success = true;
2200
1335
 
2201
- return conditional;
2202
- }
1336
+ for (let i = 0; i < middlewares.length; i++) {
1337
+ const middleware = middlewares[i];
2203
1338
 
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
- }
1339
+ try {
1340
+ // Handle conditional middleware
1341
+ if ('middleware' in middleware && 'when' in middleware) {
1342
+ if (!middleware.when(currentMachine)) {
1343
+ continue; // Skip this middleware
1344
+ }
1345
+ currentMachine = middleware.middleware(currentMachine);
1346
+ } else {
1347
+ // Regular middleware
1348
+ currentMachine = (middleware as MiddlewareFn<M>)(currentMachine);
1349
+ }
1350
+ } catch (error) {
1351
+ success = false;
1352
+ if (!continueOnError) {
1353
+ throw error;
1354
+ }
2228
1355
 
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);
1356
+ errors.push({
1357
+ error: error as Error,
1358
+ middlewareIndex: i,
1359
+ middlewareName: (middleware as any).name
1360
+ });
1361
+
1362
+ if (logErrors) {
1363
+ console.error(`Pipeline middleware error at index ${i}:`, error);
1364
+ }
1365
+
1366
+ onError?.(error as Error, i, (middleware as any).name);
1367
+ }
1368
+ }
1369
+
1370
+ return { machine: currentMachine, errors, success };
1371
+ };
2251
1372
  }
2252
1373
 
1374
+ // =============================================================================
1375
+ // SECTION: UTILITY FUNCTIONS
1376
+ // =============================================================================
1377
+
2253
1378
  /**
2254
1379
  * 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
1380
  */
2268
1381
  export function combine<M extends BaseMachine<any>>(
2269
1382
  ...middlewares: Array<MiddlewareFn<M>>
@@ -2273,18 +1386,6 @@ export function combine<M extends BaseMachine<any>>(
2273
1386
 
2274
1387
  /**
2275
1388
  * 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
1389
  */
2289
1390
  export function branch<M extends BaseMachine<any>>(
2290
1391
  branches: Array<[predicate: (machine: M) => boolean, middleware: MiddlewareFn<M>]>,
@@ -2300,6 +1401,10 @@ export function branch<M extends BaseMachine<any>>(
2300
1401
  };
2301
1402
  }
2302
1403
 
1404
+ // =============================================================================
1405
+ // SECTION: TYPE GUARDS
1406
+ // =============================================================================
1407
+
2303
1408
  /**
2304
1409
  * Type guard to check if a value is a middleware function.
2305
1410
  */
@@ -2323,3 +1428,19 @@ export function isConditionalMiddleware<M extends BaseMachine<any>>(
2323
1428
  typeof value.when === 'function'
2324
1429
  );
2325
1430
  }
1431
+
1432
+ // =============================================================================
1433
+ // SECTION: COMMON COMBINATIONS
1434
+ // =============================================================================
1435
+
1436
+ /**
1437
+ * Common middleware combination types for better DX.
1438
+ */
1439
+ export type WithDebugging<M extends BaseMachine<any>> = WithTimeTravel<WithSnapshot<WithHistory<M>>>;
1440
+
1441
+ /**
1442
+ * Convenience function for the most common debugging middleware stack.
1443
+ */
1444
+ export function withDebugging<M extends BaseMachine<any>>(machine: M): WithDebugging<M> {
1445
+ return withTimeTravel(withSnapshot(withHistory(machine)));
1446
+ }