controlled-machine 0.3.2 → 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.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["/**\n * Controlled Machine\n *\n * A controlled state machine where state lives outside the machine.\n *\n * - input: External data passed in\n * - computed: Derived values from input\n * - context: input + computed (full context available in handlers)\n * - on: Event → conditional actions\n * - effects: Watch-based side effects\n * - always: Auto-evaluated rules on context change\n */\n\n// ============================================\n// Types\n// ============================================\n\nexport type ActionItem<\n TContext,\n TPayload = undefined,\n TActions extends string = string,\n> = TActions | ((context: TContext, payload: TPayload) => void)\n\nexport type GuardItem<\n TContext,\n TPayload = undefined,\n TGuards extends string = string,\n> = TGuards | ((context: TContext, payload: TPayload) => boolean)\n\nexport type Rule<\n TContext,\n TPayload = undefined,\n TActions extends string = string,\n TGuards extends string = string,\n> = {\n when?: GuardItem<TContext, TPayload, TGuards> | GuardItem<TContext, TPayload, TGuards>[]\n do: ActionItem<TContext, TPayload, TActions> | ActionItem<TContext, TPayload, TActions>[]\n}\n\nexport type Handler<\n TContext,\n TPayload = undefined,\n TActions extends string = string,\n TGuards extends string = string,\n> =\n | TActions\n | TActions[]\n | Rule<TContext, TPayload, TActions, TGuards>[]\n | ((context: TContext, payload: TPayload) => void)\n\n// Effect helpers - utilities available in effect callbacks\nexport type EffectHelpers<TEvents extends EventsConfig> = {\n send: Send<TEvents>\n}\n\nexport type Cleanup = () => void\n\nexport type Effect<\n TContext,\n TEvents extends EventsConfig,\n TWatched = unknown,\n> = {\n watch: (context: TContext) => TWatched\n enter?: (\n context: TContext,\n helpers: EffectHelpers<TEvents>,\n ) => void | Cleanup | Promise<void>\n exit?: (context: TContext, helpers: EffectHelpers<TEvents>) => void | Cleanup\n change?: (\n context: TContext,\n prev: TWatched | undefined,\n curr: TWatched,\n helpers: EffectHelpers<TEvents>,\n ) => void | Cleanup\n}\n\n/** Helper for inferring prev/curr types from watch return type */\nexport function effect<TContext, TEvents extends EventsConfig, TWatched>(\n config: Effect<TContext, TEvents, TWatched>,\n): Effect<TContext, TEvents, TWatched> {\n return config\n}\n\nexport type EventsConfig = Record<string, unknown>\nexport type ComputedConfig = Record<string, unknown>\n\n// ============================================\n// Object-based Generic Types\n// ============================================\n\n/**\n * Object-based generic types - specify only needed types in any order\n *\n * @example\n * createMachine<{\n * input: MyInput\n * events: MyEvents\n * actions: 'foo' | 'bar'\n * }>({...})\n */\nexport type MachineTypes = {\n input?: unknown\n events?: EventsConfig\n computed?: ComputedConfig\n actions?: string\n guards?: string\n state?: string\n}\n\nexport type Input<T extends MachineTypes> = T['input']\nexport type Events<T extends MachineTypes> = T['events'] extends EventsConfig\n ? T['events']\n : Record<string, undefined>\nexport type Computed<T extends MachineTypes> = T['computed'] extends ComputedConfig\n ? T['computed']\n : Record<string, never>\nexport type Actions<T extends MachineTypes> = T['actions'] extends string\n ? T['actions']\n : string\nexport type Guards<T extends MachineTypes> = T['guards'] extends string\n ? T['guards']\n : string\nexport type State<T extends MachineTypes> = T['state'] extends string\n ? T['state']\n : string\n\n// Context = Input + Computed (full context available in handlers)\nexport type Context<T extends MachineTypes> = Input<T> & Computed<T>\n\n// State-based handler configuration\nexport type StateConfig<\n TContext,\n TEvents extends EventsConfig,\n TActions extends string = string,\n> = {\n on?: { [K in keyof TEvents]?: Handler<TContext, TEvents[K], TActions> }\n}\n\nexport type StatesConfig<\n TState extends string,\n TContext,\n TEvents extends EventsConfig,\n TActions extends string = string,\n> = {\n [K in TState]?: StateConfig<TContext, TEvents, TActions>\n}\n\nexport type Machine<T extends MachineTypes> = {\n computed?: {\n [K in keyof Computed<T>]: (input: Input<T>) => Computed<T>[K]\n }\n on?: {\n [K in keyof Events<T>]?: Handler<Context<T>, Events<T>[K], Actions<T>, Guards<T>>\n }\n states?: StatesConfig<State<T>, Context<T>, Events<T>, Actions<T>>\n always?: Rule<Context<T>, undefined, Actions<T>, Guards<T>>[]\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n effects?: Effect<Context<T>, Events<T>, any>[]\n actions?: {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n [K in Actions<T>]: (context: Context<T>, payload?: any) => void\n }\n guards?: {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n [K in Guards<T>]: (context: Context<T>, payload?: any) => boolean\n }\n}\n\nexport type Send<TEvents extends EventsConfig> = <K extends keyof TEvents>(\n event: K,\n ...args: TEvents[K] extends undefined ? [] : [payload: TEvents[K]]\n) => void\n\n// createMachine return type\nexport type MachineInstance<T extends MachineTypes> = Machine<T> & {\n send: <K extends keyof Events<T>>(\n event: K,\n input: Input<T>,\n ...args: Events<T>[K] extends undefined ? [] : [payload: Events<T>[K]]\n ) => void\n evaluate: (input: Input<T>) => void\n getComputed: (input: Input<T>) => Computed<T>\n cleanup: () => void\n}\n\n// ============================================\n// Core Logic (Pure)\n// ============================================\n\nexport function executeActions<TContext, TPayload>(\n actionNames: string | string[],\n actions: Record<string, (context: TContext, payload?: TPayload) => void>,\n context: TContext,\n payload: TPayload,\n): void {\n if (typeof actionNames === 'string') {\n actions[actionNames]?.(context, payload)\n } else {\n for (const name of actionNames) {\n actions[name]?.(context, payload)\n }\n }\n}\n\nexport function executeRuleActions<TContext, TPayload>(\n actionItems: ActionItem<TContext, TPayload> | ActionItem<TContext, TPayload>[],\n actions: Record<string, (context: TContext, payload?: TPayload) => void>,\n context: TContext,\n payload: TPayload,\n): void {\n const items = Array.isArray(actionItems) ? actionItems : [actionItems]\n\n for (const item of items) {\n if (typeof item === 'function') {\n item(context, payload)\n } else {\n actions[item]?.(context, payload)\n }\n }\n}\n\nexport function evaluateGuards<TContext, TPayload>(\n guardItems: GuardItem<TContext, TPayload> | GuardItem<TContext, TPayload>[] | undefined,\n guards: Record<string, (context: TContext, payload?: TPayload) => boolean>,\n context: TContext,\n payload: TPayload,\n): boolean {\n if (!guardItems) return true\n\n const items = Array.isArray(guardItems) ? guardItems : [guardItems]\n\n for (const item of items) {\n const guardFn = typeof item === 'function' ? item : guards[item]\n if (guardFn && !guardFn(context, payload)) {\n return false\n }\n }\n\n return true\n}\n\nexport function isRuleArray<TContext, TPayload, TActions extends string>(\n handler: Handler<TContext, TPayload, TActions>,\n): handler is Rule<TContext, TPayload, TActions>[] {\n return (\n Array.isArray(handler) &&\n handler.length > 0 &&\n typeof handler[0] === 'object' &&\n 'do' in handler[0]\n )\n}\n\nexport function executeHandler<TContext, TPayload>(\n handler: Handler<TContext, TPayload>,\n actions: Record<string, (context: TContext, payload?: TPayload) => void>,\n guards: Record<string, (context: TContext, payload?: TPayload) => boolean>,\n context: TContext,\n payload: TPayload,\n): void {\n // Single action or action array\n if (typeof handler === 'string' || (Array.isArray(handler) && !isRuleArray(handler))) {\n executeActions(handler as string | string[], actions, context, payload)\n return\n }\n\n // Rule array\n for (const rule of handler as Rule<TContext, TPayload>[]) {\n if (evaluateGuards(rule.when, guards, context, payload)) {\n executeRuleActions(rule.do, actions, context, payload)\n break\n }\n }\n}\n\nexport function computeValues<TContext, TComputed extends ComputedConfig>(\n context: TContext,\n computed?: { [K in keyof TComputed]: (context: TContext) => TComputed[K] },\n): TContext & TComputed {\n if (!computed) return context as TContext & TComputed\n\n const values = {} as TComputed\n for (const key in computed) {\n values[key] = computed[key](context)\n }\n return { ...context, ...values }\n}\n\n/**\n * Shallow comparison function - for composite watch support\n *\n * Arrays: length + === comparison for each element\n * Others: === comparison\n */\nexport function shallowEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true\n\n // Array comparison\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false\n return a.every((v, i) => v === b[i])\n }\n\n return false\n}\n\n// Effect store for tracking state\nexport type EffectStore = {\n watchedValues: Map<number, unknown>\n enterCleanups: Map<number, () => void>\n changeCleanups: Map<number, () => void>\n exitCleanups: Map<number, () => void>\n}\n\nexport function createEffectStore(): EffectStore {\n return {\n watchedValues: new Map(),\n enterCleanups: new Map(),\n changeCleanups: new Map(),\n exitCleanups: new Map(),\n }\n}\n\n/**\n * Common effects processing logic\n */\nexport function processEffects<TContext, TEvents extends EventsConfig>(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n effects: Effect<TContext, TEvents, any>[] | undefined,\n context: TContext,\n effectHelpers: EffectHelpers<TEvents>,\n store: EffectStore,\n): void {\n if (!effects) return\n\n effects.forEach((effect, i) => {\n const prev = store.watchedValues.get(i)\n const curr = effect.watch(context)\n\n if (!shallowEqual(prev, curr)) {\n // cleanup previous enter\n const enterCleanup = store.enterCleanups.get(i)\n if (enterCleanup) {\n enterCleanup()\n store.enterCleanups.delete(i)\n }\n\n // cleanup previous change\n const changeCleanup = store.changeCleanups.get(i)\n if (changeCleanup) {\n changeCleanup()\n store.changeCleanups.delete(i)\n }\n\n // change callback (can return cleanup)\n const changeResult = effect.change?.(context, prev, curr, effectHelpers)\n if (typeof changeResult === 'function') {\n store.changeCleanups.set(i, changeResult)\n }\n\n // enter (falsy → truthy)\n if (!prev && curr) {\n // cleanup previous exit\n const exitCleanup = store.exitCleanups.get(i)\n if (exitCleanup) {\n exitCleanup()\n store.exitCleanups.delete(i)\n }\n\n const enterResult = effect.enter?.(context, effectHelpers)\n if (typeof enterResult === 'function') {\n store.enterCleanups.set(i, enterResult)\n }\n }\n\n // exit (truthy → falsy)\n if (prev && !curr) {\n const exitResult = effect.exit?.(context, effectHelpers)\n if (typeof exitResult === 'function') {\n store.exitCleanups.set(i, exitResult)\n }\n }\n\n store.watchedValues.set(i, curr)\n }\n })\n}\n\n/**\n * Clear effect store\n */\nexport function clearEffectStore(store: EffectStore): void {\n store.enterCleanups.forEach((fn) => fn())\n store.enterCleanups.clear()\n store.changeCleanups.forEach((fn) => fn())\n store.changeCleanups.clear()\n store.exitCleanups.forEach((fn) => fn())\n store.exitCleanups.clear()\n store.watchedValues.clear()\n}\n\n// ============================================\n// Vanilla (non-React) + Type inference helper\n// ============================================\n\nexport function createMachine<T extends MachineTypes>(\n config: Machine<T>,\n): MachineInstance<T> {\n const effectStore = createEffectStore()\n\n const send = (<K extends keyof Events<T>>(\n event: K,\n input: Input<T>,\n ...args: Events<T>[K] extends undefined ? [] : [payload: Events<T>[K]]\n ) => {\n const context = computeValues(input, config.computed)\n const payload = args[0] as Events<T>[K]\n\n // 1. State-specific handler first\n const state = (context as { state?: State<T> }).state\n if (state && config.states?.[state]?.on?.[event]) {\n const stateHandler = config.states[state].on[event]\n executeHandler(stateHandler, config.actions ?? {}, config.guards ?? {}, context, payload)\n }\n\n // 2. Global handler\n const globalHandler = config.on?.[event]\n if (globalHandler) {\n executeHandler(globalHandler, config.actions ?? {}, config.guards ?? {}, context, payload)\n }\n }) as <K extends keyof Events<T>>(\n event: K,\n input: Input<T>,\n ...args: Events<T>[K] extends undefined ? [] : [payload: Events<T>[K]]\n ) => void\n\n // vanilla send wrapper (with input binding)\n const createEffectHelpersWithInput = (input: Input<T>): EffectHelpers<Events<T>> => ({\n send: (<K extends keyof Events<T>>(\n event: K,\n ...args: Events<T>[K] extends undefined ? [] : [payload: Events<T>[K]]\n ) => {\n send(event, input, ...args)\n }) as Send<Events<T>>,\n })\n\n const evaluate = (input: Input<T>) => {\n const context = computeValues(input, config.computed)\n const effectHelpers = createEffectHelpersWithInput(input)\n\n // always\n if (config.always) {\n const actionsMap = (config.actions ?? {}) as Record<\n string,\n (context: Context<T>) => void\n >\n const guardsMap = (config.guards ?? {}) as Record<\n string,\n (context: Context<T>) => boolean\n >\n for (const rule of config.always) {\n if (evaluateGuards(rule.when, guardsMap, context, undefined)) {\n executeRuleActions(rule.do, actionsMap, context, undefined)\n break\n }\n }\n }\n\n // effects\n processEffects(config.effects, context, effectHelpers, effectStore)\n }\n\n const getComputed = (input: Input<T>): Computed<T> => {\n const context = computeValues(input, config.computed)\n if (!config.computed) return {} as Computed<T>\n const result = {} as Computed<T>\n for (const key in config.computed) {\n result[key] = context[key]\n }\n return result\n }\n\n const cleanup = () => clearEffectStore(effectStore)\n\n return Object.assign(config, { send, evaluate, getComputed, cleanup })\n}\n"],"names":["effect"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["/**\n * Controlled Machine\n *\n * A controlled state machine library where external state (input) is passed in\n * and internal state is managed by the machine itself.\n *\n * Key Concepts:\n * - input: External data passed in from outside (e.g., React state, props)\n * - internal: Machine-managed state that persists across events\n * - computed: Derived values calculated from input + internal\n * - context: Flattened input + internal + computed (available in all handlers)\n * - on: Event handlers with conditional rules and actions\n * - states: FSM-style state-based event handlers\n * - effects: Watch-based side effects with enter/exit/change callbacks\n * - always: Auto-evaluated rules that run on every context change\n * - actions: Named action functions (can be overridden in useMachine)\n * - guards: Named guard functions for conditional logic\n */\n\n// ============================================\n// Types - Core Building Blocks\n// ============================================\n\n/**\n * Assign function type - updates internal state with partial updates\n * Only allows modifying keys defined in Internal type\n */\nexport type AssignFn<TInternal> = (updates: Partial<TInternal>) => void\n\n/**\n * ActionItem - can be a named action string or inline function\n * Inline functions receive (context, payload, assign)\n */\nexport type ActionItem<\n TContext,\n TPayload = undefined,\n TActions extends string = string,\n TInternal = unknown,\n> = TActions | ((context: TContext, payload: TPayload, assign: AssignFn<TInternal>) => void)\n\n/**\n * GuardItem - can be a named guard string or inline predicate function\n */\nexport type GuardItem<\n TContext,\n TPayload = undefined,\n TGuards extends string = string,\n> = TGuards | ((context: TContext, payload: TPayload) => boolean)\n\n/**\n * Rule - conditional action with optional guard(s)\n * @property when - Guard(s) that must pass (AND logic for arrays)\n * @property do - Action(s) to execute if guards pass\n */\nexport type Rule<\n TContext,\n TPayload = undefined,\n TActions extends string = string,\n TGuards extends string = string,\n TInternal = unknown,\n> = {\n when?: GuardItem<TContext, TPayload, TGuards> | GuardItem<TContext, TPayload, TGuards>[]\n do: ActionItem<TContext, TPayload, TActions, TInternal> | ActionItem<TContext, TPayload, TActions, TInternal>[]\n}\n\n/**\n * Handler - event handler definition\n * Can be: single action, action array, rule array, inline function, or function array\n *\n * @example\n * on: { CLICK: 'handleClick' } // single action\n * on: { SUBMIT: ['validate', 'save'] } // action array\n * on: { TOGGLE: [{ when: ctx => ctx.isOpen, do: 'close' }, { do: 'open' }] } // rule array\n * on: { INCREMENT: (ctx, _, assign) => assign({ count: ctx.count + 1 }) } // inline function\n * on: { SELECT: [(ctx, p) => ctx.onSelect(p), (_, __, a) => a({ isOpen: false })] } // function array\n */\nexport type Handler<\n TContext,\n TPayload = undefined,\n TActions extends string = string,\n TGuards extends string = string,\n TInternal = unknown,\n> =\n | TActions\n | TActions[]\n | Rule<TContext, TPayload, TActions, TGuards, TInternal>[]\n | ((context: TContext, payload: TPayload, assign: AssignFn<TInternal>) => void)\n | ((context: TContext, payload: TPayload, assign: AssignFn<TInternal>) => void)[]\n\n/**\n * EffectHelpers - utilities available in effect callbacks\n */\nexport type EffectHelpers<TEvents extends EventsConfig> = {\n send: Send<TEvents>\n}\n\n/** Cleanup function returned from effect callbacks */\nexport type Cleanup = () => void\n\n/**\n * Effect - watch-based side effect with lifecycle callbacks\n * @property watch - Function that returns the value to watch (uses shallow comparison)\n * @property enter - Called when watch value becomes truthy (can return cleanup)\n * @property exit - Called when watch value becomes falsy (can return cleanup)\n * @property change - Called on any value change with (prev, curr) (can return cleanup)\n */\nexport type Effect<\n TContext,\n TEvents extends EventsConfig,\n TWatched = unknown,\n> = {\n watch: (context: TContext) => TWatched\n enter?: (\n context: TContext,\n helpers: EffectHelpers<TEvents>,\n ) => void | Cleanup | Promise<void>\n exit?: (context: TContext, helpers: EffectHelpers<TEvents>) => void | Cleanup\n change?: (\n context: TContext,\n prev: TWatched | undefined,\n curr: TWatched,\n helpers: EffectHelpers<TEvents>,\n ) => void | Cleanup\n}\n\n/**\n * Helper function for creating effects with proper type inference\n * @example\n * effects: [\n * effect({ watch: ctx => ctx.isOpen, enter: () => console.log('opened') })\n * ]\n */\nexport function effect<TContext, TEvents extends EventsConfig, TWatched>(\n config: Effect<TContext, TEvents, TWatched>,\n): Effect<TContext, TEvents, TWatched> {\n return config\n}\n\n/** Event configuration - event name to payload type mapping */\nexport type EventsConfig = Record<string, unknown>\n/** Computed configuration - computed key to value type mapping */\nexport type ComputedConfig = Record<string, unknown>\n\n// ============================================\n// Object-based Generic Types\n// ============================================\n\n/**\n * MachineTypes - object-based generic type parameter\n * Specify only the types you need, in any order\n *\n * @example\n * createMachine<{\n * input: { count: number; setCount: (c: number) => void }\n * internal: { isOpen: boolean }\n * events: { INCREMENT: undefined; SET: { value: number } }\n * computed: { doubled: number }\n * actions: 'increment' | 'set'\n * guards: 'isPositive'\n * state: 'idle' | 'loading'\n * }>({...})\n */\nexport type MachineTypes = {\n input?: unknown // External state passed in (React state, props, etc.)\n internal?: unknown // Machine-managed state\n events?: EventsConfig // Event name → payload type\n computed?: ComputedConfig // Derived values from context\n actions?: string // Named action strings\n guards?: string // Named guard strings\n state?: string // FSM state values\n}\n\n// ============================================\n// Type Extraction Helpers\n// ============================================\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport type Input<T extends MachineTypes> = T['input'] extends object ? T['input'] : {}\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport type Internal<T extends MachineTypes> = T['internal'] extends object ? T['internal'] : {}\nexport type Events<T extends MachineTypes> = T['events'] extends EventsConfig\n ? T['events']\n : Record<string, undefined>\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport type Computed<T extends MachineTypes> = T['computed'] extends ComputedConfig\n ? T['computed']\n : {}\nexport type Actions<T extends MachineTypes> = T['actions'] extends string\n ? T['actions']\n : string\nexport type Guards<T extends MachineTypes> = T['guards'] extends string\n ? T['guards']\n : string\nexport type State<T extends MachineTypes> = T['state'] extends string\n ? T['state']\n : string\n\n// ============================================\n// Key Overlap Detection (Compile-time Safety)\n// ============================================\n\n/**\n * Detects if two types have overlapping keys\n * Returns true if any key exists in both A and B\n */\ntype HasOverlappingKeys<A, B> = keyof A & keyof B extends never ? false : true\n\n/**\n * Check all combinations of key overlaps between Input, Internal, Computed\n * Uses conditional chain (not union) to ensure proper boolean result\n */\ntype HasAnyKeyOverlap<T extends MachineTypes> =\n HasOverlappingKeys<Input<T>, Internal<T>> extends true ? true :\n HasOverlappingKeys<Input<T>, Computed<T>> extends true ? true :\n HasOverlappingKeys<Internal<T>, Computed<T>> extends true ? true :\n false\n\n/**\n * Context = Input + Internal + Computed (flat structure)\n * All properties are accessible at the same level in handlers\n *\n * If any pair has overlapping keys, Context becomes `never` (compile-time error)\n */\nexport type Context<T extends MachineTypes> =\n HasAnyKeyOverlap<T> extends true\n ? never\n : Input<T> & Internal<T> & Computed<T>\n\n// ============================================\n// State-based Handler Configuration (FSM)\n// ============================================\n\n/** Configuration for handlers within a specific state */\nexport type StateConfig<\n TContext,\n TEvents extends EventsConfig,\n TActions extends string = string,\n TGuards extends string = string,\n TInternal = unknown,\n> = {\n on?: { [K in keyof TEvents]?: Handler<TContext, TEvents[K], TActions, TGuards, TInternal> }\n}\n\n/** Map of state names to their configurations */\nexport type StatesConfig<\n TState extends string,\n TContext,\n TEvents extends EventsConfig,\n TActions extends string = string,\n TGuards extends string = string,\n TInternal = unknown,\n> = {\n [K in TState]?: StateConfig<TContext, TEvents, TActions, TGuards, TInternal>\n}\n\n/** Base context (input + internal) before computed values are added */\nexport type BaseContext<T extends MachineTypes> = Input<T> & Internal<T>\n\n// ============================================\n// Machine Configuration Type\n// ============================================\n\n/**\n * Machine - the configuration object for createMachine\n */\nexport type Machine<T extends MachineTypes> = {\n internal?: Internal<T> // Initial internal state values\n computed?: { // Computed value definitions\n [K in keyof Computed<T>]: (ctx: BaseContext<T>) => Computed<T>[K]\n }\n on?: { // Global event handlers\n [K in keyof Events<T>]?: Handler<Context<T>, Events<T>[K], Actions<T>, Guards<T>, Internal<T>>\n }\n states?: StatesConfig<State<T>, Context<T>, Events<T>, Actions<T>, Guards<T>, Internal<T>> // FSM state handlers\n always?: Rule<Context<T>, undefined, Actions<T>, Guards<T>, Internal<T>>[] // Auto-evaluated rules\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n effects?: Effect<Context<T>, Events<T>, any>[] // Watch-based side effects\n actions?: { // Named action implementations\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n [K in Actions<T>]: (ctx: Context<T>, payload: any, assign: AssignFn<Internal<T>>) => void\n }\n guards?: { // Named guard implementations\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n [K in Guards<T>]: (ctx: Context<T>, payload?: any) => boolean\n }\n}\n\n/**\n * Send - function type for dispatching events\n * Events with undefined payload can be called without arguments\n */\nexport type Send<TEvents extends EventsConfig> = <K extends keyof TEvents>(\n event: K,\n ...args: TEvents[K] extends undefined ? [] : [payload: TEvents[K]]\n) => void\n\n// ============================================\n// Snapshot Type (Return value from getSnapshot/useMachine)\n// ============================================\n\n/**\n * Snapshot = Internal + Computed + { state } (without Input)\n * This is the value returned from getSnapshot() and useMachine()\n *\n * If Internal/Computed have overlapping keys, Snapshot becomes `never`\n * If 'state' type param is defined and Internal/Computed already has 'state' key,\n * we don't add { state } again (existing state is already included)\n */\nexport type Snapshot<T extends MachineTypes> =\n HasOverlappingKeys<Internal<T>, Computed<T>> extends true\n ? never\n : Internal<T> & Computed<T> & (\n T['state'] extends string\n ? 'state' extends keyof Internal<T> | keyof Computed<T>\n ? object // already has 'state', don't add duplicate\n : { state: State<T> }\n : object\n )\n\n// ============================================\n// MachineInstance Type (createMachine return value)\n// ============================================\n\n/**\n * MachineInstance - the return type of createMachine\n * Includes all configuration plus runtime methods\n */\nexport type MachineInstance<T extends MachineTypes> = Machine<T> & {\n /** Dispatch an event with input and optional payload */\n send: <K extends keyof Events<T>>(\n event: K,\n input: Input<T>,\n ...args: Events<T>[K] extends undefined ? [] : [payload: Events<T>[K]]\n ) => void\n /** Evaluate always rules and effects (called automatically in React) */\n evaluate: (input: Input<T>) => void\n /** Get current snapshot (internal + computed + state) */\n getSnapshot: (input: Input<T>) => Snapshot<T>\n /** Get current internal state */\n getInternal: () => Internal<T>\n /** Set internal state directly */\n setInternal: (internal: Internal<T>) => void\n /** Get initial internal state (for reset) */\n getInitialInternal: () => Internal<T>\n /** Clean up all effect callbacks */\n cleanup: () => void\n}\n\n// ============================================\n// Core Logic - Pure Functions\n// ============================================\n\n/** Normalize a value to an array (single value becomes [value]) */\nfunction toArray<T>(value: T | T[]): T[] {\n return Array.isArray(value) ? value : [value]\n}\n\n/**\n * Execute action items (named actions or inline functions)\n * Handles both single actions and arrays of actions\n * Each action receives fresh context (rebuilt after previous assigns)\n */\nexport function executeRuleActions<TContext, TPayload, TInternal>(\n actionItems: ActionItem<TContext, TPayload, string, TInternal> | ActionItem<TContext, TPayload, string, TInternal>[],\n actions: Record<string, (context: TContext, payload: TPayload, assign: AssignFn<TInternal>) => void>,\n getContext: () => TContext,\n payload: TPayload,\n assign: AssignFn<TInternal>,\n): void {\n for (const item of toArray(actionItems)) {\n const context = getContext() // Fresh context for each action\n if (typeof item === 'function') {\n item(context, payload, assign)\n } else {\n actions[item]?.(context, payload, assign)\n }\n }\n}\n\n/**\n * Evaluate guard items (named guards or inline predicates)\n * Uses AND logic - all guards must pass for result to be true\n */\nexport function evaluateGuards<TContext, TPayload>(\n guardItems: GuardItem<TContext, TPayload> | GuardItem<TContext, TPayload>[] | undefined,\n guards: Record<string, (context: TContext, payload?: TPayload) => boolean>,\n context: TContext,\n payload: TPayload,\n): boolean {\n if (!guardItems) return true\n\n for (const item of toArray(guardItems)) {\n const guardFn = typeof item === 'function' ? item : guards[item]\n if (guardFn && !guardFn(context, payload)) {\n return false\n }\n }\n\n return true\n}\n\n/** Type guard to check if handler is a Rule array (has 'do' property) */\nexport function isRuleArray<TContext, TPayload, TActions extends string>(\n handler: Handler<TContext, TPayload, TActions>,\n): handler is Rule<TContext, TPayload, TActions>[] {\n return (\n Array.isArray(handler) &&\n handler.length > 0 &&\n typeof handler[0] === 'object' &&\n 'do' in handler[0]\n )\n}\n\n/** Check if handler is a function array */\nfunction isFunctionArray<TContext, TPayload, TInternal>(\n handler: Handler<TContext, TPayload, string, string, TInternal>,\n): handler is ((context: TContext, payload: TPayload, assign: AssignFn<TInternal>) => void)[] {\n return Array.isArray(handler) && handler.length > 0 && typeof handler[0] === 'function'\n}\n\n/**\n * Execute a handler (action string, action array, rule array, inline function, or function array)\n * For rule arrays, only the first matching rule executes (short-circuit)\n * Each action/function receives fresh context (rebuilt after previous assigns)\n */\nexport function executeHandler<TContext, TPayload, TInternal>(\n handler: Handler<TContext, TPayload, string, string, TInternal>,\n actions: Record<string, (context: TContext, payload: TPayload, assign: AssignFn<TInternal>) => void>,\n guards: Record<string, (context: TContext, payload?: TPayload) => boolean>,\n getContext: () => TContext,\n payload: TPayload,\n assign: AssignFn<TInternal>,\n): void {\n // Inline function handler\n if (typeof handler === 'function') {\n handler(getContext(), payload, assign)\n return\n }\n\n // Function array - execute all functions in order with fresh context\n if (isFunctionArray(handler)) {\n for (const fn of handler) {\n fn(getContext(), payload, assign)\n }\n return\n }\n\n // Single action or action array (strings)\n if (typeof handler === 'string' || (Array.isArray(handler) && !isRuleArray(handler))) {\n executeRuleActions(handler as string | string[], actions, getContext, payload, assign)\n return\n }\n\n // Rule array - first matching rule wins (guard uses fresh context)\n for (const rule of handler as Rule<TContext, TPayload, string, string, TInternal>[]) {\n if (evaluateGuards(rule.when, guards, getContext(), payload)) {\n executeRuleActions(rule.do, actions, getContext, payload, assign)\n break\n }\n }\n}\n\n/**\n * Compute derived values from base context\n * Each computed function receives the base context (input + internal)\n */\nexport function computeValues<TBase, TComputed extends ComputedConfig>(\n base: TBase,\n computed?: { [K in keyof TComputed]: (ctx: TBase) => TComputed[K] },\n): TBase & TComputed {\n if (!computed) return base as TBase & TComputed\n\n const values = {} as TComputed\n for (const key in computed) {\n values[key] = computed[key](base)\n }\n return { ...base, ...values }\n}\n\n/**\n * Build flat context from input + internal\n * Input takes priority if keys overlap (runtime)\n */\nexport function buildContext<TInput, TInternal>(\n input: TInput,\n internal: TInternal,\n): TInput & TInternal {\n return { ...internal, ...input } as TInput & TInternal\n}\n\n/**\n * Create assign function for updating internal state\n */\nexport function createAssign<TInternal>(\n getInternal: () => TInternal,\n setInternal: (internal: TInternal) => void,\n): AssignFn<TInternal> {\n return (updates: Partial<TInternal>) => {\n setInternal({ ...getInternal(), ...updates })\n }\n}\n\n/**\n * Build snapshot from internal state, context, and computed definitions\n * Snapshot = Internal + Computed + state (without Input)\n */\nexport function buildSnapshot<T extends MachineTypes>(\n internal: Internal<T>,\n context: Context<T>,\n computedDef: Machine<T>['computed'],\n): Snapshot<T> {\n const snapshot = { ...internal } as Snapshot<T>\n // Add computed values\n if (computedDef) {\n for (const key in computedDef) {\n (snapshot as Record<string, unknown>)[key] = context[key as keyof Context<T>]\n }\n }\n // Add state if exists in context\n const state = (context as { state?: State<T> }).state\n if (state !== undefined) {\n (snapshot as Record<string, unknown>).state = state\n }\n return snapshot\n}\n\n// ============================================\n// Effects Processing\n// ============================================\n\n/**\n * Shallow comparison for effect watch values\n * Arrays: compares length and each element with ===\n * Others: strict equality (===)\n */\nexport function shallowEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true\n\n // Array comparison\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false\n return a.every((v, i) => v === b[i])\n }\n\n return false\n}\n\n/**\n * EffectStore - tracks watched values and cleanup functions for effects\n * Used by both vanilla and React implementations\n */\nexport type EffectStore = {\n watchedValues: Map<number, unknown> // Previous values by effect index\n enterCleanups: Map<number, () => void> // Cleanup from enter callbacks\n changeCleanups: Map<number, () => void> // Cleanup from change callbacks\n exitCleanups: Map<number, () => void> // Cleanup from exit callbacks\n}\n\n/** Create a new effect store */\nexport function createEffectStore(): EffectStore {\n return {\n watchedValues: new Map(),\n enterCleanups: new Map(),\n changeCleanups: new Map(),\n exitCleanups: new Map(),\n }\n}\n\n/**\n * Process all effects - detect watch value changes and call appropriate callbacks\n * Called on every context change in both vanilla (evaluate) and React (useEffect)\n */\nexport function processEffects<TContext, TEvents extends EventsConfig>(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n effects: Effect<TContext, TEvents, any>[] | undefined,\n context: TContext,\n effectHelpers: EffectHelpers<TEvents>,\n store: EffectStore,\n): void {\n if (!effects) return\n\n effects.forEach((effect, i) => {\n const prev = store.watchedValues.get(i)\n const curr = effect.watch(context)\n\n // Only process if value changed (shallow comparison)\n if (!shallowEqual(prev, curr)) {\n // 1. Cleanup previous enter callback\n const enterCleanup = store.enterCleanups.get(i)\n if (enterCleanup) {\n enterCleanup()\n store.enterCleanups.delete(i)\n }\n\n // 2. Cleanup previous change callback\n const changeCleanup = store.changeCleanups.get(i)\n if (changeCleanup) {\n changeCleanup()\n store.changeCleanups.delete(i)\n }\n\n // 3. Call change callback (fires on any value change)\n const changeResult = effect.change?.(context, prev, curr, effectHelpers)\n if (typeof changeResult === 'function') {\n store.changeCleanups.set(i, changeResult)\n }\n\n // 4. Call enter callback (falsy → truthy transition)\n if (!prev && curr) {\n // Cleanup previous exit first\n const exitCleanup = store.exitCleanups.get(i)\n if (exitCleanup) {\n exitCleanup()\n store.exitCleanups.delete(i)\n }\n\n const enterResult = effect.enter?.(context, effectHelpers)\n if (typeof enterResult === 'function') {\n store.enterCleanups.set(i, enterResult)\n }\n }\n\n // 5. Call exit callback (truthy → falsy transition)\n if (prev && !curr) {\n const exitResult = effect.exit?.(context, effectHelpers)\n if (typeof exitResult === 'function') {\n store.exitCleanups.set(i, exitResult)\n }\n }\n\n // Update stored value for next comparison\n store.watchedValues.set(i, curr)\n }\n })\n}\n\n/** Clear all effect cleanups (called on unmount or cleanup) */\nexport function clearEffectStore(store: EffectStore): void {\n store.enterCleanups.forEach((fn) => fn())\n store.enterCleanups.clear()\n store.changeCleanups.forEach((fn) => fn())\n store.changeCleanups.clear()\n store.exitCleanups.forEach((fn) => fn())\n store.exitCleanups.clear()\n store.watchedValues.clear()\n}\n\n// ============================================\n// createMachine - Vanilla Implementation\n// ============================================\n\n/**\n * Create a controlled state machine instance\n *\n * The machine manages internal state and provides methods for:\n * - send: Dispatch events with input and payload\n * - evaluate: Run always rules and effects\n * - getSnapshot: Get current state (internal + computed + state)\n *\n * @example\n * const machine = createMachine<{\n * input: { count: number }\n * internal: { isOpen: boolean }\n * events: { TOGGLE: undefined }\n * computed: { doubled: number }\n * }>({\n * internal: { isOpen: false },\n * computed: { doubled: ctx => ctx.count * 2 },\n * on: { TOGGLE: (ctx, _, assign) => assign({ isOpen: !ctx.isOpen }) }\n * })\n */\nexport function createMachine<T extends MachineTypes>(\n config: Machine<T>,\n): MachineInstance<T> {\n const effectStore = createEffectStore()\n const initialInternal = (config.internal ?? {}) as Internal<T>\n\n // Machine-managed internal state\n let currentInternal = { ...initialInternal }\n\n // Update internal state\n const updateInternal = (newInternal: Internal<T>) => {\n currentInternal = newInternal\n }\n\n // Build full context: input + internal + computed\n const buildFullContext = (input: Input<T>): Context<T> => {\n const base = buildContext(input, currentInternal)\n return computeValues(base, config.computed) as Context<T>\n }\n\n /**\n * Send an event to the machine\n * Executes state-specific handlers first, then global handlers\n */\n const send = (<K extends keyof Events<T>>(\n event: K,\n input: Input<T>,\n ...args: Events<T>[K] extends undefined ? [] : [payload: Events<T>[K]]\n ) => {\n const assign = createAssign(() => currentInternal, updateInternal)\n const payload = args[0] as Events<T>[K]\n\n // getContext rebuilds context with latest internal state\n const getContext = () => buildFullContext(input)\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const actionsMap = (config.actions ?? {}) as Record<string, (ctx: Context<T>, payload: any, assign: AssignFn<Internal<T>>) => void>\n const guardsMap = (config.guards ?? {}) as Record<string, (ctx: Context<T>, payload?: unknown) => boolean>\n\n // 1. Execute state-specific handler (if in FSM mode)\n const context = getContext()\n const state = (context as { state?: State<T> }).state\n if (state && config.states?.[state]?.on?.[event]) {\n const stateHandler = config.states[state].on![event]!\n executeHandler(\n stateHandler,\n actionsMap,\n guardsMap,\n getContext,\n payload,\n assign,\n )\n }\n\n // 2. Execute global handler\n const globalHandler = config.on?.[event]\n if (globalHandler) {\n executeHandler(\n globalHandler,\n actionsMap,\n guardsMap,\n getContext,\n payload,\n assign,\n )\n }\n }) as MachineInstance<T>['send']\n\n // Create effect helpers with bound send\n const createEffectHelpers = (input: Input<T>): EffectHelpers<Events<T>> => ({\n send: (<K extends keyof Events<T>>(\n event: K,\n ...args: Events<T>[K] extends undefined ? [] : [payload: Events<T>[K]]\n ) => {\n send(event, input, ...args)\n }) as Send<Events<T>>,\n })\n\n /**\n * Evaluate the machine - runs always rules and processes effects\n * Should be called when input changes (automatic in React via useMachine)\n */\n const evaluate = (input: Input<T>) => {\n const assign = createAssign(() => currentInternal, updateInternal)\n const effectHelpers = createEffectHelpers(input)\n const getContext = () => buildFullContext(input)\n\n // Process always rules (first matching rule wins)\n if (config.always) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const actionsMap = (config.actions ?? {}) as Record<string, (ctx: Context<T>, payload: any, assign: AssignFn<Internal<T>>) => void>\n const guardsMap = (config.guards ?? {}) as Record<string, (ctx: Context<T>) => boolean>\n for (const rule of config.always) {\n if (evaluateGuards(rule.when, guardsMap, getContext(), undefined)) {\n executeRuleActions(rule.do, actionsMap, getContext, undefined, assign)\n break\n }\n }\n }\n\n // Process effects (uses current context)\n processEffects(config.effects, getContext(), effectHelpers, effectStore)\n }\n\n /** Get current snapshot (internal + computed + state) */\n const getSnapshot = (input: Input<T>): Snapshot<T> => {\n const context = buildFullContext(input)\n return buildSnapshot(currentInternal, context, config.computed)\n }\n\n // Internal state accessors\n const getInternal = (): Internal<T> => currentInternal\n const setInternal = (internal: Internal<T>) => { currentInternal = internal }\n const getInitialInternal = (): Internal<T> => initialInternal\n\n /** Clean up all effect callbacks */\n const cleanup = () => clearEffectStore(effectStore)\n\n return Object.assign(config, { send, evaluate, getSnapshot, getInternal, setInternal, getInitialInternal, cleanup })\n}\n"],"names":["effect"],"mappings":"AAoIO,SAAS,OACd,QACqC;AACrC,SAAO;AACT;AAyNA,SAAS,QAAW,OAAqB;AACvC,SAAO,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK;AAC9C;AAOO,SAAS,mBACd,aACA,SACA,YACA,SACA,QACM;AACN,aAAW,QAAQ,QAAQ,WAAW,GAAG;AACvC,UAAM,UAAU,WAAA;AAChB,QAAI,OAAO,SAAS,YAAY;AAC9B,WAAK,SAAS,SAAS,MAAM;AAAA,IAC/B,OAAO;AACL,cAAQ,IAAI,IAAI,SAAS,SAAS,MAAM;AAAA,IAC1C;AAAA,EACF;AACF;AAMO,SAAS,eACd,YACA,QACA,SACA,SACS;AACT,MAAI,CAAC,WAAY,QAAO;AAExB,aAAW,QAAQ,QAAQ,UAAU,GAAG;AACtC,UAAM,UAAU,OAAO,SAAS,aAAa,OAAO,OAAO,IAAI;AAC/D,QAAI,WAAW,CAAC,QAAQ,SAAS,OAAO,GAAG;AACzC,aAAO;AAAA,IACT;AAAA,EACF;AAEA,SAAO;AACT;AAGO,SAAS,YACd,SACiD;AACjD,SACE,MAAM,QAAQ,OAAO,KACrB,QAAQ,SAAS,KACjB,OAAO,QAAQ,CAAC,MAAM,YACtB,QAAQ,QAAQ,CAAC;AAErB;AAGA,SAAS,gBACP,SAC4F;AAC5F,SAAO,MAAM,QAAQ,OAAO,KAAK,QAAQ,SAAS,KAAK,OAAO,QAAQ,CAAC,MAAM;AAC/E;AAOO,SAAS,eACd,SACA,SACA,QACA,YACA,SACA,QACM;AAEN,MAAI,OAAO,YAAY,YAAY;AACjC,YAAQ,WAAA,GAAc,SAAS,MAAM;AACrC;AAAA,EACF;AAGA,MAAI,gBAAgB,OAAO,GAAG;AAC5B,eAAW,MAAM,SAAS;AACxB,SAAG,WAAA,GAAc,SAAS,MAAM;AAAA,IAClC;AACA;AAAA,EACF;AAGA,MAAI,OAAO,YAAY,YAAa,MAAM,QAAQ,OAAO,KAAK,CAAC,YAAY,OAAO,GAAI;AACpF,uBAAmB,SAA8B,SAAS,YAAY,SAAS,MAAM;AACrF;AAAA,EACF;AAGA,aAAW,QAAQ,SAAkE;AACnF,QAAI,eAAe,KAAK,MAAM,QAAQ,WAAA,GAAc,OAAO,GAAG;AAC5D,yBAAmB,KAAK,IAAI,SAAS,YAAY,SAAS,MAAM;AAChE;AAAA,IACF;AAAA,EACF;AACF;AAMO,SAAS,cACd,MACA,UACmB;AACnB,MAAI,CAAC,SAAU,QAAO;AAEtB,QAAM,SAAS,CAAA;AACf,aAAW,OAAO,UAAU;AAC1B,WAAO,GAAG,IAAI,SAAS,GAAG,EAAE,IAAI;AAAA,EAClC;AACA,SAAO,EAAE,GAAG,MAAM,GAAG,OAAA;AACvB;AAMO,SAAS,aACd,OACA,UACoB;AACpB,SAAO,EAAE,GAAG,UAAU,GAAG,MAAA;AAC3B;AAKO,SAAS,aACd,aACA,aACqB;AACrB,SAAO,CAAC,YAAgC;AACtC,gBAAY,EAAE,GAAG,eAAe,GAAG,SAAS;AAAA,EAC9C;AACF;AAMO,SAAS,cACd,UACA,SACA,aACa;AACb,QAAM,WAAW,EAAE,GAAG,SAAA;AAEtB,MAAI,aAAa;AACf,eAAW,OAAO,aAAa;AAC5B,eAAqC,GAAG,IAAI,QAAQ,GAAuB;AAAA,IAC9E;AAAA,EACF;AAEA,QAAM,QAAS,QAAiC;AAChD,MAAI,UAAU,QAAW;AACtB,aAAqC,QAAQ;AAAA,EAChD;AACA,SAAO;AACT;AAWO,SAAS,aAAa,GAAY,GAAqB;AAC5D,MAAI,MAAM,EAAG,QAAO;AAGpB,MAAI,MAAM,QAAQ,CAAC,KAAK,MAAM,QAAQ,CAAC,GAAG;AACxC,QAAI,EAAE,WAAW,EAAE,OAAQ,QAAO;AAClC,WAAO,EAAE,MAAM,CAAC,GAAG,MAAM,MAAM,EAAE,CAAC,CAAC;AAAA,EACrC;AAEA,SAAO;AACT;AAcO,SAAS,oBAAiC;AAC/C,SAAO;AAAA,IACL,mCAAmB,IAAA;AAAA,IACnB,mCAAmB,IAAA;AAAA,IACnB,oCAAoB,IAAA;AAAA,IACpB,kCAAkB,IAAA;AAAA,EAAI;AAE1B;AAMO,SAAS,eAEd,SACA,SACA,eACA,OACM;AACN,MAAI,CAAC,QAAS;AAEd,UAAQ,QAAQ,CAACA,SAAQ,MAAM;AAC7B,UAAM,OAAO,MAAM,cAAc,IAAI,CAAC;AACtC,UAAM,OAAOA,QAAO,MAAM,OAAO;AAGjC,QAAI,CAAC,aAAa,MAAM,IAAI,GAAG;AAE7B,YAAM,eAAe,MAAM,cAAc,IAAI,CAAC;AAC9C,UAAI,cAAc;AAChB,qBAAA;AACA,cAAM,cAAc,OAAO,CAAC;AAAA,MAC9B;AAGA,YAAM,gBAAgB,MAAM,eAAe,IAAI,CAAC;AAChD,UAAI,eAAe;AACjB,sBAAA;AACA,cAAM,eAAe,OAAO,CAAC;AAAA,MAC/B;AAGA,YAAM,eAAeA,QAAO,SAAS,SAAS,MAAM,MAAM,aAAa;AACvE,UAAI,OAAO,iBAAiB,YAAY;AACtC,cAAM,eAAe,IAAI,GAAG,YAAY;AAAA,MAC1C;AAGA,UAAI,CAAC,QAAQ,MAAM;AAEjB,cAAM,cAAc,MAAM,aAAa,IAAI,CAAC;AAC5C,YAAI,aAAa;AACf,sBAAA;AACA,gBAAM,aAAa,OAAO,CAAC;AAAA,QAC7B;AAEA,cAAM,cAAcA,QAAO,QAAQ,SAAS,aAAa;AACzD,YAAI,OAAO,gBAAgB,YAAY;AACrC,gBAAM,cAAc,IAAI,GAAG,WAAW;AAAA,QACxC;AAAA,MACF;AAGA,UAAI,QAAQ,CAAC,MAAM;AACjB,cAAM,aAAaA,QAAO,OAAO,SAAS,aAAa;AACvD,YAAI,OAAO,eAAe,YAAY;AACpC,gBAAM,aAAa,IAAI,GAAG,UAAU;AAAA,QACtC;AAAA,MACF;AAGA,YAAM,cAAc,IAAI,GAAG,IAAI;AAAA,IACjC;AAAA,EACF,CAAC;AACH;AAGO,SAAS,iBAAiB,OAA0B;AACzD,QAAM,cAAc,QAAQ,CAAC,OAAO,IAAI;AACxC,QAAM,cAAc,MAAA;AACpB,QAAM,eAAe,QAAQ,CAAC,OAAO,IAAI;AACzC,QAAM,eAAe,MAAA;AACrB,QAAM,aAAa,QAAQ,CAAC,OAAO,IAAI;AACvC,QAAM,aAAa,MAAA;AACnB,QAAM,cAAc,MAAA;AACtB;AA0BO,SAAS,cACd,QACoB;AACpB,QAAM,cAAc,kBAAA;AACpB,QAAM,kBAAmB,OAAO,YAAY,CAAA;AAG5C,MAAI,kBAAkB,EAAE,GAAG,gBAAA;AAG3B,QAAM,iBAAiB,CAAC,gBAA6B;AACnD,sBAAkB;AAAA,EACpB;AAGA,QAAM,mBAAmB,CAAC,UAAgC;AACxD,UAAM,OAAO,aAAa,OAAO,eAAe;AAChD,WAAO,cAAc,MAAM,OAAO,QAAQ;AAAA,EAC5C;AAMA,QAAM,QAAQ,CACZ,OACA,UACG,SACA;AACH,UAAM,SAAS,aAAa,MAAM,iBAAiB,cAAc;AACjE,UAAM,UAAU,KAAK,CAAC;AAGtB,UAAM,aAAa,MAAM,iBAAiB,KAAK;AAG/C,UAAM,aAAc,OAAO,WAAW,CAAA;AACtC,UAAM,YAAa,OAAO,UAAU,CAAA;AAGpC,UAAM,UAAU,WAAA;AAChB,UAAM,QAAS,QAAiC;AAChD,QAAI,SAAS,OAAO,SAAS,KAAK,GAAG,KAAK,KAAK,GAAG;AAChD,YAAM,eAAe,OAAO,OAAO,KAAK,EAAE,GAAI,KAAK;AACnD;AAAA,QACE;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAAA,IAEJ;AAGA,UAAM,gBAAgB,OAAO,KAAK,KAAK;AACvC,QAAI,eAAe;AACjB;AAAA,QACE;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAAA,IAEJ;AAAA,EACF;AAGA,QAAM,sBAAsB,CAAC,WAA+C;AAAA,IAC1E,OAAO,CACL,UACG,SACA;AACH,WAAK,OAAO,OAAO,GAAG,IAAI;AAAA,IAC5B;AAAA,EAAA;AAOF,QAAM,WAAW,CAAC,UAAoB;AACpC,UAAM,SAAS,aAAa,MAAM,iBAAiB,cAAc;AACjE,UAAM,gBAAgB,oBAAoB,KAAK;AAC/C,UAAM,aAAa,MAAM,iBAAiB,KAAK;AAG/C,QAAI,OAAO,QAAQ;AAEjB,YAAM,aAAc,OAAO,WAAW,CAAA;AACtC,YAAM,YAAa,OAAO,UAAU,CAAA;AACpC,iBAAW,QAAQ,OAAO,QAAQ;AAChC,YAAI,eAAe,KAAK,MAAM,WAAW,WAAA,GAAc,MAAS,GAAG;AACjE,6BAAmB,KAAK,IAAI,YAAY,YAAY,QAAW,MAAM;AACrE;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAGA,mBAAe,OAAO,SAAS,WAAA,GAAc,eAAe,WAAW;AAAA,EACzE;AAGA,QAAM,cAAc,CAAC,UAAiC;AACpD,UAAM,UAAU,iBAAiB,KAAK;AACtC,WAAO,cAAc,iBAAiB,SAAS,OAAO,QAAQ;AAAA,EAChE;AAGA,QAAM,cAAc,MAAmB;AACvC,QAAM,cAAc,CAAC,aAA0B;AAAE,sBAAkB;AAAA,EAAS;AAC5E,QAAM,qBAAqB,MAAmB;AAG9C,QAAM,UAAU,MAAM,iBAAiB,WAAW;AAElD,SAAO,OAAO,OAAO,QAAQ,EAAE,MAAM,UAAU,aAAa,aAAa,aAAa,oBAAoB,QAAA,CAAS;AACrH;"}
package/dist/react.cjs CHANGED
@@ -2,8 +2,15 @@
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const react = require("react");
4
4
  const index = require("./index.cjs");
5
- function useMachine(machine, options) {
6
- const { input, actions: optionsActions, guards: optionsGuards } = options;
5
+ function useMachine(machineOrFactory, options = {}) {
6
+ const { input = {}, actions: optionsActions, guards: optionsGuards } = options;
7
+ const machine = react.useMemo(
8
+ () => typeof machineOrFactory === "function" ? machineOrFactory() : machineOrFactory,
9
+ // eslint-disable-next-line react-hooks/exhaustive-deps
10
+ []
11
+ // Only run once on mount
12
+ );
13
+ const [internal, setInternal] = react.useState(() => machine.getInitialInternal());
7
14
  const mergedActions = react.useMemo(
8
15
  () => ({ ...machine.actions, ...optionsActions }),
9
16
  [machine.actions, optionsActions]
@@ -13,27 +20,21 @@ function useMachine(machine, options) {
13
20
  [machine.guards, optionsGuards]
14
21
  );
15
22
  const inputRef = react.useRef(input);
23
+ const internalRef = react.useRef(internal);
16
24
  const machineRef = react.useRef(machine);
17
25
  const mergedActionsRef = react.useRef(mergedActions);
18
26
  const mergedGuardsRef = react.useRef(mergedGuards);
19
27
  const isMountedRef = react.useRef(true);
20
28
  inputRef.current = input;
29
+ internalRef.current = internal;
21
30
  machineRef.current = machine;
22
31
  mergedActionsRef.current = mergedActions;
23
32
  mergedGuardsRef.current = mergedGuards;
24
33
  const { computed: computedDef } = machine;
25
- const context = react.useMemo(
26
- () => index.computeValues(input, computedDef),
27
- [input, computedDef]
28
- );
29
- const computed = react.useMemo(() => {
30
- if (!computedDef) return {};
31
- const result = {};
32
- for (const key in computedDef) {
33
- result[key] = context[key];
34
- }
35
- return result;
36
- }, [context, computedDef]);
34
+ const context = react.useMemo(() => {
35
+ const base = index.buildContext(input, internal);
36
+ return index.computeValues(base, computedDef);
37
+ }, [input, internal, computedDef]);
37
38
  const contextRef = react.useRef(context);
38
39
  contextRef.current = context;
39
40
  const prevContextRef = react.useRef(context);
@@ -43,13 +44,24 @@ function useMachine(machine, options) {
43
44
  changeCleanups: /* @__PURE__ */ new Map(),
44
45
  exitCleanups: /* @__PURE__ */ new Map()
45
46
  });
47
+ const updateInternal = react.useCallback((newInternal) => {
48
+ internalRef.current = newInternal;
49
+ setInternal(newInternal);
50
+ }, []);
51
+ const assign = react.useMemo(() => index.createAssign(() => internalRef.current, updateInternal), [updateInternal]);
52
+ const assignRef = react.useRef(assign);
53
+ assignRef.current = assign;
54
+ const getContext = react.useCallback(() => {
55
+ const base = index.buildContext(inputRef.current, internalRef.current);
56
+ return index.computeValues(base, machineRef.current.computed);
57
+ }, []);
46
58
  const { always } = machine;
47
59
  if (prevContextRef.current !== context && always) {
48
60
  const actionsMap = mergedActions;
49
61
  const guardsMap = mergedGuards;
50
62
  for (const rule of always) {
51
- if (index.evaluateGuards(rule.when, guardsMap, context, void 0)) {
52
- index.executeRuleActions(rule.do, actionsMap, context, void 0);
63
+ if (index.evaluateGuards(rule.when, guardsMap, getContext(), void 0)) {
64
+ index.executeRuleActions(rule.do, actionsMap, getContext, void 0, assign);
53
65
  break;
54
66
  }
55
67
  }
@@ -58,38 +70,49 @@ function useMachine(machine, options) {
58
70
  const send = react.useCallback(
59
71
  (event, ...args) => {
60
72
  const currentMachine = machineRef.current;
61
- const currentInput = inputRef.current;
62
73
  const currentActions = mergedActionsRef.current;
63
74
  const currentGuards = mergedGuardsRef.current;
64
- const currentContext = index.computeValues(
65
- currentInput,
66
- currentMachine.computed
75
+ const currentAssign = index.createAssign(
76
+ () => internalRef.current,
77
+ (newInternal) => {
78
+ internalRef.current = newInternal;
79
+ setInternal(newInternal);
80
+ }
67
81
  );
82
+ const getCurrentContext = () => {
83
+ const base = index.buildContext(inputRef.current, internalRef.current);
84
+ return index.computeValues(base, currentMachine.computed);
85
+ };
68
86
  const payload = args[0];
69
- const state2 = currentContext.state;
70
- if (state2 && currentMachine.states?.[state2]?.on?.[event]) {
71
- const stateHandler = currentMachine.states[state2].on[event];
87
+ const actionsMap = currentActions ?? {};
88
+ const guardsMap = currentGuards;
89
+ const currentContext = getCurrentContext();
90
+ const state = currentContext.state;
91
+ if (state && currentMachine.states?.[state]?.on?.[event]) {
92
+ const stateHandler = currentMachine.states[state].on[event];
72
93
  index.executeHandler(
73
94
  stateHandler,
74
- currentActions ?? {},
75
- currentGuards,
76
- currentContext,
77
- payload
95
+ actionsMap,
96
+ guardsMap,
97
+ getCurrentContext,
98
+ payload,
99
+ currentAssign
78
100
  );
79
101
  }
80
102
  const globalHandler = currentMachine.on?.[event];
81
103
  if (globalHandler) {
82
104
  index.executeHandler(
83
105
  globalHandler,
84
- currentActions ?? {},
85
- currentGuards,
86
- currentContext,
87
- payload
106
+ actionsMap,
107
+ guardsMap,
108
+ getCurrentContext,
109
+ payload,
110
+ currentAssign
88
111
  );
89
112
  }
90
113
  },
91
114
  []
92
- // no dependencies - uses refs
115
+ // No dependencies - uses refs for latest values
93
116
  );
94
117
  const safeSend = react.useCallback(
95
118
  (event, ...args) => {
@@ -114,8 +137,11 @@ function useMachine(machine, options) {
114
137
  index.clearEffectStore(store);
115
138
  };
116
139
  }, []);
117
- const state = context.state ?? "";
118
- return { send, computed, state };
140
+ const snapshot = react.useMemo(
141
+ () => index.buildSnapshot(internal, context, computedDef),
142
+ [internal, context, computedDef]
143
+ );
144
+ return [snapshot, send];
119
145
  }
120
146
  exports.useMachine = useMachine;
121
147
  //# sourceMappingURL=react.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"react.cjs","sources":["../src/react.ts"],"sourcesContent":["/**\n * Controlled Machine - React Integration\n *\n * React hook for using controlled-machine in React components.\n */\n\nimport { useCallback, useRef, useEffect, useMemo } from 'react'\nimport {\n type MachineTypes,\n type Events,\n type Computed,\n type State,\n type Send,\n type EffectHelpers,\n type EffectStore,\n type Context,\n type Actions,\n type Guards,\n computeValues,\n executeRuleActions,\n evaluateGuards,\n executeHandler,\n processEffects,\n clearEffectStore,\n MachineInstance,\n} from './index'\n\n// ============================================\n// useMachine Options Type\n// ============================================\n\nexport type UseMachineOptions<T extends MachineTypes> = {\n input: T['input']\n actions?: Partial<{\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n [K in Actions<T>]: (context: Context<T>, payload?: any) => void\n }>\n guards?: Partial<{\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n [K in Guards<T>]: (context: Context<T>, payload?: any) => boolean\n }>\n}\n\n// ============================================\n// React Hook\n// ============================================\n\nexport function useMachine<T extends MachineTypes>(\n machine: MachineInstance<T>,\n options: UseMachineOptions<T>,\n): { send: Send<Events<T>>; computed: Computed<T>; state: State<T> } {\n const { input, actions: optionsActions, guards: optionsGuards } = options\n\n // Merge actions and guards\n const mergedActions = useMemo(\n () => ({ ...machine.actions, ...optionsActions }),\n [machine.actions, optionsActions],\n )\n const mergedGuards = useMemo(\n () => ({ ...machine.guards, ...optionsGuards }) as Record<string, (context: Context<T>, payload?: unknown) => boolean>,\n [machine.guards, optionsGuards],\n )\n\n // refs for stable callbacks\n const inputRef = useRef(input)\n const machineRef = useRef(machine)\n const mergedActionsRef = useRef(mergedActions)\n const mergedGuardsRef = useRef(mergedGuards)\n const isMountedRef = useRef(true)\n\n inputRef.current = input\n machineRef.current = machine\n mergedActionsRef.current = mergedActions\n mergedGuardsRef.current = mergedGuards\n\n // compute values\n const { computed: computedDef } = machine\n const context = useMemo(\n () => computeValues(input, computedDef),\n [input, computedDef],\n )\n\n // extract computed only\n const computed = useMemo(() => {\n if (!computedDef) return {} as Computed<T>\n const result = {} as Computed<T>\n for (const key in computedDef) {\n result[key] = context[key]\n }\n return result\n }, [context, computedDef])\n\n const contextRef = useRef(context)\n contextRef.current = context\n\n const prevContextRef = useRef<Context<T>>(context)\n const effectStoreRef = useRef<EffectStore>({\n watchedValues: new Map(),\n enterCleanups: new Map(),\n changeCleanups: new Map(),\n exitCleanups: new Map(),\n })\n\n // always: auto-evaluate when context changes (synchronous, during render)\n const { always } = machine\n if (prevContextRef.current !== context && always) {\n const actionsMap = mergedActions as Record<string, (context: Context<T>) => void>\n const guardsMap = mergedGuards as Record<string, (context: Context<T>) => boolean>\n for (const rule of always) {\n if (evaluateGuards(rule.when, guardsMap, context, undefined)) {\n executeRuleActions(rule.do, actionsMap, context, undefined)\n break\n }\n }\n }\n prevContextRef.current = context\n\n // send: stable function (no deps, uses refs)\n const send: Send<Events<T>> = useCallback(\n <K extends keyof Events<T>>(\n event: K,\n ...args: Events<T>[K] extends undefined ? [] : [payload: Events<T>[K]]\n ) => {\n const currentMachine = machineRef.current\n const currentInput = inputRef.current\n const currentActions = mergedActionsRef.current\n const currentGuards = mergedGuardsRef.current\n const currentContext = computeValues(\n currentInput,\n currentMachine.computed,\n )\n const payload = args[0] as Events<T>[K]\n\n // 1. State-specific handler first\n const state = (currentContext as { state?: State<T> }).state\n if (state && currentMachine.states?.[state]?.on?.[event]) {\n const stateHandler = currentMachine.states[state].on![event]!\n executeHandler(\n stateHandler,\n currentActions ?? {},\n currentGuards,\n currentContext,\n payload,\n )\n }\n\n // 2. Global handler\n const globalHandler = currentMachine.on?.[event]\n if (globalHandler) {\n executeHandler(\n globalHandler,\n currentActions ?? {},\n currentGuards,\n currentContext,\n payload,\n )\n }\n },\n [], // no dependencies - uses refs\n )\n\n // safeSend: won't be called after unmount\n const safeSend: Send<Events<T>> = useCallback(\n <K extends keyof Events<T>>(\n event: K,\n ...args: Events<T>[K] extends undefined ? [] : [payload: Events<T>[K]]\n ) => {\n if (!isMountedRef.current) return\n send(event, ...args)\n },\n [send],\n )\n\n // effect helpers\n const effectHelpers: EffectHelpers<Events<T>> = useMemo(\n () => ({ send: safeSend }),\n [safeSend],\n )\n\n // effects: detect watch value changes\n const { effects } = machine\n useEffect(() => {\n processEffects(effects, context, effectHelpers, effectStoreRef.current)\n }, [context, effects, effectHelpers])\n\n // mount/unmount management\n useEffect(() => {\n isMountedRef.current = true\n const store = effectStoreRef.current\n return () => {\n isMountedRef.current = false\n clearEffectStore(store)\n }\n }, [])\n\n // state from context (default to empty string if not provided)\n const state = (context as { state?: State<T> }).state ?? ('' as State<T>)\n\n return { send, computed, state }\n}\n\n// Re-export types for convenience\nexport type { Send } from './index'\n"],"names":["useMemo","useRef","computeValues","evaluateGuards","executeRuleActions","useCallback","state","executeHandler","useEffect","processEffects","clearEffectStore"],"mappings":"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"}
1
+ {"version":3,"file":"react.cjs","sources":["../src/react.ts"],"sourcesContent":["/**\n * Controlled Machine - React Integration\n *\n * React hook for using controlled-machine in React components.\n *\n * Features:\n * - Internal state management via React state\n * - Automatic effect processing on context changes\n * - Stable send function reference (uses refs internally)\n * - Unmount-safe callbacks\n * - Action/guard overrides via options\n *\n * @example\n * const [snapshot, send] = useMachine(counterMachine, {\n * input: { multiplier },\n * actions: { logValue: (ctx) => console.log(ctx.count) },\n * guards: { isPositive: (ctx) => ctx.count > 0 }\n * })\n */\n\nimport { useCallback, useRef, useEffect, useMemo, useState } from 'react'\nimport {\n type MachineTypes,\n type Events,\n type State,\n type Send,\n type Snapshot,\n type EffectHelpers,\n type EffectStore,\n type Context,\n type Internal,\n type Input,\n type Actions,\n type Guards,\n type AssignFn,\n buildContext,\n computeValues,\n createAssign,\n executeRuleActions,\n evaluateGuards,\n executeHandler,\n processEffects,\n clearEffectStore,\n buildSnapshot,\n MachineInstance,\n} from './index'\n\n// ============================================\n// useMachine Options Type\n// ============================================\n\n/**\n * Options for useMachine hook\n * @property input - External state to pass to the machine (React state, props, etc.)\n * @property actions - Override or add action implementations\n * @property guards - Override or add guard implementations\n */\nexport type UseMachineOptions<T extends MachineTypes> = {\n input?: Input<T>\n actions?: Partial<{\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n [K in Actions<T>]: (context: Context<T>, payload: any, assign: AssignFn<Internal<T>>) => void\n }>\n guards?: Partial<{\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n [K in Guards<T>]: (context: Context<T>, payload?: any) => boolean\n }>\n}\n\n// ============================================\n// React Hook - useMachine\n// ============================================\n\n/**\n * React hook for using controlled-machine\n *\n * @param machineOrFactory - Machine instance or factory function (factory ensures fresh instance per component)\n * @param options - Hook options (input, actions override, guards override)\n * @returns [snapshot, send] tuple - snapshot contains internal + computed + state, send dispatches events\n *\n * @example\n * // Basic usage\n * const [snapshot, send] = useMachine(machine, { input: { count, setCount } })\n *\n * // With factory (recommended for isolation)\n * const [snapshot, send] = useMachine(() => createCounterMachine(100), { input: {} })\n *\n * // With action/guard overrides\n * const [snapshot, send] = useMachine(machine, {\n * input: { count },\n * actions: { log: (ctx) => console.log(ctx.count) },\n * guards: { canIncrement: (ctx) => ctx.count < 10 }\n * })\n */\nexport function useMachine<T extends MachineTypes>(\n machineOrFactory: MachineInstance<T> | (() => MachineInstance<T>),\n options: UseMachineOptions<T> = {},\n): [Snapshot<T>, Send<Events<T>>] {\n const { input = {} as Input<T>, actions: optionsActions, guards: optionsGuards } = options\n\n // ---- Machine initialization ----\n // Factory pattern ensures each component gets its own machine instance\n const machine = useMemo(\n () => (typeof machineOrFactory === 'function' ? machineOrFactory() : machineOrFactory),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [], // Only run once on mount\n )\n\n // ---- Internal state (React-managed) ----\n const [internal, setInternal] = useState<Internal<T>>(() => machine.getInitialInternal())\n\n // ---- Merge machine actions/guards with options overrides ----\n const mergedActions = useMemo(\n () => ({ ...machine.actions, ...optionsActions }),\n [machine.actions, optionsActions],\n )\n const mergedGuards = useMemo(\n () => ({ ...machine.guards, ...optionsGuards }) as Record<string, (context: Context<T>, payload?: unknown) => boolean>,\n [machine.guards, optionsGuards],\n )\n\n // ---- Refs for stable callbacks (avoid stale closures) ----\n const inputRef = useRef(input)\n const internalRef = useRef(internal)\n const machineRef = useRef(machine)\n const mergedActionsRef = useRef(mergedActions)\n const mergedGuardsRef = useRef(mergedGuards)\n const isMountedRef = useRef(true)\n\n // Keep refs updated with latest values\n inputRef.current = input\n internalRef.current = internal\n machineRef.current = machine\n mergedActionsRef.current = mergedActions\n mergedGuardsRef.current = mergedGuards\n\n // ---- Build full context (input + internal + computed) ----\n const { computed: computedDef } = machine\n const context = useMemo(() => {\n const base = buildContext(input, internal)\n return computeValues(base, computedDef) as Context<T>\n }, [input, internal, computedDef])\n\n const contextRef = useRef(context)\n contextRef.current = context\n\n // Track previous context for always rules\n const prevContextRef = useRef<Context<T>>(context)\n // Effect store for tracking watched values and cleanups\n const effectStoreRef = useRef<EffectStore>({\n watchedValues: new Map(),\n enterCleanups: new Map(),\n changeCleanups: new Map(),\n exitCleanups: new Map(),\n })\n\n // ---- Internal state updater ----\n // Updates ref immediately (for subsequent assigns in same handler) + triggers re-render\n const updateInternal = useCallback((newInternal: Internal<T>) => {\n internalRef.current = newInternal // Immediate update for subsequent assigns\n setInternal(newInternal) // Trigger re-render\n }, [])\n\n // ---- Assign function (for updating internal state) ----\n const assign = useMemo(() => createAssign(() => internalRef.current, updateInternal), [updateInternal])\n const assignRef = useRef(assign)\n assignRef.current = assign\n\n // ---- Always rules (evaluated during render on context change) ----\n // getContext rebuilds context with latest internal state for fresh values after assigns\n const getContext = useCallback(() => {\n const base = buildContext(inputRef.current, internalRef.current)\n return computeValues(base, machineRef.current.computed) as Context<T>\n }, [])\n\n const { always } = machine\n if (prevContextRef.current !== context && always) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const actionsMap = mergedActions as Record<string, (context: Context<T>, payload: any, assign: AssignFn<Internal<T>>) => void>\n const guardsMap = mergedGuards as Record<string, (context: Context<T>) => boolean>\n for (const rule of always) {\n if (evaluateGuards(rule.when, guardsMap, getContext(), undefined)) {\n executeRuleActions(rule.do, actionsMap, getContext, undefined, assign)\n break // First matching rule wins\n }\n }\n }\n prevContextRef.current = context\n\n // ---- Send function (stable reference, uses refs internally) ----\n const send: Send<Events<T>> = useCallback(\n <K extends keyof Events<T>>(\n event: K,\n ...args: Events<T>[K] extends undefined ? [] : [payload: Events<T>[K]]\n ) => {\n const currentMachine = machineRef.current\n const currentActions = mergedActionsRef.current\n const currentGuards = mergedGuardsRef.current\n\n // Create fresh assign - updates ref immediately + triggers re-render\n const currentAssign = createAssign(\n () => internalRef.current,\n (newInternal) => {\n internalRef.current = newInternal\n setInternal(newInternal)\n }\n )\n\n // getContext rebuilds context with latest internal state\n const getCurrentContext = () => {\n const base = buildContext(inputRef.current, internalRef.current)\n return computeValues(base, currentMachine.computed) as Context<T>\n }\n\n const payload = args[0] as Events<T>[K]\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const actionsMap = (currentActions ?? {}) as Record<string, (ctx: Context<T>, payload: any, assign: AssignFn<Internal<T>>) => void>\n const guardsMap = currentGuards as Record<string, (ctx: Context<T>, payload?: unknown) => boolean>\n\n // 1. Execute state-specific handler (if in FSM mode)\n const currentContext = getCurrentContext()\n const state = (currentContext as { state?: State<T> }).state\n if (state && currentMachine.states?.[state]?.on?.[event]) {\n const stateHandler = currentMachine.states[state].on![event]!\n executeHandler(\n stateHandler,\n actionsMap,\n guardsMap,\n getCurrentContext,\n payload,\n currentAssign,\n )\n }\n\n // 2. Execute global handler\n const globalHandler = currentMachine.on?.[event]\n if (globalHandler) {\n executeHandler(\n globalHandler,\n actionsMap,\n guardsMap,\n getCurrentContext,\n payload,\n currentAssign,\n )\n }\n },\n [], // No dependencies - uses refs for latest values\n )\n\n // ---- Safe send (no-op after unmount) ----\n const safeSend: Send<Events<T>> = useCallback(\n <K extends keyof Events<T>>(\n event: K,\n ...args: Events<T>[K] extends undefined ? [] : [payload: Events<T>[K]]\n ) => {\n if (!isMountedRef.current) return\n send(event, ...args)\n },\n [send],\n )\n\n // ---- Effect helpers (passed to effect callbacks) ----\n const effectHelpers: EffectHelpers<Events<T>> = useMemo(\n () => ({ send: safeSend }),\n [safeSend],\n )\n\n // ---- Process effects on context change ----\n const { effects } = machine\n useEffect(() => {\n processEffects(effects, context, effectHelpers, effectStoreRef.current)\n }, [context, effects, effectHelpers])\n\n // ---- Mount/unmount lifecycle ----\n useEffect(() => {\n isMountedRef.current = true\n const store = effectStoreRef.current\n return () => {\n isMountedRef.current = false\n clearEffectStore(store) // Clean up all effect callbacks on unmount\n }\n }, [])\n\n // ---- Build snapshot (internal + computed + state) ----\n const snapshot = useMemo(\n () => buildSnapshot(internal, context, computedDef),\n [internal, context, computedDef],\n )\n\n return [snapshot, send]\n}\n\n// Re-export types for convenience\nexport type { Send } from './index'\n"],"names":["useMemo","useState","useRef","buildContext","computeValues","useCallback","createAssign","evaluateGuards","executeRuleActions","executeHandler","useEffect","processEffects","clearEffectStore","buildSnapshot"],"mappings":"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"}
package/dist/react.d.cts CHANGED
@@ -1,17 +1,40 @@
1
- import { MachineTypes, Events, Computed, State, Send, Context, Actions, Guards, MachineInstance } from './index';
1
+ import { MachineTypes, Events, Send, Snapshot, Context, Internal, Input, Actions, Guards, AssignFn, MachineInstance } from './index';
2
+ /**
3
+ * Options for useMachine hook
4
+ * @property input - External state to pass to the machine (React state, props, etc.)
5
+ * @property actions - Override or add action implementations
6
+ * @property guards - Override or add guard implementations
7
+ */
2
8
  export type UseMachineOptions<T extends MachineTypes> = {
3
- input: T['input'];
9
+ input?: Input<T>;
4
10
  actions?: Partial<{
5
- [K in Actions<T>]: (context: Context<T>, payload?: any) => void;
11
+ [K in Actions<T>]: (context: Context<T>, payload: any, assign: AssignFn<Internal<T>>) => void;
6
12
  }>;
7
13
  guards?: Partial<{
8
14
  [K in Guards<T>]: (context: Context<T>, payload?: any) => boolean;
9
15
  }>;
10
16
  };
11
- export declare function useMachine<T extends MachineTypes>(machine: MachineInstance<T>, options: UseMachineOptions<T>): {
12
- send: Send<Events<T>>;
13
- computed: Computed<T>;
14
- state: State<T>;
15
- };
17
+ /**
18
+ * React hook for using controlled-machine
19
+ *
20
+ * @param machineOrFactory - Machine instance or factory function (factory ensures fresh instance per component)
21
+ * @param options - Hook options (input, actions override, guards override)
22
+ * @returns [snapshot, send] tuple - snapshot contains internal + computed + state, send dispatches events
23
+ *
24
+ * @example
25
+ * // Basic usage
26
+ * const [snapshot, send] = useMachine(machine, { input: { count, setCount } })
27
+ *
28
+ * // With factory (recommended for isolation)
29
+ * const [snapshot, send] = useMachine(() => createCounterMachine(100), { input: {} })
30
+ *
31
+ * // With action/guard overrides
32
+ * const [snapshot, send] = useMachine(machine, {
33
+ * input: { count },
34
+ * actions: { log: (ctx) => console.log(ctx.count) },
35
+ * guards: { canIncrement: (ctx) => ctx.count < 10 }
36
+ * })
37
+ */
38
+ export declare function useMachine<T extends MachineTypes>(machineOrFactory: MachineInstance<T> | (() => MachineInstance<T>), options?: UseMachineOptions<T>): [Snapshot<T>, Send<Events<T>>];
16
39
  export type { Send } from './index';
17
40
  //# sourceMappingURL=react.d.ts.map
package/dist/react.d.ts CHANGED
@@ -1,17 +1,40 @@
1
- import { MachineTypes, Events, Computed, State, Send, Context, Actions, Guards, MachineInstance } from './index';
1
+ import { MachineTypes, Events, Send, Snapshot, Context, Internal, Input, Actions, Guards, AssignFn, MachineInstance } from './index';
2
+ /**
3
+ * Options for useMachine hook
4
+ * @property input - External state to pass to the machine (React state, props, etc.)
5
+ * @property actions - Override or add action implementations
6
+ * @property guards - Override or add guard implementations
7
+ */
2
8
  export type UseMachineOptions<T extends MachineTypes> = {
3
- input: T['input'];
9
+ input?: Input<T>;
4
10
  actions?: Partial<{
5
- [K in Actions<T>]: (context: Context<T>, payload?: any) => void;
11
+ [K in Actions<T>]: (context: Context<T>, payload: any, assign: AssignFn<Internal<T>>) => void;
6
12
  }>;
7
13
  guards?: Partial<{
8
14
  [K in Guards<T>]: (context: Context<T>, payload?: any) => boolean;
9
15
  }>;
10
16
  };
11
- export declare function useMachine<T extends MachineTypes>(machine: MachineInstance<T>, options: UseMachineOptions<T>): {
12
- send: Send<Events<T>>;
13
- computed: Computed<T>;
14
- state: State<T>;
15
- };
17
+ /**
18
+ * React hook for using controlled-machine
19
+ *
20
+ * @param machineOrFactory - Machine instance or factory function (factory ensures fresh instance per component)
21
+ * @param options - Hook options (input, actions override, guards override)
22
+ * @returns [snapshot, send] tuple - snapshot contains internal + computed + state, send dispatches events
23
+ *
24
+ * @example
25
+ * // Basic usage
26
+ * const [snapshot, send] = useMachine(machine, { input: { count, setCount } })
27
+ *
28
+ * // With factory (recommended for isolation)
29
+ * const [snapshot, send] = useMachine(() => createCounterMachine(100), { input: {} })
30
+ *
31
+ * // With action/guard overrides
32
+ * const [snapshot, send] = useMachine(machine, {
33
+ * input: { count },
34
+ * actions: { log: (ctx) => console.log(ctx.count) },
35
+ * guards: { canIncrement: (ctx) => ctx.count < 10 }
36
+ * })
37
+ */
38
+ export declare function useMachine<T extends MachineTypes>(machineOrFactory: MachineInstance<T> | (() => MachineInstance<T>), options?: UseMachineOptions<T>): [Snapshot<T>, Send<Events<T>>];
16
39
  export type { Send } from './index';
17
40
  //# sourceMappingURL=react.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,MAAM,EACX,KAAK,QAAQ,EACb,KAAK,KAAK,EACV,KAAK,IAAI,EAGT,KAAK,OAAO,EACZ,KAAK,OAAO,EACZ,KAAK,MAAM,EAOX,eAAe,EAChB,MAAM,SAAS,CAAA;AAMhB,MAAM,MAAM,iBAAiB,CAAC,CAAC,SAAS,YAAY,IAAI;IACtD,KAAK,EAAE,CAAC,CAAC,OAAO,CAAC,CAAA;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;SAEf,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,GAAG,KAAK,IAAI;KAChE,CAAC,CAAA;IACF,MAAM,CAAC,EAAE,OAAO,CAAC;SAEd,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,GAAG,KAAK,OAAO;KAClE,CAAC,CAAA;CACH,CAAA;AAMD,wBAAgB,UAAU,CAAC,CAAC,SAAS,YAAY,EAC/C,OAAO,EAAE,eAAe,CAAC,CAAC,CAAC,EAC3B,OAAO,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAC5B;IAAE,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC;IAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,CAAA;CAAE,CAqJnE;AAGD,YAAY,EAAE,IAAI,EAAE,MAAM,SAAS,CAAA"}
1
+ {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,MAAM,EAEX,KAAK,IAAI,EACT,KAAK,QAAQ,EAGb,KAAK,OAAO,EACZ,KAAK,QAAQ,EACb,KAAK,KAAK,EACV,KAAK,OAAO,EACZ,KAAK,MAAM,EACX,KAAK,QAAQ,EAUb,eAAe,EAChB,MAAM,SAAS,CAAA;AAMhB;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,CAAC,CAAC,SAAS,YAAY,IAAI;IACtD,KAAK,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAA;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;SAEf,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI;KAC9F,CAAC,CAAA;IACF,MAAM,CAAC,EAAE,OAAO,CAAC;SAEd,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,GAAG,KAAK,OAAO;KAClE,CAAC,CAAA;CACH,CAAA;AAMD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,YAAY,EAC/C,gBAAgB,EAAE,eAAe,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,eAAe,CAAC,CAAC,CAAC,CAAC,EACjE,OAAO,GAAE,iBAAiB,CAAC,CAAC,CAAM,GACjC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAmMhC;AAGD,YAAY,EAAE,IAAI,EAAE,MAAM,SAAS,CAAA"}
package/dist/react.js CHANGED
@@ -1,7 +1,14 @@
1
- import { useMemo, useRef, useCallback, useEffect } from "react";
2
- import { computeValues, evaluateGuards, executeRuleActions, executeHandler, processEffects, clearEffectStore } from "./index.js";
3
- function useMachine(machine, options) {
4
- const { input, actions: optionsActions, guards: optionsGuards } = options;
1
+ import { useMemo, useState, useRef, useCallback, useEffect } from "react";
2
+ import { buildContext, computeValues, createAssign, evaluateGuards, executeRuleActions, executeHandler, processEffects, clearEffectStore, buildSnapshot } from "./index.js";
3
+ function useMachine(machineOrFactory, options = {}) {
4
+ const { input = {}, actions: optionsActions, guards: optionsGuards } = options;
5
+ const machine = useMemo(
6
+ () => typeof machineOrFactory === "function" ? machineOrFactory() : machineOrFactory,
7
+ // eslint-disable-next-line react-hooks/exhaustive-deps
8
+ []
9
+ // Only run once on mount
10
+ );
11
+ const [internal, setInternal] = useState(() => machine.getInitialInternal());
5
12
  const mergedActions = useMemo(
6
13
  () => ({ ...machine.actions, ...optionsActions }),
7
14
  [machine.actions, optionsActions]
@@ -11,27 +18,21 @@ function useMachine(machine, options) {
11
18
  [machine.guards, optionsGuards]
12
19
  );
13
20
  const inputRef = useRef(input);
21
+ const internalRef = useRef(internal);
14
22
  const machineRef = useRef(machine);
15
23
  const mergedActionsRef = useRef(mergedActions);
16
24
  const mergedGuardsRef = useRef(mergedGuards);
17
25
  const isMountedRef = useRef(true);
18
26
  inputRef.current = input;
27
+ internalRef.current = internal;
19
28
  machineRef.current = machine;
20
29
  mergedActionsRef.current = mergedActions;
21
30
  mergedGuardsRef.current = mergedGuards;
22
31
  const { computed: computedDef } = machine;
23
- const context = useMemo(
24
- () => computeValues(input, computedDef),
25
- [input, computedDef]
26
- );
27
- const computed = useMemo(() => {
28
- if (!computedDef) return {};
29
- const result = {};
30
- for (const key in computedDef) {
31
- result[key] = context[key];
32
- }
33
- return result;
34
- }, [context, computedDef]);
32
+ const context = useMemo(() => {
33
+ const base = buildContext(input, internal);
34
+ return computeValues(base, computedDef);
35
+ }, [input, internal, computedDef]);
35
36
  const contextRef = useRef(context);
36
37
  contextRef.current = context;
37
38
  const prevContextRef = useRef(context);
@@ -41,13 +42,24 @@ function useMachine(machine, options) {
41
42
  changeCleanups: /* @__PURE__ */ new Map(),
42
43
  exitCleanups: /* @__PURE__ */ new Map()
43
44
  });
45
+ const updateInternal = useCallback((newInternal) => {
46
+ internalRef.current = newInternal;
47
+ setInternal(newInternal);
48
+ }, []);
49
+ const assign = useMemo(() => createAssign(() => internalRef.current, updateInternal), [updateInternal]);
50
+ const assignRef = useRef(assign);
51
+ assignRef.current = assign;
52
+ const getContext = useCallback(() => {
53
+ const base = buildContext(inputRef.current, internalRef.current);
54
+ return computeValues(base, machineRef.current.computed);
55
+ }, []);
44
56
  const { always } = machine;
45
57
  if (prevContextRef.current !== context && always) {
46
58
  const actionsMap = mergedActions;
47
59
  const guardsMap = mergedGuards;
48
60
  for (const rule of always) {
49
- if (evaluateGuards(rule.when, guardsMap, context, void 0)) {
50
- executeRuleActions(rule.do, actionsMap, context, void 0);
61
+ if (evaluateGuards(rule.when, guardsMap, getContext(), void 0)) {
62
+ executeRuleActions(rule.do, actionsMap, getContext, void 0, assign);
51
63
  break;
52
64
  }
53
65
  }
@@ -56,38 +68,49 @@ function useMachine(machine, options) {
56
68
  const send = useCallback(
57
69
  (event, ...args) => {
58
70
  const currentMachine = machineRef.current;
59
- const currentInput = inputRef.current;
60
71
  const currentActions = mergedActionsRef.current;
61
72
  const currentGuards = mergedGuardsRef.current;
62
- const currentContext = computeValues(
63
- currentInput,
64
- currentMachine.computed
73
+ const currentAssign = createAssign(
74
+ () => internalRef.current,
75
+ (newInternal) => {
76
+ internalRef.current = newInternal;
77
+ setInternal(newInternal);
78
+ }
65
79
  );
80
+ const getCurrentContext = () => {
81
+ const base = buildContext(inputRef.current, internalRef.current);
82
+ return computeValues(base, currentMachine.computed);
83
+ };
66
84
  const payload = args[0];
67
- const state2 = currentContext.state;
68
- if (state2 && currentMachine.states?.[state2]?.on?.[event]) {
69
- const stateHandler = currentMachine.states[state2].on[event];
85
+ const actionsMap = currentActions ?? {};
86
+ const guardsMap = currentGuards;
87
+ const currentContext = getCurrentContext();
88
+ const state = currentContext.state;
89
+ if (state && currentMachine.states?.[state]?.on?.[event]) {
90
+ const stateHandler = currentMachine.states[state].on[event];
70
91
  executeHandler(
71
92
  stateHandler,
72
- currentActions ?? {},
73
- currentGuards,
74
- currentContext,
75
- payload
93
+ actionsMap,
94
+ guardsMap,
95
+ getCurrentContext,
96
+ payload,
97
+ currentAssign
76
98
  );
77
99
  }
78
100
  const globalHandler = currentMachine.on?.[event];
79
101
  if (globalHandler) {
80
102
  executeHandler(
81
103
  globalHandler,
82
- currentActions ?? {},
83
- currentGuards,
84
- currentContext,
85
- payload
104
+ actionsMap,
105
+ guardsMap,
106
+ getCurrentContext,
107
+ payload,
108
+ currentAssign
86
109
  );
87
110
  }
88
111
  },
89
112
  []
90
- // no dependencies - uses refs
113
+ // No dependencies - uses refs for latest values
91
114
  );
92
115
  const safeSend = useCallback(
93
116
  (event, ...args) => {
@@ -112,8 +135,11 @@ function useMachine(machine, options) {
112
135
  clearEffectStore(store);
113
136
  };
114
137
  }, []);
115
- const state = context.state ?? "";
116
- return { send, computed, state };
138
+ const snapshot = useMemo(
139
+ () => buildSnapshot(internal, context, computedDef),
140
+ [internal, context, computedDef]
141
+ );
142
+ return [snapshot, send];
117
143
  }
118
144
  export {
119
145
  useMachine