@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/generators.ts CHANGED
@@ -26,7 +26,7 @@
26
26
  * ```
27
27
  */
28
28
 
29
- import { Machine } from './index';
29
+
30
30
 
31
31
  /**
32
32
  * Runs a generator-based state machine flow to completion.
@@ -117,9 +117,9 @@ import { Machine } from './index';
117
117
  * }, machine);
118
118
  * ```
119
119
  */
120
- export function run<C extends object, T>(
121
- flow: (m: Machine<C>) => Generator<Machine<C>, T, Machine<C>>,
122
- initial: Machine<C>
120
+ export function run<C extends any = any, M extends { context: C } & Record<string, any> = { context: C }, T = any>(
121
+ flow: (m: M) => Generator<M, T, M>,
122
+ initial: M
123
123
  ): T {
124
124
  // Create the generator by calling the flow function with the initial machine
125
125
  const generator = flow(initial);
@@ -197,9 +197,9 @@ export function run<C extends object, T>(
197
197
  * }, machine);
198
198
  * ```
199
199
  */
200
- export function step<C extends object>(
201
- m: Machine<C>
202
- ): Generator<Machine<C>, Machine<C>, Machine<C>> {
200
+ export function step<C extends any = any, M extends { context: C } & Record<string, any> = { context: C }>(
201
+ m: M
202
+ ): Generator<M, M, M> {
203
203
  // Create an immediately-invoked generator that:
204
204
  // 1. Yields the provided machine
205
205
  // 2. Receives a value back (the next state)
@@ -229,7 +229,7 @@ export function step<C extends object>(
229
229
  * }, counter);
230
230
  * ```
231
231
  */
232
- export function yieldMachine<C extends object>(m: Machine<C>): Machine<C> {
232
+ export function yieldMachine<C extends any = any, M extends { context: C } & Record<string, any> = { context: C }>(m: M): M {
233
233
  return m;
234
234
  }
235
235
 
@@ -259,10 +259,10 @@ export function yieldMachine<C extends object>(m: Machine<C>): Machine<C> {
259
259
  * console.log(result.context.count); // 6
260
260
  * ```
261
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> {
262
+ export function runSequence<C extends any = any, M extends { context: C } & Record<string, any> = { context: C }>(
263
+ initial: M,
264
+ flows: Array<(m: M) => Generator<M, M, M>>
265
+ ): M {
266
266
  return flows.reduce((machine, flow) => {
267
267
  return run(flow, machine);
268
268
  }, initial);
@@ -295,9 +295,9 @@ export function runSequence<C extends object>(
295
295
  * }, counter);
296
296
  * ```
297
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>> {
298
+ export function createFlow<C extends any = any, M extends { context: C } & Record<string, any> = { context: C }>(
299
+ flow: (m: M) => Generator<M, M, M>
300
+ ): (m: M) => Generator<M, M, M> {
301
301
  return flow;
302
302
  }
303
303
 
@@ -328,10 +328,10 @@ export function createFlow<C extends object>(
328
328
  * // Final: 6
329
329
  * ```
330
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) => {
331
+ export function runWithDebug<C extends any = any, M extends { context: C } & Record<string, any> = { context: C }, T = any>(
332
+ flow: (m: M) => Generator<M, T, M>,
333
+ initial: M,
334
+ logger: (step: number, machine: M) => void = (step, m) => {
335
335
  console.log(`Step ${step}:`, m.context);
336
336
  }
337
337
  ): T {
@@ -380,9 +380,9 @@ export function runWithDebug<C extends object, T>(
380
380
  * }, asyncMachine);
381
381
  * ```
382
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>
383
+ export async function runAsync<C extends any = any, M extends { context: C } & Record<string, any> = { context: C }, T = any>(
384
+ flow: (m: M) => AsyncGenerator<M, T, M>,
385
+ initial: M
386
386
  ): Promise<T> {
387
387
  const generator = flow(initial);
388
388
  let current = initial;
@@ -413,9 +413,9 @@ export async function runAsync<C extends object, T>(
413
413
  * }, machine);
414
414
  * ```
415
415
  */
416
- export async function* stepAsync<C extends object>(
417
- m: Machine<C>
418
- ): AsyncGenerator<Machine<C>, Machine<C>, Machine<C>> {
416
+ export async function* stepAsync<C extends any = any, M extends { context: C } & Record<string, any> = { context: C }>(
417
+ m: M
418
+ ): AsyncGenerator<M, M, M> {
419
419
  const received = yield m;
420
420
  return received;
421
421
  }
@@ -0,0 +1,364 @@
1
+ /**
2
+ * @file Higher-Level Abstractions for @doeixd/machine
3
+ * @description
4
+ * This module provides a collection of powerful, pre-built patterns and primitives
5
+ * on top of the core `@doeixd/machine` library. These utilities are designed to
6
+ * solve common, recurring problems in state management, such as data fetching,
7
+ * hierarchical state, and toggling boolean context properties.
8
+ *
9
+ * Think of this as the "standard library" of common machine patterns.
10
+ */
11
+
12
+ import {
13
+ MachineBase,
14
+ Machine,
15
+ Transitions,
16
+ // AsyncMachine,
17
+ setContext,
18
+ Context,
19
+ // MaybePromise,
20
+ } from './index'; // Assuming this is a sibling package or in the same project
21
+
22
+ // =============================================================================
23
+ // SECTION 1: CUSTOM PRIMITIVES FOR COMPOSITION
24
+ // =============================================================================
25
+
26
+ /**
27
+ * A type utility to infer the child machine type from a parent.
28
+ */
29
+ type ChildMachine<P> = P extends MachineBase<{ child: infer C }> ? C : never;
30
+
31
+ /**
32
+ * Creates a transition method that delegates a call to a child machine.
33
+ *
34
+ * This is a higher-order function that reduces boilerplate when implementing
35
+ * hierarchical state machines. It generates a method for the parent machine that:
36
+ * 1. Checks if the specified action exists on the current child state.
37
+ * 2. If it exists, calls the action on the child.
38
+ * 3. Reconstructs the parent machine with the new child state returned by the action.
39
+ * 4. If the action doesn't exist on the child, it returns the parent machine unchanged.
40
+ *
41
+ * @template P - The parent machine type, which must have a `child` property in its context.
42
+ * @template K - The name of the action on the child machine to delegate to.
43
+ * @param actionName - The string name of the child's transition method.
44
+ * @param ...args - Any arguments to pass to the child's transition method.
45
+ * @returns The parent machine instance, with its `child` state potentially updated.
46
+ *
47
+ * @example
48
+ * ```typescript
49
+ * class Parent extends MachineBase<{ child: ChildMachine }> {
50
+ * // Instead of writing a manual delegation method...
51
+ * // save = () => {
52
+ * // if ('save' in this.context.child) {
53
+ * // const newChild = this.context.child.save();
54
+ * // return setContext(this, { child: newChild });
55
+ * // }
56
+ * // return this;
57
+ * // }
58
+ *
59
+ * // ...you can just use the primitive.
60
+ * save = delegateToChild('save');
61
+ * edit = delegateToChild('edit');
62
+ * }
63
+ * ```
64
+ */
65
+ export function delegateToChild<
66
+ P extends MachineBase<{ child: MachineBase<any> }>,
67
+ K extends keyof ChildMachine<P> & string
68
+ >(
69
+ actionName: K
70
+ ): (
71
+ ...args: ChildMachine<P>[K] extends (...a: infer A) => any ? A : never
72
+ ) => P {
73
+ return function(this: P, ...args: any[]): P {
74
+ const child = this.context.child as any;
75
+
76
+ if (typeof child[actionName] === 'function') {
77
+ const newChildState = child[actionName](...args);
78
+ return setContext(this as any, { ...this.context, child: newChildState }) as P;
79
+ }
80
+
81
+ // If the action is not available on the current child state, do nothing.
82
+ return this;
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Creates a transition method that toggles a boolean property within the machine's context.
88
+ *
89
+ * This is a simple utility to reduce boilerplate for managing boolean flags.
90
+ *
91
+ * @template M - The machine type.
92
+ * @template K - The key of the boolean property in the machine's context.
93
+ * @param prop - The string name of the context property to toggle.
94
+ * @returns A new machine instance with the toggled property.
95
+ *
96
+ * @example
97
+ * ```typescript
98
+ * class SettingsMachine extends MachineBase<{ notifications: boolean; darkMode: boolean }> {
99
+ * toggleNotifications = toggle('notifications');
100
+ * toggleDarkMode = toggle('darkMode');
101
+ * }
102
+ * ```
103
+ */
104
+ export function toggle<
105
+ M extends MachineBase<any>,
106
+ K extends keyof Context<M>
107
+ >(
108
+ prop: K
109
+ ): (this: M) => M {
110
+ return function(this: M): M {
111
+ // Ensure the property is boolean-like for a sensible toggle
112
+ if (typeof this.context[prop] !== 'boolean') {
113
+ console.warn(`[toggle primitive] Property '${String(prop)}' is not a boolean. Toggling may have unexpected results.`);
114
+ }
115
+ return setContext(this as any, {
116
+ ...this.context,
117
+ [prop]: !this.context[prop],
118
+ }) as M;
119
+ };
120
+ }
121
+
122
+
123
+ // =============================================================================
124
+ // SECTION 2: PRE-BUILT, CUSTOMIZABLE MACHINES
125
+ // =============================================================================
126
+
127
+ /**
128
+ * A fully-featured, pre-built state machine for data fetching.
129
+ * It handles loading, success, error states, cancellation, and retry logic out of the box.
130
+ *
131
+ * This machine is highly customizable through its configuration options.
132
+ */
133
+
134
+ // --- Types for the Fetch Machine ---
135
+
136
+ export type Fetcher<T, _E = Error> = (params: any) => Promise<T>;
137
+ export type OnSuccess<T> = (data: T) => void;
138
+ export type OnError<E> = (error: E) => void;
139
+
140
+ export interface FetchMachineConfig<T, E = Error> {
141
+ fetcher: Fetcher<T, E>;
142
+ initialParams?: any;
143
+ maxRetries?: number;
144
+ onSuccess?: OnSuccess<T>;
145
+ onError?: OnError<E>;
146
+ }
147
+
148
+ // --- Contexts for Fetch States ---
149
+ type IdleContext = { status: 'idle' };
150
+ type LoadingContext = { status: 'loading'; abortController: AbortController; attempts: number };
151
+ type RetryingContext = { status: 'retrying'; error: any; attempts: number };
152
+ type SuccessContext<T> = { status: 'success'; data: T };
153
+ type ErrorContext<E> = { status: 'error'; error: E };
154
+ type CanceledContext = { status: 'canceled' };
155
+
156
+ // --- Machine State Classes (internal) ---
157
+
158
+ class IdleMachine<T, E> extends MachineBase<IdleContext> {
159
+ constructor(private config: FetchMachineConfig<T, E>) { super({ status: 'idle' }); }
160
+ fetch = (params?: any) => new LoadingMachine(this.config, params ?? this.config.initialParams, 1);
161
+ }
162
+
163
+ class LoadingMachine<T, E> extends MachineBase<LoadingContext> {
164
+ constructor(private config: FetchMachineConfig<T, E>, private params: any, attempts: number) {
165
+ super({ status: 'loading', abortController: new AbortController(), attempts });
166
+ this.execute(); // Auto-execute on creation
167
+ }
168
+
169
+ private async execute() {
170
+ // This is a "fire-and-forget" call that transitions the machine internally.
171
+ // In a real implementation, this would be managed by an external runner.
172
+ // For this example, we assume an external mechanism calls `succeed`, `fail`, etc.
173
+ }
174
+
175
+ succeed = (data: T) => {
176
+ this.config.onSuccess?.(data);
177
+ return new SuccessMachine<T, E>(this.config, { status: 'success', data });
178
+ };
179
+
180
+ fail = (error: E) => {
181
+ const maxRetries = this.config.maxRetries ?? 3;
182
+ if (this.context.attempts < maxRetries) {
183
+ return new RetryingMachine<T, E>(this.config, this.params, error, this.context.attempts);
184
+ }
185
+ this.config.onError?.(error);
186
+ return new ErrorMachine<T, E>(this.config, { status: 'error', error });
187
+ };
188
+
189
+ cancel = () => {
190
+ this.context.abortController.abort();
191
+ return new CanceledMachine<T, E>(this.config);
192
+ };
193
+ }
194
+
195
+ class RetryingMachine<T, E> extends MachineBase<RetryingContext> {
196
+ constructor(private config: FetchMachineConfig<T, E>, private params: any, error: E, attempts: number) {
197
+ super({ status: 'retrying', error, attempts });
198
+ // In a real implementation, you'd have a delay here (e.g., exponential backoff)
199
+ // before transitioning to LoadingMachine again.
200
+ }
201
+
202
+ // This would be called after a delay.
203
+ retry = (params?: any) => new LoadingMachine<T, E>(this.config, params ?? this.params, this.context.attempts + 1);
204
+ }
205
+
206
+ class SuccessMachine<T, E> extends MachineBase<SuccessContext<T>> {
207
+ constructor(private config: FetchMachineConfig<T, E>, context: SuccessContext<T>) { super(context); }
208
+ refetch = (params?: any) => new LoadingMachine(this.config, params ?? this.config.initialParams, 1);
209
+ }
210
+
211
+ class ErrorMachine<T, E> extends MachineBase<ErrorContext<E>> {
212
+ constructor(private config: FetchMachineConfig<T, E>, context: ErrorContext<E>) { super(context); }
213
+ retry = (params?: any) => new LoadingMachine(this.config, params ?? this.config.initialParams, 1);
214
+ }
215
+
216
+ class CanceledMachine<T, E> extends MachineBase<CanceledContext> {
217
+ constructor(private config: FetchMachineConfig<T, E>) { super({ status: 'canceled' }); }
218
+ refetch = (params?: any) => new LoadingMachine(this.config, params ?? this.config.initialParams, 1);
219
+ }
220
+
221
+ export type FetchMachine<T, E = Error> =
222
+ | IdleMachine<T, E>
223
+ | LoadingMachine<T, E>
224
+ | RetryingMachine<T, E>
225
+ | SuccessMachine<T, E>
226
+ | ErrorMachine<T, E>
227
+ | CanceledMachine<T, E>;
228
+
229
+ /**
230
+ * Creates a pre-built, highly configurable async data-fetching machine.
231
+ *
232
+ * This factory function returns a state machine that handles the entire lifecycle
233
+ * of a data request, including loading, success, error, cancellation, and retries.
234
+ *
235
+ * @template T - The type of the data to be fetched.
236
+ * @template E - The type of the error.
237
+ * @param config - Configuration object.
238
+ * @param config.fetcher - An async function that takes params and returns the data.
239
+ * @param [config.maxRetries=3] - The number of times to retry on failure.
240
+ * @param [config.onSuccess] - Optional callback fired with the data on success.
241
+ * @param [config.onError] - Optional callback fired with the error on final failure.
242
+ * @returns An `IdleMachine` instance, ready to start fetching.
243
+ *
244
+ * @example
245
+ * ```typescript
246
+ * // 1. Define your data fetching logic
247
+ * async function fetchUser(id: number): Promise<{ id: number; name: string }> {
248
+ * const res = await fetch(`/api/users/${id}`);
249
+ * if (!res.ok) throw new Error('User not found');
250
+ * return res.json();
251
+ * }
252
+ *
253
+ * // 2. Create the machine
254
+ * const userMachine = createFetchMachine({
255
+ * fetcher: fetchUser,
256
+ * onSuccess: (user) => console.log(`Fetched: ${user.name}`),
257
+ * });
258
+ *
259
+ * // 3. Use it (e.g., in a React hook)
260
+ * // let machine = userMachine;
261
+ * // machine = await machine.fetch(123); // Transitions to Loading, then Success/Error
262
+ * ```
263
+ *
264
+ * @note This is a simplified example. For a real-world implementation, you would
265
+ * typically use this machine with a runner (like `runMachine` or `useMachine`) to
266
+ * manage the async transitions and state updates automatically.
267
+ */
268
+ export function createFetchMachine<T, E = Error>(
269
+ config: FetchMachineConfig<T, E>
270
+ ): FetchMachine<T, E> {
271
+ // A more robust implementation would validate the config here.
272
+ return new IdleMachine<T, E>(config);
273
+ }
274
+
275
+ /**
276
+ * The core type for a Parallel Machine.
277
+ * It combines two machines, M1 and M2, into a single, unified type.
278
+ * @template M1 - The first machine in the parallel composition.
279
+ * @template M2 - The second machine in the parallel composition.
280
+ */
281
+ export type ParallelMachine<
282
+ M1 extends Machine<any>,
283
+ M2 extends Machine<any>
284
+ > = Machine<Context<M1> & Context<M2>> & {
285
+ // Map transitions from M1. When called, they return a new ParallelMachine
286
+ // where M1 has transitioned but M2 remains the same.
287
+ [K in keyof Transitions<M1>]: Transitions<M1>[K] extends (...args: infer A) => infer R
288
+ ? R extends Machine<any>
289
+ ? (...args: A) => ParallelMachine<R, M2>
290
+ : never
291
+ : never;
292
+ } & {
293
+ // Map transitions from M2. When called, they return a new ParallelMachine
294
+ // where M2 has transitioned but M1 remains the same.
295
+ [K in keyof Transitions<M2>]: Transitions<M2>[K] extends (...args: infer A) => infer R
296
+ ? R extends Machine<any>
297
+ ? (...args: A) => ParallelMachine<M1, R>
298
+ : never
299
+ : never;
300
+ };
301
+
302
+
303
+ /**
304
+ * Creates a parallel machine by composing two independent machines.
305
+ *
306
+ * This function takes two machines and merges them into a single machine entity.
307
+ * Transitions from either machine can be called, and they will only affect
308
+ * their respective part of the combined state.
309
+ *
310
+ * NOTE: This primitive assumes that the transition names between the two
311
+ * machines do not collide. If both machines have a transition named `next`,
312
+ * the behavior is undefined.
313
+ *
314
+ * @param m1 The first machine instance.
315
+ * @param m2 The second machine instance.
316
+ * @returns A new ParallelMachine instance.
317
+ */
318
+ export function createParallelMachine<
319
+ M1 extends Machine<any>,
320
+ M2 extends Machine<any>
321
+ >(m1: M1, m2: M2): ParallelMachine<M1, M2> {
322
+ // 1. Combine the contexts
323
+ const combinedContext = { ...m1.context, ...m2.context };
324
+
325
+ const transitions1 = { ...m1 } as Transitions<M1>;
326
+ const transitions2 = { ...m2 } as Transitions<M2>;
327
+ delete (transitions1 as any).context;
328
+ delete (transitions2 as any).context;
329
+
330
+ const combinedTransitions = {} as any;
331
+
332
+ // 2. Re-wire transitions from the first machine
333
+ for (const key in transitions1) {
334
+ const transitionFn = (transitions1 as any)[key];
335
+ combinedTransitions[key] = (...args: any[]) => {
336
+ const nextM1 = transitionFn.apply(m1.context, args);
337
+ // Recursively create a new parallel machine with the new M1 state
338
+ return createParallelMachine(nextM1, m2);
339
+ };
340
+ }
341
+
342
+ // 3. Re-wire transitions from the second machine
343
+ for (const key in transitions2) {
344
+ const transitionFn = (transitions2 as any)[key];
345
+ combinedTransitions[key] = (...args: any[]) => {
346
+ const nextM2 = transitionFn.apply(m2.context, args);
347
+ // Recursively create a new parallel machine with the new M2 state
348
+ return createParallelMachine(m1, nextM2);
349
+ };
350
+ }
351
+
352
+ return {
353
+ context: combinedContext,
354
+ ...combinedTransitions,
355
+ } as ParallelMachine<M1, M2>;
356
+ }
357
+
358
+ // A mapped type that transforms the return types of a machine's transitions.
359
+ // For a transition that returns `NewMachineState`, this will transform it to return `T`.
360
+ export type RemapTransitions<M extends Machine<any>, T> = {
361
+ [K in keyof Transitions<M>]: Transitions<M>[K] extends (...args: infer A) => any
362
+ ? (...args: A) => T
363
+ : never;
364
+ };