@korajs/merge 0.6.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dr. Obed Ehoneah
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
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,
@@ -109,21 +110,128 @@ function addWinsSet(localArray, remoteArray, baseArray) {
109
110
  for (const item of baseArray) {
110
111
  addIfNew(serialize(item), item);
111
112
  }
113
+ const additions = /* @__PURE__ */ new Map();
112
114
  for (const item of localArray) {
113
115
  const s = serialize(item);
114
- if (addedLocal.has(s)) {
115
- addIfNew(s, item);
116
+ if (addedLocal.has(s) && !additions.has(s)) {
117
+ additions.set(s, item);
116
118
  }
117
119
  }
118
120
  for (const item of remoteArray) {
119
121
  const s = serialize(item);
120
- if (addedRemote.has(s)) {
121
- addIfNew(s, item);
122
+ if (addedRemote.has(s) && !additions.has(s)) {
123
+ additions.set(s, item);
122
124
  }
123
125
  }
126
+ for (const s of [...additions.keys()].sort()) {
127
+ addIfNew(s, additions.get(s));
128
+ }
124
129
  return result;
125
130
  }
126
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
+
127
235
  // src/strategies/atomic-merge.ts
128
236
  function mergeAtomicOps(localAtomicOp, remoteAtomicOp, baseValue) {
129
237
  if (localAtomicOp.type !== remoteAtomicOp.type) {
@@ -187,6 +295,7 @@ function mergeAtomicOps(localAtomicOp, remoteAtomicOp, baseValue) {
187
295
  }
188
296
 
189
297
  // src/strategies/yjs-richtext.ts
298
+ var import_core2 = require("@korajs/core");
190
299
  var Y = __toESM(require("yjs"), 1);
191
300
  var TEXT_KEY = "content";
192
301
  function mergeRichtext(localValue, remoteValue, baseValue) {
@@ -219,11 +328,17 @@ function toYjsUpdate(value) {
219
328
  if (value instanceof ArrayBuffer) {
220
329
  return new Uint8Array(value);
221
330
  }
222
- throw new Error("Richtext value must be a string, Uint8Array, ArrayBuffer, null, or undefined.");
331
+ if ((0, import_core2.isKoraBytesValue)(value) || (0, import_core2.isLegacyNumericByteObject)(value)) {
332
+ const decoded = (0, import_core2.decodeBytesFromOpData)(value);
333
+ return decoded instanceof Uint8Array ? decoded : stringToRichtextUpdate(decoded);
334
+ }
335
+ throw new Error(
336
+ "Richtext value must be a string, Uint8Array, ArrayBuffer, tagged { $koraBytes } object, null, or undefined."
337
+ );
223
338
  }
224
339
 
225
340
  // src/strategies/schema-strategies.ts
226
- var import_core2 = require("@korajs/core");
341
+ var import_core3 = require("@korajs/core");
227
342
  function counterMerge(localValue, remoteValue, baseValue) {
228
343
  const base = typeof baseValue === "number" ? baseValue : 0;
229
344
  const local = typeof localValue === "number" ? localValue : base;
@@ -306,6 +421,38 @@ function applySchemaStrategy(strategy, localValue, remoteValue, baseValue, local
306
421
 
307
422
  // src/engine/field-merger.ts
308
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) {
309
456
  const startTime = Date.now();
310
457
  const localData = localOp.data ?? {};
311
458
  const remoteData = remoteOp.data ?? {};
@@ -437,7 +584,14 @@ function autoMerge(fieldName, localOp, remoteOp, localValue, remoteValue, baseVa
437
584
  case "number":
438
585
  case "boolean":
439
586
  case "enum":
440
- 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": {
441
595
  const lwwResult = lastWriteWins(
442
596
  localValue,
443
597
  remoteValue,
@@ -494,6 +648,28 @@ function autoMerge(fieldName, localOp, remoteOp, localValue, remoteValue, baseVa
494
648
  startTime
495
649
  );
496
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
+ }
497
673
  }
498
674
  }
499
675
  function createResult(value, field, operationA, operationB, inputA, inputB, base, strategy, tier, startTime) {
@@ -614,13 +790,13 @@ function matchesWhere(record, where) {
614
790
  }
615
791
 
616
792
  // src/constraints/resolvers.ts
617
- var import_core3 = require("@korajs/core");
793
+ var import_core4 = require("@korajs/core");
618
794
  function resolveConstraintViolation(violation, mergedRecord, localOp, remoteOp, baseState) {
619
795
  const startTime = Date.now();
620
796
  const { constraint } = violation;
621
797
  switch (constraint.onConflict) {
622
798
  case "last-write-wins": {
623
- const comparison = import_core3.HybridLogicalClock.compare(localOp.timestamp, remoteOp.timestamp);
799
+ const comparison = import_core4.HybridLogicalClock.compare(localOp.timestamp, remoteOp.timestamp);
624
800
  const winner = comparison >= 0 ? localOp : remoteOp;
625
801
  const resolvedRecord = applyWinnerFields(mergedRecord, winner, violation.fields);
626
802
  return createResolution(
@@ -634,7 +810,7 @@ function resolveConstraintViolation(violation, mergedRecord, localOp, remoteOp,
634
810
  );
635
811
  }
636
812
  case "first-write-wins": {
637
- const comparison = import_core3.HybridLogicalClock.compare(localOp.timestamp, remoteOp.timestamp);
813
+ const comparison = import_core4.HybridLogicalClock.compare(localOp.timestamp, remoteOp.timestamp);
638
814
  const winner = comparison <= 0 ? localOp : remoteOp;
639
815
  const resolvedRecord = applyWinnerFields(mergedRecord, winner, violation.fields);
640
816
  return createResolution(
@@ -650,7 +826,7 @@ function resolveConstraintViolation(violation, mergedRecord, localOp, remoteOp,
650
826
  case "priority-field": {
651
827
  const priorityField = constraint.priorityField;
652
828
  if (priorityField === void 0) {
653
- const comparison = import_core3.HybridLogicalClock.compare(localOp.timestamp, remoteOp.timestamp);
829
+ const comparison = import_core4.HybridLogicalClock.compare(localOp.timestamp, remoteOp.timestamp);
654
830
  const winner2 = comparison >= 0 ? localOp : remoteOp;
655
831
  const resolvedRecord2 = applyWinnerFields(mergedRecord, winner2, violation.fields);
656
832
  return createResolution(
@@ -694,7 +870,7 @@ function resolveConstraintViolation(violation, mergedRecord, localOp, remoteOp,
694
870
  }
695
871
  case "custom": {
696
872
  if (constraint.resolve === void 0) {
697
- const comparison = import_core3.HybridLogicalClock.compare(localOp.timestamp, remoteOp.timestamp);
873
+ const comparison = import_core4.HybridLogicalClock.compare(localOp.timestamp, remoteOp.timestamp);
698
874
  const winner = comparison >= 0 ? localOp : remoteOp;
699
875
  const resolvedRecord2 = applyWinnerFields(mergedRecord, winner, violation.fields);
700
876
  return createResolution(
@@ -786,7 +962,7 @@ function extractFields(record, fields) {
786
962
  }
787
963
 
788
964
  // src/constraints/referential-integrity.ts
789
- var import_core4 = require("@korajs/core");
965
+ var import_core5 = require("@korajs/core");
790
966
  function buildMergeRelationLookup(schema) {
791
967
  const lookup = /* @__PURE__ */ new Map();
792
968
  const relationNames = Object.keys(schema.relations).sort();
@@ -1057,17 +1233,17 @@ function createDeleteVsInsertTrace(deleteOp, insertOp, relation, strategy, actio
1057
1233
  }
1058
1234
 
1059
1235
  // src/engine/merge-engine.ts
1060
- var import_core6 = require("@korajs/core");
1236
+ var import_core7 = require("@korajs/core");
1061
1237
 
1062
1238
  // src/constraints/state-machine-constraint.ts
1063
- var import_core5 = require("@korajs/core");
1239
+ var import_core6 = require("@korajs/core");
1064
1240
  function isValid(sm, from, to) {
1065
1241
  const constraint = {
1066
1242
  field: sm.field,
1067
1243
  collection: "",
1068
1244
  transitions: sm.transitions
1069
1245
  };
1070
- return (0, import_core5.validateTransition)(constraint, from, to).valid;
1246
+ return (0, import_core6.validateTransition)(constraint, from, to).valid;
1071
1247
  }
1072
1248
  function resolveStateMachineMerge(fieldName, localOp, remoteOp, baseState, stateMachine) {
1073
1249
  const startTime = Date.now();
@@ -1128,7 +1304,7 @@ function resolveStateMachineMerge(fieldName, localOp, remoteOp, baseState, state
1128
1304
  const localValid = isValid(stateMachine, baseStateStr, localStr);
1129
1305
  const remoteValid = isValid(stateMachine, baseStateStr, remoteStr);
1130
1306
  if (localValid && remoteValid) {
1131
- const comparison = import_core5.HybridLogicalClock.compare(localOp.timestamp, remoteOp.timestamp);
1307
+ const comparison = import_core6.HybridLogicalClock.compare(localOp.timestamp, remoteOp.timestamp);
1132
1308
  const winner = comparison >= 0 ? localStr : remoteStr;
1133
1309
  return makeResult(
1134
1310
  winner,
@@ -1280,7 +1456,7 @@ var MergeEngine = class {
1280
1456
  */
1281
1457
  mergeFields(input) {
1282
1458
  const { local, remote, baseState, collectionDef } = input;
1283
- const allFields = collectAffectedFields(local, remote, baseState, collectionDef);
1459
+ const allFields = collectAffectedFields(local, remote);
1284
1460
  const mergedData = {};
1285
1461
  const traces = [];
1286
1462
  for (const fieldName of allFields) {
@@ -1322,7 +1498,7 @@ var MergeEngine = class {
1322
1498
  */
1323
1499
  mergeWithDelete(input) {
1324
1500
  const { local, remote } = input;
1325
- const comparison = import_core6.HybridLogicalClock.compare(local.timestamp, remote.timestamp);
1501
+ const comparison = import_core7.HybridLogicalClock.compare(local.timestamp, remote.timestamp);
1326
1502
  if (comparison >= 0) {
1327
1503
  if (local.type === "delete") {
1328
1504
  return { mergedData: {}, traces: [], appliedOperation: "local", sideEffects: [] };
@@ -1345,11 +1521,8 @@ var MergeEngine = class {
1345
1521
  };
1346
1522
  }
1347
1523
  };
1348
- function collectAffectedFields(local, remote, baseState, collectionDef) {
1524
+ function collectAffectedFields(local, remote) {
1349
1525
  const fields = /* @__PURE__ */ new Set();
1350
- for (const fieldName of Object.keys(collectionDef.fields)) {
1351
- fields.add(fieldName);
1352
- }
1353
1526
  if (local.data !== null) {
1354
1527
  for (const fieldName of Object.keys(local.data)) {
1355
1528
  fields.add(fieldName);
@@ -1360,9 +1533,6 @@ function collectAffectedFields(local, remote, baseState, collectionDef) {
1360
1533
  fields.add(fieldName);
1361
1534
  }
1362
1535
  }
1363
- for (const fieldName of Object.keys(baseState)) {
1364
- fields.add(fieldName);
1365
- }
1366
1536
  return fields;
1367
1537
  }
1368
1538
  function determineAppliedOperation(traces) {
@@ -1404,6 +1574,7 @@ function determineAppliedOperation(traces) {
1404
1574
  maxMerge,
1405
1575
  mergeAtomicOps,
1406
1576
  mergeField,
1577
+ mergeObject,
1407
1578
  mergeRichtext,
1408
1579
  minMerge,
1409
1580
  resolveConstraintViolation,