@tanstack/db 0.5.33 → 0.6.1
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/collection/change-events.cjs.map +1 -1
- package/dist/cjs/collection/change-events.d.cts +3 -2
- package/dist/cjs/collection/changes.cjs +13 -4
- package/dist/cjs/collection/changes.cjs.map +1 -1
- package/dist/cjs/collection/changes.d.cts +10 -1
- package/dist/cjs/collection/cleanup-queue.cjs +89 -0
- package/dist/cjs/collection/cleanup-queue.cjs.map +1 -0
- package/dist/cjs/collection/cleanup-queue.d.cts +30 -0
- package/dist/cjs/collection/events.cjs +14 -0
- package/dist/cjs/collection/events.cjs.map +1 -1
- package/dist/cjs/collection/events.d.cts +39 -1
- package/dist/cjs/collection/index.cjs +66 -28
- package/dist/cjs/collection/index.cjs.map +1 -1
- package/dist/cjs/collection/index.d.cts +49 -36
- package/dist/cjs/collection/indexes.cjs +211 -62
- package/dist/cjs/collection/indexes.cjs.map +1 -1
- package/dist/cjs/collection/indexes.d.cts +27 -17
- package/dist/cjs/collection/lifecycle.cjs +5 -22
- package/dist/cjs/collection/lifecycle.cjs.map +1 -1
- package/dist/cjs/collection/lifecycle.d.cts +0 -1
- package/dist/cjs/collection/mutations.cjs +18 -0
- package/dist/cjs/collection/mutations.cjs.map +1 -1
- package/dist/cjs/collection/mutations.d.cts +1 -0
- package/dist/cjs/collection/state.cjs +381 -53
- package/dist/cjs/collection/state.cjs.map +1 -1
- package/dist/cjs/collection/state.d.cts +65 -1
- package/dist/cjs/collection/subscription.cjs +6 -0
- package/dist/cjs/collection/subscription.cjs.map +1 -1
- package/dist/cjs/collection/subscription.d.cts +4 -0
- package/dist/cjs/collection/sync.cjs +108 -1
- package/dist/cjs/collection/sync.cjs.map +1 -1
- package/dist/cjs/collection/sync.d.cts +2 -0
- package/dist/cjs/collection/transaction-metadata.cjs +5 -0
- package/dist/cjs/collection/transaction-metadata.cjs.map +1 -0
- package/dist/cjs/collection/transaction-metadata.d.cts +1 -0
- package/dist/cjs/errors.cjs +8 -0
- package/dist/cjs/errors.cjs.map +1 -1
- package/dist/cjs/errors.d.cts +3 -0
- package/dist/cjs/index.cjs +22 -4
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +11 -3
- package/dist/cjs/indexes/auto-index.cjs +13 -6
- package/dist/cjs/indexes/auto-index.cjs.map +1 -1
- package/dist/cjs/indexes/base-index.cjs +0 -3
- package/dist/cjs/indexes/base-index.cjs.map +1 -1
- package/dist/cjs/indexes/base-index.d.cts +2 -6
- package/dist/cjs/indexes/basic-index.cjs +361 -0
- package/dist/cjs/indexes/basic-index.cjs.map +1 -0
- package/dist/cjs/indexes/basic-index.d.cts +102 -0
- package/dist/cjs/indexes/btree-index.cjs.map +1 -1
- package/dist/cjs/indexes/btree-index.d.cts +1 -1
- package/dist/cjs/indexes/index-options.d.cts +8 -9
- package/dist/cjs/indexes/index-registry.cjs +89 -0
- package/dist/cjs/indexes/index-registry.cjs.map +1 -0
- package/dist/cjs/indexes/index-registry.d.cts +61 -0
- package/dist/cjs/local-only.cjs +5 -0
- package/dist/cjs/local-only.cjs.map +1 -1
- package/dist/cjs/query/builder/functions.cjs +27 -11
- package/dist/cjs/query/builder/functions.cjs.map +1 -1
- package/dist/cjs/query/builder/functions.d.cts +25 -3
- package/dist/cjs/query/builder/index.cjs +200 -39
- package/dist/cjs/query/builder/index.cjs.map +1 -1
- package/dist/cjs/query/builder/index.d.cts +4 -3
- package/dist/cjs/query/builder/ref-proxy.cjs.map +1 -1
- package/dist/cjs/query/builder/ref-proxy.d.cts +14 -3
- package/dist/cjs/query/builder/types.d.cts +84 -19
- package/dist/cjs/query/compiler/evaluators.cjs +51 -0
- package/dist/cjs/query/compiler/evaluators.cjs.map +1 -1
- package/dist/cjs/query/compiler/group-by.cjs +100 -28
- package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/group-by.d.cts +4 -2
- package/dist/cjs/query/compiler/index.cjs +283 -11
- package/dist/cjs/query/compiler/index.cjs.map +1 -1
- package/dist/cjs/query/compiler/index.d.cts +30 -2
- package/dist/cjs/query/compiler/order-by.cjs +29 -10
- package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/order-by.d.cts +1 -1
- package/dist/cjs/query/compiler/select.cjs +8 -0
- package/dist/cjs/query/compiler/select.cjs.map +1 -1
- package/dist/cjs/query/index.d.cts +2 -1
- package/dist/cjs/query/ir.cjs +18 -1
- package/dist/cjs/query/ir.cjs.map +1 -1
- package/dist/cjs/query/ir.d.cts +21 -1
- package/dist/cjs/query/live/collection-config-builder.cjs +501 -5
- package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
- package/dist/cjs/query/live/collection-config-builder.d.cts +7 -0
- package/dist/cjs/query/live/types.d.cts +3 -3
- package/dist/cjs/query/live/utils.cjs +43 -3
- package/dist/cjs/query/live/utils.cjs.map +1 -1
- package/dist/cjs/query/live/utils.d.cts +1 -0
- package/dist/cjs/query/live-query-collection.cjs.map +1 -1
- package/dist/cjs/query/live-query-collection.d.cts +9 -6
- package/dist/cjs/query/query-once.cjs.map +1 -1
- package/dist/cjs/query/query-once.d.cts +7 -5
- package/dist/cjs/query/subset-dedupe.cjs +9 -3
- package/dist/cjs/query/subset-dedupe.cjs.map +1 -1
- package/dist/cjs/types.d.cts +42 -8
- package/dist/cjs/utils/array-utils.cjs +27 -0
- package/dist/cjs/utils/array-utils.cjs.map +1 -0
- package/dist/cjs/utils/array-utils.d.cts +16 -0
- package/dist/cjs/utils/comparison.cjs +11 -0
- package/dist/cjs/utils/comparison.cjs.map +1 -1
- package/dist/cjs/utils/index-optimization.cjs +4 -0
- package/dist/cjs/utils/index-optimization.cjs.map +1 -1
- package/dist/cjs/utils.cjs +7 -9
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/cjs/utils.d.cts +6 -1
- package/dist/cjs/virtual-props.cjs +33 -0
- package/dist/cjs/virtual-props.cjs.map +1 -0
- package/dist/cjs/virtual-props.d.cts +196 -0
- package/dist/esm/collection/change-events.d.ts +3 -2
- package/dist/esm/collection/change-events.js.map +1 -1
- package/dist/esm/collection/changes.d.ts +10 -1
- package/dist/esm/collection/changes.js +13 -4
- package/dist/esm/collection/changes.js.map +1 -1
- package/dist/esm/collection/cleanup-queue.d.ts +30 -0
- package/dist/esm/collection/cleanup-queue.js +89 -0
- package/dist/esm/collection/cleanup-queue.js.map +1 -0
- package/dist/esm/collection/events.d.ts +39 -1
- package/dist/esm/collection/events.js +14 -0
- package/dist/esm/collection/events.js.map +1 -1
- package/dist/esm/collection/index.d.ts +49 -36
- package/dist/esm/collection/index.js +67 -29
- package/dist/esm/collection/index.js.map +1 -1
- package/dist/esm/collection/indexes.d.ts +27 -17
- package/dist/esm/collection/indexes.js +211 -62
- package/dist/esm/collection/indexes.js.map +1 -1
- package/dist/esm/collection/lifecycle.d.ts +0 -1
- package/dist/esm/collection/lifecycle.js +5 -22
- package/dist/esm/collection/lifecycle.js.map +1 -1
- package/dist/esm/collection/mutations.d.ts +1 -0
- package/dist/esm/collection/mutations.js +18 -0
- package/dist/esm/collection/mutations.js.map +1 -1
- package/dist/esm/collection/state.d.ts +65 -1
- package/dist/esm/collection/state.js +381 -53
- package/dist/esm/collection/state.js.map +1 -1
- package/dist/esm/collection/subscription.d.ts +4 -0
- package/dist/esm/collection/subscription.js +6 -0
- package/dist/esm/collection/subscription.js.map +1 -1
- package/dist/esm/collection/sync.d.ts +2 -0
- package/dist/esm/collection/sync.js +108 -1
- package/dist/esm/collection/sync.js.map +1 -1
- package/dist/esm/collection/transaction-metadata.d.ts +1 -0
- package/dist/esm/collection/transaction-metadata.js +5 -0
- package/dist/esm/collection/transaction-metadata.js.map +1 -0
- package/dist/esm/errors.d.ts +3 -0
- package/dist/esm/errors.js +8 -0
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.d.ts +11 -3
- package/dist/esm/index.js +25 -7
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/indexes/auto-index.js +13 -6
- package/dist/esm/indexes/auto-index.js.map +1 -1
- package/dist/esm/indexes/base-index.d.ts +2 -6
- package/dist/esm/indexes/base-index.js +1 -4
- package/dist/esm/indexes/base-index.js.map +1 -1
- package/dist/esm/indexes/basic-index.d.ts +102 -0
- package/dist/esm/indexes/basic-index.js +361 -0
- package/dist/esm/indexes/basic-index.js.map +1 -0
- package/dist/esm/indexes/btree-index.d.ts +1 -1
- package/dist/esm/indexes/btree-index.js.map +1 -1
- package/dist/esm/indexes/index-options.d.ts +8 -9
- package/dist/esm/indexes/index-registry.d.ts +61 -0
- package/dist/esm/indexes/index-registry.js +89 -0
- package/dist/esm/indexes/index-registry.js.map +1 -0
- package/dist/esm/local-only.js +5 -0
- package/dist/esm/local-only.js.map +1 -1
- package/dist/esm/query/builder/functions.d.ts +25 -3
- package/dist/esm/query/builder/functions.js +27 -11
- package/dist/esm/query/builder/functions.js.map +1 -1
- package/dist/esm/query/builder/index.d.ts +4 -3
- package/dist/esm/query/builder/index.js +201 -40
- package/dist/esm/query/builder/index.js.map +1 -1
- package/dist/esm/query/builder/ref-proxy.d.ts +14 -3
- package/dist/esm/query/builder/ref-proxy.js.map +1 -1
- package/dist/esm/query/builder/types.d.ts +84 -19
- package/dist/esm/query/compiler/evaluators.js +51 -0
- package/dist/esm/query/compiler/evaluators.js.map +1 -1
- package/dist/esm/query/compiler/group-by.d.ts +4 -2
- package/dist/esm/query/compiler/group-by.js +101 -29
- package/dist/esm/query/compiler/group-by.js.map +1 -1
- package/dist/esm/query/compiler/index.d.ts +30 -2
- package/dist/esm/query/compiler/index.js +285 -13
- package/dist/esm/query/compiler/index.js.map +1 -1
- package/dist/esm/query/compiler/order-by.d.ts +1 -1
- package/dist/esm/query/compiler/order-by.js +30 -11
- package/dist/esm/query/compiler/order-by.js.map +1 -1
- package/dist/esm/query/compiler/select.js +8 -0
- package/dist/esm/query/compiler/select.js.map +1 -1
- package/dist/esm/query/index.d.ts +2 -1
- package/dist/esm/query/ir.d.ts +21 -1
- package/dist/esm/query/ir.js +18 -1
- package/dist/esm/query/ir.js.map +1 -1
- package/dist/esm/query/live/collection-config-builder.d.ts +7 -0
- package/dist/esm/query/live/collection-config-builder.js +503 -7
- package/dist/esm/query/live/collection-config-builder.js.map +1 -1
- package/dist/esm/query/live/types.d.ts +3 -3
- package/dist/esm/query/live/utils.d.ts +1 -0
- package/dist/esm/query/live/utils.js +43 -3
- package/dist/esm/query/live/utils.js.map +1 -1
- package/dist/esm/query/live-query-collection.d.ts +9 -6
- package/dist/esm/query/live-query-collection.js.map +1 -1
- package/dist/esm/query/query-once.d.ts +7 -5
- package/dist/esm/query/query-once.js.map +1 -1
- package/dist/esm/query/subset-dedupe.js +9 -3
- package/dist/esm/query/subset-dedupe.js.map +1 -1
- package/dist/esm/types.d.ts +42 -8
- package/dist/esm/utils/array-utils.d.ts +16 -0
- package/dist/esm/utils/array-utils.js +27 -0
- package/dist/esm/utils/array-utils.js.map +1 -0
- package/dist/esm/utils/comparison.js +11 -0
- package/dist/esm/utils/comparison.js.map +1 -1
- package/dist/esm/utils/index-optimization.js +4 -0
- package/dist/esm/utils/index-optimization.js.map +1 -1
- package/dist/esm/utils.d.ts +6 -1
- package/dist/esm/utils.js +7 -9
- package/dist/esm/utils.js.map +1 -1
- package/dist/esm/virtual-props.d.ts +196 -0
- package/dist/esm/virtual-props.js +33 -0
- package/dist/esm/virtual-props.js.map +1 -0
- package/package.json +4 -3
- 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/electric-adapter.md +1 -1
- 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/collection/change-events.ts +13 -9
- package/src/collection/changes.ts +30 -7
- package/src/collection/cleanup-queue.ts +105 -0
- package/src/collection/events.ts +65 -0
- package/src/collection/index.ts +110 -45
- package/src/collection/indexes.ts +283 -76
- package/src/collection/lifecycle.ts +5 -26
- package/src/collection/mutations.ts +21 -0
- package/src/collection/state.ts +545 -71
- package/src/collection/subscription.ts +7 -0
- package/src/collection/sync.ts +137 -0
- package/src/collection/transaction-metadata.ts +1 -0
- package/src/errors.ts +9 -0
- package/src/index.ts +46 -3
- package/src/indexes/auto-index.ts +18 -8
- package/src/indexes/base-index.ts +2 -10
- package/src/indexes/basic-index.ts +507 -0
- package/src/indexes/btree-index.ts +1 -1
- package/src/indexes/index-options.ts +17 -37
- package/src/indexes/index-registry.ts +174 -0
- package/src/local-only.ts +7 -0
- package/src/query/builder/functions.ts +84 -7
- package/src/query/builder/index.ts +329 -9
- package/src/query/builder/ref-proxy.ts +22 -4
- package/src/query/builder/types.ts +257 -62
- package/src/query/compiler/evaluators.ts +57 -0
- package/src/query/compiler/group-by.ts +156 -35
- package/src/query/compiler/index.ts +445 -15
- package/src/query/compiler/order-by.ts +51 -12
- package/src/query/compiler/select.ts +9 -0
- package/src/query/index.ts +7 -0
- package/src/query/ir.ts +23 -2
- package/src/query/live/collection-config-builder.ts +809 -9
- package/src/query/live/types.ts +10 -4
- package/src/query/live/utils.ts +64 -3
- package/src/query/live-query-collection.ts +43 -18
- package/src/query/query-once.ts +31 -12
- package/src/query/subset-dedupe.ts +11 -7
- package/src/types.ts +49 -9
- package/src/utils/array-utils.ts +49 -0
- package/src/utils/comparison.ts +14 -0
- package/src/utils/index-optimization.ts +4 -0
- package/src/utils.ts +12 -9
- package/src/virtual-props.ts +282 -0
- package/dist/cjs/indexes/lazy-index.cjs +0 -190
- package/dist/cjs/indexes/lazy-index.cjs.map +0 -1
- package/dist/cjs/indexes/lazy-index.d.cts +0 -96
- package/dist/esm/indexes/lazy-index.d.ts +0 -96
- package/dist/esm/indexes/lazy-index.js +0 -190
- package/dist/esm/indexes/lazy-index.js.map +0 -1
- package/src/indexes/lazy-index.ts +0 -251
|
@@ -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'
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: db-core/persistence
|
|
3
|
+
description: >
|
|
4
|
+
SQLite-backed persistence for TanStack DB collections. persistedCollectionOptions
|
|
5
|
+
wraps any adapter (Electric, Query, PowerSync, or local-only) with durable local
|
|
6
|
+
storage. Platform adapters: browser (WA-SQLite OPFS), React Native (op-sqlite),
|
|
7
|
+
Expo (expo-sqlite), Electron (IPC), Node (better-sqlite3), Capacitor, Tauri,
|
|
8
|
+
Cloudflare Durable Objects. Multi-tab/multi-process coordination via
|
|
9
|
+
BrowserCollectionCoordinator / ElectronCollectionCoordinator /
|
|
10
|
+
SingleProcessCoordinator. schemaVersion for migration resets. Local-only mode
|
|
11
|
+
for offline-first without a server.
|
|
12
|
+
type: sub-skill
|
|
13
|
+
library: db
|
|
14
|
+
library_version: '0.6.0'
|
|
15
|
+
sources:
|
|
16
|
+
- 'TanStack/db:packages/db-sqlite-persistence-core/src/persisted.ts'
|
|
17
|
+
- 'TanStack/db:packages/browser-db-sqlite-persistence/src/index.ts'
|
|
18
|
+
- 'TanStack/db:packages/react-native-db-sqlite-persistence/src/index.ts'
|
|
19
|
+
- 'TanStack/db:packages/expo-db-sqlite-persistence/src/index.ts'
|
|
20
|
+
- 'TanStack/db:packages/electron-db-sqlite-persistence/src/index.ts'
|
|
21
|
+
- 'TanStack/db:packages/node-db-sqlite-persistence/src/index.ts'
|
|
22
|
+
- 'TanStack/db:examples/react/offline-transactions/src/db/persisted-todos.ts'
|
|
23
|
+
- 'TanStack/db:examples/react-native/shopping-list/src/db/collections.ts'
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
This skill builds on db-core and db-core/collection-setup. Read those first.
|
|
27
|
+
|
|
28
|
+
# SQLite Persistence
|
|
29
|
+
|
|
30
|
+
TanStack DB persistence adds a durable SQLite-backed layer to any collection. Data survives page reloads, app restarts, and offline periods. The server remains authoritative for synced collections -- persistence provides a local cache that hydrates instantly.
|
|
31
|
+
|
|
32
|
+
## Choosing a Platform Package
|
|
33
|
+
|
|
34
|
+
| Platform | Package | Create function |
|
|
35
|
+
| -------------- | ------------------------------------------------------------ | -------------------------------------------- |
|
|
36
|
+
| Browser (OPFS) | `@tanstack/browser-db-sqlite-persistence` | `createBrowserWASQLitePersistence` |
|
|
37
|
+
| React Native | `@tanstack/react-native-db-sqlite-persistence` | `createReactNativeSQLitePersistence` |
|
|
38
|
+
| Expo | `@tanstack/expo-db-sqlite-persistence` | `createExpoSQLitePersistence` |
|
|
39
|
+
| Electron | `@tanstack/electron-db-sqlite-persistence` | `createElectronSQLitePersistence` (renderer) |
|
|
40
|
+
| Node.js | `@tanstack/node-db-sqlite-persistence` | `createNodeSQLitePersistence` |
|
|
41
|
+
| Capacitor | `@tanstack/capacitor-db-sqlite-persistence` | `createCapacitorSQLitePersistence` |
|
|
42
|
+
| Tauri | `@tanstack/tauri-db-sqlite-persistence` | `createTauriSQLitePersistence` |
|
|
43
|
+
| Cloudflare DO | `@tanstack/cloudflare-durable-objects-db-sqlite-persistence` | `createCloudflareDOSQLitePersistence` |
|
|
44
|
+
|
|
45
|
+
All platform packages re-export `persistedCollectionOptions` from the core.
|
|
46
|
+
|
|
47
|
+
## Local-Only Persistence (No Server)
|
|
48
|
+
|
|
49
|
+
For purely local data with no sync backend:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { createCollection } from '@tanstack/react-db'
|
|
53
|
+
import {
|
|
54
|
+
BrowserCollectionCoordinator,
|
|
55
|
+
createBrowserWASQLitePersistence,
|
|
56
|
+
openBrowserWASQLiteOPFSDatabase,
|
|
57
|
+
persistedCollectionOptions,
|
|
58
|
+
} from '@tanstack/browser-db-sqlite-persistence'
|
|
59
|
+
|
|
60
|
+
const database = await openBrowserWASQLiteOPFSDatabase({
|
|
61
|
+
databaseName: 'my-app.sqlite',
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const coordinator = new BrowserCollectionCoordinator({
|
|
65
|
+
dbName: 'my-app',
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const persistence = createBrowserWASQLitePersistence({
|
|
69
|
+
database,
|
|
70
|
+
coordinator,
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const draftsCollection = createCollection(
|
|
74
|
+
persistedCollectionOptions<Draft, string>({
|
|
75
|
+
id: 'drafts',
|
|
76
|
+
getKey: (d) => d.id,
|
|
77
|
+
persistence,
|
|
78
|
+
schemaVersion: 1,
|
|
79
|
+
}),
|
|
80
|
+
)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Local-only collections provide `collection.utils.acceptMutations()` for applying mutations directly.
|
|
84
|
+
|
|
85
|
+
## Synced Persistence (Wrapping an Adapter)
|
|
86
|
+
|
|
87
|
+
Spread an existing adapter's options into `persistedCollectionOptions` to add persistence on top of sync:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { createCollection } from '@tanstack/react-db'
|
|
91
|
+
import { electricCollectionOptions } from '@tanstack/electric-db-collection'
|
|
92
|
+
import {
|
|
93
|
+
createReactNativeSQLitePersistence,
|
|
94
|
+
persistedCollectionOptions,
|
|
95
|
+
} from '@tanstack/react-native-db-sqlite-persistence'
|
|
96
|
+
|
|
97
|
+
const persistence = createReactNativeSQLitePersistence({ database })
|
|
98
|
+
|
|
99
|
+
const todosCollection = createCollection(
|
|
100
|
+
persistedCollectionOptions({
|
|
101
|
+
...electricCollectionOptions({
|
|
102
|
+
id: 'todos',
|
|
103
|
+
shapeOptions: { url: '/api/electric/todos' },
|
|
104
|
+
getKey: (item) => item.id,
|
|
105
|
+
}),
|
|
106
|
+
persistence,
|
|
107
|
+
schemaVersion: 1,
|
|
108
|
+
}),
|
|
109
|
+
)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
This works with any adapter: `electricCollectionOptions`, `queryCollectionOptions`, `powerSyncCollectionOptions`, etc. The `persistedCollectionOptions` wrapper intercepts the sync layer to persist data as it flows through.
|
|
113
|
+
|
|
114
|
+
## Multi-Tab / Multi-Process Coordination
|
|
115
|
+
|
|
116
|
+
Coordinators handle leader election and cross-instance communication so only one tab/process owns the database writer.
|
|
117
|
+
|
|
118
|
+
| Platform | Coordinator | Mechanism |
|
|
119
|
+
| ------------------------------------- | ------------------------------- | ---------------------------------------------- |
|
|
120
|
+
| Browser | `BrowserCollectionCoordinator` | BroadcastChannel + Web Locks |
|
|
121
|
+
| Electron | `ElectronCollectionCoordinator` | IPC (main holds DB, renderer accesses via RPC) |
|
|
122
|
+
| Single-process (RN, Expo, Node, etc.) | `SingleProcessCoordinator` | No-op (always leader) |
|
|
123
|
+
|
|
124
|
+
Browser example:
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
import { BrowserCollectionCoordinator } from '@tanstack/browser-db-sqlite-persistence'
|
|
128
|
+
|
|
129
|
+
const coordinator = new BrowserCollectionCoordinator({
|
|
130
|
+
dbName: 'my-app',
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
// Pass to persistence
|
|
134
|
+
const persistence = createBrowserWASQLitePersistence({ database, coordinator })
|
|
135
|
+
|
|
136
|
+
// Cleanup on shutdown
|
|
137
|
+
coordinator.dispose()
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Electron requires setup in both processes:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
// Main process
|
|
144
|
+
import { exposeElectronSQLitePersistence } from '@tanstack/electron-db-sqlite-persistence'
|
|
145
|
+
exposeElectronSQLitePersistence({ persistence, ipcMain })
|
|
146
|
+
|
|
147
|
+
// Renderer process
|
|
148
|
+
import {
|
|
149
|
+
createElectronSQLitePersistence,
|
|
150
|
+
ElectronCollectionCoordinator,
|
|
151
|
+
} from '@tanstack/electron-db-sqlite-persistence'
|
|
152
|
+
|
|
153
|
+
const coordinator = new ElectronCollectionCoordinator({ dbName: 'my-app' })
|
|
154
|
+
const persistence = createElectronSQLitePersistence({
|
|
155
|
+
ipcRenderer: window.electron.ipcRenderer,
|
|
156
|
+
coordinator,
|
|
157
|
+
})
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Schema Versioning
|
|
161
|
+
|
|
162
|
+
`schemaVersion` tracks the shape of persisted data. When the stored version doesn't match the code, the collection resets (drops and reloads from server for synced collections, or throws for local-only).
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
persistedCollectionOptions({
|
|
166
|
+
// ...
|
|
167
|
+
schemaVersion: 2, // bump when you change the data shape
|
|
168
|
+
})
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
There is no custom migration function -- a version mismatch triggers a full reset. For synced collections this is safe because the server re-supplies the data.
|
|
172
|
+
|
|
173
|
+
## Key Options
|
|
174
|
+
|
|
175
|
+
| Option | Type | Description |
|
|
176
|
+
| --------------- | -------------------------------- | -------------------------------------------------------- |
|
|
177
|
+
| `persistence` | `PersistedCollectionPersistence` | Platform adapter + coordinator |
|
|
178
|
+
| `schemaVersion` | `number` | Data version (default 1). Bump on schema changes |
|
|
179
|
+
| `id` | `string` | Required for local-only. Collection identifier in SQLite |
|
|
180
|
+
|
|
181
|
+
## Common Mistakes
|
|
182
|
+
|
|
183
|
+
### CRITICAL Using local-only persistence without an `id`
|
|
184
|
+
|
|
185
|
+
Wrong:
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
persistedCollectionOptions({
|
|
189
|
+
getKey: (d) => d.id,
|
|
190
|
+
persistence,
|
|
191
|
+
// missing id — generates random UUID each session, data won't persist across reloads
|
|
192
|
+
})
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Correct:
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
persistedCollectionOptions({
|
|
199
|
+
id: 'drafts',
|
|
200
|
+
getKey: (d) => d.id,
|
|
201
|
+
persistence,
|
|
202
|
+
})
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Without an explicit `id`, the code generates a random UUID each session, so persisted data is silently abandoned on every reload. Local-only persisted collections must always provide an `id`. Synced collections derive it from the adapter config.
|
|
206
|
+
|
|
207
|
+
### HIGH Forgetting the coordinator in multi-tab apps
|
|
208
|
+
|
|
209
|
+
Wrong:
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
const persistence = createBrowserWASQLitePersistence({ database })
|
|
213
|
+
// No coordinator — concurrent tabs corrupt the database
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Correct:
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
const coordinator = new BrowserCollectionCoordinator({ dbName: 'my-app' })
|
|
220
|
+
const persistence = createBrowserWASQLitePersistence({ database, coordinator })
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Without a coordinator, multiple browser tabs write to SQLite concurrently, causing data corruption. Always use `BrowserCollectionCoordinator` in browser environments.
|
|
224
|
+
|
|
225
|
+
### HIGH Not bumping schemaVersion after changing data shape
|
|
226
|
+
|
|
227
|
+
If you add, remove, or rename fields in your collection type but keep the same `schemaVersion`, the persisted SQLite data will have the old shape. For synced collections, bump the version to trigger a reset and re-sync.
|
|
228
|
+
|
|
229
|
+
### MEDIUM Not disposing the coordinator on cleanup
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
// On app shutdown or hot module reload
|
|
233
|
+
coordinator.dispose()
|
|
234
|
+
await database.close?.()
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Failing to dispose leaks BroadcastChannel subscriptions and Web Lock handles.
|
|
238
|
+
|
|
239
|
+
See also: db-core/collection-setup/SKILL.md — for adapter selection and collection configuration.
|
|
240
|
+
|
|
241
|
+
See also: offline/SKILL.md — for offline transaction queueing (complements persistence).
|
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
import type { CollectionImpl } from './index.js'
|
|
23
23
|
import type { SingleRowRefProxy } from '../query/builder/ref-proxy'
|
|
24
24
|
import type { BasicExpression, OrderBy } from '../query/ir.js'
|
|
25
|
+
import type { WithVirtualProps } from '../virtual-props.js'
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
28
|
* Returns the current state of the collection as an array of changes
|
|
@@ -58,14 +59,14 @@ export function currentStateAsChanges<
|
|
|
58
59
|
T extends object,
|
|
59
60
|
TKey extends string | number,
|
|
60
61
|
>(
|
|
61
|
-
collection: CollectionLike<T, TKey>,
|
|
62
|
+
collection: CollectionLike<WithVirtualProps<T, TKey>, TKey>,
|
|
62
63
|
options: CurrentStateAsChangesOptions = {},
|
|
63
|
-
): Array<ChangeMessage<T>> | void {
|
|
64
|
+
): Array<ChangeMessage<WithVirtualProps<T, TKey>, TKey>> | void {
|
|
64
65
|
// Helper function to collect filtered results
|
|
65
66
|
const collectFilteredResults = (
|
|
66
|
-
filterFn?: (value: T) => boolean,
|
|
67
|
-
): Array<ChangeMessage<T>> => {
|
|
68
|
-
const result: Array<ChangeMessage<T>> = []
|
|
67
|
+
filterFn?: (value: WithVirtualProps<T, TKey>) => boolean,
|
|
68
|
+
): Array<ChangeMessage<WithVirtualProps<T, TKey>, TKey>> => {
|
|
69
|
+
const result: Array<ChangeMessage<WithVirtualProps<T, TKey>, TKey>> = []
|
|
69
70
|
for (const [key, value] of collection.entries()) {
|
|
70
71
|
// If no filter function is provided, include all items
|
|
71
72
|
if (filterFn?.(value) ?? true) {
|
|
@@ -106,7 +107,7 @@ export function currentStateAsChanges<
|
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
// Convert keys to change messages
|
|
109
|
-
const result: Array<ChangeMessage<T>> = []
|
|
110
|
+
const result: Array<ChangeMessage<WithVirtualProps<T, TKey>, TKey>> = []
|
|
110
111
|
for (const key of orderedKeys) {
|
|
111
112
|
const value = collection.get(key)
|
|
112
113
|
if (value !== undefined) {
|
|
@@ -138,7 +139,7 @@ export function currentStateAsChanges<
|
|
|
138
139
|
|
|
139
140
|
if (optimizationResult.canOptimize) {
|
|
140
141
|
// Use index optimization
|
|
141
|
-
const result: Array<ChangeMessage<T>> = []
|
|
142
|
+
const result: Array<ChangeMessage<WithVirtualProps<T, TKey>, TKey>> = []
|
|
142
143
|
for (const key of optimizationResult.matchingKeys) {
|
|
143
144
|
const value = collection.get(key)
|
|
144
145
|
if (value !== undefined) {
|
|
@@ -241,9 +242,12 @@ export function createFilterFunctionFromExpression<T extends object>(
|
|
|
241
242
|
* @param options - The subscription options containing the where clause
|
|
242
243
|
* @returns A filtered callback function
|
|
243
244
|
*/
|
|
244
|
-
export function createFilteredCallback<
|
|
245
|
+
export function createFilteredCallback<
|
|
246
|
+
T extends object,
|
|
247
|
+
TKey extends string | number = string | number,
|
|
248
|
+
>(
|
|
245
249
|
originalCallback: (changes: Array<ChangeMessage<T>>) => void,
|
|
246
|
-
options: SubscribeChangesOptions,
|
|
250
|
+
options: SubscribeChangesOptions<T, TKey>,
|
|
247
251
|
): (changes: Array<ChangeMessage<T>>) => void {
|
|
248
252
|
const filterFn = createFilterFunctionFromExpression(options.whereExpression!)
|
|
249
253
|
|
|
@@ -10,6 +10,8 @@ import type { CollectionLifecycleManager } from './lifecycle.js'
|
|
|
10
10
|
import type { CollectionSyncManager } from './sync.js'
|
|
11
11
|
import type { CollectionEventsManager } from './events.js'
|
|
12
12
|
import type { CollectionImpl } from './index.js'
|
|
13
|
+
import type { CollectionStateManager } from './state.js'
|
|
14
|
+
import type { WithVirtualProps } from '../virtual-props.js'
|
|
13
15
|
|
|
14
16
|
export class CollectionChangesManager<
|
|
15
17
|
TOutput extends object = Record<string, unknown>,
|
|
@@ -21,6 +23,7 @@ export class CollectionChangesManager<
|
|
|
21
23
|
private sync!: CollectionSyncManager<TOutput, TKey, TSchema, TInput>
|
|
22
24
|
private events!: CollectionEventsManager
|
|
23
25
|
private collection!: CollectionImpl<TOutput, TKey, any, TSchema, TInput>
|
|
26
|
+
private state!: CollectionStateManager<TOutput, TKey, TSchema, TInput>
|
|
24
27
|
|
|
25
28
|
public activeSubscribersCount = 0
|
|
26
29
|
public changeSubscriptions = new Set<CollectionSubscription>()
|
|
@@ -37,11 +40,13 @@ export class CollectionChangesManager<
|
|
|
37
40
|
sync: CollectionSyncManager<TOutput, TKey, TSchema, TInput>
|
|
38
41
|
events: CollectionEventsManager
|
|
39
42
|
collection: CollectionImpl<TOutput, TKey, any, TSchema, TInput>
|
|
43
|
+
state: CollectionStateManager<TOutput, TKey, TSchema, TInput>
|
|
40
44
|
}) {
|
|
41
45
|
this.lifecycle = deps.lifecycle
|
|
42
46
|
this.sync = deps.sync
|
|
43
47
|
this.events = deps.events
|
|
44
48
|
this.collection = deps.collection
|
|
49
|
+
this.state = deps.state
|
|
45
50
|
}
|
|
46
51
|
|
|
47
52
|
/**
|
|
@@ -55,6 +60,16 @@ export class CollectionChangesManager<
|
|
|
55
60
|
}
|
|
56
61
|
}
|
|
57
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Enriches a change message with virtual properties ($synced, $origin, $key, $collectionId).
|
|
65
|
+
* Uses the "add-if-missing" pattern to preserve virtual properties from upstream collections.
|
|
66
|
+
*/
|
|
67
|
+
private enrichChangeWithVirtualProps(
|
|
68
|
+
change: ChangeMessage<TOutput, TKey>,
|
|
69
|
+
): ChangeMessage<WithVirtualProps<TOutput, TKey>, TKey> {
|
|
70
|
+
return this.state.enrichChangeMessage(change)
|
|
71
|
+
}
|
|
72
|
+
|
|
58
73
|
/**
|
|
59
74
|
* Emit events either immediately or batch them for later emission
|
|
60
75
|
*/
|
|
@@ -70,26 +85,32 @@ export class CollectionChangesManager<
|
|
|
70
85
|
}
|
|
71
86
|
|
|
72
87
|
// Either we're not batching, or we're forcing emission (user action or ending batch cycle)
|
|
73
|
-
let
|
|
88
|
+
let rawEvents = changes
|
|
74
89
|
|
|
75
90
|
if (forceEmit) {
|
|
76
91
|
// Force emit is used to end a batch (e.g. after a sync commit). Combine any
|
|
77
92
|
// buffered optimistic events with the final changes so subscribers see the
|
|
78
93
|
// whole picture, even if the sync diff is empty.
|
|
79
94
|
if (this.batchedEvents.length > 0) {
|
|
80
|
-
|
|
95
|
+
rawEvents = [...this.batchedEvents, ...changes]
|
|
81
96
|
}
|
|
82
97
|
this.batchedEvents = []
|
|
83
98
|
this.shouldBatchEvents = false
|
|
84
99
|
}
|
|
85
100
|
|
|
86
|
-
if (
|
|
101
|
+
if (rawEvents.length === 0) {
|
|
87
102
|
return
|
|
88
103
|
}
|
|
89
104
|
|
|
105
|
+
// Enrich all change messages with virtual properties
|
|
106
|
+
// This uses the "add-if-missing" pattern to preserve pass-through semantics
|
|
107
|
+
const enrichedEvents: Array<
|
|
108
|
+
ChangeMessage<WithVirtualProps<TOutput, TKey>, TKey>
|
|
109
|
+
> = rawEvents.map((change) => this.enrichChangeWithVirtualProps(change))
|
|
110
|
+
|
|
90
111
|
// Emit to all listeners
|
|
91
112
|
for (const subscription of this.changeSubscriptions) {
|
|
92
|
-
subscription.emitEvents(
|
|
113
|
+
subscription.emitEvents(enrichedEvents)
|
|
93
114
|
}
|
|
94
115
|
}
|
|
95
116
|
|
|
@@ -97,8 +118,10 @@ export class CollectionChangesManager<
|
|
|
97
118
|
* Subscribe to changes in the collection
|
|
98
119
|
*/
|
|
99
120
|
public subscribeChanges(
|
|
100
|
-
callback: (
|
|
101
|
-
|
|
121
|
+
callback: (
|
|
122
|
+
changes: Array<ChangeMessage<WithVirtualProps<TOutput, TKey>>>,
|
|
123
|
+
) => void,
|
|
124
|
+
options: SubscribeChangesOptions<TOutput, TKey> = {},
|
|
102
125
|
): CollectionSubscription {
|
|
103
126
|
// Start sync and track subscriber
|
|
104
127
|
this.addSubscriber()
|
|
@@ -113,7 +136,7 @@ export class CollectionChangesManager<
|
|
|
113
136
|
const { where, ...opts } = options
|
|
114
137
|
let whereExpression = opts.whereExpression
|
|
115
138
|
if (where) {
|
|
116
|
-
const proxy = createSingleRowRefProxy<TOutput
|
|
139
|
+
const proxy = createSingleRowRefProxy<WithVirtualProps<TOutput, TKey>>()
|
|
117
140
|
const result = where(proxy)
|
|
118
141
|
whereExpression = toExpression(result)
|
|
119
142
|
}
|