CRDT for documents - #1657
Closed
gearnode wants to merge 196 commits into
Closed
Conversation
Own the Go API and WASI adapter while using the official Rust engine as the initial correctness oracle. This avoids CGO and keeps backend replacement under our control. Add convergence and JavaScript interoperability checks so a future native Go engine can be evaluated independently before adoption. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Provide peer-specific sync state and message exchange through the owned Go API. Persistable sync state allows WebSocket sessions to reconnect without coupling callers to the Rust reference backend. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Store version-scoped Automerge snapshots in PostgreSQL and expose an authenticated WebSocket sync endpoint. Row locking and CRDT merging keep concurrent writers convergent across server instances. Add stable text cursors and a service operation so Go agents can edit the same live document without replacing the frontend state. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Seed supported ProseMirror documents into Automerge rich text and bind Tiptap transactions to the live CRDT over the authenticated WebSocket. Remote changes now appear in open editors while the existing JSON save path remains the publication and PDF projection. Fall back safely for schema nodes that cannot round-trip yet, and disable the collaborative editor after a connection loss to prevent divergence. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Render Automerge rich-text spans to validated ProseMirror JSON inside the same transaction that persists CRDT state. Agent-only edits now update publishing, PDF, MCP, and reload paths without requiring an open browser. Expose revision-guarded read and edit tools so Go agents retry safely when a customer changes the document concurrently. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Keep unsynchronized local CRDT changes across WebSocket failures and retry with bounded exponential backoff. Editing pauses while disconnected and resumes after the peer catches up with PostgreSQL state. Verify the embedded engine checksum, disable unsupported collaborative undo, and fuzz malformed Automerge documents to harden the boundary. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Persist short-lived selection presence in PostgreSQL so remote cursors work across server instances and disappear after disconnects or expiry. The editor relays throttled heartbeats and renders deterministic collaborator selections without exposing names. Also map hard breaks and horizontal rules through Automerge rich text and the server-side ProseMirror projection. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Check advisories, licenses, duplicate crates, wildcard requirements, and dependency sources with the pinned Rust toolchain. The policy allows only licenses present in the locked WASM graph and documents the single unavoidable syn version split. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Start the clean-room Go engine with bounded LEB128, RLE, column, change, compressed-change, and document decoders. Validate chunk checksums and decompression limits before exposing any operation state. Cross-check JavaScript-generated change metadata and official WASM document snapshots while keeping the reference backend as production default. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Decode v0.10 batch operations, booleans, deltas, scalars, object IDs, sequence keys, and predecessor groups into a bounded Go operation graph. Apply causal changes, resolve deterministic concurrent insert ordering, track heads, and match JavaScript materialization for concurrent text histories applied in either order. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Parse and reproduce v1 and v2 sync wire messages with bounded heads, needs, bloom filters, changes, and capability flags. Cross-check byte-for-byte output against JavaScript-generated messages while the native peer state machine remains gated. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Add a clean-room native parser for Automerge 0.10 document, change, and compressed-change chunks. Validate checksums, canonical encodings, column structure, operation identifiers, and dependency graph invariants. Keep the implementation internal and leave the reference WASM backend as the production default while native materialization remains incomplete. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Replace the first container prototype with the independently tested decoder that reconstructs snapshot changes and operations, preserves unknown data, validates ownership and causal frontiers, and fuzzes malformed histories. Retain the native materializer and sync wire codec on the stronger typed representation. Production still uses the WASM oracle until mutation, encoding, and peer-state parity are complete. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Serialize typed Go operations into official change chunks with deterministic actor tables, scalar metadata, predecessor groups, checksums, and implicit operation IDs. Load the native output in official Automerge JavaScript and verify materialized content, metadata, and hashes before enabling mutation APIs. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Implement native map and text mutations, UTF-16 splices, canonical cursors, commits, saves, snapshot extension, deterministic merges, heads, persisted sync state, and bidirectional synchronization with the reference backend. Keep WASM as the default while rich-text marks, full cursor deletion semantics, compaction, and exhaustive sync transcripts remain under differential validation. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Complete native snapshot loading, rich-text span hydration, canonical cursor behavior, causal partial-change merging, actor reuse, and bidirectional reference sync. Keep explicit reference constructors as the independent oracle. Randomized text histories, repeated mixed-peer transcripts, overlapping rich-text marks, official JavaScript change loading, race tests, and 319k malformed-input fuzz executions now pass with native New and Load. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Apply the repository whitespace policy across the collaboration code and correct staticcheck and errcheck findings for error text, string iteration, and DEFLATE reader cleanup. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Track native sync messages in flight and stop generating until the remote peer responds. This lets the server drain one outbound message and quiesce instead of closing and reconnecting in a loop. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Order every change in a sync payload by its causal dependencies before applying it. ProseMirror can emit a reconciliation child before its parent when Enter splits a block, which previously closed the WebSocket and triggered reconnect loops. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Store imported change chunks byte-for-byte instead of re-encoding them under new hashes. Queue causally incomplete changes across messages, request their missing dependencies, and apply the queue once each parent arrives. This fixes the follow-up edit loop after a successful block split and preserves unknown columns and compressed changes during persistence. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Show the block and slash menus during collaboration while filtering only the unsupported table action. Headings, lists, code blocks, blockquotes, Mermaid, and dividers are available again without enabling lossy table serialization. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Encode table and row containers as explicit Automerge block markers so row boundaries survive sync. Cells retain independent text and colspan, rowspan, and colwidth attributes while TableKit menus and commands remain enabled. Add seed migration, native Go hydration, server-side ProseMirror projection, cell-edit and row-insert plugin tests, and concurrent row convergence coverage. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Keep Tiptap's local history extension enabled with Automerge. Local table and text edits are encoded as inverse CRDT changes on undo, while remote sync transactions remain excluded from the local history stack. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Mark horizontal rules as explicit block markers instead of inline embeds. This closes the active paragraph before insertion, preventing the Automerge ProseMirror adapter from producing a null content match. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Round-trip the code-block language attribute through Automerge and ProseMirror. Mermaid blocks now retain language=mermaid after local edits, remote sync, reload, and server-side projection instead of degrading to ordinary code blocks. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Compute the causal change set missing from each peer's acknowledged heads and send exact raw change chunks. Full snapshots are now reserved for unknown baselines, while explicit need requests return only requested changes. Add a regression proving steady-state frames contain one new change and remain smaller than the accumulated document. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Share one Automerge document per active version within a server process and wake every peer immediately after applying an edit. Same-instance collaboration no longer waits for the 500 ms PostgreSQL reconciliation tick. Keep row-locked persistence and periodic refresh as durability and cross-instance fallbacks, with reference-counted room teardown after the last peer leaves. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Record that carets and selections travel as a TextSelectionValue of stable Automerge cursors rather than integer offsets, and why: offsets drift under concurrent edits while cursors stay anchored. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Browser tabs and Go agents must reference a version's document by the same automerge-repo id, or their sync and, especially, their ephemeral gossip do not line up: a peer drops an ephemeral frame whose document id it does not recognise. Add a dependency-free base58check codec and the repo document-id helpers: EncodeDocumentID/DecodeDocumentID round-trip the 16-byte id, ValidateID checks it, and DeriveDocumentID hashes an arbitrary seed (a version GID) to a stable id so every peer computes the same one without coordination. AutomergeURL/ParseAutomergeURL wrap and unwrap the automerge: scheme. The codec is validated against a genuine @automerge/automerge-repo document id, which exercises the checksum and alphabet against real upstream output rather than against itself. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Record the decisions the gateway and frontend must share to migrate document editing to the automerge-repo protocol: version-scoped routing, deterministic document-id derivation, unchanged cookie/bearer auth, and server-authoritative seeding, plus how presence, reconnect, and rollout work. This is the artifact that unblocks the endpoint: it settles the three contracts that could not be guessed as production code and points each at the primitives already built and tested in this package. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
The browser must compute the same automerge-repo document id for a version as a Go agent, or their sync and ephemeral presence do not line up. Port the base58check codec and derivation from pkg/automerge/collaboration/documentid.go to TypeScript in @probo/ui and export it for the console. The vitest suite decodes a genuine automerge-repo id and, crucially, asserts deriveDocumentId matches the Go DeriveDocumentID byte-for-byte for a set of seeds, so the two implementations cannot drift. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Update the gateway contract status now that the TypeScript deriveDocumentId mirror ships and is verified byte-identical to the Go implementation. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Mount a /repo route beside the custom /sync route so the two collaboration protocols coexist during migration. It reuses the existing auth and the hub's room, persistence, and cross-instance refresh, and drives the wire protocol with the tested adopting ServerConn: the client picks the document id, the server answers for it. Presence and cursors ride repo ephemeral gossip, fanned out to a room's other peers with the hub's ephemeral primitives. Because the repo protocol has no seed handshake, the route refuses an unseeded document rather than serve an empty one as authoritative; server-side seeding is a separate contract item. The loop is a standalone function so it can be driven end-to-end in Go by a real ClientConn with no database: one test converges a client on the seeded document, another gossips an ephemeral between two clients through the hub. Both pass under the race detector. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Update the gateway contract status: the /repo route is wired and driven end-to-end in Go, with Postgres integration, live JS interop, and server-side seeding still outstanding. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Seeding a collaboration document server-side needs the forward direction of the ProseMirror bridge, which existed only in TypeScript. Add ToSpans, the inverse of Render: it walks a ProseMirror document into the rich-text spans Text.UpdateSpans writes, mirroring how @automerge/prosemirror flattens the tree. Every block node emits a marker carrying its full ancestor path of Automerge block types; list wrappers are transparent and only their items become blocks; marks map through the shared schema ledger. A container's first paragraph folds into the container's own content the same way the renderer reconstructs it: a list item always folds its first paragraph because the renderer always re-synthesizes one, while a blockquote or table cell only folds a non-empty first paragraph (or a sole child), so an empty first paragraph with siblings survives. The gate is a round trip over the shared 283-document corpus: converting the canonical ProseMirror JSON to spans, writing them into a fresh document, and rendering it back reproduces the input exactly, which is the property server-side seeding relies on. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
The repo protocol has no seed handshake, so the /repo route previously refused an unseeded document. Seed it server-side instead: the connection that claims the seed converts the version's stored ProseMirror content to spans with prosemirror.ToSpans, writes them into the shared document, and persists. Persisting marks the state seeded, so later connections skip seeding and just sync. An in-Go test drives a real ClientConn against the loop for an unseeded version and confirms the client materializes the seeded content, covering the whole server-authoritative seeding path without a database. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Update the gateway contract: ProseMirror to spans conversion and server-authoritative seeding now ship, leaving the Postgres seed-lifecycle integration test as the remaining seeding work. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Presence and cursors are relayed between server instances over the collaboration NOTIFY channel, which until now carried only a bare document-version id meaning "this document changed". Add a typed envelope that coexists with that signal: a bare id is never valid JSON, so the two payload kinds never collide. The envelope carries the version id, the publishing instance id (so a server can ignore its own echo), and the opaque frame. Encoding rejects a payload over a NOTIFY-safe size so an oversized frame falls back to local-only fan-out rather than failing the notify. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Add NotifyCollaborationEphemeral: it encodes an opaque repo gossip frame into the cross-instance envelope and fires it over the collaboration NOTIFY channel, stamped with the publishing instance id. It is fire-and-forget, touches no document, and runs outside any transaction; an oversized frame returns an error so the caller keeps local fan-out. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Give the hub a per-instance id and teach notifyExternal to tell the two NOTIFY payload kinds apart: a bare version id still wakes peers to refresh, while an ephemeral envelope is fanned out to the room's local peers. A server ignores its own echo, since it already delivered the frame directly when it published it. The /repo loop now relays each gossiped frame to other instances through a publisher hook wired to NotifyCollaborationEphemeral, in addition to the existing local BroadcastEphemeral. A relay failure or oversized frame is logged and skipped rather than dropping the connection. Tests cover the hub delivering an external frame while suppressing its own echo, and the loop handing each gossiped frame to the publisher. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Update the gateway contract and protocol notes: repo presence and cursors now propagate between server instances over the NOTIFY channel, leaving only the Postgres delivery path to integration-test. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Repo presence carries a collaborator's caret as stable Automerge cursors rather than integer offsets, so a remote caret stays on the right character while other people type. Add the browser helper that builds a selection from the editor's caret offsets and resolves a selection back to offsets against the current document, plus a collapsed-caret check. The shape (field, anchor, head) mirrors the Go TextSelectionValue so both describe the same thing; the cursor values are the JavaScript Automerge cursor type exchanged between browser peers. A vitest suite proves a caret stays anchored across a concurrent insertion where a stored offset would drift, and that a range selection round-trips. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Record that the browser cursor-based selection helper ships and is tested for cursor stability, leaving the repo NetworkAdapter and live validation as the remaining frontend work. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
The presence decorations work in ProseMirror position space, but stable cursors live in Automerge text-offset space. Add the bridge in @probo/ui: presenceFromPmSelection turns a caret or selection into stable cursors to publish, and pmSelectionFromPresence resolves a remote selection back to ProseMirror positions against the current document. The position mapping reuses @automerge/prosemirror's own conversion helpers so it stays consistent with the editor binding. A vitest suite builds the real schema adapter and a document and checks a caret round-trips, a remote caret stays anchored across a concurrent insertion, and a selection range preserves its endpoints. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Replace the custom collaboration transport with the official automerge-repo client. connectRepoDocument opens a Repo over a WebSocketClientAdapter pointed at the version's /repo endpoint and finds the server-seeded document by its derived automerge: URL, so the client no longer seeds or runs a hand-rolled sync loop. Presence rides repo ephemeral messages carrying stable cursors: the handle maps the editor's caret to Automerge cursors on the way out and resolves remote peers' cursors back to positions on the way in, accumulating the latest per peer and pruning ones that go quiet. The editor and its presence decorations are unchanged; only the transport behind the handle differs. Delete the custom AutomergeDocumentHandle and add the automerge-repo dependencies. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
The editor now speaks the automerge-repo protocol exclusively, so drop the custom collaboration stack: the /sync route and its handler, the DB-backed collaborator-presence storage (service and coredata), and the hub's structured presence fan-out. The handler file keeps only the pieces the repo handler shares: the handler type, connection authorization, and the WebSocket read/write helpers. Presence and cursors now travel solely as repo ephemeral gossip, so the room's presence map, UpdatePresence, and the wake channel's presence fields are gone; the hub retains sync fan-out, persistence, and the opaque ephemeral relay. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
Update the gateway contract: the editor now uses the automerge-repo client over /repo with stable-cursor presence, and the legacy /sync route, custom handle, and DB presence are removed. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
This was referenced Aug 21, 2026
Closed
Contributor
Author
|
Closing this in favor of a review stack — same change, split into 6 sequential PRs for easier review:
Each PR is based on the previous stage's branch; merge in order top to bottom. |
This was referenced Aug 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
save(), replacing the change-stream saveInteroperability parity
AUTOMERGE_REQUIRE_FULL_INTEROP=1passes: 312 interop-required upstream tests covered, 0 pending.Capabilities added to close the final gaps:
isolatepins reads and writes to a historical frontier and branches using derived concurrency actors (matching Rust'swith_concurrencyscheme); merges stay hidden untilintegrate, and repeated isolate/integrate cycles reproduce the reference exactly.Savewrites a single document chunk (frontier changes plus indexed ancestry, deletes folded into successor lists, columns DEFLATE-compressed above the reference threshold), byte-identical to the reference for the same history;Save(ctx, NoCompress())andSave(ctx, DiscardOrphans())cover the option combinations, and the save falls back to the faithful change stream when a history cannot be compacted.Two behaviours are documented rather than silently asserted:
observe_counter_change_applicationis covered as native-matches-reference because the pinned reference collapses the applied counter change into a single put, and the dangling-mark error path (marking past the end of the text) is now differentially gated across randomized scenarios. SeePARITY_PLAN.md.Legacy V1 sync, JavaScript binding ergonomics, and Rust-internal rustdoc examples are intentionally out of scope and classified as api-convenience or language-specific (44 api-convenience covered, 233 pending, 123 language-specific).
Robustness fixes
Two production-reported defects were root-caused and fixed with regression gates:
ChangesSinceaborted with "cannot compute changes from unknown heads", wedging collaboration. The decoder now rebuilds each change's identity (dependencies, hash, and bytes) while loading, verified byte-identical to the reference. No stored data was lost, so affected documents recover on next load.ChangesSincealso degrades to the consistent reachable prefix instead of failing the whole read, so a single unreachable change can never again take a document offline.Performance
Several quadratic and rebuild-on-every-call paths were removed. Sequence reads, per-keystroke splices, and map-property reads are backed by incremental indexes (insertion order, materialized elements/values, cumulative UTF-16 offsets, insert-order positions, and a map-property index) that extend in place on append and rebuild lazily otherwise. The compacted save is cached against a mutation revision so repeated saves of an unchanged document are O(1). Load decode buffers are presized to the counts they are about to hold.
Native Go vs native Rust (median of 3;
Rust/Goabove 1 means Go is faster):Highlights from this pass: text typing became linear in document size (a thousand inserts fell from 7.5 ms to 1.16 ms, ~5.9x faster than Rust), repeated save fell from 3.9 ms to tens of nanoseconds (the collaboration snapshot path), and load fell from 11 ms to 6.2 ms with memory halved from 27 MB to 14 MB.
The
saverow is the cached, unchanged-document case (the server snapshot pattern); the first save after an edit rebuilds and re-encodes the document.loadis faster than the WASM oracle but still ~2.5x slower than native Rust — the remaining gap is per-operation decode allocations and change-identity reconstruction for document chunks, and is the main open optimization.Testing
Summary by cubic
Live, cross‑instance CRDT collaboration for documents using Automerge, with incremental sync, presence, and a console editor that collaborates in real time. A pure‑Go Automerge 0.10 engine is now the default (with a pinned Rust/WASM oracle); previously editors saved isolated ProseMirror JSON, now they sync live while the JSON path remains the publishing projection.
Tests
+5646-0Service
+3976-0pkg/automerge(pure‑Go default, Rust/WASM oracle): stable cursors; current‑state and diff with incremental cursors; rich‑text spans/marks/blocks; persisted sync state.@automerge/automerge-repo: incremental sync with in‑flight ack tracking and topological apply; preserved remote hashes; CBOR presence; in‑memory rooms; append‑only log with compaction; PostgreSQL NOTIFY fanout; graceful shutdown.App: console
+613-3createRichEditorAutomergeDocumentover authenticated WebSocket with reconnect/backoff; pause edits during catch‑up; render remote cursors.Package: ui
+1884-145@automerge/prosemirror: custom sync plugin with structural span updates, presence, table and divider support, and collaborative undo.createRichEditorAutomergeDocumentandsupportsRichEditorCollaboration.Package: automerge-conformance
+4517-0Package: automerge-benchmark
+2588-0Package: i18n
+5-2humanizeSecondsreturns session/persistent strings for non‑expiring values (optionalstorageType).Package: coredata
+1-1Package: helpers
+1-1Package: hooks
+1-1Package: relay
+1-1Package: routes
+1-1Other
+592-138automerge-batteryandtest-jsworkflows; Make targets for parity/fuzz/bench/fixtures; ignore WASM artifacts; trufflehog excludes.github.com/tetratelabs/wazero,github.com/coder/websocket,github.com/rivo/uniseg,github.com/fxamacker/cbor/v2; lockfile updates; benchmark script.Written for commit 23ad6ad. Summary will update on new commits.