@doeixd/machine 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +7 -0
- package/README.md +1070 -0
- package/dist/cjs/development/index.js +198 -0
- package/dist/cjs/development/index.js.map +7 -0
- package/dist/cjs/production/index.js +1 -0
- package/dist/esm/development/index.js +175 -0
- package/dist/esm/development/index.js.map +7 -0
- package/dist/esm/production/index.js +1 -0
- package/dist/types/generators.d.ts +314 -0
- package/dist/types/generators.d.ts.map +1 -0
- package/dist/types/index.d.ts +339 -0
- package/dist/types/index.d.ts.map +1 -0
- package/package.json +110 -0
- package/src/devtools.ts +74 -0
- package/src/extract.ts +190 -0
- package/src/generators.ts +421 -0
- package/src/index.ts +528 -0
- package/src/primitives.ts +191 -0
- package/src/react.ts +44 -0
- package/src/solid.ts +502 -0
- package/src/test.ts +207 -0
- package/src/utils.ts +167 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Type-level primitives for formal state machine verification.
|
|
3
|
+
* @description
|
|
4
|
+
* This file provides a Domain Specific Language (DSL) of wrapper functions.
|
|
5
|
+
* These functions serve two purposes:
|
|
6
|
+
* 1. At Runtime: They are identity functions (no-ops). They return your code exactly as is.
|
|
7
|
+
* 2. At Design/Build Time: They "brand" your transition functions with rich type metadata.
|
|
8
|
+
*
|
|
9
|
+
* This allows a static analysis tool (like `ts-morph`) to read your source code
|
|
10
|
+
* and generate a formal Statechart (JSON) that perfectly matches your implementation,
|
|
11
|
+
* including resolving Class Constructors to their names.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// SECTION: CORE METADATA TYPES
|
|
16
|
+
// =============================================================================
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A unique symbol used to "brand" a type with metadata.
|
|
20
|
+
* This key allows the static analyzer to find the metadata within a complex type signature.
|
|
21
|
+
*/
|
|
22
|
+
export const META_KEY = Symbol("MachineMeta");
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Helper type representing a Class Constructor.
|
|
26
|
+
* Used to reference target states by their class definition rather than magic strings.
|
|
27
|
+
*/
|
|
28
|
+
export type ClassConstructor = new (...args: any[]) => any;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Metadata describing a Guard condition.
|
|
32
|
+
*/
|
|
33
|
+
export interface GuardMeta {
|
|
34
|
+
/** The name of the guard (e.g., "isAdmin"). */
|
|
35
|
+
name: string;
|
|
36
|
+
/** Optional documentation explaining the logic. */
|
|
37
|
+
description?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Metadata describing an Invoked Service (async operation).
|
|
42
|
+
*/
|
|
43
|
+
export interface InvokeMeta {
|
|
44
|
+
/** The name of the service source (e.g., "fetchUserData"). */
|
|
45
|
+
src: string;
|
|
46
|
+
/** The state class to transition to on success. */
|
|
47
|
+
onDone: ClassConstructor;
|
|
48
|
+
/** The state class to transition to on error. */
|
|
49
|
+
onError: ClassConstructor;
|
|
50
|
+
/** Optional description. */
|
|
51
|
+
description?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Metadata describing a generic Action (side effect).
|
|
56
|
+
*/
|
|
57
|
+
export interface ActionMeta {
|
|
58
|
+
/** The name of the action (e.g., "logAnalytics"). */
|
|
59
|
+
name: string;
|
|
60
|
+
/** Optional description. */
|
|
61
|
+
description?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The comprehensive shape of metadata that can be encoded into a transition's type.
|
|
66
|
+
*/
|
|
67
|
+
export interface TransitionMeta {
|
|
68
|
+
/** The target state class this transition leads to. */
|
|
69
|
+
target?: ClassConstructor;
|
|
70
|
+
/** A human-readable description of the transition. */
|
|
71
|
+
description?: string;
|
|
72
|
+
/** An array of guards that must be true for this transition to be enabled. */
|
|
73
|
+
guards?: GuardMeta[];
|
|
74
|
+
/** A service to invoke upon taking this transition (or entering the state). */
|
|
75
|
+
invoke?: InvokeMeta;
|
|
76
|
+
/** Fire-and-forget side effects associated with this transition. */
|
|
77
|
+
actions?: ActionMeta[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The Branded Type.
|
|
82
|
+
* It takes a function type `F` and intersects it with a hidden metadata object `M`.
|
|
83
|
+
* This is the mechanism that carries information from your code to the compiler API.
|
|
84
|
+
*/
|
|
85
|
+
export type WithMeta<
|
|
86
|
+
F extends (...args: any[]) => any,
|
|
87
|
+
M extends TransitionMeta
|
|
88
|
+
> = F & { [META_KEY]: M };
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
// =============================================================================
|
|
92
|
+
// SECTION: ANNOTATION PRIMITIVES (THE DSL)
|
|
93
|
+
// =============================================================================
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Defines a transition to a target state class.
|
|
97
|
+
*
|
|
98
|
+
* @param target - The Class Constructor of the state being transitioned to.
|
|
99
|
+
* @param implementation - The implementation function returning the new state instance.
|
|
100
|
+
* @returns The implementation function, branded with target metadata.
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* login = transitionTo(LoggedInMachine, (user) => new LoggedInMachine({ user }));
|
|
104
|
+
*/
|
|
105
|
+
export function transitionTo<
|
|
106
|
+
T extends ClassConstructor,
|
|
107
|
+
F extends (...args: any[]) => any
|
|
108
|
+
>(
|
|
109
|
+
_target: T,
|
|
110
|
+
implementation: F
|
|
111
|
+
): WithMeta<F, { target: T }> {
|
|
112
|
+
return implementation as any;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Annotates a transition with a description for documentation generation.
|
|
117
|
+
*
|
|
118
|
+
* @param text - The description text.
|
|
119
|
+
* @param transition - The transition function (or wrapper) to annotate.
|
|
120
|
+
* @example
|
|
121
|
+
* logout = describe("Logs the user out", transitionTo(LoggedOut, ...));
|
|
122
|
+
*/
|
|
123
|
+
export function describe<
|
|
124
|
+
F extends (...args: any[]) => any,
|
|
125
|
+
M extends TransitionMeta
|
|
126
|
+
>(
|
|
127
|
+
_text: string,
|
|
128
|
+
transition: WithMeta<F, M>
|
|
129
|
+
): WithMeta<F, M & { description: string }> {
|
|
130
|
+
return transition as any;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Annotates a transition with a Guard condition.
|
|
135
|
+
* Note: This only adds metadata. You must still implement the `if` check inside your function.
|
|
136
|
+
*
|
|
137
|
+
* @param guard - Object containing the name and optional description of the guard.
|
|
138
|
+
* @param transition - The transition function to guard.
|
|
139
|
+
* @example
|
|
140
|
+
* delete = guarded({ name: "isAdmin" }, transitionTo(Deleted, ...));
|
|
141
|
+
*/
|
|
142
|
+
export function guarded<
|
|
143
|
+
F extends (...args: any[]) => any,
|
|
144
|
+
M extends TransitionMeta
|
|
145
|
+
>(
|
|
146
|
+
guard: GuardMeta,
|
|
147
|
+
transition: WithMeta<F, M>
|
|
148
|
+
): WithMeta<F, M & { guards: [typeof guard] }> {
|
|
149
|
+
return transition as any;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Annotates a transition with an Invoked Service (asynchronous effect).
|
|
154
|
+
*
|
|
155
|
+
* @param service - configuration for the service (source, onDone target, onError target).
|
|
156
|
+
* @param implementation - The async function implementation.
|
|
157
|
+
* @example
|
|
158
|
+
* load = invoke(
|
|
159
|
+
* { src: "fetchData", onDone: LoadedMachine, onError: ErrorMachine },
|
|
160
|
+
* async () => { ... }
|
|
161
|
+
* );
|
|
162
|
+
*/
|
|
163
|
+
export function invoke<
|
|
164
|
+
D extends ClassConstructor,
|
|
165
|
+
E extends ClassConstructor,
|
|
166
|
+
F extends (...args: any[]) => any
|
|
167
|
+
>(
|
|
168
|
+
service: { src: string; onDone: D; onError: E; description?: string },
|
|
169
|
+
implementation: F
|
|
170
|
+
): WithMeta<F, { invoke: typeof service }> {
|
|
171
|
+
return implementation as any;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Annotates a transition with a side-effect Action.
|
|
176
|
+
* Useful for logging, analytics, or external event firing that doesn't change state structure.
|
|
177
|
+
*
|
|
178
|
+
* @param action - Object containing the name and optional description.
|
|
179
|
+
* @param transition - The transition function to annotate.
|
|
180
|
+
* @example
|
|
181
|
+
* click = action({ name: "trackClick" }, (ctx) => ...);
|
|
182
|
+
*/
|
|
183
|
+
export function action<
|
|
184
|
+
F extends (...args: any[]) => any,
|
|
185
|
+
M extends TransitionMeta
|
|
186
|
+
>(
|
|
187
|
+
action: ActionMeta,
|
|
188
|
+
transition: WithMeta<F, M>
|
|
189
|
+
): WithMeta<F, M & { actions: [typeof action] }> {
|
|
190
|
+
return transition as any;
|
|
191
|
+
}
|
package/src/react.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file React integration for @doeixd/machine
|
|
3
|
+
* @description Provides hooks for using state machines in React components
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
7
|
+
import { runMachine, AsyncMachine, Event } from './index';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* React hook for using an async state machine.
|
|
11
|
+
* @template M - The async machine type
|
|
12
|
+
* @param machineFactory - A function that creates the initial machine instance
|
|
13
|
+
* @returns A tuple of [machine, dispatch] for state and event dispatching
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* const [machine, dispatch] = useMachine(() => createFetchingMachine());
|
|
17
|
+
*
|
|
18
|
+
* // Dispatch events
|
|
19
|
+
* dispatch({ type: 'fetchUser', args: [123] });
|
|
20
|
+
*/
|
|
21
|
+
export function useMachine<M extends AsyncMachine<any>>(
|
|
22
|
+
machineFactory: () => M
|
|
23
|
+
): [M, (event: Event<M>) => Promise<M>] {
|
|
24
|
+
// Use useState to hold the machine instance, triggering re-renders on change
|
|
25
|
+
const [machine, setMachine] = useState(machineFactory);
|
|
26
|
+
|
|
27
|
+
// Use a ref to hold the runner instance so it's stable across renders
|
|
28
|
+
const runnerRef = useRef<ReturnType<typeof runMachine<any>> | null>(null);
|
|
29
|
+
|
|
30
|
+
// Initialize the runner only once
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
runnerRef.current = runMachine(machine, (nextState) => {
|
|
33
|
+
// The magic link: when the machine's state changes, update React's state.
|
|
34
|
+
setMachine(nextState as M);
|
|
35
|
+
});
|
|
36
|
+
}, []); // Empty dependency array ensures this runs only on mount
|
|
37
|
+
|
|
38
|
+
// Memoize the dispatch function so it has a stable identity
|
|
39
|
+
const dispatch = useCallback((event: Event<M>) => {
|
|
40
|
+
return runnerRef.current?.dispatch(event) || Promise.resolve(machine);
|
|
41
|
+
}, [machine]);
|
|
42
|
+
|
|
43
|
+
return [machine, dispatch];
|
|
44
|
+
}
|