Resumable realtime delivery

The @honua/sdk-js/realtime subpath includes an opt-in, transport-neutral delivery gate for snapshot-plus-delta streams. It sits between a transport and the existing realtime reducer/store. The gate does not open or reconnect a network transport; it decides whether an event is safe to apply and whether a durable cursor can resume the exact accepted subscription.

import {
  createRealtimeFeatureStore,
  createResumableRealtimeSubscription,
} from "@honua/sdk-js/realtime";

const featureStore = createRealtimeFeatureStore();
const delivery = await createResumableRealtimeSubscription({
  context: {
    kind: "honua.realtime-resume-context",
    version: 1,
    sourceId: "incidents",
    queryFingerprint: acceptedPlan.fingerprint,
    sourceVersion: "incident-snapshot-v7",
    schemaVersion: "incident-schema-v3",
    authorizationScopeFingerprint: aclFingerprint,
  },
  checkpointStore: durableCheckpointStore,
  apply: (event, signal) => {
    if (signal.aborted) return;
    featureStore.apply(event);
  },
});

transportObserver.next = (event) => {
  if (event.type === "snapshot" || event.type === "delta" || event.type === "upsert" || event.type === "delete") {
    void delivery.enqueue(event);
  }
};

Safety model

A honua.realtime-checkpoint@1 binds all resume positions to:

Changing any bound identity produces resnapshot-required; the SDK never silently reuses the cursor. A new subscription without a compatible checkpoint also requires a replacement snapshot before deltas can apply.

Adapters project a server-expired cursor, an unsupported resume mode, or a transport-detected gap through delivery.requireResnapshot(...). That method invalidates queued work and accepts only a replacement snapshot next; it does not silently restart from the newest delta.

After a baseline exists, only the next contiguous safe-integer sequence can advance it. Older sequences are reported as duplicates. Missing sequences, conflicting top-level/nested checkpoint fields, or reuse of a recent event id at a new sequence stop delivery and require a replacement snapshot. A replacement snapshot received during ordinary live delivery must advance the existing sequence; a stale or equal snapshot cannot regress the baseline. A replacement snapshot may establish a lower sequence only after an explicit requireResnapshot(...) transition, which marks a deliberate new recovery epoch. Accepted replacement snapshots reset the bounded event-id window.

enqueue treats transport input as untrusted at runtime. Only snapshot, upsert, delete, and delta discriminators reach the consumer. The SDK captures event identity and resume metadata synchronously, projects only cursor, watermark, timestamp, sequence, and delta-token fields into the versioned checkpoint envelope, and drops unrelated fields before application or persistence. Caller mutation after enqueue therefore cannot change durable deduplication identity, and credentials or adapter metadata cannot hitchhike inside a saved checkpoint.

The persisted recent-event-id history defaults to 256 entries and has an absolute 4,096-entry safety ceiling. Oversized loaded histories are rejected before their elements are scanned; accepted histories are copied directly from their configured bounded tail.

This first gate requires a trustworthy monotonic sequence on every snapshot or delta. Cursor-only and delta-token-only protocols are not silently assigned a client sequence: their future adapters must obtain an ordering guarantee from the server or report resume as unsupported and resnapshot.

Backpressure and cancellation

maxPendingEvents bounds the applying event plus queued data events (default 64). Overflow aborts the active delivery, resolves queued work as resnapshot-required, and refuses more deltas. One replacement snapshot may wait behind an abort-ignoring consumer because it is the only recovery path; ordinary data remains bounded. Consumers should honor the supplied AbortSignal and make application idempotent, because JavaScript cannot undo a side effect already performed by a callback that ignores cancellation.

Closing or aborting the gate prevents any later result from advancing its checkpoint. Consumer errors leave the prior checkpoint unchanged. Checkpoint save errors are explicit: the in-memory accepted position remains visible with checkpointPersisted: false, phase becomes error, and no further events are accepted by that gate.

Without a checkpointStore, accepted checkpoints remain available in memory but checkpointPersisted stays false. Callers may persist them as part of their own atomic application transaction; the SDK does not claim durability it did not observe.

Checkpoint persistence occurs after successful consumer application. This is an at-least-once boundary, not a transaction spanning an arbitrary application store and checkpoint database. Applications that require atomic exactly-once effects must persist their materialized state and checkpoint transactionally, or use event ids/versions to make replay idempotent.

Scope and remaining work

This is the first production slice of issue #393, not completion of the full workstream. It does not claim:

Adapters should declare unsupported resume behavior before subscription when the protocol can determine it. Expired server cursors must be projected as an explicit replacement-snapshot transition rather than fixture fallback or an unverified continuation.