@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
package/src/solid.ts
ADDED
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Solid.js integration for @doeixd/machine
|
|
3
|
+
* @description
|
|
4
|
+
* Provides reactive primitives for using state machines with Solid.js, including
|
|
5
|
+
* hooks for both sync and async machines, store integration, and signal-based APIs.
|
|
6
|
+
*
|
|
7
|
+
* Solid.js uses fine-grained reactivity with signals and stores, which pairs
|
|
8
|
+
* beautifully with immutable state machines. This integration provides multiple
|
|
9
|
+
* approaches depending on your needs:
|
|
10
|
+
*
|
|
11
|
+
* - `createMachine()` - Signal-based reactive machine
|
|
12
|
+
* - `createMachineStore()` - Store-based reactive machine (for complex context)
|
|
13
|
+
* - `createAsyncMachine()` - Async machine with signal state
|
|
14
|
+
* - `createMachineResource()` - Resource-based async machine
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
createSignal,
|
|
19
|
+
createEffect,
|
|
20
|
+
createMemo,
|
|
21
|
+
onCleanup,
|
|
22
|
+
type Accessor,
|
|
23
|
+
type Setter
|
|
24
|
+
} from 'solid-js';
|
|
25
|
+
import { createStore, type SetStoreFunction, type Store, produce } from 'solid-js/store';
|
|
26
|
+
import { Machine, AsyncMachine, Event, Context, runMachine as runMachineCore } from './index';
|
|
27
|
+
|
|
28
|
+
// =============================================================================
|
|
29
|
+
// SIGNAL-BASED MACHINE (for simple state)
|
|
30
|
+
// =============================================================================
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Creates a reactive machine using Solid signals.
|
|
34
|
+
*
|
|
35
|
+
* This is ideal for simple state machines where the entire machine state
|
|
36
|
+
* needs to be tracked reactively. Every transition creates a new machine
|
|
37
|
+
* instance, and the signal updates automatically.
|
|
38
|
+
*
|
|
39
|
+
* @template M - The machine type.
|
|
40
|
+
*
|
|
41
|
+
* @param initialMachine - A function that returns the initial machine state.
|
|
42
|
+
* @returns A tuple of [accessor, transitions] where:
|
|
43
|
+
* - accessor: Reactive accessor for the current machine
|
|
44
|
+
* - transitions: Object with all machine transitions bound to update the signal
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```tsx
|
|
48
|
+
* const [machine, actions] = createMachine(() =>
|
|
49
|
+
* createCounterMachine({ count: 0 })
|
|
50
|
+
* );
|
|
51
|
+
*
|
|
52
|
+
* // In your component
|
|
53
|
+
* <div>
|
|
54
|
+
* <p>Count: {machine().context.count}</p>
|
|
55
|
+
* <button onClick={actions.increment}>Increment</button>
|
|
56
|
+
* </div>
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* @example With Type-State
|
|
60
|
+
* ```tsx
|
|
61
|
+
* type LoggedOut = Machine<{ status: "loggedOut" }> & {
|
|
62
|
+
* login: (user: string) => LoggedIn;
|
|
63
|
+
* };
|
|
64
|
+
*
|
|
65
|
+
* type LoggedIn = Machine<{ status: "loggedIn"; user: string }> & {
|
|
66
|
+
* logout: () => LoggedOut;
|
|
67
|
+
* };
|
|
68
|
+
*
|
|
69
|
+
* const [auth, actions] = createMachine<LoggedOut | LoggedIn>(() =>
|
|
70
|
+
* createLoggedOut()
|
|
71
|
+
* );
|
|
72
|
+
*
|
|
73
|
+
* // Conditional rendering based on state type
|
|
74
|
+
* <Show when={auth().context.status === 'loggedIn'}>
|
|
75
|
+
* <p>Welcome, {auth().context.user}</p>
|
|
76
|
+
* <button onClick={actions.logout}>Logout</button>
|
|
77
|
+
* </Show>
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
export function createMachine<M extends Machine<any>>(
|
|
81
|
+
initialMachine: () => M
|
|
82
|
+
): [Accessor<M>, TransitionHandlers<M>] {
|
|
83
|
+
const [machine, setMachine] = createSignal<M>(initialMachine());
|
|
84
|
+
|
|
85
|
+
// Extract all transition methods and bind them to update the signal
|
|
86
|
+
const { context, ...transitions } = machine();
|
|
87
|
+
|
|
88
|
+
const handlers = Object.fromEntries(
|
|
89
|
+
Object.entries(transitions).map(([key, fn]) => [
|
|
90
|
+
key,
|
|
91
|
+
(...args: any[]) => {
|
|
92
|
+
const currentMachine = machine();
|
|
93
|
+
const nextMachine = (currentMachine as any)[key](...args);
|
|
94
|
+
setMachine(() => nextMachine);
|
|
95
|
+
return nextMachine;
|
|
96
|
+
}
|
|
97
|
+
])
|
|
98
|
+
) as TransitionHandlers<M>;
|
|
99
|
+
|
|
100
|
+
return [machine, handlers];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Helper type to extract transition handlers from a machine.
|
|
105
|
+
*/
|
|
106
|
+
type TransitionHandlers<M extends Machine<any>> = {
|
|
107
|
+
[K in keyof Omit<M, 'context'>]: M[K] extends (...args: infer Args) => infer R
|
|
108
|
+
? (...args: Args) => R
|
|
109
|
+
: never;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// =============================================================================
|
|
113
|
+
// STORE-BASED MACHINE (for complex context with fine-grained reactivity)
|
|
114
|
+
// =============================================================================
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Creates a reactive machine using Solid stores.
|
|
118
|
+
*
|
|
119
|
+
* This is ideal when you have complex nested context and want fine-grained
|
|
120
|
+
* reactivity on individual properties. Instead of replacing the entire machine,
|
|
121
|
+
* transitions update the store, triggering only the affected computations.
|
|
122
|
+
*
|
|
123
|
+
* @template M - The machine type.
|
|
124
|
+
*
|
|
125
|
+
* @param initialMachine - A function that returns the initial machine state.
|
|
126
|
+
* @returns A tuple of [store, actions] where:
|
|
127
|
+
* - store: Reactive store proxy for the machine
|
|
128
|
+
* - actions: Transition handlers that update the store
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* ```tsx
|
|
132
|
+
* const [machine, actions] = createMachineStore(() =>
|
|
133
|
+
* createUserMachine({
|
|
134
|
+
* profile: { name: 'Alice', age: 30 },
|
|
135
|
+
* settings: { theme: 'dark', notifications: true }
|
|
136
|
+
* })
|
|
137
|
+
* );
|
|
138
|
+
*
|
|
139
|
+
* // Fine-grained reactivity - only updates when profile.name changes
|
|
140
|
+
* <div>
|
|
141
|
+
* <p>Name: {machine.context.profile.name}</p>
|
|
142
|
+
* <p>Age: {machine.context.profile.age}</p>
|
|
143
|
+
* <button onClick={() => actions.updateName('Bob')}>Change Name</button>
|
|
144
|
+
* </div>
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
export function createMachineStore<M extends Machine<any>>(
|
|
148
|
+
initialMachine: () => M
|
|
149
|
+
): [Store<M>, SetStoreFunction<M>, TransitionHandlers<M>] {
|
|
150
|
+
const initial = initialMachine();
|
|
151
|
+
const [store, setStore] = createStore<M>(initial);
|
|
152
|
+
|
|
153
|
+
const { context, ...transitions } = initial;
|
|
154
|
+
|
|
155
|
+
const handlers = Object.fromEntries(
|
|
156
|
+
Object.entries(transitions).map(([key, fn]) => [
|
|
157
|
+
key,
|
|
158
|
+
(...args: any[]) => {
|
|
159
|
+
const nextMachine = (store as any)[key](...args);
|
|
160
|
+
setStore(() => nextMachine);
|
|
161
|
+
return nextMachine;
|
|
162
|
+
}
|
|
163
|
+
])
|
|
164
|
+
) as TransitionHandlers<M>;
|
|
165
|
+
|
|
166
|
+
return [store, setStore, handlers];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// =============================================================================
|
|
170
|
+
// ASYNC MACHINE WITH SIGNALS
|
|
171
|
+
// =============================================================================
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Creates a reactive async machine with event dispatching.
|
|
175
|
+
*
|
|
176
|
+
* This wraps the core `runMachine` with Solid reactivity, automatically
|
|
177
|
+
* updating a signal whenever the machine state changes. Perfect for async
|
|
178
|
+
* workflows like data fetching, multi-step forms, or any stateful async logic.
|
|
179
|
+
*
|
|
180
|
+
* @template M - The async machine type.
|
|
181
|
+
*
|
|
182
|
+
* @param initialMachine - A function that returns the initial async machine state.
|
|
183
|
+
* @returns A tuple of [accessor, dispatch] where:
|
|
184
|
+
* - accessor: Reactive accessor for current machine state
|
|
185
|
+
* - dispatch: Type-safe event dispatcher
|
|
186
|
+
*
|
|
187
|
+
* @example Basic data fetching
|
|
188
|
+
* ```tsx
|
|
189
|
+
* type FetchMachine = AsyncMachine<{
|
|
190
|
+
* status: 'idle' | 'loading' | 'success' | 'error';
|
|
191
|
+
* data: any;
|
|
192
|
+
* }> & {
|
|
193
|
+
* fetch: () => Promise<FetchMachine>;
|
|
194
|
+
* retry: () => Promise<FetchMachine>;
|
|
195
|
+
* };
|
|
196
|
+
*
|
|
197
|
+
* const [state, dispatch] = createAsyncMachine(() => createFetchMachine());
|
|
198
|
+
*
|
|
199
|
+
* <div>
|
|
200
|
+
* <Switch>
|
|
201
|
+
* <Match when={state().context.status === 'idle'}>
|
|
202
|
+
* <button onClick={() => dispatch({ type: 'fetch', args: [] })}>
|
|
203
|
+
* Load Data
|
|
204
|
+
* </button>
|
|
205
|
+
* </Match>
|
|
206
|
+
* <Match when={state().context.status === 'loading'}>
|
|
207
|
+
* <p>Loading...</p>
|
|
208
|
+
* </Match>
|
|
209
|
+
* <Match when={state().context.status === 'success'}>
|
|
210
|
+
* <p>Data: {JSON.stringify(state().context.data)}</p>
|
|
211
|
+
* </Match>
|
|
212
|
+
* <Match when={state().context.status === 'error'}>
|
|
213
|
+
* <button onClick={() => dispatch({ type: 'retry', args: [] })}>
|
|
214
|
+
* Retry
|
|
215
|
+
* </button>
|
|
216
|
+
* </Match>
|
|
217
|
+
* </Switch>
|
|
218
|
+
* </div>
|
|
219
|
+
* ```
|
|
220
|
+
*
|
|
221
|
+
* @example With effects
|
|
222
|
+
* ```tsx
|
|
223
|
+
* const [state, dispatch] = createAsyncMachine(() => createAuthMachine());
|
|
224
|
+
*
|
|
225
|
+
* // React to state changes
|
|
226
|
+
* createEffect(() => {
|
|
227
|
+
* console.log('Auth state changed:', state().context.status);
|
|
228
|
+
*
|
|
229
|
+
* if (state().context.status === 'loggedIn') {
|
|
230
|
+
* // Navigate, fetch user data, etc.
|
|
231
|
+
* }
|
|
232
|
+
* });
|
|
233
|
+
* ```
|
|
234
|
+
*/
|
|
235
|
+
export function createAsyncMachine<M extends AsyncMachine<any>>(
|
|
236
|
+
initialMachine: () => M
|
|
237
|
+
): [Accessor<M>, (event: Event<M>) => Promise<M>] {
|
|
238
|
+
const [machine, setMachine] = createSignal<M>(initialMachine());
|
|
239
|
+
|
|
240
|
+
// Create the runner with signal update callback
|
|
241
|
+
const runner = runMachineCore(initialMachine(), (nextMachine) => {
|
|
242
|
+
setMachine(() => nextMachine as M);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const dispatch = async (event: Event<M>): Promise<M> => {
|
|
246
|
+
const result = await runner.dispatch(event);
|
|
247
|
+
return result as M;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
return [machine, dispatch];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// =============================================================================
|
|
254
|
+
// CONTEXT-ONLY STORE (for just the context data)
|
|
255
|
+
// =============================================================================
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Creates a Solid store for just the machine's context, with actions that
|
|
259
|
+
* transition the machine and sync the context back to the store.
|
|
260
|
+
*
|
|
261
|
+
* This is useful when you want fine-grained reactivity on context properties
|
|
262
|
+
* but don't need to track the machine instance itself.
|
|
263
|
+
*
|
|
264
|
+
* @template C - The context object type.
|
|
265
|
+
* @template M - The machine type.
|
|
266
|
+
*
|
|
267
|
+
* @param initialMachine - A function that returns the initial machine.
|
|
268
|
+
* @returns A tuple of [context store, setContext, actions].
|
|
269
|
+
*
|
|
270
|
+
* @example
|
|
271
|
+
* ```tsx
|
|
272
|
+
* const [context, setContext, actions] = createMachineContext(() =>
|
|
273
|
+
* createCounterMachine({ count: 0, name: 'Counter' })
|
|
274
|
+
* );
|
|
275
|
+
*
|
|
276
|
+
* // Direct access to context with fine-grained reactivity
|
|
277
|
+
* <div>
|
|
278
|
+
* <p>{context.name}: {context.count}</p>
|
|
279
|
+
* <button onClick={actions.increment}>+</button>
|
|
280
|
+
* </div>
|
|
281
|
+
* ```
|
|
282
|
+
*/
|
|
283
|
+
export function createMachineContext<C extends object, M extends Machine<C>>(
|
|
284
|
+
initialMachine: () => M
|
|
285
|
+
): [Store<C>, SetStoreFunction<C>, TransitionHandlers<M>] {
|
|
286
|
+
let currentMachine = initialMachine();
|
|
287
|
+
const [context, setContext] = createStore<C>(currentMachine.context);
|
|
288
|
+
|
|
289
|
+
const { context: _, ...transitions } = currentMachine;
|
|
290
|
+
|
|
291
|
+
const handlers = Object.fromEntries(
|
|
292
|
+
Object.entries(transitions).map(([key, fn]) => [
|
|
293
|
+
key,
|
|
294
|
+
(...args: any[]) => {
|
|
295
|
+
const nextMachine = (currentMachine as any)[key](...args);
|
|
296
|
+
currentMachine = nextMachine;
|
|
297
|
+
setContext(() => nextMachine.context);
|
|
298
|
+
return nextMachine;
|
|
299
|
+
}
|
|
300
|
+
])
|
|
301
|
+
) as TransitionHandlers<M>;
|
|
302
|
+
|
|
303
|
+
return [context, setContext, handlers];
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// =============================================================================
|
|
307
|
+
// MEMOIZED MACHINE DERIVATIONS
|
|
308
|
+
// =============================================================================
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Creates a memoized derivation from a machine's context.
|
|
312
|
+
*
|
|
313
|
+
* This is useful for computed values that depend on the machine state.
|
|
314
|
+
* The computation only re-runs when the accessed context properties change.
|
|
315
|
+
*
|
|
316
|
+
* @template M - The machine type.
|
|
317
|
+
* @template T - The computed value type.
|
|
318
|
+
*
|
|
319
|
+
* @param machine - Machine accessor.
|
|
320
|
+
* @param selector - Function to compute a value from the context.
|
|
321
|
+
* @returns A memoized accessor for the computed value.
|
|
322
|
+
*
|
|
323
|
+
* @example
|
|
324
|
+
* ```tsx
|
|
325
|
+
* const [machine, actions] = createMachine(() => createCart());
|
|
326
|
+
*
|
|
327
|
+
* const total = createMachineSelector(machine, (ctx) =>
|
|
328
|
+
* ctx.items.reduce((sum, item) => sum + item.price, 0)
|
|
329
|
+
* );
|
|
330
|
+
*
|
|
331
|
+
* <div>
|
|
332
|
+
* <p>Total: ${total()}</p>
|
|
333
|
+
* </div>
|
|
334
|
+
* ```
|
|
335
|
+
*/
|
|
336
|
+
export function createMachineSelector<M extends Machine<any>, T>(
|
|
337
|
+
machine: Accessor<M>,
|
|
338
|
+
selector: (context: Context<M>) => T
|
|
339
|
+
): Accessor<T> {
|
|
340
|
+
return createMemo(() => selector(machine().context));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// =============================================================================
|
|
344
|
+
// BATCH TRANSITIONS
|
|
345
|
+
// =============================================================================
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Batches multiple transitions into a single reactive update.
|
|
349
|
+
*
|
|
350
|
+
* In Solid, this uses `batch` to group updates, preventing intermediate
|
|
351
|
+
* re-renders and effects from firing.
|
|
352
|
+
*
|
|
353
|
+
* @template M - The machine type.
|
|
354
|
+
*
|
|
355
|
+
* @param machine - The current machine.
|
|
356
|
+
* @param setMachine - The setter function.
|
|
357
|
+
* @param transitions - Array of transition functions to apply.
|
|
358
|
+
* @returns The final machine state.
|
|
359
|
+
*
|
|
360
|
+
* @example
|
|
361
|
+
* ```tsx
|
|
362
|
+
* import { batch } from 'solid-js';
|
|
363
|
+
*
|
|
364
|
+
* const [machine, setMachine] = createSignal(createCounterMachine());
|
|
365
|
+
*
|
|
366
|
+
* const batchUpdate = () => {
|
|
367
|
+
* batch(() => {
|
|
368
|
+
* let m = machine();
|
|
369
|
+
* m = m.increment();
|
|
370
|
+
* m = m.add(5);
|
|
371
|
+
* m = m.increment();
|
|
372
|
+
* setMachine(m);
|
|
373
|
+
* });
|
|
374
|
+
* };
|
|
375
|
+
* ```
|
|
376
|
+
*/
|
|
377
|
+
export function batchTransitions<M extends Machine<any>>(
|
|
378
|
+
machine: M,
|
|
379
|
+
setMachine: Setter<M>,
|
|
380
|
+
...transitions: Array<(m: M) => M>
|
|
381
|
+
): M {
|
|
382
|
+
const { batch } = require('solid-js');
|
|
383
|
+
|
|
384
|
+
return batch(() => {
|
|
385
|
+
const finalMachine = transitions.reduce((m, transition) => transition(m), machine);
|
|
386
|
+
setMachine(finalMachine);
|
|
387
|
+
return finalMachine;
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// =============================================================================
|
|
392
|
+
// LIFECYCLE EFFECTS
|
|
393
|
+
// =============================================================================
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Runs an effect when entering or exiting specific machine states.
|
|
397
|
+
*
|
|
398
|
+
* This is useful for side effects that should happen on state transitions,
|
|
399
|
+
* like analytics, logging, or subscriptions.
|
|
400
|
+
*
|
|
401
|
+
* @template M - The machine type.
|
|
402
|
+
*
|
|
403
|
+
* @param machine - Machine accessor.
|
|
404
|
+
* @param statePredicate - Function to determine if we're in the target state.
|
|
405
|
+
* @param onEnter - Effect to run when entering the state.
|
|
406
|
+
* @param onExit - Optional effect to run when exiting the state.
|
|
407
|
+
*
|
|
408
|
+
* @example
|
|
409
|
+
* ```tsx
|
|
410
|
+
* const [machine, actions] = createMachine(() => createAuthMachine());
|
|
411
|
+
*
|
|
412
|
+
* createMachineEffect(
|
|
413
|
+
* machine,
|
|
414
|
+
* (m) => m.context.status === 'loggedIn',
|
|
415
|
+
* (m) => {
|
|
416
|
+
* console.log('User logged in:', m.context.username);
|
|
417
|
+
* // Start session tracking
|
|
418
|
+
* },
|
|
419
|
+
* () => {
|
|
420
|
+
* console.log('User logged out');
|
|
421
|
+
* // Clean up session
|
|
422
|
+
* }
|
|
423
|
+
* );
|
|
424
|
+
* ```
|
|
425
|
+
*/
|
|
426
|
+
export function createMachineEffect<M extends Machine<any>>(
|
|
427
|
+
machine: Accessor<M>,
|
|
428
|
+
statePredicate: (m: M) => boolean,
|
|
429
|
+
onEnter: (m: M) => void,
|
|
430
|
+
onExit?: () => void
|
|
431
|
+
): void {
|
|
432
|
+
let wasInState = false;
|
|
433
|
+
|
|
434
|
+
createEffect(() => {
|
|
435
|
+
const m = machine();
|
|
436
|
+
const isInState = statePredicate(m);
|
|
437
|
+
|
|
438
|
+
if (isInState && !wasInState) {
|
|
439
|
+
// Entering state
|
|
440
|
+
onEnter(m);
|
|
441
|
+
wasInState = true;
|
|
442
|
+
} else if (!isInState && wasInState) {
|
|
443
|
+
// Exiting state
|
|
444
|
+
onExit?.();
|
|
445
|
+
wasInState = false;
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
onCleanup(() => {
|
|
450
|
+
if (wasInState && onExit) {
|
|
451
|
+
onExit();
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Helper to create effects for specific context values.
|
|
458
|
+
*
|
|
459
|
+
* @template M - The machine type.
|
|
460
|
+
* @template T - The selected value type.
|
|
461
|
+
*
|
|
462
|
+
* @param machine - Machine accessor.
|
|
463
|
+
* @param selector - Function to select a value from context.
|
|
464
|
+
* @param effect - Effect to run when the selected value changes.
|
|
465
|
+
*
|
|
466
|
+
* @example
|
|
467
|
+
* ```tsx
|
|
468
|
+
* const [machine, actions] = createMachine(() => createCounterMachine());
|
|
469
|
+
*
|
|
470
|
+
* createMachineValueEffect(
|
|
471
|
+
* machine,
|
|
472
|
+
* (ctx) => ctx.count,
|
|
473
|
+
* (count) => {
|
|
474
|
+
* console.log('Count changed to:', count);
|
|
475
|
+
* if (count > 10) {
|
|
476
|
+
* alert('Count is high!');
|
|
477
|
+
* }
|
|
478
|
+
* }
|
|
479
|
+
* );
|
|
480
|
+
* ```
|
|
481
|
+
*/
|
|
482
|
+
export function createMachineValueEffect<M extends Machine<any>, T>(
|
|
483
|
+
machine: Accessor<M>,
|
|
484
|
+
selector: (context: Context<M>) => T,
|
|
485
|
+
effect: (value: T) => void
|
|
486
|
+
): void {
|
|
487
|
+
createEffect(() => {
|
|
488
|
+
const value = selector(machine().context);
|
|
489
|
+
effect(value);
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// =============================================================================
|
|
494
|
+
// EXPORT TYPES FOR BETTER DX
|
|
495
|
+
// =============================================================================
|
|
496
|
+
|
|
497
|
+
export type {
|
|
498
|
+
Accessor,
|
|
499
|
+
Setter,
|
|
500
|
+
Store,
|
|
501
|
+
SetStoreFunction
|
|
502
|
+
};
|
package/src/test.ts
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A utility type that represents either a value of type T or a Promise that resolves to T.
|
|
3
|
+
* @template T - The value type
|
|
4
|
+
*/
|
|
5
|
+
type MaybePromise<T> = T | Promise<T>
|
|
6
|
+
|
|
7
|
+
// /**
|
|
8
|
+
// * A record of synchronous state transition functions.
|
|
9
|
+
// * Each function receives the machine context as `this` and returns a new Machine state.
|
|
10
|
+
// * @template C - The context object type
|
|
11
|
+
// */
|
|
12
|
+
// type Functions<C extends object> =
|
|
13
|
+
// Record<string, (this: C, ...args: any[]) => Machine<C>>
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A record of asynchronous state transition functions.
|
|
17
|
+
* Each function receives the machine context as `this` and returns either a Machine or Promise<Machine>.
|
|
18
|
+
* @template C - The context object type
|
|
19
|
+
*/
|
|
20
|
+
type AsyncFunctions<C extends object> =
|
|
21
|
+
Record<string, (this: C, ...args: any[]) => MaybePromise<Machine<C>>>
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A synchronous state machine with a context object and transition functions.
|
|
25
|
+
* @template C - The context object type
|
|
26
|
+
* @example
|
|
27
|
+
* const machine: Machine<{ count: number }> = {
|
|
28
|
+
* context: { count: 0 },
|
|
29
|
+
* increment: function() {
|
|
30
|
+
* return createMachine({ count: this.count + 1 }, this)
|
|
31
|
+
* }
|
|
32
|
+
* }
|
|
33
|
+
*/
|
|
34
|
+
export type Machine<C extends object> = { context: C } & Functions<C>
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* An asynchronous state machine with a context object and async transition functions.
|
|
38
|
+
* @template C - The context object type
|
|
39
|
+
* @example
|
|
40
|
+
* const machine: AsyncMachine<{ loading: boolean }> = {
|
|
41
|
+
* context: { loading: false },
|
|
42
|
+
* fetch: async function() {
|
|
43
|
+
* return createAsyncMachine({ loading: true }, this)
|
|
44
|
+
* }
|
|
45
|
+
* }
|
|
46
|
+
*/
|
|
47
|
+
export type AsyncMachine<C extends object> = { context: C } & AsyncFunctions<C>
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Creates a synchronous state machine from a context and transition functions.
|
|
51
|
+
* @template C - The context object type
|
|
52
|
+
* @param {C} context - The initial state context
|
|
53
|
+
* @param {Functions<C>} fns - Object containing transition function definitions
|
|
54
|
+
* @returns {Machine<C>} A new machine instance
|
|
55
|
+
* @example
|
|
56
|
+
* const counter = createMachine(
|
|
57
|
+
* { count: 0 },
|
|
58
|
+
* {
|
|
59
|
+
* increment: function() {
|
|
60
|
+
* return createMachine({ count: this.count + 1 }, this)
|
|
61
|
+
* },
|
|
62
|
+
* decrement: function() {
|
|
63
|
+
* return createMachine({ count: this.count - 1 }, this)
|
|
64
|
+
* }
|
|
65
|
+
* }
|
|
66
|
+
* )
|
|
67
|
+
* const next = counter.increment() // { context: { count: 1 }, increment, decrement }
|
|
68
|
+
*/
|
|
69
|
+
export function createMachine<C extends object>(
|
|
70
|
+
context: C,
|
|
71
|
+
fns: Functions<C>
|
|
72
|
+
): Machine<C> {
|
|
73
|
+
return Object.assign({ context }, fns)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Creates an asynchronous state machine from a context and async transition functions.
|
|
78
|
+
* @template C - The context object type
|
|
79
|
+
* @param {C} context - The initial state context
|
|
80
|
+
* @param {AsyncFunctions<C>} fns - Object containing async transition function definitions
|
|
81
|
+
* @returns {AsyncMachine<C>} A new async machine instance
|
|
82
|
+
* @example
|
|
83
|
+
* const user = createAsyncMachine(
|
|
84
|
+
* { id: null, loading: false },
|
|
85
|
+
* {
|
|
86
|
+
* fetchUser: async function(userId) {
|
|
87
|
+
* const data = await fetch(`/api/users/${userId}`)
|
|
88
|
+
* return createAsyncMachine({ id: data.id, loading: false }, this)
|
|
89
|
+
* }
|
|
90
|
+
* }
|
|
91
|
+
* )
|
|
92
|
+
*/
|
|
93
|
+
export function createAsyncMachine<C extends object>(
|
|
94
|
+
context: C,
|
|
95
|
+
fns: AsyncFunctions<C>
|
|
96
|
+
): AsyncMachine<C> {
|
|
97
|
+
return Object.assign({ context }, fns)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Creates a new machine state by applying an update function to the current machine's context.
|
|
102
|
+
* Preserves all transition functions from the original machine.
|
|
103
|
+
* @template C - The context object type
|
|
104
|
+
* @param {Machine<C>} m - The current machine state
|
|
105
|
+
* @param {Function} update - A function that takes the current read-only context and returns an updated context
|
|
106
|
+
* @returns {Machine<C>} A new machine with updated context but same transition functions
|
|
107
|
+
* @example
|
|
108
|
+
* const counter = createMachine({ count: 0 }, { })
|
|
109
|
+
* const incremented = next(counter, (ctx) => ({ count: ctx.count + 1 }))
|
|
110
|
+
* // incremented.context.count === 1
|
|
111
|
+
*/
|
|
112
|
+
export function next<C extends object>(
|
|
113
|
+
m: Machine<C>,
|
|
114
|
+
update: (ctx: Readonly<C>) => C
|
|
115
|
+
): Machine<C> {
|
|
116
|
+
return createMachine(update(m.context), m)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* A discriminated union type representing an event that can be dispatched to a machine.
|
|
121
|
+
* Each event has a `type` property matching a transition function name and `args` matching that function's parameters.
|
|
122
|
+
* @template M - The machine type
|
|
123
|
+
* @example
|
|
124
|
+
* type CounterEvent = Event<Machine<{ count: number }>& {
|
|
125
|
+
* increment: () => any
|
|
126
|
+
* addValue: (n: number) => any
|
|
127
|
+
* }>
|
|
128
|
+
* // CounterEvent = { type: "increment"; args: [] } | { type: "addValue"; args: [number] }
|
|
129
|
+
*/
|
|
130
|
+
export type Event<M> = {
|
|
131
|
+
[K in keyof M & string]: M[K] extends (...args: infer A) => any
|
|
132
|
+
? { type: K; args: A }
|
|
133
|
+
: never
|
|
134
|
+
}[keyof M & string]
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Runs an asynchronous state machine with event dispatch capability.
|
|
138
|
+
* Provides a managed interface to dispatch events and track state changes.
|
|
139
|
+
* @template C - The context object type
|
|
140
|
+
* @param {AsyncMachine<C>} initial - The initial machine state
|
|
141
|
+
* @param {Function} onChange - Optional callback invoked whenever the machine state changes
|
|
142
|
+
* @returns {Object} An object with a state getter and dispatch function
|
|
143
|
+
* @returns {C} returns.state - The current machine context
|
|
144
|
+
* @returns {Function} returns.dispatch - Async function to dispatch events to the machine
|
|
145
|
+
* @example
|
|
146
|
+
* const machine = createAsyncMachine(
|
|
147
|
+
* { count: 0 },
|
|
148
|
+
* {
|
|
149
|
+
* increment: async function() {
|
|
150
|
+
* return createAsyncMachine({ count: this.count + 1 }, this)
|
|
151
|
+
* }
|
|
152
|
+
* }
|
|
153
|
+
* )
|
|
154
|
+
* const runner = runMachine(machine, (m) => console.log("State changed:", m.context))
|
|
155
|
+
* await runner.dispatch({ type: "increment", args: [] })
|
|
156
|
+
* console.log(runner.state) // { count: 1 }
|
|
157
|
+
*/
|
|
158
|
+
export function runMachine<C extends object>(
|
|
159
|
+
initial: AsyncMachine<C>,
|
|
160
|
+
onChange?: (m: AsyncMachine<C>) => void
|
|
161
|
+
) {
|
|
162
|
+
let current = initial
|
|
163
|
+
|
|
164
|
+
async function dispatch<E extends Event<typeof current>>(event: E) {
|
|
165
|
+
const fn = current[event.type] as any
|
|
166
|
+
if (!fn) throw new Error(`Unknown event: ${event.type}`)
|
|
167
|
+
const next = await fn.apply(current.context, event.args)
|
|
168
|
+
current = next
|
|
169
|
+
onChange?.(current)
|
|
170
|
+
return current
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
get state() {
|
|
175
|
+
return current.context
|
|
176
|
+
},
|
|
177
|
+
dispatch,
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
type Functions<C extends object> =
|
|
185
|
+
Record<string, (this: C, ...args: any[]) => Machine<any>>
|
|
186
|
+
|
|
187
|
+
// Simple counter example using the functional API
|
|
188
|
+
// Note: In the real library, transition functions receive context as `this`
|
|
189
|
+
const counterFns = {
|
|
190
|
+
increment: function() {
|
|
191
|
+
// `this` is bound to the context by the library
|
|
192
|
+
return createMachine({ count: (this as any).count + 1 }, counterFns);
|
|
193
|
+
},
|
|
194
|
+
decrement: function() {
|
|
195
|
+
// `this` is bound to the context by the library
|
|
196
|
+
return createMachine({ count: (this as any).count - 1 }, counterFns);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const counter = createMachine(
|
|
201
|
+
{ count: 0 },
|
|
202
|
+
counterFns
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
// Test by calling with proper context binding
|
|
206
|
+
const result = counterFns.increment.call(counter.context);
|
|
207
|
+
console.log('Result:', result.context.count);
|