first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
# sse-handler
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Hono handler factory that turns a subscription on `SseService` into a streaming HTTP response framed in the `text/event-stream` wire format. Handles per-connection heartbeats, abort-on-disconnect via the request's `AbortSignal`, `Last-Event-ID` resume from the ring buffer, and backpressure honoring the configured `queueHighWaterMark` and `overflowPolicy`. Returns a standard `Response` whose body is a `ReadableStream<Uint8Array>` so any modern runtime (Bun, Node 18+, Cloudflare Workers, Deno) streams without an adapter shim.
## Features
- Reads `:channel` from the route path; missing throws `SseError(SSE_CHANNEL_REQUIRED)` for the host's error handler to translate to 400.
- Reads `Last-Event-ID` from request headers and forwards to `service.subscribe` for replay.
- Optional `resolvePrincipalId(c)` lets consumers wire any auth stack (e.g. `c.get("user")?.id`) without this package depending on `@suaveplan/api-plugin-auth`.
- Optional `requirePrincipal: true` raises `SseError(SSE_UNAUTHENTICATED)` (HTTP 401) when the resolver returns `undefined`.
- Heartbeat scheduler writes a `: heartbeat\n\n` comment frame every `heartbeatIntervalMs` to keep idle connections alive through proxies; the timer is injectable (`setIntervalImpl`/`clearIntervalImpl`) for deterministic tests.
- Backpressure handling: when the writable controller's `desiredSize` falls to zero or the in-memory backlog exceeds `queueHighWaterMark`, applies `overflowPolicy` and increments `sse.queue_overflow_total{policy, code}`.
- Sets canonical response headers: `Content-Type: text/event-stream; charset=utf-8`, `Cache-Control: no-cache, no-transform`, `X-Accel-Buffering: no`, `Connection: keep-alive`.
## Usage: Basic
```ts
import { Hono } from "hono";
import { createSseRoute } from "@suaveplan/api-plugin-sse/middleware/sse-handler";
const app = new Hono();
app.get(
"/sse/:channel",
createSseRoute(sseService, {
heartbeatIntervalMs: 30_000,
queueHighWaterMark: 256,
overflowPolicy: "drop-oldest",
})
);
```
## Usage: Advanced
```ts
import { Hono } from "hono";
import { createSseRoute } from "@suaveplan/api-plugin-sse/middleware/sse-handler";
import { requireAuth } from "@suaveplan/api-plugin-auth/middleware/require-auth";
const app = new Hono();
// Compose with the auth plugin: require an authenticated principal,
// scope the principalId to the subscription cap.
app.get(
"/sse/:channel",
requireAuth(),
createSseRoute(sseService, {
heartbeatIntervalMs: 15_000,
queueHighWaterMark: 128,
overflowPolicy: "drop-newest",
resolvePrincipalId: (c) => c.get("user")?.id,
requirePrincipal: true,
})
);
```
## API Reference
- `interface CreateSseRouteOptions` — `{ heartbeatIntervalMs, queueHighWaterMark, overflowPolicy, resolvePrincipalId?, requirePrincipal?, setIntervalImpl?, clearIntervalImpl? }`.
- `createSseRoute(service: SseService, options: CreateSseRouteOptions): Handler` — returns a Hono handler.
## Implementation Notes
- The response body is constructed as `new ReadableStream<Uint8Array>` with an async producer running inside `start(controller)`. The producer drains `handle.events` via `for await`, frames each event with `formatSseFrame`, and pushes the encoded bytes through the controller.
- Cleanup is centralised in a single async closure that clears the heartbeat interval, calls `handle.close()`, and records the `sse.connection_duration_ms` histogram. It is invoked from both the iterator's `finally` branch and the stream's `cancel` callback so abort and natural end converge on the same code path.
- Backpressure is detected by checking `controller.desiredSize`; values of `null` (no high-water mark) and positive integers are treated as "not full". Zero or negative trigger the overflow policy.
- Heartbeat interval uses native `setInterval` by default; tests inject a fake to drive the timer deterministically (matching the FakeClock pattern used elsewhere ).