@doeixd/machine 0.0.20 → 0.0.22
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 +19 -0
- package/dist/cjs/development/core.js.map +1 -1
- package/dist/cjs/development/index.js +277 -0
- package/dist/cjs/development/index.js.map +4 -4
- package/dist/cjs/production/index.js +4 -4
- package/dist/esm/development/core.js.map +1 -1
- package/dist/esm/development/index.js +277 -0
- package/dist/esm/development/index.js.map +4 -4
- package/dist/esm/production/index.js +4 -4
- package/dist/types/actor.d.ts +153 -0
- package/dist/types/actor.d.ts.map +1 -0
- package/dist/types/index.d.ts +3 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/mixins.d.ts +118 -0
- package/dist/types/mixins.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/actor.ts +284 -0
- package/src/index.ts +18 -2
- package/src/mixins.ts +308 -0
- package/src/react.ts +95 -124
package/src/react.ts
CHANGED
|
@@ -33,6 +33,10 @@
|
|
|
33
33
|
* - **Returns:** A `Provider` and consumer hooks (`useContext`, `useSelector`, etc.).
|
|
34
34
|
* - A utility to provide a machine created with `useMachine` or `useEnsemble` to
|
|
35
35
|
* the entire component tree below it.
|
|
36
|
+
*
|
|
37
|
+
* 5. **`useActor(actor)`**:
|
|
38
|
+
* - **Best for:** Using the Actor model.
|
|
39
|
+
* - **Returns:** The current machine snapshot.
|
|
36
40
|
*/
|
|
37
41
|
|
|
38
42
|
import {
|
|
@@ -43,18 +47,22 @@ import {
|
|
|
43
47
|
createContext,
|
|
44
48
|
useContext,
|
|
45
49
|
createElement,
|
|
50
|
+
useSyncExternalStore,
|
|
46
51
|
type ReactNode,
|
|
47
52
|
} from 'react';
|
|
48
53
|
|
|
49
54
|
import {
|
|
50
55
|
Machine,
|
|
51
|
-
createRunner
|
|
56
|
+
runMachine, // Was createRunner
|
|
52
57
|
createEnsemble,
|
|
53
|
-
type Runner,
|
|
54
58
|
type Ensemble,
|
|
55
59
|
type StateStore,
|
|
60
|
+
type Actor,
|
|
61
|
+
BaseMachine
|
|
56
62
|
} from './index';
|
|
57
63
|
|
|
64
|
+
export type Runner<M extends Machine<any>> = ReturnType<typeof runMachine<M>>;
|
|
65
|
+
|
|
58
66
|
// =============================================================================
|
|
59
67
|
// HOOK 1: useMachine (Ergonomic local state)
|
|
60
68
|
// =============================================================================
|
|
@@ -62,7 +70,7 @@ import {
|
|
|
62
70
|
/**
|
|
63
71
|
* A React hook for using a self-contained, immutable state machine within a component.
|
|
64
72
|
* It provides a more ergonomic API than a raw dispatcher by returning a stable `actions`
|
|
65
|
-
* object, similar to the `
|
|
73
|
+
* object, similar to the `runMachine` primitive.
|
|
66
74
|
*
|
|
67
75
|
* This is the ideal hook for managing component-level state.
|
|
68
76
|
*
|
|
@@ -75,57 +83,33 @@ import {
|
|
|
75
83
|
* for type-narrowing.
|
|
76
84
|
* - `actions`: A stable object containing all possible transition methods,
|
|
77
85
|
* pre-bound to update the machine's state.
|
|
78
|
-
*
|
|
79
|
-
* @example
|
|
80
|
-
* ```tsx
|
|
81
|
-
* const [machine, actions] = useMachine(() => createCounterMachine({ count: 0 }));
|
|
82
|
-
*
|
|
83
|
-
* return (
|
|
84
|
-
* <div>
|
|
85
|
-
* <p>Count: {machine.context.count}</p>
|
|
86
|
-
* <button onClick={() => actions.increment()}>Increment</button>
|
|
87
|
-
* <button onClick={() => actions.add(5)}>Add 5</button>
|
|
88
|
-
* </div>
|
|
89
|
-
* );
|
|
90
|
-
* ```
|
|
91
|
-
*
|
|
92
|
-
* @example With Type-State Programming
|
|
93
|
-
* ```tsx
|
|
94
|
-
* const [auth, actions] = useMachine(() => createLoggedOutMachine());
|
|
95
|
-
*
|
|
96
|
-
* return (
|
|
97
|
-
* <div>
|
|
98
|
-
* {auth.context.status === 'loggedOut' && (
|
|
99
|
-
* <button onClick={() => actions.login('user')}>Login</button>
|
|
100
|
-
* )}
|
|
101
|
-
* {auth.context.status === 'loggedIn' && (
|
|
102
|
-
* <p>Welcome, {auth.context.username}!</p>
|
|
103
|
-
* <button onClick={() => actions.logout()}>Logout</button>
|
|
104
|
-
* )}
|
|
105
|
-
* </div>
|
|
106
|
-
* );
|
|
107
|
-
* ```
|
|
108
86
|
*/
|
|
109
87
|
export function useMachine<M extends Machine<any>>(
|
|
110
88
|
machineFactory: () => M
|
|
111
|
-
): [M,
|
|
89
|
+
): [M, Record<string, (...args: any[]) => void>] {
|
|
112
90
|
// useState holds the machine state, triggering re-renders.
|
|
113
|
-
// The factory is passed directly to useState to ensure it's only called once.
|
|
114
91
|
const [machine, setMachine] = useState(machineFactory);
|
|
115
92
|
|
|
116
93
|
// useMemo creates a stable runner instance that survives re-renders.
|
|
117
|
-
// The runner's job is to hold the *current* machine state and update our
|
|
118
|
-
// React state when a transition occurs.
|
|
119
94
|
const runner = useMemo(
|
|
120
|
-
() =>
|
|
121
|
-
// This is the magic link: when the runner's internal state changes,
|
|
122
|
-
// we update React's state, causing a re-render.
|
|
95
|
+
() => runMachine(machine, (newState) => {
|
|
123
96
|
setMachine(newState);
|
|
124
97
|
}),
|
|
125
|
-
[]
|
|
98
|
+
[]
|
|
126
99
|
);
|
|
127
100
|
|
|
128
|
-
|
|
101
|
+
// Create a stable actions object that proxies calls to the dispatcher
|
|
102
|
+
const actions = useMemo(() => {
|
|
103
|
+
return new Proxy({} as any, {
|
|
104
|
+
get: (_target, prop) => {
|
|
105
|
+
return (...args: any[]) => {
|
|
106
|
+
runner.dispatch({ type: prop as any, args: args as any } as any);
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}, [runner]);
|
|
111
|
+
|
|
112
|
+
return [machine, actions];
|
|
129
113
|
}
|
|
130
114
|
|
|
131
115
|
// =============================================================================
|
|
@@ -149,21 +133,6 @@ export function useMachine<M extends Machine<any>>(
|
|
|
149
133
|
* values. Defaults to `Object.is` for strict equality checking. Provide your own
|
|
150
134
|
* for deep comparisons of objects or arrays.
|
|
151
135
|
* @returns The selected, memoized value from the machine's state.
|
|
152
|
-
*
|
|
153
|
-
* @example
|
|
154
|
-
* ```tsx
|
|
155
|
-
* // In parent component:
|
|
156
|
-
* const [machine, actions] = useMachine(() => createUserMachine());
|
|
157
|
-
*
|
|
158
|
-
* // In child component (only re-renders when the user's name changes):
|
|
159
|
-
* function UserNameDisplay({ machine }) {
|
|
160
|
-
* const userName = useMachineSelector(
|
|
161
|
-
* machine,
|
|
162
|
-
* (m) => m.context.user.name
|
|
163
|
-
* );
|
|
164
|
-
* return <p>User: {userName}</p>;
|
|
165
|
-
* }
|
|
166
|
-
* ```
|
|
167
136
|
*/
|
|
168
137
|
export function useMachineSelector<M extends Machine<any>, T>(
|
|
169
138
|
machine: M,
|
|
@@ -172,7 +141,7 @@ export function useMachineSelector<M extends Machine<any>, T>(
|
|
|
172
141
|
): T {
|
|
173
142
|
// Store the selected value in local state.
|
|
174
143
|
const [selectedValue, setSelectedValue] = useState(() => selector(machine));
|
|
175
|
-
|
|
144
|
+
|
|
176
145
|
// Keep refs to the latest selector and comparison functions.
|
|
177
146
|
const selectorRef = useRef(selector);
|
|
178
147
|
const isEqualRef = useRef(isEqual);
|
|
@@ -209,32 +178,6 @@ export function useMachineSelector<M extends Machine<any>, T>(
|
|
|
209
178
|
* from the context.
|
|
210
179
|
* @returns A stable `Ensemble` instance. The component will reactively update
|
|
211
180
|
* when the ensemble's underlying context changes.
|
|
212
|
-
*
|
|
213
|
-
* @example
|
|
214
|
-
* ```tsx
|
|
215
|
-
* const fetchFactories = {
|
|
216
|
-
* idle: (ctx) => createMachine(ctx, { fetch: () => ({ ...ctx, status: 'loading' }) }),
|
|
217
|
-
* loading: (ctx) => createMachine(ctx, { succeed: (data) => ({ status: 'success', data }) }),
|
|
218
|
-
* // ...
|
|
219
|
-
* };
|
|
220
|
-
*
|
|
221
|
-
* function MyComponent() {
|
|
222
|
-
* const ensemble = useEnsemble(
|
|
223
|
-
* { status: 'idle', data: null },
|
|
224
|
-
* fetchFactories,
|
|
225
|
-
* (ctx) => ctx.status
|
|
226
|
-
* );
|
|
227
|
-
*
|
|
228
|
-
* return (
|
|
229
|
-
* <div>
|
|
230
|
-
* <p>Status: {ensemble.context.status}</p>
|
|
231
|
-
* {ensemble.state.context.status === 'idle' && (
|
|
232
|
-
* <button onClick={() => ensemble.actions.fetch()}>Fetch</button>
|
|
233
|
-
* )}
|
|
234
|
-
* </div>
|
|
235
|
-
* );
|
|
236
|
-
* }
|
|
237
|
-
* ```
|
|
238
181
|
*/
|
|
239
182
|
export function useEnsemble<
|
|
240
183
|
C extends object,
|
|
@@ -280,46 +223,9 @@ export function useEnsemble<
|
|
|
280
223
|
*
|
|
281
224
|
* It returns a `Provider` component and a suite of consumer hooks for accessing
|
|
282
225
|
* the state and actions.
|
|
283
|
-
*
|
|
284
|
-
* @returns An object containing:
|
|
285
|
-
* - `Provider`: The context provider component.
|
|
286
|
-
* - `useMachineContext`: Hook to get the full `[machine, actions]` tuple.
|
|
287
|
-
* - `useMachineState`: Hook to get only the reactive `machine` instance.
|
|
288
|
-
* - `useMachineActions`: Hook to get only the stable `actions` object.
|
|
289
|
-
* - `useSelector`: Hook to get a memoized slice of the machine's state.
|
|
290
|
-
*
|
|
291
|
-
* @example
|
|
292
|
-
* ```tsx
|
|
293
|
-
* // 1. Create the context
|
|
294
|
-
* const { Provider, useMachineState, useMachineActions } = createMachineContext<MyMachine>();
|
|
295
|
-
*
|
|
296
|
-
* // 2. In your top-level component, create the machine and provide it
|
|
297
|
-
* function App() {
|
|
298
|
-
* const [machine, actions] = useMachine(() => createMyMachine());
|
|
299
|
-
* return (
|
|
300
|
-
* <Provider machine={machine} actions={actions}>
|
|
301
|
-
* <ChildComponent />
|
|
302
|
-
* </Provider>
|
|
303
|
-
* );
|
|
304
|
-
* }
|
|
305
|
-
*
|
|
306
|
-
* // 3. In a deeply nested child component
|
|
307
|
-
* function ChildComponent() {
|
|
308
|
-
* const machine = useMachineState(); // Gets the current state
|
|
309
|
-
* const actions = useMachineActions(); // Gets the stable actions
|
|
310
|
-
* const name = useSelector(m => m.context.name); // Selects a slice
|
|
311
|
-
*
|
|
312
|
-
* return (
|
|
313
|
-
* <div>
|
|
314
|
-
* <p>Name: {name}</p>
|
|
315
|
-
* <button onClick={() => actions.rename('new name')}>Rename</button>
|
|
316
|
-
* </div>
|
|
317
|
-
* );
|
|
318
|
-
* }
|
|
319
|
-
* ```
|
|
320
226
|
*/
|
|
321
227
|
export function createMachineContext<M extends Machine<any>>() {
|
|
322
|
-
type MachineContextValue = [M,
|
|
228
|
+
type MachineContextValue = [M, Record<string, (...args: any[]) => void>];
|
|
323
229
|
const Context = createContext<MachineContextValue | null>(null);
|
|
324
230
|
|
|
325
231
|
const Provider = ({
|
|
@@ -328,7 +234,7 @@ export function createMachineContext<M extends Machine<any>>() {
|
|
|
328
234
|
children,
|
|
329
235
|
}: {
|
|
330
236
|
machine: M;
|
|
331
|
-
actions:
|
|
237
|
+
actions: Record<string, (...args: any[]) => void>;
|
|
332
238
|
children: ReactNode;
|
|
333
239
|
}) => {
|
|
334
240
|
// Memoize the context value to prevent unnecessary re-renders in consumers.
|
|
@@ -345,7 +251,7 @@ export function createMachineContext<M extends Machine<any>>() {
|
|
|
345
251
|
};
|
|
346
252
|
|
|
347
253
|
const useMachineState = (): M => useMachineContext()[0];
|
|
348
|
-
const useMachineActions = ():
|
|
254
|
+
const useMachineActions = (): Record<string, (...args: any[]) => void> => useMachineContext()[1];
|
|
349
255
|
|
|
350
256
|
const useSelector = <T,>(
|
|
351
257
|
selector: (state: M) => T,
|
|
@@ -362,4 +268,69 @@ export function createMachineContext<M extends Machine<any>>() {
|
|
|
362
268
|
useMachineActions,
|
|
363
269
|
useSelector,
|
|
364
270
|
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// =============================================================================
|
|
274
|
+
// HOOK 5: useActor (Actor Model)
|
|
275
|
+
// =============================================================================
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Subscribes to an Actor and returns the current snapshot.
|
|
279
|
+
* Uses `useSyncExternalStore` for concurrent features compatibility.
|
|
280
|
+
*
|
|
281
|
+
* @param actor The actor instance to subscribe to.
|
|
282
|
+
* @returns The current machine snapshot.
|
|
283
|
+
*/
|
|
284
|
+
export function useActor<M extends BaseMachine<any>>(actor: Actor<M>): M {
|
|
285
|
+
// bind is important if subscribe methods rely on `this`
|
|
286
|
+
const subscribe = useMemo(() => actor.subscribe.bind(actor), [actor]);
|
|
287
|
+
const getSnapshot = useMemo(() => actor.getSnapshot.bind(actor), [actor]);
|
|
288
|
+
|
|
289
|
+
return useSyncExternalStore(subscribe, getSnapshot);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Subscribes to an Actor and selects a slice of the state.
|
|
294
|
+
* Only re-renders when the selected slice changes.
|
|
295
|
+
*
|
|
296
|
+
* @param actor The actor instance.
|
|
297
|
+
* @param selector Function to select a part of the state.
|
|
298
|
+
* @param isEqual Optional equality function.
|
|
299
|
+
*/
|
|
300
|
+
export function useActorSelector<M extends BaseMachine<any>, T>(
|
|
301
|
+
actor: Actor<M>,
|
|
302
|
+
selector: (state: M) => T,
|
|
303
|
+
isEqual: (a: T, b: T) => boolean = Object.is
|
|
304
|
+
): T {
|
|
305
|
+
const subscribe = useMemo(() => actor.subscribe.bind(actor), [actor]);
|
|
306
|
+
const getSnapshot = useMemo(() => actor.getSnapshot.bind(actor), [actor]);
|
|
307
|
+
|
|
308
|
+
const getSelection = () => selector(getSnapshot());
|
|
309
|
+
|
|
310
|
+
const [selection, setSelection] = useState(getSelection);
|
|
311
|
+
|
|
312
|
+
// Custom selector logic since useSyncExternalStoreWithSelector is not available directly
|
|
313
|
+
// and we want to avoid extra deps.
|
|
314
|
+
// Actually, we can just use useSyncExternalStore and manage the selection stability,
|
|
315
|
+
// but useSyncExternalStore triggers if the result of getSnapshot changes (strict eq).
|
|
316
|
+
// If we wrap getSnapshot to return the selection, standard useSyncExternalStore handles it?
|
|
317
|
+
// No, useSyncExternalStore calls getSnapshot continuously during render to check for tearing.
|
|
318
|
+
// It needs to be cheap and consistent.
|
|
319
|
+
|
|
320
|
+
// Simple implementation: Subscribe and update local state only on change.
|
|
321
|
+
useEffect(() => {
|
|
322
|
+
const checkUpdate = () => {
|
|
323
|
+
const nextSelection = selector(actor.getSnapshot());
|
|
324
|
+
setSelection(prev => isEqual(prev, nextSelection) ? prev : nextSelection);
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
// Check immediately in case it changed between render and effect
|
|
328
|
+
checkUpdate();
|
|
329
|
+
|
|
330
|
+
return actor.subscribe(() => {
|
|
331
|
+
checkUpdate();
|
|
332
|
+
});
|
|
333
|
+
}, [actor, selector, isEqual]);
|
|
334
|
+
|
|
335
|
+
return selection;
|
|
365
336
|
}
|