Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions src/db-document.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,38 @@ describe.each(backends)("%s DbDocument", ({ name, codec }) => {
expect(toPlain(meta)).toEqual({ a: 1, c: 3 });
});

// A map field's key whose value is null (an imported Jupyter notebook's
// metadata can have one) stays when another key of the map changes: the
// patch carries only what changed, so a peer keeps the null key too.
it("leaves an unchanged null-valued map key out of an update's patch", () => {
const base = codec().fromString(
JSON.stringify({ id: 1, meta: { keep: null, a: 1 } }),
) as unknown as TestDoc;
expect(toPlain(getField(base.getOne({ id: 1 }), "meta"))).toEqual({ keep: null, a: 1 });

const next = base.set({ id: 1, meta: { a: 2 } } as JsMap);
expect(toPlain(getField(next.getOne({ id: 1 }), "meta"))).toEqual({ keep: null, a: 2 });
const patch = base.makePatch(next);
expect(patch).toEqual([1, [{ id: 1, meta: { a: 2 } }]]);
const peer = codec().fromString(base.toString()) as unknown as TestDoc;
const updated = peer.applyPatch(patch);
expect(toPlain(getField(updated.getOne({ id: 1 }), "meta"))).toEqual({ keep: null, a: 2 });
});

it("still sends a removed map key, and a null key that gets a value", () => {
const base = codec().fromString(
JSON.stringify({ id: 1, meta: { keep: null, a: 1 } }),
) as unknown as TestDoc;
const removed = base.set({ id: 1, meta: { a: null } } as JsMap);
expect(base.makePatch(removed)).toEqual([1, [{ id: 1, meta: { a: null } }]]);
expect(
toPlain(getField(base.applyPatch(base.makePatch(removed)).getOne({ id: 1 }), "meta")),
).toEqual({ keep: null });

const valued = base.set({ id: 1, meta: { keep: 5 } } as JsMap);
expect(base.makePatch(valued)).toEqual([1, [{ id: 1, meta: { keep: 5 } }]]);
});

if (name === "immer") {
it("applies map patches without mutating frozen nested objects", () => {
const empty = newDoc();
Expand Down
6 changes: 5 additions & 1 deletion src/db-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ export function toStr(objs: JsMap[]): string {
return lines.join("\n");
}

// The change that turns map obj1 into obj2: changed and added keys with their
// new values, removed keys as null. A key of obj1 that is unchanged is left
// out, even if its value is null: re-sending it as null would delete it from
// every peer that applies the patch.
export function mapMergePatch(obj1: JsMap, obj2: JsMap): JsMap {
const change: JsMap = {};
for (const key of Object.keys(obj1)) {
Expand All @@ -36,7 +40,7 @@ export function mapMergePatch(obj1: JsMap, obj2: JsMap): JsMap {
change[key] = val2 == null ? null : val2;
}
for (const key of Object.keys(obj2)) {
if (obj1[key] != null) continue;
if (Object.prototype.hasOwnProperty.call(obj1, key)) continue;
change[key] = obj2[key];
}
return change;
Expand Down
Loading