diff --git a/CHANGELOG.md b/CHANGELOG.md index 8839d3a2..70998b4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## 0.68.6 + +- Fixed `applySnapshot` generating no-op patches sometimes. + ## 0.68.5 - Fixed `fromSnapshotProcessor` not accepting null or undefined when a `tProp` with a default value was used. diff --git a/packages/lib/package.json b/packages/lib/package.json index d36a33f3..9cae8889 100755 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -1,6 +1,6 @@ { "name": "mobx-keystone", - "version": "0.68.5", + "version": "0.68.6", "description": "A MobX powered state management solution based on data trees with first class support for TypeScript, snapshots, patches and much more", "keywords": [ "mobx", diff --git a/packages/lib/src/snapshot/reconcileModelSnapshot.ts b/packages/lib/src/snapshot/reconcileModelSnapshot.ts index db569c16..5b98d514 100644 --- a/packages/lib/src/snapshot/reconcileModelSnapshot.ts +++ b/packages/lib/src/snapshot/reconcileModelSnapshot.ts @@ -5,6 +5,7 @@ import { isReservedModelKey, modelIdKey, modelTypeKey } from "../model/metadata" import { isModel, isModelSnapshot } from "../model/utils" import type { ModelClass } from "../modelShared/BaseModelShared" import { getModelInfoForName } from "../modelShared/modelInfo" +import { deepEquals } from "../treeUtils/deepEquals" import { runTypeCheckingAfterChange } from "../tweaker/typeChecking" import { withoutTypeChecking } from "../tweaker/withoutTypeChecking" import { failure, isArray } from "../utils" @@ -49,8 +50,11 @@ function reconcileModelSnapshot( return fromSnapshot(sn) } } else if (isArray(parent)) { - // no id inside an array, no reconciliation possible - return fromSnapshot(sn) + // no id and inside an array? no reconciliation possible, + // unless the snapshots are equivalent (note deep equals will use the snapshot of value auto) + if (!deepEquals(value, sn)) { + return fromSnapshot(sn) + } } const modelObj: AnyModel = value diff --git a/packages/lib/src/snapshot/reconcileSnapshot.ts b/packages/lib/src/snapshot/reconcileSnapshot.ts index 5917cac4..774db02f 100644 --- a/packages/lib/src/snapshot/reconcileSnapshot.ts +++ b/packages/lib/src/snapshot/reconcileSnapshot.ts @@ -4,6 +4,7 @@ import { isModel } from "../model/utils" import { fastGetParentPathIncludingDataObjects } from "../parent" import { failure, isMap, isPrimitive, isSet } from "../utils" import type { ModelPool } from "../utils/ModelPool" +import { getSnapshot } from "./getSnapshot" import { registerDefaultReconcilers } from "./registerDefaultReconcilers" type Reconciler = (value: any, sn: any, modelPool: ModelPool, parent: any) => any | undefined @@ -26,6 +27,12 @@ export function reconcileSnapshot(value: any, sn: any, modelPool: ModelPool, par return sn } + // if the snapshot passed is exactly the same as the current one + // then it is already reconciled + if (getSnapshot(value) === sn) { + return value + } + registerDefaultReconcilers() const reconcilersLen = reconcilers.length diff --git a/packages/lib/test/ref/rootRef.test.ts b/packages/lib/test/ref/rootRef.test.ts index 2ca7d967..4c2ee587 100644 --- a/packages/lib/test/ref/rootRef.test.ts +++ b/packages/lib/test/ref/rootRef.test.ts @@ -22,6 +22,8 @@ import { Ref, rootRef, runUnprotected, + tProp, + types, undoMiddleware, } from "../../src" import { autoDispose } from "../utils" @@ -807,3 +809,34 @@ test("generic typings", () => { // @ts-expect-error genericRef2(new GenericModel({ v1: 1, v2: "2" })) }) + +test("issue #456", () => { + @model("issue #456/Todo") + class Todo extends Model({ + id: idProp, + text: tProp(types.string, ""), + }) {} + + const todoRef = rootRef("issue #456/TodoRef") + + @model("issue #456/TodoList") + class TodoList extends Model({ + todos: tProp(types.array(Todo)), + selectedRef: tProp(types.ref(todoRef)), + selectedRefs: tProp(types.array(types.ref(todoRef))), + }) {} + + const todoList = new TodoList({ + todos: [new Todo({ id: "todo1" })], + selectedRef: todoRef("todo1"), + selectedRefs: [todoRef("todo1")], + }) + + const patches: Patch[][] = [] + + onPatches(todoList, (p) => patches.push(p)) + const sn = getSnapshot(todoList) + applySnapshot(todoList, sn) + + expect(patches).toHaveLength(0) +}) diff --git a/packages/lib/test/snapshot/snapshot.test.ts b/packages/lib/test/snapshot/snapshot.test.ts index 33cb4876..0fea6689 100644 --- a/packages/lib/test/snapshot/snapshot.test.ts +++ b/packages/lib/test/snapshot/snapshot.test.ts @@ -568,7 +568,7 @@ test("id-less reconciliation", () => { let id = 0 @model("idlr/i") - class I extends Model({}) { + class I extends Model({ x: prop(0) }) { id = id++ } @@ -590,15 +590,16 @@ test("id-less reconciliation", () => { applySnapshot( r, modelSnapshotOutWithMetadata(R, { - arr: [modelSnapshotOutWithMetadata(I, {}), modelSnapshotOutWithMetadata(I, {})], - m: modelSnapshotOutWithMetadata(I, {}), + arr: [modelSnapshotOutWithMetadata(I, { x: 0 }), modelSnapshotOutWithMetadata(I, { x: 1 })], + m: modelSnapshotOutWithMetadata(I, { x: 0 }), }) ) expect(r.m.id).toBe(2) - // no reconciliation inside an array - expect(r.arr[0].id).toBe(3) - expect(r.arr[1].id).toBe(4) + expect(r.arr[0].id).toBe(0) + // no reconciliation inside an array if data is different + // (in this case x is 1) + expect(r.arr[1].id).toBe(3) }) test("id-full reconciliation", () => {