Raw engine
@pointyink/rogue is the CRDT engine itself: bind any HTML root — a
contenteditable, a deck, a dashboard — and wire ops to the transport of your
choice. The HugeRTE plugin is a thin wrapper over this same API.
Install
npm i @pointyink/rogue
Bind an element
import { createRogueNode } from '@pointyink/rogue'
const node = await createRogueNode({
element: document.getElementById('editor'), // the editable root
onCommit: (payload) => { /* send payload to your transport */ },
})
The engine observes the root's DOM mutations, diffs them into CRDT operations, and
emits each op via onCommit as a binary multi_op_v0 payload
(Uint8Array). Feed remote ops back in with
node.mergeOp(payload).
Options
| Option | Type | Description |
|---|---|---|
element | HTMLElement | The editable root the engine tracks. |
replicaId | number | Deterministic replica id (defaults to random). |
onCommit | (payload) => void | Each emitted local op payload (Uint8Array). |
paste | false | { maxBytes, onRejected } | false = host owns paste; object = engine paste with an optional size cap. |
doc / win | Document / Window | Host abstraction; defaults work for a plain contenteditable. |
serializeNode | hook | Strip bookkeeping attributes when serializing a node. |
omitNode | hook | Drop a node (and its subtree) from the sync entirely. |
unwrapNode | hook | Unwrap a node, keeping its children. |
Sync client
Pair the engine with the bundled sync client for WebSocket transport, reconnect, an IndexedDB-backed op queue, and replay:
import { createSyncClient } from '@pointyink/rogue/sync-client'
const client = createSyncClient({
wsUrl: 'wss://sync.pointy.ink/ws',
docId: '9f3a...-uuid',
getToken: () => fetch('/api/token').then(r => r.json()).then(t => t.token),
mergeOp: (payload) => node.mergeOp(payload),
onStatus: (s) => console.log(s),
onReady: () => console.log('synced'),
})
Then pipe local ops into the client from the node's onCommit:
const node = await createRogueNode({
element,
onCommit: (payload) => client.send(payload),
})
createSyncClient returns { send, destroy, isReady }.
Bootstrap
On first load you want the doc to render once — not once per history op. Enter bootstrap mode before the client connects, then exit it when the initial sync finishes:
node.beginLoad()
element.setAttribute('contenteditable', 'false')
createSyncClient({
/* ... */
onReady: () => {
node.endLoad()
element.setAttribute('contenteditable', 'true')
},
})
Node API
| Method | Description |
|---|---|
mergeOp(payload) | Merge a remote op (deferred while an IME composition is active). |
beginLoad() | Merge incoming ops without rendering. |
endLoad() | Render the now-complete doc once. |
undo() | Undo one local step (Cmd/Ctrl+Z). |
redo() | Re-apply the last undone step (Cmd/Ctrl+Shift+Z, Ctrl+Y). |
drainParked() | Drain ops that arrived before their parents. |
setSyncDebug(on) | Toggle the divergence detector. |
destroy() | Detach listeners and free the WASM instance. |
Rolling your own transport
You aren't tied to the bundled client. onCommit hands you each op payload
as a Uint8Array, and mergeOp accepts the same — send them
over anything: WebSocket, WebRTC, an HTTP relay, a shared worker.
Scrub hooks
The three host hooks let the engine tell the difference between your content and
editor chrome that shouldn't sync. omitNode drops a node entirely,
unwrapNode unwraps it keeping children, and
serializeNode strips bookkeeping attributes. The editor plugins use
these to keep editor chrome — format-carets, bogus nodes, and
data-mce-* attributes — out of the CRDT. Implement them for your own
host's chrome.