first commit
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
# `principal-expiry`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## 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.
|
||||
Reference in New Issue
Block a user