Move docs/ package mirror into document-navigator plugin
Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
# `heartbeat` — ping/pong policy + evaluator
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Every WebSocket connection that the Suaveplan API host accepts is policed
|
||||
by a heartbeat: the engine pings at a fixed cadence and forcibly closes the
|
||||
connection once a documented number of consecutive pongs are missed. This
|
||||
module owns the abstract policy and the stateless evaluator that concrete
|
||||
engines call from their heartbeat tick. The defaults — 30000ms cadence,
|
||||
two missed pongs to abort — track design doc §11.4.
|
||||
|
||||
The evaluator is intentionally stateless: it consumes scalar measurements
|
||||
the engine already tracks and returns the next action. State lives with
|
||||
the engine; the contract lives here.
|
||||
|
||||
## Features
|
||||
|
||||
- `HeartbeatPolicy` interface (`heartbeatMs`, `maxMissedPongs`).
|
||||
- `DEFAULT_HEARTBEAT_POLICY` — frozen design-doc defaults.
|
||||
- `evaluateHeartbeat(msSinceLastPong, consecutiveMissedPongs, policy?)` →
|
||||
`"ok" | "ping" | "abort"`.
|
||||
- `raiseHeartbeatFailed(connectionId)` throws the canonical `WsError`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { evaluateHeartbeat } from "@suaveplan/api-ws/heartbeat";
|
||||
|
||||
const action = evaluateHeartbeat(msSinceLastPong, missedPongs);
|
||||
if (action === "ping") sendPing();
|
||||
if (action === "abort") closeAbnormal();
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
evaluateHeartbeat,
|
||||
raiseHeartbeatFailed,
|
||||
type HeartbeatPolicy,
|
||||
} from "@suaveplan/api-ws/heartbeat";
|
||||
|
||||
class HeartbeatTicker {
|
||||
private missed = 0;
|
||||
private lastPong = Date.now();
|
||||
|
||||
constructor(private readonly policy: HeartbeatPolicy) {}
|
||||
|
||||
tick(now: number, connectionId: string): void {
|
||||
const action = evaluateHeartbeat(now - this.lastPong, this.missed, this.policy);
|
||||
if (action === "ping") {
|
||||
this.missed += 1;
|
||||
} else if (action === "abort") {
|
||||
raiseHeartbeatFailed(connectionId);
|
||||
}
|
||||
}
|
||||
|
||||
onPong(now: number): void {
|
||||
this.missed = 0;
|
||||
this.lastPong = now;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface HeartbeatPolicy { heartbeatMs: number; maxMissedPongs: number }`.
|
||||
- `const DEFAULT_HEARTBEAT_POLICY: HeartbeatPolicy` (frozen).
|
||||
- `type HeartbeatAction = "ok" | "ping" | "abort"`.
|
||||
- `evaluateHeartbeat(msSinceLastPong, consecutiveMissedPongs, policy?): HeartbeatAction`.
|
||||
- `raiseHeartbeatFailed(connectionId): never`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The evaluator checks `consecutiveMissedPongs` before `msSinceLastPong`
|
||||
so an already-failed connection aborts immediately on the next tick
|
||||
even if the engine's clock briefly stalls.
|
||||
- `consecutiveMissedPongs` is the engine's counter — every ping increments
|
||||
it; every pong resets it. The evaluator does not own that state.
|
||||
- `raiseHeartbeatFailed` is `never` so callers can use it as a terminal
|
||||
branch without an explicit unreachable assertion.
|
||||
Reference in New Issue
Block a user