@doeixd/machine 1.0.3 → 1.2.0
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.
- package/README.md +48 -0
- package/dist/cjs/development/core.js.map +1 -1
- package/dist/cjs/development/delegate.js +89 -0
- package/dist/cjs/development/delegate.js.map +7 -0
- package/dist/cjs/development/index.js +383 -158
- package/dist/cjs/development/index.js.map +4 -4
- package/dist/cjs/development/minimal.js +172 -0
- package/dist/cjs/development/minimal.js.map +7 -0
- package/dist/cjs/development/react.js.map +1 -1
- package/dist/cjs/production/delegate.js +1 -0
- package/dist/cjs/production/index.js +3 -3
- package/dist/cjs/production/minimal.js +1 -0
- package/dist/esm/development/core.js.map +1 -1
- package/dist/esm/development/delegate.js +68 -0
- package/dist/esm/development/delegate.js.map +7 -0
- package/dist/esm/development/index.js +389 -158
- package/dist/esm/development/index.js.map +4 -4
- package/dist/esm/development/minimal.js +149 -0
- package/dist/esm/development/minimal.js.map +7 -0
- package/dist/esm/development/react.js.map +1 -1
- package/dist/esm/production/delegate.js +1 -0
- package/dist/esm/production/index.js +3 -3
- package/dist/esm/production/minimal.js +1 -0
- package/dist/types/delegate.d.ts +101 -0
- package/dist/types/delegate.d.ts.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/minimal.d.ts +95 -0
- package/dist/types/minimal.d.ts.map +1 -0
- package/dist/types/types.d.ts +110 -0
- package/dist/types/types.d.ts.map +1 -0
- package/package.json +25 -1
- package/src/delegate.ts +267 -0
- package/src/index.ts +13 -0
- package/src/middleware.ts +1049 -1050
- package/src/minimal.ts +269 -0
- package/src/types.ts +129 -0
package/src/middleware.ts
CHANGED
|
@@ -279,1168 +279,1167 @@ export function createMiddleware<M extends BaseMachine<any>>(
|
|
|
279
279
|
onError?.(error as Error, 'before', { transitionName, context, args });
|
|
280
280
|
}
|
|
281
281
|
};
|
|
282
|
+
}
|
|
282
283
|
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
return wrappedMachine;
|
|
286
|
-
}
|
|
287
284
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
*
|
|
291
|
-
* @template M - The machine type
|
|
292
|
-
* @param machine - The machine to add logging to
|
|
293
|
-
* @param options - Logging configuration options
|
|
294
|
-
* @returns A new machine with logging middleware
|
|
295
|
-
*/
|
|
296
|
-
export function withLogging<M extends BaseMachine<any>>(
|
|
297
|
-
machine: M,
|
|
298
|
-
options: {
|
|
299
|
-
logger?: (message: string) => void;
|
|
300
|
-
includeArgs?: boolean;
|
|
301
|
-
includeContext?: boolean;
|
|
302
|
-
} = {}
|
|
303
|
-
): M {
|
|
304
|
-
const { logger = console.log, includeArgs = false, includeContext = true } = options;
|
|
305
|
-
|
|
306
|
-
return createMiddleware(machine, {
|
|
307
|
-
before: ({ transitionName, args }) => {
|
|
308
|
-
const message = includeArgs ? `→ ${transitionName} [${args.join(', ')}]` : `→ ${transitionName}`;
|
|
309
|
-
logger(message);
|
|
310
|
-
},
|
|
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);
|
|
317
|
-
}
|
|
318
|
-
});
|
|
319
|
-
}
|
|
285
|
+
return wrappedMachine;
|
|
286
|
+
}
|
|
320
287
|
|
|
321
|
-
/**
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
}
|
|
288
|
+
/**
|
|
289
|
+
* Creates a simple logging middleware that logs all transitions.
|
|
290
|
+
*
|
|
291
|
+
* @template M - The machine type
|
|
292
|
+
* @param machine - The machine to add logging to
|
|
293
|
+
* @param options - Logging configuration options
|
|
294
|
+
* @returns A new machine with logging middleware
|
|
295
|
+
*/
|
|
296
|
+
export function withLogging<M extends BaseMachine<any>>(
|
|
297
|
+
machine: M,
|
|
298
|
+
options: {
|
|
299
|
+
logger?: (message: string) => void;
|
|
300
|
+
includeArgs?: boolean;
|
|
301
|
+
includeContext?: boolean;
|
|
302
|
+
} = {}
|
|
303
|
+
): M {
|
|
304
|
+
const { logger = console.log, includeArgs = false, includeContext = true } = options;
|
|
305
|
+
|
|
306
|
+
return createMiddleware(machine, {
|
|
307
|
+
before: ({ transitionName, args }) => {
|
|
308
|
+
const message = includeArgs ? `→ ${transitionName} [${args.join(', ')}]` : `→ ${transitionName}`;
|
|
309
|
+
logger(message);
|
|
310
|
+
},
|
|
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);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
}
|
|
352
320
|
|
|
353
|
-
/**
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
321
|
+
/**
|
|
322
|
+
* Creates analytics tracking middleware.
|
|
323
|
+
*
|
|
324
|
+
* @template M - The machine type
|
|
325
|
+
* @param machine - The machine to track
|
|
326
|
+
* @param track - Analytics tracking function
|
|
327
|
+
* @param options - Configuration options
|
|
328
|
+
* @returns A new machine with analytics tracking
|
|
329
|
+
*/
|
|
330
|
+
export function withAnalytics<M extends BaseMachine<any>>(
|
|
331
|
+
machine: M,
|
|
332
|
+
track: (event: string, data?: any) => void,
|
|
333
|
+
options: {
|
|
334
|
+
eventPrefix?: string;
|
|
335
|
+
includePrevContext?: boolean;
|
|
336
|
+
includeArgs?: boolean;
|
|
337
|
+
} = {}
|
|
338
|
+
): M {
|
|
339
|
+
const { eventPrefix = 'state_transition', includePrevContext = false, includeArgs = false } = options;
|
|
340
|
+
|
|
341
|
+
return createMiddleware(machine, {
|
|
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);
|
|
370
349
|
}
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
}
|
|
350
|
+
});
|
|
351
|
+
}
|
|
374
352
|
|
|
375
|
-
/**
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
export function
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
): M {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
353
|
+
/**
|
|
354
|
+
* Creates validation middleware that runs before transitions.
|
|
355
|
+
*
|
|
356
|
+
* @template M - The machine type
|
|
357
|
+
* @param machine - The machine to validate
|
|
358
|
+
* @param validator - Validation function
|
|
359
|
+
* @returns A new machine with validation
|
|
360
|
+
*/
|
|
361
|
+
export function withValidation<M extends BaseMachine<any>>(
|
|
362
|
+
machine: M,
|
|
363
|
+
validator: (ctx: MiddlewareContext<Context<M>>) => boolean | void
|
|
364
|
+
): M {
|
|
365
|
+
return createMiddleware(machine, {
|
|
366
|
+
before: (ctx) => {
|
|
367
|
+
const result = validator(ctx);
|
|
368
|
+
if (result === false) {
|
|
369
|
+
throw new Error(`Validation failed for transition: ${ctx.transitionName}`);
|
|
370
|
+
}
|
|
391
371
|
}
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
}
|
|
372
|
+
});
|
|
373
|
+
}
|
|
395
374
|
|
|
396
|
-
/**
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
transition: errorCtx.transitionName,
|
|
417
|
-
context: errorCtx.context,
|
|
418
|
-
...(includeArgs && { args: errorCtx.args })
|
|
419
|
-
};
|
|
420
|
-
reporter(errorCtx.error, formattedCtx);
|
|
421
|
-
}
|
|
422
|
-
});
|
|
423
|
-
}
|
|
375
|
+
/**
|
|
376
|
+
* Creates permission-checking middleware.
|
|
377
|
+
*
|
|
378
|
+
* @template M - The machine type
|
|
379
|
+
* @param machine - The machine to protect
|
|
380
|
+
* @param checker - Permission checking function
|
|
381
|
+
* @returns A new machine with permission checks
|
|
382
|
+
*/
|
|
383
|
+
export function withPermissions<M extends BaseMachine<any>>(
|
|
384
|
+
machine: M,
|
|
385
|
+
checker: (ctx: MiddlewareContext<Context<M>>) => boolean
|
|
386
|
+
): M {
|
|
387
|
+
return createMiddleware(machine, {
|
|
388
|
+
before: (ctx) => {
|
|
389
|
+
if (!checker(ctx)) {
|
|
390
|
+
throw new Error(`Unauthorized transition: ${ctx.transitionName}`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
}
|
|
424
395
|
|
|
425
|
-
/**
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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
|
|
396
|
+
/**
|
|
397
|
+
* Creates error reporting middleware.
|
|
398
|
+
*
|
|
399
|
+
* @template M - The machine type
|
|
400
|
+
* @param machine - The machine to monitor
|
|
401
|
+
* @param reporter - Error reporting function
|
|
402
|
+
* @param options - Configuration options
|
|
403
|
+
* @returns A new machine with error reporting
|
|
404
|
+
*/
|
|
405
|
+
export function withErrorReporting<M extends BaseMachine<any>>(
|
|
406
|
+
machine: M,
|
|
407
|
+
reporter: (error: Error, ctx: any) => void,
|
|
408
|
+
options: { includeArgs?: boolean } = {}
|
|
409
|
+
): M {
|
|
410
|
+
const { includeArgs = false } = options;
|
|
411
|
+
|
|
412
|
+
return createMiddleware(machine, {
|
|
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 })
|
|
453
419
|
};
|
|
454
|
-
|
|
420
|
+
reporter(errorCtx.error, formattedCtx);
|
|
455
421
|
}
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
/**
|
|
461
|
-
* Creates retry middleware for failed transitions.
|
|
462
|
-
*
|
|
463
|
-
* @template M - The machine type
|
|
464
|
-
* @param machine - The machine to add retry logic to
|
|
465
|
-
* @param options - Retry configuration
|
|
466
|
-
* @returns A new machine with retry logic
|
|
467
|
-
*/
|
|
468
|
-
export function withRetry<M extends BaseMachine<any>>(
|
|
469
|
-
machine: M,
|
|
470
|
-
options: {
|
|
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;
|
|
478
|
-
} = {}
|
|
479
|
-
): M {
|
|
480
|
-
const {
|
|
481
|
-
maxAttempts = options.maxRetries ?? 3,
|
|
482
|
-
shouldRetry = () => true,
|
|
483
|
-
backoffMs = options.delay ?? 100,
|
|
484
|
-
backoffMultiplier = 2,
|
|
485
|
-
onRetry
|
|
486
|
-
} = options;
|
|
487
|
-
|
|
488
|
-
// Create a wrapped machine that adds retry logic
|
|
489
|
-
const wrappedMachine: any = { ...machine };
|
|
422
|
+
});
|
|
423
|
+
}
|
|
490
424
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
425
|
+
/**
|
|
426
|
+
* Creates performance monitoring middleware.
|
|
427
|
+
*
|
|
428
|
+
* @template M - The machine type
|
|
429
|
+
* @param machine - The machine to monitor
|
|
430
|
+
* @param tracker - Performance tracking function
|
|
431
|
+
* @returns A new machine with performance monitoring
|
|
432
|
+
*/
|
|
433
|
+
export function withPerformanceMonitoring<M extends BaseMachine<any>>(
|
|
434
|
+
machine: M,
|
|
435
|
+
tracker: (metric: { transitionName: string; duration: number; context: Context<M> }) => void
|
|
436
|
+
): M {
|
|
437
|
+
const startTimes = new Map<string, number>();
|
|
438
|
+
|
|
439
|
+
return createMiddleware(machine, {
|
|
440
|
+
before: (ctx) => {
|
|
441
|
+
startTimes.set(ctx.transitionName, Date.now());
|
|
442
|
+
},
|
|
443
|
+
after: (result) => {
|
|
444
|
+
const startTime = startTimes.get(result.transitionName);
|
|
445
|
+
if (startTime) {
|
|
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);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
}
|
|
499
459
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
460
|
+
/**
|
|
461
|
+
* Creates retry middleware for failed transitions.
|
|
462
|
+
*
|
|
463
|
+
* @template M - The machine type
|
|
464
|
+
* @param machine - The machine to add retry logic to
|
|
465
|
+
* @param options - Retry configuration
|
|
466
|
+
* @returns A new machine with retry logic
|
|
467
|
+
*/
|
|
468
|
+
export function withRetry<M extends BaseMachine<any>>(
|
|
469
|
+
machine: M,
|
|
470
|
+
options: {
|
|
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;
|
|
478
|
+
} = {}
|
|
479
|
+
): M {
|
|
480
|
+
const {
|
|
481
|
+
maxAttempts = options.maxRetries ?? 3,
|
|
482
|
+
shouldRetry = () => true,
|
|
483
|
+
backoffMs = options.delay ?? 100,
|
|
484
|
+
backoffMultiplier = 2,
|
|
485
|
+
onRetry
|
|
486
|
+
} = options;
|
|
487
|
+
|
|
488
|
+
// Create a wrapped machine that adds retry logic
|
|
489
|
+
const wrappedMachine: any = { ...machine };
|
|
490
|
+
|
|
491
|
+
// Wrap each transition function with retry logic
|
|
492
|
+
for (const prop in machine) {
|
|
493
|
+
if (!Object.prototype.hasOwnProperty.call(machine, prop)) continue;
|
|
494
|
+
const value = machine[prop];
|
|
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
|
+
}
|
|
514
515
|
}
|
|
515
516
|
}
|
|
516
|
-
}
|
|
517
517
|
|
|
518
|
-
|
|
518
|
+
throw lastError!;
|
|
519
519
|
};
|
|
520
520
|
|
|
521
521
|
|
|
522
|
-
|
|
523
|
-
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
524
|
|
|
525
|
-
|
|
526
|
-
}
|
|
525
|
+
return wrappedMachine;
|
|
526
|
+
}
|
|
527
527
|
|
|
528
|
-
/**
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
export function createCustomMiddleware<M extends BaseMachine<any>>(
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
): (machine: M) => M {
|
|
540
|
-
|
|
541
|
-
}
|
|
528
|
+
/**
|
|
529
|
+
* Creates custom middleware from hooks.
|
|
530
|
+
*
|
|
531
|
+
* @template M - The machine type
|
|
532
|
+
* @param hooks - Middleware hooks
|
|
533
|
+
* @param options - Middleware options
|
|
534
|
+
* @returns A middleware function
|
|
535
|
+
*/
|
|
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
|
+
}
|
|
542
542
|
|
|
543
|
-
// =============================================================================
|
|
544
|
-
// SECTION: HISTORY TRACKING
|
|
545
|
-
// =============================================================================
|
|
543
|
+
// =============================================================================
|
|
544
|
+
// SECTION: HISTORY TRACKING
|
|
545
|
+
// =============================================================================
|
|
546
546
|
|
|
547
|
-
/**
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
export interface HistoryEntry {
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
}
|
|
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
|
+
}
|
|
562
562
|
|
|
563
|
-
/**
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
export interface Serializer<T = any> {
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
}
|
|
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;
|
|
569
|
+
}
|
|
570
570
|
|
|
571
|
-
/**
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
export function withHistory<M extends BaseMachine<any>>(
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
): M & { history: HistoryEntry[]; clearHistory: () => void } {
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
571
|
+
/**
|
|
572
|
+
* Creates a machine with history tracking capabilities.
|
|
573
|
+
* Records all transitions that occur, allowing you to see the sequence of state changes.
|
|
574
|
+
*
|
|
575
|
+
* @template M - The machine type
|
|
576
|
+
* @param machine - The machine to track
|
|
577
|
+
* @param options - Configuration options
|
|
578
|
+
* @returns A new machine with history tracking
|
|
579
|
+
*
|
|
580
|
+
* @example
|
|
581
|
+
* ```typescript
|
|
582
|
+
* const tracked = withHistory(counter, { maxSize: 50 });
|
|
583
|
+
* tracked.increment();
|
|
584
|
+
* console.log(tracked.history); // [{ id: "entry-1", transitionName: "increment", ... }]
|
|
585
|
+
* ```
|
|
586
|
+
*/
|
|
587
|
+
export function withHistory<M extends BaseMachine<any>>(
|
|
588
|
+
machine: M,
|
|
589
|
+
options: {
|
|
590
|
+
/** Maximum number of history entries to keep (default: unlimited) */
|
|
591
|
+
maxSize?: number;
|
|
592
|
+
/** Optional serializer for transition arguments */
|
|
593
|
+
serializer?: Serializer<any[]>;
|
|
594
|
+
/** Callback when a transition occurs */
|
|
595
|
+
onEntry?: (entry: HistoryEntry) => void;
|
|
596
|
+
} = {}
|
|
597
|
+
): M & { history: HistoryEntry[]; clearHistory: () => void } {
|
|
598
|
+
const { maxSize, serializer, onEntry } = options;
|
|
599
|
+
const history: HistoryEntry[] = [];
|
|
600
|
+
let entryId = 0;
|
|
601
|
+
|
|
602
|
+
const instrumentedMachine = createMiddleware(machine, {
|
|
603
|
+
before: ({ transitionName, args }) => {
|
|
604
|
+
const entry: HistoryEntry = {
|
|
605
|
+
id: `entry-${entryId++}`,
|
|
606
|
+
transitionName,
|
|
607
|
+
args: [...args],
|
|
608
|
+
timestamp: Date.now()
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
if (serializer) {
|
|
612
|
+
try {
|
|
613
|
+
entry.serializedArgs = serializer.serialize(args);
|
|
614
|
+
} catch (err) {
|
|
615
|
+
console.error('Failed to serialize history args:', err);
|
|
616
|
+
}
|
|
616
617
|
}
|
|
617
|
-
}
|
|
618
618
|
|
|
619
|
-
|
|
619
|
+
history.push(entry);
|
|
620
620
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
621
|
+
// Enforce max size
|
|
622
|
+
if (maxSize && history.length > maxSize) {
|
|
623
|
+
history.shift();
|
|
624
|
+
}
|
|
625
625
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
626
|
+
onEntry?.(entry);
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
629
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
}
|
|
630
|
+
// Attach history properties to the machine
|
|
631
|
+
return Object.assign(instrumentedMachine, {
|
|
632
|
+
history,
|
|
633
|
+
clearHistory: () => { history.length = 0; entryId = 0; }
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
636
|
|
|
637
|
-
// =============================================================================
|
|
638
|
-
// SECTION: SNAPSHOT TRACKING
|
|
639
|
-
// =============================================================================
|
|
637
|
+
// =============================================================================
|
|
638
|
+
// SECTION: SNAPSHOT TRACKING
|
|
639
|
+
// =============================================================================
|
|
640
640
|
|
|
641
|
-
/**
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
export interface ContextSnapshot<C extends object> {
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
}
|
|
641
|
+
/**
|
|
642
|
+
* A snapshot of machine context before and after a transition.
|
|
643
|
+
*/
|
|
644
|
+
export interface ContextSnapshot<C extends object> {
|
|
645
|
+
/** Unique ID for this snapshot */
|
|
646
|
+
id: string;
|
|
647
|
+
/** Name of the transition that caused this snapshot */
|
|
648
|
+
transitionName: string;
|
|
649
|
+
/** Context before the transition */
|
|
650
|
+
before: C;
|
|
651
|
+
/** Context after the transition */
|
|
652
|
+
after: C;
|
|
653
|
+
/** Timestamp of the snapshot */
|
|
654
|
+
timestamp: number;
|
|
655
|
+
/** Optional serialized versions of contexts */
|
|
656
|
+
serializedBefore?: string;
|
|
657
|
+
serializedAfter?: string;
|
|
658
|
+
/** Optional diff information */
|
|
659
|
+
diff?: any;
|
|
660
|
+
}
|
|
661
661
|
|
|
662
|
-
/**
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
export function withSnapshot<M extends BaseMachine<any>>(
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
): M & {
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
} {
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
662
|
+
/**
|
|
663
|
+
* Creates a machine with snapshot tracking capabilities.
|
|
664
|
+
* Records context state before and after each transition for debugging and inspection.
|
|
665
|
+
*
|
|
666
|
+
* @template M - The machine type
|
|
667
|
+
* @param machine - The machine to track
|
|
668
|
+
* @param options - Configuration options
|
|
669
|
+
* @returns A new machine with snapshot tracking
|
|
670
|
+
*
|
|
671
|
+
* @example
|
|
672
|
+
* ```typescript
|
|
673
|
+
* const tracked = withSnapshot(counter, {
|
|
674
|
+
* maxSize: 50,
|
|
675
|
+
* serializer: {
|
|
676
|
+
* serialize: (ctx) => JSON.stringify(ctx),
|
|
677
|
+
* deserialize: (str) => JSON.parse(str)
|
|
678
|
+
* }
|
|
679
|
+
* });
|
|
680
|
+
*
|
|
681
|
+
* tracked.increment();
|
|
682
|
+
* console.log(tracked.snapshots); // [{ before: { count: 0 }, after: { count: 1 }, ... }]
|
|
683
|
+
* ```
|
|
684
|
+
*/
|
|
685
|
+
export function withSnapshot<M extends BaseMachine<any>>(
|
|
686
|
+
machine: M,
|
|
687
|
+
options: {
|
|
688
|
+
/** Maximum number of snapshots to keep (default: unlimited) */
|
|
689
|
+
maxSize?: number;
|
|
690
|
+
/** Optional serializer for context */
|
|
691
|
+
serializer?: Serializer<Context<M>>;
|
|
692
|
+
/** Custom function to capture additional snapshot data */
|
|
693
|
+
captureSnapshot?: (before: Context<M>, after: Context<M>) => any;
|
|
694
|
+
/** Only capture snapshots where context actually changed */
|
|
695
|
+
onlyOnChange?: boolean;
|
|
696
|
+
} = {}
|
|
697
|
+
): M & {
|
|
698
|
+
snapshots: ContextSnapshot<Context<M>>[];
|
|
699
|
+
clearSnapshots: () => void;
|
|
700
|
+
restoreSnapshot: (snapshot: ContextSnapshot<Context<M>>['before']) => M;
|
|
701
|
+
} {
|
|
702
|
+
const {
|
|
703
|
+
maxSize,
|
|
704
|
+
serializer,
|
|
705
|
+
captureSnapshot,
|
|
706
|
+
onlyOnChange = false
|
|
707
|
+
} = options;
|
|
708
|
+
|
|
709
|
+
const snapshots: ContextSnapshot<Context<M>>[] = [];
|
|
710
|
+
let snapshotId = 0;
|
|
711
|
+
|
|
712
|
+
const instrumentedMachine = createMiddleware(machine, {
|
|
713
|
+
after: ({ transitionName, prevContext, nextContext }) => {
|
|
714
|
+
// Skip if only capturing on change and context didn't change
|
|
715
|
+
if (onlyOnChange && JSON.stringify(prevContext) === JSON.stringify(nextContext)) {
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
718
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
719
|
+
const snapshot: ContextSnapshot<Context<M>> = {
|
|
720
|
+
id: `snapshot-${snapshotId++}`,
|
|
721
|
+
transitionName,
|
|
722
|
+
before: { ...prevContext },
|
|
723
|
+
after: { ...nextContext },
|
|
724
|
+
timestamp: Date.now()
|
|
725
|
+
};
|
|
726
726
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
727
|
+
// Serialize contexts if serializer provided
|
|
728
|
+
if (serializer) {
|
|
729
|
+
try {
|
|
730
|
+
snapshot.serializedBefore = serializer.serialize(prevContext);
|
|
731
|
+
snapshot.serializedAfter = serializer.serialize(nextContext);
|
|
732
|
+
} catch (err) {
|
|
733
|
+
console.error('Failed to serialize snapshot:', err);
|
|
734
|
+
}
|
|
734
735
|
}
|
|
735
|
-
}
|
|
736
736
|
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
737
|
+
// Capture custom snapshot data
|
|
738
|
+
if (captureSnapshot) {
|
|
739
|
+
try {
|
|
740
|
+
snapshot.diff = captureSnapshot(prevContext, nextContext);
|
|
741
|
+
} catch (err) {
|
|
742
|
+
console.error('Failed to capture snapshot:', err);
|
|
743
|
+
}
|
|
743
744
|
}
|
|
744
|
-
}
|
|
745
745
|
|
|
746
|
-
|
|
746
|
+
snapshots.push(snapshot);
|
|
747
747
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
748
|
+
// Enforce max size
|
|
749
|
+
if (maxSize && snapshots.length > maxSize) {
|
|
750
|
+
snapshots.shift();
|
|
751
|
+
}
|
|
751
752
|
}
|
|
752
|
-
}
|
|
753
|
-
});
|
|
754
|
-
|
|
755
|
-
// Helper to restore machine to a previous state
|
|
756
|
-
const restoreSnapshot = (context: Context<M>): M => {
|
|
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
|
-
);
|
|
753
|
+
});
|
|
767
754
|
|
|
768
|
-
|
|
769
|
-
|
|
755
|
+
// Helper to restore machine to a previous state
|
|
756
|
+
const restoreSnapshot = (context: Context<M>): M => {
|
|
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
|
+
);
|
|
770
767
|
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
snapshots,
|
|
774
|
-
clearSnapshots: () => { snapshots.length = 0; snapshotId = 0; },
|
|
775
|
-
restoreSnapshot
|
|
776
|
-
});
|
|
777
|
-
}
|
|
768
|
+
return Object.assign({ context }, transitions) as M;
|
|
769
|
+
};
|
|
778
770
|
|
|
779
|
-
//
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
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
|
-
};
|
|
771
|
+
// Attach snapshot properties to the machine
|
|
772
|
+
return Object.assign(instrumentedMachine, {
|
|
773
|
+
snapshots,
|
|
774
|
+
clearSnapshots: () => { snapshots.length = 0; snapshotId = 0; },
|
|
775
|
+
restoreSnapshot
|
|
776
|
+
});
|
|
777
|
+
}
|
|
792
778
|
|
|
793
|
-
|
|
794
|
-
|
|
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
|
-
};
|
|
779
|
+
// =============================================================================
|
|
780
|
+
// SECTION: TIME TRAVEL DEBUGGING
|
|
781
|
+
// =============================================================================
|
|
804
782
|
|
|
805
|
-
/**
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
export type
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
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
|
-
};
|
|
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
|
+
};
|
|
824
792
|
|
|
825
|
-
/**
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
* const debugMachine = withTimeTravel(counter);
|
|
837
|
-
*
|
|
838
|
-
* // Make some transitions
|
|
839
|
-
* debugMachine.increment();
|
|
840
|
-
* debugMachine.increment();
|
|
841
|
-
* debugMachine.decrement();
|
|
842
|
-
*
|
|
843
|
-
* // Time travel to previous states
|
|
844
|
-
* const previousState = debugMachine.replayFrom(0); // Replay from start
|
|
845
|
-
* const undoLast = debugMachine.restoreSnapshot(debugMachine.snapshots[1].before);
|
|
846
|
-
*
|
|
847
|
-
* // Inspect history
|
|
848
|
-
* console.log(debugMachine.history);
|
|
849
|
-
* console.log(debugMachine.snapshots);
|
|
850
|
-
* ```
|
|
851
|
-
*/
|
|
852
|
-
export function withTimeTravel<M extends BaseMachine<any>>(
|
|
853
|
-
machine: M,
|
|
854
|
-
options: {
|
|
855
|
-
/** Maximum number of history entries/snapshots to keep */
|
|
856
|
-
maxSize?: number;
|
|
857
|
-
/** Optional serializer for persistence */
|
|
858
|
-
serializer?: Serializer;
|
|
859
|
-
/** Callback when history/snapshot events occur */
|
|
860
|
-
onRecord?: (type: 'history' | 'snapshot', data: any) => void;
|
|
861
|
-
} = {}
|
|
862
|
-
): WithTimeTravel<M> {
|
|
863
|
-
const { maxSize, serializer, onRecord } = options;
|
|
864
|
-
|
|
865
|
-
// Create separate history and snapshot tracking
|
|
866
|
-
const history: HistoryEntry[] = [];
|
|
867
|
-
const snapshots: ContextSnapshot<Context<M>>[] = [];
|
|
868
|
-
let historyId = 0;
|
|
869
|
-
let snapshotId = 0;
|
|
870
|
-
|
|
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
|
-
};
|
|
880
|
-
|
|
881
|
-
if (serializer) {
|
|
882
|
-
try {
|
|
883
|
-
entry.serializedArgs = serializer.serialize(args);
|
|
884
|
-
} catch (err) {
|
|
885
|
-
console.error('Failed to serialize history args:', err);
|
|
886
|
-
}
|
|
887
|
-
}
|
|
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
|
+
};
|
|
888
804
|
|
|
889
|
-
|
|
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
|
+
};
|
|
890
824
|
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
825
|
+
/**
|
|
826
|
+
* Creates a machine with full time travel debugging capabilities.
|
|
827
|
+
* Combines history tracking, snapshots, and replay functionality.
|
|
828
|
+
*
|
|
829
|
+
* @template M - The machine type
|
|
830
|
+
* @param machine - The machine to enhance
|
|
831
|
+
* @param options - Configuration options
|
|
832
|
+
* @returns A machine with time travel capabilities
|
|
833
|
+
*
|
|
834
|
+
* @example
|
|
835
|
+
* ```typescript
|
|
836
|
+
* const debugMachine = withTimeTravel(counter);
|
|
837
|
+
*
|
|
838
|
+
* // Make some transitions
|
|
839
|
+
* debugMachine.increment();
|
|
840
|
+
* debugMachine.increment();
|
|
841
|
+
* debugMachine.decrement();
|
|
842
|
+
*
|
|
843
|
+
* // Time travel to previous states
|
|
844
|
+
* const previousState = debugMachine.replayFrom(0); // Replay from start
|
|
845
|
+
* const undoLast = debugMachine.restoreSnapshot(debugMachine.snapshots[1].before);
|
|
846
|
+
*
|
|
847
|
+
* // Inspect history
|
|
848
|
+
* console.log(debugMachine.history);
|
|
849
|
+
* console.log(debugMachine.snapshots);
|
|
850
|
+
* ```
|
|
851
|
+
*/
|
|
852
|
+
export function withTimeTravel<M extends BaseMachine<any>>(
|
|
853
|
+
machine: M,
|
|
854
|
+
options: {
|
|
855
|
+
/** Maximum number of history entries/snapshots to keep */
|
|
856
|
+
maxSize?: number;
|
|
857
|
+
/** Optional serializer for persistence */
|
|
858
|
+
serializer?: Serializer;
|
|
859
|
+
/** Callback when history/snapshot events occur */
|
|
860
|
+
onRecord?: (type: 'history' | 'snapshot', data: any) => void;
|
|
861
|
+
} = {}
|
|
862
|
+
): WithTimeTravel<M> {
|
|
863
|
+
const { maxSize, serializer, onRecord } = options;
|
|
864
|
+
|
|
865
|
+
// Create separate history and snapshot tracking
|
|
866
|
+
const history: HistoryEntry[] = [];
|
|
867
|
+
const snapshots: ContextSnapshot<Context<M>>[] = [];
|
|
868
|
+
let historyId = 0;
|
|
869
|
+
let snapshotId = 0;
|
|
870
|
+
|
|
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
|
+
};
|
|
895
880
|
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
before: { ...prevContext },
|
|
903
|
-
after: { ...nextContext },
|
|
904
|
-
timestamp: Date.now()
|
|
905
|
-
};
|
|
906
|
-
|
|
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);
|
|
881
|
+
if (serializer) {
|
|
882
|
+
try {
|
|
883
|
+
entry.serializedArgs = serializer.serialize(args);
|
|
884
|
+
} catch (err) {
|
|
885
|
+
console.error('Failed to serialize history args:', err);
|
|
886
|
+
}
|
|
914
887
|
}
|
|
915
|
-
}
|
|
916
888
|
|
|
917
|
-
|
|
889
|
+
history.push(entry);
|
|
918
890
|
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
891
|
+
// Enforce max size
|
|
892
|
+
if (maxSize && history.length > maxSize) {
|
|
893
|
+
history.shift();
|
|
894
|
+
}
|
|
923
895
|
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
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
|
-
)
|
|
943
|
-
);
|
|
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
|
+
};
|
|
944
906
|
|
|
945
|
-
|
|
946
|
-
|
|
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
|
+
}
|
|
915
|
+
}
|
|
947
916
|
|
|
948
|
-
|
|
949
|
-
const replayFrom = (startIndex: number): M => {
|
|
950
|
-
if (startIndex < 0 || startIndex >= history.length) {
|
|
951
|
-
throw new Error(`Invalid replay start index: ${startIndex}`);
|
|
952
|
-
}
|
|
917
|
+
snapshots.push(snapshot);
|
|
953
918
|
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
}
|
|
919
|
+
// Enforce max size
|
|
920
|
+
if (maxSize && snapshots.length > maxSize) {
|
|
921
|
+
snapshots.shift();
|
|
922
|
+
}
|
|
959
923
|
|
|
960
|
-
|
|
961
|
-
|
|
924
|
+
onRecord?.('snapshot', snapshot);
|
|
925
|
+
}
|
|
926
|
+
});
|
|
962
927
|
|
|
963
|
-
//
|
|
964
|
-
const
|
|
965
|
-
|
|
966
|
-
Object.fromEntries(
|
|
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(
|
|
967
932
|
Object.entries(machine).filter(([key]) =>
|
|
968
933
|
key !== 'context' &&
|
|
934
|
+
key !== 'history' &&
|
|
935
|
+
key !== 'snapshots' &&
|
|
936
|
+
key !== 'clearHistory' &&
|
|
937
|
+
key !== 'clearSnapshots' &&
|
|
938
|
+
key !== 'restoreSnapshot' &&
|
|
939
|
+
key !== 'clearTimeTravel' &&
|
|
940
|
+
key !== 'replayFrom' &&
|
|
969
941
|
typeof machine[key as keyof M] === 'function'
|
|
970
942
|
)
|
|
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, entry.args);
|
|
980
|
-
}
|
|
981
|
-
}
|
|
943
|
+
);
|
|
982
944
|
|
|
983
|
-
|
|
984
|
-
|
|
945
|
+
return Object.assign({ context }, transitions) as M;
|
|
946
|
+
};
|
|
985
947
|
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
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>;
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
// =============================================================================
|
|
1004
|
-
// SECTION: MIDDLEWARE COMPOSITION
|
|
1005
|
-
// =============================================================================
|
|
1006
|
-
|
|
1007
|
-
/**
|
|
1008
|
-
* Compose multiple middleware functions into a single middleware stack.
|
|
1009
|
-
* Middleware is applied left-to-right (first middleware wraps outermost).
|
|
1010
|
-
*
|
|
1011
|
-
* @template M - The machine type
|
|
1012
|
-
* @param machine - The base machine
|
|
1013
|
-
* @param middlewares - Array of middleware functions
|
|
1014
|
-
* @returns A new machine with all middleware applied
|
|
1015
|
-
*/
|
|
1016
|
-
export function compose<M extends BaseMachine<any>>(
|
|
1017
|
-
machine: M,
|
|
1018
|
-
...middlewares: Array<(m: M) => M>
|
|
1019
|
-
): M {
|
|
1020
|
-
return middlewares.reduce((acc, middleware) => middleware(acc), machine);
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
/**
|
|
1024
|
-
* Type-safe middleware composition with perfect inference.
|
|
1025
|
-
* Composes multiple middlewares into a single transformation chain.
|
|
1026
|
-
*
|
|
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
|
|
1032
|
-
*/
|
|
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>;
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
// =============================================================================
|
|
1044
|
-
// SECTION: TYPE-LEVEL COMPOSITION
|
|
1045
|
-
// =============================================================================
|
|
1046
|
-
|
|
1047
|
-
/**
|
|
1048
|
-
* Type-level utility for composing middleware return types.
|
|
1049
|
-
* This enables perfect TypeScript inference when chaining middlewares.
|
|
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
|
-
|
|
1062
|
-
/**
|
|
1063
|
-
* A middleware function that transforms a machine.
|
|
1064
|
-
* @template M - The input machine type
|
|
1065
|
-
* @template R - The output machine type (usually extends M)
|
|
1066
|
-
*/
|
|
1067
|
-
export type MiddlewareFn<M extends BaseMachine<any>, R extends BaseMachine<any> = M> = (machine: M) => R;
|
|
1068
|
-
|
|
1069
|
-
/**
|
|
1070
|
-
* A conditional middleware that may or may not be applied based on a predicate.
|
|
1071
|
-
* @template M - The machine type
|
|
1072
|
-
*/
|
|
1073
|
-
export type ConditionalMiddleware<M extends BaseMachine<any>> = {
|
|
1074
|
-
/** The middleware function to apply */
|
|
1075
|
-
middleware: MiddlewareFn<M>;
|
|
1076
|
-
/** Predicate function that determines if the middleware should be applied */
|
|
1077
|
-
when: (machine: M) => boolean;
|
|
1078
|
-
};
|
|
1079
|
-
|
|
1080
|
-
/**
|
|
1081
|
-
* A named middleware entry for registry-based composition.
|
|
1082
|
-
* @template M - The machine type
|
|
1083
|
-
*/
|
|
1084
|
-
export type NamedMiddleware<M extends BaseMachine<any>> = {
|
|
1085
|
-
/** Unique name for the middleware */
|
|
1086
|
-
name: string;
|
|
1087
|
-
/** The middleware function */
|
|
1088
|
-
middleware: MiddlewareFn<M>;
|
|
1089
|
-
/** Optional description */
|
|
1090
|
-
description?: string;
|
|
1091
|
-
/** Optional priority for ordering (higher numbers = applied later) */
|
|
1092
|
-
priority?: number;
|
|
1093
|
-
};
|
|
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}`);
|
|
952
|
+
}
|
|
1094
953
|
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
continueOnError?: boolean;
|
|
1101
|
-
/** Whether to log errors from middlewares */
|
|
1102
|
-
logErrors?: boolean;
|
|
1103
|
-
/** Custom error handler */
|
|
1104
|
-
onError?: (error: Error, middlewareIndex: number, middlewareName?: string) => void;
|
|
1105
|
-
}
|
|
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
|
+
}
|
|
1106
959
|
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
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, entry.args);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
1111
982
|
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
983
|
+
return replayedMachine;
|
|
984
|
+
};
|
|
985
|
+
|
|
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>;
|
|
1001
|
+
}
|
|
1115
1002
|
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
*/
|
|
1120
|
-
class MiddlewareChainBuilder<M extends BaseMachine<any>> {
|
|
1121
|
-
constructor(private machine: M) {}
|
|
1003
|
+
// =============================================================================
|
|
1004
|
+
// SECTION: MIDDLEWARE COMPOSITION
|
|
1005
|
+
// =============================================================================
|
|
1122
1006
|
|
|
1123
1007
|
/**
|
|
1124
|
-
*
|
|
1125
|
-
*
|
|
1126
|
-
*
|
|
1008
|
+
* Compose multiple middleware functions into a single middleware stack.
|
|
1009
|
+
* Middleware is applied left-to-right (first middleware wraps outermost).
|
|
1010
|
+
*
|
|
1011
|
+
* @template M - The machine type
|
|
1012
|
+
* @param machine - The base machine
|
|
1013
|
+
* @param middlewares - Array of middleware functions
|
|
1014
|
+
* @returns A new machine with all middleware applied
|
|
1127
1015
|
*/
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
return
|
|
1016
|
+
export function compose<M extends BaseMachine<any>>(
|
|
1017
|
+
machine: M,
|
|
1018
|
+
...middlewares: Array<(m: M) => M>
|
|
1019
|
+
): M {
|
|
1020
|
+
return middlewares.reduce((acc, middleware) => middleware(acc), machine);
|
|
1133
1021
|
}
|
|
1134
1022
|
|
|
1135
1023
|
/**
|
|
1136
|
-
*
|
|
1024
|
+
* Type-safe middleware composition with perfect inference.
|
|
1025
|
+
* Composes multiple middlewares into a single transformation chain.
|
|
1026
|
+
*
|
|
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
|
|
1137
1032
|
*/
|
|
1138
|
-
|
|
1139
|
-
|
|
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>;
|
|
1140
1041
|
}
|
|
1141
|
-
}
|
|
1142
1042
|
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
* @example
|
|
1147
|
-
* ```typescript
|
|
1148
|
-
* const enhanced = chain(counter)
|
|
1149
|
-
* .with(withHistory())
|
|
1150
|
-
* .with(withSnapshot())
|
|
1151
|
-
* .with(withTimeTravel())
|
|
1152
|
-
* .build();
|
|
1153
|
-
* ```
|
|
1154
|
-
*/
|
|
1155
|
-
export function chain<M extends BaseMachine<any>>(machine: M) {
|
|
1156
|
-
return new MiddlewareChainBuilder(machine);
|
|
1157
|
-
}
|
|
1043
|
+
// =============================================================================
|
|
1044
|
+
// SECTION: TYPE-LEVEL COMPOSITION
|
|
1045
|
+
// =============================================================================
|
|
1158
1046
|
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1047
|
+
/**
|
|
1048
|
+
* Type-level utility for composing middleware return types.
|
|
1049
|
+
* This enables perfect TypeScript inference when chaining middlewares.
|
|
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;
|
|
1162
1061
|
|
|
1163
|
-
/**
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
* @returns A conditional middleware that can be called directly or used in pipelines
|
|
1170
|
-
*/
|
|
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
|
-
};
|
|
1062
|
+
/**
|
|
1063
|
+
* A middleware function that transforms a machine.
|
|
1064
|
+
* @template M - The input machine type
|
|
1065
|
+
* @template R - The output machine type (usually extends M)
|
|
1066
|
+
*/
|
|
1067
|
+
export type MiddlewareFn<M extends BaseMachine<any>, R extends BaseMachine<any> = M> = (machine: M) => R;
|
|
1178
1068
|
|
|
1179
|
-
|
|
1180
|
-
|
|
1069
|
+
/**
|
|
1070
|
+
* A conditional middleware that may or may not be applied based on a predicate.
|
|
1071
|
+
* @template M - The machine type
|
|
1072
|
+
*/
|
|
1073
|
+
export type ConditionalMiddleware<M extends BaseMachine<any>> = {
|
|
1074
|
+
/** The middleware function to apply */
|
|
1075
|
+
middleware: MiddlewareFn<M>;
|
|
1076
|
+
/** Predicate function that determines if the middleware should be applied */
|
|
1077
|
+
when: (machine: M) => boolean;
|
|
1078
|
+
};
|
|
1181
1079
|
|
|
1182
|
-
|
|
1183
|
-
|
|
1080
|
+
/**
|
|
1081
|
+
* A named middleware entry for registry-based composition.
|
|
1082
|
+
* @template M - The machine type
|
|
1083
|
+
*/
|
|
1084
|
+
export type NamedMiddleware<M extends BaseMachine<any>> = {
|
|
1085
|
+
/** Unique name for the middleware */
|
|
1086
|
+
name: string;
|
|
1087
|
+
/** The middleware function */
|
|
1088
|
+
middleware: MiddlewareFn<M>;
|
|
1089
|
+
/** Optional description */
|
|
1090
|
+
description?: string;
|
|
1091
|
+
/** Optional priority for ordering (higher numbers = applied later) */
|
|
1092
|
+
priority?: number;
|
|
1093
|
+
};
|
|
1184
1094
|
|
|
1185
|
-
/**
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
*/
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
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
|
-
}
|
|
1095
|
+
/**
|
|
1096
|
+
* Configuration for middleware pipeline execution.
|
|
1097
|
+
*/
|
|
1098
|
+
export interface PipelineConfig {
|
|
1099
|
+
/** Whether to continue execution if a middleware throws an error */
|
|
1100
|
+
continueOnError?: boolean;
|
|
1101
|
+
/** Whether to log errors from middlewares */
|
|
1102
|
+
logErrors?: boolean;
|
|
1103
|
+
/** Custom error handler */
|
|
1104
|
+
onError?: (error: Error, middlewareIndex: number, middlewareName?: string) => void;
|
|
1105
|
+
}
|
|
1203
1106
|
|
|
1204
|
-
/**
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
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
|
-
}
|
|
1107
|
+
/**
|
|
1108
|
+
* Result of pipeline execution.
|
|
1109
|
+
*/
|
|
1110
|
+
export type PipelineResult<M extends BaseMachine<any>> = M;
|
|
1221
1111
|
|
|
1222
|
-
// =============================================================================
|
|
1223
|
-
// SECTION:
|
|
1224
|
-
// =============================================================================
|
|
1112
|
+
// =============================================================================
|
|
1113
|
+
// SECTION: FLUENT API
|
|
1114
|
+
// =============================================================================
|
|
1225
1115
|
|
|
1226
|
-
/**
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1116
|
+
/**
|
|
1117
|
+
* Fluent middleware composer for building complex middleware chains.
|
|
1118
|
+
* Provides excellent TypeScript inference and IntelliSense.
|
|
1119
|
+
*/
|
|
1120
|
+
class MiddlewareChainBuilder<M extends BaseMachine<any>> {
|
|
1121
|
+
constructor(private machine: M) { }
|
|
1231
1122
|
|
|
1232
|
-
return {
|
|
1233
1123
|
/**
|
|
1234
|
-
*
|
|
1124
|
+
* Add a middleware to the composition chain.
|
|
1125
|
+
* @param middleware - The middleware function to add
|
|
1126
|
+
* @returns A new composer with the middleware applied
|
|
1235
1127
|
*/
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
if (registry.has(name)) {
|
|
1243
|
-
throw new Error(`Middleware '${name}' is already registered`);
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
registry.set(name, { name, middleware, description, priority });
|
|
1247
|
-
return this;
|
|
1248
|
-
},
|
|
1128
|
+
with<M2 extends MiddlewareFn<any, any>>(
|
|
1129
|
+
middleware: M2
|
|
1130
|
+
): MiddlewareChainBuilder<ReturnType<M2> extends BaseMachine<any> ? ReturnType<M2> : M> {
|
|
1131
|
+
const result = middleware(this.machine);
|
|
1132
|
+
return new MiddlewareChainBuilder(result as any);
|
|
1133
|
+
}
|
|
1249
1134
|
|
|
1250
1135
|
/**
|
|
1251
|
-
*
|
|
1136
|
+
* Build the final machine with all middlewares applied.
|
|
1252
1137
|
*/
|
|
1253
|
-
|
|
1254
|
-
return
|
|
1255
|
-
}
|
|
1138
|
+
build(): M {
|
|
1139
|
+
return this.machine;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1256
1142
|
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1143
|
+
/**
|
|
1144
|
+
* Create a fluent middleware chain builder.
|
|
1145
|
+
*
|
|
1146
|
+
* @example
|
|
1147
|
+
* ```typescript
|
|
1148
|
+
* const enhanced = chain(counter)
|
|
1149
|
+
* .with(withHistory())
|
|
1150
|
+
* .with(withSnapshot())
|
|
1151
|
+
* .with(withTimeTravel())
|
|
1152
|
+
* .build();
|
|
1153
|
+
* ```
|
|
1154
|
+
*/
|
|
1155
|
+
export function chain<M extends BaseMachine<any>>(machine: M) {
|
|
1156
|
+
return new MiddlewareChainBuilder(machine);
|
|
1157
|
+
}
|
|
1263
1158
|
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
get(name: string): NamedMiddleware<M> | undefined {
|
|
1268
|
-
return registry.get(name);
|
|
1269
|
-
},
|
|
1159
|
+
// =============================================================================
|
|
1160
|
+
// SECTION: CONDITIONAL MIDDLEWARE
|
|
1161
|
+
// =============================================================================
|
|
1270
1162
|
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1163
|
+
/**
|
|
1164
|
+
* Create a conditional middleware that only applies when a predicate is true.
|
|
1165
|
+
*
|
|
1166
|
+
* @template M - The machine type
|
|
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
|
|
1170
|
+
*/
|
|
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
|
+
};
|
|
1178
|
+
|
|
1179
|
+
conditional.middleware = middleware;
|
|
1180
|
+
conditional.when = predicate;
|
|
1181
|
+
|
|
1182
|
+
return conditional;
|
|
1183
|
+
}
|
|
1277
1184
|
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1185
|
+
/**
|
|
1186
|
+
* Create a middleware that only applies in development mode.
|
|
1187
|
+
*
|
|
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
|
+
}
|
|
1292
1203
|
|
|
1293
|
-
|
|
1294
|
-
|
|
1204
|
+
/**
|
|
1205
|
+
* Create a middleware that only applies when a context property matches a value.
|
|
1206
|
+
*
|
|
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
|
+
}
|
|
1295
1221
|
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
applyAll(machine: M): M {
|
|
1300
|
-
const middlewares = this.list();
|
|
1301
|
-
return composeTyped(machine, ...middlewares.map(m => m.middleware));
|
|
1302
|
-
}
|
|
1303
|
-
};
|
|
1304
|
-
}
|
|
1222
|
+
// =============================================================================
|
|
1223
|
+
// SECTION: MIDDLEWARE REGISTRY
|
|
1224
|
+
// =============================================================================
|
|
1305
1225
|
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1226
|
+
/**
|
|
1227
|
+
* Create a middleware registry for managing reusable middleware configurations.
|
|
1228
|
+
*/
|
|
1229
|
+
export function createMiddlewareRegistry<M extends BaseMachine<any>>() {
|
|
1230
|
+
const registry = new Map<string, NamedMiddleware<M>>();
|
|
1231
|
+
|
|
1232
|
+
return {
|
|
1233
|
+
/**
|
|
1234
|
+
* Register a middleware by name.
|
|
1235
|
+
*/
|
|
1236
|
+
register(
|
|
1237
|
+
name: string,
|
|
1238
|
+
middleware: MiddlewareFn<M>,
|
|
1239
|
+
description?: string,
|
|
1240
|
+
priority?: number
|
|
1241
|
+
): typeof this {
|
|
1242
|
+
if (registry.has(name)) {
|
|
1243
|
+
throw new Error(`Middleware '${name}' is already registered`);
|
|
1244
|
+
}
|
|
1309
1245
|
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1246
|
+
registry.set(name, { name, middleware, description, priority });
|
|
1247
|
+
return this;
|
|
1248
|
+
},
|
|
1249
|
+
|
|
1250
|
+
/**
|
|
1251
|
+
* Unregister a middleware by name.
|
|
1252
|
+
*/
|
|
1253
|
+
unregister(name: string): boolean {
|
|
1254
|
+
return registry.delete(name);
|
|
1255
|
+
},
|
|
1256
|
+
|
|
1257
|
+
/**
|
|
1258
|
+
* Check if a middleware is registered.
|
|
1259
|
+
*/
|
|
1260
|
+
has(name: string): boolean {
|
|
1261
|
+
return registry.has(name);
|
|
1262
|
+
},
|
|
1263
|
+
|
|
1264
|
+
/**
|
|
1265
|
+
* Get a registered middleware by name.
|
|
1266
|
+
*/
|
|
1267
|
+
get(name: string): NamedMiddleware<M> | undefined {
|
|
1268
|
+
return registry.get(name);
|
|
1269
|
+
},
|
|
1270
|
+
|
|
1271
|
+
/**
|
|
1272
|
+
* List all registered middlewares.
|
|
1273
|
+
*/
|
|
1274
|
+
list(): NamedMiddleware<M>[] {
|
|
1275
|
+
return Array.from(registry.values()).sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
|
|
1276
|
+
},
|
|
1277
|
+
|
|
1278
|
+
/**
|
|
1279
|
+
* Apply a selection of registered middlewares to a machine.
|
|
1280
|
+
* Middlewares are applied in priority order (lowest to highest).
|
|
1281
|
+
*/
|
|
1282
|
+
apply(machine: M, middlewareNames: string[]): M {
|
|
1283
|
+
const middlewares = middlewareNames
|
|
1284
|
+
.map(name => {
|
|
1285
|
+
const entry = registry.get(name);
|
|
1286
|
+
if (!entry) {
|
|
1287
|
+
throw new Error(`Middleware '${name}' is not registered`);
|
|
1288
|
+
}
|
|
1289
|
+
return entry;
|
|
1290
|
+
})
|
|
1291
|
+
.sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
|
|
1292
|
+
|
|
1293
|
+
return composeTyped(machine, ...middlewares.map(m => m.middleware));
|
|
1294
|
+
},
|
|
1295
|
+
|
|
1296
|
+
/**
|
|
1297
|
+
* Apply all registered middlewares to a machine in priority order.
|
|
1298
|
+
*/
|
|
1299
|
+
applyAll(machine: M): M {
|
|
1300
|
+
const middlewares = this.list();
|
|
1301
|
+
return composeTyped(machine, ...middlewares.map(m => m.middleware));
|
|
1302
|
+
}
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
// =============================================================================
|
|
1307
|
+
// SECTION: PIPELINES
|
|
1308
|
+
// =============================================================================
|
|
1309
|
+
|
|
1310
|
+
/**
|
|
1311
|
+
* Create a middleware pipeline with error handling and conditional execution.
|
|
1312
|
+
*
|
|
1313
|
+
* @template M - The machine type
|
|
1314
|
+
* @param config - Pipeline configuration
|
|
1315
|
+
* @returns A function that executes middlewares in a pipeline
|
|
1316
|
+
*/
|
|
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;
|
|
1330
|
+
|
|
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;
|
|
1335
|
+
|
|
1336
|
+
for (let i = 0; i < middlewares.length; i++) {
|
|
1337
|
+
const middleware = middlewares[i];
|
|
1338
|
+
|
|
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;
|
|
1344
1354
|
}
|
|
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
|
-
}
|
|
1355
1355
|
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1356
|
+
errors.push({
|
|
1357
|
+
error: error as Error,
|
|
1358
|
+
middlewareIndex: i,
|
|
1359
|
+
middlewareName: (middleware as any).name
|
|
1360
|
+
});
|
|
1361
1361
|
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1362
|
+
if (logErrors) {
|
|
1363
|
+
console.error(`Pipeline middleware error at index ${i}:`, error);
|
|
1364
|
+
}
|
|
1365
1365
|
|
|
1366
|
-
|
|
1366
|
+
onError?.(error as Error, i, (middleware as any).name);
|
|
1367
|
+
}
|
|
1367
1368
|
}
|
|
1368
|
-
}
|
|
1369
1369
|
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
}
|
|
1370
|
+
return { machine: currentMachine, errors, success };
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
1373
|
|
|
1374
|
-
// =============================================================================
|
|
1375
|
-
// SECTION: UTILITY FUNCTIONS
|
|
1376
|
-
// =============================================================================
|
|
1374
|
+
// =============================================================================
|
|
1375
|
+
// SECTION: UTILITY FUNCTIONS
|
|
1376
|
+
// =============================================================================
|
|
1377
1377
|
|
|
1378
|
-
/**
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
export function combine<M extends BaseMachine<any>>(
|
|
1382
|
-
|
|
1383
|
-
): MiddlewareFn<M> {
|
|
1384
|
-
|
|
1385
|
-
}
|
|
1378
|
+
/**
|
|
1379
|
+
* Combine multiple middlewares with short-circuiting.
|
|
1380
|
+
*/
|
|
1381
|
+
export function combine<M extends BaseMachine<any>>(
|
|
1382
|
+
...middlewares: Array<MiddlewareFn<M>>
|
|
1383
|
+
): MiddlewareFn<M> {
|
|
1384
|
+
return (machine: M) => composeTyped(machine, ...middlewares);
|
|
1385
|
+
}
|
|
1386
1386
|
|
|
1387
|
-
/**
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
export function branch<M extends BaseMachine<any>>(
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
): MiddlewareFn<M> {
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1387
|
+
/**
|
|
1388
|
+
* Create a middleware that applies different middlewares based on context.
|
|
1389
|
+
*/
|
|
1390
|
+
export function branch<M extends BaseMachine<any>>(
|
|
1391
|
+
branches: Array<[predicate: (machine: M) => boolean, middleware: MiddlewareFn<M>]>,
|
|
1392
|
+
fallback?: MiddlewareFn<M>
|
|
1393
|
+
): MiddlewareFn<M> {
|
|
1394
|
+
return (machine: M) => {
|
|
1395
|
+
for (const [predicate, middleware] of branches) {
|
|
1396
|
+
if (predicate(machine)) {
|
|
1397
|
+
return middleware(machine);
|
|
1398
|
+
}
|
|
1398
1399
|
}
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
}
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
// =============================================================================
|
|
1405
|
-
// SECTION: TYPE GUARDS
|
|
1406
|
-
// =============================================================================
|
|
1400
|
+
return fallback ? fallback(machine) : machine;
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1407
1403
|
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
export function isMiddlewareFn<M extends BaseMachine<any>>(
|
|
1412
|
-
value: any
|
|
1413
|
-
): value is MiddlewareFn<M> {
|
|
1414
|
-
return typeof value === 'function' && value.length === 1;
|
|
1415
|
-
}
|
|
1404
|
+
// =============================================================================
|
|
1405
|
+
// SECTION: TYPE GUARDS
|
|
1406
|
+
// =============================================================================
|
|
1416
1407
|
|
|
1417
|
-
/**
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
export function
|
|
1421
|
-
|
|
1422
|
-
): value is
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
'middleware' in value &&
|
|
1426
|
-
'when' in value &&
|
|
1427
|
-
isMiddlewareFn(value.middleware) &&
|
|
1428
|
-
typeof value.when === 'function'
|
|
1429
|
-
);
|
|
1430
|
-
}
|
|
1408
|
+
/**
|
|
1409
|
+
* Type guard to check if a value is a middleware function.
|
|
1410
|
+
*/
|
|
1411
|
+
export function isMiddlewareFn<M extends BaseMachine<any>>(
|
|
1412
|
+
value: any
|
|
1413
|
+
): value is MiddlewareFn<M> {
|
|
1414
|
+
return typeof value === 'function' && value.length === 1;
|
|
1415
|
+
}
|
|
1431
1416
|
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1417
|
+
/**
|
|
1418
|
+
* Type guard to check if a value is a conditional middleware.
|
|
1419
|
+
*/
|
|
1420
|
+
export function isConditionalMiddleware<M extends BaseMachine<any>>(
|
|
1421
|
+
value: any
|
|
1422
|
+
): value is ConditionalMiddleware<M> {
|
|
1423
|
+
return (
|
|
1424
|
+
value !== null &&
|
|
1425
|
+
'middleware' in value &&
|
|
1426
|
+
'when' in value &&
|
|
1427
|
+
isMiddlewareFn(value.middleware) &&
|
|
1428
|
+
typeof value.when === 'function'
|
|
1429
|
+
);
|
|
1430
|
+
}
|
|
1435
1431
|
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
export type WithDebugging<M extends BaseMachine<any>> = WithTimeTravel<WithSnapshot<WithHistory<M>>>;
|
|
1432
|
+
// =============================================================================
|
|
1433
|
+
// SECTION: COMMON COMBINATIONS
|
|
1434
|
+
// =============================================================================
|
|
1440
1435
|
|
|
1441
|
-
/**
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
export
|
|
1445
|
-
|
|
1446
|
-
|
|
1436
|
+
/**
|
|
1437
|
+
* Common middleware combination types for better DX.
|
|
1438
|
+
*/
|
|
1439
|
+
export type WithDebugging<M extends BaseMachine<any>> = WithTimeTravel<WithSnapshot<WithHistory<M>>>;
|
|
1440
|
+
/**
|
|
1441
|
+
* Convenience function for the most common debugging middleware stack.
|
|
1442
|
+
*/
|
|
1443
|
+
export function withDebugging<M extends BaseMachine<any>>(machine: M): WithDebugging<M> {
|
|
1444
|
+
return withTimeTravel(withSnapshot(withHistory(machine)));
|
|
1445
|
+
}
|