Summary
use_synced_table can serve a stale snapshot indefinitely after a successful local write, on a table whose only mounted driver unmounts in the same tick as the write. The row is committed to SQLite, but no consumer sees it until the process restarts or the app is resumed — remounting the reading route does not help, because the driver takes a cache hit and never queries the database.
To the user this looks exactly like a lost write.
Mechanism
run_table_driver starts from a process-wide in-memory cache and only falls back to SQL on a miss (wavesyncdb/src/dioxus/hooks.rs:570, dev @ dff5b6f):
// Check in-memory cache first — instant on page re-navigation.
let mut rows: Vec<E::Model> = if let Some(cached) = db.get_table_cache::<Vec<E::Model>>() {
cached // ← never touches SQLite
} else {
match E::find().all(&db).await { ... }
};
That cache (connection.rs:594 / :605, keyed by TypeId::of::<Vec<E::Model>>()) is written only by a live driver — on a change notification, a refresh_rx tick, or a Lagged/Closed recovery. Nothing on the write path invalidates or updates it.
So when the only driver for a table is dropped between the write and the notification being applied, the cache keeps the pre-write snapshot, and every later mount of that table gets a cache hit and republishes it.
The write path itself is fine: the trigger-capture table records the write, drain_and_dispatch runs after every intercepted statement, and change_tx broadcasts. The problem is purely who is alive to hear it and refresh the cache.
Reproduction
In a Dioxus app with two routes over the same table:
- Route A lists
T via use_synced_table::<T>(handle).
- Route B (an editor) also holds a
use_synced_table::<T> and writes a new row via submit_upsert, then navigates away immediately on success (nav.push/nav.go_back), unmounting itself.
- Navigate back to A — even via a full unmount/remount of A.
Expected: A shows the new row.
Actual: A shows the pre-write list. It stays stale across repeated remounts and tab switches.
What I verified
On Android (release build, Dioxus 0.7, wavesyncdb @ 8193540):
- The write did persist — four rows written this way were all present after a cold start.
- Remounting the reading route (tab away → tab back, full unmount) did not surface them.
- Backgrounding and foregrounding the app did, with no cold start: a newly written row appeared immediately afterwards (2 → 3 rows). That is
ensure_auto_resume firing refresh_rx, which does a full E::find().all() and rewrites the cache — consistent with the cache being the only thing wrong.
I did not directly instrument the driver teardown racing the notification; that is the explanation most consistent with the above, and with the fact that a resume (which bypasses the cache) is a reliable fix.
Impact
Any "edit on one screen, list on another" flow where the editor navigates away on save — a very common shape. The failure is silent (no error, the write succeeds) and looks like data loss, so it tends to get investigated as a sync bug rather than a UI cache bug.
Suggested direction
The invariant worth restoring is that the cache must never outlive its own correctness — it should not depend on a subscriber happening to be mounted. Options, roughly in order of preference:
- Invalidate on local write. Have the write dispatch path (where
change_tx is published) drop or update the affected table's cache entry, so the next driver start takes the SQL path. Keeps the navigation-speed benefit for reads while making a stale hit impossible.
- Version the cache. Store a monotonic write counter alongside each cached snapshot; a driver takes the cache only when the counter matches the connection's current value.
- Treat the cache as a first-paint hint. Publish the cached rows immediately (preserving the instant-navigation feel), then always issue the query and publish again if it differs.
Option 1 looks smallest and closest to the existing design.
Workaround for consumers
Delay the post-save navigation until the write has been observed, so the writing route's own driver stays mounted long enough to refresh the cache. This fixes the editors you patch and leaves the general case open.
Summary
use_synced_tablecan serve a stale snapshot indefinitely after a successful local write, on a table whose only mounted driver unmounts in the same tick as the write. The row is committed to SQLite, but no consumer sees it until the process restarts or the app is resumed — remounting the reading route does not help, because the driver takes a cache hit and never queries the database.To the user this looks exactly like a lost write.
Mechanism
run_table_driverstarts from a process-wide in-memory cache and only falls back to SQL on a miss (wavesyncdb/src/dioxus/hooks.rs:570,dev@ dff5b6f):That cache (
connection.rs:594/:605, keyed byTypeId::of::<Vec<E::Model>>()) is written only by a live driver — on a change notification, arefresh_rxtick, or aLagged/Closedrecovery. Nothing on the write path invalidates or updates it.So when the only driver for a table is dropped between the write and the notification being applied, the cache keeps the pre-write snapshot, and every later mount of that table gets a cache hit and republishes it.
The write path itself is fine: the trigger-capture table records the write,
drain_and_dispatchruns after every intercepted statement, andchange_txbroadcasts. The problem is purely who is alive to hear it and refresh the cache.Reproduction
In a Dioxus app with two routes over the same table:
Tviause_synced_table::<T>(handle).use_synced_table::<T>and writes a new row viasubmit_upsert, then navigates away immediately on success (nav.push/nav.go_back), unmounting itself.Expected: A shows the new row.
Actual: A shows the pre-write list. It stays stale across repeated remounts and tab switches.
What I verified
On Android (release build, Dioxus 0.7,
wavesyncdb@ 8193540):ensure_auto_resumefiringrefresh_rx, which does a fullE::find().all()and rewrites the cache — consistent with the cache being the only thing wrong.I did not directly instrument the driver teardown racing the notification; that is the explanation most consistent with the above, and with the fact that a resume (which bypasses the cache) is a reliable fix.
Impact
Any "edit on one screen, list on another" flow where the editor navigates away on save — a very common shape. The failure is silent (no error, the write succeeds) and looks like data loss, so it tends to get investigated as a sync bug rather than a UI cache bug.
Suggested direction
The invariant worth restoring is that the cache must never outlive its own correctness — it should not depend on a subscriber happening to be mounted. Options, roughly in order of preference:
change_txis published) drop or update the affected table's cache entry, so the next driver start takes the SQL path. Keeps the navigation-speed benefit for reads while making a stale hit impossible.Option 1 looks smallest and closest to the existing design.
Workaround for consumers
Delay the post-save navigation until the write has been observed, so the writing route's own driver stays mounted long enough to refresh the cache. This fixes the editors you patch and leaves the general case open.