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
@@ -404,7 +404,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { ref, computed, onActivated, onDeactivated, onMounted, onUnmounted, watch } from 'vue'
|
||||
import DOMPurify from 'dompurify'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
import { useCachedResource, type CachedResource } from '@/composables/useCachedResource'
|
||||
@@ -828,11 +828,14 @@ async function createService(name: string, port: number | null) {
|
||||
catch (e) { addServiceError.value = e instanceof Error ? e.message : 'Failed to create service' } finally { addingService.value = false }
|
||||
}
|
||||
|
||||
onMounted(() => { checkTorStatus(); loadNetworkData(); loadInterfaces(); loadDiskStatus(); loadTorServices(); loadVpnPeers(); loadFipsSummary() })
|
||||
// Only-while-visible: polls VPN status every 15s so IP updates after pairing
|
||||
// (write-through to the cached aggregate without refetching the other three
|
||||
// RPCs). An off-screen Server tab must not keep polling (T-02-03) — armed on
|
||||
// activation, cleared on deactivation; idempotent (clears any existing
|
||||
// handle first, so two consecutive activations never double-arm).
|
||||
let vpnPollInterval: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
// Poll VPN status every 15s so IP updates after pairing (write-through to
|
||||
// the cached aggregate without refetching the other three RPCs)
|
||||
const vpnPollInterval = setInterval(async () => {
|
||||
async function pollVpnStatusOnce() {
|
||||
try {
|
||||
const vpnRes = await rpcClient.vpnStatus()
|
||||
networkRes.optimistic(cur => ({
|
||||
@@ -843,8 +846,56 @@ const vpnPollInterval = setInterval(async () => {
|
||||
wgIp: vpnRes.wg_ip ?? '',
|
||||
}))
|
||||
} catch { /* ignore */ }
|
||||
}, 15000)
|
||||
onUnmounted(() => clearInterval(vpnPollInterval))
|
||||
}
|
||||
function armVpnPoll() {
|
||||
if (vpnPollInterval) clearInterval(vpnPollInterval)
|
||||
// Immediate first tick on (re)activation — a re-entered tab must not sit
|
||||
// on a 15s-stale VPN IP waiting out the interval.
|
||||
void pollVpnStatusOnce()
|
||||
vpnPollInterval = setInterval(() => void pollVpnStatusOnce(), 15000)
|
||||
}
|
||||
function disarmVpnPoll() {
|
||||
if (vpnPollInterval) { clearInterval(vpnPollInterval); vpnPollInterval = null }
|
||||
}
|
||||
|
||||
// Every-entry: loadDiskStatus is a plain fetch with no useCachedResource
|
||||
// backing it, so under KeepAlive it would otherwise run once ever and the
|
||||
// disk-space warning banner would never update again for the rest of the
|
||||
// session.
|
||||
function armServerEntryEffects() {
|
||||
void loadDiskStatus()
|
||||
armVpnPoll()
|
||||
}
|
||||
|
||||
// Vue fires onActivated immediately after onMounted on a KeepAlive-wrapped
|
||||
// component's first mount — this flag lets onMounted's call count as the
|
||||
// first activation's arm, so onActivated only re-arms (and re-issues
|
||||
// loadDiskStatus + the immediate VPN poll tick) on a genuine later
|
||||
// reactivation, not redundantly right after mount.
|
||||
let serverFreshMount = true
|
||||
onActivated(() => {
|
||||
if (serverFreshMount) { serverFreshMount = false; return }
|
||||
armServerEntryEffects()
|
||||
})
|
||||
onDeactivated(() => disarmVpnPoll())
|
||||
onUnmounted(() => disarmVpnPoll())
|
||||
|
||||
// Once-per-session seed for the six useCachedResource-backed loads below:
|
||||
// each resource's own onActivated (added in useCachedResource.ts) already
|
||||
// revalidates it, staleness-gated, on every later reactivation — same
|
||||
// pattern established for Marketplace.vue in 02-02 — so this task only
|
||||
// relocates the ONE loader here that bypasses useCachedResource entirely
|
||||
// (loadDiskStatus, folded into armServerEntryEffects() above). Server's
|
||||
// seven-call fan-out itself stays fire-and-forget/concurrent, unconverted —
|
||||
// that data-layer conversion is 02-06's job, not this task's.
|
||||
//
|
||||
// Also arms the every-entry effects directly here: onActivated is a no-op
|
||||
// outside a <KeepAlive> boundary (confirmed by ServerNetworkRefresh.test.ts,
|
||||
// which mounts this view bare), so a bare mount must not silently skip them.
|
||||
onMounted(() => {
|
||||
checkTorStatus(); loadNetworkData(); loadInterfaces(); loadTorServices(); loadVpnPeers(); loadFipsSummary()
|
||||
armServerEntryEffects()
|
||||
})
|
||||
|
||||
watch(showWifiModal, (open) => { if (open) scanWifi() })
|
||||
watch(showDnsModal, (open) => { if (open) { dnsSelectedProvider.value = networkData.value.dnsProvider || 'system'; dnsError.value = '' } })
|
||||
|
||||
Reference in New Issue
Block a user