@foretag/tanstack-db-surrealdb 0.2.2 → 0.3.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
@@ -17,14 +17,6 @@ npm install @foretag/tanstack-db-surrealdb
17
17
  bun install @foretag/tanstack-db-surrealdb
18
18
  ```
19
19
 
20
- ### JSR
21
- ```sh
22
- # NPM
23
- npx jsr add @foretag/tanstack-db-surrealdb
24
- # Bun
25
- bunx jsr add @foretag/tanstack-db-surrealdb
26
- ```
27
-
28
20
  ## Usage
29
21
  ```ts
30
22
  // db.ts
@@ -60,6 +52,8 @@ export const products = createCollection(
60
52
  )
61
53
  ```
62
54
 
55
+ For syncModes, please see [Example](https://github.com/ForetagInc/tanstack-db-surrealdb/blob/master/examples/syncMode.ts)
56
+
63
57
  ## Vite / Next.JS
64
58
 
65
59
  ### Vite
package/dist/index.d.mts CHANGED
@@ -9,23 +9,35 @@ type SyncedTable<T> = WithId<T & {
9
9
  sync_deleted?: boolean;
10
10
  updated_at?: Date;
11
11
  }>;
12
- type TableOptions<T> = {
13
- name: string;
14
- fields?: (keyof T)[];
15
- where?: ExprLike;
16
- };
12
+ type SyncMode = 'eager' | 'on-demand' | 'progressive';
17
13
  type SurrealCollectionConfig<T extends {
18
14
  id: string | RecordId;
19
15
  }> = {
20
16
  id?: string;
21
17
  db: Surreal;
22
18
  table: TableOptions<T>;
19
+ syncMode?: SyncMode;
23
20
  useLoro?: boolean;
24
21
  onError?: (e: unknown) => void;
25
22
  };
23
+ type Field<I> = keyof I | (string & {});
24
+ type FieldList<I> = '*' | ReadonlyArray<Field<I>>;
25
+ type TableOptions<T> = {
26
+ name: string;
27
+ fields?: FieldList<T>;
28
+ where?: ExprLike;
29
+ pageSize?: number;
30
+ initialPageSize?: number;
31
+ onProgress?: (info: {
32
+ table: string;
33
+ loaded: number;
34
+ lastBatch: number;
35
+ done: boolean;
36
+ }) => void;
37
+ };
26
38
 
27
39
  declare function surrealCollectionOptions<T extends SyncedTable<object>, S extends Record<string, Container> = {
28
40
  [k: string]: never;
29
- }>({ id, useLoro, onError, db, ...config }: SurrealCollectionConfig<T>): CollectionConfig<T, string | number, never, UtilsRecord>;
41
+ }>({ id, useLoro, onError, db, syncMode, ...config }: SurrealCollectionConfig<T>): CollectionConfig<T, string | number, never, UtilsRecord>;
30
42
 
31
43
  export { surrealCollectionOptions };
package/dist/index.d.ts CHANGED
@@ -9,23 +9,35 @@ type SyncedTable<T> = WithId<T & {
9
9
  sync_deleted?: boolean;
10
10
  updated_at?: Date;
11
11
  }>;
12
- type TableOptions<T> = {
13
- name: string;
14
- fields?: (keyof T)[];
15
- where?: ExprLike;
16
- };
12
+ type SyncMode = 'eager' | 'on-demand' | 'progressive';
17
13
  type SurrealCollectionConfig<T extends {
18
14
  id: string | RecordId;
19
15
  }> = {
20
16
  id?: string;
21
17
  db: Surreal;
22
18
  table: TableOptions<T>;
19
+ syncMode?: SyncMode;
23
20
  useLoro?: boolean;
24
21
  onError?: (e: unknown) => void;
25
22
  };
23
+ type Field<I> = keyof I | (string & {});
24
+ type FieldList<I> = '*' | ReadonlyArray<Field<I>>;
25
+ type TableOptions<T> = {
26
+ name: string;
27
+ fields?: FieldList<T>;
28
+ where?: ExprLike;
29
+ pageSize?: number;
30
+ initialPageSize?: number;
31
+ onProgress?: (info: {
32
+ table: string;
33
+ loaded: number;
34
+ lastBatch: number;
35
+ done: boolean;
36
+ }) => void;
37
+ };
26
38
 
27
39
  declare function surrealCollectionOptions<T extends SyncedTable<object>, S extends Record<string, Container> = {
28
40
  [k: string]: never;
29
- }>({ id, useLoro, onError, db, ...config }: SurrealCollectionConfig<T>): CollectionConfig<T, string | number, never, UtilsRecord>;
41
+ }>({ id, useLoro, onError, db, syncMode, ...config }: SurrealCollectionConfig<T>): CollectionConfig<T, string | number, never, UtilsRecord>;
30
42
 
31
43
  export { surrealCollectionOptions };
package/dist/index.js CHANGED
@@ -27,74 +27,162 @@ var import_surrealdb2 = require("surrealdb");
27
27
 
28
28
  // src/table.ts
29
29
  var import_surrealdb = require("surrealdb");
30
- function manageTable(db, useLoro, { name, ...args }) {
31
- const fields = args.fields ?? "*";
32
- const listAll = async () => {
30
+ function manageTable(db, useLoro, { name, ...args }, syncMode = "eager") {
31
+ const rawFields = args.fields ?? "*";
32
+ const fields = rawFields === "*" ? ["*"] : [...rawFields];
33
+ const cache = /* @__PURE__ */ new Map();
34
+ let fullyLoaded = false;
35
+ const pageSize = args.pageSize ?? 100;
36
+ const initialPageSize = args.initialPageSize ?? Math.min(50, pageSize);
37
+ let cursor = 0;
38
+ let progressiveTask = null;
39
+ const idKey = (id) => typeof id === "string" ? id : id.toString();
40
+ const upsertCache = (rows) => {
41
+ for (const row of rows) cache.set(idKey(row.id), row);
42
+ };
43
+ const removeFromCache = (id) => {
44
+ cache.delete(idKey(id));
45
+ };
46
+ const listCached = () => Array.from(cache.values());
47
+ const buildWhere = () => {
48
+ if (!useLoro) return args.where;
49
+ return args.where ? (0, import_surrealdb.and)(args.where, (0, import_surrealdb.eq)("sync_deleted", false)) : (0, import_surrealdb.eq)("sync_deleted", false);
50
+ };
51
+ const buildQuery = () => {
33
52
  let q = db.select(new import_surrealdb.Table(name));
34
- if (args.where) q = q.where(args.where);
35
- return await q.fields(...fields);
53
+ const cond = buildWhere();
54
+ if (cond) q = q.where(cond);
55
+ return q;
56
+ };
57
+ const applyPaging = (q, start, limit) => {
58
+ if (typeof start === "number" && q.start) q = q.start(start);
59
+ if (typeof limit === "number" && q.limit) q = q.limit(limit);
60
+ return q;
61
+ };
62
+ const fetchAll = async () => {
63
+ const rows = await buildQuery().fields(...fields);
64
+ upsertCache(rows);
65
+ fullyLoaded = true;
66
+ return rows;
67
+ };
68
+ const fetchPage = async (opts) => {
69
+ const q = applyPaging(
70
+ buildQuery(),
71
+ opts?.start ?? 0,
72
+ opts?.limit ?? pageSize
73
+ );
74
+ const rows = await q.fields(...fields);
75
+ upsertCache(rows);
76
+ if (rows.length < (opts?.limit ?? pageSize)) fullyLoaded = true;
77
+ return rows;
36
78
  };
79
+ const fetchById = async (id) => {
80
+ const key = idKey(id);
81
+ const cached = cache.get(key);
82
+ if (cached) return cached;
83
+ const res = await db.select(id);
84
+ const row = Array.isArray(res) ? res[0] : res;
85
+ if (!row) return null;
86
+ if (useLoro && row.sync_deleted)
87
+ return null;
88
+ cache.set(key, row);
89
+ return row;
90
+ };
91
+ const loadMore = async (limit = pageSize) => {
92
+ if (fullyLoaded) return { rows: [], done: true };
93
+ const rows = await fetchPage({ start: cursor, limit });
94
+ cursor += rows.length;
95
+ const done = fullyLoaded || rows.length < limit;
96
+ args.onProgress?.({
97
+ table: name,
98
+ loaded: cache.size,
99
+ lastBatch: rows.length,
100
+ done
101
+ });
102
+ return { rows, done };
103
+ };
104
+ const startProgressive = () => {
105
+ if (progressiveTask || fullyLoaded) return;
106
+ progressiveTask = (async () => {
107
+ if (cache.size === 0) await loadMore(initialPageSize);
108
+ while (!fullyLoaded) {
109
+ const { done } = await loadMore(pageSize);
110
+ if (done) break;
111
+ }
112
+ })().finally(() => {
113
+ progressiveTask = null;
114
+ });
115
+ };
116
+ const listAll = () => fetchAll();
37
117
  const listActive = async () => {
38
- if (!useLoro) return listAll();
39
- return await db.select(new import_surrealdb.Table(name)).where((0, import_surrealdb.and)(args.where, (0, import_surrealdb.eq)("sync_deleted", false))).fields(...fields);
118
+ if (syncMode === "eager") return fetchAll();
119
+ if (syncMode === "progressive") {
120
+ if (cache.size === 0) await loadMore(initialPageSize);
121
+ startProgressive();
122
+ return listCached();
123
+ }
124
+ return listCached();
40
125
  };
41
126
  const create = async (data) => {
42
127
  await db.create(new import_surrealdb.Table(name)).content(data);
43
128
  };
44
129
  const update = async (id, data) => {
45
130
  if (!useLoro) {
46
- await db.update(id).merge({
47
- ...data
48
- });
131
+ await db.update(id).merge(data);
49
132
  return;
50
133
  }
51
- try {
52
- await db.update(id).merge({
53
- ...data,
54
- sync_deleted: false,
55
- updated_at: Date.now()
56
- });
57
- } catch (error) {
58
- console.warn(
59
- `Please ensure the table ${name} has sync_deleted and updated_at fields defined`
60
- );
61
- console.error(
62
- "Failed to update record with Loro (CRDTs) with: ",
63
- error
64
- );
65
- }
134
+ await db.update(id).merge({
135
+ ...data,
136
+ sync_deleted: false,
137
+ updated_at: /* @__PURE__ */ new Date()
138
+ });
66
139
  };
67
140
  const remove = async (id) => {
68
141
  await db.delete(id);
142
+ removeFromCache(id);
69
143
  };
70
144
  const softDelete = async (id) => {
71
145
  if (!useLoro) {
72
146
  await db.delete(id);
147
+ removeFromCache(id);
73
148
  return;
74
149
  }
75
150
  await db.upsert(id).merge({
76
151
  sync_deleted: true,
77
- updated_at: Date.now()
152
+ updated_at: /* @__PURE__ */ new Date()
78
153
  });
154
+ removeFromCache(id);
79
155
  };
80
156
  const subscribe = (cb) => {
81
157
  let killed = false;
82
158
  let live;
83
- const on = ({ action, value }) => {
84
- console.debug("[Surreal Live]", name, action, value);
159
+ const on = (msg) => {
160
+ const { action, value } = msg;
85
161
  if (action === "KILLED") return;
86
- if (action === "CREATE") cb({ type: "insert", row: value });
87
- else if (action === "UPDATE")
162
+ if (action === "CREATE") {
163
+ upsertCache([value]);
164
+ cb({ type: "insert", row: value });
165
+ return;
166
+ }
167
+ if (action === "UPDATE") {
168
+ if (useLoro && value.sync_deleted) {
169
+ removeFromCache(value.id);
170
+ cb({ type: "delete", row: { id: value.id } });
171
+ return;
172
+ }
173
+ upsertCache([value]);
88
174
  cb({ type: "update", row: value });
89
- else if (action === "DELETE")
175
+ return;
176
+ }
177
+ if (action === "DELETE") {
178
+ removeFromCache(value.id);
90
179
  cb({ type: "delete", row: { id: value.id } });
180
+ }
91
181
  };
92
182
  const start = async () => {
93
- const isLiveSupported = db.isFeatureSupported(import_surrealdb.Features.LiveQueries);
94
- if (isLiveSupported) {
95
- live = await db.live(new import_surrealdb.Table(name)).where(args.where);
96
- live.subscribe(on);
97
- }
183
+ if (!db.isFeatureSupported(import_surrealdb.Features.LiveQueries)) return;
184
+ live = await db.live(new import_surrealdb.Table(name)).where(args.where);
185
+ live.subscribe(on);
98
186
  };
99
187
  void start();
100
188
  return () => {
@@ -106,25 +194,61 @@ function manageTable(db, useLoro, { name, ...args }) {
106
194
  return {
107
195
  listAll,
108
196
  listActive,
197
+ listCached,
198
+ fetchPage,
199
+ fetchById,
200
+ loadMore,
109
201
  create,
110
202
  update,
111
203
  remove,
112
204
  softDelete,
113
- subscribe
205
+ subscribe,
206
+ get isFullyLoaded() {
207
+ return fullyLoaded;
208
+ },
209
+ get cachedCount() {
210
+ return cache.size;
211
+ }
114
212
  };
115
213
  }
116
214
 
117
215
  // src/index.ts
216
+ var DEFAULT_INITIAL_PAGE_SIZE = 50;
217
+ var LOCAL_ID_VERIFY_CHUNK = 500;
218
+ var stableStringify = (value) => {
219
+ const toJson = (v) => {
220
+ if (v === null) return null;
221
+ if (typeof v === "string" || typeof v === "number" || typeof v === "boolean")
222
+ return v;
223
+ if (v instanceof Date) return v.toISOString();
224
+ if (Array.isArray(v)) return v.map(toJson);
225
+ if (typeof v === "object") {
226
+ const o = v;
227
+ const keys = Object.keys(o).sort();
228
+ const out = {};
229
+ for (const k of keys) out[k] = toJson(o[k]);
230
+ return out;
231
+ }
232
+ return String(v);
233
+ };
234
+ return JSON.stringify(toJson(value));
235
+ };
236
+ var chunk = (arr, size) => {
237
+ const out = [];
238
+ for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size));
239
+ return out;
240
+ };
118
241
  function surrealCollectionOptions({
119
242
  id,
120
243
  useLoro = false,
121
244
  onError,
122
245
  db,
246
+ syncMode,
123
247
  ...config
124
248
  }) {
125
249
  let loro;
126
250
  if (useLoro) loro = { doc: new import_loro_crdt.LoroDoc(), key: id };
127
- const keyOf = (id2) => typeof id2 === "string" ? id2 : id2.toString();
251
+ const keyOf = (rid) => typeof rid === "string" ? rid : rid.toString();
128
252
  const getKey = (row) => keyOf(row.id);
129
253
  const loroKey = loro?.key ?? id ?? "surreal";
130
254
  const loroMap = useLoro ? loro?.doc?.getMap?.(loroKey) ?? null : null;
@@ -133,16 +257,17 @@ function surrealCollectionOptions({
133
257
  const json = loroMap.toJSON?.() ?? {};
134
258
  return Object.values(json);
135
259
  };
136
- const loroPut = (row) => {
137
- if (!loroMap) return;
138
- loroMap.set(getKey(row), row);
260
+ const loroPutMany = (rows) => {
261
+ if (!loroMap || rows.length === 0) return;
262
+ for (const row of rows) loroMap.set(getKey(row), row);
139
263
  loro?.doc?.commit?.();
140
264
  };
141
- const loroRemove = (id2) => {
142
- if (!loroMap) return;
143
- loroMap.delete(id2);
265
+ const loroRemoveMany = (ids) => {
266
+ if (!loroMap || ids.length === 0) return;
267
+ for (const id2 of ids) loroMap.delete(id2);
144
268
  loro?.doc?.commit?.();
145
269
  };
270
+ const loroRemove = (id2) => loroRemoveMany([id2]);
146
271
  const pushQueue = [];
147
272
  const enqueuePush = (op) => {
148
273
  if (!useLoro) return;
@@ -154,35 +279,73 @@ function surrealCollectionOptions({
154
279
  for (const op of ops) {
155
280
  if (op.kind === "create") {
156
281
  await table.create(op.row);
157
- }
158
- if (op.kind === "update") {
282
+ } else if (op.kind === "update") {
159
283
  const rid = new import_surrealdb2.RecordId(
160
284
  config.table.name,
161
285
  op.row.id.toString()
162
286
  );
163
287
  await table.update(rid, op.row);
164
- }
165
- if (op.kind === "delete") {
166
- const rid = new import_surrealdb2.RecordId(config.table.name, op.id.toString());
288
+ } else {
289
+ const rid = new import_surrealdb2.RecordId(config.table.name, op.id);
167
290
  await table.softDelete(rid);
168
291
  }
169
292
  }
170
293
  };
171
294
  const newer = (a, b) => (a?.getTime() ?? -1) > (b?.getTime() ?? -1);
172
- const reconcileBoot = (serverRows, write) => {
173
- if (!useLoro) {
174
- diffAndEmit(serverRows, write);
175
- return;
295
+ const fetchServerByLocalIds = async (ids) => {
296
+ if (ids.length === 0) return [];
297
+ const tableName = config.table.name;
298
+ const parts = chunk(ids, LOCAL_ID_VERIFY_CHUNK);
299
+ const out = [];
300
+ for (const p of parts) {
301
+ const [res] = await db.query(
302
+ "SELECT * FROM type::table($table) WHERE id IN $ids",
303
+ {
304
+ table: tableName,
305
+ ids: p.map((x) => new import_surrealdb2.RecordId(tableName, x))
306
+ }
307
+ );
308
+ if (res) out.push(...res);
309
+ }
310
+ return out;
311
+ };
312
+ const dedupeById = (rows) => {
313
+ const m = /* @__PURE__ */ new Map();
314
+ for (const r of rows) m.set(getKey(r), r);
315
+ return Array.from(m.values());
316
+ };
317
+ let prevById = /* @__PURE__ */ new Map();
318
+ const buildMap = (rows) => new Map(rows.map((r) => [getKey(r), r]));
319
+ const same = (a, b) => {
320
+ if (useLoro) {
321
+ return (a.sync_deleted ?? false) === (b.sync_deleted ?? false) && (a.updated_at?.getTime() ?? 0) === (b.updated_at?.getTime() ?? 0);
322
+ }
323
+ return stableStringify(a) === stableStringify(b);
324
+ };
325
+ const diffAndEmit = (currentRows, write) => {
326
+ const currById = buildMap(currentRows);
327
+ for (const [id2, row] of currById) {
328
+ const prev = prevById.get(id2);
329
+ if (!prev) write({ type: "insert", value: row });
330
+ else if (!same(prev, row)) write({ type: "update", value: row });
176
331
  }
332
+ for (const [id2, prev] of prevById) {
333
+ if (!currById.has(id2)) write({ type: "delete", value: prev });
334
+ }
335
+ prevById = currById;
336
+ };
337
+ const reconcileBoot = (serverRows, write) => {
177
338
  const localRows = loroToArray();
178
339
  const serverById = new Map(serverRows.map((r) => [getKey(r), r]));
179
340
  const localById = new Map(localRows.map((r) => [getKey(r), r]));
180
341
  const ids = /* @__PURE__ */ new Set([...serverById.keys(), ...localById.keys()]);
181
342
  const current = [];
343
+ const toRemove = [];
344
+ const toPut = [];
182
345
  const applyLocal = (row) => {
183
346
  if (!row) return;
184
- if (row.sync_deleted) loroRemove(getKey(row));
185
- else loroPut(row);
347
+ if (row.sync_deleted) toRemove.push(getKey(row));
348
+ else toPut.push(row);
186
349
  };
187
350
  for (const id2 of ids) {
188
351
  const s = serverById.get(id2);
@@ -242,46 +405,11 @@ function surrealCollectionOptions({
242
405
  }
243
406
  }
244
407
  }
408
+ loroRemoveMany(toRemove);
409
+ loroPutMany(toPut);
245
410
  diffAndEmit(current, write);
246
411
  };
247
- let prevById = /* @__PURE__ */ new Map();
248
- const buildMap = (rows) => new Map(rows.map((r) => [getKey(r), r]));
249
- const same = (a, b) => {
250
- if (useLoro) {
251
- const aUpdated = a.updated_at;
252
- const bUpdated = b.updated_at;
253
- const aDeleted = a.sync_deleted ?? false;
254
- const bDeleted = b.sync_deleted ?? false;
255
- return aDeleted === bDeleted && (aUpdated?.getTime() ?? 0) === (bUpdated?.getTime() ?? 0) && JSON.stringify({
256
- ...a,
257
- updated_at: void 0,
258
- sync_deleted: void 0
259
- }) === JSON.stringify({
260
- ...b,
261
- updated_at: void 0,
262
- sync_deleted: void 0
263
- });
264
- }
265
- return JSON.stringify(a) === JSON.stringify(b);
266
- };
267
- const diffAndEmit = (currentRows, write) => {
268
- const currById = buildMap(currentRows);
269
- for (const [id2, row] of currById) {
270
- const prev = prevById.get(id2);
271
- if (!prev) {
272
- write({ type: "insert", value: row });
273
- } else if (!same(prev, row)) {
274
- write({ type: "update", value: row });
275
- }
276
- }
277
- for (const [id2, prev] of prevById) {
278
- if (!currById.has(id2)) {
279
- write({ type: "delete", value: prev });
280
- }
281
- }
282
- prevById = currById;
283
- };
284
- const table = manageTable(db, useLoro, config.table);
412
+ const table = manageTable(db, useLoro, config.table, syncMode);
285
413
  const now = () => /* @__PURE__ */ new Date();
286
414
  const sync = ({
287
415
  begin,
@@ -295,6 +423,11 @@ function surrealCollectionOptions({
295
423
  };
296
424
  }
297
425
  let offLive = null;
426
+ let work = Promise.resolve();
427
+ const enqueueWork = (fn) => {
428
+ work = work.then(fn).catch((e) => onError?.(e));
429
+ return work;
430
+ };
298
431
  const makeTombstone = (id2) => ({
299
432
  id: new import_surrealdb2.RecordId(config.table.name, id2).toString(),
300
433
  updated_at: now(),
@@ -302,43 +435,84 @@ function surrealCollectionOptions({
302
435
  });
303
436
  const start = async () => {
304
437
  try {
305
- const serverRows = await table.listAll();
306
- begin();
307
- if (useLoro) reconcileBoot(serverRows, write);
308
- else diffAndEmit(serverRows, write);
309
- commit();
310
- markReady();
438
+ let serverRows;
439
+ if (syncMode === "eager") {
440
+ serverRows = await table.listAll();
441
+ } else if (syncMode === "progressive") {
442
+ const first = await table.loadMore(
443
+ config.table.initialPageSize ?? DEFAULT_INITIAL_PAGE_SIZE
444
+ );
445
+ serverRows = first.rows;
446
+ } else {
447
+ serverRows = await table.listActive();
448
+ }
449
+ await enqueueWork(async () => {
450
+ begin();
451
+ if (useLoro) {
452
+ const localIds = loroToArray().map(getKey);
453
+ const verifiedServerRows = syncMode === "eager" ? serverRows : dedupeById([
454
+ ...serverRows,
455
+ ...await fetchServerByLocalIds(
456
+ localIds
457
+ )
458
+ ]);
459
+ reconcileBoot(verifiedServerRows, write);
460
+ } else {
461
+ diffAndEmit(serverRows, write);
462
+ }
463
+ commit();
464
+ markReady();
465
+ });
466
+ if (syncMode === "progressive") {
467
+ void (async () => {
468
+ while (!table.isFullyLoaded) {
469
+ const { rows } = await table.loadMore();
470
+ if (rows.length === 0) break;
471
+ await enqueueWork(async () => {
472
+ begin();
473
+ try {
474
+ if (useLoro) loroPutMany(rows);
475
+ diffAndEmit(rows, write);
476
+ } finally {
477
+ commit();
478
+ }
479
+ });
480
+ }
481
+ })().catch((e) => onError?.(e));
482
+ }
311
483
  await flushPushQueue();
312
484
  offLive = table.subscribe((evt) => {
313
- begin();
314
- try {
315
- if (evt.type === "insert" || evt.type === "update") {
316
- const row = evt.row;
317
- const deleted = useLoro ? row.sync_deleted ?? false : false;
318
- if (deleted) {
319
- if (useLoro) loroRemove(getKey(row));
320
- const prev = prevById.get(getKey(row)) ?? makeTombstone(getKey(row));
321
- write({ type: "delete", value: prev });
322
- prevById.delete(getKey(row));
485
+ void enqueueWork(async () => {
486
+ begin();
487
+ try {
488
+ if (evt.type === "insert" || evt.type === "update") {
489
+ const row = evt.row;
490
+ const deleted = useLoro ? row.sync_deleted ?? false : false;
491
+ if (deleted) {
492
+ if (useLoro) loroRemove(getKey(row));
493
+ const prev = prevById.get(getKey(row)) ?? makeTombstone(getKey(row));
494
+ write({ type: "delete", value: prev });
495
+ prevById.delete(getKey(row));
496
+ } else {
497
+ if (useLoro) loroPutMany([row]);
498
+ const had = prevById.has(getKey(row));
499
+ write({
500
+ type: had ? "update" : "insert",
501
+ value: row
502
+ });
503
+ prevById.set(getKey(row), row);
504
+ }
323
505
  } else {
324
- if (useLoro) loroPut(row);
325
- const had = prevById.has(getKey(row));
326
- write({
327
- type: had ? "update" : "insert",
328
- value: row
329
- });
330
- prevById.set(getKey(row), row);
506
+ const rid = getKey(evt.row);
507
+ if (useLoro) loroRemove(rid);
508
+ const prev = prevById.get(rid) ?? makeTombstone(rid);
509
+ write({ type: "delete", value: prev });
510
+ prevById.delete(rid);
331
511
  }
332
- } else if (evt.type === "delete") {
333
- const id2 = getKey(evt.row);
334
- if (useLoro) loroRemove(id2);
335
- const prev = prevById.get(id2) ?? makeTombstone(id2);
336
- write({ type: "delete", value: prev });
337
- prevById.delete(id2);
512
+ } finally {
513
+ commit();
338
514
  }
339
- } finally {
340
- commit();
341
- }
515
+ });
342
516
  });
343
517
  } catch (e) {
344
518
  onError?.(e);
@@ -355,12 +529,8 @@ function surrealCollectionOptions({
355
529
  for (const m of p.transaction.mutations) {
356
530
  if (m.type !== "insert") continue;
357
531
  const base = { ...m.modified };
358
- const row = useLoro ? {
359
- ...base,
360
- updated_at: now(),
361
- sync_deleted: false
362
- } : base;
363
- if (useLoro) loroPut(row);
532
+ const row = useLoro ? { ...base, updated_at: now(), sync_deleted: false } : base;
533
+ if (useLoro) loroPutMany([row]);
364
534
  await table.create(row);
365
535
  resultRows.push(row);
366
536
  }
@@ -372,11 +542,8 @@ function surrealCollectionOptions({
372
542
  if (m.type !== "update") continue;
373
543
  const id2 = m.key;
374
544
  const base = { ...m.modified, id: id2 };
375
- const merged = useLoro ? {
376
- ...base,
377
- updated_at: now()
378
- } : base;
379
- if (useLoro) loroPut(merged);
545
+ const merged = useLoro ? { ...base, updated_at: now() } : base;
546
+ if (useLoro) loroPutMany([merged]);
380
547
  const rid = new import_surrealdb2.RecordId(config.table.name, keyOf(id2));
381
548
  await table.update(rid, merged);
382
549
  resultRows.push(merged);
package/dist/index.mjs CHANGED
@@ -9,74 +9,162 @@ import {
9
9
  Features,
10
10
  Table
11
11
  } from "surrealdb";
12
- function manageTable(db, useLoro, { name, ...args }) {
13
- const fields = args.fields ?? "*";
14
- const listAll = async () => {
12
+ function manageTable(db, useLoro, { name, ...args }, syncMode = "eager") {
13
+ const rawFields = args.fields ?? "*";
14
+ const fields = rawFields === "*" ? ["*"] : [...rawFields];
15
+ const cache = /* @__PURE__ */ new Map();
16
+ let fullyLoaded = false;
17
+ const pageSize = args.pageSize ?? 100;
18
+ const initialPageSize = args.initialPageSize ?? Math.min(50, pageSize);
19
+ let cursor = 0;
20
+ let progressiveTask = null;
21
+ const idKey = (id) => typeof id === "string" ? id : id.toString();
22
+ const upsertCache = (rows) => {
23
+ for (const row of rows) cache.set(idKey(row.id), row);
24
+ };
25
+ const removeFromCache = (id) => {
26
+ cache.delete(idKey(id));
27
+ };
28
+ const listCached = () => Array.from(cache.values());
29
+ const buildWhere = () => {
30
+ if (!useLoro) return args.where;
31
+ return args.where ? and(args.where, eq("sync_deleted", false)) : eq("sync_deleted", false);
32
+ };
33
+ const buildQuery = () => {
15
34
  let q = db.select(new Table(name));
16
- if (args.where) q = q.where(args.where);
17
- return await q.fields(...fields);
35
+ const cond = buildWhere();
36
+ if (cond) q = q.where(cond);
37
+ return q;
38
+ };
39
+ const applyPaging = (q, start, limit) => {
40
+ if (typeof start === "number" && q.start) q = q.start(start);
41
+ if (typeof limit === "number" && q.limit) q = q.limit(limit);
42
+ return q;
43
+ };
44
+ const fetchAll = async () => {
45
+ const rows = await buildQuery().fields(...fields);
46
+ upsertCache(rows);
47
+ fullyLoaded = true;
48
+ return rows;
49
+ };
50
+ const fetchPage = async (opts) => {
51
+ const q = applyPaging(
52
+ buildQuery(),
53
+ opts?.start ?? 0,
54
+ opts?.limit ?? pageSize
55
+ );
56
+ const rows = await q.fields(...fields);
57
+ upsertCache(rows);
58
+ if (rows.length < (opts?.limit ?? pageSize)) fullyLoaded = true;
59
+ return rows;
18
60
  };
61
+ const fetchById = async (id) => {
62
+ const key = idKey(id);
63
+ const cached = cache.get(key);
64
+ if (cached) return cached;
65
+ const res = await db.select(id);
66
+ const row = Array.isArray(res) ? res[0] : res;
67
+ if (!row) return null;
68
+ if (useLoro && row.sync_deleted)
69
+ return null;
70
+ cache.set(key, row);
71
+ return row;
72
+ };
73
+ const loadMore = async (limit = pageSize) => {
74
+ if (fullyLoaded) return { rows: [], done: true };
75
+ const rows = await fetchPage({ start: cursor, limit });
76
+ cursor += rows.length;
77
+ const done = fullyLoaded || rows.length < limit;
78
+ args.onProgress?.({
79
+ table: name,
80
+ loaded: cache.size,
81
+ lastBatch: rows.length,
82
+ done
83
+ });
84
+ return { rows, done };
85
+ };
86
+ const startProgressive = () => {
87
+ if (progressiveTask || fullyLoaded) return;
88
+ progressiveTask = (async () => {
89
+ if (cache.size === 0) await loadMore(initialPageSize);
90
+ while (!fullyLoaded) {
91
+ const { done } = await loadMore(pageSize);
92
+ if (done) break;
93
+ }
94
+ })().finally(() => {
95
+ progressiveTask = null;
96
+ });
97
+ };
98
+ const listAll = () => fetchAll();
19
99
  const listActive = async () => {
20
- if (!useLoro) return listAll();
21
- return await db.select(new Table(name)).where(and(args.where, eq("sync_deleted", false))).fields(...fields);
100
+ if (syncMode === "eager") return fetchAll();
101
+ if (syncMode === "progressive") {
102
+ if (cache.size === 0) await loadMore(initialPageSize);
103
+ startProgressive();
104
+ return listCached();
105
+ }
106
+ return listCached();
22
107
  };
23
108
  const create = async (data) => {
24
109
  await db.create(new Table(name)).content(data);
25
110
  };
26
111
  const update = async (id, data) => {
27
112
  if (!useLoro) {
28
- await db.update(id).merge({
29
- ...data
30
- });
113
+ await db.update(id).merge(data);
31
114
  return;
32
115
  }
33
- try {
34
- await db.update(id).merge({
35
- ...data,
36
- sync_deleted: false,
37
- updated_at: Date.now()
38
- });
39
- } catch (error) {
40
- console.warn(
41
- `Please ensure the table ${name} has sync_deleted and updated_at fields defined`
42
- );
43
- console.error(
44
- "Failed to update record with Loro (CRDTs) with: ",
45
- error
46
- );
47
- }
116
+ await db.update(id).merge({
117
+ ...data,
118
+ sync_deleted: false,
119
+ updated_at: /* @__PURE__ */ new Date()
120
+ });
48
121
  };
49
122
  const remove = async (id) => {
50
123
  await db.delete(id);
124
+ removeFromCache(id);
51
125
  };
52
126
  const softDelete = async (id) => {
53
127
  if (!useLoro) {
54
128
  await db.delete(id);
129
+ removeFromCache(id);
55
130
  return;
56
131
  }
57
132
  await db.upsert(id).merge({
58
133
  sync_deleted: true,
59
- updated_at: Date.now()
134
+ updated_at: /* @__PURE__ */ new Date()
60
135
  });
136
+ removeFromCache(id);
61
137
  };
62
138
  const subscribe = (cb) => {
63
139
  let killed = false;
64
140
  let live;
65
- const on = ({ action, value }) => {
66
- console.debug("[Surreal Live]", name, action, value);
141
+ const on = (msg) => {
142
+ const { action, value } = msg;
67
143
  if (action === "KILLED") return;
68
- if (action === "CREATE") cb({ type: "insert", row: value });
69
- else if (action === "UPDATE")
144
+ if (action === "CREATE") {
145
+ upsertCache([value]);
146
+ cb({ type: "insert", row: value });
147
+ return;
148
+ }
149
+ if (action === "UPDATE") {
150
+ if (useLoro && value.sync_deleted) {
151
+ removeFromCache(value.id);
152
+ cb({ type: "delete", row: { id: value.id } });
153
+ return;
154
+ }
155
+ upsertCache([value]);
70
156
  cb({ type: "update", row: value });
71
- else if (action === "DELETE")
157
+ return;
158
+ }
159
+ if (action === "DELETE") {
160
+ removeFromCache(value.id);
72
161
  cb({ type: "delete", row: { id: value.id } });
162
+ }
73
163
  };
74
164
  const start = async () => {
75
- const isLiveSupported = db.isFeatureSupported(Features.LiveQueries);
76
- if (isLiveSupported) {
77
- live = await db.live(new Table(name)).where(args.where);
78
- live.subscribe(on);
79
- }
165
+ if (!db.isFeatureSupported(Features.LiveQueries)) return;
166
+ live = await db.live(new Table(name)).where(args.where);
167
+ live.subscribe(on);
80
168
  };
81
169
  void start();
82
170
  return () => {
@@ -88,25 +176,61 @@ function manageTable(db, useLoro, { name, ...args }) {
88
176
  return {
89
177
  listAll,
90
178
  listActive,
179
+ listCached,
180
+ fetchPage,
181
+ fetchById,
182
+ loadMore,
91
183
  create,
92
184
  update,
93
185
  remove,
94
186
  softDelete,
95
- subscribe
187
+ subscribe,
188
+ get isFullyLoaded() {
189
+ return fullyLoaded;
190
+ },
191
+ get cachedCount() {
192
+ return cache.size;
193
+ }
96
194
  };
97
195
  }
98
196
 
99
197
  // src/index.ts
198
+ var DEFAULT_INITIAL_PAGE_SIZE = 50;
199
+ var LOCAL_ID_VERIFY_CHUNK = 500;
200
+ var stableStringify = (value) => {
201
+ const toJson = (v) => {
202
+ if (v === null) return null;
203
+ if (typeof v === "string" || typeof v === "number" || typeof v === "boolean")
204
+ return v;
205
+ if (v instanceof Date) return v.toISOString();
206
+ if (Array.isArray(v)) return v.map(toJson);
207
+ if (typeof v === "object") {
208
+ const o = v;
209
+ const keys = Object.keys(o).sort();
210
+ const out = {};
211
+ for (const k of keys) out[k] = toJson(o[k]);
212
+ return out;
213
+ }
214
+ return String(v);
215
+ };
216
+ return JSON.stringify(toJson(value));
217
+ };
218
+ var chunk = (arr, size) => {
219
+ const out = [];
220
+ for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size));
221
+ return out;
222
+ };
100
223
  function surrealCollectionOptions({
101
224
  id,
102
225
  useLoro = false,
103
226
  onError,
104
227
  db,
228
+ syncMode,
105
229
  ...config
106
230
  }) {
107
231
  let loro;
108
232
  if (useLoro) loro = { doc: new LoroDoc(), key: id };
109
- const keyOf = (id2) => typeof id2 === "string" ? id2 : id2.toString();
233
+ const keyOf = (rid) => typeof rid === "string" ? rid : rid.toString();
110
234
  const getKey = (row) => keyOf(row.id);
111
235
  const loroKey = loro?.key ?? id ?? "surreal";
112
236
  const loroMap = useLoro ? loro?.doc?.getMap?.(loroKey) ?? null : null;
@@ -115,16 +239,17 @@ function surrealCollectionOptions({
115
239
  const json = loroMap.toJSON?.() ?? {};
116
240
  return Object.values(json);
117
241
  };
118
- const loroPut = (row) => {
119
- if (!loroMap) return;
120
- loroMap.set(getKey(row), row);
242
+ const loroPutMany = (rows) => {
243
+ if (!loroMap || rows.length === 0) return;
244
+ for (const row of rows) loroMap.set(getKey(row), row);
121
245
  loro?.doc?.commit?.();
122
246
  };
123
- const loroRemove = (id2) => {
124
- if (!loroMap) return;
125
- loroMap.delete(id2);
247
+ const loroRemoveMany = (ids) => {
248
+ if (!loroMap || ids.length === 0) return;
249
+ for (const id2 of ids) loroMap.delete(id2);
126
250
  loro?.doc?.commit?.();
127
251
  };
252
+ const loroRemove = (id2) => loroRemoveMany([id2]);
128
253
  const pushQueue = [];
129
254
  const enqueuePush = (op) => {
130
255
  if (!useLoro) return;
@@ -136,35 +261,73 @@ function surrealCollectionOptions({
136
261
  for (const op of ops) {
137
262
  if (op.kind === "create") {
138
263
  await table.create(op.row);
139
- }
140
- if (op.kind === "update") {
264
+ } else if (op.kind === "update") {
141
265
  const rid = new RecordId(
142
266
  config.table.name,
143
267
  op.row.id.toString()
144
268
  );
145
269
  await table.update(rid, op.row);
146
- }
147
- if (op.kind === "delete") {
148
- const rid = new RecordId(config.table.name, op.id.toString());
270
+ } else {
271
+ const rid = new RecordId(config.table.name, op.id);
149
272
  await table.softDelete(rid);
150
273
  }
151
274
  }
152
275
  };
153
276
  const newer = (a, b) => (a?.getTime() ?? -1) > (b?.getTime() ?? -1);
154
- const reconcileBoot = (serverRows, write) => {
155
- if (!useLoro) {
156
- diffAndEmit(serverRows, write);
157
- return;
277
+ const fetchServerByLocalIds = async (ids) => {
278
+ if (ids.length === 0) return [];
279
+ const tableName = config.table.name;
280
+ const parts = chunk(ids, LOCAL_ID_VERIFY_CHUNK);
281
+ const out = [];
282
+ for (const p of parts) {
283
+ const [res] = await db.query(
284
+ "SELECT * FROM type::table($table) WHERE id IN $ids",
285
+ {
286
+ table: tableName,
287
+ ids: p.map((x) => new RecordId(tableName, x))
288
+ }
289
+ );
290
+ if (res) out.push(...res);
291
+ }
292
+ return out;
293
+ };
294
+ const dedupeById = (rows) => {
295
+ const m = /* @__PURE__ */ new Map();
296
+ for (const r of rows) m.set(getKey(r), r);
297
+ return Array.from(m.values());
298
+ };
299
+ let prevById = /* @__PURE__ */ new Map();
300
+ const buildMap = (rows) => new Map(rows.map((r) => [getKey(r), r]));
301
+ const same = (a, b) => {
302
+ if (useLoro) {
303
+ return (a.sync_deleted ?? false) === (b.sync_deleted ?? false) && (a.updated_at?.getTime() ?? 0) === (b.updated_at?.getTime() ?? 0);
304
+ }
305
+ return stableStringify(a) === stableStringify(b);
306
+ };
307
+ const diffAndEmit = (currentRows, write) => {
308
+ const currById = buildMap(currentRows);
309
+ for (const [id2, row] of currById) {
310
+ const prev = prevById.get(id2);
311
+ if (!prev) write({ type: "insert", value: row });
312
+ else if (!same(prev, row)) write({ type: "update", value: row });
158
313
  }
314
+ for (const [id2, prev] of prevById) {
315
+ if (!currById.has(id2)) write({ type: "delete", value: prev });
316
+ }
317
+ prevById = currById;
318
+ };
319
+ const reconcileBoot = (serverRows, write) => {
159
320
  const localRows = loroToArray();
160
321
  const serverById = new Map(serverRows.map((r) => [getKey(r), r]));
161
322
  const localById = new Map(localRows.map((r) => [getKey(r), r]));
162
323
  const ids = /* @__PURE__ */ new Set([...serverById.keys(), ...localById.keys()]);
163
324
  const current = [];
325
+ const toRemove = [];
326
+ const toPut = [];
164
327
  const applyLocal = (row) => {
165
328
  if (!row) return;
166
- if (row.sync_deleted) loroRemove(getKey(row));
167
- else loroPut(row);
329
+ if (row.sync_deleted) toRemove.push(getKey(row));
330
+ else toPut.push(row);
168
331
  };
169
332
  for (const id2 of ids) {
170
333
  const s = serverById.get(id2);
@@ -224,46 +387,11 @@ function surrealCollectionOptions({
224
387
  }
225
388
  }
226
389
  }
390
+ loroRemoveMany(toRemove);
391
+ loroPutMany(toPut);
227
392
  diffAndEmit(current, write);
228
393
  };
229
- let prevById = /* @__PURE__ */ new Map();
230
- const buildMap = (rows) => new Map(rows.map((r) => [getKey(r), r]));
231
- const same = (a, b) => {
232
- if (useLoro) {
233
- const aUpdated = a.updated_at;
234
- const bUpdated = b.updated_at;
235
- const aDeleted = a.sync_deleted ?? false;
236
- const bDeleted = b.sync_deleted ?? false;
237
- return aDeleted === bDeleted && (aUpdated?.getTime() ?? 0) === (bUpdated?.getTime() ?? 0) && JSON.stringify({
238
- ...a,
239
- updated_at: void 0,
240
- sync_deleted: void 0
241
- }) === JSON.stringify({
242
- ...b,
243
- updated_at: void 0,
244
- sync_deleted: void 0
245
- });
246
- }
247
- return JSON.stringify(a) === JSON.stringify(b);
248
- };
249
- const diffAndEmit = (currentRows, write) => {
250
- const currById = buildMap(currentRows);
251
- for (const [id2, row] of currById) {
252
- const prev = prevById.get(id2);
253
- if (!prev) {
254
- write({ type: "insert", value: row });
255
- } else if (!same(prev, row)) {
256
- write({ type: "update", value: row });
257
- }
258
- }
259
- for (const [id2, prev] of prevById) {
260
- if (!currById.has(id2)) {
261
- write({ type: "delete", value: prev });
262
- }
263
- }
264
- prevById = currById;
265
- };
266
- const table = manageTable(db, useLoro, config.table);
394
+ const table = manageTable(db, useLoro, config.table, syncMode);
267
395
  const now = () => /* @__PURE__ */ new Date();
268
396
  const sync = ({
269
397
  begin,
@@ -277,6 +405,11 @@ function surrealCollectionOptions({
277
405
  };
278
406
  }
279
407
  let offLive = null;
408
+ let work = Promise.resolve();
409
+ const enqueueWork = (fn) => {
410
+ work = work.then(fn).catch((e) => onError?.(e));
411
+ return work;
412
+ };
280
413
  const makeTombstone = (id2) => ({
281
414
  id: new RecordId(config.table.name, id2).toString(),
282
415
  updated_at: now(),
@@ -284,43 +417,84 @@ function surrealCollectionOptions({
284
417
  });
285
418
  const start = async () => {
286
419
  try {
287
- const serverRows = await table.listAll();
288
- begin();
289
- if (useLoro) reconcileBoot(serverRows, write);
290
- else diffAndEmit(serverRows, write);
291
- commit();
292
- markReady();
420
+ let serverRows;
421
+ if (syncMode === "eager") {
422
+ serverRows = await table.listAll();
423
+ } else if (syncMode === "progressive") {
424
+ const first = await table.loadMore(
425
+ config.table.initialPageSize ?? DEFAULT_INITIAL_PAGE_SIZE
426
+ );
427
+ serverRows = first.rows;
428
+ } else {
429
+ serverRows = await table.listActive();
430
+ }
431
+ await enqueueWork(async () => {
432
+ begin();
433
+ if (useLoro) {
434
+ const localIds = loroToArray().map(getKey);
435
+ const verifiedServerRows = syncMode === "eager" ? serverRows : dedupeById([
436
+ ...serverRows,
437
+ ...await fetchServerByLocalIds(
438
+ localIds
439
+ )
440
+ ]);
441
+ reconcileBoot(verifiedServerRows, write);
442
+ } else {
443
+ diffAndEmit(serverRows, write);
444
+ }
445
+ commit();
446
+ markReady();
447
+ });
448
+ if (syncMode === "progressive") {
449
+ void (async () => {
450
+ while (!table.isFullyLoaded) {
451
+ const { rows } = await table.loadMore();
452
+ if (rows.length === 0) break;
453
+ await enqueueWork(async () => {
454
+ begin();
455
+ try {
456
+ if (useLoro) loroPutMany(rows);
457
+ diffAndEmit(rows, write);
458
+ } finally {
459
+ commit();
460
+ }
461
+ });
462
+ }
463
+ })().catch((e) => onError?.(e));
464
+ }
293
465
  await flushPushQueue();
294
466
  offLive = table.subscribe((evt) => {
295
- begin();
296
- try {
297
- if (evt.type === "insert" || evt.type === "update") {
298
- const row = evt.row;
299
- const deleted = useLoro ? row.sync_deleted ?? false : false;
300
- if (deleted) {
301
- if (useLoro) loroRemove(getKey(row));
302
- const prev = prevById.get(getKey(row)) ?? makeTombstone(getKey(row));
303
- write({ type: "delete", value: prev });
304
- prevById.delete(getKey(row));
467
+ void enqueueWork(async () => {
468
+ begin();
469
+ try {
470
+ if (evt.type === "insert" || evt.type === "update") {
471
+ const row = evt.row;
472
+ const deleted = useLoro ? row.sync_deleted ?? false : false;
473
+ if (deleted) {
474
+ if (useLoro) loroRemove(getKey(row));
475
+ const prev = prevById.get(getKey(row)) ?? makeTombstone(getKey(row));
476
+ write({ type: "delete", value: prev });
477
+ prevById.delete(getKey(row));
478
+ } else {
479
+ if (useLoro) loroPutMany([row]);
480
+ const had = prevById.has(getKey(row));
481
+ write({
482
+ type: had ? "update" : "insert",
483
+ value: row
484
+ });
485
+ prevById.set(getKey(row), row);
486
+ }
305
487
  } else {
306
- if (useLoro) loroPut(row);
307
- const had = prevById.has(getKey(row));
308
- write({
309
- type: had ? "update" : "insert",
310
- value: row
311
- });
312
- prevById.set(getKey(row), row);
488
+ const rid = getKey(evt.row);
489
+ if (useLoro) loroRemove(rid);
490
+ const prev = prevById.get(rid) ?? makeTombstone(rid);
491
+ write({ type: "delete", value: prev });
492
+ prevById.delete(rid);
313
493
  }
314
- } else if (evt.type === "delete") {
315
- const id2 = getKey(evt.row);
316
- if (useLoro) loroRemove(id2);
317
- const prev = prevById.get(id2) ?? makeTombstone(id2);
318
- write({ type: "delete", value: prev });
319
- prevById.delete(id2);
494
+ } finally {
495
+ commit();
320
496
  }
321
- } finally {
322
- commit();
323
- }
497
+ });
324
498
  });
325
499
  } catch (e) {
326
500
  onError?.(e);
@@ -337,12 +511,8 @@ function surrealCollectionOptions({
337
511
  for (const m of p.transaction.mutations) {
338
512
  if (m.type !== "insert") continue;
339
513
  const base = { ...m.modified };
340
- const row = useLoro ? {
341
- ...base,
342
- updated_at: now(),
343
- sync_deleted: false
344
- } : base;
345
- if (useLoro) loroPut(row);
514
+ const row = useLoro ? { ...base, updated_at: now(), sync_deleted: false } : base;
515
+ if (useLoro) loroPutMany([row]);
346
516
  await table.create(row);
347
517
  resultRows.push(row);
348
518
  }
@@ -354,11 +524,8 @@ function surrealCollectionOptions({
354
524
  if (m.type !== "update") continue;
355
525
  const id2 = m.key;
356
526
  const base = { ...m.modified, id: id2 };
357
- const merged = useLoro ? {
358
- ...base,
359
- updated_at: now()
360
- } : base;
361
- if (useLoro) loroPut(merged);
527
+ const merged = useLoro ? { ...base, updated_at: now() } : base;
528
+ if (useLoro) loroPutMany([merged]);
362
529
  const rid = new RecordId(config.table.name, keyOf(id2));
363
530
  await table.update(rid, merged);
364
531
  resultRows.push(merged);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@foretag/tanstack-db-surrealdb",
3
3
  "description": "Add Offline / Local First Caching & Syncing to your SurrealDB app with TanstackDB and Loro (CRDTs)",
4
- "version": "0.2.2",
4
+ "version": "0.3.0",
5
5
  "files": [
6
6
  "dist"
7
7
  ],