@doeixd/machine 0.0.11 → 0.0.12

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.
@@ -0,0 +1,267 @@
1
+ import { createMachine, Machine, extendTransitions } from './index';
2
+
3
+ /**
4
+ * Creates a factory for building type-safe transitions for a specific machine.
5
+ * This higher-order function captures the machine's `transitions` object in a closure,
6
+ * enabling a clean, functional pattern for defining state changes without directly
7
+ * manipulating the machine's context.
8
+ *
9
+ * This pattern promotes:
10
+ * - Pure functions for state transformations
11
+ * - Separation of transition logic from machine construction
12
+ * - Reusable transition factories across similar machines
13
+ * - Type safety through generic constraints
14
+ *
15
+ * @template C The context type of the machine
16
+ * @template C The context type of the machine
17
+ * @returns A `createTransition` function that can create transitions for machines with context type C
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * // Define your machine's transitions object
22
+ * const counterTransitions = {
23
+ * increment: function(amount: number) {
24
+ * return createMachine({ count: this.count + amount }, counterTransitions);
25
+ * }
26
+ * };
27
+ *
28
+ * // Create a transition factory
29
+ * const createCounterTransition = createTransitionFactory<{ count: number }>();
30
+ *
31
+ * // Use the factory to create pure, type-safe transitions
32
+ * const incrementBy = createCounterTransition(
33
+ * (ctx, amount: number) => ({ count: ctx.count + amount })
34
+ * );
35
+ *
36
+ * const counter = createMachine({ count: 0 }, counterTransitions);
37
+ * const newCounter = counter.increment(5); // Direct call
38
+ * // OR
39
+ * const incremented = incrementBy.call(counter, 5); // Using factory
40
+ * ```
41
+ */
42
+ export function createTransitionFactory<C extends object>() {
43
+ /**
44
+ * Takes a pure context transformer function and returns a full, type-safe
45
+ * machine transition method that can be attached to a machine.
46
+ *
47
+ * The transformer function receives the current context as its first argument,
48
+ * followed by any additional arguments passed to the transition.
49
+ *
50
+ * @template TArgs The argument types for the transition (excluding context)
51
+ * @param transformer A pure function: `(context, ...args) => nextContext`
52
+ * @returns A machine transition method that can be called on a machine instance
53
+ *
54
+ * @example
55
+ * ```typescript
56
+ * const createTodoTransition = createTransitionFactory<TodoContext>();
57
+ *
58
+ * const addTodo = createTodoTransition(
59
+ * (ctx, text: string) => ({
60
+ * ...ctx,
61
+ * todos: [...ctx.todos, { id: Date.now(), text, completed: false }]
62
+ * })
63
+ * );
64
+ *
65
+ * const updateTodo = createTodoTransition(
66
+ * (ctx, id: number, updates: Partial<Todo>) => ({
67
+ * ...ctx,
68
+ * todos: ctx.todos.map(todo =>
69
+ * todo.id === id ? { ...todo, ...updates } : todo
70
+ * )
71
+ * })
72
+ * );
73
+ * ```
74
+ */
75
+ return function createTransition<TArgs extends any[]>(
76
+ transformer: (ctx: C, ...args: TArgs) => C
77
+ ) {
78
+ return function (this: Machine<C>, ...args: TArgs): Machine<C> {
79
+ const nextContext = transformer(this.context, ...args);
80
+ // Use `this` as the transitions object, which includes all current transitions
81
+ return createMachine(nextContext, this);
82
+ };
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Creates a factory for adding new, type-safe transitions to an existing machine instance.
88
+ * This enables a functional, compositional approach to building up a machine's capabilities
89
+ * incrementally, without modifying the original machine.
90
+ *
91
+ * This pattern supports:
92
+ * - Progressive enhancement of machine behavior
93
+ * - Plugin-like extension of existing machines
94
+ * - Immutable composition (original machine unchanged)
95
+ * - Type-safe addition of new transitions
96
+ *
97
+ * @template M The machine type being extended
98
+ * @param machine The machine instance to extend
99
+ * @returns An `addTransition` function pre-configured for this machine
100
+ *
101
+ * @example
102
+ * ```typescript
103
+ * // Start with a basic counter machine
104
+ * const basicCounter = createMachine({ count: 0 }, {
105
+ * increment: function() {
106
+ * return createMachine({ count: this.count + 1 }, this);
107
+ * }
108
+ * });
109
+ *
110
+ * // Create an extender for this machine
111
+ * const extendCounter = createTransitionExtender(basicCounter);
112
+ *
113
+ * // Add new transitions functionally
114
+ * const extendedCounter = extendCounter('decrement',
115
+ * (ctx) => ({ count: ctx.count - 1 })
116
+ * ).addTransition('reset',
117
+ * (ctx) => ({ count: 0 })
118
+ * ).addTransition('add',
119
+ * (ctx, amount: number) => ({ count: ctx.count + amount })
120
+ * );
121
+ *
122
+ * // The original machine is unchanged
123
+ * console.log(basicCounter.count); // 0
124
+ *
125
+ * // The extended machine has all transitions
126
+ * const result = extendedCounter.increment().add(10).decrement();
127
+ * console.log(result.count); // 10
128
+ * ```
129
+ */
130
+ export function createTransitionExtender<M extends Machine<any>>(machine: M) {
131
+ type C = M['context'];
132
+
133
+ /**
134
+ * Adds a new transition to the machine and returns a new extender for chaining.
135
+ * The new transition is created from a pure context transformer function.
136
+ *
137
+ * @template TName The name of the new transition
138
+ * @template TArgs The argument types for the new transition
139
+ * @param name The name of the new transition method
140
+ * @param transformer A pure function that defines how the context should change
141
+ * @returns A new transition extender with the added transition
142
+ *
143
+ * @example
144
+ * ```typescript
145
+ * const userMachine = createMachine({ name: '', email: '' }, {});
146
+ * const extendUser = createTransitionExtender(userMachine);
147
+ *
148
+ * const withValidation = extendUser.addTransition('setName',
149
+ * (ctx, name: string) => {
150
+ * if (name.length < 2) throw new Error('Name too short');
151
+ * return { ...ctx, name };
152
+ * }
153
+ * ).addTransition('setEmail',
154
+ * (ctx, email: string) => {
155
+ * if (!email.includes('@')) throw new Error('Invalid email');
156
+ * return { ...ctx, email };
157
+ * }
158
+ * );
159
+ *
160
+ * const user = withValidation.machine.setName('John').setEmail('john@example.com');
161
+ * ```
162
+ */
163
+ return {
164
+ machine,
165
+
166
+ addTransition: function<
167
+ TName extends string,
168
+ TArgs extends any[],
169
+ >(
170
+ name: TName,
171
+ transformer: (ctx: C, ...args: TArgs) => C
172
+ ) {
173
+ const transitionFn = function (this: Machine<C>, ...args: TArgs) {
174
+ const nextContext = transformer(this.context, ...args);
175
+
176
+ // Use `this` as the transitions object, which includes all current transitions
177
+ return createMachine(nextContext, this);
178
+ };
179
+
180
+ // Create the extended machine
181
+ const newMachine = extendTransitions(machine, { [name]: transitionFn } as any);
182
+
183
+ // Return a new extender that includes this transition
184
+ return createTransitionExtender(newMachine);
185
+ }
186
+ };
187
+ }
188
+
189
+ /**
190
+ * A mapped type that creates the final transition method signatures based on
191
+ * an object of pure context transformers. It infers argument types and sets
192
+ * the correct return type.
193
+ */
194
+ type MachineTransitions<
195
+ T extends Record<string, (ctx: C, ...args: any[]) => C>,
196
+ C extends object
197
+ > = {
198
+ [K in keyof T]: T[K] extends (ctx: C, ...args: infer A) => C
199
+ ? (this: { context: C } & T, ...args: A) => Machine<C>
200
+ : never;
201
+ };
202
+
203
+ /**
204
+ * Creates a complete, type-safe, functional state machine using a curried, two-step
205
+ * approach that separates the initial data from the transition logic.
206
+ *
207
+ * This is a highly declarative and functional pattern for building single-state machines.
208
+ *
209
+ * @template C The context type of the machine.
210
+ * @param initialContext The starting context (data) for the machine.
211
+ * @returns A new function that takes an object of pure context-transformer
212
+ * functions and returns a fully-formed machine instance.
213
+ */
214
+ export function createFunctionalMachine<C extends object>(initialContext: C) {
215
+ /**
216
+ * This returned function is pre-configured with the `initialContext`.
217
+ *
218
+ * @template T The type of the transformers object.
219
+ * @param transformers An object where each key is a transition name and each value
220
+ * is a pure function: `(context, ...args) => nextContext`.
221
+ * @returns A fully-formed, immutable, and type-safe machine instance.
222
+ *
223
+ * @example
224
+ * ```typescript
225
+ * const createCounter = createFunctionalMachine({ count: 0 });
226
+ *
227
+ * const counter = createCounter({
228
+ * increment: (ctx) => ({ count: ctx.count + 1 }),
229
+ * decrement: (ctx) => ({ count: ctx.count - 1 }),
230
+ * add: (ctx, amount: number) => ({ count: ctx.count + amount }),
231
+ * reset: (ctx) => ({ count: 0 })
232
+ * });
233
+ *
234
+ * // Use the machine
235
+ * const updated = counter.increment().add(5).decrement();
236
+ * console.log(updated.context.count); // 5
237
+ * ```
238
+ */
239
+ return function withTransitions<
240
+ T extends Record<string, (ctx: C, ...args: any[]) => C>
241
+ >(
242
+ transformers: T
243
+ ): Machine<C> & MachineTransitions<T, C> {
244
+ // 1. Create a placeholder object for the final transitions.
245
+ const transitions: any = {};
246
+
247
+ // 2. Map the pure transformers to full machine transition methods.
248
+ const machineTransitions = Object.fromEntries(
249
+ Object.entries(transformers).map(([key, transformer]) => [
250
+ key,
251
+ function (this: { context: C }, ...args: any[]) {
252
+ // Apply the pure data transformation.
253
+ const nextContext = transformer(this.context, ...args);
254
+ // Return a new machine, passing in the `transitions` object from the closure.
255
+ // At this point, `transitions` will be fully populated.
256
+ return createMachine(nextContext, transitions);
257
+ },
258
+ ])
259
+ );
260
+
261
+ // 3. Populate the placeholder with the real transitions.
262
+ Object.assign(transitions, machineTransitions);
263
+
264
+ // 4. Create and return the initial machine instance using the provided context.
265
+ return createMachine(initialContext, transitions) as any;
266
+ };
267
+ }
package/src/index.ts CHANGED
@@ -778,10 +778,21 @@ export {
778
778
  export {
779
779
  isState,
780
780
  createEvent,
781
+ createTransition,
781
782
  mergeContext,
782
783
  pipeTransitions,
783
784
  logState,
784
785
  call,
785
786
  bindTransitions,
786
787
  BoundMachine
787
- } from './utils';
788
+ } from './utils';
789
+
790
+ // =============================================================================
791
+ // SECTION: FUNCTIONAL COMBINATORS
792
+ // =============================================================================
793
+
794
+ export {
795
+ createTransitionFactory,
796
+ createTransitionExtender,
797
+ createFunctionalMachine
798
+ } from './functional-combinators';
package/src/multi.ts CHANGED
@@ -514,6 +514,42 @@ export function createEnsemble<
514
514
  };
515
515
  }
516
516
 
517
+ /**
518
+ * Creates a factory for building type-safe, framework-agnostic Ensembles.
519
+ * This is a higher-order function that captures the application's state store
520
+ * and state-discriminant logic in a closure.
521
+ *
522
+ * This allows you to define your application's state "environment" once and then
523
+ * easily create multiple, consistent ensembles by only providing the behavioral logic.
524
+ *
525
+ * @template C The shared context type for the application.
526
+ * @param store The application's state store (e.g., from React, Zustand, etc.).
527
+ * @param getDiscriminant An accessor function that determines the current state from the context.
528
+ * @returns A `withFactories` function that is pre-configured for your app's environment.
529
+ */
530
+ export function createEnsembleFactory<C extends object>(
531
+ store: StateStore<C>,
532
+ getDiscriminant: (context: C) => keyof any
533
+ ) {
534
+ /**
535
+ * This returned function is pre-configured with the `store` and `getDiscriminant` logic.
536
+ * It takes the machine factories (the behavioral logic) and returns a complete Ensemble.
537
+ *
538
+ * @template F The type of the factories object.
539
+ * @param factories An object where each key is a state name and each value is a
540
+ * function that creates a machine instance for that state.
541
+ * @returns A fully-formed, reactive, and type-safe Ensemble instance.
542
+ */
543
+ return function withFactories<
544
+ F extends Record<string, (context: C) => Machine<C>>
545
+ >(
546
+ factories: F
547
+ ): Ensemble<ReturnType<F[keyof F]>, C> {
548
+ // We simply call the original createEnsemble with the captured arguments.
549
+ return createEnsemble(store, factories, getDiscriminant as (context: C) => keyof F);
550
+ };
551
+ }
552
+
517
553
  // =============================================================================
518
554
  // SECTION 3: GENERATOR INTEGRATION
519
555
  // =============================================================================
package/src/utils.ts CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  Transitions,
14
14
  TransitionArgs,
15
15
  setContext,
16
+ createMachine,
16
17
  } from './index'; // Assuming index.ts is in the same directory
17
18
 
18
19
  // =============================================================================
@@ -166,6 +167,79 @@ export function logState<M extends Machine<any>>(machine: M, label?: string): M
166
167
  return machine;
167
168
  }
168
169
 
170
+ /**
171
+ * A generic combinator that creates transition functions from pure context transformers.
172
+ * This enables writing transitions as simple, testable functions that only transform context,
173
+ * while automatically handling the machine creation boilerplate.
174
+ *
175
+ * @template C - The context object type.
176
+ * @template TArgs - The argument types for the transition function.
177
+ * @param getTransitions - A function that returns the transition functions object to use for the new machine.
178
+ * @param transformer - A pure function that transforms the context based on the current state and arguments.
179
+ * @returns A transition function that can be used as a machine method.
180
+ *
181
+ * @example
182
+ * ```typescript
183
+ * // Define transitions object with self-reference
184
+ * const counterTransitions = {
185
+ * increment: createTransition(
186
+ * () => counterTransitions,
187
+ * (ctx) => ({ count: ctx.count + 1 })
188
+ * ),
189
+ * add: createTransition(
190
+ * () => counterTransitions,
191
+ * (ctx, n: number) => ({ count: ctx.count + n })
192
+ * )
193
+ * };
194
+ *
195
+ * // Create machine
196
+ * const counter = createMachine({ count: 0 }, counterTransitions);
197
+ *
198
+ * // Use transitions
199
+ * const incremented = counter.increment(); // { count: 1 }
200
+ * const added = incremented.add(5); // { count: 6 }
201
+ * ```
202
+ *
203
+ * @example
204
+ * ```typescript
205
+ * // With class-based machines
206
+ * class Counter extends MachineBase<{ count: number }> {
207
+ * constructor(count = 0) {
208
+ * super({ count });
209
+ * }
210
+ *
211
+ * increment = createTransition(
212
+ * () => ({ increment: this.increment, add: this.add }),
213
+ * (ctx) => ({ count: ctx.count + 1 })
214
+ * );
215
+ *
216
+ * add = createTransition(
217
+ * () => ({ increment: this.increment, add: this.add }),
218
+ * (ctx, n: number) => ({ count: ctx.count + n })
219
+ * );
220
+ * }
221
+ * ```
222
+ *
223
+ * @remarks
224
+ * This function promotes the library's philosophy of pure, immutable transitions.
225
+ * The transformer function should be pure and only depend on its parameters.
226
+ * The returned transition function automatically creates a new machine instance,
227
+ * preserving all transitions while updating only the context.
228
+ * The getTransitions function is called lazily to avoid circular reference issues.
229
+ */
230
+ export function createTransition<
231
+ C extends object,
232
+ TArgs extends any[]
233
+ >(
234
+ getTransitions: () => Record<string, (this: C, ...args: any[]) => any>,
235
+ transformer: (ctx: C, ...args: TArgs) => C
236
+ ): (this: { context: C }, ...args: TArgs) => Machine<C> {
237
+ return function (this: { context: C }, ...args: TArgs): Machine<C> {
238
+ const nextContext = transformer(this.context, ...args);
239
+ return createMachine(nextContext, getTransitions());
240
+ };
241
+ }
242
+
169
243
  // =============================================================================
170
244
  // SECTION: TRANSITION BINDING HELPERS
171
245
  // =============================================================================