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