JavaScript SDK
The JavaScript SDK gives you a practical API for room connection, synchronized state updates, replay-aware reads, and collaboration signals. Use this page as your implementation baseline for browser or JS runtime clients.Install
Minimal setup
Read and write state
sdk.sync.push() after local mutations to flush updates. push() returns a
boolean — true if a pack was actually sent, false if there was nothing pending.
It sends only the delta accumulated since the last successful push (tracked
internally), not a full state export.
sdk.sync.get() returns the speculative/intent-lane view (including your own
unconfirmed writes); sdk.sync.getCanonical() returns the authoritative
canonical-lane view — the same intent-vs-canonical distinction described for text
reads below.
Push acknowledgement and retry. The server responds to each push() with a
pack-ack message reporting accepted/rejected counts. The SDK handles this
automatically: if any nodes are rejected, it reverts the corresponding local
optimistic state and resends. You don’t need to handle pack-ack directly unless
you’re building custom sync-status UI — see protocol/websocket-messages and
api-reference/websocket-commands#pack-ack-server-client.
Pack size limit. A single pushed pack is capped at 60 KiB of base64 (61,440
characters). Pushes exceeding this are rejected client-side — the SDK reverts the
pending marks and emits an "error" event rather than sending an oversized frame.
Batch large sets of writes into multiple smaller push() calls if you hit this.
Text operations
Use positional helpers for collaborative text edits:Text range anchors
When your editor needs stable positions under concurrent edits, use anchor-based helpers. Anchors identify where to insert or delete relative to the document structure rather than a raw integer offset that may shift under concurrent changes. Insert anchors (TextInsertAnchor):
| Shape | Meaning |
|---|---|
{ kind: "offset", pos: number } | Insert at character offset pos |
{ kind: "start" } | Insert at the very beginning |
{ kind: "end" } | Append to the end |
{ kind: "after", lamport, author } | Insert after a specific RGA node |
TextDeleteAnchor):
| Shape | Meaning |
|---|---|
{ kind: "offset", pos: number } | Delete starting at character offset pos |
{ kind: "start" } | Delete from the beginning |
{ kind: "after", lamport, author } | Delete starting after a specific RGA node |
Text reads and attribution
For attribution UIs (per-glyph author), read the visible RGA sequence:getTextAtLamport / getTextSequenceAtLamport replay only DAG nodes with transaction.lamport <= maxLamport (tombstones included). The speculative read (getText) returns the intent-lane view; getTextCanonical returns the canonical lane view.
Try the interactive surface: Collab text playground (http://127.0.0.1:4177, default room collab-text).
Presence and ephemeral collaboration
Presence is for live ephemeral state, not durable business data:Blob and file storage patterns
For file-like payloads, use the SDK CAS APIs and keep sync state as references:Runtime events
Subscribe to runtime messages for observability and integration hooks:Offline and persistence
SDK can queue and flush writes across reconnect boundaries. For peer-local durability, enable persistence explicitly:Intent vs canonical pattern
Model local optimism and authoritative outcomes in separate namespaces:intent/**for speculative writesworld/**for canonical accepted state
Replay range read
Retrieve a paginated history of key mutations from the server, useful for activity feeds, audit trails, and undo stacks:sdk.sync.readLocalReplayRange which returns synchronously from the WASM store.
Query and projection helpers
Runtime query APIs support canonical-lane analytics and read-model workflows. Usesdk.query when you need structured projections over room state rather than direct key access.
Register a query spec (once per descriptor version):
Transport options
transport.mode commonly uses:
"auto": allow negotiated transport enhancements when available"ws-only": force WebSocket-only behavior
auto, switch to ws-only only when environment constraints require strict transport control.
Error and rejection handling
Treat rejection/error signals as first-class UX and ops inputs:- Surface user-meaningful failure states
- Log structured rejection context
- Avoid silent rollback paths
Configuration reference
All options passed tocreateNodalMergeSdk:
| Option | Type | Default | Description |
|---|---|---|---|
wsUrl | string | — | WebSocket endpoint URL (required) |
roomId | string | — | Room identifier (required) |
authorKey | Uint8Array | — | Ed25519 signing key bytes for this peer |
token | unknown | — | Auth token forwarded in hello payload |
wasmModule | RequestInfo | URL | ... | — | Custom WASM module source; defaults to bundled |
tickIntervalMs | number | server default | How often local tick loop fires (ms) |
maxOpsPerTick | number | server default | Max ops batched per tick flush |
reconnect.enabled | boolean | false | Enable automatic reconnect |
reconnect.initialDelayMs | number | 500 | First reconnect delay (ms) |
reconnect.maxDelayMs | number | 8000 | Reconnect backoff ceiling (ms) |
reconnect.factor | number | 2 | Backoff multiplier |
reconnect.maxAttempts | number | unlimited | Stop reconnecting after N failures |
offline.persistenceKey | string | — | localStorage key for outbox persistence |
transport.mode | "auto" | "ws-only" | "ws-only" | Transport negotiation policy |
persistence.enabled | boolean | false | Enable peer-local graph persistence |
persistence.adapter | "indexeddb" | PeerLocalPersistenceAdapter | "indexeddb" | Persistence backend |
persistence.dbName | string | "nodalmerge-peer-local" | IndexedDB database name |
persistence.dbVersion | number | 1 | IndexedDB schema version |
persistence.debounceMs | number | 500 | Debounce interval for auto-save writes (ms) |
persistence.migrateLegacyDemo | boolean | false | Migrate pre-Phase-C demo key formats |
offline.persistenceKey controls outbox persistence (localStorage) and is independent of persistence.* which controls graph/node durability (IndexedDB).
Production checklist
- Durable server storage enabled
- Reconnect policy configured and tested
- Local persistence strategy validated
- Intent/canonical namespaces separated
- Presence restricted to ephemeral signals
- Runtime event/rejection telemetry wired
Common mistakes
- Forgetting
sdk.sync.push()after local writes - Using presence for durable domain state
- Storing file/blob bytes directly in sync string keys instead of hash references
- Mixing optimistic and canonical writes in one keyspace
- Assuming online-only behavior during QA
- Ignoring rejection pathways until late integration