@korajs/merge 1.0.0-beta.12 → 1.0.0-beta.14
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 +30 -48
- package/dist/index.cjs +18 -78
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25 -11
- package/dist/index.d.ts +25 -11
- package/dist/index.js +3 -63
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,77 +1,59 @@
|
|
|
1
1
|
# @korajs/merge
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
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
|
|
15
|
+
## How Kora merges
|
|
14
16
|
|
|
15
|
-
|
|
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
|
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
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
|
-
**
|
|
23
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
332
|
-
const decoded = (0,
|
|
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
|
|
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;
|
|
@@ -740,7 +680,7 @@ async function checkSingleConstraint(constraint, mergedRecord, recordId, collect
|
|
|
740
680
|
}
|
|
741
681
|
}
|
|
742
682
|
async function checkUniqueConstraint(constraint, mergedRecord, recordId, collection, ctx) {
|
|
743
|
-
const where = {};
|
|
683
|
+
const where = { ...constraint.where ?? {} };
|
|
744
684
|
for (const field of constraint.fields) {
|
|
745
685
|
where[field] = mergedRecord[field];
|
|
746
686
|
}
|
|
@@ -810,13 +750,13 @@ function matchesWhere(record, where) {
|
|
|
810
750
|
}
|
|
811
751
|
|
|
812
752
|
// src/constraints/resolvers.ts
|
|
813
|
-
var
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
1196
|
+
var import_core8 = require("@korajs/core");
|
|
1257
1197
|
|
|
1258
1198
|
// src/constraints/state-machine-constraint.ts
|
|
1259
|
-
var
|
|
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,
|
|
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 =
|
|
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 =
|
|
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: [] };
|