@turing-machine-js/machine 6.3.0 → 6.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,96 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [6.4.0] - 2026-05-19
8
+
9
+ Adds a new awaited hook on `TuringMachine.run`. Minor release, additive — no breaking changes. Closes [#163](https://github.com/mellonis/turing-machine-js/issues/163).
10
+
11
+ ### Added
12
+
13
+ - **`onIter` callback** on `run()`: `onIter?: (m: MachineState) => void | Promise<void>`. Fires once per iteration at end-of-iter — *after* both `onPause(before, K)` and `onPause(after, K)` dispatches on the same yield. Awaited inline, so consumers returning a Promise suspend the run loop between iters. Unaffected by the `debug` master switch — `onIter` fires on every iter regardless of whether any `state.debug` breakpoints are armed.
14
+
15
+ Use cases the hook serves:
16
+ - **Per-iter throttle** (interactive debugger / animation UIs) — `await new Promise(r => setTimeout(r, intervalMs))` inside the callback gives a "wait between iters" pattern with no `state.debug` mutation.
17
+ - **Iter-correct bookkeeping** — for downstream libraries that maintain per-iter prev state (e.g. PostMachine's `arrivalPath` tracking), `onIter` is the natural "iter K is done" boundary, after all `onPause` hooks have read their own snapshots. Avoids the prev-advance-during-onStep ordering hazard.
18
+ - **Yield-to-other-work in batched runs** — a sync `await new Promise(r => queueMicrotask(r))` per iter lets long runs interleave with other event-loop work without owning the run loop.
19
+
20
+ ### Three-hook contract recap
21
+
22
+ | Hook | Sync/Async | When | Use |
23
+ |---|---|---|---|
24
+ | `onStep` | Sync, not awaited | Mid-iter (between before/after) | Cheap tracer/logger, microtask-free hot loop |
25
+ | `onPause` | Awaited | Conditional on `state.debug[when]` match | User-authored breakpoints / debugger UI |
26
+ | `onIter` | Awaited | End of every iter | Per-iter coordination — throttle, prev-tracking, animation, yield-to-other-work |
27
+
28
+ ### Changed (docs)
29
+
30
+ - README's **Throttle pattern** section rewritten to recommend `onIter` as the canonical hook. The v6.3.0 `onPause`-rearm workaround is superseded — that pattern still works (it's just the engine-native primitives), but `onIter` is the cleaner shape.
31
+
32
+ ### Compatibility
33
+
34
+ - Default `onIter` is `undefined` → no behavior change for any existing consumer that doesn't opt in.
35
+ - Sync consumers using only `onStep` pay zero microtask overhead — `onIter` is purely opt-in.
36
+ - Peer-deps unchanged (`^6.0.0` includes 6.4.0 for the dependents).
37
+
38
+ ## [6.3.0] - 2026-05-19
39
+
40
+ Corrective minor release. **Reverts v6.2.0's `await onStep` change** and restores the original sync `onStep` contract that held since the hook was introduced. See [GitHub release](https://github.com/mellonis/turing-machine-js/releases/tag/v6.3.0) for the full post-mortem.
41
+
42
+ ### Changed
43
+
44
+ - **`TuringMachine.run`** — `await onStep(m)` reverted to `onStep(m)`; type narrowed back from `(m: MachineState) => void | Promise<void>` to `(m: MachineState) => void`. The original "Sync, ~free hook ... must not be async" contract is restored, with the docstring augmented to point readers at the new Throttle pattern section.
45
+
46
+ ### Removed
47
+
48
+ - The v6.2.0 test `async onStep is awaited between iters (#158)` (no longer the contract).
49
+
50
+ ### Added (docs)
51
+
52
+ - README: new **Throttle pattern** section under *Debugging breakpoints* showing the engine-native shape for per-iter throttle / "wait between iters" UIs — arm `state.debug.after = true` on the initial state, throttle inside `onPause`, rearm `m.nextState.debug.after = true` in the callback. Replaces the v6.2.0 throttle-in-`onStep` pattern.
53
+
54
+ ### Kept from v6.2.0 (independent improvements)
55
+
56
+ - CI `typecheck` step + dependent packages' `tsconfig` `paths`/`rootDir` fix.
57
+
58
+ ### Migration
59
+
60
+ - From v6.2.0: drop any `await` you wrote inside `onStep`. The engine ignores the returned Promise now. If you used the throttle-in-`onStep` pattern, migrate to the Throttle pattern section in the README.
61
+ - From v6.1.0 or earlier: no change for you.
62
+
63
+ ## [6.2.0] - 2026-05-19 [SUPERSEDED by 6.3.0]
64
+
65
+ > ⚠️ **This release was a mistake.** It overturned the sync `onStep` contract that had held since the hook was introduced, motivated by a downstream throttle use case that didn't actually need an engine API change. Restored to sync in [6.3.0](#630---2026-05-19). The `await onStep` semantic shipped briefly on npm; consumers should upgrade to 6.3.0 and use the README's Throttle pattern instead.
66
+
67
+ ### Changed
68
+
69
+ - **`TuringMachine.run`** — `onStep` type widened from `(m) => void` to `(m) => void | Promise<void>`; the run loop now awaits each `onStep(m)` call.
70
+
71
+ ### Internal
72
+
73
+ - CI `typecheck` step added (mirrors the spec-including tsconfig to catch TS errors in `*.spec.ts`).
74
+ - Dependent packages' `tsconfig` `paths` and `rootDir` corrected — they now resolve `@turing-machine-js/machine` from source in CI (previously required a pre-built `dist/`).
75
+
76
+ ## [6.1.0] - 2026-05-16
77
+
78
+ Minor release. One additive ergonomic improvement to `state.debug`, no breaking changes.
79
+
80
+ ### Added
81
+
82
+ - **Lazy-init `DebugConfig` + `Object.seal` on instance ([#151](https://github.com/mellonis/turing-machine-js/pull/151), closes [#150](https://github.com/mellonis/turing-machine-js/issues/150)).** `state.debug` is now always a non-null `DebugConfig` instance, lazy-initialized on first read. Chained writes like `state.debug.before = true` work on a fresh state without a prior `state.debug = {}` setup step. The `DebugConfig` instance is `Object.seal`-ed — typos like `state.debug.bofore = true` throw `TypeError` (in strict mode — default for TS-emitted modules) instead of silently creating a useless own property.
83
+
84
+ ### Changed
85
+
86
+ - **`state.debug` getter type narrowed** — `DebugConfig | null` → `DebugConfig`. Setter still accepts `null`; the semantic shifts to "reset filters" (next read returns a fresh empty config rather than literal `null`).
87
+ - **Wrapper sharing preserved** — `withOverrodeHaltState`'s `#debugRef` cell continues to share a single `DebugConfig` instance across wrapper and original; first read on either lazy-creates, both subsequent reads return the same instance.
88
+
89
+ ### Migration
90
+
91
+ No code changes required:
92
+ - `state.debug?.X` patterns keep working (the `?.` is now redundant but correct).
93
+ - `state.debug = { ... }` whole-object assignment works as before.
94
+ - `state.debug = null` works as before, with the new "reset filters" semantic.
95
+ - Consumer code checking `if (state.debug === null)` becomes dead code but doesn't crash; the narrowed getter type may surface TS warnings on those branches, safe to delete.
96
+
7
97
  ## [6.0.0] - 2026-05-09
8
98
 
9
99
  ### Changed (BREAKING)
package/README.md CHANGED
@@ -516,36 +516,29 @@ If `onPause` is not provided, breaks fire-and-resume invisibly — the trajector
516
516
 
517
517
  **Caveat:** `haltState` is a module-level singleton. Setting `haltState.debug` affects every machine in the process; clear in `afterEach` / `finally` for test isolation.
518
518
 
519
- ### Throttle pattern
519
+ ### Throttle pattern (v6.4.0+)
520
520
 
521
- For per-iter throttle / animation / "wait between steps" UIs, do the await inside `onPause`, not `onStep` `onPause` is the engine's awaited callback. To make it fire on every iter (no user-set breakpoints needed), arm `.after = true` on the initial state and rearm `m.nextState.debug.after = true` inside the callback. The chain is self-propagating:
521
+ For per-iter throttle / animation / "wait between steps" UIs, use the **`onIter`** hookan awaited callback that fires once at the end of every iter, after both `onPause` dispatches on the same yield. It's the engine-native shape for per-iter coordination:
522
522
 
523
523
  ```ts
524
- initialState.debug.after = true; // first pause-point
525
-
526
524
  await machine.run({
527
525
  initialState,
528
- debug: true,
529
- onPause: async (m) => {
530
- // m.debugBreak.after is true here on every iter.
531
- await new Promise((r) => setTimeout(r, intervalMs)); // throttle
532
-
533
- // Rearm for the next iter (engine processes m.nextState next).
534
- // `state.debug` is shared across `withOverrodeHaltState` wrappers — a
535
- // single arm reaches all of them.
536
- if (!m.nextState.isHalt) m.nextState.debug.after = true;
526
+ onIter: async (m) => {
527
+ // Fires after before(m.state) / step / after(m.state) on iter m.step.
528
+ await new Promise((r) => setTimeout(r, intervalMs));
537
529
  },
538
530
  });
539
531
  ```
540
532
 
533
+ `onIter` is unaffected by the `debug` master switch and unrelated to `state.debug` — it fires on every iter regardless of whether any breakpoints are armed. It coexists cleanly with user-authored `state.debug` breakpoints: on an iter with both `.before` and `.after` armed, the consumer sees `onPause(before)` → `onStep` → `onPause(after)` → `onIter`, in that order, on the same yield.
534
+
541
535
  A few details:
542
536
 
543
- - **Halting iter**: `m.nextState === haltState` on the last user iter. The example skips the rearm there. The halting iter's own `.after` already fired this callback (engine v5+ fixed [#108](https://github.com/mellonis/turing-machine-js/issues/108) part 1).
544
- - **`haltState.debug.after`**: throws on assignment (#108 part 2). Don't try to rearm onto haltState itself pause for halt by checking `m.nextState.isHalt` in the callback before the rearm step.
545
- - **Coexists with user-authored breaks**: if a user state also has `.before = true` set by the consumer's own code, `onPause` fires twice on that iter (before + after). Tell them apart with `m.debugBreak`.
546
- - **Click-pause** during throttling: keep a flag set from the outside; check it inside `onPause` before the `setTimeout` and surface a "real" pause (await a resolvable Promise the UI controls) instead of the throttle one. The engine doesn't need to know — it just sees a longer awaited `onPause`.
537
+ - **Halting iter**: `onIter` still fires on the iter whose `m.nextState === haltState`, after any halt-time `onPause` dispatches. Engine returns cleanly after that. Use this to land "halted" UI state in interactive consumers.
538
+ - **Click-pause / external interruption**: keep a flag set from the outside; check it inside `onIter` and `await` a resolvable Promise the UI controls (instead of the bare `setTimeout`). The engine just sees a longer awaited `onIter` no engine surface needed for the pause.
539
+ - **Sync consumers should keep using `onStep`**: it's microtask-free; `onIter` adds one awaited boundary per iter. Use the right hook for the right verb (logging/tracing `onStep`, throttle/coordination `onIter`, user breakpoints `onPause`).
547
540
 
548
- (v6.2.0 briefly widened `onStep` to async to enable a throttle-in-onStep pattern. That was a mistake — restored to sync in v6.3.0. The rearm-via-`onPause` pattern above is the engine-native shape.)
541
+ (History: v6.2.0 briefly widened `onStep` to `void | Promise<void>` and added an inline `await`, motivated by this same throttle use case. That was a mistake — restored to sync in v6.3.0. v6.3.0 documented a workaround using `onPause` self-rearm on `state.debug.after = true`; that workaround is superseded by `onIter` in v6.4.0+.)
549
542
 
550
543
  ## Special objects
551
544
 
@@ -623,6 +616,7 @@ API surface changes since v3, in past tense so the timing of each piece is expli
623
616
  - **v6.1** — `state.debug` ergonomics: the field is now always a non-null `DebugConfig` instance (lazy-initialized on first read), so chained field writes like `state.debug.before = true` work on a fresh state without a prior whole-object assignment. The `DebugConfig` instance is `Object.seal`-ed, so typos like `state.debug.bofore = true` throw `TypeError` at write time instead of silently creating a useless property. `state.debug = null` continues to work but semantically means "reset filters" — the next read returns a fresh empty `DebugConfig` (#150).
624
617
  - **v6.2** *(superseded by v6.3.0)* — widened `onStep`'s signature to `(m) => void | Promise<void>` and added an inline `await onStep(...)` in the run loop, enabling throttle-in-`onStep` patterns. This overturned the docstring-stated contract that `onStep` is sync (microtask-free); the right place for per-iter throttling is `onPause` with self-rearm (see [Throttle pattern](#throttle-pattern)). Restored in v6.3.0.
625
618
  - **v6.3** — `onStep` reverted to its v6.0–v6.1 sync contract — `(m) => void`, called synchronously inside the run loop. The Throttle pattern section documents the engine-native shape for per-iter throttle / "wait between iters" UIs. No other API changes.
619
+ - **v6.4** — New **`onIter`** hook on `run()`: awaited, fires once at the end of every iter (after both `onPause` dispatches on the same yield), unaffected by the `debug` master switch. Use for per-iter throttle / animation / coordination needing a suspend point; complements the existing sync `onStep` (tracing) and conditional `onPause` (user breakpoints). Three-hook contract is now `onStep` (sync, mid-iter) / `onPause` (awaited, on `state.debug` match) / `onIter` (awaited, end-of-iter). Additive — peer-deps unchanged. The v6.3.0 README's `onPause`-rearm throttle workaround is superseded.
626
620
 
627
621
  For the full release history, see the [GitHub releases page](https://github.com/mellonis/turing-machine-js/releases).
628
622
 
@@ -33,22 +33,14 @@ export default class TuringMachine {
33
33
  tapeBlock?: TapeBlock;
34
34
  });
35
35
  get tapeBlock(): TapeBlock;
36
- run({ initialState, stepsLimit, onStep, onPause, debug, }: RunParameter & {
36
+ run({ initialState, stepsLimit, onStep, onPause, onIter, debug, }: RunParameter & {
37
37
  /**
38
38
  * Sync, ~free hook fired on every iteration. Use for logging/tracing —
39
39
  * the hot loop runs this without a microtask boundary, so it must not
40
40
  * be async.
41
41
  *
42
42
  * For per-iter throttle / coordination ("wait between iters" UIs):
43
- * arm `state.debug.after = true` on each visited state and do the
44
- * throttle inside `onPause` (which IS awaited). See the README's
45
- * Throttle pattern section for a worked example.
46
- *
47
- * (v6.2.0 widened this to `void | Promise<void>` and added an inline
48
- * `await`. That was a mistake — it drove the awaited contract into a
49
- * hook that was deliberately documented as sync. Restored in v6.3.0;
50
- * consumers needing per-iter await belong on the `onPause`-rearm
51
- * pattern, not this hook.)
43
+ * use `onIter` (v6.4.0+, awaited at end-of-iter).
52
44
  */
53
45
  onStep?: (machineState: MachineState) => void;
54
46
  /**
@@ -65,6 +57,25 @@ export default class TuringMachine {
65
57
  * engine's reason for pausing).
66
58
  */
67
59
  onPause?: (machineState: MachineState) => void | Promise<void>;
60
+ /**
61
+ * Awaited hook fired ONCE at the end of every iteration (v6.4.0+), AFTER
62
+ * any `onPause(after, K)` dispatch on the same yield. Use for per-iter
63
+ * coordination that needs to suspend the run loop — throttling between
64
+ * iters (interactive debugger UIs), prev-state bookkeeping that must
65
+ * observe iter K's final state once all `onPause` hooks have read their
66
+ * own snapshots, yield-to-other-work in batched runs.
67
+ *
68
+ * Three-hook contract recap:
69
+ * - `onStep`: sync, microtask-free — tracing/logging during the iter
70
+ * - `onPause`: awaited, conditional on `state.debug[when]` match — user
71
+ * breakpoints with iter-correct payload
72
+ * - `onIter`: awaited, unconditional — once per iter, at end-of-iter
73
+ *
74
+ * `onIter` is unaffected by the `debug` master switch — it fires on
75
+ * every iter regardless. Sync consumers should prefer `onStep` to avoid
76
+ * the per-iter microtask boundary `onIter` carries.
77
+ */
78
+ onIter?: (machineState: MachineState) => void | Promise<void>;
68
79
  /**
69
80
  * Master switch for `onPause` dispatch. When `false`, suppresses all
70
81
  * pause-fires (before and after) regardless of `state.debug` assignments.
package/dist/index.cjs CHANGED
@@ -1060,7 +1060,7 @@ class TuringMachine {
1060
1060
  get tapeBlock() {
1061
1061
  return __classPrivateFieldGet(this, _TuringMachine_tapeBlock, "f");
1062
1062
  }
1063
- async run({ initialState, stepsLimit = 1e5, onStep, onPause, debug = true, }) {
1063
+ async run({ initialState, stepsLimit = 1e5, onStep, onPause, onIter, debug = true, }) {
1064
1064
  const generator = this.runStepByStep({ initialState, stepsLimit });
1065
1065
  for (const machineState of generator) {
1066
1066
  // Per-iter lifecycle: before → step → after. All three operate on the
@@ -1069,12 +1069,15 @@ class TuringMachine {
1069
1069
  if (debug && machineState.debugBreak?.before && onPause) {
1070
1070
  await onPause({ ...machineState, debugBreak: { before: true } });
1071
1071
  }
1072
- if (onStep instanceof Function) {
1072
+ if (onStep) {
1073
1073
  onStep(machineState);
1074
1074
  }
1075
1075
  if (debug && machineState.debugBreak?.after && onPause) {
1076
1076
  await onPause({ ...machineState, debugBreak: { after: true } });
1077
1077
  }
1078
+ if (onIter) {
1079
+ await onIter(machineState);
1080
+ }
1078
1081
  }
1079
1082
  }
1080
1083
  *runStepByStep({ initialState, stepsLimit = 1e5 }) {
package/dist/index.mjs CHANGED
@@ -1058,7 +1058,7 @@ class TuringMachine {
1058
1058
  get tapeBlock() {
1059
1059
  return __classPrivateFieldGet(this, _TuringMachine_tapeBlock, "f");
1060
1060
  }
1061
- async run({ initialState, stepsLimit = 1e5, onStep, onPause, debug = true, }) {
1061
+ async run({ initialState, stepsLimit = 1e5, onStep, onPause, onIter, debug = true, }) {
1062
1062
  const generator = this.runStepByStep({ initialState, stepsLimit });
1063
1063
  for (const machineState of generator) {
1064
1064
  // Per-iter lifecycle: before → step → after. All three operate on the
@@ -1067,12 +1067,15 @@ class TuringMachine {
1067
1067
  if (debug && machineState.debugBreak?.before && onPause) {
1068
1068
  await onPause({ ...machineState, debugBreak: { before: true } });
1069
1069
  }
1070
- if (onStep instanceof Function) {
1070
+ if (onStep) {
1071
1071
  onStep(machineState);
1072
1072
  }
1073
1073
  if (debug && machineState.debugBreak?.after && onPause) {
1074
1074
  await onPause({ ...machineState, debugBreak: { after: true } });
1075
1075
  }
1076
+ if (onIter) {
1077
+ await onIter(machineState);
1078
+ }
1076
1079
  }
1077
1080
  }
1078
1081
  *runStepByStep({ initialState, stepsLimit = 1e5 }) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@turing-machine-js/machine",
3
- "version": "6.3.0",
3
+ "version": "6.4.0",
4
4
  "description": "A convenient Turing machine",
5
5
  "engines": {
6
6
  "npm": ">=7.0.0"
@@ -38,5 +38,5 @@
38
38
  "default": "./dist/index.mjs"
39
39
  }
40
40
  },
41
- "gitHead": "6be43b1e88c4c562c9c11b495b285ff114f86b37"
41
+ "gitHead": "a8228b8a5f7f9acd1f6219af80db86368848cdbc"
42
42
  }