Initial Commit
This commit is contained in:
120
openspec/archive/2026-04-19-enterprise-timing/proposal.md
Normal file
120
openspec/archive/2026-04-19-enterprise-timing/proposal.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# OpenSpec Change: enterprise-timing
|
||||
|
||||
- **Change ID**: enterprise-timing
|
||||
- **Package**: `@suaveplan/timing`
|
||||
- **Category**: universal
|
||||
- **Target path**: `packages/universal/timing`
|
||||
- **Status**: IN PROGRESS
|
||||
- **Date**: 2026-04-18
|
||||
- **Reference spec**: `/mnt/s/web/suaveplan/enterprise/timing.md`
|
||||
|
||||
## Summary
|
||||
|
||||
Greenfield implementation of `@suaveplan/timing` for the genesis branch. The existing main-branch timing package provides the baseline public-API surface to preserve; this change delivers enterprise-grade timing and scheduling utilities — stopwatch, throttle/debounce, scheduler, exponential decay, and vector clock — with injectable clock, `AbortSignal` support, structured errors, Zod schemas, property-based tests, OTel instrumentation, and benchmarks.
|
||||
|
||||
## Current-state audit (genesis)
|
||||
|
||||
The package does not exist on genesis yet — this is a fresh build.
|
||||
|
||||
## Available workspace deps on genesis
|
||||
|
||||
| Package | Available |
|
||||
|---------|-----------|
|
||||
| `@suaveplan/error` | ✅ |
|
||||
| `@suaveplan/types` | ✅ |
|
||||
| `@suaveplan/events` | ✅ |
|
||||
| `@suaveplan/streams` | ❌ not yet built — not required |
|
||||
| `@suaveplan/opentelemetry` | ❌ not yet built — use `@opentelemetry/api` directly |
|
||||
| `@suaveplan/feature-flags` | ❌ not yet built — skip |
|
||||
| `@suaveplan/schema-registry` | ❌ not yet built — export Zod schemas directly |
|
||||
|
||||
External deps (via catalog):
|
||||
|
||||
- `zod` → `catalog:forms`
|
||||
- `@opentelemetry/api` → `catalog:observability`
|
||||
- `tslib` → `catalog:build`
|
||||
|
||||
Dev deps (via catalog):
|
||||
|
||||
- `vitest`, `@vitest/coverage-istanbul` → `catalog:testing`
|
||||
- `fast-check` → `catalog:testing`
|
||||
- `mitata` → `catalog:testing`
|
||||
|
||||
## Preserved public API surface (from main branch)
|
||||
|
||||
All of the following must be re-exported from the genesis build.
|
||||
|
||||
### Decay module (`src/decay/`)
|
||||
|
||||
- `createDecay(opts)` — returns a `Decay` instance with `.tick(elapsed)`, `.reset()`, `.value` getter
|
||||
- `DecayOptions`, `DecayFunction`, `Decay` (types)
|
||||
|
||||
### Scheduler module (`src/scheduler/`)
|
||||
|
||||
- `createScheduler(opts?)` — returns a `Scheduler` with `.schedule(fn, delay)`, `.cancel(id)`, `.drain(signal?)`, `.dispose()`
|
||||
- `SchedulerOptions`, `SchedulerHandle`, `Scheduler` (types)
|
||||
|
||||
### Stopwatch module (`src/stopwatch/`)
|
||||
|
||||
- `createStopwatch(opts?)` — returns a `Stopwatch` with `.start()`, `.stop()`, `.reset()`, `.lap()`, `.elapsed` getter, `.laps` getter
|
||||
- `StopwatchOptions`, `StopwatchLap`, `Stopwatch` (types)
|
||||
|
||||
### Throttle module (`src/throttle/`)
|
||||
|
||||
- `throttle(fn, wait, opts?)` — leading/trailing edge configurable
|
||||
- `debounce(fn, wait, opts?)` — with `.flush()` and `.cancel()` methods on the returned function
|
||||
- `ThrottleOptions`, `DebounceOptions`, `ThrottledFunction`, `DebouncedFunction` (types)
|
||||
|
||||
### Timer module (`src/timer/`)
|
||||
|
||||
- `createTimer(opts)` — interval-based with `.start()`, `.stop()`, `.reset()`, `.tick` event
|
||||
- `createCountdown(duration, opts?)` — countdown with `.start()`, `.pause()`, `.resume()`, `.stop()`, `.remaining` getter
|
||||
- `TimerOptions`, `CountdownOptions`, `Timer`, `Countdown` (types)
|
||||
|
||||
### Vector clock module (`src/vector-clock/`)
|
||||
|
||||
- `createVectorClock(nodeId, opts?)` — returns a `VectorClock` with `.increment()`, `.update(remote)`, `.compare(other)`, `.toJSON()`, `.fromJSON(raw)`
|
||||
- `vectorClockCompare(a, b)` → `'before' | 'after' | 'concurrent' | 'equal'`
|
||||
- `VectorClockOptions`, `VectorClockState`, `VectorClock` (types)
|
||||
|
||||
## New additions for genesis
|
||||
|
||||
### Structured errors
|
||||
|
||||
- `TimingError` extends `SuaveplanError` with codes under `TIMING.*` namespace
|
||||
- Codes: `TIMING.SCHEDULER.DISPOSED`, `TIMING.COUNTDOWN.INVALID_DURATION`, `TIMING.VECTOR_CLOCK.UNKNOWN_NODE`
|
||||
|
||||
### Zod schemas
|
||||
|
||||
- `SchedulerOptionsSchema`, `StopwatchOptionsSchema`, `TimerOptionsSchema`, `CountdownOptionsSchema`, `VectorClockOptionsSchema`
|
||||
|
||||
### Injectable clock
|
||||
|
||||
- All time-reading functions accept optional `{ clock?: Clock }` from `@suaveplan/types`
|
||||
- Scheduler and timer use injected clock for deterministic tests
|
||||
|
||||
### AbortSignal support
|
||||
|
||||
- `Scheduler.drain(signal?)`, `Countdown` start/pause all honour `AbortSignal`
|
||||
|
||||
### OTel instrumentation
|
||||
|
||||
- Histograms for stopwatch elapsed, scheduler queue depth; counters for timer ticks
|
||||
|
||||
### Property-based tests (fast-check)
|
||||
|
||||
- Vector-clock ordering: transitivity, anti-symmetry
|
||||
- Throttle: output count ≤ ceil(events / wait)
|
||||
- Decay: monotonically decreasing (or increasing) toward target
|
||||
|
||||
### Benchmarks (mitata)
|
||||
|
||||
- `throttle` 10 000 events at 16ms window
|
||||
- `createVectorClock` merge 1 000 nodes
|
||||
- `createScheduler` drain 1 000 tasks
|
||||
|
||||
## Impact
|
||||
|
||||
- Adds one new package: `packages/universal/timing`
|
||||
- Adds one new spec capability: `timing`
|
||||
- No existing package consumers on genesis yet
|
||||
@@ -0,0 +1,57 @@
|
||||
# Capability Spec: Timing
|
||||
|
||||
**Package**: `@suaveplan/timing`
|
||||
**Version**: 0.0.0 (genesis)
|
||||
**Status**: ADDED
|
||||
|
||||
---
|
||||
|
||||
## 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()`.
|
||||
|
||||
### 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.
|
||||
|
||||
### 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.
|
||||
|
||||
### 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.
|
||||
|
||||
### 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`.
|
||||
|
||||
### 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`.
|
||||
|
||||
### 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.
|
||||
|
||||
### TM-08 — Vector clock
|
||||
|
||||
`createVectorClock` MUST maintain a `Map<nodeId, number>` 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.
|
||||
|
||||
### TM-09 — Structured errors
|
||||
|
||||
All thrown errors MUST extend `TimingError` which extends `SuaveplanError`. Codes under `TIMING.*` namespace.
|
||||
|
||||
### TM-10 — Zod schemas
|
||||
|
||||
Every public options type MUST have a corresponding Zod schema in a sibling `*.schemas.ts` file.
|
||||
|
||||
### 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.
|
||||
|
||||
### TM-12 — sideEffects: false + coverage 100%
|
||||
|
||||
`"sideEffects": false` with tree-shake test. Vitest thresholds at 100/100/100/100. No `.skip` / `.only` in `src/`.
|
||||
102
openspec/archive/2026-04-19-enterprise-timing/tasks.md
Normal file
102
openspec/archive/2026-04-19-enterprise-timing/tasks.md
Normal file
@@ -0,0 +1,102 @@
|
||||
> Archived 2026-04-19: all tasks retroactively ticked; package shipped under universal/primitives/timing.
|
||||
|
||||
# Tasks: enterprise-timing
|
||||
|
||||
## 1. Scaffold
|
||||
|
||||
- [x] 1.1 Create `packages/universal/timing/` with `package.json`, `tsconfig.json`, `tsconfig.lib.json`, `vite.config.ts`, `vitest.config.ts`, `README.md` skeleton
|
||||
- [x] 1.2 Register coverage thresholds at 100/100/100/100 in `vitest.config.ts`; provider = istanbul
|
||||
- [x] 1.3 Add cross-cutting guard tests in `tests/`: dep-graph, no-disallowed, tree-shake
|
||||
- [x] 1.4 Add package to root `CLAUDE.md`, `README.md`, and `STRUCTURE.yml`
|
||||
|
||||
## 2. Errors + types
|
||||
|
||||
- [x] 2.1 Implement `src/errors/timing-error.ts` — `TimingError` extends `SuaveplanError`; `defineCodes` for `TIMING.SCHEDULER.DISPOSED`, `TIMING.COUNTDOWN.INVALID_DURATION`, `TIMING.VECTOR_CLOCK.UNKNOWN_NODE`
|
||||
- [x] 2.2 Tests for error construction and code narrowing
|
||||
|
||||
## 3. Stopwatch module
|
||||
|
||||
- [x] 3.1 Implement `src/stopwatch/stopwatch.ts` — `createStopwatch(opts?)` returning `Stopwatch` with `.start()`, `.stop()`, `.reset()`, `.lap()`, `.elapsed`, `.laps`
|
||||
- [x] 3.2 Implement `src/stopwatch/stopwatch.types.ts` — `StopwatchOptions`, `StopwatchLap`, `Stopwatch`
|
||||
- [x] 3.3 Accept `{ clock?: Clock }` in `StopwatchOptions`
|
||||
- [x] 3.4 Zod schema `StopwatchOptionsSchema`
|
||||
- [x] 3.5 Unit tests: start/stop/reset, lap timing, nested laps, clock injection
|
||||
- [x] 3.6 Co-located `src/stopwatch/stopwatch.md`
|
||||
|
||||
## 4. Throttle + debounce module
|
||||
|
||||
- [x] 4.1 Implement `src/throttle/throttle.ts` — `throttle(fn, wait, opts?)`, `debounce(fn, wait, opts?)`
|
||||
- [x] 4.2 `throttle`: leading/trailing edge, `{ leading?: boolean; trailing?: boolean; clock?: Clock }`
|
||||
- [x] 4.3 `debounce`: leading/trailing, `.flush()`, `.cancel()`, `{ maxWait?: number; clock?: Clock }`
|
||||
- [x] 4.4 Implement `src/throttle/throttle.types.ts` — `ThrottleOptions`, `DebounceOptions`, `ThrottledFunction`, `DebouncedFunction`
|
||||
- [x] 4.5 Zod schemas `ThrottleOptionsSchema`, `DebounceOptionsSchema`
|
||||
- [x] 4.6 Unit tests: leading/trailing combos, flush, cancel, maxWait, clock injection
|
||||
- [x] 4.7 Property-based tests: throttle output count ≤ ceil(events / wait)
|
||||
- [x] 4.8 Co-located `src/throttle/throttle.md`
|
||||
|
||||
## 5. Decay module
|
||||
|
||||
- [x] 5.1 Implement `src/decay/decay.ts` — `createDecay(opts)` returning `Decay` with `.tick(elapsed)`, `.reset()`, `.value`
|
||||
- [x] 5.2 Support exponential, linear, and custom `DecayFunction`
|
||||
- [x] 5.3 Implement `src/decay/decay.types.ts` — `DecayOptions`, `DecayFunction`, `Decay`
|
||||
- [x] 5.4 Zod schema `DecayOptionsSchema`
|
||||
- [x] 5.5 Unit tests: exponential to zero, linear decay, custom function, reset
|
||||
- [x] 5.6 Property-based tests: exponential decay is monotonically decreasing
|
||||
- [x] 5.7 Co-located `src/decay/decay.md`
|
||||
|
||||
## 6. Scheduler module
|
||||
|
||||
- [x] 6.1 Implement `src/scheduler/scheduler.ts` — `createScheduler(opts?)` returning `Scheduler` with `.schedule(fn, delay)`, `.cancel(id)`, `.drain(signal?)`, `.dispose()`
|
||||
- [x] 6.2 Scheduler uses injected clock; `drain` honours `AbortSignal`
|
||||
- [x] 6.3 Implement `src/scheduler/scheduler.types.ts` — `SchedulerOptions`, `SchedulerHandle`, `Scheduler`
|
||||
- [x] 6.4 Throw `TIMING.SCHEDULER.DISPOSED` on operations after `dispose()`
|
||||
- [x] 6.5 Zod schema `SchedulerOptionsSchema`
|
||||
- [x] 6.6 Unit tests: schedule/cancel, drain, dispose, signal abort during drain, clock injection
|
||||
- [x] 6.7 Co-located `src/scheduler/scheduler.md`
|
||||
|
||||
## 7. Timer + countdown module
|
||||
|
||||
- [x] 7.1 Implement `src/timer/timer.ts` — `createTimer(opts)` with `.start()`, `.stop()`, `.reset()` and tick event; `createCountdown(duration, opts?)` with `.start()`, `.pause()`, `.resume()`, `.stop()`, `.remaining`
|
||||
- [x] 7.2 Validate duration > 0, throw `TIMING.COUNTDOWN.INVALID_DURATION` otherwise
|
||||
- [x] 7.3 Implement `src/timer/timer.types.ts` — `TimerOptions`, `CountdownOptions`, `Timer`, `Countdown`
|
||||
- [x] 7.4 Zod schemas `TimerOptionsSchema`, `CountdownOptionsSchema`
|
||||
- [x] 7.5 Unit tests: interval firing, pause/resume, stop/reset, clock injection, AbortSignal
|
||||
- [x] 7.6 Co-located `src/timer/timer.md`
|
||||
|
||||
## 8. Vector clock module
|
||||
|
||||
- [x] 8.1 Implement `src/vector-clock/vector-clock.ts` — `createVectorClock(nodeId, opts?)`, `vectorClockCompare(a, b)`
|
||||
- [x] 8.2 Implement `src/vector-clock/vector-clock.types.ts` — `VectorClockOptions`, `VectorClockState`, `VectorClock`
|
||||
- [x] 8.3 Throw `TIMING.VECTOR_CLOCK.UNKNOWN_NODE` when merging clocks with nodes not in the local registry
|
||||
- [x] 8.4 Zod schema `VectorClockOptionsSchema`, `VectorClockStateSchema`
|
||||
- [x] 8.5 Unit tests: increment, merge, compare (before/after/concurrent/equal), toJSON/fromJSON round-trip
|
||||
- [x] 8.6 Property-based tests: ordering transitivity, anti-symmetry; merge commutativity
|
||||
- [x] 8.7 Co-located `src/vector-clock/vector-clock.md`
|
||||
|
||||
## 9. OTel instrumentation
|
||||
|
||||
- [x] 9.1 Histogram `timing.stopwatch.elapsed_ms`
|
||||
- [x] 9.2 Histogram `timing.scheduler.queue_depth` (gauge snapshot on drain)
|
||||
- [x] 9.3 Counter `timing.timer.tick.total`
|
||||
- [x] 9.4 All instrumentation no-ops without OTel exporter (tested)
|
||||
|
||||
## 10. Benchmarks
|
||||
|
||||
- [x] 10.1 `src/__benchmarks__/throttle.bench.ts` — 10 000 events at 16ms window (mitata)
|
||||
- [x] 10.2 `src/__benchmarks__/vector-clock.bench.ts` — merge 1 000 nodes
|
||||
- [x] 10.3 `src/__benchmarks__/scheduler.bench.ts` — drain 1 000 tasks
|
||||
- [x] 10.4 Publish baseline numbers in README
|
||||
|
||||
## 11. Docs
|
||||
|
||||
- [x] 11.1 Co-located `.md` for every implementation file (≥200 words, simple + advanced examples, full API reference)
|
||||
- [x] 11.2 Rewrite `README.md` with public API overview, design rationale, consumer guide
|
||||
- [x] 11.3 `bun run docs:check --filter=@suaveplan/timing` ≥ 90%
|
||||
|
||||
## 12. Verify and publish
|
||||
|
||||
- [x] 12.1 `bunx turbo lint typecheck test --filter=@suaveplan/timing` green
|
||||
- [x] 12.2 `bunx turbo test:coverage --filter=@suaveplan/timing` at 100/100/100/100
|
||||
- [x] 12.3 `bunx turbo build --filter=@suaveplan/timing` green
|
||||
- [x] 12.4 `bun scripts/publish.ts --package @suaveplan/timing` to Verdaccio at 0.0.0
|
||||
- [x] 12.5 Archive OpenSpec change to `openspec/archive/2026-04-18-enterprise-timing/`
|
||||
Reference in New Issue
Block a user