@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 +650 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +206 -2
- package/dist/index.d.ts +206 -2
- package/dist/index.js +640 -7
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|