Initial Commit
This commit is contained in:
69
openspec/archive/2026-05-26-api-ws-bun/proposal.md
Normal file
69
openspec/archive/2026-05-26-api-ws-bun/proposal.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# `@suaveplan/api-ws-bun` — `Bun.serve` WebSocket engine
|
||||
|
||||
**Package**: `@suaveplan/api-ws-bun`
|
||||
**Status**: PROPOSED
|
||||
**Type**: new-package
|
||||
**Stage**: 7 (GraphQL + WebSocket / SSE engines) — see umbrella `openspec/changes/api-plugin-foundation/`
|
||||
|
||||
## Why
|
||||
|
||||
Per design doc §11 persistent connections are first-class in the foundation: plugins declare WS endpoints in their manifest and the host runs the engine, manages connection lifecycle, fans out subscriptions. This package implements the abstract `api-ws` contract on top of `Bun.serve`'s WebSocket primitives — the fastest native path on the foundation's target runtime. Without this adapter the `api-ws` contract has no production implementation and Stage-7 acceptance ("publishes a tenant-scoped WebSocket broadcast") cannot be reached.
|
||||
|
||||
## What changes
|
||||
|
||||
Net-new package depending on `@suaveplan/api-types`, `@suaveplan/api-ws` (abstract contract), `@suaveplan/api-router` (handshake upgrade route), `@suaveplan/error`, `@suaveplan/logger`, `@suaveplan/opentelemetry`, `@suaveplan/crypto-hash` (HMAC for reconnect tokens), and `zod`.
|
||||
|
||||
- ADDED: `createBunWsEngine(opts)` — factory returning the engine value implementing `@suaveplan/api-ws`'s contract.
|
||||
- ADDED: `BunWsEngineOptions` — Zod-validated config: `maxQueuedMessages` (default 256), `heartbeatMs` (default 30000, min 5000), `missedPongLimit` (default 2), `maxConnectionsPerTenant` (default 1000), `reconnectTokenTtlMs` (default 300000 = 5 min), `reconnectTokenSecret` (required, used for HMAC), `reconnectEventBufferStrategy` (`"auto"` | `"memory"` | `"broker"`; default `"auto"` resolves to broker-backed when a Redis broker is wired, in-memory otherwise — this is the §14 open-question default), `subprotocols`.
|
||||
- ADDED: Connection lifecycle:
|
||||
- Handshake → per-request pipeline steps 1–7 → `onConnect` with `Connection` bound to `principal` + `tenantId`. Principal frozen for connection lifetime; mid-connection token expiry emits `connection.principal_expired`.
|
||||
- `ConnectionRegistry` indexed by `tenantId` for fan-out lookup.
|
||||
- `onMessage` invoked per inbound frame.
|
||||
- `onClose` invoked on either side's close.
|
||||
- ADDED: Backpressure — per-connection send queue capped at `maxQueuedMessages`. Exceeding sends `BACKPRESSURE_EVICTED` close frame and drops the connection.
|
||||
- ADDED: Heartbeats — ping every `heartbeatMs`; `missedPongLimit` missed pongs in a row → connection considered dead, `onClose` fires with `code: ABNORMAL`.
|
||||
- ADDED: Per-tenant caps — `maxConnectionsPerTenant` checked at handshake; exceeding rejects with 429 and `WS_BUN_TENANT_CAP_EXCEEDED`.
|
||||
- ADDED: Reconnect tokens — on `onClose`, host issues an HMAC-signed token (5-min TTL) carrying `{ tenant, principalId, lastEventId }`. Client reconnects with `?reconnectToken=...`; engine validates and resumes subscriptions automatically. Reconnect-event buffer storage follows `reconnectEventBufferStrategy`: when the host has a Redis broker wired (Redis-backed), the event buffer lives in Redis; otherwise it lives in-memory on the local host (default per design §11.5 paragraph 4, which this package pins as the operating default).
|
||||
- ADDED: Subscription bus integration — `conn.subscribe(topic, { tenant?, filter? })` registers against `api-ws`'s subscription bus; `bus.broadcast(topic, { tenant, payload })` triggers fan-out.
|
||||
- ADDED: `WS_BUN_*` error codes: `WS_BUN_HANDSHAKE_REJECTED`, `WS_BUN_TENANT_CAP_EXCEEDED`, `WS_BUN_BACKPRESSURE_EVICTED`, `WS_BUN_HEARTBEAT_FAILED`, `WS_BUN_RECONNECT_TOKEN_INVALID`, `WS_BUN_RECONNECT_TOKEN_EXPIRED`, `WS_BUN_RECONNECT_BUFFER_MISS`, `WS_BUN_SUBPROTOCOL_UNSUPPORTED`, `WS_BUN_CLOSE_TIMEOUT`, `WS_BUN_PRINCIPAL_EXPIRED`.
|
||||
- ADDED: OTel surface from design §11.7 — `ws.handshake.duration_ms { plugin, path }`, `ws.connections { plugin, tenant }` gauge, `ws.messages_in_total`, `ws.messages_out_total`, `ws.broadcasts_total { topic, tenant, recipients }`, `ws.backpressure_evicted_total`, `ws.heartbeat_failures_total`.
|
||||
- ADDED: `./testing` subpath — `runWsEngineConformance` from `api-ws/testing` bound to `createBunWsEngine`.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Cross-host broadcast — that's `api-ws-broker-redis`. This engine integrates with whichever broker the host wires.
|
||||
- GraphQL subscription transport — `api-graphql-yoga`/`api-graphql-apollo` ride this engine via `graphql-ws`; the transport bridging is in those packages.
|
||||
- SSE — separate engine `api-sse-bun`. Reuses the same broker abstraction but the transport differs.
|
||||
|
||||
## Source mapping
|
||||
|
||||
Genesis-original. Source of design intent: `docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md`.
|
||||
|
||||
| Requirement | Design doc section |
|
||||
|---|---|
|
||||
| Connection lifecycle (`onConnect` / `onMessage` / `onClose`) | §11.1 |
|
||||
| Handshake flow steps 1–7 + frozen principal | §11.1 (paragraph 2) |
|
||||
| `ConnectionRegistry` indexed by `tenantId` | §11.1 (paragraph 3) |
|
||||
| Subscription bus + per-tenant fan-out | §11.2 |
|
||||
| Backpressure: `maxQueuedMessages` default 256 + eviction close frame | §11.4 |
|
||||
| Heartbeats: `heartbeatMs` default 30000, 2 missed pongs → drop | §11.4 |
|
||||
| Per-tenant caps `maxConnectionsPerTenant` | §11.4 |
|
||||
| Reconnect tokens: HMAC-signed, 5-min TTL, `{tenant, principalId, lastEventId}` | §11.5 |
|
||||
| Reconnect-event-buffer storage default (broker-when-Redis / memory-otherwise) | §11.5 (paragraph 4) + §14 |
|
||||
| OTel instruments listed above | §11.7 |
|
||||
|
||||
## Verification approach
|
||||
|
||||
- `src/engine/bun-ws-engine.test.ts` — handshake, message dispatch, close handling, principal frozen at handshake.
|
||||
- `src/engine/bun-ws-engine.spec-conformance.test.ts` — runs `runWsEngineConformance("bun", createBunWsEngine)`.
|
||||
- `src/backpressure/backpressure.test.ts` — queue overflow evicts connection with `BACKPRESSURE_EVICTED`.
|
||||
- `src/heartbeat/heartbeat.test.ts` — 2 missed pongs drops connection; uses `FakeClock`.
|
||||
- `src/limits/tenant-cap.test.ts` — 1001st connection from same tenant rejected.
|
||||
- `src/reconnect/reconnect-token.test.ts` — token issue + validation + expiry + replay-rejection.
|
||||
- `src/reconnect/event-buffer.test.ts` — strategy `"auto"` resolves to broker-backed when a Redis broker is wired, in-memory otherwise.
|
||||
- `src/registry/connection-registry.test.ts` — index by tenant; cross-tenant fan-out gated by `ws.broadcast-cross-tenant` capability.
|
||||
- `tests/integration/end-to-end.test.ts` (30000ms) — real `Bun.serve` listener, real WS client, full handshake → subscribe → broadcast → close → reconnect → resume.
|
||||
|
||||
## Migration impact
|
||||
|
||||
None — additive only.
|
||||
167
openspec/archive/2026-05-26-api-ws-bun/specs/api-ws-bun/spec.md
Normal file
167
openspec/archive/2026-05-26-api-ws-bun/specs/api-ws-bun/spec.md
Normal file
@@ -0,0 +1,167 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: api-ws-bun (api-ws-bun)
|
||||
|
||||
**Package**: `@suaveplan/api-ws-bun`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement carries a `**Verified by:**` line.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Implements the abstract WebSocket engine contract
|
||||
|
||||
The package SHALL export `createBunWsEngine(opts)` producing an engine value that implements the engine contract from `@suaveplan/api-ws`. The conformance suite `runWsEngineConformance` MUST pass against this adapter.
|
||||
|
||||
#### Scenario: conformance suite passes
|
||||
|
||||
- **WHEN** `runWsEngineConformance("bun", createBunWsEngine)` runs
|
||||
- **THEN** every invariant passes
|
||||
|
||||
**Verified by:** `src/engine/bun-ws-engine.spec-conformance.test.ts::"createBunWsEngine passes runWsEngineConformance"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Handshake freezes the principal for the connection lifetime
|
||||
|
||||
At handshake the engine SHALL run per-request pipeline steps 1–7 (auth + tenant resolution + capability scoping) and bind the resolved `principal` and `tenantId` to the `Connection`. The principal MUST be immutable for the connection's lifetime. Mid-connection token expiry MUST emit `connection.principal_expired` on the host event bus; the plugin's `onClose` MUST then be invokable to drop the connection cleanly. `WS_BUN_PRINCIPAL_EXPIRED` is the code emitted with that event.
|
||||
|
||||
#### Scenario: principal bound at handshake survives subsequent token rotation
|
||||
|
||||
- **WHEN** the handshake resolves principal P AND the underlying token rotates server-side
|
||||
- **THEN** `conn.principal` still reads P AND `connection.principal_expired` fires once the original token's expiry passes
|
||||
|
||||
**Verified by:** `src/principal/principal-expiry.test.ts::"principal frozen at handshake; expiry emits connection.principal_expired"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Per-connection backpressure with default 256-message queue
|
||||
|
||||
The engine SHALL enforce a per-connection send queue capped at `maxQueuedMessages` (default 256). When the queue would exceed the cap, the connection MUST receive a `BACKPRESSURE_EVICTED` close frame AND `onClose` fires with `code: WS_BUN_BACKPRESSURE_EVICTED`. The eviction MUST be observable via `ws.backpressure_evicted_total` counter.
|
||||
|
||||
#### Scenario: queue overflow evicts the connection
|
||||
|
||||
- **WHEN** `maxQueuedMessages: 4` AND 5 sends are issued before the client reads any
|
||||
- **THEN** the connection receives a `BACKPRESSURE_EVICTED` close frame AND `ws.backpressure_evicted_total` increments by 1
|
||||
|
||||
**Verified by:** `src/backpressure/backpressure.test.ts::"queue overflow evicts connection with BACKPRESSURE_EVICTED"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Heartbeat with default 30000ms interval and 2-missed-pong drop
|
||||
|
||||
The engine SHALL send a ping frame every `heartbeatMs` (default 30000). Two consecutive missed pongs (default `missedPongLimit: 2`) MUST be treated as a dead connection — `onClose` fires with `code: ABNORMAL` AND `ws.heartbeat_failures_total` increments.
|
||||
|
||||
#### Scenario: 2 missed pongs drops the connection
|
||||
|
||||
- **WHEN** the client never responds to pings AND `missedPongLimit: 2`
|
||||
- **THEN** after the 2nd missed-pong window, the connection is dropped AND `ws.heartbeat_failures_total` increments by 1
|
||||
|
||||
**Verified by:** `src/heartbeat/heartbeat.test.ts::"2 missed pongs drops the connection with code ABNORMAL"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Per-tenant connection cap
|
||||
|
||||
The engine SHALL track active connections per tenant and reject new handshakes when a tenant's count is `>= maxConnectionsPerTenant` (default 1000). Rejection returns HTTP 429 AND `WS_BUN_TENANT_CAP_EXCEEDED`.
|
||||
|
||||
#### Scenario: 1001st connection from same tenant rejected with 429
|
||||
|
||||
- **WHEN** `maxConnectionsPerTenant: 1000` AND tenant T already has 1000 active connections
|
||||
- **THEN** the 1001st handshake returns 429 AND `WS_BUN_TENANT_CAP_EXCEEDED`
|
||||
|
||||
**Verified by:** `src/limits/tenant-cap.test.ts::"1001st connection from same tenant rejected with WS_BUN_TENANT_CAP_EXCEEDED"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Reconnect tokens with HMAC signing and 5-minute TTL
|
||||
|
||||
On `onClose` the engine SHALL issue a reconnect token: HMAC-signed (key from `reconnectTokenSecret`), 5-minute TTL (`reconnectTokenTtlMs` default 300000), payload `{ tenant, principalId, lastEventId }`. A client reconnecting with `?reconnectToken=...` MUST be validated; on success the engine resumes subscriptions automatically and the plugin's `onConnect` runs in `resume` mode with the pre-restored subscription set. Invalid token → `WS_BUN_RECONNECT_TOKEN_INVALID`. Expired token → `WS_BUN_RECONNECT_TOKEN_EXPIRED`. Replayed token (already consumed) → `WS_BUN_RECONNECT_TOKEN_INVALID`.
|
||||
|
||||
#### Scenario: valid token resumes subscriptions
|
||||
|
||||
- **WHEN** the client reconnects with a valid token within 5 minutes
|
||||
- **THEN** the resumed connection has the same subscription set as before close
|
||||
|
||||
#### Scenario: expired token is rejected
|
||||
|
||||
- **WHEN** the client reconnects 6 minutes after close
|
||||
- **THEN** the handshake fails with `WS_BUN_RECONNECT_TOKEN_EXPIRED`
|
||||
|
||||
#### Scenario: tampered token is rejected
|
||||
|
||||
- **WHEN** the client modifies the token's payload before reconnecting
|
||||
- **THEN** HMAC validation fails AND the handshake fails with `WS_BUN_RECONNECT_TOKEN_INVALID`
|
||||
|
||||
**Verified by:** `src/reconnect/reconnect-token.test.ts::"valid token resumes subscriptions in resume mode"` AND `src/reconnect/reconnect-token.test.ts::"expired token rejected with WS_BUN_RECONNECT_TOKEN_EXPIRED"` AND `src/reconnect/reconnect-token.test.ts::"tampered token rejected with WS_BUN_RECONNECT_TOKEN_INVALID"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Reconnect event-buffer storage default
|
||||
|
||||
The engine SHALL provide a per-`(tenant, topic)` event buffer used during reconnect-resume to replay events with id `> lastEventId`. Storage MUST follow `reconnectEventBufferStrategy`:
|
||||
|
||||
- `"auto"` (default): when a Redis broker (`api-ws-broker-redis`) is wired into the host, the buffer lives in Redis; otherwise it lives in-memory on the local host. This pins the design doc §11.5 paragraph 4 default ("Redis when the broker is Redis, in-memory otherwise") — explicitly resolving the §14 open question for the foundation's shipping default.
|
||||
- `"memory"`: always in-memory regardless of broker (single-host deployments).
|
||||
- `"broker"`: always broker-backed; requires a broker capable of supporting the buffer (currently only `api-ws-broker-redis`); refuses with `WS_BUN_RECONNECT_BUFFER_MISS` at boot if no compatible broker is wired.
|
||||
|
||||
#### Scenario: auto resolves to broker when Redis broker is wired
|
||||
|
||||
- **WHEN** `reconnectEventBufferStrategy: "auto"` AND `api-ws-broker-redis` is wired
|
||||
- **THEN** the engine uses the broker-backed buffer
|
||||
|
||||
#### Scenario: auto resolves to memory when no broker is wired
|
||||
|
||||
- **WHEN** `reconnectEventBufferStrategy: "auto"` AND no broker is wired (single-host)
|
||||
- **THEN** the engine uses the in-memory buffer
|
||||
|
||||
#### Scenario: broker strategy without a compatible broker refuses at boot
|
||||
|
||||
- **WHEN** `reconnectEventBufferStrategy: "broker"` AND no compatible broker is wired
|
||||
- **THEN** boot fails with `WS_BUN_RECONNECT_BUFFER_MISS`
|
||||
|
||||
**Verified by:** `src/reconnect/event-buffer.test.ts::"auto resolves to broker when Redis broker wired"` AND `src/reconnect/event-buffer.test.ts::"auto resolves to memory when no broker wired"` AND `src/reconnect/event-buffer.test.ts::"broker strategy without compatible broker refuses at boot with WS_BUN_RECONNECT_BUFFER_MISS"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Subscription bus with cross-tenant capability gate
|
||||
|
||||
`conn.subscribe(topic, { tenant?, filter? })` SHALL register the connection against the host's subscription bus, indexed by `(topic, tenant)`. `bus.broadcast(topic, { tenant, payload })` MUST fan out only to matching connections AND run optional per-connection `filter` predicates. Cross-tenant fan-out (a `bus.broadcast` whose `tenant` value differs from the broadcasting plugin's tenant) SHALL require the plugin to declare the `ws.broadcast-cross-tenant` capability; absence → refuse + audit log emitted.
|
||||
|
||||
#### Scenario: tenant-scoped broadcast delivered only to matching tenant
|
||||
|
||||
- **WHEN** a broadcast for tenant T fires AND connections C1 (tenant T) and C2 (tenant U) are subscribed
|
||||
- **THEN** C1 receives the payload AND C2 does not
|
||||
|
||||
#### Scenario: cross-tenant broadcast without capability is refused
|
||||
|
||||
- **WHEN** a plugin without `ws.broadcast-cross-tenant` issues `bus.broadcast(topic, { tenant: "other" })`
|
||||
- **THEN** the broadcast is refused AND an audit log entry is emitted
|
||||
|
||||
**Verified by:** `src/registry/connection-registry.test.ts::"tenant-scoped broadcast delivered only to matching tenant"` AND `src/registry/connection-registry.test.ts::"cross-tenant broadcast without capability refused and audit-logged"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: OTel surface
|
||||
|
||||
The engine SHALL emit every instrument listed in design §11.7:
|
||||
|
||||
- `ws.handshake.duration_ms` histogram `{ plugin, path }`.
|
||||
- `ws.connections` observable gauge `{ plugin, tenant }`.
|
||||
- `ws.messages_in_total` counter `{ plugin, path }`.
|
||||
- `ws.messages_out_total` counter `{ plugin, path }`.
|
||||
- `ws.broadcasts_total` counter `{ topic, tenant, recipients }`.
|
||||
- `ws.backpressure_evicted_total` counter `{ plugin }`.
|
||||
- `ws.heartbeat_failures_total` counter `{ plugin }`.
|
||||
|
||||
#### Scenario: handshake emits duration histogram
|
||||
|
||||
- **WHEN** a connection completes its handshake
|
||||
- **THEN** an in-memory `MeterProvider` observes one observation in `ws.handshake.duration_ms`
|
||||
|
||||
#### Scenario: broadcast emits broadcasts_total
|
||||
|
||||
- **WHEN** a tenant-scoped broadcast is issued with 3 recipients
|
||||
- **THEN** `ws.broadcasts_total{recipients=3}` increments by 1
|
||||
|
||||
**Verified by:** `src/instruments/instruments.test.ts::"handshake emits ws.handshake.duration_ms"` AND `src/instruments/instruments.test.ts::"broadcast emits ws.broadcasts_total with recipient count"`
|
||||
78
openspec/archive/2026-05-26-api-ws-bun/tasks.md
Normal file
78
openspec/archive/2026-05-26-api-ws-bun/tasks.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Tasks — `@suaveplan/api-ws-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 end-to-end
|
||||
- [x] 1.2 Read umbrella Stage 7 acceptance
|
||||
- [x] 1.3 Read design doc §14 open questions (reconnect event-buffer storage)
|
||||
- [x] 1.4 Read `openspec/AGENTS.md` Sections 13 and 14
|
||||
- [x] 1.5 Draft `proposal.md`
|
||||
- [x] 1.6 Draft `specs/api-ws-bun/spec.md`
|
||||
- [x] 1.7 Every requirement carries a `**Verified by:**` line
|
||||
|
||||
## 2. Implementation
|
||||
|
||||
- [x] 2.1 Scaffold `packages/api/api-ws-bun/`
|
||||
- [x] 2.2 `src/options/options.ts` — `BunWsEngineOptions` Zod schema
|
||||
- [x] 2.3 `src/engine/bun-ws-engine.ts` — `createBunWsEngine(opts)`
|
||||
- [x] 2.4 `src/handshake/handshake.ts` — pipeline steps 1–7 wiring; principal freezing
|
||||
- [x] 2.5 `src/registry/connection-registry.ts` — tenant-indexed registry; cross-tenant fan-out gate
|
||||
- [x] 2.6 `src/backpressure/backpressure.ts` — `maxQueuedMessages` enforcement; `BACKPRESSURE_EVICTED` close frame
|
||||
- [x] 2.7 `src/heartbeat/heartbeat.ts` — ping/pong schedule; missed-pong-limit drop
|
||||
- [x] 2.8 `src/limits/tenant-cap.ts` — `maxConnectionsPerTenant` enforcement at handshake
|
||||
- [x] 2.9 `src/reconnect/reconnect-token.ts` — HMAC token issue/validate via `@suaveplan/crypto-hash`
|
||||
- [x] 2.10 `src/reconnect/event-buffer.ts` — strategy `"auto"` resolves to broker-backed when wired, in-memory otherwise
|
||||
- [x] 2.11 `src/subscription/subscription-bus.ts` — `conn.subscribe(topic, { tenant?, filter? })` + `bus.broadcast` fan-out
|
||||
- [x] 2.12 `src/principal/principal-expiry.ts` — emit `connection.principal_expired` on mid-connection token expiry
|
||||
- [x] 2.13 `src/instruments/instruments.ts` — every OTel instrument from §11.7
|
||||
- [x] 2.14 `src/error-codes.ts` — 10 `WS_BUN_*` codes
|
||||
- [x] 2.15 `./testing` subpath — re-export `runWsEngineConformance` bound to factory
|
||||
- [x] 2.16 `src/index.ts` — explicit named re-exports
|
||||
|
||||
## 3. Tests
|
||||
|
||||
- [x] 3.1 Unit tests, every `it(...)` `{ timeout: 5000 }`
|
||||
- [x] 3.2 Conformance test runs `runWsEngineConformance`
|
||||
- [x] 3.3 Heartbeat test uses `FakeClock` (`{ timeout: 5000 }`)
|
||||
- [x] 3.4 Reconnect-token test pins issue + validate + expiry + replay-rejection
|
||||
- [x] 3.5 Event-buffer strategy test pins broker-when-Redis / memory-otherwise default
|
||||
- [x] 3.6 End-to-end test against real `Bun.serve` (`{ timeout: 30000 }`)
|
||||
- [x] 3.7 All tests import from `@suaveplan/testing/runner`
|
||||
|
||||
## 4. Documentation
|
||||
|
||||
- [x] 4.1 Sibling `.md` per implementation file
|
||||
- [x] 4.2 `README.md` with `## Modules`; document defaults table (`maxQueuedMessages`, `heartbeatMs`, `missedPongLimit`, `reconnectTokenTtlMs`, `reconnectEventBufferStrategy`)
|
||||
|
||||
## 5. Pre-archive verification
|
||||
|
||||
- [x] 5.1 `bun run check` — 0/0/0
|
||||
- [x] 5.2 `bunx turbo lint typecheck test --filter=@suaveplan/api-ws-bun` — green
|
||||
- [x] 5.3 `bunx turbo test:coverage --filter=@suaveplan/api-ws-bun` — 100/100/100/100
|
||||
- [x] 5.4 `bun scripts/openspec-conformance.ts @suaveplan/api-ws-bun` — exit 0
|
||||
- [x] 5.5 `bunx turbo build --filter=@suaveplan/api-ws-bun` — green
|
||||
|
||||
## 6. Pre-archive 7-row checklist
|
||||
|
||||
- [x] 6.1 Generics on `Connection<TPrincipal>`; `expectTypeOf` on literal-type claims
|
||||
- [x] 6.2 Zod schemas at every config boundary; reconnect-token payload schema'd
|
||||
- [x] 6.3 Every `WS_BUN_*` code thrown by a production path
|
||||
- [x] 6.4 Strict-mode tested (heartbeat on vs off; cross-tenant fan-out capability gate on vs off)
|
||||
- [x] 6.5 OTel instruments asserted against in-memory `MeterProvider`
|
||||
- [x] 6.6 Lifecycle dedup: re-`onConnect` after reconnect-resume does not re-mint state — uses resumed subscription set
|
||||
- [x] 6.7 No retroactive tick
|
||||
|
||||
## 7. Archive
|
||||
|
||||
- [x] 7.1 Fold spec delta into `openspec/specs/api-ws-bun/spec.md`
|
||||
- [x] 7.2 Move to `openspec/archive/<yyyy-mm-dd>-api-ws-bun/`
|
||||
- [x] 7.3 Tick Stage-7 row in umbrella tasks
|
||||
- [x] 7.4 Refresh `MANIFEST.md`
|
||||
Reference in New Issue
Block a user