/** * @file Solid.js integration for @doeixd/machine * @description * Provides reactive primitives for using state machines with Solid.js, including * hooks for both sync and async machines, store integration, and signal-based APIs. * * Solid.js uses fine-grained reactivity with signals and stores, which pairs * beautifully with immutable state machines. This integration provides multiple * approaches depending on your needs: * * - `createMachine()` - Signal-based reactive machine * - `createMachineStore()` - Store-based reactive machine (for complex context) * - `createAsyncMachine()` - Async machine with signal state * - `createMachineResource()` - Resource-based async machine */ import { createSignal, createEffect, createMemo, batch, getOwner, onCleanup, untrack, type Accessor, type Setter } from 'solid-js'; import { createStore, reconcile, unwrap, type SetStoreFunction, type Store } from 'solid-js/store'; import { Machine, AsyncMachine, AsyncEvent, Context, runMachine as runMachineCore, setContext as setMachineContext } from './index'; // ============================================================================= // SIGNAL-BASED MACHINE (for simple state) // ============================================================================= /** * Creates a reactive machine using Solid signals. * * This is ideal for simple state machines where the entire machine state * needs to be tracked reactively. Every transition creates a new machine * instance, and the signal updates automatically. * * @template M - The machine type. * * @param initialMachine - A function that returns the initial machine state. * @returns A tuple of [accessor, transitions] where: * - accessor: Reactive accessor for the current machine * - transitions: Object with all machine transitions bound to update the signal * * @example * ```tsx * const [machine, actions] = createMachine(() => * createCounterMachine({ count: 0 }) * ); * * // In your component *
*

Count: {machine().context.count}

* *
* ``` * * @example With Type-State * ```tsx * type LoggedOut = Machine<{ status: "loggedOut" }> & { * login: (user: string) => LoggedIn; * }; * * type LoggedIn = Machine<{ status: "loggedIn"; user: string }> & { * logout: () => LoggedOut; * }; * * const [auth, actions] = createMachine(() => * createLoggedOut() * ); * * // Conditional rendering based on state type * *

Welcome, {auth().context.user}

* *
* ``` */ export function createMachine>( initialMachine: () => M ): [Accessor, TransitionHandlers] { const [machine, setMachine] = createSignal(initialMachine()); const handlers = createTransitionHandlers( machine, (nextMachine) => setMachine(() => nextMachine) ); return [machine, handlers]; } /** * Helper type to extract transition handlers from a machine. */ type TransitionKeys = M extends unknown ? Exclude<{ [K in keyof M]: M[K] extends (...args: any[]) => any ? K : never }[keyof M], 'context'> : never; type TransitionAt = M extends unknown ? K extends keyof M ? M[K] : never : never; type TransitionHandlers> = { [K in TransitionKeys]: TransitionAt extends (...args: infer Args) => infer Result ? (...args: Args) => Result : never; }; function createTransitionHandlers>( getMachine: () => M, updateMachine: (nextMachine: M) => void ): TransitionHandlers { return new Proxy({} as TransitionHandlers, { get: (_target, key) => (...args: unknown[]) => { const currentMachine = getMachine(); const transition = currentMachine[key as keyof M]; if (typeof transition !== 'function') { throw new Error(`[Machine] Transition '${String(key)}' is not available on the current state.`); } const nextMachine = transition.apply(currentMachine, args) as unknown; if (nextMachine === null || typeof nextMachine !== 'object' || !('context' in nextMachine)) { throw new TypeError(`Transition '${String(key)}' did not return a machine with a context property.`); } updateMachine(nextMachine as M); return nextMachine as M; } }); } // ============================================================================= // STORE-BASED MACHINE (for complex context with fine-grained reactivity) // ============================================================================= /** * Creates a reactive machine using Solid stores. * * This is ideal when you have complex nested context and want fine-grained * reactivity on individual properties. Instead of replacing the entire machine, * transitions update the store, triggering only the affected computations. * * @template M - The machine type. * * @param initialMachine - A function that returns the initial machine state. * @returns A tuple of [store, actions] where: * - store: Reactive store proxy for the machine * - setStore: Store setter that keeps the action machine's context synchronized * - actions: Transition handlers that update the store * * @example * ```tsx * const [machine, actions] = createMachineStore(() => * createUserMachine({ * profile: { name: 'Alice', age: 30 }, * settings: { theme: 'dark', notifications: true } * }) * ); * * // Fine-grained reactivity - only updates when profile.name changes *
*

Name: {machine.context.profile.name}

*

Age: {machine.context.profile.age}

* *
* ``` */ export function createMachineStore>( initialMachine: () => M ): [Store, SetStoreFunction, TransitionHandlers] { const initial = initialMachine(); let currentMachine = initial; const [store, setStore] = createStore(initial); const setStoreAndMachine = ((...args: any[]) => { (setStore as (...setterArgs: any[]) => void)(...args); currentMachine = setMachineContext(currentMachine, unwrap(store).context) as M; }) as SetStoreFunction; const handlers = createTransitionHandlers( () => currentMachine, (nextMachine) => { currentMachine = nextMachine; setStore(reconcile(nextMachine)); } ); return [store, setStoreAndMachine, handlers]; } // ============================================================================= // ASYNC MACHINE WITH SIGNALS // ============================================================================= /** * Creates a reactive async machine with event dispatching. * * This wraps the core `runMachine` with Solid reactivity, automatically * updating a signal whenever the machine state changes. Perfect for async * workflows like data fetching, multi-step forms, or any stateful async logic. * * @template M - The async machine type. * * @param initialMachine - A function that returns the initial async machine state. * @returns A tuple of [accessor, dispatch] where: * - accessor: Reactive accessor for current machine state * - dispatch: Type-safe event dispatcher * * @example Basic data fetching * ```tsx * type FetchMachine = AsyncMachine<{ * status: 'idle' | 'loading' | 'success' | 'error'; * data: any; * }> & { * fetch: () => Promise; * retry: () => Promise; * }; * * const [state, dispatch] = createAsyncMachine(() => createFetchMachine()); * *
* * * * * *

Loading...

*
* *

Data: {JSON.stringify(state().context.data)}

*
* * * *
*
* ``` * * @example With effects * ```tsx * const [state, dispatch] = createAsyncMachine(() => createAuthMachine()); * * // React to state changes * createEffect(() => { * console.log('Auth state changed:', state().context.status); * * if (state().context.status === 'loggedIn') { * // Navigate, fetch user data, etc. * } * }); * ``` */ export function createAsyncMachine>( initialMachine: () => M ): [Accessor, (event: AsyncEvent) => Promise] { const initial = initialMachine(); const [machine, setMachine] = createSignal(initial); // Create the runner with signal update callback const runner = runMachineCore(initial, (nextMachine) => { setMachine(() => nextMachine as M); }); if (getOwner()) { onCleanup(() => runner.stop()); } const dispatch = async (event: AsyncEvent): Promise => { const result = await runner.dispatch(event); return result as M; }; return [machine, dispatch]; } // ============================================================================= // CONTEXT-ONLY STORE (for just the context data) // ============================================================================= /** * Creates a Solid store for just the machine's context, with actions that * transition the machine and sync the context back to the store. * * This is useful when you want fine-grained reactivity on context properties * but don't need to track the machine instance itself. * * @template C - The context object type. * @template M - The machine type. * * @param initialMachine - A function that returns the initial machine. * @returns A tuple of [context store, setContext, actions]. * Updates made through setContext are applied to the machine used by later actions. * * @example * ```tsx * const [context, setContext, actions] = createMachineContext(() => * createCounterMachine({ count: 0, name: 'Counter' }) * ); * * // Direct access to context with fine-grained reactivity *
*

{context.name}: {context.count}

* *
* ``` */ export function createMachineContext>( initialMachine: () => M ): [Store, SetStoreFunction, TransitionHandlers] { let currentMachine = initialMachine(); const [context, setContext] = createStore(currentMachine.context); const setContextAndMachine = ((...args: any[]) => { (setContext as (...setterArgs: any[]) => void)(...args); currentMachine = setMachineContext(currentMachine, unwrap(context) as C) as M; }) as SetStoreFunction; const handlers = createTransitionHandlers( () => currentMachine, (nextMachine) => { currentMachine = nextMachine; setContext(nextMachine.context); } ); return [context, setContextAndMachine, handlers]; } // ============================================================================= // MEMOIZED MACHINE DERIVATIONS // ============================================================================= /** * Creates a memoized derivation from a machine's context. * * This is useful for computed values that depend on the machine state. * The computation only re-runs when the accessed context properties change. * * @template M - The machine type. * @template T - The computed value type. * * @param machine - Machine accessor. * @param selector - Function to compute a value from the context. * @returns A memoized accessor for the computed value. * * @example * ```tsx * const [machine, actions] = createMachine(() => createCart()); * * const total = createMachineSelector(machine, (ctx) => * ctx.items.reduce((sum, item) => sum + item.price, 0) * ); * *
*

Total: ${total()}

*
* ``` */ export function createMachineSelector, T>( machine: Accessor, selector: (context: Context) => T ): Accessor { return createMemo(() => selector(machine().context)); } // ============================================================================= // BATCH TRANSITIONS // ============================================================================= /** * Batches multiple transitions into a single reactive update. * * In Solid, this uses `batch` to group updates, preventing intermediate * re-renders and effects from firing. * * @template M - The machine type. * * @param machine - The current machine. * @param setMachine - The setter function. * @param transitions - Array of transition functions to apply. * @returns The final machine state. * * @example * ```tsx * import { batch } from 'solid-js'; * * const [machine, setMachine] = createSignal(createCounterMachine()); * * const batchUpdate = () => { * batch(() => { * let m = machine(); * m = m.increment(); * m = m.add(5); * m = m.increment(); * setMachine(m); * }); * }; * ``` */ export function batchTransitions>( machine: M, setMachine: Setter, ...transitions: Array<(m: M) => M> ): M { return batch(() => { const finalMachine = transitions.reduce((m, transition) => transition(m), machine); setMachine(() => finalMachine); return finalMachine; }); } // ============================================================================= // LIFECYCLE EFFECTS // ============================================================================= /** * Runs an effect when entering or exiting specific machine states. * * This is useful for side effects that should happen on state transitions, * like analytics, logging, or subscriptions. * * @template M - The machine type. * * @param machine - Machine accessor. * @param statePredicate - Function to determine if we're in the target state. * @param onEnter - Effect to run when entering the state. * @param onExit - Optional effect to run when exiting the state. * * @example * ```tsx * const [machine, actions] = createMachine(() => createAuthMachine()); * * createMachineEffect( * machine, * (m) => m.context.status === 'loggedIn', * (m) => { * console.log('User logged in:', m.context.username); * // Start session tracking * }, * () => { * console.log('User logged out'); * // Clean up session * } * ); * ``` */ export function createMachineEffect>( machine: Accessor, statePredicate: (m: M) => boolean, onEnter: (m: M) => void, onExit?: () => void ): void { let wasInState = false; createEffect(() => { const m = machine(); const isInState = statePredicate(m); if (isInState && !wasInState) { // Entering state untrack(() => onEnter(m)); wasInState = true; } else if (!isInState && wasInState) { // Exiting state untrack(() => onExit?.()); wasInState = false; } }); onCleanup(() => { if (wasInState && onExit) { onExit(); } }); } /** * Helper to create effects for specific context values. * * @template M - The machine type. * @template T - The selected value type. * * @param machine - Machine accessor. * @param selector - Function to select a value from context. * @param effect - Effect to run when the selected value changes. * * @example * ```tsx * const [machine, actions] = createMachine(() => createCounterMachine()); * * createMachineValueEffect( * machine, * (ctx) => ctx.count, * (count) => { * console.log('Count changed to:', count); * if (count > 10) { * alert('Count is high!'); * } * } * ); * ``` */ export function createMachineValueEffect, T>( machine: Accessor, selector: (context: Context) => T, effect: (value: T) => void ): void { createEffect(() => { const value = selector(machine().context); untrack(() => effect(value)); }); } // ============================================================================= // EXPORT TYPES FOR BETTER DX // ============================================================================= export type { Accessor, Setter, Store, SetStoreFunction };