CVL Extended Contender

Purpose

The Loro Extended-style contender should test a specific architecture, not merely another Loro wrapper:

Loro root containers
  -> ID-keyed normalized CVL records
  -> typed CVL facade
  -> selector/read caches
  -> benchmark adapter

This should compete against:

  • @cv/cvl-engine-local: local normalized baseline.
  • @cv/cvl-engine-yjs: Yjs normalized contender.
  • @cv/cvl-engine-loro: current Loro Tree-first contender.

Storage Shape

Avoid canonical nested container lists. Use stable IDs and deterministic root containers:

meta
nodesById
childrenByParent
parentByNode
groupsByParent
groupRecords
groupByMember
selectionByGroup
selectedByParent
metricsByNode
textByNode

This is closer to Loro Extended's mergeable/root-container lesson and closer to CVL's actual domain model.

Public API Target

Keep the same broad engine API as the other contenders:

const engine = createEngine(seed);

engine.applyOp({ kind: "selectMembers", groupId, memberIds });
engine.measureOp(op);
engine.getSelectionIndex();
engine.getRenderProjection();
engine.encodeUpdate();
engine.applyUpdate(update);
engine.toJSON();

Then add a higher-level facade in a later pass:

engine.change(draft => {
  draft.group(groupId).select(memberIds);
  draft.node(nodeId).moveTo(parentId, index);
  draft.node(nodeId).setText(text);
});

engine.selectors.section(sectionId).subscribe(callback);
engine.selectors.group(groupId).read();
engine.selectors.renderProjection().read();

Benchmark Hypothesis

Expected strengths:

  • faster selection index than the current Loro Tree-first contender,
  • lower state bytes than Yjs if records stay compact,
  • render projection close to local baseline after cache warmup,
  • simpler mapping to future MCP/API semantics.

Expected risks:

  • Loro update bytes may still be larger than Yjs for frequent selection edits,
  • root-container maps may need careful cache invalidation,
  • order lists can become noisy if rewritten instead of patched,
  • typed facade code can become too generic if we copy Loro Extended instead of making it CVL-specific.

Current Status

Implemented as @cv/cvl-engine-loro-extended.

The first implementation:

  • uses loro-crdt directly,
  • avoids LoroTree,
  • stores normalized CVL state in deterministic root containers,
  • exposes the shared benchmark operation surface,
  • includes small cached node(id) and group(id) refs,
  • is wired into @cv/cvl-engine-bench as --adapter loro-extended.

Validation captured on 2026-05-06:

npx --yes pnpm@10.33.2 --filter @cv/cvl-engine-loro-extended check
npx --yes pnpm@10.33.2 --filter @cv/cvl-engine-loro-extended test
npx --yes pnpm@10.33.2 --filter @cv/cvl-engine-bench check
npx --yes pnpm@10.33.2 --filter @cv/cvl-engine-bench bench --adapter all --preset tiny --rounds 1

The first four-way tiny benchmark row for this contender:

Adapter Total State bytes Update bytes Selection index Render projection
Loro Extended-style 254.754 ms 255,245 758,992 CRDT update 51.413 ms 1.119 ms

This validates the architectural hypothesis enough to keep it in the competition. It does not yet prove that Loro should replace Yjs in the app.

Done Criteria

  • Exposes the same benchmark adapter contract. Done.
  • Runs in @cv/cvl-engine-bench as --adapter loro-extended. Done.
  • Passes the shared tiny fixture. Done.
  • Has unit coverage for selection groups, moves, text, metrics, and import/export. Done.
  • Produces a benchmark row next to local, Yjs, and Loro Tree-first. Done.

Prototype Result

Command:

npx --yes pnpm@10.33.2 --filter @cv/cvl-engine-bench bench --adapter loro-extended --preset tiny --rounds 1

Fresh tiny run on 2026-05-06:

Adapter Total State bytes Update bytes Selection index Render projection
Loro Extended-style normalized 317.533 ms 255,242 758,992 CRDT update 56.838 ms 1.217 ms

Notes:

  • This contender stores CVL shape in deterministic root containers (nodesById, childrenByParent, parentByNode, groupsByParent, groupRecords, groupByMember, selectionByGroup, selectedByParent, metricsByNode, textByNode) and does not use Loro Tree.
  • The first tiny result has much smaller serialized state than the previous Loro Tree-first row and a substantially faster selection-index path, while update bytes remain high for frequent selection edits.
Source: wiki/content/studies/CRDT/cvl-extended-contender.md