You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The current persistence model (atomWithLocalForage) is load-once-into-memory + write-whole-value-back, with an optimistic in-memory cache and background flush. This design is the root cause of #1820 (cross-tab clobber) and a recurring source of issues. The committed ADR (per-key-diff-merge) patches the storage layer but, by its own scope boundary, leaves stale reads and live cross-tab freshness unsolved.
This spike validates an alternative: TanStack Query owns persistence as the async source of truth, accessed the way we'd access a remote server — query-backed reads with loading states, mutations with optimistic updates — mostly outside Jotai.
The question to resolve: is this approach sound and low-risk enough to adopt for all persisted collections, and is the blast radius actually small? (Initial investigation suggests reads funnel through ~5 selector files and writes through a handful of hooks, not the feared 32-sites-everywhere.)
Hard constraints:
The shape of data in localforage/IndexedDB must not change (zero migration; upgrading users barely notice — only new loading screens).
Lean on optimistic updates to keep the UI fast.
IndexedDB stays (per ADR indexeddb-not-localstorage-for-persistence) — localStorage is ruled out.
Expected Outcome
A working proof of concept migrating userStyling (the EXTREME-severity, highest-frequency collection in #1820) to the new model, plus a recommendation + go/no-go for the remaining collections.
The PoC must demonstrate:
Foundation primitives — persistedQuery / persistedMutation factories wrapping localForage (getItem/setItem on the existing key, unchanged shape).
Boot Suspense gate — replace the top-level await Promise.all([...]) in storageAtoms.ts for this key with ensureQueryData behind the existing <Suspense> in AppStatusLoader, so synchronous getQueryData reads remain available afterward (preserving the sync-read assumption non-React consumers depend on). This is the "new loading screen."
Optimistic mutation — useVertexStyling / useEdgeStyling keep their current signatures; internally onMutate patches the cache, mutationFn re-reads fresh storage + applies the explicit delta + writes back, onError rolls back. Call sites untouched.
Findings on the known hard parts: sync-read preservation behind the boot gate; non-React mutators (schemaSyncQuery.ts, edgeConnectionsQuery.ts, nodeCountByNodeTypeQuery.ts, graphSession/storage.ts); selector-bridge correctness for mergedConfigurationSelector (joins config+schema+styling).
Why userStyling (not settings flags)
Settings flags are scalar booleans — they don't exhibit the read-modify-write clobber and wouldn't exercise optimistic delta-merge, cross-tab reconciliation, or the array-splice relocation. userStyling is the EXTREME-severity collection from #1820, the highest-frequency user mutation, and already carries the delta-based splice logic in useVertexStyling/useEdgeStyling — making it the true tracer bullet for the whole approach.
Non-goals
Migrating other collections (separate slices, gated on this spike's recommendation).
Changing the on-disk storage shape.
Rebuilding the Jotai derived-selector graph wholesale (a larger bet; revisit only if this spike recommends it).
Goal
The current persistence model (
atomWithLocalForage) is load-once-into-memory + write-whole-value-back, with an optimistic in-memory cache and background flush. This design is the root cause of #1820 (cross-tab clobber) and a recurring source of issues. The committed ADR (per-key-diff-merge) patches the storage layer but, by its own scope boundary, leaves stale reads and live cross-tab freshness unsolved.This spike validates an alternative: TanStack Query owns persistence as the async source of truth, accessed the way we'd access a remote server — query-backed reads with loading states, mutations with optimistic updates — mostly outside Jotai.
The question to resolve: is this approach sound and low-risk enough to adopt for all persisted collections, and is the blast radius actually small? (Initial investigation suggests reads funnel through ~5 selector files and writes through a handful of hooks, not the feared 32-sites-everywhere.)
Hard constraints:
indexeddb-not-localstorage-for-persistence) — localStorage is ruled out.Expected Outcome
A working proof of concept migrating
userStyling(the EXTREME-severity, highest-frequency collection in #1820) to the new model, plus a recommendation + go/no-go for the remaining collections.The PoC must demonstrate:
persistedQuery/persistedMutationfactories wrapping localForage (getItem/setItemon the existing key, unchanged shape).await Promise.all([...])instorageAtoms.tsfor this key withensureQueryDatabehind the existing<Suspense>inAppStatusLoader, so synchronousgetQueryDatareads remain available afterward (preserving the sync-read assumption non-React consumers depend on). This is the "new loading screen."useVertexStyling/useEdgeStylingkeep their current signatures; internallyonMutatepatches the cache,mutationFnre-reads fresh storage + applies the explicit delta + writes back,onErrorrolls back. Call sites untouched.invalidateQuerieslistener fixes stale reads across tabs. Reproduce the two-tab scenario and confirm both type X and type Y survive.Deliverables:
userStylingPoC branch/PR.per-key-diff-merge(this model fixes clobber and stale reads and freshness).schemaSyncQuery.ts,edgeConnectionsQuery.ts,nodeCountByNodeTypeQuery.ts,graphSession/storage.ts); selector-bridge correctness formergedConfigurationSelector(joins config+schema+styling).Why userStyling (not settings flags)
Settings flags are scalar booleans — they don't exhibit the read-modify-write clobber and wouldn't exercise optimistic delta-merge, cross-tab reconciliation, or the array-splice relocation.
userStylingis the EXTREME-severity collection from #1820, the highest-frequency user mutation, and already carries the delta-based splice logic inuseVertexStyling/useEdgeStyling— making it the true tracer bullet for the whole approach.Non-goals
Related Issues
20260616-per-key-diff-merge-cross-tab-reconciliationImportant
Internal only — this issue is maintained by the core team and is not accepting external contributions.