Skip to content

feat: lazy-load node descriptions for md-folder documents - #82

Merged
tomasz-zajac-oss merged 1 commit into
mainfrom
feature/lazy-md-node-bodies
Sep 23, 2026
Merged

tomasz-zajac-oss merged 1 commit into
mainfrom
feature/lazy-md-node-bodies

Conversation

@tomasz-zajac-oss

Copy link
Copy Markdown
Collaborator

Summary

  • Second step (after fix: stop deep-cloning the model for snapshots, undo history, and slides #81) of scaling the model to very large sizes: the md-folder backend (one .md file per node) read and parsed every node's full body into memory the instant a document was opened and kept it there for the whole session, regardless of how much of the model was actually looked at.
  • deserializeFromMdFolder gains a lazy option — node metadata (frontmatter) still loads eagerly, but bodies are left unset and their file paths returned separately (bodyPaths). diagramStore tracks pending nodes (pendingBodyNodeIds); hydrateNode fetches one node's body on demand, wired to the properties panel opening a node, via a new single-file read path for both Electron (reuses the existing file:read IPC) and web (new readOneFileFromHandle in webFolder.ts).
  • Nothing in the app tolerated a partial model before this — the Elements panel search and Quick Search (Cmd/Ctrl+P) both scan every node's description synchronously. Rather than build a permanent in-memory search index (which would just duplicate the same text and give up most of the memory win), instant search keeps working as-is on label/type/technology and simply won't match an unopened node's body — degraded, not broken. A new explicit "Search descriptions" action does an opt-in scan of just the pending nodes; matches get merged into the live model (instant to reopen), non-matches are read and discarded.
  • Correctness hazard this had to avoid: saveDocument writes every node's description back to disk on every save, so serializing an unhydrated node's description (undefined) verbatim would silently wipe its saved content on the very next save. Rather than a riskier byte-patching write path, saveDocument transiently re-reads any still-pending node's file just to build the write payload — never merged into the live model, so a save can't defeat the laziness, and the serialize/write code path itself is unchanged.
  • Scope is the md document source only — fs (single JSON file) and ls (localStorage) are all-or-nothing blobs with nothing to split.
  • Known, deliberate follow-up (not fixed here): ai/queryLanguage.ts still sees undefined descriptions for unhydrated nodes on a lazy doc.

Test plan

  • npm run typecheck passes
  • npm run test — 379/379 tests pass, including:
    • tests/lazyNodeHydration.test.ts (new) — loadDiagram marks pending nodes correctly, hydrateNode merges + clears pending without touching undo, scanDescriptions finds matches in unopened nodes and discards non-matches
    • tests/mdFolderDocument.test.ts — updated round-trip test plus a new lazy round-trip safety test: load lazily, save without ever hydrating a node, reload, confirm its description survived
    • tests/mdFolderPersistence.test.ts — updated for deserializeFromMdFolder's new { data, bodyPaths } return shape
  • Manual smoke test via npm run dev:web on the default (non-lazy, ls) sample model: confirmed zero visual/behavioral regression — description renders normally (no "Loading…" placeholder), no stray "Search descriptions" affordance appears, no console errors. (The native "Open folder" OS dialog can't be driven by headless automation, so the md-folder lazy-load path itself is covered by the integration tests above rather than an end-to-end UI run.)

🤖 Generated with Claude Code

The md-folder backend (one .md file per node) read and parsed every node's
full body into memory the instant a document was opened, and kept it there
for the whole session — for a very large model, that's the dominant memory
cost, paid up front regardless of how much of the model the user actually
looks at. Second step (after #81) of scaling the model to very large sizes.

deserializeFromMdFolder gains a `lazy` option: node metadata (frontmatter)
still loads eagerly as before, but bodies are left unset and their file
paths returned separately (bodyPaths) instead. diagramStore tracks which
nodes are still pending (pendingBodyNodeIds) and hydrateNode fetches one
node's body on demand (wired to the properties panel opening a node) via a
new single-file read path for both Electron (reusing the existing file:read
IPC) and web (a new readOneFileFromHandle in webFolder.ts).

Nothing in the app tolerated a partially-loaded model before this: the
Elements panel search and Quick Search (Cmd/Ctrl+P) both scan every node's
description synchronously. Rather than build a permanent in-memory search
index (which would just duplicate the same text and give up most of the
memory win), the existing instant search keeps working as-is on label/type/
technology (already resident) and simply won't match text in an unopened
node's body — degraded, not broken. A new explicit "Search descriptions"
action (scanDescriptions) does an opt-in scan of only the still-pending
nodes for a query; matches get merged into the live model so opening them
next is instant, non-matches are read and discarded rather than cached.

The one correctness hazard this had to avoid: saveDocument always writes
every node's description back to disk, so serializing an unhydrated node's
description (undefined) verbatim would silently wipe its saved content on
the next save. Rather than a riskier byte-patching write path, saveDocument
transiently re-reads any still-pending node's file just for the purposes of
building the write payload — never merged into the live model, so a save
can't defeat the laziness, and the serialize/write code path itself is
completely unchanged.

Scope is the `md` document source only — `fs` (single JSON file) and `ls`
(localStorage) are inherently all-or-nothing blobs with nothing to split.
The AI query layer (ai/queryLanguage.ts) still sees undefined descriptions
for unhydrated nodes on a lazy doc; that's a known, deliberately deferred
follow-up rather than something fixed here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@tomasz-zajac-oss
tomasz-zajac-oss merged commit 2c1d9ec into main Sep 23, 2026
2 checks passed
@tomasz-zajac-oss
tomasz-zajac-oss deleted the feature/lazy-md-node-bodies branch September 23, 2026 16:34
tomasz-zajac-oss pushed a commit that referenced this pull request Sep 23, 2026
deserializeFromMdFolder now returns { data, bodyPaths? } since #82.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant