@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
@@ -0,0 +1,573 @@
1
+ /**
2
+ * @file Core middleware types and basic middleware creation
3
+ */
4
+
5
+ import type { Context, BaseMachine } from '../index';
6
+
7
+ // =============================================================================
8
+ // SECTION: MIDDLEWARE TYPES
9
+ // =============================================================================
10
+
11
+ /**
12
+ * Context object passed to middleware hooks containing transition metadata.
13
+ * @template C - The context object type
14
+ */
15
+ export interface MiddlewareContext<C extends object> {
16
+ /** The name of the transition being called */
17
+ transitionName: string;
18
+ /** The current machine context before the transition */
19
+ context: Readonly<C>;
20
+ /** Arguments passed to the transition function */
21
+ args: any[];
22
+ }
23
+
24
+ /**
25
+ * Result object passed to after hooks containing transition outcome.
26
+ * @template C - The context object type
27
+ */
28
+ export interface MiddlewareResult<C extends object> {
29
+ /** The name of the transition that was called */
30
+ transitionName: string;
31
+ /** The context before the transition */
32
+ prevContext: Readonly<C>;
33
+ /** The context after the transition */
34
+ nextContext: Readonly<C>;
35
+ /** Arguments that were passed to the transition */
36
+ args: any[];
37
+ }
38
+
39
+ /**
40
+ * Error context passed to error hooks.
41
+ * @template C - The context object type
42
+ */
43
+ export interface MiddlewareError<C extends object> {
44
+ /** The name of the transition that failed */
45
+ transitionName: string;
46
+ /** The context when the error occurred */
47
+ context: Readonly<C>;
48
+ /** Arguments that were passed to the transition */
49
+ args: any[];
50
+ /** The error that was thrown */
51
+ error: Error;
52
+ }
53
+
54
+ /**
55
+ * Configuration object for middleware hooks.
56
+ * All hooks are optional - provide only the ones you need.
57
+ * @template C - The context object type
58
+ */
59
+ export interface MiddlewareHooks<C extends object> {
60
+ /**
61
+ * Called before a transition executes.
62
+ * Can be used for validation, logging, analytics, etc.
63
+ *
64
+ * @param ctx - Transition context with machine state and transition details
65
+ * @returns void to continue, CANCEL to abort silently, or Promise for async validation
66
+ */
67
+ before?: (ctx: MiddlewareContext<C>) => void | typeof CANCEL | Promise<void | typeof CANCEL>;
68
+
69
+ /**
70
+ * Called after a transition successfully executes.
71
+ * Receives both the previous and next context.
72
+ * Cannot prevent the transition (it already happened).
73
+ *
74
+ * @param result - Transition result with before/after contexts and transition details
75
+ */
76
+ after?: (result: MiddlewareResult<C>) => void | Promise<void>;
77
+
78
+ /**
79
+ * Called when a transition throws an error.
80
+ * Can be used for error reporting, recovery, etc.
81
+ *
82
+ * @param error - Error context with transition details and error information
83
+ */
84
+ error?: (error: MiddlewareError<C>) => void | Promise<void>;
85
+ }
86
+
87
+ /**
88
+ * Options for configuring middleware behavior.
89
+ */
90
+ export interface MiddlewareOptions {
91
+ /** Whether to continue execution if a hook throws an error */
92
+ continueOnError?: boolean;
93
+ /** Whether to log errors from hooks */
94
+ logErrors?: boolean;
95
+ /** Custom error handler for hook errors */
96
+ onError?: (error: Error, hookName: string, ctx: any) => void;
97
+ }
98
+
99
+ /**
100
+ * Symbol used to cancel a transition from a before hook.
101
+ */
102
+ export const CANCEL = Symbol('CANCEL');
103
+
104
+ // =============================================================================
105
+ // SECTION: CORE MIDDLEWARE FUNCTIONS
106
+ // =============================================================================
107
+
108
+ /**
109
+ * Creates a middleware function that wraps machine transitions with hooks.
110
+ *
111
+ * @template M - The machine type
112
+ * @param machine - The machine to instrument
113
+ * @param hooks - Middleware hooks to execute
114
+ * @param options - Middleware configuration options
115
+ * @returns A new machine with middleware applied
116
+ */
117
+ export function createMiddleware<M extends BaseMachine<any>>(
118
+ machine: M,
119
+ hooks: MiddlewareHooks<Context<M>>,
120
+ options: MiddlewareOptions = {}
121
+ ): M {
122
+ const { continueOnError = false, logErrors = true, onError } = options;
123
+
124
+ // Create a wrapped machine that intercepts all transition calls
125
+ const wrappedMachine: any = { ...machine };
126
+
127
+ // Copy any extra properties from the original machine (for middleware composition)
128
+ for (const prop in machine) {
129
+ if (!Object.prototype.hasOwnProperty.call(machine, prop)) continue;
130
+ if (prop !== 'context' && typeof machine[prop] !== 'function') {
131
+ wrappedMachine[prop] = machine[prop];
132
+ }
133
+ }
134
+
135
+ // Wrap each transition function
136
+ for (const prop in machine) {
137
+ if (!Object.prototype.hasOwnProperty.call(machine, prop)) continue;
138
+ const value = machine[prop];
139
+ if (typeof value === 'function' && prop !== 'context') {
140
+ wrappedMachine[prop] = function (this: any, ...args: any[]) {
141
+ const transitionName = prop;
142
+ const context = wrappedMachine.context;
143
+
144
+ // Helper function to execute the transition and after hooks
145
+ const executeTransition = () => {
146
+ // 2. Execute the actual transition
147
+ let nextMachine: any;
148
+ try {
149
+ nextMachine = value.apply(this, args);
150
+ } catch (error) {
151
+ // 3. Execute error hooks if transition failed
152
+ if (hooks.error) {
153
+ try {
154
+ // Error hooks are called synchronously for now
155
+ hooks.error({
156
+ transitionName,
157
+ context,
158
+ args: [...args],
159
+ error: error as Error
160
+ });
161
+ } catch (hookError) {
162
+ if (!continueOnError) throw hookError;
163
+ if (logErrors) console.error(`Middleware error hook error for ${transitionName}:`, hookError);
164
+ onError?.(hookError as Error, 'error', { transitionName, context, args, error });
165
+ }
166
+ }
167
+ throw error; // Re-throw the original error
168
+ }
169
+
170
+ // Ensure the returned machine has the same extra properties as the wrapped machine
171
+ const ensureMiddlewareProperties = (machine: any) => {
172
+ if (machine && typeof machine === 'object' && machine.context) {
173
+ // Copy extra properties from the wrapped machine to the returned machine
174
+ for (const prop in wrappedMachine) {
175
+ if (!Object.prototype.hasOwnProperty.call(wrappedMachine, prop)) continue;
176
+ if (prop !== 'context' && !(prop in machine)) {
177
+ machine[prop] = wrappedMachine[prop];
178
+ }
179
+ }
180
+
181
+ // Also wrap the transition functions on the returned machine
182
+ for (const prop in machine) {
183
+ if (!Object.prototype.hasOwnProperty.call(machine, prop)) continue;
184
+ const value = machine[prop];
185
+ if (typeof value === 'function' && prop !== 'context' && wrappedMachine[prop]) {
186
+ machine[prop] = wrappedMachine[prop];
187
+ }
188
+ }
189
+ }
190
+ return machine;
191
+ };
192
+
193
+ // Check if the transition is async (returns a Promise)
194
+ if (nextMachine && typeof nextMachine.then === 'function') {
195
+ // For async transitions, we need to handle the after hooks after the promise resolves
196
+ const asyncResult = nextMachine.then((resolvedMachine: any) => {
197
+ // Ensure middleware properties are attached
198
+ ensureMiddlewareProperties(resolvedMachine);
199
+
200
+ // Execute after hooks with the resolved machine
201
+ if (hooks.after) {
202
+ try {
203
+ const result = hooks.after({
204
+ transitionName,
205
+ prevContext: context,
206
+ nextContext: resolvedMachine.context,
207
+ args: [...args]
208
+ });
209
+
210
+ // Handle async after hooks
211
+ if (result && typeof result.then === 'function') {
212
+ return result.then(() => resolvedMachine);
213
+ }
214
+ } catch (error) {
215
+ if (!continueOnError) throw error;
216
+ if (logErrors) console.error(`Middleware after hook error for ${transitionName}:`, error);
217
+ onError?.(error as Error, 'after', {
218
+ transitionName,
219
+ prevContext: context,
220
+ nextContext: resolvedMachine.context,
221
+ args
222
+ });
223
+ }
224
+ }
225
+ return resolvedMachine;
226
+ });
227
+
228
+ return asyncResult;
229
+ } else {
230
+ // Ensure middleware properties are attached to synchronous transitions
231
+ ensureMiddlewareProperties(nextMachine);
232
+
233
+ // Synchronous transition
234
+ // 4. Execute after hooks
235
+ if (hooks.after) {
236
+ try {
237
+ const result = hooks.after({
238
+ transitionName,
239
+ prevContext: context,
240
+ nextContext: nextMachine.context,
241
+ args: [...args]
242
+ });
243
+
244
+ // Handle async after hooks
245
+ if (result && typeof result === 'object' && result && 'then' in result) {
246
+ return result.then(() => nextMachine).catch((error: Error) => {
247
+ if (!continueOnError) throw error;
248
+ if (logErrors) console.error(`Middleware after hook error for ${transitionName}:`, error);
249
+ onError?.(error, 'after', {
250
+ transitionName,
251
+ prevContext: context,
252
+ nextContext: nextMachine.context,
253
+ args
254
+ });
255
+ return nextMachine;
256
+ });
257
+ }
258
+ } catch (error) {
259
+ if (!continueOnError) throw error;
260
+ if (logErrors) console.error(`Middleware after hook error for ${transitionName}:`, error);
261
+ onError?.(error as Error, 'after', {
262
+ transitionName,
263
+ prevContext: context,
264
+ nextContext: nextMachine.context,
265
+ args
266
+ });
267
+ }
268
+ }
269
+
270
+ // 5. Return the next machine state
271
+ return nextMachine;
272
+ }
273
+ };
274
+
275
+ // 1. Execute before hooks synchronously if possible
276
+ if (hooks.before) {
277
+ try {
278
+ const result = hooks.before({
279
+ transitionName,
280
+ context,
281
+ args: [...args]
282
+ });
283
+
284
+ // Handle async hooks
285
+ if (result && typeof result === 'object' && result && 'then' in result) {
286
+ // For async hooks, return a promise that executes the transition after
287
+ return result.then((hookResult: any) => {
288
+ if (hookResult === CANCEL) {
289
+ return wrappedMachine;
290
+ }
291
+ return executeTransition();
292
+ }).catch((error: Error) => {
293
+ if (!continueOnError) throw error;
294
+ if (logErrors) console.error(`Middleware before hook error for ${transitionName}:`, error);
295
+ onError?.(error, 'before', { transitionName, context, args });
296
+ return executeTransition();
297
+ });
298
+ }
299
+
300
+ // Check if transition should be cancelled
301
+ if (result === CANCEL) {
302
+ return wrappedMachine; // Return the same machine instance
303
+ }
304
+ } catch (error) {
305
+ if (!continueOnError) throw error;
306
+ if (logErrors) console.error(`Middleware before hook error for ${transitionName}:`, error);
307
+ onError?.(error as Error, 'before', { transitionName, context, args });
308
+ }
309
+ };
310
+
311
+ return executeTransition();
312
+ };
313
+ }
314
+ }
315
+
316
+ return wrappedMachine;
317
+ }
318
+
319
+ /**
320
+ * Creates a simple logging middleware that logs all transitions.
321
+ *
322
+ * @template M - The machine type
323
+ * @param machine - The machine to add logging to
324
+ * @param options - Logging configuration options
325
+ * @returns A new machine with logging middleware
326
+ */
327
+ export function withLogging<M extends BaseMachine<any>>(
328
+ machine: M,
329
+ options: {
330
+ logger?: (message: string) => void;
331
+ includeArgs?: boolean;
332
+ includeContext?: boolean;
333
+ } = {}
334
+ ): M {
335
+ const { logger = console.log, includeArgs = false, includeContext = true } = options;
336
+
337
+ return createMiddleware(machine, {
338
+ before: ({ transitionName, args }) => {
339
+ const message = includeArgs ? `→ ${transitionName} [${args.join(', ')}]` : `→ ${transitionName}`;
340
+ logger(message);
341
+ },
342
+ after: ({ transitionName, nextContext }) => {
343
+ const contextStr = includeContext ? ` ${JSON.stringify(nextContext)}` : '';
344
+ logger(`✓ ${transitionName}${contextStr}`);
345
+ },
346
+ error: ({ transitionName, error }) => {
347
+ console.error(`[Machine] ${transitionName} failed:`, error);
348
+ }
349
+ });
350
+ }
351
+
352
+ /**
353
+ * Creates analytics tracking middleware.
354
+ *
355
+ * @template M - The machine type
356
+ * @param machine - The machine to track
357
+ * @param track - Analytics tracking function
358
+ * @param options - Configuration options
359
+ * @returns A new machine with analytics tracking
360
+ */
361
+ export function withAnalytics<M extends BaseMachine<any>>(
362
+ machine: M,
363
+ track: (event: string, data?: any) => void,
364
+ options: {
365
+ eventPrefix?: string;
366
+ includePrevContext?: boolean;
367
+ includeArgs?: boolean;
368
+ } = {}
369
+ ): M {
370
+ const { eventPrefix = 'state_transition', includePrevContext = false, includeArgs = false } = options;
371
+
372
+ return createMiddleware(machine, {
373
+ after: ({ transitionName, prevContext, nextContext, args }) => {
374
+ const event = `${eventPrefix}.${transitionName}`;
375
+ const data: any = { transition: transitionName };
376
+ if (includePrevContext) data.from = prevContext;
377
+ data.to = nextContext;
378
+ if (includeArgs) data.args = args;
379
+ track(event, data);
380
+ }
381
+ });
382
+ }
383
+
384
+ /**
385
+ * Creates validation middleware that runs before transitions.
386
+ *
387
+ * @template M - The machine type
388
+ * @param machine - The machine to validate
389
+ * @param validator - Validation function
390
+ * @returns A new machine with validation
391
+ */
392
+ export function withValidation<M extends BaseMachine<any>>(
393
+ machine: M,
394
+ validator: (ctx: MiddlewareContext<Context<M>>) => boolean | void
395
+ ): M {
396
+ return createMiddleware(machine, {
397
+ before: (ctx) => {
398
+ const result = validator(ctx);
399
+ if (result === false) {
400
+ throw new Error(`Validation failed for transition: ${ctx.transitionName}`);
401
+ }
402
+ }
403
+ });
404
+ }
405
+
406
+ /**
407
+ * Creates permission-checking middleware.
408
+ *
409
+ * @template M - The machine type
410
+ * @param machine - The machine to protect
411
+ * @param checker - Permission checking function
412
+ * @returns A new machine with permission checks
413
+ */
414
+ export function withPermissions<M extends BaseMachine<any>>(
415
+ machine: M,
416
+ checker: (ctx: MiddlewareContext<Context<M>>) => boolean
417
+ ): M {
418
+ return createMiddleware(machine, {
419
+ before: (ctx) => {
420
+ if (!checker(ctx)) {
421
+ throw new Error(`Unauthorized transition: ${ctx.transitionName}`);
422
+ }
423
+ }
424
+ });
425
+ }
426
+
427
+ /**
428
+ * Creates error reporting middleware.
429
+ *
430
+ * @template M - The machine type
431
+ * @param machine - The machine to monitor
432
+ * @param reporter - Error reporting function
433
+ * @param options - Configuration options
434
+ * @returns A new machine with error reporting
435
+ */
436
+ export function withErrorReporting<M extends BaseMachine<any>>(
437
+ machine: M,
438
+ reporter: (error: Error, ctx: any) => void,
439
+ options: { includeArgs?: boolean } = {}
440
+ ): M {
441
+ const { includeArgs = false } = options;
442
+
443
+ return createMiddleware(machine, {
444
+ error: (errorCtx) => {
445
+ // Format the context to match test expectations
446
+ const formattedCtx = {
447
+ transition: errorCtx.transitionName,
448
+ context: errorCtx.context,
449
+ ...(includeArgs && { args: errorCtx.args })
450
+ };
451
+ reporter(errorCtx.error, formattedCtx);
452
+ }
453
+ });
454
+ }
455
+
456
+ /**
457
+ * Creates performance monitoring middleware.
458
+ *
459
+ * @template M - The machine type
460
+ * @param machine - The machine to monitor
461
+ * @param tracker - Performance tracking function
462
+ * @returns A new machine with performance monitoring
463
+ */
464
+ export function withPerformanceMonitoring<M extends BaseMachine<any>>(
465
+ machine: M,
466
+ tracker: (metric: { transitionName: string; duration: number; context: Context<M> }) => void
467
+ ): M {
468
+ const startTimes = new Map<string, number>();
469
+
470
+ return createMiddleware(machine, {
471
+ before: (ctx) => {
472
+ startTimes.set(ctx.transitionName, Date.now());
473
+ },
474
+ after: (result) => {
475
+ const startTime = startTimes.get(result.transitionName);
476
+ if (startTime) {
477
+ const duration = Date.now() - startTime;
478
+ startTimes.delete(result.transitionName);
479
+ // For test compatibility, pass a single object as expected
480
+ const testResult = {
481
+ transitionName: result.transitionName,
482
+ duration,
483
+ context: result.nextContext || result.prevContext
484
+ };
485
+ tracker(testResult);
486
+ }
487
+ }
488
+ });
489
+ }
490
+
491
+ /**
492
+ * Creates retry middleware for failed transitions.
493
+ *
494
+ * @template M - The machine type
495
+ * @param machine - The machine to add retry logic to
496
+ * @param options - Retry configuration
497
+ * @returns A new machine with retry logic
498
+ */
499
+ export function withRetry<M extends BaseMachine<any>>(
500
+ machine: M,
501
+ options: {
502
+ maxAttempts?: number;
503
+ maxRetries?: number; // alias for maxAttempts
504
+ shouldRetry?: (error: Error, attempt: number) => boolean;
505
+ backoffMs?: number | ((attempt: number) => number);
506
+ delay?: number | ((attempt: number) => number); // alias for backoffMs
507
+ backoffMultiplier?: number; // multiplier for exponential backoff
508
+ onRetry?: (error: Error, attempt: number) => void;
509
+ } = {}
510
+ ): M {
511
+ const {
512
+ maxAttempts = options.maxRetries ?? 3,
513
+ shouldRetry = () => true,
514
+ backoffMs = options.delay ?? 100,
515
+ backoffMultiplier = 2,
516
+ onRetry
517
+ } = options;
518
+
519
+ // Create a wrapped machine that adds retry logic
520
+ const wrappedMachine: any = { ...machine };
521
+
522
+ // Wrap each transition function with retry logic
523
+ for (const prop in machine) {
524
+ if (!Object.prototype.hasOwnProperty.call(machine, prop)) continue;
525
+ const value = machine[prop];
526
+ if (typeof value === 'function' && prop !== 'context') {
527
+ wrappedMachine[prop] = async function (this: any, ...args: any[]) {
528
+ let lastError: Error;
529
+ let attempt = 0;
530
+
531
+ while (attempt < maxAttempts) {
532
+ try {
533
+ return await value.apply(this, args);
534
+ } catch (error) {
535
+ lastError = error as Error;
536
+ attempt++;
537
+
538
+ if (attempt < maxAttempts && shouldRetry(lastError, attempt)) {
539
+ onRetry?.(lastError, attempt);
540
+ const baseDelay = typeof backoffMs === 'function' ? backoffMs(attempt) : backoffMs;
541
+ const delay = baseDelay * Math.pow(backoffMultiplier, attempt - 1);
542
+ await new Promise(resolve => setTimeout(resolve, delay));
543
+ } else {
544
+ throw lastError;
545
+ }
546
+ }
547
+ }
548
+
549
+ throw lastError!;
550
+ };
551
+
552
+
553
+
554
+ }
555
+ }
556
+
557
+ return wrappedMachine;
558
+ }
559
+
560
+ /**
561
+ * Creates custom middleware from hooks.
562
+ *
563
+ * @template M - The machine type
564
+ * @param hooks - Middleware hooks
565
+ * @param options - Middleware options
566
+ * @returns A middleware function
567
+ */
568
+ export function createCustomMiddleware<M extends BaseMachine<any>>(
569
+ hooks: MiddlewareHooks<Context<M>>,
570
+ options?: MiddlewareOptions
571
+ ): (machine: M) => M {
572
+ return (machine: M) => createMiddleware(machine, hooks, options);
573
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * @file History tracking middleware
3
+ */
4
+
5
+ import type { BaseMachine } from '../index';
6
+ import { createMiddleware } from './core';
7
+
8
+ // =============================================================================
9
+ // SECTION: HISTORY TYPES
10
+ // =============================================================================
11
+
12
+ /**
13
+ * A single history entry recording a transition.
14
+ */
15
+ export interface HistoryEntry {
16
+ /** Unique ID for this history entry */
17
+ id: string;
18
+ /** Name of the transition that was called */
19
+ transitionName: string;
20
+ /** Arguments passed to the transition */
21
+ args: any[];
22
+ /** Timestamp when the transition occurred */
23
+ timestamp: number;
24
+ /** Optional serialized version of args for persistence */
25
+ serializedArgs?: string;
26
+ }
27
+
28
+ /**
29
+ * Serializer interface for converting context/args to/from strings.
30
+ */
31
+ export interface Serializer<T = any> {
32
+ serialize: (value: T) => string;
33
+ deserialize: (str: string) => T;
34
+ }
35
+
36
+ // =============================================================================
37
+ // SECTION: HISTORY MIDDLEWARE
38
+ // =============================================================================
39
+
40
+ /**
41
+ * Creates a machine with history tracking capabilities.
42
+ * Records all transitions that occur, allowing you to see the sequence of state changes.
43
+ *
44
+ * @template M - The machine type
45
+ * @param machine - The machine to track
46
+ * @param options - Configuration options
47
+ * @returns A new machine with history tracking
48
+ *
49
+ * @example
50
+ * ```typescript
51
+ * const tracked = withHistory(counter, { maxSize: 50 });
52
+ * tracked.increment();
53
+ * console.log(tracked.history); // [{ id: "entry-1", transitionName: "increment", ... }]
54
+ * ```
55
+ */
56
+ export function withHistory<M extends BaseMachine<any>>(
57
+ machine: M,
58
+ options: {
59
+ /** Maximum number of history entries to keep (default: unlimited) */
60
+ maxSize?: number;
61
+ /** Optional serializer for transition arguments */
62
+ serializer?: Serializer<any[]>;
63
+ /** Callback when a transition occurs */
64
+ onEntry?: (entry: HistoryEntry) => void;
65
+ } = {}
66
+ ): M & { history: HistoryEntry[]; clearHistory: () => void } {
67
+ const { maxSize, serializer, onEntry } = options;
68
+ const history: HistoryEntry[] = [];
69
+ let entryId = 0;
70
+
71
+ const instrumentedMachine = createMiddleware(machine, {
72
+ before: ({ transitionName, args }) => {
73
+ const entry: HistoryEntry = {
74
+ id: `entry-${entryId++}`,
75
+ transitionName,
76
+ args: [...args],
77
+ timestamp: Date.now()
78
+ };
79
+
80
+ if (serializer) {
81
+ try {
82
+ entry.serializedArgs = serializer.serialize(args);
83
+ } catch (err) {
84
+ console.error('Failed to serialize history args:', err);
85
+ }
86
+ }
87
+
88
+ history.push(entry);
89
+
90
+ // Enforce max size
91
+ if (maxSize && history.length > maxSize) {
92
+ history.shift();
93
+ }
94
+
95
+ onEntry?.(entry);
96
+ }
97
+ });
98
+
99
+ // Attach history properties to the machine
100
+ return Object.assign(instrumentedMachine, {
101
+ history,
102
+ clearHistory: () => { history.length = 0; entryId = 0; }
103
+ });
104
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @file Middleware module exports
3
+ * @description Unified exports for all middleware functionality
4
+ */
5
+
6
+ // Core middleware types and functions
7
+ export * from './core';
8
+
9
+ // Specialized middleware modules
10
+ export * from './history';
11
+ export * from './snapshot';
12
+ export * from './time-travel';
13
+ export * from './composition';