@tanstack/db 0.5.30 → 0.5.31

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.
@@ -0,0 +1,207 @@
1
+ # Transaction API Reference
2
+
3
+ ## createTransaction
4
+
5
+ ```ts
6
+ import { createTransaction } from "@tanstack/db"
7
+
8
+ const tx = createTransaction<T>({
9
+ id?: string, // defaults to crypto.randomUUID()
10
+ autoCommit?: boolean, // default true -- commit after mutate()
11
+ mutationFn: MutationFn<T>, // (params: { transaction }) => Promise<any>
12
+ metadata?: Record<string, unknown>, // custom data attached to the transaction
13
+ })
14
+ ```
15
+
16
+ ## Transaction Object
17
+
18
+ ```ts
19
+ interface Transaction<T> {
20
+ id: string
21
+ state: 'pending' | 'persisting' | 'completed' | 'failed'
22
+ mutations: Array<PendingMutation<T>>
23
+ autoCommit: boolean
24
+ createdAt: Date
25
+ sequenceNumber: number
26
+ metadata: Record<string, unknown>
27
+ error?: { message: string; error: Error }
28
+
29
+ // Deferred promise -- resolves when mutationFn completes, rejects on failure
30
+ isPersisted: {
31
+ promise: Promise<Transaction<T>>
32
+ resolve: (value: Transaction<T>) => void
33
+ reject: (reason?: any) => void
34
+ }
35
+
36
+ // Execute collection operations inside the ambient transaction context
37
+ mutate(callback: () => void): Transaction<T>
38
+
39
+ // Commit -- calls mutationFn, transitions to persisting -> completed|failed
40
+ commit(): Promise<Transaction<T>>
41
+
42
+ // Rollback -- transitions to failed, also rolls back conflicting transactions
43
+ rollback(config?: { isSecondaryRollback?: boolean }): Transaction<T>
44
+ }
45
+ ```
46
+
47
+ **Lifecycle:** `pending` -> `persisting` -> `completed` | `failed`
48
+
49
+ - `mutate()` only allowed in `pending` state (throws `TransactionNotPendingMutateError`)
50
+ - `commit()` only allowed in `pending` state (throws `TransactionNotPendingCommitError`)
51
+ - `rollback()` allowed in `pending` or `persisting` (throws `TransactionAlreadyCompletedRollbackError` if completed)
52
+ - Failed `mutationFn` automatically triggers `rollback()`
53
+ - Rollback cascades to other pending transactions sharing the same item keys
54
+
55
+ ## PendingMutation Type
56
+
57
+ ```ts
58
+ interface PendingMutation<T, TOperation = 'insert' | 'update' | 'delete'> {
59
+ mutationId: string // unique id for this mutation
60
+ original: TOperation extends 'insert' ? {} : T // state before mutation
61
+ modified: T // state after mutation
62
+ changes: Partial<T> // only the changed fields
63
+ key: any // collection-local key
64
+ globalKey: string // globally unique key (collectionId + key)
65
+ type: TOperation // "insert" | "update" | "delete"
66
+ metadata: unknown // user-provided metadata
67
+ syncMetadata: Record<string, unknown> // adapter-specific metadata
68
+ optimistic: boolean // whether applied optimistically (default true)
69
+ createdAt: Date
70
+ updatedAt: Date
71
+ collection: Collection // reference to the source collection
72
+ }
73
+ ```
74
+
75
+ ## Mutation Merging Rules
76
+
77
+ When multiple mutations target the same item (same `globalKey`) within a
78
+ transaction, they merge:
79
+
80
+ | Existing | Incoming | Result | Notes |
81
+ | -------- | -------- | --------- | ---------------------------------- |
82
+ | insert | update | insert | Merge changes, keep empty original |
83
+ | insert | delete | _removed_ | Both mutations cancel out |
84
+ | update | update | update | Union changes, keep first original |
85
+ | update | delete | delete | Delete dominates |
86
+ | delete | delete | delete | Replace with latest |
87
+ | insert | insert | insert | Replace with latest |
88
+
89
+ `(delete, update)` and `(delete, insert)` cannot occur -- the collection
90
+ prevents operations on deleted items within the same transaction.
91
+
92
+ ## getActiveTransaction / Ambient Transaction Context
93
+
94
+ ```ts
95
+ import { getActiveTransaction } from '@tanstack/db'
96
+
97
+ const tx = getActiveTransaction() // Transaction | undefined
98
+ ```
99
+
100
+ Inside `tx.mutate(() => { ... })`, the transaction is pushed onto an internal
101
+ stack. Any `collection.insert/update/delete` call automatically joins the
102
+ topmost ambient transaction. This is how `createOptimisticAction` and
103
+ `createPacedMutations` wire collection operations into their transactions.
104
+
105
+ ## createOptimisticAction
106
+
107
+ ```ts
108
+ import { createOptimisticAction } from "@tanstack/db"
109
+
110
+ const action = createOptimisticAction<TVariables>({
111
+ // Synchronous -- apply optimistic state immediately (MUST NOT return a Promise)
112
+ onMutate: (variables: TVariables) => void,
113
+
114
+ // Async -- persist to backend, wait for sync back
115
+ mutationFn: (variables: TVariables, params: { transaction }) => Promise<any>,
116
+
117
+ // Optional: same as createTransaction config
118
+ id?: string,
119
+ autoCommit?: boolean, // always true (commit happens after mutate)
120
+ metadata?: Record<string, unknown>,
121
+ })
122
+
123
+ // Returns a function: (variables: TVariables) => Transaction
124
+ const tx = action(variables)
125
+ await tx.isPersisted.promise
126
+ ```
127
+
128
+ ## createPacedMutations
129
+
130
+ ```ts
131
+ import { createPacedMutations } from "@tanstack/db"
132
+
133
+ const mutate = createPacedMutations<TVariables>({
134
+ onMutate: (variables: TVariables) => void, // synchronous optimistic update
135
+ mutationFn: MutationFn, // persists merged transaction
136
+ strategy: Strategy, // timing control
137
+ metadata?: Record<string, unknown>,
138
+ })
139
+
140
+ // Returns a function: (variables: TVariables) => Transaction
141
+ const tx = mutate(variables)
142
+ ```
143
+
144
+ Rapid calls merge into the active transaction (via `applyMutations`) until the
145
+ strategy fires the commit. A new transaction is created for subsequent calls.
146
+
147
+ ## Strategy Types
148
+
149
+ ### debounceStrategy
150
+
151
+ ```ts
152
+ import { debounceStrategy } from "@tanstack/db"
153
+
154
+ debounceStrategy({
155
+ wait: number, // ms to wait after last call before committing
156
+ leading?: boolean, // execute on the leading edge (default false)
157
+ trailing?: boolean, // execute on the trailing edge (default true)
158
+ })
159
+ ```
160
+
161
+ ### throttleStrategy
162
+
163
+ ```ts
164
+ import { throttleStrategy } from "@tanstack/db"
165
+
166
+ throttleStrategy({
167
+ wait: number, // minimum ms between commits
168
+ leading?: boolean, // execute on the leading edge
169
+ trailing?: boolean, // execute on the trailing edge
170
+ })
171
+ ```
172
+
173
+ ### queueStrategy
174
+
175
+ ```ts
176
+ import { queueStrategy } from "@tanstack/db"
177
+
178
+ queueStrategy({
179
+ wait?: number, // ms between processing items (default 0)
180
+ maxSize?: number, // drop items if queue exceeds this
181
+ addItemsTo?: "front" | "back", // default "back" (FIFO)
182
+ getItemsFrom?: "front" | "back", // default "front" (FIFO)
183
+ })
184
+ ```
185
+
186
+ Queue creates a **separate transaction per call** (unlike debounce/throttle
187
+ which merge). Each transaction commits and awaits `isPersisted` before the next
188
+ starts. Failed transactions do not block subsequent ones.
189
+
190
+ ## Transaction.isPersisted.promise
191
+
192
+ ```ts
193
+ const tx = collection.insert({ id: '1', text: 'Hello' })
194
+
195
+ try {
196
+ await tx.isPersisted.promise // resolves with the Transaction on success
197
+ console.log(tx.state) // "completed"
198
+ } catch (error) {
199
+ console.log(tx.state) // "failed"
200
+ // optimistic state has been rolled back
201
+ }
202
+ ```
203
+
204
+ The promise is a `Deferred` -- it is created at transaction construction time
205
+ and settled when `commit()` completes or `rollback()` is called. For
206
+ `autoCommit: true` transactions, the promise settles shortly after `mutate()`
207
+ returns (the commit runs asynchronously).
@@ -0,0 +1,361 @@
1
+ ---
2
+ name: meta-framework
3
+ description: >
4
+ Integrating TanStack DB with meta-frameworks (TanStack Start, Next.js,
5
+ Remix, Nuxt, SvelteKit). Client-side only: SSR is NOT supported — routes
6
+ must disable SSR. Preloading collections in route loaders with
7
+ collection.preload(). Pattern: ssr: false + await collection.preload() in
8
+ loader. Multiple collection preloading with Promise.all. Framework-specific
9
+ loader APIs.
10
+ type: composition
11
+ library: db
12
+ library_version: '0.5.30'
13
+ requires:
14
+ - db-core
15
+ - db-core/collection-setup
16
+ sources:
17
+ - 'TanStack/db:examples/react/todo/src/routes/electric.tsx'
18
+ - 'TanStack/db:examples/react/todo/src/routes/query.tsx'
19
+ - 'TanStack/db:examples/react/todo/src/start.tsx'
20
+ ---
21
+
22
+ This skill builds on db-core. Read it first for collection setup and query builder.
23
+
24
+ # TanStack DB — Meta-Framework Integration
25
+
26
+ ## Setup
27
+
28
+ TanStack DB collections are **client-side only**. SSR is not implemented. Routes using TanStack DB **must disable SSR**. The setup pattern is:
29
+
30
+ 1. Set `ssr: false` on the route
31
+ 2. Call `collection.preload()` in the route loader
32
+ 3. Use `useLiveQuery` in the component
33
+
34
+ ## TanStack Start
35
+
36
+ ### Global SSR disable
37
+
38
+ ```ts
39
+ // start.tsx
40
+ import { createStart } from '@tanstack/react-start'
41
+
42
+ export const startInstance = createStart(() => {
43
+ return {
44
+ defaultSsr: false,
45
+ }
46
+ })
47
+ ```
48
+
49
+ ### Per-route SSR disable + preload
50
+
51
+ ```tsx
52
+ import { createFileRoute } from '@tanstack/react-router'
53
+ import { useLiveQuery } from '@tanstack/react-db'
54
+
55
+ export const Route = createFileRoute('/todos')({
56
+ ssr: false,
57
+ loader: async () => {
58
+ await todoCollection.preload()
59
+ return null
60
+ },
61
+ component: TodoPage,
62
+ })
63
+
64
+ function TodoPage() {
65
+ const { data: todos } = useLiveQuery((q) => q.from({ todo: todoCollection }))
66
+ return (
67
+ <ul>
68
+ {todos.map((t) => (
69
+ <li key={t.id}>{t.text}</li>
70
+ ))}
71
+ </ul>
72
+ )
73
+ }
74
+ ```
75
+
76
+ ### Multiple collection preloading
77
+
78
+ ```tsx
79
+ export const Route = createFileRoute('/electric')({
80
+ ssr: false,
81
+ loader: async () => {
82
+ await Promise.all([todoCollection.preload(), configCollection.preload()])
83
+ return null
84
+ },
85
+ component: ElectricPage,
86
+ })
87
+ ```
88
+
89
+ ## Next.js (App Router)
90
+
91
+ ### Client component with preloading
92
+
93
+ ```tsx
94
+ // app/todos/page.tsx
95
+ 'use client'
96
+
97
+ import { useEffect, useState } from 'react'
98
+ import { useLiveQuery } from '@tanstack/react-db'
99
+
100
+ export default function TodoPage() {
101
+ const { data: todos, isLoading } = useLiveQuery((q) =>
102
+ q.from({ todo: todoCollection }),
103
+ )
104
+
105
+ if (isLoading) return <div>Loading...</div>
106
+ return (
107
+ <ul>
108
+ {todos.map((t) => (
109
+ <li key={t.id}>{t.text}</li>
110
+ ))}
111
+ </ul>
112
+ )
113
+ }
114
+ ```
115
+
116
+ Next.js App Router components using TanStack DB must be client components (`'use client'`). There is no server-side preloading — collections sync on mount.
117
+
118
+ ### With route-level preloading (experimental)
119
+
120
+ ```tsx
121
+ // app/todos/page.tsx
122
+ 'use client'
123
+
124
+ import { useEffect } from 'react'
125
+ import { useLiveQuery } from '@tanstack/react-db'
126
+
127
+ // Trigger preload immediately when module is loaded
128
+ const preloadPromise = todoCollection.preload()
129
+
130
+ export default function TodoPage() {
131
+ const { data: todos } = useLiveQuery((q) => q.from({ todo: todoCollection }))
132
+ return (
133
+ <ul>
134
+ {todos.map((t) => (
135
+ <li key={t.id}>{t.text}</li>
136
+ ))}
137
+ </ul>
138
+ )
139
+ }
140
+ ```
141
+
142
+ ## Remix
143
+
144
+ ### Client loader pattern
145
+
146
+ ```tsx
147
+ // app/routes/todos.tsx
148
+ import { useLiveQuery } from '@tanstack/react-db'
149
+ import type { ClientLoaderFunctionArgs } from '@remix-run/react'
150
+
151
+ export const clientLoader = async ({ request }: ClientLoaderFunctionArgs) => {
152
+ await todoCollection.preload()
153
+ return null
154
+ }
155
+
156
+ // Prevent server loader from running
157
+ export const loader = () => null
158
+
159
+ export default function TodoPage() {
160
+ const { data: todos } = useLiveQuery((q) => q.from({ todo: todoCollection }))
161
+ return (
162
+ <ul>
163
+ {todos.map((t) => (
164
+ <li key={t.id}>{t.text}</li>
165
+ ))}
166
+ </ul>
167
+ )
168
+ }
169
+ ```
170
+
171
+ ## Nuxt
172
+
173
+ ### Client-only component
174
+
175
+ ```vue
176
+ <!-- pages/todos.vue -->
177
+ <script setup lang="ts">
178
+ import { useLiveQuery } from '@tanstack/vue-db'
179
+
180
+ const { data: todos, isLoading } = useLiveQuery((q) =>
181
+ q.from({ todo: todoCollection }),
182
+ )
183
+ </script>
184
+
185
+ <template>
186
+ <ClientOnly>
187
+ <div v-if="isLoading">Loading...</div>
188
+ <ul v-else>
189
+ <li v-for="todo in todos" :key="todo.id">{{ todo.text }}</li>
190
+ </ul>
191
+ </ClientOnly>
192
+ </template>
193
+ ```
194
+
195
+ Wrap TanStack DB components in `<ClientOnly>` to prevent SSR.
196
+
197
+ ## SvelteKit
198
+
199
+ ### Client-side only page
200
+
201
+ ```svelte
202
+ <!-- src/routes/todos/+page.svelte -->
203
+ <script lang="ts">
204
+ import { browser } from '$app/environment'
205
+ import { useLiveQuery } from '@tanstack/svelte-db'
206
+
207
+ const todosQuery = browser
208
+ ? useLiveQuery((q) => q.from({ todo: todoCollection }))
209
+ : null
210
+ </script>
211
+
212
+ {#if todosQuery}
213
+ {#each todosQuery.data as todo (todo.id)}
214
+ <li>{todo.text}</li>
215
+ {/each}
216
+ {/if}
217
+ ```
218
+
219
+ Or disable SSR for the route:
220
+
221
+ ```ts
222
+ // src/routes/todos/+page.ts
223
+ export const ssr = false
224
+ ```
225
+
226
+ ## Core Patterns
227
+
228
+ ### What preload() does
229
+
230
+ `collection.preload()` starts the sync process and returns a promise that resolves when the collection reaches "ready" status. This means:
231
+
232
+ 1. The sync function connects to the backend
233
+ 2. Initial data is fetched and written to the collection
234
+ 3. `markReady()` is called by the adapter
235
+ 4. The promise resolves
236
+
237
+ Subsequent calls to `preload()` on an already-ready collection return immediately.
238
+
239
+ ### Collection module pattern
240
+
241
+ Define collections in a shared module, import in both loaders and components:
242
+
243
+ ```ts
244
+ // lib/collections.ts
245
+ import { createCollection, queryCollectionOptions } from '@tanstack/react-db'
246
+
247
+ export const todoCollection = createCollection(
248
+ queryCollectionOptions({ ... })
249
+ )
250
+ ```
251
+
252
+ ```tsx
253
+ // routes/todos.tsx — loader uses the same collection instance
254
+ import { todoCollection } from '../lib/collections'
255
+
256
+ export const Route = createFileRoute('/todos')({
257
+ ssr: false,
258
+ loader: async () => {
259
+ await todoCollection.preload()
260
+ return null
261
+ },
262
+ component: () => {
263
+ const { data } = useLiveQuery((q) => q.from({ todo: todoCollection }))
264
+ // ...
265
+ },
266
+ })
267
+ ```
268
+
269
+ ## Server-Side Integration
270
+
271
+ This skill covers the **client-side** read path only (preloading, live queries). For server-side concerns:
272
+
273
+ - **Electric proxy route** (forwarding shape requests to Electric) — see the [Electric adapter reference](../db-core/collection-setup/references/electric-adapter.md)
274
+ - **Mutation endpoints** (`createServerFn` in TanStack Start, API routes in Next.js/Remix) — implement using your framework's server function pattern. See the Electric adapter reference for the txid handshake that mutations must return.
275
+
276
+ ## Common Mistakes
277
+
278
+ ### CRITICAL Enabling SSR with TanStack DB
279
+
280
+ Wrong:
281
+
282
+ ```tsx
283
+ export const Route = createFileRoute('/todos')({
284
+ loader: async () => {
285
+ await todoCollection.preload()
286
+ return null
287
+ },
288
+ })
289
+ ```
290
+
291
+ Correct:
292
+
293
+ ```tsx
294
+ export const Route = createFileRoute('/todos')({
295
+ ssr: false,
296
+ loader: async () => {
297
+ await todoCollection.preload()
298
+ return null
299
+ },
300
+ })
301
+ ```
302
+
303
+ TanStack DB collections are client-side only. Without `ssr: false`, the route loader runs on the server where collections cannot sync, causing hangs or errors.
304
+
305
+ Source: examples/react/todo/src/start.tsx
306
+
307
+ ### HIGH Forgetting to preload in route loader
308
+
309
+ Wrong:
310
+
311
+ ```tsx
312
+ export const Route = createFileRoute('/todos')({
313
+ ssr: false,
314
+ component: TodoPage,
315
+ })
316
+ ```
317
+
318
+ Correct:
319
+
320
+ ```tsx
321
+ export const Route = createFileRoute('/todos')({
322
+ ssr: false,
323
+ loader: async () => {
324
+ await todoCollection.preload()
325
+ return null
326
+ },
327
+ component: TodoPage,
328
+ })
329
+ ```
330
+
331
+ Without preloading, the collection starts syncing only when the component mounts, causing a loading flash. Preloading in the route loader starts sync during navigation, making data available immediately when the component renders.
332
+
333
+ ### MEDIUM Creating separate collection instances
334
+
335
+ Wrong:
336
+
337
+ ```tsx
338
+ // routes/todos.tsx
339
+ const todoCollection = createCollection(queryCollectionOptions({ ... }))
340
+
341
+ export const Route = createFileRoute('/todos')({
342
+ ssr: false,
343
+ loader: async () => { await todoCollection.preload() },
344
+ component: () => {
345
+ const { data } = useLiveQuery((q) => q.from({ todo: todoCollection }))
346
+ },
347
+ })
348
+ ```
349
+
350
+ Correct:
351
+
352
+ ```ts
353
+ // lib/collections.ts — single shared instance
354
+ export const todoCollection = createCollection(queryCollectionOptions({ ... }))
355
+ ```
356
+
357
+ Collections are singletons. Creating multiple instances for the same data causes duplicate syncs, wasted bandwidth, and inconsistent state between components.
358
+
359
+ See also: react-db/SKILL.md, vue-db/SKILL.md, svelte-db/SKILL.md, solid-db/SKILL.md, angular-db/SKILL.md — for framework-specific hook usage.
360
+
361
+ See also: db-core/collection-setup/SKILL.md — for collection creation and adapter selection.