feat(db): implement createEffect reactive effects API#1221
feat(db): implement createEffect reactive effects API#1221
Conversation
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 detectedLatest commit: d2dbb82 The changes in this PR will be included in the next version bump. This PR includes changesets to release 16 packages
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 |
More templates
@tanstack/angular-db
@tanstack/db
@tanstack/db-ivm
@tanstack/electric-db-collection
@tanstack/offline-transactions
@tanstack/powersync-db-collection
@tanstack/query-db-collection
@tanstack/react-db
@tanstack/rxdb-db-collection
@tanstack/solid-db
@tanstack/svelte-db
@tanstack/trailbase-db-collection
@tanstack/vue-db
commit: |
|
Size Change: +5.42 kB (+5.89%) 🔍 Total Size: 97.4 kB
ℹ️ View Unchanged
|
|
Size Change: 0 B Total Size: 3.7 kB ℹ️ View Unchanged
|
…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>
kevin-dp
left a comment
There was a problem hiding this comment.
LGTM. I like that common code parts have been extracted into helpers and are reused in the effects pipeline.
| q | ||
| .from({ msg: messagesCollection }) | ||
| .where(({ msg }) => eq(msg.role, 'user')), | ||
| on: 'enter', |
There was a problem hiding this comment.
The current API of createEffect only allows you to register a single enter/update/exit handler. Sometimes we may want to register several ones. This would require storing the query in a variable and creating multiple effects. What if we could do that in a single effect definition?
createEffect({
query: ...,
onEnter: async (event) => ...,
onUpdate: async (event) => ...,
onDelete: async (event) => ...
})I also like that now you don't have to provide on and handler props but directly the handler: onEnter/onUpdate/onExit.
There was a problem hiding this comment.
Another design option for the API that is more consistent with the collections API:
createEffect({
query: (q) => ...
}).subscribeChanges(event => {
const deltaType = event.tpe
...
})There was a problem hiding this comment.
I really like:
createEffect({
query: ...,
onEnter: async (event) => ...,
onUpdate: async (event) => ...,
onDelete: async (event) => ...
})I think I change it to that.
Summary
createEffectanduseLiveQueryEffectas specified in the reactive effects RFCcreateEffectattaches an effect handler to a query's delta stream, firingenter,exit, andupdateevents as rows match/unmatch/change within the query resultwhere,join,select,orderBy+limitqueriesbuildQueryFromConfig,extractCollectionAliases,sendChangesToInput,splitUpdates, etc.) fromCollectionConfigBuilderandCollectionSubscriberintosrc/query/live/utils.tsto reduce duplicationskipInitial,onError,batchHandler,handler, and asyncdispose()withAbortSignalfor cancelling in-flight workuseLiveQueryEffectReact hook with proper mount/unmount lifecycle managementKey design decisions
createLiveQueryCollection— avoids materialising results into a collection when only deltas are neededCollectionConfigBuildercould wrapcreateEffectto further reduce duplicationstatus:changeon source collections and auto-disposes on error/cleanup (gap identified during review vsCollectionConfigBuilder)Test plan
onparameter variations (single type, array,delta)batchHandlerbatching semanticsskipInitialsuppression of initial dataQueryBuilderinstance inputorderBy+limit(top-K window, insert displacement, delete backfill, desc, in-window updates)useLiveQueryEffect(lifecycle, dep recreation, event reception)Made with Cursor