Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/backend/conat/test/sync/dkv-merge.test.ts
1451 views
1
/*
2
Testing merge conflicts with dkv
3
4
DEVELOPMENT:
5
6
pnpm test ./dkv-merge.test.ts
7
8
*/
9
10
import { dkv as createDkv } from "@cocalc/backend/conat/sync";
11
import { once } from "@cocalc/util/async-utils";
12
import { diff_match_patch } from "@cocalc/util/dmp";
13
import { before, after } from "@cocalc/backend/conat/test/setup";
14
15
beforeAll(before);
16
17
async function getKvs(opts?) {
18
const name = `test${Math.round(1000 * Math.random())}`;
19
// We disable autosave so that we have more precise control of how conflicts
20
// get resolved, etc. for testing purposes.
21
const kv1 = await createDkv({
22
name,
23
noAutosave: true,
24
...opts,
25
noCache: true,
26
});
27
const kv2 = await createDkv({
28
name,
29
noAutosave: true,
30
...opts,
31
noCache: true,
32
});
33
// @ts-ignore -- a little double check
34
if (kv1.kv === kv2.kv) {
35
throw Error("must not being using same underlying kv");
36
}
37
return { kv1, kv2 };
38
}
39
40
describe("test the default 'local first' merge conflict resolution function", () => {
41
it("sets up and resolves a merge conflict", async () => {
42
const { kv1, kv2 } = await getKvs();
43
kv1.set("x", 5);
44
kv2.set("x", 10);
45
expect(kv1["x"]).toEqual(5);
46
expect(kv2["x"]).toEqual(10);
47
48
// now make kv2 save, which makes kv1 detect a conflict:
49
await kv2.save();
50
// kv1 just resolves it in its own favor.
51
expect(kv1["x"]).toEqual(5);
52
await kv1.save();
53
54
// wait until kv2 gets a change, which will be learning about
55
// how the merge conflict got resolved.
56
if (kv2.get("x") != 5) {
57
// might have to wait
58
await once(kv2, "change");
59
}
60
expect(kv2["x"]).toEqual(5);
61
});
62
});
63
64
describe("test the default 'local first' merge conflict resolution function, but where we do the sets in the opposite order", () => {
65
it("sets up and resolves a merge conflict", async () => {
66
const { kv1, kv2 } = await getKvs();
67
kv2.set("x", 10);
68
kv1.set("x", 5);
69
expect(kv1["x"]).toEqual(5);
70
expect(kv2["x"]).toEqual(10);
71
72
// now make kv2 save, which makes kv1 detect a conflict:
73
await kv2.save();
74
// kv1 just resolves it in its own favor.
75
expect(kv1["x"]).toEqual(5);
76
await kv1.save();
77
78
// wait until kv2 gets a change, which will be learning about
79
// how the merge conflict got resolved.
80
if (kv2.get("x") != 5) {
81
// might have to wait
82
await once(kv2, "change");
83
}
84
expect(kv2["x"]).toEqual(5);
85
});
86
});
87
88
describe("test a trivial merge conflict resolution function", () => {
89
it("sets up and resolves a merge conflict", async () => {
90
const { kv1, kv2 } = await getKvs({
91
merge: () => {
92
// our merge strategy is to replace the value by 'conflict'
93
return "conflict";
94
},
95
});
96
kv1.set("x", 5);
97
kv2.set("x", 10);
98
expect(kv1["x"]).toEqual(5);
99
expect(kv2["x"]).toEqual(10);
100
101
// now make kv2 save, which makes kv1 detect a conflict:
102
await kv2.save();
103
if (kv1["x"] != "conflict") {
104
// might have to wait
105
await once(kv1, "change");
106
}
107
expect(kv1["x"]).toEqual("conflict");
108
109
await kv1.save();
110
// wait until kv2 gets a change, which will be learning about
111
// how the merge conflict got resolved.
112
if (kv2["x"] != "conflict") {
113
// might have to wait
114
await once(kv2, "change");
115
}
116
expect(kv2["x"]).toEqual("conflict");
117
});
118
});
119
120
describe("test a 3-way merge of strings conflict resolution function", () => {
121
const dmp = new diff_match_patch();
122
const threeWayMerge = (opts: {
123
prev: string;
124
local: string;
125
remote: string;
126
}) => {
127
return dmp.patch_apply(
128
dmp.patch_make(opts.prev, opts.local),
129
opts.remote,
130
)[0];
131
};
132
it("sets up and resolves a merge conflict", async () => {
133
const { kv1, kv2 } = await getKvs({
134
merge: ({ local, remote, prev = "" }) => {
135
// our merge strategy is to replace the value by 'conflict'
136
return threeWayMerge({ local, remote, prev });
137
},
138
});
139
kv1.set("x", "cocalc");
140
await kv1.save();
141
if (kv2["x"] != "cocalc") {
142
// might have to wait
143
await once(kv2, "change");
144
}
145
expect(kv2["x"]).toEqual("cocalc");
146
await kv2.save();
147
148
kv2.set("x", "cocalc!");
149
kv1.set("x", "LOVE cocalc");
150
await kv2.save();
151
if (kv1.get("x") != "LOVE cocalc!") {
152
await once(kv1, "change");
153
}
154
expect(kv1.get("x")).toEqual("LOVE cocalc!");
155
await kv1.save();
156
if (kv2.get("x") != "LOVE cocalc!") {
157
await once(kv2, "change");
158
}
159
expect(kv2.get("x")).toEqual("LOVE cocalc!");
160
});
161
});
162
163
describe("test a 3-way merge of that merges objects", () => {
164
it("sets up and resolves a merge conflict", async () => {
165
const { kv1, kv2 } = await getKvs({
166
merge: ({ local, remote }) => {
167
return { ...remote, ...local };
168
},
169
});
170
kv1.set("x", { a: 5 });
171
await kv1.save();
172
if (kv2["x"] == null) {
173
await once(kv2, "change");
174
}
175
expect(kv2["x"]).toEqual({ a: 5 });
176
177
kv1.set("x", { a: 5, b: 15, c: 12 });
178
kv2.set("x", { a: 5, b: 7, d: 3 });
179
await kv2.save();
180
if (kv1.get("x").d != 3) {
181
await once(kv1, "change");
182
}
183
expect(kv1.get("x")).toEqual({ a: 5, b: 15, c: 12, d: 3 });
184
await kv1.save();
185
if (kv2.get("x").b != 15) {
186
await once(kv2, "change");
187
}
188
expect(kv2.get("x")).toEqual({ a: 5, b: 15, c: 12, d: 3 });
189
});
190
});
191
192
afterAll(after);
193
194