<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ruslan Gilmullin</title>
    <description>The latest articles on DEV Community by Ruslan Gilmullin (@mellonis).</description>
    <link>https://dev.to/mellonis</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F337859%2F01b619ce-df73-44c6-8308-a516030a5462.jpeg</url>
      <title>DEV Community: Ruslan Gilmullin</title>
      <link>https://dev.to/mellonis</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mellonis"/>
    <language>en</language>
    <item>
      <title>A pause with two sides: the hook contract and the worker protocol</title>
      <dc:creator>Ruslan Gilmullin</dc:creator>
      <pubDate>Thu, 21 May 2026 22:43:35 +0000</pubDate>
      <link>https://dev.to/mellonis/implementing-a-pause-inside-a-worker-isnt-one-decision-its-two-contracts-at-once-the-engines-jfl</link>
      <guid>https://dev.to/mellonis/implementing-a-pause-inside-a-worker-isnt-one-decision-its-two-contracts-at-once-the-engines-jfl</guid>
      <description>&lt;p&gt;Suppose you’re writing a&amp;nbsp;Turing-machine interpreter that runs in&amp;nbsp;a&amp;nbsp;Web Worker. The UI&amp;nbsp;needs to&amp;nbsp;show a&amp;nbsp;trace&amp;nbsp;— how the machine steps from state to&amp;nbsp;state, what gets written to&amp;nbsp;the tape, how the head moves. For the user to&amp;nbsp;keep up&amp;nbsp;with what’s on&amp;nbsp;screen, the engine needs a&amp;nbsp;short delay between iterations&amp;nbsp;— milliseconds, regularly, on&amp;nbsp;every step. That’s a&amp;nbsp;throttle of&amp;nbsp;the engine between iterations&amp;nbsp;— regular and predictable, not the “Pause” of&amp;nbsp;the&amp;nbsp;UI button (which stops the machine until the user clicks “Continue”).&lt;/p&gt;

&lt;p&gt;So&amp;nbsp;where exactly in&amp;nbsp;the iteration cycle does the worker apply that throttle? There are two candidates, and the choice between them pins two contracts at&amp;nbsp;once: the engine’s hooks and the protocol between worker and main thread. Pick the right point, and you design both sides at&amp;nbsp;once; pick the wrong one, and you break both. This article is&amp;nbsp;about that choice.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ficokf1sgwiqh0jeya8jh.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ficokf1sgwiqh0jeya8jh.webp" alt="Screenshot of&amp;nbsp;demo.machines.mellonis.ru: a&amp;nbsp;Turing machine in&amp;nbsp;RUNNING_AUTO mode with the Pause button highlighted by&amp;nbsp;a&amp;nbsp;zoom-circle, the step-by-step execution trace visible on&amp;nbsp;the left" width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/mellonis/turing-machine-js" rel="noopener noreferrer"&gt;&lt;code&gt;turing-machine-js&lt;/code&gt;&lt;/a&gt; is&amp;nbsp;a&amp;nbsp;Turing-machine interpreter. The engine has an&amp;nbsp;&lt;code&gt;onStep&lt;/code&gt; hook&amp;nbsp;— the handler runs after each machine step. That’s all the engine background you need for this article. For the rest of&amp;nbsp;the engine’s design and previous API changes, see &lt;a href="https://dev.to/en/articles/three-majors-two-mistakes/"&gt;“Three majors, two mistakes: designing a&amp;nbsp;pause API for a&amp;nbsp;Turing-machine interpreter”&lt;/a&gt;&amp;nbsp;(1) and &lt;a href="https://dev.to/en/articles/dva-mazhora-odin-readme-odno-demo/"&gt;“Two majors, one README, one demo: two cheap design reviews”&lt;/a&gt;&amp;nbsp;(2).&lt;/p&gt;

&lt;p&gt;The first real consumer of&amp;nbsp;the engine&amp;nbsp;is &lt;a href="https://demo.machines.mellonis.ru" rel="noopener noreferrer"&gt;&lt;code&gt;machines-demo&lt;/code&gt;&lt;/a&gt;, an&amp;nbsp;interactive debugger. The user’s machine runs in&amp;nbsp;a&amp;nbsp;Web Worker; the&amp;nbsp;UI on&amp;nbsp;the main thread shows the trace. The same demo that, in&amp;nbsp;&lt;a href="https://dev.to/en/articles/dva-mazhora-odin-readme-odno-demo/"&gt;article (2)&lt;/a&gt;, surfaced two engine majors (&lt;code&gt;#109&lt;/code&gt;, &lt;code&gt;#108&lt;/code&gt;). This article is&amp;nbsp;the next episode in&amp;nbsp;the life of&amp;nbsp;the same engine: v6.2&amp;nbsp;→ v6.3&amp;nbsp;→ v6.4.&lt;/p&gt;

&lt;p&gt;The demo has a&amp;nbsp;RUNNING_AUTO mode: the user picks an&amp;nbsp;interval, clicks “Start”, and the machine steps automatically. Originally the mode was straightforward&amp;nbsp;— the main thread drove step-by-step execution (via &lt;code&gt;runner.step()&lt;/code&gt;, a&amp;nbsp;wrapper over the engine’s &lt;code&gt;runStepByStep()&lt;/code&gt; in&amp;nbsp;the worker), with &lt;code&gt;setTimeout(intervalMs)&lt;/code&gt; inserted between steps for the trace. Simple, and it&amp;nbsp;worked&amp;nbsp;— until the user set a&amp;nbsp;breakpoint.&lt;/p&gt;

&lt;p&gt;Breakpoints in&amp;nbsp;the engine fire inside &lt;code&gt;machine.run()&lt;/code&gt;: the iteration loop lives in&amp;nbsp;the engine itself, and on&amp;nbsp;hitting a &lt;code&gt;debug&lt;/code&gt;-marked state, it&amp;nbsp;pauses itself. &lt;code&gt;runStepByStep()&lt;/code&gt;, on&amp;nbsp;the other hand, is&amp;nbsp;a&amp;nbsp;generator: the consumer drives the loop, and breakpoints are invisible to&amp;nbsp;it. &lt;a href="https://github.com/mellonis/machines-demo/issues/43" rel="noopener noreferrer"&gt;&lt;code&gt;machines-demo#43&lt;/code&gt;&lt;/a&gt; was this hole&amp;nbsp;— the user sets a&amp;nbsp;breakpoint in&amp;nbsp;RUNNING_AUTO mode, clicks “Start”, and the breakpoint is&amp;nbsp;ignored. For breakpoints to&amp;nbsp;fire, RUNNING_AUTO had to&amp;nbsp;switch from &lt;code&gt;runStepByStep()&lt;/code&gt; to &lt;code&gt;run()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But &lt;code&gt;run()&lt;/code&gt; keeps its iteration loop inside the engine and doesn’t yield control back. Previously the throttle lived between &lt;code&gt;runner.step()&lt;/code&gt; calls on&amp;nbsp;the main thread&amp;nbsp;— that&amp;nbsp;is, on&amp;nbsp;the consumer side, where the consumer controlled the interval between steps. With the switch to&amp;nbsp;&lt;code&gt;run()&lt;/code&gt;, that consumer-side gap is&amp;nbsp;gone&amp;nbsp;— the throttle has to&amp;nbsp;either be&amp;nbsp;moved inside the engine (into an&amp;nbsp;existing or&amp;nbsp;new hook) or&amp;nbsp;be&amp;nbsp;dropped. That became the design question.&lt;/p&gt;

&lt;p&gt;One more architectural detail we&amp;nbsp;need: the main thread holds a&amp;nbsp;watchdog timer over the worker. If&amp;nbsp;the worker stays silent on&amp;nbsp;messages longer than &lt;code&gt;WORKER_TIMEOUT_MS&lt;/code&gt;, the main thread considers it&amp;nbsp;hung (e.g., the user’s machine never reaches halt) and kills&amp;nbsp;it. Whatever place we&amp;nbsp;put the throttle in&amp;nbsp;has to&amp;nbsp;live with that timer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the throttle can live
&lt;/h2&gt;

&lt;p&gt;Two candidates:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Inside &lt;code&gt;onStep&lt;/code&gt;&lt;/strong&gt;&amp;nbsp;— the synchronous hook the engine calls after each step. Its current signature is&amp;nbsp;&lt;code&gt;(state) =&amp;gt; void&lt;/code&gt;. You could widen it&amp;nbsp;to&amp;nbsp;&lt;code&gt;(state) =&amp;gt; void&amp;nbsp;| Promise&amp;lt;void&amp;gt;&lt;/code&gt;, and then the handler can &lt;code&gt;await&lt;/code&gt; the delay right there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;At&amp;nbsp;the iteration boundary&lt;/strong&gt;&amp;nbsp;— add a&amp;nbsp;new hook, designed for async waits. Naming aside, what matters is&amp;nbsp;that it&amp;nbsp;fires once at&amp;nbsp;the end of&amp;nbsp;each engine iteration, and the engine awaits the returned Promise.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both candidates answer “where does the throttle live”. But at&amp;nbsp;the same time they dictate what the worker-main protocol looks like.&lt;/p&gt;

&lt;p&gt;If&amp;nbsp;the throttle sits in&amp;nbsp;&lt;code&gt;onStep&lt;/code&gt;, it&amp;nbsp;ends up&amp;nbsp;in&amp;nbsp;the middle of&amp;nbsp;each engine iteration&amp;nbsp;— at&amp;nbsp;the point where &lt;code&gt;onStep&lt;/code&gt; is&amp;nbsp;called. Everything else in&amp;nbsp;the protocol has to&amp;nbsp;be&amp;nbsp;pinned to&amp;nbsp;that same point: the watchdog timer and the user’s click on&amp;nbsp;“Pause” both hang off this mid-iter delay.&lt;/p&gt;

&lt;p&gt;If&amp;nbsp;the throttle sits at&amp;nbsp;the iteration boundary, the worker has a&amp;nbsp;moment at&amp;nbsp;the end of&amp;nbsp;every iteration&amp;nbsp;— the engine isn’t running, it’s awaiting a&amp;nbsp;Promise. In&amp;nbsp;that moment, it’s easy to&amp;nbsp;make peace with the watchdog timer and to&amp;nbsp;be&amp;nbsp;ready for a&amp;nbsp;click on&amp;nbsp;“Pause”.&lt;/p&gt;

&lt;p&gt;Now both contracts, one at&amp;nbsp;a&amp;nbsp;time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Contract “Inside &lt;code&gt;onStep&lt;/code&gt;”
&lt;/h2&gt;

&lt;p&gt;v6.2.0 took the first path&amp;nbsp;— it&amp;nbsp;widened &lt;code&gt;onStep&lt;/code&gt;’s signature from &lt;code&gt;(state) =&amp;gt; void&lt;/code&gt; to&amp;nbsp;&lt;code&gt;(state) =&amp;gt; void&amp;nbsp;| Promise&amp;lt;void&amp;gt;&lt;/code&gt;. The handler could now &lt;code&gt;await&lt;/code&gt; the delay right where it&amp;nbsp;used to&amp;nbsp;only look at&amp;nbsp;state. From the worker’s side it&amp;nbsp;looked clean&amp;nbsp;— one hook, one wait point, no&amp;nbsp;new entities.&lt;/p&gt;

&lt;p&gt;Within a&amp;nbsp;few hours, I&amp;nbsp;marked v6.2.0&amp;nbsp;as deprecated on&amp;nbsp;npm. Not “a&amp;nbsp;newer version is&amp;nbsp;out, upgrade at&amp;nbsp;your convenience”, but an&amp;nbsp;explicit warning: this version is&amp;nbsp;&lt;em&gt;wrong&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;v6.3.0 reverted the signature widening. The reason was straightforward: &lt;code&gt;onStep&lt;/code&gt; was meant as&amp;nbsp;an&amp;nbsp;observation hook, not a&amp;nbsp;coordination one. It&amp;nbsp;runs synchronously, in&amp;nbsp;the middle of&amp;nbsp;each iteration, so&amp;nbsp;the consumer can look at&amp;nbsp;state and record it&amp;nbsp;somewhere. Adding &lt;code&gt;await&lt;/code&gt; to&amp;nbsp;its signature glues two different jobs into one: observe and hold the engine. A&amp;nbsp;reader of&amp;nbsp;the signature can no&amp;nbsp;longer tell: is&amp;nbsp;this hook “look at&amp;nbsp;this state” or&amp;nbsp;“hold the engine here”? The hook’s purpose stops being visible from the signature.&lt;/p&gt;

&lt;p&gt;The symptom that something was wrong showed up&amp;nbsp;in&amp;nbsp;the docs. The README had to&amp;nbsp;explain how to&amp;nbsp;use the widened &lt;code&gt;onStep&lt;/code&gt;. An&amp;nbsp;honest paragraph opened with a&amp;nbsp;hedge: “&lt;code&gt;onStep&lt;/code&gt; is&amp;nbsp;an&amp;nbsp;observation hook, but you can return a&amp;nbsp;Promise from&amp;nbsp;it, and the engine will await it&amp;nbsp;before the next iteration.” Neither the “but” nor the second half could be&amp;nbsp;cut. The docs were doing exactly what’s described&amp;nbsp;in &lt;a href="https://dev.to/en/articles/dva-mazhora-odin-readme-odno-demo/"&gt;article (2)&lt;/a&gt;: prose was pushing on&amp;nbsp;shape&amp;nbsp;— and the shape cracked.&lt;/p&gt;

&lt;p&gt;v6.4.0 added a&amp;nbsp;separate hook&amp;nbsp;— &lt;code&gt;onIter&lt;/code&gt;. Signature: &lt;code&gt;(state) =&amp;gt; void&amp;nbsp;| Promise&amp;lt;void&amp;gt;&lt;/code&gt;. Contract: fires once at&amp;nbsp;the end of&amp;nbsp;every iteration, asynchronously, unconditionally. The name&amp;nbsp;— &lt;code&gt;iteration boundary&lt;/code&gt;&amp;nbsp;— describes &lt;em&gt;what&lt;/em&gt; the hook does (gives a&amp;nbsp;point at&amp;nbsp;the end of&amp;nbsp;an&amp;nbsp;iteration where you can wait), not &lt;em&gt;why&lt;/em&gt; a&amp;nbsp;consumer would use&amp;nbsp;it. And it&amp;nbsp;can be&amp;nbsp;used in&amp;nbsp;different ways: for throttling between trace steps, for syncing with the&amp;nbsp;UI, for aborting a&amp;nbsp;run (e.g., via &lt;code&gt;AbortSignal&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The engine’s hook contract now distinguishes three jobs:&lt;/p&gt;

&lt;p&gt;—&amp;nbsp;&lt;strong&gt;&lt;code&gt;onStep&lt;/code&gt;&lt;/strong&gt;&amp;nbsp;— synchronous, mid-iteration. Observation of&amp;nbsp;state.&lt;br&gt;
—&amp;nbsp;&lt;strong&gt;&lt;code&gt;onPause&lt;/code&gt;&lt;/strong&gt;&amp;nbsp;— asynchronous, &lt;em&gt;conditional&lt;/em&gt; (fires only on&amp;nbsp;breakpoints). The boundary between running and stopped.&lt;br&gt;
—&amp;nbsp;&lt;strong&gt;&lt;code&gt;onIter&lt;/code&gt;&lt;/strong&gt;&amp;nbsp;— asynchronous, &lt;em&gt;unconditional&lt;/em&gt; (fires on&amp;nbsp;every iteration). Coordination: holding the engine for the consumer’s async work.&lt;/p&gt;

&lt;p&gt;Three hooks, three jobs. Observation shouldn’t hold the engine; for that, there’s a&amp;nbsp;separate asynchronous hook. Holding shouldn’t hide inside an&amp;nbsp;observation hook; for that, there’s &lt;code&gt;onIter&lt;/code&gt;. A&amp;nbsp;pause from a&amp;nbsp;breakpoint is&amp;nbsp;its own scenario altogether; for that, there’s &lt;code&gt;onPause&lt;/code&gt;, whose firing event is&amp;nbsp;spelled out in&amp;nbsp;its name.&lt;/p&gt;

&lt;h2&gt;
  
  
  Contract “At&amp;nbsp;the iteration boundary”
&lt;/h2&gt;

&lt;p&gt;Where the throttle lives is&amp;nbsp;now clear: inside the&amp;nbsp;&lt;code&gt;onIter&lt;/code&gt; handler. But the&amp;nbsp;&lt;code&gt;onIter&lt;/code&gt; handler runs &lt;em&gt;inside&lt;/em&gt; the worker, while the user’s pause comes from &lt;em&gt;outside&lt;/em&gt;&amp;nbsp;— from the main thread, on&amp;nbsp;a&amp;nbsp;click. They need a&amp;nbsp;protocol.&lt;/p&gt;

&lt;p&gt;This protocol has four mechanisms: a&amp;nbsp;pair of&amp;nbsp;&lt;code&gt;idle&lt;/code&gt;/&lt;code&gt;busy&lt;/code&gt; messages, a&amp;nbsp;&lt;code&gt;stepRequested&lt;/code&gt; flag, a&amp;nbsp;synthetic &lt;code&gt;paused&lt;/code&gt; event, and an&amp;nbsp;&lt;code&gt;intervalMs&lt;/code&gt; field in&amp;nbsp;&lt;code&gt;resume&lt;/code&gt;. Each one closes a&amp;nbsp;specific problem that the throttle’s location creates.&lt;/p&gt;

&lt;h3&gt;
  
  
  The &lt;code&gt;idle&lt;/code&gt; and &lt;code&gt;busy&lt;/code&gt; messages: pausing the watchdog
&lt;/h3&gt;

&lt;p&gt;The watchdog timer mentioned earlier starts to&amp;nbsp;get in&amp;nbsp;the way once we’re in &lt;code&gt;onIter&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A&amp;nbsp;throttle in&amp;nbsp;&lt;code&gt;onIter&lt;/code&gt;, on&amp;nbsp;every iteration, can easily exceed the timeout&amp;nbsp;— especially if&amp;nbsp;the user dials &lt;code&gt;intervalMs&lt;/code&gt; to&amp;nbsp;“slow” (the demo lets you set the interval&amp;nbsp;to, say, two seconds). The result is&amp;nbsp;a&amp;nbsp;false alarm: the worker isn’t hung, it’s just waiting.&lt;/p&gt;

&lt;p&gt;The solution: the worker itself tells the main thread when it’s “working” and when it’s “waiting”. Before &lt;code&gt;await&lt;/code&gt;-ing the throttle in&amp;nbsp;&lt;code&gt;onIter&lt;/code&gt;, the worker sends an&amp;nbsp;&lt;code&gt;idle&lt;/code&gt; message. After the throttle resolves, it&amp;nbsp;sends a &lt;code&gt;busy&lt;/code&gt;. The main thread suspends the watchdog for the gap between &lt;code&gt;idle&lt;/code&gt; and&amp;nbsp;&lt;code&gt;busy&lt;/code&gt;. The watchdog still catches real hangs (e.g., an&amp;nbsp;unreachable halt state) but no&amp;nbsp;longer confuses them with legitimate trace slowdown.&lt;/p&gt;

&lt;p&gt;Without this wrapping, the watchdog has two bad options. Either pick a&amp;nbsp;generous threshold&amp;nbsp;— then real hangs go&amp;nbsp;undetected (or&amp;nbsp;detected too late). Or&amp;nbsp;pick a&amp;nbsp;tight threshold&amp;nbsp;— and you raise the probability of&amp;nbsp;a&amp;nbsp;false alarm (the worker gets killed on&amp;nbsp;a&amp;nbsp;legitimate delay). The&amp;nbsp;&lt;code&gt;idle&lt;/code&gt;/&lt;code&gt;busy&lt;/code&gt; messages resolve both at&amp;nbsp;once.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step is&amp;nbsp;a&amp;nbsp;flag inside the worker, not engine state
&lt;/h3&gt;

&lt;p&gt;When the user clicks “Step”, the main thread sends the worker a&amp;nbsp;&lt;code&gt;resume&lt;/code&gt; message with a&amp;nbsp;&lt;code&gt;step: true&lt;/code&gt; flag&amp;nbsp;— “advance the machine one iteration and pause again”. The implementation: the worker sets &lt;code&gt;stepRequested = true&lt;/code&gt;. On&amp;nbsp;the next &lt;code&gt;onIter&lt;/code&gt;, the handler sees the flag, resets it&amp;nbsp;to&amp;nbsp;&lt;code&gt;false&lt;/code&gt;, synthesizes a&amp;nbsp;&lt;code&gt;paused&lt;/code&gt; event, and waits again. The machine has run exactly one iteration.&lt;/p&gt;

&lt;p&gt;The key point: “step” is&amp;nbsp;a&amp;nbsp;flag &lt;em&gt;inside the worker&lt;/em&gt;, not part of&amp;nbsp;the engine’s state. The engine doesn’t know what “step” means&amp;nbsp;— it&amp;nbsp;simply finds itself in&amp;nbsp;a&amp;nbsp;waiting state again on&amp;nbsp;the next iteration. The coordination lives outside the engine. &lt;code&gt;onIter&lt;/code&gt; remains a&amp;nbsp;thin &lt;code&gt;await&lt;/code&gt; hook&amp;nbsp;— it&amp;nbsp;doesn’t mutate the engine, it&amp;nbsp;only pauses the engine’s execution. That’s what made &lt;code&gt;onIter&lt;/code&gt; viable as&amp;nbsp;a&amp;nbsp;contract: there’s no&amp;nbsp;“step-specific logic” in&amp;nbsp;it, only a&amp;nbsp;Promise the worker controls itself. If&amp;nbsp;“step” had been part of&amp;nbsp;the engine’s state, the&amp;nbsp;&lt;code&gt;onIter&lt;/code&gt; contract would have to&amp;nbsp;extend to&amp;nbsp;“and also flip this flag”, and the hook would stop being thin.&lt;/p&gt;

&lt;h3&gt;
  
  
  No&amp;nbsp;throttle needed when Step is&amp;nbsp;clicked
&lt;/h3&gt;

&lt;p&gt;When the user clicks “Step”, a&amp;nbsp;throttle is&amp;nbsp;redundant&amp;nbsp;— the user plays the role of&amp;nbsp;the timer, clicking as&amp;nbsp;many times and as&amp;nbsp;fast as&amp;nbsp;they need. The implementation reflects this: on&amp;nbsp;Step, &lt;code&gt;intervalMs&lt;/code&gt; is&amp;nbsp;ignored, and the&amp;nbsp;&lt;code&gt;onIter&lt;/code&gt; handler doesn’t insert a&amp;nbsp;wait. This is&amp;nbsp;the cleanest expression of&amp;nbsp;“observation ≠ coordination” inside the worker: when the user is&amp;nbsp;in&amp;nbsp;control, the engine follows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Synthetic pause on&amp;nbsp;click
&lt;/h3&gt;

&lt;p&gt;The scenario: the machine is&amp;nbsp;in&amp;nbsp;RUNNING_AUTO&amp;nbsp;— the engine is&amp;nbsp;running with &lt;code&gt;onIter&lt;/code&gt; awaiting &lt;code&gt;intervalMs&lt;/code&gt; between iterations. The user clicks the “Pause” button in&amp;nbsp;the UI. The click arrives on&amp;nbsp;the main thread. The main thread sends the worker a &lt;code&gt;pause&lt;/code&gt; message. At&amp;nbsp;that moment, the worker is&amp;nbsp;holding the engine in&amp;nbsp;a&amp;nbsp;throttle&amp;nbsp;— the Promise hasn’t resolved yet. What do&amp;nbsp;we&amp;nbsp;do?&lt;/p&gt;

&lt;p&gt;The solution is&amp;nbsp;to&amp;nbsp;synthesize the same kind of&amp;nbsp;pause as&amp;nbsp;one from a&amp;nbsp;breakpoint. The UI&amp;nbsp;already has a&amp;nbsp;handler in&amp;nbsp;place: on &lt;code&gt;paused&lt;/code&gt; from the worker, it&amp;nbsp;switches to&amp;nbsp;“stopped”. If&amp;nbsp;the “Pause” click went down a&amp;nbsp;different path, a&amp;nbsp;second branch with the same behavior would appear. That’s why the worker synthesizes &lt;code&gt;paused&lt;/code&gt; — to&amp;nbsp;the&amp;nbsp;UI, a&amp;nbsp;“Pause” click is&amp;nbsp;indistinguishable from a&amp;nbsp;breakpoint firing.&lt;/p&gt;

&lt;p&gt;The worker cancels the throttle (resolves it&amp;nbsp;forcibly, ahead of&amp;nbsp;schedule), sets &lt;code&gt;stepRequested = false&lt;/code&gt;, and, from inside &lt;code&gt;onIter&lt;/code&gt; after returning from the wait, dispatches a&amp;nbsp;&lt;code&gt;paused&lt;/code&gt; event&amp;nbsp;— &lt;em&gt;synthetic&lt;/em&gt;, not one that arose from the engine’s normal flow. Under the hood, the two scenarios differ (one cancels the throttle, the other lets it&amp;nbsp;finish naturally), but the contract from outside is&amp;nbsp;one&amp;nbsp;— a&amp;nbsp;single “pause” scenario.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;intervalMs&lt;/code&gt; is&amp;nbsp;a&amp;nbsp;parameter of&amp;nbsp;&lt;code&gt;resume&lt;/code&gt;, not&amp;nbsp;&lt;code&gt;run()&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The throttle in&amp;nbsp;&lt;code&gt;onIter&lt;/code&gt; is&amp;nbsp;sized by &lt;code&gt;intervalMs&lt;/code&gt;. Where does the worker get that value from? Not from the parameters passed to&amp;nbsp;&lt;code&gt;run()&lt;/code&gt; at&amp;nbsp;start. From the latest&amp;nbsp;&lt;code&gt;resume&lt;/code&gt; message: each one arrives carrying a&amp;nbsp;current&amp;nbsp;&lt;code&gt;intervalMs&lt;/code&gt; from the main thread.&lt;/p&gt;

&lt;p&gt;The reason is&amp;nbsp;that the user must be&amp;nbsp;able to&amp;nbsp;change the delay between throttles during the run. The value in&amp;nbsp;the&amp;nbsp;UI is&amp;nbsp;read &lt;em&gt;at&amp;nbsp;click time&lt;/em&gt;, not at&amp;nbsp;&lt;code&gt;run()&lt;/code&gt; start; the next&amp;nbsp;&lt;code&gt;resume&lt;/code&gt; carries the new value, and starting with the next iteration, the throttle adjusts. A&amp;nbsp;small cost for the protocol, a&amp;nbsp;big win for the UX.&lt;/p&gt;

&lt;h2&gt;
  
  
  Duality, explicit
&lt;/h2&gt;

&lt;p&gt;The throttle lives in&amp;nbsp;one point of&amp;nbsp;an&amp;nbsp;iteration. From the engine’s perspective, that point is&amp;nbsp;the&amp;nbsp;&lt;code&gt;onIter&lt;/code&gt; handler: a&amp;nbsp;hook called once at&amp;nbsp;the end of&amp;nbsp;each iteration, asynchronously. From the protocol’s perspective, the same point is&amp;nbsp;the gap between &lt;code&gt;idle&lt;/code&gt; and&amp;nbsp;&lt;code&gt;busy&lt;/code&gt; messages&amp;nbsp;— a&amp;nbsp;gap where the watchdog is&amp;nbsp;suspended, a&amp;nbsp;“Pause” click can softly cancel the throttle, and&amp;nbsp;&lt;code&gt;intervalMs&lt;/code&gt; is&amp;nbsp;read from the latest &lt;code&gt;resume&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One point, two APIs. Every downstream design choice depends on&amp;nbsp;this one. Put it&amp;nbsp;in&amp;nbsp;the middle of&amp;nbsp;an&amp;nbsp;observation hook, and you break both sides at&amp;nbsp;once: the docs on&amp;nbsp;a&amp;nbsp;widened &lt;code&gt;onStep&lt;/code&gt; can’t be&amp;nbsp;written without a&amp;nbsp;“but”, the&amp;nbsp;&lt;code&gt;idle&lt;/code&gt;/&lt;code&gt;busy&lt;/code&gt; wrapping has nothing to&amp;nbsp;attach&amp;nbsp;to (the same hook stands for both observation and waiting), and the synthetic &lt;code&gt;paused&lt;/code&gt; event has to&amp;nbsp;be&amp;nbsp;dispatched from an&amp;nbsp;unnatural place. Without a&amp;nbsp;clean iteration boundary, both the hook and the protocol suffer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Separating observation from coordination is&amp;nbsp;the reason the engine’s hook contract distinguishes three jobs instead of&amp;nbsp;merging them into one. Gluing them together is&amp;nbsp;the temptation v6.2.0 succumbed&amp;nbsp;to. And the only thing that let me&amp;nbsp;realize the approach was wrong within a&amp;nbsp;few hours was that the engine’s README couldn’t be&amp;nbsp;written honestly, and the demo’s smoke test wouldn’t pass. Two almost-free design reviews (&lt;a href="https://dev.to/en/articles/dva-mazhora-odin-readme-odno-demo/"&gt;article (2)&lt;/a&gt;).&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Code: &lt;a href="https://github.com/mellonis/turing-machine-js" rel="noopener noreferrer"&gt;&lt;code&gt;turing-machine-js&lt;/code&gt;&lt;/a&gt; (engine, v6.2&amp;nbsp;→ v6.4) and &lt;a href="https://demo.machines.mellonis.ru" rel="noopener noreferrer"&gt;&lt;code&gt;machines-demo&lt;/code&gt;&lt;/a&gt; (the demo).&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webworker</category>
      <category>apidesign</category>
      <category>asyncprogramming</category>
    </item>
    <item>
      <title>Two majors, one README, one demo: two cheap design reviews</title>
      <dc:creator>Ruslan Gilmullin</dc:creator>
      <pubDate>Tue, 19 May 2026 08:35:03 +0000</pubDate>
      <link>https://dev.to/mellonis/two-majors-one-readme-one-demo-two-cheap-design-reviews-2pgn</link>
      <guid>https://dev.to/mellonis/two-majors-one-readme-one-demo-two-cheap-design-reviews-2pgn</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Writing the docs is what surfaced both mistakes. There’s a meta-lesson in there about how docs are the cheapest design review you can run, but that’s another post.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is “that other post” — and the first thing it has to do is correct the teaser. The teaser oversells. Docs caught one of the two mistakes. The other was caught by the first real consumer of the API, which I was building in parallel. The two reviews worked in tandem: docs review the shape of an API, the consumer reviews the use of it. Together they catch what tests can’t see.&lt;/p&gt;

&lt;p&gt;If you ship anything behind an interface — a library, a CLI, any entity behind a contract — these are the two reviews you don’t want to skip.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F74qgkrikcjjpt6eihwk5.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F74qgkrikcjjpt6eihwk5.webp" alt="Screenshot of demo.machines.mellonis.ru running a Brainfuck UTM: multi-tape view on the left, engine source on the right" width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://mellonis.ru/en/articles/three-majors-two-mistakes/" rel="noopener noreferrer"&gt;“Three majors, two mistakes”&lt;/a&gt; covers the engine and the v4 pause API — &lt;code&gt;onStep&lt;/code&gt;, &lt;code&gt;onDebugBreak&lt;/code&gt;, per-state &lt;code&gt;debug&lt;/code&gt; flags — in detail. I’ll lean on it here without rehashing it. The v4 → v6 trajectory shipped three breaking majors: a hook rename, a halt-semantics hardening, and a dispatch-tick collapse. The first two surfaced in the demo. The third surfaced in the docs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The demo case: v4 → v5
&lt;/h2&gt;

&lt;p&gt;While shipping v4 I started building &lt;a href="https://demo.machines.mellonis.ru" rel="noopener noreferrer"&gt;&lt;code&gt;machines-demo&lt;/code&gt;&lt;/a&gt; — an interactive Turing-machine debugger and the engine's first non-test client.&lt;/p&gt;

&lt;p&gt;The demo is a natural first consumer: it has a dual purpose. The product goal — public distribution and showing the engine in action. The technical goal — road-testing changes and validating concepts against a live API surface. Both goals make building the demo an integral part of the release cycle, not an optional add-on.&lt;/p&gt;

&lt;p&gt;The demo used both hooks at once: &lt;code&gt;onStep&lt;/code&gt; populated a per-iteration command buffer for the trace UI; &lt;code&gt;onDebugBreak&lt;/code&gt; drove the pause/resume cycle.&lt;/p&gt;

&lt;p&gt;The demo built. The tests passed. But it was uncomfortable to write, for two reasons.&lt;/p&gt;

&lt;p&gt;First, &lt;code&gt;onDebugBreak&lt;/code&gt;’s after-fire came with data from the previous yield — the same data the previous &lt;code&gt;onStep&lt;/code&gt; had already shown. The demo processed the same thing twice, and the question “why two hooks for one event?” was being asked not by me but by the code itself, in a way. I filed &lt;a href="https://github.com/mellonis/turing-machine-js/issues/109" rel="noopener noreferrer"&gt;&lt;code&gt;turing-machine-js#109&lt;/code&gt;&lt;/a&gt; as an RFC about the relationship between these hooks, listing four sketches; the resolution narrowed to &lt;em&gt;naming&lt;/em&gt;. &lt;code&gt;onDebugBreak&lt;/code&gt; framed the purpose of use as “debugging”, while the consumer’s verb was “pause”. Shameless rename, no alias. v5.&lt;/p&gt;

&lt;p&gt;Second, the demo’s UI gained a “pause before halt” scenario — letting the user glimpse the machine’s final state before it shuts down. The natural implementation: a &lt;code&gt;debug&lt;/code&gt; flag on &lt;code&gt;haltState&lt;/code&gt; itself. The first test case set &lt;code&gt;haltState.debug = { before: true, after: true }&lt;/code&gt; because the symmetry looked right. Only &lt;code&gt;before&lt;/code&gt; fired. Worse: the after-fire on the iteration that &lt;em&gt;led to&lt;/em&gt; halt never reached the consumer — the loop exited as soon as &lt;code&gt;state.isHalt&lt;/code&gt; became &lt;code&gt;true&lt;/code&gt;. &lt;a href="https://github.com/mellonis/turing-machine-js/issues/108" rel="noopener noreferrer"&gt;&lt;code&gt;turing-machine-js#108&lt;/code&gt;&lt;/a&gt; split it in two: restore the lost after-fire (bug); throw on assignment to &lt;code&gt;haltState.debug.after&lt;/code&gt; (API).&lt;/p&gt;

&lt;p&gt;Both complaints came from the consumer side. The demo didn’t surface a code bug — it surfaced an &lt;em&gt;API interaction format&lt;/em&gt;. The names didn’t fit the use. Being permissive in input didn’t match what the user was trying to do. The tests verified behavior against the engine’s own internal model — ok, green. The demo verified behavior against the consumer’s mental model — and produced two specific complaints (not ok).&lt;/p&gt;

&lt;h2&gt;
  
  
  The docs case: v5 → v6
&lt;/h2&gt;

&lt;p&gt;v5 shipped. The README needed updating. The new dispatch-order section had to explain — in words — when each of &lt;code&gt;onPause(before)&lt;/code&gt;, &lt;code&gt;onStep&lt;/code&gt;, and &lt;code&gt;onPause(after)&lt;/code&gt; fired relative to the iteration they described.&lt;/p&gt;

&lt;p&gt;The first honest paragraph went something like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;onPause(after, K)&lt;/code&gt; fires on iteration K+1’s yield, with the payload substituted from iteration K’s snapshot, before &lt;code&gt;onPause(before, K+1)&lt;/code&gt; or &lt;code&gt;onStep(K+1)&lt;/code&gt; fire.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I stared at that sentence for a while. There was no shorter version. The reader wasn’t supposed to need a sentence about substitution.&lt;/p&gt;

&lt;p&gt;The code worked. The tests passed. The demo consumed the hooks correctly. The mistake wasn’t in any of those — it was in the &lt;em&gt;shape&lt;/em&gt; of the dispatch, and that shape was only visible when you had to put it into words.&lt;/p&gt;

&lt;p&gt;The fix collapsed the lifecycle: &lt;code&gt;before(K) → step(K) → after(K)&lt;/code&gt; on the same yield. No substitution. No cross-iteration scheduling. No final drain for the halting iteration (in “Three majors, two mistakes” I called it “post-loop drain”; I’d probably shorten it to “final drain” now). The README paragraph now reads:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;On iteration K’s yield, hooks fire in lifecycle order.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And that’s the kind of sentence the reader glides past without effort. &lt;a href="https://github.com/mellonis/turing-machine-js/issues/119" rel="noopener noreferrer"&gt;&lt;code&gt;turing-machine-js#119&lt;/code&gt;&lt;/a&gt; shipped as v6.&lt;/p&gt;

&lt;p&gt;A direct quote from the previous article:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The code worked, the tests passed, and the docs were correct. The shape was just wrong.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What I’d add: the docs were correct only on the condition that the reader accepted three explanatory sentences they shouldn’t have had to read. That’s not “correct docs” — that’s docs apologizing for the shape.&lt;/p&gt;

&lt;p&gt;The docs review caught the &lt;em&gt;shape&lt;/em&gt;, not the &lt;em&gt;use&lt;/em&gt;. The demo worked. The tests passed. This time, only &lt;em&gt;prose pressure&lt;/em&gt; uncovered the issues hiding in the implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three reviews, three layers
&lt;/h2&gt;

&lt;p&gt;Here’s what each one checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tests&lt;/strong&gt; verify code against itself. Internal consistency. The engine yields what the engine should yield. Green tests are the baseline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The first real consumer&lt;/strong&gt; verifies code against a consumer’s mental model. &lt;em&gt;Does the API format match what the user is trying to do?&lt;/em&gt; Reviewing real interaction with the API forced API changes: a rename and a halt-after restriction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The docs as *prose&lt;/strong&gt;* (not JSDoc — a connected README narrative) verify code against the author’s own explanation. &lt;em&gt;Does the design have an honest one-paragraph description?&lt;/em&gt; Without one, we had to look hard at the substitution dance — and abandon it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each review catches what the layer below misses. Tests don’t catch shape; the consumer doesn’t catch interaction problems it can skillfully bypass; the docs don’t catch UX wrinkles they don’t have to mention.&lt;/p&gt;

&lt;p&gt;The cost is asymmetric. Building a real consumer is the most expensive of the three reviews; writing the docs is the cheapest. Still, even the most expensive costs less than reworking the API after the problems surface for users — hence “cheap” in the title. The real consumer has to be built &lt;em&gt;before&lt;/em&gt; the major cut — it’s the only review that makes the API comfortable to use. Docs then paper over what’s left.&lt;/p&gt;

&lt;h2&gt;
  
  
  Heuristics for the next major
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Build the first real consumer before cutting the major.&lt;/strong&gt; Not a test fixture — a consumer with its own mental model. The difficulties it encounters are the same difficulties your users will encounter later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write the dispatch-order paragraph before locking the dispatch order.&lt;/strong&gt; If you can’t describe in one sentence what fires when, the dispatch is wrong. Decisions of this kind belong at the design stage, not documentation — when not a single line of code has been written yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the docs back as a stranger.&lt;/strong&gt; If a paragraph reads as an apology for the shape, the docs are working — they’ve &lt;em&gt;revealed&lt;/em&gt; a shape mistake. Fix the shape, not the paragraph.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Three majors. One README. One demo. The tests had nothing to say.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Code: &lt;a href="https://github.com/mellonis/turing-machine-js" rel="noopener noreferrer"&gt;&lt;code&gt;turing-machine-js&lt;/code&gt;&lt;/a&gt; (engine) and &lt;a href="https://demo.machines.mellonis.ru" rel="noopener noreferrer"&gt;&lt;code&gt;machines-demo&lt;/code&gt;&lt;/a&gt; (the first non-test consumer where v4 → v5 surfaced).&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>design</category>
      <category>softwareengineering</category>
      <category>writing</category>
    </item>
    <item>
      <title>Three majors, two mistakes: designing a pause API for a Turing-machine interpreter</title>
      <dc:creator>Ruslan Gilmullin</dc:creator>
      <pubDate>Tue, 19 May 2026 08:34:35 +0000</pubDate>
      <link>https://dev.to/mellonis/three-majors-two-mistakes-designing-a-pause-api-for-a-turing-machine-interpreter-137j</link>
      <guid>https://dev.to/mellonis/three-majors-two-mistakes-designing-a-pause-api-for-a-turing-machine-interpreter-137j</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmfch2qzlchz0zwrbi9dy.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmfch2qzlchz0zwrbi9dy.webp" alt="Screenshot of demo.machines.mellonis.ru running a Turing machine" width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I spent the last two weeks shipping four breaking major versions of &lt;a href="https://github.com/mellonis/turing-machine-js" rel="noopener noreferrer"&gt;&lt;code&gt;@turing-machine-js/machine&lt;/code&gt;&lt;/a&gt; — v3, v4, v5, v6 — and the most interesting part wasn’t any single feature. It was watching the same API surface (a pause/breakpoint hook on the run loop) get redesigned twice in three versions, each time because the previous shape exposed something it shouldn’t have.&lt;/p&gt;

&lt;p&gt;This post is the post-mortem. If you’re designing pause/step/breakpoint APIs for a generator-loop interpreter, scheduler, or DSL runtime, the mistakes I made are easy to make and worth seeing in someone else’s code first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;The engine runs a Turing machine. Internally it's a generator: each &lt;code&gt;yield&lt;/code&gt; corresponds to one step (one transition firing on one tape symbol). The driver loop looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;initialState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onStep&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nf"&gt;runStepByStep&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;initialState&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;onStep&lt;/span&gt;&lt;span class="p"&gt;?.(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isHalt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;MachineState&lt;/code&gt; (&lt;code&gt;m&lt;/code&gt;) is a snapshot per iteration: the state about to execute, the current and next symbols, the moves the heads will make. A consumer (a logger, a UI, a test) can hang off &lt;code&gt;onStep&lt;/code&gt; to observe.&lt;/p&gt;

&lt;p&gt;What I wanted to add in v4 was a way to &lt;em&gt;pause&lt;/em&gt; execution at chosen points — like a breakpoint in a debugger. Any &lt;code&gt;State&lt;/code&gt; should be able to carry a &lt;code&gt;debug&lt;/code&gt; config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;myState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;debug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;before&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;symA&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;after&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;symA&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;...and the run loop should give the consumer a chance to inspect the machine and decide when to resume.&lt;/p&gt;

&lt;h2&gt;
  
  
  v4: ship it
&lt;/h2&gt;

&lt;p&gt;The v4 shape was straightforward. I made &lt;code&gt;run()&lt;/code&gt; async, added an &lt;code&gt;onDebugBreak&lt;/code&gt; hook, and routed it like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;machine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;initialState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;onStep&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* every step */&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;onDebugBreak&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;debugBreak&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;before&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;before:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;debugBreak&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;after:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// hold here until promise resolves&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The per-state &lt;code&gt;debug&lt;/code&gt; field is mutable. &lt;code&gt;state.debug = { before: true }&lt;/code&gt; sets a pre-step break; &lt;code&gt;before: [symA]&lt;/code&gt; filters by what the head is reading. The hook is awaited, so any consumer can implement “freeze until the human clicks Resume” by simply not resolving the promise.&lt;/p&gt;

&lt;p&gt;I also wanted breakpoints on halt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;haltState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;debug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;before&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;  &lt;span class="c1"&gt;// pause before exit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That worked. Symbol-list filters on &lt;code&gt;haltState&lt;/code&gt; were silent no-ops, because &lt;code&gt;haltState&lt;/code&gt; has no head symbol to filter by. Fine, I thought — be permissive in input, the wildcard &lt;code&gt;true&lt;/code&gt; is the one that matters.&lt;/p&gt;

&lt;p&gt;Two things in this design were wrong. I didn’t notice either until I started writing docs and a UI on top.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 1: the hook describes the engine, not the consumer
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;onDebugBreak&lt;/code&gt; reads, on paper, like a perfectly reasonable name. The engine fires a “debug break”; you hook into it.&lt;/p&gt;

&lt;p&gt;But what does the consumer &lt;em&gt;do&lt;/em&gt; in that hook? They pause. They inspect. They wait for input. They resume. The hook isn’t really notifying you that a thing happened — it’s offering you a cooperation point.&lt;/p&gt;

&lt;p&gt;The name &lt;code&gt;onDebugBreak&lt;/code&gt; carries one specific framing: “debugging”. But the same hook is exactly what you want for a step-through visualization, a slow-motion playback control, an animation tween, a “press space to advance” UI. None of those are debugging. They’re all &lt;em&gt;pausing&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This sounds like a small thing. It’s not, because once consumers see the name they design around it: their code path is called &lt;code&gt;handleDebugBreak&lt;/code&gt;, their state machine has a &lt;code&gt;debugging&lt;/code&gt; boolean, their UI button says “Stop debugging”. The name leaks into every consumer’s vocabulary.&lt;/p&gt;

&lt;p&gt;I wrote an RFC (&lt;a href="https://github.com/mellonis/turing-machine-js/issues/109" rel="noopener noreferrer"&gt;turing-machine-js#109&lt;/a&gt;) and renamed it in v5. Hard rename, no deprecation alias:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;await machine.run({
&lt;/span&gt;    initialState,
&lt;span class="gd"&gt;-   onDebugBreak: (m) =&amp;gt; { ... },
&lt;/span&gt;&lt;span class="gi"&gt;+   onPause: (m) =&amp;gt; { ... },
&lt;/span&gt;  });
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The payload field &lt;code&gt;m.debugBreak&lt;/code&gt; stayed — it’s metadata describing &lt;em&gt;why&lt;/em&gt; this yield fired (&lt;code&gt;{ before: true }&lt;/code&gt; or &lt;code&gt;{ after: true }&lt;/code&gt;), and “break” works fine inside the payload. But the hook name is the consumer’s contract: &lt;code&gt;onPause&lt;/code&gt; describes what &lt;em&gt;they&lt;/em&gt; do, not what the engine does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule for next time:&lt;/strong&gt; when naming a hook, name the consumer’s verb, not the engine’s event. Hooks are cooperation points, not event listeners.&lt;/p&gt;

&lt;h2&gt;
  
  
  Restriction: &lt;code&gt;haltState.debug.after&lt;/code&gt; is nonsense
&lt;/h2&gt;

&lt;p&gt;The second v4 mistake hid under an instinct to be permissive in input. &lt;code&gt;haltState.debug.after&lt;/code&gt; accepted writes silently. It just didn’t fire.&lt;/p&gt;

&lt;p&gt;Why? Because the &lt;code&gt;after&lt;/code&gt; semantics in this engine mean “fire on the iteration &lt;em&gt;after&lt;/em&gt; the one where the filter matched”. That makes perfect sense for normal states — you transition out of state K, the next iter (K+1) runs, &lt;em&gt;and at that yield&lt;/em&gt; the consumer is told “by the way, K’s after-filter fired last step”. But halt is terminal. There is no K+1 after halt. The &lt;code&gt;after&lt;/code&gt; event has nothing to anchor on.&lt;/p&gt;

&lt;p&gt;I’d silently swallowed &lt;code&gt;haltState.debug.after = true&lt;/code&gt; in v4. Worse, I’d let &lt;code&gt;{ before: true, after: true }&lt;/code&gt; through — half the assignment was meaningful, half was a no-op, and nothing told the consumer.&lt;/p&gt;

&lt;p&gt;In v5 I made both throw at write time (&lt;a href="https://github.com/mellonis/turing-machine-js/issues/108" rel="noopener noreferrer"&gt;turing-machine-js#108&lt;/a&gt; part 2):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- haltState.debug = { before: true, after: true }; // v5: throws — 'after' on halt has nothing to anchor on
&lt;/span&gt;&lt;span class="gi"&gt;+ haltState.debug = { before: true };
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Rule for next time:&lt;/strong&gt; if a configuration has no semantics, throw early. Silently no-op input &lt;em&gt;looks&lt;/em&gt; user-friendly until a consumer spends an afternoon wondering why their UI doesn’t fire on halt. “Be permissive” is a false economy when permissiveness silences a real mistake.&lt;/p&gt;

&lt;p&gt;While I was in there, I also fixed a related bug: the halting iter’s &lt;em&gt;own&lt;/em&gt; &lt;code&gt;after&lt;/code&gt; filter wasn’t firing either (&lt;a href="https://github.com/mellonis/turing-machine-js/issues/108" rel="noopener noreferrer"&gt;turing-machine-js#108&lt;/a&gt; part 1). The driver loop exited as soon as &lt;code&gt;state.isHalt&lt;/code&gt; became true — and the after-event from the previous iter, which would normally fire on this final yield, got dropped on the floor. v5 added a post-loop drain to fire it.&lt;/p&gt;

&lt;p&gt;I’ll come back to this.&lt;/p&gt;

&lt;p&gt;And one v5 bonus: a master switch on &lt;code&gt;run()&lt;/code&gt; for the whole pause system.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;machine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;initialState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;onPause&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When &lt;code&gt;false&lt;/code&gt;, all pause-fires are suppressed regardless of what &lt;code&gt;state.debug&lt;/code&gt; says across the graph (&lt;a href="https://github.com/mellonis/turing-machine-js/issues/106" rel="noopener noreferrer"&gt;turing-machine-js#106&lt;/a&gt;). You can A/B “debug mode” without rewriting the graph or clearing every &lt;code&gt;state.debug&lt;/code&gt; field. The flag dispatches &lt;code&gt;onPause&lt;/code&gt;; the underlying &lt;code&gt;m.debugBreak&lt;/code&gt; payload field still populates on the generator’s yields (it’s a property of the iteration, not of how &lt;code&gt;run()&lt;/code&gt; chose to surface it).&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 2: the substitution dance
&lt;/h2&gt;

&lt;p&gt;This is the one I’m proudest to have caught, because the code worked, the tests passed, and the docs were correct. The shape was just wrong.&lt;/p&gt;

&lt;p&gt;In v4 and v5, &lt;code&gt;after K&lt;/code&gt; (the after-fire for iteration K) actually fired on iteration &lt;strong&gt;K+1&lt;/strong&gt;’s yield. The driver loop carried a &lt;code&gt;pendingAfterFromPrev&lt;/code&gt; flag across yields, and on the next iter’s yield it dispatched the &lt;code&gt;after&lt;/code&gt; hook &lt;em&gt;first&lt;/em&gt;, then the &lt;code&gt;before&lt;/code&gt; hook, then &lt;code&gt;onStep&lt;/code&gt;. The hook for K’s after-fire had to see K’s state — not K+1’s. So I &lt;em&gt;substituted&lt;/em&gt; the previous yield’s snapshot into the &lt;code&gt;m.state&lt;/code&gt; field of K+1’s yield, just for the duration of the &lt;code&gt;after&lt;/code&gt; dispatch.&lt;/p&gt;

&lt;p&gt;This worked. But it had three knock-on effects:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The substitution leaked.&lt;/strong&gt; Consumers wanted access to the un-substituted, “real” iteration state (the one the &lt;code&gt;step&lt;/code&gt; hook saw) — see &lt;a href="https://github.com/mellonis/turing-machine-js/issues/107" rel="noopener noreferrer"&gt;turing-machine-js#107&lt;/a&gt;. Some users were reading raw &lt;code&gt;MachineState.debugBreak&lt;/code&gt; from &lt;code&gt;runStepByStep&lt;/code&gt; directly and were surprised that &lt;code&gt;m.state&lt;/code&gt; referred to the iter on which the after-fired-from, not the iter that produced it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The halt case needed a special path.&lt;/strong&gt; As mentioned: the halting iter’s own &lt;code&gt;after&lt;/code&gt; fires &lt;em&gt;after&lt;/em&gt; the loop exits. v5 added a post-loop drain to handle it. That drain is its own code path, with its own substitution, separate from the in-loop dispatch.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The generator’s return type widened.&lt;/strong&gt; Because the post-loop drain needed to &lt;em&gt;return&lt;/em&gt; a final yielded value out of the iterator, &lt;code&gt;runStepByStep&lt;/code&gt;’s return type became &lt;code&gt;Generator&amp;lt;MachineState, MachineState | null&amp;gt;&lt;/code&gt; — a yield type &lt;em&gt;and&lt;/em&gt; a return type. The canonical &lt;code&gt;for..of&lt;/code&gt; consumer doesn’t see the return value, but anyone reading the type signature now had to understand why there were two slots.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All of this came from one design choice: dispatching &lt;code&gt;after K&lt;/code&gt; on iter K+1’s yield instead of on iter K’s own yield.&lt;/p&gt;

&lt;p&gt;In v6 I collapsed it (&lt;a href="https://github.com/mellonis/turing-machine-js/issues/119" rel="noopener noreferrer"&gt;turing-machine-js#119&lt;/a&gt;). The per-iter lifecycle is now plainly &lt;code&gt;before → step → after&lt;/code&gt;. The &lt;code&gt;after&lt;/code&gt; fire rides on the same yield as the iter that produced it. No substitution. No &lt;code&gt;pendingAfterFromPrev&lt;/code&gt;. No post-loop drain. The generator return type narrows back to &lt;code&gt;Generator&amp;lt;MachineState&amp;gt;&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// v6&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;machine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;initialState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;onStep&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* unchanged */&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;onPause&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;debugBreak&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;before&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;before:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;debugBreak&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;after:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dispatch order &lt;em&gt;across&lt;/em&gt; hooks changed (any test asserting &lt;code&gt;pause(after K-1) → pause(before K) → step(K)&lt;/code&gt; had to flip to &lt;code&gt;pause(before K) → step(K) → pause(after K)&lt;/code&gt;), but the set of dispatched calls and per-iter semantics are unchanged. Consumers treating the hooks as independent observers see no change at all (&lt;a href="https://github.com/mellonis/turing-machine-js/issues/107" rel="noopener noreferrer"&gt;turing-machine-js#107&lt;/a&gt; — “expose un-substituted state” — disappeared as a problem: there’s no substitution to expose around).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule for next time:&lt;/strong&gt; if your hook payload requires substituting state from a different iteration than the one that’s yielding, the dispatch sequence is wrong. Lifecycle phases (&lt;code&gt;before&lt;/code&gt;, &lt;code&gt;step&lt;/code&gt;, &lt;code&gt;after&lt;/code&gt;) belong on the iter they describe. The substitution wasn’t a clever trick — it was a leak telling me the events were on the wrong tick.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I’d carry to the next pause/breakpoint API
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Name hooks for the consumer's verb, not the engine's event.&lt;/strong&gt; &lt;code&gt;onPause&lt;/code&gt;, not &lt;code&gt;onDebugBreak&lt;/code&gt;. &lt;code&gt;onProgress&lt;/code&gt;, not &lt;code&gt;onChunkReceived&lt;/code&gt;. The name leaks into every consumer's mental model — pick the framing you want them to inherit.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Throw on impossible configurations early.&lt;/strong&gt; “Permissive in input” is fine for shapes that &lt;em&gt;might&lt;/em&gt; be meaningful; it's a trap for shapes that &lt;em&gt;can't&lt;/em&gt; be. &lt;code&gt;haltState.debug.after = true&lt;/code&gt; had no possible semantics. Silently swallowing it cost an afternoon to one user before I noticed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Per-iter phases belong on the iter they describe.&lt;/strong&gt; If you're tempted to dispatch &lt;code&gt;after K&lt;/code&gt; on K+1's yield, ask what payload you have to substitute to make it look right. The substitution is the signal.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A master switch is cheap and worth it.&lt;/strong&gt; A boolean on the entry point to flip the whole feature off (without rewriting graph-level flags) makes the feature safe to ship to production and toggle in tests. &lt;code&gt;run({ debug: false })&lt;/code&gt; was one of the smaller v5 adds and one of the most-used by downstream consumers.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Breaking changes are worth the major bump.&lt;/strong&gt; Three majors in two weeks sounds expensive. It isn't, if the lockstep downstream is one repo (in my case &lt;a href="https://github.com/mellonis/post-machine-js" rel="noopener noreferrer"&gt;&lt;code&gt;@post-machine-js/machine&lt;/code&gt;&lt;/a&gt;, which bumped its peer dep &lt;code&gt;^4.0.0&lt;/code&gt; → &lt;code&gt;^6.0.0&lt;/code&gt; and renamed its own internal &lt;code&gt;__onDebugBreak&lt;/code&gt; → &lt;code&gt;__onPause&lt;/code&gt; in step). API ergonomics compound across every consumer for the life of the library — pay the migration cost while the API is young.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The v6 shape is the one I should have shipped in v4. I didn't have the vocabulary then — I was thinking “expose the engine's events” instead of “design the consumer's contract”. Writing the docs is what surfaced both mistakes. There's a meta-lesson in there about how docs are the cheapest design review you can run, but that's another post.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Code:&lt;/em&gt; &lt;a href="https://github.com/mellonis/turing-machine-js" rel="noopener noreferrer"&gt;&lt;code&gt;turing-machine-js&lt;/code&gt;&lt;/a&gt; &lt;em&gt;(engine) and&lt;/em&gt; &lt;a href="https://github.com/mellonis/post-machine-js" rel="noopener noreferrer"&gt;&lt;code&gt;post-machine-js&lt;/code&gt;&lt;/a&gt; &lt;em&gt;(a Post machine built on top, kept in lockstep). The interactive demo at&lt;/em&gt; &lt;a href="https://demo.machines.mellonis.ru" rel="noopener noreferrer"&gt;demo.machines.mellonis.ru&lt;/a&gt; &lt;em&gt;consumes the v6&lt;/em&gt; &lt;code&gt;onPause&lt;/code&gt; &lt;em&gt;hook for its Step / Run / Stop controls.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>architecture</category>
      <category>javascript</category>
      <category>softwareengineering</category>
    </item>
  </channel>
</rss>
