diff --git a/.changeset/fix-svelte-query-shrink-array.md b/.changeset/fix-svelte-query-shrink-array.md new file mode 100644 index 00000000000..ee6ca8ff28f --- /dev/null +++ b/.changeset/fix-svelte-query-shrink-array.md @@ -0,0 +1,5 @@ +--- +'@tanstack/svelte-query': patch +--- + +Fix `createQueries` crashing with `TypeError: 'deleteProperty' on proxy: trap returned falsish for property 'N'` when two or more items were removed from its reactive array in the same update. diff --git a/packages/svelte-query/src/containers.svelte.ts b/packages/svelte-query/src/containers.svelte.ts index 60d27c68431..379fa0f2bd0 100644 --- a/packages/svelte-query/src/containers.svelte.ts +++ b/packages/svelte-query/src/containers.svelte.ts @@ -80,7 +80,10 @@ export function createRawRef>( } return true } - return false + // Spec semantics: deleting a missing prop is a no-op. Returning false here + // makes `delete` throw in strict mode, which broke #10341 when `update()` + // iterated stale indices over an already-shrunk array. + return true }, }) @@ -88,6 +91,13 @@ export function createRawRef>( const existingKeys = Object.keys(out) const newKeys = Object.keys(newValue) const keysToRemove = existingKeys.filter((key) => !newKeys.includes(key)) + // Arrays: delete in descending index order so each `deleteProperty` trap + // sees the slot it is removing as the current tail (length-- stays valid). + // Forward iteration would shrink the array under our feet and the next + // index would no longer be `in target`, tripping the trap. + if (Array.isArray(newValue)) { + keysToRemove.sort((a, b) => Number(b) - Number(a)) + } for (const key of keysToRemove) { // @ts-expect-error delete out[key] diff --git a/packages/svelte-query/tests/containers.svelte.test.ts b/packages/svelte-query/tests/containers.svelte.test.ts index 3511dbb5b5d..849dc2849c0 100644 --- a/packages/svelte-query/tests/containers.svelte.test.ts +++ b/packages/svelte-query/tests/containers.svelte.test.ts @@ -198,6 +198,24 @@ describe('createRawRef', () => { expect(ref).toEqual([7, 8, 9]) }) + it('should handle shrinking an array by more than one entry at once', () => { + // Regression for #10341: createQueries crashed with + // `TypeError: can't delete property 'N': proxy deleteProperty handler returned false` + // when two or more items were removed from the reactive array in a single update. + const [ref, update] = createRawRef([1, 2, 3, 4, 5]) + + expect(ref).toEqual([1, 2, 3, 4, 5]) + + update([1, 2]) + expect(ref).toEqual([1, 2]) + + update([1, 2, 3, 4]) + expect(ref).toEqual([1, 2, 3, 4]) + + update([]) + expect(ref).toEqual([]) + }) + it('should behave like a regular object when not using `update`', () => { const [ref] = createRawRef>({ a: 1, b: 2 })