@doeixd/machine 0.0.7 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +130 -272
- package/dist/cjs/development/index.js +1121 -18
- package/dist/cjs/development/index.js.map +4 -4
- package/dist/cjs/production/index.js +5 -5
- package/dist/esm/development/index.js +1121 -18
- package/dist/esm/development/index.js.map +4 -4
- package/dist/esm/production/index.js +5 -5
- package/dist/types/extract.d.ts.map +1 -1
- package/dist/types/index.d.ts +64 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/middleware.d.ts +1048 -0
- package/dist/types/middleware.d.ts.map +1 -0
- package/dist/types/primitives.d.ts +205 -3
- package/dist/types/primitives.d.ts.map +1 -1
- package/dist/types/runtime-extract.d.ts.map +1 -1
- package/dist/types/utils.d.ts +111 -6
- package/dist/types/utils.d.ts.map +1 -1
- package/package.json +10 -7
- package/scripts/extract-statechart.ts +351 -0
- package/src/adapters.ts +407 -0
- package/src/extract.ts +60 -20
- package/src/index.ts +201 -8
- package/src/middleware.ts +2325 -0
- package/src/primitives.ts +454 -3
- package/src/runtime-extract.ts +15 -0
- package/src/utils.ts +221 -6
package/src/primitives.ts
CHANGED
|
@@ -15,6 +15,14 @@
|
|
|
15
15
|
// SECTION: CORE METADATA TYPES
|
|
16
16
|
// =============================================================================
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Options passed to async transition functions, including cancellation support.
|
|
20
|
+
*/
|
|
21
|
+
export interface TransitionOptions {
|
|
22
|
+
/** AbortSignal for cancelling long-running async operations. */
|
|
23
|
+
signal: AbortSignal;
|
|
24
|
+
}
|
|
25
|
+
|
|
18
26
|
/**
|
|
19
27
|
* A unique symbol used to "brand" a type with metadata.
|
|
20
28
|
* This key allows the static analyzer to find the metadata within a complex type signature.
|
|
@@ -23,11 +31,20 @@ export const META_KEY = Symbol("MachineMeta");
|
|
|
23
31
|
|
|
24
32
|
/**
|
|
25
33
|
* Runtime metadata symbol.
|
|
34
|
+
/**
|
|
26
35
|
* Non-enumerable property key for storing metadata on function objects at runtime.
|
|
27
36
|
* @internal
|
|
28
37
|
*/
|
|
29
38
|
export const RUNTIME_META = Symbol('__machine_runtime_meta__');
|
|
30
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Local definition of Machine type to avoid circular imports.
|
|
42
|
+
* @internal
|
|
43
|
+
*/
|
|
44
|
+
type Machine<C extends object> = {
|
|
45
|
+
readonly context: C;
|
|
46
|
+
} & Record<string, (...args: any[]) => Machine<any>>;
|
|
47
|
+
|
|
31
48
|
/**
|
|
32
49
|
* Helper type representing a Class Constructor.
|
|
33
50
|
* Used to reference target states by their class definition rather than magic strings.
|
|
@@ -211,6 +228,7 @@ export function describe<
|
|
|
211
228
|
* Annotates a transition with a Guard condition.
|
|
212
229
|
* Note: This only adds metadata. You must still implement the `if` check inside your function.
|
|
213
230
|
*
|
|
231
|
+
* @deprecated Use the runtime `guard()` primitive instead. Its `options.description` is used for static analysis.
|
|
214
232
|
* @param guard - Object containing the name and optional description of the guard.
|
|
215
233
|
* @param transition - The transition function to guard.
|
|
216
234
|
* @example
|
|
@@ -236,17 +254,20 @@ export function guarded<
|
|
|
236
254
|
* Annotates a transition with an Invoked Service (asynchronous effect).
|
|
237
255
|
*
|
|
238
256
|
* @param service - configuration for the service (source, onDone target, onError target).
|
|
239
|
-
* @param implementation - The async function implementation.
|
|
257
|
+
* @param implementation - The async function implementation that receives an AbortSignal.
|
|
240
258
|
* @example
|
|
241
259
|
* load = invoke(
|
|
242
260
|
* { src: "fetchData", onDone: LoadedMachine, onError: ErrorMachine },
|
|
243
|
-
* async () => {
|
|
261
|
+
* async ({ signal }) => {
|
|
262
|
+
* const response = await fetch('/api/data', { signal });
|
|
263
|
+
* return new LoadedMachine({ data: await response.json() });
|
|
264
|
+
* }
|
|
244
265
|
* );
|
|
245
266
|
*/
|
|
246
267
|
export function invoke<
|
|
247
268
|
D extends ClassConstructor,
|
|
248
269
|
E extends ClassConstructor,
|
|
249
|
-
F extends (
|
|
270
|
+
F extends (options: { signal: AbortSignal }) => any
|
|
250
271
|
>(
|
|
251
272
|
service: { src: string; onDone: D; onError: E; description?: string },
|
|
252
273
|
implementation: F
|
|
@@ -289,6 +310,436 @@ export function action<
|
|
|
289
310
|
return transition as any;
|
|
290
311
|
}
|
|
291
312
|
|
|
313
|
+
// =============================================================================
|
|
314
|
+
// SECTION: RUNTIME GUARDS
|
|
315
|
+
// =============================================================================
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Configuration options for guard behavior when conditions fail.
|
|
319
|
+
*/
|
|
320
|
+
export interface GuardOptions<C extends object = any, TFailure extends Machine<any> = Machine<C>> {
|
|
321
|
+
/** What to do when guard fails */
|
|
322
|
+
onFail?: 'throw' | 'ignore' | GuardFallback<C, TFailure>;
|
|
323
|
+
|
|
324
|
+
/** Custom error message for 'throw' mode */
|
|
325
|
+
errorMessage?: string;
|
|
326
|
+
|
|
327
|
+
/** Additional metadata for statechart extraction */
|
|
328
|
+
description?: string;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* A fallback machine or function that returns a machine when guard fails.
|
|
333
|
+
*/
|
|
334
|
+
export type GuardFallback<C extends object, TFailure extends Machine<any> = Machine<C>> =
|
|
335
|
+
| ((this: Machine<C>, ...args: any[]) => TFailure)
|
|
336
|
+
| TFailure;
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* A guarded transition that checks conditions at runtime before executing.
|
|
340
|
+
* Can be called with either machine or context as 'this' binding.
|
|
341
|
+
*/
|
|
342
|
+
export type GuardedTransition<
|
|
343
|
+
C extends object,
|
|
344
|
+
TSuccess extends Machine<any>,
|
|
345
|
+
TFailure extends Machine<any> = Machine<C>
|
|
346
|
+
> = {
|
|
347
|
+
(...args: any[]): TSuccess | TFailure | Promise<TSuccess | TFailure>;
|
|
348
|
+
readonly __guard: true;
|
|
349
|
+
readonly condition: (ctx: C, ...args: any[]) => boolean | Promise<boolean>;
|
|
350
|
+
readonly transition: (...args: any[]) => TSuccess;
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Creates a synchronous runtime guard that checks conditions before executing transitions.
|
|
355
|
+
* This provides actual runtime protection with synchronous execution - use this for the majority of cases.
|
|
356
|
+
*
|
|
357
|
+
* @template C - The context type
|
|
358
|
+
* @template TSuccess - The transition return type when condition passes
|
|
359
|
+
* @template TFailure - The fallback return type when condition fails (defaults to Machine<C>)
|
|
360
|
+
* @param condition - Synchronous function that returns true if transition should proceed
|
|
361
|
+
* @param transition - The transition function to execute if condition passes
|
|
362
|
+
* @param options - Configuration for guard failure behavior
|
|
363
|
+
* @returns A synchronous guarded transition function
|
|
364
|
+
*
|
|
365
|
+
* @example
|
|
366
|
+
* ```typescript
|
|
367
|
+
* const machine = createMachine({ balance: 100 }, {
|
|
368
|
+
* withdraw: guard(
|
|
369
|
+
* (ctx, amount) => ctx.balance >= amount,
|
|
370
|
+
* function(amount: number) {
|
|
371
|
+
* return createMachine({ balance: this.balance - amount }, this);
|
|
372
|
+
* },
|
|
373
|
+
* { onFail: 'throw', errorMessage: 'Insufficient funds' }
|
|
374
|
+
* )
|
|
375
|
+
* });
|
|
376
|
+
*
|
|
377
|
+
* machine.withdraw(50); // ✅ Works synchronously
|
|
378
|
+
* machine.withdraw(200); // ❌ Throws "Insufficient funds"
|
|
379
|
+
* ```
|
|
380
|
+
*/
|
|
381
|
+
export function guard<
|
|
382
|
+
C extends object,
|
|
383
|
+
TSuccess extends Machine<any>,
|
|
384
|
+
TFailure extends Machine<any> = Machine<C>
|
|
385
|
+
>(
|
|
386
|
+
condition: (ctx: C, ...args: any[]) => boolean,
|
|
387
|
+
transition: (...args: any[]) => TSuccess,
|
|
388
|
+
options: GuardOptions<C, TFailure> = {}
|
|
389
|
+
): (...args: any[]) => TSuccess | TFailure {
|
|
390
|
+
const { onFail = 'throw', errorMessage, description } = options;
|
|
391
|
+
|
|
392
|
+
// Merge defaults into options for the metadata
|
|
393
|
+
const fullOptions = { ...options, onFail, errorMessage, description };
|
|
394
|
+
|
|
395
|
+
// Create the guarded transition function (synchronous)
|
|
396
|
+
const guardedTransition = function(this: C | Machine<C>, ...args: any[]): TSuccess | TFailure {
|
|
397
|
+
// Detect if 'this' is a machine or just context
|
|
398
|
+
const isMachine = typeof this === 'object' && 'context' in this;
|
|
399
|
+
const ctx = isMachine ? (this as Machine<C>).context : (this as C);
|
|
400
|
+
|
|
401
|
+
// Evaluate the condition (synchronously)
|
|
402
|
+
const conditionResult = condition(ctx, ...args);
|
|
403
|
+
|
|
404
|
+
if (conditionResult) {
|
|
405
|
+
// Condition passed, execute the transition
|
|
406
|
+
// Transition functions expect 'this' to be the context
|
|
407
|
+
const contextForTransition = isMachine ? (this as Machine<C>).context : (this as C);
|
|
408
|
+
return transition.apply(contextForTransition, args);
|
|
409
|
+
} else {
|
|
410
|
+
// Condition failed, handle according to options
|
|
411
|
+
if (onFail === 'throw') {
|
|
412
|
+
const message = errorMessage || 'Guard condition failed';
|
|
413
|
+
throw new Error(message);
|
|
414
|
+
} else if (onFail === 'ignore') {
|
|
415
|
+
if (isMachine) {
|
|
416
|
+
// Return the current machine unchanged
|
|
417
|
+
return this as TSuccess | TFailure;
|
|
418
|
+
} else {
|
|
419
|
+
// Cannot ignore when called with context binding
|
|
420
|
+
throw new Error('Cannot use "ignore" mode with context-only binding. Use full machine binding or provide fallback.');
|
|
421
|
+
}
|
|
422
|
+
} else if (typeof onFail === 'function') {
|
|
423
|
+
// Custom fallback function - call with machine as 'this'
|
|
424
|
+
if (isMachine) {
|
|
425
|
+
return onFail.apply(this as Machine<C>, args) as TSuccess | TFailure;
|
|
426
|
+
} else {
|
|
427
|
+
throw new Error('Cannot use function fallback with context-only binding. Use full machine binding.');
|
|
428
|
+
}
|
|
429
|
+
} else {
|
|
430
|
+
// Static fallback machine
|
|
431
|
+
return onFail as TSuccess | TFailure;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
// Attach metadata for type branding and statechart extraction
|
|
437
|
+
Object.defineProperty(guardedTransition, '__guard', { value: true, enumerable: false });
|
|
438
|
+
Object.defineProperty(guardedTransition, 'condition', { value: condition, enumerable: false });
|
|
439
|
+
Object.defineProperty(guardedTransition, 'transition', { value: transition, enumerable: false });
|
|
440
|
+
Object.defineProperty(guardedTransition, 'options', { value: fullOptions, enumerable: false });
|
|
441
|
+
|
|
442
|
+
// Attach runtime metadata for statechart extraction
|
|
443
|
+
attachRuntimeMeta(guardedTransition, {
|
|
444
|
+
description: description || 'Synchronous guarded transition',
|
|
445
|
+
guards: [{ name: 'runtime_guard', description: description || 'Synchronous condition check' }]
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
return guardedTransition;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Creates a runtime guard that checks conditions before executing transitions.
|
|
453
|
+
* This provides actual runtime protection, unlike the `guarded` primitive which only adds metadata.
|
|
454
|
+
* Use this when your condition or transition logic is asynchronous.
|
|
455
|
+
*
|
|
456
|
+
* @template C - The context type
|
|
457
|
+
* @template TSuccess - The transition return type when condition passes
|
|
458
|
+
* @template TFailure - The fallback return type when condition fails (defaults to Machine<C>)
|
|
459
|
+
* @param condition - Function that returns true if transition should proceed (can be async)
|
|
460
|
+
* @param transition - The transition function to execute if condition passes
|
|
461
|
+
* @param options - Configuration for guard failure behavior
|
|
462
|
+
* @returns A guarded transition function that returns a Promise
|
|
463
|
+
*
|
|
464
|
+
* @example
|
|
465
|
+
* ```typescript
|
|
466
|
+
* const machine = createMachine({ balance: 100 }, {
|
|
467
|
+
* withdraw: guardAsync(
|
|
468
|
+
* async (ctx, amount) => {
|
|
469
|
+
* // Simulate API call to check balance
|
|
470
|
+
* await new Promise(resolve => setTimeout(resolve, 100));
|
|
471
|
+
* return ctx.balance >= amount;
|
|
472
|
+
* },
|
|
473
|
+
* async function(amount: number) {
|
|
474
|
+
* // Simulate API call to process withdrawal
|
|
475
|
+
* await new Promise(resolve => setTimeout(resolve, 100));
|
|
476
|
+
* return createMachine({ balance: this.balance - amount }, this);
|
|
477
|
+
* },
|
|
478
|
+
* { onFail: 'throw', errorMessage: 'Insufficient funds' }
|
|
479
|
+
* )
|
|
480
|
+
* });
|
|
481
|
+
*
|
|
482
|
+
* await machine.withdraw(50); // ✅ Works
|
|
483
|
+
* await machine.withdraw(200); // ❌ Throws "Insufficient funds"
|
|
484
|
+
* ```
|
|
485
|
+
*/
|
|
486
|
+
export function guardAsync<
|
|
487
|
+
C extends object,
|
|
488
|
+
TSuccess extends Machine<any>,
|
|
489
|
+
TFailure extends Machine<any> = Machine<C>
|
|
490
|
+
>(
|
|
491
|
+
condition: (ctx: C, ...args: any[]) => boolean | Promise<boolean>,
|
|
492
|
+
transition: (...args: any[]) => TSuccess,
|
|
493
|
+
options: GuardOptions<C, TFailure> = {}
|
|
494
|
+
): GuardedTransition<C, TSuccess, TFailure> {
|
|
495
|
+
const { onFail = 'throw', errorMessage, description } = options;
|
|
496
|
+
|
|
497
|
+
// Merge defaults into options for the metadata
|
|
498
|
+
const fullOptions = { ...options, onFail, errorMessage, description };
|
|
499
|
+
|
|
500
|
+
// Create the guarded transition function
|
|
501
|
+
const guardedTransition = async function(this: C | Machine<C>, ...args: any[]): Promise<TSuccess | TFailure> {
|
|
502
|
+
// Detect if 'this' is a machine or just context
|
|
503
|
+
const isMachine = typeof this === 'object' && 'context' in this;
|
|
504
|
+
const ctx = isMachine ? (this as Machine<C>).context : (this as C);
|
|
505
|
+
|
|
506
|
+
// Evaluate the condition
|
|
507
|
+
const conditionResult = await Promise.resolve(condition(ctx, ...args));
|
|
508
|
+
|
|
509
|
+
if (conditionResult) {
|
|
510
|
+
// Condition passed, execute the transition
|
|
511
|
+
// Transition functions expect 'this' to be the context
|
|
512
|
+
const contextForTransition = isMachine ? (this as Machine<C>).context : (this as C);
|
|
513
|
+
return transition.apply(contextForTransition, args);
|
|
514
|
+
} else {
|
|
515
|
+
// Condition failed, handle according to options
|
|
516
|
+
if (onFail === 'throw') {
|
|
517
|
+
const message = errorMessage || 'Guard condition failed';
|
|
518
|
+
throw new Error(message);
|
|
519
|
+
} else if (onFail === 'ignore') {
|
|
520
|
+
if (isMachine) {
|
|
521
|
+
// Return the current machine unchanged
|
|
522
|
+
return this as TSuccess | TFailure;
|
|
523
|
+
} else {
|
|
524
|
+
// Cannot ignore when called with context binding
|
|
525
|
+
throw new Error('Cannot use "ignore" mode with context-only binding. Use full machine binding or provide fallback.');
|
|
526
|
+
}
|
|
527
|
+
} else if (typeof onFail === 'function') {
|
|
528
|
+
// Custom fallback function - call with machine as 'this'
|
|
529
|
+
if (isMachine) {
|
|
530
|
+
return onFail.apply(this as Machine<C>, args) as TSuccess | TFailure;
|
|
531
|
+
} else {
|
|
532
|
+
throw new Error('Cannot use function fallback with context-only binding. Use full machine binding.');
|
|
533
|
+
}
|
|
534
|
+
} else {
|
|
535
|
+
// Static fallback machine
|
|
536
|
+
return onFail as TSuccess | TFailure;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
// Attach metadata for type branding and statechart extraction
|
|
542
|
+
Object.defineProperty(guardedTransition, '__guard', { value: true, enumerable: false });
|
|
543
|
+
Object.defineProperty(guardedTransition, 'condition', { value: condition, enumerable: false });
|
|
544
|
+
Object.defineProperty(guardedTransition, 'transition', { value: transition, enumerable: false });
|
|
545
|
+
Object.defineProperty(guardedTransition, 'options', { value: fullOptions, enumerable: false });
|
|
546
|
+
|
|
547
|
+
// Attach runtime metadata for statechart extraction
|
|
548
|
+
attachRuntimeMeta(guardedTransition, {
|
|
549
|
+
description: description || 'Runtime guarded transition',
|
|
550
|
+
guards: [{ name: 'runtime_guard', description: description || 'Runtime condition check' }]
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
return guardedTransition as GuardedTransition<C, TSuccess, TFailure>;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Creates a synchronous guard that checks conditions before executing transitions.
|
|
558
|
+
* This is the synchronous counterpart to `guard()` - use this when your machine
|
|
559
|
+
* doesn't need async transitions to avoid unnecessary Promise overhead.
|
|
560
|
+
*
|
|
561
|
+
* @template C - The context type
|
|
562
|
+
* @template T - The transition return type
|
|
563
|
+
* @param condition - Function that returns true if transition should proceed (must be synchronous)
|
|
564
|
+
* @param transition - The transition function to execute if condition passes (must be synchronous)
|
|
565
|
+
* @param options - Configuration for guard failure behavior
|
|
566
|
+
* @returns A synchronous guarded transition function
|
|
567
|
+
*
|
|
568
|
+
* @example
|
|
569
|
+
* ```typescript
|
|
570
|
+
* const machine = createMachine({ balance: 100 }, {
|
|
571
|
+
* withdraw: guardSync(
|
|
572
|
+
* (ctx, amount) => ctx.balance >= amount,
|
|
573
|
+
* function(amount: number) {
|
|
574
|
+
* return createMachine({ balance: this.balance - amount }, this);
|
|
575
|
+
* },
|
|
576
|
+
* { onFail: 'throw', errorMessage: 'Insufficient funds' }
|
|
577
|
+
* )
|
|
578
|
+
* });
|
|
579
|
+
*
|
|
580
|
+
* machine.withdraw(50); // ✅ Works synchronously
|
|
581
|
+
* machine.withdraw(200); // ❌ Throws "Insufficient funds"
|
|
582
|
+
* ```
|
|
583
|
+
*/
|
|
584
|
+
export function guardSync<
|
|
585
|
+
C extends object,
|
|
586
|
+
TSuccess extends Machine<any>,
|
|
587
|
+
TFailure extends Machine<any> = Machine<C>
|
|
588
|
+
>(
|
|
589
|
+
condition: (ctx: C, ...args: any[]) => boolean,
|
|
590
|
+
transition: (...args: any[]) => TSuccess,
|
|
591
|
+
options: GuardOptions<C, TFailure> = {}
|
|
592
|
+
): GuardedTransition<C, TSuccess, TFailure> {
|
|
593
|
+
const { onFail = 'throw', errorMessage, description } = options;
|
|
594
|
+
|
|
595
|
+
// Merge defaults into options for the metadata
|
|
596
|
+
const fullOptions = { ...options, onFail, errorMessage, description };
|
|
597
|
+
|
|
598
|
+
// Create the guarded transition function (synchronous)
|
|
599
|
+
const guardedTransition = function(this: C | Machine<C>, ...args: any[]): TSuccess | TFailure {
|
|
600
|
+
// Detect if 'this' is a machine or just context
|
|
601
|
+
const isMachine = typeof this === 'object' && 'context' in this;
|
|
602
|
+
const ctx = isMachine ? (this as Machine<C>).context : (this as C);
|
|
603
|
+
|
|
604
|
+
// Evaluate the condition (synchronously)
|
|
605
|
+
const conditionResult = condition(ctx, ...args);
|
|
606
|
+
|
|
607
|
+
if (conditionResult) {
|
|
608
|
+
// Condition passed, execute the transition
|
|
609
|
+
// Transition functions expect 'this' to be the context
|
|
610
|
+
const contextForTransition = isMachine ? (this as Machine<C>).context : (this as C);
|
|
611
|
+
return transition.apply(contextForTransition, args);
|
|
612
|
+
} else {
|
|
613
|
+
// Condition failed, handle according to options
|
|
614
|
+
if (onFail === 'throw') {
|
|
615
|
+
const message = errorMessage || 'Guard condition failed';
|
|
616
|
+
throw new Error(message);
|
|
617
|
+
} else if (onFail === 'ignore') {
|
|
618
|
+
if (isMachine) {
|
|
619
|
+
// Return the current machine unchanged
|
|
620
|
+
return this as TSuccess | TFailure;
|
|
621
|
+
} else {
|
|
622
|
+
// Cannot ignore when called with context binding
|
|
623
|
+
throw new Error('Cannot use "ignore" mode with context-only binding. Use full machine binding or provide fallback.');
|
|
624
|
+
}
|
|
625
|
+
} else if (typeof onFail === 'function') {
|
|
626
|
+
// Custom fallback function - call with machine as 'this'
|
|
627
|
+
if (isMachine) {
|
|
628
|
+
return onFail.apply(this as Machine<C>, args) as TSuccess | TFailure;
|
|
629
|
+
} else {
|
|
630
|
+
throw new Error('Cannot use function fallback with context-only binding. Use full machine binding.');
|
|
631
|
+
}
|
|
632
|
+
} else {
|
|
633
|
+
// Static fallback machine
|
|
634
|
+
return onFail as TSuccess | TFailure;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
// Attach metadata for type branding and statechart extraction
|
|
640
|
+
Object.defineProperty(guardedTransition, '__guard', { value: true, enumerable: false });
|
|
641
|
+
Object.defineProperty(guardedTransition, 'condition', { value: condition, enumerable: false });
|
|
642
|
+
Object.defineProperty(guardedTransition, 'transition', { value: transition, enumerable: false });
|
|
643
|
+
Object.defineProperty(guardedTransition, 'options', { value: fullOptions, enumerable: false });
|
|
644
|
+
|
|
645
|
+
// Attach runtime metadata for statechart extraction
|
|
646
|
+
attachRuntimeMeta(guardedTransition, {
|
|
647
|
+
description: description || 'Synchronous guarded transition',
|
|
648
|
+
guards: [{ name: 'runtime_guard_sync', description: description || 'Synchronous condition check' }]
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
return guardedTransition as GuardedTransition<C, TSuccess, TFailure>;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Fluent API for creating synchronous guarded transitions.
|
|
656
|
+
* Provides a more readable way to define conditional transitions with synchronous execution.
|
|
657
|
+
*
|
|
658
|
+
* @template C - The context type
|
|
659
|
+
* @param condition - Synchronous function that returns true if transition should proceed
|
|
660
|
+
* @returns A fluent interface for defining the guarded transition
|
|
661
|
+
*
|
|
662
|
+
* @example
|
|
663
|
+
* ```typescript
|
|
664
|
+
* const machine = createMachine({ isAdmin: false }, {
|
|
665
|
+
* deleteUser: whenGuard((ctx) => ctx.isAdmin)
|
|
666
|
+
* .do(function(userId: string) {
|
|
667
|
+
* return createMachine({ ...this.context, deleted: userId }, this);
|
|
668
|
+
* })
|
|
669
|
+
* .else(function() {
|
|
670
|
+
* return createMachine({ ...this.context, error: 'Unauthorized' }, this);
|
|
671
|
+
* })
|
|
672
|
+
* });
|
|
673
|
+
* ```
|
|
674
|
+
*/
|
|
675
|
+
export function whenGuard<C extends object>(
|
|
676
|
+
condition: (ctx: C, ...args: any[]) => boolean
|
|
677
|
+
) {
|
|
678
|
+
return {
|
|
679
|
+
/**
|
|
680
|
+
* Define the transition to execute when the condition passes.
|
|
681
|
+
* Returns a guarded transition that can optionally have an else clause.
|
|
682
|
+
*/
|
|
683
|
+
do<T extends Machine<any>>(transition: (...args: any[]) => T) {
|
|
684
|
+
const guarded = guard(condition, transition);
|
|
685
|
+
|
|
686
|
+
// Add fluent else method to the guarded transition
|
|
687
|
+
(guarded as any).else = function<F extends Machine<any>>(fallback: (...args: any[]) => F) {
|
|
688
|
+
return guard(condition, transition, { onFail: fallback });
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
return guarded;
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Fluent API for creating asynchronous guarded transitions.
|
|
698
|
+
* Provides a more readable way to define conditional transitions with async execution.
|
|
699
|
+
*
|
|
700
|
+
* @template C - The context type
|
|
701
|
+
* @param condition - Function that returns true if transition should proceed (can be async)
|
|
702
|
+
* @returns A fluent interface for defining the guarded transition
|
|
703
|
+
*
|
|
704
|
+
* @example
|
|
705
|
+
* ```typescript
|
|
706
|
+
* const machine = createMachine({ isAdmin: false }, {
|
|
707
|
+
* deleteUser: whenGuardAsync(async (ctx) => {
|
|
708
|
+
* // Simulate API call
|
|
709
|
+
* await checkPermissions(ctx.userId);
|
|
710
|
+
* return ctx.isAdmin;
|
|
711
|
+
* })
|
|
712
|
+
* .do(async function(userId: string) {
|
|
713
|
+
* await deleteUserFromDB(userId);
|
|
714
|
+
* return createMachine({ ...this.context, deleted: userId }, this);
|
|
715
|
+
* })
|
|
716
|
+
* .else(function() {
|
|
717
|
+
* return createMachine({ ...this.context, error: 'Unauthorized' }, this);
|
|
718
|
+
* })
|
|
719
|
+
* });
|
|
720
|
+
* ```
|
|
721
|
+
*/
|
|
722
|
+
export function whenGuardAsync<C extends object>(
|
|
723
|
+
condition: (ctx: C, ...args: any[]) => boolean | Promise<boolean>
|
|
724
|
+
) {
|
|
725
|
+
return {
|
|
726
|
+
/**
|
|
727
|
+
* Define the transition to execute when the condition passes.
|
|
728
|
+
* Returns a guarded transition that can optionally have an else clause.
|
|
729
|
+
*/
|
|
730
|
+
do<T extends Machine<any>>(transition: (...args: any[]) => T) {
|
|
731
|
+
const guarded = guardAsync(condition, transition);
|
|
732
|
+
|
|
733
|
+
// Add fluent else method to the guarded transition
|
|
734
|
+
(guarded as any).else = function<F extends Machine<any>>(fallback: (...args: any[]) => F) {
|
|
735
|
+
return guardAsync(condition, transition, { onFail: fallback });
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
return guarded;
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
|
|
292
743
|
/**
|
|
293
744
|
* Flexible metadata wrapper for functional and type-state patterns.
|
|
294
745
|
*
|
package/src/runtime-extract.ts
CHANGED
|
@@ -69,6 +69,21 @@ export function extractStateNode(stateInstance: any): any {
|
|
|
69
69
|
transition.actions = meta.actions.map(a => a.name);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
stateNode.on[key] = transition;
|
|
73
|
+
}
|
|
74
|
+
// If has guards but no target, it's a guarded transition (runtime guard)
|
|
75
|
+
else if (meta.guards && meta.guards.length > 0) {
|
|
76
|
+
// For runtime guards, we can't determine the target statically
|
|
77
|
+
// So we create a transition with a placeholder target and guard condition
|
|
78
|
+
const transition: any = {
|
|
79
|
+
target: 'GuardedTransition', // Placeholder - actual target determined at runtime
|
|
80
|
+
cond: meta.guards.map(g => g.name).join(' && ')
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
if (meta.description) {
|
|
84
|
+
transition.description = meta.description;
|
|
85
|
+
}
|
|
86
|
+
|
|
72
87
|
stateNode.on[key] = transition;
|
|
73
88
|
}
|
|
74
89
|
}
|