# `slot` _(no submodules)_ ## Purpose The `slot` module defines `BlueGreenSlot` — the per-plugin atomic mount point that backs hot-reload. It exposes three positions (`live`, `shadow`, `draining`) plus a `flip()` primitive that promotes shadow to live in a single observable step, returning a `DrainingHandle` for the previous live slot so callers can wait for in-flight requests to settle before disposal. Per design doc §8.1, the per-plugin version pointer is the single atomic point that determines which module gets a given request. This module is that pointer's contract. The reference implementation `InMemoryBlueGreenSlot` lives here so the conformance harness and Stage-1 testing can exercise the full lifecycle; production `api-host` is expected to compose this primitive with its registry. ## Features - `BlueGreenSlot` interface — abstract contract every adapter pins to. - `RouterSlot` — `{ slotId, handler, version }` triple; ids are deterministic (`.v`). - `DrainingHandle` — `{ slotId, settled, abort() }` returned from `flip()`. - `flip()` is atomic — sets shadow→null, live→shadow, and returns the previous-live slot as draining in one synchronous step. - `dispose(slotId, { timeoutMs })` — Promise.race against settlement; fires `ROUTER.SLOT_DISPOSE_TIMEOUT` and wires AbortSignal on timeout. - `flip()` with `shadow === null` throws `ROUTER.SLOT_FLIP_FAILED`. - Pending-request bookkeeping via `acquire(slotId)` / `release(slotId)` so the host pipeline can decrement on response settlement. ## Usage: Basic ```ts import { InMemoryBlueGreenSlot } from "@suaveplan/api-router/slot"; const slot = new InMemoryBlueGreenSlot("auth", () => "v1"); slot.setShadow(() => "v2"); const draining = slot.flip(); // slot.live is now v2; draining points at v1 await slot.dispose(draining.slotId, { timeoutMs: 5_000 }); ``` ## Usage: Advanced ```ts import { InMemoryBlueGreenSlot, type DrainingHandle } from "@suaveplan/api-router/slot"; const slot = new InMemoryBlueGreenSlot("billing", () => "v1"); // Two in-flight requests against v1 slot.acquire(slot.live.slotId); slot.acquire(slot.live.slotId); slot.setShadow(() => "v2"); const draining: DrainingHandle = slot.flip(); // Race: dispose with short timeout will throw SLOT_DISPOSE_TIMEOUT and // wire the abort signal onto the still-pending requests. try { await slot.dispose(draining.slotId, { timeoutMs: 50 }); } catch (e) { // RouterError code "ROUTER.SLOT_DISPOSE_TIMEOUT"; in-flight requests // see draining.settled remain pending, but their abort signal is // tripped. } ``` ## API Reference - `BlueGreenSlot` — interface with `live`, `shadow`, `setShadow`, `flip`, `dispose`, `acquire`, `release`. - `RouterSlot` — `{ slotId: string, handler: SlotHandler, version: number }`. - `SlotPosition` — `"live" | "shadow" | "draining"`. - `DrainingHandle` — `{ slotId, settled: Promise, abort(): void }`. - `SlotHandler` — `(...args: unknown[]) => unknown` (type-erased). - `InMemoryBlueGreenSlot` — reference impl; constructor takes `(plugin: string, initialHandler: SlotHandler)`. ## Implementation Notes `flip()` is implemented as a synchronous three-line atomic: read shadow, clear shadow, swap live. JavaScript's single-threaded execution makes this observably atomic to all callers within the same realm. The previous-live slot's state (including its abort controller and pending counter) is moved into the `draining` map so subsequent `dispose` / `acquire` / `release` calls can address it by slot id. `dispose` races a timeout against a settlement promise — once all pending requests `release`, the promise resolves and dispose returns cleanly. On timeout, the slot's `AbortController` is tripped; existing request flows that wired their fetch onto `signalFor(slotId)` will observe the abort and abandon their work. The thrown `RouterError` carries the slot id and remaining pending count in `context` for diagnostic logging.