Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,98 @@
# `slot`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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 (`<plugin>.v<n>`).
- `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<void>, 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.