|
2 | 2 | name: db-core/custom-adapter |
3 | 3 | description: > |
4 | 4 | Building custom collection adapters for new backends. SyncConfig interface: |
5 | | - sync function receiving begin, write, commit, markReady, truncate primitives. |
6 | | - ChangeMessage format (insert, update, delete). loadSubset for on-demand sync. |
7 | | - LoadSubsetOptions (where, orderBy, limit, cursor). Expression parsing: |
8 | | - parseWhereExpression, parseOrderByExpression, extractSimpleComparisons, |
9 | | - parseLoadSubsetOptions. Collection options creator pattern. rowUpdateMode |
10 | | - (partial vs full). Subscription lifecycle and cleanup functions. |
| 5 | + sync function receiving begin, write, commit, markReady, truncate, metadata |
| 6 | + primitives. ChangeMessage format (insert, update, delete). loadSubset for |
| 7 | + on-demand sync. LoadSubsetOptions (where, orderBy, limit, cursor). Expression |
| 8 | + parsing: parseWhereExpression, parseOrderByExpression, |
| 9 | + extractSimpleComparisons, parseLoadSubsetOptions. Collection options creator |
| 10 | + pattern. rowUpdateMode (partial vs full). Subscription lifecycle and cleanup |
| 11 | + functions. Persisted sync metadata API (metadata.row and metadata.collection) |
| 12 | + for storing per-row and per-collection adapter state. |
11 | 13 | type: sub-skill |
12 | 14 | library: db |
13 | | -library_version: '0.5.30' |
| 15 | +library_version: '0.6.0' |
14 | 16 | sources: |
15 | 17 | - 'TanStack/db:docs/guides/collection-options-creator.md' |
16 | 18 | - 'TanStack/db:packages/db/src/collection/sync.ts' |
@@ -38,7 +40,7 @@ function myBackendCollectionOptions<T>(config: { |
38 | 40 | return { |
39 | 41 | getKey: config.getKey, |
40 | 42 | sync: { |
41 | | - sync: ({ begin, write, commit, markReady, collection }) => { |
| 43 | + sync: ({ begin, write, commit, markReady, metadata, collection }) => { |
42 | 44 | let isInitialSyncComplete = false |
43 | 45 | const bufferedEvents: Array<any> = [] |
44 | 46 |
|
@@ -157,6 +159,53 @@ Mutation handlers must not resolve until server changes have synced back to the |
157 | 159 | 4. **Version/timestamp**: wait until sync stream catches up to mutation time |
158 | 160 | 5. **Provider method**: `await backend.waitForPendingWrites()` |
159 | 161 |
|
| 162 | +### Persisted sync metadata |
| 163 | + |
| 164 | +The `metadata` API on the sync config allows adapters to store per-row and per-collection metadata that persists across sync transactions. This is useful for tracking resume tokens, cursors, LSNs, or other adapter-specific state. |
| 165 | + |
| 166 | +The `metadata` object is available as a property on the sync config argument alongside `begin`, `write`, `commit`, etc. It is always provided, but without persistence the metadata is in-memory only and does not survive reloads. With persistence, metadata is durable across sessions. |
| 167 | + |
| 168 | +```ts |
| 169 | +sync: ({ begin, write, commit, markReady, metadata }) => { |
| 170 | + // Row metadata: store per-row state (e.g. server version, ETag) |
| 171 | + metadata.row.get(key) // => unknown | undefined |
| 172 | + metadata.row.set(key, { version: 3, etag: 'abc' }) |
| 173 | + metadata.row.delete(key) |
| 174 | + |
| 175 | + // Collection metadata: store per-collection state (e.g. resume cursor) |
| 176 | + metadata.collection.get('cursor') // => unknown | undefined |
| 177 | + metadata.collection.set('cursor', 'token_abc123') |
| 178 | + metadata.collection.delete('cursor') |
| 179 | + metadata.collection.list() // => [{ key: 'cursor', value: 'token_abc123' }] |
| 180 | + metadata.collection.list('resume') // filter by prefix |
| 181 | +} |
| 182 | +``` |
| 183 | + |
| 184 | +Row metadata writes are tied to the current transaction. When a row is deleted via `write({ type: 'delete', ... })`, its row metadata is automatically deleted. When a row is inserted, its metadata is set from `message.metadata` if provided, or deleted otherwise. |
| 185 | + |
| 186 | +Collection metadata writes staged before `truncate()` are preserved and commit atomically with the truncate transaction. |
| 187 | + |
| 188 | +**Typical usage — resume token:** |
| 189 | + |
| 190 | +```ts |
| 191 | +sync: ({ begin, write, commit, markReady, metadata }) => { |
| 192 | + const lastCursor = metadata.collection.get('cursor') as string | undefined |
| 193 | + |
| 194 | + const stream = subscribeFromCursor(lastCursor) |
| 195 | + stream.on('data', (batch) => { |
| 196 | + begin() |
| 197 | + for (const item of batch.items) { |
| 198 | + write({ type: item.type, key: item.id, value: item.data }) |
| 199 | + } |
| 200 | + metadata.collection.set('cursor', batch.cursor) |
| 201 | + commit() |
| 202 | + }) |
| 203 | + |
| 204 | + stream.on('ready', () => markReady()) |
| 205 | + return () => stream.close() |
| 206 | +} |
| 207 | +``` |
| 208 | + |
160 | 209 | ### Expression parsing for predicate push-down |
161 | 210 |
|
162 | 211 | ```ts |
@@ -282,4 +331,4 @@ Source: packages/db/src/collection/sync.ts:110 |
282 | 331 |
|
283 | 332 | Getting-started simplicity (localOnly, eager mode) conflicts with production correctness (on-demand sync, race condition prevention, proper markReady handling). Agents optimizing for quick setup tend to skip buffering, markReady, and cleanup functions. |
284 | 333 |
|
285 | | -See also: db-core/collection-setup/SKILL.md -- for built-in adapter patterns to model after. |
| 334 | +See also: db-core/collection-setup/SKILL.md — for built-in adapter patterns to model after. |
0 commit comments