Skip to content

Add support for array mutating methods in proxy change tracking #267

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 16, 2025
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
7 changes: 7 additions & 0 deletions .changeset/tasty-walls-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@tanstack/db": patch
---

• Add proper tracking for array mutating methods (push, pop, shift, unshift, splice, sort, reverse, fill, copyWithin)
• Fix existing array tests that were misleadingly named but didn't actually call the methods they claimed to test
• Add comprehensive test coverage for all supported array mutating methods
24 changes: 24 additions & 0 deletions packages/db/src/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,30 @@ export function createChangeProxy<

// If the value is a function, bind it to the ptarget
if (typeof value === `function`) {
// For Array methods that modify the array
if (Array.isArray(ptarget)) {
const methodName = prop.toString()
const modifyingMethods = new Set([
`pop`,
`push`,
`shift`,
`unshift`,
`splice`,
`sort`,
`reverse`,
`fill`,
`copyWithin`,
])

if (modifyingMethods.has(methodName)) {
return function (...args: Array<unknown>) {
const result = value.apply(changeTracker.copy_, args)
markChanged(changeTracker)
return result
}
}
}

// For Map and Set methods that modify the collection
if (ptarget instanceof Map || ptarget instanceof Set) {
const methodName = prop.toString()
Expand Down
99 changes: 81 additions & 18 deletions packages/db/tests/proxy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -728,9 +728,9 @@ describe(`Proxy Library`, () => {
const objs = [{ items: [`apple`, `banana`, `cherry`] }]
const { proxies, getChanges } = createArrayChangeProxy(objs)

// Create a new array without the last element
// Call pop() method directly
// @ts-expect-error ok possibly undefined
proxies[0].items = proxies[0].items.slice(0, -1)
proxies[0].items.pop()

expect(getChanges()).toEqual([
{
Expand All @@ -745,10 +745,9 @@ describe(`Proxy Library`, () => {
const objs = [{ items: [`apple`, `banana`, `cherry`] }]
const { proxies, getChanges } = createArrayChangeProxy(objs)

// Create a new array without the first element

// Call shift() method directly
// @ts-expect-error ok possibly undefined
proxies[0].items = proxies[0].items.slice(1)
proxies[0].items.shift()

expect(getChanges()).toEqual([
{
Expand All @@ -763,33 +762,38 @@ describe(`Proxy Library`, () => {
const objs = [{ items: [`banana`, `cherry`] }]
const { proxies, getChanges } = createArrayChangeProxy(objs)

// Create a new array with an element added at the beginning
// Call unshift() method directly
// @ts-expect-error ok possibly undefined

proxies[0].items = [`apple`, ...proxies[0].items]
proxies[0].items.unshift(`apple`)

expect(getChanges()).toEqual([
{
items: [`apple`, `banana`, `cherry`],
},
])
// @ts-expect-error ok possibly undefined

expect(objs[0].items).toEqual([`banana`, `cherry`])
})

it(`should track array push() operations`, () => {
const obj = { items: [`apple`, `banana`] }
const { proxy, getChanges } = createChangeProxy(obj)

proxy.items.push(`cherry`)

expect(getChanges()).toEqual({
items: [`apple`, `banana`, `cherry`],
})
expect(obj.items).toEqual([`apple`, `banana`])
})

it(`should track array splice() operations`, () => {
const objs = [{ items: [`apple`, `banana`, `cherry`, `date`] }]
const { proxies, getChanges } = createArrayChangeProxy(objs)

// Create a new array with elements replaced in the middle
// @ts-expect-error ok possibly undefined

const newItems = [...proxies[0].items]
newItems.splice(1, 2, `blueberry`, `cranberry`)
// Call splice() method directly
// @ts-expect-error ok possibly undefined

proxies[0].items = newItems
proxies[0].items.splice(1, 2, `blueberry`, `cranberry`)

expect(getChanges()).toEqual([
{
Expand All @@ -804,9 +808,9 @@ describe(`Proxy Library`, () => {
const objs = [{ items: [`cherry`, `apple`, `banana`] }]
const { proxies, getChanges } = createArrayChangeProxy(objs)

// Create a new sorted array
// Call sort() method directly
// @ts-expect-error ok possibly undefined
proxies[0].items = [...proxies[0].items].sort()
proxies[0].items.sort()

expect(getChanges()).toEqual([
{
Expand All @@ -817,6 +821,65 @@ describe(`Proxy Library`, () => {
expect(objs[0].items).toEqual([`cherry`, `apple`, `banana`])
})

it(`should track array reverse() operations`, () => {
const objs = [{ items: [`apple`, `banana`, `cherry`] }]
const { proxies, getChanges } = createArrayChangeProxy(objs)

// Call reverse() method directly
// @ts-expect-error ok possibly undefined
proxies[0].items.reverse()

expect(getChanges()).toEqual([
{
items: [`cherry`, `banana`, `apple`],
},
])
// @ts-expect-error ok possibly undefined
expect(objs[0].items).toEqual([`apple`, `banana`, `cherry`])
})

it(`should track array fill() operations`, () => {
const objs = [{ items: [`apple`, `banana`, `cherry`] }]
const { proxies, getChanges } = createArrayChangeProxy(objs)

// Call fill() method directly
// @ts-expect-error ok possibly undefined
proxies[0].items.fill(`orange`, 1, 3)

expect(getChanges()).toEqual([
{
items: [`apple`, `orange`, `orange`],
},
])
// @ts-expect-error ok possibly undefined
expect(objs[0].items).toEqual([`apple`, `banana`, `cherry`])
})

it(`should track array copyWithin() operations`, () => {
const objs = [
{ items: [`apple`, `banana`, `cherry`, `date`, `elderberry`] },
]
const { proxies, getChanges } = createArrayChangeProxy(objs)

// Call copyWithin() method directly - copy elements from index 3-4 to index 0-1
// @ts-expect-error ok possibly undefined
proxies[0].items.copyWithin(0, 3, 5)

expect(getChanges()).toEqual([
{
items: [`date`, `elderberry`, `cherry`, `date`, `elderberry`],
},
])
// @ts-expect-error ok possibly undefined
expect(objs[0].items).toEqual([
`apple`,
`banana`,
`cherry`,
`date`,
`elderberry`,
])
})

it(`should track changes in multi-dimensional arrays`, () => {
const objs = [
{
Expand Down