@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
|
@@ -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).
|
|
@@ -302,8 +302,20 @@ function processJoin(
|
|
|
302
302
|
return
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
-
//
|
|
306
|
-
const joinKeys =
|
|
305
|
+
// Deduplicate and filter null keys before requesting snapshot
|
|
306
|
+
const joinKeys = [
|
|
307
|
+
...new Set(
|
|
308
|
+
data
|
|
309
|
+
.getInner()
|
|
310
|
+
.map(([[joinKey]]) => joinKey)
|
|
311
|
+
.filter((key) => key != null),
|
|
312
|
+
),
|
|
313
|
+
]
|
|
314
|
+
|
|
315
|
+
if (joinKeys.length === 0) {
|
|
316
|
+
return
|
|
317
|
+
}
|
|
318
|
+
|
|
307
319
|
const lazyJoinRef = new PropRef(followRefResult.path)
|
|
308
320
|
const loaded = lazySourceSubscription.requestSnapshot({
|
|
309
321
|
where: inArray(lazyJoinRef, joinKeys),
|
|
@@ -227,7 +227,8 @@ export class CollectionConfigBuilder<
|
|
|
227
227
|
id: this.id,
|
|
228
228
|
getKey:
|
|
229
229
|
this.config.getKey ||
|
|
230
|
-
((item) =>
|
|
230
|
+
((item: any) =>
|
|
231
|
+
(this.resultKeys.get(item) ?? item.$key) as string | number),
|
|
231
232
|
sync: this.getSyncConfig(),
|
|
232
233
|
compare: this.compare,
|
|
233
234
|
defaultStringCollation: this.compareOptions,
|
|
@@ -1515,6 +1516,7 @@ function createChildCollectionEntry(
|
|
|
1515
1516
|
},
|
|
1516
1517
|
},
|
|
1517
1518
|
startSync: true,
|
|
1519
|
+
gcTime: 0,
|
|
1518
1520
|
})
|
|
1519
1521
|
|
|
1520
1522
|
const entry: ChildCollectionEntry = {
|