117 lines
6.1 KiB
Markdown
117 lines
6.1 KiB
Markdown
<!-- markdownlint-disable MD024 -->
|
|
|
|
# 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"`
|
|
|
|
<!-- markdownlint-enable MD024 -->
|