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