@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.
@@ -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
+ }