This repository was archived by the owner on Sep 11, 2026. It is now read-only.
Repository navigation
Move documentation to DocC and add runnable example app - #67
Merged
Merged
Conversation
`SupabaseRemote.upsert` sent exactly one row and `drainOutbox` looped entry by entry, so a 500-row backlog was 500 sequential round trips: ~50s of pure latency at 100ms RTT, with the engine reporting `.syncing` the whole time. That is the common case — first sync after an import, a device back from a long offline stretch, `retryDeadLetters()` with no argument, any bulk operation. Consecutive entries sharing a table and an op now go out as one request: an array body for the upsert, `in.(…)` for the tombstone update, capped at 256 rows. Chunking is adjacency-only — entries are never reordered to make a bigger batch — so the FK-safe order `orderForUpload` produced survives intact. Per-entry failure isolation is the part worth keeping, and it is kept: a rejected batch names the request, not the row, so the chunk is re-run one entry at a time and exactly the offending entries are classified, retried and parked as before. The failed batch itself charges no attempts, or one poison row would spend the retry budget of every healthy row beside it. `SyncRemote` grows batched forms with default implementations that loop over the single-row calls, so an existing conformance keeps working and only misses the round-trip saving. Batch representations are matched back to their uploads by primary key rather than arrival order, so a reordered response can't write one row's server state onto another; the write-back for a whole batch now lands in one transaction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015y9Y9kec3hj8S35oLQXxvQ
There was no way to see HappySync working without wiring a Supabase project, writing a schema with the right triggers and RLS, and building an app. Every integration started from prose, and the only proof the API works was a private consumer nobody evaluating the library can read. `Examples/` is a separate SPM package — an app that consumes HappySync should look like one — with a two-table schema (`lists` → `items`, real FK) that is enough to show every behaviour that matters: a plain GRDB write uploading with no engine call, an expression update, a cascade delete tombstoning children before their parent, reads staying plain `ValueObservation`, `isHealthy` vs a green tick on `.idle`, a doorbell ring pulling a change from "device B", a refused write parking, and the dead-letter repair that unparks it — plus sign-out → stop → wipe → sign-in in the one order that's safe. It runs against `InMemorySyncRemote`, so there is no backend to stand up: clone and run. `supabase/migrations/` carries the server half of the contract for pointing it at a real project, since that's the half a fake can't check. The demo is split so the sync wiring lives in a SwiftUI-free `DemoCore`, and `Tests/DemoCoreTests` asserts the same behaviours headlessly — CI runs them, so the example can't rot into something that merely compiles. Also makes `SyncSchema.migrator()` / `register(into:)` public. Both have been documented as the way to share a migrator since the wiki was written, and neither was actually reachable from outside the module. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015y9Y9kec3hj8S35oLQXxvQ
The source carries unusually good documentation — `SyncTable`'s field comments explain not just what each option does but when to reach for it and what breaks if you get it wrong — and none of it was reachable from a browser. There was no catalog, so Swift Package Index rendered nothing and Xcode Quick Help was the only way in. Meanwhile the wiki duplicated a subset of the same content by hand and had already started to drift. `Sources/HappySync/Documentation.docc/` now carries a landing page (the model in five sentences, plus curation so SyncEngine/SyncTable/SyncStatus/DeadLetter lead the symbol list) and the five articles, ported from the wiki so there is one copy: Getting Started, Server Setup, Operations & Troubleshooting, Testing, and Architecture. `.spi.yml` turns it into hosted documentation; the README links it. The wiki pages that held that content are now one-paragraph pointers to it. The sync contract stays in `docs/SYNC-CONTRACT.md` and is linked, not duplicated — it is language-neutral and belongs outside the Swift docs. Also adds the doc comments the two public symbols were missing (`SyncOp`'s cases and `SyncCursor`'s fields), and a CI job that builds the catalog, so an unresolvable symbol link surfaces on the PR rather than on Swift Package Index. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015y9Y9kec3hj8S35oLQXxvQ
`SyncTable` was eight stringly-typed parameters, so a rename in a Swift record or a GRDB migration left the manifest stale with no build error. #49 catches that at launch; this catches it at compile time, which is the difference between a crash on a tester's device and a red squiggle. `SyncTable.table(Recipe.self, …)` reads the table name off the record's `databaseTableName`, and `dependsOn:` takes record types rather than names. It returns the same `SyncTable` the string initializer does — validation, trigger install and the drain are one code path, so this is a spelling choice rather than a mode, and a manifest can mix both forms. The string form stays for tables with no Swift record type. Columns stay as checked as the record makes them: GRDB's conventional `enum Columns` ties each name to a single declaration, while a bare `Column("nutrition")` is exactly as unchecked as the string was. Key paths can't give column names without a `CodingKeys` bridge the app would have to write, so how much checking to buy stays the consumer's choice — and either way the launch check against the live schema still runs, since a record can be ahead of the migration that creates its table. Deliberately not included: `engine.save(_:)` / `engine.delete(_:)`. Since #48 made writes plain GRDB, a typed write API would re-introduce the parallel write path the trigger capture removed — and #56 dropped `delete(table:pk:)` for the same reason. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015y9Y9kec3hj8S35oLQXxvQ
The engine subscribes during its first sync pass, so a ring fired immediately after start() can land before anything is listening — the periodic poll would still converge, but not inside the assertion's window. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015y9Y9kec3hj8S35oLQXxvQ
14 tasks done
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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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
This PR consolidates HappySync's documentation into the source code via DocC, making it discoverable on Swift Package Index and eliminating duplication between wiki pages and API docs. It also adds a complete, runnable example app demonstrating the engine with the in-memory fake backend.
Key Changes
Documentation Migration
Sources/HappySync/Documentation.docc/with pages for Getting Started, Server Setup, Testing, Operations, and Architecture.spi.ymlconfiguration to enable SPI documentation buildsExample Application
Examples/package with a complete two-table SwiftUI demo (lists and items with cascade delete)DemoSyncwires the engine toInMemorySyncRemoteinstead of Supabase, requiring no backend setupDemoStoreprovides view state with plain GRDBValueObservationfor readsDemoAppandContentViewdemonstrate the UI patternsDemoSyncTestsassert the same behaviors the app exhibits (writes uploading, cascading deletes, remote changes via doorbell, dead-letter repair, sign-out quiescing)supabase/migrations/)API Enhancements
uploadChunkSizeparameter toSyncEngine.forTesting()to control batch boundaries (issue Outbox drain uploads one row per HTTP request — batch the upsert per table #54)defaultUploadChunkSize = 256as a public constantSyncTable.table(Record.self, ...)typed manifest builder that reads table names from GRDB records, catching renames at compile time (issue Typed table manifest over GRDB TableRecord — replace stringly-typed SyncTable #50)SyncRemote.upsertBatch()protocol method for batched uploads with a default loop-based implementationTest Coverage
BatchUploadTests.swiftcovering chunking logic and healthy/unhealthy upload pathsTypedManifestTests.swiftverifying typed manifest equivalence with string-based declarationsDocumentation in Code
SyncEngine,SyncTable,SyncRemote, andSyncOpNotable Details
https://claude.ai/code/session_015y9Y9kec3hj8S35oLQXxvQ