Skip to content

Throttling & coalescing ​

Control how often state-sync refreshes when invalidation events arrive rapidly.

The problem ​

Backend emits 100 events per second. Without throttling, the engine calls getSnapshot() 100 times. Most of those calls fetch the same data.

Throttling options ​

typescript
import { createRevisionSync } from '@statesync/core';

const sync = createRevisionSync({
  topic: 'metrics',
  subscriber,
  provider,
  applier,
  throttling: {
    debounceMs: 200,  // Wait 200ms of silence before refresh
    throttleMs: 1000, // At most 1 refresh per second
    leading: true,    // Fire immediately on first event (default)
    trailing: true,   // Fire after quiet period ends (default)
  },
});

What each option does ​

OptionEffectDefault
debounceMsWaits N ms of silence before triggering refresh—
throttleMsAt most 1 refresh per N ms—
leadingFire immediately on first eventtrue
trailingFire after quiet period endstrue

Visual: 10 events in 500ms ​

Events:  ×  ×  ×  ×  ×  ×  ×  ×  ×  ×
Time:    0  50 100 150 200 250 300 350 400 450   700ms

No throttling:     10 refreshes ❌
debounceMs: 200:    1 refresh at ~650ms (200ms after last event)
throttleMs: 500:    2 refreshes (at 0ms and 500ms)
Both combined:      2 refreshes (leading at 0ms, trailing at ~650ms)

Example: fast-updating dashboard ​

typescript
import type { InvalidationSubscriber, SnapshotProvider, Revision } from '@statesync/core';
import { createRevisionSync, createConsoleLogger } from '@statesync/core';

// Mock: backend sends events every 50ms
let revision = 0;
const handlers = new Set<(e: { topic: string; revision: string }) => void>();

const subscriber: InvalidationSubscriber = {
  async subscribe(handler) {
    handlers.add(handler);
    return () => handlers.delete(handler);
  },
};

let fetchCount = 0;
const provider: SnapshotProvider<{ value: number }> = {
  async getSnapshot() {
    fetchCount++;
    console.log(`  getSnapshot() called (total: ${fetchCount})`);
    return { revision: revision.toString() as Revision, data: { value: revision } };
  },
};

const applier = {
  apply(snapshot: { revision: string; data: { value: number } }) {
    console.log(`  applied: revision=${snapshot.revision}`);
  },
};

// --- Without throttling ---
const syncNoThrottle = createRevisionSync({
  topic: 'demo',
  subscriber, provider, applier,
});

// --- With throttling ---
const syncThrottled = createRevisionSync({
  topic: 'demo',
  subscriber, provider, applier,
  throttling: { debounceMs: 200, throttleMs: 500 },
});

// Simulate 10 rapid events
function simulateBurst() {
  for (let i = 0; i < 10; i++) {
    revision++;
    for (const h of handlers) {
      h({ topic: 'demo', revision: revision.toString() });
    }
  }
}

Expected behavior ​

Without throttling:
  getSnapshot() called (total: 1)   // coalescing still helps:
  getSnapshot() called (total: 2)   // engine queues at most 1 pending refresh
  applied: revision=10

With throttling (debounceMs: 200, throttleMs: 500):
  getSnapshot() called (total: 1)   // leading: immediate
  applied: revision=10              // gets latest revision
  // ...200ms of silence...
  getSnapshot() called (total: 2)   // trailing: fires once after debounce
  applied: revision=10              // same revision, no-op (revision gate)

Even without throttling, the engine's built-in coalescing ensures at most 1 queued refresh. Throttling adds an extra layer — useful when even 2 refreshes per burst is too many.

When to use what ​

ScenarioConfig
Backend emits rarely (< 1/sec)No throttling needed
Moderate updates (1-10/sec)debounceMs: 100
High-frequency updates (10-100/sec)throttleMs: 500
Bursty + long pausesdebounceMs: 200 + throttleMs: 1000
User typing (save on pause)debounceMs: 500, leading: false

Key points ​

  1. Coalescing is always on — even without throttling, the engine queues at most 1 refresh while another is in flight.

  2. Throttling adds delay control — debounceMs waits for silence, throttleMs sets max frequency.

  3. leading + trailing — leading: true gives instant response, trailing: true ensures the final state is always applied.

  4. Revision gate is the safety net — if throttled refresh fetches the same revision, the applier is not called.

See also ​

Released under the MIT License.