@tanstack/db 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/index.cjs +14 -14
- package/dist/cjs/query/compiler/joins.cjs +8 -1
- package/dist/cjs/query/compiler/joins.cjs.map +1 -1
- package/dist/cjs/query/live/collection-config-builder.cjs +3 -2
- package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
- package/dist/esm/index.js +4 -4
- package/dist/esm/query/compiler/joins.js +8 -1
- package/dist/esm/query/compiler/joins.js.map +1 -1
- package/dist/esm/query/live/collection-config-builder.js +3 -2
- package/dist/esm/query/live/collection-config-builder.js.map +1 -1
- package/package.json +3 -2
- package/skills/db-core/SKILL.md +4 -2
- package/skills/db-core/collection-setup/SKILL.md +30 -11
- package/skills/db-core/collection-setup/references/powersync-adapter.md +4 -0
- package/skills/db-core/collection-setup/references/query-adapter.md +32 -0
- package/skills/db-core/custom-adapter/SKILL.md +58 -9
- package/skills/db-core/live-queries/SKILL.md +162 -2
- package/skills/db-core/mutations-optimistic/SKILL.md +1 -1
- package/skills/db-core/persistence/SKILL.md +241 -0
- package/skills/meta-framework/SKILL.md +1 -1
- package/src/query/compiler/joins.ts +14 -2
- package/src/query/live/collection-config-builder.ts +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/db",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "A reactive client store for building super fast apps on sync",
|
|
5
5
|
"author": "Kyle Mathews",
|
|
6
6
|
"license": "MIT",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"homepage": "https://tanstack.com/db",
|
|
13
13
|
"keywords": [
|
|
14
14
|
"optimistic",
|
|
15
|
-
"typescript"
|
|
15
|
+
"typescript",
|
|
16
|
+
"tanstack-intent"
|
|
16
17
|
],
|
|
17
18
|
"type": "module",
|
|
18
19
|
"main": "dist/cjs/index.cjs",
|
package/skills/db-core/SKILL.md
CHANGED
|
@@ -10,7 +10,7 @@ description: >
|
|
|
10
10
|
createPacedMutations. Entry point for all TanStack DB skills.
|
|
11
11
|
type: core
|
|
12
12
|
library: db
|
|
13
|
-
library_version: '0.
|
|
13
|
+
library_version: '0.6.0'
|
|
14
14
|
---
|
|
15
15
|
|
|
16
16
|
# TanStack DB — Core Concepts
|
|
@@ -33,6 +33,7 @@ hooks. In framework projects, import from the framework package directly.
|
|
|
33
33
|
| Query data with where, join, groupBy, select | db-core/live-queries/SKILL.md |
|
|
34
34
|
| Insert, update, delete with optimistic UI | db-core/mutations-optimistic/SKILL.md |
|
|
35
35
|
| Build a custom sync adapter | db-core/custom-adapter/SKILL.md |
|
|
36
|
+
| Persist collections to SQLite (offline cache) | db-core/persistence/SKILL.md |
|
|
36
37
|
| Preload collections in route loaders | meta-framework/SKILL.md |
|
|
37
38
|
| Add offline transaction queueing | offline/SKILL.md (in @tanstack/offline-transactions) |
|
|
38
39
|
|
|
@@ -54,8 +55,9 @@ For framework-specific hooks:
|
|
|
54
55
|
- Using React hooks? → react-db
|
|
55
56
|
- Preloading in route loaders (Start, Next, Remix)? → meta-framework
|
|
56
57
|
- Building an adapter for a new backend? → db-core/custom-adapter
|
|
58
|
+
- Persisting collections to SQLite? → db-core/persistence
|
|
57
59
|
- Need offline transaction persistence? → offline
|
|
58
60
|
|
|
59
61
|
## Version
|
|
60
62
|
|
|
61
|
-
Targets @tanstack/db v0.
|
|
63
|
+
Targets @tanstack/db v0.6.0.
|
|
@@ -6,13 +6,14 @@ description: >
|
|
|
6
6
|
(ElectricSQL real-time sync), powerSyncCollectionOptions (PowerSync SQLite),
|
|
7
7
|
rxdbCollectionOptions (RxDB), trailbaseCollectionOptions (TrailBase),
|
|
8
8
|
localOnlyCollectionOptions, localStorageCollectionOptions. CollectionConfig
|
|
9
|
-
options: getKey, schema, sync, gcTime, autoIndex,
|
|
10
|
-
progressive). StandardSchema validation
|
|
11
|
-
lifecycle (idle/loading/ready/error).
|
|
12
|
-
Electric txid tracking
|
|
9
|
+
options: getKey, schema, sync, gcTime, autoIndex (default off), defaultIndexType,
|
|
10
|
+
syncMode (eager/on-demand, plus progressive for Electric). StandardSchema validation
|
|
11
|
+
with Zod/Valibot/ArkType. Collection lifecycle (idle/loading/ready/error).
|
|
12
|
+
Adapter-specific sync patterns including Electric txid tracking, Query direct
|
|
13
|
+
writes, and PowerSync query-driven sync with onLoad/onLoadSubset hooks.
|
|
13
14
|
type: sub-skill
|
|
14
15
|
library: db
|
|
15
|
-
library_version: '0.
|
|
16
|
+
library_version: '0.6.0'
|
|
16
17
|
sources:
|
|
17
18
|
- 'TanStack/db:docs/overview.md'
|
|
18
19
|
- 'TanStack/db:docs/guides/schemas.md'
|
|
@@ -98,11 +99,29 @@ queryCollectionOptions({
|
|
|
98
99
|
})
|
|
99
100
|
```
|
|
100
101
|
|
|
101
|
-
| Mode | Best for
|
|
102
|
-
| ------------- |
|
|
103
|
-
| `eager` | Mostly-static datasets
|
|
104
|
-
| `on-demand` | Search, catalogs, large tables
|
|
105
|
-
| `progressive` | Collaborative apps needing instant first paint | Any |
|
|
102
|
+
| Mode | Best for | Data size |
|
|
103
|
+
| ------------- | -------------------------------------------------------------- | --------- |
|
|
104
|
+
| `eager` | Mostly-static datasets | <10k rows |
|
|
105
|
+
| `on-demand` | Search, catalogs, large tables | >50k rows |
|
|
106
|
+
| `progressive` | Collaborative apps needing instant first paint (Electric only) | Any |
|
|
107
|
+
|
|
108
|
+
## Indexing
|
|
109
|
+
|
|
110
|
+
Indexing is opt-in. The `autoIndex` option defaults to `"off"`. To enable automatic indexing, set `autoIndex: "eager"` and provide a `defaultIndexType`:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
import { BasicIndex } from '@tanstack/db'
|
|
114
|
+
|
|
115
|
+
createCollection(
|
|
116
|
+
queryCollectionOptions({
|
|
117
|
+
autoIndex: 'eager',
|
|
118
|
+
defaultIndexType: BasicIndex,
|
|
119
|
+
// ...
|
|
120
|
+
}),
|
|
121
|
+
)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Without `defaultIndexType`, setting `autoIndex: "eager"` throws a `CollectionConfigurationError`. You can also create indexes manually with `collection.createIndex()` and remove them with `collection.removeIndex()`.
|
|
106
125
|
|
|
107
126
|
## Core Patterns
|
|
108
127
|
|
|
@@ -255,7 +274,7 @@ app.post('/api/todos', async (req, res) => {
|
|
|
255
274
|
})
|
|
256
275
|
```
|
|
257
276
|
|
|
258
|
-
`pg_current_xact_id()` must be queried inside the same SQL transaction as the mutation. Otherwise the txid doesn't match and `awaitTxId`
|
|
277
|
+
`pg_current_xact_id()` must be queried inside the same SQL transaction as the mutation. Otherwise the txid doesn't match and `awaitTxId` times out (default 5 seconds).
|
|
259
278
|
|
|
260
279
|
Source: docs/collections/electric-collection.md
|
|
261
280
|
|
|
@@ -196,6 +196,10 @@ await tx.commit()
|
|
|
196
196
|
await tx.isPersisted.promise
|
|
197
197
|
```
|
|
198
198
|
|
|
199
|
+
## On-Demand Sync Mode
|
|
200
|
+
|
|
201
|
+
PowerSync supports `on-demand` sync mode (query-driven sync), where only rows matching active live query predicates are loaded from SQLite into the collection. This can be combined with Sync Streams via `onLoad` (eager) or `onLoadSubset` (on-demand) hooks to also control which data the PowerSync Service syncs to the device. Use `extractSimpleComparisons` or `parseWhereExpression` to derive Sync Stream parameters dynamically from live query predicates.
|
|
202
|
+
|
|
199
203
|
## Complete Example
|
|
200
204
|
|
|
201
205
|
```typescript
|
|
@@ -176,6 +176,38 @@ const productsCollection = createCollection(
|
|
|
176
176
|
)
|
|
177
177
|
```
|
|
178
178
|
|
|
179
|
+
## Common Mistakes
|
|
180
|
+
|
|
181
|
+
### HIGH Function-based queryKey without shared prefix
|
|
182
|
+
|
|
183
|
+
Wrong:
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
queryCollectionOptions({
|
|
187
|
+
queryKey: (opts) => {
|
|
188
|
+
if (opts.where) {
|
|
189
|
+
return ['products-filtered', JSON.stringify(opts.where)]
|
|
190
|
+
}
|
|
191
|
+
return ['products-all']
|
|
192
|
+
},
|
|
193
|
+
})
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Correct:
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
queryCollectionOptions({
|
|
200
|
+
queryKey: (opts) => {
|
|
201
|
+
if (opts.where) {
|
|
202
|
+
return ['products', JSON.stringify(opts.where)]
|
|
203
|
+
}
|
|
204
|
+
return ['products']
|
|
205
|
+
},
|
|
206
|
+
})
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
When using a function-based `queryKey`, all derived keys must share the base key (`queryKey({})`) as a prefix. TanStack Query uses prefix matching for cache operations; if derived keys don't share the base prefix, cache updates silently miss entries, leading to stale data.
|
|
210
|
+
|
|
179
211
|
## Key Behaviors
|
|
180
212
|
|
|
181
213
|
- `queryFn` result is treated as **complete state** -- missing items are deleted
|
|
@@ -2,15 +2,17 @@
|
|
|
2
2
|
name: db-core/custom-adapter
|
|
3
3
|
description: >
|
|
4
4
|
Building custom collection adapters for new backends. SyncConfig interface:
|
|
5
|
-
sync function receiving begin, write, commit, markReady, truncate
|
|
6
|
-
ChangeMessage format (insert, update, delete). loadSubset for
|
|
7
|
-
LoadSubsetOptions (where, orderBy, limit, cursor). Expression
|
|
8
|
-
parseWhereExpression, parseOrderByExpression,
|
|
9
|
-
parseLoadSubsetOptions. Collection options creator
|
|
10
|
-
(partial vs full). Subscription lifecycle and cleanup
|
|
5
|
+
sync function receiving begin, write, commit, markReady, truncate, metadata
|
|
6
|
+
primitives. ChangeMessage format (insert, update, delete). loadSubset for
|
|
7
|
+
on-demand sync. LoadSubsetOptions (where, orderBy, limit, cursor). Expression
|
|
8
|
+
parsing: parseWhereExpression, parseOrderByExpression,
|
|
9
|
+
extractSimpleComparisons, parseLoadSubsetOptions. Collection options creator
|
|
10
|
+
pattern. rowUpdateMode (partial vs full). Subscription lifecycle and cleanup
|
|
11
|
+
functions. Persisted sync metadata API (metadata.row and metadata.collection)
|
|
12
|
+
for storing per-row and per-collection adapter state.
|
|
11
13
|
type: sub-skill
|
|
12
14
|
library: db
|
|
13
|
-
library_version: '0.
|
|
15
|
+
library_version: '0.6.0'
|
|
14
16
|
sources:
|
|
15
17
|
- 'TanStack/db:docs/guides/collection-options-creator.md'
|
|
16
18
|
- 'TanStack/db:packages/db/src/collection/sync.ts'
|
|
@@ -38,7 +40,7 @@ function myBackendCollectionOptions<T>(config: {
|
|
|
38
40
|
return {
|
|
39
41
|
getKey: config.getKey,
|
|
40
42
|
sync: {
|
|
41
|
-
sync: ({ begin, write, commit, markReady, collection }) => {
|
|
43
|
+
sync: ({ begin, write, commit, markReady, metadata, collection }) => {
|
|
42
44
|
let isInitialSyncComplete = false
|
|
43
45
|
const bufferedEvents: Array<any> = []
|
|
44
46
|
|
|
@@ -157,6 +159,53 @@ Mutation handlers must not resolve until server changes have synced back to the
|
|
|
157
159
|
4. **Version/timestamp**: wait until sync stream catches up to mutation time
|
|
158
160
|
5. **Provider method**: `await backend.waitForPendingWrites()`
|
|
159
161
|
|
|
162
|
+
### Persisted sync metadata
|
|
163
|
+
|
|
164
|
+
The `metadata` API on the sync config allows adapters to store per-row and per-collection metadata that persists across sync transactions. This is useful for tracking resume tokens, cursors, LSNs, or other adapter-specific state.
|
|
165
|
+
|
|
166
|
+
The `metadata` object is available as a property on the sync config argument alongside `begin`, `write`, `commit`, etc. It is always provided, but without persistence the metadata is in-memory only and does not survive reloads. With persistence, metadata is durable across sessions.
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
sync: ({ begin, write, commit, markReady, metadata }) => {
|
|
170
|
+
// Row metadata: store per-row state (e.g. server version, ETag)
|
|
171
|
+
metadata.row.get(key) // => unknown | undefined
|
|
172
|
+
metadata.row.set(key, { version: 3, etag: 'abc' })
|
|
173
|
+
metadata.row.delete(key)
|
|
174
|
+
|
|
175
|
+
// Collection metadata: store per-collection state (e.g. resume cursor)
|
|
176
|
+
metadata.collection.get('cursor') // => unknown | undefined
|
|
177
|
+
metadata.collection.set('cursor', 'token_abc123')
|
|
178
|
+
metadata.collection.delete('cursor')
|
|
179
|
+
metadata.collection.list() // => [{ key: 'cursor', value: 'token_abc123' }]
|
|
180
|
+
metadata.collection.list('resume') // filter by prefix
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Row metadata writes are tied to the current transaction. When a row is deleted via `write({ type: 'delete', ... })`, its row metadata is automatically deleted. When a row is inserted, its metadata is set from `message.metadata` if provided, or deleted otherwise.
|
|
185
|
+
|
|
186
|
+
Collection metadata writes staged before `truncate()` are preserved and commit atomically with the truncate transaction.
|
|
187
|
+
|
|
188
|
+
**Typical usage — resume token:**
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
sync: ({ begin, write, commit, markReady, metadata }) => {
|
|
192
|
+
const lastCursor = metadata.collection.get('cursor') as string | undefined
|
|
193
|
+
|
|
194
|
+
const stream = subscribeFromCursor(lastCursor)
|
|
195
|
+
stream.on('data', (batch) => {
|
|
196
|
+
begin()
|
|
197
|
+
for (const item of batch.items) {
|
|
198
|
+
write({ type: item.type, key: item.id, value: item.data })
|
|
199
|
+
}
|
|
200
|
+
metadata.collection.set('cursor', batch.cursor)
|
|
201
|
+
commit()
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
stream.on('ready', () => markReady())
|
|
205
|
+
return () => stream.close()
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
160
209
|
### Expression parsing for predicate push-down
|
|
161
210
|
|
|
162
211
|
```ts
|
|
@@ -282,4 +331,4 @@ Source: packages/db/src/collection/sync.ts:110
|
|
|
282
331
|
|
|
283
332
|
Getting-started simplicity (localOnly, eager mode) conflicts with production correctness (on-demand sync, race condition prevention, proper markReady handling). Agents optimizing for quick setup tend to skip buffering, markReady, and cleanup functions.
|
|
284
333
|
|
|
285
|
-
See also: db-core/collection-setup/SKILL.md
|
|
334
|
+
See also: db-core/collection-setup/SKILL.md — for built-in adapter patterns to model after.
|
|
@@ -7,10 +7,14 @@ description: >
|
|
|
7
7
|
isUndefined, and, or, not. Aggregates: count, sum, avg, min, max. String
|
|
8
8
|
functions: upper, lower, length, concat, coalesce. Math: add. $selected
|
|
9
9
|
namespace. createLiveQueryCollection. Derived collections. Predicate push-down.
|
|
10
|
-
Incremental view maintenance via differential dataflow (d2ts).
|
|
10
|
+
Incremental view maintenance via differential dataflow (d2ts). Virtual
|
|
11
|
+
properties ($synced, $origin, $key, $collectionId). Includes subqueries
|
|
12
|
+
for hierarchical data. toArray and concat(toArray(...)) scalar includes.
|
|
13
|
+
queryOnce for one-shot queries. createEffect for reactive side effects
|
|
14
|
+
(onEnter, onUpdate, onExit, onBatch).
|
|
11
15
|
type: sub-skill
|
|
12
16
|
library: db
|
|
13
|
-
library_version: '0.
|
|
17
|
+
library_version: '0.6.0'
|
|
14
18
|
sources:
|
|
15
19
|
- 'TanStack/db:docs/guides/live-queries.md'
|
|
16
20
|
- 'TanStack/db:packages/db/src/query/builder/index.ts'
|
|
@@ -191,6 +195,162 @@ const activeUserPosts = createLiveQueryCollection((q) =>
|
|
|
191
195
|
|
|
192
196
|
Create derived collections once at module scope and reuse them. Do not recreate on every render or navigation.
|
|
193
197
|
|
|
198
|
+
## Virtual Properties
|
|
199
|
+
|
|
200
|
+
Live query results include computed, read-only virtual properties on every row:
|
|
201
|
+
|
|
202
|
+
- `$synced`: `true` when the row is confirmed by sync; `false` when it is still optimistic.
|
|
203
|
+
- `$origin`: `"local"` if the last confirmed change came from this client, otherwise `"remote"`.
|
|
204
|
+
- `$key`: the row key for the result.
|
|
205
|
+
- `$collectionId`: the source collection ID.
|
|
206
|
+
|
|
207
|
+
These props are added automatically and can be used in `where`, `select`, and `orderBy` clauses. Do not persist them back to storage.
|
|
208
|
+
|
|
209
|
+
## Includes (Subqueries in Select)
|
|
210
|
+
|
|
211
|
+
Embed a correlated subquery inside `select()` to produce hierarchical (nested) data. The subquery must contain a `where` with an `eq()` that correlates a parent field with a child field. Three materialization modes are available.
|
|
212
|
+
|
|
213
|
+
### Collection includes (default)
|
|
214
|
+
|
|
215
|
+
Return a child `Collection` on each parent row:
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
import { eq, createLiveQueryCollection } from '@tanstack/db'
|
|
219
|
+
|
|
220
|
+
const projectsWithIssues = createLiveQueryCollection((q) =>
|
|
221
|
+
q.from({ p: projectsCollection }).select(({ p }) => ({
|
|
222
|
+
id: p.id,
|
|
223
|
+
name: p.name,
|
|
224
|
+
issues: q
|
|
225
|
+
.from({ i: issuesCollection })
|
|
226
|
+
.where(({ i }) => eq(i.projectId, p.id))
|
|
227
|
+
.select(({ i }) => ({
|
|
228
|
+
id: i.id,
|
|
229
|
+
title: i.title,
|
|
230
|
+
})),
|
|
231
|
+
})),
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
// Each row's `issues` is a live Collection
|
|
235
|
+
for (const project of projectsWithIssues) {
|
|
236
|
+
console.log(project.name, project.issues.toArray)
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Array includes with toArray()
|
|
241
|
+
|
|
242
|
+
Wrap the subquery in `toArray()` to get a plain array of scalar values instead of a Collection:
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
import { eq, toArray, createLiveQueryCollection } from '@tanstack/db'
|
|
246
|
+
|
|
247
|
+
const messagesWithParts = createLiveQueryCollection((q) =>
|
|
248
|
+
q.from({ m: messagesCollection }).select(({ m }) => ({
|
|
249
|
+
id: m.id,
|
|
250
|
+
contentParts: toArray(
|
|
251
|
+
q
|
|
252
|
+
.from({ c: chunksCollection })
|
|
253
|
+
.where(({ c }) => eq(c.messageId, m.id))
|
|
254
|
+
.orderBy(({ c }) => c.timestamp)
|
|
255
|
+
.select(({ c }) => c.text),
|
|
256
|
+
),
|
|
257
|
+
})),
|
|
258
|
+
)
|
|
259
|
+
// row.contentParts is string[]
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Concatenated scalar with concat(toArray())
|
|
263
|
+
|
|
264
|
+
Wrap `toArray()` in `concat()` to join the scalar results into a single string:
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
import { eq, toArray, concat, createLiveQueryCollection } from '@tanstack/db'
|
|
268
|
+
|
|
269
|
+
const messagesWithContent = createLiveQueryCollection((q) =>
|
|
270
|
+
q.from({ m: messagesCollection }).select(({ m }) => ({
|
|
271
|
+
id: m.id,
|
|
272
|
+
content: concat(
|
|
273
|
+
toArray(
|
|
274
|
+
q
|
|
275
|
+
.from({ c: chunksCollection })
|
|
276
|
+
.where(({ c }) => eq(c.messageId, m.id))
|
|
277
|
+
.orderBy(({ c }) => c.timestamp)
|
|
278
|
+
.select(({ c }) => c.text),
|
|
279
|
+
),
|
|
280
|
+
),
|
|
281
|
+
})),
|
|
282
|
+
)
|
|
283
|
+
// row.content is a single concatenated string
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Includes rules
|
|
287
|
+
|
|
288
|
+
- The subquery **must** have a `where` clause with an `eq()` correlating a parent alias with a child alias. The library extracts this automatically as the join condition.
|
|
289
|
+
- `toArray()` and `concat(toArray())` require the subquery to use a **scalar** `select` (e.g., `select(({ c }) => c.text)`), not an object select.
|
|
290
|
+
- Collection includes (bare subquery) require an **object** `select`.
|
|
291
|
+
- Includes subqueries are compiled into the same incremental pipeline as the parent query -- they are not separate live queries.
|
|
292
|
+
|
|
293
|
+
## One-Shot Queries with queryOnce
|
|
294
|
+
|
|
295
|
+
For non-reactive, one-time snapshots use `queryOnce`. It creates a live query collection, preloads it, extracts the results, and cleans up automatically.
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
import { eq, queryOnce } from '@tanstack/db'
|
|
299
|
+
|
|
300
|
+
const activeUsers = await queryOnce((q) =>
|
|
301
|
+
q
|
|
302
|
+
.from({ user: usersCollection })
|
|
303
|
+
.where(({ user }) => eq(user.active, true))
|
|
304
|
+
.select(({ user }) => ({ id: user.id, name: user.name })),
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
// With findOne — resolves to T | undefined
|
|
308
|
+
const user = await queryOnce((q) =>
|
|
309
|
+
q
|
|
310
|
+
.from({ user: usersCollection })
|
|
311
|
+
.where(({ user }) => eq(user.id, userId))
|
|
312
|
+
.findOne(),
|
|
313
|
+
)
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Use `queryOnce` for scripts, loaders, data export, tests, or AI/LLM context building. For UI bindings and reactive updates, use live queries instead.
|
|
317
|
+
|
|
318
|
+
## Reactive Effects (createEffect)
|
|
319
|
+
|
|
320
|
+
Reactive effects respond to query result _changes_ without materializing the full result set. Effects fire callbacks when rows enter, exit, or update within a query result — like a database trigger on an arbitrary live query.
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
import { createEffect, eq } from '@tanstack/db'
|
|
324
|
+
|
|
325
|
+
const effect = createEffect({
|
|
326
|
+
query: (q) =>
|
|
327
|
+
q
|
|
328
|
+
.from({ msg: messagesCollection })
|
|
329
|
+
.where(({ msg }) => eq(msg.role, 'user')),
|
|
330
|
+
skipInitial: true,
|
|
331
|
+
onEnter: async (event, ctx) => {
|
|
332
|
+
await processNewMessage(event.value, { signal: ctx.signal })
|
|
333
|
+
},
|
|
334
|
+
onExit: (event) => {
|
|
335
|
+
console.log('Message left result set:', event.key)
|
|
336
|
+
},
|
|
337
|
+
onError: (error, event) => {
|
|
338
|
+
console.error(`Failed to process ${event.key}:`, error)
|
|
339
|
+
},
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
// Dispose when no longer needed
|
|
343
|
+
await effect.dispose()
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
| Use case | Approach |
|
|
347
|
+
| ------------------------------- | ----------------------------------------------------- |
|
|
348
|
+
| Display query results in UI | Live query collection + `useLiveQuery` |
|
|
349
|
+
| React to changes (side effects) | `createEffect` with `onEnter` / `onUpdate` / `onExit` |
|
|
350
|
+
| Inspect full batch of changes | `createEffect` with `onBatch` |
|
|
351
|
+
|
|
352
|
+
Key options: `id` (optional), `query`, `skipInitial` (skip existing rows on init), `onEnter`, `onUpdate`, `onExit`, `onBatch`, `onError`, `onSourceError`. The `ctx.signal` aborts when the effect is disposed.
|
|
353
|
+
|
|
194
354
|
## Common Mistakes
|
|
195
355
|
|
|
196
356
|
### CRITICAL: Using === instead of eq()
|
|
@@ -9,7 +9,7 @@ description: >
|
|
|
9
9
|
onInsert/onUpdate/onDelete handlers. PendingMutation type. Transaction.isPersisted.
|
|
10
10
|
type: sub-skill
|
|
11
11
|
library: db
|
|
12
|
-
library_version: '0.
|
|
12
|
+
library_version: '0.6.0'
|
|
13
13
|
sources:
|
|
14
14
|
- 'TanStack/db:docs/guides/mutations.md'
|
|
15
15
|
- 'TanStack/db:packages/db/src/transactions.ts'
|