controlled-machine 0.3.2 → 0.4.1

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