Skip to content

feat: #36 expose invalidate_forest_cache in fula-flutter + fula-js bindings (0.6.9)#37

Merged
ehsan6sha merged 3 commits into
mainfrom
fix/36-expose-invalidate-forest-cache
Jun 12, 2026
Merged

feat: #36 expose invalidate_forest_cache in fula-flutter + fula-js bindings (0.6.9)#37
ehsan6sha merged 3 commits into
mainfrom
fix/36-expose-invalidate-forest-cache

Conversation

@ehsan6sha

Copy link
Copy Markdown
Member

Fixes #36 — the encrypted client caches each bucket's forest for the client lifetime, so a long-lived session never observes another device's uploads: list_from_forest / get_object_flat keep resolving against the session-stale index (FxFiles repro: a mobile web tab kept showing a 2-file listing indefinitely while desktop had uploaded a third). The dirty-safe escape hatch — EncryptedClient::invalidate_forest_cache / invalidate_all_forest_caches (encryption.rs:9015/:9029) — existed but was unreachable from Dart and JS.

Verified not addressed in 0.6.8 before starting: that release (#35) fixed #34 entirely inside fula-crypto; grep confirms zero invalidate references in either binding crate on main. (0.6.8's two-client reconcile only covers stale writers via the 412 path; pure readers stay pinned forever, exactly as the issue reports.)

What this adds

  • fula-flutter (crates/fula-flutter/src/api/forest.rs): invalidate_forest_cache(client, bucket) and invalidate_all_forest_caches(client) — guard read → call through, exactly the issue's sketch. Docs spell out the dirty-safe contract (pending changes are never evicted; flush first) and the intended app wiring (pull-to-refresh / tab-resume / reconnect / SWR revalidation).
  • Dart bindings regenerated with the pinned codegen (flutter_rust_bridge_codegen 2.11.1 — same as the hash-consistent release pipeline): invalidateForestCache / invalidateAllForestCaches now exist on lib/src/api/forest.dart. flutter analyze: no issues. Diff is purely additive (new functions + their generated glue).
  • fula-js npm parity (crates/fula-js/src/lib.rs): same two exports, invalidateForestCache / invalidateAllForestCaches, and CI's wasm .d.ts export verification now guards both so parity can't silently regress.

Tests

crates/fula-client/tests/issue_36_invalidate_forest_cache.rs — acceptance criteria pinned at the layer both bindings delegate to, against the stateful conditional-PUT mock master (3/3 passing):

  1. Cross-device staleness escape (acceptance bullet 1, verbatim): device A lists 2 files → device B uploads a third → A's listing still shows 2 (pins the lifetime-pinning premise the binding escapes) → invalidate_forest_cache + re-list on the same client instance → 3 files, and B's file downloads byte-exactly. No client rebuild.
  2. Dirty-safe (acceptance bullet 2): a deferred (unflushed) upload survives invalidation — has_pending_forest_changes stays true and the pending entry stays listed; after flush_forest, invalidation takes effect and the reloaded forest still lists the persisted file.
  3. Bulk variant: invalidate_all_forest_caches drops clean forests (they refresh and see the other device's upload) and keeps dirty ones.

crates/fula-flutter/tests/issue_36_invalidate_bridge_test.rs — pins the binding wiring: the functions live on the FRB-scanned crate::api surface and are safe, idempotent no-ops on a client with no loaded forest (the "call unconditionally from every revalidation path" pattern).

Validation

  • cargo test -p fula-client --test issue_36_invalidate_forest_cache — 3 passed
  • cargo test -p fula-flutter — all suites green incl. the new bridge test
  • cargo check -p fula-js --target wasm32-unknown-unknown / -p fula-flutter --target wasm32-unknown-unknown / native — clean
  • flutter analyze on packages/fula_client — no issues

Version

0.6.8 → 0.6.9 (workspace, fula-js + fula-flutter crates, fula_client pubspec/podspec, changelog). The fula-js/fula-wasm npm packages keep their own 0.2.x line.

Consumer side: FxFiles' FulaApiService.invalidateForestCache(bucket) (commit cfaa185) is already wired to call this on every SWR revalidation/force path — it picks the Rust call up as soon as 0.6.9 ships, replacing the interim full-client-rebuild workaround.

🤖 Generated with Claude Code

ehsan6sha and others added 3 commits June 12, 2026 14:57
…+ fula-js

The encrypted client caches each bucket forest for the client lifetime,
so a long-lived session never observes another device's uploads -
listings and downloads resolve against the session-stale index
indefinitely (FxFiles repro: a mobile web tab kept a 2-file listing
while desktop had uploaded a 3rd). The dirty-safe escape hatch
(EncryptedClient::invalidate_forest_cache / invalidate_all_forest_caches,
encryption.rs:9015/:9029) existed but was unreachable from Dart and JS.

- fula-flutter: invalidate_forest_cache(client, bucket) +
  invalidate_all_forest_caches(client) bridge fns (guard read ->
  call through); Dart bindings regenerated with the pinned
  flutter_rust_bridge_codegen 2.11.1 (invalidateForestCache /
  invalidateAllForestCaches on api/forest); flutter analyze clean.
- fula-js: same two exports (invalidateForestCache /
  invalidateAllForestCaches) for npm parity; CI's wasm .d.ts export
  check now guards both.

Tests:
- fula-client/tests/issue_36_invalidate_forest_cache.rs pins the
  acceptance criteria at the layer both bindings delegate to, against
  the stateful conditional-PUT mock master:
  * stale listing on an EXISTING client refreshes after invalidate +
    re-list (sees another device's upload; no client rebuild) - and
    pins the lifetime-pinning premise (without invalidate the listing
    stays stale);
  * a dirty (unsaved) forest is NOT evicted - pending entry and
    has_pending_forest_changes survive; after flush, invalidation
    takes effect and the persisted file is still listed;
  * invalidate_all drops clean forests (they refresh) and keeps dirty
    ones.
- fula-flutter/tests/issue_36_invalidate_bridge_test.rs pins the
  binding wiring: fns live on the FRB-scanned crate::api surface and
  are safe idempotent no-ops on a client with no loaded forest.

Fixes #36

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ndings)

Workspace + fula-js + fula-flutter crate versions, fula_client pubspec/
podspec, changelog entry. The fula-js / fula-wasm npm packages keep
their own 0.2.x version line.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…36)

invalidate_forest_cache had a TOCTOU window between the is_dirty read
and the remove(): a concurrent deferred put could dirty the entry
in-between and the eviction would silently drop the pending index
entry. Reachable in practice now that #36 exposes the call to app
refresh paths running concurrently with uploads. DashMap::remove_if
holds the shard lock across predicate + removal, closing the window.
(invalidate_all_forest_caches already used the atomic retain().)

Also adds invalidate_is_per_bucket_isolated: invalidating bucket A
refreshes A and must NOT evict bucket B (B stays session-pinned until
its own invalidate) - guards the cache keying in both directions.

Flagged by external review (Gemini) during pre-merge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant