@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/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' || prop === 'context') {
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
- * Access the underlying machine's context directly.
329
- */
330
- get context(): M extends { context: infer C } ? C : never {
331
- return this.wrappedMachine.context;
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
  }