feat(02-04): main-tab side effects placed for activate/deactivate lifecycle
Demo images / Build & push demo images (push) Has been cancelled

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>
This commit is contained in:
archipelago
2026-07-30 15:04:41 -04:00
co-authored by Claude Fable 5
parent a579556a4f
commit f177a505b4
9 changed files with 878 additions and 82 deletions
+16 -1
View File
@@ -97,7 +97,22 @@ export function useCachedResource<T>(opts: CachedResourceOptions<T>): CachedReso
// loses focus). Without this a kept-alive tab would paint instantly
// forever and never revalidate. Vue no-ops onActivated outside a
// <KeepAlive> boundary, so this is safe for every existing consumer.
onActivated(() => refreshIfStale())
//
// `immediate: false` resources are a distinct case (found in 02-04's
// audit, once Cloud.vue/Server.vue's main-tab paths joined
// KEEP_ALIVE_PATHS): `stale()` is true for any never-fetched entry
// (`fetchedAt === null`), so a bare `refreshIfStale()` here would fire
// the fetch the moment the tab is first activated — defeating a resource
// deliberately marked "fetch on first use" (e.g. a tab-gated fetch that
// should wait for the user to open that sub-tab). Only auto-revalidate
// an `immediate: false` resource on activation once it has actually been
// fetched at least once; before that, activation is a no-op and the
// resource's own explicit trigger (a watcher, an onMounted/onActivated
// "kick") still owns the first fetch.
onActivated(() => {
if (opts.immediate === false && entry.fetchedAt === null) return
refreshIfStale()
})
}
if (opts.immediate ?? true) refreshIfStale()