@korajs/store 0.4.0 → 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.
Files changed (69) hide show
  1. package/dist/adapters/better-sqlite3.d.cts +1 -1
  2. package/dist/adapters/better-sqlite3.d.ts +1 -1
  3. package/dist/adapters/indexeddb.cjs +410 -17
  4. package/dist/adapters/indexeddb.cjs.map +1 -1
  5. package/dist/adapters/indexeddb.d.cts +23 -5
  6. package/dist/adapters/indexeddb.d.ts +23 -5
  7. package/dist/adapters/indexeddb.js +163 -18
  8. package/dist/adapters/indexeddb.js.map +1 -1
  9. package/dist/adapters/sqlite-wasm-shared-host.cjs +68 -0
  10. package/dist/adapters/sqlite-wasm-shared-host.cjs.map +1 -0
  11. package/dist/adapters/sqlite-wasm-shared-host.d.cts +2 -0
  12. package/dist/adapters/sqlite-wasm-shared-host.d.ts +2 -0
  13. package/dist/adapters/sqlite-wasm-shared-host.js +66 -0
  14. package/dist/adapters/sqlite-wasm-shared-host.js.map +1 -0
  15. package/dist/adapters/sqlite-wasm-worker.cjs +34 -5
  16. package/dist/adapters/sqlite-wasm-worker.cjs.map +1 -1
  17. package/dist/adapters/sqlite-wasm-worker.js +34 -5
  18. package/dist/adapters/sqlite-wasm-worker.js.map +1 -1
  19. package/dist/adapters/sqlite-wasm.cjs +249 -2
  20. package/dist/adapters/sqlite-wasm.cjs.map +1 -1
  21. package/dist/adapters/sqlite-wasm.d.cts +15 -3
  22. package/dist/adapters/sqlite-wasm.d.ts +15 -3
  23. package/dist/adapters/sqlite-wasm.js +2 -2
  24. package/dist/audit-trace-store-AM4EWQNZ.js +15 -0
  25. package/dist/backup-JN3LB4YT.js +14 -0
  26. package/dist/backup-JN3LB4YT.js.map +1 -0
  27. package/dist/chunk-56LL4XP6.js +205 -0
  28. package/dist/chunk-56LL4XP6.js.map +1 -0
  29. package/dist/chunk-6AHYGQQT.js +425 -0
  30. package/dist/chunk-6AHYGQQT.js.map +1 -0
  31. package/dist/chunk-7BPETY5Y.js +570 -0
  32. package/dist/chunk-7BPETY5Y.js.map +1 -0
  33. package/dist/chunk-AGN3CIPB.js +188 -0
  34. package/dist/chunk-AGN3CIPB.js.map +1 -0
  35. package/dist/chunk-EZSTD2XC.js +380 -0
  36. package/dist/chunk-EZSTD2XC.js.map +1 -0
  37. package/dist/chunk-MYSJN3DF.js +117 -0
  38. package/dist/chunk-MYSJN3DF.js.map +1 -0
  39. package/dist/{chunk-DXKLAQ6P.js → chunk-XO4LMPAH.js} +1 -1
  40. package/dist/chunk-XO4LMPAH.js.map +1 -0
  41. package/dist/export-audit-5GV6VRDZ.js +15 -0
  42. package/dist/export-audit-5GV6VRDZ.js.map +1 -0
  43. package/dist/index.cjs +2662 -577
  44. package/dist/index.cjs.map +1 -1
  45. package/dist/index.d.cts +565 -187
  46. package/dist/index.d.ts +565 -187
  47. package/dist/index.js +1381 -739
  48. package/dist/index.js.map +1 -1
  49. package/dist/internal.cjs +789 -0
  50. package/dist/internal.cjs.map +1 -0
  51. package/dist/internal.d.cts +154 -0
  52. package/dist/internal.d.ts +154 -0
  53. package/dist/internal.js +48 -0
  54. package/dist/internal.js.map +1 -0
  55. package/dist/{sqlite-wasm-channel-46AOWNPM.js → sqlite-wasm-channel-HDDW6UBH.js} +2 -2
  56. package/dist/sqlite-wasm-channel-HDDW6UBH.js.map +1 -0
  57. package/dist/{sqlite-wasm-channel-Lakjuk2E.d.cts → sqlite-wasm-channel-I9y_jObg.d.cts} +1 -0
  58. package/dist/{sqlite-wasm-channel-Lakjuk2E.d.ts → sqlite-wasm-channel-I9y_jObg.d.ts} +1 -0
  59. package/dist/types-BMyHmwvn.d.ts +163 -0
  60. package/dist/types-CTr00g_d.d.cts +163 -0
  61. package/dist/types-DepnRDk_.d.cts +212 -0
  62. package/dist/types-DepnRDk_.d.ts +212 -0
  63. package/package.json +23 -2
  64. package/dist/chunk-DXKLAQ6P.js.map +0 -1
  65. package/dist/chunk-ZP5AXQ3Z.js +0 -179
  66. package/dist/chunk-ZP5AXQ3Z.js.map +0 -1
  67. package/dist/types-DF-KDSK1.d.cts +0 -106
  68. package/dist/types-DF-KDSK1.d.ts +0 -106
  69. /package/dist/{sqlite-wasm-channel-46AOWNPM.js.map → audit-trace-store-AM4EWQNZ.js.map} +0 -0
package/dist/index.cjs CHANGED
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
8
11
  var __export = (target, all) => {
9
12
  for (var name in all)
10
13
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -27,15 +30,921 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
30
  ));
28
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
32
 
33
+ // src/serialization/richtext-serializer.ts
34
+ function encodeRichtext(value) {
35
+ if (value === null || value === void 0) {
36
+ return null;
37
+ }
38
+ if (typeof value === "string") {
39
+ const doc = new Y.Doc();
40
+ doc.getText(TEXT_KEY).insert(0, value);
41
+ return Y.encodeStateAsUpdate(doc);
42
+ }
43
+ if (value instanceof Uint8Array) {
44
+ return value;
45
+ }
46
+ if (value instanceof ArrayBuffer) {
47
+ return new Uint8Array(value);
48
+ }
49
+ throw new Error("Richtext value must be a string, Uint8Array, ArrayBuffer, null, or undefined.");
50
+ }
51
+ function decodeRichtext(value) {
52
+ if (value === null || value === void 0) {
53
+ return null;
54
+ }
55
+ if (typeof Buffer !== "undefined" && Buffer.isBuffer(value)) {
56
+ return new Uint8Array(value);
57
+ }
58
+ if (value instanceof Uint8Array) {
59
+ return value;
60
+ }
61
+ if (value instanceof ArrayBuffer) {
62
+ return new Uint8Array(value);
63
+ }
64
+ throw new Error(
65
+ "Richtext storage value must be Uint8Array, ArrayBuffer, Buffer, null, or undefined."
66
+ );
67
+ }
68
+ function richtextToPlainText(value) {
69
+ const encoded = encodeRichtext(value);
70
+ if (!encoded) return "";
71
+ const doc = new Y.Doc();
72
+ Y.applyUpdate(doc, encoded);
73
+ return doc.getText(TEXT_KEY).toString();
74
+ }
75
+ function richtextStatesEqual(a, b) {
76
+ try {
77
+ return richtextToPlainText(a) === richtextToPlainText(b);
78
+ } catch {
79
+ return false;
80
+ }
81
+ }
82
+ var Y, TEXT_KEY;
83
+ var init_richtext_serializer = __esm({
84
+ "src/serialization/richtext-serializer.ts"() {
85
+ "use strict";
86
+ Y = __toESM(require("yjs"), 1);
87
+ TEXT_KEY = "content";
88
+ }
89
+ });
90
+
91
+ // src/serialization/serializer.ts
92
+ function serializeRecord(data, fields) {
93
+ const result = {};
94
+ for (const [key, value] of Object.entries(data)) {
95
+ const descriptor = fields[key];
96
+ if (!descriptor) {
97
+ result[key] = value;
98
+ continue;
99
+ }
100
+ result[key] = serializeValue(value, descriptor);
101
+ }
102
+ return result;
103
+ }
104
+ function deserializeRecord(row, fields) {
105
+ const result = {
106
+ id: row.id,
107
+ createdAt: row._created_at,
108
+ updatedAt: row._updated_at
109
+ };
110
+ for (const [key, descriptor] of Object.entries(fields)) {
111
+ const rawValue = row[key];
112
+ if (rawValue === void 0 || rawValue === null) {
113
+ result[key] = rawValue ?? null;
114
+ continue;
115
+ }
116
+ result[key] = deserializeValue(rawValue, descriptor);
117
+ }
118
+ return result;
119
+ }
120
+ function serializeOperation(op) {
121
+ const hasMetadata = op.transactionId !== void 0 || op.mutationName !== void 0;
122
+ let dataPayload = null;
123
+ if (op.data) {
124
+ dataPayload = { ...op.data };
125
+ if (op.atomicOps !== void 0 && Object.keys(op.atomicOps).length > 0) {
126
+ dataPayload[ATOMIC_OPS_KEY] = op.atomicOps;
127
+ }
128
+ if (op.transactionId !== void 0) {
129
+ dataPayload[TX_ID_KEY] = op.transactionId;
130
+ }
131
+ if (op.mutationName !== void 0) {
132
+ dataPayload[MUTATION_NAME_KEY] = op.mutationName;
133
+ }
134
+ } else if (hasMetadata) {
135
+ dataPayload = {};
136
+ if (op.transactionId !== void 0) {
137
+ dataPayload[TX_ID_KEY] = op.transactionId;
138
+ }
139
+ if (op.mutationName !== void 0) {
140
+ dataPayload[MUTATION_NAME_KEY] = op.mutationName;
141
+ }
142
+ }
143
+ return {
144
+ id: op.id,
145
+ node_id: op.nodeId,
146
+ type: op.type,
147
+ record_id: op.recordId,
148
+ data: dataPayload ? JSON.stringify(dataPayload) : null,
149
+ previous_data: op.previousData ? JSON.stringify(op.previousData) : null,
150
+ timestamp: import_core4.HybridLogicalClock.serialize(op.timestamp),
151
+ sequence_number: op.sequenceNumber,
152
+ causal_deps: JSON.stringify(op.causalDeps),
153
+ schema_version: op.schemaVersion
154
+ };
155
+ }
156
+ function deserializeOperation(row) {
157
+ let data = null;
158
+ let atomicOps;
159
+ let transactionId;
160
+ let mutationName;
161
+ if (row.data) {
162
+ const parsed = JSON.parse(row.data);
163
+ if (ATOMIC_OPS_KEY in parsed) {
164
+ atomicOps = parsed[ATOMIC_OPS_KEY];
165
+ }
166
+ if (TX_ID_KEY in parsed) {
167
+ transactionId = parsed[TX_ID_KEY];
168
+ }
169
+ if (MUTATION_NAME_KEY in parsed) {
170
+ mutationName = parsed[MUTATION_NAME_KEY];
171
+ }
172
+ const { [ATOMIC_OPS_KEY]: _a, [TX_ID_KEY]: _t, [MUTATION_NAME_KEY]: _m, ...rest } = parsed;
173
+ data = Object.keys(rest).length > 0 ? rest : null;
174
+ }
175
+ return {
176
+ id: row.id,
177
+ nodeId: row.node_id,
178
+ type: row.type,
179
+ collection: "",
180
+ // Collection name is derived from the table name by the caller
181
+ recordId: row.record_id,
182
+ data,
183
+ previousData: row.previous_data ? JSON.parse(row.previous_data) : null,
184
+ timestamp: import_core4.HybridLogicalClock.deserialize(row.timestamp),
185
+ sequenceNumber: row.sequence_number,
186
+ causalDeps: JSON.parse(row.causal_deps),
187
+ schemaVersion: row.schema_version,
188
+ ...atomicOps !== void 0 ? { atomicOps } : {},
189
+ ...transactionId !== void 0 ? { transactionId } : {},
190
+ ...mutationName !== void 0 ? { mutationName } : {}
191
+ };
192
+ }
193
+ function deserializeOperationWithCollection(row, collection) {
194
+ const op = deserializeOperation(row);
195
+ return { ...op, collection };
196
+ }
197
+ function serializeValue(value, descriptor) {
198
+ if (value === null || value === void 0) {
199
+ return null;
200
+ }
201
+ switch (descriptor.kind) {
202
+ case "boolean":
203
+ return value ? 1 : 0;
204
+ case "array":
205
+ return JSON.stringify(value);
206
+ case "richtext":
207
+ return encodeRichtext(value);
208
+ default:
209
+ return value;
210
+ }
211
+ }
212
+ function deserializeValue(value, descriptor) {
213
+ switch (descriptor.kind) {
214
+ case "boolean":
215
+ return value === 1 || value === true;
216
+ case "array":
217
+ if (typeof value === "string") {
218
+ return JSON.parse(value);
219
+ }
220
+ return value;
221
+ case "richtext":
222
+ return decodeRichtext(value);
223
+ default:
224
+ return value;
225
+ }
226
+ }
227
+ var import_core4, ATOMIC_OPS_KEY, TX_ID_KEY, MUTATION_NAME_KEY;
228
+ var init_serializer = __esm({
229
+ "src/serialization/serializer.ts"() {
230
+ "use strict";
231
+ import_core4 = require("@korajs/core");
232
+ init_richtext_serializer();
233
+ ATOMIC_OPS_KEY = "__kora_atomic_ops__";
234
+ TX_ID_KEY = "__kora_tx_id__";
235
+ MUTATION_NAME_KEY = "__kora_mutation__";
236
+ }
237
+ });
238
+
239
+ // src/backup/backup.ts
240
+ var backup_exports = {};
241
+ __export(backup_exports, {
242
+ exportBackup: () => exportBackup,
243
+ readBackupManifest: () => readBackupManifest,
244
+ restoreBackup: () => restoreBackup,
245
+ verifyBackupChecksum: () => verifyBackupChecksum
246
+ });
247
+ function encodeSection(name, content) {
248
+ const nameBytes = new TextEncoder().encode(name);
249
+ const header = new Uint8Array(8);
250
+ const dv = new DataView(header.buffer);
251
+ dv.setUint32(0, nameBytes.length, true);
252
+ dv.setUint32(4, content.length, true);
253
+ const result = new Uint8Array(header.length + nameBytes.length + content.length);
254
+ result.set(header, 0);
255
+ result.set(nameBytes, 8);
256
+ result.set(content, 8 + nameBytes.length);
257
+ return result;
258
+ }
259
+ function encodeJsonSection(name, data) {
260
+ const json = JSON.stringify(data);
261
+ return encodeSection(name, new TextEncoder().encode(json));
262
+ }
263
+ function readUint32(view, offset) {
264
+ return view.getUint32(offset, true);
265
+ }
266
+ function parseSections(data) {
267
+ const sections = [];
268
+ const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
269
+ let offset = 0;
270
+ while (offset + 8 <= data.byteLength) {
271
+ const nameLen = readUint32(dv, offset);
272
+ const contentLen = readUint32(dv, offset + 4);
273
+ offset += 8;
274
+ if (offset + nameLen + contentLen > data.byteLength) break;
275
+ const name = new TextDecoder().decode(data.slice(offset, offset + nameLen));
276
+ offset += nameLen;
277
+ const content = data.slice(offset, offset + contentLen);
278
+ offset += contentLen;
279
+ sections.push({ name, content });
280
+ }
281
+ return sections;
282
+ }
283
+ function findSection(sections, name) {
284
+ for (const s of sections) {
285
+ if (s.name === name) return s.content;
286
+ }
287
+ return null;
288
+ }
289
+ function parseJsonSection(sections, name) {
290
+ const content = findSection(sections, name);
291
+ if (!content) return null;
292
+ return JSON.parse(new TextDecoder().decode(content));
293
+ }
294
+ async function exportBackup(adapter, schema, nodeId, schemaVersion, options) {
295
+ const onProgress = options?.onProgress ?? (() => {
296
+ });
297
+ const includeRecords = options?.includeRecords ?? true;
298
+ const collectionFilter = options?.collections;
299
+ const collections = collectionFilter ? Object.keys(schema.collections).filter((c) => collectionFilter.includes(c)) : Object.keys(schema.collections);
300
+ onProgress({ phase: "reading", progress: 0, message: "Reading version vector" });
301
+ const versionVector = await readVersionVector(adapter);
302
+ onProgress({ phase: "reading", progress: 0.1, message: "Reading metadata" });
303
+ const meta = await readMeta(adapter);
304
+ onProgress({ phase: "reading", progress: 0.2, message: "Reading operations" });
305
+ const operations = await readAllOperations(adapter, schema);
306
+ onProgress({ phase: "reading", progress: 0.5, message: "Reading records" });
307
+ const sections = [];
308
+ let allContentForChecksum = new Uint8Array(0);
309
+ const addSection = (name, data) => {
310
+ sections.push(data);
311
+ const newLen = allContentForChecksum.length + data.length;
312
+ const combined = new Uint8Array(newLen);
313
+ combined.set(allContentForChecksum, 0);
314
+ combined.set(data, allContentForChecksum.length);
315
+ allContentForChecksum = combined;
316
+ };
317
+ const vvObj = {};
318
+ for (const [nid, seq] of versionVector) {
319
+ vvObj[nid] = seq;
320
+ }
321
+ addSection("version_vector", encodeJsonSection("version_vector", vvObj));
322
+ const metaObj = {};
323
+ for (const row of meta) {
324
+ metaObj[row.key] = row.value;
325
+ }
326
+ addSection("meta", encodeJsonSection("meta", metaObj));
327
+ const opLines = `${operations.map((op) => JSON.stringify(op)).join("\n")}
328
+ `;
329
+ addSection("operations", encodeSection("operations", new TextEncoder().encode(opLines)));
330
+ let totalRecords = 0;
331
+ if (includeRecords) {
332
+ for (const col of collections) {
333
+ const records = await readCollectionRecords(adapter, col);
334
+ if (records.length > 0) {
335
+ const lines = `${records.map((r) => JSON.stringify(r)).join("\n")}
336
+ `;
337
+ const sectionName = `records:${col}`;
338
+ addSection(sectionName, encodeSection(sectionName, new TextEncoder().encode(lines)));
339
+ totalRecords += records.length;
340
+ }
341
+ }
342
+ }
343
+ onProgress({ phase: "reading", progress: 0.9, message: "Computing checksum" });
344
+ const checksumHex = await computeSha256(allContentForChecksum);
345
+ onProgress({ phase: "writing", progress: 1, message: "Finalizing" });
346
+ const manifest = {
347
+ version: BACKUP_VERSION,
348
+ createdAt: Date.now(),
349
+ nodeId,
350
+ schemaVersion,
351
+ operationCount: operations.length,
352
+ collections,
353
+ includesRecords: includeRecords,
354
+ checksum: checksumHex
355
+ };
356
+ const header = new Uint8Array(0);
357
+ const manifestSection = encodeJsonSection("manifest", manifest);
358
+ const checksumSection = encodeSection("checksum", new TextEncoder().encode(checksumHex));
359
+ const totalLen = manifestSection.length + allContentForChecksum.length + checksumSection.length;
360
+ const result = new Uint8Array(totalLen);
361
+ let pos = 0;
362
+ result.set(manifestSection, pos);
363
+ pos += manifestSection.length;
364
+ result.set(allContentForChecksum, pos);
365
+ pos += allContentForChecksum.length;
366
+ result.set(checksumSection, pos);
367
+ return result;
368
+ }
369
+ function readBackupManifest(data) {
370
+ const sections = parseSections(data);
371
+ const manifest = parseJsonSection(sections, "manifest");
372
+ if (!manifest) {
373
+ throw new Error("Invalid backup: manifest section not found");
374
+ }
375
+ return manifest;
376
+ }
377
+ async function verifyBackupChecksum(data) {
378
+ const sections = parseSections(data);
379
+ const manifest = parseJsonSection(sections, "manifest");
380
+ if (!manifest) return false;
381
+ const storedChecksum = manifest.checksum;
382
+ if (!storedChecksum) return false;
383
+ const manifestSection = findSection(sections, "manifest");
384
+ if (!manifestSection) return false;
385
+ const checksumSection = findSection(sections, "checksum");
386
+ const checksumBytes = checksumSection ? new TextEncoder().encode(manifest.checksum) : new Uint8Array(0);
387
+ const contentSections = sections.filter((s) => s.name !== "manifest" && s.name !== "checksum");
388
+ let contentForHash = new Uint8Array(0);
389
+ for (const section of contentSections) {
390
+ const sectionBytes = encodeSection(section.name, section.content);
391
+ const newLen = contentForHash.length + sectionBytes.length;
392
+ const combined = new Uint8Array(newLen);
393
+ combined.set(contentForHash, 0);
394
+ combined.set(sectionBytes, contentForHash.length);
395
+ contentForHash = combined;
396
+ }
397
+ const computed = await computeSha256(contentForHash);
398
+ return computed === storedChecksum;
399
+ }
400
+ async function restoreBackup(adapter, schema, data, options) {
401
+ const startTime = Date.now();
402
+ const onProgress = options?.onProgress ?? (() => {
403
+ });
404
+ const collectionFilter = options?.collections;
405
+ onProgress({ phase: "verifying", progress: 0, message: "Parsing backup" });
406
+ const sections = parseSections(data);
407
+ const manifest = parseJsonSection(sections, "manifest");
408
+ if (!manifest) {
409
+ return {
410
+ operationsRestored: 0,
411
+ recordsRestored: 0,
412
+ success: false,
413
+ error: "Invalid backup: manifest section not found",
414
+ duration: Date.now() - startTime
415
+ };
416
+ }
417
+ if (manifest.version !== BACKUP_VERSION) {
418
+ return {
419
+ operationsRestored: 0,
420
+ recordsRestored: 0,
421
+ success: false,
422
+ error: `Unsupported backup version: ${manifest.version}`,
423
+ duration: Date.now() - startTime
424
+ };
425
+ }
426
+ onProgress({ phase: "verifying", progress: 0.2, message: "Verifying checksum" });
427
+ const valid = await verifyBackupChecksum(data);
428
+ if (!valid) {
429
+ return {
430
+ operationsRestored: 0,
431
+ recordsRestored: 0,
432
+ success: false,
433
+ error: "Backup checksum mismatch: data may be corrupted",
434
+ duration: Date.now() - startTime
435
+ };
436
+ }
437
+ onProgress({ phase: "restoring", progress: 0.3, message: "Loading operations" });
438
+ const opsContent = findSection(sections, "operations");
439
+ let operations = [];
440
+ if (opsContent) {
441
+ const text = new TextDecoder().decode(opsContent);
442
+ const lines = text.trim().split("\n").filter((l) => l.length > 0);
443
+ operations = lines.map((line) => JSON.parse(line));
444
+ }
445
+ if (collectionFilter) {
446
+ operations = operations.filter((op) => collectionFilter.includes(op.collection));
447
+ }
448
+ const vvData = parseJsonSection(sections, "version_vector");
449
+ const metaData = parseJsonSection(sections, "meta");
450
+ onProgress({ phase: "restoring", progress: 0.4, message: "Applying operations" });
451
+ if (options?.merge) {
452
+ await adapter.transaction(async (tx) => {
453
+ if (vvData) {
454
+ for (const [nid, seq] of Object.entries(vvData)) {
455
+ await tx.execute(
456
+ "INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
457
+ [nid, seq]
458
+ );
459
+ }
460
+ }
461
+ if (metaData) {
462
+ for (const [key, value] of Object.entries(metaData)) {
463
+ await tx.execute("INSERT OR REPLACE INTO _kora_meta (key, value) VALUES (?, ?)", [
464
+ key,
465
+ value
466
+ ]);
467
+ }
468
+ }
469
+ for (const op of operations) {
470
+ const collection = op.collection;
471
+ const opRow = {
472
+ id: op.id,
473
+ node_id: op.nodeId,
474
+ type: op.type,
475
+ record_id: op.recordId,
476
+ data: op.data !== null ? JSON.stringify(op.data) : null,
477
+ previous_data: op.previousData !== null ? JSON.stringify(op.previousData) : null,
478
+ timestamp: JSON.stringify(op.timestamp),
479
+ sequence_number: op.sequenceNumber,
480
+ causal_deps: JSON.stringify(op.causalDeps),
481
+ schema_version: op.schemaVersion
482
+ };
483
+ await tx.execute(
484
+ `INSERT OR IGNORE INTO _kora_ops_${collection} (id, node_id, type, record_id, data, previous_data, timestamp, sequence_number, causal_deps, schema_version) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
485
+ [
486
+ opRow.id,
487
+ opRow.node_id,
488
+ opRow.type,
489
+ opRow.record_id,
490
+ opRow.data,
491
+ opRow.previous_data,
492
+ opRow.timestamp,
493
+ opRow.sequence_number,
494
+ opRow.causal_deps,
495
+ opRow.schema_version
496
+ ]
497
+ );
498
+ }
499
+ });
500
+ } else {
501
+ await adapter.transaction(async (tx) => {
502
+ for (const collection of Object.keys(schema.collections)) {
503
+ await tx.execute(`DELETE FROM _kora_ops_${collection}`);
504
+ await tx.execute(`DELETE FROM ${collection}`);
505
+ }
506
+ await tx.execute("DELETE FROM _kora_version_vector");
507
+ await tx.execute("DELETE FROM _kora_meta");
508
+ if (vvData) {
509
+ for (const [nid, seq] of Object.entries(vvData)) {
510
+ await tx.execute(
511
+ "INSERT INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
512
+ [nid, seq]
513
+ );
514
+ }
515
+ }
516
+ if (metaData) {
517
+ for (const [key, value] of Object.entries(metaData)) {
518
+ await tx.execute("INSERT INTO _kora_meta (key, value) VALUES (?, ?)", [key, value]);
519
+ }
520
+ }
521
+ for (const op of operations) {
522
+ const collection = op.collection;
523
+ await tx.execute(
524
+ `INSERT INTO _kora_ops_${collection} (id, node_id, type, record_id, data, previous_data, timestamp, sequence_number, causal_deps, schema_version) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
525
+ [
526
+ op.id,
527
+ op.nodeId,
528
+ op.type,
529
+ op.recordId,
530
+ op.data !== null ? JSON.stringify(op.data) : null,
531
+ op.previousData !== null ? JSON.stringify(op.previousData) : null,
532
+ JSON.stringify(op.timestamp),
533
+ op.sequenceNumber,
534
+ JSON.stringify(op.causalDeps),
535
+ op.schemaVersion
536
+ ]
537
+ );
538
+ }
539
+ });
540
+ }
541
+ onProgress({ phase: "restoring", progress: 0.8, message: "Restoring records" });
542
+ let recordsRestored = 0;
543
+ if (manifest.includesRecords) {
544
+ for (const col of manifest.collections) {
545
+ if (collectionFilter && !collectionFilter.includes(col)) continue;
546
+ const sectionContent = findSection(sections, `records:${col}`);
547
+ if (!sectionContent) continue;
548
+ const text = new TextDecoder().decode(sectionContent);
549
+ const lines = text.trim().split("\n").filter((l) => l.length > 0);
550
+ const records = lines.map((line) => JSON.parse(line));
551
+ if (records.length === 0) continue;
552
+ await adapter.transaction(async (tx) => {
553
+ for (const record of records) {
554
+ const keys = Object.keys(record);
555
+ const placeholders = keys.map(() => "?").join(", ");
556
+ const values = keys.map((k) => {
557
+ const v = record[k];
558
+ if (typeof v === "boolean") return v ? 1 : 0;
559
+ if (Array.isArray(v) || typeof v === "object" && v !== null) return JSON.stringify(v);
560
+ return v;
561
+ });
562
+ await tx.execute(
563
+ `INSERT OR REPLACE INTO ${col} (${keys.join(", ")}) VALUES (${placeholders})`,
564
+ values
565
+ );
566
+ }
567
+ });
568
+ recordsRestored += records.length;
569
+ }
570
+ }
571
+ onProgress({ phase: "restoring", progress: 1, message: "Done" });
572
+ return {
573
+ operationsRestored: operations.length,
574
+ recordsRestored,
575
+ success: true,
576
+ duration: Date.now() - startTime
577
+ };
578
+ }
579
+ async function readVersionVector(adapter) {
580
+ const rows = await adapter.query(
581
+ "SELECT node_id, sequence_number FROM _kora_version_vector"
582
+ );
583
+ const vv = /* @__PURE__ */ new Map();
584
+ for (const row of rows) {
585
+ vv.set(row.node_id, row.sequence_number);
586
+ }
587
+ return vv;
588
+ }
589
+ async function readMeta(adapter) {
590
+ return adapter.query("SELECT key, value FROM _kora_meta");
591
+ }
592
+ async function readAllOperations(adapter, schema) {
593
+ const allOps = [];
594
+ for (const collectionName of Object.keys(schema.collections)) {
595
+ const rows = await adapter.query(
596
+ `SELECT * FROM _kora_ops_${collectionName} ORDER BY sequence_number ASC`
597
+ );
598
+ for (const row of rows) {
599
+ allOps.push(deserializeOperationWithCollection(row, collectionName));
600
+ }
601
+ }
602
+ return allOps;
603
+ }
604
+ async function readCollectionRecords(adapter, collection) {
605
+ return adapter.query(`SELECT * FROM ${collection} WHERE _deleted = 0`);
606
+ }
607
+ async function computeSha256(data) {
608
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
609
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
610
+ return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
611
+ }
612
+ var import_core5, BACKUP_VERSION;
613
+ var init_backup = __esm({
614
+ "src/backup/backup.ts"() {
615
+ "use strict";
616
+ import_core5 = require("@korajs/core");
617
+ init_serializer();
618
+ BACKUP_VERSION = 1;
619
+ }
620
+ });
621
+
622
+ // src/audit/audit-json.ts
623
+ function auditJsonReplacer(_key, value) {
624
+ if (value instanceof Uint8Array) {
625
+ let binary = "";
626
+ for (let i = 0; i < value.length; i++) {
627
+ binary += String.fromCharCode(value[i]);
628
+ }
629
+ return { [WIRE_BYTES_KEY]: btoa(binary) };
630
+ }
631
+ return value;
632
+ }
633
+ function auditJsonReviver(_key, value) {
634
+ if (value !== null && typeof value === "object" && WIRE_BYTES_KEY in value && typeof value[WIRE_BYTES_KEY] === "string") {
635
+ const encoded = value[WIRE_BYTES_KEY];
636
+ if (typeof encoded !== "string") {
637
+ return value;
638
+ }
639
+ const binary = atob(encoded);
640
+ const bytes = new Uint8Array(binary.length);
641
+ for (let i = 0; i < binary.length; i++) {
642
+ bytes[i] = binary.charCodeAt(i);
643
+ }
644
+ return bytes;
645
+ }
646
+ return value;
647
+ }
648
+ function serializeAuditJson(value) {
649
+ return JSON.stringify(value, auditJsonReplacer);
650
+ }
651
+ function deserializeAuditJson(json) {
652
+ return JSON.parse(json, auditJsonReviver);
653
+ }
654
+ var WIRE_BYTES_KEY;
655
+ var init_audit_json = __esm({
656
+ "src/audit/audit-json.ts"() {
657
+ "use strict";
658
+ WIRE_BYTES_KEY = "__kora_bytes__";
659
+ }
660
+ });
661
+
662
+ // src/audit/audit-trace-store.ts
663
+ var audit_trace_store_exports = {};
664
+ __export(audit_trace_store_exports, {
665
+ appendAuditTrace: () => appendAuditTrace,
666
+ auditTraceIndexFields: () => auditTraceIndexFields,
667
+ countAuditTraces: () => countAuditTraces,
668
+ persistedAuditTraceFromEvent: () => persistedAuditTraceFromEvent,
669
+ readAuditTraces: () => readAuditTraces
670
+ });
671
+ function persistedAuditTraceFromEvent(event) {
672
+ return {
673
+ id: (0, import_core16.generateUUIDv7)(),
674
+ recordedAt: Date.now(),
675
+ eventType: event.type,
676
+ constraint: event.type === "constraint:violated" ? event.constraint : void 0,
677
+ trace: event.trace
678
+ };
679
+ }
680
+ async function appendAuditTrace(adapter, trace) {
681
+ const collection = trace.trace.operationA.collection;
682
+ const recordId = trace.trace.operationA.recordId;
683
+ await adapter.execute(
684
+ `INSERT INTO _kora_audit_traces (
685
+ id, recorded_at, event_type, collection, record_id, field, strategy, tier, constraint_name, trace_json
686
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
687
+ [
688
+ trace.id,
689
+ trace.recordedAt,
690
+ trace.eventType,
691
+ collection,
692
+ recordId,
693
+ trace.trace.field,
694
+ trace.trace.strategy,
695
+ trace.trace.tier,
696
+ trace.constraint ?? null,
697
+ serializeAuditJson(trace)
698
+ ]
699
+ );
700
+ }
701
+ async function readAuditTraces(adapter, query) {
702
+ const conditions = [];
703
+ const params = [];
704
+ if (query?.collections && query.collections.length > 0) {
705
+ const placeholders = query.collections.map(() => "?").join(", ");
706
+ conditions.push(`collection IN (${placeholders})`);
707
+ params.push(...query.collections);
708
+ }
709
+ if (query?.since !== void 0) {
710
+ conditions.push("recorded_at >= ?");
711
+ params.push(query.since);
712
+ }
713
+ if (query?.until !== void 0) {
714
+ conditions.push("recorded_at <= ?");
715
+ params.push(query.until);
716
+ }
717
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
718
+ const rows = await adapter.query(
719
+ `SELECT * FROM _kora_audit_traces ${whereClause} ORDER BY recorded_at ASC, id ASC`,
720
+ params
721
+ );
722
+ return rows.map(rowToPersistedTrace);
723
+ }
724
+ function rowToPersistedTrace(row) {
725
+ return deserializeAuditJson(row.trace_json);
726
+ }
727
+ async function countAuditTraces(adapter) {
728
+ const rows = await adapter.query(
729
+ "SELECT COUNT(*) as count FROM _kora_audit_traces"
730
+ );
731
+ return rows[0]?.count ?? 0;
732
+ }
733
+ function auditTraceIndexFields(trace) {
734
+ return {
735
+ collection: trace.operationA.collection,
736
+ recordId: trace.operationA.recordId
737
+ };
738
+ }
739
+ var import_core16;
740
+ var init_audit_trace_store = __esm({
741
+ "src/audit/audit-trace-store.ts"() {
742
+ "use strict";
743
+ import_core16 = require("@korajs/core");
744
+ init_audit_json();
745
+ }
746
+ });
747
+
748
+ // src/audit/export-audit.ts
749
+ var export_audit_exports = {};
750
+ __export(export_audit_exports, {
751
+ decodeAuditExport: () => decodeAuditExport,
752
+ exportAudit: () => exportAudit,
753
+ readAuditExportManifest: () => readAuditExportManifest,
754
+ verifyAuditExportChecksum: () => verifyAuditExportChecksum
755
+ });
756
+ function encodeSection2(name, content) {
757
+ const nameBytes = new TextEncoder().encode(name);
758
+ const header = new Uint8Array(8);
759
+ const view = new DataView(header.buffer);
760
+ view.setUint32(0, nameBytes.length, true);
761
+ view.setUint32(4, content.length, true);
762
+ const result = new Uint8Array(header.length + nameBytes.length + content.length);
763
+ result.set(header, 0);
764
+ result.set(nameBytes, 8);
765
+ result.set(content, 8 + nameBytes.length);
766
+ return result;
767
+ }
768
+ function encodeJsonSection2(name, data) {
769
+ return encodeSection2(name, new TextEncoder().encode(serializeAuditJson(data)));
770
+ }
771
+ function encodeNdjsonSection(name, records) {
772
+ const lines = `${records.map((record) => serializeAuditJson(record)).join("\n")}
773
+ `;
774
+ return encodeSection2(name, new TextEncoder().encode(lines));
775
+ }
776
+ function readUint322(view, offset) {
777
+ return view.getUint32(offset, true);
778
+ }
779
+ function parseSections2(data) {
780
+ const sections = [];
781
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
782
+ let offset = 0;
783
+ while (offset + 8 <= data.byteLength) {
784
+ const nameLen = readUint322(view, offset);
785
+ const contentLen = readUint322(view, offset + 4);
786
+ offset += 8;
787
+ if (offset + nameLen + contentLen > data.byteLength) {
788
+ break;
789
+ }
790
+ const name = new TextDecoder().decode(data.slice(offset, offset + nameLen));
791
+ offset += nameLen;
792
+ const content = data.slice(offset, offset + contentLen);
793
+ offset += contentLen;
794
+ sections.push({ name, content });
795
+ }
796
+ return sections;
797
+ }
798
+ function findSection2(sections, name) {
799
+ for (const section of sections) {
800
+ if (section.name === name) {
801
+ return section.content;
802
+ }
803
+ }
804
+ return null;
805
+ }
806
+ async function computeSha2562(data) {
807
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
808
+ return Array.from(new Uint8Array(hashBuffer)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
809
+ }
810
+ async function readAllOperations2(adapter, schema, collectionFilter) {
811
+ const collections = collectionFilter ? Object.keys(schema.collections).filter((name) => collectionFilter.includes(name)) : Object.keys(schema.collections);
812
+ const allOps = [];
813
+ for (const collectionName of collections) {
814
+ const rows = await adapter.query(
815
+ `SELECT * FROM _kora_ops_${collectionName} ORDER BY sequence_number ASC`
816
+ );
817
+ for (const row of rows) {
818
+ allOps.push(deserializeOperationWithCollection(row, collectionName));
819
+ }
820
+ }
821
+ return allOps;
822
+ }
823
+ function filterOperationsByCollections(operations, collectionFilter) {
824
+ if (!collectionFilter || collectionFilter.length === 0) {
825
+ return operations;
826
+ }
827
+ const allowed = new Set(collectionFilter);
828
+ return operations.filter((op) => allowed.has(op.collection));
829
+ }
830
+ async function exportAudit(adapter, schema, nodeId, schemaVersion, options) {
831
+ const onProgress = options?.onProgress ?? (() => {
832
+ });
833
+ const traceQuery = {
834
+ collections: options?.collections,
835
+ since: options?.since,
836
+ until: options?.until
837
+ };
838
+ onProgress({ phase: "reading", progress: 0, message: "Reading operations" });
839
+ const operations = filterOperationsByCollections(
840
+ await readAllOperations2(adapter, schema, options?.collections),
841
+ options?.collections
842
+ );
843
+ onProgress({ phase: "reading", progress: 0.4, message: "Reading merge traces" });
844
+ const mergeTraces = await readAuditTraces(adapter, traceQuery);
845
+ onProgress({ phase: "writing", progress: 0.7, message: "Encoding audit export" });
846
+ const sections = [];
847
+ let contentForChecksum = new Uint8Array(0);
848
+ const addSection = (section) => {
849
+ sections.push(section);
850
+ const combined = new Uint8Array(contentForChecksum.length + section.length);
851
+ combined.set(contentForChecksum, 0);
852
+ combined.set(section, contentForChecksum.length);
853
+ contentForChecksum = combined;
854
+ };
855
+ addSection(encodeNdjsonSection("operations", operations));
856
+ addSection(encodeNdjsonSection("merge_traces", mergeTraces));
857
+ onProgress({ phase: "verifying", progress: 0.9, message: "Computing checksum" });
858
+ const checksum = await computeSha2562(contentForChecksum);
859
+ const manifest = {
860
+ version: 1,
861
+ exportedAt: Date.now(),
862
+ nodeId,
863
+ schemaVersion,
864
+ operationCount: operations.length,
865
+ mergeTraceCount: mergeTraces.length,
866
+ checksum
867
+ };
868
+ const manifestSection = encodeJsonSection2("manifest", manifest);
869
+ const checksumSection = encodeSection2("checksum", new TextEncoder().encode(checksum));
870
+ onProgress({ phase: "writing", progress: 1, message: "Finalizing" });
871
+ const parts = [manifestSection, ...sections, checksumSection];
872
+ const finalOutput = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0));
873
+ let offset = 0;
874
+ for (const part of parts) {
875
+ finalOutput.set(part, offset);
876
+ offset += part.length;
877
+ }
878
+ return finalOutput;
879
+ }
880
+ function decodeAuditExport(data) {
881
+ const sections = parseSections2(data);
882
+ const manifest = JSON.parse(
883
+ new TextDecoder().decode(findSection2(sections, "manifest") ?? new Uint8Array())
884
+ );
885
+ const operationsContent = findSection2(sections, "operations");
886
+ const mergeTracesContent = findSection2(sections, "merge_traces");
887
+ const operations = parseNdjsonSection(operationsContent);
888
+ const mergeTraces = parseNdjsonSection(mergeTracesContent);
889
+ return { manifest, operations, mergeTraces };
890
+ }
891
+ function readAuditExportManifest(data) {
892
+ const sections = parseSections2(data);
893
+ const manifestContent = findSection2(sections, "manifest");
894
+ if (!manifestContent) {
895
+ throw new Error("Invalid audit export: missing manifest section");
896
+ }
897
+ return JSON.parse(new TextDecoder().decode(manifestContent));
898
+ }
899
+ async function verifyAuditExportChecksum(data) {
900
+ const sections = parseSections2(data);
901
+ const manifest = readAuditExportManifest(data);
902
+ const contentSections = sections.filter(
903
+ (section) => section.name !== "manifest" && section.name !== "checksum"
904
+ );
905
+ let content = new Uint8Array(0);
906
+ for (const section of contentSections) {
907
+ const encoded = encodeSection2(section.name, section.content);
908
+ const combined = new Uint8Array(content.length + encoded.length);
909
+ combined.set(content, 0);
910
+ combined.set(encoded, content.length);
911
+ content = combined;
912
+ }
913
+ const checksum = await computeSha2562(content);
914
+ return checksum === manifest.checksum;
915
+ }
916
+ function parseNdjsonSection(content) {
917
+ if (!content || content.length === 0) {
918
+ return [];
919
+ }
920
+ const text = new TextDecoder().decode(content).trim();
921
+ if (text.length === 0) {
922
+ return [];
923
+ }
924
+ return text.split("\n").map((line) => deserializeAuditJson(line));
925
+ }
926
+ var init_export_audit = __esm({
927
+ "src/audit/export-audit.ts"() {
928
+ "use strict";
929
+ init_serializer();
930
+ init_audit_json();
931
+ init_audit_trace_store();
932
+ }
933
+ });
934
+
30
935
  // src/index.ts
31
936
  var index_exports = {};
32
937
  __export(index_exports, {
33
938
  AdapterError: () => AdapterError,
939
+ COMPACTION_BASELINE_META_KEY: () => COMPACTION_BASELINE_META_KEY,
34
940
  Collection: () => Collection,
35
941
  InvalidStateTransitionError: () => InvalidStateTransitionError,
942
+ LAST_ACKED_SERVER_VECTOR_META_KEY: () => LAST_ACKED_SERVER_VECTOR_META_KEY,
36
943
  PersistenceError: () => PersistenceError,
37
944
  QueryBuilder: () => QueryBuilder,
38
945
  QueryError: () => QueryError,
946
+ QueryStore: () => QueryStore,
947
+ QueryStoreCache: () => QueryStoreCache,
39
948
  RecordNotFoundError: () => RecordNotFoundError,
40
949
  SequenceManager: () => SequenceManager,
41
950
  Store: () => Store,
@@ -45,13 +954,32 @@ __export(index_exports, {
45
954
  TransactionContext: () => TransactionContext,
46
955
  WorkerInitError: () => WorkerInitError,
47
956
  WorkerTimeoutError: () => WorkerTimeoutError,
957
+ asRichTextSyncEngine: () => asRichTextSyncEngine,
958
+ assertQueryReady: () => assertQueryReady,
959
+ collectOperationsAheadOfServer: () => collectOperationsAheadOfServer,
960
+ compactOperationLog: () => compactOperationLog,
961
+ computeAckCompactionWatermark: () => computeAckCompactionWatermark,
962
+ createRichTextController: () => createRichTextController,
963
+ decodeAuditExport: () => decodeAuditExport,
48
964
  decodeRichtext: () => decodeRichtext,
965
+ deserializeVersionVectorFromMeta: () => deserializeVersionVectorFromMeta,
49
966
  encodeRichtext: () => encodeRichtext,
967
+ exportBackup: () => exportBackup,
968
+ getSharedQueryStoreCache: () => getSharedQueryStoreCache,
969
+ mergeVersionVectors: () => mergeVersionVectors,
970
+ persistedAuditTraceFromEvent: () => persistedAuditTraceFromEvent,
50
971
  pluralize: () => pluralize,
972
+ readAuditExportManifest: () => readAuditExportManifest,
973
+ readBackupManifest: () => readBackupManifest,
974
+ restoreBackup: () => restoreBackup,
975
+ richtextStatesEqual: () => richtextStatesEqual,
51
976
  richtextToPlainText: () => richtextToPlainText,
977
+ serializeVersionVectorToMeta: () => serializeVersionVectorToMeta,
52
978
  singularize: () => singularize,
53
979
  validateStateTransition: () => validateStateTransition,
54
- validateUpdateStateMachine: () => validateUpdateStateMachine
980
+ validateUpdateStateMachine: () => validateUpdateStateMachine,
981
+ verifyAuditExportChecksum: () => verifyAuditExportChecksum,
982
+ verifyBackupChecksum: () => verifyBackupChecksum
55
983
  });
56
984
  module.exports = __toCommonJS(index_exports);
57
985
 
@@ -107,11 +1035,175 @@ var PersistenceError = class extends import_core.KoraError {
107
1035
  }
108
1036
  };
109
1037
 
1038
+ // src/compaction/types.ts
1039
+ var COMPACTION_BASELINE_META_KEY = "compaction_baseline_at";
1040
+
1041
+ // src/compaction/compact-operation-log.ts
1042
+ var import_core2 = require("@korajs/core");
1043
+ function computeAckCompactionWatermark(serverVector) {
1044
+ const watermark = (0, import_core2.createVersionVector)();
1045
+ for (const [nodeId, seq] of serverVector) {
1046
+ if (seq > 0) {
1047
+ watermark.set(nodeId, seq);
1048
+ }
1049
+ }
1050
+ return watermark;
1051
+ }
1052
+ async function compactOperationLog(adapter, schema, strategy, serverVector) {
1053
+ if (strategy.mode === "never") {
1054
+ return { deletedCount: 0, watermark: (0, import_core2.createVersionVector)() };
1055
+ }
1056
+ const watermark = computeAckCompactionWatermark(serverVector);
1057
+ if (watermark.size === 0) {
1058
+ return { deletedCount: 0, watermark };
1059
+ }
1060
+ const ageCutoffWall = strategy.mode === "after-days" ? Date.now() - strategy.days * 24 * 60 * 60 * 1e3 : null;
1061
+ const ageCutoffSerialized = ageCutoffWall !== null ? import_core2.HybridLogicalClock.serialize({
1062
+ wallTime: ageCutoffWall,
1063
+ logical: 0,
1064
+ nodeId: ""
1065
+ }).slice(0, 15) : null;
1066
+ let deletedCount = 0;
1067
+ const collectionNames = Object.keys(schema.collections);
1068
+ await adapter.transaction(async (tx) => {
1069
+ for (const collectionName of collectionNames) {
1070
+ const table = `_kora_ops_${collectionName}`;
1071
+ for (const [nodeId, maxSeq] of watermark) {
1072
+ if (maxSeq <= 0) continue;
1073
+ const countSql = buildCountSql(table, ageCutoffSerialized !== null);
1074
+ const countParams = ageCutoffSerialized !== null ? [nodeId, maxSeq, ageCutoffSerialized] : [nodeId, maxSeq];
1075
+ const countRows = await tx.query(countSql, countParams);
1076
+ deletedCount += countRows[0]?.count ?? 0;
1077
+ const deleteSql = buildDeleteSql(table, ageCutoffSerialized !== null);
1078
+ await tx.execute(deleteSql, countParams);
1079
+ }
1080
+ }
1081
+ await tx.execute("INSERT OR REPLACE INTO _kora_meta (key, value) VALUES (?, ?)", [
1082
+ COMPACTION_BASELINE_META_KEY,
1083
+ String(Date.now())
1084
+ ]);
1085
+ });
1086
+ return { deletedCount, watermark };
1087
+ }
1088
+ function buildCountSql(table, withAge) {
1089
+ if (withAge) {
1090
+ return `SELECT COUNT(*) as count FROM ${table} WHERE node_id = ? AND sequence_number <= ? AND SUBSTR(timestamp, 1, 15) < ?`;
1091
+ }
1092
+ return `SELECT COUNT(*) as count FROM ${table} WHERE node_id = ? AND sequence_number <= ?`;
1093
+ }
1094
+ function buildDeleteSql(table, withAge) {
1095
+ if (withAge) {
1096
+ return `DELETE FROM ${table} WHERE node_id = ? AND sequence_number <= ? AND SUBSTR(timestamp, 1, 15) < ?`;
1097
+ }
1098
+ return `DELETE FROM ${table} WHERE node_id = ? AND sequence_number <= ?`;
1099
+ }
1100
+
1101
+ // src/sync/sync-state.ts
1102
+ var import_core3 = require("@korajs/core");
1103
+ var LAST_ACKED_SERVER_VECTOR_META_KEY = "last_acked_server_vector";
1104
+ var DELTA_CURSOR_META_KEY = "delta_cursor";
1105
+ function serializeVersionVectorToMeta(vector) {
1106
+ const record = {};
1107
+ for (const [nodeId, seq] of vector) {
1108
+ record[nodeId] = seq;
1109
+ }
1110
+ return JSON.stringify(record);
1111
+ }
1112
+ function deserializeVersionVectorFromMeta(value) {
1113
+ const parsed = JSON.parse(value);
1114
+ const vector = (0, import_core3.createVersionVector)();
1115
+ for (const [nodeId, seq] of Object.entries(parsed)) {
1116
+ if (typeof seq === "number" && seq >= 0) {
1117
+ vector.set(nodeId, seq);
1118
+ }
1119
+ }
1120
+ return vector;
1121
+ }
1122
+ function mergeVersionVectors(a, b) {
1123
+ const merged = new Map(a);
1124
+ for (const [nodeId, seq] of b) {
1125
+ merged.set(nodeId, Math.max(merged.get(nodeId) ?? 0, seq));
1126
+ }
1127
+ return merged;
1128
+ }
1129
+ async function collectOperationsAheadOfServer(localVector, serverVector, fetchRange) {
1130
+ const missing = [];
1131
+ for (const [nodeId, localSeq] of localVector) {
1132
+ const serverSeq = serverVector.get(nodeId) ?? 0;
1133
+ if (localSeq > serverSeq) {
1134
+ const ops = await fetchRange(nodeId, serverSeq + 1, localSeq);
1135
+ missing.push(...ops);
1136
+ }
1137
+ }
1138
+ missing.sort((a, b) => a.sequenceNumber - b.sequenceNumber);
1139
+ return missing;
1140
+ }
1141
+ async function loadLastAckedServerVector(adapter) {
1142
+ const rows = await adapter.query("SELECT value FROM _kora_meta WHERE key = ?", [
1143
+ LAST_ACKED_SERVER_VECTOR_META_KEY
1144
+ ]);
1145
+ const row = rows[0];
1146
+ if (!row?.value) {
1147
+ return (0, import_core3.createVersionVector)();
1148
+ }
1149
+ try {
1150
+ return deserializeVersionVectorFromMeta(row.value);
1151
+ } catch {
1152
+ return (0, import_core3.createVersionVector)();
1153
+ }
1154
+ }
1155
+ async function saveLastAckedServerVector(adapter, vector) {
1156
+ await adapter.execute("INSERT OR REPLACE INTO _kora_meta (key, value) VALUES (?, ?)", [
1157
+ LAST_ACKED_SERVER_VECTOR_META_KEY,
1158
+ serializeVersionVectorToMeta(vector)
1159
+ ]);
1160
+ }
1161
+ async function loadDeltaCursor(adapter) {
1162
+ const rows = await adapter.query("SELECT value FROM _kora_meta WHERE key = ?", [
1163
+ DELTA_CURSOR_META_KEY
1164
+ ]);
1165
+ return rows[0]?.value ?? null;
1166
+ }
1167
+ async function saveDeltaCursor(adapter, cursor) {
1168
+ if (cursor === null) {
1169
+ await adapter.execute("DELETE FROM _kora_meta WHERE key = ?", [DELTA_CURSOR_META_KEY]);
1170
+ return;
1171
+ }
1172
+ await adapter.execute("INSERT OR REPLACE INTO _kora_meta (key, value) VALUES (?, ?)", [
1173
+ DELTA_CURSOR_META_KEY,
1174
+ cursor
1175
+ ]);
1176
+ }
1177
+
110
1178
  // src/store/store.ts
111
- var import_core8 = require("@korajs/core");
1179
+ var import_core17 = require("@korajs/core");
1180
+ init_backup();
112
1181
 
113
- // src/collection/collection.ts
114
- var import_core4 = require("@korajs/core");
1182
+ // src/mutations/execute-delete.ts
1183
+ var import_core7 = require("@korajs/core");
1184
+
1185
+ // src/lww/row-version.ts
1186
+ var import_core6 = require("@korajs/core");
1187
+ var EMPTY_ROW_VERSION = "";
1188
+ function serializeRowVersion(timestamp) {
1189
+ return import_core6.HybridLogicalClock.serialize(timestamp);
1190
+ }
1191
+ function rowVersionFromRecord(row) {
1192
+ const version = row._version;
1193
+ if (typeof version === "string" && version.length > 0) {
1194
+ return import_core6.HybridLogicalClock.deserialize(version);
1195
+ }
1196
+ return { wallTime: row._updated_at, logical: 0, nodeId: "" };
1197
+ }
1198
+ function isIncomingNewerThanRow(incoming, row) {
1199
+ return import_core6.HybridLogicalClock.compare(incoming, rowVersionFromRecord(row)) > 0;
1200
+ }
1201
+ function lwwVersionWhereClause(remoteVersion) {
1202
+ return {
1203
+ sql: "(_version = ? OR _version < ?)",
1204
+ params: [EMPTY_ROW_VERSION, remoteVersion]
1205
+ };
1206
+ }
115
1207
 
116
1208
  // src/query/sql-builder.ts
117
1209
  function buildSelectQuery(descriptor, fields) {
@@ -164,12 +1256,32 @@ function buildUpdateQuery(collection, id, changes) {
164
1256
  const sql = `UPDATE ${collection} SET ${setClauses.join(", ")} WHERE id = ?`;
165
1257
  return { sql, params };
166
1258
  }
167
- function buildSoftDeleteQuery(collection, id, updatedAt) {
1259
+ function buildSoftDeleteQuery(collection, id, updatedAt, version) {
1260
+ if (version !== void 0) {
1261
+ return {
1262
+ sql: `UPDATE ${collection} SET _deleted = 1, _updated_at = ?, _version = ? WHERE id = ?`,
1263
+ params: [updatedAt, version, id]
1264
+ };
1265
+ }
168
1266
  return {
169
1267
  sql: `UPDATE ${collection} SET _deleted = 1, _updated_at = ? WHERE id = ?`,
170
1268
  params: [updatedAt, id]
171
1269
  };
172
1270
  }
1271
+ function buildLwwUpdateQuery(collection, id, changes, remoteVersion) {
1272
+ const setClauses = Object.keys(changes).map((col) => `${col} = ?`);
1273
+ const lww = lwwVersionWhereClause(remoteVersion);
1274
+ const sql = `UPDATE ${collection} SET ${setClauses.join(", ")} WHERE id = ? AND ${lww.sql}`;
1275
+ const params = [...Object.values(changes), id, ...lww.params];
1276
+ return { sql, params };
1277
+ }
1278
+ function buildLwwSoftDeleteQuery(collection, id, updatedAt, version) {
1279
+ const lww = lwwVersionWhereClause(version);
1280
+ return {
1281
+ sql: `UPDATE ${collection} SET _deleted = 1, _updated_at = ?, _version = ? WHERE id = ? AND ${lww.sql}`,
1282
+ params: [updatedAt, version, id, ...lww.params]
1283
+ };
1284
+ }
173
1285
  var VALID_OPERATORS = /* @__PURE__ */ new Set(["$eq", "$ne", "$gt", "$gte", "$lt", "$lte", "$in"]);
174
1286
  function buildWhereClauseParts(where, fields, params) {
175
1287
  const conditions = [];
@@ -255,200 +1367,188 @@ function validateFieldName(fieldName, fields) {
255
1367
  `Unknown field "${fieldName}" in query. Available fields: ${[...allowedFields].join(", ")}`,
256
1368
  { field: fieldName }
257
1369
  );
258
- }
259
- }
260
-
261
- // src/serialization/serializer.ts
262
- var import_core2 = require("@korajs/core");
263
-
264
- // src/serialization/richtext-serializer.ts
265
- var Y = __toESM(require("yjs"), 1);
266
- var TEXT_KEY = "content";
267
- function encodeRichtext(value) {
268
- if (value === null || value === void 0) {
269
- return null;
270
- }
271
- if (typeof value === "string") {
272
- const doc = new Y.Doc();
273
- doc.getText(TEXT_KEY).insert(0, value);
274
- return Y.encodeStateAsUpdate(doc);
275
- }
276
- if (value instanceof Uint8Array) {
277
- return value;
278
- }
279
- if (value instanceof ArrayBuffer) {
280
- return new Uint8Array(value);
281
- }
282
- throw new Error("Richtext value must be a string, Uint8Array, ArrayBuffer, null, or undefined.");
283
- }
284
- function decodeRichtext(value) {
285
- if (value === null || value === void 0) {
286
- return null;
287
- }
288
- if (typeof Buffer !== "undefined" && Buffer.isBuffer(value)) {
289
- return new Uint8Array(value);
290
- }
291
- if (value instanceof Uint8Array) {
292
- return value;
293
- }
294
- if (value instanceof ArrayBuffer) {
295
- return new Uint8Array(value);
296
- }
297
- throw new Error(
298
- "Richtext storage value must be Uint8Array, ArrayBuffer, Buffer, null, or undefined."
299
- );
300
- }
301
- function richtextToPlainText(value) {
302
- const encoded = encodeRichtext(value);
303
- if (!encoded) return "";
304
- const doc = new Y.Doc();
305
- Y.applyUpdate(doc, encoded);
306
- return doc.getText(TEXT_KEY).toString();
307
- }
308
-
309
- // src/serialization/serializer.ts
310
- function serializeRecord(data, fields) {
311
- const result = {};
312
- for (const [key, value] of Object.entries(data)) {
313
- const descriptor = fields[key];
314
- if (!descriptor) {
315
- result[key] = value;
316
- continue;
317
- }
318
- result[key] = serializeValue(value, descriptor);
319
- }
320
- return result;
321
- }
322
- function deserializeRecord(row, fields) {
323
- const result = {
324
- id: row.id,
325
- createdAt: row._created_at,
326
- updatedAt: row._updated_at
327
- };
328
- for (const [key, descriptor] of Object.entries(fields)) {
329
- const rawValue = row[key];
330
- if (rawValue === void 0 || rawValue === null) {
331
- result[key] = rawValue ?? null;
332
- continue;
333
- }
334
- result[key] = deserializeValue(rawValue, descriptor);
335
- }
336
- return result;
337
- }
338
- var ATOMIC_OPS_KEY = "__kora_atomic_ops__";
339
- var TX_ID_KEY = "__kora_tx_id__";
340
- var MUTATION_NAME_KEY = "__kora_mutation__";
341
- function serializeOperation(op) {
342
- const hasMetadata = op.transactionId !== void 0 || op.mutationName !== void 0;
343
- let dataPayload = null;
344
- if (op.data) {
345
- dataPayload = { ...op.data };
346
- if (op.atomicOps !== void 0 && Object.keys(op.atomicOps).length > 0) {
347
- dataPayload[ATOMIC_OPS_KEY] = op.atomicOps;
348
- }
349
- if (op.transactionId !== void 0) {
350
- dataPayload[TX_ID_KEY] = op.transactionId;
351
- }
352
- if (op.mutationName !== void 0) {
353
- dataPayload[MUTATION_NAME_KEY] = op.mutationName;
354
- }
355
- } else if (hasMetadata) {
356
- dataPayload = {};
357
- if (op.transactionId !== void 0) {
358
- dataPayload[TX_ID_KEY] = op.transactionId;
359
- }
360
- if (op.mutationName !== void 0) {
361
- dataPayload[MUTATION_NAME_KEY] = op.mutationName;
362
- }
363
- }
364
- return {
365
- id: op.id,
366
- node_id: op.nodeId,
367
- type: op.type,
368
- record_id: op.recordId,
369
- data: dataPayload ? JSON.stringify(dataPayload) : null,
370
- previous_data: op.previousData ? JSON.stringify(op.previousData) : null,
371
- timestamp: import_core2.HybridLogicalClock.serialize(op.timestamp),
372
- sequence_number: op.sequenceNumber,
373
- causal_deps: JSON.stringify(op.causalDeps),
374
- schema_version: op.schemaVersion
375
- };
1370
+ }
376
1371
  }
377
- function deserializeOperation(row) {
378
- let data = null;
379
- let atomicOps;
380
- let transactionId;
381
- let mutationName;
382
- if (row.data) {
383
- const parsed = JSON.parse(row.data);
384
- if (ATOMIC_OPS_KEY in parsed) {
385
- atomicOps = parsed[ATOMIC_OPS_KEY];
386
- }
387
- if (TX_ID_KEY in parsed) {
388
- transactionId = parsed[TX_ID_KEY];
1372
+
1373
+ // src/mutations/execute-delete.ts
1374
+ init_serializer();
1375
+
1376
+ // src/store/sequence-allocator.ts
1377
+ async function allocateNextSequenceNumber(adapter, nodeId) {
1378
+ const rows = await adapter.query(
1379
+ `INSERT INTO _kora_version_vector (node_id, sequence_number) VALUES (?, 1)
1380
+ ON CONFLICT(node_id) DO UPDATE SET sequence_number = sequence_number + 1
1381
+ RETURNING sequence_number`,
1382
+ [nodeId]
1383
+ );
1384
+ const seq = rows[0]?.sequence_number;
1385
+ if (seq === void 0) {
1386
+ throw new Error(`Failed to allocate sequence number for node "${nodeId}"`);
1387
+ }
1388
+ return seq;
1389
+ }
1390
+ async function allocateNextSequenceInTransaction(tx, nodeId) {
1391
+ const rows = await tx.query(
1392
+ `INSERT INTO _kora_version_vector (node_id, sequence_number) VALUES (?, 1)
1393
+ ON CONFLICT(node_id) DO UPDATE SET sequence_number = sequence_number + 1
1394
+ RETURNING sequence_number`,
1395
+ [nodeId]
1396
+ );
1397
+ const seq = rows[0]?.sequence_number;
1398
+ if (seq === void 0) {
1399
+ throw new Error(`Failed to allocate sequence number for node "${nodeId}" in transaction`);
1400
+ }
1401
+ return seq;
1402
+ }
1403
+ async function readSequenceNumber(adapter, nodeId) {
1404
+ const rows = await adapter.query(
1405
+ "SELECT sequence_number FROM _kora_version_vector WHERE node_id = ?",
1406
+ [nodeId]
1407
+ );
1408
+ return rows[0]?.sequence_number ?? 0;
1409
+ }
1410
+
1411
+ // src/mutations/resolve-causal-deps.ts
1412
+ function resolveCausalDeps(ctx) {
1413
+ const merged = [];
1414
+ for (const id of ctx.extraCausalDeps ?? []) {
1415
+ if (!merged.includes(id)) {
1416
+ merged.push(id);
389
1417
  }
390
- if (MUTATION_NAME_KEY in parsed) {
391
- mutationName = parsed[MUTATION_NAME_KEY];
1418
+ }
1419
+ for (const id of ctx.causalTracker?.nextCausalDeps(ctx.collection, ctx.inTransaction) ?? []) {
1420
+ if (!merged.includes(id)) {
1421
+ merged.push(id);
392
1422
  }
393
- const { [ATOMIC_OPS_KEY]: _a, [TX_ID_KEY]: _t, [MUTATION_NAME_KEY]: _m, ...rest } = parsed;
394
- data = Object.keys(rest).length > 0 ? rest : null;
395
1423
  }
396
- return {
397
- id: row.id,
398
- nodeId: row.node_id,
399
- type: row.type,
400
- collection: "",
401
- // Collection name is derived from the table name by the caller
402
- recordId: row.record_id,
403
- data,
404
- previousData: row.previous_data ? JSON.parse(row.previous_data) : null,
405
- timestamp: import_core2.HybridLogicalClock.deserialize(row.timestamp),
406
- sequenceNumber: row.sequence_number,
407
- causalDeps: JSON.parse(row.causal_deps),
408
- schemaVersion: row.schema_version,
409
- ...atomicOps !== void 0 ? { atomicOps } : {},
410
- ...transactionId !== void 0 ? { transactionId } : {},
411
- ...mutationName !== void 0 ? { mutationName } : {}
412
- };
413
- }
414
- function deserializeOperationWithCollection(row, collection) {
415
- const op = deserializeOperation(row);
416
- return { ...op, collection };
1424
+ return merged;
417
1425
  }
418
- function serializeValue(value, descriptor) {
419
- if (value === null || value === void 0) {
420
- return null;
1426
+
1427
+ // src/mutations/execute-delete.ts
1428
+ async function executeDelete(ctx, id, options) {
1429
+ const currentRows = await ctx.adapter.query(
1430
+ `SELECT * FROM ${ctx.collection} WHERE id = ? AND _deleted = 0`,
1431
+ [id]
1432
+ );
1433
+ if (!currentRows[0]) {
1434
+ throw new RecordNotFoundError(ctx.collection, id);
1435
+ }
1436
+ const causalDeps = resolveCausalDeps(ctx);
1437
+ let operation = options?.operation;
1438
+ const cascadedOps = [];
1439
+ await ctx.adapter.transaction(async (tx) => {
1440
+ if (!operation) {
1441
+ const sequenceNumber = await allocateNextSequenceInTransaction(tx, ctx.nodeId);
1442
+ operation = await (0, import_core7.createOperation)(
1443
+ {
1444
+ nodeId: ctx.nodeId,
1445
+ type: "delete",
1446
+ collection: ctx.collection,
1447
+ recordId: id,
1448
+ data: null,
1449
+ previousData: null,
1450
+ sequenceNumber,
1451
+ causalDeps,
1452
+ schemaVersion: ctx.schema.version
1453
+ },
1454
+ ctx.clock
1455
+ );
1456
+ ctx.causalTracker?.afterOperation(ctx.collection, operation.id, ctx.inTransaction);
1457
+ }
1458
+ if (ctx.relationEnforcer && !options?.skipReferentialEnforcement) {
1459
+ const enforcementResult = await ctx.relationEnforcer.enforceDelete(ctx.collection, id, tx, [
1460
+ operation.id
1461
+ ]);
1462
+ cascadedOps.push(...enforcementResult.operations);
1463
+ }
1464
+ const version = serializeRowVersion(operation.timestamp);
1465
+ const deleteQuery = buildSoftDeleteQuery(
1466
+ ctx.collection,
1467
+ id,
1468
+ operation.timestamp.wallTime,
1469
+ version
1470
+ );
1471
+ const opInsert = buildInsertQuery(
1472
+ `_kora_ops_${ctx.collection}`,
1473
+ serializeOperation(operation)
1474
+ );
1475
+ await tx.execute(deleteQuery.sql, deleteQuery.params);
1476
+ await tx.execute(opInsert.sql, opInsert.params);
1477
+ });
1478
+ if (!operation) {
1479
+ throw new Error("Delete operation was not created");
421
1480
  }
422
- switch (descriptor.kind) {
423
- case "boolean":
424
- return value ? 1 : 0;
425
- case "array":
426
- return JSON.stringify(value);
427
- case "richtext":
428
- return encodeRichtext(value);
429
- default:
430
- return value;
1481
+ ctx.onMutation(ctx.collection, operation);
1482
+ for (const cascadedOp of cascadedOps) {
1483
+ ctx.onMutation(cascadedOp.collection, cascadedOp);
431
1484
  }
1485
+ return [operation, ...cascadedOps];
432
1486
  }
433
- function deserializeValue(value, descriptor) {
434
- switch (descriptor.kind) {
435
- case "boolean":
436
- return value === 1 || value === true;
437
- case "array":
438
- if (typeof value === "string") {
439
- return JSON.parse(value);
440
- }
441
- return value;
442
- case "richtext":
443
- return decodeRichtext(value);
444
- default:
445
- return value;
1487
+
1488
+ // src/mutations/execute-insert.ts
1489
+ var import_core8 = require("@korajs/core");
1490
+ init_serializer();
1491
+ async function executeInsert(ctx, data) {
1492
+ const validated = (0, import_core8.validateRecord)(ctx.collection, ctx.definition, data, "insert");
1493
+ const recordId = (0, import_core8.generateUUIDv7)();
1494
+ for (const [fieldName, descriptor] of Object.entries(ctx.definition.fields)) {
1495
+ if (descriptor.auto && descriptor.kind === "timestamp") {
1496
+ validated[fieldName] = Date.now();
1497
+ }
446
1498
  }
1499
+ const causalDeps = resolveCausalDeps(ctx);
1500
+ let operation;
1501
+ let record;
1502
+ await ctx.adapter.transaction(async (tx) => {
1503
+ const sequenceNumber = await allocateNextSequenceInTransaction(tx, ctx.nodeId);
1504
+ operation = await (0, import_core8.createOperation)(
1505
+ {
1506
+ nodeId: ctx.nodeId,
1507
+ type: "insert",
1508
+ collection: ctx.collection,
1509
+ recordId,
1510
+ data: { ...validated },
1511
+ previousData: null,
1512
+ sequenceNumber,
1513
+ causalDeps,
1514
+ schemaVersion: ctx.schema.version
1515
+ },
1516
+ ctx.clock
1517
+ );
1518
+ ctx.causalTracker?.afterOperation(ctx.collection, operation.id, ctx.inTransaction);
1519
+ const serializedData = serializeRecord(validated, ctx.definition.fields);
1520
+ const version = serializeRowVersion(operation.timestamp);
1521
+ record = {
1522
+ id: recordId,
1523
+ ...serializedData,
1524
+ _created_at: operation.timestamp.wallTime,
1525
+ _updated_at: operation.timestamp.wallTime,
1526
+ _version: version
1527
+ };
1528
+ const builtInsert = buildInsertQuery(ctx.collection, record);
1529
+ const opInsert = buildInsertQuery(
1530
+ `_kora_ops_${ctx.collection}`,
1531
+ serializeOperation(operation)
1532
+ );
1533
+ await tx.execute(builtInsert.sql, builtInsert.params);
1534
+ await tx.execute(opInsert.sql, opInsert.params);
1535
+ });
1536
+ ctx.onMutation(ctx.collection, operation);
1537
+ return {
1538
+ id: recordId,
1539
+ ...validated,
1540
+ createdAt: operation.timestamp.wallTime,
1541
+ updatedAt: operation.timestamp.wallTime
1542
+ };
447
1543
  }
448
1544
 
1545
+ // src/mutations/execute-update.ts
1546
+ var import_core10 = require("@korajs/core");
1547
+ init_serializer();
1548
+
449
1549
  // src/state-machine/state-validator.ts
450
- var import_core3 = require("@korajs/core");
451
- var InvalidStateTransitionError = class extends import_core3.KoraError {
1550
+ var import_core9 = require("@korajs/core");
1551
+ var InvalidStateTransitionError = class extends import_core9.KoraError {
452
1552
  constructor(collection, recordId, field, fromState, toState, allowedStates) {
453
1553
  super(
454
1554
  `Invalid state transition in collection "${collection}": cannot transition field "${field}" from "${fromState}" to "${toState}". Allowed transitions from "${fromState}": ${allowedStates.length > 0 ? allowedStates.join(", ") : "(none -- terminal state)"}`,
@@ -482,7 +1582,7 @@ function validateStateTransition(collectionName, recordId, stateMachine, current
482
1582
  collection: collectionName,
483
1583
  transitions: stateMachine.transitions
484
1584
  };
485
- const transitionResult = (0, import_core3.validateTransition)(constraint, currentState, newState);
1585
+ const transitionResult = (0, import_core9.validateTransition)(constraint, currentState, newState);
486
1586
  if (transitionResult.valid) {
487
1587
  return { valid: true, allowedStates: transitionResult.allowedTargets };
488
1588
  }
@@ -528,18 +1628,103 @@ function validateUpdateStateMachine(collectionName, recordId, collectionDef, cur
528
1628
  return filtered;
529
1629
  }
530
1630
 
1631
+ // src/mutations/execute-update.ts
1632
+ async function executeUpdate(ctx, id, data) {
1633
+ const currentRows = await ctx.adapter.query(
1634
+ `SELECT * FROM ${ctx.collection} WHERE id = ? AND _deleted = 0`,
1635
+ [id]
1636
+ );
1637
+ const currentRow = currentRows[0];
1638
+ if (!currentRow) {
1639
+ throw new RecordNotFoundError(ctx.collection, id);
1640
+ }
1641
+ let validated = (0, import_core10.validateRecord)(ctx.collection, ctx.definition, data, "update");
1642
+ const currentRecord = deserializeRecord(currentRow, ctx.definition.fields);
1643
+ validated = validateUpdateStateMachine(
1644
+ ctx.collection,
1645
+ id,
1646
+ ctx.definition,
1647
+ currentRecord,
1648
+ validated
1649
+ );
1650
+ if (Object.keys(validated).length === 0) {
1651
+ return { ...currentRecord };
1652
+ }
1653
+ const previousData = {};
1654
+ const resolvedData = {};
1655
+ const atomicOps = {};
1656
+ for (const key of Object.keys(validated)) {
1657
+ const value = validated[key];
1658
+ previousData[key] = currentRecord[key];
1659
+ if ((0, import_core10.isAtomicOp)(value)) {
1660
+ resolvedData[key] = (0, import_core10.resolveAtomicOp)(currentRecord[key], value);
1661
+ atomicOps[key] = (0, import_core10.toAtomicOp)(value);
1662
+ } else {
1663
+ resolvedData[key] = value;
1664
+ }
1665
+ }
1666
+ const hasAtomicOps = Object.keys(atomicOps).length > 0;
1667
+ const causalDeps = resolveCausalDeps(ctx);
1668
+ let operation;
1669
+ await ctx.adapter.transaction(async (tx) => {
1670
+ const sequenceNumber = await allocateNextSequenceInTransaction(tx, ctx.nodeId);
1671
+ operation = await (0, import_core10.createOperation)(
1672
+ {
1673
+ nodeId: ctx.nodeId,
1674
+ type: "update",
1675
+ collection: ctx.collection,
1676
+ recordId: id,
1677
+ data: { ...resolvedData },
1678
+ previousData,
1679
+ sequenceNumber,
1680
+ causalDeps,
1681
+ schemaVersion: ctx.schema.version,
1682
+ ...hasAtomicOps ? { atomicOps } : {}
1683
+ },
1684
+ ctx.clock
1685
+ );
1686
+ ctx.causalTracker?.afterOperation(ctx.collection, operation.id, ctx.inTransaction);
1687
+ const serializedChanges = serializeRecord(resolvedData, ctx.definition.fields);
1688
+ const version = serializeRowVersion(operation.timestamp);
1689
+ const updateQuery = buildUpdateQuery(ctx.collection, id, {
1690
+ ...serializedChanges,
1691
+ _updated_at: operation.timestamp.wallTime,
1692
+ _version: version
1693
+ });
1694
+ const opInsert = buildInsertQuery(
1695
+ `_kora_ops_${ctx.collection}`,
1696
+ serializeOperation(operation)
1697
+ );
1698
+ await tx.execute(updateQuery.sql, updateQuery.params);
1699
+ await tx.execute(opInsert.sql, opInsert.params);
1700
+ });
1701
+ ctx.onMutation(ctx.collection, operation);
1702
+ const rows = await ctx.adapter.query(
1703
+ `SELECT * FROM ${ctx.collection} WHERE id = ? AND _deleted = 0`,
1704
+ [id]
1705
+ );
1706
+ const row = rows[0];
1707
+ if (!row) {
1708
+ throw new RecordNotFoundError(ctx.collection, id);
1709
+ }
1710
+ return deserializeRecord(row, ctx.definition.fields);
1711
+ }
1712
+
531
1713
  // src/collection/collection.ts
1714
+ init_serializer();
532
1715
  var Collection = class {
533
- constructor(name, definition, schema, adapter, clock, nodeId, getSequenceNumber, onMutation, relationEnforcer) {
1716
+ constructor(name, definition, schema, adapter, clock, nodeId, allocateSequenceNumber, onMutation, relationEnforcer, mutationHandler, causalTracker) {
534
1717
  this.name = name;
535
1718
  this.definition = definition;
536
1719
  this.schema = schema;
537
1720
  this.adapter = adapter;
538
1721
  this.clock = clock;
539
1722
  this.nodeId = nodeId;
540
- this.getSequenceNumber = getSequenceNumber;
1723
+ this.allocateSequenceNumber = allocateSequenceNumber;
541
1724
  this.onMutation = onMutation;
542
- this.relationEnforcer = relationEnforcer ?? null;
1725
+ this.relationEnforcer = relationEnforcer;
1726
+ this.mutationHandler = mutationHandler;
1727
+ this.causalTracker = causalTracker;
543
1728
  }
544
1729
  name;
545
1730
  definition;
@@ -547,72 +1732,32 @@ var Collection = class {
547
1732
  adapter;
548
1733
  clock;
549
1734
  nodeId;
550
- getSequenceNumber;
1735
+ allocateSequenceNumber;
551
1736
  onMutation;
552
1737
  relationEnforcer;
553
- /**
554
- * Insert a new record into the collection.
555
- * Generates a UUID v7 for the id, validates data, and persists atomically.
556
- *
557
- * @param data - The record data (auto fields and defaults are applied automatically)
558
- * @returns The inserted record with id, createdAt, updatedAt
559
- */
560
- async insert(data) {
561
- const validated = (0, import_core4.validateRecord)(this.name, this.definition, data, "insert");
562
- const recordId = (0, import_core4.generateUUIDv7)();
563
- const now = Date.now();
564
- for (const [fieldName, descriptor] of Object.entries(this.definition.fields)) {
565
- if (descriptor.auto && descriptor.kind === "timestamp") {
566
- validated[fieldName] = now;
567
- }
568
- }
569
- const sequenceNumber = this.getSequenceNumber();
570
- const operation = await (0, import_core4.createOperation)(
571
- {
572
- nodeId: this.nodeId,
573
- type: "insert",
574
- collection: this.name,
575
- recordId,
576
- data: { ...validated },
577
- previousData: null,
578
- sequenceNumber,
579
- causalDeps: [],
580
- schemaVersion: this.schema.version
581
- },
582
- this.clock
583
- );
584
- const serializedData = serializeRecord(validated, this.definition.fields);
585
- const record = {
586
- id: recordId,
587
- ...serializedData,
588
- _created_at: now,
589
- _updated_at: now
590
- };
591
- const insertQuery = buildInsertQuery(this.name, record);
592
- const opRow = serializeOperation(operation);
593
- const opInsert = buildInsertQuery(
594
- `_kora_ops_${this.name}`,
595
- opRow
596
- );
597
- await this.adapter.transaction(async (tx) => {
598
- await tx.execute(insertQuery.sql, insertQuery.params);
599
- await tx.execute(opInsert.sql, opInsert.params);
600
- await tx.execute(
601
- "INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
602
- [this.nodeId, sequenceNumber]
603
- );
604
- });
605
- this.onMutation(this.name, operation);
1738
+ mutationHandler;
1739
+ causalTracker;
1740
+ mutationContext() {
606
1741
  return {
607
- id: recordId,
608
- ...validated,
609
- createdAt: now,
610
- updatedAt: now
1742
+ collection: this.name,
1743
+ definition: this.definition,
1744
+ schema: this.schema,
1745
+ adapter: this.adapter,
1746
+ clock: this.clock,
1747
+ nodeId: this.nodeId,
1748
+ allocateSequenceNumber: this.allocateSequenceNumber,
1749
+ onMutation: this.onMutation,
1750
+ relationEnforcer: this.relationEnforcer,
1751
+ causalTracker: this.causalTracker,
1752
+ inTransaction: false
611
1753
  };
612
1754
  }
613
- /**
614
- * Find a record by its ID. Returns null if not found or soft-deleted.
615
- */
1755
+ async insert(data) {
1756
+ if (this.mutationHandler) {
1757
+ return this.mutationHandler.insert(this.name, data);
1758
+ }
1759
+ return executeInsert(this.mutationContext(), data);
1760
+ }
616
1761
  async findById(id) {
617
1762
  const rows = await this.adapter.query(
618
1763
  `SELECT * FROM ${this.name} WHERE id = ? AND _deleted = 0`,
@@ -622,175 +1767,34 @@ var Collection = class {
622
1767
  if (!row) return null;
623
1768
  return deserializeRecord(row, this.definition.fields);
624
1769
  }
625
- /**
626
- * Update an existing record. Only the provided fields are changed.
627
- *
628
- * @param id - The record ID to update
629
- * @param data - Partial data with only the fields to change
630
- * @returns The updated record
631
- * @throws {RecordNotFoundError} If the record doesn't exist or is deleted
632
- */
633
1770
  async update(id, data) {
634
- const currentRows = await this.adapter.query(
635
- `SELECT * FROM ${this.name} WHERE id = ? AND _deleted = 0`,
636
- [id]
637
- );
638
- const currentRow = currentRows[0];
639
- if (!currentRow) {
640
- throw new RecordNotFoundError(this.name, id);
641
- }
642
- let validated = (0, import_core4.validateRecord)(this.name, this.definition, data, "update");
643
- const now = Date.now();
644
- const currentRecord = deserializeRecord(currentRow, this.definition.fields);
645
- validated = validateUpdateStateMachine(
646
- this.name,
647
- id,
648
- this.definition,
649
- currentRecord,
650
- validated
651
- );
652
- if (Object.keys(validated).length === 0) {
653
- const fullRecord = await this.findById(id);
654
- if (!fullRecord) {
655
- throw new RecordNotFoundError(this.name, id);
656
- }
657
- return fullRecord;
658
- }
659
- const previousData = {};
660
- const resolvedData = {};
661
- const atomicOps = {};
662
- for (const key of Object.keys(validated)) {
663
- const value = validated[key];
664
- previousData[key] = currentRecord[key];
665
- if ((0, import_core4.isAtomicOp)(value)) {
666
- resolvedData[key] = (0, import_core4.resolveAtomicOp)(currentRecord[key], value);
667
- atomicOps[key] = (0, import_core4.toAtomicOp)(value);
668
- } else {
669
- resolvedData[key] = value;
670
- }
671
- }
672
- const hasAtomicOps = Object.keys(atomicOps).length > 0;
673
- const sequenceNumber = this.getSequenceNumber();
674
- const operation = await (0, import_core4.createOperation)(
675
- {
676
- nodeId: this.nodeId,
677
- type: "update",
678
- collection: this.name,
679
- recordId: id,
680
- data: { ...resolvedData },
681
- previousData,
682
- sequenceNumber,
683
- causalDeps: [],
684
- schemaVersion: this.schema.version,
685
- ...hasAtomicOps ? { atomicOps } : {}
686
- },
687
- this.clock
688
- );
689
- const serializedChanges = serializeRecord(resolvedData, this.definition.fields);
690
- const updateQuery = buildUpdateQuery(this.name, id, {
691
- ...serializedChanges,
692
- _updated_at: now
693
- });
694
- const opRow = serializeOperation(operation);
695
- const opInsert = buildInsertQuery(
696
- `_kora_ops_${this.name}`,
697
- opRow
698
- );
699
- await this.adapter.transaction(async (tx) => {
700
- await tx.execute(updateQuery.sql, updateQuery.params);
701
- await tx.execute(opInsert.sql, opInsert.params);
702
- await tx.execute(
703
- "INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
704
- [this.nodeId, sequenceNumber]
705
- );
706
- });
707
- this.onMutation(this.name, operation);
708
- const updatedRow = await this.findById(id);
709
- if (!updatedRow) {
710
- throw new RecordNotFoundError(this.name, id);
1771
+ if (this.mutationHandler) {
1772
+ return this.mutationHandler.update(this.name, id, data);
711
1773
  }
712
- return updatedRow;
1774
+ return executeUpdate(this.mutationContext(), id, data);
713
1775
  }
714
- /**
715
- * Soft-delete a record by its ID.
716
- *
717
- * When a RelationEnforcer is configured, referential integrity policies
718
- * are enforced within the same transaction:
719
- * - **cascade**: Recursively deletes all referencing records
720
- * - **set-null**: Sets foreign keys to null on referencing records
721
- * - **restrict**: Throws ReferentialIntegrityError if references exist
722
- * - **no-action**: Does nothing (dangling references are left)
723
- *
724
- * All cascaded operations are created atomically within the same transaction
725
- * and share causal dependencies with the original delete.
726
- *
727
- * @param id - The record ID to delete
728
- * @throws {RecordNotFoundError} If the record doesn't exist or is already deleted
729
- * @throws {ReferentialIntegrityError} If a 'restrict' policy is violated
730
- */
731
1776
  async delete(id) {
732
- const currentRows = await this.adapter.query(
733
- `SELECT * FROM ${this.name} WHERE id = ? AND _deleted = 0`,
734
- [id]
735
- );
736
- if (!currentRows[0]) {
737
- throw new RecordNotFoundError(this.name, id);
738
- }
739
- const now = Date.now();
740
- const sequenceNumber = this.getSequenceNumber();
741
- const operation = await (0, import_core4.createOperation)(
742
- {
743
- nodeId: this.nodeId,
744
- type: "delete",
745
- collection: this.name,
746
- recordId: id,
747
- data: null,
748
- previousData: null,
749
- sequenceNumber,
750
- causalDeps: [],
751
- schemaVersion: this.schema.version
752
- },
753
- this.clock
754
- );
755
- const deleteQuery = buildSoftDeleteQuery(this.name, id, now);
756
- const opRow = serializeOperation(operation);
757
- const opInsert = buildInsertQuery(
758
- `_kora_ops_${this.name}`,
759
- opRow
760
- );
761
- const cascadedOps = [];
762
- await this.adapter.transaction(async (tx) => {
763
- if (this.relationEnforcer) {
764
- const enforcementResult = await this.relationEnforcer.enforceDelete(
765
- this.name,
766
- id,
767
- tx,
768
- [operation.id]
769
- );
770
- cascadedOps.push(...enforcementResult.operations);
771
- }
772
- await tx.execute(deleteQuery.sql, deleteQuery.params);
773
- await tx.execute(opInsert.sql, opInsert.params);
774
- await tx.execute(
775
- "INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
776
- [this.nodeId, sequenceNumber]
777
- );
778
- });
779
- this.onMutation(this.name, operation);
780
- for (const cascadedOp of cascadedOps) {
781
- this.onMutation(cascadedOp.collection, cascadedOp);
1777
+ if (this.mutationHandler) {
1778
+ await this.mutationHandler.delete(this.name, id);
1779
+ return;
782
1780
  }
1781
+ await executeDelete(this.mutationContext(), id);
783
1782
  }
784
- /** Get the collection name */
785
1783
  getName() {
786
1784
  return this.name;
787
1785
  }
788
- /** Get the collection definition */
789
1786
  getDefinition() {
790
1787
  return this.definition;
791
1788
  }
1789
+ /** Replace the mutation handler (e.g. after ApplyPipeline is wired in createApp). */
1790
+ setMutationHandler(handler) {
1791
+ this.mutationHandler = handler;
1792
+ }
792
1793
  };
793
1794
 
1795
+ // src/query/query-builder.ts
1796
+ init_serializer();
1797
+
794
1798
  // src/query/pluralize.ts
795
1799
  function isVowel(char) {
796
1800
  if (!char) return false;
@@ -1085,7 +2089,8 @@ var QueryError2 = class extends Error {
1085
2089
  };
1086
2090
 
1087
2091
  // src/relations/relation-enforcer.ts
1088
- var import_core5 = require("@korajs/core");
2092
+ var import_core11 = require("@korajs/core");
2093
+ init_serializer();
1089
2094
 
1090
2095
  // src/relations/relation-lookup.ts
1091
2096
  function buildRelationLookup(schema) {
@@ -1109,7 +2114,7 @@ function getIncomingRelations(lookup, collection) {
1109
2114
  }
1110
2115
 
1111
2116
  // src/relations/relation-enforcer.ts
1112
- var ReferentialIntegrityError = class extends import_core5.KoraError {
2117
+ var ReferentialIntegrityError = class extends import_core11.KoraError {
1113
2118
  constructor(collection, recordId, referencingCollection, relationName, referencingCount) {
1114
2119
  super(
1115
2120
  `Cannot delete record "${recordId}" from "${collection}": ${referencingCount} record(s) in "${referencingCollection}" reference it via relation "${relationName}" with onDelete: 'restrict'. Delete or reassign the referencing records first.`,
@@ -1131,13 +2136,11 @@ var RelationEnforcer = class {
1131
2136
  adapter;
1132
2137
  clock;
1133
2138
  nodeId;
1134
- getSequenceNumber;
1135
2139
  constructor(config) {
1136
2140
  this.schema = config.schema;
1137
2141
  this.adapter = config.adapter;
1138
2142
  this.clock = config.clock;
1139
2143
  this.nodeId = config.nodeId;
1140
- this.getSequenceNumber = config.getSequenceNumber;
1141
2144
  this.lookup = buildRelationLookup(config.schema);
1142
2145
  }
1143
2146
  /**
@@ -1199,8 +2202,8 @@ var RelationEnforcer = class {
1199
2202
  const operations = [];
1200
2203
  for (const row of referencingRows) {
1201
2204
  const now = Date.now();
1202
- const sequenceNumber = this.getSequenceNumber();
1203
- const operation = await (0, import_core5.createOperation)(
2205
+ const sequenceNumber = await allocateNextSequenceInTransaction(tx, this.nodeId);
2206
+ const operation = await (0, import_core11.createOperation)(
1204
2207
  {
1205
2208
  nodeId: this.nodeId,
1206
2209
  type: "delete",
@@ -1216,16 +2219,11 @@ var RelationEnforcer = class {
1216
2219
  );
1217
2220
  const deleteQuery = buildSoftDeleteQuery(sourceCollection, row.id, now);
1218
2221
  await tx.execute(deleteQuery.sql, deleteQuery.params);
1219
- const opRow = serializeOperation(operation);
1220
2222
  const opInsert = buildInsertQuery(
1221
2223
  `_kora_ops_${sourceCollection}`,
1222
- opRow
2224
+ serializeOperation(operation)
1223
2225
  );
1224
2226
  await tx.execute(opInsert.sql, opInsert.params);
1225
- await tx.execute(
1226
- "INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
1227
- [this.nodeId, sequenceNumber]
1228
- );
1229
2227
  operations.push(operation);
1230
2228
  const cascadeResult = await this.enforceDelete(sourceCollection, row.id, tx, [operation.id]);
1231
2229
  operations.push(...cascadeResult.operations);
@@ -1251,10 +2249,10 @@ var RelationEnforcer = class {
1251
2249
  const operations = [];
1252
2250
  for (const row of referencingRows) {
1253
2251
  const now = Date.now();
1254
- const sequenceNumber = this.getSequenceNumber();
2252
+ const sequenceNumber = await allocateNextSequenceInTransaction(tx, this.nodeId);
1255
2253
  const updateData = { [foreignKeyField]: null };
1256
2254
  const previousData = { [foreignKeyField]: deletedRecordId };
1257
- const operation = await (0, import_core5.createOperation)(
2255
+ const operation = await (0, import_core11.createOperation)(
1258
2256
  {
1259
2257
  nodeId: this.nodeId,
1260
2258
  type: "update",
@@ -1280,10 +2278,6 @@ var RelationEnforcer = class {
1280
2278
  opRow
1281
2279
  );
1282
2280
  await tx.execute(opInsert.sql, opInsert.params);
1283
- await tx.execute(
1284
- "INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
1285
- [this.nodeId, sequenceNumber]
1286
- );
1287
2281
  operations.push(operation);
1288
2282
  }
1289
2283
  return operations;
@@ -1318,8 +2312,172 @@ var RelationEnforcer = class {
1318
2312
  }
1319
2313
  };
1320
2314
 
2315
+ // src/replay/replay-to.ts
2316
+ var import_core12 = require("@korajs/core");
2317
+ var import_internal = require("@korajs/core/internal");
2318
+ function collectCausalClosure(allOps, targetOperationId) {
2319
+ const opMap = /* @__PURE__ */ new Map();
2320
+ for (const op of allOps) {
2321
+ opMap.set(op.id, op);
2322
+ }
2323
+ const target = opMap.get(targetOperationId);
2324
+ if (!target) {
2325
+ throw new import_core12.OperationError(
2326
+ `Operation "${targetOperationId}" not found in the local operation log`,
2327
+ {
2328
+ operationId: targetOperationId
2329
+ }
2330
+ );
2331
+ }
2332
+ const included = /* @__PURE__ */ new Set();
2333
+ const stack = [targetOperationId];
2334
+ while (stack.length > 0) {
2335
+ const id = stack.pop();
2336
+ if (id === void 0 || included.has(id)) {
2337
+ continue;
2338
+ }
2339
+ included.add(id);
2340
+ const op = opMap.get(id);
2341
+ if (!op) {
2342
+ continue;
2343
+ }
2344
+ for (const depId of op.causalDeps) {
2345
+ if (opMap.has(depId)) {
2346
+ stack.push(depId);
2347
+ }
2348
+ }
2349
+ }
2350
+ const subset = allOps.filter((op) => included.has(op.id));
2351
+ return (0, import_internal.topologicalSort)(subset);
2352
+ }
2353
+ function buildReplaySnapshot(schema, allOps, targetOperationId) {
2354
+ const operationsApplied = collectCausalClosure(allOps, targetOperationId);
2355
+ const targetOperation = operationsApplied.find((op) => op.id === targetOperationId);
2356
+ if (!targetOperation) {
2357
+ throw new import_core12.OperationError(`Operation "${targetOperationId}" not found after causal sort`, {
2358
+ operationId: targetOperationId
2359
+ });
2360
+ }
2361
+ const memory = /* @__PURE__ */ new Map();
2362
+ for (const op of operationsApplied) {
2363
+ applyOperationToMemory(memory, op, schema);
2364
+ }
2365
+ const collections = materializeCollections(schema, memory);
2366
+ return {
2367
+ targetOperation,
2368
+ operationsApplied,
2369
+ collections,
2370
+ findRecord(collection, recordId) {
2371
+ const colMap = memory.get(collection);
2372
+ const record = colMap?.get(recordId);
2373
+ if (!record || record.deleted) {
2374
+ return null;
2375
+ }
2376
+ const definition = schema.collections[collection];
2377
+ if (!definition) {
2378
+ return null;
2379
+ }
2380
+ return toCollectionRecord(record, definition);
2381
+ }
2382
+ };
2383
+ }
2384
+ function applyOperationToMemory(state, op, schema) {
2385
+ const definition = schema.collections[op.collection];
2386
+ if (!definition) {
2387
+ return;
2388
+ }
2389
+ let colMap = state.get(op.collection);
2390
+ if (!colMap) {
2391
+ colMap = /* @__PURE__ */ new Map();
2392
+ state.set(op.collection, colMap);
2393
+ }
2394
+ const wallTime = op.timestamp.wallTime;
2395
+ switch (op.type) {
2396
+ case "insert": {
2397
+ if (!op.data) {
2398
+ return;
2399
+ }
2400
+ colMap.set(op.recordId, {
2401
+ id: op.recordId,
2402
+ fields: { ...op.data },
2403
+ deleted: false,
2404
+ createdAt: wallTime,
2405
+ updatedAt: wallTime
2406
+ });
2407
+ break;
2408
+ }
2409
+ case "update": {
2410
+ if (!op.data) {
2411
+ return;
2412
+ }
2413
+ const existing = colMap.get(op.recordId);
2414
+ if (existing && !existing.deleted) {
2415
+ existing.fields = { ...existing.fields, ...op.data };
2416
+ existing.updatedAt = wallTime;
2417
+ break;
2418
+ }
2419
+ if (existing?.deleted) {
2420
+ return;
2421
+ }
2422
+ colMap.set(op.recordId, {
2423
+ id: op.recordId,
2424
+ fields: { ...op.data },
2425
+ deleted: false,
2426
+ createdAt: wallTime,
2427
+ updatedAt: wallTime
2428
+ });
2429
+ break;
2430
+ }
2431
+ case "delete": {
2432
+ const existing = colMap.get(op.recordId);
2433
+ if (existing) {
2434
+ existing.deleted = true;
2435
+ existing.updatedAt = wallTime;
2436
+ return;
2437
+ }
2438
+ colMap.set(op.recordId, {
2439
+ id: op.recordId,
2440
+ fields: {},
2441
+ deleted: true,
2442
+ createdAt: wallTime,
2443
+ updatedAt: wallTime
2444
+ });
2445
+ break;
2446
+ }
2447
+ }
2448
+ }
2449
+ function materializeCollections(schema, memory) {
2450
+ const collections = {};
2451
+ for (const [collectionName, definition] of Object.entries(schema.collections)) {
2452
+ const colMap = memory.get(collectionName);
2453
+ const records = [];
2454
+ if (colMap) {
2455
+ for (const record of colMap.values()) {
2456
+ if (!record.deleted) {
2457
+ records.push(toCollectionRecord(record, definition));
2458
+ }
2459
+ }
2460
+ }
2461
+ collections[collectionName] = records;
2462
+ }
2463
+ return collections;
2464
+ }
2465
+ function toCollectionRecord(record, definition) {
2466
+ const result = {
2467
+ id: record.id,
2468
+ createdAt: record.createdAt,
2469
+ updatedAt: record.updatedAt
2470
+ };
2471
+ for (const [fieldName] of Object.entries(definition.fields)) {
2472
+ if (fieldName in record.fields) {
2473
+ result[fieldName] = record.fields[fieldName];
2474
+ }
2475
+ }
2476
+ return result;
2477
+ }
2478
+
1321
2479
  // src/sequences/sequence-manager.ts
1322
- var import_core6 = require("@korajs/core");
2480
+ var import_core13 = require("@korajs/core");
1323
2481
  var SequenceManager = class {
1324
2482
  adapter;
1325
2483
  nodeId;
@@ -1337,7 +2495,7 @@ var SequenceManager = class {
1337
2495
  async next(name, config) {
1338
2496
  const scope = config?.scope ?? "";
1339
2497
  const startAt = config?.startAt ?? 1;
1340
- const format = config?.format ?? (0, import_core6.defaultSequenceFormat)(name);
2498
+ const format = config?.format ?? (0, import_core13.defaultSequenceFormat)(name);
1341
2499
  let counter = 0;
1342
2500
  await this.adapter.transaction(async (tx) => {
1343
2501
  const rows = await tx.query(
@@ -1359,7 +2517,7 @@ var SequenceManager = class {
1359
2517
  );
1360
2518
  }
1361
2519
  });
1362
- return (0, import_core6.formatSequenceValue)(format, counter, this.nodeId);
2520
+ return (0, import_core13.formatSequenceValue)(format, counter, this.nodeId);
1363
2521
  }
1364
2522
  /**
1365
2523
  * Get the current counter value without incrementing.
@@ -1401,6 +2559,9 @@ var SequenceManager = class {
1401
2559
  }
1402
2560
  };
1403
2561
 
2562
+ // src/store/store.ts
2563
+ init_serializer();
2564
+
1404
2565
  // src/subscription/bloom-filter.ts
1405
2566
  var FNV_OFFSET_BASIS = 2166136261;
1406
2567
  var FNV_PRIME = 16777619;
@@ -1561,6 +2722,7 @@ var SubscriptionManager = class {
1561
2722
  subscriptions = /* @__PURE__ */ new Map();
1562
2723
  pendingCollections = /* @__PURE__ */ new Set();
1563
2724
  flushScheduled = false;
2725
+ onQuerySubscribed;
1564
2726
  // Bloom filter state
1565
2727
  bloomFilter = null;
1566
2728
  bloomDirty = false;
@@ -1577,6 +2739,7 @@ var SubscriptionManager = class {
1577
2739
  this.bloomThreshold = options?.bloomThreshold ?? DEFAULT_BLOOM_THRESHOLD;
1578
2740
  this.bloomExpectedItems = options?.bloomExpectedItems ?? DEFAULT_BLOOM_EXPECTED_ITEMS;
1579
2741
  this.bloomFalsePositiveRate = options?.bloomFalsePositiveRate ?? DEFAULT_BLOOM_FALSE_POSITIVE_RATE;
2742
+ this.onQuerySubscribed = options?.onQuerySubscribed;
1580
2743
  }
1581
2744
  /**
1582
2745
  * Register a new subscription.
@@ -1597,9 +2760,11 @@ var SubscriptionManager = class {
1597
2760
  };
1598
2761
  this.subscriptions.set(id, subscription);
1599
2762
  this.bloomDirty = true;
2763
+ const externalCleanup = this.onQuerySubscribed?.(descriptor);
1600
2764
  return () => {
1601
2765
  this.subscriptions.delete(id);
1602
2766
  this.bloomDirty = true;
2767
+ externalCleanup?.();
1603
2768
  };
1604
2769
  }
1605
2770
  /**
@@ -1620,6 +2785,7 @@ var SubscriptionManager = class {
1620
2785
  };
1621
2786
  this.subscriptions.set(id, subscription);
1622
2787
  this.bloomDirty = true;
2788
+ const externalCleanup = this.onQuerySubscribed?.(descriptor);
1623
2789
  executeFn().then((results) => {
1624
2790
  if (this.subscriptions.has(id)) {
1625
2791
  subscription.lastResults = results;
@@ -1629,6 +2795,7 @@ var SubscriptionManager = class {
1629
2795
  return () => {
1630
2796
  this.subscriptions.delete(id);
1631
2797
  this.bloomDirty = true;
2798
+ externalCleanup?.();
1632
2799
  };
1633
2800
  }
1634
2801
  /**
@@ -1764,10 +2931,7 @@ var SubscriptionManager = class {
1764
2931
  * any included collection dependencies.
1765
2932
  */
1766
2933
  rebuildBloomFilter() {
1767
- const filter = new SubscriptionBloomFilter(
1768
- this.bloomExpectedItems,
1769
- this.bloomFalsePositiveRate
1770
- );
2934
+ const filter = new SubscriptionBloomFilter(this.bloomExpectedItems, this.bloomFalsePositiveRate);
1771
2935
  for (const sub of this.subscriptions.values()) {
1772
2936
  filter.add(sub.descriptor.collection);
1773
2937
  if (sub.descriptor.includeCollections) {
@@ -1787,18 +2951,33 @@ var SubscriptionManager = class {
1787
2951
  });
1788
2952
  }
1789
2953
  /**
1790
- * Compare two result sets. Uses length check + JSON comparison as pragmatic approach.
1791
- * Sufficient for typical query results. Can be optimized to id-based diffing if profiling shows need.
2954
+ * Compare two result sets. Uses ID-based comparison first (same length, same IDs),
2955
+ * then deep comparison of remaining fields when IDs match.
2956
+ * Avoids JSON.stringify which is O(n) and fragile to field ordering differences.
1792
2957
  */
1793
2958
  resultsEqual(prev, next) {
1794
2959
  if (prev.length !== next.length) return false;
1795
2960
  if (prev.length === 0) return true;
1796
- return JSON.stringify(prev) === JSON.stringify(next);
2961
+ for (let i = 0; i < prev.length; i++) {
2962
+ if (prev[i]?.id !== next[i]?.id) return false;
2963
+ }
2964
+ for (let i = 0; i < prev.length; i++) {
2965
+ const a = prev[i];
2966
+ const b = next[i];
2967
+ const keysA = Object.keys(a);
2968
+ const keysB = Object.keys(b);
2969
+ if (keysA.length !== keysB.length) return false;
2970
+ for (const key of keysA) {
2971
+ if (a[key] !== b[key]) return false;
2972
+ }
2973
+ }
2974
+ return true;
1797
2975
  }
1798
2976
  };
1799
2977
 
1800
2978
  // src/transaction/transaction-context.ts
1801
- var import_core7 = require("@korajs/core");
2979
+ var import_core14 = require("@korajs/core");
2980
+ init_serializer();
1802
2981
  var TransactionContext = class {
1803
2982
  transactionId;
1804
2983
  mutationName;
@@ -1808,7 +2987,7 @@ var TransactionContext = class {
1808
2987
  config;
1809
2988
  constructor(config) {
1810
2989
  this.config = config;
1811
- this.transactionId = (0, import_core7.generateUUIDv7)();
2990
+ this.transactionId = (0, import_core14.generateUUIDv7)();
1812
2991
  }
1813
2992
  /**
1814
2993
  * Set a human-readable mutation name for this transaction.
@@ -1855,16 +3034,48 @@ var TransactionContext = class {
1855
3034
  if (this.buffer.length === 0) {
1856
3035
  return { operations: [], affectedCollections: /* @__PURE__ */ new Set() };
1857
3036
  }
3037
+ const handler = this.config.localMutationHandler;
3038
+ if (handler?.commitTransaction) {
3039
+ return handler.commitTransaction({
3040
+ entries: this.buffer.map((entry) => ({
3041
+ operation: entry.operation,
3042
+ commands: entry.commands,
3043
+ collection: entry.collection
3044
+ })),
3045
+ transactionId: this.transactionId,
3046
+ ...this.mutationName !== void 0 ? { mutationName: this.mutationName } : {}
3047
+ });
3048
+ }
1858
3049
  const operations = [];
1859
3050
  const affectedCollections = /* @__PURE__ */ new Set();
1860
3051
  await this.config.adapter.transaction(async (tx) => {
1861
3052
  for (const entry of this.buffer) {
3053
+ if (entry.operation.type === "delete" && this.config.relationEnforcer) {
3054
+ const cascadeResult = await this.config.relationEnforcer.enforceDelete(
3055
+ entry.collection,
3056
+ entry.operation.recordId,
3057
+ tx,
3058
+ [entry.operation.id]
3059
+ );
3060
+ for (const cascadedOp of cascadeResult.operations) {
3061
+ operations.push(cascadedOp);
3062
+ affectedCollections.add(cascadedOp.collection);
3063
+ this.config.causalTracker?.afterOperation(cascadedOp.collection, cascadedOp.id, true);
3064
+ }
3065
+ }
1862
3066
  for (const cmd of entry.commands) {
1863
3067
  await tx.execute(cmd.sql, cmd.params);
1864
3068
  }
1865
3069
  operations.push(entry.operation);
1866
3070
  affectedCollections.add(entry.collection);
1867
3071
  }
3072
+ const finalSeq = this.config.sequenceAllocator.getHighWaterMark();
3073
+ if (finalSeq > 0) {
3074
+ await tx.execute(
3075
+ "INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
3076
+ [this.config.nodeId, finalSeq]
3077
+ );
3078
+ }
1868
3079
  });
1869
3080
  return { operations, affectedCollections };
1870
3081
  }
@@ -1891,16 +3102,17 @@ var TransactionContext = class {
1891
3102
  }
1892
3103
  async insert(collectionName, definition, data) {
1893
3104
  this.ensureActive();
1894
- const validated = (0, import_core7.validateRecord)(collectionName, definition, data, "insert");
1895
- const recordId = (0, import_core7.generateUUIDv7)();
3105
+ const validated = (0, import_core14.validateRecord)(collectionName, definition, data, "insert");
3106
+ const recordId = (0, import_core14.generateUUIDv7)();
1896
3107
  const now = Date.now();
1897
3108
  for (const [fieldName, descriptor] of Object.entries(definition.fields)) {
1898
3109
  if (descriptor.auto && descriptor.kind === "timestamp") {
1899
3110
  validated[fieldName] = now;
1900
3111
  }
1901
3112
  }
1902
- const sequenceNumber = this.config.getSequenceNumber();
1903
- const operation = await (0, import_core7.createOperation)(
3113
+ const sequenceNumber = await this.config.sequenceAllocator.allocate();
3114
+ const causalDeps = this.config.causalTracker?.nextCausalDeps(collectionName, true) ?? [];
3115
+ const operation = await (0, import_core14.createOperation)(
1904
3116
  {
1905
3117
  nodeId: this.config.nodeId,
1906
3118
  type: "insert",
@@ -1909,19 +3121,22 @@ var TransactionContext = class {
1909
3121
  data: { ...validated },
1910
3122
  previousData: null,
1911
3123
  sequenceNumber,
1912
- causalDeps: [],
3124
+ causalDeps,
1913
3125
  schemaVersion: this.config.schema.version,
1914
3126
  transactionId: this.transactionId,
1915
3127
  ...this.mutationName !== void 0 ? { mutationName: this.mutationName } : {}
1916
3128
  },
1917
3129
  this.config.clock
1918
3130
  );
3131
+ this.config.causalTracker?.afterOperation(collectionName, operation.id, true);
1919
3132
  const serializedData = serializeRecord(validated, definition.fields);
3133
+ const version = serializeRowVersion(operation.timestamp);
1920
3134
  const record = {
1921
3135
  id: recordId,
1922
3136
  ...serializedData,
1923
- _created_at: now,
1924
- _updated_at: now
3137
+ _created_at: operation.timestamp.wallTime,
3138
+ _updated_at: operation.timestamp.wallTime,
3139
+ _version: version
1925
3140
  };
1926
3141
  const insertQuery = buildInsertQuery(collectionName, record);
1927
3142
  const opRow = serializeOperation(operation);
@@ -1934,11 +3149,7 @@ var TransactionContext = class {
1934
3149
  collection: collectionName,
1935
3150
  commands: [
1936
3151
  { sql: insertQuery.sql, params: insertQuery.params },
1937
- { sql: opInsert.sql, params: opInsert.params },
1938
- {
1939
- sql: "INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
1940
- params: [this.config.nodeId, sequenceNumber]
1941
- }
3152
+ { sql: opInsert.sql, params: opInsert.params }
1942
3153
  ]
1943
3154
  });
1944
3155
  return {
@@ -1954,7 +3165,7 @@ var TransactionContext = class {
1954
3165
  if (!currentRecord) {
1955
3166
  throw new RecordNotFoundError(collectionName, id);
1956
3167
  }
1957
- const validated = (0, import_core7.validateRecord)(collectionName, definition, data, "update");
3168
+ const validated = (0, import_core14.validateRecord)(collectionName, definition, data, "update");
1958
3169
  const now = Date.now();
1959
3170
  const previousData = {};
1960
3171
  const resolvedData = {};
@@ -1962,16 +3173,17 @@ var TransactionContext = class {
1962
3173
  for (const key of Object.keys(validated)) {
1963
3174
  const value = validated[key];
1964
3175
  previousData[key] = currentRecord[key];
1965
- if ((0, import_core7.isAtomicOp)(value)) {
1966
- resolvedData[key] = (0, import_core7.resolveAtomicOp)(currentRecord[key], value);
1967
- atomicOps[key] = (0, import_core7.toAtomicOp)(value);
3176
+ if ((0, import_core14.isAtomicOp)(value)) {
3177
+ resolvedData[key] = (0, import_core14.resolveAtomicOp)(currentRecord[key], value);
3178
+ atomicOps[key] = (0, import_core14.toAtomicOp)(value);
1968
3179
  } else {
1969
3180
  resolvedData[key] = value;
1970
3181
  }
1971
3182
  }
1972
3183
  const hasAtomicOps = Object.keys(atomicOps).length > 0;
1973
- const sequenceNumber = this.config.getSequenceNumber();
1974
- const operation = await (0, import_core7.createOperation)(
3184
+ const sequenceNumber = await this.config.sequenceAllocator.allocate();
3185
+ const causalDeps = this.config.causalTracker?.nextCausalDeps(collectionName, true) ?? [];
3186
+ const operation = await (0, import_core14.createOperation)(
1975
3187
  {
1976
3188
  nodeId: this.config.nodeId,
1977
3189
  type: "update",
@@ -1980,7 +3192,7 @@ var TransactionContext = class {
1980
3192
  data: { ...resolvedData },
1981
3193
  previousData,
1982
3194
  sequenceNumber,
1983
- causalDeps: [],
3195
+ causalDeps,
1984
3196
  schemaVersion: this.config.schema.version,
1985
3197
  transactionId: this.transactionId,
1986
3198
  ...this.mutationName !== void 0 ? { mutationName: this.mutationName } : {},
@@ -1988,10 +3200,13 @@ var TransactionContext = class {
1988
3200
  },
1989
3201
  this.config.clock
1990
3202
  );
3203
+ this.config.causalTracker?.afterOperation(collectionName, operation.id, true);
1991
3204
  const serializedChanges = serializeRecord(resolvedData, definition.fields);
3205
+ const version = serializeRowVersion(operation.timestamp);
1992
3206
  const updateQuery = buildUpdateQuery(collectionName, id, {
1993
3207
  ...serializedChanges,
1994
- _updated_at: now
3208
+ _updated_at: operation.timestamp.wallTime,
3209
+ _version: version
1995
3210
  });
1996
3211
  const opRow = serializeOperation(operation);
1997
3212
  const opInsert = buildInsertQuery(
@@ -2003,11 +3218,7 @@ var TransactionContext = class {
2003
3218
  collection: collectionName,
2004
3219
  commands: [
2005
3220
  { sql: updateQuery.sql, params: updateQuery.params },
2006
- { sql: opInsert.sql, params: opInsert.params },
2007
- {
2008
- sql: "INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
2009
- params: [this.config.nodeId, sequenceNumber]
2010
- }
3221
+ { sql: opInsert.sql, params: opInsert.params }
2011
3222
  ]
2012
3223
  });
2013
3224
  return {
@@ -2023,8 +3234,9 @@ var TransactionContext = class {
2023
3234
  throw new RecordNotFoundError(collectionName, id);
2024
3235
  }
2025
3236
  const now = Date.now();
2026
- const sequenceNumber = this.config.getSequenceNumber();
2027
- const operation = await (0, import_core7.createOperation)(
3237
+ const sequenceNumber = await this.config.sequenceAllocator.allocate();
3238
+ const causalDeps = this.config.causalTracker?.nextCausalDeps(collectionName, true) ?? [];
3239
+ const operation = await (0, import_core14.createOperation)(
2028
3240
  {
2029
3241
  nodeId: this.config.nodeId,
2030
3242
  type: "delete",
@@ -2033,13 +3245,14 @@ var TransactionContext = class {
2033
3245
  data: null,
2034
3246
  previousData: null,
2035
3247
  sequenceNumber,
2036
- causalDeps: [],
3248
+ causalDeps,
2037
3249
  schemaVersion: this.config.schema.version,
2038
3250
  transactionId: this.transactionId,
2039
3251
  ...this.mutationName !== void 0 ? { mutationName: this.mutationName } : {}
2040
3252
  },
2041
3253
  this.config.clock
2042
3254
  );
3255
+ this.config.causalTracker?.afterOperation(collectionName, operation.id, true);
2043
3256
  const deleteQuery = buildSoftDeleteQuery(collectionName, id, now);
2044
3257
  const opRow = serializeOperation(operation);
2045
3258
  const opInsert = buildInsertQuery(
@@ -2051,11 +3264,7 @@ var TransactionContext = class {
2051
3264
  collection: collectionName,
2052
3265
  commands: [
2053
3266
  { sql: deleteQuery.sql, params: deleteQuery.params },
2054
- { sql: opInsert.sql, params: opInsert.params },
2055
- {
2056
- sql: "INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
2057
- params: [this.config.nodeId, sequenceNumber]
2058
- }
3267
+ { sql: opInsert.sql, params: opInsert.params }
2059
3268
  ]
2060
3269
  });
2061
3270
  }
@@ -2106,25 +3315,86 @@ var TransactionContext = class {
2106
3315
  }
2107
3316
  };
2108
3317
 
3318
+ // src/transaction/transaction-sequence.ts
3319
+ var TransactionSequenceAllocator = class {
3320
+ constructor(adapter, nodeId) {
3321
+ this.adapter = adapter;
3322
+ this.nodeId = nodeId;
3323
+ }
3324
+ adapter;
3325
+ nodeId;
3326
+ loaded = false;
3327
+ watermark = 0;
3328
+ async allocate() {
3329
+ if (!this.loaded) {
3330
+ this.watermark = await readSequenceNumber(this.adapter, this.nodeId);
3331
+ this.loaded = true;
3332
+ }
3333
+ this.watermark++;
3334
+ return this.watermark;
3335
+ }
3336
+ getHighWaterMark() {
3337
+ return this.watermark;
3338
+ }
3339
+ };
3340
+
3341
+ // src/store/tab-node-id.ts
3342
+ var import_core15 = require("@korajs/core");
3343
+ function perTabStorageKey(dbName) {
3344
+ return `kora-tab-node-${dbName}`;
3345
+ }
3346
+ function loadPerTabNodeId(dbName) {
3347
+ if (typeof sessionStorage === "undefined") {
3348
+ return null;
3349
+ }
3350
+ return sessionStorage.getItem(perTabStorageKey(dbName));
3351
+ }
3352
+ function savePerTabNodeId(dbName, nodeId) {
3353
+ if (typeof sessionStorage === "undefined") {
3354
+ return;
3355
+ }
3356
+ sessionStorage.setItem(perTabStorageKey(dbName), nodeId);
3357
+ }
3358
+ function resolvePerTabNodeId(dbName) {
3359
+ const existing = loadPerTabNodeId(dbName);
3360
+ if (existing) {
3361
+ return existing;
3362
+ }
3363
+ const nodeId = (0, import_core15.generateUUIDv7)();
3364
+ savePerTabNodeId(dbName, nodeId);
3365
+ return nodeId;
3366
+ }
3367
+
2109
3368
  // src/store/store.ts
2110
3369
  var Store = class {
2111
3370
  opened = false;
2112
3371
  nodeId = "";
2113
3372
  sequenceNumber = 0;
2114
- versionVector = (0, import_core8.createVersionVector)();
3373
+ versionVector = (0, import_core17.createVersionVector)();
2115
3374
  clock = null;
2116
3375
  collections = /* @__PURE__ */ new Map();
2117
- subscriptionManager = new SubscriptionManager();
3376
+ subscriptionManager;
2118
3377
  sequenceManager = null;
2119
3378
  schema;
2120
3379
  adapter;
2121
3380
  configNodeId;
3381
+ dbName;
3382
+ isolation;
2122
3383
  emitter;
3384
+ localMutationHandler;
3385
+ relationEnforcer = null;
3386
+ causalTracker = null;
2123
3387
  constructor(config) {
2124
3388
  this.schema = config.schema;
2125
3389
  this.adapter = config.adapter;
2126
3390
  this.configNodeId = config.nodeId;
3391
+ this.dbName = config.dbName ?? "kora-db";
3392
+ this.isolation = config.isolation ?? "shared";
2127
3393
  this.emitter = config.emitter ?? null;
3394
+ this.localMutationHandler = config.localMutationHandler ?? null;
3395
+ this.subscriptionManager = new SubscriptionManager({
3396
+ onQuerySubscribed: config.onQuerySubscribed
3397
+ });
2128
3398
  }
2129
3399
  /**
2130
3400
  * Open the store: initialize the database, load or generate a node ID,
@@ -2134,18 +3404,18 @@ var Store = class {
2134
3404
  await this.adapter.open(this.schema);
2135
3405
  await this.runMigrationsIfNeeded();
2136
3406
  this.nodeId = await this.loadOrGenerateNodeId();
2137
- this.clock = new import_core8.HybridLogicalClock(this.nodeId);
3407
+ this.clock = new import_core17.HybridLogicalClock(this.nodeId);
3408
+ this.causalTracker = new import_core17.CausalTracker();
2138
3409
  this.sequenceManager = new SequenceManager(this.adapter, this.nodeId);
2139
3410
  this.sequenceNumber = await this.loadSequenceNumber();
2140
3411
  this.versionVector = await this.loadVersionVector();
2141
3412
  const hasRelations = Object.keys(this.schema.relations).length > 0;
2142
- const relationEnforcer = hasRelations ? new RelationEnforcer({
3413
+ this.relationEnforcer = hasRelations ? new RelationEnforcer({
2143
3414
  schema: this.schema,
2144
3415
  adapter: this.adapter,
2145
3416
  clock: this.clock,
2146
- nodeId: this.nodeId,
2147
- getSequenceNumber: () => this.nextSequenceNumber()
2148
- }) : void 0;
3417
+ nodeId: this.nodeId
3418
+ }) : null;
2149
3419
  for (const [name, definition] of Object.entries(this.schema.collections)) {
2150
3420
  const col = new Collection(
2151
3421
  name,
@@ -2154,14 +3424,17 @@ var Store = class {
2154
3424
  this.adapter,
2155
3425
  this.clock,
2156
3426
  this.nodeId,
2157
- () => this.nextSequenceNumber(),
3427
+ () => this.allocateSequenceNumber(),
2158
3428
  (collectionName, operation) => {
3429
+ this.recordOperationSequence(operation);
2159
3430
  this.subscriptionManager.notify(collectionName, operation);
2160
3431
  if (this.emitter) {
2161
3432
  this.emitter.emit({ type: "operation:created", operation });
2162
3433
  }
2163
3434
  },
2164
- relationEnforcer
3435
+ this.relationEnforcer,
3436
+ this.localMutationHandler,
3437
+ this.causalTracker
2165
3438
  );
2166
3439
  this.collections.set(name, col);
2167
3440
  }
@@ -2209,112 +3482,358 @@ var Store = class {
2209
3482
  };
2210
3483
  }
2211
3484
  /**
2212
- * Get the current version vector.
3485
+ * Get the current version vector.
3486
+ */
3487
+ getVersionVector() {
3488
+ this.ensureOpen();
3489
+ return new Map(this.versionVector);
3490
+ }
3491
+ /**
3492
+ * Get the node ID for this store instance.
3493
+ */
3494
+ getNodeId() {
3495
+ this.ensureOpen();
3496
+ return this.nodeId;
3497
+ }
3498
+ /**
3499
+ * Apply a remote operation received from sync.
3500
+ * Checks for duplicates, applies to the data table, persists the operation,
3501
+ * and updates the version vector.
3502
+ */
3503
+ async applyRemoteOperation(op, options) {
3504
+ this.ensureOpen();
3505
+ const collection = op.collection;
3506
+ const definition = this.schema.collections[collection];
3507
+ if (!definition) {
3508
+ return "skipped";
3509
+ }
3510
+ const existing = await this.adapter.query(
3511
+ `SELECT id FROM _kora_ops_${collection} WHERE id = ?`,
3512
+ [op.id]
3513
+ );
3514
+ if (existing.length > 0) {
3515
+ return "duplicate";
3516
+ }
3517
+ if (this.clock) {
3518
+ this.clock.receive(op.timestamp);
3519
+ }
3520
+ const remoteVersion = serializeRowVersion(op.timestamp);
3521
+ const wallTime = op.timestamp.wallTime;
3522
+ await this.adapter.transaction(async (tx) => {
3523
+ if (op.type === "insert" && op.data) {
3524
+ const existing2 = await tx.query(
3525
+ `SELECT _updated_at, _version FROM ${collection} WHERE id = ?`,
3526
+ [op.recordId]
3527
+ );
3528
+ const row = existing2[0];
3529
+ if (row && !isIncomingNewerThanRow(op.timestamp, row)) {
3530
+ } else {
3531
+ const serializedData = serializeRecord(op.data, definition.fields);
3532
+ const record = {
3533
+ id: op.recordId,
3534
+ ...serializedData,
3535
+ _created_at: wallTime,
3536
+ _updated_at: wallTime,
3537
+ _version: remoteVersion
3538
+ };
3539
+ const insertQuery = buildInsertQuery(collection, record);
3540
+ await tx.execute(insertQuery.sql, insertQuery.params);
3541
+ }
3542
+ } else if (op.type === "update" && op.data) {
3543
+ const serializedChanges = serializeRecord(op.data, definition.fields);
3544
+ const updatePayload = {
3545
+ ...serializedChanges,
3546
+ _updated_at: wallTime,
3547
+ _version: remoteVersion
3548
+ };
3549
+ if (options?.reactivateIfDeleted) {
3550
+ updatePayload._deleted = 0;
3551
+ }
3552
+ const updateQuery = buildLwwUpdateQuery(
3553
+ collection,
3554
+ op.recordId,
3555
+ updatePayload,
3556
+ remoteVersion
3557
+ );
3558
+ await tx.execute(updateQuery.sql, updateQuery.params);
3559
+ } else if (op.type === "delete") {
3560
+ const deleteQuery = buildLwwSoftDeleteQuery(
3561
+ collection,
3562
+ op.recordId,
3563
+ wallTime,
3564
+ remoteVersion
3565
+ );
3566
+ await tx.execute(deleteQuery.sql, deleteQuery.params);
3567
+ }
3568
+ const opRow = serializeOperation(op);
3569
+ const opInsert = buildInsertQuery(
3570
+ `_kora_ops_${collection}`,
3571
+ opRow
3572
+ );
3573
+ await tx.execute(opInsert.sql, opInsert.params);
3574
+ const currentSeq = this.versionVector.get(op.nodeId) ?? 0;
3575
+ if (op.sequenceNumber > currentSeq) {
3576
+ this.versionVector.set(op.nodeId, op.sequenceNumber);
3577
+ await tx.execute(
3578
+ "INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
3579
+ [op.nodeId, op.sequenceNumber]
3580
+ );
3581
+ }
3582
+ });
3583
+ this.subscriptionManager.notify(collection, op);
3584
+ return "applied";
3585
+ }
3586
+ /**
3587
+ * Get operations from a node within a sequence number range.
3588
+ * Implements the OperationLog interface for computeDelta.
3589
+ */
3590
+ async getRange(nodeId, fromSeq, toSeq) {
3591
+ return this.getOperationRange(nodeId, fromSeq, toSeq);
3592
+ }
3593
+ /**
3594
+ * Get operations from a node within a sequence number range.
3595
+ */
3596
+ async getOperationRange(nodeId, fromSeq, toSeq) {
3597
+ this.ensureOpen();
3598
+ const allOps = [];
3599
+ for (const collectionName of Object.keys(this.schema.collections)) {
3600
+ const rows = await this.adapter.query(
3601
+ `SELECT * FROM _kora_ops_${collectionName} WHERE node_id = ? AND sequence_number >= ? AND sequence_number <= ? ORDER BY sequence_number ASC`,
3602
+ [nodeId, fromSeq, toSeq]
3603
+ );
3604
+ for (const row of rows) {
3605
+ allOps.push(deserializeOperationWithCollection(row, collectionName));
3606
+ }
3607
+ }
3608
+ allOps.sort((a, b) => a.sequenceNumber - b.sequenceNumber);
3609
+ return allOps;
3610
+ }
3611
+ /**
3612
+ * Load every operation from the local append-only log across all collections.
3613
+ * Used by sync delta computation, backup export, and time-travel replay.
3614
+ */
3615
+ async getAllOperations() {
3616
+ this.ensureOpen();
3617
+ const allOps = [];
3618
+ for (const collectionName of Object.keys(this.schema.collections)) {
3619
+ const rows = await this.adapter.query(
3620
+ `SELECT * FROM _kora_ops_${collectionName} ORDER BY sequence_number ASC`
3621
+ );
3622
+ for (const row of rows) {
3623
+ allOps.push(deserializeOperationWithCollection(row, collectionName));
3624
+ }
3625
+ }
3626
+ return allOps;
3627
+ }
3628
+ /**
3629
+ * Rebuild an in-memory snapshot of materialized state at a causal cut in the op log.
3630
+ * Does not mutate the live store — intended for DevTools time-travel inspection.
3631
+ *
3632
+ * @param operationId - Content-addressed id of the operation to replay through (inclusive)
3633
+ * @throws {OperationError} When the operation id is not present in the local log
3634
+ */
3635
+ async replayTo(operationId) {
3636
+ this.ensureOpen();
3637
+ const start = Date.now();
3638
+ const allOps = await this.getAllOperations();
3639
+ const snapshot = buildReplaySnapshot(this.schema, allOps, operationId);
3640
+ if (this.emitter) {
3641
+ this.emitter.emit({
3642
+ type: "replay:completed",
3643
+ targetOperationId: operationId,
3644
+ operationsApplied: snapshot.operationsApplied.length,
3645
+ duration: Date.now() - start
3646
+ });
3647
+ }
3648
+ return snapshot;
3649
+ }
3650
+ /**
3651
+ * Persist a merge trace to the durable audit log.
3652
+ */
3653
+ async appendAuditTrace(trace) {
3654
+ this.ensureOpen();
3655
+ const { appendAuditTrace: append } = await Promise.resolve().then(() => (init_audit_trace_store(), audit_trace_store_exports));
3656
+ await append(this.adapter, trace);
3657
+ }
3658
+ /**
3659
+ * Read persisted audit traces with optional filters.
3660
+ */
3661
+ async getAuditTraces(query) {
3662
+ this.ensureOpen();
3663
+ const { readAuditTraces: readAuditTraces2 } = await Promise.resolve().then(() => (init_audit_trace_store(), audit_trace_store_exports));
3664
+ return readAuditTraces2(this.adapter, query);
3665
+ }
3666
+ /**
3667
+ * Export operations and merge traces as a portable audit bundle.
3668
+ */
3669
+ async exportAudit(options) {
3670
+ this.ensureOpen();
3671
+ const { exportAudit: doExport } = await Promise.resolve().then(() => (init_export_audit(), export_audit_exports));
3672
+ return doExport(this.adapter, this.schema, this.nodeId, this.schema.version, options);
3673
+ }
3674
+ /**
3675
+ * Get the schema definition.
3676
+ */
3677
+ getSchema() {
3678
+ return this.schema;
3679
+ }
3680
+ /**
3681
+ * Route local CRUD through the unified apply pipeline (korajs ApplyPipeline).
3682
+ */
3683
+ setLocalMutationHandler(handler) {
3684
+ this.localMutationHandler = handler;
3685
+ for (const col of this.collections.values()) {
3686
+ col.setMutationHandler(handler);
3687
+ }
3688
+ }
3689
+ /**
3690
+ * Build mutation context for a collection (used by ApplyPipeline side effects).
3691
+ */
3692
+ createMutationContext(collection, options) {
3693
+ this.ensureOpen();
3694
+ const definition = this.schema.collections[collection];
3695
+ if (!definition || !this.clock) {
3696
+ throw new StoreNotOpenError();
3697
+ }
3698
+ return {
3699
+ collection,
3700
+ definition,
3701
+ schema: this.schema,
3702
+ adapter: this.adapter,
3703
+ clock: this.clock,
3704
+ nodeId: this.nodeId,
3705
+ allocateSequenceNumber: () => this.allocateSequenceNumber(),
3706
+ onMutation: (collectionName, operation) => {
3707
+ this.recordOperationSequence(operation);
3708
+ this.subscriptionManager.notify(collectionName, operation);
3709
+ if (this.emitter) {
3710
+ this.emitter.emit({ type: "operation:created", operation });
3711
+ }
3712
+ },
3713
+ relationEnforcer: this.relationEnforcer,
3714
+ causalTracker: this.causalTracker,
3715
+ inTransaction: options?.inTransaction ?? false,
3716
+ extraCausalDeps: options?.extraCausalDeps
3717
+ };
3718
+ }
3719
+ /**
3720
+ * Load a materialized row by ID, including soft-deleted tombstones.
3721
+ */
3722
+ async findMaterializedRow(collection, recordId) {
3723
+ this.ensureOpen();
3724
+ const definition = this.schema.collections[collection];
3725
+ if (!definition) {
3726
+ return null;
3727
+ }
3728
+ const rows = await this.adapter.query(
3729
+ `SELECT * FROM ${collection} WHERE id = ?`,
3730
+ [recordId]
3731
+ );
3732
+ const row = rows[0];
3733
+ if (!row) {
3734
+ return null;
3735
+ }
3736
+ return {
3737
+ record: deserializeRecord(row, definition.fields),
3738
+ deleted: row._deleted === 1
3739
+ };
3740
+ }
3741
+ /**
3742
+ * Latest operation from this device for a record (used for delete-vs-update merge on sync).
2213
3743
  */
2214
- getVersionVector() {
3744
+ /**
3745
+ * Load the last server version vector acknowledged by this client (persisted in `_kora_meta`).
3746
+ */
3747
+ async loadLastAckedServerVector() {
2215
3748
  this.ensureOpen();
2216
- return new Map(this.versionVector);
3749
+ return loadLastAckedServerVector(this.adapter);
2217
3750
  }
2218
3751
  /**
2219
- * Get the node ID for this store instance.
3752
+ * Persist the last server version vector this client believes the server has applied.
2220
3753
  */
2221
- getNodeId() {
3754
+ async saveLastAckedServerVector(vector) {
2222
3755
  this.ensureOpen();
2223
- return this.nodeId;
3756
+ await saveLastAckedServerVector(this.adapter, vector);
2224
3757
  }
2225
3758
  /**
2226
- * Apply a remote operation received from sync.
2227
- * Checks for duplicates, applies to the data table, persists the operation,
2228
- * and updates the version vector.
3759
+ * Load persisted delta cursor for resuming paginated initial sync.
2229
3760
  */
2230
- async applyRemoteOperation(op) {
3761
+ async loadDeltaCursor() {
2231
3762
  this.ensureOpen();
2232
- const collection = op.collection;
2233
- const definition = this.schema.collections[collection];
2234
- if (!definition) {
2235
- return "skipped";
2236
- }
2237
- const existing = await this.adapter.query(
2238
- `SELECT id FROM _kora_ops_${collection} WHERE id = ?`,
2239
- [op.id]
3763
+ return loadDeltaCursor(this.adapter);
3764
+ }
3765
+ /**
3766
+ * Persist or clear the delta cursor for paginated initial sync resume.
3767
+ */
3768
+ async saveDeltaCursor(cursor) {
3769
+ this.ensureOpen();
3770
+ await saveDeltaCursor(this.adapter, cursor);
3771
+ }
3772
+ /**
3773
+ * Local operations not yet reflected on the server version vector.
3774
+ */
3775
+ async getUnsyncedOperations(serverVector) {
3776
+ this.ensureOpen();
3777
+ return collectOperationsAheadOfServer(
3778
+ this.getVersionVector(),
3779
+ serverVector,
3780
+ (nodeId, fromSeq, toSeq) => this.getOperationRange(nodeId, fromSeq, toSeq)
2240
3781
  );
2241
- if (existing.length > 0) {
2242
- return "duplicate";
2243
- }
2244
- if (this.clock) {
2245
- this.clock.receive(op.timestamp);
2246
- }
2247
- await this.adapter.transaction(async (tx) => {
2248
- if (op.type === "insert" && op.data) {
2249
- const serializedData = serializeRecord(op.data, definition.fields);
2250
- const now = op.timestamp.wallTime;
2251
- const record = {
2252
- id: op.recordId,
2253
- ...serializedData,
2254
- _created_at: now,
2255
- _updated_at: now
2256
- };
2257
- const insertQuery = buildInsertQuery(collection, record);
2258
- await tx.execute(insertQuery.sql, insertQuery.params);
2259
- } else if (op.type === "update" && op.data) {
2260
- const serializedChanges = serializeRecord(op.data, definition.fields);
2261
- const updateQuery = buildUpdateQuery(collection, op.recordId, {
2262
- ...serializedChanges,
2263
- _updated_at: op.timestamp.wallTime
2264
- });
2265
- await tx.execute(updateQuery.sql, updateQuery.params);
2266
- } else if (op.type === "delete") {
2267
- const deleteQuery = buildSoftDeleteQuery(collection, op.recordId, op.timestamp.wallTime);
2268
- await tx.execute(deleteQuery.sql, deleteQuery.params);
2269
- }
2270
- const opRow = serializeOperation(op);
2271
- const opInsert = buildInsertQuery(
2272
- `_kora_ops_${collection}`,
2273
- opRow
2274
- );
2275
- await tx.execute(opInsert.sql, opInsert.params);
2276
- const currentSeq = this.versionVector.get(op.nodeId) ?? 0;
2277
- if (op.sequenceNumber > currentSeq) {
2278
- this.versionVector.set(op.nodeId, op.sequenceNumber);
2279
- await tx.execute(
2280
- "INSERT OR REPLACE INTO _kora_version_vector (node_id, sequence_number) VALUES (?, ?)",
2281
- [op.nodeId, op.sequenceNumber]
2282
- );
2283
- }
2284
- });
2285
- this.subscriptionManager.notify(collection, op);
2286
- return "applied";
2287
3782
  }
2288
3783
  /**
2289
- * Get operations from a node within a sequence number range.
2290
- * Implements the OperationLog interface for computeDelta.
3784
+ * Count of local operations ahead of the server version vector.
2291
3785
  */
2292
- getRange(nodeId, fromSeq, toSeq) {
2293
- return [];
3786
+ async countUnsyncedOperations(serverVector) {
3787
+ const ops = await this.getUnsyncedOperations(serverVector);
3788
+ return ops.length;
2294
3789
  }
2295
3790
  /**
2296
- * Async version of getRange for use by the sync layer.
3791
+ * Compact the local operation log using materialized rows as the baseline.
3792
+ * Only removes ops the server has acknowledged (per {@link CompactionStrategy}).
2297
3793
  */
2298
- async getOperationRange(nodeId, fromSeq, toSeq) {
3794
+ async compact(strategy) {
2299
3795
  this.ensureOpen();
2300
- const allOps = [];
2301
- for (const collectionName of Object.keys(this.schema.collections)) {
2302
- const rows = await this.adapter.query(
2303
- `SELECT * FROM _kora_ops_${collectionName} WHERE node_id = ? AND sequence_number >= ? AND sequence_number <= ? ORDER BY sequence_number ASC`,
2304
- [nodeId, fromSeq, toSeq]
2305
- );
2306
- for (const row of rows) {
2307
- allOps.push(deserializeOperationWithCollection(row, collectionName));
2308
- }
3796
+ if (strategy.mode === "never") {
3797
+ return compactOperationLog(this.adapter, this.schema, strategy, (0, import_core17.createVersionVector)());
2309
3798
  }
2310
- allOps.sort((a, b) => a.sequenceNumber - b.sequenceNumber);
2311
- return allOps;
3799
+ const serverVector = strategy.serverVector ?? await loadLastAckedServerVector(this.adapter);
3800
+ return compactOperationLog(this.adapter, this.schema, strategy, serverVector);
2312
3801
  }
2313
3802
  /**
2314
- * Get the schema definition.
3803
+ * Merge session remote vector with persisted last-acked vector (max per node).
2315
3804
  */
2316
- getSchema() {
2317
- return this.schema;
3805
+ mergeServerVectors(sessionVector, persistedVector) {
3806
+ return mergeVersionVectors(persistedVector, sessionVector);
3807
+ }
3808
+ async getLatestLocalOperationForRecord(collection, recordId) {
3809
+ this.ensureOpen();
3810
+ const rows = await this.adapter.query(
3811
+ `SELECT * FROM _kora_ops_${collection} WHERE node_id = ? AND record_id = ? ORDER BY sequence_number DESC LIMIT 1`,
3812
+ [this.nodeId, recordId]
3813
+ );
3814
+ const row = rows[0];
3815
+ if (!row) {
3816
+ return null;
3817
+ }
3818
+ return deserializeOperationWithCollection(row, collection);
3819
+ }
3820
+ /**
3821
+ * Latest operation for a record from any node (for 3-way merge when local op log is empty).
3822
+ */
3823
+ async getLatestOperationForRecord(collection, recordId) {
3824
+ this.ensureOpen();
3825
+ const rows = await this.adapter.query(
3826
+ `SELECT * FROM _kora_ops_${collection} WHERE record_id = ?`,
3827
+ [recordId]
3828
+ );
3829
+ let latest = null;
3830
+ for (const row of rows) {
3831
+ const op = deserializeOperationWithCollection(row, collection);
3832
+ if (!latest || import_core17.HybridLogicalClock.compare(op.timestamp, latest.timestamp) > 0) {
3833
+ latest = op;
3834
+ }
3835
+ }
3836
+ return latest;
2318
3837
  }
2319
3838
  /** Expose the subscription manager for direct access (e.g., by QueryBuilder) */
2320
3839
  getSubscriptionManager() {
@@ -2348,7 +3867,10 @@ var Store = class {
2348
3867
  adapter: this.adapter,
2349
3868
  clock: this.clock,
2350
3869
  nodeId: this.nodeId,
2351
- getSequenceNumber: () => this.nextSequenceNumber()
3870
+ sequenceAllocator: new TransactionSequenceAllocator(this.adapter, this.nodeId),
3871
+ relationEnforcer: this.relationEnforcer,
3872
+ causalTracker: this.causalTracker,
3873
+ localMutationHandler: this.localMutationHandler
2352
3874
  });
2353
3875
  }
2354
3876
  /**
@@ -2363,10 +3885,13 @@ var Store = class {
2363
3885
  */
2364
3886
  async transaction(fn) {
2365
3887
  const tx = this.createTransaction();
3888
+ this.causalTracker?.beginTransaction();
2366
3889
  try {
2367
3890
  await fn(tx);
2368
- const { operations, affectedCollections } = await tx.commit();
3891
+ const { operations } = await tx.commit();
3892
+ this.causalTracker?.clearTransaction();
2369
3893
  for (const op of operations) {
3894
+ this.recordOperationSequence(op);
2370
3895
  this.subscriptionManager.notify(op.collection, op);
2371
3896
  if (this.emitter) {
2372
3897
  this.emitter.emit({ type: "operation:created", operation: op });
@@ -2375,13 +3900,57 @@ var Store = class {
2375
3900
  return operations;
2376
3901
  } catch (error) {
2377
3902
  tx.rollback();
3903
+ this.causalTracker?.clearTransaction();
2378
3904
  throw error;
2379
3905
  }
2380
3906
  }
2381
- nextSequenceNumber() {
2382
- this.sequenceNumber++;
2383
- this.versionVector.set(this.nodeId, this.sequenceNumber);
2384
- return this.sequenceNumber;
3907
+ /**
3908
+ * Export all data as a portable backup binary.
3909
+ * Includes operations, version vector, metadata, and optionally materialized records.
3910
+ *
3911
+ * @param options - Backup options (includeRecords, collections, onProgress)
3912
+ * @returns Backup as a Uint8Array
3913
+ */
3914
+ async exportBackup(options) {
3915
+ this.ensureOpen();
3916
+ const { exportBackup: doExport } = await Promise.resolve().then(() => (init_backup(), backup_exports));
3917
+ return doExport(this.adapter, this.schema, this.nodeId, this.schema.version, options);
3918
+ }
3919
+ /**
3920
+ * Restore data from a backup binary.
3921
+ *
3922
+ * @param data - The backup data
3923
+ * @param options - Restore options (merge, collections, onProgress)
3924
+ * @returns Result of the restore operation
3925
+ */
3926
+ async importBackup(data, options) {
3927
+ this.ensureOpen();
3928
+ const { restoreBackup: doRestore } = await Promise.resolve().then(() => (init_backup(), backup_exports));
3929
+ return doRestore(this.adapter, this.schema, data, options);
3930
+ }
3931
+ /**
3932
+ * Read backup manifest without loading the entire backup.
3933
+ *
3934
+ * @param data - The raw backup data
3935
+ * @returns The backup manifest
3936
+ */
3937
+ static readBackupManifest(data) {
3938
+ return readBackupManifest(data);
3939
+ }
3940
+ recordOperationSequence(operation) {
3941
+ const prev = this.versionVector.get(operation.nodeId) ?? 0;
3942
+ if (operation.sequenceNumber > prev) {
3943
+ this.versionVector.set(operation.nodeId, operation.sequenceNumber);
3944
+ }
3945
+ if (operation.nodeId === this.nodeId) {
3946
+ this.sequenceNumber = Math.max(this.sequenceNumber, operation.sequenceNumber);
3947
+ }
3948
+ }
3949
+ async allocateSequenceNumber() {
3950
+ const seq = await allocateNextSequenceNumber(this.adapter, this.nodeId);
3951
+ this.sequenceNumber = seq;
3952
+ this.versionVector.set(this.nodeId, seq);
3953
+ return seq;
2385
3954
  }
2386
3955
  /**
2387
3956
  * Check the stored schema version and run any pending migrations.
@@ -2403,7 +3972,7 @@ var Store = class {
2403
3972
  for (let v = storedVersion + 1; v <= targetVersion; v++) {
2404
3973
  const migration = migrations[v];
2405
3974
  if (!migration) continue;
2406
- const sqlStatements = (0, import_core8.migrationStepsToSQL)(migration.steps);
3975
+ const sqlStatements = (0, import_core17.migrationStepsToSQL)(migration.steps);
2407
3976
  for (const sql of sqlStatements) {
2408
3977
  try {
2409
3978
  await this.adapter.execute(sql);
@@ -2464,19 +4033,24 @@ var Store = class {
2464
4033
  }
2465
4034
  async loadOrGenerateNodeId() {
2466
4035
  if (this.configNodeId) {
2467
- await this.adapter.execute(
2468
- "INSERT OR REPLACE INTO _kora_meta (key, value) VALUES ('node_id', ?)",
2469
- [this.configNodeId]
2470
- );
4036
+ if (this.isolation !== "per-tab") {
4037
+ await this.adapter.execute(
4038
+ "INSERT OR REPLACE INTO _kora_meta (key, value) VALUES ('node_id', ?)",
4039
+ [this.configNodeId]
4040
+ );
4041
+ }
2471
4042
  return this.configNodeId;
2472
4043
  }
4044
+ if (this.isolation === "per-tab") {
4045
+ return resolvePerTabNodeId(this.dbName);
4046
+ }
2473
4047
  const rows = await this.adapter.query(
2474
4048
  "SELECT value FROM _kora_meta WHERE key = 'node_id'"
2475
4049
  );
2476
4050
  if (rows[0]) {
2477
4051
  return rows[0].value;
2478
4052
  }
2479
- const newNodeId = (0, import_core8.generateUUIDv7)();
4053
+ const newNodeId = (0, import_core17.generateUUIDv7)();
2480
4054
  await this.adapter.execute("INSERT INTO _kora_meta (key, value) VALUES ('node_id', ?)", [
2481
4055
  newNodeId
2482
4056
  ]);
@@ -2493,7 +4067,7 @@ var Store = class {
2493
4067
  const rows = await this.adapter.query(
2494
4068
  "SELECT node_id, sequence_number FROM _kora_version_vector"
2495
4069
  );
2496
- const vector = (0, import_core8.createVersionVector)();
4070
+ const vector = (0, import_core17.createVersionVector)();
2497
4071
  for (const row of rows) {
2498
4072
  vector.set(row.node_id, row.sequence_number);
2499
4073
  }
@@ -2505,14 +4079,506 @@ var Store = class {
2505
4079
  }
2506
4080
  }
2507
4081
  };
4082
+
4083
+ // src/reactivity/query-store.ts
4084
+ var EMPTY_ARRAY = Object.freeze([]);
4085
+ var QueryStore = class {
4086
+ snapshot = EMPTY_ARRAY;
4087
+ listeners = /* @__PURE__ */ new Set();
4088
+ unsubscribeQuery = null;
4089
+ active = false;
4090
+ queryBuilder;
4091
+ constructor(queryBuilder) {
4092
+ this.queryBuilder = queryBuilder;
4093
+ }
4094
+ /**
4095
+ * Subscribe to snapshot changes.
4096
+ *
4097
+ * @returns Unsubscribe function
4098
+ */
4099
+ subscribe = (onStoreChange) => {
4100
+ this.listeners.add(onStoreChange);
4101
+ if (!this.active) {
4102
+ this.startSubscription();
4103
+ }
4104
+ return () => {
4105
+ this.listeners.delete(onStoreChange);
4106
+ if (this.listeners.size === 0) {
4107
+ this.stopSubscription();
4108
+ }
4109
+ };
4110
+ };
4111
+ /** Synchronous read of the latest query results. */
4112
+ getSnapshot = () => {
4113
+ return this.snapshot;
4114
+ };
4115
+ /** Tear down listeners and the underlying query subscription. */
4116
+ destroy() {
4117
+ this.stopSubscription();
4118
+ this.listeners.clear();
4119
+ this.snapshot = EMPTY_ARRAY;
4120
+ }
4121
+ startSubscription() {
4122
+ this.active = true;
4123
+ this.unsubscribeQuery = this.queryBuilder.subscribe((results) => {
4124
+ if (!this.active) return;
4125
+ this.snapshot = Object.freeze([...results]);
4126
+ this.notifyListeners();
4127
+ });
4128
+ }
4129
+ stopSubscription() {
4130
+ this.active = false;
4131
+ if (this.unsubscribeQuery) {
4132
+ this.unsubscribeQuery();
4133
+ this.unsubscribeQuery = null;
4134
+ }
4135
+ }
4136
+ notifyListeners() {
4137
+ for (const listener of this.listeners) {
4138
+ listener();
4139
+ }
4140
+ }
4141
+ };
4142
+
4143
+ // src/reactivity/assert-query-ready.ts
4144
+ var import_core18 = require("@korajs/core");
4145
+ var LEGACY_PENDING_COLLECTION = "__pending__";
4146
+ function assertQueryReady(query) {
4147
+ const descriptor = query.getDescriptor();
4148
+ if (descriptor.collection === LEGACY_PENDING_COLLECTION) {
4149
+ throw new import_core18.AppNotReadyError(
4150
+ "Cannot use useQuery() before app.ready. Await app.ready or wrap your UI in <KoraProvider app={app}>."
4151
+ );
4152
+ }
4153
+ }
4154
+
4155
+ // src/reactivity/query-store-cache.ts
4156
+ var QueryStoreCache = class {
4157
+ constructor(scopeKey = "default") {
4158
+ this.scopeKey = scopeKey;
4159
+ }
4160
+ scopeKey;
4161
+ entries = /* @__PURE__ */ new Map();
4162
+ getOrCreate(queryBuilder) {
4163
+ const key = this.getKey(queryBuilder);
4164
+ const existing = this.entries.get(key);
4165
+ if (existing) {
4166
+ existing.refCount++;
4167
+ return existing.queryStore;
4168
+ }
4169
+ const queryStore = new QueryStore(queryBuilder);
4170
+ this.entries.set(key, {
4171
+ queryStore,
4172
+ refCount: 1
4173
+ });
4174
+ return queryStore;
4175
+ }
4176
+ release(queryBuilder) {
4177
+ const key = this.getKey(queryBuilder);
4178
+ const entry = this.entries.get(key);
4179
+ if (!entry) {
4180
+ return;
4181
+ }
4182
+ entry.refCount--;
4183
+ if (entry.refCount <= 0) {
4184
+ entry.queryStore.destroy();
4185
+ this.entries.delete(key);
4186
+ }
4187
+ }
4188
+ clear() {
4189
+ for (const entry of this.entries.values()) {
4190
+ entry.queryStore.destroy();
4191
+ }
4192
+ this.entries.clear();
4193
+ }
4194
+ get size() {
4195
+ return this.entries.size;
4196
+ }
4197
+ getKey(queryBuilder) {
4198
+ return `${this.scopeKey}:${JSON.stringify(queryBuilder.getDescriptor())}`;
4199
+ }
4200
+ };
4201
+ var sharedCache = null;
4202
+ function getSharedQueryStoreCache() {
4203
+ if (!sharedCache) {
4204
+ sharedCache = new QueryStoreCache();
4205
+ }
4206
+ return sharedCache;
4207
+ }
4208
+
4209
+ // src/richtext/create-richtext-controller.ts
4210
+ var Y2 = __toESM(require("yjs"), 1);
4211
+ init_richtext_serializer();
4212
+ var LOAD_ORIGIN = "kora-load";
4213
+ var REMOTE_ORIGIN = "kora-remote";
4214
+ var DOC_CHANNEL_ORIGIN = "kora-doc-channel";
4215
+ var TEXT_KEY2 = "content";
4216
+ var COMPACT_AFTER_DELTAS = 20;
4217
+ var PERSIST_DEBOUNCE_MS = 400;
4218
+ function createRichTextController(options) {
4219
+ const {
4220
+ collection,
4221
+ collectionName,
4222
+ recordId,
4223
+ fieldName,
4224
+ store,
4225
+ syncEngine = null,
4226
+ useDocChannel
4227
+ } = options;
4228
+ const doc = new Y2.Doc();
4229
+ const text = doc.getText(TEXT_KEY2);
4230
+ const undoManager = new Y2.UndoManager(text);
4231
+ let user = options.user;
4232
+ let disposed = false;
4233
+ let ready = false;
4234
+ let error = null;
4235
+ let canUndo = false;
4236
+ let canRedo = false;
4237
+ let cursors = [];
4238
+ const listeners = /* @__PURE__ */ new Set();
4239
+ let snapshot = {
4240
+ ready: false,
4241
+ error: null,
4242
+ canUndo: false,
4243
+ canRedo: false,
4244
+ cursors: []
4245
+ };
4246
+ const baseUpdateRef = { current: null };
4247
+ const pendingDeltasRef = { current: [] };
4248
+ const docChannelActiveRef = { current: false };
4249
+ let persistTimer = null;
4250
+ let recordUnsubscribe = null;
4251
+ let docChannelUnsubscribe = null;
4252
+ let awarenessUnsubscribe = null;
4253
+ const refreshSnapshot = () => {
4254
+ snapshot = {
4255
+ ready,
4256
+ error,
4257
+ canUndo,
4258
+ canRedo,
4259
+ cursors: [...cursors]
4260
+ };
4261
+ };
4262
+ const notify = () => {
4263
+ refreshSnapshot();
4264
+ for (const listener of listeners) {
4265
+ listener();
4266
+ }
4267
+ };
4268
+ const syncHistoryState = () => {
4269
+ canUndo = undoManager.undoStack.length > 0;
4270
+ canRedo = undoManager.redoStack.length > 0;
4271
+ notify();
4272
+ };
4273
+ const getSnapshot = () => snapshot;
4274
+ const resolveAwarenessUser = () => {
4275
+ if (user) {
4276
+ return user;
4277
+ }
4278
+ if (syncEngine) {
4279
+ const existing = syncEngine.getAwarenessManager().getLocalState()?.user;
4280
+ if (existing) {
4281
+ return existing;
4282
+ }
4283
+ }
4284
+ return {
4285
+ name: "Anonymous",
4286
+ color: "#6366f1"
4287
+ };
4288
+ };
4289
+ const setCursor = (anchor, head) => {
4290
+ if (!syncEngine) {
4291
+ return;
4292
+ }
4293
+ const awareness = syncEngine.getAwarenessManager();
4294
+ const state = {
4295
+ user: resolveAwarenessUser(),
4296
+ cursor: {
4297
+ collection: collectionName,
4298
+ recordId,
4299
+ field: fieldName,
4300
+ anchor,
4301
+ head
4302
+ }
4303
+ };
4304
+ awareness.setLocalState(state);
4305
+ };
4306
+ const clearCursor = () => {
4307
+ if (!syncEngine) {
4308
+ return;
4309
+ }
4310
+ const awareness = syncEngine.getAwarenessManager();
4311
+ const current = awareness.getLocalState();
4312
+ if (!current?.cursor) {
4313
+ return;
4314
+ }
4315
+ if (current.cursor.collection !== collectionName || current.cursor.recordId !== recordId || current.cursor.field !== fieldName) {
4316
+ return;
4317
+ }
4318
+ awareness.setLocalState({ user: current.user });
4319
+ };
4320
+ const applyRemoteSnapshot = (value) => {
4321
+ const encoded = encodeRichtextInput(value);
4322
+ if (!encoded) {
4323
+ return;
4324
+ }
4325
+ const currentSnapshot = Y2.encodeStateAsUpdate(doc);
4326
+ if (updatesEqual(currentSnapshot, encoded)) {
4327
+ return;
4328
+ }
4329
+ Y2.applyUpdate(doc, encoded, REMOTE_ORIGIN);
4330
+ baseUpdateRef.current = encoded;
4331
+ pendingDeltasRef.current = [];
4332
+ syncHistoryState();
4333
+ };
4334
+ const flushPersist = async () => {
4335
+ const snapshot2 = composeRichtextSnapshot(baseUpdateRef.current, pendingDeltasRef.current);
4336
+ if (pendingDeltasRef.current.length >= COMPACT_AFTER_DELTAS) {
4337
+ baseUpdateRef.current = snapshot2;
4338
+ pendingDeltasRef.current = [];
4339
+ }
4340
+ try {
4341
+ await collection.update(recordId, {
4342
+ [fieldName]: snapshot2
4343
+ });
4344
+ } catch (cause) {
4345
+ if (!disposed) {
4346
+ error = cause instanceof Error ? cause : new Error(String(cause));
4347
+ notify();
4348
+ }
4349
+ }
4350
+ };
4351
+ const schedulePersist = () => {
4352
+ if (persistTimer) {
4353
+ clearTimeout(persistTimer);
4354
+ }
4355
+ persistTimer = setTimeout(() => {
4356
+ persistTimer = null;
4357
+ void flushPersist();
4358
+ }, PERSIST_DEBOUNCE_MS);
4359
+ };
4360
+ const onDocUpdate = (update, origin) => {
4361
+ syncHistoryState();
4362
+ if (origin === LOAD_ORIGIN || origin === REMOTE_ORIGIN || origin === DOC_CHANNEL_ORIGIN) {
4363
+ return;
4364
+ }
4365
+ pendingDeltasRef.current.push(update);
4366
+ if (docChannelActiveRef.current && syncEngine) {
4367
+ syncEngine.getRichtextDocChannel?.().send(collectionName, recordId, fieldName, update);
4368
+ schedulePersist();
4369
+ return;
4370
+ }
4371
+ void flushPersist();
4372
+ };
4373
+ const subscribeRecordChanges = () => {
4374
+ recordUnsubscribe?.();
4375
+ recordUnsubscribe = store.collection(collectionName).where({ id: recordId }).subscribe((results) => {
4376
+ const record = results[0];
4377
+ if (!record) {
4378
+ return;
4379
+ }
4380
+ applyRemoteSnapshot(record[fieldName]);
4381
+ });
4382
+ };
4383
+ const subscribeDocChannel = () => {
4384
+ docChannelUnsubscribe?.();
4385
+ if (!ready || !syncEngine || !docChannelActiveRef.current) {
4386
+ return;
4387
+ }
4388
+ const channel = syncEngine.getRichtextDocChannel?.();
4389
+ if (!channel) {
4390
+ return;
4391
+ }
4392
+ docChannelUnsubscribe = channel.subscribe(collectionName, recordId, fieldName, (update) => {
4393
+ Y2.applyUpdate(doc, update, DOC_CHANNEL_ORIGIN);
4394
+ syncHistoryState();
4395
+ });
4396
+ };
4397
+ const updateCursors = () => {
4398
+ if (!syncEngine) {
4399
+ cursors = [];
4400
+ notify();
4401
+ return;
4402
+ }
4403
+ const awareness = syncEngine.getAwarenessManager();
4404
+ const localClientId = awareness.clientId;
4405
+ const states = awareness.getStates();
4406
+ const fieldCursors = [];
4407
+ for (const [clientId, state] of states) {
4408
+ if (clientId === localClientId) continue;
4409
+ if (!state.cursor) continue;
4410
+ if (state.cursor.collection !== collectionName || state.cursor.recordId !== recordId || state.cursor.field !== fieldName) {
4411
+ continue;
4412
+ }
4413
+ fieldCursors.push({
4414
+ clientId,
4415
+ userName: state.user.name,
4416
+ color: state.user.color,
4417
+ anchor: state.cursor.anchor,
4418
+ head: state.cursor.head
4419
+ });
4420
+ }
4421
+ cursors = fieldCursors;
4422
+ notify();
4423
+ };
4424
+ const subscribeAwareness = () => {
4425
+ awarenessUnsubscribe?.();
4426
+ if (!syncEngine) {
4427
+ cursors = [];
4428
+ notify();
4429
+ return;
4430
+ }
4431
+ const awareness = syncEngine.getAwarenessManager();
4432
+ awarenessUnsubscribe = awareness.on("change", () => {
4433
+ updateCursors();
4434
+ });
4435
+ updateCursors();
4436
+ };
4437
+ const initialize = async () => {
4438
+ ready = false;
4439
+ error = null;
4440
+ notify();
4441
+ try {
4442
+ const record = await collection.findById(recordId);
4443
+ if (disposed) return;
4444
+ doc.transact(() => {
4445
+ const target = doc.getText(TEXT_KEY2);
4446
+ target.delete(0, target.length);
4447
+ }, LOAD_ORIGIN);
4448
+ const encoded = encodeRichtextInput(record?.[fieldName]);
4449
+ baseUpdateRef.current = encoded;
4450
+ pendingDeltasRef.current = [];
4451
+ if (encoded) {
4452
+ Y2.applyUpdate(doc, encoded, LOAD_ORIGIN);
4453
+ }
4454
+ const channel = syncEngine?.getRichtextDocChannel?.();
4455
+ docChannelActiveRef.current = channel?.shouldUseChannel(encoded?.length ?? 0, useDocChannel) ?? false;
4456
+ ready = true;
4457
+ syncHistoryState();
4458
+ subscribeRecordChanges();
4459
+ subscribeDocChannel();
4460
+ subscribeAwareness();
4461
+ notify();
4462
+ } catch (cause) {
4463
+ if (disposed) return;
4464
+ error = cause instanceof Error ? cause : new Error(String(cause));
4465
+ notify();
4466
+ }
4467
+ };
4468
+ doc.on("update", onDocUpdate);
4469
+ void initialize();
4470
+ return {
4471
+ doc,
4472
+ text,
4473
+ getSnapshot,
4474
+ subscribe(listener) {
4475
+ listeners.add(listener);
4476
+ return () => {
4477
+ listeners.delete(listener);
4478
+ };
4479
+ },
4480
+ undo() {
4481
+ undoManager.undo();
4482
+ syncHistoryState();
4483
+ },
4484
+ redo() {
4485
+ undoManager.redo();
4486
+ syncHistoryState();
4487
+ },
4488
+ setCursor,
4489
+ clearCursor,
4490
+ setUser(nextUser) {
4491
+ user = nextUser;
4492
+ },
4493
+ destroy() {
4494
+ if (disposed) {
4495
+ return;
4496
+ }
4497
+ disposed = true;
4498
+ if (persistTimer) {
4499
+ clearTimeout(persistTimer);
4500
+ persistTimer = null;
4501
+ }
4502
+ doc.off("update", onDocUpdate);
4503
+ recordUnsubscribe?.();
4504
+ docChannelUnsubscribe?.();
4505
+ awarenessUnsubscribe?.();
4506
+ clearCursor();
4507
+ undoManager.destroy();
4508
+ baseUpdateRef.current = null;
4509
+ pendingDeltasRef.current = [];
4510
+ docChannelActiveRef.current = false;
4511
+ listeners.clear();
4512
+ }
4513
+ };
4514
+ }
4515
+ function encodeRichtextInput(value) {
4516
+ if (value === null || value === void 0) {
4517
+ return null;
4518
+ }
4519
+ try {
4520
+ return encodeRichtext(value);
4521
+ } catch {
4522
+ if (typeof value === "string") {
4523
+ const fallbackDoc = new Y2.Doc();
4524
+ fallbackDoc.getText(TEXT_KEY2).insert(0, value);
4525
+ return Y2.encodeStateAsUpdate(fallbackDoc);
4526
+ }
4527
+ throw new Error("Richtext record value must be a string, Uint8Array, ArrayBuffer, or null.");
4528
+ }
4529
+ }
4530
+ function composeRichtextSnapshot(base, deltas) {
4531
+ const mergedDoc = new Y2.Doc();
4532
+ if (base) {
4533
+ Y2.applyUpdate(mergedDoc, base);
4534
+ }
4535
+ for (const delta of deltas) {
4536
+ Y2.applyUpdate(mergedDoc, delta);
4537
+ }
4538
+ return Y2.encodeStateAsUpdate(mergedDoc);
4539
+ }
4540
+ function updatesEqual(left, right) {
4541
+ if (left.length !== right.length) {
4542
+ return false;
4543
+ }
4544
+ for (let index = 0; index < left.length; index++) {
4545
+ if (left[index] !== right[index]) {
4546
+ return false;
4547
+ }
4548
+ }
4549
+ return true;
4550
+ }
4551
+
4552
+ // src/richtext/as-rich-text-sync-engine.ts
4553
+ function asRichTextSyncEngine(engine) {
4554
+ if (engine == null) {
4555
+ return null;
4556
+ }
4557
+ return engine;
4558
+ }
4559
+
4560
+ // src/index.ts
4561
+ init_richtext_serializer();
4562
+
4563
+ // src/backup/index.ts
4564
+ init_backup();
4565
+
4566
+ // src/audit/index.ts
4567
+ init_audit_trace_store();
4568
+ init_audit_json();
4569
+ init_export_audit();
2508
4570
  // Annotate the CommonJS export names for ESM import in node:
2509
4571
  0 && (module.exports = {
2510
4572
  AdapterError,
4573
+ COMPACTION_BASELINE_META_KEY,
2511
4574
  Collection,
2512
4575
  InvalidStateTransitionError,
4576
+ LAST_ACKED_SERVER_VECTOR_META_KEY,
2513
4577
  PersistenceError,
2514
4578
  QueryBuilder,
2515
4579
  QueryError,
4580
+ QueryStore,
4581
+ QueryStoreCache,
2516
4582
  RecordNotFoundError,
2517
4583
  SequenceManager,
2518
4584
  Store,
@@ -2522,12 +4588,31 @@ var Store = class {
2522
4588
  TransactionContext,
2523
4589
  WorkerInitError,
2524
4590
  WorkerTimeoutError,
4591
+ asRichTextSyncEngine,
4592
+ assertQueryReady,
4593
+ collectOperationsAheadOfServer,
4594
+ compactOperationLog,
4595
+ computeAckCompactionWatermark,
4596
+ createRichTextController,
4597
+ decodeAuditExport,
2525
4598
  decodeRichtext,
4599
+ deserializeVersionVectorFromMeta,
2526
4600
  encodeRichtext,
4601
+ exportBackup,
4602
+ getSharedQueryStoreCache,
4603
+ mergeVersionVectors,
4604
+ persistedAuditTraceFromEvent,
2527
4605
  pluralize,
4606
+ readAuditExportManifest,
4607
+ readBackupManifest,
4608
+ restoreBackup,
4609
+ richtextStatesEqual,
2528
4610
  richtextToPlainText,
4611
+ serializeVersionVectorToMeta,
2529
4612
  singularize,
2530
4613
  validateStateTransition,
2531
- validateUpdateStateMachine
4614
+ validateUpdateStateMachine,
4615
+ verifyAuditExportChecksum,
4616
+ verifyBackupChecksum
2532
4617
  });
2533
4618
  //# sourceMappingURL=index.cjs.map