diff --git a/__tests__/patch.js b/__tests__/patch.js index c4aff9a6..65a83b73 100644 --- a/__tests__/patch.js +++ b/__tests__/patch.js @@ -790,6 +790,74 @@ runPatchTests( ] ) +// #1355 +runPatchTests( + "arrays - truncate with length, then grow", + {x: [1]}, + d => { + d.x.length = 0 + d.x[1] = 2 + }, + [ + {op: "replace", path: ["x", 0], value: undefined}, + {op: "add", path: ["x", 1], value: 2} + ], + [ + {op: "replace", path: ["x", 0], value: 1}, + {op: "remove", path: ["x", 1]} + ], + {x: [undefined, 2]} +) + +runPatchTests( + "arrays - truncate with length, then restore the length", + {x: [1, 2]}, + d => { + d.x.length = 0 + d.x.length = 2 + }, + [ + {op: "replace", path: ["x", 0], value: undefined}, + {op: "replace", path: ["x", 1], value: undefined} + ], + [ + {op: "replace", path: ["x", 0], value: 1}, + {op: "replace", path: ["x", 1], value: 2} + ], + {x: [undefined, undefined]} +) + +runPatchTests( + "arrays - truncate with length, then push", + {x: [1, 2, 3]}, + d => { + d.x.length = 1 + d.x.push(4, 5) + }, + [ + {op: "replace", path: ["x", 1], value: 4}, + {op: "replace", path: ["x", 2], value: 5} + ], + [ + {op: "replace", path: ["x", 1], value: 2}, + {op: "replace", path: ["x", 2], value: 3} + ], + {x: [1, 4, 5]} +) + +runPatchTests( + "arrays - truncate with length, then grow back to the same values", + {x: [1, 2]}, + d => { + d.x.length = 0 + d.x[0] = 1 + d.x[1] = 2 + }, + [], + [], + {x: [1, 2]} +) + runPatchTests( "arrays - pop twice", {x: [1, 2, 3]}, diff --git a/src/core/proxy.ts b/src/core/proxy.ts index 8a2ee62d..363dfe73 100644 --- a/src/core/proxy.ts +++ b/src/core/proxy.ts @@ -284,7 +284,21 @@ arrayTraps.set = function (state, prop, value) { isNaN(parseInt(prop as any)) ) die(14) - return objectTraps.set!.call(this, state[0], prop, value, state[0]) + const arrayState = state[0] + const oldLength = prop === "length" ? latest(arrayState).length : 0 + const result = objectTraps.set!.call( + this, + arrayState, + prop, + value, + arrayState + ) + // Shrinking `length` drops the tail without going through `deleteProperty`, + // so mark the dropped base indices as assigned. Otherwise, if the array grows + // back, patch generation sees no assignment for them and emits no `replace`. + const end = Math.min(oldLength, arrayState.base_.length) + for (let i = +value; i < end; i++) arrayState.assigned_!.set("" + i, true) + return result } // Access a property without creating an Immer draft.