@foretag/tanstack-db-surrealdb 0.5.8 → 0.6.1

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/dist/index.js CHANGED
@@ -6,12 +6,80 @@ var surrealdb = require('surrealdb');
6
6
  var db = require('@tanstack/db');
7
7
 
8
8
  // src/index.ts
9
- var recordIdInternPool = /* @__PURE__ */ new Map();
10
- var internRecordId = (canonical) => {
11
- const cached = recordIdInternPool.get(canonical);
9
+
10
+ // src/crdt.ts
11
+ var toRecord = (value) => typeof value === "object" && value !== null ? value : {};
12
+ var materializeLoroJson = (doc, id) => {
13
+ const root = toRecord(doc.getMap("root").toJSON());
14
+ return {
15
+ id,
16
+ ...root
17
+ };
18
+ };
19
+ var applyLoroJsonChange = (doc, change) => {
20
+ const root = doc.getMap("root");
21
+ if (change.type === "delete") {
22
+ root.set("deleted", true);
23
+ return;
24
+ }
25
+ const value = toRecord(change.value);
26
+ for (const [key, fieldValue] of Object.entries(value)) {
27
+ if (key === "id") continue;
28
+ root.set(key, fieldValue);
29
+ }
30
+ };
31
+ var materializeLoroRichtext = (doc, id) => {
32
+ const metadata = toRecord(doc.getMap("root").toJSON());
33
+ const content = doc.getText("content").toString();
34
+ return {
35
+ id,
36
+ content,
37
+ ...metadata
38
+ };
39
+ };
40
+ var applyLoroRichtextChange = (doc, change) => {
41
+ const text = doc.getText("content");
42
+ const metadata = doc.getMap("root");
43
+ if (change.type === "delete") {
44
+ metadata.set("deleted", true);
45
+ return;
46
+ }
47
+ const value = toRecord(change.value);
48
+ for (const [key, fieldValue] of Object.entries(value)) {
49
+ if (key === "id") continue;
50
+ if (key === "content") {
51
+ if (typeof fieldValue === "string") text.update(fieldValue);
52
+ continue;
53
+ }
54
+ metadata.set(key, fieldValue);
55
+ }
56
+ };
57
+ var createLoroProfile = (profile) => {
58
+ if (profile === "richtext") {
59
+ return {
60
+ materialize: materializeLoroRichtext,
61
+ applyLocalChange: applyLoroRichtextChange
62
+ };
63
+ }
64
+ return {
65
+ materialize: materializeLoroJson,
66
+ applyLocalChange: applyLoroJsonChange
67
+ };
68
+ };
69
+ var recordIdIdentityPool = /* @__PURE__ */ new Map();
70
+ var nativeRecordIdPool = /* @__PURE__ */ new Map();
71
+ var internRecordIdIdentity = (canonical, preferred) => {
72
+ const cached = recordIdIdentityPool.get(canonical);
73
+ if (cached !== void 0) return cached;
74
+ const created = preferred ?? canonicalToNativeRecordId(canonical);
75
+ recordIdIdentityPool.set(canonical, created);
76
+ return created;
77
+ };
78
+ var getNativeRecordId = (canonical) => {
79
+ const cached = nativeRecordIdPool.get(canonical);
12
80
  if (cached) return cached;
13
81
  const created = canonicalToNativeRecordId(canonical);
14
- recordIdInternPool.set(canonical, created);
82
+ nativeRecordIdPool.set(canonical, created);
15
83
  return created;
16
84
  };
17
85
  var canonicalToNativeRecordId = (canonical) => {
@@ -56,6 +124,26 @@ var isRecordIdString = (value) => {
56
124
  return idx > 0 && idx < value.length - 1;
57
125
  };
58
126
  var looksLikeTableName = (value) => /^[A-Za-z_][A-Za-z0-9_-]*$/.test(value);
127
+ var getCtorName = (value) => {
128
+ if (!value || typeof value !== "object") return void 0;
129
+ const ctor = value.constructor;
130
+ return typeof ctor?.name === "string" ? ctor.name : void 0;
131
+ };
132
+ var isCrossRuntimeRecordIdObject = (value) => {
133
+ if (!value || typeof value !== "object" || value instanceof surrealdb.RecordId) {
134
+ return false;
135
+ }
136
+ const obj = value;
137
+ if (typeof obj.toString !== "function") return false;
138
+ if (!("table" in obj) || !("id" in obj)) return false;
139
+ const ctorName = getCtorName(value);
140
+ if (!ctorName || !/^RecordId\d*$/.test(ctorName)) return false;
141
+ const tableValue = obj.table;
142
+ const table = typeof tableValue === "string" ? tableValue : tableValue != null ? String(tableValue) : "";
143
+ const idValue = obj.id;
144
+ const idIsPrimitive = typeof idValue === "string" || typeof idValue === "number" || typeof idValue === "bigint";
145
+ return idIsPrimitive && looksLikeTableName(stripOuterQuotes(table).trim());
146
+ };
59
147
  var toCanonicalRecordIdString = (value) => {
60
148
  const normalized = toRecordIdString(value);
61
149
  if (!isRecordIdString(normalized)) return void 0;
@@ -72,6 +160,16 @@ var unwrapIdWrapper = (value) => {
72
160
  if (keys.length !== 1 || keys[0] !== "id") return void 0;
73
161
  return obj.id;
74
162
  };
163
+ var asCanonicalRecordIdFromCrossRuntimeObject = (value) => {
164
+ if (!isCrossRuntimeRecordIdObject(value)) return void 0;
165
+ const obj = value;
166
+ const tableRaw = typeof obj.table === "string" ? obj.table : String(obj.table);
167
+ const table = stripOuterQuotes(tableRaw).trim();
168
+ const id = String(obj.id);
169
+ const fromFields = table && id && looksLikeTableName(table) ? `${table}:${id}` : void 0;
170
+ if (fromFields) return toCanonicalRecordIdString(fromFields);
171
+ return toCanonicalRecordIdString(String(obj.toString()));
172
+ };
75
173
  var asCanonicalRecordIdString = (value) => {
76
174
  if (typeof value === "string") {
77
175
  return toCanonicalRecordIdString(value);
@@ -79,6 +177,8 @@ var asCanonicalRecordIdString = (value) => {
79
177
  if (value instanceof surrealdb.RecordId) {
80
178
  return toCanonicalRecordIdString(value.toString());
81
179
  }
180
+ const crossRuntimeCanonical = asCanonicalRecordIdFromCrossRuntimeObject(value);
181
+ if (crossRuntimeCanonical) return crossRuntimeCanonical;
82
182
  const wrappedId = unwrapIdWrapper(value);
83
183
  if (wrappedId === void 0 || wrappedId === value) return void 0;
84
184
  return asCanonicalRecordIdString(wrappedId);
@@ -87,52 +187,36 @@ var toNativeRecordIdLikeValue = (value) => {
87
187
  if (value instanceof surrealdb.RecordId) {
88
188
  const canonical2 = asCanonicalRecordIdString(value);
89
189
  if (!canonical2) return value;
90
- return internRecordId(canonical2);
190
+ return getNativeRecordId(canonical2);
91
191
  }
92
192
  const canonical = asCanonicalRecordIdString(value);
93
193
  if (!canonical) return value;
94
- return internRecordId(canonical);
194
+ return getNativeRecordId(canonical);
95
195
  };
96
- var preferRecordIdLikeIdentity = (value) => {
97
- const canonical = asCanonicalRecordIdString(value);
98
- if (!canonical) return normalizeRecordIdLikeValue(value);
196
+ var normalizeRecordIdLikeValue = (value) => {
99
197
  if (value instanceof surrealdb.RecordId) {
100
- recordIdInternPool.set(canonical, value);
101
- return value;
102
- }
103
- const wrappedId = unwrapIdWrapper(value);
104
- if (wrappedId instanceof surrealdb.RecordId) {
105
- recordIdInternPool.set(canonical, wrappedId);
106
- return wrappedId;
107
- }
108
- return internRecordId(canonical);
109
- };
110
- var preferRecordIdLikeIdentityDeep = (value) => {
111
- const preferred = preferRecordIdLikeIdentity(value);
112
- if (Array.isArray(preferred)) {
113
- return preferred.map(
114
- (item) => preferRecordIdLikeIdentityDeep(item)
115
- );
198
+ const canonical2 = asCanonicalRecordIdString(value);
199
+ if (!canonical2) return value;
200
+ return internRecordIdIdentity(canonical2, value);
116
201
  }
117
- if (isPlainObject(preferred)) {
118
- const out = {};
119
- for (const [k, v] of Object.entries(preferred)) {
120
- out[k] = preferRecordIdLikeIdentityDeep(v);
202
+ if (typeof value === "object" && value !== null) {
203
+ const canonical2 = asCanonicalRecordIdString(value);
204
+ if (!canonical2) return value;
205
+ if (isCrossRuntimeRecordIdObject(value)) {
206
+ return internRecordIdIdentity(canonical2, value);
121
207
  }
122
- return out;
208
+ const wrappedId = unwrapIdWrapper(value);
209
+ if (wrappedId instanceof surrealdb.RecordId || isCrossRuntimeRecordIdObject(wrappedId)) {
210
+ return internRecordIdIdentity(canonical2, wrappedId);
211
+ }
212
+ return internRecordIdIdentity(canonical2);
123
213
  }
124
- return preferred;
125
- };
126
- var normalizeRecordIdLikeValue = (value) => {
127
- if (value instanceof surrealdb.RecordId) return toNativeRecordIdLikeValue(value);
128
- if (typeof value === "object" && value !== null)
129
- return toNativeRecordIdLikeValue(value);
130
214
  if (typeof value !== "string") return value;
131
215
  const trimmed = value.trim();
132
216
  const unquoted = stripOuterQuotes(trimmed);
133
217
  const canonical = toCanonicalRecordIdString(unquoted) ?? (unquoted === trimmed ? void 0 : toCanonicalRecordIdString(trimmed));
134
218
  if (canonical) {
135
- return internRecordId(canonical);
219
+ return internRecordIdIdentity(canonical);
136
220
  }
137
221
  return value;
138
222
  };
@@ -169,126 +253,6 @@ var toRecordId = (tableName, id) => {
169
253
  const key = normalized.startsWith(prefixed) ? normalized.slice(prefixed.length) : normalized;
170
254
  return new surrealdb.RecordId(tableName, key);
171
255
  };
172
- var parseRecordIdLike = (value) => {
173
- const normalized = normalizeRecordIdLikeValue(value);
174
- if (normalized instanceof surrealdb.RecordId) {
175
- return {
176
- table: String(normalized.table),
177
- id: String(normalized.id)
178
- };
179
- }
180
- const str = toRecordIdString(String(normalized));
181
- const idx = str.indexOf(":");
182
- if (idx <= 0 || idx >= str.length - 1) {
183
- throw new Error(
184
- `Expected a record id in 'table:id' format, received '${String(value)}'.`
185
- );
186
- }
187
- return {
188
- table: str.slice(0, idx),
189
- id: str.slice(idx + 1)
190
- };
191
- };
192
- var eqRecordId = (field, value) => {
193
- const parsed = parseRecordIdLike(value);
194
- const fieldRef = field;
195
- const canonical = `${parsed.table}:${parsed.id}`;
196
- return db.or(
197
- db.eq(fieldRef, canonical),
198
- db.eq(fieldRef.id, parsed.id)
199
- );
200
- };
201
- var serializeValue = (value) => {
202
- const canonicalRecordId = asCanonicalRecordIdString(value);
203
- if (canonicalRecordId) {
204
- return {
205
- __type: "recordid",
206
- value: canonicalRecordId
207
- };
208
- }
209
- const normalized = normalizeRecordIdLikeValue(value);
210
- if (normalized instanceof surrealdb.RecordId) {
211
- return {
212
- __type: "recordid",
213
- value: toRecordIdString(normalized)
214
- };
215
- }
216
- if (normalized === void 0) {
217
- return { __type: "undefined" };
218
- }
219
- if (typeof normalized === "number") {
220
- if (Number.isNaN(normalized)) return { __type: "nan" };
221
- if (normalized === Number.POSITIVE_INFINITY) {
222
- return { __type: "infinity", sign: 1 };
223
- }
224
- if (normalized === Number.NEGATIVE_INFINITY) {
225
- return { __type: "infinity", sign: -1 };
226
- }
227
- }
228
- if (normalized === null || typeof normalized === "string" || typeof normalized === "number" || typeof normalized === "boolean") {
229
- return normalized;
230
- }
231
- if (normalized instanceof Date) {
232
- return { __type: "date", value: normalized.toJSON() };
233
- }
234
- if (Array.isArray(normalized)) {
235
- return normalized.map((item) => serializeValue(item));
236
- }
237
- if (typeof normalized === "object") {
238
- const entries = Object.entries(
239
- normalized
240
- ).sort(([a], [b]) => a.localeCompare(b));
241
- return Object.fromEntries(
242
- entries.map(([key, item]) => [key, serializeValue(item)])
243
- );
244
- }
245
- return normalized;
246
- };
247
- var serializeExpression = (expr) => {
248
- if (!expr) return null;
249
- switch (expr.type) {
250
- case "val":
251
- return {
252
- type: "val",
253
- value: serializeValue(expr.value)
254
- };
255
- case "ref":
256
- return {
257
- type: "ref",
258
- path: [...expr.path]
259
- };
260
- case "func":
261
- return {
262
- type: "func",
263
- name: expr.name,
264
- args: expr.args.map(
265
- (arg) => serializeExpression(arg)
266
- )
267
- };
268
- default:
269
- return null;
270
- }
271
- };
272
- var serializeSurrealSubsetOptions = (options) => {
273
- if (!options) return void 0;
274
- const out = {};
275
- if (options.where) out.where = serializeExpression(options.where);
276
- if (options.orderBy?.length) {
277
- out.orderBy = options.orderBy.map((clause) => ({
278
- expression: serializeExpression(
279
- clause.expression
280
- ),
281
- direction: clause.compareOptions.direction,
282
- nulls: clause.compareOptions.nulls,
283
- stringSort: clause.compareOptions.stringSort,
284
- locale: clause.compareOptions.locale,
285
- localeOptions: clause.compareOptions.localeOptions
286
- }));
287
- }
288
- if (options.limit !== void 0) out.limit = options.limit;
289
- if (options.offset !== void 0) out.offset = options.offset;
290
- return Object.keys(out).length ? JSON.stringify(out) : void 0;
291
- };
292
256
  var IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
293
257
  var firstRow = (res) => {
294
258
  if (!res) return void 0;
@@ -327,9 +291,10 @@ var mapRelationPath = (path, relation) => {
327
291
  return path;
328
292
  };
329
293
  var normalizeFilterValue = (value) => {
330
- if (Array.isArray(value)) {
294
+ if (Array.isArray(value))
331
295
  return value.map((item) => normalizeFilterValue(item));
332
- }
296
+ if (value && typeof value === "object")
297
+ return normalizeRecordIdLikeValue(value);
333
298
  const native = toNativeRecordIdLikeValue(value);
334
299
  if (native !== value) return native;
335
300
  return normalizeRecordIdLikeValue(value);
@@ -350,7 +315,7 @@ var joinLogical = (op, args) => {
350
315
  sql: parts.map((part) => `(${part})`).join(` ${op} `)
351
316
  };
352
317
  };
353
- var buildSubsetQuery = (table, useLoro, subset) => {
318
+ var buildSubsetQuery = (table, subset) => {
354
319
  let paramIdx = 0;
355
320
  const params = { table: table.name };
356
321
  const nextParam = (value) => {
@@ -371,7 +336,7 @@ var buildSubsetQuery = (table, useLoro, subset) => {
371
336
  }
372
337
  if (isReferencePathCandidate(value)) {
373
338
  throw new Error(
374
- "Got a field reference on the right side of a where comparison. Pass a concrete value (string/RecordId), not a reactive proxy/path."
339
+ "Got a field reference on the right side of a where comparison. Pass a concrete value."
375
340
  );
376
341
  }
377
342
  return {
@@ -439,7 +404,6 @@ var buildSubsetQuery = (table, useLoro, subset) => {
439
404
  const cursorWhere = whereSqlFrom(subset.cursor.whereFrom);
440
405
  if (cursorWhere) whereParts.push(cursorWhere);
441
406
  }
442
- if (useLoro) whereParts.push("sync_deleted = false");
443
407
  const whereSql = whereParts.length ? ` WHERE ${whereParts.map((part) => `(${part})`).join(" AND ")}` : "";
444
408
  const order = db.parseOrderByExpression(subset?.orderBy);
445
409
  const orderSql = order.length ? ` ORDER BY ${order.map(
@@ -454,14 +418,14 @@ var buildSubsetQuery = (table, useLoro, subset) => {
454
418
  params
455
419
  };
456
420
  };
457
- function manageTable(db, useLoro, config) {
421
+ function manageTable(db, config) {
458
422
  const { name } = config;
459
423
  const table = new surrealdb.Table(name);
460
424
  const listAll = async () => {
461
425
  return loadSubset();
462
426
  };
463
427
  const loadSubset = async (subset) => {
464
- const { sql, params } = buildSubsetQuery(config, useLoro, subset);
428
+ const { sql, params } = buildSubsetQuery(config, subset);
465
429
  const [res] = await db.query(sql, params);
466
430
  return res ?? [];
467
431
  };
@@ -478,29 +442,11 @@ function manageTable(db, useLoro, config) {
478
442
  };
479
443
  const update = async (id, data) => {
480
444
  const { id: _ignoredId, ...rest } = data;
481
- if (!useLoro) {
482
- await db.update(id).merge(rest);
483
- return;
484
- }
485
- await db.update(id).merge({
486
- ...rest,
487
- sync_deleted: false,
488
- updated_at: Date.now()
489
- });
445
+ await db.update(id).merge(rest);
490
446
  };
491
447
  const remove = async (id) => {
492
448
  await db.delete(id);
493
449
  };
494
- const softDelete = async (id) => {
495
- if (!useLoro) {
496
- await db.delete(id);
497
- return;
498
- }
499
- await db.upsert(id).merge({
500
- sync_deleted: true,
501
- updated_at: Date.now()
502
- });
503
- };
504
450
  const subscribe = (cb) => {
505
451
  let killed = false;
506
452
  let live;
@@ -530,93 +476,205 @@ function manageTable(db, useLoro, config) {
530
476
  create,
531
477
  update,
532
478
  remove,
533
- softDelete,
479
+ softDelete: remove,
534
480
  subscribe
535
481
  };
536
482
  }
537
483
 
538
- // src/index.ts
539
- var TEMP_ID_PREFIX = "__temp__";
540
- var NOOP = () => {
541
- };
542
- var SUBSCRIBE_PATCHED = Symbol("surrealdbSubscribePatched");
543
- var patchSubscribeChangesForRecordIds = (collection) => {
544
- if (!collection || typeof collection !== "object") return;
545
- const target = collection;
546
- if (target[SUBSCRIBE_PATCHED]) return;
547
- if (typeof target.subscribeChanges !== "function") return;
548
- const original = target.subscribeChanges.bind(target);
549
- target.subscribeChanges = (callback, options) => {
550
- const nextOptions = options ? { ...options } : options;
551
- if (nextOptions?.whereExpression)
552
- normalizeExpressionLiteralsInPlace(nextOptions.whereExpression);
553
- if (typeof nextOptions?.where === "function") {
554
- const originalWhere = nextOptions.where;
555
- nextOptions.where = (row) => {
556
- const expr = originalWhere(row);
557
- normalizeExpressionLiteralsWithExistingIdentityInPlace(expr);
558
- return expr;
559
- };
560
- }
561
- return original(callback, nextOptions);
562
- };
563
- target[SUBSCRIBE_PATCHED] = true;
484
+ // src/util.ts
485
+ var encoder = new TextEncoder();
486
+ var decoder = new TextDecoder();
487
+ var hasBuffer = typeof Buffer !== "undefined";
488
+ function toBytes(value) {
489
+ if (typeof value === "string") return encoder.encode(value);
490
+ return encoder.encode(JSON.stringify(value));
491
+ }
492
+ function fromBytes(bytes, deserialize = false) {
493
+ if (deserialize) return JSON.parse(decoder.decode(bytes));
494
+ return decoder.decode(bytes);
495
+ }
496
+ function toBase64(bytes) {
497
+ if (hasBuffer) return Buffer.from(bytes).toString("base64");
498
+ return btoa(String.fromCharCode(...bytes));
499
+ }
500
+ function fromBase64(base64) {
501
+ if (hasBuffer) return new Uint8Array(Buffer.from(base64, "base64"));
502
+ const bin = atob(base64);
503
+ const out = new Uint8Array(bin.length);
504
+ for (let i = 0; i < bin.length; i++) {
505
+ out[i] = bin.charCodeAt(i);
506
+ }
507
+ return out;
508
+ }
509
+
510
+ // src/encryption/index.ts
511
+ var DEFAULT_ALG = "AES-256-GCM";
512
+ var DEFAULT_KID = "default";
513
+ var DEFAULT_VERSION = 1;
514
+ var resolveCrypto = () => {
515
+ if (typeof globalThis.crypto !== "undefined") return globalThis.crypto;
516
+ throw new Error("Web Crypto API is not available in this runtime.");
564
517
  };
565
- var normalizeExpressionLiteralsInPlace = (expr) => {
566
- if (!expr || typeof expr !== "object") return;
567
- const node = expr;
568
- if (node.type === "val") {
569
- node.value = preferRecordIdLikeIdentityDeep(node.value);
570
- return;
518
+ var toCryptoBytes = (value) => new Uint8Array(value);
519
+ var WebCryptoAESGCM = class _WebCryptoAESGCM {
520
+ alg;
521
+ version;
522
+ kid;
523
+ resolveKey;
524
+ constructor(key, options = {}) {
525
+ this.alg = options.alg ?? DEFAULT_ALG;
526
+ this.version = options.version ?? DEFAULT_VERSION;
527
+ this.kid = options.kid ?? DEFAULT_KID;
528
+ this.resolveKey = options.resolveKey ?? ((incomingKid) => {
529
+ if (incomingKid !== this.kid) {
530
+ throw new Error(`No key configured for kid '${incomingKid}'.`);
531
+ }
532
+ return key;
533
+ });
571
534
  }
572
- if (node.type === "func" && Array.isArray(node.args)) {
573
- for (const arg of node.args) normalizeExpressionLiteralsInPlace(arg);
535
+ static async fromRawKey(rawKey, options = {}) {
536
+ const key = await resolveCrypto().subtle.importKey(
537
+ "raw",
538
+ toCryptoBytes(rawKey),
539
+ "AES-GCM",
540
+ false,
541
+ ["encrypt", "decrypt"]
542
+ );
543
+ return new _WebCryptoAESGCM(key, options);
574
544
  }
575
- };
576
- var normalizeExpressionLiteralsWithExistingIdentityInPlace = (expr) => {
577
- if (!expr || typeof expr !== "object") return;
578
- const node = expr;
579
- if (node.type === "val") {
580
- node.value = normalizeRecordIdLikeValueDeep(node.value);
581
- return;
545
+ async keyFor(kid) {
546
+ return await this.resolveKey(kid);
547
+ }
548
+ async encrypt(input) {
549
+ const crypto = resolveCrypto();
550
+ const alg = input.alg ?? this.alg;
551
+ const kid = input.kid ?? this.kid;
552
+ const v = input.v ?? this.version;
553
+ const nonce = crypto.getRandomValues(new Uint8Array(12));
554
+ const key = await this.keyFor(kid);
555
+ const ct = new Uint8Array(
556
+ await crypto.subtle.encrypt(
557
+ {
558
+ name: "AES-GCM",
559
+ iv: nonce,
560
+ additionalData: input.aad ? toCryptoBytes(input.aad) : void 0
561
+ },
562
+ key,
563
+ toCryptoBytes(input.plaintext)
564
+ )
565
+ );
566
+ return {
567
+ v,
568
+ alg,
569
+ kid,
570
+ n: toBase64(nonce),
571
+ ct: toBase64(ct)
572
+ };
582
573
  }
583
- if (node.type === "func" && Array.isArray(node.args)) {
584
- for (const arg of node.args)
585
- normalizeExpressionLiteralsWithExistingIdentityInPlace(arg);
574
+ async decrypt({ envelope, aad }) {
575
+ if (envelope.alg !== this.alg) {
576
+ throw new Error(
577
+ `Unsupported envelope algorithm '${envelope.alg}'. Expected '${this.alg}'.`
578
+ );
579
+ }
580
+ const key = await this.keyFor(envelope.kid);
581
+ const crypto = resolveCrypto();
582
+ const plaintext = await crypto.subtle.decrypt(
583
+ {
584
+ name: "AES-GCM",
585
+ iv: toCryptoBytes(fromBase64(envelope.n)),
586
+ additionalData: aad ? toCryptoBytes(aad) : void 0
587
+ },
588
+ key,
589
+ toCryptoBytes(fromBase64(envelope.ct))
590
+ );
591
+ return new Uint8Array(plaintext);
586
592
  }
587
593
  };
588
- var normalizeSubsetLiteralsInPlace = (subset) => {
589
- if (!subset) return;
590
- normalizeExpressionLiteralsInPlace(subset.where);
591
- normalizeExpressionLiteralsInPlace(subset.cursor?.whereFrom);
592
- normalizeExpressionLiteralsInPlace(subset.cursor?.whereCurrent);
594
+
595
+ // src/index.ts
596
+ var TEMP_ID_PREFIX = "__temp__";
597
+ var ENVELOPE_FIELDS = [
598
+ "version",
599
+ "algorithm",
600
+ "key_id",
601
+ "nonce",
602
+ "ciphertext"
603
+ ];
604
+ var NOOP = () => {
605
+ };
606
+ var isTableObject = (value) => typeof value === "object" && value !== null && "name" in value && typeof value.name === "string";
607
+ var toTableOptions = (table) => {
608
+ if (typeof table === "string") return { name: table };
609
+ if (table instanceof surrealdb.Table) return { name: table.name };
610
+ if (isTableObject(table)) return table;
611
+ throw new Error("Expected table as string, Table, or { name }.");
612
+ };
613
+ var toTableResource = (table) => {
614
+ const normalized = toTableOptions(table);
615
+ return new surrealdb.Table(normalized.name);
616
+ };
617
+ var tableNameOf = (table) => toTableOptions(table).name;
618
+ var getWriteUtils = (utils) => typeof utils === "object" && utils !== null ? utils : {};
619
+ var firstRow2 = (result) => {
620
+ if (!result) return void 0;
621
+ if (Array.isArray(result)) return result[0];
622
+ return result;
593
623
  };
624
+ var omitUndefined = (obj) => Object.fromEntries(
625
+ Object.entries(obj).filter(([, value]) => value !== void 0)
626
+ );
594
627
  var createTempRecordId = (tableName) => {
595
628
  const suffix = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
596
629
  return new surrealdb.RecordId(tableName, `${TEMP_ID_PREFIX}${suffix}`);
597
630
  };
598
631
  var isTempId = (id, tableName) => {
599
- if (id instanceof surrealdb.RecordId) {
600
- const recordKey = id.id;
601
- return typeof recordKey === "string" && recordKey.startsWith(TEMP_ID_PREFIX);
602
- }
603
- const raw = toRecordIdString(id);
604
- const key = raw.startsWith(`${tableName}:`) ? raw.slice(tableName.length + 1) : raw;
632
+ const normalized = toRecordIdString(id);
633
+ const key = normalized.startsWith(`${tableName}:`) ? normalized.slice(tableName.length + 1) : normalized;
605
634
  return key.startsWith(TEMP_ID_PREFIX);
606
635
  };
607
- function toCleanup(res) {
608
- if (!res) return NOOP;
609
- if (typeof res === "function") return res;
610
- const cleanup = res.cleanup ?? res.unsubscribe ?? res.dispose;
611
- return typeof cleanup === "function" ? cleanup : NOOP;
612
- }
613
- function hasLoadSubset(res) {
614
- return typeof res === "object" && res !== null && "loadSubset" in res;
636
+ var toEnvelope = (value) => {
637
+ const version = value.version;
638
+ const algorithm = value.algorithm;
639
+ const keyId = value.key_id;
640
+ const nonce = value.nonce;
641
+ const ciphertext = value.ciphertext;
642
+ if (typeof version !== "number" || typeof algorithm !== "string" || typeof keyId !== "string" || typeof nonce !== "string" || typeof ciphertext !== "string") {
643
+ return null;
644
+ }
645
+ return {
646
+ v: version,
647
+ alg: algorithm,
648
+ kid: keyId,
649
+ n: nonce,
650
+ ct: ciphertext
651
+ };
652
+ };
653
+ var toStoredEnvelope = (envelope) => ({
654
+ version: envelope.v,
655
+ algorithm: envelope.alg,
656
+ key_id: envelope.kid,
657
+ nonce: envelope.n,
658
+ ciphertext: envelope.ct
659
+ });
660
+ var stripEnvelopeFields = (value) => {
661
+ const copy = { ...value };
662
+ for (const key of ENVELOPE_FIELDS) delete copy[key];
663
+ return copy;
664
+ };
665
+ var toRecordArray = (rows) => {
666
+ if (!rows) return [];
667
+ return Array.isArray(rows) ? rows : [rows];
668
+ };
669
+ async function queryRows(db, sql, bindings) {
670
+ const result = await db.query(sql, bindings ?? {});
671
+ if (Array.isArray(result)) {
672
+ const first = result[0];
673
+ if (Array.isArray(first)) return first;
674
+ return [];
675
+ }
676
+ return [];
615
677
  }
616
- var getWriteUtils = (utils) => typeof utils === "object" && utils !== null ? utils : {};
617
- var omitUndefined = (obj) => Object.fromEntries(
618
- Object.entries(obj).filter(([, value]) => value !== void 0)
619
- );
620
678
  function createInsertSchema(tableName) {
621
679
  return {
622
680
  "~standard": {
@@ -639,216 +697,669 @@ function createInsertSchema(tableName) {
639
697
  }
640
698
  };
641
699
  }
642
- function surrealCollectionOptions({
643
- id,
644
- useLoro = false,
645
- onError,
646
- db,
647
- queryClient,
648
- queryKey,
649
- syncMode = "eager",
650
- ...config
651
- }) {
652
- let loro;
653
- if (useLoro) loro = { doc: new loroCrdt.LoroDoc(), key: id };
654
- const resolvedQueryKey = syncMode === "on-demand" ? (opts = {}) => {
655
- normalizeSubsetLiteralsInPlace(opts);
656
- const serialized = serializeSurrealSubsetOptions(opts);
657
- return serialized ? [...queryKey, serialized] : [...queryKey];
658
- } : queryKey;
659
- const table = manageTable(db, useLoro, config.table);
660
- const keyOf = (rid) => toRecordIdString(rid);
661
- const getKey = (row) => keyOf(row.id);
662
- const normalizeMutationId = (rid) => toRecordId(config.table.name, rid);
663
- const withRecordId = (row) => {
700
+ function defaultAad(ctx) {
701
+ if (ctx.kind === "base") return toBytes(`${ctx.table}:${ctx.id}`);
702
+ const base = ctx.baseTable ?? ctx.table;
703
+ return toBytes(`${ctx.table}:${base}:${ctx.id}`);
704
+ }
705
+ var syncModeFrom = (syncMode) => syncMode ?? "eager";
706
+ var subsetCacheKey = (subset) => JSON.stringify(subset, (_key, value) => {
707
+ if (value instanceof Date) return value.toISOString();
708
+ if (value instanceof surrealdb.RecordId) return toRecordIdString(value);
709
+ if (typeof value === "function") return void 0;
710
+ if (value && typeof value === "object" && "table" in value && "id" in value) {
711
+ try {
712
+ return toRecordIdString(String(value));
713
+ } catch {
714
+ return value;
715
+ }
716
+ }
717
+ return value;
718
+ });
719
+ function modernSurrealCollectionOptions(config) {
720
+ const {
721
+ db,
722
+ table,
723
+ queryClient,
724
+ queryKey,
725
+ onError,
726
+ e2ee,
727
+ crdt,
728
+ syncMode: inputSyncMode
729
+ } = config;
730
+ if (!queryClient || !queryKey) {
731
+ throw new Error("queryClient and queryKey are required.");
732
+ }
733
+ const syncMode = syncModeFrom(inputSyncMode);
734
+ const isOnDemandLike = syncMode === "on-demand" || syncMode === "progressive";
735
+ const isStrictOnDemand = syncMode === "on-demand";
736
+ const queryDrivenSyncMode = isOnDemandLike ? "on-demand" : "eager";
737
+ const tableOptions = toTableOptions(table);
738
+ const tableName = tableOptions.name;
739
+ const tableResource = toTableResource(table);
740
+ const subsetIds = /* @__PURE__ */ new Map();
741
+ const activeOnDemandIds = /* @__PURE__ */ new Set();
742
+ const e2eeEnabled = e2ee?.enabled === true;
743
+ const crdtEnabled = crdt?.enabled === true;
744
+ const defaultCrdtProfile = crdtEnabled ? createLoroProfile(crdt.profile) : void 0;
745
+ const materializeCrdt = crdt?.materialize ?? defaultCrdtProfile?.materialize;
746
+ const applyCrdtLocalChange = crdt?.applyLocalChange ?? defaultCrdtProfile?.applyLocalChange;
747
+ if (crdtEnabled && (!materializeCrdt || !applyCrdtLocalChange)) {
748
+ throw new Error(
749
+ "CRDT profile adapter is missing materialize/applyLocalChange handlers."
750
+ );
751
+ }
752
+ const tableAccess = manageTable(db, tableOptions);
753
+ const docs = /* @__PURE__ */ new Map();
754
+ const aadFor = (ctx) => (e2ee?.aad ?? defaultAad)(ctx);
755
+ const getKey = (row) => toRecordIdString(row.id);
756
+ const normalizeMutationId = (id) => toRecordId(tableName, id);
757
+ const normalizeRow = (row) => {
664
758
  const normalized = normalizeRecordIdLikeValueDeep(row);
665
759
  return {
666
760
  ...normalized,
667
761
  id: normalizeMutationId(normalized.id)
668
762
  };
669
763
  };
670
- const toLoroStoredRow = (row) => ({ ...row, id: keyOf(row.id) });
671
- const loroKey = loro?.key ?? id ?? "surreal";
672
- const loroMap = useLoro ? loro?.doc?.getMap?.(loroKey) ?? null : null;
673
- const commitLoro = () => {
674
- loro?.doc?.commit?.();
764
+ const decodeBaseRow = async (row) => {
765
+ if (!e2eeEnabled) {
766
+ return normalizeRow(row);
767
+ }
768
+ const envelope = toEnvelope(row);
769
+ if (!envelope) return normalizeRow(row);
770
+ const id = toRecordKeyString(row.id);
771
+ const bytes = await e2ee.crypto.decrypt({
772
+ envelope,
773
+ aad: aadFor({ table: tableName, id, kind: "base" })
774
+ });
775
+ const payload = fromBytes(bytes, true);
776
+ const merged = {
777
+ ...stripEnvelopeFields(row),
778
+ ...payload,
779
+ id: normalizeMutationId(row.id)
780
+ };
781
+ return normalizeRow(merged);
782
+ };
783
+ const encodeBaseRow = async (row, id) => {
784
+ if (!e2eeEnabled) return row;
785
+ const envelope = await e2ee.crypto.encrypt({
786
+ plaintext: toBytes(row),
787
+ aad: aadFor({ table: tableName, id, kind: "base" })
788
+ });
789
+ return toStoredEnvelope(envelope);
675
790
  };
676
- const loroPut = (row, commit = true) => {
677
- if (!loroMap) return;
678
- loroMap.set(getKey(row), toLoroStoredRow(row));
679
- if (commit) commitLoro();
791
+ const updatesTableName = crdtEnabled ? tableNameOf(crdt.updatesTable) : void 0;
792
+ const updatesTable = crdtEnabled ? toTableResource(crdt.updatesTable) : void 0;
793
+ const snapshotsTableName = crdtEnabled && crdt.snapshotsTable ? tableNameOf(crdt.snapshotsTable) : void 0;
794
+ crdtEnabled && crdt.snapshotsTable ? toTableResource(crdt.snapshotsTable) : void 0;
795
+ const getDoc = (id) => {
796
+ const existing = docs.get(id);
797
+ if (existing) return existing;
798
+ const doc = new loroCrdt.LoroDoc();
799
+ docs.set(id, doc);
800
+ return doc;
680
801
  };
681
- const loroRemove = (idStr, commit = true) => {
682
- if (!loroMap) return;
683
- loroMap.delete(idStr);
684
- if (commit) commitLoro();
802
+ const docRef = (id) => new surrealdb.RecordId(tableName, id);
803
+ const idFromDocRef = (doc) => toRecordKeyString(doc);
804
+ const resolveActor = (id, change) => {
805
+ if (!crdtEnabled) return void 0;
806
+ const candidate = crdt.actor ?? crdt.localActorId;
807
+ if (typeof candidate === "function") {
808
+ return candidate({ id, change });
809
+ }
810
+ return candidate;
685
811
  };
686
- const mergeLocalOverServer = (serverRows) => {
687
- if (!useLoro || !loroMap) return serverRows;
688
- const localJson = loroMap.toJSON?.() ?? {};
689
- const out = [];
690
- for (const s of serverRows) {
691
- const idStr = getKey(s);
692
- const l = localJson[idStr];
693
- if (!l) {
694
- out.push(s);
695
- continue;
812
+ const decodeUpdateBytes = async (row, kind) => {
813
+ if (!e2eeEnabled) {
814
+ if (kind === "snapshot") {
815
+ const snapshot = row.snapshot_bytes;
816
+ if (!snapshot) return new Uint8Array();
817
+ return fromBase64(snapshot);
696
818
  }
697
- if ((l.sync_deleted ?? false) === true) continue;
698
- out.push(withRecordId(l));
819
+ const update = row.update_bytes;
820
+ if (!update) return new Uint8Array();
821
+ return fromBase64(update);
699
822
  }
700
- return out;
823
+ const envelope = toEnvelope(row);
824
+ if (!envelope) return new Uint8Array();
825
+ const id = idFromDocRef(row.doc);
826
+ return e2ee.crypto.decrypt({
827
+ envelope,
828
+ aad: aadFor({
829
+ table: kind === "snapshot" ? snapshotsTableName ?? tableName : updatesTableName ?? tableName,
830
+ baseTable: tableName,
831
+ id,
832
+ kind
833
+ })
834
+ });
835
+ };
836
+ const encodeUpdatePayload = async (bytes, id, kind) => {
837
+ if (!e2eeEnabled) {
838
+ return { update_bytes: toBase64(bytes) };
839
+ }
840
+ const targetTable = updatesTableName;
841
+ const envelope = await e2ee.crypto.encrypt({
842
+ plaintext: bytes,
843
+ aad: aadFor({
844
+ table: targetTable ?? tableName,
845
+ baseTable: tableName,
846
+ id,
847
+ kind
848
+ })
849
+ });
850
+ return toStoredEnvelope(envelope);
851
+ };
852
+ const persistCrdtUpdate = async (id, bytes, change) => {
853
+ if (!crdtEnabled || !updatesTable) return;
854
+ const payload = await encodeUpdatePayload(bytes, id, "update");
855
+ const actor = resolveActor(id, change);
856
+ const row = {
857
+ doc: docRef(id),
858
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
859
+ ...actor ? { actor } : {},
860
+ ...payload
861
+ };
862
+ await db.create(updatesTable).content(row);
863
+ };
864
+ const persistMaterialized = async (materialized) => {
865
+ if (!crdtEnabled || crdt.persistMaterializedView !== true) return;
866
+ const id = toRecordKeyString(materialized.id);
867
+ if (!e2eeEnabled) {
868
+ const { id: _ignoredId, ...rest } = materialized;
869
+ await db.upsert(normalizeMutationId(materialized.id)).merge(rest);
870
+ return;
871
+ }
872
+ const payload = await encodeBaseRow(
873
+ omitUndefined({
874
+ ...materialized,
875
+ id: void 0
876
+ }),
877
+ id
878
+ );
879
+ await db.upsert(normalizeMutationId(materialized.id)).merge(payload);
880
+ };
881
+ const hydratePlainRows = async (rows, write, begin, commit, type = "insert") => {
882
+ if (!rows.length) return;
883
+ begin();
884
+ try {
885
+ for (const row of rows) {
886
+ const decoded = await decodeBaseRow(row);
887
+ write({ type, value: decoded });
888
+ }
889
+ } finally {
890
+ commit();
891
+ }
892
+ };
893
+ const hydrateCrdtDoc = async (id, write, begin, commit, writeType = "insert") => {
894
+ if (!crdtEnabled || !updatesTableName) return;
895
+ const doc = getDoc(id);
896
+ let since;
897
+ if (snapshotsTableName) {
898
+ const snapshots = await queryRows(
899
+ db,
900
+ `SELECT * FROM type::table($table) WHERE doc = $doc ORDER BY ts DESC LIMIT 1;`,
901
+ {
902
+ table: snapshotsTableName,
903
+ doc: docRef(id)
904
+ }
905
+ );
906
+ const snapshot = snapshots[0];
907
+ if (snapshot) {
908
+ const bytes = await decodeUpdateBytes(snapshot, "snapshot");
909
+ if (bytes.byteLength) doc.import(bytes);
910
+ since = typeof snapshot.ts === "string" ? snapshot.ts : snapshot.ts instanceof Date ? snapshot.ts.toISOString() : void 0;
911
+ }
912
+ }
913
+ const updates = await queryRows(
914
+ db,
915
+ since ? `SELECT * FROM type::table($table) WHERE doc = $doc AND ts > $since ORDER BY ts ASC;` : `SELECT * FROM type::table($table) WHERE doc = $doc ORDER BY ts ASC;`,
916
+ {
917
+ table: updatesTableName,
918
+ doc: docRef(id),
919
+ since
920
+ }
921
+ );
922
+ for (const update of updates) {
923
+ const bytes = await decodeUpdateBytes(update, "update");
924
+ if (!bytes.byteLength) continue;
925
+ doc.import(bytes);
926
+ }
927
+ const materialized = normalizeRow(materializeCrdt(doc, id));
928
+ begin();
929
+ try {
930
+ write({ type: writeType, value: materialized });
931
+ } finally {
932
+ commit();
933
+ }
934
+ };
935
+ const updateActiveOnDemandIds = () => {
936
+ activeOnDemandIds.clear();
937
+ for (const ids of subsetIds.values()) {
938
+ for (const id of ids) activeOnDemandIds.add(id);
939
+ }
940
+ };
941
+ const createSyncRuntime = (ctx) => {
942
+ let cleanupBaseLive = NOOP;
943
+ let cleanupUpdateLive = NOOP;
944
+ let killed = false;
945
+ const ensureBaseLive = async () => {
946
+ if (cleanupBaseLive !== NOOP) return;
947
+ if (!db.isFeatureSupported?.(surrealdb.Features.LiveQueries)) return;
948
+ const live = await db.live(
949
+ tableResource
950
+ );
951
+ if (killed) {
952
+ await live.kill();
953
+ return;
954
+ }
955
+ live.subscribe(async (message) => {
956
+ if (message.action === "KILLED") return;
957
+ const row = message.value;
958
+ const id = toRecordKeyString(row.id);
959
+ const wasVisible = activeOnDemandIds.has(id);
960
+ if (isStrictOnDemand && !wasVisible && message.action !== "DELETE")
961
+ return;
962
+ if (message.action === "DELETE") {
963
+ for (const ids of subsetIds.values()) ids.delete(id);
964
+ updateActiveOnDemandIds();
965
+ if (isStrictOnDemand && !wasVisible) return;
966
+ ctx.begin();
967
+ try {
968
+ ctx.write({
969
+ type: "delete",
970
+ key: `${tableName}:${id}`
971
+ });
972
+ } finally {
973
+ ctx.commit();
974
+ }
975
+ return;
976
+ }
977
+ const decoded = await decodeBaseRow(row);
978
+ ctx.begin();
979
+ try {
980
+ ctx.write({
981
+ type: message.action === "CREATE" ? "insert" : "update",
982
+ value: decoded
983
+ });
984
+ } finally {
985
+ ctx.commit();
986
+ }
987
+ });
988
+ cleanupBaseLive = () => {
989
+ void live.kill().catch(() => void 0);
990
+ cleanupBaseLive = NOOP;
991
+ };
992
+ };
993
+ const ensureUpdateLive = async () => {
994
+ if (!crdtEnabled || !updatesTable) return;
995
+ if (cleanupUpdateLive !== NOOP) return;
996
+ if (!db.isFeatureSupported?.(surrealdb.Features.LiveQueries)) return;
997
+ const live = await db.live(
998
+ updatesTable
999
+ );
1000
+ if (killed) {
1001
+ await live.kill();
1002
+ return;
1003
+ }
1004
+ live.subscribe(async (message) => {
1005
+ if (message.action === "KILLED") return;
1006
+ if (message.action === "DELETE") return;
1007
+ const value = message.value;
1008
+ const id = idFromDocRef(value.doc);
1009
+ if (value.actor && value.actor === resolveActor(id)) return;
1010
+ if (isStrictOnDemand && !activeOnDemandIds.has(id)) return;
1011
+ const doc = getDoc(id);
1012
+ const bytes = await decodeUpdateBytes(value, "update");
1013
+ if (!bytes.byteLength) return;
1014
+ doc.import(bytes);
1015
+ const materialized = normalizeRow(materializeCrdt(doc, id));
1016
+ ctx.begin();
1017
+ try {
1018
+ ctx.write({ type: "update", value: materialized });
1019
+ } finally {
1020
+ ctx.commit();
1021
+ }
1022
+ });
1023
+ cleanupUpdateLive = () => {
1024
+ void live.kill().catch(() => void 0);
1025
+ cleanupUpdateLive = NOOP;
1026
+ };
1027
+ };
1028
+ const loadSubset = async (subset) => {
1029
+ if (!isOnDemandLike) return;
1030
+ const key = subsetCacheKey(subset);
1031
+ const rows = await tableAccess.loadSubset(subset);
1032
+ const ids = new Set(
1033
+ rows.map(
1034
+ (row) => toRecordKeyString(row.id)
1035
+ )
1036
+ );
1037
+ subsetIds.set(key, ids);
1038
+ updateActiveOnDemandIds();
1039
+ if (!crdtEnabled) {
1040
+ await hydratePlainRows(
1041
+ rows,
1042
+ ctx.write,
1043
+ ctx.begin,
1044
+ ctx.commit,
1045
+ "insert"
1046
+ );
1047
+ await ensureBaseLive();
1048
+ return;
1049
+ }
1050
+ for (const id of ids) {
1051
+ await hydrateCrdtDoc(
1052
+ id,
1053
+ ctx.write,
1054
+ ctx.begin,
1055
+ ctx.commit,
1056
+ "insert"
1057
+ );
1058
+ }
1059
+ await ensureUpdateLive();
1060
+ };
1061
+ const unloadSubset = (subset) => {
1062
+ if (!isOnDemandLike) return;
1063
+ const key = subsetCacheKey(subset);
1064
+ subsetIds.delete(key);
1065
+ updateActiveOnDemandIds();
1066
+ if (subsetIds.size === 0) {
1067
+ cleanupBaseLive();
1068
+ cleanupUpdateLive();
1069
+ }
1070
+ };
1071
+ const startRealtime = async () => {
1072
+ if (!crdtEnabled) {
1073
+ await ensureBaseLive();
1074
+ return;
1075
+ }
1076
+ await ensureUpdateLive();
1077
+ };
1078
+ const cleanup = () => {
1079
+ killed = true;
1080
+ subsetIds.clear();
1081
+ updateActiveOnDemandIds();
1082
+ cleanupBaseLive();
1083
+ cleanupUpdateLive();
1084
+ };
1085
+ return {
1086
+ startRealtime,
1087
+ cleanup,
1088
+ loadSubset,
1089
+ unloadSubset
1090
+ };
701
1091
  };
702
1092
  const base = queryDbCollection.queryCollectionOptions({
703
- schema: createInsertSchema(config.table.name),
1093
+ schema: createInsertSchema(tableName),
704
1094
  getKey,
705
- queryKey: resolvedQueryKey,
1095
+ queryKey,
706
1096
  queryClient,
707
- syncMode,
1097
+ syncMode: queryDrivenSyncMode,
708
1098
  queryFn: async ({ meta }) => {
709
1099
  try {
710
- const subset = syncMode === "on-demand" ? meta?.loadSubsetOptions : void 0;
711
- normalizeSubsetLiteralsInPlace(subset);
712
- const rows = syncMode === "eager" ? await table.listAll() : await table.loadSubset(subset);
713
- return mergeLocalOverServer(rows).map(
714
- (row) => withRecordId(row)
1100
+ if (isOnDemandLike && !meta?.loadSubsetOptions) {
1101
+ return [];
1102
+ }
1103
+ if (!crdtEnabled) {
1104
+ if (!isOnDemandLike) {
1105
+ const rows2 = await toRecordArray(
1106
+ await db.select(tableResource)
1107
+ );
1108
+ const decoded2 = await Promise.all(
1109
+ rows2.map(
1110
+ (row) => decodeBaseRow(row)
1111
+ )
1112
+ );
1113
+ return decoded2;
1114
+ }
1115
+ const rows = await tableAccess.loadSubset(
1116
+ meta?.loadSubsetOptions
1117
+ );
1118
+ const decoded = await Promise.all(
1119
+ rows.map(
1120
+ (row) => decodeBaseRow(row)
1121
+ )
1122
+ );
1123
+ return decoded;
1124
+ }
1125
+ if (isOnDemandLike) return [];
1126
+ if (!updatesTableName) return [];
1127
+ const updates = await queryRows(
1128
+ db,
1129
+ `SELECT * FROM type::table($table) ORDER BY ts ASC;`,
1130
+ { table: updatesTableName }
1131
+ );
1132
+ for (const update of updates) {
1133
+ const id = idFromDocRef(update.doc);
1134
+ const doc = getDoc(id);
1135
+ const bytes = await decodeUpdateBytes(update, "update");
1136
+ if (!bytes.byteLength) continue;
1137
+ doc.import(bytes);
1138
+ }
1139
+ return [...docs.entries()].map(
1140
+ ([id, doc]) => normalizeRow(materializeCrdt(doc, id))
715
1141
  );
716
- } catch (e) {
717
- onError?.(e);
1142
+ } catch (error) {
1143
+ onError?.(error);
718
1144
  return [];
719
1145
  }
720
1146
  },
721
- onInsert: (async (p) => {
722
- const now = /* @__PURE__ */ new Date();
723
- const resultRows = [];
724
- let shouldCommitLoro = false;
725
- for (const m of p.transaction.mutations) {
726
- if (m.type !== "insert") continue;
727
- const baseRow = { ...m.modified };
728
- const row = useLoro ? {
729
- ...baseRow,
730
- updated_at: now,
731
- sync_deleted: false
732
- } : baseRow;
733
- const normalizedRow = withRecordId(row);
734
- if (useLoro) {
735
- loroPut(normalizedRow, false);
736
- shouldCommitLoro = true;
737
- }
738
- if (isTempId(normalizedRow.id, config.table.name)) {
739
- const tempKey = keyOf(normalizedRow.id);
740
- const { id: _id, ...payload } = normalizedRow;
741
- const persisted = await table.create(payload);
742
- const resolvedRow = persisted?.id ? withRecordId({
743
- ...normalizedRow,
744
- ...persisted,
745
- id: persisted.id
746
- }) : normalizedRow;
747
- if (useLoro && persisted?.id) {
748
- loroRemove(tempKey, false);
749
- loroPut(resolvedRow, false);
1147
+ onInsert: (async (params) => {
1148
+ const out = [];
1149
+ const writeUtils = getWriteUtils(params.collection.utils);
1150
+ for (const mutation of params.transaction.mutations) {
1151
+ if (mutation.type !== "insert") continue;
1152
+ const normalized = normalizeRow(mutation.modified);
1153
+ if (!crdtEnabled) {
1154
+ const idKey = toRecordKeyString(normalized.id);
1155
+ const payload = omitUndefined({
1156
+ ...normalized,
1157
+ id: void 0
1158
+ });
1159
+ const recordPayload = await encodeBaseRow(payload, idKey);
1160
+ if (isTempId(normalized.id, tableName)) {
1161
+ const created = await db.create(tableResource).content(recordPayload);
1162
+ const createdRow = firstRow2(
1163
+ toRecordArray(created)
1164
+ );
1165
+ const createdId = createdRow && createdRow.id ? createdRow.id : normalized.id;
1166
+ const resolved = {
1167
+ ...normalized,
1168
+ id: normalizeMutationId(createdId)
1169
+ };
1170
+ out.push(resolved);
1171
+ writeUtils.writeUpsert?.(resolved);
1172
+ continue;
750
1173
  }
751
- resultRows.push(resolvedRow);
752
- } else {
753
- const persisted = await table.create(normalizedRow);
754
- resultRows.push(
755
- persisted ? withRecordId({
756
- ...normalizedRow,
757
- ...persisted
758
- }) : normalizedRow
759
- );
1174
+ await db.insert(tableResource, {
1175
+ id: normalizeMutationId(normalized.id),
1176
+ ...recordPayload
1177
+ });
1178
+ out.push(normalized);
1179
+ writeUtils.writeUpsert?.(normalized);
1180
+ continue;
760
1181
  }
1182
+ const id = toRecordKeyString(normalized.id);
1183
+ const doc = getDoc(id);
1184
+ const vv = doc.oplogVersion();
1185
+ const localChange = {
1186
+ type: "insert",
1187
+ value: normalized
1188
+ };
1189
+ applyCrdtLocalChange(doc, localChange);
1190
+ const bytes = doc.export({ mode: "update", from: vv });
1191
+ await persistCrdtUpdate(id, bytes, localChange);
1192
+ const materialized = normalizeRow(materializeCrdt(doc, id));
1193
+ await persistMaterialized(materialized);
1194
+ out.push(materialized);
1195
+ writeUtils.writeUpsert?.(materialized);
761
1196
  }
762
- if (shouldCommitLoro) commitLoro();
763
- return resultRows;
1197
+ return out;
764
1198
  }),
765
- onUpdate: (async (p) => {
766
- const now = /* @__PURE__ */ new Date();
767
- const writeUtils = getWriteUtils(p.collection.utils);
768
- let shouldCommitLoro = false;
769
- for (const m of p.transaction.mutations) {
770
- if (m.type !== "update") continue;
771
- const idKey = m.key;
1199
+ onUpdate: (async (params) => {
1200
+ const writeUtils = getWriteUtils(params.collection.utils);
1201
+ for (const mutation of params.transaction.mutations) {
1202
+ if (mutation.type !== "update") continue;
1203
+ const mutationId = normalizeMutationId(
1204
+ mutation.key
1205
+ );
772
1206
  const normalizedModified = omitUndefined(
773
1207
  normalizeRecordIdLikeFields({
774
- ...m.modified
1208
+ ...mutation.modified
775
1209
  })
776
1210
  );
777
- const baseRow = {
1211
+ const normalized = normalizeRow({
778
1212
  ...normalizedModified,
779
- id: normalizeMutationId(idKey)
780
- };
781
- const row = useLoro ? { ...baseRow, updated_at: now } : baseRow;
782
- const normalizedRow = withRecordId(row);
783
- if (useLoro) {
784
- loroPut(normalizedRow, false);
785
- shouldCommitLoro = true;
1213
+ id: mutationId
1214
+ });
1215
+ if (!crdtEnabled) {
1216
+ if (!e2eeEnabled) {
1217
+ await db.update(mutationId).merge(normalizedModified);
1218
+ writeUtils.writeUpsert?.(normalized);
1219
+ continue;
1220
+ }
1221
+ const current = await db.select(mutationId);
1222
+ const currentRows = toRecordArray(
1223
+ current
1224
+ );
1225
+ const currentRow = currentRows[0];
1226
+ const decodedCurrent = currentRow ? await decodeBaseRow(currentRow) : { id: mutationId };
1227
+ const merged = {
1228
+ ...decodedCurrent,
1229
+ ...normalizedModified
1230
+ };
1231
+ delete merged.id;
1232
+ const encoded = await encodeBaseRow(
1233
+ omitUndefined(merged),
1234
+ toRecordKeyString(mutationId)
1235
+ );
1236
+ await db.update(mutationId).merge(encoded);
1237
+ writeUtils.writeUpsert?.(
1238
+ normalizeRow({ ...decodedCurrent, ...merged })
1239
+ );
1240
+ continue;
786
1241
  }
787
- await table.update(normalizeMutationId(idKey), normalizedRow);
788
- writeUtils.writeUpsert?.(normalizedRow);
1242
+ const id = toRecordKeyString(mutationId);
1243
+ const doc = getDoc(id);
1244
+ const vv = doc.oplogVersion();
1245
+ const localChange = {
1246
+ type: "update",
1247
+ value: normalized
1248
+ };
1249
+ applyCrdtLocalChange(doc, localChange);
1250
+ const bytes = doc.export({ mode: "update", from: vv });
1251
+ await persistCrdtUpdate(id, bytes, localChange);
1252
+ const materialized = normalizeRow(materializeCrdt(doc, id));
1253
+ await persistMaterialized(materialized);
1254
+ writeUtils.writeUpsert?.(materialized);
789
1255
  }
790
- if (shouldCommitLoro) commitLoro();
791
1256
  return { refetch: false };
792
1257
  }),
793
- onDelete: (async (p) => {
794
- const writeUtils = getWriteUtils(p.collection.utils);
795
- let shouldCommitLoro = false;
796
- for (const m of p.transaction.mutations) {
797
- if (m.type !== "delete") continue;
798
- const idKey = m.key;
799
- const key = keyOf(idKey);
800
- if (useLoro) {
801
- loroRemove(key, false);
802
- shouldCommitLoro = true;
1258
+ onDelete: (async (params) => {
1259
+ const writeUtils = getWriteUtils(params.collection.utils);
1260
+ for (const mutation of params.transaction.mutations) {
1261
+ if (mutation.type !== "delete") continue;
1262
+ const mutationId = normalizeMutationId(
1263
+ mutation.key
1264
+ );
1265
+ const id = toRecordKeyString(mutationId);
1266
+ if (!crdtEnabled) {
1267
+ await db.delete(mutationId);
1268
+ writeUtils.writeDelete?.(`${tableName}:${id}`);
1269
+ continue;
803
1270
  }
804
- await table.softDelete(normalizeMutationId(idKey));
805
- writeUtils.writeDelete?.(key);
1271
+ const doc = getDoc(id);
1272
+ const vv = doc.oplogVersion();
1273
+ const localChange = {
1274
+ type: "delete",
1275
+ value: { id: mutationId }
1276
+ };
1277
+ applyCrdtLocalChange(doc, localChange);
1278
+ const bytes = doc.export({ mode: "update", from: vv });
1279
+ await persistCrdtUpdate(id, bytes, localChange);
1280
+ writeUtils.writeDelete?.(`${tableName}:${id}`);
806
1281
  }
807
- if (shouldCommitLoro) commitLoro();
808
1282
  return { refetch: false };
809
1283
  })
810
1284
  });
811
1285
  const baseSync = base.sync?.sync;
812
1286
  const sync = baseSync ? {
813
1287
  sync: (ctx) => {
814
- patchSubscribeChangesForRecordIds(
815
- ctx.collection
1288
+ const canRunBaseSync = typeof ctx.collection?.on === "function";
1289
+ const baseResult = canRunBaseSync ? baseSync(ctx) : void 0;
1290
+ const baseCleanup = typeof baseResult === "function" ? baseResult : typeof baseResult === "object" && baseResult && "cleanup" in baseResult && typeof baseResult.cleanup === "function" ? baseResult.cleanup : NOOP;
1291
+ const runtime = createSyncRuntime(
1292
+ ctx
816
1293
  );
817
- const baseRes = baseSync(ctx);
818
- const baseCleanup = toCleanup(baseRes);
819
- if (!db.isFeatureSupported(surrealdb.Features.LiveQueries)) {
820
- return baseRes;
821
- }
822
- const offLive = table.subscribe((evt) => {
823
- if (useLoro) {
824
- if (evt.type === "delete") {
825
- loroRemove(getKey(evt.row));
826
- } else {
827
- loroPut(evt.row);
1294
+ const start = async () => {
1295
+ if (!isOnDemandLike) {
1296
+ if (!crdtEnabled) {
1297
+ const rows = toRecordArray(
1298
+ await db.select(tableResource)
1299
+ );
1300
+ await hydratePlainRows(
1301
+ rows,
1302
+ ctx.write,
1303
+ ctx.begin,
1304
+ ctx.commit,
1305
+ "insert"
1306
+ );
1307
+ await runtime.startRealtime();
1308
+ ctx.markReady();
1309
+ return;
828
1310
  }
829
- }
830
- void queryClient.invalidateQueries({ queryKey, exact: false }).catch((e) => onError?.(e));
831
- });
832
- if (hasLoadSubset(baseRes)) {
833
- const resObj = baseRes;
834
- const rawLoadSubset = resObj.loadSubset;
835
- const loadSubset = typeof rawLoadSubset === "function" ? (opts) => {
836
- normalizeSubsetLiteralsInPlace(opts);
837
- return rawLoadSubset(opts);
838
- } : rawLoadSubset;
839
- return {
840
- ...resObj,
841
- loadSubset,
842
- cleanup: () => {
843
- offLive();
844
- baseCleanup();
1311
+ if (updatesTableName) {
1312
+ const updates = await queryRows(
1313
+ db,
1314
+ `SELECT * FROM type::table($table) ORDER BY ts ASC;`,
1315
+ { table: updatesTableName }
1316
+ );
1317
+ for (const update of updates) {
1318
+ const id = idFromDocRef(update.doc);
1319
+ const doc = getDoc(id);
1320
+ const bytes = await decodeUpdateBytes(
1321
+ update,
1322
+ "update"
1323
+ );
1324
+ if (!bytes.byteLength) continue;
1325
+ doc.import(bytes);
1326
+ }
1327
+ ctx.begin();
1328
+ try {
1329
+ for (const [id, doc] of docs.entries()) {
1330
+ ctx.write({
1331
+ type: "insert",
1332
+ value: normalizeRow(
1333
+ materializeCrdt(doc, id)
1334
+ )
1335
+ });
1336
+ }
1337
+ } finally {
1338
+ ctx.commit();
1339
+ }
1340
+ await runtime.startRealtime();
845
1341
  }
846
- };
847
- }
848
- return (() => {
849
- offLive();
850
- baseCleanup();
851
- });
1342
+ ctx.markReady();
1343
+ return;
1344
+ }
1345
+ ctx.markReady();
1346
+ if (syncMode === "progressive") {
1347
+ void runtime.loadSubset({}).catch((error) => onError?.(error));
1348
+ }
1349
+ };
1350
+ void start().catch((error) => onError?.(error));
1351
+ return {
1352
+ cleanup: () => {
1353
+ runtime.cleanup();
1354
+ baseCleanup();
1355
+ },
1356
+ loadSubset: async (subset) => {
1357
+ await runtime.loadSubset(subset);
1358
+ },
1359
+ unloadSubset: (subset) => {
1360
+ runtime.unloadSubset(subset);
1361
+ }
1362
+ };
852
1363
  }
853
1364
  } : void 0;
854
1365
  return {
@@ -856,8 +1367,16 @@ function surrealCollectionOptions({
856
1367
  sync: sync ?? base.sync
857
1368
  };
858
1369
  }
1370
+ function surrealCollectionOptions(config) {
1371
+ return modernSurrealCollectionOptions(config);
1372
+ }
859
1373
 
860
- exports.eqRecordId = eqRecordId;
1374
+ exports.WebCryptoAESGCM = WebCryptoAESGCM;
1375
+ exports.applyLoroJsonChange = applyLoroJsonChange;
1376
+ exports.applyLoroRichtextChange = applyLoroRichtextChange;
1377
+ exports.createLoroProfile = createLoroProfile;
1378
+ exports.materializeLoroJson = materializeLoroJson;
1379
+ exports.materializeLoroRichtext = materializeLoroRichtext;
861
1380
  exports.surrealCollectionOptions = surrealCollectionOptions;
862
1381
  exports.toRecordKeyString = toRecordKeyString;
863
1382
  //# sourceMappingURL=index.js.map