# Worker & Protocol

`worker/` (protocol + worker entrypoint) and `runtime/` (main-thread lifecycle, transport, frame retention). Everything between the host application and the engine.

Protocol
  `worker/messages.ts` — discriminated unions over `postMessage`
Transport
  `runtime/transport.ts` — wraps a `Worker` factory; queues messages until the worker boots
Host handle
  `createSimulation()` → `Simulation` (stores: `status`, `frames`; events; `run/pause/reset/ack/getFrame/dispose`)
Flow control
  Ack-based backpressure — the consumer’s pace bounds worker memory/compute

## Message protocol

Host → Worker (`ToWorkerMessage`)
Type | Payload | Effect in the worker
--- | --- | ---
`init` | `sdcpn, extensions?, initialMarking, parameterValues, seed, dt, maxTime (number | null), maxFramesAhead?, batchSize?` | Runs `buildSimulation()` (compiles user code, packs frame 0), replies `frame(0)` then `ready`. Resets `lastAckedFrame = -1`.
`start` | — | Starts the async compute loop (no-op if running; refuses from complete/error).
`pause` | — | Stops the loop, keeps all state, replies `paused`.
`stop` | — | Discards the `SimulationInstance` entirely.
`setBackpressure` | `maxFramesAhead?, batchSize?` | Live-updates loop tuning.
`ack` | `frameNumber` | `lastAckedFrame = max(lastAckedFrame, n)` — unblocks computation.

Worker → Host (`ToMainMessage`)
Type | Payload | Effect on the main thread
--- | --- | ---
`ready` | `initialFrameCount` | Resolves the `createSimulation` promise; status → `Ready`.
`frame` / `frames` | `SimulationFramePayload` = `{ time, frame: ArrayBuffer }` (single / batch) | Appended to the frame store; `frames` store publishes `{count, latest: reader}`.
`paused` | `frameNumber` | Status → `Paused`.
`complete` | `reason: "deadlock" | "maxTime", frameNumber` | Status → `Complete`; emitted on the event stream.
`error` | `message, itemId` (offending SDCPN item if known) | Status → `Error`; rejects init if still initializing.

## Sequence — one quick simulation

² = the frame `ArrayBuffer` is structured-clone copied across the boundary (no transfer list); the worker keeps its own copy in `SimulationInstance.frames[]`.

## Backpressure — the ack contract

    // worker compute loop (simplified from simulation.worker.ts)
    while (isRunning) {
      if (lastAckedFrame < 0 || currentFrame - lastAckedFrame >= maxFramesAhead) {
        await delay(10); continue;          // wait for the consumer
      }
      batch = compute up to batchSize frames (stop on complete/error)
      post "frame" | "frames"
      await delay(0);                        // let pause/stop/ack messages in
    }

Play mode (core `playback` module) | maxFramesAhead | batchSize | Ack behaviour
--- | --- | --- | ---
`viewOnly` | n/a | No core backpressure profile — the React playback provider pauses the worker and never acks, so nothing new is computed.
`computeBuffer` | 40 | 10 | Acks when playback is within ~0.5 s of the last computed frame — keeps a small rolling buffer ahead of the playhead.
`computeMax` | 10000 | 500 | Acks every arrival → compute as fast as possible.
Worker defaults | 1000 | 1000 | Used when `init` omits the settings.

## Lifecycle

Main-thread `SimulationState`. The worker mirrors a smaller internal status (`ready / running / complete / error`); complete and error are terminal for the worker — `reset()` sends `stop` and requires a fresh `init` to run again.

## Main-thread runtime responsibilities (`createSimulation`)

- Snapshot preparation: sanitizes the SDCPN for the enabled extensions and flattens component instances so the main-thread frame layout matches what the worker produces (place-count mismatch throws on read).
- Transport: `createWorkerTransport(createWorker)` queues outbound messages until the worker resolves; test code can inject a pre-built `SimulationTransport` instead.
- Retention: `createInMemorySimulationFrameStore(sdcpn)` compiles the reader factory once and appends every payload — it keeps all frames so playback can scrub. Alternative stores (latest-only, sliding window, persisted) can be swapped in without changing consumers.
- Publication: `status` and `frames` readable stores + `events` stream; hosts subscribe with `store.subscribe(listener)`.

Refactoring seams. (1) Frames could be posted with a transfer list to halve copies — but not as-is: `computeNextFrame()` re-reads the latest stored frame to compute the next one, so transferring (detaching) the posted buffer first requires stepping from a retained working copy (e.g. latest-only retention or a double buffer). (2) `reset()` keeps the main-thread status `Ready` but the worker has discarded its simulation — a subsequent `run()` sends `start` to a worker with nothing to run; a full re-init is what actually happens in the UI flow. (3) The 10 ms poll while waiting for acks is a busy-wait; a promise-per-ack would be exact.