@korajs/merge 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.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.
@@ -563,7 +1172,8 @@ var MergeEngine = class {
563
1172
  return {
564
1173
  mergedData: {},
565
1174
  traces: [],
566
- appliedOperation: "merged"
1175
+ appliedOperation: "merged",
1176
+ sideEffects: []
567
1177
  };
568
1178
  }
569
1179
  if (input.local.type === "delete" || input.remote.type === "delete") {
@@ -581,6 +1191,7 @@ var MergeEngine = class {
581
1191
  );
582
1192
  let mergedData = fieldResult.mergedData;
583
1193
  const allTraces = [...fieldResult.traces];
1194
+ const allSideEffects = [...fieldResult.sideEffects];
584
1195
  for (const violation of violations) {
585
1196
  const resolution = resolveConstraintViolation(
586
1197
  violation,
@@ -591,11 +1202,15 @@ var MergeEngine = class {
591
1202
  );
592
1203
  mergedData = resolution.resolvedRecord;
593
1204
  allTraces.push(resolution.trace);
1205
+ if (resolution.sideEffects) {
1206
+ allSideEffects.push(...resolution.sideEffects);
1207
+ }
594
1208
  }
595
1209
  return {
596
1210
  mergedData,
597
1211
  traces: allTraces,
598
- appliedOperation: determineAppliedOperation(allTraces)
1212
+ appliedOperation: determineAppliedOperation(allTraces),
1213
+ sideEffects: allSideEffects
599
1214
  };
600
1215
  }
601
1216
  return fieldResult;
@@ -619,6 +1234,20 @@ var MergeEngine = class {
619
1234
  if (fieldDef === void 0) {
620
1235
  continue;
621
1236
  }
1237
+ if (collectionDef.stateMachine !== void 0 && isStateMachineField(collectionDef, fieldName)) {
1238
+ const smResult = resolveStateMachineMerge(
1239
+ fieldName,
1240
+ local,
1241
+ remote,
1242
+ baseState,
1243
+ collectionDef.stateMachine
1244
+ );
1245
+ mergedData[fieldName] = smResult.value;
1246
+ if (smResult.trace.strategy !== "state-machine-no-conflict-unchanged") {
1247
+ traces.push(smResult.trace);
1248
+ }
1249
+ continue;
1250
+ }
622
1251
  const resolver = collectionDef.resolvers[fieldName];
623
1252
  const result = mergeField(fieldName, local, remote, baseState, fieldDef, resolver);
624
1253
  mergedData[fieldName] = result.value;
@@ -629,7 +1258,8 @@ var MergeEngine = class {
629
1258
  return {
630
1259
  mergedData,
631
1260
  traces,
632
- appliedOperation: determineAppliedOperation(traces)
1261
+ appliedOperation: determineAppliedOperation(traces),
1262
+ sideEffects: []
633
1263
  };
634
1264
  }
635
1265
  /**
@@ -638,24 +1268,26 @@ var MergeEngine = class {
638
1268
  */
639
1269
  mergeWithDelete(input) {
640
1270
  const { local, remote } = input;
641
- const comparison = HybridLogicalClock3.compare(local.timestamp, remote.timestamp);
1271
+ const comparison = HybridLogicalClock6.compare(local.timestamp, remote.timestamp);
642
1272
  if (comparison >= 0) {
643
1273
  if (local.type === "delete") {
644
- return { mergedData: {}, traces: [], appliedOperation: "local" };
1274
+ return { mergedData: {}, traces: [], appliedOperation: "local", sideEffects: [] };
645
1275
  }
646
1276
  return {
647
1277
  mergedData: { ...input.baseState, ...local.data ?? {} },
648
1278
  traces: [],
649
- appliedOperation: "local"
1279
+ appliedOperation: "local",
1280
+ sideEffects: []
650
1281
  };
651
1282
  }
652
1283
  if (remote.type === "delete") {
653
- return { mergedData: {}, traces: [], appliedOperation: "remote" };
1284
+ return { mergedData: {}, traces: [], appliedOperation: "remote", sideEffects: [] };
654
1285
  }
655
1286
  return {
656
1287
  mergedData: { ...input.baseState, ...remote.data ?? {} },
657
1288
  traces: [],
658
- appliedOperation: "remote"
1289
+ appliedOperation: "remote",
1290
+ sideEffects: []
659
1291
  };
660
1292
  }
661
1293
  };
@@ -707,12 +1339,22 @@ function determineAppliedOperation(traces) {
707
1339
  export {
708
1340
  MergeEngine,
709
1341
  addWinsSet,
1342
+ appendOnlyMerge,
1343
+ applySchemaStrategy,
1344
+ buildMergeRelationLookup,
710
1345
  checkConstraints,
1346
+ checkReferentialIntegrityOnDelete,
1347
+ counterMerge,
711
1348
  lastWriteWins,
1349
+ maxMerge,
1350
+ mergeAtomicOps,
712
1351
  mergeField,
713
1352
  mergeRichtext,
1353
+ minMerge,
714
1354
  resolveConstraintViolation,
1355
+ resolveDeleteVsInsertConflict,
715
1356
  richtextToString,
1357
+ serverAuthoritativeMerge,
716
1358
  stringToRichtextUpdate
717
1359
  };
718
1360
  //# sourceMappingURL=index.js.map