@korajs/merge 1.0.0-beta.0 → 1.0.0-beta.10
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 +187 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +37 -1
- package/dist/index.d.ts +37 -1
- package/dist/index.js +186 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.cjs
CHANGED
|
@@ -42,6 +42,7 @@ __export(index_exports, {
|
|
|
42
42
|
maxMerge: () => maxMerge,
|
|
43
43
|
mergeAtomicOps: () => mergeAtomicOps,
|
|
44
44
|
mergeField: () => mergeField,
|
|
45
|
+
mergeObject: () => mergeObject,
|
|
45
46
|
mergeRichtext: () => mergeRichtext,
|
|
46
47
|
minMerge: () => minMerge,
|
|
47
48
|
resolveConstraintViolation: () => resolveConstraintViolation,
|
|
@@ -128,6 +129,109 @@ function addWinsSet(localArray, remoteArray, baseArray) {
|
|
|
128
129
|
return result;
|
|
129
130
|
}
|
|
130
131
|
|
|
132
|
+
// src/strategies/object-merge.ts
|
|
133
|
+
function mergeObject(localValue, remoteValue, baseValue, localTs, remoteTs) {
|
|
134
|
+
const local = asPlainObject(localValue);
|
|
135
|
+
const remote = asPlainObject(remoteValue);
|
|
136
|
+
const base = asPlainObject(baseValue);
|
|
137
|
+
if (local === null || remote === null) {
|
|
138
|
+
const winner = lastWriteWins(localValue, remoteValue, localTs, remoteTs).value;
|
|
139
|
+
return asPlainObject(winner) ?? {};
|
|
140
|
+
}
|
|
141
|
+
const baseObj = base ?? {};
|
|
142
|
+
const result = {};
|
|
143
|
+
const keys = /* @__PURE__ */ new Set([
|
|
144
|
+
...Object.keys(local),
|
|
145
|
+
...Object.keys(remote),
|
|
146
|
+
...Object.keys(baseObj)
|
|
147
|
+
]);
|
|
148
|
+
for (const key of keys) {
|
|
149
|
+
const inLocal = key in local;
|
|
150
|
+
const inRemote = key in remote;
|
|
151
|
+
const inBase = key in baseObj;
|
|
152
|
+
const localVal = local[key];
|
|
153
|
+
const remoteVal = remote[key];
|
|
154
|
+
const baseVal = baseObj[key];
|
|
155
|
+
const localWrote = inLocal && (!inBase || !valuesEqual(localVal, baseVal));
|
|
156
|
+
const remoteWrote = inRemote && (!inBase || !valuesEqual(remoteVal, baseVal));
|
|
157
|
+
const localRemoved = !inLocal && inBase;
|
|
158
|
+
const remoteRemoved = !inRemote && inBase;
|
|
159
|
+
if (localWrote && remoteWrote) {
|
|
160
|
+
result[key] = mergeWrittenValues(localVal, remoteVal, baseVal, localTs, remoteTs);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (localWrote && !remoteRemoved) {
|
|
164
|
+
result[key] = localVal;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (remoteWrote && !localRemoved) {
|
|
168
|
+
result[key] = remoteVal;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (localWrote && remoteRemoved) {
|
|
172
|
+
result[key] = localVal;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (remoteWrote && localRemoved) {
|
|
176
|
+
result[key] = remoteVal;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (localRemoved || remoteRemoved) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (inBase) {
|
|
183
|
+
result[key] = baseVal;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
188
|
+
function mergeWrittenValues(localVal, remoteVal, baseVal, localTs, remoteTs) {
|
|
189
|
+
if (asPlainObject(localVal) !== null && asPlainObject(remoteVal) !== null) {
|
|
190
|
+
return mergeObject(localVal, remoteVal, baseVal, localTs, remoteTs);
|
|
191
|
+
}
|
|
192
|
+
if (Array.isArray(localVal) && Array.isArray(remoteVal)) {
|
|
193
|
+
const baseArr = Array.isArray(baseVal) ? baseVal : [];
|
|
194
|
+
return addWinsSet(localVal, remoteVal, baseArr);
|
|
195
|
+
}
|
|
196
|
+
return lastWriteWins(localVal, remoteVal, localTs, remoteTs).value;
|
|
197
|
+
}
|
|
198
|
+
function asPlainObject(value) {
|
|
199
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
const proto = Object.getPrototypeOf(value);
|
|
203
|
+
if (proto !== null && proto !== Object.prototype) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
return value;
|
|
207
|
+
}
|
|
208
|
+
function valuesEqual(a, b) {
|
|
209
|
+
if (a === b) {
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
return stableStringify(a) === stableStringify(b);
|
|
213
|
+
}
|
|
214
|
+
function stableStringify(value) {
|
|
215
|
+
return JSON.stringify(sortKeysDeep(value));
|
|
216
|
+
}
|
|
217
|
+
function sortKeysDeep(value) {
|
|
218
|
+
if (Array.isArray(value)) {
|
|
219
|
+
return value.map(sortKeysDeep);
|
|
220
|
+
}
|
|
221
|
+
if (typeof value === "object" && value !== null) {
|
|
222
|
+
const proto = Object.getPrototypeOf(value);
|
|
223
|
+
if (proto === null || proto === Object.prototype) {
|
|
224
|
+
const record = value;
|
|
225
|
+
const sorted = {};
|
|
226
|
+
for (const key of Object.keys(record).sort()) {
|
|
227
|
+
sorted[key] = sortKeysDeep(record[key]);
|
|
228
|
+
}
|
|
229
|
+
return sorted;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return value;
|
|
233
|
+
}
|
|
234
|
+
|
|
131
235
|
// src/strategies/atomic-merge.ts
|
|
132
236
|
function mergeAtomicOps(localAtomicOp, remoteAtomicOp, baseValue) {
|
|
133
237
|
if (localAtomicOp.type !== remoteAtomicOp.type) {
|
|
@@ -317,6 +421,38 @@ function applySchemaStrategy(strategy, localValue, remoteValue, baseValue, local
|
|
|
317
421
|
|
|
318
422
|
// src/engine/field-merger.ts
|
|
319
423
|
function mergeField(fieldName, localOp, remoteOp, baseState, fieldDescriptor, resolver) {
|
|
424
|
+
const result = mergeFieldInner(fieldName, localOp, remoteOp, baseState, fieldDescriptor, resolver);
|
|
425
|
+
if (fieldDescriptor.kind === "secret") {
|
|
426
|
+
return {
|
|
427
|
+
value: result.value,
|
|
428
|
+
trace: {
|
|
429
|
+
...result.trace,
|
|
430
|
+
operationA: redactOperationField(result.trace.operationA, fieldName),
|
|
431
|
+
operationB: redactOperationField(result.trace.operationB, fieldName),
|
|
432
|
+
inputA: SECRET_REDACTED,
|
|
433
|
+
inputB: SECRET_REDACTED,
|
|
434
|
+
base: result.trace.base == null ? result.trace.base : SECRET_REDACTED,
|
|
435
|
+
output: SECRET_REDACTED
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
return result;
|
|
440
|
+
}
|
|
441
|
+
var SECRET_REDACTED = "[secret]";
|
|
442
|
+
function redactOperationField(operation, field) {
|
|
443
|
+
const redactRecord = (record) => {
|
|
444
|
+
if (record === null || !(field in record)) {
|
|
445
|
+
return record;
|
|
446
|
+
}
|
|
447
|
+
return { ...record, [field]: SECRET_REDACTED };
|
|
448
|
+
};
|
|
449
|
+
return {
|
|
450
|
+
...operation,
|
|
451
|
+
data: redactRecord(operation.data),
|
|
452
|
+
previousData: redactRecord(operation.previousData)
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
function mergeFieldInner(fieldName, localOp, remoteOp, baseState, fieldDescriptor, resolver) {
|
|
320
456
|
const startTime = Date.now();
|
|
321
457
|
const localData = localOp.data ?? {};
|
|
322
458
|
const remoteData = remoteOp.data ?? {};
|
|
@@ -448,7 +584,14 @@ function autoMerge(fieldName, localOp, remoteOp, localValue, remoteValue, baseVa
|
|
|
448
584
|
case "number":
|
|
449
585
|
case "boolean":
|
|
450
586
|
case "enum":
|
|
451
|
-
case "timestamp":
|
|
587
|
+
case "timestamp":
|
|
588
|
+
// A blob field's value is a small content-addressed reference; concurrent
|
|
589
|
+
// replacements of the whole reference resolve by last-write-wins (the bytes
|
|
590
|
+
// themselves are immutable and deduplicated by hash in the blob store).
|
|
591
|
+
case "blob":
|
|
592
|
+
// A secret field's stored value (ciphertext or hash) resolves by
|
|
593
|
+
// last-write-wins; the merge never sees or emits plaintext (redacted below).
|
|
594
|
+
case "secret": {
|
|
452
595
|
const lwwResult = lastWriteWins(
|
|
453
596
|
localValue,
|
|
454
597
|
remoteValue,
|
|
@@ -487,6 +630,26 @@ function autoMerge(fieldName, localOp, remoteOp, localValue, remoteValue, baseVa
|
|
|
487
630
|
);
|
|
488
631
|
}
|
|
489
632
|
case "richtext": {
|
|
633
|
+
if (typeof localValue === "string" || typeof remoteValue === "string") {
|
|
634
|
+
const lwwResult = lastWriteWins(
|
|
635
|
+
localValue,
|
|
636
|
+
remoteValue,
|
|
637
|
+
localOp.timestamp,
|
|
638
|
+
remoteOp.timestamp
|
|
639
|
+
);
|
|
640
|
+
return createResult(
|
|
641
|
+
lwwResult.value,
|
|
642
|
+
fieldName,
|
|
643
|
+
localOp,
|
|
644
|
+
remoteOp,
|
|
645
|
+
localValue,
|
|
646
|
+
remoteValue,
|
|
647
|
+
baseValue,
|
|
648
|
+
"richtext-lww",
|
|
649
|
+
1,
|
|
650
|
+
startTime
|
|
651
|
+
);
|
|
652
|
+
}
|
|
490
653
|
const merged = mergeRichtext(
|
|
491
654
|
localValue,
|
|
492
655
|
remoteValue,
|
|
@@ -505,6 +668,28 @@ function autoMerge(fieldName, localOp, remoteOp, localValue, remoteValue, baseVa
|
|
|
505
668
|
startTime
|
|
506
669
|
);
|
|
507
670
|
}
|
|
671
|
+
case "object":
|
|
672
|
+
case "json": {
|
|
673
|
+
const merged = mergeObject(
|
|
674
|
+
localValue,
|
|
675
|
+
remoteValue,
|
|
676
|
+
baseValue,
|
|
677
|
+
localOp.timestamp,
|
|
678
|
+
remoteOp.timestamp
|
|
679
|
+
);
|
|
680
|
+
return createResult(
|
|
681
|
+
merged,
|
|
682
|
+
fieldName,
|
|
683
|
+
localOp,
|
|
684
|
+
remoteOp,
|
|
685
|
+
localValue,
|
|
686
|
+
remoteValue,
|
|
687
|
+
baseValue,
|
|
688
|
+
"object-lww-map",
|
|
689
|
+
1,
|
|
690
|
+
startTime
|
|
691
|
+
);
|
|
692
|
+
}
|
|
508
693
|
}
|
|
509
694
|
}
|
|
510
695
|
function createResult(value, field, operationA, operationB, inputA, inputB, base, strategy, tier, startTime) {
|
|
@@ -1409,6 +1594,7 @@ function determineAppliedOperation(traces) {
|
|
|
1409
1594
|
maxMerge,
|
|
1410
1595
|
mergeAtomicOps,
|
|
1411
1596
|
mergeField,
|
|
1597
|
+
mergeObject,
|
|
1412
1598
|
mergeRichtext,
|
|
1413
1599
|
minMerge,
|
|
1414
1600
|
resolveConstraintViolation,
|