■ pointy/docs

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

OptionTypeDescription
elementHTMLElementThe editable root the engine tracks.
replicaIdnumberDeterministic replica id (defaults to random).
onCommit(payload) => voidEach emitted local op payload (Uint8Array).
pastefalse | { maxBytes, onRejected }false = host owns paste; object = engine paste with an optional size cap.
doc / winDocument / WindowHost abstraction; defaults work for a plain contenteditable.
serializeNodehookStrip bookkeeping attributes when serializing a node.
omitNodehookDrop a node (and its subtree) from the sync entirely.
unwrapNodehookUnwrap 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

MethodDescription
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.