@doeixd/machine 0.0.6 → 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/src/index.ts CHANGED
@@ -25,13 +25,33 @@ export type Machine<C extends object> = {
25
25
 
26
26
  /**
27
27
  * The shape of an asynchronous machine, where transitions can return Promises.
28
- * @template C - The context (state) object type.
28
+ * Async transitions receive an AbortSignal as the last parameter for cancellation support.
29
+ * @template C - The context object type.
29
30
  */
30
31
  export type AsyncMachine<C extends object> = {
31
32
  /** The readonly state of the machine. */
32
33
  readonly context: C;
33
34
  } & Record<string, (...args: any[]) => MaybePromise<AsyncMachine<any>>>;
34
35
 
36
+ /**
37
+ * Utility type to extract the parameters of an async transition function,
38
+ * which includes TransitionOptions as the last parameter.
39
+ */
40
+ export type AsyncTransitionArgs<M extends AsyncMachine<any>, K extends keyof M & string> =
41
+ M[K] extends (...args: infer A) => any
42
+ ? A extends [...infer Rest, TransitionOptions]
43
+ ? Rest
44
+ : A
45
+ : never;
46
+
47
+ /**
48
+ * Options passed to async transition functions, including cancellation support.
49
+ */
50
+ export interface TransitionOptions {
51
+ /** AbortSignal for cancelling long-running async operations. */
52
+ signal: AbortSignal;
53
+ }
54
+
35
55
 
36
56
  // =============================================================================
37
57
  // SECTION: TYPE UTILITIES & INTROSPECTION
@@ -130,7 +150,12 @@ export function createMachine<C extends object, T extends Record<string, (this:
130
150
  context: C,
131
151
  fns: T
132
152
  ): { context: C } & T {
133
- return Object.assign({ context }, fns);
153
+ // If fns is a machine (has context property), extract just the transition functions
154
+ const transitions = 'context' in fns ? Object.fromEntries(
155
+ Object.entries(fns).filter(([key]) => key !== 'context')
156
+ ) : fns;
157
+ const machine = Object.assign({ context }, transitions);
158
+ return machine as { context: C } & T;
134
159
  }
135
160
 
136
161
  /**
@@ -263,6 +288,79 @@ export function extendTransitions<
263
288
  return createMachine(context, combinedTransitions) as M & T;
264
289
  }
265
290
 
291
+ /**
292
+ * Combines two machine factories into a single factory that creates machines with merged context and transitions.
293
+ * This allows you to compose independent state machines that operate on different parts of the same context.
294
+ *
295
+ * The resulting factory takes the parameters of the first factory, while the second factory is called with no arguments.
296
+ * Context properties are merged (second factory's context takes precedence on conflicts).
297
+ * Transition names must not conflict between the two machines.
298
+ *
299
+ * @template F1 - The first factory function type.
300
+ * @template F2 - The second factory function type.
301
+ * @param factory1 - The first machine factory (provides parameters and primary context).
302
+ * @param factory2 - The second machine factory (provides additional context and transitions).
303
+ * @returns A new factory function that creates combined machines.
304
+ *
305
+ * @example
306
+ * ```typescript
307
+ * // Define two independent machines
308
+ * const createCounter = (initial: number) =>
309
+ * createMachine({ count: initial }, {
310
+ * increment: function() { return createMachine({ count: this.count + 1 }, this); },
311
+ * decrement: function() { return createMachine({ count: this.count - 1 }, this); }
312
+ * });
313
+ *
314
+ * const createLogger = () =>
315
+ * createMachine({ logs: [] as string[] }, {
316
+ * log: function(message: string) {
317
+ * return createMachine({ logs: [...this.logs, message] }, this);
318
+ * },
319
+ * clear: function() {
320
+ * return createMachine({ logs: [] }, this);
321
+ * }
322
+ * });
323
+ *
324
+ * // Combine them
325
+ * const createCounterWithLogging = combineFactories(createCounter, createLogger);
326
+ *
327
+ * // Use the combined factory
328
+ * const machine = createCounterWithLogging(5); // { count: 5, logs: [] }
329
+ * const incremented = machine.increment(); // { count: 6, logs: [] }
330
+ * const logged = incremented.log("Count incremented"); // { count: 6, logs: ["Count incremented"] }
331
+ * ```
332
+ */
333
+ export function combineFactories<
334
+ F1 extends (...args: any[]) => Machine<any>,
335
+ F2 extends () => Machine<any>
336
+ >(
337
+ factory1: F1,
338
+ factory2: F2
339
+ ): (
340
+ ...args: Parameters<F1>
341
+ ) => Machine<Context<ReturnType<F1>> & Context<ReturnType<F2>>> &
342
+ Omit<ReturnType<F1>, 'context'> &
343
+ Omit<ReturnType<F2>, 'context'> {
344
+ return (...args: Parameters<F1>) => {
345
+ // Create instances from both factories
346
+ const machine1 = factory1(...args);
347
+ const machine2 = factory2();
348
+
349
+ // Merge contexts (machine2 takes precedence on conflicts)
350
+ const combinedContext = { ...machine1.context, ...machine2.context };
351
+
352
+ // Extract transitions from both machines
353
+ const { context: _, ...transitions1 } = machine1;
354
+ const { context: __, ...transitions2 } = machine2;
355
+
356
+ // Combine transitions (TypeScript will catch conflicts at compile time)
357
+ const combinedTransitions = { ...transitions1, ...transitions2 };
358
+
359
+ // Create the combined machine
360
+ return createMachine(combinedContext, combinedTransitions) as any;
361
+ };
362
+ }
363
+
266
364
  /**
267
365
  * Creates a builder function from a "template" machine instance.
268
366
  * This captures the behavior of a machine and returns a factory that can stamp out
@@ -361,27 +459,61 @@ export function hasState<
361
459
  /**
362
460
  * Runs an asynchronous state machine with a managed lifecycle and event dispatch capability.
363
461
  * This is the "interpreter" for async machines, handling state updates and side effects.
462
+ * Provides automatic AbortController management to prevent async race conditions.
364
463
  *
365
464
  * @template M - The initial machine type.
366
465
  * @param initial - The initial machine state.
367
466
  * @param onChange - Optional callback invoked with the new machine state after every transition.
368
- * @returns An object with a `state` getter for the current context and an async `dispatch` function.
467
+ * @returns An object with a `state` getter for the current context, an async `dispatch` function, and a `stop` method.
369
468
  */
370
469
  export function runMachine<M extends AsyncMachine<any>>(
371
470
  initial: M,
372
471
  onChange?: (m: M) => void
373
472
  ) {
374
473
  let current = initial;
474
+ // Keep track of the controller for the currently-running async transition.
475
+ let activeController: AbortController | null = null;
375
476
 
376
477
  async function dispatch<E extends Event<typeof current>>(event: E): Promise<M> {
478
+ // 1. If an async transition is already in progress, cancel it.
479
+ if (activeController) {
480
+ activeController.abort();
481
+ activeController = null;
482
+ }
483
+
377
484
  const fn = (current as any)[event.type];
378
485
  if (typeof fn !== 'function') {
379
486
  throw new Error(`[Machine] Unknown event type '${String(event.type)}' on current state.`);
380
487
  }
381
- const nextState = await fn.apply(current.context, event.args);
382
- current = nextState;
383
- onChange?.(current);
384
- return current;
488
+
489
+ // 2. Create a new AbortController for this new transition.
490
+ const controller = new AbortController();
491
+ activeController = controller;
492
+
493
+ try {
494
+ // 3. Pass the signal to the transition function.
495
+ const nextStatePromise = fn.apply(current.context, [...event.args, { signal: controller.signal }]);
496
+
497
+ const nextState = await nextStatePromise;
498
+
499
+ // 4. If this promise resolved but has since been aborted, do not update state.
500
+ // This prevents the race condition.
501
+ if (controller.signal.aborted) {
502
+ // Return the *current* state, as if the transition never completed.
503
+ return current;
504
+ }
505
+
506
+ current = nextState;
507
+ onChange?.(current);
508
+ return current;
509
+
510
+ } finally {
511
+ // 5. Clean up the controller once the transition is complete (resolved or rejected).
512
+ // Only clear it if it's still the active one.
513
+ if (activeController === controller) {
514
+ activeController = null;
515
+ }
516
+ }
385
517
  }
386
518
 
387
519
  return {
@@ -391,6 +523,13 @@ export function runMachine<M extends AsyncMachine<any>>(
391
523
  },
392
524
  /** Dispatches a type-safe event to the machine, triggering a transition. */
393
525
  dispatch,
526
+ /** Stops any pending async operation and cleans up resources. */
527
+ stop: () => {
528
+ if (activeController) {
529
+ activeController.abort();
530
+ activeController = null;
531
+ }
532
+ },
394
533
  };
395
534
  }
396
535
 
@@ -535,6 +674,8 @@ export {
535
674
  transitionTo,
536
675
  describe,
537
676
  guarded,
677
+ guard,
678
+ whenGuard,
538
679
  invoke,
539
680
  action,
540
681
  metadata,
@@ -544,7 +685,10 @@ export {
544
685
  type InvokeMeta,
545
686
  type ActionMeta,
546
687
  type ClassConstructor,
547
- type WithMeta
688
+ type WithMeta,
689
+ type GuardOptions,
690
+ type GuardFallback,
691
+ type GuardedTransition
548
692
  } from './primitives';
549
693
 
550
694
  // =============================================================================
@@ -574,4 +718,66 @@ export {
574
718
  export { RUNTIME_META, type RuntimeTransitionMeta } from './primitives';
575
719
 
576
720
 
577
- export * from './multi'
721
+ export * from './multi'
722
+
723
+ export * from './higher-order'
724
+
725
+ // =============================================================================
726
+ // SECTION: MIDDLEWARE & INTERCEPTION
727
+ // =============================================================================
728
+
729
+ export {
730
+ createMiddleware,
731
+ withLogging,
732
+ withAnalytics,
733
+ withValidation,
734
+ withPermissions,
735
+ withErrorReporting,
736
+ withPerformanceMonitoring,
737
+ withRetry,
738
+ withHistory,
739
+ withSnapshot,
740
+ withTimeTravel,
741
+ compose,
742
+ composeTyped,
743
+ createPipeline,
744
+ createMiddlewareRegistry,
745
+ when,
746
+ inDevelopment,
747
+ whenContext,
748
+ combine,
749
+ branch,
750
+ isMiddlewareFn,
751
+ isConditionalMiddleware,
752
+ createCustomMiddleware,
753
+ type MiddlewareHooks,
754
+ type MiddlewareOptions,
755
+ type MiddlewareContext,
756
+ type MiddlewareResult,
757
+ type MiddlewareError,
758
+ type HistoryEntry,
759
+ type ContextSnapshot,
760
+ type Serializer,
761
+ type MiddlewareFn,
762
+ type ConditionalMiddleware,
763
+ type NamedMiddleware,
764
+ type PipelineConfig,
765
+ type PipelineResult,
766
+ chain,
767
+ withDebugging
768
+ } from './middleware';
769
+
770
+ // =============================================================================
771
+ // SECTION: UTILITIES & HELPERS
772
+ // =============================================================================
773
+
774
+ export {
775
+ isState,
776
+ createEvent,
777
+ mergeContext,
778
+ pipeTransitions,
779
+ logState,
780
+ call,
781
+ bindTransitions,
782
+ BoundMachine
783
+ } from './utils';