controlled-machine 0.3.1 → 0.4.0

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/dist/index.d.cts CHANGED
@@ -1,118 +1,291 @@
1
1
  /**
2
2
  * Controlled Machine
3
3
  *
4
- * A controlled state machine where state lives outside the machine.
4
+ * A controlled state machine library where external state (input) is passed in
5
+ * and internal state is managed by the machine itself.
5
6
  *
6
- * - input: External data passed in
7
- * - computed: Derived values from input
8
- * - context: input + computed (full context available in handlers)
9
- * - on: Event conditional actions
10
- * - effects: Watch-based side effects
11
- * - always: Auto-evaluated rules on context change
12
- */
13
- export type Rule<TContext, TPayload = undefined, TActions extends string = string, TGuards extends string = string> = {
14
- when?: ((context: TContext, payload: TPayload) => boolean) | TGuards;
15
- do: TActions | TActions[];
7
+ * Key Concepts:
8
+ * - input: External data passed in from outside (e.g., React state, props)
9
+ * - internal: Machine-managed state that persists across events
10
+ * - computed: Derived values calculated from input + internal
11
+ * - context: Flattened input + internal + computed (available in all handlers)
12
+ * - on: Event handlers with conditional rules and actions
13
+ * - states: FSM-style state-based event handlers
14
+ * - effects: Watch-based side effects with enter/exit/change callbacks
15
+ * - always: Auto-evaluated rules that run on every context change
16
+ * - actions: Named action functions (can be overridden in useMachine)
17
+ * - guards: Named guard functions for conditional logic
18
+ */
19
+ /**
20
+ * Assign function type - updates internal state with partial updates
21
+ * Only allows modifying keys defined in Internal type
22
+ */
23
+ export type AssignFn<TInternal> = (updates: Partial<TInternal>) => void;
24
+ /**
25
+ * ActionItem - can be a named action string or inline function
26
+ * Inline functions receive (context, payload, assign)
27
+ */
28
+ export type ActionItem<TContext, TPayload = undefined, TActions extends string = string, TInternal = unknown> = TActions | ((context: TContext, payload: TPayload, assign: AssignFn<TInternal>) => void);
29
+ /**
30
+ * GuardItem - can be a named guard string or inline predicate function
31
+ */
32
+ export type GuardItem<TContext, TPayload = undefined, TGuards extends string = string> = TGuards | ((context: TContext, payload: TPayload) => boolean);
33
+ /**
34
+ * Rule - conditional action with optional guard(s)
35
+ * @property when - Guard(s) that must pass (AND logic for arrays)
36
+ * @property do - Action(s) to execute if guards pass
37
+ */
38
+ export type Rule<TContext, TPayload = undefined, TActions extends string = string, TGuards extends string = string, TInternal = unknown> = {
39
+ when?: GuardItem<TContext, TPayload, TGuards> | GuardItem<TContext, TPayload, TGuards>[];
40
+ do: ActionItem<TContext, TPayload, TActions, TInternal> | ActionItem<TContext, TPayload, TActions, TInternal>[];
16
41
  };
17
- export type Handler<TContext, TPayload = undefined, TActions extends string = string, TGuards extends string = string> = TActions | TActions[] | Rule<TContext, TPayload, TActions, TGuards>[] | ((context: TContext, payload: TPayload) => void);
42
+ /**
43
+ * Handler - event handler definition
44
+ * Can be: single action, action array, rule array, inline function, or function array
45
+ *
46
+ * @example
47
+ * on: { CLICK: 'handleClick' } // single action
48
+ * on: { SUBMIT: ['validate', 'save'] } // action array
49
+ * on: { TOGGLE: [{ when: ctx => ctx.isOpen, do: 'close' }, { do: 'open' }] } // rule array
50
+ * on: { INCREMENT: (ctx, _, assign) => assign({ count: ctx.count + 1 }) } // inline function
51
+ * on: { SELECT: [(ctx, p) => ctx.onSelect(p), (_, __, a) => a({ isOpen: false })] } // function array
52
+ */
53
+ export type Handler<TContext, TPayload = undefined, TActions extends string = string, TGuards extends string = string, TInternal = unknown> = TActions | TActions[] | Rule<TContext, TPayload, TActions, TGuards, TInternal>[] | ((context: TContext, payload: TPayload, assign: AssignFn<TInternal>) => void) | ((context: TContext, payload: TPayload, assign: AssignFn<TInternal>) => void)[];
54
+ /**
55
+ * EffectHelpers - utilities available in effect callbacks
56
+ */
18
57
  export type EffectHelpers<TEvents extends EventsConfig> = {
19
58
  send: Send<TEvents>;
20
59
  };
60
+ /** Cleanup function returned from effect callbacks */
21
61
  export type Cleanup = () => void;
62
+ /**
63
+ * Effect - watch-based side effect with lifecycle callbacks
64
+ * @property watch - Function that returns the value to watch (uses shallow comparison)
65
+ * @property enter - Called when watch value becomes truthy (can return cleanup)
66
+ * @property exit - Called when watch value becomes falsy (can return cleanup)
67
+ * @property change - Called on any value change with (prev, curr) (can return cleanup)
68
+ */
22
69
  export type Effect<TContext, TEvents extends EventsConfig, TWatched = unknown> = {
23
70
  watch: (context: TContext) => TWatched;
24
71
  enter?: (context: TContext, helpers: EffectHelpers<TEvents>) => void | Cleanup | Promise<void>;
25
72
  exit?: (context: TContext, helpers: EffectHelpers<TEvents>) => void | Cleanup;
26
73
  change?: (context: TContext, prev: TWatched | undefined, curr: TWatched, helpers: EffectHelpers<TEvents>) => void | Cleanup;
27
74
  };
28
- /** Helper for inferring prev/curr types from watch return type */
75
+ /**
76
+ * Helper function for creating effects with proper type inference
77
+ * @example
78
+ * effects: [
79
+ * effect({ watch: ctx => ctx.isOpen, enter: () => console.log('opened') })
80
+ * ]
81
+ */
29
82
  export declare function effect<TContext, TEvents extends EventsConfig, TWatched>(config: Effect<TContext, TEvents, TWatched>): Effect<TContext, TEvents, TWatched>;
83
+ /** Event configuration - event name to payload type mapping */
30
84
  export type EventsConfig = Record<string, unknown>;
85
+ /** Computed configuration - computed key to value type mapping */
31
86
  export type ComputedConfig = Record<string, unknown>;
32
87
  /**
33
- * Object-based generic types - specify only needed types in any order
88
+ * MachineTypes - object-based generic type parameter
89
+ * Specify only the types you need, in any order
34
90
  *
35
91
  * @example
36
92
  * createMachine<{
37
- * input: MyInput
38
- * events: MyEvents
39
- * actions: 'foo' | 'bar'
93
+ * input: { count: number; setCount: (c: number) => void }
94
+ * internal: { isOpen: boolean }
95
+ * events: { INCREMENT: undefined; SET: { value: number } }
96
+ * computed: { doubled: number }
97
+ * actions: 'increment' | 'set'
98
+ * guards: 'isPositive'
99
+ * state: 'idle' | 'loading'
40
100
  * }>({...})
41
101
  */
42
102
  export type MachineTypes = {
43
103
  input?: unknown;
104
+ internal?: unknown;
44
105
  events?: EventsConfig;
45
106
  computed?: ComputedConfig;
46
107
  actions?: string;
47
108
  guards?: string;
48
109
  state?: string;
49
110
  };
50
- export type Input<T extends MachineTypes> = T['input'];
111
+ export type Input<T extends MachineTypes> = T['input'] extends object ? T['input'] : {};
112
+ export type Internal<T extends MachineTypes> = T['internal'] extends object ? T['internal'] : {};
51
113
  export type Events<T extends MachineTypes> = T['events'] extends EventsConfig ? T['events'] : Record<string, undefined>;
52
- export type Computed<T extends MachineTypes> = T['computed'] extends ComputedConfig ? T['computed'] : Record<string, never>;
114
+ export type Computed<T extends MachineTypes> = T['computed'] extends ComputedConfig ? T['computed'] : {};
53
115
  export type Actions<T extends MachineTypes> = T['actions'] extends string ? T['actions'] : string;
54
116
  export type Guards<T extends MachineTypes> = T['guards'] extends string ? T['guards'] : string;
55
117
  export type State<T extends MachineTypes> = T['state'] extends string ? T['state'] : string;
56
- export type Context<T extends MachineTypes> = Input<T> & Computed<T>;
57
- export type StateConfig<TContext, TEvents extends EventsConfig, TActions extends string = string> = {
118
+ /**
119
+ * Detects if two types have overlapping keys
120
+ * Returns true if any key exists in both A and B
121
+ */
122
+ type HasOverlappingKeys<A, B> = keyof A & keyof B extends never ? false : true;
123
+ /**
124
+ * Check all combinations of key overlaps between Input, Internal, Computed
125
+ * Uses conditional chain (not union) to ensure proper boolean result
126
+ */
127
+ type HasAnyKeyOverlap<T extends MachineTypes> = HasOverlappingKeys<Input<T>, Internal<T>> extends true ? true : HasOverlappingKeys<Input<T>, Computed<T>> extends true ? true : HasOverlappingKeys<Internal<T>, Computed<T>> extends true ? true : false;
128
+ /**
129
+ * Context = Input + Internal + Computed (flat structure)
130
+ * All properties are accessible at the same level in handlers
131
+ *
132
+ * If any pair has overlapping keys, Context becomes `never` (compile-time error)
133
+ */
134
+ export type Context<T extends MachineTypes> = HasAnyKeyOverlap<T> extends true ? never : Input<T> & Internal<T> & Computed<T>;
135
+ /** Configuration for handlers within a specific state */
136
+ export type StateConfig<TContext, TEvents extends EventsConfig, TActions extends string = string, TGuards extends string = string, TInternal = unknown> = {
58
137
  on?: {
59
- [K in keyof TEvents]?: Handler<TContext, TEvents[K], TActions>;
138
+ [K in keyof TEvents]?: Handler<TContext, TEvents[K], TActions, TGuards, TInternal>;
60
139
  };
61
140
  };
62
- export type StatesConfig<TState extends string, TContext, TEvents extends EventsConfig, TActions extends string = string> = {
63
- [K in TState]?: StateConfig<TContext, TEvents, TActions>;
141
+ /** Map of state names to their configurations */
142
+ export type StatesConfig<TState extends string, TContext, TEvents extends EventsConfig, TActions extends string = string, TGuards extends string = string, TInternal = unknown> = {
143
+ [K in TState]?: StateConfig<TContext, TEvents, TActions, TGuards, TInternal>;
64
144
  };
145
+ /** Base context (input + internal) before computed values are added */
146
+ export type BaseContext<T extends MachineTypes> = Input<T> & Internal<T>;
147
+ /**
148
+ * Machine - the configuration object for createMachine
149
+ */
65
150
  export type Machine<T extends MachineTypes> = {
151
+ internal?: Internal<T>;
66
152
  computed?: {
67
- [K in keyof Computed<T>]: (input: Input<T>) => Computed<T>[K];
153
+ [K in keyof Computed<T>]: (ctx: BaseContext<T>) => Computed<T>[K];
68
154
  };
69
155
  on?: {
70
- [K in keyof Events<T>]?: Handler<Context<T>, Events<T>[K], Actions<T>, Guards<T>>;
156
+ [K in keyof Events<T>]?: Handler<Context<T>, Events<T>[K], Actions<T>, Guards<T>, Internal<T>>;
71
157
  };
72
- states?: StatesConfig<State<T>, Context<T>, Events<T>, Actions<T>>;
73
- always?: Rule<Context<T>, undefined, Actions<T>, Guards<T>>[];
158
+ states?: StatesConfig<State<T>, Context<T>, Events<T>, Actions<T>, Guards<T>, Internal<T>>;
159
+ always?: Rule<Context<T>, undefined, Actions<T>, Guards<T>, Internal<T>>[];
74
160
  effects?: Effect<Context<T>, Events<T>, any>[];
75
161
  actions?: {
76
- [K in Actions<T>]: (context: Context<T>, payload?: any) => void;
162
+ [K in Actions<T>]: (ctx: Context<T>, payload: any, assign: AssignFn<Internal<T>>) => void;
77
163
  };
78
164
  guards?: {
79
- [K in Guards<T>]: (context: Context<T>, payload?: any) => boolean;
165
+ [K in Guards<T>]: (ctx: Context<T>, payload?: any) => boolean;
80
166
  };
81
167
  };
168
+ /**
169
+ * Send - function type for dispatching events
170
+ * Events with undefined payload can be called without arguments
171
+ */
82
172
  export type Send<TEvents extends EventsConfig> = <K extends keyof TEvents>(event: K, ...args: TEvents[K] extends undefined ? [] : [payload: TEvents[K]]) => void;
173
+ /**
174
+ * Snapshot = Internal + Computed + { state } (without Input)
175
+ * This is the value returned from getSnapshot() and useMachine()
176
+ *
177
+ * If Internal/Computed have overlapping keys, Snapshot becomes `never`
178
+ * If 'state' type param is defined and Internal/Computed already has 'state' key,
179
+ * we don't add { state } again (existing state is already included)
180
+ */
181
+ export type Snapshot<T extends MachineTypes> = HasOverlappingKeys<Internal<T>, Computed<T>> extends true ? never : Internal<T> & Computed<T> & (T['state'] extends string ? 'state' extends keyof Internal<T> | keyof Computed<T> ? object : {
182
+ state: State<T>;
183
+ } : object);
184
+ /**
185
+ * MachineInstance - the return type of createMachine
186
+ * Includes all configuration plus runtime methods
187
+ */
83
188
  export type MachineInstance<T extends MachineTypes> = Machine<T> & {
189
+ /** Dispatch an event with input and optional payload */
84
190
  send: <K extends keyof Events<T>>(event: K, input: Input<T>, ...args: Events<T>[K] extends undefined ? [] : [payload: Events<T>[K]]) => void;
191
+ /** Evaluate always rules and effects (called automatically in React) */
85
192
  evaluate: (input: Input<T>) => void;
86
- getComputed: (input: Input<T>) => Computed<T>;
193
+ /** Get current snapshot (internal + computed + state) */
194
+ getSnapshot: (input: Input<T>) => Snapshot<T>;
195
+ /** Get current internal state */
196
+ getInternal: () => Internal<T>;
197
+ /** Set internal state directly */
198
+ setInternal: (internal: Internal<T>) => void;
199
+ /** Get initial internal state (for reset) */
200
+ getInitialInternal: () => Internal<T>;
201
+ /** Clean up all effect callbacks */
87
202
  cleanup: () => void;
88
203
  };
89
- export declare function executeActions<TContext, TPayload>(actionNames: string | string[], actions: Record<string, (context: TContext, payload?: TPayload) => void>, context: TContext, payload: TPayload): void;
204
+ /**
205
+ * Execute action items (named actions or inline functions)
206
+ * Handles both single actions and arrays of actions
207
+ * Each action receives fresh context (rebuilt after previous assigns)
208
+ */
209
+ export declare function executeRuleActions<TContext, TPayload, TInternal>(actionItems: ActionItem<TContext, TPayload, string, TInternal> | ActionItem<TContext, TPayload, string, TInternal>[], actions: Record<string, (context: TContext, payload: TPayload, assign: AssignFn<TInternal>) => void>, getContext: () => TContext, payload: TPayload, assign: AssignFn<TInternal>): void;
210
+ /**
211
+ * Evaluate guard items (named guards or inline predicates)
212
+ * Uses AND logic - all guards must pass for result to be true
213
+ */
214
+ export declare function evaluateGuards<TContext, TPayload>(guardItems: GuardItem<TContext, TPayload> | GuardItem<TContext, TPayload>[] | undefined, guards: Record<string, (context: TContext, payload?: TPayload) => boolean>, context: TContext, payload: TPayload): boolean;
215
+ /** Type guard to check if handler is a Rule array (has 'do' property) */
90
216
  export declare function isRuleArray<TContext, TPayload, TActions extends string>(handler: Handler<TContext, TPayload, TActions>): handler is Rule<TContext, TPayload, TActions>[];
91
- export declare function executeHandler<TContext, TPayload>(handler: Handler<TContext, TPayload>, actions: Record<string, (context: TContext, payload?: TPayload) => void>, guards: Record<string, (context: TContext, payload?: TPayload) => boolean>, context: TContext, payload: TPayload): void;
92
- export declare function computeValues<TContext, TComputed extends ComputedConfig>(context: TContext, computed?: {
93
- [K in keyof TComputed]: (context: TContext) => TComputed[K];
94
- }): TContext & TComputed;
95
217
  /**
96
- * Shallow comparison function - for composite watch support
97
- *
98
- * Arrays: length + === comparison for each element
99
- * Others: === comparison
218
+ * Execute a handler (action string, action array, rule array, inline function, or function array)
219
+ * For rule arrays, only the first matching rule executes (short-circuit)
220
+ * Each action/function receives fresh context (rebuilt after previous assigns)
221
+ */
222
+ export declare function executeHandler<TContext, TPayload, TInternal>(handler: Handler<TContext, TPayload, string, string, TInternal>, actions: Record<string, (context: TContext, payload: TPayload, assign: AssignFn<TInternal>) => void>, guards: Record<string, (context: TContext, payload?: TPayload) => boolean>, getContext: () => TContext, payload: TPayload, assign: AssignFn<TInternal>): void;
223
+ /**
224
+ * Compute derived values from base context
225
+ * Each computed function receives the base context (input + internal)
226
+ */
227
+ export declare function computeValues<TBase, TComputed extends ComputedConfig>(base: TBase, computed?: {
228
+ [K in keyof TComputed]: (ctx: TBase) => TComputed[K];
229
+ }): TBase & TComputed;
230
+ /**
231
+ * Build flat context from input + internal
232
+ * Input takes priority if keys overlap (runtime)
233
+ */
234
+ export declare function buildContext<TInput, TInternal>(input: TInput, internal: TInternal): TInput & TInternal;
235
+ /**
236
+ * Create assign function for updating internal state
237
+ */
238
+ export declare function createAssign<TInternal>(getInternal: () => TInternal, setInternal: (internal: TInternal) => void): AssignFn<TInternal>;
239
+ /**
240
+ * Build snapshot from internal state, context, and computed definitions
241
+ * Snapshot = Internal + Computed + state (without Input)
242
+ */
243
+ export declare function buildSnapshot<T extends MachineTypes>(internal: Internal<T>, context: Context<T>, computedDef: Machine<T>['computed']): Snapshot<T>;
244
+ /**
245
+ * Shallow comparison for effect watch values
246
+ * Arrays: compares length and each element with ===
247
+ * Others: strict equality (===)
100
248
  */
101
249
  export declare function shallowEqual(a: unknown, b: unknown): boolean;
250
+ /**
251
+ * EffectStore - tracks watched values and cleanup functions for effects
252
+ * Used by both vanilla and React implementations
253
+ */
102
254
  export type EffectStore = {
103
255
  watchedValues: Map<number, unknown>;
104
256
  enterCleanups: Map<number, () => void>;
105
257
  changeCleanups: Map<number, () => void>;
106
258
  exitCleanups: Map<number, () => void>;
107
259
  };
260
+ /** Create a new effect store */
108
261
  export declare function createEffectStore(): EffectStore;
109
262
  /**
110
- * Common effects processing logic
263
+ * Process all effects - detect watch value changes and call appropriate callbacks
264
+ * Called on every context change in both vanilla (evaluate) and React (useEffect)
111
265
  */
112
266
  export declare function processEffects<TContext, TEvents extends EventsConfig>(effects: Effect<TContext, TEvents, any>[] | undefined, context: TContext, effectHelpers: EffectHelpers<TEvents>, store: EffectStore): void;
267
+ /** Clear all effect cleanups (called on unmount or cleanup) */
268
+ export declare function clearEffectStore(store: EffectStore): void;
113
269
  /**
114
- * Clear effect store
270
+ * Create a controlled state machine instance
271
+ *
272
+ * The machine manages internal state and provides methods for:
273
+ * - send: Dispatch events with input and payload
274
+ * - evaluate: Run always rules and effects
275
+ * - getSnapshot: Get current state (internal + computed + state)
276
+ *
277
+ * @example
278
+ * const machine = createMachine<{
279
+ * input: { count: number }
280
+ * internal: { isOpen: boolean }
281
+ * events: { TOGGLE: undefined }
282
+ * computed: { doubled: number }
283
+ * }>({
284
+ * internal: { isOpen: false },
285
+ * computed: { doubled: ctx => ctx.count * 2 },
286
+ * on: { TOGGLE: (ctx, _, assign) => assign({ isOpen: !ctx.isOpen }) }
287
+ * })
115
288
  */
116
- export declare function clearEffectStore(store: EffectStore): void;
117
289
  export declare function createMachine<T extends MachineTypes>(config: Machine<T>): MachineInstance<T>;
290
+ export {};
118
291
  //# sourceMappingURL=index.d.ts.map