@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 +90 -0
- package/README.md +12 -18
- package/dist/classes/TuringMachine.d.ts +21 -10
- package/dist/index.cjs +5 -2
- package/dist/index.mjs +5 -2
- package/package.json +2 -2
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,
|
|
521
|
+
For per-iter throttle / animation / "wait between steps" UIs, use the **`onIter`** hook — an 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
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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**: `
|
|
544
|
-
-
|
|
545
|
-
- **
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
+
"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": "
|
|
41
|
+
"gitHead": "a8228b8a5f7f9acd1f6219af80db86368848cdbc"
|
|
42
42
|
}
|