@korajs/merge 1.0.0-beta.11 → 1.0.0-beta.13

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/README.md CHANGED
@@ -1,77 +1,59 @@
1
1
  # @korajs/merge
2
2
 
3
- Three-tier conflict resolution engine for Kora.js. Handles concurrent modifications across offline devices and produces deterministic, commutative merge results.
3
+ Cross-record rules for Kora.js: constraint checks (unique, capacity, referential), referential
4
+ integrity on delete, and rich-text helpers.
4
5
 
5
- > Most developers don't install this directly. Use [`korajs`](https://www.npmjs.com/package/korajs) instead.
6
+ > Most apps do not install this directly. Records are merged by the per-field fold in
7
+ > `@korajs/core` on every device and server; `createApp` and the sync server wire this package.
6
8
 
7
9
  ## Install
8
10
 
9
11
  ```bash
10
- pnpm add @korajs/merge
12
+ pnpm add @korajs/merge@beta
11
13
  ```
12
14
 
13
- ## How It Works
15
+ ## How Kora merges
14
16
 
15
- The merge engine resolves conflicts in three tiers:
17
+ Since 1.0.0-beta.13 every replica computes a record from its operations with one deterministic
18
+ per-field CRDT (the fold in `@korajs/core`), so the result depends only on the set of operations,
19
+ not their order or duplicates:
16
20
 
17
- **Tier 1 -- Auto-Merge (default for all fields):**
18
- - `string`, `number`, `boolean`, `enum`, `timestamp` -- Last-Write-Wins via HLC
19
- - `array` -- Add-wins set (union of elements)
20
- - `richtext` -- Yjs CRDT (character-level merge)
21
+ - strings, numbers, booleans, enums, timestamps: the later write wins (hybrid logical clock)
22
+ - arrays: an element set (concurrent additions and removals both apply)
23
+ - objects: per top-level key
24
+ - rich text: Yjs, character by character
25
+ - `.merge('counter' | 'max' | 'min' | 'append-only' | 'server-authoritative')` and custom
26
+ `resolve` functions where declared
21
27
 
22
- **Tier 2 -- Constraint Validation:**
23
- After auto-merge, constraints (unique, capacity, referential) are checked. Violations trigger the configured `onConflict` strategy.
24
-
25
- **Tier 3 -- Custom Resolvers:**
26
- For domain-specific logic that neither auto-merge nor constraints can handle.
27
-
28
- ## Usage
28
+ Constraints span records, so the **sync server** enforces them: a violating operation is refused,
29
+ and races are corrected with deterministic server operations.
29
30
 
31
+ <!-- docs-check: standalone -->
30
32
  ```typescript
31
- import { MergeEngine } from '@korajs/merge'
32
-
33
- const engine = new MergeEngine({ schema })
34
-
35
- // Merge two concurrent operations
36
- const result = engine.merge(localOperation, remoteOperation)
37
-
38
- // result.value -- the resolved value
39
- // result.trace -- full MergeTrace for debugging/DevTools
40
- // result.tier -- which tier resolved it (1, 2, or 3)
41
- ```
33
+ import { defineSchema, t } from 'korajs'
42
34
 
43
- ### Custom Resolver
44
-
45
- ```typescript
46
- const schema = defineSchema({
35
+ export const schema = defineSchema({
36
+ version: 1,
47
37
  collections: {
48
38
  inventory: {
49
39
  fields: {
50
- productId: t.string(),
51
- quantity: t.number(),
52
- },
53
- resolve: {
54
- quantity: (local, remote, base) => {
55
- // Additive merge: apply both deltas
56
- const localDelta = local - base
57
- const remoteDelta = remote - base
58
- return Math.max(0, base + localDelta + remoteDelta)
59
- },
40
+ sku: t.string(),
41
+ quantity: t.number().merge('counter'), // concurrent changes add up
60
42
  },
43
+ constraints: [{ type: 'unique', fields: ['sku'], onConflict: 'first-write-wins' }],
61
44
  },
62
45
  },
63
46
  })
64
47
  ```
65
48
 
66
- ## Guarantees
49
+ `MergeEngine` and the pairwise strategy functions implement the 1.0.0-beta.12 merge. They remain
50
+ only for `experimental.legacyMerge` in this release and will be removed.
67
51
 
68
- - **Deterministic** -- same operations always produce the same result
69
- - **Commutative** -- `merge(A, B)` equals `merge(B, A)`
70
- - **Idempotent** -- applying the same operation twice has no additional effect
71
- - **Traceable** -- every decision produces a `MergeTrace` for inspection
52
+ ## Documentation
53
+
54
+ [Conflict Resolution](https://korajs.dev/guide/conflict-resolution) and the
55
+ [Merge API reference](https://korajs.dev/api/merge).
72
56
 
73
57
  ## License
74
58
 
75
59
  MIT
76
-
77
- See the [full documentation](https://github.com/ehoneahobed/kora) for guides, API reference, and examples.
package/dist/index.cjs CHANGED
@@ -64,69 +64,9 @@ function lastWriteWins(localValue, remoteValue, localTimestamp, remoteTimestamp)
64
64
  }
65
65
 
66
66
  // src/strategies/add-wins-set.ts
67
+ var import_core2 = require("@korajs/core");
67
68
  function addWinsSet(localArray, remoteArray, baseArray) {
68
- const serialize = (v) => JSON.stringify(v);
69
- const baseSet = new Set(baseArray.map(serialize));
70
- const localSet = new Set(localArray.map(serialize));
71
- const remoteSet = new Set(remoteArray.map(serialize));
72
- const addedLocal = /* @__PURE__ */ new Set();
73
- for (const s of localSet) {
74
- if (!baseSet.has(s)) {
75
- addedLocal.add(s);
76
- }
77
- }
78
- const addedRemote = /* @__PURE__ */ new Set();
79
- for (const s of remoteSet) {
80
- if (!baseSet.has(s)) {
81
- addedRemote.add(s);
82
- }
83
- }
84
- const removedLocal = /* @__PURE__ */ new Set();
85
- for (const s of baseSet) {
86
- if (!localSet.has(s)) {
87
- removedLocal.add(s);
88
- }
89
- }
90
- const removedRemote = /* @__PURE__ */ new Set();
91
- for (const s of baseSet) {
92
- if (!remoteSet.has(s)) {
93
- removedRemote.add(s);
94
- }
95
- }
96
- const removedByBoth = /* @__PURE__ */ new Set();
97
- for (const s of removedLocal) {
98
- if (removedRemote.has(s)) {
99
- removedByBoth.add(s);
100
- }
101
- }
102
- const resultSerialized = /* @__PURE__ */ new Set();
103
- const result = [];
104
- const addIfNew = (serialized, value) => {
105
- if (!resultSerialized.has(serialized) && !removedByBoth.has(serialized)) {
106
- resultSerialized.add(serialized);
107
- result.push(value);
108
- }
109
- };
110
- for (const item of baseArray) {
111
- addIfNew(serialize(item), item);
112
- }
113
- const additions = /* @__PURE__ */ new Map();
114
- for (const item of localArray) {
115
- const s = serialize(item);
116
- if (addedLocal.has(s) && !additions.has(s)) {
117
- additions.set(s, item);
118
- }
119
- }
120
- for (const item of remoteArray) {
121
- const s = serialize(item);
122
- if (addedRemote.has(s) && !additions.has(s)) {
123
- additions.set(s, item);
124
- }
125
- }
126
- for (const s of [...additions.keys()].sort()) {
127
- addIfNew(s, additions.get(s));
128
- }
129
- return result;
69
+ return (0, import_core2.mergeArraySet)(localArray, remoteArray, baseArray);
130
70
  }
131
71
 
132
72
  // src/strategies/object-merge.ts
@@ -295,7 +235,7 @@ function mergeAtomicOps(localAtomicOp, remoteAtomicOp, baseValue) {
295
235
  }
296
236
 
297
237
  // src/strategies/yjs-richtext.ts
298
- var import_core2 = require("@korajs/core");
238
+ var import_core3 = require("@korajs/core");
299
239
  var Y = __toESM(require("yjs"), 1);
300
240
  var TEXT_KEY = "content";
301
241
  function mergeRichtext(localValue, remoteValue, baseValue) {
@@ -328,8 +268,8 @@ function toYjsUpdate(value) {
328
268
  if (value instanceof ArrayBuffer) {
329
269
  return new Uint8Array(value);
330
270
  }
331
- if ((0, import_core2.isKoraBytesValue)(value) || (0, import_core2.isLegacyNumericByteObject)(value)) {
332
- const decoded = (0, import_core2.decodeBytesFromOpData)(value);
271
+ if ((0, import_core3.isKoraBytesValue)(value) || (0, import_core3.isLegacyNumericByteObject)(value)) {
272
+ const decoded = (0, import_core3.decodeBytesFromOpData)(value);
333
273
  return decoded instanceof Uint8Array ? decoded : stringToRichtextUpdate(decoded);
334
274
  }
335
275
  throw new Error(
@@ -338,7 +278,7 @@ function toYjsUpdate(value) {
338
278
  }
339
279
 
340
280
  // src/strategies/schema-strategies.ts
341
- var import_core3 = require("@korajs/core");
281
+ var import_core4 = require("@korajs/core");
342
282
  function counterMerge(localValue, remoteValue, baseValue) {
343
283
  const base = typeof baseValue === "number" ? baseValue : 0;
344
284
  const local = typeof localValue === "number" ? localValue : base;
@@ -810,13 +750,13 @@ function matchesWhere(record, where) {
810
750
  }
811
751
 
812
752
  // src/constraints/resolvers.ts
813
- var import_core4 = require("@korajs/core");
753
+ var import_core5 = require("@korajs/core");
814
754
  function resolveConstraintViolation(violation, mergedRecord, localOp, remoteOp, baseState) {
815
755
  const startTime = Date.now();
816
756
  const { constraint } = violation;
817
757
  switch (constraint.onConflict) {
818
758
  case "last-write-wins": {
819
- const comparison = import_core4.HybridLogicalClock.compare(localOp.timestamp, remoteOp.timestamp);
759
+ const comparison = import_core5.HybridLogicalClock.compare(localOp.timestamp, remoteOp.timestamp);
820
760
  const winner = comparison >= 0 ? localOp : remoteOp;
821
761
  const resolvedRecord = applyWinnerFields(mergedRecord, winner, violation.fields);
822
762
  return createResolution(
@@ -830,7 +770,7 @@ function resolveConstraintViolation(violation, mergedRecord, localOp, remoteOp,
830
770
  );
831
771
  }
832
772
  case "first-write-wins": {
833
- const comparison = import_core4.HybridLogicalClock.compare(localOp.timestamp, remoteOp.timestamp);
773
+ const comparison = import_core5.HybridLogicalClock.compare(localOp.timestamp, remoteOp.timestamp);
834
774
  const winner = comparison <= 0 ? localOp : remoteOp;
835
775
  const resolvedRecord = applyWinnerFields(mergedRecord, winner, violation.fields);
836
776
  return createResolution(
@@ -846,7 +786,7 @@ function resolveConstraintViolation(violation, mergedRecord, localOp, remoteOp,
846
786
  case "priority-field": {
847
787
  const priorityField = constraint.priorityField;
848
788
  if (priorityField === void 0) {
849
- const comparison = import_core4.HybridLogicalClock.compare(localOp.timestamp, remoteOp.timestamp);
789
+ const comparison = import_core5.HybridLogicalClock.compare(localOp.timestamp, remoteOp.timestamp);
850
790
  const winner2 = comparison >= 0 ? localOp : remoteOp;
851
791
  const resolvedRecord2 = applyWinnerFields(mergedRecord, winner2, violation.fields);
852
792
  return createResolution(
@@ -890,7 +830,7 @@ function resolveConstraintViolation(violation, mergedRecord, localOp, remoteOp,
890
830
  }
891
831
  case "custom": {
892
832
  if (constraint.resolve === void 0) {
893
- const comparison = import_core4.HybridLogicalClock.compare(localOp.timestamp, remoteOp.timestamp);
833
+ const comparison = import_core5.HybridLogicalClock.compare(localOp.timestamp, remoteOp.timestamp);
894
834
  const winner = comparison >= 0 ? localOp : remoteOp;
895
835
  const resolvedRecord2 = applyWinnerFields(mergedRecord, winner, violation.fields);
896
836
  return createResolution(
@@ -982,7 +922,7 @@ function extractFields(record, fields) {
982
922
  }
983
923
 
984
924
  // src/constraints/referential-integrity.ts
985
- var import_core5 = require("@korajs/core");
925
+ var import_core6 = require("@korajs/core");
986
926
  function buildMergeRelationLookup(schema) {
987
927
  const lookup = /* @__PURE__ */ new Map();
988
928
  const relationNames = Object.keys(schema.relations).sort();
@@ -1253,17 +1193,17 @@ function createDeleteVsInsertTrace(deleteOp, insertOp, relation, strategy, actio
1253
1193
  }
1254
1194
 
1255
1195
  // src/engine/merge-engine.ts
1256
- var import_core7 = require("@korajs/core");
1196
+ var import_core8 = require("@korajs/core");
1257
1197
 
1258
1198
  // src/constraints/state-machine-constraint.ts
1259
- var import_core6 = require("@korajs/core");
1199
+ var import_core7 = require("@korajs/core");
1260
1200
  function isValid(sm, from, to) {
1261
1201
  const constraint = {
1262
1202
  field: sm.field,
1263
1203
  collection: "",
1264
1204
  transitions: sm.transitions
1265
1205
  };
1266
- return (0, import_core6.validateTransition)(constraint, from, to).valid;
1206
+ return (0, import_core7.validateTransition)(constraint, from, to).valid;
1267
1207
  }
1268
1208
  function resolveStateMachineMerge(fieldName, localOp, remoteOp, baseState, stateMachine) {
1269
1209
  const startTime = Date.now();
@@ -1324,7 +1264,7 @@ function resolveStateMachineMerge(fieldName, localOp, remoteOp, baseState, state
1324
1264
  const localValid = isValid(stateMachine, baseStateStr, localStr);
1325
1265
  const remoteValid = isValid(stateMachine, baseStateStr, remoteStr);
1326
1266
  if (localValid && remoteValid) {
1327
- const comparison = import_core6.HybridLogicalClock.compare(localOp.timestamp, remoteOp.timestamp);
1267
+ const comparison = import_core7.HybridLogicalClock.compare(localOp.timestamp, remoteOp.timestamp);
1328
1268
  const winner = comparison >= 0 ? localStr : remoteStr;
1329
1269
  return makeResult(
1330
1270
  winner,
@@ -1518,7 +1458,7 @@ var MergeEngine = class {
1518
1458
  */
1519
1459
  mergeWithDelete(input) {
1520
1460
  const { local, remote } = input;
1521
- const comparison = import_core7.HybridLogicalClock.compare(local.timestamp, remote.timestamp);
1461
+ const comparison = import_core8.HybridLogicalClock.compare(local.timestamp, remote.timestamp);
1522
1462
  if (comparison >= 0) {
1523
1463
  if (local.type === "delete") {
1524
1464
  return { mergedData: {}, traces: [], appliedOperation: "local", sideEffects: [] };