@pyreon/machine 0.10.0 → 0.11.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/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/machine.ts"],"sourcesContent":["import { signal } from '@pyreon/reactivity'\nimport type {\n EnterCallback,\n InferEvents,\n InferStates,\n Machine,\n MachineConfig,\n MachineEvent,\n TransitionCallback,\n TransitionConfig,\n} from './types'\n\n/**\n * Create a reactive state machine — a constrained signal with type-safe transitions.\n *\n * The returned instance is callable (reads like a signal) and exposes\n * `send()`, `matches()`, `can()`, and listeners for state changes.\n *\n * @param config - Machine definition with initial state and state configs\n * @returns A reactive machine instance\n *\n * @example\n * ```tsx\n * const machine = createMachine({\n * initial: 'idle',\n * states: {\n * idle: { on: { FETCH: 'loading' } },\n * loading: { on: { SUCCESS: 'done', ERROR: 'error' } },\n * done: {},\n * error: { on: { RETRY: 'loading' } },\n * },\n * })\n *\n * machine() // 'idle'\n * machine.send('FETCH')\n * machine() // 'loading'\n *\n * // Reactive in JSX\n * {() => machine.matches('loading') && <Spinner />}\n * ```\n */\nexport function createMachine<\n const TConfig extends MachineConfig<string, string>,\n>(config: TConfig): Machine<InferStates<TConfig>, InferEvents<TConfig>> {\n type TState = InferStates<TConfig>\n type TEvent = InferEvents<TConfig>\n\n const { initial, states } = config as unknown as MachineConfig<TState, TEvent>\n\n // Validate initial state\n if (!(initial in states)) {\n throw new Error(\n `[@pyreon/machine] Initial state '${initial}' is not defined in states`,\n )\n }\n\n const current = signal<TState>(initial)\n const enterListeners = new Map<TState, Set<EnterCallback<TEvent>>>()\n const transitionListeners = new Set<TransitionCallback<TState, TEvent>>()\n\n function resolveTransition(event: TEvent, payload?: unknown): TState | null {\n const stateConfig = states[current.peek()]\n if (!stateConfig?.on) return null\n\n const transition = stateConfig.on[event] as\n | TransitionConfig<TState>\n | undefined\n if (!transition) return null\n\n if (typeof transition === 'string') {\n return transition\n }\n\n // Guarded transition\n if (transition.guard && !transition.guard(payload)) {\n return null\n }\n\n return transition.target\n }\n\n // The machine instance — callable like a signal\n function machine(): TState {\n return current()\n }\n\n machine.send = (event: TEvent, payload?: unknown): void => {\n const target = resolveTransition(event, payload)\n if (target === null) return\n\n const from = current.peek()\n const machineEvent: MachineEvent<TEvent> = { type: event, payload }\n\n current.set(target)\n\n // Fire transition listeners\n for (const cb of transitionListeners) {\n cb(from, target, machineEvent)\n }\n\n // Fire enter listeners for the target state\n const listeners = enterListeners.get(target)\n if (listeners) {\n for (const cb of listeners) {\n cb(machineEvent)\n }\n }\n }\n\n machine.matches = (...matchStates: TState[]): boolean => {\n const state = current()\n return matchStates.includes(state)\n }\n\n machine.can = (event: TEvent): boolean => {\n const stateConfig = states[current()]\n if (!stateConfig?.on) return false\n\n const transition = stateConfig.on[event]\n if (!transition) return false\n\n // For guarded transitions, we can't know without payload\n // Return true if the event exists (guard may still reject)\n return true\n }\n\n machine.nextEvents = (): TEvent[] => {\n const stateConfig = states[current()]\n if (!stateConfig?.on) return []\n return Object.keys(stateConfig.on) as TEvent[]\n }\n\n machine.reset = (): void => {\n current.set(initial)\n }\n\n machine.onEnter = (\n state: TState,\n callback: EnterCallback<TEvent>,\n ): (() => void) => {\n if (!enterListeners.has(state)) {\n enterListeners.set(state, new Set())\n }\n enterListeners.get(state)!.add(callback)\n\n return () => {\n enterListeners.get(state)?.delete(callback)\n }\n }\n\n machine.onTransition = (\n callback: TransitionCallback<TState, TEvent>,\n ): (() => void) => {\n transitionListeners.add(callback)\n return () => {\n transitionListeners.delete(callback)\n }\n }\n\n machine.dispose = (): void => {\n enterListeners.clear()\n transitionListeners.clear()\n }\n\n return machine as Machine<TState, TEvent>\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,SAAgB,cAEd,QAAsE;CAItE,MAAM,EAAE,SAAS,WAAW;AAG5B,KAAI,EAAE,WAAW,QACf,OAAM,IAAI,MACR,oCAAoC,QAAQ,4BAC7C;CAGH,MAAM,UAAU,OAAe,QAAQ;CACvC,MAAM,iCAAiB,IAAI,KAAyC;CACpE,MAAM,sCAAsB,IAAI,KAAyC;CAEzE,SAAS,kBAAkB,OAAe,SAAkC;EAC1E,MAAM,cAAc,OAAO,QAAQ,MAAM;AACzC,MAAI,CAAC,aAAa,GAAI,QAAO;EAE7B,MAAM,aAAa,YAAY,GAAG;AAGlC,MAAI,CAAC,WAAY,QAAO;AAExB,MAAI,OAAO,eAAe,SACxB,QAAO;AAIT,MAAI,WAAW,SAAS,CAAC,WAAW,MAAM,QAAQ,CAChD,QAAO;AAGT,SAAO,WAAW;;CAIpB,SAAS,UAAkB;AACzB,SAAO,SAAS;;AAGlB,SAAQ,QAAQ,OAAe,YAA4B;EACzD,MAAM,SAAS,kBAAkB,OAAO,QAAQ;AAChD,MAAI,WAAW,KAAM;EAErB,MAAM,OAAO,QAAQ,MAAM;EAC3B,MAAM,eAAqC;GAAE,MAAM;GAAO;GAAS;AAEnE,UAAQ,IAAI,OAAO;AAGnB,OAAK,MAAM,MAAM,oBACf,IAAG,MAAM,QAAQ,aAAa;EAIhC,MAAM,YAAY,eAAe,IAAI,OAAO;AAC5C,MAAI,UACF,MAAK,MAAM,MAAM,UACf,IAAG,aAAa;;AAKtB,SAAQ,WAAW,GAAG,gBAAmC;EACvD,MAAM,QAAQ,SAAS;AACvB,SAAO,YAAY,SAAS,MAAM;;AAGpC,SAAQ,OAAO,UAA2B;EACxC,MAAM,cAAc,OAAO,SAAS;AACpC,MAAI,CAAC,aAAa,GAAI,QAAO;AAG7B,MAAI,CADe,YAAY,GAAG,OACjB,QAAO;AAIxB,SAAO;;AAGT,SAAQ,mBAA6B;EACnC,MAAM,cAAc,OAAO,SAAS;AACpC,MAAI,CAAC,aAAa,GAAI,QAAO,EAAE;AAC/B,SAAO,OAAO,KAAK,YAAY,GAAG;;AAGpC,SAAQ,cAAoB;AAC1B,UAAQ,IAAI,QAAQ;;AAGtB,SAAQ,WACN,OACA,aACiB;AACjB,MAAI,CAAC,eAAe,IAAI,MAAM,CAC5B,gBAAe,IAAI,uBAAO,IAAI,KAAK,CAAC;AAEtC,iBAAe,IAAI,MAAM,CAAE,IAAI,SAAS;AAExC,eAAa;AACX,kBAAe,IAAI,MAAM,EAAE,OAAO,SAAS;;;AAI/C,SAAQ,gBACN,aACiB;AACjB,sBAAoB,IAAI,SAAS;AACjC,eAAa;AACX,uBAAoB,OAAO,SAAS;;;AAIxC,SAAQ,gBAAsB;AAC5B,iBAAe,OAAO;AACtB,sBAAoB,OAAO;;AAG7B,QAAO"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/machine.ts"],"sourcesContent":["import { signal } from \"@pyreon/reactivity\"\nimport type {\n EnterCallback,\n InferEvents,\n InferStates,\n Machine,\n MachineConfig,\n MachineEvent,\n TransitionCallback,\n TransitionConfig,\n} from \"./types\"\n\n/**\n * Create a reactive state machine — a constrained signal with type-safe transitions.\n *\n * The returned instance is callable (reads like a signal) and exposes\n * `send()`, `matches()`, `can()`, and listeners for state changes.\n *\n * @param config - Machine definition with initial state and state configs\n * @returns A reactive machine instance\n *\n * @example\n * ```tsx\n * const machine = createMachine({\n * initial: 'idle',\n * states: {\n * idle: { on: { FETCH: 'loading' } },\n * loading: { on: { SUCCESS: 'done', ERROR: 'error' } },\n * done: {},\n * error: { on: { RETRY: 'loading' } },\n * },\n * })\n *\n * machine() // 'idle'\n * machine.send('FETCH')\n * machine() // 'loading'\n *\n * // Reactive in JSX\n * {() => machine.matches('loading') && <Spinner />}\n * ```\n */\nexport function createMachine<const TConfig extends MachineConfig<string, string>>(\n config: TConfig,\n): Machine<InferStates<TConfig>, InferEvents<TConfig>> {\n type TState = InferStates<TConfig>\n type TEvent = InferEvents<TConfig>\n\n const { initial, states } = config as unknown as MachineConfig<TState, TEvent>\n\n // Validate initial state\n if (!(initial in states)) {\n throw new Error(`[@pyreon/machine] Initial state '${initial}' is not defined in states`)\n }\n\n const current = signal<TState>(initial)\n const enterListeners = new Map<TState, Set<EnterCallback<TEvent>>>()\n const transitionListeners = new Set<TransitionCallback<TState, TEvent>>()\n\n function resolveTransition(event: TEvent, payload?: unknown): TState | null {\n const stateConfig = states[current.peek()]\n if (!stateConfig?.on) return null\n\n const transition = stateConfig.on[event] as TransitionConfig<TState> | undefined\n if (!transition) return null\n\n if (typeof transition === \"string\") {\n return transition\n }\n\n // Guarded transition\n if (transition.guard && !transition.guard(payload)) {\n return null\n }\n\n return transition.target\n }\n\n // The machine instance — callable like a signal\n function machine(): TState {\n return current()\n }\n\n machine.send = (event: TEvent, payload?: unknown): void => {\n const target = resolveTransition(event, payload)\n if (target === null) return\n\n const from = current.peek()\n const machineEvent: MachineEvent<TEvent> = { type: event, payload }\n\n current.set(target)\n\n // Fire transition listeners\n for (const cb of transitionListeners) {\n cb(from, target, machineEvent)\n }\n\n // Fire enter listeners for the target state\n const listeners = enterListeners.get(target)\n if (listeners) {\n for (const cb of listeners) {\n cb(machineEvent)\n }\n }\n }\n\n machine.matches = (...matchStates: TState[]): boolean => {\n const state = current()\n return matchStates.includes(state)\n }\n\n machine.can = (event: TEvent): boolean => {\n const stateConfig = states[current()]\n if (!stateConfig?.on) return false\n\n const transition = stateConfig.on[event]\n if (!transition) return false\n\n // For guarded transitions, we can't know without payload\n // Return true if the event exists (guard may still reject)\n return true\n }\n\n machine.nextEvents = (): TEvent[] => {\n const stateConfig = states[current()]\n if (!stateConfig?.on) return []\n return Object.keys(stateConfig.on) as TEvent[]\n }\n\n machine.reset = (): void => {\n current.set(initial)\n }\n\n machine.onEnter = (state: TState, callback: EnterCallback<TEvent>): (() => void) => {\n if (!enterListeners.has(state)) {\n enterListeners.set(state, new Set())\n }\n enterListeners.get(state)!.add(callback)\n\n return () => {\n enterListeners.get(state)?.delete(callback)\n }\n }\n\n machine.onTransition = (callback: TransitionCallback<TState, TEvent>): (() => void) => {\n transitionListeners.add(callback)\n return () => {\n transitionListeners.delete(callback)\n }\n }\n\n machine.dispose = (): void => {\n enterListeners.clear()\n transitionListeners.clear()\n }\n\n return machine as Machine<TState, TEvent>\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,SAAgB,cACd,QACqD;CAIrD,MAAM,EAAE,SAAS,WAAW;AAG5B,KAAI,EAAE,WAAW,QACf,OAAM,IAAI,MAAM,oCAAoC,QAAQ,4BAA4B;CAG1F,MAAM,UAAU,OAAe,QAAQ;CACvC,MAAM,iCAAiB,IAAI,KAAyC;CACpE,MAAM,sCAAsB,IAAI,KAAyC;CAEzE,SAAS,kBAAkB,OAAe,SAAkC;EAC1E,MAAM,cAAc,OAAO,QAAQ,MAAM;AACzC,MAAI,CAAC,aAAa,GAAI,QAAO;EAE7B,MAAM,aAAa,YAAY,GAAG;AAClC,MAAI,CAAC,WAAY,QAAO;AAExB,MAAI,OAAO,eAAe,SACxB,QAAO;AAIT,MAAI,WAAW,SAAS,CAAC,WAAW,MAAM,QAAQ,CAChD,QAAO;AAGT,SAAO,WAAW;;CAIpB,SAAS,UAAkB;AACzB,SAAO,SAAS;;AAGlB,SAAQ,QAAQ,OAAe,YAA4B;EACzD,MAAM,SAAS,kBAAkB,OAAO,QAAQ;AAChD,MAAI,WAAW,KAAM;EAErB,MAAM,OAAO,QAAQ,MAAM;EAC3B,MAAM,eAAqC;GAAE,MAAM;GAAO;GAAS;AAEnE,UAAQ,IAAI,OAAO;AAGnB,OAAK,MAAM,MAAM,oBACf,IAAG,MAAM,QAAQ,aAAa;EAIhC,MAAM,YAAY,eAAe,IAAI,OAAO;AAC5C,MAAI,UACF,MAAK,MAAM,MAAM,UACf,IAAG,aAAa;;AAKtB,SAAQ,WAAW,GAAG,gBAAmC;EACvD,MAAM,QAAQ,SAAS;AACvB,SAAO,YAAY,SAAS,MAAM;;AAGpC,SAAQ,OAAO,UAA2B;EACxC,MAAM,cAAc,OAAO,SAAS;AACpC,MAAI,CAAC,aAAa,GAAI,QAAO;AAG7B,MAAI,CADe,YAAY,GAAG,OACjB,QAAO;AAIxB,SAAO;;AAGT,SAAQ,mBAA6B;EACnC,MAAM,cAAc,OAAO,SAAS;AACpC,MAAI,CAAC,aAAa,GAAI,QAAO,EAAE;AAC/B,SAAO,OAAO,KAAK,YAAY,GAAG;;AAGpC,SAAQ,cAAoB;AAC1B,UAAQ,IAAI,QAAQ;;AAGtB,SAAQ,WAAW,OAAe,aAAkD;AAClF,MAAI,CAAC,eAAe,IAAI,MAAM,CAC5B,gBAAe,IAAI,uBAAO,IAAI,KAAK,CAAC;AAEtC,iBAAe,IAAI,MAAM,CAAE,IAAI,SAAS;AAExC,eAAa;AACX,kBAAe,IAAI,MAAM,EAAE,OAAO,SAAS;;;AAI/C,SAAQ,gBAAgB,aAA+D;AACrF,sBAAoB,IAAI,SAAS;AACjC,eAAa;AACX,uBAAoB,OAAO,SAAS;;;AAIxC,SAAQ,gBAAsB;AAC5B,iBAAe,OAAO;AACtB,sBAAoB,OAAO;;AAG7B,QAAO"}
@@ -1 +1 @@
1
- {"version":3,"file":"index2.d.ts","names":[],"sources":["../../../src/types.ts","../../../src/machine.ts"],"mappings":";;AAGA;;KAAY,gBAAA,0BACR,MAAA;EACE,MAAA,EAAQ,MAAA;EAAQ,KAAA,GAAQ,OAAA;AAAA;;;;UAKb,WAAA;EACf,EAAA,GAAK,OAAA,CAAQ,MAAA,CAAO,MAAA,EAAQ,gBAAA,CAAiB,MAAA;AAAA;AAD/C;;;AAAA,UAOiB,aAAA;EACf,OAAA,EAAS,MAAA;EACT,MAAA,EAAQ,MAAA,CAAO,MAAA,EAAQ,WAAA,CAAY,MAAA,EAAQ,MAAA;AAAA;;;;UAM5B,YAAA;EACf,IAAA,EAAM,MAAA;EACN,OAAA;AAAA;;;;KAMU,aAAA,oCACV,KAAA,EAAO,YAAA,CAAa,MAAA;;;AAjBtB;KAuBY,kBAAA,oEAGP,IAAA,EAAM,MAAA,EAAQ,EAAA,EAAI,MAAA,EAAQ,KAAA,EAAO,YAAA,CAAa,MAAA;;;;UAKlC,OAAA;EA7B4B;EAAA,IA+BvC,MAAA;EA/BI;EAkCR,IAAA,GAAO,KAAA,EAAO,MAAA,EAAQ,OAAA;EAlCR;EAqCd,OAAA,MAAa,MAAA,EAAQ,MAAA;EAvC+B;EA0CpD,GAAA,GAAM,KAAA,EAAO,MAAA;EAzCJ;EA4CT,UAAA,QAAkB,MAAA;EA3CV;EA8CR,KAAA;EA9CuB;EAiDvB,OAAA,GAAU,KAAA,EAAO,MAAA,EAAQ,QAAA,EAAU,aAAA,CAAc,MAAA;EAjDN;EAoD3C,YAAA,GAAe,QAAA,EAAU,kBAAA,CAAmB,MAAA,EAAQ,MAAA;EApDH;EAuDjD,OAAA;AAAA;;KAMU,WAAA,MAAiB,CAAA;EAAY,MAAA,EAAQ,MAAA;AAAA,IAC7C,CAAA;;KAIQ,WAAA,MAAiB,CAAA;EAC3B,MAAA,EAAQ,MAAA;IAAiB,EAAA,GAAK,OAAA,CAAQ,MAAA;EAAA;AAAA,IAEpC,CAAA;;;AArFJ;;;;;;;;;;;;AAOA;;;;;;;;;;;;;;;;;AAPA,iBCsCgB,aAAA,uBACQ,aAAA,iBAAA,CACtB,MAAA,EAAQ,OAAA,GAAU,OAAA,CAAQ,WAAA,CAAY,OAAA,GAAU,WAAA,CAAY,OAAA"}
1
+ {"version":3,"file":"index2.d.ts","names":[],"sources":["../../../src/types.ts","../../../src/machine.ts"],"mappings":";;AAGA;;KAAY,gBAAA,0BACR,MAAA;EACE,MAAA,EAAQ,MAAA;EAAQ,KAAA,GAAQ,OAAA;AAAA;;;;UAKb,WAAA;EACf,EAAA,GAAK,OAAA,CAAQ,MAAA,CAAO,MAAA,EAAQ,gBAAA,CAAiB,MAAA;AAAA;AAD/C;;;AAAA,UAOiB,aAAA;EACf,OAAA,EAAS,MAAA;EACT,MAAA,EAAQ,MAAA,CAAO,MAAA,EAAQ,WAAA,CAAY,MAAA,EAAQ,MAAA;AAAA;;;;UAM5B,YAAA;EACf,IAAA,EAAM,MAAA;EACN,OAAA;AAAA;;;;KAMU,aAAA,oCAAiD,KAAA,EAAO,YAAA,CAAa,MAAA;;;AAhBjF;KAqBY,kBAAA,oEACV,IAAA,EAAM,MAAA,EACN,EAAA,EAAI,MAAA,EACJ,KAAA,EAAO,YAAA,CAAa,MAAA;;;;UAML,OAAA;EA5B4B;EAAA,IA8BvC,MAAA;EA9BI;EAiCR,IAAA,GAAO,KAAA,EAAO,MAAA,EAAQ,OAAA;EAjCR;EAoCd,OAAA,MAAa,MAAA,EAAQ,MAAA;EAtC+B;EAyCpD,GAAA,GAAM,KAAA,EAAO,MAAA;EAxCJ;EA2CT,UAAA,QAAkB,MAAA;EA1CV;EA6CR,KAAA;EA7CuB;EAgDvB,OAAA,GAAU,KAAA,EAAO,MAAA,EAAQ,QAAA,EAAU,aAAA,CAAc,MAAA;EAhDN;EAmD3C,YAAA,GAAe,QAAA,EAAU,kBAAA,CAAmB,MAAA,EAAQ,MAAA;EAnDH;EAsDjD,OAAA;AAAA;;KAMU,WAAA,MAAiB,CAAA;EAAY,MAAA,EAAQ,MAAA;AAAA,IAA6B,CAAA;;KAGlE,WAAA,MAAiB,CAAA;EAC3B,MAAA,EAAQ,MAAA;IAAiB,EAAA,GAAK,OAAA,CAAQ,MAAA;EAAA;AAAA,IAEpC,CAAA;;;AAlFJ;;;;;;;;;;;;AAOA;;;;;;;;;;;;;;;;;AAPA,iBCsCgB,aAAA,uBAAoC,aAAA,iBAAA,CAClD,MAAA,EAAQ,OAAA,GACP,OAAA,CAAQ,WAAA,CAAY,OAAA,GAAU,WAAA,CAAY,OAAA"}
package/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "@pyreon/machine",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Reactive state machines for Pyreon — constrained signals with type-safe transitions",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "https://github.com/pyreon/fundamentals.git",
9
- "directory": "packages/machine"
8
+ "url": "https://github.com/pyreon/pyreon.git",
9
+ "directory": "packages/fundamentals/machine"
10
10
  },
11
11
  "homepage": "https://github.com/pyreon/fundamentals/tree/main/packages/machine#readme",
12
12
  "bugs": {
13
- "url": "https://github.com/pyreon/fundamentals/issues"
13
+ "url": "https://github.com/pyreon/pyreon/issues"
14
14
  },
15
15
  "publishConfig": {
16
16
  "access": "public"
@@ -37,9 +37,15 @@
37
37
  "build": "vl_rolldown_build",
38
38
  "dev": "vl_rolldown_build-watch",
39
39
  "test": "vitest run",
40
- "typecheck": "tsc --noEmit"
40
+ "typecheck": "tsc --noEmit",
41
+ "lint": "biome check ."
41
42
  },
42
43
  "peerDependencies": {
43
- "@pyreon/reactivity": ">=0.7.0 <0.8.0"
44
+ "@pyreon/reactivity": "^0.11.0"
45
+ },
46
+ "devDependencies": {
47
+ "@happy-dom/global-registrator": "^20.8.3",
48
+ "@pyreon/reactivity": "^0.11.0",
49
+ "@vitus-labs/tools-lint": "^1.11.0"
44
50
  }
45
51
  }
package/src/index.ts CHANGED
@@ -25,7 +25,7 @@
25
25
  * ```
26
26
  */
27
27
 
28
- export { createMachine } from './machine'
28
+ export { createMachine } from "./machine"
29
29
 
30
30
  // Types
31
31
  export type {
@@ -38,4 +38,4 @@ export type {
38
38
  StateConfig,
39
39
  TransitionCallback,
40
40
  TransitionConfig,
41
- } from './types'
41
+ } from "./types"
package/src/machine.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { signal } from '@pyreon/reactivity'
1
+ import { signal } from "@pyreon/reactivity"
2
2
  import type {
3
3
  EnterCallback,
4
4
  InferEvents,
@@ -8,7 +8,7 @@ import type {
8
8
  MachineEvent,
9
9
  TransitionCallback,
10
10
  TransitionConfig,
11
- } from './types'
11
+ } from "./types"
12
12
 
13
13
  /**
14
14
  * Create a reactive state machine — a constrained signal with type-safe transitions.
@@ -39,9 +39,9 @@ import type {
39
39
  * {() => machine.matches('loading') && <Spinner />}
40
40
  * ```
41
41
  */
42
- export function createMachine<
43
- const TConfig extends MachineConfig<string, string>,
44
- >(config: TConfig): Machine<InferStates<TConfig>, InferEvents<TConfig>> {
42
+ export function createMachine<const TConfig extends MachineConfig<string, string>>(
43
+ config: TConfig,
44
+ ): Machine<InferStates<TConfig>, InferEvents<TConfig>> {
45
45
  type TState = InferStates<TConfig>
46
46
  type TEvent = InferEvents<TConfig>
47
47
 
@@ -49,9 +49,7 @@ export function createMachine<
49
49
 
50
50
  // Validate initial state
51
51
  if (!(initial in states)) {
52
- throw new Error(
53
- `[@pyreon/machine] Initial state '${initial}' is not defined in states`,
54
- )
52
+ throw new Error(`[@pyreon/machine] Initial state '${initial}' is not defined in states`)
55
53
  }
56
54
 
57
55
  const current = signal<TState>(initial)
@@ -62,12 +60,10 @@ export function createMachine<
62
60
  const stateConfig = states[current.peek()]
63
61
  if (!stateConfig?.on) return null
64
62
 
65
- const transition = stateConfig.on[event] as
66
- | TransitionConfig<TState>
67
- | undefined
63
+ const transition = stateConfig.on[event] as TransitionConfig<TState> | undefined
68
64
  if (!transition) return null
69
65
 
70
- if (typeof transition === 'string') {
66
+ if (typeof transition === "string") {
71
67
  return transition
72
68
  }
73
69
 
@@ -134,10 +130,7 @@ export function createMachine<
134
130
  current.set(initial)
135
131
  }
136
132
 
137
- machine.onEnter = (
138
- state: TState,
139
- callback: EnterCallback<TEvent>,
140
- ): (() => void) => {
133
+ machine.onEnter = (state: TState, callback: EnterCallback<TEvent>): (() => void) => {
141
134
  if (!enterListeners.has(state)) {
142
135
  enterListeners.set(state, new Set())
143
136
  }
@@ -148,9 +141,7 @@ export function createMachine<
148
141
  }
149
142
  }
150
143
 
151
- machine.onTransition = (
152
- callback: TransitionCallback<TState, TEvent>,
153
- ): (() => void) => {
144
+ machine.onTransition = (callback: TransitionCallback<TState, TEvent>): (() => void) => {
154
145
  transitionListeners.add(callback)
155
146
  return () => {
156
147
  transitionListeners.delete(callback)
@@ -1,167 +1,167 @@
1
- import { computed, effect, signal } from '@pyreon/reactivity'
2
- import { describe, expect, it, vi } from 'vitest'
3
- import { createMachine } from '../index'
1
+ import { computed, effect, signal } from "@pyreon/reactivity"
2
+ import { describe, expect, it, vi } from "vitest"
3
+ import { createMachine } from "../index"
4
4
 
5
- describe('createMachine', () => {
5
+ describe("createMachine", () => {
6
6
  // ─── Basic state and transitions ─────────────────────────────────────
7
7
 
8
- describe('basic transitions', () => {
9
- it('starts in initial state', () => {
8
+ describe("basic transitions", () => {
9
+ it("starts in initial state", () => {
10
10
  const m = createMachine({
11
- initial: 'idle',
11
+ initial: "idle",
12
12
  states: {
13
- idle: { on: { START: 'running' } },
13
+ idle: { on: { START: "running" } },
14
14
  running: {},
15
15
  },
16
16
  })
17
- expect(m()).toBe('idle')
17
+ expect(m()).toBe("idle")
18
18
  })
19
19
 
20
- it('transitions on valid event', () => {
20
+ it("transitions on valid event", () => {
21
21
  const m = createMachine({
22
- initial: 'idle',
22
+ initial: "idle",
23
23
  states: {
24
- idle: { on: { START: 'running' } },
25
- running: { on: { STOP: 'idle' } },
24
+ idle: { on: { START: "running" } },
25
+ running: { on: { STOP: "idle" } },
26
26
  },
27
27
  })
28
- m.send('START')
29
- expect(m()).toBe('running')
28
+ m.send("START")
29
+ expect(m()).toBe("running")
30
30
  })
31
31
 
32
- it('ignores invalid events (no-op)', () => {
32
+ it("ignores invalid events (no-op)", () => {
33
33
  const m = createMachine({
34
- initial: 'idle',
34
+ initial: "idle",
35
35
  states: {
36
- idle: { on: { START: 'running' } },
37
- running: { on: { STOP: 'idle' } },
36
+ idle: { on: { START: "running" } },
37
+ running: { on: { STOP: "idle" } },
38
38
  },
39
39
  })
40
- m.send('STOP' as any) // not valid in 'idle'
41
- expect(m()).toBe('idle')
40
+ m.send("STOP" as any) // not valid in 'idle'
41
+ expect(m()).toBe("idle")
42
42
  })
43
43
 
44
- it('supports self-transitions', () => {
44
+ it("supports self-transitions", () => {
45
45
  const m = createMachine({
46
- initial: 'counting',
46
+ initial: "counting",
47
47
  states: {
48
- counting: { on: { INCREMENT: 'counting' } },
48
+ counting: { on: { INCREMENT: "counting" } },
49
49
  },
50
50
  })
51
- m.send('INCREMENT')
52
- expect(m()).toBe('counting')
51
+ m.send("INCREMENT")
52
+ expect(m()).toBe("counting")
53
53
  })
54
54
 
55
- it('supports multiple transitions from one state', () => {
55
+ it("supports multiple transitions from one state", () => {
56
56
  const m = createMachine({
57
- initial: 'idle',
57
+ initial: "idle",
58
58
  states: {
59
- idle: { on: { FETCH: 'loading', CANCEL: 'cancelled' } },
59
+ idle: { on: { FETCH: "loading", CANCEL: "cancelled" } },
60
60
  loading: {},
61
61
  cancelled: {},
62
62
  },
63
63
  })
64
64
 
65
- m.send('CANCEL')
66
- expect(m()).toBe('cancelled')
65
+ m.send("CANCEL")
66
+ expect(m()).toBe("cancelled")
67
67
  })
68
68
 
69
- it('handles states with no transitions (final states)', () => {
69
+ it("handles states with no transitions (final states)", () => {
70
70
  const m = createMachine({
71
- initial: 'idle',
71
+ initial: "idle",
72
72
  states: {
73
- idle: { on: { DONE: 'finished' } },
73
+ idle: { on: { DONE: "finished" } },
74
74
  finished: {},
75
75
  },
76
76
  })
77
- m.send('DONE')
78
- expect(m()).toBe('finished')
79
- m.send('DONE') // ignored — no transitions from 'finished'
80
- expect(m()).toBe('finished')
77
+ m.send("DONE")
78
+ expect(m()).toBe("finished")
79
+ m.send("DONE") // ignored — no transitions from 'finished'
80
+ expect(m()).toBe("finished")
81
81
  })
82
82
 
83
- it('throws on invalid initial state', () => {
83
+ it("throws on invalid initial state", () => {
84
84
  expect(() =>
85
85
  createMachine({
86
- initial: 'nonexistent' as any,
86
+ initial: "nonexistent" as any,
87
87
  states: {
88
88
  idle: {},
89
89
  },
90
90
  }),
91
- ).toThrow('[@pyreon/machine] Initial state')
91
+ ).toThrow("[@pyreon/machine] Initial state")
92
92
  })
93
93
  })
94
94
 
95
95
  // ─── Guards ──────────────────────────────────────────────────────────
96
96
 
97
- describe('guards', () => {
98
- it('transitions when guard returns true', () => {
97
+ describe("guards", () => {
98
+ it("transitions when guard returns true", () => {
99
99
  const m = createMachine({
100
- initial: 'editing',
100
+ initial: "editing",
101
101
  states: {
102
102
  editing: {
103
103
  on: {
104
- SUBMIT: { target: 'submitting', guard: () => true },
104
+ SUBMIT: { target: "submitting", guard: () => true },
105
105
  },
106
106
  },
107
107
  submitting: {},
108
108
  },
109
109
  })
110
- m.send('SUBMIT')
111
- expect(m()).toBe('submitting')
110
+ m.send("SUBMIT")
111
+ expect(m()).toBe("submitting")
112
112
  })
113
113
 
114
- it('blocks transition when guard returns false', () => {
114
+ it("blocks transition when guard returns false", () => {
115
115
  const m = createMachine({
116
- initial: 'editing',
116
+ initial: "editing",
117
117
  states: {
118
118
  editing: {
119
119
  on: {
120
- SUBMIT: { target: 'submitting', guard: () => false },
120
+ SUBMIT: { target: "submitting", guard: () => false },
121
121
  },
122
122
  },
123
123
  submitting: {},
124
124
  },
125
125
  })
126
- m.send('SUBMIT')
127
- expect(m()).toBe('editing')
126
+ m.send("SUBMIT")
127
+ expect(m()).toBe("editing")
128
128
  })
129
129
 
130
- it('guard receives event payload', () => {
130
+ it("guard receives event payload", () => {
131
131
  const guardFn = vi.fn((payload?: unknown) => {
132
132
  return (payload as any)?.valid === true
133
133
  })
134
134
 
135
135
  const m = createMachine({
136
- initial: 'editing',
136
+ initial: "editing",
137
137
  states: {
138
138
  editing: {
139
139
  on: {
140
- SUBMIT: { target: 'submitting', guard: guardFn },
140
+ SUBMIT: { target: "submitting", guard: guardFn },
141
141
  },
142
142
  },
143
143
  submitting: {},
144
144
  },
145
145
  })
146
146
 
147
- m.send('SUBMIT', { valid: false })
148
- expect(m()).toBe('editing')
147
+ m.send("SUBMIT", { valid: false })
148
+ expect(m()).toBe("editing")
149
149
  expect(guardFn).toHaveBeenCalledWith({ valid: false })
150
150
 
151
- m.send('SUBMIT', { valid: true })
152
- expect(m()).toBe('submitting')
151
+ m.send("SUBMIT", { valid: true })
152
+ expect(m()).toBe("submitting")
153
153
  })
154
154
 
155
- it('guard with reactive signal', () => {
155
+ it("guard with reactive signal", () => {
156
156
  const isValid = signal(false)
157
157
 
158
158
  const m = createMachine({
159
- initial: 'editing',
159
+ initial: "editing",
160
160
  states: {
161
161
  editing: {
162
162
  on: {
163
163
  SUBMIT: {
164
- target: 'submitting',
164
+ target: "submitting",
165
165
  guard: () => isValid.peek(),
166
166
  },
167
167
  },
@@ -170,154 +170,154 @@ describe('createMachine', () => {
170
170
  },
171
171
  })
172
172
 
173
- m.send('SUBMIT')
174
- expect(m()).toBe('editing')
173
+ m.send("SUBMIT")
174
+ expect(m()).toBe("editing")
175
175
 
176
176
  isValid.set(true)
177
- m.send('SUBMIT')
178
- expect(m()).toBe('submitting')
177
+ m.send("SUBMIT")
178
+ expect(m()).toBe("submitting")
179
179
  })
180
180
  })
181
181
 
182
182
  // ─── matches ─────────────────────────────────────────────────────────
183
183
 
184
- describe('matches()', () => {
185
- it('returns true for current state', () => {
184
+ describe("matches()", () => {
185
+ it("returns true for current state", () => {
186
186
  const m = createMachine({
187
- initial: 'idle',
187
+ initial: "idle",
188
188
  states: {
189
- idle: { on: { START: 'running' } },
189
+ idle: { on: { START: "running" } },
190
190
  running: {},
191
191
  },
192
192
  })
193
- expect(m.matches('idle')).toBe(true)
194
- expect(m.matches('running')).toBe(false)
193
+ expect(m.matches("idle")).toBe(true)
194
+ expect(m.matches("running")).toBe(false)
195
195
  })
196
196
 
197
- it('supports multiple states', () => {
197
+ it("supports multiple states", () => {
198
198
  const m = createMachine({
199
- initial: 'loading',
199
+ initial: "loading",
200
200
  states: {
201
201
  idle: {},
202
202
  loading: {},
203
203
  error: {},
204
204
  },
205
205
  })
206
- expect(m.matches('loading', 'error')).toBe(true)
207
- expect(m.matches('idle', 'error')).toBe(false)
206
+ expect(m.matches("loading", "error")).toBe(true)
207
+ expect(m.matches("idle", "error")).toBe(false)
208
208
  })
209
209
 
210
- it('is reactive in effects', () => {
210
+ it("is reactive in effects", () => {
211
211
  const m = createMachine({
212
- initial: 'idle',
212
+ initial: "idle",
213
213
  states: {
214
- idle: { on: { START: 'running' } },
215
- running: { on: { STOP: 'idle' } },
214
+ idle: { on: { START: "running" } },
215
+ running: { on: { STOP: "idle" } },
216
216
  },
217
217
  })
218
218
  const results: boolean[] = []
219
219
 
220
220
  effect(() => {
221
- results.push(m.matches('running'))
221
+ results.push(m.matches("running"))
222
222
  })
223
223
 
224
224
  expect(results).toEqual([false])
225
225
 
226
- m.send('START')
226
+ m.send("START")
227
227
  expect(results).toEqual([false, true])
228
228
 
229
- m.send('STOP')
229
+ m.send("STOP")
230
230
  expect(results).toEqual([false, true, false])
231
231
  })
232
232
  })
233
233
 
234
234
  // ─── can ─────────────────────────────────────────────────────────────
235
235
 
236
- describe('can()', () => {
237
- it('returns true for valid events', () => {
236
+ describe("can()", () => {
237
+ it("returns true for valid events", () => {
238
238
  const m = createMachine({
239
- initial: 'idle',
239
+ initial: "idle",
240
240
  states: {
241
- idle: { on: { START: 'running' } },
242
- running: { on: { STOP: 'idle' } },
241
+ idle: { on: { START: "running" } },
242
+ running: { on: { STOP: "idle" } },
243
243
  },
244
244
  })
245
- expect(m.can('START')).toBe(true)
246
- expect(m.can('STOP')).toBe(false)
245
+ expect(m.can("START")).toBe(true)
246
+ expect(m.can("STOP")).toBe(false)
247
247
  })
248
248
 
249
- it('is reactive', () => {
249
+ it("is reactive", () => {
250
250
  const m = createMachine({
251
- initial: 'idle',
251
+ initial: "idle",
252
252
  states: {
253
- idle: { on: { START: 'running' } },
254
- running: { on: { STOP: 'idle' } },
253
+ idle: { on: { START: "running" } },
254
+ running: { on: { STOP: "idle" } },
255
255
  },
256
256
  })
257
257
  const results: boolean[] = []
258
258
 
259
259
  effect(() => {
260
- results.push(m.can('STOP'))
260
+ results.push(m.can("STOP"))
261
261
  })
262
262
 
263
263
  expect(results).toEqual([false])
264
264
 
265
- m.send('START')
265
+ m.send("START")
266
266
  expect(results).toEqual([false, true])
267
267
  })
268
268
 
269
- it('returns true for guarded transitions (guard not evaluated)', () => {
269
+ it("returns true for guarded transitions (guard not evaluated)", () => {
270
270
  const m = createMachine({
271
- initial: 'editing',
271
+ initial: "editing",
272
272
  states: {
273
273
  editing: {
274
274
  on: {
275
- SUBMIT: { target: 'submitting', guard: () => false },
275
+ SUBMIT: { target: "submitting", guard: () => false },
276
276
  },
277
277
  },
278
278
  submitting: {},
279
279
  },
280
280
  })
281
281
  // can() returns true because the event exists, even though guard would fail
282
- expect(m.can('SUBMIT')).toBe(true)
282
+ expect(m.can("SUBMIT")).toBe(true)
283
283
  })
284
284
  })
285
285
 
286
286
  // ─── nextEvents ──────────────────────────────────────────────────────
287
287
 
288
- describe('nextEvents()', () => {
289
- it('returns available events from current state', () => {
288
+ describe("nextEvents()", () => {
289
+ it("returns available events from current state", () => {
290
290
  const m = createMachine({
291
- initial: 'idle',
291
+ initial: "idle",
292
292
  states: {
293
- idle: { on: { FETCH: 'loading', RESET: 'idle' } },
294
- loading: { on: { SUCCESS: 'done', ERROR: 'error' } },
293
+ idle: { on: { FETCH: "loading", RESET: "idle" } },
294
+ loading: { on: { SUCCESS: "done", ERROR: "error" } },
295
295
  done: {},
296
296
  error: {},
297
297
  },
298
298
  })
299
- expect(m.nextEvents()).toEqual(expect.arrayContaining(['FETCH', 'RESET']))
299
+ expect(m.nextEvents()).toEqual(expect.arrayContaining(["FETCH", "RESET"]))
300
300
  })
301
301
 
302
- it('returns empty array for final states', () => {
302
+ it("returns empty array for final states", () => {
303
303
  const m = createMachine({
304
- initial: 'idle',
304
+ initial: "idle",
305
305
  states: {
306
- idle: { on: { DONE: 'finished' } },
306
+ idle: { on: { DONE: "finished" } },
307
307
  finished: {},
308
308
  },
309
309
  })
310
- m.send('DONE')
310
+ m.send("DONE")
311
311
  expect(m.nextEvents()).toEqual([])
312
312
  })
313
313
 
314
- it('is reactive', () => {
314
+ it("is reactive", () => {
315
315
  const m = createMachine({
316
- initial: 'idle',
316
+ initial: "idle",
317
317
  states: {
318
- idle: { on: { START: 'running' } },
319
- running: { on: { STOP: 'idle', PAUSE: 'paused' } },
320
- paused: { on: { RESUME: 'running' } },
318
+ idle: { on: { START: "running" } },
319
+ running: { on: { STOP: "idle", PAUSE: "paused" } },
320
+ paused: { on: { RESUME: "running" } },
321
321
  },
322
322
  })
323
323
  const results: string[][] = []
@@ -326,40 +326,40 @@ describe('createMachine', () => {
326
326
  results.push(m.nextEvents())
327
327
  })
328
328
 
329
- m.send('START')
329
+ m.send("START")
330
330
  expect(results).toHaveLength(2)
331
- expect(results[1]).toEqual(expect.arrayContaining(['STOP', 'PAUSE']))
331
+ expect(results[1]).toEqual(expect.arrayContaining(["STOP", "PAUSE"]))
332
332
  })
333
333
  })
334
334
 
335
335
  // ─── reset ───────────────────────────────────────────────────────────
336
336
 
337
- describe('reset()', () => {
338
- it('returns to initial state', () => {
337
+ describe("reset()", () => {
338
+ it("returns to initial state", () => {
339
339
  const m = createMachine({
340
- initial: 'idle',
340
+ initial: "idle",
341
341
  states: {
342
- idle: { on: { START: 'running' } },
343
- running: { on: { STOP: 'idle' } },
342
+ idle: { on: { START: "running" } },
343
+ running: { on: { STOP: "idle" } },
344
344
  },
345
345
  })
346
- m.send('START')
347
- expect(m()).toBe('running')
346
+ m.send("START")
347
+ expect(m()).toBe("running")
348
348
 
349
349
  m.reset()
350
- expect(m()).toBe('idle')
350
+ expect(m()).toBe("idle")
351
351
  })
352
352
  })
353
353
 
354
354
  // ─── Reactivity ────────────────────────────────────────────────────
355
355
 
356
- describe('reactivity', () => {
357
- it('machine() is reactive in effect', () => {
356
+ describe("reactivity", () => {
357
+ it("machine() is reactive in effect", () => {
358
358
  const m = createMachine({
359
- initial: 'a',
359
+ initial: "a",
360
360
  states: {
361
- a: { on: { NEXT: 'b' } },
362
- b: { on: { NEXT: 'c' } },
361
+ a: { on: { NEXT: "b" } },
362
+ b: { on: { NEXT: "c" } },
363
363
  c: {},
364
364
  },
365
365
  })
@@ -369,141 +369,141 @@ describe('createMachine', () => {
369
369
  states.push(m())
370
370
  })
371
371
 
372
- m.send('NEXT')
373
- m.send('NEXT')
372
+ m.send("NEXT")
373
+ m.send("NEXT")
374
374
 
375
- expect(states).toEqual(['a', 'b', 'c'])
375
+ expect(states).toEqual(["a", "b", "c"])
376
376
  })
377
377
 
378
- it('machine() is reactive in computed', () => {
378
+ it("machine() is reactive in computed", () => {
379
379
  const m = createMachine({
380
- initial: 'idle',
380
+ initial: "idle",
381
381
  states: {
382
- idle: { on: { LOAD: 'loading' } },
383
- loading: { on: { DONE: 'idle' } },
382
+ idle: { on: { LOAD: "loading" } },
383
+ loading: { on: { DONE: "idle" } },
384
384
  },
385
385
  })
386
386
 
387
- const isLoading = computed(() => m() === 'loading')
387
+ const isLoading = computed(() => m() === "loading")
388
388
  expect(isLoading()).toBe(false)
389
389
 
390
- m.send('LOAD')
390
+ m.send("LOAD")
391
391
  expect(isLoading()).toBe(true)
392
392
 
393
- m.send('DONE')
393
+ m.send("DONE")
394
394
  expect(isLoading()).toBe(false)
395
395
  })
396
396
  })
397
397
 
398
398
  // ─── onEnter ─────────────────────────────────────────────────────────
399
399
 
400
- describe('onEnter()', () => {
401
- it('fires when entering a state', () => {
400
+ describe("onEnter()", () => {
401
+ it("fires when entering a state", () => {
402
402
  const m = createMachine({
403
- initial: 'idle',
403
+ initial: "idle",
404
404
  states: {
405
- idle: { on: { LOAD: 'loading' } },
406
- loading: { on: { DONE: 'idle' } },
405
+ idle: { on: { LOAD: "loading" } },
406
+ loading: { on: { DONE: "idle" } },
407
407
  },
408
408
  })
409
409
  const entered: string[] = []
410
410
 
411
- m.onEnter('loading', (event) => {
411
+ m.onEnter("loading", (event) => {
412
412
  entered.push(event.type)
413
413
  })
414
414
 
415
- m.send('LOAD')
416
- expect(entered).toEqual(['LOAD'])
415
+ m.send("LOAD")
416
+ expect(entered).toEqual(["LOAD"])
417
417
  })
418
418
 
419
- it('does not fire for other states', () => {
419
+ it("does not fire for other states", () => {
420
420
  const m = createMachine({
421
- initial: 'a',
421
+ initial: "a",
422
422
  states: {
423
- a: { on: { GO: 'b' } },
424
- b: { on: { GO: 'c' } },
423
+ a: { on: { GO: "b" } },
424
+ b: { on: { GO: "c" } },
425
425
  c: {},
426
426
  },
427
427
  })
428
428
  const fn = vi.fn()
429
429
 
430
- m.onEnter('c', fn)
431
- m.send('GO') // a → b
430
+ m.onEnter("c", fn)
431
+ m.send("GO") // a → b
432
432
  expect(fn).not.toHaveBeenCalled()
433
433
 
434
- m.send('GO') // b → c
434
+ m.send("GO") // b → c
435
435
  expect(fn).toHaveBeenCalledOnce()
436
436
  })
437
437
 
438
- it('receives event payload', () => {
438
+ it("receives event payload", () => {
439
439
  const m = createMachine({
440
- initial: 'idle',
440
+ initial: "idle",
441
441
  states: {
442
- idle: { on: { SELECT: 'selected' } },
442
+ idle: { on: { SELECT: "selected" } },
443
443
  selected: {},
444
444
  },
445
445
  })
446
446
  let received: unknown = null
447
447
 
448
- m.onEnter('selected', (event) => {
448
+ m.onEnter("selected", (event) => {
449
449
  received = event.payload
450
450
  })
451
451
 
452
- m.send('SELECT', { id: 42 })
452
+ m.send("SELECT", { id: 42 })
453
453
  expect(received).toEqual({ id: 42 })
454
454
  })
455
455
 
456
- it('fires on self-transitions', () => {
456
+ it("fires on self-transitions", () => {
457
457
  const m = createMachine({
458
- initial: 'counting',
458
+ initial: "counting",
459
459
  states: {
460
- counting: { on: { INC: 'counting' } },
460
+ counting: { on: { INC: "counting" } },
461
461
  },
462
462
  })
463
463
  const fn = vi.fn()
464
464
 
465
- m.onEnter('counting', fn)
466
- m.send('INC')
467
- m.send('INC')
465
+ m.onEnter("counting", fn)
466
+ m.send("INC")
467
+ m.send("INC")
468
468
 
469
469
  expect(fn).toHaveBeenCalledTimes(2)
470
470
  })
471
471
 
472
- it('returns unsubscribe function', () => {
472
+ it("returns unsubscribe function", () => {
473
473
  const m = createMachine({
474
- initial: 'a',
474
+ initial: "a",
475
475
  states: {
476
- a: { on: { GO: 'b' } },
477
- b: { on: { GO: 'a' } },
476
+ a: { on: { GO: "b" } },
477
+ b: { on: { GO: "a" } },
478
478
  },
479
479
  })
480
480
  const fn = vi.fn()
481
481
 
482
- const unsub = m.onEnter('b', fn)
483
- m.send('GO') // a → b
482
+ const unsub = m.onEnter("b", fn)
483
+ m.send("GO") // a → b
484
484
  expect(fn).toHaveBeenCalledOnce()
485
485
 
486
486
  unsub()
487
- m.send('GO') // b → a
488
- m.send('GO') // a → b again
487
+ m.send("GO") // b → a
488
+ m.send("GO") // a → b again
489
489
  expect(fn).toHaveBeenCalledOnce() // not called again
490
490
  })
491
491
 
492
- it('multiple listeners for same state', () => {
492
+ it("multiple listeners for same state", () => {
493
493
  const m = createMachine({
494
- initial: 'idle',
494
+ initial: "idle",
495
495
  states: {
496
- idle: { on: { GO: 'active' } },
496
+ idle: { on: { GO: "active" } },
497
497
  active: {},
498
498
  },
499
499
  })
500
500
  const fn1 = vi.fn()
501
501
  const fn2 = vi.fn()
502
502
 
503
- m.onEnter('active', fn1)
504
- m.onEnter('active', fn2)
503
+ m.onEnter("active", fn1)
504
+ m.onEnter("active", fn2)
505
505
 
506
- m.send('GO')
506
+ m.send("GO")
507
507
  expect(fn1).toHaveBeenCalledOnce()
508
508
  expect(fn2).toHaveBeenCalledOnce()
509
509
  })
@@ -511,13 +511,13 @@ describe('createMachine', () => {
511
511
 
512
512
  // ─── onTransition ────────────────────────────────────────────────────
513
513
 
514
- describe('onTransition()', () => {
515
- it('fires on every transition', () => {
514
+ describe("onTransition()", () => {
515
+ it("fires on every transition", () => {
516
516
  const m = createMachine({
517
- initial: 'a',
517
+ initial: "a",
518
518
  states: {
519
- a: { on: { NEXT: 'b' } },
520
- b: { on: { NEXT: 'c' } },
519
+ a: { on: { NEXT: "b" } },
520
+ b: { on: { NEXT: "c" } },
521
521
  c: {},
522
522
  },
523
523
  })
@@ -527,70 +527,70 @@ describe('createMachine', () => {
527
527
  transitions.push([from, to, event.type])
528
528
  })
529
529
 
530
- m.send('NEXT')
531
- m.send('NEXT')
530
+ m.send("NEXT")
531
+ m.send("NEXT")
532
532
 
533
533
  expect(transitions).toEqual([
534
- ['a', 'b', 'NEXT'],
535
- ['b', 'c', 'NEXT'],
534
+ ["a", "b", "NEXT"],
535
+ ["b", "c", "NEXT"],
536
536
  ])
537
537
  })
538
538
 
539
- it('does not fire when event is ignored', () => {
539
+ it("does not fire when event is ignored", () => {
540
540
  const m = createMachine({
541
- initial: 'idle',
541
+ initial: "idle",
542
542
  states: {
543
- idle: { on: { START: 'running' } },
543
+ idle: { on: { START: "running" } },
544
544
  running: {},
545
545
  },
546
546
  })
547
547
  const fn = vi.fn()
548
548
 
549
549
  m.onTransition(fn)
550
- m.send('STOP' as any) // invalid event
550
+ m.send("STOP" as any) // invalid event
551
551
  expect(fn).not.toHaveBeenCalled()
552
552
  })
553
553
 
554
- it('returns unsubscribe function', () => {
554
+ it("returns unsubscribe function", () => {
555
555
  const m = createMachine({
556
- initial: 'a',
556
+ initial: "a",
557
557
  states: {
558
- a: { on: { GO: 'b' } },
559
- b: { on: { GO: 'a' } },
558
+ a: { on: { GO: "b" } },
559
+ b: { on: { GO: "a" } },
560
560
  },
561
561
  })
562
562
  const fn = vi.fn()
563
563
 
564
564
  const unsub = m.onTransition(fn)
565
- m.send('GO')
565
+ m.send("GO")
566
566
  expect(fn).toHaveBeenCalledOnce()
567
567
 
568
568
  unsub()
569
- m.send('GO')
569
+ m.send("GO")
570
570
  expect(fn).toHaveBeenCalledOnce() // not called again
571
571
  })
572
572
  })
573
573
 
574
574
  // ─── dispose ─────────────────────────────────────────────────────────
575
575
 
576
- describe('dispose()', () => {
577
- it('removes all listeners', () => {
576
+ describe("dispose()", () => {
577
+ it("removes all listeners", () => {
578
578
  const m = createMachine({
579
- initial: 'a',
579
+ initial: "a",
580
580
  states: {
581
- a: { on: { GO: 'b' } },
582
- b: { on: { GO: 'a' } },
581
+ a: { on: { GO: "b" } },
582
+ b: { on: { GO: "a" } },
583
583
  },
584
584
  })
585
585
  const enterFn = vi.fn()
586
586
  const transitionFn = vi.fn()
587
587
 
588
- m.onEnter('b', enterFn)
588
+ m.onEnter("b", enterFn)
589
589
  m.onTransition(transitionFn)
590
590
 
591
591
  m.dispose()
592
592
 
593
- m.send('GO')
593
+ m.send("GO")
594
594
  expect(enterFn).not.toHaveBeenCalled()
595
595
  expect(transitionFn).not.toHaveBeenCalled()
596
596
  })
@@ -598,139 +598,139 @@ describe('createMachine', () => {
598
598
 
599
599
  // ─── Real-world patterns ───────────────────────────────────────────
600
600
 
601
- describe('real-world patterns', () => {
602
- it('multi-step wizard', () => {
601
+ describe("real-world patterns", () => {
602
+ it("multi-step wizard", () => {
603
603
  const m = createMachine({
604
- initial: 'step1',
604
+ initial: "step1",
605
605
  states: {
606
- step1: { on: { NEXT: 'step2' } },
607
- step2: { on: { NEXT: 'step3', BACK: 'step1' } },
608
- step3: { on: { SUBMIT: 'submitting', BACK: 'step2' } },
609
- submitting: { on: { SUCCESS: 'done', ERROR: 'step3' } },
606
+ step1: { on: { NEXT: "step2" } },
607
+ step2: { on: { NEXT: "step3", BACK: "step1" } },
608
+ step3: { on: { SUBMIT: "submitting", BACK: "step2" } },
609
+ submitting: { on: { SUCCESS: "done", ERROR: "step3" } },
610
610
  done: {},
611
611
  },
612
612
  })
613
613
 
614
- m.send('NEXT') // step1 → step2
615
- m.send('NEXT') // step2 → step3
616
- expect(m()).toBe('step3')
614
+ m.send("NEXT") // step1 → step2
615
+ m.send("NEXT") // step2 → step3
616
+ expect(m()).toBe("step3")
617
617
 
618
- m.send('BACK') // step3 → step2
619
- expect(m()).toBe('step2')
618
+ m.send("BACK") // step3 → step2
619
+ expect(m()).toBe("step2")
620
620
 
621
- m.send('NEXT') // step2 → step3
622
- m.send('SUBMIT') // step3 → submitting
623
- expect(m()).toBe('submitting')
621
+ m.send("NEXT") // step2 → step3
622
+ m.send("SUBMIT") // step3 → submitting
623
+ expect(m()).toBe("submitting")
624
624
 
625
- m.send('SUCCESS')
626
- expect(m()).toBe('done')
625
+ m.send("SUCCESS")
626
+ expect(m()).toBe("done")
627
627
 
628
628
  // Final state — no more transitions
629
- m.send('SUBMIT')
630
- expect(m()).toBe('done')
629
+ m.send("SUBMIT")
630
+ expect(m()).toBe("done")
631
631
  })
632
632
 
633
- it('async fetch with onEnter', () => {
633
+ it("async fetch with onEnter", () => {
634
634
  const m = createMachine({
635
- initial: 'idle',
635
+ initial: "idle",
636
636
  states: {
637
- idle: { on: { FETCH: 'loading' } },
638
- loading: { on: { SUCCESS: 'done', ERROR: 'error' } },
639
- done: { on: { REFETCH: 'loading' } },
640
- error: { on: { RETRY: 'loading' } },
637
+ idle: { on: { FETCH: "loading" } },
638
+ loading: { on: { SUCCESS: "done", ERROR: "error" } },
639
+ done: { on: { REFETCH: "loading" } },
640
+ error: { on: { RETRY: "loading" } },
641
641
  },
642
642
  })
643
643
 
644
644
  const data = signal<string | null>(null)
645
645
 
646
- m.onEnter('loading', () => {
646
+ m.onEnter("loading", () => {
647
647
  // Simulate async — in real code this would be an API call
648
- data.set('loaded data')
649
- m.send('SUCCESS')
648
+ data.set("loaded data")
649
+ m.send("SUCCESS")
650
650
  })
651
651
 
652
- m.send('FETCH')
653
- expect(m()).toBe('done')
654
- expect(data()).toBe('loaded data')
652
+ m.send("FETCH")
653
+ expect(m()).toBe("done")
654
+ expect(data()).toBe("loaded data")
655
655
  })
656
656
 
657
- it('toggle with reactive UI', () => {
657
+ it("toggle with reactive UI", () => {
658
658
  const m = createMachine({
659
- initial: 'off',
659
+ initial: "off",
660
660
  states: {
661
- off: { on: { TOGGLE: 'on' } },
662
- on: { on: { TOGGLE: 'off' } },
661
+ off: { on: { TOGGLE: "on" } },
662
+ on: { on: { TOGGLE: "off" } },
663
663
  },
664
664
  })
665
665
 
666
666
  const labels: string[] = []
667
667
  effect(() => {
668
- labels.push(m.matches('on') ? 'ON' : 'OFF')
668
+ labels.push(m.matches("on") ? "ON" : "OFF")
669
669
  })
670
670
 
671
- m.send('TOGGLE')
672
- m.send('TOGGLE')
673
- m.send('TOGGLE')
671
+ m.send("TOGGLE")
672
+ m.send("TOGGLE")
673
+ m.send("TOGGLE")
674
674
 
675
- expect(labels).toEqual(['OFF', 'ON', 'OFF', 'ON'])
675
+ expect(labels).toEqual(["OFF", "ON", "OFF", "ON"])
676
676
  })
677
677
 
678
- it('form with validation guard', () => {
678
+ it("form with validation guard", () => {
679
679
  const isValid = signal(false)
680
680
 
681
681
  const m = createMachine({
682
- initial: 'editing',
682
+ initial: "editing",
683
683
  states: {
684
684
  editing: {
685
685
  on: {
686
686
  SUBMIT: {
687
- target: 'submitting',
687
+ target: "submitting",
688
688
  guard: () => isValid.peek(),
689
689
  },
690
690
  },
691
691
  },
692
- submitting: { on: { SUCCESS: 'done', ERROR: 'editing' } },
692
+ submitting: { on: { SUCCESS: "done", ERROR: "editing" } },
693
693
  done: {},
694
694
  },
695
695
  })
696
696
 
697
- m.send('SUBMIT') // guard fails
698
- expect(m()).toBe('editing')
697
+ m.send("SUBMIT") // guard fails
698
+ expect(m()).toBe("editing")
699
699
 
700
700
  isValid.set(true)
701
- m.send('SUBMIT') // guard passes
702
- expect(m()).toBe('submitting')
701
+ m.send("SUBMIT") // guard passes
702
+ expect(m()).toBe("submitting")
703
703
  })
704
704
 
705
- it('player with pause/resume', () => {
705
+ it("player with pause/resume", () => {
706
706
  const m = createMachine({
707
- initial: 'stopped',
707
+ initial: "stopped",
708
708
  states: {
709
- stopped: { on: { PLAY: 'playing' } },
710
- playing: { on: { PAUSE: 'paused', STOP: 'stopped' } },
711
- paused: { on: { PLAY: 'playing', STOP: 'stopped' } },
709
+ stopped: { on: { PLAY: "playing" } },
710
+ playing: { on: { PAUSE: "paused", STOP: "stopped" } },
711
+ paused: { on: { PLAY: "playing", STOP: "stopped" } },
712
712
  },
713
713
  })
714
714
 
715
- m.send('PLAY')
716
- expect(m()).toBe('playing')
715
+ m.send("PLAY")
716
+ expect(m()).toBe("playing")
717
717
 
718
- m.send('PAUSE')
719
- expect(m()).toBe('paused')
718
+ m.send("PAUSE")
719
+ expect(m()).toBe("paused")
720
720
 
721
- m.send('PLAY')
722
- expect(m()).toBe('playing')
721
+ m.send("PLAY")
722
+ expect(m()).toBe("playing")
723
723
 
724
- m.send('STOP')
725
- expect(m()).toBe('stopped')
724
+ m.send("STOP")
725
+ expect(m()).toBe("stopped")
726
726
  })
727
727
 
728
- it('analytics tracking via onTransition', () => {
728
+ it("analytics tracking via onTransition", () => {
729
729
  const m = createMachine({
730
- initial: 'step1',
730
+ initial: "step1",
731
731
  states: {
732
- step1: { on: { NEXT: 'step2' } },
733
- step2: { on: { NEXT: 'step3' } },
732
+ step1: { on: { NEXT: "step2" } },
733
+ step2: { on: { NEXT: "step3" } },
734
734
  step3: {},
735
735
  },
736
736
  })
@@ -740,27 +740,27 @@ describe('createMachine', () => {
740
740
  tracked.push(`${from} → ${to}`)
741
741
  })
742
742
 
743
- m.send('NEXT')
744
- m.send('NEXT')
743
+ m.send("NEXT")
744
+ m.send("NEXT")
745
745
 
746
- expect(tracked).toEqual(['step1 → step2', 'step2 → step3'])
746
+ expect(tracked).toEqual(["step1 → step2", "step2 → step3"])
747
747
  })
748
748
 
749
- it('reusable machine definition', () => {
749
+ it("reusable machine definition", () => {
750
750
  const toggleDef = {
751
- initial: 'off' as const,
751
+ initial: "off" as const,
752
752
  states: {
753
- off: { on: { TOGGLE: 'on' as const } },
754
- on: { on: { TOGGLE: 'off' as const } },
753
+ off: { on: { TOGGLE: "on" as const } },
754
+ on: { on: { TOGGLE: "off" as const } },
755
755
  },
756
756
  }
757
757
 
758
758
  const m1 = createMachine(toggleDef)
759
759
  const m2 = createMachine(toggleDef)
760
760
 
761
- m1.send('TOGGLE')
762
- expect(m1()).toBe('on')
763
- expect(m2()).toBe('off') // independent instance
761
+ m1.send("TOGGLE")
762
+ expect(m1()).toBe("on")
763
+ expect(m2()).toBe("off") // independent instance
764
764
  })
765
765
  })
766
766
  })
package/src/types.ts CHANGED
@@ -31,17 +31,16 @@ export interface MachineEvent<TEvent extends string = string> {
31
31
  /**
32
32
  * Callback for onEnter — receives the event that caused the transition.
33
33
  */
34
- export type EnterCallback<TEvent extends string = string> = (
35
- event: MachineEvent<TEvent>,
36
- ) => void
34
+ export type EnterCallback<TEvent extends string = string> = (event: MachineEvent<TEvent>) => void
37
35
 
38
36
  /**
39
37
  * Callback for onTransition — receives from state, to state, and the event.
40
38
  */
41
- export type TransitionCallback<
42
- TState extends string = string,
43
- TEvent extends string = string,
44
- > = (from: TState, to: TState, event: MachineEvent<TEvent>) => void
39
+ export type TransitionCallback<TState extends string = string, TEvent extends string = string> = (
40
+ from: TState,
41
+ to: TState,
42
+ event: MachineEvent<TEvent>,
43
+ ) => void
45
44
 
46
45
  /**
47
46
  * The machine instance returned by `createMachine()`.
@@ -78,9 +77,7 @@ export interface Machine<TState extends string, TEvent extends string> {
78
77
  // ─── Type inference helpers ──────────────────────────────────────────────────
79
78
 
80
79
  /** Extract state names from a machine config */
81
- export type InferStates<T> = T extends { states: Record<infer S, unknown> }
82
- ? S & string
83
- : never
80
+ export type InferStates<T> = T extends { states: Record<infer S, unknown> } ? S & string : never
84
81
 
85
82
  /** Extract event names from a machine config */
86
83
  export type InferEvents<T> = T extends {