fix(#290 review): drop stale cached children on boot + gate size warn (F1/F2/F3)

F1 (MEDIUM regression): a collapsed-but-cached branch showed STALE children on
re-expand after reload (the cache keeps children of any ever-expanded branch;
refreshOpenBranches only refreshes OPEN branches, but the fetch guard skips a branch
that has cached children). New pruneCollapsedChildren(tree, openIds) resets children
to [] (keeps hasChildren) for every node NOT in the persisted open-set, recursing
into open nodes — a once-per-mount boot effect. A pruned collapsed branch is then the
'unloaded' shape handleToggle re-fetches, so its first expand reconciles fresh (as
pre-cache). Open branches keep their children (refreshOpenBranches handles them, no
double fetch). Test: a collapsed cached branch with a stale child fetches fresh on
first expand after boot.
F2: gate the >4MB size-guard console.warn behind the writeFailureWarned once-flag
(like the quota branch) so editing a huge tree no longer re-warns every ~500ms; test
that an oversized tree is not persisted + warns exactly once.
F3: narrow the use-auth privacy comment (only tree caches are swept; other
localStorage entries remain).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
agent_coder
2026-07-02 22:25:05 +03:00
parent b349676eae
commit e9d5d493d3
6 changed files with 317 additions and 3 deletions
@@ -293,6 +293,41 @@ export function loadedOpenBranchIds(
return ids;
}
/**
* Boot-cache hygiene (#159 #8): the persisted tree keeps the children of EVERY
* branch ever expanded — collapsing a branch never prunes them. So on reload a
* COLLAPSED branch hydrates with its old cached children, and `handleToggle`
* skips the lazy-load on first expand (children already present) → it shows
* STALE children (renamed / moved / deleted while the user was offline) with no
* reconcile. `refreshOpenBranches` only refreshes OPEN branches, so collapsed
* ones slip through.
*
* Fix: drop the cached children of every node NOT in the persisted open-set,
* resetting it to the canonical UNLOADED shape (`children: []`, `hasChildren`
* untouched — see pageToTreeNode). Its first expand then lazy-loads fresh, just
* as it did before the tree was cached to localStorage. OPEN branches keep
* their children (refreshOpenBranches reconciles those, so they must not be
* dropped here) and are recursed into so a collapsed branch nested under an
* open one is pruned too.
*/
export function pruneCollapsedChildren(
tree: SpaceTreeNode[],
openIds: ReadonlySet<string>,
): SpaceTreeNode[] {
return tree.map((node) => {
const hasLoadedChildren = !!node.children && node.children.length > 0;
if (!openIds.has(node.id)) {
// Collapsed: drop the whole cached subtree so it reads as unloaded.
return hasLoadedChildren ? { ...node, children: [] } : node;
}
// Open: keep it, but recurse into its children (a nested collapsed branch
// must still be pruned).
return hasLoadedChildren
? { ...node, children: pruneCollapsedChildren(node.children, openIds) }
: node;
});
}
// Collect every node id in the tree (roots, branches, leaves). Used by
// collapseAll to clear the open-state map for all current-space nodes.
export function collectAllIds(nodes: SpaceTreeNode[]): string[] {