@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 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,