Files
suaveplan/openspec/archive/2026-05-26-api-router-hono/specs/api-router-hono/spec.md
2026-05-27 23:29:53 +01:00

6.1 KiB

Capability Spec Delta: api-router-hono (api-router-hono)

Package: @suaveplan/api-router-hono Status: PROPOSED

Every requirement carries a **Verified by:** line at authoring time. (See openspec/AGENTS.md Section 13.)

ADDED Requirements

Requirement: Direct surface implements the Router contract

The package SHALL export createHonoRouter(opts) returning a Router (from @suaveplan/api-router) backed by a Hono instance. The returned router MUST pass runRouterConformance from @suaveplan/api-router/testing.

Scenario: factory returns a conformant Router

  • WHEN createHonoRouter({ basePath: "/" }) is called
  • THEN the result satisfies the Router type AND runRouterConformance("hono-direct", () => createHonoRouter(...)) reports zero failures

Scenario: route registration is order-independent for static paths

  • WHEN /a/b is registered after /a/:p
  • THEN a request to /a/b matches the static route AND a request to /a/c matches the param route

Verified by: src/factory/create-hono-router.spec-conformance.test.ts::"hono-direct conforms to Router contract" AND src/factory/create-hono-router.test.ts::"static routes win over param routes when registered in either order"


Requirement: Worker-proxy surface implements the Router contract

The package SHALL export createHonoRouterWorkerProxy(port, opts) returning a Router whose calls are marshalled over a MessagePort via @suaveplan/api-worker-rpc. The proxy MUST pass the same runRouterConformance suite as the direct surface — no invariant may regress under marshalling.

Scenario: proxy passes every conformance invariant

  • WHEN runRouterConformance("hono-worker-proxy", () => createHonoRouterWorkerProxy(channel.port2, ...)) runs against a real MessageChannel bridged to a direct router on channel.port1
  • THEN every invariant passes AND no invariant is skipped

Scenario: AbortSignal propagates across the proxy

  • WHEN a route handler is invoked through the proxy AND the caller aborts via AbortSignal
  • THEN the in-flight handler on the host side observes signal.aborted === true within one event-loop tick

Verified by: src/proxy/create-hono-router-worker-proxy.spec-conformance.test.ts::"hono-worker-proxy conforms to Router contract" AND src/proxy/create-hono-router-worker-proxy.test.ts::"AbortSignal aborts the host-side handler"


Requirement: Dual-surface conformance gate

The adapter SHALL pass its capability's conformance suite under BOTH the direct surface AND the worker-proxy surface in the same test run. Failing either surface MUST fail the package's pre-publish gate.

Scenario: both surfaces run side by side

  • WHEN the conformance suite executes
  • THEN two describe blocks emit results — one labelled hono-direct, one labelled hono-worker-proxy — AND both report zero failures

Verified by: src/factory/create-hono-router.spec-conformance.test.ts::"hono-direct conforms to Router contract" AND src/proxy/create-hono-router-worker-proxy.spec-conformance.test.ts::"hono-worker-proxy conforms to Router contract"


Requirement: Blue-green slot management

The package SHALL expose HonoBlueGreenSlot implementing the BlueGreenSlot contract from api-router. It maintains two Hono instances and an atomic active-slot pointer. New requests route to the active slot; in-flight requests on the inactive slot complete normally. A swap() call MUST be gated on a health-probe result when strategy === "swap-on-health".

Scenario: in-flight requests complete on the old slot

  • WHEN a request is in-flight on the blue slot AND swap() cuts over to green
  • THEN the in-flight blue request completes its response AND any new request after swap() lands on green

Scenario: health-gated swap rolls back on failure

  • WHEN the new slot's health probe throws OR returns false
  • THEN the pointer remains on the old slot AND ROUTER_HONO_BLUE_GREEN_SWAP_FAILED is thrown to the caller

Verified by: src/blue-green/hono-blue-green-slot.test.ts::"in-flight requests complete on the old slot after swap" AND src/blue-green/hono-blue-green-slot.test.ts::"swap rolls back when health probe fails"


Requirement: Streaming routes bypass buffering

Routes registered with streaming: true SHALL receive the raw ReadableStream body and SHALL write responses via a streaming bridge that emits chunks to the underlying socket without intermediate buffering.

Scenario: streaming handler writes chunks incrementally

  • WHEN a streaming handler emits 3 chunks separated by event-loop ticks
  • THEN the client observes 3 distinct read events AND no buffering across the whole response

Verified by: src/streaming/streaming-bridge.test.ts::"streaming handler emits chunks incrementally without buffering"


Requirement: Options are Zod-validated

HonoRouterOptions SHALL be parsed through a Zod schema. Invalid options (negative maxBodyBytes, non-positive streamingTimeoutMs, unknown strategy) MUST throw at factory construction.

Scenario: invalid options throw at construction

  • WHEN createHonoRouter({ maxBodyBytes: -1 } as never) is called
  • THEN a ZodError-wrapped SuaveplanError with code ROUTER_HONO_INVALID_OPTIONS is thrown synchronously

Verified by: src/options/hono-router-options.test.ts::"invalid options throw at factory construction"


Requirement: Telemetry instruments emitted

The adapter SHALL emit api.router.match_duration_ms (histogram, tagged with route, method, outcome) and api.router.blue_green_swaps_total (counter) on every match and every successful swap respectively.

Scenario: histogram records on every route match

  • WHEN 5 requests are dispatched through createHonoRouter
  • THEN the in-memory MeterProvider records exactly 5 observations on api.router.match_duration_ms

Verified by: src/factory/create-hono-router.spec-conformance.test.ts::"emits api.router.match_duration_ms on every match"