first commit
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user