Skip to content

Conversation

@KyleAMathews
Copy link
Collaborator

Previously, modifications to array items retrieved via iteration methods were not tracked by the change proxy because these methods returned raw array elements instead of proxied versions. This caused getChanges() to return an empty object, which in turn caused createOptimisticAction's mutationFn to never be called when using patterns like:

collection.update(id, (draft) => {
  const item = draft.items.find(x => x.id === targetId)
  if (item) {
    item.value = newValue // This change was not tracked!
  }
})

The fix adds proxy handling for array iteration methods similar to how Map/Set iteration is already handled, ensuring that callbacks receive proxied elements and returned elements are properly proxied.

…n-demand

Add comprehensive tests to verify that createOptimisticAction works correctly
with syncMode "on-demand" collections. These tests were created while
investigating a reported bug where mutationFn was not being called when using
on-demand collections.

Tests added:
- Basic on-demand collection with insert
- Collection not started (idle state)
- Collection in loading state (not ready)
- On-demand collection with live query filter
- UPDATE operation on on-demand collection
- Debug test verifying mutations array is populated

All tests pass, suggesting the reported issue may have been fixed by recent
scheduler bug fixes or is specific to a configuration not covered here.
These tests reproduce the bug where modifications to array items retrieved
via iteration methods (find(), forEach, for...of) are not tracked by the
change proxy.

Root cause: When using array iteration methods like find(), the proxy
returns raw array elements instead of proxied versions. This causes
mutations to these elements to not be tracked, resulting in getChanges()
returning an empty object.

This directly causes the createOptimisticAction bug where mutationFn
is never called when modifying nested array items via:
  draft.nested.array.find(...).property = value

Failing tests:
- find() returns unproxied items
- forEach() iterates over unproxied items
- for...of iterates over unproxied items

Working test:
- Direct index access [0] works because the proxy's get handler
  intercepts numeric index access and returns proxied items

The fix requires handling array iteration methods similar to how
Map/Set iteration is already handled in the proxy.
This fixes a bug where modifications to array items retrieved via
iteration methods like find(), forEach(), and for...of were not
being tracked by the change proxy.

The root cause was that array iteration methods were returning raw
array elements instead of proxied versions. When users modified these
raw elements, the changes were not tracked, causing getChanges() to
return an empty object.

This directly caused the createOptimisticAction bug where mutationFn
was never called when using patterns like:
  draft.nested.array.find(...).property = value

The fix adds handling for array iteration methods similar to how
Map/Set iteration is already handled:

- find(), findLast(): Return proxied element when found
- filter(): Return array of proxied elements
- forEach(), map(), flatMap(), some(), every(): Pass proxied elements
  to callbacks so mutations are tracked
- reduce(), reduceRight(): Pass proxied elements to reducer callback
- Symbol.iterator (for...of): Return iterator that yields proxied elements

Fixes the Discord-reported bug where syncMode "on-demand" collections
with createOptimisticAction would have onMutate run but mutationFn
never called.
- Remove "bug" references from proxy test comments
- Remove on-demand syncMode tests from optimistic-action.test.ts that were
  always passing (the actual fix was in proxy.ts array iteration handling)
@changeset-bot
Copy link

changeset-bot bot commented Nov 26, 2025

🦋 Changeset detected

Latest commit: d52f46b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 11 packages
Name Type
@tanstack/db Patch
@tanstack/angular-db Patch
@tanstack/db-collection-e2e Patch
@tanstack/electric-db-collection Patch
@tanstack/powersync-db-collection Patch
@tanstack/react-db Patch
@tanstack/rxdb-db-collection Patch
@tanstack/solid-db Patch
@tanstack/svelte-db Patch
@tanstack/trailbase-db-collection Patch
@tanstack/vue-db Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Cast through 'unknown' first when converting changeTracker.copy_ to
Array<unknown> to satisfy TypeScript's type checking.
@pkg-pr-new
Copy link

pkg-pr-new bot commented Nov 26, 2025

More templates

@tanstack/angular-db

npm i https://pkg.pr.new/@tanstack/angular-db@910

@tanstack/db

npm i https://pkg.pr.new/@tanstack/db@910

@tanstack/db-ivm

npm i https://pkg.pr.new/@tanstack/db-ivm@910

@tanstack/electric-db-collection

npm i https://pkg.pr.new/@tanstack/electric-db-collection@910

@tanstack/offline-transactions

npm i https://pkg.pr.new/@tanstack/offline-transactions@910

@tanstack/powersync-db-collection

npm i https://pkg.pr.new/@tanstack/powersync-db-collection@910

@tanstack/query-db-collection

npm i https://pkg.pr.new/@tanstack/query-db-collection@910

@tanstack/react-db

npm i https://pkg.pr.new/@tanstack/react-db@910

@tanstack/rxdb-db-collection

npm i https://pkg.pr.new/@tanstack/rxdb-db-collection@910

@tanstack/solid-db

npm i https://pkg.pr.new/@tanstack/solid-db@910

@tanstack/svelte-db

npm i https://pkg.pr.new/@tanstack/svelte-db@910

@tanstack/trailbase-db-collection

npm i https://pkg.pr.new/@tanstack/trailbase-db-collection@910

@tanstack/vue-db

npm i https://pkg.pr.new/@tanstack/vue-db@910

commit: d52f46b

@github-actions
Copy link
Contributor

github-actions bot commented Nov 26, 2025

Size Change: +471 B (+0.54%)

Total Size: 86.9 kB

Filename Size Change
./packages/db/dist/esm/proxy.js 3.69 kB +471 B (+14.64%) ⚠️
ℹ️ View Unchanged
Filename Size
./packages/db/dist/esm/collection/change-events.js 1.38 kB
./packages/db/dist/esm/collection/changes.js 977 B
./packages/db/dist/esm/collection/events.js 388 B
./packages/db/dist/esm/collection/index.js 3.24 kB
./packages/db/dist/esm/collection/indexes.js 1.1 kB
./packages/db/dist/esm/collection/lifecycle.js 1.67 kB
./packages/db/dist/esm/collection/mutations.js 2.26 kB
./packages/db/dist/esm/collection/state.js 3.43 kB
./packages/db/dist/esm/collection/subscription.js 2.48 kB
./packages/db/dist/esm/collection/sync.js 2.37 kB
./packages/db/dist/esm/deferred.js 207 B
./packages/db/dist/esm/errors.js 4.19 kB
./packages/db/dist/esm/event-emitter.js 748 B
./packages/db/dist/esm/index.js 2.64 kB
./packages/db/dist/esm/indexes/auto-index.js 742 B
./packages/db/dist/esm/indexes/base-index.js 766 B
./packages/db/dist/esm/indexes/btree-index.js 1.87 kB
./packages/db/dist/esm/indexes/lazy-index.js 1.1 kB
./packages/db/dist/esm/indexes/reverse-index.js 513 B
./packages/db/dist/esm/local-only.js 837 B
./packages/db/dist/esm/local-storage.js 2.1 kB
./packages/db/dist/esm/optimistic-action.js 359 B
./packages/db/dist/esm/paced-mutations.js 496 B
./packages/db/dist/esm/query/builder/functions.js 733 B
./packages/db/dist/esm/query/builder/index.js 3.96 kB
./packages/db/dist/esm/query/builder/ref-proxy.js 917 B
./packages/db/dist/esm/query/compiler/evaluators.js 1.35 kB
./packages/db/dist/esm/query/compiler/expressions.js 430 B
./packages/db/dist/esm/query/compiler/group-by.js 1.8 kB
./packages/db/dist/esm/query/compiler/index.js 1.96 kB
./packages/db/dist/esm/query/compiler/joins.js 2 kB
./packages/db/dist/esm/query/compiler/order-by.js 1.25 kB
./packages/db/dist/esm/query/compiler/select.js 1.07 kB
./packages/db/dist/esm/query/expression-helpers.js 1.43 kB
./packages/db/dist/esm/query/ir.js 673 B
./packages/db/dist/esm/query/live-query-collection.js 360 B
./packages/db/dist/esm/query/live/collection-config-builder.js 5.33 kB
./packages/db/dist/esm/query/live/collection-registry.js 264 B
./packages/db/dist/esm/query/live/collection-subscriber.js 1.74 kB
./packages/db/dist/esm/query/live/internal.js 130 B
./packages/db/dist/esm/query/optimizer.js 2.56 kB
./packages/db/dist/esm/query/predicate-utils.js 2.88 kB
./packages/db/dist/esm/query/subset-dedupe.js 921 B
./packages/db/dist/esm/scheduler.js 1.3 kB
./packages/db/dist/esm/SortedMap.js 1.18 kB
./packages/db/dist/esm/strategies/debounceStrategy.js 247 B
./packages/db/dist/esm/strategies/queueStrategy.js 428 B
./packages/db/dist/esm/strategies/throttleStrategy.js 246 B
./packages/db/dist/esm/transactions.js 2.9 kB
./packages/db/dist/esm/utils.js 881 B
./packages/db/dist/esm/utils/browser-polyfills.js 304 B
./packages/db/dist/esm/utils/btree.js 5.61 kB
./packages/db/dist/esm/utils/comparison.js 852 B
./packages/db/dist/esm/utils/index-optimization.js 1.51 kB
./packages/db/dist/esm/utils/type-guards.js 157 B

compressed-size-action::db-package-size

@github-actions
Copy link
Contributor

github-actions bot commented Nov 26, 2025

Size Change: 0 B

Total Size: 3.34 kB

ℹ️ View Unchanged
Filename Size
./packages/react-db/dist/esm/index.js 225 B
./packages/react-db/dist/esm/useLiveInfiniteQuery.js 1.17 kB
./packages/react-db/dist/esm/useLiveQuery.js 1.11 kB
./packages/react-db/dist/esm/useLiveSuspenseQuery.js 431 B
./packages/react-db/dist/esm/usePacedMutations.js 401 B

compressed-size-action::react-db-package-size

- Hoist CALLBACK_ITERATION_METHODS Set to module scope for better perf
- Extract isProxiableObject helper to reduce code duplication
- Add tests for filter(), findLast(), some(), and reduce() change tracking
Copy link
Collaborator

@samwillis samwillis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this looks good, the test pass (and look good) and validate the implementation.

One observation is that the main get method on the proxy is now humungous, we need to break it up into helpers.

Approving, but like good to pull out at least this branch as a helper.


// For Array methods that iterate with callbacks and may return elements
// These need to pass proxied elements to callbacks and return proxied results
if (CALLBACK_ITERATION_METHODS.has(methodName)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parent get method that this branch is part of has got very big. With this change it's a 450 line function. Can we move this branch out into a helper function for handling iteration.

I think we should also consider further breaking up this function.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah good time for a refactor yeah — Claude did it pretty and the get method is now ~100 lines of code

expect(obj.date).toEqual(originalDate)
})

// Test that array iteration methods return proxied elements for change tracking
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should group these tests in a describe

- Extract createArrayIterationHandler() for find/filter/forEach/etc.
- Extract createArrayIteratorHandler() for Symbol.iterator
- Reduces main get() handler by ~120 lines
- Group array iteration tests in describe block per review feedback
- Hoist ARRAY_MODIFYING_METHODS Set to module scope
- Hoist MAP_SET_MODIFYING_METHODS Set to module scope
- Hoist MAP_SET_ITERATOR_METHODS Set to module scope
- Extract createModifyingMethodHandler() for unified collection mutation wrapping
- Extract createMapSetIteratorHandler() for Map/Set iteration (~200 lines)
- Main get() handler reduced from ~450 lines to ~100 lines
@KyleAMathews KyleAMathews merged commit 295cb45 into main Nov 26, 2025
7 checks passed
@KyleAMathews KyleAMathews deleted the claude/fix-createOptimisticAction-01A3N3BTNq8jMtZdBkc2kCUg branch November 26, 2025 16:18
@github-actions github-actions bot mentioned this pull request Nov 26, 2025
@github-actions
Copy link
Contributor

🎉 This PR has been released!

Thank you for your contribution!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants