@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,238 @@
1
+ # Electric Adapter Reference
2
+
3
+ ## Install
4
+
5
+ ```bash
6
+ pnpm add @tanstack/electric-db-collection @tanstack/react-db
7
+ ```
8
+
9
+ ## Required Config
10
+
11
+ ```typescript
12
+ import { createCollection } from '@tanstack/react-db'
13
+ import { electricCollectionOptions } from '@tanstack/electric-db-collection'
14
+
15
+ const collection = createCollection(
16
+ electricCollectionOptions({
17
+ shapeOptions: { url: '/api/todos' },
18
+ getKey: (item) => item.id,
19
+ }),
20
+ )
21
+ ```
22
+
23
+ - `shapeOptions` -- ElectricSQL ShapeStream config; `url` is the proxy URL to Electric
24
+ - `getKey` -- extracts unique key from each item
25
+
26
+ ## Optional Config
27
+
28
+ | Option | Default | Description |
29
+ | --------------------- | ------- | --------------------------------------------------- |
30
+ | `id` | (none) | Unique collection identifier |
31
+ | `schema` | (none) | StandardSchema validator |
32
+ | `shapeOptions.params` | (none) | Additional shape params (e.g. `{ table: 'todos' }`) |
33
+ | `onInsert` | (none) | Persistence handler; should return `{ txid }` |
34
+ | `onUpdate` | (none) | Persistence handler; should return `{ txid }` |
35
+ | `onDelete` | (none) | Persistence handler; should return `{ txid }` |
36
+
37
+ ## Three Sync Strategies
38
+
39
+ ### 1. Txid Return (Recommended)
40
+
41
+ Handler returns `{ txid }`. Client waits for that txid in the Electric stream.
42
+
43
+ ```typescript
44
+ onInsert: async ({ transaction }) => {
45
+ const response = await api.todos.create(transaction.mutations[0].modified)
46
+ return { txid: response.txid }
47
+ },
48
+ ```
49
+
50
+ ### 2. awaitMatch (Custom Match)
51
+
52
+ Use when txids are unavailable. Import `isChangeMessage` to match on message content.
53
+
54
+ ```typescript
55
+ import { isChangeMessage } from "@tanstack/electric-db-collection"
56
+
57
+ onInsert: async ({ transaction, collection }) => {
58
+ const newItem = transaction.mutations[0].modified
59
+ await api.todos.create(newItem)
60
+ await collection.utils.awaitMatch(
61
+ (message) =>
62
+ isChangeMessage(message) &&
63
+ message.headers.operation === "insert" &&
64
+ message.value.text === newItem.text,
65
+ 5000 // timeout ms, defaults to 3000
66
+ )
67
+ },
68
+ ```
69
+
70
+ ### 3. Simple Timeout (Prototyping)
71
+
72
+ ```typescript
73
+ onInsert: async ({ transaction }) => {
74
+ await api.todos.create(transaction.mutations[0].modified)
75
+ await new Promise((resolve) => setTimeout(resolve, 2000))
76
+ },
77
+ ```
78
+
79
+ ## Utility Methods (`collection.utils`)
80
+
81
+ - `awaitTxId(txid, timeout?)` -- wait for txid in Electric stream; default timeout 30s
82
+ - `awaitMatch(matchFn, timeout?)` -- wait for message matching predicate; default timeout 3000ms
83
+
84
+ ### Helper Exports
85
+
86
+ ```typescript
87
+ import {
88
+ isChangeMessage,
89
+ isControlMessage,
90
+ } from '@tanstack/electric-db-collection'
91
+ // isChangeMessage(msg) -- true for insert/update/delete
92
+ // isControlMessage(msg) -- true for up-to-date/must-refetch
93
+ ```
94
+
95
+ ## generateTxId Backend Pattern
96
+
97
+ The txid **must** be queried inside the same Postgres transaction as the mutation.
98
+
99
+ ```typescript
100
+ async function generateTxId(tx: any): Promise<number> {
101
+ const result = await tx`SELECT pg_current_xact_id()::xid::text as txid`
102
+ const txid = result[0]?.txid
103
+ if (txid === undefined) throw new Error('Failed to get transaction ID')
104
+ return parseInt(txid, 10)
105
+ }
106
+
107
+ async function createTodo(data) {
108
+ let txid!: number
109
+ const result = await sql.begin(async (tx) => {
110
+ txid = await generateTxId(tx) // INSIDE the transaction
111
+ const [todo] = await tx`INSERT INTO todos ${tx(data)} RETURNING *`
112
+ return todo
113
+ })
114
+ return { todo: result, txid }
115
+ }
116
+ ```
117
+
118
+ Querying txid outside the transaction produces a mismatched txid -- `awaitTxId` stalls indefinitely.
119
+
120
+ ## Schema vs Parser: Two Separate Paths
121
+
122
+ When using Electric with a schema, data enters the collection via **two independent paths**:
123
+
124
+ 1. **Sync path** — Electric's `ShapeStream` applies the `parser` from `shapeOptions`. The schema is NOT applied to synced data.
125
+ 2. **Mutation path** — `insert()` and `update()` run through the collection schema. The parser is not involved.
126
+
127
+ For types that need transformation (e.g., `timestamptz`), you need BOTH configured:
128
+
129
+ ```typescript
130
+ const todosCollection = createCollection(
131
+ electricCollectionOptions({
132
+ schema: z.object({
133
+ id: z.string(),
134
+ text: z.string(),
135
+ completed: z.boolean(), // Electric auto-parses bools
136
+ created_at: z.coerce.date(), // mutation path: coerce string → Date
137
+ }),
138
+ shapeOptions: {
139
+ url: '/api/todos',
140
+ parser: {
141
+ timestamptz: (value: string) => new Date(value), // sync path: parse incoming strings
142
+ },
143
+ },
144
+ getKey: (item) => item.id,
145
+ }),
146
+ )
147
+ ```
148
+
149
+ ### Postgres → Electric type handling
150
+
151
+ | PG type | Electric auto-parses? | Schema needed? | Parser needed? |
152
+ | -------------- | --------------------- | ----------------- | --------------------------------------------------- |
153
+ | `text`, `uuid` | Yes (string) | `z.string()` | No |
154
+ | `int4`, `int8` | Yes (number) | `z.number()` | No |
155
+ | `bool` | Yes (boolean) | `z.boolean()` | No |
156
+ | `timestamptz` | No (stays string) | `z.coerce.date()` | Yes — `parser: { timestamptz: (v) => new Date(v) }` |
157
+ | `jsonb` | Yes (parsed object) | As needed | No |
158
+
159
+ Note: `z.coerce.date()` is Zod-specific. Other StandardSchema libraries have their own coercion patterns.
160
+
161
+ ## Proxy Route
162
+
163
+ Electric collections connect to a proxy URL (`shapeOptions.url`), not directly to Electric. Your app server must forward shape requests to Electric, passing through the Electric protocol query params.
164
+
165
+ The proxy route must:
166
+
167
+ 1. Accept GET requests at the URL you specify in `shapeOptions.url`
168
+ 2. Forward all query parameters (these are Electric protocol params like `offset`, `handle`, `live`, etc.)
169
+ 3. Proxy the response (SSE stream) back to the client
170
+ 4. Optionally add authentication headers or filter params
171
+
172
+ Implementation depends on your framework — use `createServerFn` in TanStack Start, API routes in Next.js, `loader` in Remix, etc. See the `@electric-sql/client` skills for proxy route examples:
173
+
174
+ ```bash
175
+ npx @electric-sql/client intent list
176
+ ```
177
+
178
+ ## Electric Client Skills
179
+
180
+ For deeper Electric-specific guidance (ShapeStream config, shape filtering, etc.), load the Electric client's built-in skills:
181
+
182
+ ```bash
183
+ npx @electric-sql/client intent list
184
+ ```
185
+
186
+ ## Debug Logging
187
+
188
+ ```javascript
189
+ localStorage.debug = 'ts/db:electric'
190
+ ```
191
+
192
+ ## Complete Example
193
+
194
+ Always use a schema — types are inferred automatically, avoiding generic placement confusion.
195
+
196
+ ```typescript
197
+ import { createCollection } from '@tanstack/react-db'
198
+ import { electricCollectionOptions } from '@tanstack/electric-db-collection'
199
+ import { z } from 'zod'
200
+
201
+ const todoSchema = z.object({
202
+ id: z.string(),
203
+ text: z.string().min(1),
204
+ completed: z.boolean(),
205
+ created_at: z.coerce.date(),
206
+ })
207
+
208
+ const todosCollection = createCollection(
209
+ electricCollectionOptions({
210
+ id: 'todos',
211
+ schema: todoSchema,
212
+ getKey: (item) => item.id,
213
+ shapeOptions: {
214
+ url: '/api/todos',
215
+ params: { table: 'todos' },
216
+ parser: {
217
+ timestamptz: (value: string) => new Date(value), // sync path
218
+ },
219
+ },
220
+ onInsert: async ({ transaction }) => {
221
+ const response = await api.todos.create(transaction.mutations[0].modified)
222
+ return { txid: response.txid }
223
+ },
224
+ onUpdate: async ({ transaction }) => {
225
+ const { original, changes } = transaction.mutations[0]
226
+ const response = await api.todos.update({
227
+ where: { id: original.id },
228
+ data: changes,
229
+ })
230
+ return { txid: response.txid }
231
+ },
232
+ onDelete: async ({ transaction }) => {
233
+ const response = await api.todos.delete(transaction.mutations[0].key)
234
+ return { txid: response.txid }
235
+ },
236
+ }),
237
+ )
238
+ ```
@@ -0,0 +1,220 @@
1
+ # Local Adapters Reference
2
+
3
+ Both adapters are included in the core package.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm add @tanstack/react-db
9
+ ```
10
+
11
+ ---
12
+
13
+ ## localOnlyCollectionOptions
14
+
15
+ In-memory only. No persistence. No cross-tab sync.
16
+
17
+ ### Required Config
18
+
19
+ ```typescript
20
+ import {
21
+ createCollection,
22
+ localOnlyCollectionOptions,
23
+ } from '@tanstack/react-db'
24
+
25
+ const collection = createCollection(
26
+ localOnlyCollectionOptions({
27
+ id: 'ui-state',
28
+ getKey: (item) => item.id,
29
+ }),
30
+ )
31
+ ```
32
+
33
+ - `id` -- unique collection identifier
34
+ - `getKey` -- extracts unique key from each item
35
+
36
+ ### Optional Config
37
+
38
+ | Option | Default | Description |
39
+ | ------------- | ------- | -------------------------------------- |
40
+ | `schema` | (none) | StandardSchema validator |
41
+ | `initialData` | (none) | Array of items to populate on creation |
42
+ | `onInsert` | (none) | Handler before confirming inserts |
43
+ | `onUpdate` | (none) | Handler before confirming updates |
44
+ | `onDelete` | (none) | Handler before confirming deletes |
45
+
46
+ ### Direct Mutations
47
+
48
+ ```typescript
49
+ collection.insert({ id: 'theme', mode: 'dark' })
50
+ collection.update('theme', (draft) => {
51
+ draft.mode = 'light'
52
+ })
53
+ collection.delete('theme')
54
+ ```
55
+
56
+ ### initialData
57
+
58
+ ```typescript
59
+ localOnlyCollectionOptions({
60
+ id: 'ui-state',
61
+ getKey: (item) => item.id,
62
+ initialData: [
63
+ { id: 'sidebar', isOpen: false },
64
+ { id: 'theme', mode: 'light' },
65
+ ],
66
+ })
67
+ ```
68
+
69
+ ### acceptMutations in Manual Transactions
70
+
71
+ When using `createTransaction`, call `collection.utils.acceptMutations(transaction)` in `mutationFn`:
72
+
73
+ ```typescript
74
+ import { createTransaction } from '@tanstack/react-db'
75
+
76
+ const tx = createTransaction({
77
+ mutationFn: async ({ transaction }) => {
78
+ // Handle server mutations first, then:
79
+ localData.utils.acceptMutations(transaction)
80
+ },
81
+ })
82
+ tx.mutate(() => {
83
+ localData.insert({ id: 'draft-1', data: '...' })
84
+ })
85
+ await tx.commit()
86
+ ```
87
+
88
+ ---
89
+
90
+ ## localStorageCollectionOptions
91
+
92
+ Persists to `localStorage`. Cross-tab sync via storage events. Survives reloads.
93
+
94
+ ### Required Config
95
+
96
+ ```typescript
97
+ import {
98
+ createCollection,
99
+ localStorageCollectionOptions,
100
+ } from '@tanstack/react-db'
101
+
102
+ const collection = createCollection(
103
+ localStorageCollectionOptions({
104
+ id: 'user-preferences',
105
+ storageKey: 'app-user-prefs',
106
+ getKey: (item) => item.id,
107
+ }),
108
+ )
109
+ ```
110
+
111
+ - `id` -- unique collection identifier
112
+ - `storageKey` -- localStorage key for all collection data
113
+ - `getKey` -- extracts unique key from each item
114
+
115
+ ### Optional Config
116
+
117
+ | Option | Default | Description |
118
+ | ----------------- | -------------- | -------------------------------------------------------------------- |
119
+ | `schema` | (none) | StandardSchema validator |
120
+ | `storage` | `localStorage` | Custom storage (`sessionStorage` or any localStorage-compatible API) |
121
+ | `storageEventApi` | `window` | Event API for cross-tab sync |
122
+ | `onInsert` | (none) | Handler on insert |
123
+ | `onUpdate` | (none) | Handler on update |
124
+ | `onDelete` | (none) | Handler on delete |
125
+
126
+ ### Using sessionStorage
127
+
128
+ ```typescript
129
+ localStorageCollectionOptions({
130
+ id: 'session-data',
131
+ storageKey: 'session-key',
132
+ storage: sessionStorage,
133
+ getKey: (item) => item.id,
134
+ })
135
+ ```
136
+
137
+ ### Custom Storage Backend
138
+
139
+ Provide any object with `getItem`, `setItem`, `removeItem`:
140
+
141
+ ```typescript
142
+ const encryptedStorage = {
143
+ getItem: (key) => {
144
+ const v = localStorage.getItem(key)
145
+ return v ? decrypt(v) : null
146
+ },
147
+ setItem: (key, value) => localStorage.setItem(key, encrypt(value)),
148
+ removeItem: (key) => localStorage.removeItem(key),
149
+ }
150
+ localStorageCollectionOptions({
151
+ id: 'secure',
152
+ storageKey: 'enc-key',
153
+ storage: encryptedStorage,
154
+ getKey: (i) => i.id,
155
+ })
156
+ ```
157
+
158
+ ### acceptMutations
159
+
160
+ Same as LocalOnly -- call `collection.utils.acceptMutations(transaction)` in manual transactions.
161
+
162
+ ---
163
+
164
+ ## Comparison
165
+
166
+ | Feature | LocalOnly | LocalStorage |
167
+ | --------------- | ---------------- | ------------ |
168
+ | Persistence | None (in-memory) | localStorage |
169
+ | Cross-tab sync | No | Yes |
170
+ | Survives reload | No | Yes |
171
+ | Performance | Fastest | Fast |
172
+ | Size limits | Memory | ~5-10MB |
173
+
174
+ ## Complete Example
175
+
176
+ ```typescript
177
+ import {
178
+ createCollection,
179
+ localOnlyCollectionOptions,
180
+ localStorageCollectionOptions,
181
+ } from '@tanstack/react-db'
182
+ import { z } from 'zod'
183
+
184
+ // In-memory UI state
185
+ const modalState = createCollection(
186
+ localOnlyCollectionOptions({
187
+ id: 'modal-state',
188
+ getKey: (item) => item.id,
189
+ initialData: [
190
+ { id: 'confirm-delete', isOpen: false },
191
+ { id: 'settings', isOpen: false },
192
+ ],
193
+ }),
194
+ )
195
+
196
+ // Persistent user prefs
197
+ const userPrefs = createCollection(
198
+ localStorageCollectionOptions({
199
+ id: 'user-preferences',
200
+ storageKey: 'app-user-prefs',
201
+ getKey: (item) => item.id,
202
+ schema: z.object({
203
+ id: z.string(),
204
+ theme: z.enum(['light', 'dark', 'auto']),
205
+ language: z.string(),
206
+ notifications: z.boolean(),
207
+ }),
208
+ }),
209
+ )
210
+
211
+ modalState.update('settings', (draft) => {
212
+ draft.isOpen = true
213
+ })
214
+ userPrefs.insert({
215
+ id: 'current-user',
216
+ theme: 'dark',
217
+ language: 'en',
218
+ notifications: true,
219
+ })
220
+ ```
@@ -0,0 +1,241 @@
1
+ # PowerSync Adapter Reference
2
+
3
+ ## Install
4
+
5
+ ```bash
6
+ pnpm add @tanstack/powersync-db-collection @powersync/web @journeyapps/wa-sqlite
7
+ ```
8
+
9
+ ## Required Config
10
+
11
+ ```typescript
12
+ import { createCollection } from '@tanstack/react-db'
13
+ import { powerSyncCollectionOptions } from '@tanstack/powersync-db-collection'
14
+ import { Schema, Table, column, PowerSyncDatabase } from '@powersync/web'
15
+
16
+ const APP_SCHEMA = new Schema({
17
+ documents: new Table({
18
+ name: column.text,
19
+ author: column.text,
20
+ created_at: column.text,
21
+ archived: column.integer,
22
+ }),
23
+ })
24
+
25
+ const db = new PowerSyncDatabase({
26
+ database: { dbFilename: 'app.sqlite' },
27
+ schema: APP_SCHEMA,
28
+ })
29
+
30
+ const documentsCollection = createCollection(
31
+ powerSyncCollectionOptions({
32
+ database: db,
33
+ table: APP_SCHEMA.props.documents,
34
+ }),
35
+ )
36
+ ```
37
+
38
+ - `database` -- `PowerSyncDatabase` instance
39
+ - `table` -- PowerSync `Table` from schema (provides `getKey` and type inference)
40
+
41
+ ## Optional Config (with defaults)
42
+
43
+ | Option | Default | Description |
44
+ | ------------------------ | ------- | ------------------------------------------------------------------------------------- |
45
+ | `schema` | (none) | StandardSchema for mutation validation |
46
+ | `deserializationSchema` | (none) | Transforms SQLite types to output types; required when input types differ from SQLite |
47
+ | `onDeserializationError` | (none) | Fatal error handler; **required** when using `schema` or `deserializationSchema` |
48
+ | `serializer` | (none) | Per-field functions to serialize output types back to SQLite |
49
+ | `syncBatchSize` | `1000` | Batch size for initial sync |
50
+
51
+ ### SQLite Type Mapping
52
+
53
+ | PowerSync Column | TypeScript Type |
54
+ | ---------------- | ---------------- |
55
+ | `column.text` | `string \| null` |
56
+ | `column.integer` | `number \| null` |
57
+ | `column.real` | `number \| null` |
58
+
59
+ All columns nullable by default. `id: string` is always included automatically.
60
+
61
+ ## Conversions (4 patterns)
62
+
63
+ ### 1. Type Inference Only (no schema)
64
+
65
+ ```typescript
66
+ const collection = createCollection(
67
+ powerSyncCollectionOptions({
68
+ database: db,
69
+ table: APP_SCHEMA.props.documents,
70
+ }),
71
+ )
72
+ // Input/Output: { id: string, name: string | null, created_at: string | null, ... }
73
+ ```
74
+
75
+ ### 2. Schema Validation (same SQLite types)
76
+
77
+ ```typescript
78
+ const schema = z.object({
79
+ id: z.string(),
80
+ name: z.string().min(3),
81
+ author: z.string(),
82
+ created_at: z.string(),
83
+ archived: z.number(),
84
+ })
85
+ const collection = createCollection(
86
+ powerSyncCollectionOptions({
87
+ database: db,
88
+ table: APP_SCHEMA.props.documents,
89
+ schema,
90
+ onDeserializationError: (error) => {
91
+ /* fatal */
92
+ },
93
+ }),
94
+ )
95
+ ```
96
+
97
+ ### 3. Transform SQLite to Rich Output Types
98
+
99
+ ```typescript
100
+ const schema = z.object({
101
+ id: z.string(),
102
+ name: z.string().nullable(),
103
+ created_at: z
104
+ .string()
105
+ .nullable()
106
+ .transform((val) => (val ? new Date(val) : null)),
107
+ archived: z
108
+ .number()
109
+ .nullable()
110
+ .transform((val) => (val != null ? val > 0 : null)),
111
+ })
112
+ const collection = createCollection(
113
+ powerSyncCollectionOptions({
114
+ database: db,
115
+ table: APP_SCHEMA.props.documents,
116
+ schema,
117
+ onDeserializationError: (error) => {
118
+ /* fatal */
119
+ },
120
+ serializer: { created_at: (value) => (value ? value.toISOString() : null) },
121
+ }),
122
+ )
123
+ // Input: { created_at: string | null, ... }
124
+ // Output: { created_at: Date | null, archived: boolean | null, ... }
125
+ ```
126
+
127
+ ### 4. Custom Input + Output with deserializationSchema
128
+
129
+ ```typescript
130
+ const schema = z.object({
131
+ id: z.string(),
132
+ name: z.string(),
133
+ created_at: z.date(),
134
+ archived: z.boolean(),
135
+ })
136
+ const deserializationSchema = z.object({
137
+ id: z.string(),
138
+ name: z.string(),
139
+ created_at: z.string().transform((val) => new Date(val)),
140
+ archived: z.number().transform((val) => val > 0),
141
+ })
142
+ const collection = createCollection(
143
+ powerSyncCollectionOptions({
144
+ database: db,
145
+ table: APP_SCHEMA.props.documents,
146
+ schema,
147
+ deserializationSchema,
148
+ onDeserializationError: (error) => {
149
+ /* fatal */
150
+ },
151
+ }),
152
+ )
153
+ // Input: { created_at: Date, archived: boolean }
154
+ // Output: { created_at: Date, archived: boolean }
155
+ ```
156
+
157
+ ## Metadata Tracking
158
+
159
+ Enable on the table, then pass metadata with operations:
160
+
161
+ ```typescript
162
+ const APP_SCHEMA = new Schema({
163
+ documents: new Table({ name: column.text }, { trackMetadata: true }),
164
+ })
165
+
166
+ await collection.insert(
167
+ { id: crypto.randomUUID(), name: 'Report' },
168
+ { metadata: { source: 'web-app', userId: 'user-123' } },
169
+ ).isPersisted.promise
170
+ ```
171
+
172
+ Metadata appears as `entry.metadata` (stringified JSON) in PowerSync `CrudEntry`.
173
+
174
+ ## Advanced Transactions
175
+
176
+ ```typescript
177
+ import { createTransaction } from '@tanstack/react-db'
178
+ import { PowerSyncTransactor } from '@tanstack/powersync-db-collection'
179
+
180
+ const tx = createTransaction({
181
+ autoCommit: false,
182
+ mutationFn: async ({ transaction }) => {
183
+ await new PowerSyncTransactor({ database: db }).applyTransaction(
184
+ transaction,
185
+ )
186
+ },
187
+ })
188
+ tx.mutate(() => {
189
+ documentsCollection.insert({
190
+ id: crypto.randomUUID(),
191
+ name: 'Doc 1',
192
+ created_at: new Date().toISOString(),
193
+ })
194
+ })
195
+ await tx.commit()
196
+ await tx.isPersisted.promise
197
+ ```
198
+
199
+ ## Complete Example
200
+
201
+ ```typescript
202
+ import { Schema, Table, column, PowerSyncDatabase } from '@powersync/web'
203
+ import { createCollection } from '@tanstack/react-db'
204
+ import { powerSyncCollectionOptions } from '@tanstack/powersync-db-collection'
205
+ import { z } from 'zod'
206
+
207
+ const APP_SCHEMA = new Schema({
208
+ tasks: new Table({
209
+ title: column.text,
210
+ due_date: column.text,
211
+ completed: column.integer,
212
+ }),
213
+ })
214
+ const db = new PowerSyncDatabase({
215
+ database: { dbFilename: 'app.sqlite' },
216
+ schema: APP_SCHEMA,
217
+ })
218
+
219
+ const taskSchema = z.object({
220
+ id: z.string(),
221
+ title: z.string().nullable(),
222
+ due_date: z
223
+ .string()
224
+ .nullable()
225
+ .transform((val) => (val ? new Date(val) : null)),
226
+ completed: z
227
+ .number()
228
+ .nullable()
229
+ .transform((val) => (val != null ? val > 0 : null)),
230
+ })
231
+
232
+ const tasksCollection = createCollection(
233
+ powerSyncCollectionOptions({
234
+ database: db,
235
+ table: APP_SCHEMA.props.tasks,
236
+ schema: taskSchema,
237
+ onDeserializationError: (error) => console.error('Fatal:', error),
238
+ syncBatchSize: 500,
239
+ }),
240
+ )
241
+ ```