Initial Commit
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
# OpenSpec Change: enterprise-network
|
||||
|
||||
- **Change ID**: enterprise-network
|
||||
- **Package**: `@suaveplan/network`
|
||||
- **Category**: universal
|
||||
- **Target path**: `universal/network/network`
|
||||
- **Status**: PROPOSED
|
||||
- **Date**: 2026-04-24
|
||||
- **Reference spec**: `/mnt/s/web/suaveplan/enterprise/network.md`
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Deliver `@suaveplan/network` on the genesis branch from scratch at enterprise quality. The package provides core networking utilities for universal TypeScript: a typed `HttpClient` with middleware pipeline, `WebSocketClient` with auto-reconnect and backpressure, URL construction/parsing helpers, fetch timeout and cancellation wrappers, request/response interceptors, and network-quality probing. Injectable `fetch` and `WebSocket` constructors for test-time substitution. No hard browser/Node platform deps.
|
||||
|
||||
---
|
||||
|
||||
## Current-state audit (genesis)
|
||||
|
||||
The package does not exist on genesis yet — this is a fresh build. No prior source, no test results, no coverage data. The main-branch package had 85.40% coverage.
|
||||
|
||||
---
|
||||
|
||||
## Available workspace deps on genesis
|
||||
|
||||
| Package | Role |
|
||||
|---|---|
|
||||
| `@suaveplan/error` (`workspace:^`) | Base class for `NetworkError`; `NETWORK.*` codes |
|
||||
| `@suaveplan/types` (`workspace:^`) | `LoggerLike`, `Clock` |
|
||||
| `@suaveplan/retry` (`workspace:^`) | Exponential backoff + circuit breaker on HTTP/WS paths |
|
||||
| `@suaveplan/serialize` (`workspace:^`) | JSON request/response body serialisation |
|
||||
| `zod` (`catalog:forms`) | Zod schemas for all public option types |
|
||||
| `@opentelemetry/api` (`catalog:observability`) | Spans + counters on every request |
|
||||
|
||||
Dev deps: `vitest`, `@vitest/coverage-istanbul`, `fast-check`, `mitata` (all `catalog:testing`).
|
||||
|
||||
---
|
||||
|
||||
## Public API
|
||||
|
||||
### HttpClient
|
||||
|
||||
```ts
|
||||
class NetworkError extends SuaveplanError
|
||||
|
||||
type HttpMethod = 'GET' | 'HEAD' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS'
|
||||
|
||||
interface HttpRequest {
|
||||
url: string
|
||||
method?: HttpMethod // default: 'GET'
|
||||
headers?: Record<string, string>
|
||||
body?: unknown // serialised via @suaveplan/serialize
|
||||
timeoutMs?: number // default: 30000
|
||||
signal?: AbortSignal
|
||||
}
|
||||
|
||||
interface HttpResponse<T = unknown> {
|
||||
status: number
|
||||
statusText: string
|
||||
headers: Record<string, string>
|
||||
body: T
|
||||
url: string
|
||||
durationMs: number
|
||||
}
|
||||
|
||||
type HttpMiddleware = (request: HttpRequest, next: (req: HttpRequest) => Promise<HttpResponse>) => Promise<HttpResponse>
|
||||
|
||||
interface HttpClientOptions {
|
||||
baseUrl?: string
|
||||
defaultHeaders?: Record<string, string>
|
||||
defaultTimeoutMs?: number
|
||||
middleware?: HttpMiddleware[]
|
||||
fetch?: typeof globalThis.fetch
|
||||
logger?: LoggerLike
|
||||
clock?: Clock
|
||||
}
|
||||
|
||||
class HttpClient {
|
||||
constructor(options?: HttpClientOptions)
|
||||
request<T = unknown>(req: HttpRequest): Promise<HttpResponse<T>>
|
||||
get<T = unknown>(path: string, options?: Omit<HttpRequest, 'url' | 'method'>): Promise<HttpResponse<T>>
|
||||
post<T = unknown>(path: string, body: unknown, options?: Omit<HttpRequest, 'url' | 'method' | 'body'>): Promise<HttpResponse<T>>
|
||||
put<T = unknown>(path: string, body: unknown, options?: Omit<HttpRequest, 'url' | 'method' | 'body'>): Promise<HttpResponse<T>>
|
||||
patch<T = unknown>(path: string, body: unknown, options?: Omit<HttpRequest, 'url' | 'method' | 'body'>): Promise<HttpResponse<T>>
|
||||
delete<T = unknown>(path: string, options?: Omit<HttpRequest, 'url' | 'method'>): Promise<HttpResponse<T>>
|
||||
use(middleware: HttpMiddleware): this
|
||||
}
|
||||
|
||||
function createHttpClient(options?: HttpClientOptions): HttpClient
|
||||
```
|
||||
|
||||
### WebSocketClient
|
||||
|
||||
```ts
|
||||
type WsReadyState = 'connecting' | 'open' | 'closing' | 'closed'
|
||||
type WsMessage = string | ArrayBuffer | Uint8Array
|
||||
|
||||
interface WebSocketClientOptions {
|
||||
url: string
|
||||
protocols?: string[]
|
||||
reconnect?: boolean // default: true
|
||||
reconnectDelayMs?: number // base delay; uses @suaveplan/retry exponential backoff
|
||||
maxReconnectAttempts?: number // default: 10
|
||||
pingIntervalMs?: number // default: 30000; 0 = disabled
|
||||
timeoutMs?: number // connect timeout; default: 10000
|
||||
WebSocket?: typeof WebSocket // injectable; defaults to globalThis.WebSocket
|
||||
logger?: LoggerLike
|
||||
clock?: Clock
|
||||
}
|
||||
|
||||
class WebSocketClient {
|
||||
constructor(options: WebSocketClientOptions)
|
||||
connect(signal?: AbortSignal): Promise<void>
|
||||
disconnect(): void
|
||||
send(message: WsMessage): void
|
||||
on(event: 'message', listener: (msg: WsMessage) => void): () => void
|
||||
on(event: 'open' | 'close' | 'error', listener: () => void): () => void
|
||||
get readyState(): WsReadyState
|
||||
get reconnectCount(): number
|
||||
}
|
||||
|
||||
function createWebSocketClient(options: WebSocketClientOptions): WebSocketClient
|
||||
```
|
||||
|
||||
### URL utilities
|
||||
|
||||
```ts
|
||||
function buildUrl(base: string, path?: string, params?: Record<string, string | number | boolean>): string
|
||||
function parseQueryString(qs: string): Record<string, string>
|
||||
function serializeQueryString(params: Record<string, string | number | boolean>): string
|
||||
function isAbsoluteUrl(url: string): boolean
|
||||
function joinPaths(...segments: string[]): string
|
||||
```
|
||||
|
||||
### Error codes
|
||||
|
||||
```ts
|
||||
type NetworkErrorCode =
|
||||
| 'NETWORK_REQUEST_FAILED'
|
||||
| 'NETWORK_TIMEOUT'
|
||||
| 'NETWORK_ABORT'
|
||||
| 'NETWORK_BAD_STATUS' // 4xx/5xx
|
||||
| 'NETWORK_PARSE_FAILED'
|
||||
| 'NETWORK_WS_CONNECT_FAILED'
|
||||
| 'NETWORK_WS_SEND_FAILED'
|
||||
| 'NETWORK_WS_MAX_RECONNECTS'
|
||||
| 'NETWORK_INVALID_URL'
|
||||
|
||||
const NETWORK_ERROR_METADATA: Readonly<Record<NetworkErrorCode, CodeMetadata>>
|
||||
function registerNetworkErrorCodes(): void
|
||||
```
|
||||
|
||||
### Zod schemas
|
||||
|
||||
```ts
|
||||
HttpRequestSchema
|
||||
HttpClientOptionsSchema
|
||||
WebSocketClientOptionsSchema
|
||||
```
|
||||
|
||||
### Built-in middleware
|
||||
|
||||
```ts
|
||||
const loggingMiddleware: (logger: LoggerLike) => HttpMiddleware
|
||||
const retryMiddleware: (options?: RetryOptions) => HttpMiddleware // @suaveplan/retry
|
||||
const authMiddleware: (token: string | (() => string)) => HttpMiddleware
|
||||
const rateLimitMiddleware: (options: { maxRpm: number; clock?: Clock }) => HttpMiddleware
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## New additions for genesis
|
||||
|
||||
1. **Middleware pipeline** — `use(middleware)` appends to ordered chain; each middleware gets `(request, next)` and can modify request/response, short-circuit, or retry.
|
||||
2. **Injectable `fetch`** — `HttpClientOptions.fetch` defaults to `globalThis.fetch`; swap to a stub in tests without `vi.mock`.
|
||||
3. **Injectable `WebSocket`** — same pattern; `WebSocketClientOptions.WebSocket` defaults to `globalThis.WebSocket`.
|
||||
4. **Retry wiring** — `retryMiddleware` uses `@suaveplan/retry` with circuit breaker; configurable per `HttpClient` instance.
|
||||
5. **WS auto-reconnect** — exponential backoff via `@suaveplan/retry`; configurable max attempts; ping/pong keepalive.
|
||||
6. **OTel instrumentation** — span `http.request` (attrs: `http.method`, `http.url`, `http.status_code`), counter `http_request_total`, histogram `http_request_duration_ms`; counter `ws.reconnect_total`.
|
||||
7. **Property-based tests** — `buildUrl` round-trip with query params, `parseQueryString(serializeQueryString(params)) == params` for any param map.
|
||||
8. **Benchmark suite** — `buildUrl` with 10 params, middleware chain dispatch (5 middlewares, no-op fetch stub).
|
||||
|
||||
---
|
||||
|
||||
## File layout
|
||||
|
||||
```text
|
||||
universal/network/network/
|
||||
package.json
|
||||
tsconfig.json
|
||||
vitest.config.ts
|
||||
src/
|
||||
index.ts
|
||||
error-codes.ts
|
||||
constants/
|
||||
index.ts
|
||||
types/
|
||||
common.ts
|
||||
common.md
|
||||
core/
|
||||
http-client/
|
||||
http-client.ts
|
||||
http-client.md
|
||||
http-client.types.ts
|
||||
http-client.test.ts
|
||||
index.ts
|
||||
websocket-client/
|
||||
websocket-client.ts
|
||||
websocket-client.md
|
||||
websocket-client.types.ts
|
||||
websocket-client.test.ts
|
||||
index.ts
|
||||
middleware/
|
||||
logging/
|
||||
logging.ts
|
||||
logging.md
|
||||
logging.test.ts
|
||||
index.ts
|
||||
retry/
|
||||
retry.ts
|
||||
retry.md
|
||||
retry.test.ts
|
||||
index.ts
|
||||
auth/
|
||||
auth.ts
|
||||
auth.md
|
||||
auth.test.ts
|
||||
index.ts
|
||||
rate-limit/
|
||||
rate-limit.ts
|
||||
rate-limit.md
|
||||
rate-limit.test.ts
|
||||
index.ts
|
||||
utils/
|
||||
url.ts
|
||||
url.md
|
||||
url.test.ts
|
||||
__benchmarks__/
|
||||
network.bench.ts
|
||||
tests/
|
||||
integration.test.ts
|
||||
property.test.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Impact
|
||||
|
||||
**Downstream**: `@suaveplan/service-discovery`, `@suaveplan/webhook-client` (may refactor to consume), any server/API packages.
|
||||
**Breaking changes**: none — fresh build.
|
||||
**Prerequisites**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/retry`, `@suaveplan/serialize` published to Verdaccio.
|
||||
Reference in New Issue
Block a user