aifsmjs 0.5.8 → 0.6.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.
Files changed (70) hide show
  1. package/README.md +14 -7
  2. package/README_ZHTW.md +14 -7
  3. package/dist/{chunk-ZLQ7HZCE.js → chunk-D6H64FSI.js} +66 -46
  4. package/dist/chunk-D6H64FSI.js.map +1 -0
  5. package/dist/{chunk-NEJYZAKR.js → chunk-Q45LGXHO.js} +3 -3
  6. package/dist/{chunk-NEJYZAKR.js.map → chunk-Q45LGXHO.js.map} +1 -1
  7. package/dist/{chunk-CDK25FTD.cjs → chunk-QCTA2X4J.cjs} +7 -3
  8. package/dist/chunk-QCTA2X4J.cjs.map +1 -0
  9. package/dist/{chunk-A7U7QQL5.js → chunk-SSNKGEVB.js} +7 -4
  10. package/dist/chunk-SSNKGEVB.js.map +1 -0
  11. package/dist/{chunk-I354FONA.cjs → chunk-VGLF5NQH.cjs} +69 -46
  12. package/dist/chunk-VGLF5NQH.cjs.map +1 -0
  13. package/dist/{chunk-FHTQ7LSQ.cjs → chunk-VV5TKFQO.cjs} +4 -4
  14. package/dist/{chunk-FHTQ7LSQ.cjs.map → chunk-VV5TKFQO.cjs.map} +1 -1
  15. package/dist/{chunk-VZCSHTOI.js → chunk-XA24A7VP.js} +156 -157
  16. package/dist/chunk-XA24A7VP.js.map +1 -0
  17. package/dist/{chunk-2MME5V4F.cjs → chunk-YK25NVFC.cjs} +160 -161
  18. package/dist/chunk-YK25NVFC.cjs.map +1 -0
  19. package/dist/effects/index.cjs +3 -4
  20. package/dist/effects/index.cjs.map +1 -1
  21. package/dist/effects/index.d.cts +1 -1
  22. package/dist/effects/index.d.ts +1 -1
  23. package/dist/effects/index.js +2 -3
  24. package/dist/effects/index.js.map +1 -1
  25. package/dist/guards/index.cjs +5 -6
  26. package/dist/guards/index.cjs.map +1 -1
  27. package/dist/guards/index.d.cts +1 -1
  28. package/dist/guards/index.d.ts +1 -1
  29. package/dist/guards/index.js +2 -3
  30. package/dist/guards/index.js.map +1 -1
  31. package/dist/index.cjs +31 -28
  32. package/dist/index.d.cts +66 -18
  33. package/dist/index.d.ts +66 -18
  34. package/dist/index.js +3 -4
  35. package/dist/inspect/index.cjs +0 -2
  36. package/dist/inspect/index.cjs.map +1 -1
  37. package/dist/inspect/index.d.cts +1 -1
  38. package/dist/inspect/index.d.ts +1 -1
  39. package/dist/inspect/index.js +0 -2
  40. package/dist/inspect/index.js.map +1 -1
  41. package/dist/pbt/index.cjs +69 -48
  42. package/dist/pbt/index.cjs.map +1 -1
  43. package/dist/pbt/index.d.cts +29 -16
  44. package/dist/pbt/index.d.ts +29 -16
  45. package/dist/pbt/index.js +61 -40
  46. package/dist/pbt/index.js.map +1 -1
  47. package/dist/replay/index.cjs +4 -5
  48. package/dist/replay/index.d.cts +1 -1
  49. package/dist/replay/index.d.ts +1 -1
  50. package/dist/replay/index.js +3 -4
  51. package/dist/timer/index.cjs +18 -5
  52. package/dist/timer/index.cjs.map +1 -1
  53. package/dist/timer/index.d.cts +6 -0
  54. package/dist/timer/index.d.ts +6 -0
  55. package/dist/timer/index.js +18 -5
  56. package/dist/timer/index.js.map +1 -1
  57. package/dist/{types-DIM7QTtf.d.ts → types-CrDxFfBx.d.cts} +64 -16
  58. package/dist/{types-DIM7QTtf.d.cts → types-CrDxFfBx.d.ts} +64 -16
  59. package/llms-full.txt +89 -17
  60. package/package.json +57 -22
  61. package/dist/chunk-2MME5V4F.cjs.map +0 -1
  62. package/dist/chunk-A7U7QQL5.js.map +0 -1
  63. package/dist/chunk-CDK25FTD.cjs.map +0 -1
  64. package/dist/chunk-I354FONA.cjs.map +0 -1
  65. package/dist/chunk-PZ5AY32C.js +0 -9
  66. package/dist/chunk-PZ5AY32C.js.map +0 -1
  67. package/dist/chunk-Q7SFCCGT.cjs +0 -11
  68. package/dist/chunk-Q7SFCCGT.cjs.map +0 -1
  69. package/dist/chunk-VZCSHTOI.js.map +0 -1
  70. package/dist/chunk-ZLQ7HZCE.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/timer/scheduler.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["export type AfterHandle = Readonly<{\n cancel(): void;\n}>;\n\nexport type SetTimeoutFn = (fn: () => void, ms: number) => unknown;\nexport type ClearTimeoutFn = (handle: unknown) => void;\n\nexport type AfterOptions = Readonly<{\n /**\n * If supplied and aborted, the callback never runs and any pending timer is\n * cleared. Aborting after fire is a no-op.\n */\n signal?: AbortSignal;\n /**\n * Override `setTimeout` (testing, SSR, custom loops). Defaults to globalThis.\n */\n setTimeout?: SetTimeoutFn;\n /**\n * Override `clearTimeout`. Must match the `setTimeout` you injected.\n */\n clearTimeout?: ClearTimeoutFn;\n}>;\n\nconst NOOP: AfterHandle = Object.freeze({ cancel: () => {} });\n\nfunction resolveTimers(opts: AfterOptions | undefined): {\n st: SetTimeoutFn;\n ct: ClearTimeoutFn;\n} {\n return {\n st: opts?.setTimeout ?? ((fn, ms) => globalThis.setTimeout(fn, ms)),\n ct: opts?.clearTimeout ?? ((h) => globalThis.clearTimeout(h as number)),\n };\n}\n\n/**\n * Schedule `fn` to run after `ms` milliseconds. Returns a handle whose\n * `cancel()` clears the pending timer. Optional `signal` aborts the timer when\n * triggered. Aborting after the callback fires is a no-op.\n *\n * The abort listener is registered with `{ once: true }` as a baseline, but\n * `{ once: true }` alone does NOT prevent listener accumulation when the same\n * signal is reused across many timers: it only removes the listener when the\n * signal aborts, not when the timer fires normally or `cancel()` is called.\n * We therefore explicitly call `signal.removeEventListener(\"abort\", cancel)`\n * inside the fire callback and at the end of `cancel()` so that a shared,\n * long-lived signal never accumulates dead listeners across timer reuse.\n */\nexport function after(ms: number, fn: () => void, opts?: AfterOptions): AfterHandle {\n if (opts?.signal?.aborted) return NOOP;\n\n const { st, ct } = resolveTimers(opts);\n let fired = false;\n let cancelled = false;\n // `cancel` and the timer handle reference each other. A const cell holds the\n // handle so `cancel` can be defined BEFORE `st(...)` runs (letting a custom\n // `st` that fires its callback synchronously reference `cancel` without\n // hitting the temporal-dead-zone) while still being able to clear the handle\n // assigned afterwards. A synchronous fire sets `fired=true`, so cancel() never\n // reads the still-unset handle in that path.\n const timer: { handle?: ReturnType<typeof st> } = {};\n\n const cancel = () => {\n if (fired || cancelled) return;\n cancelled = true;\n if (timer.handle !== undefined) ct(timer.handle);\n // Detach the abort listener so a reused signal does not accumulate dead\n // closures after this timer is cancelled.\n if (opts?.signal) opts.signal.removeEventListener(\"abort\", cancel);\n };\n\n timer.handle = st(() => {\n fired = true;\n /* v8 ignore next — defensive race guard: cancel() sets cancelled=true and clears the timer, but if a custom setTimeout fires after clear, this short-circuits fn(). */\n if (cancelled) return;\n // Detach the abort listener now that the timer has fired — the listener\n // will never be invoked and must not accumulate on a reused signal.\n if (opts?.signal) opts.signal.removeEventListener(\"abort\", cancel);\n fn();\n }, ms);\n\n // Attach only if the timer has not already fired synchronously (a custom `st`\n // may fire inline); otherwise the listener would be registered AFTER the fire\n // path's removal ran and would then leak until the signal aborts.\n if (opts?.signal && !fired) {\n opts.signal.addEventListener(\"abort\", cancel, { once: true });\n }\n\n return Object.freeze({ cancel });\n}\n\nexport type Scheduler = Readonly<{\n after(ms: number, fn: () => void, opts?: AfterOptions): AfterHandle;\n cancelAll(): void;\n readonly size: number;\n}>;\n\n/**\n * Build a scheduler that tracks every pending `after()` so they can be\n * cancelled together (e.g. on machine destroy). Each `after` returns a handle\n * whose `cancel()` also removes it from the tracking set.\n *\n * `defaults` are merged into every call — typically you inject `setTimeout` /\n * `clearTimeout` once at construction.\n */\nexport function createScheduler(defaults?: AfterOptions): Scheduler {\n const pending = new Set<AfterHandle>();\n\n const sched: Scheduler = {\n after(ms, fn, opts) {\n const merged: AfterOptions = { ...defaults, ...opts };\n // Signal handling is lifted to the scheduler layer: we own one abort\n // listener per timer and route it through the scheduler-level cancel so\n // the abort path also removes the handle from `pending`. The inner\n // after() therefore must NOT see the signal — otherwise it would clear\n // its timer on abort without ever touching `pending`, leaking the entry\n // (FSM-R-01, path a).\n const { signal, ...innerOpts } = merged;\n\n // Path b: scheduling on an already-aborted signal must not grow the Set.\n // after() returns NOOP in that case; tracking it would be a permanent\n // dead entry. Return the same NOOP without adding.\n if (signal?.aborted) return NOOP;\n\n // Forward-reference slot so `wrapped`/`cancel` can find the tracked\n // handle before it is constructed below.\n const slot: { ref?: AfterHandle } = {};\n let fired = false;\n let settled = false; // true once removed from pending (fire or cancel)\n\n const detachAbort = () => {\n if (signal) signal.removeEventListener(\"abort\", onAbort);\n };\n\n // Single removal path shared by fire, explicit cancel, and abort.\n const settle = () => {\n if (settled) return;\n settled = true;\n if (slot.ref) pending.delete(slot.ref);\n detachAbort();\n };\n\n const wrapped = () => {\n fired = true;\n settle();\n fn();\n };\n\n // Scheduler-level cancel: cancel the inner timer AND drop from pending\n // AND detach the abort listener. Registered on the abort path too.\n const cancel = () => {\n inner.cancel();\n settle();\n };\n function onAbort() {\n cancel();\n }\n\n const inner = after(ms, wrapped, innerOpts);\n\n const handle: AfterHandle = Object.freeze({ cancel });\n slot.ref = handle;\n\n // Path c (sync-fire): a custom setTimeout may fire `wrapped` inline,\n // during the after() call above — before we reach here. In that case the\n // timer is already done; adding it now would leave a permanent dead\n // entry. Only track timers that are still live.\n if (!fired) {\n pending.add(handle);\n // Attach the abort listener only for a live timer on a real signal.\n // { once: true } removes it on abort; settle()/detachAbort() remove it\n // on fire/cancel so a long-lived shared signal never accumulates dead\n // listeners.\n if (signal) signal.addEventListener(\"abort\", onAbort, { once: true });\n }\n\n return handle;\n },\n cancelAll() {\n for (const h of pending) h.cancel();\n pending.clear();\n },\n get size() {\n return pending.size;\n },\n };\n return sched;\n}\n"]}
1
+ {"version":3,"sources":["../../src/timer/scheduler.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["export type AfterHandle = Readonly<{\n cancel(): void;\n}>;\n\nexport type SetTimeoutFn = (fn: () => void, ms: number) => unknown;\nexport type ClearTimeoutFn = (handle: unknown) => void;\n\nexport type AfterOptions = Readonly<{\n /**\n * If supplied and aborted, the callback never runs and any pending timer is\n * cleared. Aborting after fire is a no-op.\n */\n signal?: AbortSignal;\n /**\n * Override `setTimeout` (testing, SSR, custom loops). Defaults to globalThis.\n */\n setTimeout?: SetTimeoutFn;\n /**\n * Override `clearTimeout`. Must match the `setTimeout` you injected.\n */\n clearTimeout?: ClearTimeoutFn;\n}>;\n\nconst NOOP: AfterHandle = Object.freeze({ cancel: () => {} });\n\n// Largest delay setTimeout honours (2^31-1 ms, about 24.8 days); hosts treat\n// anything larger as ~1 ms. Every delay handed to setTimeout goes through\n// clampDelay (ai*js timer rule; no timer chaining).\nconst MAX_DELAY = 2_147_483_647;\nconst clampDelay = (ms: number): number => Math.min(ms, MAX_DELAY);\n\n// Argument validation shared by after() and createScheduler().after(), run\n// before any side effect. aifsmjs/timer exports no error class, so misuse is\n// a prefixed built-in RangeError / TypeError.\nfunction checkArgs(ms: unknown, fn: unknown): void {\n if (!Number.isFinite(ms) || (ms as number) < 0) {\n throw new RangeError(\"aifsmjs: after() ms must be a finite number >= 0\");\n }\n if (typeof fn !== \"function\") throw new TypeError(\"aifsmjs: after() fn must be a function\");\n}\n\nfunction resolveTimers(opts: AfterOptions | undefined): {\n st: SetTimeoutFn;\n ct: ClearTimeoutFn;\n} {\n return {\n st: opts?.setTimeout ?? ((fn, ms) => globalThis.setTimeout(fn, ms)),\n ct: opts?.clearTimeout ?? ((h) => globalThis.clearTimeout(h as number)),\n };\n}\n\n/**\n * Schedule `fn` to run after `ms` milliseconds. Returns a handle whose\n * `cancel()` clears the pending timer. Optional `signal` aborts the timer when\n * triggered. Aborting after the callback fires is a no-op.\n *\n * `ms` must be a finite number >= 0 (`NaN`, `Infinity`, negatives and\n * non-numbers throw `RangeError`) and `fn` a function (`TypeError`); both are\n * checked before anything else, including an already-aborted `signal`. A\n * finite `ms` above 2^31-1 (about 24.8 days) is clamped to 2^31-1 when handed\n * to `setTimeout`. To mean \"never\", do not schedule.\n *\n * The abort listener is registered with `{ once: true }` as a baseline, but\n * `{ once: true }` alone does NOT prevent listener accumulation when the same\n * signal is reused across many timers: it only removes the listener when the\n * signal aborts, not when the timer fires normally or `cancel()` is called.\n * We therefore explicitly call `signal.removeEventListener(\"abort\", cancel)`\n * inside the fire callback and at the end of `cancel()` so that a shared,\n * long-lived signal never accumulates dead listeners across timer reuse.\n */\nexport function after(ms: number, fn: () => void, opts?: AfterOptions): AfterHandle {\n checkArgs(ms, fn);\n if (opts?.signal?.aborted) return NOOP;\n\n const { st, ct } = resolveTimers(opts);\n let fired = false;\n let cancelled = false;\n // `cancel` and the timer handle reference each other. A const cell holds the\n // handle so `cancel` can be defined BEFORE `st(...)` runs (letting a custom\n // `st` that fires its callback synchronously reference `cancel` without\n // hitting the temporal-dead-zone) while still being able to clear the handle\n // assigned afterwards. A synchronous fire sets `fired=true`, so cancel() never\n // reads the still-unset handle in that path.\n const timer: { handle?: ReturnType<typeof st> } = {};\n\n const cancel = () => {\n if (fired || cancelled) return;\n cancelled = true;\n if (timer.handle !== undefined) ct(timer.handle);\n // Detach the abort listener so a reused signal does not accumulate dead\n // closures after this timer is cancelled.\n if (opts?.signal) opts.signal.removeEventListener(\"abort\", cancel);\n };\n\n timer.handle = st(() => {\n fired = true;\n /* v8 ignore next — defensive race guard: cancel() sets cancelled=true and clears the timer, but if a custom setTimeout fires after clear, this short-circuits fn(). */\n if (cancelled) return;\n // Detach the abort listener now that the timer has fired — the listener\n // will never be invoked and must not accumulate on a reused signal.\n if (opts?.signal) opts.signal.removeEventListener(\"abort\", cancel);\n fn();\n }, clampDelay(ms));\n\n // Attach only if the timer has not already fired synchronously (a custom `st`\n // may fire inline); otherwise the listener would be registered AFTER the fire\n // path's removal ran and would then leak until the signal aborts.\n if (opts?.signal && !fired) {\n opts.signal.addEventListener(\"abort\", cancel, { once: true });\n }\n\n return Object.freeze({ cancel });\n}\n\nexport type Scheduler = Readonly<{\n after(ms: number, fn: () => void, opts?: AfterOptions): AfterHandle;\n cancelAll(): void;\n readonly size: number;\n}>;\n\n/**\n * Build a scheduler that tracks every pending `after()` so they can be\n * cancelled together (e.g. on machine destroy). Each `after` returns a handle\n * whose `cancel()` also removes it from the tracking set.\n *\n * `defaults` are merged into every call — typically you inject `setTimeout` /\n * `clearTimeout` once at construction.\n */\nexport function createScheduler(defaults?: AfterOptions): Scheduler {\n const pending = new Set<AfterHandle>();\n\n const sched: Scheduler = {\n after(ms, fn, opts) {\n // Same validation as after(), before the aborted-signal shortcut and\n // before `pending` is touched.\n checkArgs(ms, fn);\n // Field-by-field merge with `??`: an explicitly-undefined per-call field\n // (common when forwarding optional options in JS, or in TS without\n // exactOptionalPropertyTypes) must fall back to the scheduler's default,\n // not silently win over it the way `{ ...defaults, ...opts }` would.\n // Built with `exactOptionalPropertyTypes` in mind: an option that ends\n // up undefined after the merge is left OUT of the object rather than\n // set to `undefined`, so the AfterOptions type is honoured exactly.\n const signal = opts?.signal ?? defaults?.signal;\n const setTimeoutFn = opts?.setTimeout ?? defaults?.setTimeout;\n const clearTimeoutFn = opts?.clearTimeout ?? defaults?.clearTimeout;\n // Signal handling is lifted to the scheduler layer: we own one abort\n // listener per timer and route it through the scheduler-level cancel so\n // the abort path also removes the handle from `pending`. The inner\n // after() therefore must NOT see the signal — otherwise it would clear\n // its timer on abort without ever touching `pending`, leaking the entry\n // (FSM-R-01, path a).\n const innerOpts: AfterOptions = {\n ...(setTimeoutFn !== undefined && { setTimeout: setTimeoutFn }),\n ...(clearTimeoutFn !== undefined && { clearTimeout: clearTimeoutFn }),\n };\n\n // Path b: scheduling on an already-aborted signal must not grow the Set.\n // after() returns NOOP in that case; tracking it would be a permanent\n // dead entry. Return the same NOOP without adding.\n if (signal?.aborted) return NOOP;\n\n // Forward-reference slot so `wrapped`/`cancel` can find the tracked\n // handle before it is constructed below.\n const slot: { ref?: AfterHandle } = {};\n let fired = false;\n let settled = false; // true once removed from pending (fire or cancel)\n\n const detachAbort = () => {\n if (signal) signal.removeEventListener(\"abort\", onAbort);\n };\n\n // Single removal path shared by fire, explicit cancel, and abort.\n const settle = () => {\n if (settled) return;\n settled = true;\n if (slot.ref) pending.delete(slot.ref);\n detachAbort();\n };\n\n const wrapped = () => {\n fired = true;\n settle();\n fn();\n };\n\n // Scheduler-level cancel: cancel the inner timer AND drop from pending\n // AND detach the abort listener. Registered on the abort path too.\n const cancel = () => {\n inner.cancel();\n settle();\n };\n function onAbort() {\n cancel();\n }\n\n const inner = after(ms, wrapped, innerOpts);\n\n const handle: AfterHandle = Object.freeze({ cancel });\n slot.ref = handle;\n\n // Path c (sync-fire): a custom setTimeout may fire `wrapped` inline,\n // during the after() call above — before we reach here. In that case the\n // timer is already done; adding it now would leave a permanent dead\n // entry. Only track timers that are still live.\n if (!fired) {\n pending.add(handle);\n // Attach the abort listener only for a live timer on a real signal.\n // { once: true } removes it on abort; settle()/detachAbort() remove it\n // on fire/cancel so a long-lived shared signal never accumulates dead\n // listeners.\n if (signal) signal.addEventListener(\"abort\", onAbort, { once: true });\n }\n\n return handle;\n },\n cancelAll() {\n for (const h of pending) h.cancel();\n pending.clear();\n },\n get size() {\n return pending.size;\n },\n };\n return sched;\n}\n"]}
@@ -150,16 +150,19 @@ type MiddlewareContext<Ctx, Evt, States extends string> = Readonly<{
150
150
  /**
151
151
  * The triggering event. May be the user's `Evt` (from `send()` or an
152
152
  * explicit `reset(event)`) or the `ResetEvent` sentinel emitted by a
153
- * `reset()` with no event argument.
153
+ * `reset()` with no event argument. This is the caller's event object,
154
+ * passed unfrozen; treat it as read-only.
154
155
  */
155
156
  event: Evt | ResetEvent;
157
+ /** Deep-frozen effect descriptors (payloads included) about to be dispatched. */
156
158
  effects: readonly Effect[];
157
159
  changed: boolean;
158
160
  }>;
159
161
  type Middleware<Ctx, Evt, States extends string> = (ctx: MiddlewareContext<Ctx, Evt, States>, next: () => void) => void;
160
162
  /**
161
- * Payload of the `'transition'` runtime event — emitted after each `send()` or
162
- * `reset()` that actually changed the snapshot value.
163
+ * Payload of the `'transition'` runtime event — emitted whenever a transition
164
+ * fired (`changed === true`), including an internal transition whose state
165
+ * `value` did not change (only its `context` did).
163
166
  */
164
167
  type RuntimeTransitionEvent<Ctx, Evt, States extends string> = Readonly<{
165
168
  prev: Snapshot<Ctx, States>;
@@ -170,9 +173,11 @@ type RuntimeTransitionEvent<Ctx, Evt, States extends string> = Readonly<{
170
173
  }>;
171
174
  /**
172
175
  * Payload of the `'error'` runtime event — currently emitted for async effect
173
- * handler rejections (which would otherwise become unhandled). Synchronous
174
- * throws from effect handlers and middleware still propagate to the caller of
175
- * `send()` / `reset()`.
176
+ * handler rejections (which would otherwise become unhandled). With no
177
+ * `'error'` listener (none registered, or cleared by `dispose()`) a rejection
178
+ * is discarded; outside production (`NODE_ENV !== "production"`) it is also
179
+ * reported via `console.warn`. Synchronous throws from effect handlers and
180
+ * middleware still propagate to the caller of `send()` / `reset()`.
176
181
  */
177
182
  type RuntimeErrorEvent<Evt> = Readonly<{
178
183
  error: unknown;
@@ -189,6 +194,24 @@ interface Runtime<Ctx, Evt extends {
189
194
  getSnapshot(): Snapshot<Ctx, States>;
190
195
  /** Alias for `getSnapshot()`. */
191
196
  snapshot(): Snapshot<Ctx, States>;
197
+ /**
198
+ * Process `event`: `step()` -> sub-machine lifecycle -> commit ->
199
+ * middleware -> effects -> `subscribe` listeners -> `'transition'`
200
+ * listeners, then return the committed snapshot.
201
+ *
202
+ * Run-to-completion: a `send()`/`reset()` made while this runtime is already
203
+ * processing an event (from middleware, an effect handler, a listener, or a
204
+ * child runtime's listener) is queued FIFO and processed after the current
205
+ * event's last notification, with the same full sequence. Such a nested
206
+ * call returns the snapshot committed at the time of the call, not the
207
+ * outcome of its own event — read `getSnapshot()` after the outermost call
208
+ * returns (or subscribe). A throw from any queued event discards the rest
209
+ * of the queue and propagates from the outermost call.
210
+ *
211
+ * Throws `RuntimeDisposedError` after `dispose()`, and
212
+ * `InvalidDefinitionError` when `event` is not an object with a string
213
+ * `type`.
214
+ */
192
215
  send(event: Evt): Snapshot<Ctx, States>;
193
216
  /**
194
217
  * Predict whether sending `event` would fire a transition. Reuses
@@ -196,11 +219,23 @@ interface Runtime<Ctx, Evt extends {
196
219
  * are expected to be pure; `can` then matches `send` for the same input.
197
220
  */
198
221
  can(event: Evt): boolean;
222
+ /**
223
+ * Call `listener` with the committed snapshot after every event that fired
224
+ * a transition (`changed === true`), after middleware and effects and before
225
+ * `'transition'` listeners. A listener removed during a notification round
226
+ * is skipped for the rest of it; one added waits for the next event.
227
+ * Throws `InvalidDefinitionError` if `listener` is not a function. Returns
228
+ * an unsubscribe function (a no-op after `dispose()`).
229
+ */
199
230
  subscribe(listener: (snap: Snapshot<Ctx, States>) => void): () => void;
200
231
  /**
201
232
  * EventTarget-like typed listener API. Returns an unsubscribe function.
202
233
  * `options.signal` removes the listener when aborted; `options.once`
203
- * removes the listener after the first invocation. After `dispose()`,
234
+ * removes the listener before its first invocation. A listener removed
235
+ * while an event is being dispatched (by its unsubscribe, `once`, its
236
+ * signal, or `dispose()`) is skipped for the rest of that dispatch; one
237
+ * added waits for the next event. Throws `InvalidDefinitionError` for an
238
+ * unknown event `type` or a non-function `listener`. After `dispose()`,
204
239
  * `on()` is a no-op and returns a no-op unsubscribe.
205
240
  */
206
241
  on<K extends keyof RuntimeEventMap<Ctx, Evt, States>>(type: K, listener: (payload: RuntimeEventMap<Ctx, Evt, States>[K]) => void, options?: {
@@ -208,17 +243,25 @@ interface Runtime<Ctx, Evt extends {
208
243
  once?: boolean;
209
244
  }): () => void;
210
245
  /**
211
- * Re-initialise the runtime to the definition's initial snapshot. Triggers
212
- * subscribers but does NOT run entry actions (reset = re-birth, not
213
- * "transition into initial"). Throws RuntimeDisposedError if disposed.
214
- * If an `event` is supplied, middleware sees it as the trigger; otherwise
215
- * a sentinel `{ type: "@@aifsmjs/RESET" }` is synthesised.
246
+ * Re-initialise the runtime to the definition's initial snapshot. Does NOT
247
+ * run entry actions (reset = re-birth, not "transition into initial"); the
248
+ * current sub-machine child is always replaced. Notifies subscribers,
249
+ * middleware (`changed: true`) and `'transition'` listeners whenever the
250
+ * value, status, or context reference differs from the initial snapshot.
251
+ * Throws RuntimeDisposedError if disposed. If an `event` is supplied (an
252
+ * object with a string `type`, else `InvalidDefinitionError`), middleware
253
+ * sees it as the trigger; otherwise a sentinel
254
+ * `{ type: "@@aifsmjs/RESET" }` is synthesised. Run-to-completion like
255
+ * `send()`: a nested call is queued.
216
256
  */
217
257
  reset(event?: Evt): Snapshot<Ctx, States>;
218
258
  /**
219
259
  * Tear down: abort the internal AbortController (effect handlers see signal
220
260
  * fire), clear listeners, and mark this runtime as disposed. Subsequent
221
- * send()/reset() calls throw RuntimeDisposedError. Idempotent.
261
+ * send()/reset() calls throw RuntimeDisposedError. Idempotent and never
262
+ * throws. Never queued: called during a dispatch it runs at once, drops any
263
+ * queued send()/reset() calls, and the outer call returns the last
264
+ * committed snapshot.
222
265
  */
223
266
  dispose(): void;
224
267
  /**
@@ -237,11 +280,16 @@ interface Runtime<Ctx, Evt extends {
237
280
  * Returns the currently active sub-Runtime for the current parent state,
238
281
  * or undefined if:
239
282
  * - the current state has no `sub` definition, OR
240
- * - the sub-Runtime failed to initialise (SubMachineError was thrown
241
- * from `send()` / `reset()` / `createRuntime` per the spec contract),
242
- * OR
283
+ * - the previous child's `dispose()` threw during a transition
284
+ * (SubMachineError phase "dispose"; the parent stays in its state and
285
+ * the child is recreated when the state is re-entered), OR
243
286
  * - the parent runtime has been disposed.
244
287
  *
288
+ * When a transition's new child fails to initialise (SubMachineError phase
289
+ * "init"), the previous child is left untouched and is still returned. A
290
+ * child that fails to initialise at `createRuntime` bootstrap makes
291
+ * `createRuntime` itself throw, so there is no runtime to ask.
292
+ *
245
293
  * The returned Runtime is typed at the loosest sub-machine signature.
246
294
  * Caller casts to the concrete sub type.
247
295
  *
@@ -150,16 +150,19 @@ type MiddlewareContext<Ctx, Evt, States extends string> = Readonly<{
150
150
  /**
151
151
  * The triggering event. May be the user's `Evt` (from `send()` or an
152
152
  * explicit `reset(event)`) or the `ResetEvent` sentinel emitted by a
153
- * `reset()` with no event argument.
153
+ * `reset()` with no event argument. This is the caller's event object,
154
+ * passed unfrozen; treat it as read-only.
154
155
  */
155
156
  event: Evt | ResetEvent;
157
+ /** Deep-frozen effect descriptors (payloads included) about to be dispatched. */
156
158
  effects: readonly Effect[];
157
159
  changed: boolean;
158
160
  }>;
159
161
  type Middleware<Ctx, Evt, States extends string> = (ctx: MiddlewareContext<Ctx, Evt, States>, next: () => void) => void;
160
162
  /**
161
- * Payload of the `'transition'` runtime event — emitted after each `send()` or
162
- * `reset()` that actually changed the snapshot value.
163
+ * Payload of the `'transition'` runtime event — emitted whenever a transition
164
+ * fired (`changed === true`), including an internal transition whose state
165
+ * `value` did not change (only its `context` did).
163
166
  */
164
167
  type RuntimeTransitionEvent<Ctx, Evt, States extends string> = Readonly<{
165
168
  prev: Snapshot<Ctx, States>;
@@ -170,9 +173,11 @@ type RuntimeTransitionEvent<Ctx, Evt, States extends string> = Readonly<{
170
173
  }>;
171
174
  /**
172
175
  * Payload of the `'error'` runtime event — currently emitted for async effect
173
- * handler rejections (which would otherwise become unhandled). Synchronous
174
- * throws from effect handlers and middleware still propagate to the caller of
175
- * `send()` / `reset()`.
176
+ * handler rejections (which would otherwise become unhandled). With no
177
+ * `'error'` listener (none registered, or cleared by `dispose()`) a rejection
178
+ * is discarded; outside production (`NODE_ENV !== "production"`) it is also
179
+ * reported via `console.warn`. Synchronous throws from effect handlers and
180
+ * middleware still propagate to the caller of `send()` / `reset()`.
176
181
  */
177
182
  type RuntimeErrorEvent<Evt> = Readonly<{
178
183
  error: unknown;
@@ -189,6 +194,24 @@ interface Runtime<Ctx, Evt extends {
189
194
  getSnapshot(): Snapshot<Ctx, States>;
190
195
  /** Alias for `getSnapshot()`. */
191
196
  snapshot(): Snapshot<Ctx, States>;
197
+ /**
198
+ * Process `event`: `step()` -> sub-machine lifecycle -> commit ->
199
+ * middleware -> effects -> `subscribe` listeners -> `'transition'`
200
+ * listeners, then return the committed snapshot.
201
+ *
202
+ * Run-to-completion: a `send()`/`reset()` made while this runtime is already
203
+ * processing an event (from middleware, an effect handler, a listener, or a
204
+ * child runtime's listener) is queued FIFO and processed after the current
205
+ * event's last notification, with the same full sequence. Such a nested
206
+ * call returns the snapshot committed at the time of the call, not the
207
+ * outcome of its own event — read `getSnapshot()` after the outermost call
208
+ * returns (or subscribe). A throw from any queued event discards the rest
209
+ * of the queue and propagates from the outermost call.
210
+ *
211
+ * Throws `RuntimeDisposedError` after `dispose()`, and
212
+ * `InvalidDefinitionError` when `event` is not an object with a string
213
+ * `type`.
214
+ */
192
215
  send(event: Evt): Snapshot<Ctx, States>;
193
216
  /**
194
217
  * Predict whether sending `event` would fire a transition. Reuses
@@ -196,11 +219,23 @@ interface Runtime<Ctx, Evt extends {
196
219
  * are expected to be pure; `can` then matches `send` for the same input.
197
220
  */
198
221
  can(event: Evt): boolean;
222
+ /**
223
+ * Call `listener` with the committed snapshot after every event that fired
224
+ * a transition (`changed === true`), after middleware and effects and before
225
+ * `'transition'` listeners. A listener removed during a notification round
226
+ * is skipped for the rest of it; one added waits for the next event.
227
+ * Throws `InvalidDefinitionError` if `listener` is not a function. Returns
228
+ * an unsubscribe function (a no-op after `dispose()`).
229
+ */
199
230
  subscribe(listener: (snap: Snapshot<Ctx, States>) => void): () => void;
200
231
  /**
201
232
  * EventTarget-like typed listener API. Returns an unsubscribe function.
202
233
  * `options.signal` removes the listener when aborted; `options.once`
203
- * removes the listener after the first invocation. After `dispose()`,
234
+ * removes the listener before its first invocation. A listener removed
235
+ * while an event is being dispatched (by its unsubscribe, `once`, its
236
+ * signal, or `dispose()`) is skipped for the rest of that dispatch; one
237
+ * added waits for the next event. Throws `InvalidDefinitionError` for an
238
+ * unknown event `type` or a non-function `listener`. After `dispose()`,
204
239
  * `on()` is a no-op and returns a no-op unsubscribe.
205
240
  */
206
241
  on<K extends keyof RuntimeEventMap<Ctx, Evt, States>>(type: K, listener: (payload: RuntimeEventMap<Ctx, Evt, States>[K]) => void, options?: {
@@ -208,17 +243,25 @@ interface Runtime<Ctx, Evt extends {
208
243
  once?: boolean;
209
244
  }): () => void;
210
245
  /**
211
- * Re-initialise the runtime to the definition's initial snapshot. Triggers
212
- * subscribers but does NOT run entry actions (reset = re-birth, not
213
- * "transition into initial"). Throws RuntimeDisposedError if disposed.
214
- * If an `event` is supplied, middleware sees it as the trigger; otherwise
215
- * a sentinel `{ type: "@@aifsmjs/RESET" }` is synthesised.
246
+ * Re-initialise the runtime to the definition's initial snapshot. Does NOT
247
+ * run entry actions (reset = re-birth, not "transition into initial"); the
248
+ * current sub-machine child is always replaced. Notifies subscribers,
249
+ * middleware (`changed: true`) and `'transition'` listeners whenever the
250
+ * value, status, or context reference differs from the initial snapshot.
251
+ * Throws RuntimeDisposedError if disposed. If an `event` is supplied (an
252
+ * object with a string `type`, else `InvalidDefinitionError`), middleware
253
+ * sees it as the trigger; otherwise a sentinel
254
+ * `{ type: "@@aifsmjs/RESET" }` is synthesised. Run-to-completion like
255
+ * `send()`: a nested call is queued.
216
256
  */
217
257
  reset(event?: Evt): Snapshot<Ctx, States>;
218
258
  /**
219
259
  * Tear down: abort the internal AbortController (effect handlers see signal
220
260
  * fire), clear listeners, and mark this runtime as disposed. Subsequent
221
- * send()/reset() calls throw RuntimeDisposedError. Idempotent.
261
+ * send()/reset() calls throw RuntimeDisposedError. Idempotent and never
262
+ * throws. Never queued: called during a dispatch it runs at once, drops any
263
+ * queued send()/reset() calls, and the outer call returns the last
264
+ * committed snapshot.
222
265
  */
223
266
  dispose(): void;
224
267
  /**
@@ -237,11 +280,16 @@ interface Runtime<Ctx, Evt extends {
237
280
  * Returns the currently active sub-Runtime for the current parent state,
238
281
  * or undefined if:
239
282
  * - the current state has no `sub` definition, OR
240
- * - the sub-Runtime failed to initialise (SubMachineError was thrown
241
- * from `send()` / `reset()` / `createRuntime` per the spec contract),
242
- * OR
283
+ * - the previous child's `dispose()` threw during a transition
284
+ * (SubMachineError phase "dispose"; the parent stays in its state and
285
+ * the child is recreated when the state is re-entered), OR
243
286
  * - the parent runtime has been disposed.
244
287
  *
288
+ * When a transition's new child fails to initialise (SubMachineError phase
289
+ * "init"), the previous child is left untouched and is still returned. A
290
+ * child that fails to initialise at `createRuntime` bootstrap makes
291
+ * `createRuntime` itself throw, so there is no runtime to ask.
292
+ *
245
293
  * The returned Runtime is typed at the loosest sub-machine signature.
246
294
  * Caller casts to the concrete sub type.
247
295
  *
package/llms-full.txt CHANGED
@@ -15,7 +15,7 @@ The short index lives at `llms.txt` (see https://llmstxt.org/).
15
15
 
16
16
  Small deterministic FSM library for replayable TypeScript/JavaScript state machines. Definitions are plain data; guards/actions/effects are injected at runtime.
17
17
 
18
- > **Status: 0.5.8 - stable 1.0-track core.** Core FSM, guards, effects, inspect, replay, PBT helpers, scheduler, and sub-machines are live.
18
+ > **Status: 0.6.0 - stable 1.0-track core.** Core FSM, guards, effects, inspect, replay, PBT helpers, scheduler, and sub-machines are live.
19
19
 
20
20
  ## Install
21
21
 
@@ -62,7 +62,7 @@ Prefer `setup<Ctx, Evt>().defineMachine()` for state inference. Use bare `define
62
62
  | --- | --- |
63
63
  | `aifsmjs` | `setup`, `defineMachine`, `createRuntime`, `createMachine`, `step`, `assign`, snapshots, runtime/errors/types. |
64
64
  | `aifsmjs/guards` | `and`, `or`, `not`, `stateIn`. Guards must be synchronous. |
65
- | `aifsmjs/effects` | `enqueue.effect()` descriptors and `runEffects()`. |
65
+ | `aifsmjs/effects` | `createEnqueuer()` and `runEffects()`. |
66
66
  | `aifsmjs/inspect` | Read-only middleware helpers: `logger`, `persist`, `recorder`. |
67
67
  | `aifsmjs/replay` | Pure event-log replay. |
68
68
  | `aifsmjs/pbt` | fast-check property helpers. |
@@ -71,18 +71,25 @@ Prefer `setup<Ctx, Evt>().defineMachine()` for state inference. Use bare `define
71
71
  ## Lifecycle Rules
72
72
 
73
73
  - `step(def, snapshot, event, impl)` is pure and returns `{ snapshot, effects, changed }`.
74
- - `createRuntime()` owns mutable runtime state, dispatches effects after commit, and emits transition/error/dispose events.
74
+ - `createRuntime()` owns mutable runtime state. Per event it commits, then runs middleware, effects, `subscribe` listeners and `'transition'` listeners, in that order.
75
+ - `send()`/`reset()` are run-to-completion: a call made from middleware, an effect handler or a listener is queued and runs after the current event's notifications, with the same full sequence.
75
76
  - Guards and reducers are sync. Thenable guards throw `AsyncGuardError`.
76
- - Effects are fire-and-forget descriptors. Async rejection is routed to the runtime `"error"` channel.
77
- - `reset()` rewinds the snapshot and notifies listeners, but does not run entry actions.
77
+ - Effects are fire-and-forget descriptors. Async rejection is routed to the runtime `"error"` channel; with no `"error"` listener it is dropped (with a `console.warn` outside production).
78
+ - `reset()` rewinds to the initial snapshot without running entry actions, and notifies listeners whenever the value, status or context reference changes.
78
79
  - `dispose()` is idempotent; post-dispose `send()`/`reset()` throw `RuntimeDisposedError`.
80
+ - Misused arguments to `defineMachine`, `createRuntime` and the runtime methods throw `InvalidDefinitionError`.
79
81
 
80
82
  ## Sharp Edges
81
83
 
82
- - Middleware and synchronous effect throws happen after snapshot commit. A throw can leave the committed snapshot visible without later notification.
83
- - Sub-machine replacement can roll back on init failure, but dispose failure has already torn down the old child.
84
+ - Middleware, synchronous effect and subscriber throws happen after snapshot commit. A throw can leave the committed snapshot visible without later notification, and it drops any queued `send()`/`reset()` calls.
85
+ - A nested `send()` returns the snapshot committed at the time of the call, not the outcome of its own event. Read `getSnapshot()` after the outermost call returns, or subscribe.
86
+ - A listener removed while a notification is running (unsubscribe, `once`, `signal`, `dispose()`) is skipped for the rest of that round.
87
+ - Middleware never freezes your event, but it deep-freezes effect descriptors, including any object you passed as a payload.
88
+ - Actions on an object context must return a plain-object patch (or nothing); the merge keeps the context's prototype. Returning `false`, `0` or `""` throws `InvalidActionResultError`. Prefer plain-object contexts.
89
+ - Sub-machine replacement builds the new child first: on init failure the old child stays live; on dispose failure the old child is already torn down and the new one is discarded.
84
90
  - `subRuntime()` can return a disposed child handle if external code disposed it; it is recreated only after the parent exits and re-enters the sub state.
85
91
  - `setup().defineMachine()` uses `NoInfer` so states infer from `keyof states`; keep regression tests for exact optional property configurations.
92
+ - `after()` throws `RangeError` for `NaN`, `Infinity` or a negative delay, and clamps delays above 2^31-1 ms (about 24.8 days). To mean "never", do not schedule.
86
93
  - Do not perform async I/O inside guards or actions. Send events from effects instead.
87
94
 
88
95
  ## AI Context
@@ -105,7 +112,54 @@ MIT
105
112
 
106
113
  All notable changes to aifsmjs are summarized here.
107
114
 
108
- ## [Unreleased]
115
+ ## [0.6.0] - 2026-09-29
116
+
117
+ ### Breaking
118
+
119
+ - `Runtime.send()` / `Runtime.reset()`: a call made while the runtime is already processing an event (from middleware, an effect handler, a `subscribe` or `'transition'` listener, or a child runtime's listener) is now queued and processed after the current event's last notification (run-to-completion) instead of running inside it, because the README-recommended "send from an effect" pattern delivered notifications in reverse and left subscribers holding a stale snapshot; the nested call returns the snapshot committed at that moment, and an error from a queued event propagates from the outermost call. Migration: read `getSnapshot()` after the outer `send()`/`reset()` returns (or subscribe) instead of using a nested call's return value or reading the snapshot right after it, and catch errors around the outermost call rather than around a nested `send()`.
120
+ - `Runtime.subscribe()` / `Runtime.on()` / `Runtime.onTransition()`: a listener removed while a notification is running (by its unsubscribe function, `once`, its `signal`, or `dispose()`) is now skipped for the rest of that round instead of still receiving the in-flight event, per the ai*js fan-out re-entrancy rule. Migration: if a removed listener must still see the current event, remove it after the call returns (for example `queueMicrotask(off)`), and do not rely on the remaining listeners running after a mid-notification `dispose()`.
121
+ - `mergeContext()` / action results: for an object context (not an array or binary view) a plain-object patch is now merged into a copy that keeps the context's prototype, where a class-instance context used to be replaced by the bare patch and lose its other fields and methods, and a non-nullish primitive result such as `false` or `0` now throws `InvalidActionResultError` from `step()`/`send()` instead of replacing the context. Migration: return a plain-object partial (or `undefined`) from actions on object contexts; to replace the context wholesale, return a new instance or array (non-plain objects still replace it).
122
+ - `Runtime.send()` / `Runtime.reset()` sub-machine lifecycle: when a transition replaces a child, the new child is now constructed before the old child is disposed, so an init failure leaves the old child live and still returned by `subRuntime()` (it used to be disposed first, leaving `undefined`). Migration: code that runs while a new child is being created must not assume the previous sibling child is already disposed, and code that expected `subRuntime()` to be `undefined` after a `SubMachineError` with `phase: "init"` should expect the previous child.
123
+ - `after()` / `createScheduler().after()`: an `ms` that is `NaN`, `±Infinity`, negative or not a number now throws `RangeError`, and a non-function `fn` throws `TypeError`, synchronously and before any timer is set (they used to fire after about 1 ms, or throw later from inside the timer), and a finite delay above 2^31-1 ms is clamped to 2^31-1 instead of firing almost at once. Migration: pass a finite `ms >= 0` and a function; callers using `Infinity` to mean "never" should simply not schedule.
124
+ - `defineMachine()` / `setup().defineMachine()` / `createMachine()` / `createRuntime()` / `Runtime.send()` / `Runtime.reset()` / `Runtime.subscribe()` / `Runtime.on()` / `Runtime.onTransition()`: argument misuse now throws `InvalidDefinitionError` (`aifsmjs: <subject> must be <constraint>`) at the call — a non-object definition, `states`, state or transition entry; a non-object `impl` or options object, or a `middleware` option that is not an array of functions; an event that is not an object with a string `type`; a non-function listener or an unknown `on()` event type — instead of a bare `TypeError` (at the call or at a later `send()`), a listener that threw at every notification, or a silently accepted value. Migration: pass `{}` as `impl` when a machine uses no named implementations, pass object events with a string `type` and function listeners, and catch `InvalidDefinitionError` where you caught `TypeError`.
125
+
126
+ ### Changes
127
+
128
+ - Added: `InvalidActionResultError` (root export, `name === "InvalidActionResultError"`, with the offending `actionName`) for an action that returns a non-nullish primitive for an object context.
129
+ - Added: `mergeContext(current, patch, actionName?)` takes an optional action name for that error (default `"<inline>"`).
130
+ - Changed: `InvalidDefinitionError` is also the argument-validation error of the definition/runtime boundary; its non-object `states` message now reads `aifsmjs: definition states must be an object`.
131
+ - Changed: an async effect rejection with no `'error'` listener (none registered, or cleared by `dispose()`) is still discarded, but is now reported via `console.warn` when `NODE_ENV !== "production"`; production behaviour is unchanged and it never becomes an unhandled rejection.
132
+ - Changed: `aifsmjs/pbt`'s `properties` is a frozen object carrying the same eight functions instead of a module namespace object, which drops tsup's shared `__export` helper chunk (about 210 B gzip) from every subpath entry.
133
+ - Changed: `step()` returns a shared frozen empty `effects` array when nothing fires.
134
+ - Changed: size budgets in `scripts/check-size.mjs` (maintainer-approved for 0.6.0): `dist/index.js` 6,500 -> 6,700 B and `dist/pbt/index.js` 8,500 -> 8,800 B, other budgets unchanged; measured gzip closures 0.5.9 -> 0.6.0: index 6,359 -> 6,654, guards 1,375 -> 1,161, effects 1,574 -> 1,365, inspect 552 -> 329, replay 3,115 -> 3,139, pbt 8,471 -> 8,718, timer 1,071 -> 1,018 B.
135
+ - Fixed: transition/implementation lookups (`state.on[event.type]`, guard/action/effect refs) now resolve by own key only, so an undeclared event type or ref named after an `Object.prototype` member (`toString`, `constructor`, `__proto__`, ...) is no longer treated as a declared transition.
136
+ - Fixed: `deepFreeze` no longer throws on binary data (`ArrayBuffer` views, e.g. `Uint8Array`) reached through context or event payloads, in dev snapshots or via middleware in production.
137
+ - Fixed: `deepFreeze` recurses through an object that is already shallow-frozen (e.g. an effect descriptor), instead of stopping there — middleware can no longer mutate an effect payload before dispatch, and an already shallow-frozen dev context is still deep-frozen.
138
+ - Fixed: runtime event listeners are isolated per-listener — a throwing `'dispose'` or `'error'` listener no longer prevents later listeners for the same event from running.
139
+ - Fixed: `assignDoesNotMutate` detects mutation by a structural fingerprint instead of `structuredClone`, so it no longer false-fails for a pure machine whose context holds a class instance or a callback.
140
+ - Fixed: `setup().defineMachine()` infers `States` from `keyof states` only, so a terminal state written as `{}` or `{ final: true }` no longer collapses the inferred state union.
141
+ - Fixed: an explicit `context: undefined` passed to `defineMachine` / `setup().defineMachine` now defaults to `{}`, the same as an absent `context` key.
142
+ - Fixed: dev-mode detection reads `process.env.NODE_ENV` directly, so Vite / webpack 5 define-replacement enables dev-only deep-freezing in browser builds that have no `process` global.
143
+ - Fixed: the PBT `snapshotAlwaysFrozen` and `reachableStatesSubsetDeclared` properties no longer dispatch real effects while driving generated commands through a runtime.
144
+ - Fixed: `createScheduler().after()` merges `signal`/`setTimeout`/`clearTimeout` field-by-field with `??` instead of an object spread, so an explicitly-undefined per-call option no longer silently overrides the scheduler's default.
145
+ - Fixed: `MachineConfig`, the parameter type of `defineMachine`, is re-exported from the package root.
146
+ - Fixed: `reset()` now notifies subscribers, middleware (`changed: true`) and `'transition'` listeners when the context reference (or status) differs from the initial snapshot; it used to compare the state value alone and stay silent.
147
+ - Fixed: middleware no longer deep-freezes the caller's event object (and its payload graph) in any `NODE_ENV`; `MiddlewareContext.event` is the caller's object, passed unfrozen.
148
+ - Fixed: a parent `send()`/`reset()` issued from a child's `'dispose'` listener during a transition is queued until the transition commits, so a live child can no longer be left in a state that has no `sub`.
149
+ - Fixed: a parent disposed by a child's `'dispose'` listener during a transition no longer adopts the replacement child; the replacement is disposed and `subRuntime()` returns `undefined`.
150
+ - Fixed: `send()` decides whether a same-value transition is external from the guard pass that produced the snapshot, so each guard runs once per event and a non-idempotent guard can no longer desync the sub-machine lifecycle from the committed state.
151
+ - Fixed: `defineMachine()` rejects a sub-machine cycle through initial states with `InvalidDefinitionError` instead of letting `createRuntime()` recurse until the stack overflows; self-references through non-initial states stay legal.
152
+ - Fixed: `snapshotAlwaysFrozen`, `reachableStatesSubsetDeclared` and `replayEqualsFold` dispose the runtime they create for each generated run, so its `AbortSignal` fires and no run leaks a live runtime.
153
+ - Fixed: `package.json` `exports` nests `types` under `import` and `require` (`require.types` points at the `.d.cts` files) for every subpath, so `node16` / `nodenext` CommonJS consumers no longer hit TS1479 / TS1471; `verify-exports` walks nested conditions.
154
+ - Docs: corrected the `aifsmjs/effects` Public Surface row (README/README_ZHTW) to name the real export, `createEnqueuer()`, instead of `enqueue.effect()`.
155
+ - Docs: STABILITY.md's Behavioral Contract states the run-to-completion and fan-out clauses, the reset, merge, argument-validation and timer rules, and the new sub-machine order; README and README_ZHTW Lifecycle Rules and Sharp Edges mirror them, and the `Runtime` / `MiddlewareContext` JSDoc says the same.
156
+
157
+ ## [0.5.9] - 2026-06-29
158
+
159
+ - Fixed: `dispose()` never throws and always completes teardown even if a `'dispose'` event listener throws (external-signal abort cleanups no longer leak); restores the never-throws / idempotency contract.
160
+ - Fixed: sub-machine definitions are now deep-validated at construction — an unknown transition target or a declared-async guard inside a `sub` is rejected by `defineMachine` (with a cycle guard) instead of surfacing only at `child.send()`.
161
+ - Fixed: PBT replay/assign oracles use structural deep-equality (`node:util` `isDeepStrictEqual`) instead of `JSON.stringify`, which mis-handled key order, `undefined` keys, `Map`/`Set`/`Date`, and `BigInt`.
162
+ - Docs: clarified that `replay()` reproduces parent value+context only (sub-machine state is not modelled) and that production snapshots are frozen at the top level only.
109
163
 
110
164
  ## [0.5.8] - 2026-06-14
111
165
 
@@ -135,30 +189,48 @@ All notable changes to aifsmjs are summarized here.
135
189
 
136
190
  | Surface | Status | Notes |
137
191
  | --- | --- | --- |
138
- | `aifsmjs` root | Stable | Definition/runtime/step/snapshot APIs and core errors. |
192
+ | `aifsmjs` root | Stable | Definition/runtime/step/snapshot APIs and core errors: `InvalidDefinitionError`, `InvalidActionResultError`, `UnknownActionError`, `UnknownGuardError`, `AsyncGuardError`, `RuntimeDisposedError`, `SubMachineError`. |
139
193
  | `aifsmjs/guards` | Stable | Sync guard combinators. |
140
194
  | `aifsmjs/effects` | Stable | Effect descriptors and dispatcher helper. |
141
195
  | `aifsmjs/inspect` | Stable | Read-only middleware helpers. |
142
196
  | `aifsmjs/replay` | Stable | Pure log replay. |
143
197
  | `aifsmjs/pbt` | Stable | fast-check helpers. |
144
- | `aifsmjs/timer` | Stable | Timer/scheduler helpers. |
198
+ | `aifsmjs/timer` | Stable | Timer/scheduler helpers. Exports no error class: misuse throws a built-in `RangeError`/`TypeError` whose message starts with `aifsmjs: `. |
145
199
 
146
200
  ## Behavioral Contract
147
201
 
148
202
  - Definition data is serializable when using string refs instead of inline functions.
149
- - `step()` is pure and never dispatches effects.
150
- - Runtime commit happens before middleware, effect dispatch, and listener notification.
151
- - Async effects are fire-and-forget; rejections emit runtime `"error"`.
152
- - `reset()` does not run entry actions.
153
- - `dispose()` aborts runtime signal, clears listeners, and is idempotent.
203
+ - `step()` is pure and never dispatches effects. Each guard on the path to the chosen transition runs at most once per event, and `send()` decides the sub-machine lifecycle from that same guard pass.
204
+ - `send()`/`reset()` are run-to-completion: for one event, commit -> middleware -> effects -> `subscribe` listeners -> `'transition'` listeners all complete before any event sent from inside them is processed; nested `send()`/`reset()` calls are queued FIFO and processed afterwards with the same full sequence; a nested call returns the snapshot committed at the time of the call, not the outcome of its own event — read `getSnapshot()` after the outermost call returns. A listener that sends on every notification of a machine that always transitions keeps the drain running.
205
+ - A throw from any event in that sequence (a `SubMachineError`, an unknown action or guard, an `InvalidActionResultError`, or a synchronous middleware, effect-handler or listener throw) drops the calls still queued and propagates from the outermost `send()`/`reset()`; the snapshot stays at the last successful commit. `dispose()` is never queued: it runs at once, drops queued calls, and the outer call returns the last committed snapshot without throwing (the event in progress finishes with cleared listeners and an aborted signal). Parent and child runtimes queue independently.
206
+ - Listener fan-out (`subscribe`, `on`, `onTransition`) is synchronous over a copy of the listener set taken when the notification starts: a listener added meanwhile first fires on the next event, and one removed meanwhile (its unsubscribe function, `once`, its `signal`, or `dispose()`) is skipped for the rest of that round. A `once` listener is removed before it is called. A throwing `on()` listener does not stop the others; the first error is rethrown once all have run.
207
+ - Middleware receives the caller's event object by reference and never freezes it (treat it as read-only). The middleware context object is frozen, `prev`/`next` are frozen to the depth below, and the effect descriptors are deep-frozen, payloads included.
208
+ - Async effects are fire-and-forget; rejections emit runtime `"error"`. A rejection with no `'error'` listener is discarded in production and reported via `console.warn` when `NODE_ENV !== "production"`; it never becomes an unhandled rejection.
209
+ - `reset()` does not run entry actions and always replaces the current sub-machine child. `reset()` notifies subscribers, middleware (`changed: true`) and `'transition'` listeners whenever the value, status, or context reference differs from the initial snapshot.
210
+ - An action result is merged into an object context (not an array or binary view) by a shallow copy that keeps the context's prototype; only own enumerable string and symbol properties are carried, not `#private` or non-enumerable members, so prefer plain-object contexts. A non-nullish primitive result (`false`, `0`, `""`, ...) for an object context throws `InvalidActionResultError`; any other non-plain-object result (an array, a class instance) replaces the context, as does any result for a primitive or array context.
211
+ - Argument misuse at the definition/runtime boundary (`defineMachine`, `setup().defineMachine`, `createMachine`, `createRuntime`, `send`, `reset`, `subscribe`, `on`, `onTransition`) throws `InvalidDefinitionError` (`aifsmjs: <subject> must be <constraint>`) before anything is created or registered. Pure helpers (`step`, `replay`, `mergeContext`, guard combinators, `runEffects`, inspect middleware, PBT properties) trust their typed arguments.
212
+ - `after()` / `createScheduler().after()`: `ms` must be a finite number >= 0 (`RangeError`) and `fn` a function (`TypeError`), checked before any timer or listener exists; a finite delay above 2^31-1 ms (about 24.8 days) is clamped to 2^31-1 when handed to `setTimeout`.
213
+ - `dispose()` aborts runtime signal, clears listeners, and is idempotent. A throwing `'dispose'` listener is swallowed and never aborts teardown.
214
+
215
+ ## Replay caveat
216
+
217
+ `replay()` and `step()` reproduce only the **parent** machine's `value` + `context` (`aifsmjs/replay`, "Pure event-log replay" in the README). Sub-machine state is **not** modelled: the pure lifecycle has no `sub` references, so a replayed/stepped snapshot reflects the parent state alone and never re-instantiates, advances, or restores any child runtime. To capture child state for time-travel or incident reproduction, snapshot the child separately from the live runtime via `subRuntime()`.
218
+
219
+ ## Snapshot freezing depth
220
+
221
+ Snapshot freezing is depth-dependent on `NODE_ENV`:
222
+
223
+ - **Dev** (`NODE_ENV !== "production"`): the whole snapshot tree is deep-frozen, so accidental nested mutation throws immediately.
224
+ - **Production** (`NODE_ENV === "production"`): only the **top-level** snapshot object is frozen (`Object.freeze`). Nested `context` is **caller-owned and not deeply frozen** — treat it as read-only by convention; the library does not enforce immutability of nested context in prod.
154
225
 
155
226
  ## Sub-machines
156
227
 
157
228
  Sub-machines are stable but sharp:
158
229
 
159
230
  - Entry lazily creates the child; exit disposes it.
160
- - Init failure rolls back parent transition.
161
- - Dispose failure happens after the old child is already torn down and surfaces as `SubMachineError`.
231
+ - Entry constructs the new child before the old child is disposed; init failure leaves the old child live and the parent unchanged; dispose failure surfaces after the old child is torn down and the new child is discarded.
232
+ - A `send()`/`reset()` on the parent from a child's listener during the parent's transition is queued until that transition has committed.
233
+ - Sub definitions may reference themselves or each other only through non-initial states; `defineMachine` rejects a cycle through initial-state subs, which the runtime would otherwise boot without end.
162
234
  - External child disposal leaves a stale handle until the parent leaves/re-enters the state.
163
235
 
164
236
  ## Drafts