@korajs/merge 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -70,6 +70,68 @@ function addWinsSet(localArray, remoteArray, baseArray) {
70
70
  return result;
71
71
  }
72
72
 
73
+ // src/strategies/atomic-merge.ts
74
+ function mergeAtomicOps(localAtomicOp, remoteAtomicOp, baseValue) {
75
+ if (localAtomicOp.type !== remoteAtomicOp.type) {
76
+ return { merged: false };
77
+ }
78
+ switch (localAtomicOp.type) {
79
+ case "increment": {
80
+ const base = typeof baseValue === "number" ? baseValue : 0;
81
+ const localDelta = localAtomicOp.value;
82
+ const remoteDelta = remoteAtomicOp.value;
83
+ return {
84
+ merged: true,
85
+ value: base + localDelta + remoteDelta,
86
+ strategy: "atomic-increment"
87
+ };
88
+ }
89
+ case "max": {
90
+ const base = typeof baseValue === "number" ? baseValue : Number.NEGATIVE_INFINITY;
91
+ const localVal = localAtomicOp.value;
92
+ const remoteVal = remoteAtomicOp.value;
93
+ return {
94
+ merged: true,
95
+ value: Math.max(base, localVal, remoteVal),
96
+ strategy: "atomic-max"
97
+ };
98
+ }
99
+ case "min": {
100
+ const base = typeof baseValue === "number" ? baseValue : Number.POSITIVE_INFINITY;
101
+ const localVal = localAtomicOp.value;
102
+ const remoteVal = remoteAtomicOp.value;
103
+ return {
104
+ merged: true,
105
+ value: Math.min(base, localVal, remoteVal),
106
+ strategy: "atomic-min"
107
+ };
108
+ }
109
+ case "append": {
110
+ const base = Array.isArray(baseValue) ? [...baseValue] : [];
111
+ base.push(localAtomicOp.value);
112
+ base.push(remoteAtomicOp.value);
113
+ return {
114
+ merged: true,
115
+ value: base,
116
+ strategy: "atomic-append"
117
+ };
118
+ }
119
+ case "remove": {
120
+ const base = Array.isArray(baseValue) ? [...baseValue] : [];
121
+ const localItem = localAtomicOp.value;
122
+ const remoteItem = remoteAtomicOp.value;
123
+ const result = base.filter((item) => item !== localItem && item !== remoteItem);
124
+ return {
125
+ merged: true,
126
+ value: result,
127
+ strategy: "atomic-remove"
128
+ };
129
+ }
130
+ default:
131
+ return { merged: false };
132
+ }
133
+ }
134
+
73
135
  // src/strategies/yjs-richtext.ts
74
136
  import * as Y from "yjs";
75
137
  var TEXT_KEY = "content";
@@ -106,6 +168,88 @@ function toYjsUpdate(value) {
106
168
  throw new Error("Richtext value must be a string, Uint8Array, ArrayBuffer, null, or undefined.");
107
169
  }
108
170
 
171
+ // src/strategies/schema-strategies.ts
172
+ import "@korajs/core";
173
+ function counterMerge(localValue, remoteValue, baseValue) {
174
+ const base = typeof baseValue === "number" ? baseValue : 0;
175
+ const local = typeof localValue === "number" ? localValue : base;
176
+ const remote = typeof remoteValue === "number" ? remoteValue : base;
177
+ const localDelta = local - base;
178
+ const remoteDelta = remote - base;
179
+ return base + localDelta + remoteDelta;
180
+ }
181
+ function maxMerge(localValue, remoteValue, baseValue) {
182
+ const vals = [];
183
+ if (typeof baseValue === "number") vals.push(baseValue);
184
+ if (typeof localValue === "number") vals.push(localValue);
185
+ if (typeof remoteValue === "number") vals.push(remoteValue);
186
+ if (vals.length === 0) return baseValue;
187
+ return Math.max(...vals);
188
+ }
189
+ function minMerge(localValue, remoteValue, baseValue) {
190
+ const vals = [];
191
+ if (typeof baseValue === "number") vals.push(baseValue);
192
+ if (typeof localValue === "number") vals.push(localValue);
193
+ if (typeof remoteValue === "number") vals.push(remoteValue);
194
+ if (vals.length === 0) return baseValue;
195
+ return Math.min(...vals);
196
+ }
197
+ function appendOnlyMerge(localValue, remoteValue, baseValue) {
198
+ const base = Array.isArray(baseValue) ? baseValue : [];
199
+ const local = Array.isArray(localValue) ? localValue : [];
200
+ const remote = Array.isArray(remoteValue) ? remoteValue : [];
201
+ const serialize = (v) => JSON.stringify(v);
202
+ const baseSet = new Set(base.map(serialize));
203
+ const result = [...base];
204
+ const resultSet = new Set(base.map(serialize));
205
+ for (const item of local) {
206
+ const s = serialize(item);
207
+ if (!baseSet.has(s) && !resultSet.has(s)) {
208
+ result.push(item);
209
+ resultSet.add(s);
210
+ }
211
+ }
212
+ for (const item of remote) {
213
+ const s = serialize(item);
214
+ if (!baseSet.has(s) && !resultSet.has(s)) {
215
+ result.push(item);
216
+ resultSet.add(s);
217
+ }
218
+ }
219
+ return result;
220
+ }
221
+ function serverAuthoritativeMerge(_localValue, remoteValue, _baseValue) {
222
+ return remoteValue;
223
+ }
224
+ function applySchemaStrategy(strategy, localValue, remoteValue, baseValue, localTimestamp, remoteTimestamp) {
225
+ switch (strategy) {
226
+ case "counter":
227
+ return {
228
+ value: counterMerge(localValue, remoteValue, baseValue),
229
+ strategyName: "schema-counter"
230
+ };
231
+ case "max":
232
+ return { value: maxMerge(localValue, remoteValue, baseValue), strategyName: "schema-max" };
233
+ case "min":
234
+ return { value: minMerge(localValue, remoteValue, baseValue), strategyName: "schema-min" };
235
+ case "append-only":
236
+ return {
237
+ value: appendOnlyMerge(localValue, remoteValue, baseValue),
238
+ strategyName: "schema-append-only"
239
+ };
240
+ case "server-authoritative":
241
+ return {
242
+ value: serverAuthoritativeMerge(localValue, remoteValue, baseValue),
243
+ strategyName: "schema-server-authoritative"
244
+ };
245
+ case "lww":
246
+ case "union":
247
+ return null;
248
+ default:
249
+ return null;
250
+ }
251
+ }
252
+
109
253
  // src/engine/field-merger.ts
110
254
  function mergeField(fieldName, localOp, remoteOp, baseState, fieldDescriptor, resolver) {
111
255
  const startTime = Date.now();
@@ -175,6 +319,53 @@ function mergeField(fieldName, localOp, remoteOp, baseState, fieldDescriptor, re
175
319
  startTime
176
320
  );
177
321
  }
322
+ const localAtomicOp = localOp.atomicOps?.[fieldName];
323
+ const remoteAtomicOp = remoteOp.atomicOps?.[fieldName];
324
+ if (localAtomicOp !== void 0 && remoteAtomicOp !== void 0) {
325
+ const atomicResult = mergeAtomicOps(
326
+ localAtomicOp,
327
+ remoteAtomicOp,
328
+ baseValue
329
+ );
330
+ if (atomicResult.merged) {
331
+ return createResult(
332
+ atomicResult.value,
333
+ fieldName,
334
+ localOp,
335
+ remoteOp,
336
+ localValue,
337
+ remoteValue,
338
+ baseValue,
339
+ atomicResult.strategy,
340
+ 1,
341
+ startTime
342
+ );
343
+ }
344
+ }
345
+ if (fieldDescriptor.mergeStrategy !== null && fieldDescriptor.mergeStrategy !== void 0) {
346
+ const schemaResult = applySchemaStrategy(
347
+ fieldDescriptor.mergeStrategy,
348
+ localValue,
349
+ remoteValue,
350
+ baseValue,
351
+ localOp.timestamp,
352
+ remoteOp.timestamp
353
+ );
354
+ if (schemaResult !== null) {
355
+ return createResult(
356
+ schemaResult.value,
357
+ fieldName,
358
+ localOp,
359
+ remoteOp,
360
+ localValue,
361
+ remoteValue,
362
+ baseValue,
363
+ schemaResult.strategyName,
364
+ 1,
365
+ startTime
366
+ );
367
+ }
368
+ }
178
369
  return autoMerge(
179
370
  fieldName,
180
371
  localOp,
@@ -369,13 +560,13 @@ function matchesWhere(record, where) {
369
560
  }
370
561
 
371
562
  // src/constraints/resolvers.ts
372
- import { HybridLogicalClock as HybridLogicalClock2 } from "@korajs/core";
563
+ import { HybridLogicalClock as HybridLogicalClock3 } from "@korajs/core";
373
564
  function resolveConstraintViolation(violation, mergedRecord, localOp, remoteOp, baseState) {
374
565
  const startTime = Date.now();
375
566
  const { constraint } = violation;
376
567
  switch (constraint.onConflict) {
377
568
  case "last-write-wins": {
378
- const comparison = HybridLogicalClock2.compare(localOp.timestamp, remoteOp.timestamp);
569
+ const comparison = HybridLogicalClock3.compare(localOp.timestamp, remoteOp.timestamp);
379
570
  const winner = comparison >= 0 ? localOp : remoteOp;
380
571
  const resolvedRecord = applyWinnerFields(mergedRecord, winner, violation.fields);
381
572
  return createResolution(
@@ -389,7 +580,7 @@ function resolveConstraintViolation(violation, mergedRecord, localOp, remoteOp,
389
580
  );
390
581
  }
391
582
  case "first-write-wins": {
392
- const comparison = HybridLogicalClock2.compare(localOp.timestamp, remoteOp.timestamp);
583
+ const comparison = HybridLogicalClock3.compare(localOp.timestamp, remoteOp.timestamp);
393
584
  const winner = comparison <= 0 ? localOp : remoteOp;
394
585
  const resolvedRecord = applyWinnerFields(mergedRecord, winner, violation.fields);
395
586
  return createResolution(
@@ -405,7 +596,7 @@ function resolveConstraintViolation(violation, mergedRecord, localOp, remoteOp,
405
596
  case "priority-field": {
406
597
  const priorityField = constraint.priorityField;
407
598
  if (priorityField === void 0) {
408
- const comparison = HybridLogicalClock2.compare(localOp.timestamp, remoteOp.timestamp);
599
+ const comparison = HybridLogicalClock3.compare(localOp.timestamp, remoteOp.timestamp);
409
600
  const winner2 = comparison >= 0 ? localOp : remoteOp;
410
601
  const resolvedRecord2 = applyWinnerFields(mergedRecord, winner2, violation.fields);
411
602
  return createResolution(
@@ -449,7 +640,7 @@ function resolveConstraintViolation(violation, mergedRecord, localOp, remoteOp,
449
640
  }
450
641
  case "custom": {
451
642
  if (constraint.resolve === void 0) {
452
- const comparison = HybridLogicalClock2.compare(localOp.timestamp, remoteOp.timestamp);
643
+ const comparison = HybridLogicalClock3.compare(localOp.timestamp, remoteOp.timestamp);
453
644
  const winner = comparison >= 0 ? localOp : remoteOp;
454
645
  const resolvedRecord2 = applyWinnerFields(mergedRecord, winner, violation.fields);
455
646
  return createResolution(
@@ -540,8 +731,426 @@ function extractFields(record, fields) {
540
731
  return result;
541
732
  }
542
733
 
734
+ // src/constraints/referential-integrity.ts
735
+ import "@korajs/core";
736
+ function buildMergeRelationLookup(schema) {
737
+ const lookup = /* @__PURE__ */ new Map();
738
+ const relationNames = Object.keys(schema.relations).sort();
739
+ for (const relationName of relationNames) {
740
+ const relation = schema.relations[relationName];
741
+ if (relation === void 0) {
742
+ continue;
743
+ }
744
+ const targetCollection = relation.to;
745
+ const existing = lookup.get(targetCollection) ?? [];
746
+ existing.push({
747
+ relationName,
748
+ sourceCollection: relation.from,
749
+ foreignKeyField: relation.field,
750
+ onDelete: relation.onDelete
751
+ });
752
+ lookup.set(targetCollection, existing);
753
+ }
754
+ for (const [, relations] of lookup) {
755
+ relations.sort(
756
+ (a, b) => a.relationName < b.relationName ? -1 : a.relationName > b.relationName ? 1 : 0
757
+ );
758
+ }
759
+ return lookup;
760
+ }
761
+ async function checkReferentialIntegrityOnDelete(deleteOp, schema, ctx, relationLookup) {
762
+ const lookup = relationLookup ?? buildMergeRelationLookup(schema);
763
+ const incomingRelations = lookup.get(deleteOp.collection);
764
+ if (incomingRelations === void 0 || incomingRelations.length === 0) {
765
+ return { allowed: true, sideEffectOps: [], traces: [] };
766
+ }
767
+ const allSideEffects = [];
768
+ const allTraces = [];
769
+ for (const relation of incomingRelations) {
770
+ const startTime = Date.now();
771
+ const referencingRecords = await ctx.queryRecords(relation.sourceCollection, {
772
+ [relation.foreignKeyField]: deleteOp.recordId
773
+ });
774
+ const sortedRecords = [...referencingRecords].sort((a, b) => {
775
+ const idA = String(a.id ?? "");
776
+ const idB = String(b.id ?? "");
777
+ return idA < idB ? -1 : idA > idB ? 1 : 0;
778
+ });
779
+ if (sortedRecords.length === 0) {
780
+ const trace = createReferentialTrace(
781
+ deleteOp,
782
+ relation,
783
+ `referential-${relation.onDelete}`,
784
+ null,
785
+ null,
786
+ true,
787
+ startTime
788
+ );
789
+ allTraces.push(trace);
790
+ continue;
791
+ }
792
+ switch (relation.onDelete) {
793
+ case "restrict": {
794
+ const trace = createReferentialTrace(
795
+ deleteOp,
796
+ relation,
797
+ "referential-restrict",
798
+ sortedRecords.map((r) => String(r.id ?? "")),
799
+ null,
800
+ false,
801
+ startTime
802
+ );
803
+ allTraces.push(trace);
804
+ return {
805
+ allowed: false,
806
+ sideEffectOps: [],
807
+ traces: allTraces
808
+ };
809
+ }
810
+ case "cascade": {
811
+ for (const record of sortedRecords) {
812
+ const recordId = String(record.id ?? "");
813
+ allSideEffects.push({
814
+ type: "delete",
815
+ collection: relation.sourceCollection,
816
+ recordId,
817
+ data: null,
818
+ previousData: null,
819
+ policy: "cascade",
820
+ relationName: relation.relationName
821
+ });
822
+ }
823
+ const trace = createReferentialTrace(
824
+ deleteOp,
825
+ relation,
826
+ "referential-cascade",
827
+ sortedRecords.map((r) => String(r.id ?? "")),
828
+ sortedRecords.map((r) => ({ type: "delete", recordId: String(r.id ?? "") })),
829
+ true,
830
+ startTime
831
+ );
832
+ allTraces.push(trace);
833
+ break;
834
+ }
835
+ case "set-null": {
836
+ for (const record of sortedRecords) {
837
+ const recordId = String(record.id ?? "");
838
+ const previousValue = record[relation.foreignKeyField];
839
+ allSideEffects.push({
840
+ type: "update",
841
+ collection: relation.sourceCollection,
842
+ recordId,
843
+ data: { [relation.foreignKeyField]: null },
844
+ previousData: { [relation.foreignKeyField]: previousValue },
845
+ policy: "set-null",
846
+ relationName: relation.relationName
847
+ });
848
+ }
849
+ const trace = createReferentialTrace(
850
+ deleteOp,
851
+ relation,
852
+ "referential-set-null",
853
+ sortedRecords.map((r) => String(r.id ?? "")),
854
+ sortedRecords.map((r) => ({
855
+ type: "update",
856
+ recordId: String(r.id ?? ""),
857
+ field: relation.foreignKeyField,
858
+ newValue: null
859
+ })),
860
+ true,
861
+ startTime
862
+ );
863
+ allTraces.push(trace);
864
+ break;
865
+ }
866
+ case "no-action": {
867
+ const trace = createReferentialTrace(
868
+ deleteOp,
869
+ relation,
870
+ "referential-no-action",
871
+ sortedRecords.map((r) => String(r.id ?? "")),
872
+ null,
873
+ true,
874
+ startTime
875
+ );
876
+ allTraces.push(trace);
877
+ break;
878
+ }
879
+ }
880
+ }
881
+ return {
882
+ allowed: true,
883
+ sideEffectOps: allSideEffects,
884
+ traces: allTraces
885
+ };
886
+ }
887
+ function resolveDeleteVsInsertConflict(deleteOp, insertOp, relation) {
888
+ const startTime = Date.now();
889
+ switch (relation.onDelete) {
890
+ case "restrict": {
891
+ const trace = createDeleteVsInsertTrace(
892
+ deleteOp,
893
+ insertOp,
894
+ relation,
895
+ "referential-restrict",
896
+ "block-delete",
897
+ startTime
898
+ );
899
+ return {
900
+ action: "block-delete",
901
+ sideEffects: [],
902
+ trace
903
+ };
904
+ }
905
+ case "cascade": {
906
+ const sideEffect = {
907
+ type: "delete",
908
+ collection: relation.sourceCollection,
909
+ recordId: insertOp.recordId,
910
+ data: null,
911
+ previousData: null,
912
+ policy: "cascade",
913
+ relationName: relation.relationName
914
+ };
915
+ const trace = createDeleteVsInsertTrace(
916
+ deleteOp,
917
+ insertOp,
918
+ relation,
919
+ "referential-cascade",
920
+ "allow-delete",
921
+ startTime
922
+ );
923
+ return {
924
+ action: "allow-delete",
925
+ sideEffects: [sideEffect],
926
+ trace
927
+ };
928
+ }
929
+ case "set-null": {
930
+ const fkValue = insertOp.data !== null ? insertOp.data[relation.foreignKeyField] : void 0;
931
+ const sideEffect = {
932
+ type: "update",
933
+ collection: relation.sourceCollection,
934
+ recordId: insertOp.recordId,
935
+ data: { [relation.foreignKeyField]: null },
936
+ previousData: { [relation.foreignKeyField]: fkValue ?? null },
937
+ policy: "set-null",
938
+ relationName: relation.relationName
939
+ };
940
+ const trace = createDeleteVsInsertTrace(
941
+ deleteOp,
942
+ insertOp,
943
+ relation,
944
+ "referential-set-null",
945
+ "allow-delete",
946
+ startTime
947
+ );
948
+ return {
949
+ action: "allow-delete",
950
+ sideEffects: [sideEffect],
951
+ trace
952
+ };
953
+ }
954
+ case "no-action": {
955
+ const trace = createDeleteVsInsertTrace(
956
+ deleteOp,
957
+ insertOp,
958
+ relation,
959
+ "referential-no-action",
960
+ "allow-delete",
961
+ startTime
962
+ );
963
+ return {
964
+ action: "allow-delete",
965
+ sideEffects: [],
966
+ trace
967
+ };
968
+ }
969
+ }
970
+ }
971
+ function createReferentialTrace(deleteOp, relation, strategy, referencingRecordIds, sideEffects, allowed, startTime) {
972
+ return {
973
+ operationA: deleteOp,
974
+ // For referential integrity checks, operationB is the same as operationA
975
+ // because we are checking constraints against the delete operation itself,
976
+ // not merging two conflicting operations.
977
+ operationB: deleteOp,
978
+ field: `${relation.sourceCollection}.${relation.foreignKeyField}`,
979
+ strategy,
980
+ inputA: { recordId: deleteOp.recordId, collection: deleteOp.collection },
981
+ inputB: referencingRecordIds,
982
+ base: null,
983
+ output: { allowed, sideEffects },
984
+ tier: 2,
985
+ constraintViolated: `referential:${relation.relationName}`,
986
+ duration: Date.now() - startTime
987
+ };
988
+ }
989
+ function createDeleteVsInsertTrace(deleteOp, insertOp, relation, strategy, action, startTime) {
990
+ return {
991
+ operationA: deleteOp,
992
+ operationB: insertOp,
993
+ field: `${relation.sourceCollection}.${relation.foreignKeyField}`,
994
+ strategy,
995
+ inputA: { type: "delete", recordId: deleteOp.recordId, collection: deleteOp.collection },
996
+ inputB: { type: "insert", recordId: insertOp.recordId, collection: insertOp.collection },
997
+ base: null,
998
+ output: { action },
999
+ tier: 2,
1000
+ constraintViolated: `referential:${relation.relationName}`,
1001
+ duration: Date.now() - startTime
1002
+ };
1003
+ }
1004
+
1005
+ // src/engine/merge-engine.ts
1006
+ import { HybridLogicalClock as HybridLogicalClock6 } from "@korajs/core";
1007
+
1008
+ // src/constraints/state-machine-constraint.ts
1009
+ import { HybridLogicalClock as HybridLogicalClock5, validateTransition } from "@korajs/core";
1010
+ function isValid(sm, from, to) {
1011
+ const constraint = {
1012
+ field: sm.field,
1013
+ collection: "",
1014
+ transitions: sm.transitions
1015
+ };
1016
+ return validateTransition(constraint, from, to).valid;
1017
+ }
1018
+ function resolveStateMachineMerge(fieldName, localOp, remoteOp, baseState, stateMachine) {
1019
+ const startTime = Date.now();
1020
+ const baseValue = baseState[fieldName];
1021
+ const localData = localOp.data ?? {};
1022
+ const remoteData = remoteOp.data ?? {};
1023
+ const localValue = localData[fieldName];
1024
+ const remoteValue = remoteData[fieldName];
1025
+ const baseStateStr = typeof baseValue === "string" ? baseValue : "";
1026
+ const localStr = typeof localValue === "string" ? localValue : baseStateStr;
1027
+ const remoteStr = typeof remoteValue === "string" ? remoteValue : baseStateStr;
1028
+ const localChanged = fieldName in localData;
1029
+ const remoteChanged = fieldName in remoteData;
1030
+ if (localChanged && !remoteChanged) {
1031
+ const valid = isValid(stateMachine, baseStateStr, localStr);
1032
+ return makeResult(
1033
+ valid ? localStr : baseStateStr,
1034
+ fieldName,
1035
+ localOp,
1036
+ remoteOp,
1037
+ localValue,
1038
+ baseValue,
1039
+ baseValue,
1040
+ valid ? "state-machine-no-conflict-local" : "state-machine-invalid-local",
1041
+ valid ? null : `Invalid transition from "${baseStateStr}" to "${localStr}"`,
1042
+ startTime
1043
+ );
1044
+ }
1045
+ if (!localChanged && remoteChanged) {
1046
+ const valid = isValid(stateMachine, baseStateStr, remoteStr);
1047
+ return makeResult(
1048
+ valid ? remoteStr : baseStateStr,
1049
+ fieldName,
1050
+ localOp,
1051
+ remoteOp,
1052
+ baseValue,
1053
+ remoteValue,
1054
+ baseValue,
1055
+ valid ? "state-machine-no-conflict-remote" : "state-machine-invalid-remote",
1056
+ valid ? null : `Invalid transition from "${baseStateStr}" to "${remoteStr}"`,
1057
+ startTime
1058
+ );
1059
+ }
1060
+ if (!localChanged && !remoteChanged) {
1061
+ return makeResult(
1062
+ baseStateStr,
1063
+ fieldName,
1064
+ localOp,
1065
+ remoteOp,
1066
+ baseValue,
1067
+ baseValue,
1068
+ baseValue,
1069
+ "state-machine-no-conflict-unchanged",
1070
+ null,
1071
+ startTime
1072
+ );
1073
+ }
1074
+ const localValid = isValid(stateMachine, baseStateStr, localStr);
1075
+ const remoteValid = isValid(stateMachine, baseStateStr, remoteStr);
1076
+ if (localValid && remoteValid) {
1077
+ const comparison = HybridLogicalClock5.compare(localOp.timestamp, remoteOp.timestamp);
1078
+ const winner = comparison >= 0 ? localStr : remoteStr;
1079
+ return makeResult(
1080
+ winner,
1081
+ fieldName,
1082
+ localOp,
1083
+ remoteOp,
1084
+ localValue,
1085
+ remoteValue,
1086
+ baseValue,
1087
+ "state-machine-lww",
1088
+ null,
1089
+ startTime
1090
+ );
1091
+ }
1092
+ if (localValid && !remoteValid) {
1093
+ return makeResult(
1094
+ localStr,
1095
+ fieldName,
1096
+ localOp,
1097
+ remoteOp,
1098
+ localValue,
1099
+ remoteValue,
1100
+ baseValue,
1101
+ "state-machine-valid-wins",
1102
+ `Remote transition from "${baseStateStr}" to "${remoteStr}" is invalid; local "${localStr}" wins`,
1103
+ startTime
1104
+ );
1105
+ }
1106
+ if (!localValid && remoteValid) {
1107
+ return makeResult(
1108
+ remoteStr,
1109
+ fieldName,
1110
+ localOp,
1111
+ remoteOp,
1112
+ localValue,
1113
+ remoteValue,
1114
+ baseValue,
1115
+ "state-machine-valid-wins",
1116
+ `Local transition from "${baseStateStr}" to "${localStr}" is invalid; remote "${remoteStr}" wins`,
1117
+ startTime
1118
+ );
1119
+ }
1120
+ return makeResult(
1121
+ baseStateStr,
1122
+ fieldName,
1123
+ localOp,
1124
+ remoteOp,
1125
+ localValue,
1126
+ remoteValue,
1127
+ baseValue,
1128
+ "state-machine-both-invalid",
1129
+ `Both transitions invalid from "${baseStateStr}": local to "${localStr}", remote to "${remoteStr}". Keeping base state.`,
1130
+ startTime
1131
+ );
1132
+ }
1133
+ function isStateMachineField(collectionDef, fieldName) {
1134
+ return collectionDef.stateMachine !== void 0 && collectionDef.stateMachine.field === fieldName;
1135
+ }
1136
+ function makeResult(value, field, operationA, operationB, inputA, inputB, base, strategy, constraintViolated, startTime) {
1137
+ const trace = {
1138
+ operationA,
1139
+ operationB,
1140
+ field,
1141
+ strategy,
1142
+ inputA,
1143
+ inputB,
1144
+ base,
1145
+ output: value,
1146
+ tier: 2,
1147
+ constraintViolated,
1148
+ duration: Date.now() - startTime
1149
+ };
1150
+ return { value, trace };
1151
+ }
1152
+
543
1153
  // src/engine/merge-engine.ts
544
- import { HybridLogicalClock as HybridLogicalClock3 } from "@korajs/core";
545
1154
  var MergeEngine = class {
546
1155
  /**
547
1156
  * Merge two concurrent operations with all three tiers.
@@ -619,6 +1228,20 @@ var MergeEngine = class {
619
1228
  if (fieldDef === void 0) {
620
1229
  continue;
621
1230
  }
1231
+ if (collectionDef.stateMachine !== void 0 && isStateMachineField(collectionDef, fieldName)) {
1232
+ const smResult = resolveStateMachineMerge(
1233
+ fieldName,
1234
+ local,
1235
+ remote,
1236
+ baseState,
1237
+ collectionDef.stateMachine
1238
+ );
1239
+ mergedData[fieldName] = smResult.value;
1240
+ if (smResult.trace.strategy !== "state-machine-no-conflict-unchanged") {
1241
+ traces.push(smResult.trace);
1242
+ }
1243
+ continue;
1244
+ }
622
1245
  const resolver = collectionDef.resolvers[fieldName];
623
1246
  const result = mergeField(fieldName, local, remote, baseState, fieldDef, resolver);
624
1247
  mergedData[fieldName] = result.value;
@@ -638,7 +1261,7 @@ var MergeEngine = class {
638
1261
  */
639
1262
  mergeWithDelete(input) {
640
1263
  const { local, remote } = input;
641
- const comparison = HybridLogicalClock3.compare(local.timestamp, remote.timestamp);
1264
+ const comparison = HybridLogicalClock6.compare(local.timestamp, remote.timestamp);
642
1265
  if (comparison >= 0) {
643
1266
  if (local.type === "delete") {
644
1267
  return { mergedData: {}, traces: [], appliedOperation: "local" };
@@ -707,12 +1330,22 @@ function determineAppliedOperation(traces) {
707
1330
  export {
708
1331
  MergeEngine,
709
1332
  addWinsSet,
1333
+ appendOnlyMerge,
1334
+ applySchemaStrategy,
1335
+ buildMergeRelationLookup,
710
1336
  checkConstraints,
1337
+ checkReferentialIntegrityOnDelete,
1338
+ counterMerge,
711
1339
  lastWriteWins,
1340
+ maxMerge,
1341
+ mergeAtomicOps,
712
1342
  mergeField,
713
1343
  mergeRichtext,
1344
+ minMerge,
714
1345
  resolveConstraintViolation,
1346
+ resolveDeleteVsInsertConflict,
715
1347
  richtextToString,
1348
+ serverAuthoritativeMerge,
716
1349
  stringToRichtextUpdate
717
1350
  };
718
1351
  //# sourceMappingURL=index.js.map