Skip to content

feat(samples): three devices sharing one database, with no server between them - #667

Merged
pal-tamas merged 1 commit into
mainfrom
feat/crdt-sample
Aug 10, 2026
Merged

feat(samples): three devices sharing one database, with no server between them#667
pal-tamas merged 1 commit into
mainfrom
feat/crdt-sample

Conversation

@pal-tamas

Copy link
Copy Markdown
Owner

Three devices — Phone, Laptop, Tablet — each with its own SQLite file and its own replica identity,
sharing a bucket and nothing else. Normally these would be three phones; here they are three files in
one process, which is the only difference that matters. Item 7 of #642.

Stacked on #666 (which is stacked on #665). Retarget as those merge.

RASK_CRSQLITE_PATH=/path/to/crsqlite.dylib dotnet run --project samples/Rask.Example.Crdt

The thing to try

Take two devices offline, edit different fields of the same todo on each — the priority on one, the
done flag on the other — bring both back and press Sync everyone. Both edits survive. Do the same with
a LastModified column and one of them is gone.

An E2E drives exactly that through a browser, so the claim is tested rather than asserted in prose.

Each device's link to the bucket has a switch, and flipping it makes every call fail as a real client
would see it — so the demo exercises the real offline path rather than a case the engine knows about.
The status line says "your edits are saved and will sync later" rather than reporting a failure,
because the edit is already committed to that device's own database.

FolderObjectStore

The same IObjectStore over a directory. It is what lets the sample run with no cloud credentials, but
it is not only a test double:

  • a single-machine deployment with no reason to pay for object storage;
  • a folder something else already replicates — pointed at a Syncthing share, devices converge with
    no central server at all; pointed at iCloud Drive or Dropbox, the replication is somebody else's
    problem.

Objects are written beside their key and moved into place, so a reader listing concurrently sees either
nothing or the whole object — which matters most when another process is replicating the folder while it
is being written. Keys that would escape the root are refused rather than normalised, since a key can
come back from a listing of a folder other people also write to. TryCreateAsync maps to the
filesystem's own atomic create, so the conditional-create pattern works there too.

The missing-binary problem, handled honestly

cr-sqlite ships per-platform and is not redistributed here. Without RASK_CRSQLITE_PATH the page
explains what to download instead of failing at the first query — a missing extension otherwise surfaces
as no such function: crsql_as_crr, which says nothing about what to do.

That state is asserted too, so the E2E file never quietly skips in full on a machine without the
binary; one of the two paths always runs. Verified by mutation: breaking the setup-card assertion turns
it red on a no-extension run, so it is not passing vacuously.

Testing

dotnet test tests/Rask.ObjectStore.Tests                  # 78 passed (16 new)
RASK_CRSQLITE_PATH=… <pre-push E2E gate>                  # 64 browser journeys (was 60)

The four new journeys ran inside the real gate with the extension present, and pass without it via the
setup-card path.

Local gates: dotnet format clean, dotnet build -warnaserror clean, 336 + 320 unit, 64 browser
journeys, 26 CLI build-gate.

A note on the build

Building the E2E project tripped the MSBuild node-reuse bake failure (#650) — the guard added in
#652 caught it and named the fix (-nodeReuse:false). Pre-existing, not introduced here, but worth
knowing it is still reachable from an ordinary project build.

…ween them

samples/Rask.Example.Crdt runs three replicas -- Phone, Laptop, Tablet -- each with its own SQLite
file and its own replica identity, sharing a bucket and nothing else. Normally these would be three
phones; here they are three files in one process, which is the only difference that matters. Item 7
of #642. Stacked on #665 and feat/crdt-bucket-sync.

The thing the sample exists to show: take two devices offline, edit DIFFERENT FIELDS of the same todo
on each -- the priority on one, the done flag on the other -- then bring both back and sync. Both
edits survive. Do the same with a LastModified column and one of them is gone. An E2E drives exactly
that through a browser, so the claim is tested rather than asserted in prose.

Each device's link to the bucket has a switch, and flipping it makes every call fail as a real client
would see it. So the demo exercises the real offline path rather than a case the engine knows about,
and the status line says "your edits are saved and will sync later" rather than reporting a failure --
the edit is already committed to that device's own database.

FolderObjectStore is the other half: the same IObjectStore over a directory, which is what lets the
sample run with no cloud credentials. It is not only a test double. It also covers a single-machine
deployment with no reason to pay for object storage, and -- the interesting case -- A FOLDER
SOMETHING ELSE ALREADY REPLICATES: pointed at a Syncthing share, devices converge with no central
server at all; pointed at iCloud Drive or Dropbox, the replication is somebody else's problem.
Objects are written beside their key and moved into place, so a reader listing concurrently sees
either nothing or the whole object, which matters most when another process is replicating the folder
while it is being written. Keys that would escape the root are refused rather than normalised, since
a key can come back from a listing of a folder other people also write to. TryCreateAsync maps to the
filesystem's own atomic create, so the conditional-create pattern works there too.

cr-sqlite's native binary is per-platform and is not redistributed here, so without RASK_CRSQLITE_PATH
the page explains what to download instead of failing at the first query -- a missing extension
otherwise surfaces as "no such function: crsql_as_crr", which says nothing about what to do. That
state is asserted too, so one of the two paths always runs in the E2E gate rather than the whole file
quietly skipping on a machine without the binary. Verified by mutation: breaking the setup-card
assertion turns it red on a run with no extension, so it is not passing vacuously.

The sample joins the standalone-app shape of Rask.Example.Sqlite -- plain Bootstrap classes over the
core elements, no Rask.Bootstrap or validation packages -- so it is added to the same in-repo
implicit-usings exclusion list, whose generated usings would otherwise not resolve.
@pal-tamas
pal-tamas merged commit eaa5dcd into main Aug 10, 2026
9 checks passed
@pal-tamas
pal-tamas deleted the feat/crdt-sample branch August 10, 2026 09:13
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