Skip to content

fix(crdt): finish the sync defects from the Dart port, server and crdt-js - #39

Merged
juicycleff merged 14 commits into
mainfrom
fix/crdt-sync-followups
Oct 8, 2026
Merged

juicycleff merged 14 commits into
mainfrom
fix/crdt-sync-followups

Conversation

@juicycleff

Copy link
Copy Markdown
Contributor

This is the rest of the sync fixes from the Dart port's defect list. #35 shipped the first three (old offline edits, hook rejections, multi-table pulls), and #37 did the late-stamp work. This branch has everything after that, on top of main.

Running it

go test -race ./crdt/...
(cd extension && go test ./...)
(cd crdt-js && npm ci && npm test)
(cd crdt-dart && dart pub get && dart test)

The Dart suite needs the Flutter 3.47 toolchain, and Go for its conformance server.

Server

  • WebSocket subscribe takes an optional since, and each poll goes out as one changes frame. presence_get gets a presence_snapshot back and unsubscribe stops the stream. Both used to come back as errors. Re-subscribing stops the old poller too, which used to keep running until the connection closed.
  • Presence events reach every SSE and WebSocket stream. SubscribePresence(ctx) gives each stream its own channel. PresenceChannel() still works but it's deprecated, because readers on it split the events between them.
  • A push the server refuses (a validation failure, or a BeforeInboundChange hook error) comes back as 422, not 500. You can test for it with errors.Is(err, crdt.ErrPushRejected), and if you serve pushes from your own handler, crdt.PushErrorStatus(err) gives you the status. The error text is the same.
  • Set elements are keyed by their json.Marshal bytes everywhere. Go used to key incoming ops by whatever bytes the client sent and its own writes by json.Marshal, so one value could sit under two keys and a remove could miss it. Sets stored before this get re-keyed when they're read or merged, and their removed markers move with them.
  • A full changes page also takes the other rows at its last cursor position. One local write stamps every field it touches with the same position, and the page could cut through them.

crdt-js

  • Applying a counter, set, list or document op keeps the newer field clock, like Go. A redelivered older op used to move it back.
  • Set keys and element order match Go byte for byte.
  • SyncEngine stops resending a push the server refused. It pushes that batch one change at a time, keeps what the server takes, drops the rest and reports them through onPushRejected. Before, one bad change went out again every 30 seconds and held up everything queued behind it. It also recognises the 500 that older servers send for the same rejections.
  • Undo and redo write new changes that get pushed, for every field type. Undo used to change local state only, so the next pull put the undone value back.

Dart

One conformance test only counted change frames. It now takes a changes batch too. The client and the sync engine already handled both.

Things you should know before merging

  • Undoing a field's first write leaves it null, since the wire has no field delete. A record delete can only be undone while its tombstone is still pending. Undo also needs room in the pending queue, and throws OfflineQueueFull when it's full.
  • Anything that looks for a 500 on a rejected push needs to look for 422.
  • A full changes page can run past 10,000 rows to finish the rows at its last position. That edge query has only run against the test fakes, not a real database.
  • extension now uses SubscribePresence and PushErrorStatus, and extension/go.mod requires grove v1.7.1 for them. That tag doesn't exist yet. Tag v1.7.1 from a commit that has this branch, or extension/v1.7.1 will require a grove without those APIs.

@juicycleff
juicycleff merged commit 485c8bd into main Oct 8, 2026
24 checks passed
@juicycleff
juicycleff deleted the fix/crdt-sync-followups branch October 8, 2026 13:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant