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-crdtdirectly, - avoids
LoroTree, - stores normalized CVL state in deterministic root containers,
- exposes the shared benchmark operation surface,
- includes small cached
node(id)andgroup(id)refs, - is wired into
@cv/cvl-engine-benchas--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-benchas--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.