9.3 KiB
9.3 KiB
name, description
| name | description |
|---|---|
| sp-timing | Timing and scheduling utilities with exponential decay smoothing, mockable timers, task scheduler, stopwatch, throttle/debounce, and distributed vector clocks. |
Timing for Suaveplan
Comprehensive timing and scheduling utility library. Provides frame-rate independent exponential decay smoothing, injectable timer abstractions with mock timers, concurrent task scheduling, high-precision stopwatch, throttle/debounce/rate-limiting, and distributed vector clocks for event ordering.
Architecture
| Component | Responsibility |
|---|---|
decay |
Frame-rate independent exponential smoothing: target + (current - target) * exp(-decayRate * dt) |
createSmoother |
Stateful smoother with .value, .update(target, dt), .snap(value), .setDecay(rate) |
decayVec2D / decayVec3D |
Convenience wrappers for vector smoothing |
DefaultTimer |
Wraps global setTimeout / setInterval / Date.now() |
MockTimer |
Deterministic fake timer with advance(ms), execution tracking, and maxCallbacks safety limit |
TaskScheduler |
Event-driven concurrent task scheduler with maxConcurrentTasks, recurring/delayed tasks, and cancellation |
Stopwatch |
Event-driven stopwatch with lap timing, statistics, and configurable precision |
throttle |
Limits execution to at most once per delay; supports leading and trailing edges |
debounce |
Delays execution until calls stop; supports leading, trailing, and maxWait |
rateLimit |
Sliding-window rate limiter; returns undefined when limit exceeded |
VectorClockManager |
Distributed vector clock with event recording, causal ordering (happenedBefore), concurrency detection (areConcurrent), and sync partners |
File Structure
foundation/timing/timing/
src/decay/decay.ts # decay(), createSmoother(), decayVec(), decay2D, decay3D
src/timer/timer.ts # DefaultTimer, MockTimer, createTimer(), createMockTimer()
src/scheduler/scheduler.ts # TaskScheduler, createScheduler(), createHighConcurrencyScheduler()
src/stopwatch/stopwatch.ts # Stopwatch, createStopwatch(), createHighPrecisionStopwatch()
src/throttle/throttle.ts # throttle(), debounce(), rateLimit(), once(), times()
src/vector-clock/vector-clock.ts # VectorClockManager, createVectorClockManager()
src/index.ts # Barrel re-export
Sibling packages:
@suaveplan/events— Direct dependency providingEventEmitterbase class forTaskScheduler,Stopwatch, andVectorClockManager.@suaveplan/testing— Dev dependency providing test harness.@suaveplan/datetime— Complementary for calendar/date formatting; timing focuses on elapsed time and scheduling.@suaveplan/game-loop— Likely consumer ofdecayfunctions for frame-rate independent animation.
Patterns
Quick Start
import { decay, throttle, createStopwatch } from "@suaveplan/timing";
// Smooth a value toward target
const smoothed = decay(0, 100, 16, 0.016);
// Throttle a function
const throttled = throttle(() => save(), 1000);
// Time an operation
const sw = createStopwatch();
sw.start();
// ... work ...
const lap = sw.lap();
Decay — Frame-Rate Independent Smoothing
import { decay, createSmoother, decayVec2D } from "@suaveplan/timing";
// Direct decay
decay(0, 100, 16, 0.016); // ≈ 79.3
// Stateful smoother
const smoother = createSmoother(0, 16);
smoother.update(100, 0.016);
console.log(smoother.value); // ≈ 79.3
smoother.snap(50); // instantly set to 50
smoother.setDecay(8); // change decay rate
// 2D vector smoothing
const current: [number, number] = [0, 0];
const target: [number, number] = [100, 200];
decayVec2D(current, target, 16, 0.016);
// [≈79.3, ≈158.6]
MockTimer — Deterministic Testing
import { createMockTimer } from "@suaveplan/timing";
const timer = createMockTimer();
const fn = vi.fn();
timer.setTimeout(fn, 500);
timer.advanceTime(500);
expect(fn).toHaveBeenCalledOnce();
// Intervals
const fn2 = vi.fn();
timer.setInterval(fn2, 100);
timer.advanceTime(300);
expect(fn2).toHaveBeenCalledTimes(3);
TaskScheduler — Recurring Tasks with Concurrency
import { createScheduler } from "@suaveplan/timing";
const scheduler = createScheduler({ maxConcurrentTasks: 2 });
// One-time task
scheduler.scheduleTask(async () => { /* work */ }, "task-data");
// Recurring task (max 3 runs)
scheduler.scheduleRecurringTask(
async () => { /* work */ },
"recurring-data",
50, // interval ms
{ maxRuns: 3 }
);
// Delayed task
scheduler.scheduleDelayedTask(
async () => { /* work */ },
"delayed-data",
1000 // delay ms
);
scheduler.start();
// Event-driven
scheduler.on("task:completed", (event) => {
console.log("Completed:", event.taskId);
});
Stopwatch — Lap Timing and Statistics
import { createStopwatch, createHighPrecisionStopwatch } from "@suaveplan/timing";
const sw = createStopwatch({ autoStart: true });
// ... work ...
const lap1 = sw.lap(); // { lapTime, totalTime }
// ... more work ...
const lap2 = sw.lap();
const stats = sw.getStats();
// { fastestLap, slowestLap, averageLapTime, totalTime, lapCount }
sw.stop();
sw.formatTime(1234567); // "20:34.567"
Throttle / Debounce — Leading and Trailing Edge
import { throttle, debounce } from "@suaveplan/timing";
// Throttle with leading + trailing
const throttled = throttle(fn, 100, { leading: true, trailing: true });
throttled(); // leading edge executes immediately
throttled(); // queued for trailing edge
// after 100ms: trailing edge executes
// Debounce with maxWait
const debounced = debounce(fn, 100, { maxWait: 200 });
debounced();
// forces execution after 200ms even if calls keep coming
// Cancel pending executions
throttled.cancel();
debounced.flush(); // execute immediately if pending
Rate Limiting
import { rateLimit, once, times } from "@suaveplan/timing";
// Sliding window rate limiter
const limited = rateLimit(fn, 5, 60000); // 5 calls per minute
limited(); // executes
// ... 5 calls later ...
limited(); // undefined (limit exceeded)
// Execute at most once
const init = once(initialize);
init(); // runs
init(); // returns cached result
// Execute at most N times
const counter = times(fn, 3);
counter(); // 1
counter(); // 2
counter(); // 3
counter(); // undefined
Vector Clock — Distributed Event Ordering
import { createVectorClockManager } from "@suaveplan/timing";
const node1 = createVectorClockManager("node1");
const node2 = createVectorClockManager("node2");
node1.addSyncPartner("node2");
node2.addSyncPartner("node1");
// Record local event
const event1 = node1.recordEvent("user:created", { id: 1 });
// Receive remote event
node2.receiveEvent(event1);
// Causal ordering
const event2 = node2.recordEvent("user:updated", { id: 1 });
expect(node1.happenedBefore(event1, event2)).toBe(true);
// Detect concurrent events
const ev1 = node1.recordEvent("a", "d");
const ev2 = node2.recordEvent("b", "d");
expect(node1.areConcurrent(ev1, ev2)).toBe(true);
// Ordered history
const ordered = node1.getOrderedEvents();
Constraints
- Decay never overshoots — Due to the exponential formula, the result always approaches but never exceeds the target.
- Decay with
dt = 0returnscurrentunchanged;decayRate = 0freezes the value. - MockTimer cannot advance backwards —
advanceTime(-n)throws. - MockTimer callbacks that throw are caught and logged — Execution of remaining callbacks continues.
- Scheduler
stop()does not abort running tasks — It clears pending timers but currently executing async tasks continue until completion. - Scheduler error handlers that throw are silently caught — Prevents infinite error loops.
- Stopwatch precision rounding —
Math.round(time / precision) * precision; a 5ms elapsed time with precision 10 rounds to 10ms. - Throttle
trailingedge may fire after cancellation — If cancelled during the delay window, the trailing call is dropped. - Rate limit returns
undefinedon exceeded limits — It does not queue calls. - Vector clock
nodeIdmust be non-empty string — Constructor validates this. - Vector clock duplicate events are silently rejected — Same
eventIdreturnsfalsefromreceiveEvent. - Vector clock
maxHistorySizeevicts oldest events — Default is 1000; old events are dropped when exceeded.
Checklist
- Use
createSmootherfor stateful animation values; usedecay/decayVec2Dfor one-off smoothing. - Use
MockTimerinstead ofvi.useFakeTimers()for deterministic timer testing in this ecosystem. - Use
TaskSchedulerinstead of rawsetIntervalfor recurring background tasks — it provides concurrency control and events. - Inject a custom
timestampProviderintoStopwatch,TaskScheduler, orVectorClockManagerfor deterministic tests. - Use
debouncewithmaxWaitfor search inputs that must eventually fire even if the user keeps typing. - Use
throttlewithleading: true, trailing: truefor scroll/resize handlers that need both immediate and final updates. - Use
VectorClockManagerfor distributed event ordering — never rely on wall-clock time across nodes. - Remember
scheduler.stop()does not cancel in-flight async tasks — handle cancellation separately if needed.