@korajs/core 0.3.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -20,36 +20,70 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ APPLY_FAILURE_CODES: () => APPLY_FAILURE_CODES,
24
+ APPLY_RESULTS: () => APPLY_RESULTS,
23
25
  ArrayFieldBuilder: () => ArrayFieldBuilder,
24
26
  CONNECTION_QUALITIES: () => CONNECTION_QUALITIES,
27
+ CausalTracker: () => CausalTracker,
25
28
  ClockDriftError: () => ClockDriftError,
26
29
  EnumFieldBuilder: () => EnumFieldBuilder,
27
30
  FieldBuilder: () => FieldBuilder,
28
31
  HybridLogicalClock: () => HybridLogicalClock,
32
+ KORA_ERROR_FIX_SUGGESTIONS: () => KORA_ERROR_FIX_SUGGESTIONS,
29
33
  KoraError: () => KoraError,
30
34
  MERGE_STRATEGIES: () => MERGE_STRATEGIES,
31
35
  MergeConflictError: () => MergeConflictError,
36
+ MigrationBuilder: () => MigrationBuilder,
37
+ MigrationRollbackError: () => MigrationRollbackError,
32
38
  OperationError: () => OperationError,
39
+ RollbackBuilder: () => RollbackBuilder,
33
40
  SchemaValidationError: () => SchemaValidationError,
34
41
  StorageError: () => StorageError,
35
42
  SyncError: () => SyncError,
36
43
  advanceVector: () => advanceVector,
44
+ applyOperationTransforms: () => applyOperationTransforms,
45
+ buildScopeMap: () => buildScopeMap,
46
+ buildStateMachineConstraints: () => buildStateMachineConstraints,
47
+ canAutoRollback: () => canAutoRollback,
48
+ collectSchemaScopeFields: () => collectSchemaScopeFields,
49
+ collectSchemaScopeValueKeys: () => collectSchemaScopeValueKeys,
37
50
  computeDelta: () => computeDelta,
38
51
  createOperation: () => createOperation,
52
+ createReversibleMigration: () => createReversibleMigration,
39
53
  createVersionVector: () => createVersionVector,
54
+ defaultApplyFailureReason: () => defaultApplyFailureReason,
55
+ defaultSequenceFormat: () => defaultSequenceFormat,
40
56
  defineSchema: () => defineSchema,
41
57
  deserializeVector: () => deserializeVector,
42
58
  dominates: () => dominates,
59
+ extractScopeValuesFromClaims: () => extractScopeValuesFromClaims,
43
60
  extractTimestamp: () => extractTimestamp,
61
+ formatSequenceValue: () => formatSequenceValue,
44
62
  generateFullDDL: () => generateFullDDL,
63
+ generateProtoDefinitions: () => generateProtoDefinitions,
64
+ generateRollbackSteps: () => generateRollbackSteps,
45
65
  generateSQL: () => generateSQL,
46
66
  generateUUIDv7: () => generateUUIDv7,
67
+ getCollectionScopeBindings: () => getCollectionScopeBindings,
68
+ getKoraErrorFix: () => getKoraErrorFix,
69
+ getTransitionMap: () => getTransitionMap,
70
+ hasSchemaSyncRules: () => hasSchemaSyncRules,
71
+ isApplyFailure: () => isApplyFailure,
72
+ isAtomicOp: () => isAtomicOp,
73
+ isCollectionSyncScoped: () => isCollectionSyncScoped,
47
74
  isValidOperation: () => isValidOperation,
48
75
  isValidUUIDv7: () => isValidUUIDv7,
49
76
  mergeVectors: () => mergeVectors,
77
+ migrate: () => migrate,
78
+ migrationStepsToSQL: () => migrationStepsToSQL,
79
+ op: () => op,
80
+ resolveAtomicOp: () => resolveAtomicOp,
81
+ rollbackStepsToSQL: () => rollbackStepsToSQL,
50
82
  serializeVector: () => serializeVector,
51
83
  t: () => t,
84
+ toAtomicOp: () => toAtomicOp,
52
85
  validateRecord: () => validateRecord,
86
+ validateTransition: () => validateTransition,
53
87
  vectorsEqual: () => vectorsEqual,
54
88
  verifyOperationIntegrity: () => verifyOperationIntegrity
55
89
  });
@@ -65,6 +99,23 @@ var MERGE_STRATEGIES = [
65
99
  ];
66
100
  var CONNECTION_QUALITIES = ["excellent", "good", "fair", "poor", "offline"];
67
101
 
102
+ // src/errors/error-fixes.ts
103
+ var KORA_ERROR_FIX_SUGGESTIONS = {
104
+ SCHEMA_VALIDATION: "Review your defineSchema() definition: every collection needs at least one field and a positive version.",
105
+ OPERATION_ERROR: "Check the operation payload matches your schema field types and required fields.",
106
+ MERGE_CONFLICT: "Add a custom resolver for the conflicting field in defineSchema(), or adjust constraint onConflict rules.",
107
+ SYNC_ERROR: "Verify the sync server URL, auth token, and that the server accepts your schema version.",
108
+ STORAGE_ERROR: "Confirm the storage adapter is supported in this environment and the database path is writable.",
109
+ CLOCK_DRIFT: "Sync device wall-clock time (NTP). Kora refuses new timestamps when drift exceeds five minutes.",
110
+ PERSISTENCE_ERROR: "IndexedDB persistence failed; check browser storage settings and available disk quota.",
111
+ MISSING_WORKER_URL: "Pass store.workerUrl pointing to sqlite-wasm-worker.js (see create-kora-app templates).",
112
+ INVALID_SYNC_URL: "Use ws:// or wss:// for WebSocket transport, or http(s):// for HTTP long-polling.",
113
+ APP_NOT_READY: "Await app.ready before querying or mutating collections, or wrap the tree in <KoraProvider app={app}>."
114
+ };
115
+ function getKoraErrorFix(code) {
116
+ return KORA_ERROR_FIX_SUGGESTIONS[code];
117
+ }
118
+
68
119
  // src/errors/errors.ts
69
120
  var KoraError = class extends Error {
70
121
  constructor(message, code, context) {
@@ -75,6 +126,16 @@ var KoraError = class extends Error {
75
126
  }
76
127
  code;
77
128
  context;
129
+ /**
130
+ * Actionable hint for resolving this error (from registry or error context).
131
+ */
132
+ get fix() {
133
+ const fromContext = this.context?.fix;
134
+ if (typeof fromContext === "string" && fromContext.length > 0) {
135
+ return fromContext;
136
+ }
137
+ return getKoraErrorFix(this.code);
138
+ }
78
139
  };
79
140
  var SchemaValidationError = class extends KoraError {
80
141
  constructor(message, context) {
@@ -172,6 +233,7 @@ var HybridLogicalClock = class {
172
233
  */
173
234
  receive(remote) {
174
235
  const physicalTime = this.timeSource.now();
236
+ const wasColdStart = this.wallTime === 0;
175
237
  if (physicalTime > this.wallTime && physicalTime > remote.wallTime) {
176
238
  this.wallTime = physicalTime;
177
239
  this.logical = 0;
@@ -183,7 +245,9 @@ var HybridLogicalClock = class {
183
245
  } else {
184
246
  this.logical++;
185
247
  }
186
- this.checkDrift(physicalTime);
248
+ if (!wasColdStart) {
249
+ this.checkDrift(physicalTime);
250
+ }
187
251
  return { wallTime: this.wallTime, logical: this.logical, nodeId: this.nodeId };
188
252
  }
189
253
  /**
@@ -232,6 +296,50 @@ var HybridLogicalClock = class {
232
296
  }
233
297
  };
234
298
 
299
+ // src/causal/causal-tracker.ts
300
+ var CausalTracker = class {
301
+ lastOpIdByCollection = /* @__PURE__ */ new Map();
302
+ lastTransactionOpId = null;
303
+ /**
304
+ * Start a new transaction boundary. Clears in-transaction op ids.
305
+ */
306
+ beginTransaction() {
307
+ this.lastTransactionOpId = null;
308
+ }
309
+ /**
310
+ * Clear the transaction boundary without recording ops (after rollback).
311
+ */
312
+ clearTransaction() {
313
+ this.lastTransactionOpId = null;
314
+ }
315
+ /**
316
+ * Compute causal dependencies for the next operation in a collection.
317
+ * Uses direct parents only: collection head and the previous op in the open transaction.
318
+ */
319
+ nextCausalDeps(collection, inTransaction) {
320
+ const deps = [];
321
+ const lastInCollection = this.lastOpIdByCollection.get(collection);
322
+ if (lastInCollection !== void 0) {
323
+ deps.push(lastInCollection);
324
+ }
325
+ if (inTransaction && this.lastTransactionOpId !== null) {
326
+ if (!deps.includes(this.lastTransactionOpId)) {
327
+ deps.push(this.lastTransactionOpId);
328
+ }
329
+ }
330
+ return deps;
331
+ }
332
+ /**
333
+ * Record an operation after it has been created and assigned an id.
334
+ */
335
+ afterOperation(collection, operationId, inTransaction) {
336
+ this.lastOpIdByCollection.set(collection, operationId);
337
+ if (inTransaction) {
338
+ this.lastTransactionOpId = operationId;
339
+ }
340
+ }
341
+ };
342
+
235
343
  // src/identifiers/uuid-v7.ts
236
344
  var defaultRandom = globalThis.crypto;
237
345
  function generateUUIDv7(timestamp = Date.now(), randomSource = defaultRandom) {
@@ -268,23 +376,61 @@ function formatUUID(bytes) {
268
376
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
269
377
  }
270
378
 
379
+ // src/operations/apply-result.ts
380
+ var APPLY_RESULTS = ["applied", "duplicate", "skipped", "rejected", "deferred"];
381
+ var APPLY_FAILURE_CODES = {
382
+ APPLY_FAILED: "APPLY_FAILED",
383
+ APPLY_SKIPPED: "APPLY_SKIPPED",
384
+ APPLY_REJECTED: "APPLY_REJECTED",
385
+ APPLY_DEFERRED: "APPLY_DEFERRED",
386
+ CLOCK_DRIFT: "CLOCK_DRIFT",
387
+ REFERENTIAL_INTEGRITY: "REFERENTIAL_INTEGRITY",
388
+ SCHEMA_MISMATCH: "SCHEMA_MISMATCH"
389
+ };
390
+ function isApplyFailure(result) {
391
+ return result === "skipped" || result === "rejected" || result === "deferred";
392
+ }
393
+ function defaultApplyFailureReason(result, overrides) {
394
+ const base = result === "deferred" ? {
395
+ code: APPLY_FAILURE_CODES.APPLY_DEFERRED,
396
+ message: "Operation apply was deferred.",
397
+ retriable: true
398
+ } : result === "rejected" ? {
399
+ code: APPLY_FAILURE_CODES.APPLY_REJECTED,
400
+ message: "Operation apply was rejected.",
401
+ retriable: false
402
+ } : {
403
+ code: APPLY_FAILURE_CODES.APPLY_SKIPPED,
404
+ message: "Operation apply was skipped.",
405
+ retriable: false
406
+ };
407
+ return { ...base, ...overrides };
408
+ }
409
+
271
410
  // src/operations/content-hash.ts
272
411
  async function computeOperationId(input, timestamp) {
273
- const canonical = canonicalize({
412
+ const hashInput = {
274
413
  type: input.type,
275
414
  collection: input.collection,
276
415
  recordId: input.recordId,
277
416
  data: input.data,
278
417
  timestamp,
279
418
  nodeId: input.nodeId
280
- });
419
+ };
420
+ if (input.atomicOps !== void 0 && Object.keys(input.atomicOps).length > 0) {
421
+ hashInput.atomicOps = input.atomicOps;
422
+ }
423
+ const canonical = canonicalize(hashInput);
281
424
  const encoded = new TextEncoder().encode(canonical);
282
425
  const hashBuffer = await globalThis.crypto.subtle.digest("SHA-256", encoded);
283
426
  return bufferToHex(hashBuffer);
284
427
  }
285
428
  function canonicalize(obj) {
286
- if (obj === null || obj === void 0) {
287
- return JSON.stringify(obj);
429
+ if (obj === null) {
430
+ return "null";
431
+ }
432
+ if (obj === void 0) {
433
+ return "null";
288
434
  }
289
435
  if (typeof obj !== "object") {
290
436
  return JSON.stringify(obj);
@@ -296,7 +442,7 @@ function canonicalize(obj) {
296
442
  const keys = Object.keys(obj).sort();
297
443
  const pairs = keys.map((key) => {
298
444
  const value = obj[key];
299
- return `${JSON.stringify(key)}:${canonicalize(value)}`;
445
+ return `${JSON.stringify(key)}:${canonicalize(value === void 0 ? null : value)}`;
300
446
  });
301
447
  return `{${pairs.join(",")}}`;
302
448
  }
@@ -322,7 +468,10 @@ async function createOperation(input, clock) {
322
468
  timestamp,
323
469
  sequenceNumber: input.sequenceNumber,
324
470
  causalDeps: [...input.causalDeps],
325
- schemaVersion: input.schemaVersion
471
+ schemaVersion: input.schemaVersion,
472
+ ...input.atomicOps !== void 0 && Object.keys(input.atomicOps).length > 0 ? { atomicOps: { ...input.atomicOps } } : {},
473
+ ...input.transactionId !== void 0 ? { transactionId: input.transactionId } : {},
474
+ ...input.mutationName !== void 0 ? { mutationName: input.mutationName } : {}
326
475
  };
327
476
  return deepFreeze(operation);
328
477
  }
@@ -390,29 +539,31 @@ function validateOperationParams(input) {
390
539
  });
391
540
  }
392
541
  }
393
- async function verifyOperationIntegrity(op) {
542
+ async function verifyOperationIntegrity(op2) {
394
543
  const input = {
395
- nodeId: op.nodeId,
396
- type: op.type,
397
- collection: op.collection,
398
- recordId: op.recordId,
399
- data: op.data,
400
- previousData: op.previousData,
401
- sequenceNumber: op.sequenceNumber,
402
- causalDeps: op.causalDeps,
403
- schemaVersion: op.schemaVersion
544
+ nodeId: op2.nodeId,
545
+ type: op2.type,
546
+ collection: op2.collection,
547
+ recordId: op2.recordId,
548
+ data: op2.data,
549
+ previousData: op2.previousData,
550
+ sequenceNumber: op2.sequenceNumber,
551
+ causalDeps: op2.causalDeps,
552
+ schemaVersion: op2.schemaVersion,
553
+ ...op2.atomicOps !== void 0 ? { atomicOps: op2.atomicOps } : {}
404
554
  };
405
- const serializedTs = HybridLogicalClock.serialize(op.timestamp);
555
+ const serializedTs = HybridLogicalClock.serialize(op2.timestamp);
406
556
  const expectedId = await computeOperationId(input, serializedTs);
407
- return op.id === expectedId;
557
+ return op2.id === expectedId;
408
558
  }
409
559
  function isValidOperation(value) {
410
560
  if (typeof value !== "object" || value === null) return false;
411
- const op = value;
412
- return typeof op.id === "string" && typeof op.nodeId === "string" && (op.type === "insert" || op.type === "update" || op.type === "delete") && typeof op.collection === "string" && typeof op.recordId === "string" && typeof op.sequenceNumber === "number" && Array.isArray(op.causalDeps) && typeof op.schemaVersion === "number" && typeof op.timestamp === "object" && op.timestamp !== null;
561
+ const op2 = value;
562
+ return typeof op2.id === "string" && typeof op2.nodeId === "string" && (op2.type === "insert" || op2.type === "update" || op2.type === "delete") && typeof op2.collection === "string" && typeof op2.recordId === "string" && typeof op2.sequenceNumber === "number" && Array.isArray(op2.causalDeps) && typeof op2.schemaVersion === "number" && typeof op2.timestamp === "object" && op2.timestamp !== null;
413
563
  }
414
564
  function deepFreeze(obj) {
415
565
  if (typeof obj !== "object" || obj === null) return obj;
566
+ if (ArrayBuffer.isView(obj)) return obj;
416
567
  Object.freeze(obj);
417
568
  for (const value of Object.values(obj)) {
418
569
  if (typeof value === "object" && value !== null && !Object.isFrozen(value)) {
@@ -422,10 +573,257 @@ function deepFreeze(obj) {
422
573
  return obj;
423
574
  }
424
575
 
576
+ // src/operations/atomic-ops.ts
577
+ var KORA_ATOMIC_OP = /* @__PURE__ */ Symbol.for("kora.atomic_op");
578
+ function isAtomicOp(value) {
579
+ return typeof value === "object" && value !== null && KORA_ATOMIC_OP in value && value[KORA_ATOMIC_OP] === true;
580
+ }
581
+ function resolveAtomicOp(currentValue, sentinel) {
582
+ switch (sentinel.type) {
583
+ case "increment": {
584
+ const current = typeof currentValue === "number" ? currentValue : 0;
585
+ const operand = sentinel.value;
586
+ return current + operand;
587
+ }
588
+ case "max": {
589
+ const current = typeof currentValue === "number" ? currentValue : Number.NEGATIVE_INFINITY;
590
+ return Math.max(current, sentinel.value);
591
+ }
592
+ case "min": {
593
+ const current = typeof currentValue === "number" ? currentValue : Number.POSITIVE_INFINITY;
594
+ return Math.min(current, sentinel.value);
595
+ }
596
+ case "append": {
597
+ const current = Array.isArray(currentValue) ? [...currentValue] : [];
598
+ current.push(sentinel.value);
599
+ return current;
600
+ }
601
+ case "remove": {
602
+ if (!Array.isArray(currentValue)) return [];
603
+ return currentValue.filter((item) => item !== sentinel.value);
604
+ }
605
+ }
606
+ }
607
+ function createSentinel(type, value) {
608
+ return Object.freeze({
609
+ [KORA_ATOMIC_OP]: true,
610
+ type,
611
+ value
612
+ });
613
+ }
614
+ var op = {
615
+ /**
616
+ * Increment a number field by the given amount.
617
+ * Concurrent increments compose: the sum of all deltas is applied to the base.
618
+ *
619
+ * @param n - The amount to increment (use negative values to decrement)
620
+ */
621
+ increment(n) {
622
+ return createSentinel("increment", n);
623
+ },
624
+ /**
625
+ * Decrement a number field by the given amount.
626
+ * Syntactic sugar for `op.increment(-n)`.
627
+ *
628
+ * @param n - The amount to decrement
629
+ */
630
+ decrement(n) {
631
+ return createSentinel("increment", -n);
632
+ },
633
+ /**
634
+ * Set the field to the maximum of the current value and the given value.
635
+ * Concurrent max operations take the maximum of all values.
636
+ *
637
+ * @param n - The value to compare against the current value
638
+ */
639
+ max(n) {
640
+ return createSentinel("max", n);
641
+ },
642
+ /**
643
+ * Set the field to the minimum of the current value and the given value.
644
+ * Concurrent min operations take the minimum of all values.
645
+ *
646
+ * @param n - The value to compare against the current value
647
+ */
648
+ min(n) {
649
+ return createSentinel("min", n);
650
+ },
651
+ /**
652
+ * Append an item to an array field.
653
+ * Concurrent appends include all appended items.
654
+ *
655
+ * @param item - The item to append to the array
656
+ */
657
+ append(item) {
658
+ return createSentinel("append", item);
659
+ },
660
+ /**
661
+ * Remove an item from an array field (by value equality).
662
+ * Concurrent removes of the same item are idempotent.
663
+ *
664
+ * @param item - The item to remove from the array
665
+ */
666
+ remove(item) {
667
+ return createSentinel("remove", item);
668
+ }
669
+ };
670
+ function toAtomicOp(sentinel) {
671
+ return { type: sentinel.type, value: sentinel.value };
672
+ }
673
+
674
+ // src/scopes/sync-scope-bindings.ts
675
+ function hasSchemaSyncRules(schema) {
676
+ return schema.sync !== void 0 && Object.keys(schema.sync).length > 0;
677
+ }
678
+ function getCollectionScopeBindings(schema, collectionName) {
679
+ const syncRule = schema.sync?.[collectionName];
680
+ if (syncRule && Object.keys(syncRule.where).length > 0) {
681
+ return { ...syncRule.where };
682
+ }
683
+ const scopeFields = schema.collections[collectionName]?.scope ?? [];
684
+ if (scopeFields.length === 0) {
685
+ return null;
686
+ }
687
+ const bindings = {};
688
+ for (const field of scopeFields) {
689
+ bindings[field] = field;
690
+ }
691
+ return bindings;
692
+ }
693
+ function isCollectionSyncScoped(schema, collectionName) {
694
+ if (hasSchemaSyncRules(schema)) {
695
+ if (schema.sync?.[collectionName]) {
696
+ return true;
697
+ }
698
+ const legacyScope = schema.collections[collectionName]?.scope ?? [];
699
+ return legacyScope.length > 0;
700
+ }
701
+ return getCollectionScopeBindings(schema, collectionName) !== null;
702
+ }
703
+ function collectSchemaScopeValueKeys(schema) {
704
+ const keys = /* @__PURE__ */ new Set();
705
+ for (const collection of Object.values(schema.collections)) {
706
+ for (const field of collection.scope) {
707
+ keys.add(field);
708
+ }
709
+ }
710
+ if (schema.sync) {
711
+ for (const rule of Object.values(schema.sync)) {
712
+ for (const scopeKey of Object.values(rule.where)) {
713
+ keys.add(scopeKey);
714
+ }
715
+ }
716
+ }
717
+ return [...keys];
718
+ }
719
+ function normalizeSyncRuleWhere(collectionName, where) {
720
+ const normalized = {};
721
+ for (const [field, scopeKey] of Object.entries(where)) {
722
+ if (scopeKey === true) {
723
+ normalized[field] = field;
724
+ continue;
725
+ }
726
+ if (typeof scopeKey === "string" && scopeKey.length > 0) {
727
+ normalized[field] = scopeKey;
728
+ continue;
729
+ }
730
+ throw new Error(
731
+ `Invalid sync rule for collection "${collectionName}": where.${field} must be true or a non-empty scope key string`
732
+ );
733
+ }
734
+ return normalized;
735
+ }
736
+
737
+ // src/state-machine/state-machine.ts
738
+ function validateTransition(constraint, fromValue, toValue) {
739
+ const from = String(fromValue);
740
+ const to = String(toValue);
741
+ const allowedTargets = constraint.transitions[from] ?? [];
742
+ return {
743
+ valid: allowedTargets.includes(to),
744
+ from,
745
+ to,
746
+ field: constraint.field,
747
+ collection: constraint.collection,
748
+ allowedTargets
749
+ };
750
+ }
751
+ function buildStateMachineConstraints(schema) {
752
+ const constraints = [];
753
+ for (const [collectionName, collection] of Object.entries(schema.collections)) {
754
+ for (const [fieldName, descriptor] of Object.entries(collection.fields)) {
755
+ if (descriptor.kind === "enum" && descriptor.transitions !== null) {
756
+ constraints.push({
757
+ field: fieldName,
758
+ collection: collectionName,
759
+ transitions: descriptor.transitions
760
+ });
761
+ }
762
+ }
763
+ }
764
+ return constraints;
765
+ }
766
+ function getTransitionMap(schema, collection, field) {
767
+ const collectionDef = schema.collections[collection];
768
+ if (!collectionDef) {
769
+ return null;
770
+ }
771
+ const fieldDef = collectionDef.fields[field];
772
+ if (!fieldDef || fieldDef.kind !== "enum") {
773
+ return null;
774
+ }
775
+ return fieldDef.transitions;
776
+ }
777
+ function validateStateMachineDefinition(collectionName, sm, fields) {
778
+ const fieldDef = fields[sm.field];
779
+ if (!fieldDef) {
780
+ throw new SchemaValidationError(
781
+ `State machine field "${sm.field}" does not exist in collection "${collectionName}". Available fields: ${Object.keys(fields).join(", ")}`,
782
+ { collection: collectionName, field: sm.field }
783
+ );
784
+ }
785
+ if (fieldDef.kind !== "enum") {
786
+ throw new SchemaValidationError(
787
+ `State machine field "${sm.field}" in collection "${collectionName}" must be an enum field, but got "${fieldDef.kind}"`,
788
+ { collection: collectionName, field: sm.field, kind: fieldDef.kind }
789
+ );
790
+ }
791
+ const enumValues = fieldDef.enumValues;
792
+ if (!enumValues || enumValues.length === 0) {
793
+ throw new SchemaValidationError(
794
+ `State machine field "${sm.field}" in collection "${collectionName}" has no enum values defined`,
795
+ { collection: collectionName, field: sm.field }
796
+ );
797
+ }
798
+ const validValues = new Set(enumValues);
799
+ for (const [state, targets] of Object.entries(sm.transitions)) {
800
+ if (!validValues.has(state)) {
801
+ throw new SchemaValidationError(
802
+ `State machine transition source "${state}" is not a valid enum value for field "${sm.field}" in collection "${collectionName}". Valid values: ${[...validValues].join(", ")}`,
803
+ { collection: collectionName, field: sm.field, state }
804
+ );
805
+ }
806
+ for (const target of targets) {
807
+ if (!validValues.has(target)) {
808
+ throw new SchemaValidationError(
809
+ `State machine transition target "${target}" is not a valid enum value for field "${sm.field}" in collection "${collectionName}". Valid values: ${[...validValues].join(", ")}`,
810
+ { collection: collectionName, field: sm.field, state, target }
811
+ );
812
+ }
813
+ }
814
+ }
815
+ if (sm.onInvalidTransition !== "reject" && sm.onInvalidTransition !== "last-valid-state") {
816
+ throw new SchemaValidationError(
817
+ `State machine onInvalidTransition must be "reject" or "last-valid-state", got "${sm.onInvalidTransition}" in collection "${collectionName}"`,
818
+ { collection: collectionName, onInvalidTransition: sm.onInvalidTransition }
819
+ );
820
+ }
821
+ }
822
+
425
823
  // src/schema/define.ts
426
824
  var COLLECTION_NAME_RE = /^[a-z][a-z0-9_]*$/;
427
825
  var FIELD_NAME_RE = /^[a-z][a-zA-Z0-9_]*$/;
428
- var RESERVED_FIELDS = /* @__PURE__ */ new Set(["id", "_created_at", "_updated_at", "_deleted"]);
826
+ var RESERVED_FIELDS = /* @__PURE__ */ new Set(["id", "_created_at", "_updated_at", "_version", "_deleted"]);
429
827
  function defineSchema(input) {
430
828
  validateVersion(input.version);
431
829
  const collections = {};
@@ -443,7 +841,40 @@ function defineSchema(input) {
443
841
  relations[name] = { ...relationInput };
444
842
  }
445
843
  }
446
- return { version: input.version, collections, relations };
844
+ const migrations = {};
845
+ if (input.migrations) {
846
+ for (const [versionStr, migration] of Object.entries(input.migrations)) {
847
+ const version = Number(versionStr);
848
+ if (!Number.isInteger(version) || version < 2) {
849
+ throw new SchemaValidationError(
850
+ `Migration key "${versionStr}" is invalid. Must be an integer >= 2 (version 1 is the initial schema).`,
851
+ { version: versionStr }
852
+ );
853
+ }
854
+ if (version > input.version) {
855
+ throw new SchemaValidationError(
856
+ `Migration version ${version} exceeds schema version ${input.version}. Migrations must target versions <= schema version.`,
857
+ { migrationVersion: version, schemaVersion: input.version }
858
+ );
859
+ }
860
+ if (!migration.steps || migration.steps.length === 0) {
861
+ throw new SchemaValidationError(
862
+ `Migration for version ${version} has no steps. Use migrate() to define at least one step.`,
863
+ { version }
864
+ );
865
+ }
866
+ migrations[version] = migration;
867
+ }
868
+ }
869
+ const sync = buildSyncRules(input.sync, collections);
870
+ applySyncRulesToCollectionScope(collections, sync);
871
+ return {
872
+ version: input.version,
873
+ collections,
874
+ relations,
875
+ migrations,
876
+ ...sync ? { sync } : {}
877
+ };
447
878
  }
448
879
  function validateVersion(version) {
449
880
  if (typeof version !== "number" || !Number.isInteger(version) || version < 1) {
@@ -505,7 +936,81 @@ function buildCollection(name, input) {
505
936
  resolvers[fieldName] = resolver;
506
937
  }
507
938
  }
508
- return { fields, indexes, constraints, resolvers };
939
+ const scope = [];
940
+ if (input.scope) {
941
+ for (const scopeField of input.scope) {
942
+ if (!(scopeField in fields)) {
943
+ throw new SchemaValidationError(
944
+ `Scope field "${scopeField}" does not exist in collection "${name}". Available fields: ${Object.keys(fields).join(", ")}`,
945
+ { collection: name, field: scopeField }
946
+ );
947
+ }
948
+ scope.push(scopeField);
949
+ }
950
+ }
951
+ let stateMachine;
952
+ if (input.stateMachine) {
953
+ validateStateMachineDefinition(name, input.stateMachine, fields);
954
+ stateMachine = { ...input.stateMachine };
955
+ }
956
+ return { fields, indexes, constraints, resolvers, scope, stateMachine };
957
+ }
958
+ function buildSyncRules(syncInput, collections) {
959
+ if (!syncInput) {
960
+ return void 0;
961
+ }
962
+ const sync = {};
963
+ for (const [collectionName, ruleInput] of Object.entries(syncInput)) {
964
+ if (!(collectionName in collections)) {
965
+ throw new SchemaValidationError(
966
+ `Sync rule references collection "${collectionName}" which does not exist. Available collections: ${Object.keys(collections).join(", ")}`,
967
+ { collection: collectionName }
968
+ );
969
+ }
970
+ if (!ruleInput.where || Object.keys(ruleInput.where).length === 0) {
971
+ throw new SchemaValidationError(
972
+ `Sync rule for collection "${collectionName}" must declare at least one where binding`,
973
+ { collection: collectionName }
974
+ );
975
+ }
976
+ const collection = collections[collectionName];
977
+ if (!collection) {
978
+ continue;
979
+ }
980
+ for (const fieldName of Object.keys(ruleInput.where)) {
981
+ if (!(fieldName in collection.fields)) {
982
+ throw new SchemaValidationError(
983
+ `Sync rule where field "${fieldName}" does not exist in collection "${collectionName}". Available fields: ${Object.keys(collection.fields).join(", ")}`,
984
+ { collection: collectionName, field: fieldName }
985
+ );
986
+ }
987
+ }
988
+ try {
989
+ sync[collectionName] = { where: normalizeSyncRuleWhere(collectionName, ruleInput.where) };
990
+ } catch (error) {
991
+ throw new SchemaValidationError(
992
+ error instanceof Error ? error.message : "Invalid sync rule",
993
+ { collection: collectionName }
994
+ );
995
+ }
996
+ }
997
+ return sync;
998
+ }
999
+ function applySyncRulesToCollectionScope(collections, sync) {
1000
+ if (!sync) {
1001
+ return;
1002
+ }
1003
+ for (const [collectionName, rule] of Object.entries(sync)) {
1004
+ const collection = collections[collectionName];
1005
+ if (!collection) {
1006
+ continue;
1007
+ }
1008
+ for (const fieldName of Object.keys(rule.where)) {
1009
+ if (!collection.scope.includes(fieldName)) {
1010
+ collection.scope.push(fieldName);
1011
+ }
1012
+ }
1013
+ }
509
1014
  }
510
1015
  function validateFieldName(collection, fieldName) {
511
1016
  if (RESERVED_FIELDS.has(fieldName)) {
@@ -594,6 +1099,7 @@ function generateSQL(collectionName, collection, relations) {
594
1099
  }
595
1100
  columns.push("_created_at INTEGER NOT NULL");
596
1101
  columns.push("_updated_at INTEGER NOT NULL");
1102
+ columns.push("_version TEXT NOT NULL DEFAULT ''");
597
1103
  columns.push("_deleted INTEGER NOT NULL DEFAULT 0");
598
1104
  statements.push(`CREATE TABLE IF NOT EXISTS ${collectionName} (
599
1105
  ${columns.join(",\n ")}
@@ -603,6 +1109,10 @@ function generateSQL(collectionName, collection, relations) {
603
1109
  statements.push(`--kora:safe-alter
604
1110
  ALTER TABLE ${collectionName} ADD COLUMN ${colDef}`);
605
1111
  }
1112
+ statements.push(
1113
+ `--kora:safe-alter
1114
+ ALTER TABLE ${collectionName} ADD COLUMN _version TEXT NOT NULL DEFAULT ''`
1115
+ );
606
1116
  for (const indexField of collection.indexes) {
607
1117
  statements.push(
608
1118
  `CREATE INDEX IF NOT EXISTS idx_${collectionName}_${indexField} ON ${collectionName} (${indexField})`
@@ -639,6 +1149,21 @@ function generateFullDDL(schema) {
639
1149
  statements.push(
640
1150
  "CREATE TABLE IF NOT EXISTS _kora_version_vector (\n node_id TEXT PRIMARY KEY NOT NULL,\n sequence_number INTEGER NOT NULL\n)"
641
1151
  );
1152
+ statements.push(
1153
+ "CREATE TABLE IF NOT EXISTS _kora_sequences (\n name TEXT NOT NULL,\n scope TEXT NOT NULL DEFAULT '',\n node_id TEXT NOT NULL,\n counter INTEGER NOT NULL DEFAULT 0,\n PRIMARY KEY (name, scope, node_id)\n)"
1154
+ );
1155
+ statements.push(
1156
+ "CREATE TABLE IF NOT EXISTS _kora_sync_queue (\n id TEXT PRIMARY KEY NOT NULL,\n payload TEXT NOT NULL\n)"
1157
+ );
1158
+ statements.push(
1159
+ "CREATE TABLE IF NOT EXISTS _kora_audit_traces (\n id TEXT PRIMARY KEY NOT NULL,\n recorded_at INTEGER NOT NULL,\n event_type TEXT NOT NULL,\n collection TEXT NOT NULL,\n record_id TEXT NOT NULL,\n field TEXT NOT NULL,\n strategy TEXT NOT NULL,\n tier INTEGER NOT NULL,\n constraint_name TEXT,\n trace_json TEXT NOT NULL\n)"
1160
+ );
1161
+ statements.push(
1162
+ "CREATE INDEX IF NOT EXISTS idx_kora_audit_traces_recorded_at ON _kora_audit_traces (recorded_at)"
1163
+ );
1164
+ statements.push(
1165
+ "CREATE INDEX IF NOT EXISTS idx_kora_audit_traces_collection ON _kora_audit_traces (collection)"
1166
+ );
642
1167
  for (const [name, collection] of Object.entries(schema.collections)) {
643
1168
  statements.push(...generateSQL(name, collection, schema.relations));
644
1169
  }
@@ -692,23 +1217,47 @@ var FieldBuilder = class _FieldBuilder {
692
1217
  _required;
693
1218
  _defaultValue;
694
1219
  _auto;
695
- constructor(kind, required = true, defaultValue = void 0, auto = false) {
1220
+ _mergeStrategy;
1221
+ constructor(kind, required = true, defaultValue = void 0, auto = false, mergeStrategy = null) {
696
1222
  this._kind = kind;
697
1223
  this._required = required;
698
1224
  this._defaultValue = defaultValue;
699
1225
  this._auto = auto;
1226
+ this._mergeStrategy = mergeStrategy;
700
1227
  }
701
1228
  /** Mark this field as optional (not required on insert) */
702
1229
  optional() {
703
- return new _FieldBuilder(this._kind, false, this._defaultValue, this._auto);
1230
+ return new _FieldBuilder(this._kind, false, this._defaultValue, this._auto, this._mergeStrategy);
704
1231
  }
705
1232
  /** Set a default value for this field. Implicitly makes the field optional. */
706
1233
  default(value) {
707
- return new _FieldBuilder(this._kind, false, value, this._auto);
1234
+ return new _FieldBuilder(this._kind, false, value, this._auto, this._mergeStrategy);
708
1235
  }
709
1236
  /** Mark this field as auto-populated (e.g., createdAt timestamps). Developers cannot set auto fields. */
710
1237
  auto() {
711
- return new _FieldBuilder(this._kind, false, void 0, true);
1238
+ return new _FieldBuilder(this._kind, false, void 0, true, this._mergeStrategy);
1239
+ }
1240
+ /**
1241
+ * Declare a merge strategy for this field.
1242
+ * Controls how concurrent modifications are resolved during sync.
1243
+ *
1244
+ * @param strategy - The merge strategy to use:
1245
+ * - `'lww'`: Last-write-wins (default for scalar fields)
1246
+ * - `'counter'`: Sum of deltas from base (for numbers)
1247
+ * - `'max'`: Keep the maximum value (for numbers/timestamps)
1248
+ * - `'min'`: Keep the minimum value (for numbers/timestamps)
1249
+ * - `'union'`: Set-union merge (default for arrays)
1250
+ * - `'append-only'`: Concatenate additions (for arrays)
1251
+ * - `'server-authoritative'`: Always prefer the remote/server value
1252
+ */
1253
+ merge(strategy) {
1254
+ return new _FieldBuilder(
1255
+ this._kind,
1256
+ this._required,
1257
+ this._defaultValue,
1258
+ this._auto,
1259
+ strategy
1260
+ );
712
1261
  }
713
1262
  /** @internal Build the final FieldDescriptor. Used by defineSchema(). */
714
1263
  _build() {
@@ -718,24 +1267,102 @@ var FieldBuilder = class _FieldBuilder {
718
1267
  defaultValue: this._defaultValue,
719
1268
  auto: this._auto,
720
1269
  enumValues: null,
721
- itemKind: null
1270
+ itemKind: null,
1271
+ mergeStrategy: this._mergeStrategy,
1272
+ transitions: null
722
1273
  };
723
1274
  }
724
1275
  };
725
1276
  var EnumFieldBuilder = class _EnumFieldBuilder extends FieldBuilder {
726
1277
  _enumValues;
727
- constructor(values, required = true, defaultValue = void 0, auto = false) {
728
- super("enum", required, defaultValue, auto);
1278
+ _transitions;
1279
+ constructor(values, required = true, defaultValue = void 0, auto = false, mergeStrategy = null, transitions = null) {
1280
+ super("enum", required, defaultValue, auto, mergeStrategy);
729
1281
  this._enumValues = values;
1282
+ this._transitions = transitions;
730
1283
  }
731
1284
  optional() {
732
- return new _EnumFieldBuilder(this._enumValues, false, this._defaultValue, this._auto);
1285
+ return new _EnumFieldBuilder(
1286
+ this._enumValues,
1287
+ false,
1288
+ this._defaultValue,
1289
+ this._auto,
1290
+ this._mergeStrategy,
1291
+ this._transitions
1292
+ );
733
1293
  }
734
1294
  default(value) {
735
- return new _EnumFieldBuilder(this._enumValues, false, value, this._auto);
1295
+ return new _EnumFieldBuilder(
1296
+ this._enumValues,
1297
+ false,
1298
+ value,
1299
+ this._auto,
1300
+ this._mergeStrategy,
1301
+ this._transitions
1302
+ );
736
1303
  }
737
1304
  auto() {
738
- return new _EnumFieldBuilder(this._enumValues, false, void 0, true);
1305
+ return new _EnumFieldBuilder(
1306
+ this._enumValues,
1307
+ false,
1308
+ void 0,
1309
+ true,
1310
+ this._mergeStrategy,
1311
+ this._transitions
1312
+ );
1313
+ }
1314
+ merge(strategy) {
1315
+ return new _EnumFieldBuilder(
1316
+ this._enumValues,
1317
+ this._required,
1318
+ this._defaultValue,
1319
+ this._auto,
1320
+ strategy,
1321
+ this._transitions
1322
+ );
1323
+ }
1324
+ /**
1325
+ * Declare allowed state transitions for this enum field.
1326
+ * Enables state machine validation during mutations and merges.
1327
+ *
1328
+ * @param map - Map of state to allowed next states
1329
+ *
1330
+ * @example
1331
+ * ```typescript
1332
+ * t.enum(['draft', 'pending', 'confirmed', 'cancelled']).transitions({
1333
+ * draft: ['pending', 'cancelled'],
1334
+ * pending: ['confirmed', 'cancelled'],
1335
+ * confirmed: [],
1336
+ * cancelled: [],
1337
+ * })
1338
+ * ```
1339
+ */
1340
+ transitions(map) {
1341
+ const validValues = new Set(this._enumValues);
1342
+ for (const [state, targets] of Object.entries(map)) {
1343
+ if (!validValues.has(state)) {
1344
+ throw new SchemaValidationError(
1345
+ `Invalid source state "${state}" in transition map. Valid values: ${[...validValues].join(", ")}`,
1346
+ { state, validValues: [...validValues] }
1347
+ );
1348
+ }
1349
+ for (const target of targets) {
1350
+ if (!validValues.has(target)) {
1351
+ throw new SchemaValidationError(
1352
+ `Invalid target state "${target}" in transition from "${state}". Valid values: ${[...validValues].join(", ")}`,
1353
+ { state, target, validValues: [...validValues] }
1354
+ );
1355
+ }
1356
+ }
1357
+ }
1358
+ return new _EnumFieldBuilder(
1359
+ this._enumValues,
1360
+ this._required,
1361
+ this._defaultValue,
1362
+ this._auto,
1363
+ this._mergeStrategy,
1364
+ map
1365
+ );
739
1366
  }
740
1367
  _build() {
741
1368
  return {
@@ -744,14 +1371,16 @@ var EnumFieldBuilder = class _EnumFieldBuilder extends FieldBuilder {
744
1371
  defaultValue: this._defaultValue,
745
1372
  auto: this._auto,
746
1373
  enumValues: this._enumValues,
747
- itemKind: null
1374
+ itemKind: null,
1375
+ mergeStrategy: this._mergeStrategy,
1376
+ transitions: this._transitions
748
1377
  };
749
1378
  }
750
1379
  };
751
1380
  var ArrayFieldBuilder = class _ArrayFieldBuilder extends FieldBuilder {
752
1381
  _itemKind;
753
- constructor(itemBuilder, required = true, defaultValue = void 0, auto = false) {
754
- super("array", required, defaultValue, auto);
1382
+ constructor(itemBuilder, required = true, defaultValue = void 0, auto = false, mergeStrategy = null) {
1383
+ super("array", required, defaultValue, auto, mergeStrategy);
755
1384
  this._itemKind = itemBuilder._build().kind;
756
1385
  }
757
1386
  optional() {
@@ -759,14 +1388,36 @@ var ArrayFieldBuilder = class _ArrayFieldBuilder extends FieldBuilder {
759
1388
  new FieldBuilder(this._itemKind),
760
1389
  false,
761
1390
  this._defaultValue,
762
- this._auto
1391
+ this._auto,
1392
+ this._mergeStrategy
763
1393
  );
764
1394
  }
765
1395
  default(value) {
766
- return new _ArrayFieldBuilder(new FieldBuilder(this._itemKind), false, value, this._auto);
1396
+ return new _ArrayFieldBuilder(
1397
+ new FieldBuilder(this._itemKind),
1398
+ false,
1399
+ value,
1400
+ this._auto,
1401
+ this._mergeStrategy
1402
+ );
767
1403
  }
768
1404
  auto() {
769
- return new _ArrayFieldBuilder(new FieldBuilder(this._itemKind), false, void 0, true);
1405
+ return new _ArrayFieldBuilder(
1406
+ new FieldBuilder(this._itemKind),
1407
+ false,
1408
+ void 0,
1409
+ true,
1410
+ this._mergeStrategy
1411
+ );
1412
+ }
1413
+ merge(strategy) {
1414
+ return new _ArrayFieldBuilder(
1415
+ new FieldBuilder(this._itemKind),
1416
+ this._required,
1417
+ this._defaultValue,
1418
+ this._auto,
1419
+ strategy
1420
+ );
770
1421
  }
771
1422
  _build() {
772
1423
  return {
@@ -775,7 +1426,9 @@ var ArrayFieldBuilder = class _ArrayFieldBuilder extends FieldBuilder {
775
1426
  defaultValue: this._defaultValue,
776
1427
  auto: this._auto,
777
1428
  enumValues: null,
778
- itemKind: this._itemKind
1429
+ itemKind: this._itemKind,
1430
+ mergeStrategy: this._mergeStrategy,
1431
+ transitions: null
779
1432
  };
780
1433
  }
781
1434
  };
@@ -829,10 +1482,14 @@ function validateRecord(collection, collectionDef, data, operationType) {
829
1482
  }
830
1483
  if (operationType === "update") {
831
1484
  if (hasValue) {
832
- if (value !== void 0 && value !== null) {
1485
+ if (isAtomicOp(value)) {
1486
+ result[fieldName] = value;
1487
+ } else if (value !== void 0 && value !== null) {
833
1488
  validateFieldValue(collection, fieldName, descriptor, value);
1489
+ result[fieldName] = value;
1490
+ } else {
1491
+ result[fieldName] = value;
834
1492
  }
835
- result[fieldName] = value;
836
1493
  }
837
1494
  continue;
838
1495
  }
@@ -995,34 +1652,34 @@ function matchesJsType(value, expected) {
995
1652
  function topologicalSort(operations) {
996
1653
  if (operations.length <= 1) return [...operations];
997
1654
  const opMap = /* @__PURE__ */ new Map();
998
- for (const op of operations) {
999
- opMap.set(op.id, op);
1655
+ for (const op2 of operations) {
1656
+ opMap.set(op2.id, op2);
1000
1657
  }
1001
1658
  const inDegree = /* @__PURE__ */ new Map();
1002
1659
  const dependents = /* @__PURE__ */ new Map();
1003
- for (const op of operations) {
1004
- if (!inDegree.has(op.id)) {
1005
- inDegree.set(op.id, 0);
1660
+ for (const op2 of operations) {
1661
+ if (!inDegree.has(op2.id)) {
1662
+ inDegree.set(op2.id, 0);
1006
1663
  }
1007
- if (!dependents.has(op.id)) {
1008
- dependents.set(op.id, []);
1664
+ if (!dependents.has(op2.id)) {
1665
+ dependents.set(op2.id, []);
1009
1666
  }
1010
- for (const depId of op.causalDeps) {
1667
+ for (const depId of op2.causalDeps) {
1011
1668
  if (opMap.has(depId)) {
1012
- inDegree.set(op.id, (inDegree.get(op.id) ?? 0) + 1);
1669
+ inDegree.set(op2.id, (inDegree.get(op2.id) ?? 0) + 1);
1013
1670
  const deps = dependents.get(depId);
1014
1671
  if (deps) {
1015
- deps.push(op.id);
1672
+ deps.push(op2.id);
1016
1673
  } else {
1017
- dependents.set(depId, [op.id]);
1674
+ dependents.set(depId, [op2.id]);
1018
1675
  }
1019
1676
  }
1020
1677
  }
1021
1678
  }
1022
1679
  const heap = new MinHeap(compareByTimestamp);
1023
- for (const op of operations) {
1024
- if ((inDegree.get(op.id) ?? 0) === 0) {
1025
- heap.push(op);
1680
+ for (const op2 of operations) {
1681
+ if ((inDegree.get(op2.id) ?? 0) === 0) {
1682
+ heap.push(op2);
1026
1683
  }
1027
1684
  }
1028
1685
  const result = [];
@@ -1034,8 +1691,8 @@ function topologicalSort(operations) {
1034
1691
  const deg = (inDegree.get(depId) ?? 0) - 1;
1035
1692
  inDegree.set(depId, deg);
1036
1693
  if (deg === 0) {
1037
- const op = opMap.get(depId);
1038
- if (op) heap.push(op);
1694
+ const op2 = opMap.get(depId);
1695
+ if (op2) heap.push(op2);
1039
1696
  }
1040
1697
  }
1041
1698
  }
@@ -1076,28 +1733,30 @@ var MinHeap = class {
1076
1733
  return top;
1077
1734
  }
1078
1735
  bubbleUp(index) {
1079
- while (index > 0) {
1080
- const parentIndex = index - 1 >> 1;
1081
- if (this.cmp(this.data[index], this.data[parentIndex]) >= 0) break;
1082
- this.swap(index, parentIndex);
1083
- index = parentIndex;
1736
+ let current = index;
1737
+ while (current > 0) {
1738
+ const parentIndex = current - 1 >> 1;
1739
+ if (this.cmp(this.data[current], this.data[parentIndex]) >= 0) break;
1740
+ this.swap(current, parentIndex);
1741
+ current = parentIndex;
1084
1742
  }
1085
1743
  }
1086
1744
  sinkDown(index) {
1087
1745
  const length = this.data.length;
1746
+ let current = index;
1088
1747
  while (true) {
1089
- let smallest = index;
1090
- const left = 2 * index + 1;
1091
- const right = 2 * index + 2;
1748
+ let smallest = current;
1749
+ const left = 2 * current + 1;
1750
+ const right = 2 * current + 2;
1092
1751
  if (left < length && this.cmp(this.data[left], this.data[smallest]) < 0) {
1093
1752
  smallest = left;
1094
1753
  }
1095
1754
  if (right < length && this.cmp(this.data[right], this.data[smallest]) < 0) {
1096
1755
  smallest = right;
1097
1756
  }
1098
- if (smallest === index) break;
1099
- this.swap(index, smallest);
1100
- index = smallest;
1757
+ if (smallest === current) break;
1758
+ this.swap(current, smallest);
1759
+ current = smallest;
1101
1760
  }
1102
1761
  }
1103
1762
  swap(i, j) {
@@ -1136,12 +1795,13 @@ function vectorsEqual(a, b) {
1136
1795
  }
1137
1796
  return true;
1138
1797
  }
1139
- function computeDelta(localVector, remoteVector, operationLog) {
1798
+ async function computeDelta(localVector, remoteVector, operationLog) {
1140
1799
  const missing = [];
1141
1800
  for (const [nodeId, localSeq] of localVector) {
1142
1801
  const remoteSeq = remoteVector.get(nodeId) ?? 0;
1143
1802
  if (localSeq > remoteSeq) {
1144
- missing.push(...operationLog.getRange(nodeId, remoteSeq + 1, localSeq));
1803
+ const ops = await operationLog.getRange(nodeId, remoteSeq + 1, localSeq);
1804
+ missing.push(...ops);
1145
1805
  }
1146
1806
  }
1147
1807
  return topologicalSort(missing);
@@ -1154,38 +1814,716 @@ function deserializeVector(s) {
1154
1814
  const entries = JSON.parse(s);
1155
1815
  return new Map(entries);
1156
1816
  }
1817
+
1818
+ // src/scopes/build-scope-map.ts
1819
+ function buildScopeMap(schema, scopeValues) {
1820
+ const result = {};
1821
+ const partialSync = hasSchemaSyncRules(schema);
1822
+ for (const collName of Object.keys(schema.collections)) {
1823
+ if (partialSync && !isCollectionSyncScoped(schema, collName)) {
1824
+ continue;
1825
+ }
1826
+ const bindings = getCollectionScopeBindings(schema, collName);
1827
+ if (bindings) {
1828
+ const collScope = {};
1829
+ for (const [field, scopeKey] of Object.entries(bindings)) {
1830
+ if (scopeKey in scopeValues) {
1831
+ collScope[field] = scopeValues[scopeKey];
1832
+ }
1833
+ }
1834
+ result[collName] = collScope;
1835
+ } else {
1836
+ result[collName] = {};
1837
+ }
1838
+ }
1839
+ return result;
1840
+ }
1841
+
1842
+ // src/scopes/extract-scope-from-claims.ts
1843
+ function collectSchemaScopeFields(schema) {
1844
+ return collectSchemaScopeValueKeys(schema);
1845
+ }
1846
+ function extractScopeValuesFromClaims(schema, claims) {
1847
+ const scopeFields = collectSchemaScopeValueKeys(schema);
1848
+ if (scopeFields.length === 0) {
1849
+ return {};
1850
+ }
1851
+ const nestedScope = claims.scope;
1852
+ const scopeObject = typeof nestedScope === "object" && nestedScope !== null && !Array.isArray(nestedScope) ? nestedScope : {};
1853
+ const result = {};
1854
+ for (const field of scopeFields) {
1855
+ if (field in claims) {
1856
+ result[field] = claims[field];
1857
+ continue;
1858
+ }
1859
+ if (field in scopeObject) {
1860
+ result[field] = scopeObject[field];
1861
+ continue;
1862
+ }
1863
+ if (field === "userId" && typeof claims.sub === "string") {
1864
+ result.userId = claims.sub;
1865
+ }
1866
+ }
1867
+ return result;
1868
+ }
1869
+
1870
+ // src/sequences/sequence-format.ts
1871
+ function formatSequenceValue(template, counter, nodeId, now) {
1872
+ const date = now ?? /* @__PURE__ */ new Date();
1873
+ const yyyy = String(date.getUTCFullYear());
1874
+ const mm = String(date.getUTCMonth() + 1).padStart(2, "0");
1875
+ const dd = String(date.getUTCDate()).padStart(2, "0");
1876
+ const dateStr = `${yyyy}${mm}${dd}`;
1877
+ return template.replace(/\{([^}]+)\}/g, (_match, token) => {
1878
+ if (token === "date") return dateStr;
1879
+ if (token === "node4") return nodeId.slice(0, 4);
1880
+ if (token === "node8") return nodeId.slice(0, 8);
1881
+ if (token === "seq") return String(counter).padStart(4, "0");
1882
+ if (token.startsWith("seq:")) {
1883
+ const width = Number.parseInt(token.slice(4), 10);
1884
+ if (Number.isNaN(width) || width < 1) {
1885
+ return String(counter).padStart(4, "0");
1886
+ }
1887
+ return String(counter).padStart(width, "0");
1888
+ }
1889
+ return `{${token}}`;
1890
+ });
1891
+ }
1892
+ function defaultSequenceFormat(name) {
1893
+ return `${name}-{seq:4}`;
1894
+ }
1895
+
1896
+ // src/migration/apply-operation-transforms.ts
1897
+ var MAX_TRANSFORM_STEPS = 32;
1898
+ function applyOperationTransforms(operation, targetSchemaVersion, transforms) {
1899
+ let current = operation;
1900
+ for (let step = 0; step < MAX_TRANSFORM_STEPS; step++) {
1901
+ if (current.schemaVersion === targetSchemaVersion) {
1902
+ return current;
1903
+ }
1904
+ const transform = transforms.find(
1905
+ (candidate) => candidate.fromVersion === current.schemaVersion
1906
+ );
1907
+ if (!transform) {
1908
+ return null;
1909
+ }
1910
+ const next = transform.transform(current);
1911
+ if (next === null) {
1912
+ return null;
1913
+ }
1914
+ current = next;
1915
+ }
1916
+ return null;
1917
+ }
1918
+
1919
+ // src/migrations/migration-builder.ts
1920
+ var RollbackBuilder = class {
1921
+ _steps = [];
1922
+ /**
1923
+ * Add a field during rollback (to reverse a removeField).
1924
+ */
1925
+ addField(collection, field, builder) {
1926
+ this._steps.push({ type: "addField", collection, field, descriptor: builder._build() });
1927
+ return this;
1928
+ }
1929
+ /**
1930
+ * Remove a field during rollback (to reverse an addField).
1931
+ */
1932
+ removeField(collection, field) {
1933
+ this._steps.push({ type: "removeField", collection, field });
1934
+ return this;
1935
+ }
1936
+ /**
1937
+ * Rename a field during rollback (to reverse a renameField).
1938
+ */
1939
+ renameField(collection, from, to) {
1940
+ this._steps.push({ type: "renameField", collection, from, to });
1941
+ return this;
1942
+ }
1943
+ /**
1944
+ * Add an index during rollback (to reverse a removeIndex).
1945
+ */
1946
+ addIndex(collection, field) {
1947
+ this._steps.push({ type: "addIndex", collection, field });
1948
+ return this;
1949
+ }
1950
+ /**
1951
+ * Remove an index during rollback (to reverse an addIndex).
1952
+ */
1953
+ removeIndex(collection, field) {
1954
+ this._steps.push({ type: "removeIndex", collection, field });
1955
+ return this;
1956
+ }
1957
+ /**
1958
+ * Run a backfill during rollback (to reverse a previous backfill).
1959
+ */
1960
+ backfill(collection, transform) {
1961
+ this._steps.push({ type: "backfill", collection, transform });
1962
+ return this;
1963
+ }
1964
+ /**
1965
+ * Get the accumulated rollback steps.
1966
+ */
1967
+ _getSteps() {
1968
+ return [...this._steps];
1969
+ }
1970
+ };
1971
+ var MigrationBuilder = class _MigrationBuilder {
1972
+ steps;
1973
+ rollbackSteps;
1974
+ safelyReversible;
1975
+ constructor(steps = [], rollbackSteps, safelyReversible) {
1976
+ this.steps = steps;
1977
+ this.rollbackSteps = rollbackSteps;
1978
+ this.safelyReversible = safelyReversible ?? this._computeSafelyReversible(steps, rollbackSteps);
1979
+ }
1980
+ /**
1981
+ * Add a new field to a collection.
1982
+ * The field builder provides the type and default value.
1983
+ */
1984
+ addField(collection, field, builder) {
1985
+ return new _MigrationBuilder([
1986
+ ...this.steps,
1987
+ { type: "addField", collection, field, descriptor: builder._build() }
1988
+ ]);
1989
+ }
1990
+ /**
1991
+ * Remove a field from a collection.
1992
+ * Optionally accepts a field builder to preserve the descriptor for rollback.
1993
+ * Without the descriptor, rollback of this step requires a custom `.down()`.
1994
+ *
1995
+ * @param collection - The collection name
1996
+ * @param field - The field name to remove
1997
+ * @param builder - Optional field builder preserving the type info for rollback
1998
+ */
1999
+ removeField(collection, field, builder) {
2000
+ const step = builder ? { type: "removeField", collection, field, descriptor: builder._build() } : { type: "removeField", collection, field };
2001
+ return new _MigrationBuilder([...this.steps, step]);
2002
+ }
2003
+ /**
2004
+ * Rename a field in a collection.
2005
+ * Implemented as ALTER TABLE RENAME COLUMN (SQLite 3.25+).
2006
+ */
2007
+ renameField(collection, from, to) {
2008
+ return new _MigrationBuilder([...this.steps, { type: "renameField", collection, from, to }]);
2009
+ }
2010
+ /**
2011
+ * Add an index on a field.
2012
+ */
2013
+ addIndex(collection, field) {
2014
+ return new _MigrationBuilder([...this.steps, { type: "addIndex", collection, field }]);
2015
+ }
2016
+ /**
2017
+ * Remove an index on a field.
2018
+ */
2019
+ removeIndex(collection, field) {
2020
+ return new _MigrationBuilder([...this.steps, { type: "removeIndex", collection, field }]);
2021
+ }
2022
+ /**
2023
+ * Backfill records in a collection using a transform function.
2024
+ * The transform receives each record and returns the fields to update.
2025
+ * Runs after structural changes (addField, renameField, etc.).
2026
+ *
2027
+ * @param collection - The collection name
2028
+ * @param transform - Forward transform function
2029
+ * @param reverseTransform - Optional reverse transform for rollback support
2030
+ */
2031
+ backfill(collection, transform, reverseTransform) {
2032
+ return new _MigrationBuilder([
2033
+ ...this.steps,
2034
+ { type: "backfill", collection, transform, reverseTransform }
2035
+ ]);
2036
+ }
2037
+ /**
2038
+ * Define explicit rollback steps for this migration.
2039
+ * If not called, inverse steps are auto-generated from forward steps.
2040
+ *
2041
+ * @param fn - Function that receives a RollbackBuilder to define rollback steps
2042
+ * @returns A new MigrationBuilder with the rollback steps attached
2043
+ *
2044
+ * @example
2045
+ * ```typescript
2046
+ * migrate()
2047
+ * .addField('todos', 'priority', t.enum(['low', 'medium', 'high']).default('medium'))
2048
+ * .addIndex('todos', 'priority')
2049
+ * .down((rollback) => {
2050
+ * rollback
2051
+ * .removeIndex('todos', 'priority')
2052
+ * .removeField('todos', 'priority')
2053
+ * })
2054
+ * ```
2055
+ */
2056
+ down(fn) {
2057
+ const rollbackBuilder = new RollbackBuilder();
2058
+ fn(rollbackBuilder);
2059
+ const rbSteps = rollbackBuilder._getSteps();
2060
+ return new _MigrationBuilder(this.steps, rbSteps, true);
2061
+ }
2062
+ /**
2063
+ * Determine if a migration is safely reversible based on its steps.
2064
+ * A migration is not safely reversible if:
2065
+ * - It has a backfill step without a reverseTransform and no explicit rollback
2066
+ * - It has a removeField without a stored descriptor and no explicit rollback
2067
+ */
2068
+ _computeSafelyReversible(steps, rollbackSteps) {
2069
+ if (rollbackSteps !== void 0) {
2070
+ return true;
2071
+ }
2072
+ for (const step of steps) {
2073
+ if (step.type === "backfill" && !step.reverseTransform) {
2074
+ return false;
2075
+ }
2076
+ if (step.type === "removeField" && !step.descriptor) {
2077
+ return false;
2078
+ }
2079
+ }
2080
+ return true;
2081
+ }
2082
+ };
2083
+ function migrate() {
2084
+ return new MigrationBuilder();
2085
+ }
2086
+
2087
+ // src/migrations/migration-rollback.ts
2088
+ var MigrationRollbackError = class extends KoraError {
2089
+ constructor(step) {
2090
+ super(
2091
+ `Cannot auto-generate rollback for "${step.type}" step on collection "${step.collection}". Provide an explicit .down() definition for this migration.`,
2092
+ "MIGRATION_ROLLBACK",
2093
+ { stepType: step.type, collection: step.collection }
2094
+ );
2095
+ this.name = "MigrationRollbackError";
2096
+ }
2097
+ };
2098
+ function canAutoRollback(step) {
2099
+ switch (step.type) {
2100
+ case "addField":
2101
+ case "addIndex":
2102
+ case "removeIndex":
2103
+ case "renameField":
2104
+ return true;
2105
+ case "removeField":
2106
+ case "backfill":
2107
+ return false;
2108
+ }
2109
+ }
2110
+ function generateRollbackSteps(forwardSteps) {
2111
+ const rollbackSteps = [];
2112
+ for (let i = forwardSteps.length - 1; i >= 0; i--) {
2113
+ const step = forwardSteps[i];
2114
+ if (step === void 0) continue;
2115
+ const rollback = generateSingleRollbackStep(step);
2116
+ rollbackSteps.push(rollback);
2117
+ }
2118
+ return rollbackSteps;
2119
+ }
2120
+ function generateSingleRollbackStep(step) {
2121
+ switch (step.type) {
2122
+ case "addField":
2123
+ return { type: "removeField", collection: step.collection, field: step.field };
2124
+ case "removeField":
2125
+ if (step.descriptor) {
2126
+ return {
2127
+ type: "addField",
2128
+ collection: step.collection,
2129
+ field: step.field,
2130
+ descriptor: step.descriptor
2131
+ };
2132
+ }
2133
+ throw new MigrationRollbackError(step);
2134
+ case "renameField":
2135
+ return { type: "renameField", collection: step.collection, from: step.to, to: step.from };
2136
+ case "addIndex":
2137
+ return { type: "removeIndex", collection: step.collection, field: step.field };
2138
+ case "removeIndex":
2139
+ return { type: "addIndex", collection: step.collection, field: step.field };
2140
+ case "backfill":
2141
+ if (step.reverseTransform) {
2142
+ return {
2143
+ type: "backfill",
2144
+ collection: step.collection,
2145
+ transform: step.reverseTransform
2146
+ };
2147
+ }
2148
+ throw new MigrationRollbackError(step);
2149
+ }
2150
+ }
2151
+ function createReversibleMigration(upSteps, downSteps, fromVersion, toVersion) {
2152
+ const down = downSteps !== null ? [...downSteps] : generateRollbackSteps(upSteps);
2153
+ return {
2154
+ up: [...upSteps],
2155
+ down,
2156
+ fromVersion,
2157
+ toVersion
2158
+ };
2159
+ }
2160
+
2161
+ // src/migrations/migration-sql.ts
2162
+ function migrationStepsToSQL(steps) {
2163
+ const statements = [];
2164
+ for (const step of steps) {
2165
+ switch (step.type) {
2166
+ case "addField":
2167
+ statements.push(addFieldSQL(step.collection, step.field, step.descriptor));
2168
+ break;
2169
+ case "removeField":
2170
+ statements.push(`ALTER TABLE ${step.collection} DROP COLUMN ${step.field}`);
2171
+ break;
2172
+ case "renameField":
2173
+ statements.push(`ALTER TABLE ${step.collection} RENAME COLUMN ${step.from} TO ${step.to}`);
2174
+ break;
2175
+ case "addIndex":
2176
+ statements.push(
2177
+ `CREATE INDEX IF NOT EXISTS idx_${step.collection}_${step.field} ON ${step.collection} (${step.field})`
2178
+ );
2179
+ break;
2180
+ case "removeIndex":
2181
+ statements.push(`DROP INDEX IF EXISTS idx_${step.collection}_${step.field}`);
2182
+ break;
2183
+ case "backfill":
2184
+ break;
2185
+ }
2186
+ }
2187
+ return statements;
2188
+ }
2189
+ function rollbackStepsToSQL(migration) {
2190
+ const rollbackSteps = migration.rollbackSteps ?? generateRollbackSteps(migration.steps);
2191
+ return migrationStepsToSQL(rollbackSteps);
2192
+ }
2193
+ function addFieldSQL(collection, field, descriptor) {
2194
+ const sqlType = mapFieldType2(descriptor);
2195
+ const parts = [`ALTER TABLE ${collection} ADD COLUMN ${field}`, sqlType];
2196
+ if (descriptor.defaultValue !== void 0) {
2197
+ parts.push(`DEFAULT ${sqlDefault2(descriptor.defaultValue)}`);
2198
+ }
2199
+ if (descriptor.kind === "enum" && descriptor.enumValues) {
2200
+ const values = descriptor.enumValues.map((v) => `'${v}'`).join(", ");
2201
+ parts.push(`CHECK (${field} IN (${values}))`);
2202
+ }
2203
+ return parts.join(" ");
2204
+ }
2205
+ function mapFieldType2(descriptor) {
2206
+ switch (descriptor.kind) {
2207
+ case "string":
2208
+ return "TEXT";
2209
+ case "number":
2210
+ return "REAL";
2211
+ case "boolean":
2212
+ return "INTEGER";
2213
+ case "enum":
2214
+ return "TEXT";
2215
+ case "timestamp":
2216
+ return "INTEGER";
2217
+ case "array":
2218
+ return "TEXT";
2219
+ case "richtext":
2220
+ return "BLOB";
2221
+ }
2222
+ }
2223
+ function sqlDefault2(value) {
2224
+ if (value === null) return "NULL";
2225
+ if (typeof value === "string") return `'${value}'`;
2226
+ if (typeof value === "number") return String(value);
2227
+ if (typeof value === "boolean") return value ? "1" : "0";
2228
+ return `'${JSON.stringify(value)}'`;
2229
+ }
2230
+
2231
+ // src/codegen/proto-generator.ts
2232
+ var SCALAR_TYPE_MAP = {
2233
+ string: "string",
2234
+ number: "double",
2235
+ boolean: "bool",
2236
+ timestamp: "int64",
2237
+ richtext: "bytes"
2238
+ };
2239
+ var ARRAY_ITEM_TYPE_MAP = {
2240
+ string: "string",
2241
+ number: "double",
2242
+ boolean: "bool",
2243
+ timestamp: "int64"
2244
+ };
2245
+ function toPascalCase(name) {
2246
+ return name.split("_").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
2247
+ }
2248
+ function toSnakeCase(name) {
2249
+ return name.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
2250
+ }
2251
+ function resolveProtoType(fieldName, descriptor, messageName) {
2252
+ if (descriptor.kind === "enum") {
2253
+ return `${messageName}${toPascalCase(fieldName)}`;
2254
+ }
2255
+ if (descriptor.kind === "array") {
2256
+ const itemType = descriptor.itemKind ? ARRAY_ITEM_TYPE_MAP[descriptor.itemKind] ?? "string" : "string";
2257
+ return itemType;
2258
+ }
2259
+ return SCALAR_TYPE_MAP[descriptor.kind];
2260
+ }
2261
+ function generateEnumBlock(enumTypeName, enumValues, indent) {
2262
+ const lines = [];
2263
+ lines.push(`${indent}enum ${enumTypeName} {`);
2264
+ lines.push(`${indent} ${enumTypeName.toUpperCase()}_UNSPECIFIED = 0;`);
2265
+ for (let i = 0; i < enumValues.length; i++) {
2266
+ const value = enumValues[i];
2267
+ if (value === void 0) continue;
2268
+ lines.push(`${indent} ${enumTypeName.toUpperCase()}_${value.toUpperCase()} = ${i + 1};`);
2269
+ }
2270
+ lines.push(`${indent}}`);
2271
+ return lines.join("\n");
2272
+ }
2273
+ function generateCollectionMessage(collectionName, collection, typeMap) {
2274
+ const messageName = `${toPascalCase(collectionName)}Record`;
2275
+ const lines = [];
2276
+ const nestedEnums = [];
2277
+ lines.push(`message ${messageName} {`);
2278
+ lines.push(" string id = 1;");
2279
+ typeMap.set(`${collectionName}.id`, "string");
2280
+ let fieldNumber = 2;
2281
+ const entries = Object.entries(collection.fields);
2282
+ for (const [fieldName, descriptor] of entries) {
2283
+ const protoFieldName = toSnakeCase(fieldName);
2284
+ const protoType = resolveProtoType(fieldName, descriptor, messageName);
2285
+ const mapKey = `${collectionName}.${fieldName}`;
2286
+ if (descriptor.kind === "enum" && descriptor.enumValues) {
2287
+ const enumTypeName = `${messageName}${toPascalCase(fieldName)}`;
2288
+ nestedEnums.push(generateEnumBlock(enumTypeName, descriptor.enumValues, " "));
2289
+ typeMap.set(mapKey, enumTypeName);
2290
+ lines.push(` ${enumTypeName} ${protoFieldName} = ${fieldNumber};`);
2291
+ } else if (descriptor.kind === "array") {
2292
+ typeMap.set(mapKey, `repeated ${protoType}`);
2293
+ lines.push(` repeated ${protoType} ${protoFieldName} = ${fieldNumber};`);
2294
+ } else {
2295
+ typeMap.set(mapKey, protoType);
2296
+ lines.push(` ${protoType} ${protoFieldName} = ${fieldNumber};`);
2297
+ }
2298
+ fieldNumber++;
2299
+ }
2300
+ if (nestedEnums.length > 0) {
2301
+ lines.push("");
2302
+ for (const enumBlock of nestedEnums) {
2303
+ lines.push(enumBlock);
2304
+ }
2305
+ }
2306
+ lines.push("}");
2307
+ return lines.join("\n");
2308
+ }
2309
+ var KORA_OPERATION_MESSAGE = `message KoraOperation {
2310
+ string id = 1;
2311
+ string node_id = 2;
2312
+ string type = 3;
2313
+ string collection = 4;
2314
+ string record_id = 5;
2315
+ bytes data = 6;
2316
+ bytes previous_data = 7;
2317
+ int64 wall_time = 8;
2318
+ int32 logical = 9;
2319
+ string timestamp_node_id = 10;
2320
+ int64 sequence_number = 11;
2321
+ repeated string causal_deps = 12;
2322
+ int32 schema_version = 13;
2323
+ }`;
2324
+ var OPERATION_BATCH_MESSAGE = `message OperationBatch {
2325
+ repeated KoraOperation operations = 1;
2326
+ bool is_final = 2;
2327
+ }`;
2328
+ var HANDSHAKE_MESSAGE = `message HandshakeMessage {
2329
+ map<string, int64> version_vector = 1;
2330
+ int32 schema_version = 2;
2331
+ string node_id = 3;
2332
+ }`;
2333
+ var HANDSHAKE_RESPONSE_MESSAGE = `message HandshakeResponse {
2334
+ map<string, int64> version_vector = 1;
2335
+ int32 schema_version = 2;
2336
+ }`;
2337
+ var ACKNOWLEDGMENT_MESSAGE = `message Acknowledgment {
2338
+ int64 sequence_number = 1;
2339
+ string node_id = 2;
2340
+ }`;
2341
+ function buildJsonDescriptor(schema) {
2342
+ const nested = {};
2343
+ for (const [collectionName, collection] of Object.entries(schema.collections)) {
2344
+ const messageName = `${toPascalCase(collectionName)}Record`;
2345
+ const fields = {
2346
+ id: { type: "string", id: 1 }
2347
+ };
2348
+ const nestedTypes = {};
2349
+ let fieldNumber = 2;
2350
+ for (const [fieldName, descriptor] of Object.entries(collection.fields)) {
2351
+ const protoFieldName = toSnakeCase(fieldName);
2352
+ const fieldDef = { id: fieldNumber };
2353
+ if (descriptor.kind === "enum" && descriptor.enumValues) {
2354
+ const enumTypeName = `${messageName}${toPascalCase(fieldName)}`;
2355
+ fieldDef.type = enumTypeName;
2356
+ const enumValuesObj = {
2357
+ [`${enumTypeName.toUpperCase()}_UNSPECIFIED`]: 0
2358
+ };
2359
+ for (let i = 0; i < descriptor.enumValues.length; i++) {
2360
+ const val = descriptor.enumValues[i];
2361
+ if (val === void 0) continue;
2362
+ enumValuesObj[`${enumTypeName.toUpperCase()}_${val.toUpperCase()}`] = i + 1;
2363
+ }
2364
+ nestedTypes[enumTypeName] = { values: enumValuesObj };
2365
+ } else if (descriptor.kind === "array") {
2366
+ const itemType = descriptor.itemKind ? ARRAY_ITEM_TYPE_MAP[descriptor.itemKind] ?? "string" : "string";
2367
+ fieldDef.type = itemType;
2368
+ fieldDef.rule = "repeated";
2369
+ } else {
2370
+ fieldDef.type = SCALAR_TYPE_MAP[descriptor.kind];
2371
+ }
2372
+ fields[protoFieldName] = fieldDef;
2373
+ fieldNumber++;
2374
+ }
2375
+ const messageDescriptor = { fields };
2376
+ if (Object.keys(nestedTypes).length > 0) {
2377
+ messageDescriptor.nested = nestedTypes;
2378
+ }
2379
+ nested[messageName] = messageDescriptor;
2380
+ }
2381
+ nested.KoraOperation = {
2382
+ fields: {
2383
+ id: { type: "string", id: 1 },
2384
+ node_id: { type: "string", id: 2 },
2385
+ type: { type: "string", id: 3 },
2386
+ collection: { type: "string", id: 4 },
2387
+ record_id: { type: "string", id: 5 },
2388
+ data: { type: "bytes", id: 6 },
2389
+ previous_data: { type: "bytes", id: 7 },
2390
+ wall_time: { type: "int64", id: 8 },
2391
+ logical: { type: "int32", id: 9 },
2392
+ timestamp_node_id: { type: "string", id: 10 },
2393
+ sequence_number: { type: "int64", id: 11 },
2394
+ causal_deps: { type: "string", id: 12, rule: "repeated" },
2395
+ schema_version: { type: "int32", id: 13 }
2396
+ }
2397
+ };
2398
+ nested.OperationBatch = {
2399
+ fields: {
2400
+ operations: { type: "KoraOperation", id: 1, rule: "repeated" },
2401
+ is_final: { type: "bool", id: 2 }
2402
+ }
2403
+ };
2404
+ nested.HandshakeMessage = {
2405
+ fields: {
2406
+ version_vector: { keyType: "string", type: "int64", id: 1 },
2407
+ schema_version: { type: "int32", id: 2 },
2408
+ node_id: { type: "string", id: 3 }
2409
+ }
2410
+ };
2411
+ nested.HandshakeResponse = {
2412
+ fields: {
2413
+ version_vector: { keyType: "string", type: "int64", id: 1 },
2414
+ schema_version: { type: "int32", id: 2 }
2415
+ }
2416
+ };
2417
+ nested.Acknowledgment = {
2418
+ fields: {
2419
+ sequence_number: { type: "int64", id: 1 },
2420
+ node_id: { type: "string", id: 2 }
2421
+ }
2422
+ };
2423
+ return {
2424
+ nested: {
2425
+ kora: { nested }
2426
+ }
2427
+ };
2428
+ }
2429
+ function generateProtoDefinitions(schema) {
2430
+ const typeMap = /* @__PURE__ */ new Map();
2431
+ const sections = [];
2432
+ sections.push('syntax = "proto3";');
2433
+ sections.push("");
2434
+ sections.push("package kora;");
2435
+ const collectionEntries = Object.entries(schema.collections);
2436
+ if (collectionEntries.length > 0) {
2437
+ sections.push("");
2438
+ sections.push("// Collection record messages");
2439
+ for (const [collectionName, collection] of collectionEntries) {
2440
+ sections.push("");
2441
+ sections.push(generateCollectionMessage(collectionName, collection, typeMap));
2442
+ }
2443
+ }
2444
+ sections.push("");
2445
+ sections.push("// Sync protocol messages");
2446
+ sections.push("");
2447
+ sections.push(KORA_OPERATION_MESSAGE);
2448
+ sections.push("");
2449
+ sections.push(OPERATION_BATCH_MESSAGE);
2450
+ sections.push("");
2451
+ sections.push(HANDSHAKE_MESSAGE);
2452
+ sections.push("");
2453
+ sections.push(HANDSHAKE_RESPONSE_MESSAGE);
2454
+ sections.push("");
2455
+ sections.push(ACKNOWLEDGMENT_MESSAGE);
2456
+ const proto = `${sections.join("\n")}
2457
+ `;
2458
+ const jsonDescriptor = buildJsonDescriptor(schema);
2459
+ return { proto, typeMap, jsonDescriptor };
2460
+ }
1157
2461
  // Annotate the CommonJS export names for ESM import in node:
1158
2462
  0 && (module.exports = {
2463
+ APPLY_FAILURE_CODES,
2464
+ APPLY_RESULTS,
1159
2465
  ArrayFieldBuilder,
1160
2466
  CONNECTION_QUALITIES,
2467
+ CausalTracker,
1161
2468
  ClockDriftError,
1162
2469
  EnumFieldBuilder,
1163
2470
  FieldBuilder,
1164
2471
  HybridLogicalClock,
2472
+ KORA_ERROR_FIX_SUGGESTIONS,
1165
2473
  KoraError,
1166
2474
  MERGE_STRATEGIES,
1167
2475
  MergeConflictError,
2476
+ MigrationBuilder,
2477
+ MigrationRollbackError,
1168
2478
  OperationError,
2479
+ RollbackBuilder,
1169
2480
  SchemaValidationError,
1170
2481
  StorageError,
1171
2482
  SyncError,
1172
2483
  advanceVector,
2484
+ applyOperationTransforms,
2485
+ buildScopeMap,
2486
+ buildStateMachineConstraints,
2487
+ canAutoRollback,
2488
+ collectSchemaScopeFields,
2489
+ collectSchemaScopeValueKeys,
1173
2490
  computeDelta,
1174
2491
  createOperation,
2492
+ createReversibleMigration,
1175
2493
  createVersionVector,
2494
+ defaultApplyFailureReason,
2495
+ defaultSequenceFormat,
1176
2496
  defineSchema,
1177
2497
  deserializeVector,
1178
2498
  dominates,
2499
+ extractScopeValuesFromClaims,
1179
2500
  extractTimestamp,
2501
+ formatSequenceValue,
1180
2502
  generateFullDDL,
2503
+ generateProtoDefinitions,
2504
+ generateRollbackSteps,
1181
2505
  generateSQL,
1182
2506
  generateUUIDv7,
2507
+ getCollectionScopeBindings,
2508
+ getKoraErrorFix,
2509
+ getTransitionMap,
2510
+ hasSchemaSyncRules,
2511
+ isApplyFailure,
2512
+ isAtomicOp,
2513
+ isCollectionSyncScoped,
1183
2514
  isValidOperation,
1184
2515
  isValidUUIDv7,
1185
2516
  mergeVectors,
2517
+ migrate,
2518
+ migrationStepsToSQL,
2519
+ op,
2520
+ resolveAtomicOp,
2521
+ rollbackStepsToSQL,
1186
2522
  serializeVector,
1187
2523
  t,
2524
+ toAtomicOp,
1188
2525
  validateRecord,
2526
+ validateTransition,
1189
2527
  vectorsEqual,
1190
2528
  verifyOperationIntegrity
1191
2529
  });