@doeixd/machine 0.0.6 → 0.0.7
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/README.md +158 -0
- package/dist/cjs/development/index.js +275 -5
- package/dist/cjs/development/index.js.map +4 -4
- package/dist/cjs/production/index.js +5 -5
- package/dist/esm/development/index.js +275 -5
- package/dist/esm/development/index.js.map +4 -4
- package/dist/esm/production/index.js +5 -5
- package/dist/types/extract.d.ts +40 -4
- package/dist/types/extract.d.ts.map +1 -1
- package/dist/types/generators.d.ts +40 -9
- package/dist/types/generators.d.ts.map +1 -1
- package/dist/types/higher-order.d.ts +221 -0
- package/dist/types/higher-order.d.ts.map +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/utils.d.ts +208 -0
- package/dist/types/utils.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/extract.ts +180 -8
- package/src/generators.ts +25 -25
- package/src/higher-order.ts +364 -0
- package/src/index.ts +18 -1
- package/src/utils.ts +171 -5
|
@@ -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
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -574,4 +574,21 @@ export {
|
|
|
574
574
|
export { RUNTIME_META, type RuntimeTransitionMeta } from './primitives';
|
|
575
575
|
|
|
576
576
|
|
|
577
|
-
export * from './multi'
|
|
577
|
+
export * from './multi'
|
|
578
|
+
|
|
579
|
+
export * from './higher-order'
|
|
580
|
+
|
|
581
|
+
// =============================================================================
|
|
582
|
+
// SECTION: UTILITIES & HELPERS
|
|
583
|
+
// =============================================================================
|
|
584
|
+
|
|
585
|
+
export {
|
|
586
|
+
isState,
|
|
587
|
+
createEvent,
|
|
588
|
+
mergeContext,
|
|
589
|
+
pipeTransitions,
|
|
590
|
+
logState,
|
|
591
|
+
call,
|
|
592
|
+
bindTransitions,
|
|
593
|
+
BoundMachine
|
|
594
|
+
} from './utils';
|
package/src/utils.ts
CHANGED
|
@@ -158,10 +158,176 @@ export async function pipeTransitions<M extends AsyncMachine<any>>(
|
|
|
158
158
|
* );
|
|
159
159
|
*/
|
|
160
160
|
export function logState<M extends Machine<any>>(machine: M, label?: string): M {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
161
|
+
if (label) {
|
|
162
|
+
console.log(label, machine.context);
|
|
163
|
+
} else {
|
|
164
|
+
console.log(machine.context);
|
|
165
|
+
}
|
|
166
|
+
return machine;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// =============================================================================
|
|
170
|
+
// SECTION: TRANSITION BINDING HELPERS
|
|
171
|
+
// =============================================================================
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Calls a transition function with an explicit `this` context.
|
|
175
|
+
* Useful for invoking transition methods with proper context binding.
|
|
176
|
+
*
|
|
177
|
+
* @template C - The context type that the function expects as `this`.
|
|
178
|
+
* @template F - The function type with a `this` parameter.
|
|
179
|
+
* @template A - The argument types for the function.
|
|
180
|
+
* @param fn - The transition function to call.
|
|
181
|
+
* @param context - The context object to bind as `this`.
|
|
182
|
+
* @param args - Arguments to pass to the function.
|
|
183
|
+
* @returns The result of calling the function with the given context and arguments.
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* type MyContext = { count: number };
|
|
187
|
+
* const increment = function(this: MyContext) { return this.count + 1; };
|
|
188
|
+
* const result = call(increment, { count: 5 }); // Returns 6
|
|
189
|
+
*
|
|
190
|
+
* // Particularly useful with machine transitions:
|
|
191
|
+
* import { call } from '@doeixd/machine/utils';
|
|
192
|
+
* const nextMachine = yield* step(call(m.increment, m.context));
|
|
193
|
+
*/
|
|
194
|
+
export function call<C, F extends (this: C, ...args: any[]) => any>(
|
|
195
|
+
fn: F,
|
|
196
|
+
context: C,
|
|
197
|
+
...args: Parameters<F> extends [any, ...infer Rest] ? Rest : never
|
|
198
|
+
): ReturnType<F> {
|
|
199
|
+
return fn.apply(context, args);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Binds all transition methods of a machine to its context automatically.
|
|
204
|
+
* Returns a Proxy that intercepts method calls and binds them to `machine.context`.
|
|
205
|
+
* This eliminates the need to use `.call(m.context, ...)` for every transition.
|
|
206
|
+
*
|
|
207
|
+
* Automatically recursively wraps returned machines, enabling seamless chaining
|
|
208
|
+
* in generator-based flows.
|
|
209
|
+
*
|
|
210
|
+
* @template M - The machine type with a `context` property and transition methods.
|
|
211
|
+
* @param machine - The machine instance to wrap.
|
|
212
|
+
* @returns A Proxy of the machine where all callable properties (transitions) are automatically bound to the machine's context.
|
|
213
|
+
*
|
|
214
|
+
* @example
|
|
215
|
+
* type CounterContext = { count: number };
|
|
216
|
+
* const counter = bindTransitions(createMachine({ count: 0 }, {
|
|
217
|
+
* increment(this: CounterContext) { return createCounter(this.count + 1); }
|
|
218
|
+
* }));
|
|
219
|
+
*
|
|
220
|
+
* // Now you can call transitions directly without .call():
|
|
221
|
+
* const next = counter.increment(); // Works! This is automatically bound.
|
|
222
|
+
*
|
|
223
|
+
* // Particularly useful with generators:
|
|
224
|
+
* const result = run(function* (m) {
|
|
225
|
+
* m = yield* step(m.increment()); // Clean syntax
|
|
226
|
+
* m = yield* step(m.add(5)); // No .call() needed
|
|
227
|
+
* return m;
|
|
228
|
+
* }, bindTransitions(counter));
|
|
229
|
+
*
|
|
230
|
+
* @remarks
|
|
231
|
+
* The Proxy preserves all original properties and methods. Non-callable properties
|
|
232
|
+
* are accessed directly from the machine. Callable properties are wrapped to bind
|
|
233
|
+
* them to `machine.context` before invocation. Returned machines are automatically
|
|
234
|
+
* re-wrapped to maintain binding across transition chains.
|
|
235
|
+
*/
|
|
236
|
+
export function bindTransitions<M extends { context: any }>(machine: M): M {
|
|
237
|
+
return new Proxy(machine, {
|
|
238
|
+
get(target, prop) {
|
|
239
|
+
const value = target[prop as keyof M];
|
|
240
|
+
|
|
241
|
+
// If it's a callable property (transition method), bind it to context
|
|
242
|
+
if (typeof value === 'function') {
|
|
243
|
+
return function(...args: any[]) {
|
|
244
|
+
const result = value.apply(target.context, args);
|
|
245
|
+
// Recursively wrap returned machines to maintain binding
|
|
246
|
+
if (result && typeof result === 'object' && 'context' in result) {
|
|
247
|
+
return bindTransitions(result);
|
|
248
|
+
}
|
|
249
|
+
return result;
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Otherwise, return the value as-is
|
|
254
|
+
return value;
|
|
255
|
+
},
|
|
256
|
+
}) as M;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* A strongly-typed wrapper class for binding transitions to machine context.
|
|
261
|
+
* Unlike the Proxy-based `bindTransitions`, this class preserves full type safety
|
|
262
|
+
* and provides better IDE support through explicit property forwarding.
|
|
263
|
+
*
|
|
264
|
+
* @template M - The machine type with a `context` property and transition methods.
|
|
265
|
+
*
|
|
266
|
+
* @example
|
|
267
|
+
* type CounterContext = { count: number };
|
|
268
|
+
* const counter = createMachine({ count: 0 }, {
|
|
269
|
+
* increment(this: CounterContext) { return createCounter(this.count + 1); }
|
|
270
|
+
* });
|
|
271
|
+
*
|
|
272
|
+
* const bound = new BoundMachine(counter);
|
|
273
|
+
*
|
|
274
|
+
* // All transitions are automatically bound to context
|
|
275
|
+
* const result = run(function* (m) {
|
|
276
|
+
* m = yield* step(m.increment());
|
|
277
|
+
* m = yield* step(m.add(5));
|
|
278
|
+
* return m.context.count;
|
|
279
|
+
* }, bound);
|
|
280
|
+
*
|
|
281
|
+
* @remarks
|
|
282
|
+
* Advantages over Proxy-based `bindTransitions`:
|
|
283
|
+
* - Full type safety with TypeScript's type system
|
|
284
|
+
* - Returned machines are automatically re-wrapped
|
|
285
|
+
* - Better IDE autocompletion and hover information
|
|
286
|
+
* - No type casting needed
|
|
287
|
+
*
|
|
288
|
+
* Disadvantages:
|
|
289
|
+
* - Requires explicit instance creation: `new BoundMachine(m)` vs `bindTransitions(m)`
|
|
290
|
+
* - Not a transparent drop-in replacement for the original machine
|
|
291
|
+
*/
|
|
292
|
+
export class BoundMachine<M extends { context: any }> {
|
|
293
|
+
private readonly wrappedMachine: M;
|
|
294
|
+
[key: string | symbol]: any;
|
|
295
|
+
|
|
296
|
+
constructor(machine: M) {
|
|
297
|
+
this.wrappedMachine = machine;
|
|
298
|
+
|
|
299
|
+
// Create a proxy to intercept property access
|
|
300
|
+
return new Proxy(this, {
|
|
301
|
+
get: (target, prop) => {
|
|
302
|
+
// Handle direct property access to wrapped machine
|
|
303
|
+
if (prop === 'wrappedMachine' || prop === 'context') {
|
|
304
|
+
return Reflect.get(target, prop);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const value = this.wrappedMachine[prop as keyof M];
|
|
308
|
+
|
|
309
|
+
// Bind transition methods to context
|
|
310
|
+
if (typeof value === 'function') {
|
|
311
|
+
return (...args: any[]) => {
|
|
312
|
+
const result = value.apply(this.wrappedMachine.context, args);
|
|
313
|
+
// Recursively wrap returned machines
|
|
314
|
+
if (result && typeof result === 'object' && 'context' in result) {
|
|
315
|
+
return new BoundMachine(result);
|
|
316
|
+
}
|
|
317
|
+
return result;
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Return non-function properties directly
|
|
322
|
+
return value;
|
|
323
|
+
},
|
|
324
|
+
}) as any;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Access the underlying machine's context directly.
|
|
329
|
+
*/
|
|
330
|
+
get context(): M extends { context: infer C } ? C : never {
|
|
331
|
+
return this.wrappedMachine.context;
|
|
165
332
|
}
|
|
166
|
-
return machine;
|
|
167
333
|
}
|