@korajs/store 1.0.0-beta.12 → 1.0.0-beta.14

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 (92) hide show
  1. package/README.md +29 -37
  2. package/dist/adapters/better-sqlite3.cjs +90 -4
  3. package/dist/adapters/better-sqlite3.cjs.map +1 -1
  4. package/dist/adapters/better-sqlite3.d.cts +21 -12
  5. package/dist/adapters/better-sqlite3.d.ts +21 -12
  6. package/dist/adapters/better-sqlite3.js +69 -5
  7. package/dist/adapters/better-sqlite3.js.map +1 -1
  8. package/dist/adapters/indexeddb.cjs +1316 -610
  9. package/dist/adapters/indexeddb.cjs.map +1 -1
  10. package/dist/adapters/indexeddb.d.cts +29 -3
  11. package/dist/adapters/indexeddb.d.ts +29 -3
  12. package/dist/adapters/indexeddb.js +247 -221
  13. package/dist/adapters/indexeddb.js.map +1 -1
  14. package/dist/adapters/sqlite-wasm-worker.cjs +856 -63
  15. package/dist/adapters/sqlite-wasm-worker.cjs.map +1 -1
  16. package/dist/adapters/sqlite-wasm-worker.js +529 -63
  17. package/dist/adapters/sqlite-wasm-worker.js.map +1 -1
  18. package/dist/adapters/sqlite-wasm.cjs +1032 -344
  19. package/dist/adapters/sqlite-wasm.cjs.map +1 -1
  20. package/dist/adapters/sqlite-wasm.d.cts +239 -18
  21. package/dist/adapters/sqlite-wasm.d.ts +239 -18
  22. package/dist/adapters/sqlite-wasm.js +111 -5
  23. package/dist/adapters/sqlite-wasm.js.map +1 -1
  24. package/dist/backup-IHKVMUAM.js +25 -0
  25. package/dist/chunk-5AQSGMJV.js +5471 -0
  26. package/dist/chunk-5AQSGMJV.js.map +1 -0
  27. package/dist/chunk-5JMGMPUE.js +591 -0
  28. package/dist/chunk-5JMGMPUE.js.map +1 -0
  29. package/dist/{chunk-CYO3YKFL.js → chunk-5ZRRF6R2.js} +2 -2
  30. package/dist/chunk-A6GFH5IG.js +36 -0
  31. package/dist/chunk-A6GFH5IG.js.map +1 -0
  32. package/dist/chunk-BILSA6XZ.js +608 -0
  33. package/dist/chunk-BILSA6XZ.js.map +1 -0
  34. package/dist/chunk-BQSIL6TT.js +1095 -0
  35. package/dist/chunk-BQSIL6TT.js.map +1 -0
  36. package/dist/{chunk-GSJ7EEUC.js → chunk-J5AGXF3D.js} +58 -9
  37. package/dist/chunk-J5AGXF3D.js.map +1 -0
  38. package/dist/chunk-KK3GBONL.js +112 -0
  39. package/dist/chunk-KK3GBONL.js.map +1 -0
  40. package/dist/chunk-KTVNANUH.js +876 -0
  41. package/dist/chunk-KTVNANUH.js.map +1 -0
  42. package/dist/chunk-QW4UU7NG.js +1003 -0
  43. package/dist/chunk-QW4UU7NG.js.map +1 -0
  44. package/dist/chunk-SRHDJF5B.js +186 -0
  45. package/dist/chunk-SRHDJF5B.js.map +1 -0
  46. package/dist/chunk-UC2LGFPV.js +810 -0
  47. package/dist/chunk-UC2LGFPV.js.map +1 -0
  48. package/dist/{export-audit-ODGQTQIK.js → export-audit-J2QXRHD2.js} +3 -3
  49. package/dist/index.cjs +9533 -4246
  50. package/dist/index.cjs.map +1 -1
  51. package/dist/index.d.cts +1143 -110
  52. package/dist/index.d.ts +1143 -110
  53. package/dist/index.js +298 -3191
  54. package/dist/index.js.map +1 -1
  55. package/dist/internal.cjs +581 -264
  56. package/dist/internal.cjs.map +1 -1
  57. package/dist/internal.d.cts +15 -85
  58. package/dist/internal.d.ts +15 -85
  59. package/dist/internal.js +10 -8
  60. package/dist/restore-MNDPR557.js +299 -0
  61. package/dist/restore-MNDPR557.js.map +1 -0
  62. package/dist/sql-builder-D0tC3sv9.d.cts +615 -0
  63. package/dist/sql-builder-D5UMzHEo.d.ts +615 -0
  64. package/dist/sqlite-wasm-channel-DVX93Uiq.d.cts +141 -0
  65. package/dist/sqlite-wasm-channel-DVX93Uiq.d.ts +141 -0
  66. package/dist/sqlite-wasm-channel-QCCNBYCU.js +12 -0
  67. package/dist/store-HNGX4KTT.js +16 -0
  68. package/dist/store-HNGX4KTT.js.map +1 -0
  69. package/dist/{types-BslDOTk0.d.ts → types-Dh-xyUjP.d.cts} +99 -26
  70. package/dist/{types-BslDOTk0.d.cts → types-Dh-xyUjP.d.ts} +99 -26
  71. package/package.json +3 -2
  72. package/dist/backup-5S7K2Z35.js +0 -14
  73. package/dist/chunk-GSJ7EEUC.js.map +0 -1
  74. package/dist/chunk-HS6DBMST.js +0 -383
  75. package/dist/chunk-HS6DBMST.js.map +0 -1
  76. package/dist/chunk-RJIEKEN4.js +0 -751
  77. package/dist/chunk-RJIEKEN4.js.map +0 -1
  78. package/dist/chunk-TUR7RGEP.js +0 -84
  79. package/dist/chunk-TUR7RGEP.js.map +0 -1
  80. package/dist/chunk-VDBJQ75K.js +0 -78
  81. package/dist/chunk-VDBJQ75K.js.map +0 -1
  82. package/dist/chunk-YT7MRFCP.js +0 -687
  83. package/dist/chunk-YT7MRFCP.js.map +0 -1
  84. package/dist/sqlite-wasm-channel-GGHA2BXU.js +0 -12
  85. package/dist/sqlite-wasm-channel-L6o3bWQK.d.cts +0 -92
  86. package/dist/sqlite-wasm-channel-L6o3bWQK.d.ts +0 -92
  87. package/dist/types-BOO9EPt1.d.ts +0 -170
  88. package/dist/types-C_GDavG-.d.cts +0 -170
  89. /package/dist/{backup-5S7K2Z35.js.map → backup-IHKVMUAM.js.map} +0 -0
  90. /package/dist/{chunk-CYO3YKFL.js.map → chunk-5ZRRF6R2.js.map} +0 -0
  91. /package/dist/{export-audit-ODGQTQIK.js.map → export-audit-J2QXRHD2.js.map} +0 -0
  92. /package/dist/{sqlite-wasm-channel-GGHA2BXU.js.map → sqlite-wasm-channel-QCCNBYCU.js.map} +0 -0
package/dist/internal.cjs CHANGED
@@ -44,8 +44,8 @@ __export(internal_exports, {
44
44
  executeDelete: () => executeDelete,
45
45
  executeInsert: () => executeInsert,
46
46
  executeUpdate: () => executeUpdate,
47
+ hasUnsyncedOwnOperations: () => hasUnsyncedOwnOperations,
47
48
  isIncomingNewerThanRow: () => isIncomingNewerThanRow,
48
- resolveCausalDeps: () => resolveCausalDeps,
49
49
  rowVersionFromRecord: () => rowVersionFromRecord,
50
50
  serializeOperation: () => serializeOperation,
51
51
  serializeRecord: () => serializeRecord,
@@ -149,8 +149,10 @@ function deserializeRecord(row, fields) {
149
149
  var ATOMIC_OPS_KEY = "__kora_atomic_ops__";
150
150
  var TX_ID_KEY = "__kora_tx_id__";
151
151
  var MUTATION_NAME_KEY = "__kora_mutation__";
152
+ var FIELD_VERSIONS_KEY = "__kora_field_versions__";
153
+ var HASH_VERSION_KEY = "__kora_hash_version__";
152
154
  function serializeOperation(op) {
153
- const hasMetadata = op.transactionId !== void 0 || op.mutationName !== void 0;
155
+ const hasMetadata = op.transactionId !== void 0 || op.mutationName !== void 0 || op.hashVersion !== void 0;
154
156
  let dataPayload = null;
155
157
  if (op.data) {
156
158
  dataPayload = { ...op.data };
@@ -163,6 +165,12 @@ function serializeOperation(op) {
163
165
  if (op.mutationName !== void 0) {
164
166
  dataPayload[MUTATION_NAME_KEY] = op.mutationName;
165
167
  }
168
+ if (op.fieldVersions !== void 0) {
169
+ dataPayload[FIELD_VERSIONS_KEY] = op.fieldVersions;
170
+ }
171
+ if (op.hashVersion !== void 0) {
172
+ dataPayload[HASH_VERSION_KEY] = op.hashVersion;
173
+ }
166
174
  } else if (hasMetadata) {
167
175
  dataPayload = {};
168
176
  if (op.transactionId !== void 0) {
@@ -171,6 +179,9 @@ function serializeOperation(op) {
171
179
  if (op.mutationName !== void 0) {
172
180
  dataPayload[MUTATION_NAME_KEY] = op.mutationName;
173
181
  }
182
+ if (op.hashVersion !== void 0) {
183
+ dataPayload[HASH_VERSION_KEY] = op.hashVersion;
184
+ }
174
185
  }
175
186
  return {
176
187
  id: op.id,
@@ -190,6 +201,8 @@ function deserializeOperation(row) {
190
201
  let atomicOps;
191
202
  let transactionId;
192
203
  let mutationName;
204
+ let fieldVersions;
205
+ let hashVersion;
193
206
  if (row.data) {
194
207
  const parsed = JSON.parse(row.data);
195
208
  if (ATOMIC_OPS_KEY in parsed) {
@@ -201,8 +214,21 @@ function deserializeOperation(row) {
201
214
  if (MUTATION_NAME_KEY in parsed) {
202
215
  mutationName = parsed[MUTATION_NAME_KEY];
203
216
  }
204
- const { [ATOMIC_OPS_KEY]: _a, [TX_ID_KEY]: _t, [MUTATION_NAME_KEY]: _m, ...rest } = parsed;
205
- data = Object.keys(rest).length > 0 ? rest : null;
217
+ if (FIELD_VERSIONS_KEY in parsed) {
218
+ fieldVersions = parsed[FIELD_VERSIONS_KEY];
219
+ }
220
+ if (HASH_VERSION_KEY in parsed) {
221
+ hashVersion = parsed[HASH_VERSION_KEY];
222
+ }
223
+ const {
224
+ [ATOMIC_OPS_KEY]: _a,
225
+ [TX_ID_KEY]: _t,
226
+ [MUTATION_NAME_KEY]: _m,
227
+ [FIELD_VERSIONS_KEY]: _f,
228
+ [HASH_VERSION_KEY]: _h,
229
+ ...rest
230
+ } = parsed;
231
+ data = row.type !== "delete" || Object.keys(rest).length > 0 ? rest : null;
206
232
  }
207
233
  return {
208
234
  id: row.id,
@@ -219,13 +245,26 @@ function deserializeOperation(row) {
219
245
  schemaVersion: row.schema_version,
220
246
  ...atomicOps !== void 0 ? { atomicOps } : {},
221
247
  ...transactionId !== void 0 ? { transactionId } : {},
222
- ...mutationName !== void 0 ? { mutationName } : {}
248
+ ...mutationName !== void 0 ? { mutationName } : {},
249
+ ...fieldVersions !== void 0 ? { fieldVersions } : {},
250
+ ...hashVersion !== void 0 ? { hashVersion } : {}
223
251
  };
224
252
  }
225
253
  function deserializeOperationWithCollection(row, collection) {
226
254
  const op = deserializeOperation(row);
227
255
  return { ...op, collection };
228
256
  }
257
+ function toRichtextReadShape(data, fields) {
258
+ let out = data;
259
+ for (const [name, descriptor] of Object.entries(fields)) {
260
+ if (descriptor.kind !== "richtext" || !(name in data)) continue;
261
+ const value = data[name];
262
+ if (value === null || value === void 0 || value instanceof Uint8Array) continue;
263
+ if (out === data) out = { ...data };
264
+ out[name] = encodeRichtext(value);
265
+ }
266
+ return out;
267
+ }
229
268
  function serializeValue(value, descriptor) {
230
269
  if (value === null || value === void 0) {
231
270
  return null;
@@ -241,9 +280,16 @@ function serializeValue(value, descriptor) {
241
280
  case "richtext":
242
281
  return encodeRichtext(value);
243
282
  default:
244
- return value;
283
+ return typeof value === "string" && isRawTextKind(descriptor.kind) ? (0, import_core2.encodeStoredText)(value) : value;
245
284
  }
246
285
  }
286
+ var RAW_TEXT_KINDS = /* @__PURE__ */ new Set(["string", "secret"]);
287
+ function isRawTextKind(kind) {
288
+ return RAW_TEXT_KINDS.has(kind);
289
+ }
290
+ function encodeStoredFilterValue(value, descriptor) {
291
+ return typeof value === "string" && descriptor !== void 0 && isRawTextKind(descriptor.kind) ? (0, import_core2.encodeStoredText)(value) : value;
292
+ }
247
293
  function deserializeValue(value, descriptor) {
248
294
  switch (descriptor.kind) {
249
295
  case "boolean":
@@ -263,7 +309,7 @@ function deserializeValue(value, descriptor) {
263
309
  case "richtext":
264
310
  return decodeRichtext(value);
265
311
  default:
266
- return value;
312
+ return typeof value === "string" && isRawTextKind(descriptor.kind) ? (0, import_core2.decodeStoredText)(value) : value;
267
313
  }
268
314
  }
269
315
 
@@ -325,15 +371,20 @@ function buildSelectQuery(descriptor, fields) {
325
371
  if (descriptor.orderBy.length > 0) {
326
372
  const orderParts = descriptor.orderBy.map((o) => {
327
373
  validateFieldName(o.field, fields);
328
- return `${(0, import_core5.quoteIdent)(o.field)} ${o.direction.toUpperCase()}`;
374
+ return `${(0, import_core5.quoteIdent)(resolveColumnName(o.field, fields))} ${sqlSortDirection(o.direction, o.field)}`;
329
375
  });
330
376
  parts.push(`ORDER BY ${orderParts.join(", ")}`);
331
377
  }
332
- if (descriptor.limit !== void 0) {
333
- parts.push(`LIMIT ${descriptor.limit}`);
378
+ const limit = descriptor.limit !== void 0 ? assertQueryCount(descriptor.limit, "limit") : null;
379
+ const offset = descriptor.offset !== void 0 ? assertQueryCount(descriptor.offset, "offset") : null;
380
+ if (limit !== null) {
381
+ parts.push("LIMIT ?");
382
+ params.push(limit);
334
383
  }
335
- if (descriptor.offset !== void 0) {
336
- parts.push(`OFFSET ${descriptor.offset}`);
384
+ if (offset !== null) {
385
+ if (limit === null) parts.push("LIMIT -1");
386
+ parts.push("OFFSET ?");
387
+ params.push(offset);
337
388
  }
338
389
  return { sql: parts.join(" "), params };
339
390
  }
@@ -401,6 +452,7 @@ function buildWhereClauseParts(where, fields, params) {
401
452
  for (const [fieldName, value] of Object.entries(where)) {
402
453
  validateFieldName(fieldName, fields);
403
454
  const descriptor = fields[fieldName];
455
+ const column = resolveColumnName(fieldName, fields);
404
456
  if (value !== null && typeof value === "object" && !Array.isArray(value)) {
405
457
  const ops = value;
406
458
  for (const [op, opValue] of Object.entries(ops)) {
@@ -411,18 +463,18 @@ function buildWhereClauseParts(where, fields, params) {
411
463
  validOperators: [...VALID_OPERATORS]
412
464
  });
413
465
  }
414
- conditions.push(buildOperatorCondition(fieldName, op, opValue, descriptor, params));
466
+ conditions.push(buildOperatorCondition(fieldName, column, op, opValue, descriptor, params));
415
467
  }
416
468
  } else {
417
- conditions.push(buildOperatorCondition(fieldName, "$eq", value, descriptor, params));
469
+ conditions.push(buildOperatorCondition(fieldName, column, "$eq", value, descriptor, params));
418
470
  }
419
471
  }
420
472
  if (conditions.length === 0) return null;
421
473
  return conditions.join(" AND ");
422
474
  }
423
- function buildOperatorCondition(fieldName, operator, value, descriptor, params) {
424
- const sqlValue = descriptor?.kind === "boolean" && typeof value === "boolean" ? value ? 1 : 0 : value;
425
- const column = (0, import_core5.quoteIdent)(fieldName);
475
+ function buildOperatorCondition(fieldName, columnName, operator, value, descriptor, params) {
476
+ const sqlValue = toSqlFilterValue(value, descriptor);
477
+ const column = (0, import_core5.quoteIdent)(columnName);
426
478
  switch (operator) {
427
479
  case "$eq":
428
480
  if (sqlValue === null) {
@@ -457,9 +509,7 @@ function buildOperatorCondition(fieldName, operator, value, descriptor, params)
457
509
  }
458
510
  const placeholders = sqlValue.map(() => "?");
459
511
  for (const item of sqlValue) {
460
- params.push(
461
- descriptor?.kind === "boolean" && typeof item === "boolean" ? item ? 1 : 0 : item
462
- );
512
+ params.push(toSqlFilterValue(item, descriptor));
463
513
  }
464
514
  return `${column} IN (${placeholders.join(", ")})`;
465
515
  }
@@ -467,12 +517,54 @@ function buildOperatorCondition(fieldName, operator, value, descriptor, params)
467
517
  throw new QueryError(`Unknown operator "${operator}"`, { operator });
468
518
  }
469
519
  }
520
+ function toSqlFilterValue(value, descriptor) {
521
+ if (descriptor?.kind === "boolean" && typeof value === "boolean") return value ? 1 : 0;
522
+ return encodeStoredFilterValue(value, descriptor);
523
+ }
524
+ var VIRTUAL_TIMESTAMP_FIELDS = Object.freeze({
525
+ createdAt: "_created_at",
526
+ updatedAt: "_updated_at"
527
+ });
528
+ var VIRTUAL_TIMESTAMP_FIELD_NAMES = Object.freeze([
529
+ "createdAt",
530
+ "updatedAt"
531
+ ]);
532
+ var SORT_DIRECTIONS = Object.freeze({
533
+ asc: "ASC",
534
+ desc: "DESC"
535
+ });
536
+ function resolveColumnName(fieldName, fields) {
537
+ if (Object.hasOwn(fields, fieldName)) return fieldName;
538
+ if (fieldName === "createdAt" || fieldName === "updatedAt") {
539
+ return VIRTUAL_TIMESTAMP_FIELDS[fieldName];
540
+ }
541
+ return fieldName;
542
+ }
543
+ function sqlSortDirection(direction, field) {
544
+ const key = typeof direction === "string" ? direction.toLowerCase() : "";
545
+ const keyword = Object.hasOwn(SORT_DIRECTIONS, key) ? SORT_DIRECTIONS[key] : void 0;
546
+ if (keyword === void 0) {
547
+ throw new QueryError(`Invalid sort direction for field "${field}". Use 'asc' or 'desc'.`, {
548
+ field,
549
+ direction: typeof direction === "string" ? direction.slice(0, 64) : typeof direction
550
+ });
551
+ }
552
+ return keyword;
553
+ }
554
+ function assertQueryCount(value, clause) {
555
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
556
+ throw new QueryError(
557
+ `Invalid ${clause}: expected a non-negative safe integer. Pass a whole number, for example .${clause}(10).`,
558
+ { clause, received: typeof value === "number" ? value : typeof value }
559
+ );
560
+ }
561
+ return value;
562
+ }
470
563
  function validateFieldName(fieldName, fields) {
471
564
  const allowedFields = /* @__PURE__ */ new Set([
472
565
  ...Object.keys(fields),
473
566
  "id",
474
- "createdAt",
475
- "updatedAt",
567
+ ...VIRTUAL_TIMESTAMP_FIELD_NAMES,
476
568
  "_created_at",
477
569
  "_updated_at"
478
570
  ]);
@@ -484,8 +576,137 @@ function validateFieldName(fieldName, fields) {
484
576
  }
485
577
  }
486
578
 
487
- // src/mutations/execute-insert.ts
579
+ // src/mutations/run-local-write.ts
580
+ var import_core7 = require("@korajs/core");
581
+
582
+ // src/mutations/write-context.ts
488
583
  var import_core6 = require("@korajs/core");
584
+ var CausalScope = class {
585
+ constructor(tracker, chainOperations) {
586
+ this.tracker = tracker;
587
+ this.chainOperations = chainOperations;
588
+ }
589
+ tracker;
590
+ chainOperations;
591
+ lastByCollection = /* @__PURE__ */ new Map();
592
+ lastInScope = null;
593
+ recorded = [];
594
+ /**
595
+ * Direct causal parents for the next operation in `collection`.
596
+ *
597
+ * @param collection - Collection of the operation being built
598
+ * @param extra - Explicit parents (e.g. the delete that caused a cascade)
599
+ */
600
+ depsFor(collection, extra = []) {
601
+ const deps = [];
602
+ const add = (id) => {
603
+ if (id && !deps.includes(id)) deps.push(id);
604
+ };
605
+ for (const id of extra) add(id);
606
+ const local = this.lastByCollection.get(collection);
607
+ if (local !== void 0) {
608
+ add(local);
609
+ } else {
610
+ for (const id of this.tracker?.nextCausalDeps(collection, false) ?? []) add(id);
611
+ }
612
+ if (this.chainOperations) add(this.lastInScope);
613
+ return deps;
614
+ }
615
+ /** Record an operation built in this scope. */
616
+ record(collection, operationId) {
617
+ this.lastByCollection.set(collection, operationId);
618
+ this.lastInScope = operationId;
619
+ this.recorded.push({ collection, id: operationId });
620
+ }
621
+ /** Publish this scope's heads to the shared tracker. Call only after commit. */
622
+ publish() {
623
+ for (const { collection, id } of this.recorded) {
624
+ this.tracker?.afterOperation(collection, id, false);
625
+ }
626
+ }
627
+ };
628
+ var SequenceBlock = class {
629
+ constructor(tx, nodeId) {
630
+ this.tx = tx;
631
+ this.nodeId = nodeId;
632
+ }
633
+ tx;
634
+ nodeId;
635
+ last = null;
636
+ persisted = 0;
637
+ /** The next number of the block. */
638
+ async take() {
639
+ if (this.last === null) {
640
+ const rows = await this.tx.query(
641
+ `INSERT INTO _kora_version_vector (node_id, sequence_number) VALUES (?, 1)
642
+ ON CONFLICT(node_id) DO UPDATE SET sequence_number = sequence_number + 1
643
+ RETURNING sequence_number`,
644
+ [this.nodeId]
645
+ );
646
+ const first = rows[0]?.sequence_number;
647
+ if (first === void 0) {
648
+ throw new import_core6.KoraError(
649
+ `Failed to reserve a sequence number for node "${this.nodeId}"`,
650
+ "SEQUENCE_RESERVATION_FAILED",
651
+ { nodeId: this.nodeId }
652
+ );
653
+ }
654
+ this.last = first;
655
+ this.persisted = first;
656
+ return first;
657
+ }
658
+ this.last += 1;
659
+ return this.last;
660
+ }
661
+ /** Persist the end of the block. Must run inside the transaction, before commit. */
662
+ async close() {
663
+ if (this.last === null || this.last === this.persisted) return;
664
+ await this.tx.execute(
665
+ "UPDATE _kora_version_vector SET sequence_number = MAX(sequence_number, ?) WHERE node_id = ?",
666
+ [this.last, this.nodeId]
667
+ );
668
+ this.persisted = this.last;
669
+ }
670
+ };
671
+ async function withWriteScope(tx, nodeId, causal, write) {
672
+ const scope = { tx, sequence: new SequenceBlock(tx, nodeId), causal };
673
+ const result = await write(scope);
674
+ await scope.sequence.close();
675
+ return result;
676
+ }
677
+
678
+ // src/mutations/run-local-write.ts
679
+ async function runLocalWrite(ctx, write) {
680
+ const causal = new CausalScope(ctx.causalTracker, false);
681
+ let result;
682
+ try {
683
+ await ctx.adapter.transaction(async (tx) => {
684
+ result = await withWriteScope(tx, ctx.nodeId, causal, write);
685
+ });
686
+ } catch (error) {
687
+ ctx.onStorageError?.(error);
688
+ throw error;
689
+ }
690
+ if (!result) {
691
+ throw new import_core7.KoraError("Local write transaction completed without a result", "WRITE_INCOMPLETE", {
692
+ collection: ctx.collection
693
+ });
694
+ }
695
+ causal.publish();
696
+ publishOperations(ctx, result);
697
+ return result;
698
+ }
699
+ function publishOperations(ctx, result) {
700
+ const ops = [];
701
+ if (result.operation) ops.push(result.operation);
702
+ ops.push(...result.sideEffects);
703
+ for (const op of ops) {
704
+ ctx.onMutation(op.collection, op);
705
+ }
706
+ }
707
+
708
+ // src/mutations/write-ops.ts
709
+ var import_core10 = require("@korajs/core");
489
710
 
490
711
  // src/lww/field-versions.ts
491
712
  function parseFieldVersions(raw) {
@@ -511,119 +732,20 @@ function parseFieldVersions(raw) {
511
732
  function serializeFieldVersions(versions) {
512
733
  return JSON.stringify(versions);
513
734
  }
514
- function fieldVersionsForFields(fieldNames, version) {
515
- const versions = {};
516
- for (const field of fieldNames) {
517
- versions[field] = version;
518
- }
519
- return versions;
520
- }
521
-
522
- // src/store/sequence-allocator.ts
523
- async function allocateNextSequenceInTransaction(tx, nodeId) {
524
- const rows = await tx.query(
525
- `INSERT INTO _kora_version_vector (node_id, sequence_number) VALUES (?, 1)
526
- ON CONFLICT(node_id) DO UPDATE SET sequence_number = sequence_number + 1
527
- RETURNING sequence_number`,
528
- [nodeId]
529
- );
530
- const seq = rows[0]?.sequence_number;
531
- if (seq === void 0) {
532
- throw new Error(`Failed to allocate sequence number for node "${nodeId}" in transaction`);
533
- }
534
- return seq;
535
- }
536
-
537
- // src/mutations/resolve-causal-deps.ts
538
- function resolveCausalDeps(ctx) {
539
- const merged = [];
540
- for (const id of ctx.extraCausalDeps ?? []) {
541
- if (!merged.includes(id)) {
542
- merged.push(id);
543
- }
544
- }
545
- for (const id of ctx.causalTracker?.nextCausalDeps(ctx.collection, ctx.inTransaction) ?? []) {
546
- if (!merged.includes(id)) {
547
- merged.push(id);
735
+ function stampFieldVersions(currentRaw, fields, version) {
736
+ const versions = parseFieldVersions(currentRaw);
737
+ for (const field of fields) {
738
+ const existing = versions[field];
739
+ if (existing === void 0 || existing < version) {
740
+ versions[field] = version;
548
741
  }
549
742
  }
550
- return merged;
743
+ return serializeFieldVersions(versions);
551
744
  }
552
745
 
553
- // src/mutations/execute-insert.ts
554
- async function executeInsert(ctx, data) {
555
- const validated = (0, import_core6.validateRecord)(ctx.collection, ctx.definition, data, "insert");
556
- const recordId = (0, import_core6.generateUUIDv7)();
557
- for (const [fieldName, descriptor] of Object.entries(ctx.definition.fields)) {
558
- if (descriptor.auto && descriptor.kind === "timestamp") {
559
- validated[fieldName] = Date.now();
560
- }
561
- }
562
- const writeData = await (0, import_core6.transformSecretFieldsForWrite)(
563
- validated,
564
- ctx.definition,
565
- ctx.secretKeyProvider
566
- );
567
- const causalDeps = resolveCausalDeps(ctx);
568
- let operation;
569
- let record;
570
- await ctx.adapter.transaction(async (tx) => {
571
- const sequenceNumber = await allocateNextSequenceInTransaction(tx, ctx.nodeId);
572
- operation = await (0, import_core6.createOperation)(
573
- {
574
- nodeId: ctx.nodeId,
575
- type: "insert",
576
- collection: ctx.collection,
577
- recordId,
578
- // Binary richtext values are tagged as canonical JSON BEFORE the
579
- // operation is content-hashed, so the hash input, persisted JSON,
580
- // and wire payload are the identical value.
581
- data: encodeRichtextFieldsForOpData(writeData, ctx.definition.fields),
582
- previousData: null,
583
- sequenceNumber,
584
- causalDeps,
585
- schemaVersion: ctx.schema.version
586
- },
587
- ctx.clock
588
- );
589
- ctx.causalTracker?.afterOperation(ctx.collection, operation.id, ctx.inTransaction);
590
- const serializedData = serializeRecord(writeData, ctx.definition.fields);
591
- const version = serializeRowVersion(operation.timestamp);
592
- record = {
593
- id: recordId,
594
- ...serializedData,
595
- _created_at: operation.timestamp.wallTime,
596
- _updated_at: operation.timestamp.wallTime,
597
- _version: version,
598
- // Stamp every inserted field with this operation's version so later
599
- // per-field LWW compares against a real writer, not the fallback.
600
- _field_versions: serializeFieldVersions(
601
- fieldVersionsForFields(Object.keys(serializedData), version)
602
- )
603
- };
604
- const builtInsert = buildInsertQuery(ctx.collection, record);
605
- const opInsert = buildInsertQuery(
606
- `_kora_ops_${ctx.collection}`,
607
- serializeOperation(operation)
608
- );
609
- await tx.execute(builtInsert.sql, builtInsert.params);
610
- await tx.execute(opInsert.sql, opInsert.params);
611
- });
612
- ctx.onMutation(ctx.collection, operation);
613
- return {
614
- id: recordId,
615
- ...writeData,
616
- createdAt: operation.timestamp.wallTime,
617
- updatedAt: operation.timestamp.wallTime
618
- };
619
- }
620
-
621
- // src/mutations/execute-update.ts
622
- var import_core8 = require("@korajs/core");
623
-
624
746
  // src/state-machine/state-validator.ts
625
- var import_core7 = require("@korajs/core");
626
- var InvalidStateTransitionError = class extends import_core7.KoraError {
747
+ var import_core8 = require("@korajs/core");
748
+ var InvalidStateTransitionError = class extends import_core8.KoraError {
627
749
  constructor(collection, recordId, field, fromState, toState, allowedStates) {
628
750
  super(
629
751
  `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)"}`,
@@ -657,7 +779,7 @@ function validateStateTransition(collectionName, recordId, stateMachine, current
657
779
  collection: collectionName,
658
780
  transitions: stateMachine.transitions
659
781
  };
660
- const transitionResult = (0, import_core7.validateTransition)(constraint, currentState, newState);
782
+ const transitionResult = (0, import_core8.validateTransition)(constraint, currentState, newState);
661
783
  if (transitionResult.valid) {
662
784
  return { valid: true, allowedStates: transitionResult.allowedTargets };
663
785
  }
@@ -675,10 +797,25 @@ function validateStateTransition(collectionName, recordId, stateMachine, current
675
797
  return { valid: false, allowedStates };
676
798
  }
677
799
  function validateUpdateStateMachine(collectionName, recordId, collectionDef, currentRecord, updateData) {
800
+ let result = updateData;
678
801
  const stateMachine = collectionDef.stateMachine;
679
- if (stateMachine === void 0) {
680
- return updateData;
802
+ if (stateMachine !== void 0) {
803
+ result = applyStateMachine(collectionName, recordId, stateMachine, currentRecord, result);
804
+ }
805
+ for (const [field, descriptor] of Object.entries(collectionDef.fields)) {
806
+ if (descriptor.kind !== "enum" || !descriptor.transitions) continue;
807
+ if (stateMachine?.field === field) continue;
808
+ result = applyStateMachine(
809
+ collectionName,
810
+ recordId,
811
+ { field, transitions: descriptor.transitions, onInvalidTransition: "reject" },
812
+ currentRecord,
813
+ result
814
+ );
681
815
  }
816
+ return result;
817
+ }
818
+ function applyStateMachine(collectionName, recordId, stateMachine, currentRecord, updateData) {
682
819
  const stateField = stateMachine.field;
683
820
  if (!(stateField in updateData)) {
684
821
  return updateData;
@@ -703,161 +840,341 @@ function validateUpdateStateMachine(collectionName, recordId, collectionDef, cur
703
840
  return filtered;
704
841
  }
705
842
 
706
- // src/mutations/execute-update.ts
707
- async function executeUpdate(ctx, id, data) {
708
- const currentRows = await ctx.adapter.query(
709
- `SELECT * FROM ${(0, import_core8.quoteIdent)(ctx.collection)} WHERE id = ? AND _deleted = 0`,
710
- [id]
711
- );
712
- const currentRow = currentRows[0];
843
+ // src/mutations/secret-write.ts
844
+ var import_core9 = require("@korajs/core");
845
+ function toAtRestWriteData(data, definition, secretKeyProvider) {
846
+ return (0, import_core9.transformSecretFieldsForWrite)(data, definition, secretKeyProvider);
847
+ }
848
+
849
+ // src/mutations/write-ops.ts
850
+ async function prepareInsert(env, collection, data) {
851
+ const definition = requireDefinition(env, collection);
852
+ const validated = (0, import_core10.validateRecord)(collection, definition, data, "insert");
853
+ const now = Date.now();
854
+ for (const [fieldName, descriptor] of Object.entries(definition.fields)) {
855
+ if (descriptor.auto && descriptor.kind === "timestamp") {
856
+ validated[fieldName] = now;
857
+ }
858
+ }
859
+ const writeData = await toAtRestWriteData(validated, definition, env.secretKeyProvider);
860
+ return { collection, recordId: (0, import_core10.generateUUIDv7)(), data: writeData };
861
+ }
862
+ async function writeInsertInTx(env, scope, insert, extraCausalDeps = []) {
863
+ const definition = requireDefinition(env, insert.collection);
864
+ const operation = await buildLocalOperation(env, scope, {
865
+ type: "insert",
866
+ collection: insert.collection,
867
+ recordId: insert.recordId,
868
+ // Binary richtext values are tagged as canonical JSON BEFORE hashing, so the
869
+ // hash input, persisted JSON and wire payload are the identical value.
870
+ data: encodeRichtextFieldsForOpData(insert.data, definition.fields),
871
+ previousData: null,
872
+ extraCausalDeps
873
+ });
874
+ if (env.fold) {
875
+ await appendOperationRow(scope, operation);
876
+ await env.fold.applyInTx(scope.tx, operation, "local");
877
+ } else {
878
+ const serialized = serializeRecord(insert.data, definition.fields);
879
+ const version = serializeRowVersion(operation.timestamp);
880
+ const row = {
881
+ id: insert.recordId,
882
+ ...serialized,
883
+ _created_at: operation.timestamp.wallTime,
884
+ _updated_at: operation.timestamp.wallTime,
885
+ _version: version,
886
+ // Every inserted field is stamped with this operation's version, so later
887
+ // per-field LWW compares against a real writer, not the row fallback.
888
+ _field_versions: stampFieldVersions(null, Object.keys(serialized), version)
889
+ };
890
+ const rowInsert = buildInsertQuery(insert.collection, row);
891
+ await scope.tx.execute(rowInsert.sql, rowInsert.params);
892
+ await appendOperationRow(scope, operation);
893
+ }
894
+ return {
895
+ operation,
896
+ record: {
897
+ id: insert.recordId,
898
+ // A richtext value written as a string returns as the bytes reads return.
899
+ ...toRichtextReadShape(insert.data, definition.fields),
900
+ createdAt: operation.timestamp.wallTime,
901
+ updatedAt: operation.timestamp.wallTime
902
+ },
903
+ sideEffects: []
904
+ };
905
+ }
906
+ async function writeUpdateInTx(env, scope, collection, id, data, extraCausalDeps = []) {
907
+ const definition = requireDefinition(env, collection);
908
+ const currentRow = await readLiveRow(scope, collection, id);
713
909
  if (!currentRow) {
714
- throw new RecordNotFoundError(ctx.collection, id);
715
- }
716
- let validated = (0, import_core8.validateRecord)(ctx.collection, ctx.definition, data, "update");
717
- const currentRecord = deserializeRecord(currentRow, ctx.definition.fields);
718
- validated = validateUpdateStateMachine(
719
- ctx.collection,
720
- id,
721
- ctx.definition,
722
- currentRecord,
723
- validated
724
- );
725
- if (Object.keys(validated).length === 0) {
726
- return { ...currentRecord };
910
+ throw new RecordNotFoundError(collection, id);
911
+ }
912
+ const currentRecord = deserializeRecord(currentRow, definition.fields);
913
+ const allowed = validateUpdateStateMachine(collection, id, definition, currentRecord, data);
914
+ if (Object.keys(allowed).length === 0) {
915
+ return { operation: null, record: currentRecord, sideEffects: [] };
727
916
  }
728
917
  const previousData = {};
729
- const resolvedData = {};
918
+ const resolved = {};
730
919
  const atomicOps = {};
731
- for (const key of Object.keys(validated)) {
732
- const value = validated[key];
920
+ for (const [key, value] of Object.entries(allowed)) {
733
921
  previousData[key] = currentRecord[key];
734
- if ((0, import_core8.isAtomicOp)(value)) {
735
- resolvedData[key] = (0, import_core8.resolveAtomicOp)(currentRecord[key], value);
736
- atomicOps[key] = (0, import_core8.toAtomicOp)(value);
922
+ if ((0, import_core10.isAtomicOp)(value)) {
923
+ resolved[key] = (0, import_core10.resolveAtomicOp)(currentRecord[key], value);
924
+ const descriptor = definition.fields[key];
925
+ if (descriptor) (0, import_core10.assertResolvedFieldValue)(collection, key, descriptor, resolved[key]);
926
+ atomicOps[key] = (0, import_core10.toAtomicOp)(value);
737
927
  } else {
738
- resolvedData[key] = value;
928
+ resolved[key] = value;
739
929
  }
740
930
  }
741
- const writeData = await (0, import_core8.transformSecretFieldsForWrite)(
742
- resolvedData,
743
- ctx.definition,
744
- ctx.secretKeyProvider
745
- );
746
- const hasAtomicOps = Object.keys(atomicOps).length > 0;
747
- const causalDeps = resolveCausalDeps(ctx);
748
- let operation;
749
- await ctx.adapter.transaction(async (tx) => {
750
- const sequenceNumber = await allocateNextSequenceInTransaction(tx, ctx.nodeId);
751
- operation = await (0, import_core8.createOperation)(
752
- {
753
- nodeId: ctx.nodeId,
754
- type: "update",
755
- collection: ctx.collection,
756
- recordId: id,
757
- // Binary richtext values (new and previous) are tagged as canonical
758
- // JSON BEFORE the operation is content-hashed, so the hash input,
759
- // persisted JSON, and wire payload are the identical value.
760
- data: encodeRichtextFieldsForOpData(writeData, ctx.definition.fields),
761
- previousData: encodeRichtextFieldsForOpData(previousData, ctx.definition.fields),
762
- sequenceNumber,
763
- causalDeps,
764
- schemaVersion: ctx.schema.version,
765
- ...hasAtomicOps ? { atomicOps } : {}
766
- },
767
- ctx.clock
768
- );
769
- ctx.causalTracker?.afterOperation(ctx.collection, operation.id, ctx.inTransaction);
770
- const serializedChanges = serializeRecord(writeData, ctx.definition.fields);
931
+ const writeData = await toAtRestWriteData(resolved, definition, env.secretKeyProvider);
932
+ const operation = await buildLocalOperation(env, scope, {
933
+ type: "update",
934
+ collection,
935
+ recordId: id,
936
+ data: encodeRichtextFieldsForOpData(writeData, definition.fields),
937
+ previousData: encodeRichtextFieldsForOpData(previousData, definition.fields),
938
+ extraCausalDeps,
939
+ ...Object.keys(atomicOps).length > 0 ? { atomicOps } : {}
940
+ });
941
+ if (env.fold) {
942
+ await appendOperationRow(scope, operation);
943
+ await env.fold.applyInTx(scope.tx, operation, "local");
944
+ } else {
945
+ const serializedChanges = serializeRecord(writeData, definition.fields);
771
946
  const version = serializeRowVersion(operation.timestamp);
772
- const mergedFieldVersions = parseFieldVersions(currentRow._field_versions);
773
- for (const changedField of Object.keys(serializedChanges)) {
774
- mergedFieldVersions[changedField] = version;
775
- }
776
- const updateQuery = buildUpdateQuery(ctx.collection, id, {
947
+ const rowUpdate = buildUpdateQuery(collection, id, {
777
948
  ...serializedChanges,
778
949
  _updated_at: operation.timestamp.wallTime,
779
950
  _version: version,
780
- _field_versions: serializeFieldVersions(mergedFieldVersions)
951
+ _field_versions: stampFieldVersions(
952
+ currentRow._field_versions,
953
+ Object.keys(serializedChanges),
954
+ version
955
+ )
781
956
  });
782
- const opInsert = buildInsertQuery(
783
- `_kora_ops_${ctx.collection}`,
784
- serializeOperation(operation)
785
- );
786
- await tx.execute(updateQuery.sql, updateQuery.params);
787
- await tx.execute(opInsert.sql, opInsert.params);
957
+ await scope.tx.execute(rowUpdate.sql, rowUpdate.params);
958
+ await appendOperationRow(scope, operation);
959
+ }
960
+ const updatedRow = await readLiveRow(scope, collection, id);
961
+ if (!updatedRow) {
962
+ throw new RecordNotFoundError(collection, id);
963
+ }
964
+ return {
965
+ operation,
966
+ record: deserializeRecord(updatedRow, definition.fields),
967
+ sideEffects: []
968
+ };
969
+ }
970
+ async function writeDeleteInTx(env, scope, collection, id, options = {}) {
971
+ const definition = requireDefinition(env, collection);
972
+ const currentRow = await readLiveRow(scope, collection, id);
973
+ if (!currentRow) {
974
+ throw new RecordNotFoundError(collection, id);
975
+ }
976
+ const operation = await buildLocalOperation(env, scope, {
977
+ type: "delete",
978
+ collection,
979
+ recordId: id,
980
+ data: null,
981
+ previousData: null,
982
+ extraCausalDeps: options.extraCausalDeps ?? []
788
983
  });
789
- ctx.onMutation(ctx.collection, operation);
790
- const rows = await ctx.adapter.query(
791
- `SELECT * FROM ${(0, import_core8.quoteIdent)(ctx.collection)} WHERE id = ? AND _deleted = 0`,
984
+ if (!options.skipReferentialEnforcement && env.beforeLocalDelete) {
985
+ await env.beforeLocalDelete(operation, scope.tx);
986
+ }
987
+ if (env.fold) {
988
+ await appendOperationRow(scope, operation);
989
+ await env.fold.applyInTx(scope.tx, operation, "local");
990
+ } else {
991
+ const version = serializeRowVersion(operation.timestamp);
992
+ const softDelete = buildSoftDeleteQuery(collection, id, operation.timestamp.wallTime, version);
993
+ await scope.tx.execute(softDelete.sql, softDelete.params);
994
+ await appendOperationRow(scope, operation);
995
+ }
996
+ const sideEffects = [];
997
+ if (!options.skipReferentialEnforcement && env.relationEnforcer) {
998
+ const result = await env.relationEnforcer.enforceDelete(
999
+ collection,
1000
+ id,
1001
+ scope,
1002
+ env,
1003
+ operation.id
1004
+ );
1005
+ sideEffects.push(...result.operations);
1006
+ }
1007
+ return {
1008
+ operation,
1009
+ record: deserializeRecord(currentRow, definition.fields),
1010
+ sideEffects
1011
+ };
1012
+ }
1013
+ async function buildLocalOperation(env, scope, input) {
1014
+ env.assertLocalWriteAllowed?.();
1015
+ const sequenceNumber = await scope.sequence.take();
1016
+ const causalDeps = scope.causal.depsFor(input.collection, input.extraCausalDeps);
1017
+ const operation = await (0, import_core10.createOperation)(
1018
+ {
1019
+ nodeId: env.nodeId,
1020
+ type: input.type,
1021
+ collection: input.collection,
1022
+ recordId: input.recordId,
1023
+ data: input.data,
1024
+ previousData: input.previousData,
1025
+ sequenceNumber,
1026
+ causalDeps,
1027
+ schemaVersion: env.schema.version,
1028
+ ...input.atomicOps !== void 0 ? { atomicOps: input.atomicOps } : {},
1029
+ ...env.transactionId !== void 0 ? { transactionId: env.transactionId } : {},
1030
+ ...env.mutationName !== void 0 ? { mutationName: env.mutationName } : {}
1031
+ },
1032
+ env.clock
1033
+ );
1034
+ const maxBytes = env.maxOperationBytes ?? import_core10.DEFAULT_MAX_OPERATION_BYTES;
1035
+ const bytes = (0, import_core10.measureOperationBytes)(operation);
1036
+ if (bytes > maxBytes) {
1037
+ throw new import_core10.OperationTooLargeError(input.collection, input.recordId, bytes, maxBytes);
1038
+ }
1039
+ scope.causal.record(input.collection, operation.id);
1040
+ return operation;
1041
+ }
1042
+ async function appendOperationRow(scope, operation) {
1043
+ const opInsert = buildInsertQuery(
1044
+ `_kora_ops_${operation.collection}`,
1045
+ serializeOperation(operation)
1046
+ );
1047
+ await scope.tx.execute(opInsert.sql, opInsert.params);
1048
+ }
1049
+ async function readLiveRow(scope, collection, id) {
1050
+ const rows = await scope.tx.query(
1051
+ `SELECT * FROM ${(0, import_core10.quoteIdent)(collection)} WHERE id = ? AND _deleted = 0`,
792
1052
  [id]
793
1053
  );
794
- const row = rows[0];
795
- if (!row) {
796
- throw new RecordNotFoundError(ctx.collection, id);
1054
+ return rows[0];
1055
+ }
1056
+ function requireDefinition(env, collection) {
1057
+ const definition = env.schema.collections[collection];
1058
+ if (!definition) {
1059
+ throw new import_core10.KoraError(
1060
+ `Unknown collection "${collection}". Available: ${Object.keys(env.schema.collections).join(", ")}`,
1061
+ "UNKNOWN_COLLECTION",
1062
+ { collection }
1063
+ );
797
1064
  }
798
- return deserializeRecord(row, ctx.definition.fields);
1065
+ return definition;
1066
+ }
1067
+
1068
+ // src/mutations/execute-insert.ts
1069
+ async function executeInsert(ctx, data) {
1070
+ const prepared = await prepareInsert(ctx, ctx.collection, data);
1071
+ const result = await runLocalWrite(
1072
+ ctx,
1073
+ (scope) => writeInsertInTx(ctx, scope, prepared, ctx.extraCausalDeps ?? [])
1074
+ );
1075
+ return result.record;
1076
+ }
1077
+
1078
+ // src/mutations/execute-update.ts
1079
+ var import_core11 = require("@korajs/core");
1080
+ async function executeUpdate(ctx, id, data) {
1081
+ const validated = (0, import_core11.validateRecord)(ctx.collection, ctx.definition, data, "update");
1082
+ const result = await runLocalWrite(
1083
+ ctx,
1084
+ (scope) => writeUpdateInTx(ctx, scope, ctx.collection, id, validated, ctx.extraCausalDeps ?? [])
1085
+ );
1086
+ return result.record;
799
1087
  }
800
1088
 
801
1089
  // src/mutations/execute-delete.ts
802
- var import_core9 = require("@korajs/core");
803
1090
  async function executeDelete(ctx, id, options) {
804
- const currentRows = await ctx.adapter.query(
805
- `SELECT * FROM ${(0, import_core9.quoteIdent)(ctx.collection)} WHERE id = ? AND _deleted = 0`,
806
- [id]
1091
+ const result = await runLocalWrite(
1092
+ ctx,
1093
+ (scope) => writeDeleteInTx(ctx, scope, ctx.collection, id, {
1094
+ skipReferentialEnforcement: options?.skipReferentialEnforcement ?? false,
1095
+ extraCausalDeps: ctx.extraCausalDeps ?? []
1096
+ })
807
1097
  );
808
- if (!currentRows[0]) {
809
- throw new RecordNotFoundError(ctx.collection, id);
810
- }
811
- const causalDeps = resolveCausalDeps(ctx);
812
- let operation = options?.operation;
813
- const cascadedOps = [];
814
- await ctx.adapter.transaction(async (tx) => {
815
- if (!operation) {
816
- const sequenceNumber = await allocateNextSequenceInTransaction(tx, ctx.nodeId);
817
- operation = await (0, import_core9.createOperation)(
818
- {
819
- nodeId: ctx.nodeId,
820
- type: "delete",
821
- collection: ctx.collection,
822
- recordId: id,
823
- data: null,
824
- previousData: null,
825
- sequenceNumber,
826
- causalDeps,
827
- schemaVersion: ctx.schema.version
828
- },
829
- ctx.clock
830
- );
831
- ctx.causalTracker?.afterOperation(ctx.collection, operation.id, ctx.inTransaction);
1098
+ return result.operation ? [result.operation, ...result.sideEffects] : result.sideEffects;
1099
+ }
1100
+
1101
+ // src/sync/sync-state.ts
1102
+ var import_core12 = require("@korajs/core");
1103
+
1104
+ // src/sync/sync-durability.ts
1105
+ var OWN_ACKED_THROUGH_META_KEY = "own_acked_through";
1106
+
1107
+ // src/sync/local-sync-records.ts
1108
+ var TERMINAL_REJECTIONS_TABLE = "_kora_terminal_rejections";
1109
+ var LOCAL_NODES_TABLE = "_kora_local_nodes";
1110
+ var TERMINAL_REJECTIONS_DDL = `CREATE TABLE IF NOT EXISTS ${TERMINAL_REJECTIONS_TABLE} (
1111
+ operation_id TEXT PRIMARY KEY NOT NULL,
1112
+ node_id TEXT,
1113
+ sequence_number INTEGER,
1114
+ code TEXT NOT NULL,
1115
+ rejected_at INTEGER NOT NULL
1116
+ )`;
1117
+ var LOCAL_NODES_DDL = `CREATE TABLE IF NOT EXISTS ${LOCAL_NODES_TABLE} (
1118
+ node_id TEXT PRIMARY KEY NOT NULL,
1119
+ created_at INTEGER NOT NULL,
1120
+ accepted INTEGER NOT NULL DEFAULT 0,
1121
+ held INTEGER NOT NULL DEFAULT 0,
1122
+ refused_cycle INTEGER,
1123
+ principal TEXT,
1124
+ binding TEXT,
1125
+ refused_principals TEXT
1126
+ )`;
1127
+ async function hasUnsyncedOwnOperations(db) {
1128
+ const tables = new Set(
1129
+ (await db.query("SELECT name FROM sqlite_master WHERE type = 'table'")).map(
1130
+ (row) => row.name
1131
+ )
1132
+ );
1133
+ const opsTables = [...tables].filter((name) => name.startsWith("_kora_ops_"));
1134
+ if (opsTables.length === 0 || !tables.has("_kora_meta")) return false;
1135
+ const nodes = /* @__PURE__ */ new Set();
1136
+ if (tables.has(LOCAL_NODES_TABLE)) {
1137
+ for (const row of await db.query(
1138
+ `SELECT node_id FROM ${LOCAL_NODES_TABLE}`
1139
+ )) {
1140
+ nodes.add(row.node_id);
832
1141
  }
833
- if (ctx.relationEnforcer && !options?.skipReferentialEnforcement) {
834
- const enforcementResult = await ctx.relationEnforcer.enforceDelete(ctx.collection, id, tx, [
835
- operation.id
836
- ]);
837
- cascadedOps.push(...enforcementResult.operations);
1142
+ }
1143
+ const meta = await db.query(
1144
+ "SELECT key, value FROM _kora_meta WHERE key = 'node_id' OR key = ? OR key LIKE ?",
1145
+ [OWN_ACKED_THROUGH_META_KEY, `${OWN_ACKED_THROUGH_META_KEY}:%`]
1146
+ );
1147
+ const prefixes = /* @__PURE__ */ new Map();
1148
+ for (const row of meta) {
1149
+ if (row.key === "node_id") {
1150
+ nodes.add(row.value);
1151
+ } else if (row.key === OWN_ACKED_THROUGH_META_KEY) {
1152
+ try {
1153
+ const parsed = JSON.parse(row.value);
1154
+ if (typeof parsed.nodeId === "string") {
1155
+ nodes.add(parsed.nodeId);
1156
+ if (typeof parsed.sequence === "number") prefixes.set(parsed.nodeId, parsed.sequence);
1157
+ }
1158
+ } catch {
1159
+ }
1160
+ } else {
1161
+ const nodeId = row.key.slice(OWN_ACKED_THROUGH_META_KEY.length + 1);
1162
+ const sequence = Number(row.value);
1163
+ if (!prefixes.has(nodeId) && Number.isInteger(sequence)) prefixes.set(nodeId, sequence);
838
1164
  }
839
- const version = serializeRowVersion(operation.timestamp);
840
- const deleteQuery = buildSoftDeleteQuery(
841
- ctx.collection,
842
- id,
843
- operation.timestamp.wallTime,
844
- version
845
- );
846
- const opInsert = buildInsertQuery(
847
- `_kora_ops_${ctx.collection}`,
848
- serializeOperation(operation)
849
- );
850
- await tx.execute(deleteQuery.sql, deleteQuery.params);
851
- await tx.execute(opInsert.sql, opInsert.params);
852
- });
853
- if (!operation) {
854
- throw new Error("Delete operation was not created");
855
1165
  }
856
- ctx.onMutation(ctx.collection, operation);
857
- for (const cascadedOp of cascadedOps) {
858
- ctx.onMutation(cascadedOp.collection, cascadedOp);
1166
+ const exclude = tables.has(TERMINAL_REJECTIONS_TABLE) ? ` AND id NOT IN (SELECT operation_id FROM ${TERMINAL_REJECTIONS_TABLE})` : "";
1167
+ for (const nodeId of nodes) {
1168
+ const prefix = prefixes.get(nodeId) ?? 0;
1169
+ for (const table of opsTables) {
1170
+ const rows = await db.query(
1171
+ `SELECT COUNT(*) AS n FROM "${table.replace(/"/g, '""')}" WHERE node_id = ? AND sequence_number > ?${exclude}`,
1172
+ [nodeId, prefix]
1173
+ );
1174
+ if (Number(rows[0]?.n ?? 0) > 0) return true;
1175
+ }
859
1176
  }
860
- return [operation, ...cascadedOps];
1177
+ return false;
861
1178
  }
862
1179
  // Annotate the CommonJS export names for ESM import in node:
863
1180
  0 && (module.exports = {
@@ -875,8 +1192,8 @@ async function executeDelete(ctx, id, options) {
875
1192
  executeDelete,
876
1193
  executeInsert,
877
1194
  executeUpdate,
1195
+ hasUnsyncedOwnOperations,
878
1196
  isIncomingNewerThanRow,
879
- resolveCausalDeps,
880
1197
  rowVersionFromRecord,
881
1198
  serializeOperation,
882
1199
  serializeRecord,