@doeixd/machine 0.0.7 → 0.0.8
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 +1001 -18
- package/dist/cjs/development/index.js.map +4 -4
- package/dist/cjs/production/index.js +5 -5
- package/dist/esm/development/index.js +1001 -18
- package/dist/esm/development/index.js.map +4 -4
- package/dist/esm/production/index.js +5 -5
- package/dist/types/index.d.ts +63 -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 +105 -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 +2 -2
- package/src/adapters.ts +407 -0
- package/src/extract.ts +1 -1
- package/src/index.ts +197 -8
- package/src/middleware.ts +2325 -0
- package/src/primitives.ts +194 -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.
|
|
@@ -236,17 +253,20 @@ export function guarded<
|
|
|
236
253
|
* Annotates a transition with an Invoked Service (asynchronous effect).
|
|
237
254
|
*
|
|
238
255
|
* @param service - configuration for the service (source, onDone target, onError target).
|
|
239
|
-
* @param implementation - The async function implementation.
|
|
256
|
+
* @param implementation - The async function implementation that receives an AbortSignal.
|
|
240
257
|
* @example
|
|
241
258
|
* load = invoke(
|
|
242
259
|
* { src: "fetchData", onDone: LoadedMachine, onError: ErrorMachine },
|
|
243
|
-
* async () => {
|
|
260
|
+
* async ({ signal }) => {
|
|
261
|
+
* const response = await fetch('/api/data', { signal });
|
|
262
|
+
* return new LoadedMachine({ data: await response.json() });
|
|
263
|
+
* }
|
|
244
264
|
* );
|
|
245
265
|
*/
|
|
246
266
|
export function invoke<
|
|
247
267
|
D extends ClassConstructor,
|
|
248
268
|
E extends ClassConstructor,
|
|
249
|
-
F extends (
|
|
269
|
+
F extends (options: { signal: AbortSignal }) => any
|
|
250
270
|
>(
|
|
251
271
|
service: { src: string; onDone: D; onError: E; description?: string },
|
|
252
272
|
implementation: F
|
|
@@ -289,6 +309,177 @@ export function action<
|
|
|
289
309
|
return transition as any;
|
|
290
310
|
}
|
|
291
311
|
|
|
312
|
+
// =============================================================================
|
|
313
|
+
// SECTION: RUNTIME GUARDS
|
|
314
|
+
// =============================================================================
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Configuration options for guard behavior when conditions fail.
|
|
318
|
+
*/
|
|
319
|
+
export interface GuardOptions<C extends object = any> {
|
|
320
|
+
/** What to do when guard fails */
|
|
321
|
+
onFail?: 'throw' | 'ignore' | GuardFallback<C>;
|
|
322
|
+
|
|
323
|
+
/** Custom error message for 'throw' mode */
|
|
324
|
+
errorMessage?: string;
|
|
325
|
+
|
|
326
|
+
/** Additional metadata for statechart extraction */
|
|
327
|
+
description?: string;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* A fallback machine or function that returns a machine when guard fails.
|
|
332
|
+
*/
|
|
333
|
+
export type GuardFallback<C extends object> =
|
|
334
|
+
| ((this: Machine<C>, ...args: any[]) => Machine<C>)
|
|
335
|
+
| Machine<C>;
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* A guarded transition that checks conditions at runtime before executing.
|
|
339
|
+
* Can be called with either machine or context as 'this' binding.
|
|
340
|
+
*/
|
|
341
|
+
export type GuardedTransition<C extends object, T extends Machine<any>> = {
|
|
342
|
+
(...args: any[]): T | Machine<C> | Promise<T | Machine<C>>;
|
|
343
|
+
readonly __guard: true;
|
|
344
|
+
readonly condition: (ctx: C, ...args: any[]) => boolean | Promise<boolean>;
|
|
345
|
+
readonly transition: (...args: any[]) => T;
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Creates a runtime guard that checks conditions before executing transitions.
|
|
350
|
+
* This provides actual runtime protection, unlike the `guarded` primitive which only adds metadata.
|
|
351
|
+
*
|
|
352
|
+
* @template C - The context type
|
|
353
|
+
* @template T - The transition return type
|
|
354
|
+
* @param condition - Function that returns true if transition should proceed
|
|
355
|
+
* @param transition - The transition function to execute if condition passes
|
|
356
|
+
* @param options - Configuration for guard failure behavior
|
|
357
|
+
* @returns A guarded transition function
|
|
358
|
+
*
|
|
359
|
+
* @example
|
|
360
|
+
* ```typescript
|
|
361
|
+
* const machine = createMachine({ balance: 100 }, {
|
|
362
|
+
* withdraw: guard(
|
|
363
|
+
* (ctx, amount) => ctx.balance >= amount,
|
|
364
|
+
* function(amount: number) {
|
|
365
|
+
* return createMachine({ balance: this.balance - amount }, this);
|
|
366
|
+
* },
|
|
367
|
+
* { onFail: 'throw', errorMessage: 'Insufficient funds' }
|
|
368
|
+
* )
|
|
369
|
+
* });
|
|
370
|
+
*
|
|
371
|
+
* machine.withdraw(50); // ✅ Works
|
|
372
|
+
* machine.withdraw(200); // ❌ Throws "Insufficient funds"
|
|
373
|
+
* ```
|
|
374
|
+
*/
|
|
375
|
+
export function guard<C extends object, T extends Machine<any>>(
|
|
376
|
+
condition: (ctx: C, ...args: any[]) => boolean | Promise<boolean>,
|
|
377
|
+
transition: (...args: any[]) => T,
|
|
378
|
+
options: GuardOptions<C> = {}
|
|
379
|
+
): GuardedTransition<C, T> {
|
|
380
|
+
const { onFail = 'throw', errorMessage, description } = options;
|
|
381
|
+
|
|
382
|
+
// Merge defaults into options for the metadata
|
|
383
|
+
const fullOptions = { ...options, onFail, errorMessage, description };
|
|
384
|
+
|
|
385
|
+
// Create the guarded transition function
|
|
386
|
+
const guardedTransition = async function(this: C | Machine<C>, ...args: any[]): Promise<T | Machine<C>> {
|
|
387
|
+
// Detect if 'this' is a machine or just context
|
|
388
|
+
const isMachine = typeof this === 'object' && 'context' in this;
|
|
389
|
+
const ctx = isMachine ? (this as Machine<C>).context : (this as C);
|
|
390
|
+
|
|
391
|
+
// Evaluate the condition
|
|
392
|
+
const conditionResult = await Promise.resolve(condition(ctx, ...args));
|
|
393
|
+
|
|
394
|
+
if (conditionResult) {
|
|
395
|
+
// Condition passed, execute the transition
|
|
396
|
+
// Transition functions expect 'this' to be the context
|
|
397
|
+
const contextForTransition = isMachine ? (this as Machine<C>).context : (this as C);
|
|
398
|
+
return transition.apply(contextForTransition, args);
|
|
399
|
+
} else {
|
|
400
|
+
// Condition failed, handle according to options
|
|
401
|
+
if (onFail === 'throw') {
|
|
402
|
+
const message = errorMessage || 'Guard condition failed';
|
|
403
|
+
throw new Error(message);
|
|
404
|
+
} else if (onFail === 'ignore') {
|
|
405
|
+
if (isMachine) {
|
|
406
|
+
// Return the current machine unchanged
|
|
407
|
+
return this as Machine<C>;
|
|
408
|
+
} else {
|
|
409
|
+
// Cannot ignore when called with context binding
|
|
410
|
+
throw new Error('Cannot use "ignore" mode with context-only binding. Use full machine binding or provide fallback.');
|
|
411
|
+
}
|
|
412
|
+
} else if (typeof onFail === 'function') {
|
|
413
|
+
// Custom fallback function - call with machine as 'this'
|
|
414
|
+
if (isMachine) {
|
|
415
|
+
return onFail.apply(this as Machine<C>, args);
|
|
416
|
+
} else {
|
|
417
|
+
throw new Error('Cannot use function fallback with context-only binding. Use full machine binding.');
|
|
418
|
+
}
|
|
419
|
+
} else {
|
|
420
|
+
// Static fallback machine
|
|
421
|
+
return onFail;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
// Attach metadata for type branding and statechart extraction
|
|
427
|
+
Object.defineProperty(guardedTransition, '__guard', { value: true, enumerable: false });
|
|
428
|
+
Object.defineProperty(guardedTransition, 'condition', { value: condition, enumerable: false });
|
|
429
|
+
Object.defineProperty(guardedTransition, 'transition', { value: transition, enumerable: false });
|
|
430
|
+
Object.defineProperty(guardedTransition, 'options', { value: fullOptions, enumerable: false });
|
|
431
|
+
|
|
432
|
+
// Attach runtime metadata for statechart extraction
|
|
433
|
+
attachRuntimeMeta(guardedTransition, {
|
|
434
|
+
description: description || 'Runtime guarded transition',
|
|
435
|
+
guards: [{ name: 'runtime_guard', description: description || 'Runtime condition check' }]
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
return guardedTransition as GuardedTransition<C, T>;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Fluent API for creating guarded transitions.
|
|
443
|
+
* Provides a more readable way to define conditional transitions.
|
|
444
|
+
*
|
|
445
|
+
* @template C - The context type
|
|
446
|
+
* @param condition - Function that returns true if transition should proceed
|
|
447
|
+
* @returns A fluent interface for defining the guarded transition
|
|
448
|
+
*
|
|
449
|
+
* @example
|
|
450
|
+
* ```typescript
|
|
451
|
+
* const machine = createMachine({ isAdmin: false }, {
|
|
452
|
+
* deleteUser: whenGuard((ctx) => ctx.isAdmin)
|
|
453
|
+
* .do(function(userId: string) {
|
|
454
|
+
* return createMachine({ ...this.context, deleted: userId }, this);
|
|
455
|
+
* })
|
|
456
|
+
* .else(function() {
|
|
457
|
+
* return createMachine({ ...this.context, error: 'Unauthorized' }, this);
|
|
458
|
+
* })
|
|
459
|
+
* });
|
|
460
|
+
* ```
|
|
461
|
+
*/
|
|
462
|
+
export function whenGuard<C extends object>(
|
|
463
|
+
condition: (ctx: C, ...args: any[]) => boolean | Promise<boolean>
|
|
464
|
+
) {
|
|
465
|
+
return {
|
|
466
|
+
/**
|
|
467
|
+
* Define the transition to execute when the condition passes.
|
|
468
|
+
* Returns a guarded transition that can optionally have an else clause.
|
|
469
|
+
*/
|
|
470
|
+
do<T extends Machine<any>>(transition: (...args: any[]) => T) {
|
|
471
|
+
const guarded = guard(condition, transition);
|
|
472
|
+
|
|
473
|
+
// Add fluent else method to the guarded transition
|
|
474
|
+
(guarded as any).else = function<F extends Machine<any>>(fallback: (...args: any[]) => F) {
|
|
475
|
+
return guard(condition, transition, { onFail: fallback });
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
return guarded;
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
292
483
|
/**
|
|
293
484
|
* Flexible metadata wrapper for functional and type-state patterns.
|
|
294
485
|
*
|
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
|
}
|
package/src/utils.ts
CHANGED
|
@@ -300,9 +300,12 @@ export class BoundMachine<M extends { context: any }> {
|
|
|
300
300
|
return new Proxy(this, {
|
|
301
301
|
get: (target, prop) => {
|
|
302
302
|
// Handle direct property access to wrapped machine
|
|
303
|
-
if (prop === 'wrappedMachine'
|
|
303
|
+
if (prop === 'wrappedMachine') {
|
|
304
304
|
return Reflect.get(target, prop);
|
|
305
305
|
}
|
|
306
|
+
if (prop === 'context') {
|
|
307
|
+
return this.wrappedMachine.context;
|
|
308
|
+
}
|
|
306
309
|
|
|
307
310
|
const value = this.wrappedMachine[prop as keyof M];
|
|
308
311
|
|
|
@@ -323,11 +326,223 @@ export class BoundMachine<M extends { context: any }> {
|
|
|
323
326
|
},
|
|
324
327
|
}) as any;
|
|
325
328
|
}
|
|
329
|
+
}
|
|
326
330
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
331
|
+
/**
|
|
332
|
+
* Creates a sequence machine that orchestrates multi-step flows by automatically
|
|
333
|
+
* advancing through a series of machines. When the current machine reaches a "final"
|
|
334
|
+
* state (determined by the isFinal predicate), the sequence automatically transitions
|
|
335
|
+
* to the next machine in the sequence.
|
|
336
|
+
*
|
|
337
|
+
* This implementation uses a functional approach with object delegation rather than Proxy.
|
|
338
|
+
*/
|
|
339
|
+
function createSequenceMachine<
|
|
340
|
+
M extends readonly [Machine<any>, ...Machine<any>[]]
|
|
341
|
+
>(
|
|
342
|
+
machines: M,
|
|
343
|
+
isFinal: (machine: M[number]) => boolean
|
|
344
|
+
): M[number] {
|
|
345
|
+
if (machines.length === 0) {
|
|
346
|
+
throw new Error('Sequence must contain at least one machine');
|
|
332
347
|
}
|
|
348
|
+
|
|
349
|
+
let currentIndex = 0;
|
|
350
|
+
let currentMachine = machines[0];
|
|
351
|
+
|
|
352
|
+
const createDelegationObject = (machine: M[number]) => {
|
|
353
|
+
const delegationObject = Object.create(machine);
|
|
354
|
+
|
|
355
|
+
// The context getter returns the machine's own context
|
|
356
|
+
// (machine is the specific machine instance this delegation object represents)
|
|
357
|
+
Object.defineProperty(delegationObject, 'context', {
|
|
358
|
+
get: () => machine.context,
|
|
359
|
+
enumerable: true,
|
|
360
|
+
configurable: true
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// Override all methods to add advancement logic
|
|
364
|
+
const originalProto = Object.getPrototypeOf(machine);
|
|
365
|
+
const methodNames = Object.getOwnPropertyNames(originalProto).filter(name =>
|
|
366
|
+
name !== 'constructor' && name !== 'context' && typeof (machine as any)[name] === 'function'
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
for (const methodName of methodNames) {
|
|
370
|
+
const methodKey = methodName as keyof any;
|
|
371
|
+
|
|
372
|
+
(delegationObject as any)[methodKey] = (...args: unknown[]) => {
|
|
373
|
+
const result = (currentMachine as any)[methodKey](...args);
|
|
374
|
+
|
|
375
|
+
// Handle both sync and async results
|
|
376
|
+
const handleResult = (resultMachine: unknown) => {
|
|
377
|
+
return advanceIfNeeded(resultMachine as M[number]);
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
// If the result is a Promise, handle it asynchronously
|
|
381
|
+
if (result && typeof (result as any).then === 'function') {
|
|
382
|
+
return (result as Promise<unknown>).then(handleResult);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Otherwise, handle synchronously
|
|
386
|
+
return handleResult(result);
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return delegationObject;
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
const advanceIfNeeded = (machine: M[number]): M[number] => {
|
|
394
|
+
currentMachine = machine;
|
|
395
|
+
|
|
396
|
+
// Check if we should advance to the next machine
|
|
397
|
+
if (isFinal(currentMachine) && currentIndex < machines.length - 1) {
|
|
398
|
+
currentIndex++;
|
|
399
|
+
currentMachine = machines[currentIndex];
|
|
400
|
+
// Create a new delegation object for the new currentMachine
|
|
401
|
+
return createDelegationObject(currentMachine);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return machine;
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
return createDelegationObject(currentMachine);
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Creates a sequence machine that orchestrates multi-step flows by automatically
|
|
411
|
+
* advancing through a series of machines. When the current machine reaches a "final"
|
|
412
|
+
* state (determined by the isFinal predicate), the sequence automatically transitions
|
|
413
|
+
* to the next machine in the sequence.
|
|
414
|
+
*
|
|
415
|
+
* This is perfect for wizard-style flows, multi-step processes, or any scenario where
|
|
416
|
+
* you need to chain machines together with automatic progression.
|
|
417
|
+
*
|
|
418
|
+
* @template M - The tuple of machine types in the sequence.
|
|
419
|
+
* @param machines - The machines to sequence, in order.
|
|
420
|
+
* @param isFinal - A predicate function that determines when a machine is in a final state.
|
|
421
|
+
* Called after each transition to check if the sequence should advance.
|
|
422
|
+
* @returns A new machine that wraps the sequence, delegating to the current machine
|
|
423
|
+
* and automatically advancing when each machine reaches its final state.
|
|
424
|
+
*
|
|
425
|
+
* @example
|
|
426
|
+
* ```typescript
|
|
427
|
+
* // Define form machines with final states
|
|
428
|
+
* class NameForm extends MachineBase<{ name: string; valid: boolean }> {
|
|
429
|
+
* submit = (name: string) => new NameForm({ name, valid: name.length > 0 });
|
|
430
|
+
* }
|
|
431
|
+
*
|
|
432
|
+
* class EmailForm extends MachineBase<{ email: string; valid: boolean }> {
|
|
433
|
+
* submit = (email: string) => new EmailForm({ email, valid: email.includes('@') });
|
|
434
|
+
* }
|
|
435
|
+
*
|
|
436
|
+
* class PasswordForm extends MachineBase<{ password: string; valid: boolean }> {
|
|
437
|
+
* submit = (password: string) => new PasswordForm({ password, valid: password.length >= 8 });
|
|
438
|
+
* }
|
|
439
|
+
*
|
|
440
|
+
* // Create sequence that advances when each form becomes valid
|
|
441
|
+
* const wizard = sequence(
|
|
442
|
+
* [new NameForm({ name: '', valid: false }),
|
|
443
|
+
* new EmailForm({ email: '', valid: false }),
|
|
444
|
+
* new PasswordForm({ password: '', valid: false })],
|
|
445
|
+
* (machine) => machine.context.valid // Advance when valid becomes true
|
|
446
|
+
* );
|
|
447
|
+
*
|
|
448
|
+
* // Usage - automatically advances through forms
|
|
449
|
+
* let current = wizard;
|
|
450
|
+
* current = current.submit('John'); // Still on NameForm (not valid yet)
|
|
451
|
+
* current = current.submit('John Doe'); // Advances to EmailForm (name is valid)
|
|
452
|
+
* current = current.submit('john@'); // Still on EmailForm (not valid yet)
|
|
453
|
+
* current = current.submit('john@example.com'); // Advances to PasswordForm
|
|
454
|
+
* current = current.submit('12345678'); // Advances to end of sequence
|
|
455
|
+
* ```
|
|
456
|
+
*
|
|
457
|
+
* @example
|
|
458
|
+
* ```typescript
|
|
459
|
+
* // Async sequence with API calls
|
|
460
|
+
* const authSequence = sequence(
|
|
461
|
+
* [new LoginForm(), new TwoFactorForm(), new Dashboard()],
|
|
462
|
+
* (machine) => machine.context.authenticated === true
|
|
463
|
+
* );
|
|
464
|
+
*
|
|
465
|
+
* // The sequence handles async transitions automatically
|
|
466
|
+
* const finalState = await authSequence.login('user@example.com', 'password');
|
|
467
|
+
* ```
|
|
468
|
+
*
|
|
469
|
+
* @example
|
|
470
|
+
* ```typescript
|
|
471
|
+
* // Complex predicate - advance based on multiple conditions
|
|
472
|
+
* const complexSequence = sequence(
|
|
473
|
+
* [step1Machine, step2Machine, step3Machine],
|
|
474
|
+
* (machine) => {
|
|
475
|
+
* // Advance when all required fields are filled AND validated
|
|
476
|
+
* return machine.context.requiredFields.every(f => f.filled) &&
|
|
477
|
+
* machine.context.validationErrors.length === 0;
|
|
478
|
+
* }
|
|
479
|
+
* );
|
|
480
|
+
* ```
|
|
481
|
+
*
|
|
482
|
+
* @remarks
|
|
483
|
+
* - The sequence maintains the union type of all machines in the sequence
|
|
484
|
+
* - Transitions are delegated to the current machine in the sequence
|
|
485
|
+
* - When a machine reaches a final state, the sequence automatically advances
|
|
486
|
+
* - If the sequence reaches the end, further transitions return the final machine
|
|
487
|
+
* - The isFinal predicate is called after every transition to check advancement
|
|
488
|
+
* - Works with both sync and async machines (returns MaybePromise)
|
|
489
|
+
*/
|
|
490
|
+
export function sequence<
|
|
491
|
+
M extends readonly [Machine<any>, ...Machine<any>[]]
|
|
492
|
+
>(
|
|
493
|
+
machines: M,
|
|
494
|
+
isFinal: (machine: M[number]) => boolean
|
|
495
|
+
): M[number] {
|
|
496
|
+
return createSequenceMachine(machines, isFinal);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Convenience overload for sequencing exactly 2 machines.
|
|
501
|
+
* Provides better type inference and IntelliSense for common 2-step flows.
|
|
502
|
+
*
|
|
503
|
+
* @example
|
|
504
|
+
* ```typescript
|
|
505
|
+
* const flow = sequence2(
|
|
506
|
+
* new LoginForm(),
|
|
507
|
+
* new Dashboard(),
|
|
508
|
+
* (machine) => machine.context.authenticated
|
|
509
|
+
* );
|
|
510
|
+
* ```
|
|
511
|
+
*/
|
|
512
|
+
export function sequence2<
|
|
513
|
+
M1 extends Machine<any>,
|
|
514
|
+
M2 extends Machine<any>
|
|
515
|
+
>(
|
|
516
|
+
machine1: M1,
|
|
517
|
+
machine2: M2,
|
|
518
|
+
isFinal: (machine: M1 | M2) => boolean
|
|
519
|
+
): M1 | M2 {
|
|
520
|
+
return sequence([machine1, machine2], isFinal);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Convenience overload for sequencing exactly 3 machines.
|
|
525
|
+
* Provides better type inference and IntelliSense for common 3-step flows.
|
|
526
|
+
*
|
|
527
|
+
* @example
|
|
528
|
+
* ```typescript
|
|
529
|
+
* const wizard = sequence3(
|
|
530
|
+
* new NameForm({ name: '', valid: false }),
|
|
531
|
+
* new EmailForm({ email: '', valid: false }),
|
|
532
|
+
* new PasswordForm({ password: '', valid: false }),
|
|
533
|
+
* (machine) => machine.context.valid
|
|
534
|
+
* );
|
|
535
|
+
* ```
|
|
536
|
+
*/
|
|
537
|
+
export function sequence3<
|
|
538
|
+
M1 extends Machine<any>,
|
|
539
|
+
M2 extends Machine<any>,
|
|
540
|
+
M3 extends Machine<any>
|
|
541
|
+
>(
|
|
542
|
+
machine1: M1,
|
|
543
|
+
machine2: M2,
|
|
544
|
+
machine3: M3,
|
|
545
|
+
isFinal: (machine: M1 | M2 | M3) => boolean
|
|
546
|
+
): M1 | M2 | M3 {
|
|
547
|
+
return sequence([machine1, machine2, machine3], isFinal);
|
|
333
548
|
}
|