fix(sync): store what Firestore returns in a form Hive can hold - #555
fix(sync): store what Firestore returns in a form Hive can hold#555MOHITKOURAV01 wants to merge 2 commits into
Conversation
Hive encodes a fixed set of types plus anything with a registered
TypeAdapter, and this app registers none. Every synced document carries
a Timestamp under `synced_at`, so every write-back from Firestore into
Hive threw on the first document it touched:
HiveError: Cannot write, unknown type: Timestamp.
That single throw is why cloud sync never completed. `syncCycleLogs`
committed its batch, blew up reading the result back, caught its own
exception and re-queued the entire history — which the next flush pushed
again and the next sync re-queued again, on every launch. `pullCycleLogs`
aborted on its first document, so nothing remote was ever merged.
It also silently disabled the conflict resolution the class is built on:
the local half of every `synced_at` comparison came back out of Hive,
where a Timestamp had never been storable, so `localTime` was always null
and the server unconditionally won.
Everything crossing the boundary now goes through `toStorable`, and both
sides of the comparison through `syncedAt`, which reads a Timestamp and
the ISO string it becomes as the same instant.
Two structural fixes alongside. The write-back is in its own try, because
the batch has already committed by then and re-queuing for a local
caching failure is what made the queue self-refilling. And `pullCycleLogs`
catches per document, so one unreadable log costs one log.
`device_id` was `currentUserId`, the same value on every device an
account is signed in to, which carried no information for the conflict
resolution it was added for. It is now a random per-install id created on
first use — not a hardware identifier; it only has to answer "was this
the same install?".
Closes ishita2740#550
|
@MOHITKOURAV01 is attempting to deploy a commit to the ishita2740's projects Team on Vercel. A member of the Team first needs to authorize it. |
`setUpLocalStorage` installs a mock method-call handler for flutter_secure_storage, which needs the binding. Matches how firestore_service_test.dart and profile_provider_test.dart do it.
|
A note on the red Flutter check, so it is not read as this branch's doing.
All 80 analyzer errors are Two consequences worth stating plainly:
Happy to open a separate PR restoring |
Closes #550.
Hive encodes
int,double,bool,String,List,Map,DateTimeand
Uint8List, plus anything with a registeredTypeAdapter— and thisapp registers none:
Every synced document carries a
Timestampundersynced_at, and everypath in
FirestoreServicehands documents straight toLocalStorageService. So each write-back threw on the first document ittouched:
That one throw is why cloud sync never completed:
syncCycleLogscommitted its batch and then blew up reading theresult back. The
catchmarked the sync failed and re-queued the wholehistory — data that was already on the server.
flushPendingQueuepushed it again and deleted the queue keys; the next
syncCycleLogsre-queued all of it. On every launch, every login, and every
connectivity change, with the indicator never leaving error/pending.
pullCycleLogshad no per-document guard, so the first documentaborted the loop and nothing remote was ever merged.
syncProfile/pullProfilethe same, viasaveProfile.It also silently disabled the conflict resolution the class is built on.
localTimewas read back out of Hive, where aTimestamphad never beenstorable, so it was always
null, thelocalTime == nullbranch was theonly reachable one, and the server won unconditionally. Last-write-wins
had no local write to compare against.
What changed
toStorablerewrites a document into something Hive can encode:TimestampandDateTimebecome ISO-8601 strings,DocumentReferenceits path,
GeoPointa plain map, and nested maps and lists are walked.ISO strings are what the pending queue already stores under
queued_atand what
start_dateis, so local documents stay one shape rather thantwo.
syncedAtreads aTimestampand the ISO string it becomes as thesame instant, so
serverWinscompares two real times. Ties still go tothe server: a document written and immediately read back has the same
stamp on both sides, and treating that as a conflict would strand the
local copy on its pre-resolution value.
The write-back moved into its own
try. The batch has committed bythat point, so a failure there is a local caching problem — re-queuing
the whole history for it is what made the queue refill itself from the
very sync that had just emptied it.
_isSyncingmoved into afinallywhile I was in there, so no path can leave the flag set and turn every
later sync into a silent no-op.
pullCycleLogscatches per document, so one unreadable log costs onelog rather than the whole merge.
device_idwasLocalStorageService.currentUserIdin all four writepaths — the same value on every device an account is signed in to, so two
devices were indistinguishable and the field carried no information for
the conflict resolution it exists for. It is now a random per-install id
created on first use and stored unscoped, because it identifies the
handset rather than the account. Deliberately not a hardware identifier:
a locally generated random value answers "was this the same install?",
which is the only question the sync asks.
Tests
rhythma_flutter/test/services/firestore_hive_round_trip_test.dart— 20cases.
The first one is the bug, asserted against a real Hive box rather
than a mock: putting an unconverted Firestore document in throws, and the
same document goes in once converted. A mocked box that happily accepted
a
Timestampwould have made every other test here pass while productionkept throwing, so it is worth reading first. It catches by hand rather
than with
throwsAbecause Hive serialises inside the Futureputreturns, and whether the error surfaces synchronously is an
implementation detail the test should not depend on.
The last-write-wins group covers the case that could not previously
happen at all — a newer local write surviving — plus the tie, the empty
local, and an unstamped server document.
Verified the changed files parse cleanly. I don't have a Flutter SDK on
this machine, so
flutter analyzeandflutter testwill get theirfirst real run in CI.