Skip to content

feat(db): implement createEffect reactive effects API#1221

Open
samwillis wants to merge 11 commits intomainfrom
samwillis/create-effect
Open

feat(db): implement createEffect reactive effects API#1221
samwillis wants to merge 11 commits intomainfrom
samwillis/create-effect

Conversation

@samwillis
Copy link
Collaborator

Summary

  • Implements createEffect and useLiveQueryEffect as specified in the reactive effects RFC
  • createEffect attaches an effect handler to a query's delta stream, firing enter, exit, and update events as rows match/unmatch/change within the query result
  • Builds a D2 differential dataflow pipeline directly (no intermediate collection materialisation), with full support for where, join, select, orderBy + limit queries
  • Extracts shared utilities (buildQueryFromConfig, extractCollectionAliases, sendChangesToInput, splitUpdates, etc.) from CollectionConfigBuilder and CollectionSubscriber into src/query/live/utils.ts to reduce duplication
  • Integrates with the transaction-scoped scheduler to coalesce multiple source changes within a single transaction into one graph run
  • Supports skipInitial, onError, batchHandler, handler, and async dispose() with AbortSignal for cancelling in-flight work
  • Auto-disposes when source collections enter error or cleaned-up state
  • Adds useLiveQueryEffect React hook with proper mount/unmount lifecycle management

Key design decisions

  • Direct D2 pipeline rather than wrapping createLiveQueryCollection — avoids materialising results into a collection when only deltas are needed
  • Shared utilities extracted to enable a future refactor where CollectionConfigBuilder could wrap createEffect to further reduce duplication
  • Source error handling — listens for status:change on source collections and auto-disposes on error/cleanup (gap identified during review vs CollectionConfigBuilder)

Test plan

  • 37 unit tests covering:
    • Basic delta events (enter, exit, update)
    • Filtered queries (where clause matching/unmatching)
    • on parameter variations (single type, array, delta)
    • batchHandler batching semantics
    • skipInitial suppression of initial data
    • Error handling (sync, async, console fallback)
    • Disposal (event suppression, in-flight await, AbortSignal, idempotency)
    • Auto-generated and custom IDs
    • QueryBuilder instance input
    • Join queries with multiple source collections
    • Select projections
    • Transaction coalescing (single batch) vs immediate execution (separate batches)
    • Truncate handling (re-insert, full clear, post-truncate data)
    • orderBy + limit (top-K window, insert displacement, delete backfill, desc, in-window updates)
    • Source error handling (auto-dispose on cleanup)
  • 3 React hook tests for useLiveQueryEffect (lifecycle, dep recreation, event reception)

Made with Cursor

samwillis and others added 2 commits February 7, 2026 13:39
Add reactive effects API that fires handlers when rows enter, exit, or
update within a query result, processing deltas without materializing
the full result set.

- Extract shared helpers (extractCollectionsFromQuery, extractCollectionAliases,
  buildQueryFromConfig, sendChangesToInput, splitUpdates) into query/live/utils.ts
- Implement EffectPipelineRunner with D2 graph, source subscriptions, output
  accumulation with previousValue tracking, skipInitial support, join support,
  and transaction-scoped scheduler integration for coalesced graph runs
- Implement createEffect with handler/batchHandler invocation, error routing,
  AbortSignal disposal, and in-flight async handler tracking
- Implement useLiveQueryEffect React hook with useEffect lifecycle management
- Add 27 core tests and 3 React hook tests

Co-authored-by: Cursor <cursoragent@cursor.com>
- Add source error/cleanup detection: EffectPipelineRunner now listens
  for status:change on source collections and auto-disposes the effect
  when a source enters error or cleaned-up state
- Remove no-op onClear scheduler listener (effects have no pending
  state to clear, unlike CollectionConfigBuilder)
- Add 3 truncate tests verifying correct exit/enter events after
  truncate, full-clear truncate, and post-truncate re-insertion
- Add 5 orderBy+limit tests covering top-K window semantics: initial
  window, insert displacement, delete backfill, desc ordering, and
  in-window updates
- Add 2 source error handling tests verifying auto-disposal and
  event suppression on collection cleanup

Co-authored-by: Cursor <cursoragent@cursor.com>
@changeset-bot
Copy link

changeset-bot bot commented Feb 7, 2026

🦋 Changeset detected

Latest commit: d2dbb82

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

This PR includes changesets to release 16 packages
Name Type
@tanstack/db Minor
@tanstack/react-db Minor
@tanstack/angular-db Patch
@tanstack/electric-db-collection Patch
@tanstack/offline-transactions Patch
@tanstack/powersync-db-collection Patch
@tanstack/query-db-collection Patch
@tanstack/rxdb-db-collection Patch
@tanstack/solid-db Patch
@tanstack/svelte-db Patch
@tanstack/trailbase-db-collection Patch
@tanstack/vue-db Patch
todos Patch
@tanstack/db-example-paced-mutations-demo Patch
offline-transactions-react-native Patch
@tanstack/db-example-react-todo 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

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 7, 2026

More templates

@tanstack/angular-db

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

@tanstack/db

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

@tanstack/db-ivm

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

@tanstack/electric-db-collection

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

@tanstack/offline-transactions

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

@tanstack/powersync-db-collection

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

@tanstack/query-db-collection

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

@tanstack/react-db

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

@tanstack/rxdb-db-collection

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

@tanstack/solid-db

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

@tanstack/svelte-db

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

@tanstack/trailbase-db-collection

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

@tanstack/vue-db

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

commit: d2dbb82

@github-actions
Copy link
Contributor

github-actions bot commented Feb 7, 2026

Size Change: +5.42 kB (+5.89%) 🔍

Total Size: 97.4 kB

Filename Size Change
./packages/db/dist/esm/index.js 2.72 kB +23 B (+0.85%)
./packages/db/dist/esm/query/effect.js 4.76 kB +4.76 kB (new file) 🆕
./packages/db/dist/esm/query/live/collection-config-builder.js 5.19 kB -240 B (-4.42%)
./packages/db/dist/esm/query/live/collection-subscriber.js 1.94 kB -485 B (-20.02%) 🎉
./packages/db/dist/esm/query/live/utils.js 1.36 kB +1.36 kB (new file) 🆕
ℹ️ View Unchanged
Filename Size
./packages/db/dist/esm/collection/change-events.js 1.39 kB
./packages/db/dist/esm/collection/changes.js 1.22 kB
./packages/db/dist/esm/collection/events.js 388 B
./packages/db/dist/esm/collection/index.js 3.32 kB
./packages/db/dist/esm/collection/indexes.js 1.1 kB
./packages/db/dist/esm/collection/lifecycle.js 1.75 kB
./packages/db/dist/esm/collection/mutations.js 2.34 kB
./packages/db/dist/esm/collection/state.js 3.49 kB
./packages/db/dist/esm/collection/subscription.js 3.71 kB
./packages/db/dist/esm/collection/sync.js 2.41 kB
./packages/db/dist/esm/deferred.js 207 B
./packages/db/dist/esm/errors.js 4.7 kB
./packages/db/dist/esm/event-emitter.js 748 B
./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 2.17 kB
./packages/db/dist/esm/indexes/lazy-index.js 1.1 kB
./packages/db/dist/esm/indexes/reverse-index.js 538 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/proxy.js 3.75 kB
./packages/db/dist/esm/query/builder/functions.js 733 B
./packages/db/dist/esm/query/builder/index.js 4.09 kB
./packages/db/dist/esm/query/builder/ref-proxy.js 1.05 kB
./packages/db/dist/esm/query/compiler/evaluators.js 1.42 kB
./packages/db/dist/esm/query/compiler/expressions.js 430 B
./packages/db/dist/esm/query/compiler/group-by.js 1.81 kB
./packages/db/dist/esm/query/compiler/index.js 2.02 kB
./packages/db/dist/esm/query/compiler/joins.js 2.07 kB
./packages/db/dist/esm/query/compiler/order-by.js 1.45 kB
./packages/db/dist/esm/query/compiler/select.js 1.06 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-registry.js 264 B
./packages/db/dist/esm/query/live/internal.js 145 B
./packages/db/dist/esm/query/optimizer.js 2.56 kB
./packages/db/dist/esm/query/predicate-utils.js 2.97 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.3 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 924 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 952 B
./packages/db/dist/esm/utils/cursor.js 457 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 Feb 7, 2026

Size Change: 0 B

Total Size: 3.7 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.34 kB
./packages/react-db/dist/esm/useLiveSuspenseQuery.js 559 B
./packages/react-db/dist/esm/usePacedMutations.js 401 B

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

samwillis and others added 8 commits February 7, 2026 14:24
…join)

Co-authored-by: Cursor <cursoragent@cursor.com>
Lazy aliases (marked by the join compiler) should not eagerly load
initial state — the join tap operator loads exactly the rows needed
on demand. For on-demand collections, the previous behavior would
trigger a full server fetch for data meant to be lazily loaded.

Co-authored-by: Cursor <cursoragent@cursor.com>
Add loadMoreIfNeeded integration so effects with orderBy+limit queries
can pull more data from source collections when the pipeline filters
items out of the topK window. This supports on-demand collections where
data should be loaded incrementally.

Changes:
- Pass real optimizableOrderByCollections to compileQuery so the topK
  operator gets dataNeeded() and index-based cursor support
- Add ordered subscription path: requestLimitedSnapshot for initial data,
  splitUpdates for ordered changes, trackSentValues for cursor positioning
- Add loadMoreIfNeeded/loadNextItems called after each graph run step
- Pass orderBy/limit hints to unordered subscriptions for server-side
  optimization
- Add 3 tests for lazy loading with filter, hints, and exit-triggered load

Co-authored-by: Cursor <cursoragent@cursor.com>
…riber

Deduplicate logic used by both EffectPipelineRunner and
CollectionSubscriber by extracting four helpers into utils.ts:

- filterDuplicateInserts: prevent duplicate D2 inserts via sentKeys
- trackBiggestSentValue: cursor tracking for ordered subscriptions
- computeSubscriptionOrderByHints: normalize orderBy/limit for subscriptions
- computeOrderedLoadCursor: build minValues/loadRequestKey for lazy loading

Both consumers now import from the shared module, removing ~200 lines
of near-identical inline logic.

Co-authored-by: Cursor <cursoragent@cursor.com>
Must-fix correctness issues:
1. batchHandler promise rejections now attach .catch() before tracking
   to prevent unhandled rejection warnings
2. Two-phase disposal: graph/inputs/pipeline cleanup is deferred when
   dispose() is called mid-graph-run; while-loop stops if disposed
3. Buffer flush drains robustly per-alias (delete from map before drain),
   applies ordered tracking/splitting for buffered ordered alias data
4. previousValue for coalesced batches uses first delete (pre-batch state)
   and last insert (post-batch state) instead of most-recent of each

Semantics/DX improvements:
5. Explicitly set includeInitialState: false for ordered aliases
6. Exit events no longer duplicate value as previousValue — previousValue
   is only present on update events
7. Batch boundary: accumulate across entire while-loop and emit once per
   scheduler run instead of after each graph.run() step
8. onSourceError callback on EffectConfig lets users handle source errors
   without relying on console.error; auto-dispose still occurs after

Tests added for all 8 fixes (5 new tests).

Co-authored-by: Cursor <cursoragent@cursor.com>
Adds a comprehensive "Reactive Effects (createEffect)" section covering
delta events, handler types, skipInitial, error handling, disposal,
query features, transaction coalescing, and the React useLiveQueryEffect hook.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@samwillis samwillis marked this pull request as ready for review February 7, 2026 18:32
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.

1 participant