Files
skills/plugins/library-navigator/skills/sp-timing/SKILL.md
2026-05-29 08:20:56 +01:00

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 providing EventEmitter base class for TaskScheduler, Stopwatch, and VectorClockManager.
  • @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 of decay functions 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 = 0 returns current unchanged; decayRate = 0 freezes 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 trailing edge may fire after cancellation — If cancelled during the delay window, the trailing call is dropped.
  • Rate limit returns undefined on exceeded limits — It does not queue calls.
  • Vector clock nodeId must be non-empty string — Constructor validates this.
  • Vector clock duplicate events are silently rejected — Same eventId returns false from receiveEvent.
  • Vector clock maxHistorySize evicts oldest events — Default is 1000; old events are dropped when exceeded.

Checklist

  1. Use createSmoother for stateful animation values; use decay / decayVec2D for one-off smoothing.
  2. Use MockTimer instead of vi.useFakeTimers() for deterministic timer testing in this ecosystem.
  3. Use TaskScheduler instead of raw setInterval for recurring background tasks — it provides concurrency control and events.
  4. Inject a custom timestampProvider into Stopwatch, TaskScheduler, or VectorClockManager for deterministic tests.
  5. Use debounce with maxWait for search inputs that must eventually fire even if the user keeps typing.
  6. Use throttle with leading: true, trailing: true for scroll/resize handlers that need both immediate and final updates.
  7. Use VectorClockManager for distributed event ordering — never rely on wall-clock time across nodes.
  8. Remember scheduler.stop() does not cancel in-flight async tasks — handle cancellation separately if needed.