@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.cjs CHANGED
@@ -32,12 +32,22 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  MergeEngine: () => MergeEngine,
34
34
  addWinsSet: () => addWinsSet,
35
+ appendOnlyMerge: () => appendOnlyMerge,
36
+ applySchemaStrategy: () => applySchemaStrategy,
37
+ buildMergeRelationLookup: () => buildMergeRelationLookup,
35
38
  checkConstraints: () => checkConstraints,
39
+ checkReferentialIntegrityOnDelete: () => checkReferentialIntegrityOnDelete,
40
+ counterMerge: () => counterMerge,
36
41
  lastWriteWins: () => lastWriteWins,
42
+ maxMerge: () => maxMerge,
43
+ mergeAtomicOps: () => mergeAtomicOps,
37
44
  mergeField: () => mergeField,
38
45
  mergeRichtext: () => mergeRichtext,
46
+ minMerge: () => minMerge,
39
47
  resolveConstraintViolation: () => resolveConstraintViolation,
48
+ resolveDeleteVsInsertConflict: () => resolveDeleteVsInsertConflict,
40
49
  richtextToString: () => richtextToString,
50
+ serverAuthoritativeMerge: () => serverAuthoritativeMerge,
41
51
  stringToRichtextUpdate: () => stringToRichtextUpdate
42
52
  });
43
53
  module.exports = __toCommonJS(index_exports);
@@ -114,6 +124,68 @@ function addWinsSet(localArray, remoteArray, baseArray) {
114
124
  return result;
115
125
  }
116
126
 
127
+ // src/strategies/atomic-merge.ts
128
+ function mergeAtomicOps(localAtomicOp, remoteAtomicOp, baseValue) {
129
+ if (localAtomicOp.type !== remoteAtomicOp.type) {
130
+ return { merged: false };
131
+ }
132
+ switch (localAtomicOp.type) {
133
+ case "increment": {
134
+ const base = typeof baseValue === "number" ? baseValue : 0;
135
+ const localDelta = localAtomicOp.value;
136
+ const remoteDelta = remoteAtomicOp.value;
137
+ return {
138
+ merged: true,
139
+ value: base + localDelta + remoteDelta,
140
+ strategy: "atomic-increment"
141
+ };
142
+ }
143
+ case "max": {
144
+ const base = typeof baseValue === "number" ? baseValue : Number.NEGATIVE_INFINITY;
145
+ const localVal = localAtomicOp.value;
146
+ const remoteVal = remoteAtomicOp.value;
147
+ return {
148
+ merged: true,
149
+ value: Math.max(base, localVal, remoteVal),
150
+ strategy: "atomic-max"
151
+ };
152
+ }
153
+ case "min": {
154
+ const base = typeof baseValue === "number" ? baseValue : Number.POSITIVE_INFINITY;
155
+ const localVal = localAtomicOp.value;
156
+ const remoteVal = remoteAtomicOp.value;
157
+ return {
158
+ merged: true,
159
+ value: Math.min(base, localVal, remoteVal),
160
+ strategy: "atomic-min"
161
+ };
162
+ }
163
+ case "append": {
164
+ const base = Array.isArray(baseValue) ? [...baseValue] : [];
165
+ base.push(localAtomicOp.value);
166
+ base.push(remoteAtomicOp.value);
167
+ return {
168
+ merged: true,
169
+ value: base,
170
+ strategy: "atomic-append"
171
+ };
172
+ }
173
+ case "remove": {
174
+ const base = Array.isArray(baseValue) ? [...baseValue] : [];
175
+ const localItem = localAtomicOp.value;
176
+ const remoteItem = remoteAtomicOp.value;
177
+ const result = base.filter((item) => item !== localItem && item !== remoteItem);
178
+ return {
179
+ merged: true,
180
+ value: result,
181
+ strategy: "atomic-remove"
182
+ };
183
+ }
184
+ default:
185
+ return { merged: false };
186
+ }
187
+ }
188
+
117
189
  // src/strategies/yjs-richtext.ts
118
190
  var Y = __toESM(require("yjs"), 1);
119
191
  var TEXT_KEY = "content";
@@ -150,6 +222,88 @@ function toYjsUpdate(value) {
150
222
  throw new Error("Richtext value must be a string, Uint8Array, ArrayBuffer, null, or undefined.");
151
223
  }
152
224
 
225
+ // src/strategies/schema-strategies.ts
226
+ var import_core2 = require("@korajs/core");
227
+ function counterMerge(localValue, remoteValue, baseValue) {
228
+ const base = typeof baseValue === "number" ? baseValue : 0;
229
+ const local = typeof localValue === "number" ? localValue : base;
230
+ const remote = typeof remoteValue === "number" ? remoteValue : base;
231
+ const localDelta = local - base;
232
+ const remoteDelta = remote - base;
233
+ return base + localDelta + remoteDelta;
234
+ }
235
+ function maxMerge(localValue, remoteValue, baseValue) {
236
+ const vals = [];
237
+ if (typeof baseValue === "number") vals.push(baseValue);
238
+ if (typeof localValue === "number") vals.push(localValue);
239
+ if (typeof remoteValue === "number") vals.push(remoteValue);
240
+ if (vals.length === 0) return baseValue;
241
+ return Math.max(...vals);
242
+ }
243
+ function minMerge(localValue, remoteValue, baseValue) {
244
+ const vals = [];
245
+ if (typeof baseValue === "number") vals.push(baseValue);
246
+ if (typeof localValue === "number") vals.push(localValue);
247
+ if (typeof remoteValue === "number") vals.push(remoteValue);
248
+ if (vals.length === 0) return baseValue;
249
+ return Math.min(...vals);
250
+ }
251
+ function appendOnlyMerge(localValue, remoteValue, baseValue) {
252
+ const base = Array.isArray(baseValue) ? baseValue : [];
253
+ const local = Array.isArray(localValue) ? localValue : [];
254
+ const remote = Array.isArray(remoteValue) ? remoteValue : [];
255
+ const serialize = (v) => JSON.stringify(v);
256
+ const baseSet = new Set(base.map(serialize));
257
+ const result = [...base];
258
+ const resultSet = new Set(base.map(serialize));
259
+ for (const item of local) {
260
+ const s = serialize(item);
261
+ if (!baseSet.has(s) && !resultSet.has(s)) {
262
+ result.push(item);
263
+ resultSet.add(s);
264
+ }
265
+ }
266
+ for (const item of remote) {
267
+ const s = serialize(item);
268
+ if (!baseSet.has(s) && !resultSet.has(s)) {
269
+ result.push(item);
270
+ resultSet.add(s);
271
+ }
272
+ }
273
+ return result;
274
+ }
275
+ function serverAuthoritativeMerge(_localValue, remoteValue, _baseValue) {
276
+ return remoteValue;
277
+ }
278
+ function applySchemaStrategy(strategy, localValue, remoteValue, baseValue, localTimestamp, remoteTimestamp) {
279
+ switch (strategy) {
280
+ case "counter":
281
+ return {
282
+ value: counterMerge(localValue, remoteValue, baseValue),
283
+ strategyName: "schema-counter"
284
+ };
285
+ case "max":
286
+ return { value: maxMerge(localValue, remoteValue, baseValue), strategyName: "schema-max" };
287
+ case "min":
288
+ return { value: minMerge(localValue, remoteValue, baseValue), strategyName: "schema-min" };
289
+ case "append-only":
290
+ return {
291
+ value: appendOnlyMerge(localValue, remoteValue, baseValue),
292
+ strategyName: "schema-append-only"
293
+ };
294
+ case "server-authoritative":
295
+ return {
296
+ value: serverAuthoritativeMerge(localValue, remoteValue, baseValue),
297
+ strategyName: "schema-server-authoritative"
298
+ };
299
+ case "lww":
300
+ case "union":
301
+ return null;
302
+ default:
303
+ return null;
304
+ }
305
+ }
306
+
153
307
  // src/engine/field-merger.ts
154
308
  function mergeField(fieldName, localOp, remoteOp, baseState, fieldDescriptor, resolver) {
155
309
  const startTime = Date.now();
@@ -219,6 +373,53 @@ function mergeField(fieldName, localOp, remoteOp, baseState, fieldDescriptor, re
219
373
  startTime
220
374
  );
221
375
  }
376
+ const localAtomicOp = localOp.atomicOps?.[fieldName];
377
+ const remoteAtomicOp = remoteOp.atomicOps?.[fieldName];
378
+ if (localAtomicOp !== void 0 && remoteAtomicOp !== void 0) {
379
+ const atomicResult = mergeAtomicOps(
380
+ localAtomicOp,
381
+ remoteAtomicOp,
382
+ baseValue
383
+ );
384
+ if (atomicResult.merged) {
385
+ return createResult(
386
+ atomicResult.value,
387
+ fieldName,
388
+ localOp,
389
+ remoteOp,
390
+ localValue,
391
+ remoteValue,
392
+ baseValue,
393
+ atomicResult.strategy,
394
+ 1,
395
+ startTime
396
+ );
397
+ }
398
+ }
399
+ if (fieldDescriptor.mergeStrategy !== null && fieldDescriptor.mergeStrategy !== void 0) {
400
+ const schemaResult = applySchemaStrategy(
401
+ fieldDescriptor.mergeStrategy,
402
+ localValue,
403
+ remoteValue,
404
+ baseValue,
405
+ localOp.timestamp,
406
+ remoteOp.timestamp
407
+ );
408
+ if (schemaResult !== null) {
409
+ return createResult(
410
+ schemaResult.value,
411
+ fieldName,
412
+ localOp,
413
+ remoteOp,
414
+ localValue,
415
+ remoteValue,
416
+ baseValue,
417
+ schemaResult.strategyName,
418
+ 1,
419
+ startTime
420
+ );
421
+ }
422
+ }
222
423
  return autoMerge(
223
424
  fieldName,
224
425
  localOp,
@@ -413,13 +614,13 @@ function matchesWhere(record, where) {
413
614
  }
414
615
 
415
616
  // src/constraints/resolvers.ts
416
- var import_core2 = require("@korajs/core");
617
+ var import_core3 = require("@korajs/core");
417
618
  function resolveConstraintViolation(violation, mergedRecord, localOp, remoteOp, baseState) {
418
619
  const startTime = Date.now();
419
620
  const { constraint } = violation;
420
621
  switch (constraint.onConflict) {
421
622
  case "last-write-wins": {
422
- const comparison = import_core2.HybridLogicalClock.compare(localOp.timestamp, remoteOp.timestamp);
623
+ const comparison = import_core3.HybridLogicalClock.compare(localOp.timestamp, remoteOp.timestamp);
423
624
  const winner = comparison >= 0 ? localOp : remoteOp;
424
625
  const resolvedRecord = applyWinnerFields(mergedRecord, winner, violation.fields);
425
626
  return createResolution(
@@ -433,7 +634,7 @@ function resolveConstraintViolation(violation, mergedRecord, localOp, remoteOp,
433
634
  );
434
635
  }
435
636
  case "first-write-wins": {
436
- const comparison = import_core2.HybridLogicalClock.compare(localOp.timestamp, remoteOp.timestamp);
637
+ const comparison = import_core3.HybridLogicalClock.compare(localOp.timestamp, remoteOp.timestamp);
437
638
  const winner = comparison <= 0 ? localOp : remoteOp;
438
639
  const resolvedRecord = applyWinnerFields(mergedRecord, winner, violation.fields);
439
640
  return createResolution(
@@ -449,7 +650,7 @@ function resolveConstraintViolation(violation, mergedRecord, localOp, remoteOp,
449
650
  case "priority-field": {
450
651
  const priorityField = constraint.priorityField;
451
652
  if (priorityField === void 0) {
452
- const comparison = import_core2.HybridLogicalClock.compare(localOp.timestamp, remoteOp.timestamp);
653
+ const comparison = import_core3.HybridLogicalClock.compare(localOp.timestamp, remoteOp.timestamp);
453
654
  const winner2 = comparison >= 0 ? localOp : remoteOp;
454
655
  const resolvedRecord2 = applyWinnerFields(mergedRecord, winner2, violation.fields);
455
656
  return createResolution(
@@ -493,7 +694,7 @@ function resolveConstraintViolation(violation, mergedRecord, localOp, remoteOp,
493
694
  }
494
695
  case "custom": {
495
696
  if (constraint.resolve === void 0) {
496
- const comparison = import_core2.HybridLogicalClock.compare(localOp.timestamp, remoteOp.timestamp);
697
+ const comparison = import_core3.HybridLogicalClock.compare(localOp.timestamp, remoteOp.timestamp);
497
698
  const winner = comparison >= 0 ? localOp : remoteOp;
498
699
  const resolvedRecord2 = applyWinnerFields(mergedRecord, winner, violation.fields);
499
700
  return createResolution(
@@ -584,8 +785,426 @@ function extractFields(record, fields) {
584
785
  return result;
585
786
  }
586
787
 
788
+ // src/constraints/referential-integrity.ts
789
+ var import_core4 = require("@korajs/core");
790
+ function buildMergeRelationLookup(schema) {
791
+ const lookup = /* @__PURE__ */ new Map();
792
+ const relationNames = Object.keys(schema.relations).sort();
793
+ for (const relationName of relationNames) {
794
+ const relation = schema.relations[relationName];
795
+ if (relation === void 0) {
796
+ continue;
797
+ }
798
+ const targetCollection = relation.to;
799
+ const existing = lookup.get(targetCollection) ?? [];
800
+ existing.push({
801
+ relationName,
802
+ sourceCollection: relation.from,
803
+ foreignKeyField: relation.field,
804
+ onDelete: relation.onDelete
805
+ });
806
+ lookup.set(targetCollection, existing);
807
+ }
808
+ for (const [, relations] of lookup) {
809
+ relations.sort(
810
+ (a, b) => a.relationName < b.relationName ? -1 : a.relationName > b.relationName ? 1 : 0
811
+ );
812
+ }
813
+ return lookup;
814
+ }
815
+ async function checkReferentialIntegrityOnDelete(deleteOp, schema, ctx, relationLookup) {
816
+ const lookup = relationLookup ?? buildMergeRelationLookup(schema);
817
+ const incomingRelations = lookup.get(deleteOp.collection);
818
+ if (incomingRelations === void 0 || incomingRelations.length === 0) {
819
+ return { allowed: true, sideEffectOps: [], traces: [] };
820
+ }
821
+ const allSideEffects = [];
822
+ const allTraces = [];
823
+ for (const relation of incomingRelations) {
824
+ const startTime = Date.now();
825
+ const referencingRecords = await ctx.queryRecords(relation.sourceCollection, {
826
+ [relation.foreignKeyField]: deleteOp.recordId
827
+ });
828
+ const sortedRecords = [...referencingRecords].sort((a, b) => {
829
+ const idA = String(a.id ?? "");
830
+ const idB = String(b.id ?? "");
831
+ return idA < idB ? -1 : idA > idB ? 1 : 0;
832
+ });
833
+ if (sortedRecords.length === 0) {
834
+ const trace = createReferentialTrace(
835
+ deleteOp,
836
+ relation,
837
+ `referential-${relation.onDelete}`,
838
+ null,
839
+ null,
840
+ true,
841
+ startTime
842
+ );
843
+ allTraces.push(trace);
844
+ continue;
845
+ }
846
+ switch (relation.onDelete) {
847
+ case "restrict": {
848
+ const trace = createReferentialTrace(
849
+ deleteOp,
850
+ relation,
851
+ "referential-restrict",
852
+ sortedRecords.map((r) => String(r.id ?? "")),
853
+ null,
854
+ false,
855
+ startTime
856
+ );
857
+ allTraces.push(trace);
858
+ return {
859
+ allowed: false,
860
+ sideEffectOps: [],
861
+ traces: allTraces
862
+ };
863
+ }
864
+ case "cascade": {
865
+ for (const record of sortedRecords) {
866
+ const recordId = String(record.id ?? "");
867
+ allSideEffects.push({
868
+ type: "delete",
869
+ collection: relation.sourceCollection,
870
+ recordId,
871
+ data: null,
872
+ previousData: null,
873
+ policy: "cascade",
874
+ relationName: relation.relationName
875
+ });
876
+ }
877
+ const trace = createReferentialTrace(
878
+ deleteOp,
879
+ relation,
880
+ "referential-cascade",
881
+ sortedRecords.map((r) => String(r.id ?? "")),
882
+ sortedRecords.map((r) => ({ type: "delete", recordId: String(r.id ?? "") })),
883
+ true,
884
+ startTime
885
+ );
886
+ allTraces.push(trace);
887
+ break;
888
+ }
889
+ case "set-null": {
890
+ for (const record of sortedRecords) {
891
+ const recordId = String(record.id ?? "");
892
+ const previousValue = record[relation.foreignKeyField];
893
+ allSideEffects.push({
894
+ type: "update",
895
+ collection: relation.sourceCollection,
896
+ recordId,
897
+ data: { [relation.foreignKeyField]: null },
898
+ previousData: { [relation.foreignKeyField]: previousValue },
899
+ policy: "set-null",
900
+ relationName: relation.relationName
901
+ });
902
+ }
903
+ const trace = createReferentialTrace(
904
+ deleteOp,
905
+ relation,
906
+ "referential-set-null",
907
+ sortedRecords.map((r) => String(r.id ?? "")),
908
+ sortedRecords.map((r) => ({
909
+ type: "update",
910
+ recordId: String(r.id ?? ""),
911
+ field: relation.foreignKeyField,
912
+ newValue: null
913
+ })),
914
+ true,
915
+ startTime
916
+ );
917
+ allTraces.push(trace);
918
+ break;
919
+ }
920
+ case "no-action": {
921
+ const trace = createReferentialTrace(
922
+ deleteOp,
923
+ relation,
924
+ "referential-no-action",
925
+ sortedRecords.map((r) => String(r.id ?? "")),
926
+ null,
927
+ true,
928
+ startTime
929
+ );
930
+ allTraces.push(trace);
931
+ break;
932
+ }
933
+ }
934
+ }
935
+ return {
936
+ allowed: true,
937
+ sideEffectOps: allSideEffects,
938
+ traces: allTraces
939
+ };
940
+ }
941
+ function resolveDeleteVsInsertConflict(deleteOp, insertOp, relation) {
942
+ const startTime = Date.now();
943
+ switch (relation.onDelete) {
944
+ case "restrict": {
945
+ const trace = createDeleteVsInsertTrace(
946
+ deleteOp,
947
+ insertOp,
948
+ relation,
949
+ "referential-restrict",
950
+ "block-delete",
951
+ startTime
952
+ );
953
+ return {
954
+ action: "block-delete",
955
+ sideEffects: [],
956
+ trace
957
+ };
958
+ }
959
+ case "cascade": {
960
+ const sideEffect = {
961
+ type: "delete",
962
+ collection: relation.sourceCollection,
963
+ recordId: insertOp.recordId,
964
+ data: null,
965
+ previousData: null,
966
+ policy: "cascade",
967
+ relationName: relation.relationName
968
+ };
969
+ const trace = createDeleteVsInsertTrace(
970
+ deleteOp,
971
+ insertOp,
972
+ relation,
973
+ "referential-cascade",
974
+ "allow-delete",
975
+ startTime
976
+ );
977
+ return {
978
+ action: "allow-delete",
979
+ sideEffects: [sideEffect],
980
+ trace
981
+ };
982
+ }
983
+ case "set-null": {
984
+ const fkValue = insertOp.data !== null ? insertOp.data[relation.foreignKeyField] : void 0;
985
+ const sideEffect = {
986
+ type: "update",
987
+ collection: relation.sourceCollection,
988
+ recordId: insertOp.recordId,
989
+ data: { [relation.foreignKeyField]: null },
990
+ previousData: { [relation.foreignKeyField]: fkValue ?? null },
991
+ policy: "set-null",
992
+ relationName: relation.relationName
993
+ };
994
+ const trace = createDeleteVsInsertTrace(
995
+ deleteOp,
996
+ insertOp,
997
+ relation,
998
+ "referential-set-null",
999
+ "allow-delete",
1000
+ startTime
1001
+ );
1002
+ return {
1003
+ action: "allow-delete",
1004
+ sideEffects: [sideEffect],
1005
+ trace
1006
+ };
1007
+ }
1008
+ case "no-action": {
1009
+ const trace = createDeleteVsInsertTrace(
1010
+ deleteOp,
1011
+ insertOp,
1012
+ relation,
1013
+ "referential-no-action",
1014
+ "allow-delete",
1015
+ startTime
1016
+ );
1017
+ return {
1018
+ action: "allow-delete",
1019
+ sideEffects: [],
1020
+ trace
1021
+ };
1022
+ }
1023
+ }
1024
+ }
1025
+ function createReferentialTrace(deleteOp, relation, strategy, referencingRecordIds, sideEffects, allowed, startTime) {
1026
+ return {
1027
+ operationA: deleteOp,
1028
+ // For referential integrity checks, operationB is the same as operationA
1029
+ // because we are checking constraints against the delete operation itself,
1030
+ // not merging two conflicting operations.
1031
+ operationB: deleteOp,
1032
+ field: `${relation.sourceCollection}.${relation.foreignKeyField}`,
1033
+ strategy,
1034
+ inputA: { recordId: deleteOp.recordId, collection: deleteOp.collection },
1035
+ inputB: referencingRecordIds,
1036
+ base: null,
1037
+ output: { allowed, sideEffects },
1038
+ tier: 2,
1039
+ constraintViolated: `referential:${relation.relationName}`,
1040
+ duration: Date.now() - startTime
1041
+ };
1042
+ }
1043
+ function createDeleteVsInsertTrace(deleteOp, insertOp, relation, strategy, action, startTime) {
1044
+ return {
1045
+ operationA: deleteOp,
1046
+ operationB: insertOp,
1047
+ field: `${relation.sourceCollection}.${relation.foreignKeyField}`,
1048
+ strategy,
1049
+ inputA: { type: "delete", recordId: deleteOp.recordId, collection: deleteOp.collection },
1050
+ inputB: { type: "insert", recordId: insertOp.recordId, collection: insertOp.collection },
1051
+ base: null,
1052
+ output: { action },
1053
+ tier: 2,
1054
+ constraintViolated: `referential:${relation.relationName}`,
1055
+ duration: Date.now() - startTime
1056
+ };
1057
+ }
1058
+
1059
+ // src/engine/merge-engine.ts
1060
+ var import_core6 = require("@korajs/core");
1061
+
1062
+ // src/constraints/state-machine-constraint.ts
1063
+ var import_core5 = require("@korajs/core");
1064
+ function isValid(sm, from, to) {
1065
+ const constraint = {
1066
+ field: sm.field,
1067
+ collection: "",
1068
+ transitions: sm.transitions
1069
+ };
1070
+ return (0, import_core5.validateTransition)(constraint, from, to).valid;
1071
+ }
1072
+ function resolveStateMachineMerge(fieldName, localOp, remoteOp, baseState, stateMachine) {
1073
+ const startTime = Date.now();
1074
+ const baseValue = baseState[fieldName];
1075
+ const localData = localOp.data ?? {};
1076
+ const remoteData = remoteOp.data ?? {};
1077
+ const localValue = localData[fieldName];
1078
+ const remoteValue = remoteData[fieldName];
1079
+ const baseStateStr = typeof baseValue === "string" ? baseValue : "";
1080
+ const localStr = typeof localValue === "string" ? localValue : baseStateStr;
1081
+ const remoteStr = typeof remoteValue === "string" ? remoteValue : baseStateStr;
1082
+ const localChanged = fieldName in localData;
1083
+ const remoteChanged = fieldName in remoteData;
1084
+ if (localChanged && !remoteChanged) {
1085
+ const valid = isValid(stateMachine, baseStateStr, localStr);
1086
+ return makeResult(
1087
+ valid ? localStr : baseStateStr,
1088
+ fieldName,
1089
+ localOp,
1090
+ remoteOp,
1091
+ localValue,
1092
+ baseValue,
1093
+ baseValue,
1094
+ valid ? "state-machine-no-conflict-local" : "state-machine-invalid-local",
1095
+ valid ? null : `Invalid transition from "${baseStateStr}" to "${localStr}"`,
1096
+ startTime
1097
+ );
1098
+ }
1099
+ if (!localChanged && remoteChanged) {
1100
+ const valid = isValid(stateMachine, baseStateStr, remoteStr);
1101
+ return makeResult(
1102
+ valid ? remoteStr : baseStateStr,
1103
+ fieldName,
1104
+ localOp,
1105
+ remoteOp,
1106
+ baseValue,
1107
+ remoteValue,
1108
+ baseValue,
1109
+ valid ? "state-machine-no-conflict-remote" : "state-machine-invalid-remote",
1110
+ valid ? null : `Invalid transition from "${baseStateStr}" to "${remoteStr}"`,
1111
+ startTime
1112
+ );
1113
+ }
1114
+ if (!localChanged && !remoteChanged) {
1115
+ return makeResult(
1116
+ baseStateStr,
1117
+ fieldName,
1118
+ localOp,
1119
+ remoteOp,
1120
+ baseValue,
1121
+ baseValue,
1122
+ baseValue,
1123
+ "state-machine-no-conflict-unchanged",
1124
+ null,
1125
+ startTime
1126
+ );
1127
+ }
1128
+ const localValid = isValid(stateMachine, baseStateStr, localStr);
1129
+ const remoteValid = isValid(stateMachine, baseStateStr, remoteStr);
1130
+ if (localValid && remoteValid) {
1131
+ const comparison = import_core5.HybridLogicalClock.compare(localOp.timestamp, remoteOp.timestamp);
1132
+ const winner = comparison >= 0 ? localStr : remoteStr;
1133
+ return makeResult(
1134
+ winner,
1135
+ fieldName,
1136
+ localOp,
1137
+ remoteOp,
1138
+ localValue,
1139
+ remoteValue,
1140
+ baseValue,
1141
+ "state-machine-lww",
1142
+ null,
1143
+ startTime
1144
+ );
1145
+ }
1146
+ if (localValid && !remoteValid) {
1147
+ return makeResult(
1148
+ localStr,
1149
+ fieldName,
1150
+ localOp,
1151
+ remoteOp,
1152
+ localValue,
1153
+ remoteValue,
1154
+ baseValue,
1155
+ "state-machine-valid-wins",
1156
+ `Remote transition from "${baseStateStr}" to "${remoteStr}" is invalid; local "${localStr}" wins`,
1157
+ startTime
1158
+ );
1159
+ }
1160
+ if (!localValid && remoteValid) {
1161
+ return makeResult(
1162
+ remoteStr,
1163
+ fieldName,
1164
+ localOp,
1165
+ remoteOp,
1166
+ localValue,
1167
+ remoteValue,
1168
+ baseValue,
1169
+ "state-machine-valid-wins",
1170
+ `Local transition from "${baseStateStr}" to "${localStr}" is invalid; remote "${remoteStr}" wins`,
1171
+ startTime
1172
+ );
1173
+ }
1174
+ return makeResult(
1175
+ baseStateStr,
1176
+ fieldName,
1177
+ localOp,
1178
+ remoteOp,
1179
+ localValue,
1180
+ remoteValue,
1181
+ baseValue,
1182
+ "state-machine-both-invalid",
1183
+ `Both transitions invalid from "${baseStateStr}": local to "${localStr}", remote to "${remoteStr}". Keeping base state.`,
1184
+ startTime
1185
+ );
1186
+ }
1187
+ function isStateMachineField(collectionDef, fieldName) {
1188
+ return collectionDef.stateMachine !== void 0 && collectionDef.stateMachine.field === fieldName;
1189
+ }
1190
+ function makeResult(value, field, operationA, operationB, inputA, inputB, base, strategy, constraintViolated, startTime) {
1191
+ const trace = {
1192
+ operationA,
1193
+ operationB,
1194
+ field,
1195
+ strategy,
1196
+ inputA,
1197
+ inputB,
1198
+ base,
1199
+ output: value,
1200
+ tier: 2,
1201
+ constraintViolated,
1202
+ duration: Date.now() - startTime
1203
+ };
1204
+ return { value, trace };
1205
+ }
1206
+
587
1207
  // src/engine/merge-engine.ts
588
- var import_core3 = require("@korajs/core");
589
1208
  var MergeEngine = class {
590
1209
  /**
591
1210
  * Merge two concurrent operations with all three tiers.
@@ -663,6 +1282,20 @@ var MergeEngine = class {
663
1282
  if (fieldDef === void 0) {
664
1283
  continue;
665
1284
  }
1285
+ if (collectionDef.stateMachine !== void 0 && isStateMachineField(collectionDef, fieldName)) {
1286
+ const smResult = resolveStateMachineMerge(
1287
+ fieldName,
1288
+ local,
1289
+ remote,
1290
+ baseState,
1291
+ collectionDef.stateMachine
1292
+ );
1293
+ mergedData[fieldName] = smResult.value;
1294
+ if (smResult.trace.strategy !== "state-machine-no-conflict-unchanged") {
1295
+ traces.push(smResult.trace);
1296
+ }
1297
+ continue;
1298
+ }
666
1299
  const resolver = collectionDef.resolvers[fieldName];
667
1300
  const result = mergeField(fieldName, local, remote, baseState, fieldDef, resolver);
668
1301
  mergedData[fieldName] = result.value;
@@ -682,7 +1315,7 @@ var MergeEngine = class {
682
1315
  */
683
1316
  mergeWithDelete(input) {
684
1317
  const { local, remote } = input;
685
- const comparison = import_core3.HybridLogicalClock.compare(local.timestamp, remote.timestamp);
1318
+ const comparison = import_core6.HybridLogicalClock.compare(local.timestamp, remote.timestamp);
686
1319
  if (comparison >= 0) {
687
1320
  if (local.type === "delete") {
688
1321
  return { mergedData: {}, traces: [], appliedOperation: "local" };
@@ -752,12 +1385,22 @@ function determineAppliedOperation(traces) {
752
1385
  0 && (module.exports = {
753
1386
  MergeEngine,
754
1387
  addWinsSet,
1388
+ appendOnlyMerge,
1389
+ applySchemaStrategy,
1390
+ buildMergeRelationLookup,
755
1391
  checkConstraints,
1392
+ checkReferentialIntegrityOnDelete,
1393
+ counterMerge,
756
1394
  lastWriteWins,
1395
+ maxMerge,
1396
+ mergeAtomicOps,
757
1397
  mergeField,
758
1398
  mergeRichtext,
1399
+ minMerge,
759
1400
  resolveConstraintViolation,
1401
+ resolveDeleteVsInsertConflict,
760
1402
  richtextToString,
1403
+ serverAuthoritativeMerge,
761
1404
  stringToRichtextUpdate
762
1405
  });
763
1406
  //# sourceMappingURL=index.cjs.map