@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.cjs +666 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +218 -2
- package/dist/index.d.ts +218 -2
- package/dist/index.js +656 -14
- package/dist/index.js.map +1 -1
- package/package.json +4 -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.
|
|
@@ -607,7 +1226,8 @@ var MergeEngine = class {
|
|
|
607
1226
|
return {
|
|
608
1227
|
mergedData: {},
|
|
609
1228
|
traces: [],
|
|
610
|
-
appliedOperation: "merged"
|
|
1229
|
+
appliedOperation: "merged",
|
|
1230
|
+
sideEffects: []
|
|
611
1231
|
};
|
|
612
1232
|
}
|
|
613
1233
|
if (input.local.type === "delete" || input.remote.type === "delete") {
|
|
@@ -625,6 +1245,7 @@ var MergeEngine = class {
|
|
|
625
1245
|
);
|
|
626
1246
|
let mergedData = fieldResult.mergedData;
|
|
627
1247
|
const allTraces = [...fieldResult.traces];
|
|
1248
|
+
const allSideEffects = [...fieldResult.sideEffects];
|
|
628
1249
|
for (const violation of violations) {
|
|
629
1250
|
const resolution = resolveConstraintViolation(
|
|
630
1251
|
violation,
|
|
@@ -635,11 +1256,15 @@ var MergeEngine = class {
|
|
|
635
1256
|
);
|
|
636
1257
|
mergedData = resolution.resolvedRecord;
|
|
637
1258
|
allTraces.push(resolution.trace);
|
|
1259
|
+
if (resolution.sideEffects) {
|
|
1260
|
+
allSideEffects.push(...resolution.sideEffects);
|
|
1261
|
+
}
|
|
638
1262
|
}
|
|
639
1263
|
return {
|
|
640
1264
|
mergedData,
|
|
641
1265
|
traces: allTraces,
|
|
642
|
-
appliedOperation: determineAppliedOperation(allTraces)
|
|
1266
|
+
appliedOperation: determineAppliedOperation(allTraces),
|
|
1267
|
+
sideEffects: allSideEffects
|
|
643
1268
|
};
|
|
644
1269
|
}
|
|
645
1270
|
return fieldResult;
|
|
@@ -663,6 +1288,20 @@ var MergeEngine = class {
|
|
|
663
1288
|
if (fieldDef === void 0) {
|
|
664
1289
|
continue;
|
|
665
1290
|
}
|
|
1291
|
+
if (collectionDef.stateMachine !== void 0 && isStateMachineField(collectionDef, fieldName)) {
|
|
1292
|
+
const smResult = resolveStateMachineMerge(
|
|
1293
|
+
fieldName,
|
|
1294
|
+
local,
|
|
1295
|
+
remote,
|
|
1296
|
+
baseState,
|
|
1297
|
+
collectionDef.stateMachine
|
|
1298
|
+
);
|
|
1299
|
+
mergedData[fieldName] = smResult.value;
|
|
1300
|
+
if (smResult.trace.strategy !== "state-machine-no-conflict-unchanged") {
|
|
1301
|
+
traces.push(smResult.trace);
|
|
1302
|
+
}
|
|
1303
|
+
continue;
|
|
1304
|
+
}
|
|
666
1305
|
const resolver = collectionDef.resolvers[fieldName];
|
|
667
1306
|
const result = mergeField(fieldName, local, remote, baseState, fieldDef, resolver);
|
|
668
1307
|
mergedData[fieldName] = result.value;
|
|
@@ -673,7 +1312,8 @@ var MergeEngine = class {
|
|
|
673
1312
|
return {
|
|
674
1313
|
mergedData,
|
|
675
1314
|
traces,
|
|
676
|
-
appliedOperation: determineAppliedOperation(traces)
|
|
1315
|
+
appliedOperation: determineAppliedOperation(traces),
|
|
1316
|
+
sideEffects: []
|
|
677
1317
|
};
|
|
678
1318
|
}
|
|
679
1319
|
/**
|
|
@@ -682,24 +1322,26 @@ var MergeEngine = class {
|
|
|
682
1322
|
*/
|
|
683
1323
|
mergeWithDelete(input) {
|
|
684
1324
|
const { local, remote } = input;
|
|
685
|
-
const comparison =
|
|
1325
|
+
const comparison = import_core6.HybridLogicalClock.compare(local.timestamp, remote.timestamp);
|
|
686
1326
|
if (comparison >= 0) {
|
|
687
1327
|
if (local.type === "delete") {
|
|
688
|
-
return { mergedData: {}, traces: [], appliedOperation: "local" };
|
|
1328
|
+
return { mergedData: {}, traces: [], appliedOperation: "local", sideEffects: [] };
|
|
689
1329
|
}
|
|
690
1330
|
return {
|
|
691
1331
|
mergedData: { ...input.baseState, ...local.data ?? {} },
|
|
692
1332
|
traces: [],
|
|
693
|
-
appliedOperation: "local"
|
|
1333
|
+
appliedOperation: "local",
|
|
1334
|
+
sideEffects: []
|
|
694
1335
|
};
|
|
695
1336
|
}
|
|
696
1337
|
if (remote.type === "delete") {
|
|
697
|
-
return { mergedData: {}, traces: [], appliedOperation: "remote" };
|
|
1338
|
+
return { mergedData: {}, traces: [], appliedOperation: "remote", sideEffects: [] };
|
|
698
1339
|
}
|
|
699
1340
|
return {
|
|
700
1341
|
mergedData: { ...input.baseState, ...remote.data ?? {} },
|
|
701
1342
|
traces: [],
|
|
702
|
-
appliedOperation: "remote"
|
|
1343
|
+
appliedOperation: "remote",
|
|
1344
|
+
sideEffects: []
|
|
703
1345
|
};
|
|
704
1346
|
}
|
|
705
1347
|
};
|
|
@@ -752,12 +1394,22 @@ function determineAppliedOperation(traces) {
|
|
|
752
1394
|
0 && (module.exports = {
|
|
753
1395
|
MergeEngine,
|
|
754
1396
|
addWinsSet,
|
|
1397
|
+
appendOnlyMerge,
|
|
1398
|
+
applySchemaStrategy,
|
|
1399
|
+
buildMergeRelationLookup,
|
|
755
1400
|
checkConstraints,
|
|
1401
|
+
checkReferentialIntegrityOnDelete,
|
|
1402
|
+
counterMerge,
|
|
756
1403
|
lastWriteWins,
|
|
1404
|
+
maxMerge,
|
|
1405
|
+
mergeAtomicOps,
|
|
757
1406
|
mergeField,
|
|
758
1407
|
mergeRichtext,
|
|
1408
|
+
minMerge,
|
|
759
1409
|
resolveConstraintViolation,
|
|
1410
|
+
resolveDeleteVsInsertConflict,
|
|
760
1411
|
richtextToString,
|
|
1412
|
+
serverAuthoritativeMerge,
|
|
761
1413
|
stringToRichtextUpdate
|
|
762
1414
|
});
|
|
763
1415
|
//# sourceMappingURL=index.cjs.map
|