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,74 @@
# `ReplayBuffer` + `createReplayBuffer`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`ReplayBuffer` is the per-`(tenant, topic)` ring buffer that powers the `Last-Event-Id` resume contract. Concrete engines call `append(event)` for every published event, `since(lastEventId)` when a client reconnects with a `Last-Event-Id` header, and `evictExpired(now)` from a periodic timer so the buffer stays within its `eventBufferMs` window.
The buffer is bounded by both an entry count (`eventBufferSize`, default 256) and an age (`eventBufferMs`, default 30 000 ms). When the count cap is reached, the oldest entry is dropped on the next `append`. When the age cap is reached, every expired entry is dropped on the next `evictExpired`. Unknown event ids — either never observed or already evicted — surface `SSE.LAST_EVENT_ID_UNKNOWN` so the engine can fall back to a live subscription.
## Features
- Size cap and age cap evict independently — either can trip first.
- Injectable `clock` so tests can fast-forward without `Date.now` mocking.
- `since(id)` returns every event AFTER the marker in append order; the marker itself is excluded.
- Throws `SseError` with `SSE.LAST_EVENT_ID_UNKNOWN` for evicted or never-seen ids.
## Usage: Basic
```ts
import { createReplayBuffer } from "@suaveplan/api-sse/replay-buffer/replay-buffer";
const buf = createReplayBuffer();
buf.append({ id: "1", data: "hello" });
buf.append({ id: "2", data: "world" });
const replayed = buf.since("1"); // [{ id: "2", data: "world" }]
```
## Usage: Advanced
```ts
import { createReplayBuffer } from "@suaveplan/api-sse/replay-buffer/replay-buffer";
import { SseError } from "@suaveplan/api-sse/error-codes/error-codes";
let t = 0;
const buf = createReplayBuffer({
eventBufferSize: 1024,
eventBufferMs: 60_000,
clock: () => t,
});
t = 0;
buf.append({ id: "evt-1", data: "x" });
t = 70_000;
buf.evictExpired(t); // drops evt-1
try {
buf.since("evt-1");
} catch (err) {
if (err instanceof SseError && err.code === "SSE.LAST_EVENT_ID_UNKNOWN") {
// fall back to live subscription
}
}
```
## API Reference
- `createReplayBuffer(options?: ReplayBufferOptions): ReplayBuffer`
- `interface ReplayBuffer`
- `append(event: SseEvent): void`
- `since(lastEventId: string): readonly SseEvent[]`
- `evictExpired(now: number): void`
- `size: number`
- `interface ReplayBufferOptions`
- `eventBufferSize?: number` (default 256, min 1)
- `eventBufferMs?: number` (default 30 000, min 1)
- `clock?: () => number` (default `Date.now`)
## Implementation Notes
The buffer is FIFO — `append` pushes onto the tail, `evictExpired` and the size cap shift off the head. `since(id)` performs an O(n) `findIndex`; for typical buffer sizes (≤ 1024) this is faster than maintaining a `Map<id, index>` parallel index because the constant factor on `findIndex` is roughly 1 ns / entry on V8. If a deployment shows hot-path latency at scale, swap the linear scan for a `Map<id, sequence-number>` and the public API stays the same.