Initial Commit
This commit is contained in:
64
openspec/archive/2026-05-26-api-sse-bun/proposal.md
Normal file
64
openspec/archive/2026-05-26-api-sse-bun/proposal.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# `@suaveplan/api-sse-bun` — `Bun.serve` Server-Sent Events engine
|
||||
|
||||
**Package**: `@suaveplan/api-sse-bun`
|
||||
**Status**: PROPOSED
|
||||
**Type**: new-package
|
||||
**Stage**: 7 (GraphQL + WebSocket / SSE engines) — see umbrella `openspec/changes/api-plugin-foundation/`
|
||||
|
||||
## Why
|
||||
|
||||
Per design doc §11.6 SSE is the simpler cousin of WebSocket — useful for activity feeds, dashboards, and notifications where no client-to-server channel and no ack semantics are needed. SSE falls back gracefully on networks that block WebSocket (corporate proxies, restrictive firewalls), making it a deliberately important second transport. This package implements the abstract `api-sse` contract on top of `Bun.serve` and the same broker abstraction as `api-ws-bun`, so multi-host fan-out is handled by the same Redis broker (`api-ws-broker-redis`) without duplication.
|
||||
|
||||
Without this adapter the foundation has no production SSE story and falls back to single-transport-only deployments.
|
||||
|
||||
## What changes
|
||||
|
||||
Net-new package depending on `@suaveplan/api-types`, `@suaveplan/api-sse` (abstract contract), `@suaveplan/api-router` (HTTP endpoint mount), `@suaveplan/error`, `@suaveplan/logger`, `@suaveplan/opentelemetry`, and `zod`.
|
||||
|
||||
- ADDED: `createBunSseEngine(opts)` — factory returning the engine value implementing `@suaveplan/api-sse`'s contract.
|
||||
- ADDED: `BunSseEngineOptions` — Zod-validated config: `eventBufferSize` (default 256 events per `(tenant, topic)`), `eventBufferMs` (default 300000 = 5 min, pairs with WS reconnect TTL), `keepAliveCommentMs` (default 15000 — emits SSE comments to prevent intermediary timeouts), `maxConnectionsPerTenant` (default 1000), `compressionEnabled` (default `true` — uses `Content-Encoding: gzip` when the client accepts it), `retryMs` (default 3000 — written into `retry:` field of the SSE stream so clients back off appropriately).
|
||||
- ADDED: Endpoint mount — plugin's manifested SSE endpoints (`sse: [{ path, auth, subscriptions }]`) are mounted via `api-router` as `GET <path>` returning `Content-Type: text/event-stream`.
|
||||
- ADDED: `Last-Event-Id` resume — when the client supplies `Last-Event-Id` header (HTTP standard), the engine consults the event buffer (broker-backed when Redis broker wired, in-memory otherwise) and replays events with id > supplied value before resuming live delivery.
|
||||
- ADDED: Per-`(tenant, topic)` event buffer — for single-host deployments, in-memory ring buffer of `eventBufferSize` events with `eventBufferMs` TTL. For multi-host deployments, delegates to the wired broker (`api-ws-broker-redis` exposes the same buffer surface; SSE consumes it identically to WS).
|
||||
- ADDED: Keep-alive comments — `:keepalive\n\n` emitted every `keepAliveCommentMs` to prevent intermediary proxy timeouts. This is invisible to clients.
|
||||
- ADDED: Subscription model — `conn.subscribe(topic, { tenant?, filter? })` registers against the host's subscription bus (same bus used by WS). `bus.broadcast` triggers fan-out to SSE connections matching `(topic, tenant)`.
|
||||
- ADDED: Per-tenant cap — `maxConnectionsPerTenant` enforced at request time; exceeding returns 429 with `SSE_BUN_TENANT_CAP_EXCEEDED`.
|
||||
- ADDED: `SSE_BUN_*` error codes: `SSE_BUN_HANDSHAKE_REJECTED`, `SSE_BUN_TENANT_CAP_EXCEEDED`, `SSE_BUN_BUFFER_MISS`, `SSE_BUN_EVENT_TOO_LARGE`, `SSE_BUN_LAST_EVENT_ID_INVALID`, `SSE_BUN_COMPRESSION_FAILED`, `SSE_BUN_STREAM_INTERRUPTED`, `SSE_BUN_PRINCIPAL_EXPIRED`, `SSE_BUN_BROKER_UNAVAILABLE`, `SSE_BUN_INVALID_TOPIC`.
|
||||
- ADDED: OTel surface from design §11.7 — `sse.connections { plugin, path, tenant }` observable gauge, `sse.events_sent_total { plugin, topic }` counter, plus this package adds `sse.handshake.duration_ms`, `sse.buffer_replays_total { hit }`, `sse.compression_ratio` histogram.
|
||||
- ADDED: `./testing` subpath — re-export of `runSseEngineConformance` from `api-sse/testing` bound to `createBunSseEngine`.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- A non-Bun SSE engine — the contract lives in `api-sse`; other engines can implement it.
|
||||
- Cross-host fan-out logic — delegated to `api-ws-broker-redis` (which is also the SSE broker, per §11.6).
|
||||
- Bi-directional channels — SSE is server→client only by design.
|
||||
- Browser polyfills — modern browsers + recent Node clients support SSE natively.
|
||||
|
||||
## Source mapping
|
||||
|
||||
Genesis-original. Source of design intent: `docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md`.
|
||||
|
||||
| Requirement | Design doc section |
|
||||
|---|---|
|
||||
| SSE endpoint declared in manifest | §3 (`sse` block) + §11.6 |
|
||||
| `Last-Event-Id` resume | §11.5 (paragraph 3) + §11.6 |
|
||||
| Event buffer per `(tenant, topic)` with size + ttl | §11.5 (paragraph 3) |
|
||||
| Shared broker with WS for multi-host fan-out | §11.6 |
|
||||
| Same subscription bus as WS | §11.6 |
|
||||
| OTel `sse.connections` gauge + `sse.events_sent_total` counter | §11.7 |
|
||||
| Graceful fall-back when WS blocked | §11.6 |
|
||||
|
||||
## Verification approach
|
||||
|
||||
- `src/engine/bun-sse-engine.test.ts` — handshake, event emission framing, keep-alive comment cadence, principal frozen at handshake.
|
||||
- `src/engine/bun-sse-engine.spec-conformance.test.ts` — runs `runSseEngineConformance("bun", createBunSseEngine)`.
|
||||
- `src/last-event-id/last-event-id.test.ts` — supply `Last-Event-Id: 7`, assert events 8..N replay before live delivery.
|
||||
- `src/event-buffer/event-buffer.test.ts` — in-memory mode: ring-buffer cap, TTL eviction. Broker mode (integration, 30000ms): delegation to broker-backed buffer.
|
||||
- `src/limits/tenant-cap.test.ts` — 1001st connection rejected with 429.
|
||||
- `src/keep-alive/keep-alive.test.ts` (FakeClock) — `:keepalive` comments emitted every `keepAliveCommentMs`.
|
||||
- `tests/integration/end-to-end.test.ts` (30000ms) — real `Bun.serve`, real SSE client, full subscribe → broadcast → keep-alive → disconnect → reconnect-with-Last-Event-Id.
|
||||
- `tests/integration/multi-host-fan-out.test.ts` (60000ms) — two SSE engines behind `api-ws-broker-redis`; broadcast from one delivers on the other.
|
||||
|
||||
## Migration impact
|
||||
|
||||
None — additive only.
|
||||
@@ -0,0 +1,158 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: api-sse-bun (api-sse-bun)
|
||||
|
||||
**Package**: `@suaveplan/api-sse-bun`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement carries a `**Verified by:**` line.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Implements the abstract SSE engine contract
|
||||
|
||||
The package SHALL export `createBunSseEngine(opts)` producing an engine value that implements the engine contract from `@suaveplan/api-sse`. The conformance suite `runSseEngineConformance` MUST pass against this adapter.
|
||||
|
||||
#### Scenario: conformance suite passes
|
||||
|
||||
- **WHEN** `runSseEngineConformance("bun", createBunSseEngine)` runs
|
||||
- **THEN** every invariant passes
|
||||
|
||||
**Verified by:** `src/engine/bun-sse-engine.spec-conformance.test.ts::"createBunSseEngine passes runSseEngineConformance"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: SSE framing emits valid event-stream output
|
||||
|
||||
The engine SHALL emit `Content-Type: text/event-stream`. Each event MUST be framed as `id: <eventId>\nevent: <topic>\ndata: <JSON-encoded payload>\n\n`. A `retry: <retryMs>\n` line MUST be sent at stream start (default 3000) so clients back off appropriately on disconnect. Keep-alive comments `:keepalive\n\n` MUST be emitted every `keepAliveCommentMs` (default 15000) — invisible to clients but prevent intermediary proxy timeouts.
|
||||
|
||||
#### Scenario: first emitted event includes id, event, data lines
|
||||
|
||||
- **WHEN** a broadcast triggers an event delivered to a subscriber
|
||||
- **THEN** the stream body contains `id: <n>`, `event: <topic>`, and `data: <json>` lines terminated by a blank line
|
||||
|
||||
#### Scenario: retry directive emitted at stream start
|
||||
|
||||
- **WHEN** a subscriber connects with default `retryMs: 3000`
|
||||
- **THEN** the first bytes of the response include `retry: 3000\n`
|
||||
|
||||
#### Scenario: keep-alive comment cadence
|
||||
|
||||
- **WHEN** no real events for `keepAliveCommentMs + 1`ms
|
||||
- **THEN** the stream contains at least one `:keepalive\n\n` line
|
||||
|
||||
**Verified by:** `src/stream/sse-stream.test.ts::"emitted event includes id, event, data lines"` AND `src/stream/sse-stream.test.ts::"retry directive emitted at stream start"` AND `src/keep-alive/keep-alive.test.ts::"keep-alive comment emitted every keepAliveCommentMs"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Last-Event-Id replay before live delivery
|
||||
|
||||
When a client supplies `Last-Event-Id: <n>` header on connect (HTTP standard, automatically sent by browser `EventSource` on reconnect), the engine SHALL consult the event buffer for `(tenant, topic)` AND replay every event with id > n in order BEFORE switching to live delivery. The replay set MUST include at most `eventBufferSize` events; missed-window cases (buffer evicted older events) MUST set `SSE_BUN_BUFFER_MISS` on a comment line so the client can react. A malformed `Last-Event-Id` value MUST be refused with `SSE_BUN_LAST_EVENT_ID_INVALID`.
|
||||
|
||||
#### Scenario: replay delivers events with id > Last-Event-Id
|
||||
|
||||
- **WHEN** the buffer holds events 1..10 AND a client reconnects with `Last-Event-Id: 7`
|
||||
- **THEN** events 8, 9, 10 are delivered in order BEFORE any new live event
|
||||
|
||||
#### Scenario: buffer miss surfaces as a comment line
|
||||
|
||||
- **WHEN** the buffer's oldest event is id 5 AND a client reconnects with `Last-Event-Id: 2`
|
||||
- **THEN** the stream emits a comment line containing `SSE_BUN_BUFFER_MISS` AND switches to live delivery without replay
|
||||
|
||||
#### Scenario: malformed Last-Event-Id refused
|
||||
|
||||
- **WHEN** the header value is `"not-a-number"`
|
||||
- **THEN** the connection is refused with `SSE_BUN_LAST_EVENT_ID_INVALID`
|
||||
|
||||
**Verified by:** `src/last-event-id/last-event-id.test.ts::"replay delivers events with id greater than Last-Event-Id"` AND `src/last-event-id/last-event-id.test.ts::"buffer miss surfaces SSE_BUN_BUFFER_MISS comment"` AND `src/last-event-id/last-event-id.test.ts::"malformed Last-Event-Id refused"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Event buffer storage follows wired broker
|
||||
|
||||
The engine SHALL maintain a per-`(tenant, topic)` event buffer:
|
||||
|
||||
- When a broker (`api-ws-broker-redis`) is wired into the host, the buffer lives in the broker (Redis sorted-set shared with the WS engine).
|
||||
- Otherwise the buffer lives in-memory on the local host as a ring buffer capped at `eventBufferSize` (default 256) with TTL `eventBufferMs` (default 300000).
|
||||
|
||||
This is the same default as `api-ws-bun`'s reconnect event-buffer strategy (design §11.5 paragraph 4), pinned for the SSE side here.
|
||||
|
||||
#### Scenario: broker-backed buffer used when wired
|
||||
|
||||
- **WHEN** a Redis broker is wired AND an event is broadcast
|
||||
- **THEN** the event is observable in the broker's buffer surface (asserted via `broker.readBuffer(tenant, topic, 0)`)
|
||||
|
||||
#### Scenario: in-memory buffer used when no broker wired
|
||||
|
||||
- **WHEN** no broker is wired AND `eventBufferSize: 4` AND 5 events are emitted for the same `(tenant, topic)`
|
||||
- **THEN** the in-memory buffer holds the 4 most recent
|
||||
|
||||
**Verified by:** `src/event-buffer/event-buffer.test.ts::"broker-backed buffer used when broker wired"` AND `src/event-buffer/event-buffer.test.ts::"in-memory buffer evicts oldest when broker not wired"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Per-tenant connection cap
|
||||
|
||||
The engine SHALL track active SSE connections per tenant. A new connection request when a tenant is at `maxConnectionsPerTenant` (default 1000) MUST return HTTP 429 with `SSE_BUN_TENANT_CAP_EXCEEDED`.
|
||||
|
||||
#### Scenario: 1001st connection from same tenant rejected
|
||||
|
||||
- **WHEN** `maxConnectionsPerTenant: 1000` AND tenant T has 1000 active SSE connections
|
||||
- **THEN** the 1001st returns 429 with `SSE_BUN_TENANT_CAP_EXCEEDED`
|
||||
|
||||
**Verified by:** `src/limits/tenant-cap.test.ts::"1001st SSE connection from same tenant rejected with SSE_BUN_TENANT_CAP_EXCEEDED"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Same subscription bus as WebSocket engine
|
||||
|
||||
`conn.subscribe(topic, { tenant?, filter? })` SHALL register the SSE connection against the host's shared subscription bus — the same bus used by `api-ws-bun`. `bus.broadcast(topic, { tenant, payload })` MUST fan out to BOTH SSE and WS subscribers matching `(topic, tenant)` without any code path duplication between engines.
|
||||
|
||||
#### Scenario: WS and SSE subscribers both receive the same broadcast
|
||||
|
||||
- **WHEN** a WS client and an SSE client are both subscribed to `(topic, tenantT)` AND `bus.broadcast(topic, { tenant: T, payload })` fires
|
||||
- **THEN** both clients receive the payload
|
||||
|
||||
**Verified by:** `tests/integration/end-to-end.test.ts::"WS and SSE subscribers both receive same broadcast via shared bus"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Optional gzip compression
|
||||
|
||||
When `compressionEnabled: true` (default) AND the client sends `Accept-Encoding: gzip`, the engine SHALL emit `Content-Encoding: gzip` AND gzip-stream the response body. Compression failure (e.g. encoder error) MUST fall back to uncompressed streaming AND emit `SSE_BUN_COMPRESSION_FAILED` to logs (NOT to the client — they get a working uncompressed stream).
|
||||
|
||||
#### Scenario: gzip used when client accepts
|
||||
|
||||
- **WHEN** the client sends `Accept-Encoding: gzip`
|
||||
- **THEN** the response has `Content-Encoding: gzip` AND the body decompresses to valid SSE framing
|
||||
|
||||
#### Scenario: compression failure falls back to plain
|
||||
|
||||
- **WHEN** the gzip encoder throws AND a client is mid-stream
|
||||
- **THEN** the stream falls back to uncompressed delivery AND a log entry with `SSE_BUN_COMPRESSION_FAILED` is emitted
|
||||
|
||||
**Verified by:** `src/compression/compression.test.ts::"gzip emitted when client sends Accept-Encoding: gzip"` AND `src/compression/compression.test.ts::"compression failure falls back to plain with SSE_BUN_COMPRESSION_FAILED log"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: OTel surface
|
||||
|
||||
The engine SHALL emit:
|
||||
|
||||
- `sse.connections` observable gauge `{ plugin, path, tenant }`.
|
||||
- `sse.events_sent_total` counter `{ plugin, topic }`.
|
||||
- `sse.handshake.duration_ms` histogram.
|
||||
- `sse.buffer_replays_total` counter `{ hit }` (`"hit"` when replay returned events, `"miss"` on `SSE_BUN_BUFFER_MISS`).
|
||||
- `sse.compression_ratio` histogram (uncompressed-size / compressed-size when compression is on).
|
||||
|
||||
#### Scenario: event delivery emits events_sent_total
|
||||
|
||||
- **WHEN** a broadcast delivers to one SSE subscriber
|
||||
- **THEN** `sse.events_sent_total{ plugin, topic }` increments by 1
|
||||
|
||||
#### Scenario: connection lifecycle reflected in gauge
|
||||
|
||||
- **WHEN** a connection opens then closes
|
||||
- **THEN** the `sse.connections` gauge briefly rises then falls
|
||||
|
||||
**Verified by:** `src/instruments/instruments.test.ts::"event delivery emits sse.events_sent_total"` AND `src/instruments/instruments.test.ts::"connection lifecycle reflected in sse.connections gauge"`
|
||||
77
openspec/archive/2026-05-26-api-sse-bun/tasks.md
Normal file
77
openspec/archive/2026-05-26-api-sse-bun/tasks.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# Tasks — `@suaveplan/api-sse-bun`
|
||||
|
||||
> Every checkbox MUST reflect work actually done in this change.
|
||||
|
||||
## 0. Pre-flight (non-negotiable)
|
||||
|
||||
- [x] 0.1 Confirm clean working tree (`git status` reports nothing) and no in-flight worktrees with uncommitted work (`git worktree list` then `git status` in each)
|
||||
- [x] 0.2 If any worktrees exist under `.claude/worktrees/`, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (`git worktree remove`) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
|
||||
- [x] 0.3 Confirm the package set in `MANIFEST.md` matches `find packages -maxdepth 3 -name package.json` count. Drift here means a prior change archived without regenerating the manifest.
|
||||
|
||||
## 1. Spec authoring
|
||||
|
||||
- [x] 1.1 Read design doc §11.5, §11.6, §11.7
|
||||
- [x] 1.2 Read umbrella Stage 7 acceptance
|
||||
- [x] 1.3 Read sibling `openspec/changes/api-ws-bun/` and `openspec/changes/api-ws-broker-redis/` for broker integration
|
||||
- [x] 1.4 Read `openspec/AGENTS.md` Sections 13 and 14
|
||||
- [x] 1.5 Draft `proposal.md`
|
||||
- [x] 1.6 Draft `specs/api-sse-bun/spec.md`
|
||||
- [x] 1.7 Every requirement carries a `**Verified by:**` line
|
||||
|
||||
## 2. Implementation
|
||||
|
||||
- [x] 2.1 Scaffold `packages/api-stack/api/api-sse-bun/`
|
||||
- [x] 2.2 `src/options/options.ts` — `BunSseEngineOptions` Zod schema
|
||||
- [x] 2.3 `src/engine/bun-sse-engine.ts` — `createBunSseEngine(opts)`
|
||||
- [x] 2.4 `src/stream/sse-stream.ts` — SSE framing (`id:`, `event:`, `data:`, `retry:`, `:keepalive` comments)
|
||||
- [x] 2.5 `src/handshake/handshake.ts` — pipeline steps 1–7 wiring; principal freeze
|
||||
- [x] 2.6 `src/last-event-id/last-event-id.ts` — parse `Last-Event-Id` header; consult event buffer
|
||||
- [x] 2.7 `src/event-buffer/event-buffer.ts` — in-memory ring buffer; delegate to broker when wired
|
||||
- [x] 2.8 `src/keep-alive/keep-alive.ts` — `keepAliveCommentMs` scheduler
|
||||
- [x] 2.9 `src/limits/tenant-cap.ts` — `maxConnectionsPerTenant` enforcement
|
||||
- [x] 2.10 `src/compression/compression.ts` — gzip stream when client accepts; emit `Content-Encoding: gzip`
|
||||
- [x] 2.11 `src/subscription/subscription-bus.ts` — registers against the shared subscription bus
|
||||
- [x] 2.12 `src/instruments/instruments.ts` — `sse.*` OTel instruments per §11.7 + this package's extensions
|
||||
- [x] 2.13 `src/error-codes/error-codes.ts` — 10 `SSE_BUN_*` codes
|
||||
- [x] 2.14 `./testing` subpath — re-export `runSseEngineConformance` bound to factory
|
||||
- [x] 2.15 `src/index.ts` — explicit named re-exports
|
||||
|
||||
## 3. Tests
|
||||
|
||||
- [x] 3.1 Unit tests, every `it(...)` `{ timeout: 5000 }`
|
||||
- [x] 3.2 Conformance test runs `runSseEngineConformance`
|
||||
- [x] 3.3 Keep-alive test uses injectable `schedule` (FakeClock-style) and asserts cadence
|
||||
- [x] 3.4 Last-Event-Id replay test pins replay-before-live ordering
|
||||
- [x] 3.5 End-to-end test against real `Bun.serve` (`{ timeout: 30000 }`) — gracefully skips under non-Bun runtimes (vitest+node) since `Bun.serve` is the only transport; the in-process handshake suite drives the same `ReadableStream` path.
|
||||
- [ ] 3.6 Multi-host fan-out test against real Redis broker — deferred to `api-ws-broker-redis` keystone (the broker package is the natural owner of the integration); the engine's broker-backed event buffer is unit-tested with a real `SseBroker` implementation in this package.
|
||||
- [x] 3.7 All tests import from `@suaveplan/testing/runner`
|
||||
|
||||
## 4. Documentation
|
||||
|
||||
- [x] 4.1 Sibling `.md` per implementation file (13/13 = 100%)
|
||||
- [x] 4.2 `README.md` with `## Modules`; document defaults table and explain SSE's "no client→server channel, no acks" boundary vs WS
|
||||
|
||||
## 5. Pre-archive verification
|
||||
|
||||
- [x] 5.1 `bun run check` — 0/0/0
|
||||
- [x] 5.2 `bunx turbo lint typecheck test --filter=@suaveplan/api-sse-bun` — green
|
||||
- [x] 5.3 `bunx turbo test:coverage --filter=@suaveplan/api-sse-bun` — 100/100/100/100 (306 stmts, 102 branches, 73 funcs, 304 lines)
|
||||
- [x] 5.4 `bun scripts/openspec-conformance.ts packages/api-stack/api/api-sse-bun` — exit 0
|
||||
- [x] 5.5 `bunx turbo build --filter=@suaveplan/api-sse-bun` — green
|
||||
|
||||
## 6. Pre-archive 7-row checklist
|
||||
|
||||
- [x] 6.1 Generics on engine surface (`BunSseEngine.activeConnections` is `ReadonlyMap<string, ActiveConnection>`); literal-type claims pinned via `as const` on `SSE_BUN_CODES`.
|
||||
- [x] 6.2 Zod schemas on options (`resolveOptions` strict-parses every primitive knob); subscription payloads carry the typed `SseEvent` schema from `@suaveplan/api-sse`.
|
||||
- [x] 6.3 Every `SSE_BUN_*` code thrown by a production path (HANDSHAKE_REJECTED, TENANT_CAP_EXCEEDED, BUFFER_MISS, EVENT_TOO_LARGE, LAST_EVENT_ID_INVALID, COMPRESSION_FAILED, STREAM_INTERRUPTED, PRINCIPAL_EXPIRED, BROKER_UNAVAILABLE, INVALID_TOPIC) — the registry-name-vs-throw audit is part of the conformance run.
|
||||
- [x] 6.4 Strict-mode tested (compression on vs off; broker wired vs in-memory event buffer)
|
||||
- [x] 6.5 OTel instruments asserted against fake `MeterProvider` (`src/instruments/instruments.test.ts`)
|
||||
- [ ] 6.6 Lifecycle dedup: reconnect with same `Last-Event-Id` does not re-deliver already-delivered events — partially verified (replay test pins replay-before-live ordering); explicit "no double-delivery on reconnect" race is deferred to a later integration suite that owns the multi-host fan-out path.
|
||||
- [x] 6.7 No retroactive tick
|
||||
|
||||
## 7. Archive
|
||||
|
||||
- [x] 7.1 Fold spec delta into `openspec/specs/api-sse-bun/spec.md`
|
||||
- [x] 7.2 Move to `openspec/archive/<yyyy-mm-dd>-api-sse-bun/`
|
||||
- [x] 7.3 Tick Stage-7 row in umbrella tasks
|
||||
- [x] 7.4 Refresh `MANIFEST.md`
|
||||
Reference in New Issue
Block a user