@korajs/merge 1.0.0-beta.0 → 1.0.0-beta.1
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 +167 -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 +166 -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,
|
|
@@ -505,6 +648,28 @@ function autoMerge(fieldName, localOp, remoteOp, localValue, remoteValue, baseVa
|
|
|
505
648
|
startTime
|
|
506
649
|
);
|
|
507
650
|
}
|
|
651
|
+
case "object":
|
|
652
|
+
case "json": {
|
|
653
|
+
const merged = mergeObject(
|
|
654
|
+
localValue,
|
|
655
|
+
remoteValue,
|
|
656
|
+
baseValue,
|
|
657
|
+
localOp.timestamp,
|
|
658
|
+
remoteOp.timestamp
|
|
659
|
+
);
|
|
660
|
+
return createResult(
|
|
661
|
+
merged,
|
|
662
|
+
fieldName,
|
|
663
|
+
localOp,
|
|
664
|
+
remoteOp,
|
|
665
|
+
localValue,
|
|
666
|
+
remoteValue,
|
|
667
|
+
baseValue,
|
|
668
|
+
"object-lww-map",
|
|
669
|
+
1,
|
|
670
|
+
startTime
|
|
671
|
+
);
|
|
672
|
+
}
|
|
508
673
|
}
|
|
509
674
|
}
|
|
510
675
|
function createResult(value, field, operationA, operationB, inputA, inputB, base, strategy, tier, startTime) {
|
|
@@ -1409,6 +1574,7 @@ function determineAppliedOperation(traces) {
|
|
|
1409
1574
|
maxMerge,
|
|
1410
1575
|
mergeAtomicOps,
|
|
1411
1576
|
mergeField,
|
|
1577
|
+
mergeObject,
|
|
1412
1578
|
mergeRichtext,
|
|
1413
1579
|
minMerge,
|
|
1414
1580
|
resolveConstraintViolation,
|