@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.
- package/LICENSE +7 -0
- package/README.md +1070 -0
- package/dist/cjs/development/index.js +198 -0
- package/dist/cjs/development/index.js.map +7 -0
- package/dist/cjs/production/index.js +1 -0
- package/dist/esm/development/index.js +175 -0
- package/dist/esm/development/index.js.map +7 -0
- package/dist/esm/production/index.js +1 -0
- package/dist/types/generators.d.ts +314 -0
- package/dist/types/generators.d.ts.map +1 -0
- package/dist/types/index.d.ts +339 -0
- package/dist/types/index.d.ts.map +1 -0
- package/package.json +110 -0
- package/src/devtools.ts +74 -0
- package/src/extract.ts +190 -0
- package/src/generators.ts +421 -0
- package/src/index.ts +528 -0
- package/src/primitives.ts +191 -0
- package/src/react.ts +44 -0
- package/src/solid.ts +502 -0
- package/src/test.ts +207 -0
- package/src/utils.ts +167 -0
|
@@ -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
|
+
}
|