first commit
This commit is contained in:
62
docs/api-stack/api/api-router-hono/README.md
Normal file
62
docs/api-stack/api/api-router-hono/README.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# @suaveplan/api-router-hono — concrete Hono router adapter for the @suaveplan/api plugin system. Implements the abstract Router contract from @suaveplan/api-router on top of a Hono instance, including the HonoBlueGreenSlot atomic-flip primitive and bidirectional Middleware <-> Hono MiddlewareHandler adapters. Passes runRouterConformance.
|
||||
|
||||
Stage-4 concrete Hono router adapter for `@suaveplan/api`. Implements the abstract `Router` contract from `@suaveplan/api-router` on top of a Hono `Hono` instance and provides the matching `BlueGreenSlot` implementation for hot-reload.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [adapter](./src/adapter/adapter.md) — `HonoRouter` is The concrete `Router` adapter for the API plugin system. It implements every public method…
|
||||
- [adapter](./src/adapter/adapter.md) — `HonoRouter` is The concrete `Router` adapter for the API plugin system. It implements every public method…
|
||||
- [blue-green-slot](./src/blue-green-slot/blue-green-slot.md) — `HonoBlueGreenSlot` is the per-plugin atomic flip primitive backing hot-reload for a Hono-backed `Router`. It holds at…
|
||||
- [blue-green-slot](./src/blue-green-slot/blue-green-slot.md) — `HonoBlueGreenSlot` is the per-plugin atomic flip primitive backing hot-reload for a Hono-backed `Router`. It holds at…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — This module defines the canonical, frozen error-code namespace `ROUTER_HONO.*` shared by every production path inside `…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — This module defines the canonical, frozen error-code namespace `ROUTER_HONO.*` shared by every production path inside `…
|
||||
- [middleware-adapter](./src/middleware-adapter/middleware-adapter.md) — This module bidirectionally bridges the onion-model `Middleware` type from `@suaveplan/api-router` and Hono's n…
|
||||
- [middleware-adapter](./src/middleware-adapter/middleware-adapter.md) — This module bidirectionally bridges the onion-model `Middleware` type from `@suaveplan/api-router` and Hono's n…
|
||||
- [pipeline](./src/pipeline/pipeline.md) — `@suaveplan/api-host`'s request pipeline drives its routing through a `PipelineRouter`-shaped seam — `match(method, url…
|
||||
- [pipeline](./src/pipeline/pipeline.md) — `@suaveplan/api-host`'s request pipeline drives its routing through a `PipelineRouter`-shaped seam — `match(method, url…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## What it ships
|
||||
|
||||
- `HonoRouter` / `createHonoRouter()` — concrete `Router` adapter that subclasses the abstract base, mirrors every verb registration onto a private Hono app, and exposes `getHonoApp()` for ops integrations.
|
||||
- `HonoBlueGreenSlot` — `BlueGreenSlot` impl with atomic flip, in-flight counter, AbortSignal-aware `dispose(timeoutMs)`.
|
||||
- `toHonoMiddleware` / `fromHonoMiddleware` — bidirectional bridge between the onion-model `Middleware` and Hono's `MiddlewareHandler`.
|
||||
- `ROUTER_HONO_CODES` / `RouterHonoError` — stable error-code registry (`ROUTER_HONO.HONO_INTERNAL_ERROR`, `ROUTER_HONO.INVALID_HONO_PEER`).
|
||||
|
||||
## Conformance
|
||||
|
||||
The package passes `runRouterConformance` from `@suaveplan/api-router/testing` — see `tests/conformance.test.ts`. Adding new behaviours requires re-running the harness against the adapter.
|
||||
|
||||
## Modules
|
||||
|
||||
- [`src/adapter/adapter.md`](./src/adapter/adapter.md) — adapter façade + factory.
|
||||
- [`src/blue-green-slot/blue-green-slot.md`](./src/blue-green-slot/blue-green-slot.md) — hot-reload primitive.
|
||||
- [`src/middleware-adapter/middleware-adapter.md`](./src/middleware-adapter/middleware-adapter.md) — middleware bridge.
|
||||
- [`src/error-codes/error-codes.md`](./src/error-codes/error-codes.md) — error registry.
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { createHonoRouter, HonoBlueGreenSlot } from "@suaveplan/api-router-hono";
|
||||
|
||||
const router = createHonoRouter({ plugin: "billing" });
|
||||
router.get("/invoices/:id", () => null);
|
||||
|
||||
const slot = new HonoBlueGreenSlot("billing", () => "v1");
|
||||
slot.setShadow(() => "v2");
|
||||
const draining = slot.flip();
|
||||
await slot.dispose(draining.slotId, { timeoutMs: 1_000 });
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `@suaveplan/api-router` — abstract contract + conformance harness.
|
||||
- `@suaveplan/api-types` — `OtelSpanLike`, `TenantId`, `Principal`.
|
||||
- `@suaveplan/error` — `SuaveplanError` + `defineCodes`.
|
||||
- `hono` (peer ≥4.0.0) — wire-side dispatch.
|
||||
|
||||
## Version
|
||||
|
||||
`0.1.0` — initial Stage-4 ship. Graduates to `1.0.0` when the plugin system as a whole is declared production-ready.
|
||||
|
||||
64
docs/api-stack/api/api-router-hono/src/adapter/adapter.md
Normal file
64
docs/api-stack/api/api-router-hono/src/adapter/adapter.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# `adapter` — Hono-backed `Router` adapter
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`HonoRouter` is The concrete `Router` adapter for the API plugin system. It implements every public method of the abstract `Router` contract from `@suaveplan/api-router` (all 7 verb methods, `all`, `use`, `mount`, `match`, `collidesWith`, `chain`) and mirrors every route registration onto a private Hono `Hono` instance. The abstract base owns the match table and collision detection; the Hono instance owns the wire-side dispatch when the host wires a real listener via `getHonoApp()`.
|
||||
|
||||
The adapter exists so a plugin's `RouteSpec[]` can serve traffic through Hono's fast trie router while still being introspectable by the host's collision detector, blue-green slot manager, and OpenAPI emitter — all of which speak the abstract `Router` surface.
|
||||
|
||||
## Features
|
||||
|
||||
- Subclasses `Router`, inheriting `match`/`collidesWith`/`mount` verbatim.
|
||||
- Mirrors registrations onto a private Hono app with a 501 stub handler so the abstract pipeline owns dispatch.
|
||||
- `getHonoApp()` exposes the underlying Hono instance for ops integrations and direct middleware installation.
|
||||
- `createHonoRouter()` factory returns the adapter typed as the abstract `Router` so the `runRouterConformance` harness accepts it unchanged.
|
||||
- Wraps Hono-internal registration throws in `RouterHonoError` (`ROUTER_HONO.HONO_INTERNAL_ERROR`).
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { createHonoRouter } from "@suaveplan/api-router-hono/adapter";
|
||||
|
||||
const router = createHonoRouter({ plugin: "billing" });
|
||||
router.get("/invoices/:id", () => null);
|
||||
const match = router.match("GET", "/invoices/inv_42");
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { HonoRouter } from "@suaveplan/api-router-hono/adapter";
|
||||
import { Hono } from "hono";
|
||||
|
||||
const router = new HonoRouter({ plugin: "tenants" });
|
||||
router.post("/tenants", { auth: { required: true } }, () => null);
|
||||
router.mount(
|
||||
"/admin",
|
||||
(() => {
|
||||
const child = new HonoRouter({ plugin: "admin" });
|
||||
child.get("/healthz", () => null);
|
||||
return child;
|
||||
})()
|
||||
);
|
||||
|
||||
const app: Hono = router.getHonoApp();
|
||||
app.use("*", async (c, next) => {
|
||||
c.header("x-app", "suaveplan");
|
||||
await next();
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class HonoRouter extends Router` — public constructor `new HonoRouter({ plugin? })`.
|
||||
- `router.getHonoApp(): Hono` — accessor.
|
||||
- `createHonoRouter(options?): Router` — factory returning the conformance-friendly `Router` view.
|
||||
- Throws `RouterHonoError` with `ROUTER_HONO.HONO_INTERNAL_ERROR` on a Hono-side registration failure.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`mirror()` normalises trailing slashes and dispatches to the appropriate Hono verb method. `HEAD` is registered via `Hono.on("HEAD", path, ...)` because the runtime Hono surface omits a dedicated `.head()` shorthand. The mirror stub returns a 501 because in-process dispatch goes through the abstract `Router.match()` path — the Hono app is wired only when the host attaches a `fetch` adapter. The match table from the abstract base is the single source of truth for the per-request pipeline (design doc §6).
|
||||
@@ -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.
|
||||
@@ -0,0 +1,52 @@
|
||||
# `error-codes` — stable Router-Hono error registry
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module defines the canonical, frozen error-code namespace `ROUTER_HONO.*` shared by every production path inside `@suaveplan/api-router-hono`. Stable codes are how downstream observers (`@suaveplan/api-host`, structured-log consumers, OpenTelemetry dashboards) discriminate adapter failures from generic application errors without resorting to brittle string matching on `error.message`.
|
||||
|
||||
The registry is intentionally narrow — Hono itself owns most failure modes (404, 405, body parse, etc.) and surfaces them as ordinary HTTP responses. The two `ROUTER_HONO.*` codes carved out here cover the two scenarios where the adapter cannot delegate cleanly to Hono: a Hono-internal throw bubbling through the bridge, and a programmer-supplied peer (`getHonoApp()` consumers handing back a non-Hono shim).
|
||||
|
||||
## Features
|
||||
|
||||
- `ROUTER_HONO.HONO_INTERNAL_ERROR` — wraps any unexpected throw originating inside the Hono request/response bridge.
|
||||
- `ROUTER_HONO.INVALID_HONO_PEER` — guard for the `getHonoApp()` accessor and middleware-adapter when a non-Hono value is supplied.
|
||||
- `RouterHonoError` extends `SuaveplanError<RouterHonoCode>` so cause chains and Zod-validated wire format flow through `@suaveplan/error` unchanged.
|
||||
- `defineCodes` produces a frozen, namespaced map — duplicate codes fail at module load.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { ROUTER_HONO_CODES, RouterHonoError } from "@suaveplan/api-router-hono/error-codes";
|
||||
|
||||
throw new RouterHonoError(
|
||||
ROUTER_HONO_CODES.HONO_INTERNAL_ERROR,
|
||||
"hono raised during dispatch",
|
||||
{ cause: caught },
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { RouterHonoError } from "@suaveplan/api-router-hono/error-codes";
|
||||
import type { RouterHonoCode } from "@suaveplan/api-router-hono/error-codes";
|
||||
|
||||
function handle(err: unknown): RouterHonoCode | null {
|
||||
if (err instanceof RouterHonoError) return err.code;
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `ROUTER_HONO_CODES` — `Readonly<Record<"HONO_INTERNAL_ERROR" | "INVALID_HONO_PEER", "ROUTER_HONO.HONO_INTERNAL_ERROR" | "ROUTER_HONO.INVALID_HONO_PEER">>`.
|
||||
- `RouterHonoCode` — string-literal union of the resolved codes.
|
||||
- `RouterHonoError` — `SuaveplanError<RouterHonoCode>` subclass with `code`, `message`, optional `cause`, optional `context`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`defineCodes` enforces alphabetisation and uniqueness at build time. Adding a code requires extending the tuple here AND wiring at least one production throw site (see `adapter.ts` and `middleware-adapter.ts`). The registry is consumed by the `error-codes.test.ts` enumeration which asserts every entry maps to a value of the form `ROUTER_HONO.<NAME>` so observers can pattern-match the namespace prefix.
|
||||
@@ -0,0 +1,65 @@
|
||||
# `middleware-adapter` — Middleware ⇄ Hono MiddlewareHandler bridge
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module bidirectionally bridges the onion-model `Middleware` type from `@suaveplan/api-router` and Hono's native `MiddlewareHandler`. The bridge is what lets a `Router` adapter installed on top of a Hono app participate in the same per-request pipeline — inbound headers become `RouterRequest`, outbound builder results become Web `Response` objects, and downstream Hono middlewares observe the `next()` boundary as their own.
|
||||
|
||||
The conversion is intentionally one-directional in effect: `toHonoMiddleware` runs the suaveplan middleware ahead of Hono's downstream chain, while `fromHonoMiddleware` defers strictly to the `next()` because Hono context cannot be safely synthesised outside a real dispatch. Real Hono middlewares get installed via `getHonoApp().use(...)` directly.
|
||||
|
||||
## Features
|
||||
|
||||
- `toHonoMiddleware(mw)` — adapts a `Middleware` to a `MiddlewareHandler` Hono can install.
|
||||
- `fromHonoMiddleware(handler)` — produces a `Middleware` that defers to the chain; throws `ROUTER_HONO.INVALID_HONO_PEER` for non-functions.
|
||||
- Header/query reflection from `c.req.raw` to `MapHeaders` so handlers run against the canonical suaveplan
|
||||
- Wraps Hono-internal throws in `RouterHonoError` with code `HONO_INTERNAL_ERROR` and a cause-chain.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { toHonoMiddleware } from "@suaveplan/api-router-hono/middleware-adapter";
|
||||
import type { Middleware } from "@suaveplan/api-router";
|
||||
import { Hono } from "hono";
|
||||
|
||||
const trace: Middleware = async (req, next) => {
|
||||
console.log(req.requestId);
|
||||
return next();
|
||||
};
|
||||
|
||||
const app = new Hono();
|
||||
app.use("*", toHonoMiddleware(trace));
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
fromHonoMiddleware,
|
||||
toHonoMiddleware,
|
||||
} from "@suaveplan/api-router-hono/middleware-adapter";
|
||||
import { createHonoRouter } from "@suaveplan/api-router-hono/adapter";
|
||||
|
||||
const router = createHonoRouter();
|
||||
|
||||
router.use(async (req, next) => {
|
||||
const res = await next();
|
||||
return { ...res, headers: { ...res.headers, "x-trace": req.requestId } };
|
||||
});
|
||||
|
||||
const app = router.getHonoApp();
|
||||
app.use("*", toHonoMiddleware(async (_req, next) => next()));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `toHonoMiddleware(mw: Middleware): MiddlewareHandler` — wraps a suaveplan middleware.
|
||||
- `fromHonoMiddleware(handler: MiddlewareHandler): Middleware` — wraps a Hono middleware.
|
||||
|
||||
Both helpers preserve the `RouterError` / `RouterHonoError` cause chains so observability stays end-to-end.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`contextToRouterRequest` reads `x-tenant-id` and `x-request-id` headers (case-insensitive) to populate the corresponding fields. Both default to safe stand-ins (`"default"` tenant, time-derived request id) so unit tests bypass middleware setup. `routerResponseToWebResponse` JSON-stringifies non-string bodies and sets `content-type: application/json` when no body content-type is already present.
|
||||
130
docs/api-stack/api/api-router-hono/src/pipeline/pipeline.md
Normal file
130
docs/api-stack/api/api-router-hono/src/pipeline/pipeline.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# `pipeline` — `HonoRouter`'s `PipelineRouter` facet
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`@suaveplan/api-host`'s request pipeline drives its routing through a
|
||||
`PipelineRouter`-shaped seam — `match(method, url) → PipelineRoute | null` —
|
||||
where `PipelineRoute` carries `plugin`, `path`, `options`, `handler`,
|
||||
`params`, `statusMap`. That shape is **NOT** the same as
|
||||
`@suaveplan/api-router`'s abstract `Router.match(...)` (which returns a
|
||||
`RouteMatch`), so the original `HonoRouter` adapter did not satisfy the host
|
||||
contract. Foundation-coherence Gap #1 closes that mismatch by adding a
|
||||
**second facet** to `HonoRouter` — `registerPipelineRoute(...)` +
|
||||
`matchPipeline(...)` — backed by a parallel metadata table that lives next
|
||||
to the existing abstract-router table.
|
||||
|
||||
The facet is additive and structural. The host depends on
|
||||
`@suaveplan/api-router-hono` not at all; the wiring is purely shape-driven —
|
||||
`HonoRouter.matchPipeline()` returns a value assignable to
|
||||
`@suaveplan/api-types/pipeline-router`'s `PipelineRoute` interface because every required
|
||||
field is present at the same path-type.
|
||||
|
||||
## Features
|
||||
|
||||
- **Parallel metadata table** — `PipelineRouteTable` keyed by
|
||||
`${method}:${normalisedPath}`. Distinct from the abstract router's
|
||||
`routes[]`, so the two facets evolve independently.
|
||||
- **Plugin ownership** — every registration carries a `plugin` field; the
|
||||
host pipeline reads it directly off the matched route.
|
||||
- **Status map propagation** — each route stores an optional
|
||||
`Record<string, number>` (the manifest's plugin-error → HTTP status
|
||||
overrides) and the host reads it on failure-envelope construction.
|
||||
- **Collision detection** — registering the same `(method, path-shape)`
|
||||
under a DIFFERENT plugin throws
|
||||
`ROUTER_HONO.PIPELINE_ROUTE_COLLISION`. Re-registering by the same
|
||||
plugin overwrites (the host reload-driver hot-reload flow).
|
||||
- **Param parsing** — `:id` segments are decoded via
|
||||
`decodeURIComponent` and surfaced in the returned route's `params`.
|
||||
- **Hot-reload teardown** — `unregisterPipelinePlugin(plugin)` removes
|
||||
every entry owned by `plugin` in a single call; used by the host
|
||||
reload driver between versions.
|
||||
- **Snapshot** — `listPipelineRoutes()` returns a readonly view for admin
|
||||
tooling.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { HonoRouter } from "@suaveplan/api-router-hono/adapter";
|
||||
|
||||
const r = new HonoRouter();
|
||||
r.registerPipelineRoute({
|
||||
plugin: "billing",
|
||||
method: "GET",
|
||||
path: "/invoices/:id",
|
||||
handler: async (ctx) => ({ id: (ctx as { params: { id: string } }).params.id }),
|
||||
statusMap: { "BILLING.NOT_FOUND": 404 },
|
||||
});
|
||||
|
||||
const match = r.matchPipeline("GET", "/invoices/inv_42");
|
||||
// → { plugin: "billing", path: "/invoices/:id", params: { id: "inv_42" }, ... }
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { HonoRouter, PipelineRouteTable } from "@suaveplan/api-router-hono";
|
||||
|
||||
// Pre-build a metadata table outside the router (e.g. inside a host
|
||||
// reload driver) and replay it into a fresh router on flip.
|
||||
const table = new PipelineRouteTable();
|
||||
table.register({
|
||||
plugin: "p",
|
||||
method: "POST",
|
||||
path: "/x",
|
||||
handler: () => null,
|
||||
options: { auditable: true, idempotent: true },
|
||||
});
|
||||
|
||||
const r = new HonoRouter();
|
||||
for (const route of table.list()) {
|
||||
r.registerPipelineRoute(route);
|
||||
}
|
||||
|
||||
// Hot-reload: tear down the prior version of `billing` then load the new
|
||||
// route set.
|
||||
r.unregisterPipelinePlugin("billing");
|
||||
r.registerPipelineRoute({
|
||||
plugin: "billing",
|
||||
method: "GET",
|
||||
path: "/invoices/:id",
|
||||
handler: newHandler,
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class PipelineRouteTable` —
|
||||
- `register(reg: HonoPipelineRegistration): void`
|
||||
- `unregister(method: string, path: string): void`
|
||||
- `unregisterPlugin(plugin: string): void`
|
||||
- `list(): readonly HonoPipelineRoute[]`
|
||||
- `match(method: string, url: string): HonoPipelineRoute | null`
|
||||
- `size(): number`
|
||||
- `interface HonoPipelineRoute` — `{ plugin, method, path, options, handler, params, statusMap }`. Structurally compatible with `@suaveplan/api-types/pipeline-router`'s `PipelineRoute`.
|
||||
- `interface HonoPipelineRegistration` — `{ plugin, method, path, handler, options?, statusMap? }`.
|
||||
- `interface HonoPipelineRouteOptions` — narrow subset of the host's `PipelineRouteOptions`: `auth`, `csrf`, `streaming`, `idempotent`, `runtimeValidation`, `auditable`, `rateLimit`, `request`, `responses`.
|
||||
- `interface HonoPipelineHandler` — `(ctx: unknown) => Promise<unknown> | unknown`. `ctx` is the host's `RequestContext` at runtime; the adapter does not enforce the type to avoid an inverted dep on `@suaveplan/api-types`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The table stores a `Map<shapeKey, StoredEntry>` where `shapeKey` is
|
||||
`${method}:${path-with-:-erased}` — `/users/:id` and `/users/:other` map
|
||||
to the same key, mirroring `@suaveplan/api-router`'s collision-detection
|
||||
semantics. Path normalisation strips trailing slashes and tolerates both
|
||||
bare paths and full URLs in `match()`.
|
||||
|
||||
Param decoding uses `decodeURIComponent`. Non-URL-shaped inputs to
|
||||
`match()` fall back to a manual `?`-split so the implementation handles
|
||||
the host pipeline's pass-through of `request.url` (always a full URL) and
|
||||
the test harness's path-only calls (`"/x"`).
|
||||
|
||||
The facet is intentionally decoupled from Hono's HTTP dispatch path. Hono
|
||||
still owns request handling for callers that invoke `app.fetch(req)`
|
||||
directly (ops integrations, raw-Hono middleware). The pipeline facet is a
|
||||
metadata sidecar — the host's `RequestPipeline.handle(...)` consults the
|
||||
sidecar before invoking the matched handler with its own constructed
|
||||
`RequestContext`.
|
||||
Reference in New Issue
Block a user