From 242ea5d4259fcc1d83ba3c1e824dd5757107c806 Mon Sep 17 00:00:00 2001 From: Roman Claudiu Date: Sat, 7 Nov 2026 16:41:29 +0200 Subject: [PATCH] fix: emit replace patches for indices dropped by shrinking length Assigning a smaller `length` to a draft array truncated `copy_` without marking the dropped indices as assigned. If the array then grew back to at least its base length, generateArrayPatches skipped those indices, so the patches did not replay to the result and the inverse patches did not restore the base. Mark the dropped base indices as assigned in the array set trap, as deleting them one by one already does. Fixes #1355 --- __tests__/patch.js | 68 ++++++++++++++++++++++++++++++++++++++++++++++ src/core/proxy.ts | 16 ++++++++++- 2 files changed, 83 insertions(+), 1 deletion(-) 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.