first commit
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
# `blue-green-slot` — Hono-flavoured atomic hot-reload primitive
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## 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 `<plugin>.v<version>` 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<void>`.
|
||||
- `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.
|
||||
Reference in New Issue
Block a user