# `blue-green-slot` — Hono-flavoured atomic hot-reload primitive _(no submodules)_ ## Purpose `HonoBlueGreenSlot` is the per-plugin atomic flip primitive backing hot-reload for a Hono-backed `Router`. It holds at most three positions — `live`, `shadow`, and a map of `draining` slots — and lets the host swap a plugin's route table in O(1) by replacing a single mutable reference. The pending-request counter tracks in-flight work on draining slots so the host can wait for traffic to bleed off before the old module is GC'd. Two invariants govern correctness: every `flip()` produces a `DrainingHandle` whose `settled` promise resolves once `release()` has matched every `acquire()`, and `dispose(slotId, { timeoutMs })` races a timer against that settle — on timeout it aborts the in-flight requests via the slot's `AbortController`. ## Features - O(1) atomic flip via a single mutable `liveState` reference. - Pending-request counter (`acquire`/`release`) with FIFO drain-waiters. - `signalFor(slotId)` exposes the per-slot `AbortSignal` for streaming handlers. - Slot IDs derived deterministically from `.v` for readable logs. - Throws `ROUTER.SLOT_FLIP_FAILED` when no shadow is set. - Throws `ROUTER.SLOT_DISPOSE_TIMEOUT` after aborting on timeout. ## Usage: Basic ```ts import { HonoBlueGreenSlot } from "@suaveplan/api-router-hono/blue-green-slot"; const slot = new HonoBlueGreenSlot("billing", () => "v1"); slot.setShadow(() => "v2"); const draining = slot.flip(); await slot.dispose(draining.slotId, { timeoutMs: 1_000 }); ``` ## Usage: Advanced ```ts import { HonoBlueGreenSlot } from "@suaveplan/api-router-hono/blue-green-slot"; const slot = new HonoBlueGreenSlot("invoices", () => "v1"); slot.acquire(slot.live.slotId); // long-running request begins slot.setShadow(() => "v2"); const draining = slot.flip(); const disposal = slot.dispose(draining.slotId, { timeoutMs: 5_000 }); // later, when the long-running request finishes: slot.release(draining.slotId); await disposal; ``` ## API Reference - `new HonoBlueGreenSlot(plugin, handler)`. - `slot.live: RouterSlot`. - `slot.shadow: RouterSlot | null`. - `slot.setShadow(handler)`. - `slot.flip(): DrainingHandle`. - `slot.dispose(slotId, { timeoutMs }): Promise`. - `slot.acquire(slotId)`, `slot.release(slotId)`. - `slot.signalFor(slotId): AbortSignal | null`. ## Implementation Notes The pending counter clamps at zero so over-release is a no-op rather than a thrown error — the host is the single owner of the counter and tests that double-release should fail at the host layer, not silently here. `signalFor` is layered on top of the `AbortController` per draining slot so handlers can wire `req.signal.addEventListener("abort", ...)` to terminate streaming responses immediately after a timed-out dispose.