plans/20260306-merge-loro-crates.md
crates/loro and crates/loro-internal into One CrateDate: 2026-03-06
Status: Draft
Primary package target: loro
Compatibility stance: Compatibility-first by default, with explicit decision points for semver-breaking cleanup
Not Started, In Progress, Blocked, or Done.Done when all exit criteria are satisfied.Today the Rust implementation is split across two crates:
crates/loro: the public, documented, stable-facing facade.crates/loro-internal: the engine crate that contains most of the implementation.This split currently creates two distinct problems:
The highest-cost conversions are not the outer LoroDoc wrapper by itself. The main overhead lives in:
crates/loro/src/event.rsValueOrHandler to ValueOrContainer conversionsDiff and DiffBatch re-materializationThe split also creates long-term maintenance cost:
crates/loro-wasm depend directly on low-level internal APIsAs a result, physically moving files without collapsing the duplicate type layers would reduce organizational complexity, but it would not remove the main runtime overhead we care about.
This plan aims to make loro the only published Rust crate for the core implementation while preserving correctness and keeping migration risk controlled.
The target state is:
loroLoroDocloro-internalcrates/loro and crates/loro-internal into one canonical Rust crate.loro::LoroDoc::new() must preserve current auto-commit behavior unless an explicit public API decision says otherwise.loro-wasm pending-event flush invariant must remain valid.crates/loro no longer depends on loro-internal.loro-internal.DiffEvent reconstruction on first-party hot paths.ValueOrHandler to ValueOrContainer conversion on canonical APIs.crates/loro/tests remains a valid public compatibility suite.loro, not with a hidden engine crate.crates/loro/src/lib.rs wraps InnerLoroDoc and internal handlers with public facade types.crates/loro/src/event.rs reconstructs event, diff, and batch types from internal representations.crates/loro-internal/src/lib.rs exposes far more engine surface than should become a long-term public contract.crates/loro-wasm imports many low-level items from loro-internal, so the merge must include a consumer migration plan, not only a crate move.| Phase | Name | Status | Depends On | Main Output |
|---|---|---|---|---|
| 0 | Lock behavior and perf baseline | Not Started | None | Baseline tests and benchmark numbers |
| 1 | Import engine into loro | Not Started | Phase 0 | loro owns implementation modules |
| 2 | Merge canonical LoroDoc semantics | Not Started | Phase 1 | One canonical document type |
| 3 | Collapse container and value surface | Not Started | Phase 2 | One canonical container/value layer |
| 4 | Collapse event, diff, and undo surface | Not Started | Phase 3 | One canonical event/diff/undo layer |
| 5 | Migrate loro-wasm and first-party consumers | Not Started | Phase 4 | No first-party dependency on loro-internal |
| 6 | Remove shim and finalize cleanup | Not Started | Phase 5 | Single-crate steady state |
Status: Not Started
Freeze the current public behavior and record baseline performance before changing crate boundaries.
Without a baseline, later phases can accidentally change semantics while still compiling cleanly. This phase turns the current behavior into an explicit contract.
crates/lorocrates/loro-internalcrates/loro.crates/loro/tests as the public compatibility suite.crates/loro-internal/tests that must remain green throughout the migration.cargo test -p lorocargo test -p loro-internalcargo bench -p loro-internal eventcargo bench -p loro-internal pendingcargo bench -p loro-internal listloroStatus: Not Started
Make loro own the implementation modules while preserving the current public behavior.
The recommended strategy is to move the implementation into crates/loro first, then reduce duplicate surfaces in later phases. This keeps the published crate name stable while avoiding a large semantic rewrite in the same step.
crates/loro/Cargo.tomlcrates/loro/src/**crates/loro-internal/Cargo.tomlcrates/loro-internal/src/**crates/loro to host the current engine implementation.crates/loro and crates/loro-internal.loro feature contract.crates/loro compile against the local engine modules instead of the path dependency on loro-internal.crates/loro-internal into a temporary compatibility shim that re-exports from loro.loro builds without a path dependency on loro-internalloro-internal still exists as a temporary forwarding cratecargo tree -p loro no longer shows loro-internal as a dependency edge.loro and remain green.cargo test -p lorocargo test -p loro-internalloroLoroDoc SemanticsStatus: Not Started
Remove the outer LoroDoc { doc: InnerLoroDoc } split and make one canonical LoroDoc type own the merged behavior.
fork, fork_at, from_snapshotdoc() behaviorinner(), with_oplog, and with_stateLoroDoc.new()from_snapshot()fork_at()doc() returned from attached containersinner() survives, is deprecated, or is replaced by a narrower API.with_oplog and with_state.loro-wasm keep the same behavior.LoroDocLoroDoc is no longer a facade around a second document type.cargo test -p loronew()from_snapshot()fork_at()doc()Status: Not Started
Remove the duplicated container and value layers where the facade currently wraps internal handlers and value enums.
LoroList, LoroMap, LoroText, LoroTree, LoroMovableList, LoroCounterContainerValueOrContainerContainerTraitLoroText as aliases, renamed canonical types, or compatibility wrappers.Container and the internal handler enum into one canonical representation where practical.ValueOrContainer and ValueOrHandler into one canonical representation where practical.ContainerTrait if it only exists to bridge two type layers.for_eachvaluesget_by_pathget_by_str_pathjsonpathloro.cargo test -p loroStatus: Not Started
Remove the current event and diff reconstruction layer, which is likely the highest-value runtime simplification in the merge.
This is the most sensitive API surface. The internal and public event shapes are not identical today, so this phase needs an explicit decision rather than an implicit refactor.
DiffEventContainerDiffDiffDiffBatchBefore implementation starts, choose one of these approaches and record it in the Decision Log:
subscribe remains temporarily as a compatibility wrapper.UndoManager callback payloads to use the canonical event or diff types.DiffEvent::from bridge.cargo test -p loroloro-wasm and Other First-Party ConsumersStatus: Not Started
Remove first-party dependencies on loro-internal and make loro the only crate used by workspace consumers.
crates/loro-wasmloro-internalcrates/loro-wasm imports from loro-internal to loro.loro rather than exposing the entire engine root.loro-internal imports.loro-internalloro-wasm builds against loroloro-internal, except the temporary shim crate itself.loro-wasm behavior remains correct.cargo test -p loro-wasmpnpm -C crates/loro-wasm build-releaseloro_internalloro-wasmStatus: Not Started
Delete the temporary compatibility crate and complete the transition to a true single-crate architecture.
crates/loro-internalcrates/loro-internal.loro crate.loro-internal crate in the workspaceloro-internal.loro-internal references left, except historical changelog textloro-wasm may force a broader internal support surface than desired.loro-internal::LoroDoc::new() as equivalent to the current public loro::LoroDoc::new().loro-wasm?LoroText / LoroList / LoroMap, or should they be renamed around the current internal handler names?test(loro): lock behavior and perf baselinerefactor(loro): import internal engine into public craterefactor(loro): merge canonical LoroDoc semanticsrefactor(loro): collapse container and value surfacerefactor(loro): collapse event, diff, and undo surfacerefactor(wasm): migrate first-party consumers to lororefactor(loro): remove internal shim and finalize cleanupThis plan is complete when all of the following are true:
loro is the only core Rust crate for the implementation.loro-internal has been removed.loro.