@foretag/tanstack-db-surrealdb 0.6.12 → 0.7.0

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/README.md CHANGED
@@ -8,10 +8,6 @@ TanStack DB collection adapter for SurrealDB JS with:
8
8
  - Optional Loro CRDT replication (`json`, `richtext`)
9
9
  - Query-driven sync modes (`eager`, `on-demand`, `progressive`)
10
10
 
11
- ### Roadmap
12
-
13
- - Persistence: [Issue](https://github.com/TanStack/db/issues/865)
14
-
15
11
  ## Install
16
12
 
17
13
  ```sh
@@ -34,45 +30,138 @@ const queryClient = new QueryClient();
34
30
  type Product = { id: string; name: string; price: number };
35
31
 
36
32
  export const products = createCollection(
37
- surrealCollectionOptions<Product>({
38
- db,
39
- table: { name: 'product' },
40
- queryClient,
41
- queryKey: ['product'],
42
- syncMode: 'eager',
43
- }),
33
+ surrealCollectionOptions<Product>({
34
+ db,
35
+ table: { name: 'product' },
36
+ queryClient,
37
+ queryKey: ['product'],
38
+ syncMode: 'eager',
39
+ }),
44
40
  );
45
41
  ```
46
42
 
43
+ ## Persistence
44
+
45
+ TanStack DB persistence can wrap this adapter directly. The adapter now returns a
46
+ stable collection `id` by default using:
47
+
48
+ ```ts
49
+ surreal:${tableName}:${hashKey(queryKey)}
50
+ ```
51
+
52
+ That makes it safe to compose with `persistedCollectionOptions(...)` across
53
+ restarts. If you need a custom persistence boundary, pass `id` explicitly and it
54
+ will be preserved.
55
+
56
+ If you want less boilerplate, use `persistedSurrealCollectionOptions(...)` from
57
+ this package and pass the runtime-specific `persistence` adapter plus
58
+ `schemaVersion` directly.
59
+
60
+ Create the SQLite database and persistence adapter once per app/runtime, export
61
+ that shared `persistence`, and reuse it across every persisted collection in
62
+ the app.
63
+
64
+ ```ts
65
+ // Create once, reuse everywhere
66
+ const sqlite = await openBrowserWASQLiteOPFSDatabase({
67
+ databaseName: 'tanstack-db.sqlite',
68
+ });
69
+
70
+ export const persistence = createBrowserWASQLitePersistence({
71
+ database: sqlite,
72
+ });
73
+ ```
74
+
75
+ Browser-first example:
76
+
77
+ ```ts
78
+ // persistence.ts
79
+ import { createCollection } from '@tanstack/db';
80
+ import { QueryClient } from '@tanstack/query-core';
81
+ import {
82
+ createBrowserWASQLitePersistence,
83
+ openBrowserWASQLiteOPFSDatabase,
84
+ } from '@tanstack/browser-db-sqlite-persistence';
85
+ import { Surreal } from 'surrealdb';
86
+ import { persistedSurrealCollectionOptions } from '@foretag/tanstack-db-surrealdb';
87
+
88
+ const db = new Surreal();
89
+ const queryClient = new QueryClient();
90
+
91
+ const sqlite = await openBrowserWASQLiteOPFSDatabase({
92
+ databaseName: 'tanstack-db.sqlite',
93
+ });
94
+
95
+ export const persistence = createBrowserWASQLitePersistence({
96
+ database: sqlite,
97
+ });
98
+
99
+ type Product = { id: string; name: string; price: number };
100
+ type Category = { id: string; name: string };
101
+
102
+ export const products = createCollection(
103
+ persistedSurrealCollectionOptions<Product>({
104
+ persistence,
105
+ schemaVersion: 1,
106
+ db,
107
+ table: { name: 'product' },
108
+ queryClient,
109
+ queryKey: ['product'],
110
+ syncMode: 'eager',
111
+ }),
112
+ );
113
+
114
+ export const categories = createCollection(
115
+ persistedSurrealCollectionOptions<Category>({
116
+ persistence,
117
+ schemaVersion: 1,
118
+ db,
119
+ table: { name: 'category' },
120
+ queryClient,
121
+ queryKey: ['category'],
122
+ syncMode: 'eager',
123
+ }),
124
+ );
125
+ ```
126
+
127
+ You only need one `openBrowserWASQLiteOPFSDatabase(...)` call and one
128
+ `createBrowserWASQLitePersistence(...)` call per browser app, not per
129
+ collection
130
+
47
131
  ## Adapter API
48
132
 
49
133
  ```ts
50
134
  type SurrealCollectionOptions<T> = {
51
- db: Surreal;
52
- table: Table | { name: string; relation?: boolean } | string;
53
- queryClient: QueryClient;
54
- queryKey: readonly unknown[];
55
- syncMode?: 'eager' | 'on-demand' | 'progressive';
56
- e2ee?: {
57
- enabled: boolean;
58
- crypto: CryptoProvider;
59
- aad?: (ctx: { table: string; id: string; kind: 'base'|'update'|'snapshot'; baseTable?: string }) => Uint8Array;
60
- };
61
- crdt?: {
62
- enabled: boolean;
63
- profile: 'json' | 'richtext';
64
- updatesTable: Table | { name: string } | string;
65
- snapshotsTable?: Table | { name: string } | string;
66
- // Optional overrides. If omitted, adapter uses built-in handlers for `profile`.
67
- materialize?: (doc: LoroDoc, id: string) => T;
68
- applyLocalChange?: (doc: LoroDoc, change: { type: 'insert'|'update'|'delete'; value: T }) => void;
69
- persistMaterializedView?: boolean;
70
- actor?: string | ((ctx: { id: string; change?: { type: 'insert'|'update'|'delete'; value: T } }) => string | undefined);
71
- localActorId?: string; // deprecated
72
- };
135
+ id?: string;
136
+ db: Surreal;
137
+ table: Table | { name: string; relation?: boolean } | string;
138
+ queryClient: QueryClient;
139
+ queryKey: readonly unknown[];
140
+ syncMode?: 'eager' | 'on-demand' | 'progressive';
141
+ e2ee?: {
142
+ enabled: boolean;
143
+ crypto: CryptoProvider;
144
+ aad?: (ctx: { table: string; id: string; kind: 'base'|'update'|'snapshot'; baseTable?: string }) => Uint8Array;
145
+ };
146
+ crdt?: {
147
+ enabled: boolean;
148
+ profile: 'json' | 'richtext';
149
+ updatesTable: Table | { name: string } | string;
150
+ snapshotsTable?: Table | { name: string } | string;
151
+ // Optional overrides. If omitted, adapter uses built-in handlers for `profile`.
152
+ materialize?: (doc: LoroDoc, id: string) => T;
153
+ applyLocalChange?: (doc: LoroDoc, change: { type: 'insert'|'update'|'delete'; value: T }) => void;
154
+ persistMaterializedView?: boolean;
155
+ actor?: string | ((ctx: { id: string; change?: { type: 'insert'|'update'|'delete'; value: T } }) => string | undefined);
156
+ localActorId?: string; // deprecated
157
+ };
73
158
  };
74
159
  ```
75
160
 
161
+ `id` is optional. When omitted, the adapter derives a stable collection id from
162
+ the Surreal table name and `queryKey` so TanStack DB persistence wrappers can
163
+ reuse the same persisted collection state across restarts.
164
+
76
165
  ## E2EE
77
166
 
78
167
  Envelope fields stored in Surreal records:
@@ -242,14 +331,14 @@ If you run snapshot compaction from a trusted backend/service account, grant cre
242
331
  const provider = await WebCryptoAESGCM.fromRawKey(rawKey, { kid: 'org-key-2026-01' });
243
332
 
244
333
  const secrets = createCollection(
245
- surrealCollectionOptions<{ id: string; title: string; body: string }>({
246
- db,
247
- table: { name: 'secret_note' },
248
- queryClient,
249
- queryKey: ['secret-note'],
250
- syncMode: 'eager',
251
- e2ee: { enabled: true, crypto: provider },
252
- }),
334
+ surrealCollectionOptions<{ id: string; title: string; body: string }>({
335
+ db,
336
+ table: { name: 'secret_note' },
337
+ queryClient,
338
+ queryKey: ['secret-note'],
339
+ syncMode: 'eager',
340
+ e2ee: { enabled: true, crypto: provider },
341
+ }),
253
342
  );
254
343
  ```
255
344
 
@@ -257,20 +346,20 @@ const secrets = createCollection(
257
346
 
258
347
  ```ts
259
348
  const docs = createCollection(
260
- surrealCollectionOptions<{ id: string; content: string; title?: string }>({
261
- db,
262
- table: { name: 'doc' },
263
- queryClient,
264
- queryKey: ['doc'],
265
- syncMode: 'on-demand',
266
- crdt: {
267
- enabled: true,
268
- profile: 'richtext',
269
- updatesTable: { name: 'crdt_update' },
270
- snapshotsTable: { name: 'crdt_snapshot' },
271
- actor: ({ id }) => id.startsWith('team-a') ? 'device:team-a:abc' : 'device:team-b:abc',
272
- },
273
- }),
349
+ surrealCollectionOptions<{ id: string; content: string; title?: string }>({
350
+ db,
351
+ table: { name: 'doc' },
352
+ queryClient,
353
+ queryKey: ['doc'],
354
+ syncMode: 'on-demand',
355
+ crdt: {
356
+ enabled: true,
357
+ profile: 'richtext',
358
+ updatesTable: { name: 'crdt_update' },
359
+ snapshotsTable: { name: 'crdt_snapshot' },
360
+ actor: ({ id }) => id.startsWith('team-a') ? 'device:team-a:abc' : 'device:team-b:abc',
361
+ },
362
+ }),
274
363
  );
275
364
  ```
276
365
 
@@ -280,18 +369,18 @@ const docs = createCollection(
280
369
  import { RecordId } from 'surrealdb';
281
370
 
282
371
  type CalendarEvent = {
283
- id: RecordId<'calendar_event'>;
284
- owner: RecordId<'account'>;
285
- title: string;
286
- start_at: string;
372
+ id: RecordId<'calendar_event'>;
373
+ owner: RecordId<'account'>;
374
+ title: string;
375
+ start_at: string;
287
376
  };
288
377
 
289
378
  await calendarEvents.insert({
290
- // id is Optional on insert
291
- id: new RecordId('calendar_event', 'evt-001'),
292
- owner: new RecordId('account', 'user-123'),
293
- title: 'Planning',
294
- start_at: '2026-02-23T10:00:00.000Z',
379
+ // id is Optional on insert
380
+ id: new RecordId('calendar_event', 'evt-001'),
381
+ owner: new RecordId('account', 'user-123'),
382
+ title: 'Planning',
383
+ start_at: '2026-02-23T10:00:00.000Z',
295
384
  });
296
385
  ```
297
386
 
@@ -303,20 +392,20 @@ Full runnable example: `examples/record-id.ts`.
303
392
  import { createLiveQueryCollection, eq } from '@tanstack/db';
304
393
 
305
394
  const files = createCollection(
306
- surrealCollectionOptions<{ id: string; owner: string; updated_at: string; name: string }>({
307
- db,
308
- table: { name: 'file' },
309
- queryClient,
310
- queryKey: ['file'],
311
- syncMode: 'on-demand',
312
- }),
395
+ surrealCollectionOptions<{ id: string; owner: string; updated_at: string; name: string }>({
396
+ db,
397
+ table: { name: 'file' },
398
+ queryClient,
399
+ queryKey: ['file'],
400
+ syncMode: 'on-demand',
401
+ }),
313
402
  );
314
403
 
315
404
  const ownerFiles = createLiveQueryCollection((q) =>
316
- q
317
- .from({ files })
318
- .where(({ files }) => eq(files.owner, 'account:abc'))
319
- .select(({ files }) => files),
405
+ q
406
+ .from({ files })
407
+ .where(({ files }) => eq(files.owner, 'account:abc'))
408
+ .select(({ files }) => files),
320
409
  );
321
410
 
322
411
  await ownerFiles.preload();
package/dist/index.d.mts CHANGED
@@ -1,9 +1,17 @@
1
+ import * as _tanstack_db_sqlite_persistence_core from '@tanstack/db-sqlite-persistence-core';
2
+ import { PersistedCollectionPersistence } from '@tanstack/db-sqlite-persistence-core';
1
3
  import { StandardSchemaV1 } from '@standard-schema/spec';
2
4
  import { CollectionConfig, UtilsRecord, LoadSubsetOptions, OperationConfig, Transaction } from '@tanstack/db';
3
- import { Table, Surreal, RecordId } from 'surrealdb';
5
+ import { RecordId, Surreal, Table } from 'surrealdb';
4
6
  import { QueryClient } from '@tanstack/query-core';
5
7
  import { LoroDoc } from 'loro-crdt';
6
8
 
9
+ type MutationInput<T extends {
10
+ id: string | RecordId;
11
+ }> = Omit<T, 'id'> & {
12
+ id?: T['id'];
13
+ };
14
+
7
15
  type EncryptInput = {
8
16
  plaintext: Bytes;
9
17
  aad?: Bytes;
@@ -116,6 +124,12 @@ type SurrealCRDTOptions<T extends object> = {
116
124
  };
117
125
  type AdapterSyncMode = 'eager' | 'on-demand' | 'progressive';
118
126
  type SurrealCollectionOptions<T extends object> = Omit<CollectionConfig<T>, 'onInsert' | 'onUpdate' | 'onDelete' | 'sync' | 'getKey' | 'syncMode'> & {
127
+ /**
128
+ * Optional stable collection identity.
129
+ * When omitted, the adapter derives one from the Surreal table name and queryKey
130
+ * so wrappers like TanStack DB persistence can reuse the same collection across restarts.
131
+ */
132
+ id?: string;
119
133
  db: Surreal;
120
134
  table: TableLike;
121
135
  queryKey: readonly unknown[];
@@ -125,11 +139,16 @@ type SurrealCollectionOptions<T extends object> = Omit<CollectionConfig<T>, 'onI
125
139
  crdt?: SurrealCRDTOptions<T>;
126
140
  onError?: (error: unknown) => void;
127
141
  };
142
+ type PersistedSurrealCollectionOptions<T extends object> = SurrealCollectionOptions<T> & {
143
+ persistence: PersistedCollectionPersistence;
144
+ schemaVersion?: number;
145
+ };
128
146
  type SurrealCollectionOptionsReturn<T extends {
129
147
  id: string | RecordId;
130
148
  }> = CollectionConfig<T, string, StandardSchemaV1<Omit<T, 'id'> & {
131
149
  id?: T['id'];
132
150
  }, T>, UtilsRecord> & {
151
+ id: string;
133
152
  schema: StandardSchemaV1<Omit<T, 'id'> & {
134
153
  id?: T['id'];
135
154
  }, T>;
@@ -150,19 +169,21 @@ declare const createLoroProfile: <T extends object = Record<string, unknown>>(pr
150
169
 
151
170
  declare const toRecordKeyString: (rid: RecordId | string) => string;
152
171
 
153
- type MutationInput<T extends {
154
- id: string | RecordId;
155
- }> = Omit<T, 'id'> & {
156
- id?: T['id'];
157
- };
158
172
  declare function surrealCollectionOptions<T extends SyncedTable<object>>(config: SurrealCollectionOptions<T>): CollectionConfig<T, string, StandardSchemaV1<MutationInput<T>, T>, UtilsRecord> & {
159
173
  schema: StandardSchemaV1<MutationInput<T>, T>;
160
174
  utils: UtilsRecord;
161
175
  };
176
+ declare function persistedSurrealCollectionOptions<T extends SyncedTable<object>>(config: PersistedSurrealCollectionOptions<T>): CollectionConfig<T, string, StandardSchemaV1<Omit<T, "id"> & {
177
+ id?: T["id"];
178
+ }, T>, UtilsRecord> & {
179
+ persistence: _tanstack_db_sqlite_persistence_core.PersistedCollectionPersistence & {
180
+ coordinator: _tanstack_db_sqlite_persistence_core.PersistedCollectionCoordinator;
181
+ };
182
+ };
162
183
  declare module '@tanstack/db' {
163
184
  interface Collection<T extends object = Record<string, unknown>, TKey extends string | number = string | number, TUtils extends UtilsRecord = UtilsRecord, TSchema extends StandardSchemaV1 = StandardSchemaV1, TInsertInput extends object = T> {
164
185
  delete(keys: Array<TKey | RecordId | string> | TKey | RecordId | string, config?: OperationConfig): Transaction<Record<string, never>>;
165
186
  }
166
187
  }
167
188
 
168
- export { type AADContext, type AdapterE2EEOptions, type AdapterSyncMode, type Bytes, type CRDTActorContext, type CryptoProvider, type DecryptInput, type E2EEConfig, type EncryptInput, type EncryptedEnvelope, type EncryptionAADContext, type EncryptionEnvelope, type EnvelopeKind, type LocalChange, type LoroProfile, type SurrealCRDTOptions, type SurrealCollectionOptions, type SurrealCollectionOptionsReturn, type SurrealE2EEOptions, type SurrealSubset, type SyncedTable, type TableLike, type TableOptions, WebCryptoAESGCM, type WithId, applyLoroJsonChange, applyLoroRichtextChange, createLoroProfile, materializeLoroJson, materializeLoroRichtext, surrealCollectionOptions, toRecordKeyString };
189
+ export { type AADContext, type AdapterE2EEOptions, type AdapterSyncMode, type Bytes, type CRDTActorContext, type CryptoProvider, type DecryptInput, type E2EEConfig, type EncryptInput, type EncryptedEnvelope, type EncryptionAADContext, type EncryptionEnvelope, type EnvelopeKind, type LocalChange, type LoroProfile, type PersistedSurrealCollectionOptions, type SurrealCRDTOptions, type SurrealCollectionOptions, type SurrealCollectionOptionsReturn, type SurrealE2EEOptions, type SurrealSubset, type SyncedTable, type TableLike, type TableOptions, WebCryptoAESGCM, type WithId, applyLoroJsonChange, applyLoroRichtextChange, createLoroProfile, materializeLoroJson, materializeLoroRichtext, persistedSurrealCollectionOptions, surrealCollectionOptions, toRecordKeyString };
package/dist/index.d.ts CHANGED
@@ -1,9 +1,17 @@
1
+ import * as _tanstack_db_sqlite_persistence_core from '@tanstack/db-sqlite-persistence-core';
2
+ import { PersistedCollectionPersistence } from '@tanstack/db-sqlite-persistence-core';
1
3
  import { StandardSchemaV1 } from '@standard-schema/spec';
2
4
  import { CollectionConfig, UtilsRecord, LoadSubsetOptions, OperationConfig, Transaction } from '@tanstack/db';
3
- import { Table, Surreal, RecordId } from 'surrealdb';
5
+ import { RecordId, Surreal, Table } from 'surrealdb';
4
6
  import { QueryClient } from '@tanstack/query-core';
5
7
  import { LoroDoc } from 'loro-crdt';
6
8
 
9
+ type MutationInput<T extends {
10
+ id: string | RecordId;
11
+ }> = Omit<T, 'id'> & {
12
+ id?: T['id'];
13
+ };
14
+
7
15
  type EncryptInput = {
8
16
  plaintext: Bytes;
9
17
  aad?: Bytes;
@@ -116,6 +124,12 @@ type SurrealCRDTOptions<T extends object> = {
116
124
  };
117
125
  type AdapterSyncMode = 'eager' | 'on-demand' | 'progressive';
118
126
  type SurrealCollectionOptions<T extends object> = Omit<CollectionConfig<T>, 'onInsert' | 'onUpdate' | 'onDelete' | 'sync' | 'getKey' | 'syncMode'> & {
127
+ /**
128
+ * Optional stable collection identity.
129
+ * When omitted, the adapter derives one from the Surreal table name and queryKey
130
+ * so wrappers like TanStack DB persistence can reuse the same collection across restarts.
131
+ */
132
+ id?: string;
119
133
  db: Surreal;
120
134
  table: TableLike;
121
135
  queryKey: readonly unknown[];
@@ -125,11 +139,16 @@ type SurrealCollectionOptions<T extends object> = Omit<CollectionConfig<T>, 'onI
125
139
  crdt?: SurrealCRDTOptions<T>;
126
140
  onError?: (error: unknown) => void;
127
141
  };
142
+ type PersistedSurrealCollectionOptions<T extends object> = SurrealCollectionOptions<T> & {
143
+ persistence: PersistedCollectionPersistence;
144
+ schemaVersion?: number;
145
+ };
128
146
  type SurrealCollectionOptionsReturn<T extends {
129
147
  id: string | RecordId;
130
148
  }> = CollectionConfig<T, string, StandardSchemaV1<Omit<T, 'id'> & {
131
149
  id?: T['id'];
132
150
  }, T>, UtilsRecord> & {
151
+ id: string;
133
152
  schema: StandardSchemaV1<Omit<T, 'id'> & {
134
153
  id?: T['id'];
135
154
  }, T>;
@@ -150,19 +169,21 @@ declare const createLoroProfile: <T extends object = Record<string, unknown>>(pr
150
169
 
151
170
  declare const toRecordKeyString: (rid: RecordId | string) => string;
152
171
 
153
- type MutationInput<T extends {
154
- id: string | RecordId;
155
- }> = Omit<T, 'id'> & {
156
- id?: T['id'];
157
- };
158
172
  declare function surrealCollectionOptions<T extends SyncedTable<object>>(config: SurrealCollectionOptions<T>): CollectionConfig<T, string, StandardSchemaV1<MutationInput<T>, T>, UtilsRecord> & {
159
173
  schema: StandardSchemaV1<MutationInput<T>, T>;
160
174
  utils: UtilsRecord;
161
175
  };
176
+ declare function persistedSurrealCollectionOptions<T extends SyncedTable<object>>(config: PersistedSurrealCollectionOptions<T>): CollectionConfig<T, string, StandardSchemaV1<Omit<T, "id"> & {
177
+ id?: T["id"];
178
+ }, T>, UtilsRecord> & {
179
+ persistence: _tanstack_db_sqlite_persistence_core.PersistedCollectionPersistence & {
180
+ coordinator: _tanstack_db_sqlite_persistence_core.PersistedCollectionCoordinator;
181
+ };
182
+ };
162
183
  declare module '@tanstack/db' {
163
184
  interface Collection<T extends object = Record<string, unknown>, TKey extends string | number = string | number, TUtils extends UtilsRecord = UtilsRecord, TSchema extends StandardSchemaV1 = StandardSchemaV1, TInsertInput extends object = T> {
164
185
  delete(keys: Array<TKey | RecordId | string> | TKey | RecordId | string, config?: OperationConfig): Transaction<Record<string, never>>;
165
186
  }
166
187
  }
167
188
 
168
- export { type AADContext, type AdapterE2EEOptions, type AdapterSyncMode, type Bytes, type CRDTActorContext, type CryptoProvider, type DecryptInput, type E2EEConfig, type EncryptInput, type EncryptedEnvelope, type EncryptionAADContext, type EncryptionEnvelope, type EnvelopeKind, type LocalChange, type LoroProfile, type SurrealCRDTOptions, type SurrealCollectionOptions, type SurrealCollectionOptionsReturn, type SurrealE2EEOptions, type SurrealSubset, type SyncedTable, type TableLike, type TableOptions, WebCryptoAESGCM, type WithId, applyLoroJsonChange, applyLoroRichtextChange, createLoroProfile, materializeLoroJson, materializeLoroRichtext, surrealCollectionOptions, toRecordKeyString };
189
+ export { type AADContext, type AdapterE2EEOptions, type AdapterSyncMode, type Bytes, type CRDTActorContext, type CryptoProvider, type DecryptInput, type E2EEConfig, type EncryptInput, type EncryptedEnvelope, type EncryptionAADContext, type EncryptionEnvelope, type EnvelopeKind, type LocalChange, type LoroProfile, type PersistedSurrealCollectionOptions, type SurrealCRDTOptions, type SurrealCollectionOptions, type SurrealCollectionOptionsReturn, type SurrealE2EEOptions, type SurrealSubset, type SyncedTable, type TableLike, type TableOptions, WebCryptoAESGCM, type WithId, applyLoroJsonChange, applyLoroRichtextChange, createLoroProfile, materializeLoroJson, materializeLoroRichtext, persistedSurrealCollectionOptions, surrealCollectionOptions, toRecordKeyString };