Initial Commit
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
# OpenSpec Change: enterprise-network-websockets
|
||||
|
||||
- **Change ID**: enterprise-network-websockets
|
||||
- **Package**: `@suaveplan/network-websockets`
|
||||
- **Category**: universal/network
|
||||
- **Target path**: `universal/network/network-websockets`
|
||||
- **Status**: SUPERSEDED-BY-GENESIS-WAVE
|
||||
- **Date**: 2026-04-24
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Deliver `@suaveplan/network-websockets` on the genesis branch from scratch at enterprise quality. The package provides WebSocket protocol utilities: typed message framing, connection lifecycle management (connect/disconnect/reconnect), heartbeat/ping-pong, backpressure-aware message queuing, multiplexing over a single WebSocket connection, and injectable transport adapters. Genesis adds structured errors via `@suaveplan/error`, Zod schemas for all message and option types, OTel instrumentation, `AbortSignal` on async operations, retry/reconnect via `@suaveplan/retry`, event-bus integration for connection state changes, and property-based tests for message framing invariants.
|
||||
|
||||
---
|
||||
|
||||
## Current-state audit (genesis)
|
||||
|
||||
The package does not exist on genesis yet — this is a fresh build.
|
||||
|
||||
---
|
||||
|
||||
## Available workspace deps on genesis
|
||||
|
||||
| Package | Role |
|
||||
|---|---|
|
||||
| `@suaveplan/error` (`workspace:^`) | Base class for `NetworkWebsocketsError`; stable error codes |
|
||||
| `@suaveplan/types` (`workspace:^`) | `LoggerLike`, `Clock`, `EventBusLike` |
|
||||
| `@suaveplan/events` (`workspace:^`) | Event bus for connection state changes |
|
||||
| `@suaveplan/logger` (`workspace:^`) | Structured logging |
|
||||
| `@suaveplan/retry` (`workspace:^`) | Reconnect with exponential backoff |
|
||||
| `@suaveplan/serialize` (`workspace:^`) | Message serialization (JSON/binary) |
|
||||
| `zod` (`catalog:forms`) | Zod schemas for all public types |
|
||||
| `@opentelemetry/api` (`catalog:observability`) | Spans + counters on connect/message/disconnect |
|
||||
| `tslib` (`catalog:build`) | Runtime helpers |
|
||||
|
||||
Dev deps: `vitest`, `@vitest/coverage-istanbul`, `fast-check` (all `catalog:testing`).
|
||||
|
||||
---
|
||||
|
||||
## Public API surface
|
||||
|
||||
### Core
|
||||
|
||||
```ts
|
||||
function createWebSocketClient(options: WebSocketClientOptions): WebSocketClient
|
||||
|
||||
interface WebSocketClient {
|
||||
readonly id: string
|
||||
readonly url: string
|
||||
readonly state: ConnectionState
|
||||
connect(signal?: AbortSignal): Promise<void>
|
||||
disconnect(code?: number, reason?: string): void
|
||||
send(message: WebSocketMessage, signal?: AbortSignal): Promise<void>
|
||||
subscribe<T>(channel: string, handler: MessageHandler<T>): () => void
|
||||
on(event: ConnectionEvent, handler: (data: unknown) => void): () => void
|
||||
}
|
||||
```
|
||||
|
||||
### Types
|
||||
|
||||
```ts
|
||||
type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'closed'
|
||||
type ConnectionEvent = 'connect' | 'disconnect' | 'reconnect' | 'error' | 'message'
|
||||
type MessageHandler<T> = (message: T, meta: MessageMeta) => void
|
||||
|
||||
interface WebSocketMessage {
|
||||
channel?: string
|
||||
type: string
|
||||
payload: unknown
|
||||
id?: string
|
||||
correlationId?: string
|
||||
}
|
||||
|
||||
interface MessageMeta {
|
||||
receivedAt: number
|
||||
channel?: string
|
||||
type: string
|
||||
}
|
||||
|
||||
interface WebSocketClientOptions {
|
||||
url: string
|
||||
protocols?: string[]
|
||||
reconnect?: ReconnectOptions
|
||||
heartbeat?: HeartbeatOptions
|
||||
maxQueueSize?: number
|
||||
serialization?: 'json' | 'msgpack'
|
||||
logger?: LoggerLike
|
||||
clock?: Clock
|
||||
eventBus?: EventBusLike
|
||||
adapter?: WebSocketAdapter
|
||||
}
|
||||
|
||||
interface ReconnectOptions {
|
||||
enabled?: boolean
|
||||
maxAttempts?: number
|
||||
initialDelayMs?: number
|
||||
maxDelayMs?: number
|
||||
}
|
||||
|
||||
interface HeartbeatOptions {
|
||||
enabled?: boolean
|
||||
intervalMs?: number
|
||||
timeoutMs?: number
|
||||
}
|
||||
```
|
||||
|
||||
### Transport adapter
|
||||
|
||||
```ts
|
||||
interface WebSocketAdapter {
|
||||
connect(url: string, protocols?: string[]): WebSocketHandle
|
||||
}
|
||||
|
||||
interface WebSocketHandle {
|
||||
readonly readyState: number
|
||||
send(data: string | Uint8Array): void
|
||||
close(code?: number, reason?: string): void
|
||||
onopen: (() => void) | null
|
||||
onclose: ((code: number, reason: string) => void) | null
|
||||
onmessage: ((data: string | Uint8Array) => void) | null
|
||||
onerror: ((error: Error) => void) | null
|
||||
}
|
||||
```
|
||||
|
||||
### Error codes
|
||||
|
||||
```ts
|
||||
type NetworkWebsocketsErrorCode =
|
||||
| 'WS_CONNECTION_FAILED'
|
||||
| 'WS_SEND_FAILED'
|
||||
| 'WS_QUEUE_FULL'
|
||||
| 'WS_PROTOCOL_ERROR'
|
||||
| 'WS_TIMEOUT'
|
||||
| 'WS_ABORT'
|
||||
|
||||
function registerNetworkWebsocketsErrorCodes(): void
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Genesis additions
|
||||
|
||||
1. **Structured `NetworkWebsocketsError`** extending `SuaveplanError` with stable `WS.*` codes
|
||||
2. **Zod schemas** for `WebSocketClientOptions`, `WebSocketMessage`, `ReconnectOptions`, `HeartbeatOptions`
|
||||
3. **Injectable `WebSocketAdapter`** — decouple from native WebSocket for testing
|
||||
4. **Multiplexed channels** — `subscribe(channel, handler)` routes messages by `message.channel`
|
||||
5. **Backpressure queue** — bounded send queue; `WS_QUEUE_FULL` when `maxQueueSize` exceeded
|
||||
6. **Heartbeat/ping-pong** — injectable interval via `Clock`
|
||||
7. **Reconnect via `@suaveplan/retry`** with configurable backoff
|
||||
8. **OTel instrumentation** — span `ws.connect`, counter `ws.messages_sent_total`, `ws.messages_received_total`, histogram `ws.message_size_bytes`
|
||||
|
||||
---
|
||||
|
||||
## File layout
|
||||
|
||||
```text
|
||||
universal/network/network-websockets/
|
||||
package.json
|
||||
tsconfig.json
|
||||
vitest.config.ts
|
||||
src/
|
||||
index.ts
|
||||
error-codes.ts
|
||||
constants/
|
||||
index.ts
|
||||
types/
|
||||
common.ts
|
||||
common.md
|
||||
core/
|
||||
client/
|
||||
client.ts
|
||||
client.md
|
||||
client.types.ts
|
||||
client.test.ts
|
||||
index.ts
|
||||
queue/
|
||||
queue.ts
|
||||
queue.md
|
||||
queue.test.ts
|
||||
index.ts
|
||||
heartbeat/
|
||||
heartbeat.ts
|
||||
heartbeat.md
|
||||
heartbeat.test.ts
|
||||
index.ts
|
||||
adapters/
|
||||
native-adapter.ts
|
||||
native-adapter.md
|
||||
native-adapter.test.ts
|
||||
index.ts
|
||||
tests/
|
||||
integration.test.ts
|
||||
property.test.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Impact
|
||||
|
||||
**Workspace dep prerequisites**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/events`, `@suaveplan/logger`, `@suaveplan/retry`, `@suaveplan/serialize` must be published.
|
||||
@@ -0,0 +1,86 @@
|
||||
# Tasks: enterprise-network-websockets
|
||||
|
||||
**Change ID**: enterprise-network-websockets
|
||||
**Package**: `@suaveplan/network-websockets`
|
||||
**Target path**: `universal/network/network-websockets`
|
||||
**Prerequisite packages on Verdaccio**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/events`, `@suaveplan/logger`, `@suaveplan/retry`, `@suaveplan/serialize`
|
||||
|
||||
---
|
||||
|
||||
## 1. Scaffold
|
||||
|
||||
- [x] Create `universal/network/network-websockets/` with `src/`, `tests/`, and all feature subdirectories.
|
||||
- [x] Write `package.json` at version `0.1.0`: `"sideEffects": false`, exports map, deps per `proposal.md`.
|
||||
- [x] Write `tsconfig.json` extending `@suaveplan/typescript-config/library.json`.
|
||||
- [x] Write `vitest.config.ts` using `createTestConfig()`.
|
||||
- [x] Add `lint`, `typecheck`, `test`, `test:coverage`, `build`, `docs:check` scripts.
|
||||
|
||||
---
|
||||
|
||||
## 2. Types and error codes
|
||||
|
||||
- [x] Define all public types in `src/types/common.ts`: `WebSocketClientOptions`, `WebSocketMessage`, `MessageMeta`, `ReconnectOptions`, `HeartbeatOptions`, `ConnectionState`, `ConnectionEvent`, `MessageHandler`, `WebSocketAdapter`, `WebSocketHandle`.
|
||||
- [x] Write `src/types/common.md` (≥200 words, simple + advanced examples).
|
||||
- [x] Define error codes in `src/error-codes.ts`: `NetworkWebsocketsErrorCode` union, metadata, `registerNetworkWebsocketsErrorCodes()`.
|
||||
- [x] Write Zod schemas in `src/types/schemas.ts` + `src/types/schemas.md`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Message queue (`src/core/queue/`)
|
||||
|
||||
- [x] Implement bounded `MessageQueue` (ring buffer, `maxQueueSize`).
|
||||
- [x] `enqueue(message)` — returns `false` and throws `NetworkWebsocketsError(WS_QUEUE_FULL)` when full.
|
||||
- [x] `dequeue()` / `drain()` — returns next message or all queued.
|
||||
- [x] Write `queue.test.ts` + `queue.md`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Heartbeat (`src/core/heartbeat/`)
|
||||
|
||||
- [x] Implement `HeartbeatManager`:
|
||||
- On `start()`, send a `ping` frame on every `intervalMs` interval (injectable `Clock`).
|
||||
- On `pong` received, reset timer.
|
||||
- On `timeoutMs` exceeded without pong, emit `timeout` event.
|
||||
- [x] Write `heartbeat.test.ts` + `heartbeat.md`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Native WebSocket adapter (`src/adapters/`)
|
||||
|
||||
- [x] Implement `createNativeWebSocketAdapter()` — wraps `globalThis.WebSocket` (browser) or `Bun.WebSocket` (Bun server).
|
||||
- [x] Guard with platform check; throw `WS_CONNECTION_FAILED` if WebSocket not available.
|
||||
- [x] Write `native-adapter.test.ts` (uses fake adapter from `@suaveplan/network-testing` if available, else skipped) + `native-adapter.md`.
|
||||
|
||||
---
|
||||
|
||||
## 6. WebSocket client (`src/core/client/`)
|
||||
|
||||
- [x] Implement `createWebSocketClient(options)`:
|
||||
- Validates options with Zod.
|
||||
- Uses `options.adapter ?? createNativeWebSocketAdapter()`.
|
||||
- State machine: `disconnected → connecting → connected`; reconnect loop via `@suaveplan/retry`.
|
||||
- [x] Implement `client.connect(signal?)` — starts connection, heartbeat.
|
||||
- [x] Implement `client.disconnect(code?, reason?)` — graceful close.
|
||||
- [x] Implement `client.send(message, signal?)` — enqueues if not connected; drains queue on connect; serializes via `@suaveplan/serialize`.
|
||||
- [x] Implement `client.subscribe(channel, handler)` — routes incoming messages by `message.channel`; returns unsubscribe.
|
||||
- [x] Implement `client.on(event, handler)` — connection lifecycle events.
|
||||
- [x] OTel: span `ws.connect`, counters `ws.messages_sent_total`, `ws.messages_received_total`.
|
||||
- [x] Write `client.test.ts` (≤500 lines) using injectable fake adapter.
|
||||
- [x] Write `client.md` (≥200 words).
|
||||
|
||||
---
|
||||
|
||||
## 7. Property-based tests (`tests/property.test.ts`)
|
||||
|
||||
- [x] Message queue invariants: `enqueue` count - `dequeue` count = current queue depth.
|
||||
- [x] Channel routing: messages with `channel: 'X'` are always routed to `channel: 'X'` subscribers only.
|
||||
|
||||
---
|
||||
|
||||
## 8. Docs and verify
|
||||
|
||||
- [x] All per-file `.md` files per layout in `proposal.md`.
|
||||
- [x] `universal/network/network-websockets/README.md`.
|
||||
- [x] `bunx turbo lint typecheck test test:coverage --filter=@suaveplan/network-websockets` — all green, 100% coverage.
|
||||
- [x] `bun scripts/publish.ts --package @suaveplan/network-websockets`.
|
||||
- [x] Update `CLAUDE.md`. Archive change folder.
|
||||
Reference in New Issue
Block a user