Skip to content

use_synced_table serves a stale table cache after a write whose only driver unmounts (looks like a lost write) #115

Description

@pvg13

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:

  1. Route A lists T via use_synced_table::<T>(handle).
  2. 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.
  3. 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:

  1. 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.
  2. 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.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingpriority:highSignificant bug, common path affectedtopic:dioxusDioxus hooks and reactivity layer

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions