Skip to content

fix(core): reject root-level primitives in initialState#83

Merged
zxch3n merged 3 commits into
mainfrom
fix/mirror-root-primitive-error
May 9, 2026
Merged

fix(core): reject root-level primitives in initialState#83
zxch3n merged 3 commits into
mainfrom
fix/mirror-root-primitive-error

Conversation

@lodystage
Copy link
Copy Markdown
Contributor

@lodystage lodystage Bot commented Apr 29, 2026

Summary

  • LoroDoc only stores container types (Map/List/MovableList/Text/Tree) at the document root, but Mirror previously accepted root-level primitives in initialState (e.g. { version: 0 }) and silently kept them in memory while never persisting them to the doc — making mirror.getState() drift from doc.toJSON().
  • Reject primitives at the type level via a new RootInitialValue constraint on initialState, and at runtime with a clear error that points users to wrap the value in a root LoroMap (e.g. a meta map).
  • null/undefined at the root are still treated as "absent" and silently skipped.

Repro (now fails fast)

const doc = new LoroDoc();
new Mirror({ doc, initialState: { version: 0, notes: [] } });
// TS error on `version`; throws at runtime if `as any` is used.

Test plan

  • Added 4 runtime tests in packages/core/tests/mirror.test.ts covering: bare number, bare boolean, primitive under a (mistyped) schema, and null/undefined skip-behavior.
  • Added a type-level test using @ts-expect-error to lock in the TypeScript rejection of root-level number/boolean.
  • All 456 tests pass; 0 type errors.

🤖 Generated with Claude Code

zxch3n and others added 3 commits April 29, 2026 20:42
LoroDoc only stores container types (Map/List/MovableList/Text/Tree)
at the document root, but Mirror previously accepted root-level
primitives (e.g. `{ version: 0 }`) in initialState and silently kept
them in memory while never writing them to the doc. This caused
`mirror.getState()` to drift from `doc.toJSON()`.

Now reject primitives both at compile time (via a `RootInitialValue`
constraint on `initialState`) and at runtime (clear error pointing
users to wrap the value in a root LoroMap). `null`/`undefined` are
still accepted as "absent".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The jotai LoroMirrorAtomConfig and react UseLoroStoreOptions
redeclare initialState with a looser type than MirrorOptions, which
broke the build after the core constraint was tightened. Mirror the
RootInitialValue intersection in both wrappers and re-export the
type from loro-mirror so consumers can reuse it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@zxch3n zxch3n merged commit 7a521bc into main May 9, 2026
1 check passed
@github-actions github-actions Bot mentioned this pull request May 9, 2026
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