Skip to content

perf(core): add container-tree initialization and lazy list views - #98

Merged
zxch3n merged 15 commits into
fix/ignore-root-eventsfrom
perf/structured-read-state
Sep 10, 2026
Merged

zxch3n merged 15 commits into
fix/ignore-root-eventsfrom
perf/structured-read-state

Conversation

@zxch3n

@zxch3n zxch3n commented Sep 5, 2026 •

Copy link
Copy Markdown
Member

Mirror initializes from selected LoroDoc.toContainerTree() roots and exposes opt-in LazyList read views for schema-declared lists. Explicit container types/IDs distinguish containers from opaque Value nodes, avoiding per-parent shallow rereads. Lazy lists retain indexes and hydrate requested item subtrees; writes use mirror.list(path).

This PR includes the bulk foundation formerly reviewed as #95 and the merged #96 LazyList implementation. Its base is #97 (fix/ignore-root-events). Review the complete current diff, including LazyList; #96 is no longer a separate pending stack entry.

The actual dependency is loro-crdt 1.16.0. Older API compatibility paths remain; the documented container-tree nesting-limit error falls back to handle reads. Existing nested insertion/deletion lifecycle and literal-container-ID regressions are covered.

Main has been merged, preserving both #99's single-text-event optimization/benchmark and the bulk/lazy benchmarks. Validation after integration: pnpm check passes (build, 618 tests / 59 files, lint 19 warnings / 0 errors, typecheck).

A separate follow-up fixes nested scalar Ignore leakage, lazy setState guards below eager lists, and lazy Tree data initialization. Those repairs are not included in the validation above. These are library-level checks, not a 3000-round Lody end-to-end acceptance result.

Follow-up repairs: #101.

…call

Mirror initialization previously walked the document container by
container (~10 wasm crossings per container: id/kind/get/keys/toJSON).
buildRootStateSnapshot now reads the whole doc with a single
doc.getDeepValueWithID() call and builds state, $cid markers,
containerRegistry, inferOptionsByContainerId and rootPathById in one JS
walk that replicates registerChildContainer/containerToMirrorState
semantics exactly. No container handles are created during init; trees
and unhandled kinds fall back to the existing handle-based logic, and
the legacy traversal is kept as a fallback for loro-crdt versions
without getDeepValueWithID.

- Defensive cid parsing: tolerates both the Debug-like
  "idx:N, id:cid:..." format and bare container ids.
- loro-crdt <1.15 drops the {value,cid} wrapper for empty containers,
  making them indistinguishable from embedded plain values; the walk
  disambiguates via the parent's cached getShallowValue() (one extra
  call per affected parent only).
- Adds a seeded parity fuzz test (bulk vs legacy: state, $cid via
  cidsEqual, registry, infer options) and a dependency-free benchmark
  (pnpm bench:bulk-init, ~67k-container session-history-shaped doc):
  wasm crossings during init drop from ~527k to 1.
@zxch3n zxch3n changed the title perf(core): initialize Mirror from explicit readState nodes perf(core): initialize Mirror from selected container trees Sep 5, 2026
@zxch3n
zxch3n changed the base branch from perf/bulk-init-deep-value to fix/ignore-root-events September 6, 2026 15:46
zxch3n and others added 6 commits September 9, 2026 20:25
* perf(core): copy paths for single text events

Avoid general Immer drafting for existing text leaves while retaining descriptors and immutable snapshots. Add real-replica parity checks, a deterministic fast-path guard, and a reproducible event-application benchmark.

Model: gpt-5

* perf(core): copy dense array ancestors without defining each property

Validate ordinary array descriptors before direct value copying and retain descriptor fallback for unusual arrays. Move differential assertions outside Wasm callbacks, add array parity and work guards, expand paired benchmarks, and move implementation notes below the quick start.

Model: gpt-5

* fix(core): route array subclasses through general event handling

Check array prototypes during path traversal, remove the redundant dense-copy guard, and verify subclass prototype parity with the general path. Removing the guard now fails the subclass regression.

Model: gpt-5
* feat(core): add lazy list option and LazyList read view

schema.LoroList(item, idSelector, { lazy: { index, maxHydrated?, tailKeep? } })
keeps large lists out of memory: Mirror state holds a stable LazyList
instance at the lazy key instead of an array. Ids come from one
list.getShallowValue() call; the fields named in lazy.index are always
loaded (extracted from the bulk deep-value payload when available, one
map.getShallowValue() per item otherwise); items hydrate on demand via
hydrate(from, to) with LRU eviction (maxHydrated, default 200) that
exempts active subscribeRange windows and the trailing tailKeep items
(default 20). Hydrated data is keyed by item container id, never by
position, so positional views stay correct across concurrent edits.

- Loro events are intercepted before the immutable state walk:
  structural deltas update ids/length/version and shift hydrated slots
  by id; events inside hydrated items reuse applyEventBatchToState
  (structural sharing identical to non-lazy lists); events inside
  non-hydrated items only refresh index fields and bump version.
- setState updates touching a lazy path throw LazyListWriteError and
  point to mirror.list(path); the write API
  (push/insert/deleteById/updateById/updateAt) reuses the existing
  diff/apply machinery so it produces exactly the Loro ops an
  equivalent setState array edit would (round-trip tested).
- Lazy lists are excluded from the ephemeral compose;
  checkStateConsistency verifies lazy paths by instance identity.
- InferType/InferInputType resolve lazy lists to
  LazyList<Item, Partial<Item>>; LazyList, LazyListImpl,
  LazyListWriteError, isLazyList and LazyListWriter are exported.
- React: useLazyRange(list, from, to) hydrates a window, re-renders on
  in-range changes via subscribeRange, and releases on unmount.
- Benchmark (scripts/lazy-list-bench.mjs, 2000 items x ~30 nested
  containers, ~58k containers): lazy init p50 20.5 ms, hydrate(0, 30)
  p50 2.1 ms, eager control (bulk init) p50 117.7 ms.

* fix(core): preserve lazy item batches and hydration lifetimes

Model: gpt-6

* chore: validate lazy lists on loro-crdt 1.16.0

* perf(core): hydrate lazy items with container trees

* fix: preserve nested lazy updates and observe tail growth

* fix(core): normalize inserted lazy items and release deleted subtrees
Keep bulk, lazy-list and single-text-event benchmark commands.

Model: gpt-6
@zxch3n zxch3n changed the title perf(core): initialize Mirror from selected container trees perf(core): add container-tree initialization and lazy list views Sep 10, 2026
@zxch3n
zxch3n merged commit b54548e into fix/ignore-root-events Sep 10, 2026
1 check passed
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