The property that matters
A collaboration demo with two browser windows proves almost nothing about convergence. Real delivery is delayed, duplicated, interleaved, and interrupted by reconnects. The useful property is narrower and testable: after every accepted update has been delivered, each logical replica should encode the same canonical graph.
System Synthesis stores nodes and edges in a Yjs document. Each entity is a nested map rather than one opaque JSON value. That distinction matters because a rename and a position change should merge independently instead of replacing the entire node.
Make the network adversarial but deterministic
The harness uses a seeded pseudo-random generator. Every operation, selected client, delay, duplication, and delivery order can therefore be replayed from a failing seed. Randomness expands coverage; the seed turns a failure back into a normal test case.
- Create 10 client documents plus two logical server documents.
- Generate 1,000 graph operations including add, remove, rename, move, reconnect, and user-local undo.
- Encode every Yjs update and place it in a simulated delivery queue.
- Inject duplicate messages and deliver portions of the queue out of order.
- Simulate a server restart by rebuilding from durable state and replaying the ordered tail.
- Drain the queue, canonicalize every graph, and compare hashes.
for (const operation of seededOperations(1000)) {
const update = applyOperation(clients[operation.client], operation)
network.enqueue(update, {
duplicate: random.boolean(0.12),
delay: random.integer(0, 20),
})
if (operation.checkpoint) network.deliverRandomBatch()
}
network.drain()
expect(canonicalHashes(allReplicas)).toHaveSize(1)Canonical comparison
Raw Yjs encodings are not a useful equality check. Internal item ordering can differ while the user-visible graph is equivalent. The harness projects each document into a plain graph representation, sorts nodes and edges by stable identifiers, normalizes optional fields, and hashes the resulting serialization.
This comparison also prevents a weak test from passing because every replica lost the same data. Alongside equal hashes, the harness checks graph invariants and the expected effects of acknowledged operations.
Undo must be origin-aware
A shared editor should not let one user's undo reverse another user's work. Y.UndoManager therefore tracks only transactions created with the local user's origin. The convergence harness mixes local undo with remote delivery and verifies that remote changes remain present.
This is a good example of the difference between structural convergence and product behaviour. Yjs can make replicas agree, but the application still has to define whose operations an undo command is allowed to reverse.
What the test does not prove
A successful simulation is not a formal proof of every possible interleaving, and it does not measure live Socket.IO capacity. It gives a reproducible, high-variance check of the collaboration model implemented in the repository.
- It does not preserve both users' semantic intent when they edit the same scalar field.
- It does not make the product offline-first; editing pauses while disconnected.
- It does not replace authenticated API and WebSocket integration tests.
- It does not measure latency or production throughput.