Files
skills/docs/api-stack/api/api-router-hono/src/blue-green-slot/blue-green-slot.md
2026-05-29 08:20:56 +01:00

2.8 KiB

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 <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

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

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.