@korajs/merge 0.1.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 ADDED
@@ -0,0 +1,763 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ MergeEngine: () => MergeEngine,
34
+ addWinsSet: () => addWinsSet,
35
+ checkConstraints: () => checkConstraints,
36
+ lastWriteWins: () => lastWriteWins,
37
+ mergeField: () => mergeField,
38
+ mergeRichtext: () => mergeRichtext,
39
+ resolveConstraintViolation: () => resolveConstraintViolation,
40
+ richtextToString: () => richtextToString,
41
+ stringToRichtextUpdate: () => stringToRichtextUpdate
42
+ });
43
+ module.exports = __toCommonJS(index_exports);
44
+
45
+ // src/strategies/lww.ts
46
+ var import_core = require("@korajs/core");
47
+ function lastWriteWins(localValue, remoteValue, localTimestamp, remoteTimestamp) {
48
+ const comparison = import_core.HybridLogicalClock.compare(localTimestamp, remoteTimestamp);
49
+ if (comparison >= 0) {
50
+ return { value: localValue, winner: "local" };
51
+ }
52
+ return { value: remoteValue, winner: "remote" };
53
+ }
54
+
55
+ // src/strategies/add-wins-set.ts
56
+ function addWinsSet(localArray, remoteArray, baseArray) {
57
+ const serialize = (v) => JSON.stringify(v);
58
+ const baseSet = new Set(baseArray.map(serialize));
59
+ const localSet = new Set(localArray.map(serialize));
60
+ const remoteSet = new Set(remoteArray.map(serialize));
61
+ const addedLocal = /* @__PURE__ */ new Set();
62
+ for (const s of localSet) {
63
+ if (!baseSet.has(s)) {
64
+ addedLocal.add(s);
65
+ }
66
+ }
67
+ const addedRemote = /* @__PURE__ */ new Set();
68
+ for (const s of remoteSet) {
69
+ if (!baseSet.has(s)) {
70
+ addedRemote.add(s);
71
+ }
72
+ }
73
+ const removedLocal = /* @__PURE__ */ new Set();
74
+ for (const s of baseSet) {
75
+ if (!localSet.has(s)) {
76
+ removedLocal.add(s);
77
+ }
78
+ }
79
+ const removedRemote = /* @__PURE__ */ new Set();
80
+ for (const s of baseSet) {
81
+ if (!remoteSet.has(s)) {
82
+ removedRemote.add(s);
83
+ }
84
+ }
85
+ const removedByBoth = /* @__PURE__ */ new Set();
86
+ for (const s of removedLocal) {
87
+ if (removedRemote.has(s)) {
88
+ removedByBoth.add(s);
89
+ }
90
+ }
91
+ const resultSerialized = /* @__PURE__ */ new Set();
92
+ const result = [];
93
+ const addIfNew = (serialized, value) => {
94
+ if (!resultSerialized.has(serialized) && !removedByBoth.has(serialized)) {
95
+ resultSerialized.add(serialized);
96
+ result.push(value);
97
+ }
98
+ };
99
+ for (const item of baseArray) {
100
+ addIfNew(serialize(item), item);
101
+ }
102
+ for (const item of localArray) {
103
+ const s = serialize(item);
104
+ if (addedLocal.has(s)) {
105
+ addIfNew(s, item);
106
+ }
107
+ }
108
+ for (const item of remoteArray) {
109
+ const s = serialize(item);
110
+ if (addedRemote.has(s)) {
111
+ addIfNew(s, item);
112
+ }
113
+ }
114
+ return result;
115
+ }
116
+
117
+ // src/strategies/yjs-richtext.ts
118
+ var Y = __toESM(require("yjs"), 1);
119
+ var TEXT_KEY = "content";
120
+ function mergeRichtext(localValue, remoteValue, baseValue) {
121
+ const mergedDoc = new Y.Doc();
122
+ Y.applyUpdate(mergedDoc, toYjsUpdate(baseValue));
123
+ Y.applyUpdate(mergedDoc, toYjsUpdate(localValue));
124
+ Y.applyUpdate(mergedDoc, toYjsUpdate(remoteValue));
125
+ return Y.encodeStateAsUpdate(mergedDoc);
126
+ }
127
+ function richtextToString(value) {
128
+ const doc = new Y.Doc();
129
+ Y.applyUpdate(doc, toYjsUpdate(value));
130
+ return doc.getText(TEXT_KEY).toString();
131
+ }
132
+ function stringToRichtextUpdate(value) {
133
+ const doc = new Y.Doc();
134
+ doc.getText(TEXT_KEY).insert(0, value);
135
+ return Y.encodeStateAsUpdate(doc);
136
+ }
137
+ function toYjsUpdate(value) {
138
+ if (value === null || value === void 0) {
139
+ return Y.encodeStateAsUpdate(new Y.Doc());
140
+ }
141
+ if (typeof value === "string") {
142
+ return stringToRichtextUpdate(value);
143
+ }
144
+ if (value instanceof Uint8Array) {
145
+ return value;
146
+ }
147
+ if (value instanceof ArrayBuffer) {
148
+ return new Uint8Array(value);
149
+ }
150
+ throw new Error("Richtext value must be a string, Uint8Array, ArrayBuffer, null, or undefined.");
151
+ }
152
+
153
+ // src/engine/field-merger.ts
154
+ function mergeField(fieldName, localOp, remoteOp, baseState, fieldDescriptor, resolver) {
155
+ const startTime = Date.now();
156
+ const localData = localOp.data ?? {};
157
+ const remoteData = remoteOp.data ?? {};
158
+ const localPrevious = localOp.previousData ?? {};
159
+ const remotePrevious = remoteOp.previousData ?? {};
160
+ const localChanged = fieldName in localData;
161
+ const remoteChanged = fieldName in remoteData;
162
+ const baseValue = baseState[fieldName];
163
+ if (localChanged && !remoteChanged) {
164
+ return createResult(
165
+ localData[fieldName],
166
+ fieldName,
167
+ localOp,
168
+ remoteOp,
169
+ localData[fieldName],
170
+ baseValue,
171
+ baseValue,
172
+ "no-conflict-local",
173
+ 1,
174
+ startTime
175
+ );
176
+ }
177
+ if (!localChanged && remoteChanged) {
178
+ return createResult(
179
+ remoteData[fieldName],
180
+ fieldName,
181
+ localOp,
182
+ remoteOp,
183
+ baseValue,
184
+ remoteData[fieldName],
185
+ baseValue,
186
+ "no-conflict-remote",
187
+ 1,
188
+ startTime
189
+ );
190
+ }
191
+ if (!localChanged && !remoteChanged) {
192
+ return createResult(
193
+ baseValue,
194
+ fieldName,
195
+ localOp,
196
+ remoteOp,
197
+ baseValue,
198
+ baseValue,
199
+ baseValue,
200
+ "no-conflict-unchanged",
201
+ 1,
202
+ startTime
203
+ );
204
+ }
205
+ const localValue = localData[fieldName];
206
+ const remoteValue = remoteData[fieldName];
207
+ if (resolver !== void 0) {
208
+ const resolved = resolver(localValue, remoteValue, baseValue);
209
+ return createResult(
210
+ resolved,
211
+ fieldName,
212
+ localOp,
213
+ remoteOp,
214
+ localValue,
215
+ remoteValue,
216
+ baseValue,
217
+ "custom",
218
+ 3,
219
+ startTime
220
+ );
221
+ }
222
+ return autoMerge(
223
+ fieldName,
224
+ localOp,
225
+ remoteOp,
226
+ localValue,
227
+ remoteValue,
228
+ baseValue,
229
+ fieldDescriptor,
230
+ startTime
231
+ );
232
+ }
233
+ function autoMerge(fieldName, localOp, remoteOp, localValue, remoteValue, baseValue, fieldDescriptor, startTime) {
234
+ switch (fieldDescriptor.kind) {
235
+ case "string":
236
+ case "number":
237
+ case "boolean":
238
+ case "enum":
239
+ case "timestamp": {
240
+ const lwwResult = lastWriteWins(
241
+ localValue,
242
+ remoteValue,
243
+ localOp.timestamp,
244
+ remoteOp.timestamp
245
+ );
246
+ return createResult(
247
+ lwwResult.value,
248
+ fieldName,
249
+ localOp,
250
+ remoteOp,
251
+ localValue,
252
+ remoteValue,
253
+ baseValue,
254
+ "lww",
255
+ 1,
256
+ startTime
257
+ );
258
+ }
259
+ case "array": {
260
+ const baseArr = Array.isArray(baseValue) ? baseValue : [];
261
+ const localArr = Array.isArray(localValue) ? localValue : [];
262
+ const remoteArr = Array.isArray(remoteValue) ? remoteValue : [];
263
+ const merged = addWinsSet(localArr, remoteArr, baseArr);
264
+ return createResult(
265
+ merged,
266
+ fieldName,
267
+ localOp,
268
+ remoteOp,
269
+ localValue,
270
+ remoteValue,
271
+ baseValue,
272
+ "add-wins-set",
273
+ 1,
274
+ startTime
275
+ );
276
+ }
277
+ case "richtext": {
278
+ const merged = mergeRichtext(
279
+ localValue,
280
+ remoteValue,
281
+ baseValue
282
+ );
283
+ return createResult(
284
+ merged,
285
+ fieldName,
286
+ localOp,
287
+ remoteOp,
288
+ localValue,
289
+ remoteValue,
290
+ baseValue,
291
+ "crdt-text",
292
+ 1,
293
+ startTime
294
+ );
295
+ }
296
+ }
297
+ }
298
+ function createResult(value, field, operationA, operationB, inputA, inputB, base, strategy, tier, startTime) {
299
+ const trace = {
300
+ operationA,
301
+ operationB,
302
+ field,
303
+ strategy,
304
+ inputA,
305
+ inputB,
306
+ base,
307
+ output: value,
308
+ tier,
309
+ constraintViolated: null,
310
+ duration: Date.now() - startTime
311
+ };
312
+ return { value, trace };
313
+ }
314
+
315
+ // src/constraints/constraint-checker.ts
316
+ async function checkConstraints(mergedRecord, recordId, collection, collectionDef, constraintContext) {
317
+ const violations = [];
318
+ for (const constraint of collectionDef.constraints) {
319
+ if (constraint.type !== "referential" && constraint.where !== void 0 && !matchesWhere(mergedRecord, constraint.where)) {
320
+ continue;
321
+ }
322
+ const violation = await checkSingleConstraint(
323
+ constraint,
324
+ mergedRecord,
325
+ recordId,
326
+ collection,
327
+ constraintContext
328
+ );
329
+ if (violation !== null) {
330
+ violations.push(violation);
331
+ }
332
+ }
333
+ return violations;
334
+ }
335
+ async function checkSingleConstraint(constraint, mergedRecord, recordId, collection, ctx) {
336
+ switch (constraint.type) {
337
+ case "unique":
338
+ return checkUniqueConstraint(constraint, mergedRecord, recordId, collection, ctx);
339
+ case "capacity":
340
+ return checkCapacityConstraint(constraint, mergedRecord, collection, ctx);
341
+ case "referential":
342
+ return checkReferentialConstraint(constraint, mergedRecord, collection, ctx);
343
+ }
344
+ }
345
+ async function checkUniqueConstraint(constraint, mergedRecord, recordId, collection, ctx) {
346
+ const where = {};
347
+ for (const field of constraint.fields) {
348
+ where[field] = mergedRecord[field];
349
+ }
350
+ const existing = await ctx.queryRecords(collection, where);
351
+ const duplicates = existing.filter((r) => r.id !== recordId);
352
+ if (duplicates.length > 0) {
353
+ return {
354
+ constraint,
355
+ fields: constraint.fields,
356
+ message: `Unique constraint violated on fields [${constraint.fields.join(", ")}] in collection "${collection}": duplicate value(s) found`
357
+ };
358
+ }
359
+ return null;
360
+ }
361
+ async function checkCapacityConstraint(constraint, mergedRecord, collection, ctx) {
362
+ const where = constraint.where ?? {};
363
+ const count = await ctx.countRecords(collection, where);
364
+ if (count > 0 && constraint.fields.length > 0) {
365
+ const groupWhere = { ...where };
366
+ for (const field of constraint.fields) {
367
+ groupWhere[field] = mergedRecord[field];
368
+ }
369
+ const groupCount = await ctx.countRecords(collection, groupWhere);
370
+ if (groupCount > 1) {
371
+ return {
372
+ constraint,
373
+ fields: constraint.fields,
374
+ message: `Capacity constraint violated on fields [${constraint.fields.join(", ")}] in collection "${collection}": group count ${groupCount} exceeds limit`
375
+ };
376
+ }
377
+ }
378
+ return null;
379
+ }
380
+ async function checkReferentialConstraint(constraint, mergedRecord, collection, ctx) {
381
+ if (constraint.fields.length === 0) {
382
+ return null;
383
+ }
384
+ const fkField = constraint.fields[0];
385
+ if (fkField === void 0) {
386
+ return null;
387
+ }
388
+ const fkValue = mergedRecord[fkField];
389
+ if (fkValue === null || fkValue === void 0) {
390
+ return null;
391
+ }
392
+ const referencedCollection = constraint.where !== void 0 ? constraint.where.collection : void 0;
393
+ if (referencedCollection === void 0) {
394
+ return null;
395
+ }
396
+ const referenced = await ctx.queryRecords(referencedCollection, { id: fkValue });
397
+ if (referenced.length === 0) {
398
+ return {
399
+ constraint,
400
+ fields: constraint.fields,
401
+ message: `Referential constraint violated on field "${fkField}" in collection "${collection}": referenced record not found in "${referencedCollection}" with id "${String(fkValue)}"`
402
+ };
403
+ }
404
+ return null;
405
+ }
406
+ function matchesWhere(record, where) {
407
+ for (const [key, value] of Object.entries(where)) {
408
+ if (record[key] !== value) {
409
+ return false;
410
+ }
411
+ }
412
+ return true;
413
+ }
414
+
415
+ // src/constraints/resolvers.ts
416
+ var import_core2 = require("@korajs/core");
417
+ function resolveConstraintViolation(violation, mergedRecord, localOp, remoteOp, baseState) {
418
+ const startTime = Date.now();
419
+ const { constraint } = violation;
420
+ switch (constraint.onConflict) {
421
+ case "last-write-wins": {
422
+ const comparison = import_core2.HybridLogicalClock.compare(localOp.timestamp, remoteOp.timestamp);
423
+ const winner = comparison >= 0 ? localOp : remoteOp;
424
+ const resolvedRecord = applyWinnerFields(mergedRecord, winner, violation.fields);
425
+ return createResolution(
426
+ resolvedRecord,
427
+ violation,
428
+ localOp,
429
+ remoteOp,
430
+ baseState,
431
+ "constraint-lww",
432
+ startTime
433
+ );
434
+ }
435
+ case "first-write-wins": {
436
+ const comparison = import_core2.HybridLogicalClock.compare(localOp.timestamp, remoteOp.timestamp);
437
+ const winner = comparison <= 0 ? localOp : remoteOp;
438
+ const resolvedRecord = applyWinnerFields(mergedRecord, winner, violation.fields);
439
+ return createResolution(
440
+ resolvedRecord,
441
+ violation,
442
+ localOp,
443
+ remoteOp,
444
+ baseState,
445
+ "constraint-fww",
446
+ startTime
447
+ );
448
+ }
449
+ case "priority-field": {
450
+ const priorityField = constraint.priorityField;
451
+ if (priorityField === void 0) {
452
+ const comparison = import_core2.HybridLogicalClock.compare(localOp.timestamp, remoteOp.timestamp);
453
+ const winner2 = comparison >= 0 ? localOp : remoteOp;
454
+ const resolvedRecord2 = applyWinnerFields(mergedRecord, winner2, violation.fields);
455
+ return createResolution(
456
+ resolvedRecord2,
457
+ violation,
458
+ localOp,
459
+ remoteOp,
460
+ baseState,
461
+ "constraint-priority-fallback-lww",
462
+ startTime
463
+ );
464
+ }
465
+ const localPriority = getFieldValue(localOp, priorityField, mergedRecord);
466
+ const remotePriority = getFieldValue(remoteOp, priorityField, mergedRecord);
467
+ const winner = comparePriority(localPriority, remotePriority) >= 0 ? localOp : remoteOp;
468
+ const resolvedRecord = applyWinnerFields(mergedRecord, winner, violation.fields);
469
+ return createResolution(
470
+ resolvedRecord,
471
+ violation,
472
+ localOp,
473
+ remoteOp,
474
+ baseState,
475
+ "constraint-priority",
476
+ startTime
477
+ );
478
+ }
479
+ case "server-decides": {
480
+ const resolvedRecord = {
481
+ ...mergedRecord,
482
+ _pendingServerResolution: true
483
+ };
484
+ return createResolution(
485
+ resolvedRecord,
486
+ violation,
487
+ localOp,
488
+ remoteOp,
489
+ baseState,
490
+ "constraint-server-decides",
491
+ startTime
492
+ );
493
+ }
494
+ case "custom": {
495
+ if (constraint.resolve === void 0) {
496
+ const comparison = import_core2.HybridLogicalClock.compare(localOp.timestamp, remoteOp.timestamp);
497
+ const winner = comparison >= 0 ? localOp : remoteOp;
498
+ const resolvedRecord2 = applyWinnerFields(mergedRecord, winner, violation.fields);
499
+ return createResolution(
500
+ resolvedRecord2,
501
+ violation,
502
+ localOp,
503
+ remoteOp,
504
+ baseState,
505
+ "constraint-custom-fallback-lww",
506
+ startTime
507
+ );
508
+ }
509
+ const resolvedRecord = { ...mergedRecord };
510
+ for (const field of violation.fields) {
511
+ const localVal = getFieldValue(localOp, field, mergedRecord);
512
+ const remoteVal = getFieldValue(remoteOp, field, mergedRecord);
513
+ const baseVal = baseState[field];
514
+ resolvedRecord[field] = constraint.resolve(localVal, remoteVal, baseVal);
515
+ }
516
+ return createResolution(
517
+ resolvedRecord,
518
+ violation,
519
+ localOp,
520
+ remoteOp,
521
+ baseState,
522
+ "constraint-custom",
523
+ startTime
524
+ );
525
+ }
526
+ }
527
+ }
528
+ function applyWinnerFields(mergedRecord, winner, fields) {
529
+ const result = { ...mergedRecord };
530
+ const winnerData = winner.data ?? {};
531
+ for (const field of fields) {
532
+ if (field in winnerData) {
533
+ result[field] = winnerData[field];
534
+ }
535
+ }
536
+ return result;
537
+ }
538
+ function getFieldValue(op, field, mergedRecord) {
539
+ const data = op.data ?? {};
540
+ if (field in data) {
541
+ return data[field];
542
+ }
543
+ return mergedRecord[field];
544
+ }
545
+ function comparePriority(a, b) {
546
+ if (typeof a === "number" && typeof b === "number") {
547
+ return a - b;
548
+ }
549
+ if (typeof a === "string" && typeof b === "string") {
550
+ return a < b ? -1 : a > b ? 1 : 0;
551
+ }
552
+ return String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0;
553
+ }
554
+ function createResolution(resolvedRecord, violation, localOp, remoteOp, baseState, strategy, startTime) {
555
+ const field = violation.fields.join(", ");
556
+ const trace = {
557
+ operationA: localOp,
558
+ operationB: remoteOp,
559
+ field,
560
+ strategy,
561
+ inputA: extractFieldValues(localOp, violation.fields),
562
+ inputB: extractFieldValues(remoteOp, violation.fields),
563
+ base: extractFields(baseState, violation.fields),
564
+ output: extractFields(resolvedRecord, violation.fields),
565
+ tier: 2,
566
+ constraintViolated: violation.message,
567
+ duration: Date.now() - startTime
568
+ };
569
+ return { resolvedRecord, trace };
570
+ }
571
+ function extractFieldValues(op, fields) {
572
+ const data = op.data ?? {};
573
+ const result = {};
574
+ for (const field of fields) {
575
+ result[field] = data[field];
576
+ }
577
+ return result;
578
+ }
579
+ function extractFields(record, fields) {
580
+ const result = {};
581
+ for (const field of fields) {
582
+ result[field] = record[field];
583
+ }
584
+ return result;
585
+ }
586
+
587
+ // src/engine/merge-engine.ts
588
+ var import_core3 = require("@korajs/core");
589
+ var MergeEngine = class {
590
+ /**
591
+ * Merge two concurrent operations with all three tiers.
592
+ *
593
+ * Flow:
594
+ * 1. Determine which fields conflict (both ops modified the same field)
595
+ * 2. For non-conflicting fields: take the changed value from whichever op modified it
596
+ * 3. For conflicting fields: Tier 3 custom resolver if exists, else Tier 1 auto-merge
597
+ * 4. Assemble candidate merged record
598
+ * 5. If constraintContext provided: run Tier 2 constraint checks and resolve violations
599
+ * 6. Return MergeResult with all traces
600
+ *
601
+ * @param input - The two operations, base state, and collection definition
602
+ * @param constraintContext - Optional DB lookup interface for Tier 2 constraints
603
+ * @returns The merged data and traces for DevTools
604
+ */
605
+ async merge(input, constraintContext) {
606
+ if (input.local.type === "delete" && input.remote.type === "delete") {
607
+ return {
608
+ mergedData: {},
609
+ traces: [],
610
+ appliedOperation: "merged"
611
+ };
612
+ }
613
+ if (input.local.type === "delete" || input.remote.type === "delete") {
614
+ return this.mergeWithDelete(input);
615
+ }
616
+ const fieldResult = this.mergeFields(input);
617
+ if (constraintContext !== void 0 && input.collectionDef.constraints.length > 0) {
618
+ const recordWithId = { id: input.local.recordId, ...fieldResult.mergedData };
619
+ const violations = await checkConstraints(
620
+ recordWithId,
621
+ input.local.recordId,
622
+ input.local.collection,
623
+ input.collectionDef,
624
+ constraintContext
625
+ );
626
+ let mergedData = fieldResult.mergedData;
627
+ const allTraces = [...fieldResult.traces];
628
+ for (const violation of violations) {
629
+ const resolution = resolveConstraintViolation(
630
+ violation,
631
+ mergedData,
632
+ input.local,
633
+ input.remote,
634
+ input.baseState
635
+ );
636
+ mergedData = resolution.resolvedRecord;
637
+ allTraces.push(resolution.trace);
638
+ }
639
+ return {
640
+ mergedData,
641
+ traces: allTraces,
642
+ appliedOperation: determineAppliedOperation(allTraces)
643
+ };
644
+ }
645
+ return fieldResult;
646
+ }
647
+ /**
648
+ * Synchronous field-level merge (Tier 1 + Tier 3 only).
649
+ *
650
+ * Useful when constraint context is unavailable or not needed.
651
+ * Skips Tier 2 constraint checking entirely.
652
+ *
653
+ * @param input - The two operations, base state, and collection definition
654
+ * @returns The merged data and traces for DevTools
655
+ */
656
+ mergeFields(input) {
657
+ const { local, remote, baseState, collectionDef } = input;
658
+ const allFields = collectAffectedFields(local, remote, baseState, collectionDef);
659
+ const mergedData = {};
660
+ const traces = [];
661
+ for (const fieldName of allFields) {
662
+ const fieldDef = collectionDef.fields[fieldName];
663
+ if (fieldDef === void 0) {
664
+ continue;
665
+ }
666
+ const resolver = collectionDef.resolvers[fieldName];
667
+ const result = mergeField(fieldName, local, remote, baseState, fieldDef, resolver);
668
+ mergedData[fieldName] = result.value;
669
+ if (result.trace.strategy !== "no-conflict-local" && result.trace.strategy !== "no-conflict-remote" && result.trace.strategy !== "no-conflict-unchanged") {
670
+ traces.push(result.trace);
671
+ }
672
+ }
673
+ return {
674
+ mergedData,
675
+ traces,
676
+ appliedOperation: determineAppliedOperation(traces)
677
+ };
678
+ }
679
+ /**
680
+ * Handle merge when one operation is a delete.
681
+ * Default: delete wins (LWW on the record level).
682
+ */
683
+ mergeWithDelete(input) {
684
+ const { local, remote } = input;
685
+ const comparison = import_core3.HybridLogicalClock.compare(local.timestamp, remote.timestamp);
686
+ if (comparison >= 0) {
687
+ if (local.type === "delete") {
688
+ return { mergedData: {}, traces: [], appliedOperation: "local" };
689
+ }
690
+ return {
691
+ mergedData: { ...input.baseState, ...local.data ?? {} },
692
+ traces: [],
693
+ appliedOperation: "local"
694
+ };
695
+ }
696
+ if (remote.type === "delete") {
697
+ return { mergedData: {}, traces: [], appliedOperation: "remote" };
698
+ }
699
+ return {
700
+ mergedData: { ...input.baseState, ...remote.data ?? {} },
701
+ traces: [],
702
+ appliedOperation: "remote"
703
+ };
704
+ }
705
+ };
706
+ function collectAffectedFields(local, remote, baseState, collectionDef) {
707
+ const fields = /* @__PURE__ */ new Set();
708
+ for (const fieldName of Object.keys(collectionDef.fields)) {
709
+ fields.add(fieldName);
710
+ }
711
+ if (local.data !== null) {
712
+ for (const fieldName of Object.keys(local.data)) {
713
+ fields.add(fieldName);
714
+ }
715
+ }
716
+ if (remote.data !== null) {
717
+ for (const fieldName of Object.keys(remote.data)) {
718
+ fields.add(fieldName);
719
+ }
720
+ }
721
+ for (const fieldName of Object.keys(baseState)) {
722
+ fields.add(fieldName);
723
+ }
724
+ return fields;
725
+ }
726
+ function determineAppliedOperation(traces) {
727
+ if (traces.length === 0) {
728
+ return "merged";
729
+ }
730
+ let allLocal = true;
731
+ let allRemote = true;
732
+ for (const trace of traces) {
733
+ if (trace.strategy === "lww" || trace.strategy === "constraint-lww") {
734
+ if (trace.output === trace.inputA) {
735
+ allRemote = false;
736
+ } else if (trace.output === trace.inputB) {
737
+ allLocal = false;
738
+ } else {
739
+ allLocal = false;
740
+ allRemote = false;
741
+ }
742
+ } else {
743
+ allLocal = false;
744
+ allRemote = false;
745
+ }
746
+ }
747
+ if (allLocal) return "local";
748
+ if (allRemote) return "remote";
749
+ return "merged";
750
+ }
751
+ // Annotate the CommonJS export names for ESM import in node:
752
+ 0 && (module.exports = {
753
+ MergeEngine,
754
+ addWinsSet,
755
+ checkConstraints,
756
+ lastWriteWins,
757
+ mergeField,
758
+ mergeRichtext,
759
+ resolveConstraintViolation,
760
+ richtextToString,
761
+ stringToRichtextUpdate
762
+ });
763
+ //# sourceMappingURL=index.cjs.map