23 KiB
phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, requirements-note, coverage, duration, completed, status
| phase | plan | subsystem | tags | requires | provides | affects | tech-stack | key-files | key-decisions | patterns-established | requirements-completed | requirements-note | coverage | duration | completed | status | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 02-ui-performance | 05 | ui |
|
|
|
|
|
|
|
|
PERF-02 is NOT marked complete — it also spans 02-06 and 02-07 (Server/Home data layer, Chat/AIUI), which still extend the KeepAlive/cache architecture to the remaining main tabs, per the precedent set by 02-02/02-03/02-04's own summaries. |
|
50min | 2026-07-30 | complete |
Phase 02 Plan 05: Mesh Tab Cache + Graphics Lifecycle Summary
Mesh's six fetch groups (status/peers/messages/deadman/blockheaders, transport status, federation nodes, self onion, self DID, contacts) each behind a keyed useCachedResource with per-group TTL/persist decisions and a shared RefreshIndicator, plus the Leaflet map's listener/ResizeObserver lifecycle bounded across tab deactivation — no D3 simulation was found to exist in Mesh.vue's tree, contrary to RESEARCH.md's premise
Performance
- Duration: ~50 min
- Completed: 2026-07-30T20:17:44Z
- Tasks: 2 (Task 1 auto/tdd, Task 2 auto/tdd)
- Files modified: 8 (2 new test files, 6 modified)
Accomplishments
-
Six fetch groups cached behind
useCachedResource, all hosted inMesh.vue(see key-decisions for why, not instores/mesh.ts/stores/transport.ts), eachimmediate: falseand force-refreshed only when stale viarefreshMeshGroupIfStale()inside a singlePromise.allSettledarray inarmMeshLive:Key Fetcher wraps TTL Persist Reason mesh.refresh-allmesh.refreshAll()(status/peers/messages/deadman/block-headers)10,000ms falsePeer reachability is the liveness-critical surface T-02-13 forbids freezing; carries peer DIDs/pubkeys (identity payload) mesh.transport-statustransport.fetchStatus()10,000ms trueLive transport-availability state, but no peer identity payload — explicitly the one group the plan names as safe to persist mesh.federation-nodesrefreshFederationNodes()30,000ms falseD-06 default (not liveness-critical); carries DID/pubkey/onion mesh.self-onionrefreshSelfOnion()300,000ms falseThis node's own onion address is effectively static; identity payload mesh.self-didrefreshSelfDid()300,000ms falseThis node's own DID is effectively static; identity payload mesh.contactsrefreshContacts()30,000ms falseUser-set aliases/contact records — identity payload per T-02-01 -
armMeshLive's six-wayPromise.all([...])fan-out replaced withPromise.allSettled(meshCachedGroups.map(refreshMeshGroupIfStale))— proven concurrent in test (all six underlying RPCs have already fired by the very next synchronous line after mount, before anyawait), and proven non-blocking (one group's store action rejecting still lets the other five complete and the post-fan-out.then()—refreshOutboxCount+ deep-link matching — still runs). -
dedup: trueadded to every RPC call backing the six groups:mesh.status,mesh.peers,mesh.messages,mesh.deadman-status,mesh.block-headers(mesh.ts),transport.status(transport.ts), and the convenience methodsgetNodeDid,getTorAddress,meshContactsList,federationListNodes(rpc-client.ts). -
RefreshIndicatoradded to the Mesh header (new.mesh-title-rowflex wrapper around the existing<h1>, no change to any pre-existing selector/animation), driven bymeshRefreshIndicatorState—'refreshing'whenever any of the six groups is inloadState === 'refreshing'. -
MeshMap.vue's Leaflet lifecycle:
onMounted's window-resize-listener +ResizeObserversetup refactored into idempotentarmMapVisibility()/disarmMapVisibility(), dual-registered ononMounted+onActivated(with a fresh-mount guard) and torn down ononDeactivated, mirroring Mesh.vue's ownarmMeshLive/teardownMeshLiveEffectsidiom from 02-04. The Leaflet instance itself is never destroyed/recreated by this (initMap()'s own guard already makes construction idempotent); reactivation callsmap.invalidateSize()vianextTickso a map laid out off screen re-tiles at its real size. -
Flagged premise mismatch, documented and tested: RESEARCH.md's Task 2 premise ("Mesh is... a live D3 force simulation and a Leaflet map") does not hold — a full grep across
neode-ui/srcford3/forceSimulation/simulationfound zero hits inMesh.vue's orMeshMap.vue's component tree. The only D3 force simulation in the codebase belongs toNetworkMap.vue(used byFederation.vue, a different view entirely, out of this plan's scope). Task 2's D3-specificmust_havestruths ("the D3 force simulation stops...", "restarts only if data changed...", "cancels any pending animation-frame callback...") are therefore vacuously true (there is nothing to leak) — only the real Leaflet-map-specific truths were implemented and tested.
Task Commits
Each task was committed atomically:
- Task 1: Cache the six Mesh fetch groups without serializing them -
31389bcc(feat, tdd) - Task 2: Bound the D3 simulation and Leaflet map across deactivation -
abdfa07a(feat, tdd — D3 portion vacuous per the flagged finding above; Leaflet portion real)
Plan metadata: (this commit)
Note: both tasks are TDD tasks; tests were written and made to pass within each task's own commit, per this repo's established single-commit-per-task convention (see 02-01/02-02/02-03/02-04 history).
Files Created/Modified
neode-ui/src/views/Mesh.vue— sixuseCachedResourceentries,meshCachedGroups/refreshMeshGroupIfStale/meshRefreshIndicatorState,armMeshLive's fan-out converted toPromise.allSettled,RefreshIndicatoradded to the headerneode-ui/src/stores/mesh.ts—dedup: trueonfetchStatus/fetchPeers/fetchMessages/fetchDeadmanStatus/fetchBlockHeaders's RPC callsneode-ui/src/stores/transport.ts—dedup: trueonfetchStatus/fetchPeers's RPC callsneode-ui/src/api/rpc-client.ts—dedup: trueongetNodeDid,getTorAddress,meshContactsList,federationListNodesneode-ui/src/components/MeshMap.vue—armMapVisibility()/disarmMapVisibility()idempotent pair, dual-registeredonMounted/onActivated,onDeactivatedteardown,nextTick-scheduledinvalidateSize()on reactivationneode-ui/src/views/mesh/mesh-styles.css— new.mesh-title-rowrule (flex wrapper for the title + RefreshIndicator; no existing selector touched)neode-ui/src/views/__tests__/meshTabCache.test.ts— new; 8 tests covering Task 1's five behaviors plus dedup/persist/indicator-wiring assertionsneode-ui/src/components/__tests__/meshMapLifecycle.test.ts— new; 4 tests covering Task 2's real (Leaflet-only) behaviors
Decisions Made
See key-decisions in frontmatter for the full list. Highlights:
- Cache placement (mesh.refreshAll/transport.fetchStatus): the
useCachedResource()call itself lives inMesh.vue, not insidestores/mesh.ts/stores/transport.ts, even though it wraps those stores' own actions. Reason, verified against Vue's source (node_modules/@vue/runtime-core/dist/runtime-core.cjs.js'sinjectHook): Pinia'sdefineStore(id, setup)runs its setup function inside a bareeffectScope(), not a real component instance (currentInstanceisnull), soonActivated()called from inside a Pinia store setup is a documented Vue no-op (dev warning only, never throws) — it would compile but would never actually revalidate anything on tab reactivation. Mesh.vue is the one call site that legitimately owns the KeepAlive/component lifecycle. The fetchers still literally re-invokemesh.refreshAll()/transport.fetchStatus()unchanged, so those store actions' other callers (clearAllMesh(), andWeb5SendReceiveModals.vue's pre-send mesh-only check, which must never read a TTL-gated cache before moving money) keep their existing guaranteed-fresh behavior untouched. - MeshMap.vue added to files_modified beyond the plan's literal list (
Mesh.vue,mesh.ts,transport.ts,meshTabCache.test.ts) — the Leaflet map instance, its window listener, and itsResizeObserverall live inMeshMap.vue, a child component<MeshMap v-if="showMapPanel">inside Mesh.vue's template. Fixing the map's activate/deactivate correctness structurally requires editing where the instance lives; this is a minimal, in-spirit, non-architectural addition (Rule 3 auto-fix — same class of judgment call 02-02 made when it addeddashboardViewWrappers.tsoutside its own original file list). - Test file split:
meshMapLifecycle.test.tsis a new, separate file rather than appended tomeshTabCache.test.ts, because itsvi.mock('@/stores/mesh', ...)(a minimal plain-object stub) andvi.mock('leaflet', ...)are hoisted to the top of whichever file they're declared in by vitest/esbuild, and would clobbermeshTabCache.test.ts's need for the realmesh/transport/resourcesPinia stores (needed so the six-group cache/dedup/persist logic under test is genuinely exercised, not stubbed away). This mirrors the exact precedent 02-02 set withMarketplaceRefresh.test.tsfor the same class ofvi.mock-hoisting conflict. - Persist/TTL table: see Accomplishments above — every group carrying this node's own DID/onion or any peer identity payload (peers, federation nodes, contacts/aliases) is
persist: false; onlytransport.status(an aggregate, no identity fields) persists, matching the plan's own explicit carve-out. - FLAGGED (not auto-backstopped): the D3 force-simulation premise from RESEARCH.md is factually incorrect for the current codebase state. This was verified by grepping the entire
neode-ui/srctree (not justMesh.vue) ford3,forceSimulation, andsimulation— the only hits belong tosrc/components/federation/NetworkMap.vue, imported exclusively byFederation.vue. Mesh.vue's only graphics context is the Leaflet map (MeshMap.vue). This is surfaced here for human review per the assumptions_and_flagged_items convention rather than silently reinterpreting the task; the real, testable Leaflet-lifecycle work (which the plan also required) was implemented and covered in full.
Deviations from Plan
Auto-fixed Issues
1. [Rule 3 - Blocking] MeshMap.vue added to files_modified to fix the Leaflet map's lifecycle
- Found during: Task 2, tracing where the Leaflet instance/listener/observer actually live
- Issue: The plan's
files_modifiedfor this task only listsMesh.vueandmeshTabCache.test.ts, but the Leaflet map, its window resize listener, and itsResizeObserverall live in the child componentMeshMap.vue(<MeshMap v-if="showMapPanel">in Mesh.vue's template) — Mesh.vue itself has no direct graphics-context code to edit. - Fix: Extended scope to
neode-ui/src/components/MeshMap.vue(addedarmMapVisibility/disarmMapVisibility, dualonMounted/onActivatedregistration,onDeactivatedteardown) and a new companion test fileneode-ui/src/components/__tests__/meshMapLifecycle.test.ts. - Files modified:
neode-ui/src/components/MeshMap.vue,neode-ui/src/components/__tests__/meshMapLifecycle.test.ts(new) - Verification: 4 new tests pass (one map instance across 3 cycles, invalidateSize on activate, listener add/remove counts, ResizeObserver connect/disconnect counts); full suite, type-check, build all green.
- Committed in:
abdfa07a(Task 2 commit)
2. [Rule 1 - Bug in own test authoring, caught before commit] meshMapLifecycle.test.ts's listener-count test needed a mount-time baseline
- Found during: Task 2, first test run
- Issue:
armMapVisibility()'s idempotent remove-then-add idiom meanswindow.removeEventListener('resize', ...)is also called once during the very first mount (not just on deactivate) — the test's naive assertion (expect(removeSpy...).toBe(1)after the first deactivate) was off by one. - Fix: Baseline both
addSpy/removeSpycounts immediately after mount, then assert relative increments across the deactivate/activate cycle. - Files modified:
neode-ui/src/components/__tests__/meshMapLifecycle.test.ts - Verification: Test passes; the underlying implementation was correct all along, only the test's assumption was wrong.
- Committed in:
abdfa07a(Task 2 commit)
3. [Rule 1 - Bug in own test authoring, caught before commit] meshTabCache.test.ts's dedup:true assertion needed to scope to the six groups' own methods
- Found during: Task 1, first test run
- Issue:
refreshOutboxCount()(called from the fan-out's.then(), not one of the six cached groups) issues anrpcClient.call({method: 'mesh.outbox'})withoutdedup: true— a blanket "every captured rpcClient.call has dedup:true" assertion incorrectly failed on this unrelated, out-of-scope call. - Fix: Restricted the assertion to the six groups' own RPC methods (
mesh.status,mesh.peers,mesh.messages,mesh.deadman-status,mesh.block-headers,transport.status). - Files modified:
neode-ui/src/views/__tests__/meshTabCache.test.ts - Verification: Test passes; confirms all six groups' fetchers (and no others) are asserted against dedup:true.
- Committed in:
31389bcc(Task 1 commit)
Total deviations: 3 (1 Rule 3 scope extension necessary to fulfill the task's literal requirement; 2 Rule 1 fixes to the test file's own assertions, caught and corrected before either commit landed — no production-code bugs found) Impact on plan: The MeshMap.vue extension is the only deviation with lasting scope impact, and it is narrowly targeted (lifecycle hooks only, no restructuring, no visual change) and fully test-covered. No scope creep beyond what Task 2's literal must_haves required.
Known Stubs
None — no stub data, placeholder text, or unwired data sources were introduced. Every cached group's fetcher performs a real RPC round-trip through the existing store/view functions; nothing renders hardcoded empty/mock data.
Threat Flags
None beyond what the plan's own <threat_model> already anticipated (T-02-01, T-02-03, T-02-13, T-02-16) — no new network endpoints, auth paths, or trust-boundary-crossing surface was introduced by this plan.
Issues Encountered
- The D3-force-simulation premise mismatch (see Decisions Made) — resolved by verifying via grep and treating the affected truths as vacuously satisfied, with the finding surfaced prominently here for human review rather than silently reinterpreting the task's scope.
- Two test-authoring bugs in the new test files themselves (both fixed before either commit — see Deviations 2 and 3 above); no production-code bugs were found during this plan.
User Setup Required
None - no external service configuration required.
Next Phase Readiness
- Mesh's six fetch groups and its Leaflet map lifecycle are now on the same cached/activate-deactivate architecture as every other main tab (02-02/02-04's tracer + lifecycle-audit foundation extended to the heaviest remaining tab).
- The
refreshMeshGroupIfStale/Promise.allSettledpattern (and the "useCachedResource must be hosted at a real component, not inside a Pinia store setup" finding) is available for 02-06 (Server/Home) if either store owns a fetch action with other callers needing a guaranteed-fresh read. - For 02-08 (on-device verification): heap-growth across many Mesh tab cycles is a property no jsdom unit test can settle —
meshMapLifecycle.test.tsproves exactly one Leaflet map instance is constructed across repeated activate/deactivate cycles in a synthetic harness, but real browser memory tooling on archi-dev-box is where the D-03 bounded-memory claim gets its final check, per the plan's ownverification: backstopmarker. - Flag carried forward: if a future audit finds Mesh.vue (or any other main tab) genuinely does need a D3-based visualization (e.g., if
HopVizModal.vue's message-hop graphic is later rebuilt with D3), re-open this finding — the current absence was verified for the codebase state as of this plan's execution, not asserted as a permanent architectural constraint. - No blockers for 02-06/02-07.
Phase: 02-ui-performance Completed: 2026-07-30