entry(key, persist = true) and optimistic(key, update) silently defaulted
to persist:true after the first call for a key, and optimistic() didn't
accept a persist argument at all. Every current call site happened to be
safe, but the invariant was unenforced: a future caller invoking
store.optimistic() before any useCachedResource({persist:false}) has run
for that key in the same tick would silently start writing to
sessionStorage with no indication anything is wrong (T-02-01).
persist is now a required argument on both functions (no default), and the
per-key decision is recorded and asserted (dev-only warning) against any
later call that disagrees. useCachedResource's optimistic() wrapper now
threads its own already-resolved persist value through automatically, so
no existing composable caller changes behavior. The two call sites that
use the resources store directly (Cloud.vue/PeerFiles.vue's per-peer
browse cache) now pass persist:true explicitly, matching their existing
behavior exactly.
Co-Authored-By: Claude <noreply@anthropic.com>
Task 1 of 02-04 — audits every side effect owned by Home.vue, web5/Web5.vue,
Chat.vue, Cloud.vue, Server.vue and Mesh.vue and places each into one of
three buckets (once-per-session, every-entry, only-while-visible) so their
instances are safe to keep alive once KEEP_ALIVE_PATHS widens in Task 2.
- Home.vue: systemStats/wallet polling, the wsClient wallet-push
subscription and its debounce timer follow activate/deactivate with an
immediate re-sync on entry; hydrateWalletSnapshot/checkUpdateStatus/cloud
usage stay once-per-session.
- Chat.vue: the window `message` listener and ContextBroker follow
activate/deactivate; aiuiConnected is never reset on deactivate since the
iframe's one-time 'ready' message won't resend on re-entry.
- Web5.vue: the six child-component data loaders (none use
useCachedResource internally) and the 30s LND poll move to
activate/deactivate; the DID lookup and intro flag stay once-per-session.
- Cloud.vue: the per-peer transport/reachability warm-cache
(loadPeerFiles/loadCounts/loadPeers) re-runs every entry — the one path
here that bypasses useCachedResource and would otherwise render stale peer
reachability (T-02-13).
- Server.vue: the previously module-scope-armed 15s VPN poll interval now
follows activate/deactivate (it used to run forever regardless of
visibility); loadDiskStatus becomes every-entry.
- Mesh.vue: the entire live-communications surface (window/document
listeners, the 5s/15s poll intervals, the ws peer-push subscription, and
the six-way federation/self/contacts refresh) follows activate/deactivate;
a share-to-mesh handoff via direct navigation is now correctly picked up
on every activation, not just the first mount.
- useCachedResource.ts: onActivated's staleness check now skips an
`immediate: false` resource that has never been explicitly fetched, so a
tab-gated lazy resource (Cloud.vue's Paid Files / My Files walk) isn't
eagerly force-loaded the moment its owning view is kept alive.
- Every arm/disarm pair is idempotent and duplicated into both onMounted and
onActivated, since onActivated is a no-op outside a KeepAlive boundary
(caught by CloudPeersRefresh.test.ts, which mounts Cloud.vue bare) —
fresh-mount guard flags avoid double-firing the heavier loaders
(Home/Mesh/Web5/Server) on a KeepAlive-wrapped first mount.
- New neode-ui/src/views/dashboard/__tests__/keepAliveLifecycle.test.ts
covers the six lifecycle behaviors plus a real-view assertion
(Server.vue's VPN poll, mounted inside a real KeepAlive).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wires the phase's shared architecture end to end through one main tab
(Marketplace, the worst-measured revisit per 02-FINDINGS.md):
- keepAliveRoutes.ts: exact-match route classifier (shouldKeepAlive,
KEEP_ALIVE_PATHS, KEEP_ALIVE_MAX=6), seeded with only the tracer tab's
path. Deliberately not KeepAlive `include` name-matching (async
components under `<script setup>` have no inferable name).
- DashboardRouterView.vue: extracts Dashboard.vue's nested RouterView into
a host where <KeepAlive> is a permanent element (never torn down by
v-if) with its child conditionally present via shouldKeepAlive(route);
a sibling Transition renders non-cached routes. Both original wrapper
shapes (full-bleed chat/mesh vs. padded/scrollable default) are
preserved via computed helpers on one stable, unkeyed wrapper div; the
:key moves onto <component> itself. Adds per-route scroll retention
since the scroll container is now stable across navigations.
- useCachedResource.ts: registers onActivated(() => refreshIfStale())
alongside the existing onScopeDispose block, closing the gap where a
kept-alive tab would otherwise never revalidate on reactivation
(onScopeDispose doesn't fire on deactivate; window focus doesn't fire
on an in-SPA tab switch). No-ops safely for all 8 existing consumers
outside a KeepAlive boundary.
- useRouteTransitions.ts: exports TAB_ORDER so 02-04 can widen
KEEP_ALIVE_PATHS from the same source of truth.
- Dashboard.vue: renders DashboardRouterView in place of the inline
block; removes the now-superseded detail-route scroll save/restore
(querySelector target no longer exists post-restructure — the new
per-route Map in DashboardRouterView.vue is a strict superset).
Tests: keepAliveTabs.test.ts proves an included path's instance survives
a round trip (1 mount, 2 activations) while a detail path remounts (2
mounts); useCachedResource.test.ts proves no refetch inside the TTL,
exactly one refetch after it lapses, safe use outside KeepAlive, and
keep-last-value + sticky-ready semantics on a rejected refresh.
Full suite (706 tests), type-check, and build all green; built bundle
carries the new KeepAlive wiring (web/dist/neode-ui/assets).
- PeerFiles.vue reads the SAME `cloud.peer-browse:<onion>` entry Cloud.vue's
per-peer fan-in fills, so Cloud → peer files paints instantly from cache
and revalidates behind it; catalog/error/loading/transport are now
computed views over the store entry.
- preview-peer fan-out is capped at 3 concurrent with a queue (was one 30s
RPC per media item, all at once, unbounded) and aborts on unmount.
- browse + preview RPCs drop to maxRetries:1 — retry×3 turned one slow
peer into a 90s spinner.
- fix useCachedResource's interface types: `ReturnType<typeof computed<T>>`
resolves to the writable overload (WritableComputedRef), which broke
vue-tsc against the plain computed() returns; use ComputedRef<T>.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Part B1+B2 of docs/FIPS-UPTIME-AND-UI-STATE-PLAN.md. Foundation for pages
that render instantly from cache on revisit and revalidate in the
background, instead of unmount-refetch-spinner on every navigation.
- stores/resources.ts: keyed {data, loadState, fetchedAt, error} entries
with sticky-ready (never regress ready→loading), keep-last-value on
error, per-key in-flight dedup, sessionStorage snapshot hydrate,
debounced invalidate() fan-out, optimistic-update-with-rollback
- composables/useCachedResource.ts: SWR hook over the store — synchronous
hydrate, TTL-gated background revalidate, revalidate-on-focus,
abort-on-unmount fetcher signal
- rpc-client: AbortSignal support (aborts pending retries too), opt-in
in-flight dedup keyed method+params, per-call maxRetries override
- 10 tests covering the SWR semantics
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>