feat(02-04): main-tab side effects placed for activate/deactivate lifecycle
Demo images / Build & push demo images (push) Has been cancelled
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:
co-authored by
Claude Fable 5
parent
a579556a4f
commit
f177a505b4
@@ -127,4 +127,79 @@ describe('useCachedResource', () => {
|
||||
expect(resource!.data.value).toBe('v1') // keep-last-known-value
|
||||
expect(resource!.error.value).toBe('offline')
|
||||
})
|
||||
|
||||
// 02-04: found while auditing Cloud.vue/Server.vue's lazy (`immediate:
|
||||
// false`) resources ahead of adding their routes to KEEP_ALIVE_PATHS.
|
||||
// Without this guard, onActivated's refreshIfStale() would treat a
|
||||
// never-fetched entry as stale and eagerly fire the "fetch on first use"
|
||||
// resource the moment the tab is first activated, even though the caller
|
||||
// never explicitly requested it (e.g. a tab-gated Paid Files fetch that
|
||||
// should wait until that sub-tab is opened).
|
||||
it('does not eagerly fetch an immediate:false resource on activation before it has been explicitly requested, but does revalidate it once it has', async () => {
|
||||
vi.useFakeTimers()
|
||||
vi.setSystemTime(new Date(2030, 0, 1, 0, 0, 0))
|
||||
const fetcher = vi.fn().mockResolvedValue('lazy-v1')
|
||||
let resource: ReturnType<typeof useCachedResource<string>> | null = null
|
||||
|
||||
const Consumer = defineComponent({
|
||||
setup() {
|
||||
resource = useCachedResource<string>({
|
||||
key: 'test.lazy-key',
|
||||
fetcher,
|
||||
ttlMs: 1000,
|
||||
persist: false,
|
||||
immediate: false,
|
||||
})
|
||||
return () => h('div', resource!.data.value ?? '')
|
||||
},
|
||||
})
|
||||
const Other = defineComponent({ render: () => h('div', 'other') })
|
||||
|
||||
const Host = defineComponent({
|
||||
setup() {
|
||||
const show = ref(true)
|
||||
return { show }
|
||||
},
|
||||
render() {
|
||||
return h(KeepAlive, null, () =>
|
||||
this.show ? h(Consumer, { key: 'consumer' }) : h(Other, { key: 'other' }),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
const wrapper = mount(Host)
|
||||
await flushPromises()
|
||||
expect(fetcher).not.toHaveBeenCalled() // immediate: false — not fetched on mount
|
||||
|
||||
// Deactivate then reactivate — still never explicitly requested, so
|
||||
// activation must not be the thing that fetches it.
|
||||
;(wrapper.vm as unknown as { show: boolean }).show = false
|
||||
await wrapper.vm.$nextTick()
|
||||
;(wrapper.vm as unknown as { show: boolean }).show = true
|
||||
await wrapper.vm.$nextTick()
|
||||
await flushPromises()
|
||||
expect(fetcher).not.toHaveBeenCalled()
|
||||
|
||||
// The caller explicitly requests it now (e.g. the user opened the tab).
|
||||
await resource!.refresh()
|
||||
expect(fetcher).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Deactivate within the TTL, reactivate — no additional fetch.
|
||||
;(wrapper.vm as unknown as { show: boolean }).show = false
|
||||
await wrapper.vm.$nextTick()
|
||||
;(wrapper.vm as unknown as { show: boolean }).show = true
|
||||
await wrapper.vm.$nextTick()
|
||||
await flushPromises()
|
||||
expect(fetcher).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Deactivate, advance past the TTL, reactivate — now it revalidates,
|
||||
// because it has been fetched before.
|
||||
;(wrapper.vm as unknown as { show: boolean }).show = false
|
||||
await wrapper.vm.$nextTick()
|
||||
vi.setSystemTime(new Date(2030, 0, 1, 0, 0, 2)) // +2s, past the 1s TTL
|
||||
;(wrapper.vm as unknown as { show: boolean }).show = true
|
||||
await wrapper.vm.$nextTick()
|
||||
await flushPromises()
|
||||
expect(fetcher).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user