Skip to content

Commit 2b48f76

Browse files
committed
fix(core): prevent spurious changes for unchanged ""/null in maps
- Use property presence checks (key in obj) in diffMap instead of falsy checks to detect adds/deletes, avoiding treating ""/null as missing. - Preserve strict equality/deepEqual path so unchanged "" and null emit no changes. - Add tests ensuring unchanged empty string and null fields produce zero changes. - No impact on list/movable/text/tree diff behavior.
1 parent 0442154 commit 2b48f76

File tree

2 files changed

+45
-2
lines changed

2 files changed

+45
-2
lines changed

packages/core/src/core/diff.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -998,8 +998,9 @@ export function diffMap<S extends ObjectLike>(
998998
childSchema?.getContainerType() ??
999999
tryInferContainerType(newItem, inferOptions);
10001000

1001-
// added new key
1002-
if (!oldItem) {
1001+
// Added new key: detect by property presence, not truthiness.
1002+
// Using `!oldItem` breaks for valid falsy values like "" or null.
1003+
if (!(key in oldStateObj)) {
10031004
// Inserted a new container
10041005
if (containerType) {
10051006
changes.push({
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { describe, it, expect } from "vitest";
2+
import { LoroDoc } from "loro-crdt";
3+
import { diffContainer } from "../../src/core/diff";
4+
import { schema } from "../../src/schema";
5+
6+
describe("diffMap equality for falsy primitives", () => {
7+
it("does not emit changes when string field stays ''", () => {
8+
const doc = new LoroDoc();
9+
// Ensure the container exists (name must match schema field)
10+
doc.getMap("profile");
11+
12+
const rootSchema = schema({
13+
profile: schema.LoroMap({
14+
bio: schema.String(),
15+
}),
16+
});
17+
18+
const oldState = { profile: { bio: "" } } as const;
19+
const newState = { profile: { bio: "" } } as const;
20+
21+
const changes = diffContainer(doc, oldState, newState, "", rootSchema);
22+
expect(changes.length).toBe(0);
23+
});
24+
25+
it("does not emit changes when field stays null", () => {
26+
const doc = new LoroDoc();
27+
doc.getMap("profile");
28+
29+
const rootSchema = schema({
30+
profile: schema.LoroMap({
31+
note: schema.String(),
32+
}),
33+
});
34+
35+
const oldState = { profile: { note: null } } as const;
36+
const newState = { profile: { note: null } } as const;
37+
38+
const changes = diffContainer(doc, oldState, newState, "", rootSchema);
39+
expect(changes.length).toBe(0);
40+
});
41+
});
42+

0 commit comments

Comments
 (0)