# `principal-expiry` _(no submodules)_ ## Purpose Tracks the underlying token expiry for a frozen principal so the engine can drop the connection at the right moment. The contract requires the `Connection.principal` field to be immutable for the connection's lifetime; this module is the signal the engine uses to flip the connection state to `closing` once the original token would have lapsed. The watcher is clock-injected so tests can drive expiry deterministically without sleeping. In production the engine polls `check()` from a regular interval (default: aligned to the heartbeat timer). ## Features - Single-fire — `onExpired` runs at most once per watcher instance. - Throws `WS_BUN.PRINCIPAL_EXPIRED` after firing so the engine sees the same shaped error as every other WS_BUN failure mode. - `hasFired` accessor for test assertions and lifecycle bookkeeping. ## Usage: Basic ```ts import { PrincipalExpiryWatcher } from "@suaveplan/api-ws-bun"; const watcher = new PrincipalExpiryWatcher({ connectionId: "c1", principalId: "u1", expiresAt: Date.now() + 60_000, clock: { now: () => Date.now() }, onExpired: ({ connectionId }) => emitBus(connectionId), }); setInterval(() => { try { watcher.check(); } catch { // engine closes the connection } }, 5_000); ``` ## Usage: Advanced ```ts import { PrincipalExpiryWatcher, WS_BUN_CODES } from "@suaveplan/api-ws-bun"; let busEvent: { connectionId: string; principalId: string } | null = null; const clock = { value: 0 }; const watcher = new PrincipalExpiryWatcher({ connectionId: "c1", principalId: "u1", expiresAt: 100, clock: { now: () => clock.value }, onExpired: (event) => { busEvent = event; }, }); clock.value = 99; watcher.check(); // no-op clock.value = 100; try { watcher.check(); } catch (err) { // err.code === WS_BUN_CODES.PRINCIPAL_EXPIRED } console.log(busEvent?.principalId); // "u1" ``` ## API Reference - `PrincipalExpiryClock` — `{ now(): number }`. - `PrincipalExpiryListener` — `(event) => void`. - `PrincipalExpiryWatcherOptions` — `{ connectionId, principalId, expiresAt, clock, onExpired }`. - `new PrincipalExpiryWatcher(options)` — constructor. - `watcher.check(): void` — run an expiry check. - `watcher.hasFired: boolean` — has the expiry already triggered. ## Implementation Notes The watcher does NOT subscribe to an event bus on its own — the engine wires the `onExpired` callback into whatever bus the host owns. This keeps `principal-expiry` free of `EventBus` dependencies and lets the test suite assert on `onExpired` directly via a captured closure. `check()` uses `>=` so the moment `now === expiresAt`, the principal is treated as expired. This matches the convention used elsewhere in the package (e.g. tenant-cap), keeping the "cliff" semantics uniform.