@doeixd/machine 0.0.4

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,421 @@
1
+ /**
2
+ * @file Generator-based state machine composition utilities.
3
+ * @description
4
+ * This module provides a generator-based approach to composing state machine transitions.
5
+ * Instead of chaining method calls or using composition functions, you can write
6
+ * imperative-style code using generators that feels like sequential, synchronous code
7
+ * while maintaining the immutability and type safety of the state machine model.
8
+ *
9
+ * This pattern is particularly useful for:
10
+ * - Multi-step workflows where each step depends on the previous
11
+ * - Complex transition logic that would be unwieldy with chaining
12
+ * - When you want imperative control flow (if/else, loops) with immutable state
13
+ * - Testing scenarios where you want to control the flow step-by-step
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * const result = run(function* (machine) {
18
+ * // Each yield passes control back and receives the next state
19
+ * let m = yield* step(machine.increment());
20
+ * m = yield* step(m.add(5));
21
+ * if (m.context.count > 10) {
22
+ * m = yield* step(m.reset());
23
+ * }
24
+ * return m.context.count;
25
+ * }, initialMachine);
26
+ * ```
27
+ */
28
+
29
+ import { Machine } from './index';
30
+
31
+ /**
32
+ * Runs a generator-based state machine flow to completion.
33
+ *
34
+ * This function executes a generator that yields machine states and returns a final value.
35
+ * Each yield passes the current machine state back to the generator, allowing you to
36
+ * write imperative-style code while maintaining immutability.
37
+ *
38
+ * **How it works:**
39
+ * 1. The generator function receives the initial machine
40
+ * 2. Each `yield` expression produces a new machine state
41
+ * 3. That state is sent back into the generator via `next()`
42
+ * 4. The generator can use the received state for the next operation
43
+ * 5. When the generator returns, that value is returned from `run()`
44
+ *
45
+ * **Key insight:** The generator doesn't mutate state—it yields new immutable states
46
+ * at each step, creating a clear audit trail of state transitions.
47
+ *
48
+ * @template C - The context object type for the machine.
49
+ * @template T - The return type of the generator (can be any type).
50
+ *
51
+ * @param flow - A generator function that receives a machine and yields machines,
52
+ * eventually returning a value of type T.
53
+ * @param initial - The initial machine state to start the flow.
54
+ *
55
+ * @returns The final value returned by the generator.
56
+ *
57
+ * @example Basic usage with counter
58
+ * ```typescript
59
+ * const counter = createMachine({ count: 0 }, {
60
+ * increment: function() {
61
+ * return createMachine({ count: this.count + 1 }, this);
62
+ * },
63
+ * add: function(n: number) {
64
+ * return createMachine({ count: this.count + n }, this);
65
+ * }
66
+ * });
67
+ *
68
+ * const finalCount = run(function* (m) {
69
+ * m = yield* step(m.increment()); // count: 1
70
+ * m = yield* step(m.add(5)); // count: 6
71
+ * m = yield* step(m.increment()); // count: 7
72
+ * return m.context.count;
73
+ * }, counter);
74
+ *
75
+ * console.log(finalCount); // 7
76
+ * ```
77
+ *
78
+ * @example Conditional logic
79
+ * ```typescript
80
+ * const result = run(function* (m) {
81
+ * m = yield* step(m.increment());
82
+ *
83
+ * if (m.context.count > 5) {
84
+ * m = yield* step(m.reset());
85
+ * } else {
86
+ * m = yield* step(m.add(10));
87
+ * }
88
+ *
89
+ * return m;
90
+ * }, counter);
91
+ * ```
92
+ *
93
+ * @example Loops and accumulation
94
+ * ```typescript
95
+ * const sum = run(function* (m) {
96
+ * let total = 0;
97
+ *
98
+ * for (let i = 0; i < 5; i++) {
99
+ * m = yield* step(m.increment());
100
+ * total += m.context.count;
101
+ * }
102
+ *
103
+ * return total;
104
+ * }, counter);
105
+ * ```
106
+ *
107
+ * @example Error handling
108
+ * ```typescript
109
+ * const result = run(function* (m) {
110
+ * try {
111
+ * m = yield* step(m.riskyOperation());
112
+ * m = yield* step(m.processResult());
113
+ * } catch (error) {
114
+ * m = yield* step(m.handleError(error));
115
+ * }
116
+ * return m;
117
+ * }, machine);
118
+ * ```
119
+ */
120
+ export function run<C extends object, T>(
121
+ flow: (m: Machine<C>) => Generator<Machine<C>, T, Machine<C>>,
122
+ initial: Machine<C>
123
+ ): T {
124
+ // Create the generator by calling the flow function with the initial machine
125
+ const generator = flow(initial);
126
+
127
+ // Track the current machine state as we iterate
128
+ let current = initial;
129
+
130
+ // Iterate the generator until completion
131
+ while (true) {
132
+ // Send the current machine state into the generator and get the next yielded value
133
+ // The generator receives `current` as the result of its last yield expression
134
+ const { value, done } = generator.next(current);
135
+
136
+ // If the generator has returned (done), we have our final value
137
+ if (done) {
138
+ return value;
139
+ }
140
+
141
+ // Otherwise, the yielded value becomes our new current state
142
+ // This state will be sent back into the generator on the next iteration
143
+ current = value;
144
+ }
145
+ }
146
+
147
+ /**
148
+ * A helper function to yield a machine state and receive the next state back.
149
+ *
150
+ * This function creates a mini-generator that yields the provided machine and
151
+ * returns whatever value the outer runner sends back. It's designed to be used
152
+ * with `yield*` (yield delegation) inside your main generator.
153
+ *
154
+ * **Why use this helper?**
155
+ * - Makes the intent clear: "step to this state"
156
+ * - Provides a consistent API for state transitions
157
+ * - Enables type inference for the received state
158
+ * - Works seamlessly with the `run()` function
159
+ *
160
+ * **What `yield*` does:**
161
+ * `yield*` delegates to another generator. When you write `yield* step(m)`,
162
+ * control passes to the `step` generator, which yields `m`, then returns the
163
+ * value sent back by the runner.
164
+ *
165
+ * @template C - The context object type for the machine.
166
+ *
167
+ * @param m - The machine state to yield.
168
+ *
169
+ * @returns A generator that yields the machine and returns the received state.
170
+ *
171
+ * @example Basic stepping
172
+ * ```typescript
173
+ * run(function* (machine) {
174
+ * // Yield this state and receive the next one
175
+ * const next = yield* step(machine.increment());
176
+ * console.log(next.context.count);
177
+ * return next;
178
+ * }, counter);
179
+ * ```
180
+ *
181
+ * @example Without step (more verbose)
182
+ * ```typescript
183
+ * run(function* (machine) {
184
+ * // This is what step() does internally
185
+ * const next = yield machine.increment();
186
+ * return next;
187
+ * }, counter);
188
+ * ```
189
+ *
190
+ * @example Chaining with step
191
+ * ```typescript
192
+ * run(function* (m) {
193
+ * m = yield* step(m.action1());
194
+ * m = yield* step(m.action2());
195
+ * m = yield* step(m.action3());
196
+ * return m;
197
+ * }, machine);
198
+ * ```
199
+ */
200
+ export function step<C extends object>(
201
+ m: Machine<C>
202
+ ): Generator<Machine<C>, Machine<C>, Machine<C>> {
203
+ // Create an immediately-invoked generator that:
204
+ // 1. Yields the provided machine
205
+ // 2. Receives a value back (the next state)
206
+ // 3. Returns that received value
207
+ return (function* () {
208
+ const received = yield m;
209
+ return received;
210
+ })();
211
+ }
212
+
213
+ /**
214
+ * Alternative to `step` that doesn't require `yield*`.
215
+ * This is semantically identical but uses direct yielding.
216
+ *
217
+ * Use this if you prefer the simpler syntax without delegation.
218
+ *
219
+ * @template C - The context object type.
220
+ * @param m - The machine to yield.
221
+ * @returns The same machine (passed through).
222
+ *
223
+ * @example
224
+ * ```typescript
225
+ * run(function* (m) {
226
+ * m = yield m.increment(); // No yield* needed
227
+ * m = yield m.add(5);
228
+ * return m;
229
+ * }, counter);
230
+ * ```
231
+ */
232
+ export function yieldMachine<C extends object>(m: Machine<C>): Machine<C> {
233
+ return m;
234
+ }
235
+
236
+ /**
237
+ * Runs multiple generator flows in sequence, passing the result of each to the next.
238
+ *
239
+ * This is useful for composing multiple generator-based workflows into a pipeline.
240
+ *
241
+ * @template C - The context object type.
242
+ * @param initial - The initial machine state.
243
+ * @param flows - An array of generator functions to run in sequence.
244
+ * @returns The final machine state after all flows complete.
245
+ *
246
+ * @example
247
+ * ```typescript
248
+ * const flow1 = function* (m: Machine<{ count: number }>) {
249
+ * m = yield* step(m.increment());
250
+ * return m;
251
+ * };
252
+ *
253
+ * const flow2 = function* (m: Machine<{ count: number }>) {
254
+ * m = yield* step(m.add(5));
255
+ * return m;
256
+ * };
257
+ *
258
+ * const result = runSequence(counter, [flow1, flow2]);
259
+ * console.log(result.context.count); // 6
260
+ * ```
261
+ */
262
+ export function runSequence<C extends object>(
263
+ initial: Machine<C>,
264
+ flows: Array<(m: Machine<C>) => Generator<Machine<C>, Machine<C>, Machine<C>>>
265
+ ): Machine<C> {
266
+ return flows.reduce((machine, flow) => {
267
+ return run(flow, machine);
268
+ }, initial);
269
+ }
270
+
271
+ /**
272
+ * Creates a reusable generator flow that can be composed into other flows.
273
+ *
274
+ * This allows you to define common state machine patterns as reusable building blocks.
275
+ *
276
+ * @template C - The context object type.
277
+ * @param flow - A generator function representing a reusable flow.
278
+ * @returns A function that can be used with `yield*` in other generators.
279
+ *
280
+ * @example
281
+ * ```typescript
282
+ * // Define a reusable flow
283
+ * const incrementThrice = createFlow(function* (m: Machine<{ count: number }>) {
284
+ * m = yield* step(m.increment());
285
+ * m = yield* step(m.increment());
286
+ * m = yield* step(m.increment());
287
+ * return m;
288
+ * });
289
+ *
290
+ * // Use it in another flow
291
+ * const result = run(function* (m) {
292
+ * m = yield* incrementThrice(m);
293
+ * m = yield* step(m.add(10));
294
+ * return m;
295
+ * }, counter);
296
+ * ```
297
+ */
298
+ export function createFlow<C extends object>(
299
+ flow: (m: Machine<C>) => Generator<Machine<C>, Machine<C>, Machine<C>>
300
+ ): (m: Machine<C>) => Generator<Machine<C>, Machine<C>, Machine<C>> {
301
+ return flow;
302
+ }
303
+
304
+ /**
305
+ * Runs a generator flow with debugging output at each step.
306
+ *
307
+ * This is useful for understanding the state transitions in your flow.
308
+ *
309
+ * @template C - The context object type.
310
+ * @template T - The return type.
311
+ * @param flow - The generator function to run.
312
+ * @param initial - The initial machine state.
313
+ * @param logger - Optional custom logger function.
314
+ * @returns The final value from the generator.
315
+ *
316
+ * @example
317
+ * ```typescript
318
+ * const result = runWithDebug(function* (m) {
319
+ * m = yield* step(m.increment());
320
+ * m = yield* step(m.add(5));
321
+ * return m.context.count;
322
+ * }, counter);
323
+ *
324
+ * // Output:
325
+ * // Step 0: { count: 0 }
326
+ * // Step 1: { count: 1 }
327
+ * // Step 2: { count: 6 }
328
+ * // Final: 6
329
+ * ```
330
+ */
331
+ export function runWithDebug<C extends object, T>(
332
+ flow: (m: Machine<C>) => Generator<Machine<C>, T, Machine<C>>,
333
+ initial: Machine<C>,
334
+ logger: (step: number, machine: Machine<C>) => void = (step, m) => {
335
+ console.log(`Step ${step}:`, m.context);
336
+ }
337
+ ): T {
338
+ const generator = flow(initial);
339
+ let current = initial;
340
+ let stepCount = 0;
341
+
342
+ logger(stepCount, current);
343
+
344
+ while (true) {
345
+ const { value, done } = generator.next(current);
346
+
347
+ if (done) {
348
+ console.log('Final:', value);
349
+ return value;
350
+ }
351
+
352
+ current = value;
353
+ stepCount++;
354
+ logger(stepCount, current);
355
+ }
356
+ }
357
+
358
+ // =============================================================================
359
+ // ASYNC GENERATOR SUPPORT
360
+ // =============================================================================
361
+
362
+ /**
363
+ * Async version of `run` for async state machines.
364
+ *
365
+ * This allows you to use async/await inside your generator flows while maintaining
366
+ * the same compositional benefits.
367
+ *
368
+ * @template C - The context object type.
369
+ * @template T - The return type.
370
+ * @param flow - An async generator function.
371
+ * @param initial - The initial machine state.
372
+ * @returns A promise that resolves to the final value.
373
+ *
374
+ * @example
375
+ * ```typescript
376
+ * const result = await runAsync(async function* (m) {
377
+ * m = yield* stepAsync(await m.fetchData());
378
+ * m = yield* stepAsync(await m.processData());
379
+ * return m.context;
380
+ * }, asyncMachine);
381
+ * ```
382
+ */
383
+ export async function runAsync<C extends object, T>(
384
+ flow: (m: Machine<C>) => AsyncGenerator<Machine<C>, T, Machine<C>>,
385
+ initial: Machine<C>
386
+ ): Promise<T> {
387
+ const generator = flow(initial);
388
+ let current = initial;
389
+
390
+ while (true) {
391
+ const { value, done } = await generator.next(current);
392
+
393
+ if (done) {
394
+ return value;
395
+ }
396
+
397
+ current = value;
398
+ }
399
+ }
400
+
401
+ /**
402
+ * Async version of `step` for async generators.
403
+ *
404
+ * @template C - The context object type.
405
+ * @param m - The machine to yield.
406
+ * @returns An async generator.
407
+ *
408
+ * @example
409
+ * ```typescript
410
+ * await runAsync(async function* (m) {
411
+ * m = yield* stepAsync(await m.asyncOperation());
412
+ * return m;
413
+ * }, machine);
414
+ * ```
415
+ */
416
+ export async function* stepAsync<C extends object>(
417
+ m: Machine<C>
418
+ ): AsyncGenerator<Machine<C>, Machine<C>, Machine<C>> {
419
+ const received = yield m;
420
+ return received;
421
+ }