Skip to content

fix(core): drop doc events targeting schema.Ignore fields - #97

Merged
zxch3n merged 3 commits into
mainfrom
fix/ignore-root-events
Sep 10, 2026
Merged

zxch3n merged 3 commits into
mainfrom
fix/ignore-root-events

Conversation

@zxch3n

@zxch3n zxch3n commented Sep 3, 2026

Copy link
Copy Markdown
Member

Problem

A root field declared schema.Ignore() was honored by buildRootStateSnapshot (the initial snapshot skips it) but not by the incremental event path. A doc event under an ignored root (event.path[0] === key, or event.target resolving to the ignored root container) was still normalized, its containers registered, and applyEventBatchToState materialized the delta into state. Over a streaming session an "ignored" list gets rebuilt item by item as a partial array with wrong indices, and the registry grows with every container.

Downstream (Lody) currently works around this by monkeypatching the private normalizeLoroEventBatch / registerContainersFromLoroEvent methods on the Mirror instance to drop those events.

Fix

Mirror now filters such events up front, before any registration/application, in:

  • handleLoroEvent (remote/local doc subscription path)
  • applyLocalLoroChanges (captureLocalDocEvent consumer)
  • finalizeEphemeralPatches

An event is dropped when:

  • event.path[0] is an ignored root key, or
  • the path resolves (structurally, via getChildSchema) to a nested ignore map field at any depth, or
  • the path is unusable and event.target's root container (via the parent() chain + inverted doc.getShallowValue()) maps to an ignored root key.

Semantics:

  • Dropped events cause no state change, no container registration, no subscriber notification.
  • A batch mixing ignored and non-ignored events applies the kept part and notifies once.
  • Fast path: schemas without any Ignore field return the batch unchanged (zero overhead).
  • setState writes to ignored keys stay memory-only — unchanged (the diff already skipped them).

Typing

RootSchemaDefinition now accepts IgnoreSchemaType at the root via the new exported RootFieldSchemaType = ContainerSchemaType | IgnoreSchemaType, so a root schema.Ignore() no longer needs a cast. InferType of the field is unknown.

Tests

New packages/core/tests/ignore-root-events.test.ts:

  1. Root history: schema.Ignore() + peer appending items to history → state has no history, registry unchanged, subscriber not called.
  2. One commit mixing a history append and a session map set → state updates session only, subscriber called exactly once, only session registered.
  3. setState writing to the ignored key stays memory-only (state has the value, doc.toJSON() does not).
  4. Nested Ignore map field: events inside it are dropped (no state/registry/notify), while the parent-path creation event is still applied (unchanged pre-existing semantics).

Plus a type test in inferType.test-d.ts proving a root schema.Ignore() compiles without a cast.

Full suite: 528 tests pass, lint/typecheck clean.

Published loro-crdt 1.16.0 follow-up

Fixed the nested Ignore consistency regression: the comparison snapshot now carries the in-memory Ignore value, excluding both ignored document values and their container identities from comparison. Ordinary sibling values and identities remain checked. The regression fails before the fix and passes after it, including a normal setState after a committed ignored-list append. Updated core/react/jotai loro-crdt requirements and lockfile to ^1.16.0 / 1.16.0. Build, 529 tests, lint (0 errors), and typecheck pass.

A root field declared schema.Ignore() was honored at initialization but
not by the incremental event path: doc events under an ignored root were
still normalized, their containers registered, and their deltas
materialized into state — rebuilding a streaming ignored list item by
item with wrong indices and growing the container registry with every
container. Consumers had to monkeypatch private Mirror methods
(normalizeLoroEventBatch / registerContainersFromLoroEvent) to drop
those events.

Mirror now filters such events up front in handleLoroEvent and in the
local capture paths (applyLocalLoroChanges, finalizeEphemeralPatches):
events whose path[0] is an ignored root key, whose target's root
container maps to one, or whose path resolves to a nested Ignore map
field are dropped before registration/application. A batch containing
only ignored events causes no state change and no subscriber
notification; a mixed batch notifies once for the kept part.

setState writes to ignored keys remain memory-only, unchanged.

Also widens RootSchemaDefinition (new RootFieldSchemaType) so a root
schema.Ignore() no longer needs a cast.
@zxch3n
zxch3n merged commit 217012a into main Sep 10, 2026
1 check passed
@github-actions github-actions Bot mentioned this pull request Sep 10, 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