Skip to content

refactor(joint-react): immutable lazy Map-backed cells container (O(1) commits)#3423

Open
samuelgja wants to merge 1 commit into
clientIO:masterfrom
samuelgja:chore/joint-react-state-update
Open

refactor(joint-react): immutable lazy Map-backed cells container (O(1) commits)#3423
samuelgja wants to merge 1 commit into
clientIO:masterfrom
samuelgja:chore/joint-react-state-update

Conversation

@samuelgja

Copy link
Copy Markdown
Contributor

Description

Rewrites the @joint/react cells container from a mutable array-of-immutable-items to a lazy, memoised, Map-backed immutable snapshot, and rewires the reactive layer around it.

Container (state-container.ts)

  • Map<CellId, Cell> is the single source of truth: get(id) / has(id) / getSize() are O(1).
  • getSnapshot() builds Array.from(byId.values()) lazily and memoises it; batchSet(changeSet) mutates only the Map and invalidates the memo. Commits are O(change-set), never O(n) — no array copy per commit. The first consumer to read after a commit builds the array once; every other consumer shares that reference (so React never tears). If nothing reads it (a drag with only per-id subscribers), it is never built.
  • getIds() is a separately-memoised id list, invalidated only on add/remove — so "which cells exist" readers do no work during a drag.
  • API surface shrank: removed set / delete / reset / commitChanges / getVersion / subscribeToSize and the indexById / items bookkeeping.

Reactive layer

  • graph-projection accumulates one { added, changed, removed } change set per commit and flushes via batchSet, preserving the existing deferCommit / batch timing.
  • Consumers drop their [...getAll()] copies and hold the immutable reference (onCellsChange; useGraphTransaction rollback: O(n) copy → O(1) ref-hold).
  • useContainerKeys deleted; the portal id-list uses a small memoised useCellIds(). The hand-rolled useSyncExternalStore cell subscriptions (useElementDataSnapshot, useUnresolvedElement, LinkItem) now reuse useCells.
  • useCells uses a per-id snapshot token, so single-cell forms only re-read when that cell changes.

Motivation and Context

The container previously handed out a live mutable array, forcing every consumer into a defensive [...getAll()] copy. The goal was a fully immutable snapshot (stable refs consumers can hand straight to React state, no copies) without regressing the O(1) drag hot path — a naive "rebuild the array every commit" immutable design regresses to O(n)/commit; the lazy memoised design avoids that.

Benchmarks (back-to-back on one machine, old vs new)

  • Container commit (isolated, real container): O(1)batchSet is flat at ~0.4µs from 100 → 100,000 cells. At 1,000,000 nodes, updating 50 cells = 11µs (O(change-set), independent of graph size).
  • React uncontrolled drag (GraphProvider, N=1000, memoised element components): 890µs → 439µs (~2× faster).
  • React controlled drag: dominated by the onCellsChange full-array re-push → graph.syncCells(N), which is inherently O(N) and equal old-vs-new (within jsdom noise). Controlled apps that consume onIncrementalCellsChange (the delta) stay O(1).
  • Fan-out: dragging one cell re-renders only that cell, not the other N-1 — in both modes, both versions.

Net: immutable + simpler + uncontrolled ~2× faster + container commits O(1), no regression.

Correctness

  • Adversarial review caught + fixed a real bug (test-first): a new link swept into the changed bucket left getIds() stale, so its portal would not render until the next structural commit (getSnapshot / getIds tearing).
  • New tests: container memoisation + multi-consumer snapshot consistency (no tearing), real-case render-count contracts (a drag re-renders only the dragged cell), a connected-links regression, and rewritten container / drag benches.
  • @joint/react: 1017 tests · @joint/react-plus: 235 tests · typecheck + lint + knip green.

Screenshots (if appropriate):

N/A (headless library).

…erformance and clarity

- Introduced a pending change set in graph projection to batch updates and reduce unnecessary re-renders.
- Replaced direct cell manipulation with staged writes and removals to optimize incremental updates.
- Updated the container interface to support batch updates, improving efficiency during state changes.
- Refactored subscription methods for better clarity and performance.
- Adjusted the way cells are synchronized from the graph to ensure consistency and reduce overhead.
- Cleaned up code formatting and improved readability in various components.
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.

2 participants