ProvenPin Install

Yjs CRDT document sync with state vectors, relay and undo: working versions (tested 2026-10-15)

Sync two Yjs docs by exchanging state-vector diffs, converge concurrent text and map edits, relay live updates like a provider, undo, and merge updates out of order.

passed5 passed · 0 failed2026-10-15 20:11 UTC
Status
passed
Last passed
2026-10-15 20:11 UTC
Tests
5 / 5
Language
TypeScript
Runtime
node v22.23.3
pinned versions3
yjs@13.6.33@types/node@26.6.5tsx@4.23.15
run it
npm install --no-audit --no-fund --ignore-scripts=false
npm test
test environment
node v22.23.3
linux 6.17.0-1022-azure
x64
runner: github-actions 38799498972

The code

Exactly the files that passed. Your agent can pull them with get_recipe("yjs-crdt-doc-sync") (install), or see them on GitHub.

package.json
{
  "name": "yjs-crdt-doc-sync",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "node --import tsx --test test/*.test.ts"
  },
  "dependencies": {
    "yjs": "13.6.33"
  },
  "devDependencies": {
    "@types/node": "26.6.5",
    "tsx": "4.23.15"
  }
}
src/sync.ts
import * as Y from "yjs";

/** Exchange only the missing updates between two docs using state vectors (the y-protocols sync step 1/2). */
export function syncDocs(a: Y.Doc, b: Y.Doc) {
  const aMissing = Y.encodeStateAsUpdate(b, Y.encodeStateVector(a));
  const bMissing = Y.encodeStateAsUpdate(a, Y.encodeStateVector(b));
  Y.applyUpdate(a, aMissing, "remote");
  Y.applyUpdate(b, bMissing, "remote");
  return { aBytes: aMissing.byteLength, bBytes: bMissing.byteLength };
}

export function newDoc(clientID: number) {
  const d = new Y.Doc();
  d.clientID = clientID; // fixed ids make conflict resolution deterministic in tests
  return d;
}

/** Wire a doc pair so each local update is relayed to the other, like a websocket provider would. */
export function connect(a: Y.Doc, b: Y.Doc) {
  const relay = (to: Y.Doc) => (update: Uint8Array, origin: unknown) => {
    if (origin !== "remote") Y.applyUpdate(to, update, "remote");
  };
  const ha = relay(b), hb = relay(a);
  a.on("update", ha);
  b.on("update", hb);
  return () => { a.off("update", ha); b.off("update", hb); };
}
test/sync.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import * as Y from "yjs";
import { syncDocs, newDoc, connect } from "../src/sync.ts";

test("concurrent text edits merge identically on both peers", () => {
  const a = newDoc(1), b = newDoc(2);
  a.getText("t").insert(0, "hello");
  syncDocs(a, b);
  a.getText("t").insert(5, " world");
  b.getText("t").insert(0, ">> ");
  syncDocs(a, b);
  assert.equal(a.getText("t").toString(), ">> hello world");
  assert.equal(b.getText("t").toString(), a.getText("t").toString());
});

test("concurrent map writes converge to the same winner", () => {
  const a = newDoc(1), b = newDoc(2);
  a.getMap("m").set("status", "draft-a");
  b.getMap("m").set("status", "draft-b");
  syncDocs(a, b);
  const va = a.getMap("m").get("status"), vb = b.getMap("m").get("status");
  assert.equal(va, vb);
  assert.ok(va === "draft-a" || va === "draft-b");
});

test("state-vector sync sends only the diff", () => {
  const a = newDoc(1), b = newDoc(2);
  a.getArray<number>("arr").push(Array.from({ length: 500 }, (_, i) => i));
  syncDocs(a, b);
  a.getArray<number>("arr").push([999]);
  const { bBytes, aBytes } = syncDocs(a, b);
  assert.ok(bBytes < 50, `diff should be tiny, got ${bBytes}`);
  assert.ok(aBytes <= 2);
  assert.equal(b.getArray<number>("arr").length, 501);
});

test("live relay plus undo manager", () => {
  const a = newDoc(1), b = newDoc(2);
  const stop = connect(a, b);
  const undo = new Y.UndoManager(a.getText("t"));
  a.getText("t").insert(0, "abc");
  assert.equal(b.getText("t").toString(), "abc");
  undo.undo();
  assert.equal(b.getText("t").toString(), "");
  stop();
  a.getText("t").insert(0, "x");
  assert.equal(b.getText("t").toString(), "");
});

test("updates can be merged and replayed in any order", () => {
  const a = newDoc(1);
  const updates: Uint8Array[] = [];
  a.on("update", (u: Uint8Array) => updates.push(u));
  a.getText("t").insert(0, "one ");
  a.getText("t").insert(4, "two");
  const c = newDoc(3);
  Y.applyUpdate(c, Y.mergeUpdates(updates.reverse()));
  assert.equal(c.getText("t").toString(), "one two");
});

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.