165 lines
5.2 KiB
Markdown
165 lines
5.2 KiB
Markdown
# OpenSpec Change: enterprise-network-standby-transport
|
||
|
||
- **Change ID**: enterprise-network-standby-transport
|
||
- **Package**: `@suaveplan/network-standby-transport`
|
||
- **Category**: universal/network
|
||
- **Target path**: `universal/network/network-standby-transport`
|
||
- **Status**: SUPERSEDED-BY-GENESIS-WAVE
|
||
- **Date**: 2026-04-24
|
||
|
||
---
|
||
|
||
## Summary
|
||
|
||
Deliver `@suaveplan/network-standby-transport` on the genesis branch from scratch at enterprise quality. The package provides a standby/failover transport layer that maintains a pool of transport connections ranked by quality, promotes the best available transport when the primary degrades, and transparently re-routes in-flight messages without loss. Genesis adds structured errors via `@suaveplan/error`, Zod schemas for transport and pool configuration, OTel instrumentation on promotion events, `AbortSignal` on async operations, injectable `Clock` for promotion timers, event-bus integration for standby state changes, and property-based tests for promotion ordering.
|
||
|
||
---
|
||
|
||
## 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 `StandbyTransportError`; stable error codes |
|
||
| `@suaveplan/types` (`workspace:^`) | `LoggerLike`, `Clock`, `EventBusLike` |
|
||
| `@suaveplan/events` (`workspace:^`) | Event bus for standby state changes |
|
||
| `@suaveplan/logger` (`workspace:^`) | Structured logging |
|
||
| `@suaveplan/retry` (`workspace:^`) | Retry on transport failures |
|
||
| `zod` (`catalog:forms`) | Zod schemas |
|
||
| `@opentelemetry/api` (`catalog:observability`) | Spans + counters on transport promotion |
|
||
| `tslib` (`catalog:build`) | Runtime helpers |
|
||
|
||
Dev deps: `vitest`, `@vitest/coverage-istanbul`, `fast-check` (all `catalog:testing`).
|
||
|
||
---
|
||
|
||
## Public API surface
|
||
|
||
### Core
|
||
|
||
```ts
|
||
function createStandbyTransportPool(options: StandbyTransportPoolOptions): StandbyTransportPool
|
||
|
||
interface StandbyTransportPool {
|
||
readonly primary: Transport | null
|
||
readonly standby: ReadonlyArray<Transport>
|
||
readonly state: PoolState
|
||
add(transport: Transport): void
|
||
remove(transportId: string): void
|
||
send(message: unknown, signal?: AbortSignal): Promise<void>
|
||
promote(transportId: string): void
|
||
on(event: PoolEvent, handler: (data: PoolEventData) => void): () => void
|
||
}
|
||
```
|
||
|
||
### Transport interface
|
||
|
||
```ts
|
||
interface Transport {
|
||
readonly id: string
|
||
readonly priority: number
|
||
readonly state: TransportState
|
||
readonly score: number // 0–100 quality score
|
||
send(message: unknown, signal?: AbortSignal): Promise<void>
|
||
connect(signal?: AbortSignal): Promise<void>
|
||
disconnect(): void
|
||
on(event: TransportEvent, handler: (data: unknown) => void): () => void
|
||
}
|
||
|
||
type TransportState = 'disconnected' | 'connecting' | 'connected' | 'degraded' | 'failed'
|
||
type TransportEvent = 'connect' | 'disconnect' | 'degrade' | 'recover' | 'score-change'
|
||
type PoolState = 'healthy' | 'degraded' | 'standby-active' | 'no-transport'
|
||
type PoolEvent = 'primary-promoted' | 'standby-promoted' | 'transport-failed' | 'state-change'
|
||
|
||
interface PoolEventData {
|
||
previousPrimary?: Transport
|
||
newPrimary?: Transport
|
||
reason?: string
|
||
}
|
||
```
|
||
|
||
### Options
|
||
|
||
```ts
|
||
interface StandbyTransportPoolOptions {
|
||
promotionThreshold?: number // min score to promote, default 30
|
||
degradationThreshold?: number // score below which primary is considered degraded, default 60
|
||
promotionDelayMs?: number // debounce window before promotion, default 1000
|
||
maxStandby?: number
|
||
logger?: LoggerLike
|
||
clock?: Clock
|
||
eventBus?: EventBusLike
|
||
}
|
||
```
|
||
|
||
### Error codes
|
||
|
||
```ts
|
||
type StandbyTransportErrorCode =
|
||
| 'STANDBY_NO_TRANSPORT'
|
||
| 'STANDBY_SEND_FAILED'
|
||
| 'STANDBY_TRANSPORT_NOT_FOUND'
|
||
| 'STANDBY_ALL_FAILED'
|
||
| 'STANDBY_ABORT'
|
||
|
||
function registerStandbyTransportErrorCodes(): void
|
||
```
|
||
|
||
---
|
||
|
||
## Genesis additions
|
||
|
||
1. **Structured `StandbyTransportError`** with stable `STANDBY.*` codes
|
||
2. **Zod schemas** for `StandbyTransportPoolOptions`
|
||
3. **Score-based promotion** — promotion triggered when primary score drops below `degradationThreshold`
|
||
4. **Promotion debounce** — uses `@suaveplan/timing` throttle to avoid promotion thrash
|
||
5. **In-flight message re-routing** — queued messages replayed on new primary after promotion
|
||
6. **Injectable `Clock`** for promotion delay testing
|
||
7. **OTel instrumentation** — span `standby.promote`, counter `standby.promotions_total`, histogram `standby.promotion_delay_ms`
|
||
8. **Property-based tests** — pool always has a primary when transports are available; promotion selects highest-scoring standby
|
||
|
||
---
|
||
|
||
## File layout
|
||
|
||
```text
|
||
universal/network/network-standby-transport/
|
||
package.json
|
||
tsconfig.json
|
||
vitest.config.ts
|
||
src/
|
||
index.ts
|
||
error-codes.ts
|
||
constants/
|
||
index.ts
|
||
types/
|
||
common.ts
|
||
common.md
|
||
core/
|
||
pool/
|
||
pool.ts
|
||
pool.md
|
||
pool.types.ts
|
||
pool.test.ts
|
||
index.ts
|
||
promoter/
|
||
promoter.ts
|
||
promoter.md
|
||
promoter.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` must be published.
|