# Capability Spec: Timing **Package**: `@suaveplan/timing` **Version**: 0.0.0 (genesis) **Status**: ADDED --- ## Requirements ### TM-01 — Injectable clock Every module that reads wall-clock time MUST accept `{ clock?: Clock }` from `@suaveplan/types`. No `Date.now()` or `setTimeout` at module scope. Default clock uses `Date.now()`. **Verified by:** `src/stopwatch/stopwatch.test.ts` AND `src/throttle/throttle.test.ts` AND `src/timer/timer.test.ts` AND `src/timing.spec-conformance.test.ts` AND `tests/no-disallowed.test.ts` ### TM-02 — AbortSignal support `Scheduler.drain(signal?)` and streaming/iterative helpers MUST honour `AbortSignal`. Abort resolves the operation cleanly without throwing unless the signal fires mid-operation, in which case an `AbortError`-compatible rejection is emitted. **Verified by:** `src/scheduler/scheduler.test.ts` AND `src/timing.spec-conformance.test.ts` ### TM-03 — Stopwatch `createStopwatch` MUST return a `Stopwatch` whose `.elapsed` is monotonically non-decreasing between `.start()` and `.stop()`. `.lap()` MUST record the split time relative to the previous lap start (or the stopwatch start). `.reset()` MUST clear all laps and reset elapsed to 0. **Verified by:** `src/stopwatch/stopwatch.test.ts` AND `src/timing.spec-conformance.test.ts` ### TM-04 — Throttle / debounce `throttle` MUST emit at most one invocation per `wait` window (leading or trailing edge, configurable). `debounce` MUST delay invocation until `wait` ms after the last call; `.flush()` MUST invoke immediately; `.cancel()` MUST suppress the pending invocation. `maxWait` option on `debounce` caps total delay. **Verified by:** `src/throttle/throttle.test.ts` AND `src/throttle/debounce.test.ts` AND `src/timing.spec-conformance.test.ts` ### TM-05 — Decay `createDecay` MUST accept a `DecayFunction` (or preset name `'exponential' | 'linear'`). `.tick(elapsed)` advances the decay by `elapsed` milliseconds. The returned `.value` MUST be bounded in `[target, initial]` (or `[initial, target]` for growth). `.reset()` MUST restore `initial`. **Verified by:** `src/decay/decay.test.ts` ### TM-06 — Scheduler `createScheduler` MUST execute scheduled callbacks at or after their requested delay using the injected clock. `.cancel(id)` MUST prevent the callback from firing. `.drain(signal?)` MUST flush all pending callbacks in delay order. After `.dispose()`, any operation MUST throw `TimingError` with code `TIMING.SCHEDULER.DISPOSED`. **Verified by:** `src/scheduler/scheduler.test.ts` AND `src/timing.spec-conformance.test.ts` ### TM-07 — Timer and countdown `createTimer` MUST fire its tick event at each `interval`. `createCountdown` MUST validate that `duration > 0`; otherwise throw `TimingError` with code `TIMING.COUNTDOWN.INVALID_DURATION`. `.pause()` / `.resume()` MUST preserve `.remaining` across pause/resume cycles. **Verified by:** `src/timer/timer.test.ts` AND `src/timing.spec-conformance.test.ts` ### TM-08 — Vector clock `createVectorClock` MUST maintain a `Map` counter. `.increment()` MUST increment the local node's counter. `.update(remote)` MUST take the component-wise maximum. `vectorClockCompare` MUST return `'before' | 'after' | 'concurrent' | 'equal'` correctly for all pairs. `.toJSON()` / `.fromJSON()` MUST produce a lossless round-trip. **Verified by:** `src/vector-clock/vector-clock.test.ts` AND `src/timing.spec-conformance.test.ts` ### TM-09 — Structured errors All thrown errors MUST extend `TimingError` which extends `SuaveplanError`. Codes under `TIMING.*` namespace. **Verified by:** `src/errors/timing-error.test.ts` ### TM-10 — Zod schemas Every public options type MUST have a corresponding Zod schema in a sibling `*.schemas.ts` file. **Verified by:** `src/*/*.schemas.ts` AND `tests/dep-graph.test.ts` ### TM-11 — OTel instrumentation Stopwatch elapsed, scheduler queue depth, and timer tick counter MUST emit via `@opentelemetry/api`. All instruments are no-ops when no exporter is registered. **Verified by:** `src/otel/timing-otel.test.ts` AND `src/timing.spec-conformance.test.ts` ### TM-12 — sideEffects: false + coverage 100% `"sideEffects": false` with tree-shake test. Vitest thresholds at 100/100/100/100. No `.skip` / `.only` in `src/`. **Verified by:** `tests/tree-shake.test.ts` AND `tests/no-disallowed.test.ts` AND coverage gate ### TM-13 — Sliding-window rate limit The package SHALL expose `rateLimit(fn, options): (...args: TArgs) => TReturn | RateLimitedSentinel` where `RateLimitOptions = { maxCalls: number; windowMs: number; clock?: Clock }`. The wrapped function SHALL maintain a FIFO of timestamps; on every call it SHALL evict entries older than `now - windowMs` (where `now` comes from `options.clock` if supplied, otherwise from the system clock injected at construction). When the FIFO size is below `maxCalls`, the wrapped function SHALL push the current timestamp and invoke `fn`. When the size equals `maxCalls`, the wrapper SHALL return the exported sentinel `RATE_LIMITED` instead of invoking `fn`. The factory SHALL throw `TimingError` with code `TIMING.RATE_LIMIT_INVALID_WINDOW` when `maxCalls < 1` or `windowMs <= 0`. **Verified by:** `src/throttle/rate-limit/rate-limit.test.ts` AND `src/throttle/rate-limit/rate-limit.spec-conformance.test.ts` ### TM-14 — RATE_LIMITED sentinel symbol The package SHALL export a unique symbol `RATE_LIMITED` and a type alias `RateLimitedSentinel = typeof RATE_LIMITED`. Callers MUST be able to disambiguate "rate-limited" from "function returned undefined" via reference equality on the sentinel. **Verified by:** `src/throttle/rate-limit/rate-limit.spec-conformance.test.ts` ### TM-15 — once-only invocation with throw-and-retry The package SHALL expose `once(fn): (...args: TArgs) => TReturn`. The wrapper SHALL invoke `fn` on the first successful call and cache its return value. Subsequent calls SHALL return the cached value WITHOUT re-invoking `fn`. When `fn` throws, the wrapper SHALL re-throw the exception AND remain in the not-yet-run state so the next call retries. **Verified by:** `src/throttle/once/once.test.ts` AND `src/throttle/once/once.spec-conformance.test.ts` ### TM-16 — bounded-times invocation The package SHALL expose `times(fn, maxCalls): (...args: TArgs) => TReturn | undefined`. The wrapper SHALL invoke `fn` for the first `maxCalls` calls and return `undefined` for every subsequent call without invoking `fn`. **Verified by:** `src/throttle/times/times.test.ts` AND `src/throttle/times/times.spec-conformance.test.ts` ### TM-17 — Throttle-family subpath exports The package SHALL expose subpath exports `./throttle/rate-limit`, `./throttle/once`, and `./throttle/times` in `package.json::exports`. Each SHALL resolve to its co-located `dist/throttle//index.js` plus `.d.ts`. Importing only `once` SHALL NOT pull in `rateLimit` or `times`. **Verified by:** `src/throttle/rate-limit/rate-limit.spec-conformance.test.ts` AND `src/throttle/once/once.spec-conformance.test.ts` AND `src/throttle/times/times.spec-conformance.test.ts` --- ## Error codes - `TIMING.SCHEDULER.DISPOSED` - `TIMING.COUNTDOWN.INVALID_DURATION` - `TIMING.VECTOR_CLOCK.UNKNOWN_NODE` - `TIMING.RATE_LIMIT_INVALID_WINDOW`