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,57 @@
# format
## Purpose
Serialises `SseEvent` objects into the `text/event-stream` wire format defined by the WHATWG HTML Standard so the SSE handler can write framed bytes directly to the response body. Also exposes a small helper for the heartbeat comment frame that keeps idle connections alive through proxies. Pure functions — no I/O, no state, suitable for use anywhere in the runtime.
## Features
- Canonical SSE framing: `id:`, `event:`, `retry:`, `data:` lines followed by the blank-line frame terminator.
- Multi-line string payloads expanded to one `data:` line per source line per the spec, so browser-side parsers join them with `\n` automatically.
- Non-string payloads passed through `JSON.stringify` so the wire format is always plain text.
- `formatHeartbeatFrame()` emits a `:`-prefixed comment frame that EventSource ignores but proxies see as activity.
## Usage: Basic
```ts
import { formatSseFrame } from "@suaveplan/api-plugin-sse/formatter/format";
const frame = formatSseFrame({ id: "42", event: "ping", data: { n: 1 } });
// "id: 42\nevent: ping\ndata: {\"n\":1}\n\n"
controller.enqueue(new TextEncoder().encode(frame));
```
## Usage: Advanced
```ts
import {
formatHeartbeatFrame,
formatSseFrame,
} from "@suaveplan/api-plugin-sse/formatter/format";
// Manual stream construction that bypasses the bundled handler.
const stream = new ReadableStream<Uint8Array>({
start(controller) {
const encoder = new TextEncoder();
const interval = setInterval(() => {
controller.enqueue(encoder.encode(formatHeartbeatFrame()));
}, 30_000);
// ... wire up your own subscriber loop
return () => clearInterval(interval);
},
});
// retry: hint for client reconnect-after value
const reconnectHint = formatSseFrame({ id: "0", retry: 5_000, data: "" });
```
## API Reference
- `formatSseFrame(event: SseEvent): string` — returns the canonical wire-format string. `event.data` is JSON-stringified when not already a string; multi-line strings are split into one `data:` line per source line.
- `formatHeartbeatFrame(): string` — returns `": heartbeat\n\n"`. EventSource ignores comment frames; HTTP intermediaries observe activity and keep the connection open.
## Implementation Notes
- The serialiser inserts a single space after the colon (`id: 42`, not `id:42`). The space is per spec recommendation and is stripped by the browser parser.
- Binary payloads are not supported — the wire format is line-oriented text. Base64-encode binary data into a string field if you need to transmit blobs.
- Field ordering (`id`, `event`, `retry`, `data`) is consistent across every call so dashboards relying on byte-level matching see stable output. The spec does not mandate ordering, but consistency simplifies snapshot tests downstream.

View File

@@ -0,0 +1,61 @@
# parse
## Purpose
Async streaming parser for `text/event-stream`. Decodes a `ReadableStreamDefaultReader<Uint8Array>` into a sequence of `SseEvent` objects, yielding one event per frame. The parser is used by the integration test client (which subscribes via `fetch` and reads the body manually) and is available to any consumer that needs to consume SSE without depending on the browser's `EventSource`.
## Features
- WHATWG-conformant frame parsing: handles `\n`, `\r\n`, and `\r` line endings; recognises `id`, `event`, `retry`, `data` fields; ignores comment lines (`:`) and unknown field names.
- Multi-line `data:` lines reassembled with `\n` (canonical browser behavior).
- Streaming: frame boundaries that span chunk reads are handled transparently via a buffered append-and-split.
- Zero dependencies — relies only on `TextDecoder` from the runtime.
## Usage: Basic
```ts
import { parseSseStream } from "@suaveplan/api-plugin-sse/formatter/parse";
const res = await fetch("/sse/notifications");
const reader = res.body!.getReader();
for await (const event of parseSseStream(reader)) {
console.log(event.id, JSON.parse(event.data as string));
}
```
## Usage: Advanced
```ts
import { parseSseStream } from "@suaveplan/api-plugin-sse/formatter/parse";
// Reconnect loop with Last-Event-ID resume.
let lastId: string | undefined;
while (true) {
const headers: HeadersInit = {};
if (lastId !== undefined) headers["Last-Event-ID"] = lastId;
const res = await fetch("/sse/c", { headers });
if (!res.ok || res.body === null) {
await new Promise((r) => setTimeout(r, 1_000));
continue;
}
try {
for await (const event of parseSseStream(res.body.getReader())) {
if (event.id !== "") lastId = event.id;
handle(event);
}
} catch {
// network blip — retry from lastId
}
}
```
## API Reference
- `parseSseStream(reader: ReadableStreamDefaultReader<Uint8Array>): AsyncIterableIterator<SseEvent>` — async iterator yielding one `SseEvent` per frame. The iterator ends when the underlying reader signals `done`.
## Implementation Notes
- The parser preserves the empty-string `data` field on retry-only frames so callers can observe `retry:` updates without inventing a sentinel for "no data".
- Comment-only frames (e.g. heartbeats) are skipped silently; if your application needs to observe heartbeats, switch to a custom reader or use the `sse.heartbeats_total` OTel counter the server emits.
- The parser does NOT validate UTF-8 sequences strictly — `TextDecoder` is invoked with `stream: true` and silently substitutes replacement characters for invalid bytes. This matches the browser's behavior.
- Each frame is parsed independently; there is no global cursor state between frames except the residual byte buffer, so the iterator is safe to abandon at any point (caller is responsible for cancelling the underlying reader).