Skip to content

Lifecycle Contract

createRevisionSync(options) returns a RevisionSyncHandle -- a lightweight controller for the sync loop.

RevisionSyncOptions

OptionTypeRequiredDescription
topicstringYesNon-empty topic identifier
subscriberInvalidationSubscriberYesDelivers invalidation events
providerSnapshotProvider<T>YesReturns the latest snapshot
applierSnapshotApplier<T>YesWrites state into your store
shouldRefresh(event) => booleanNoFilter which events trigger a refresh
loggerLoggerNoDebug/warn/error logging
onError(ctx: SyncErrorContext) => voidNoError callback for all phases
throttlingInvalidationThrottlingOptionsNoDebounce/throttle refresh rate

Throttling options

OptionTypeDefaultDescription
debounceMsnumber-Wait for N ms of silence before refreshing
throttleMsnumber-At most 1 refresh per N ms
leadingbooleantrueFire on the leading edge (throttle only)
trailingbooleantrueFire on the trailing edge (throttle only)

RevisionSyncHandle

start()

  • Subscribes to invalidation events, then performs an initial refresh.
  • Idempotent: repeated calls are a no-op.
  • Throws if called after stop().
  • On failure (subscribe or initial refresh), rolls back state: unsubscribes, resets started = false.

stop()

  • Unsubscribes from events and blocks further applies.
  • Idempotent: repeated calls are a no-op.
  • After stop(), the handle is dead -- start() will throw.

refresh()

  • One-shot: fetch snapshot from provider and apply if newer.
  • Allowed before start() -- useful for eager prefetch without subscription.
  • No-op after stop() -- does not throw, silently skips.
  • Supports coalescing: at most 1 refresh is queued while one is in-flight.

getLocalRevision()

  • Returns the last successfully applied revision.
  • "0" until the first successful apply.

Error Phases

Every error passed to onError includes a phase field that indicates where it happened:

PhaseWhat happenedWas the applier called?
startFailed during handle.start() setupNo
subscribeFailed to subscribe to invalidation eventsNo
invalidationError processing an invalidation eventNo
getSnapshotProvider failed to return a snapshotNo
protocolRevision validation failed (non-canonical, empty topic)No
applyApplier threw while applying a snapshotYes (apply failed)
refreshUnclassified error inside the refresh loopDepends
throttleError in the throttle/coalescing layerNo

Observability fields (best-effort)

SyncErrorContext may additionally include fields useful for triage/metrics:

FieldTypeDescription
topic?TopicTopic identifier
localRevision?RevisionLocal revision at the time of the error
eventRevision?RevisionRevision from the invalidation event
snapshotRevision?RevisionRevision from the snapshot
sourceId?stringChange originator (if transport provides it)
sourceEvent?unknownRaw event payload (transport-specific)
attempt?numberCurrent retry attempt number
willRetry?booleanWhether the engine will retry after this error
nextDelayMs?numberDelay before the next retry attempt

These fields are best-effort: the engine fills them when the information is available in the current phase.

Behavior on apply error:

  • During start() — the start() promise rejects; the subscription is rolled back (unsubscribe, started = false).
  • During invalidation-triggered refresh — onError is emitted; the subscription continues (the next invalidation can trigger refresh again).
  • During manual refresh() — the error propagates to the caller.

Classification order within refresh() errors:

  1. getSnapshot — provider failed to return data
  2. protocol — revision validation failed
  3. apply — applier failed to apply the snapshot
  4. refresh — fallback for unexpected errors

Each phase is emitted at most once per error (deduplicated via the alreadyEmitted flag).

onError callback

  • Called for errors in all phases: subscribe, refresh, protocol, etc.
  • If onError throws — the engine catches and logs it, and continues running.
  • The engine never crashes due to a user-provided onError callback.

Call order

createRevisionSync(options)  →  handle (inactive, localRevision = "0")

handle.refresh()             →  optional: one-shot fetch + apply

handle.start()               →  subscribe → initial refresh

[invalidation events]        →  automatic refresh cycle (with coalescing + throttle)

handle.stop()                →  unsubscribe, dispose throttle, block further apply

See also

Released under the MIT License.