cojson 0.10.8 → 0.10.15
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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +6 -0
- package/dist/coValueCore.js +3 -9
- package/dist/coValueCore.js.map +1 -1
- package/dist/coValues/account.js +9 -10
- package/dist/coValues/account.js.map +1 -1
- package/dist/coValues/group.js +1 -3
- package/dist/coValues/group.js.map +1 -1
- package/dist/localNode.js +3 -5
- package/dist/localNode.js.map +1 -1
- package/dist/permissions.js +2 -5
- package/dist/permissions.js.map +1 -1
- package/dist/tests/PeerKnownStates.test.js +82 -0
- package/dist/tests/PeerKnownStates.test.js.map +1 -0
- package/dist/tests/PeerState.test.js +188 -0
- package/dist/tests/PeerState.test.js.map +1 -0
- package/dist/tests/PriorityBasedMessageQueue.test.js +120 -0
- package/dist/tests/PriorityBasedMessageQueue.test.js.map +1 -0
- package/dist/tests/SyncStateManager.test.js +127 -0
- package/dist/tests/SyncStateManager.test.js.map +1 -0
- package/dist/tests/account.test.js +66 -0
- package/dist/tests/account.test.js.map +1 -0
- package/dist/tests/coList.test.js +120 -0
- package/dist/tests/coList.test.js.map +1 -0
- package/dist/tests/coMap.test.js +164 -0
- package/dist/tests/coMap.test.js.map +1 -0
- package/dist/tests/coPlainText.test.js +99 -0
- package/dist/tests/coPlainText.test.js.map +1 -0
- package/dist/tests/coStream.test.js +206 -0
- package/dist/tests/coStream.test.js.map +1 -0
- package/dist/tests/coValueCore.test.js +164 -0
- package/dist/tests/coValueCore.test.js.map +1 -0
- package/dist/tests/coValueState.test.js +364 -0
- package/dist/tests/coValueState.test.js.map +1 -0
- package/dist/tests/crypto.test.js +144 -0
- package/dist/tests/crypto.test.js.map +1 -0
- package/dist/tests/cryptoImpl.test.js +144 -0
- package/dist/tests/cryptoImpl.test.js.map +1 -0
- package/dist/tests/group.test.js +446 -0
- package/dist/tests/group.test.js.map +1 -0
- package/dist/tests/logger.test.js +118 -0
- package/dist/tests/logger.test.js.map +1 -0
- package/dist/tests/permissions.test.js +1858 -0
- package/dist/tests/permissions.test.js.map +1 -0
- package/dist/tests/priority.test.js +61 -0
- package/dist/tests/priority.test.js.map +1 -0
- package/dist/tests/sync.test.js +1548 -0
- package/dist/tests/sync.test.js.map +1 -0
- package/dist/tests/testUtils.js +312 -0
- package/dist/tests/testUtils.js.map +1 -0
- package/package.json +1 -1
- package/src/coValueCore.ts +3 -9
- package/src/coValues/account.ts +15 -15
- package/src/coValues/group.ts +1 -3
- package/src/localNode.ts +3 -5
- package/src/permissions.ts +2 -11
- package/src/tests/coValueCore.test.ts +2 -2
- package/src/tests/permissions.test.ts +45 -57
- package/tsconfig.json +1 -1
|
@@ -0,0 +1,1548 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
2
|
+
import { expectMap } from "../coValue.js";
|
|
3
|
+
import { RawCoMap } from "../coValues/coMap.js";
|
|
4
|
+
import { WasmCrypto } from "../crypto/WasmCrypto.js";
|
|
5
|
+
import { stableStringify } from "../jsonStringify.js";
|
|
6
|
+
import { LocalNode } from "../localNode.js";
|
|
7
|
+
import { getPriorityFromHeader } from "../priority.js";
|
|
8
|
+
import { connectedPeers, newQueuePair } from "../streamUtils.js";
|
|
9
|
+
import { blockMessageTypeOnOutgoingPeer, connectNodeToSyncServer, connectTwoPeers, createConnectedTestAgentNode, createConnectedTestNode, createTestMetricReader, createTestNode, loadCoValueOrFail, randomAnonymousAccountAndSessionID, setupSyncServer, tearDownTestMetricReader, waitFor, } from "./testUtils.js";
|
|
10
|
+
const Crypto = await WasmCrypto.create();
|
|
11
|
+
let jazzCloud = setupSyncServer();
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
jazzCloud = setupSyncServer();
|
|
14
|
+
});
|
|
15
|
+
test("Node replies with initial tx and header to empty subscribe", async () => {
|
|
16
|
+
const [admin, session] = randomAnonymousAccountAndSessionID();
|
|
17
|
+
const node = new LocalNode(admin, session, Crypto);
|
|
18
|
+
const group = node.createGroup();
|
|
19
|
+
const map = group.createMap();
|
|
20
|
+
map.set("hello", "world", "trusting");
|
|
21
|
+
const [inRx, inTx] = newQueuePair();
|
|
22
|
+
const [outRx, outTx] = newQueuePair();
|
|
23
|
+
const outRxQ = outRx[Symbol.asyncIterator]();
|
|
24
|
+
node.syncManager.addPeer({
|
|
25
|
+
id: "test",
|
|
26
|
+
incoming: inRx,
|
|
27
|
+
outgoing: outTx,
|
|
28
|
+
role: "peer",
|
|
29
|
+
crashOnClose: true,
|
|
30
|
+
});
|
|
31
|
+
await inTx.push({
|
|
32
|
+
action: "load",
|
|
33
|
+
id: map.core.id,
|
|
34
|
+
header: false,
|
|
35
|
+
sessions: {},
|
|
36
|
+
});
|
|
37
|
+
// expect((await outRxQ.next()).value).toMatchObject(admStateEx(admin.id));
|
|
38
|
+
expect((await outRxQ.next()).value).toMatchObject(groupStateEx(group));
|
|
39
|
+
const mapTellKnownStateMsg = (await outRxQ.next()).value;
|
|
40
|
+
expect(mapTellKnownStateMsg).toEqual({
|
|
41
|
+
action: "known",
|
|
42
|
+
...map.core.knownState(),
|
|
43
|
+
});
|
|
44
|
+
// expect((await outRxQ.next()).value).toMatchObject(admContEx(admin.id));
|
|
45
|
+
expect((await outRxQ.next()).value).toMatchObject(groupContentEx(group));
|
|
46
|
+
const newContentMsg = (await outRxQ.next()).value;
|
|
47
|
+
const expectedHeader = {
|
|
48
|
+
type: "comap",
|
|
49
|
+
ruleset: { type: "ownedByGroup", group: group.id },
|
|
50
|
+
meta: null,
|
|
51
|
+
createdAt: map.core.header.createdAt,
|
|
52
|
+
uniqueness: map.core.header.uniqueness,
|
|
53
|
+
};
|
|
54
|
+
expect(newContentMsg).toEqual({
|
|
55
|
+
action: "content",
|
|
56
|
+
id: map.core.id,
|
|
57
|
+
header: expectedHeader,
|
|
58
|
+
new: {
|
|
59
|
+
[node.currentSessionID]: {
|
|
60
|
+
after: 0,
|
|
61
|
+
newTransactions: [
|
|
62
|
+
{
|
|
63
|
+
privacy: "trusting",
|
|
64
|
+
madeAt: map.core.sessionLogs.get(node.currentSessionID)
|
|
65
|
+
.transactions[0].madeAt,
|
|
66
|
+
changes: stableStringify([
|
|
67
|
+
{
|
|
68
|
+
op: "set",
|
|
69
|
+
key: "hello",
|
|
70
|
+
value: "world",
|
|
71
|
+
},
|
|
72
|
+
]),
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
lastSignature: map.core.sessionLogs.get(node.currentSessionID)
|
|
76
|
+
.lastSignature,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
priority: getPriorityFromHeader(map.core.header),
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
test("Node replies with only new tx to subscribe with some known state", async () => {
|
|
83
|
+
const [admin, session] = randomAnonymousAccountAndSessionID();
|
|
84
|
+
const node = new LocalNode(admin, session, Crypto);
|
|
85
|
+
const group = node.createGroup();
|
|
86
|
+
const map = group.createMap();
|
|
87
|
+
map.set("hello", "world", "trusting");
|
|
88
|
+
map.set("goodbye", "world", "trusting");
|
|
89
|
+
const [inRx, inTx] = newQueuePair();
|
|
90
|
+
const [outRx, outTx] = newQueuePair();
|
|
91
|
+
const outRxQ = outRx[Symbol.asyncIterator]();
|
|
92
|
+
node.syncManager.addPeer({
|
|
93
|
+
id: "test",
|
|
94
|
+
incoming: inRx,
|
|
95
|
+
outgoing: outTx,
|
|
96
|
+
role: "peer",
|
|
97
|
+
crashOnClose: true,
|
|
98
|
+
});
|
|
99
|
+
await inTx.push({
|
|
100
|
+
action: "load",
|
|
101
|
+
id: map.core.id,
|
|
102
|
+
header: true,
|
|
103
|
+
sessions: {
|
|
104
|
+
[node.currentSessionID]: 1,
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
// expect((await outRxQ.next()).value).toMatchObject(admStateEx(admin.id));
|
|
108
|
+
expect((await outRxQ.next()).value).toMatchObject(groupStateEx(group));
|
|
109
|
+
const mapTellKnownStateMsg = (await outRxQ.next()).value;
|
|
110
|
+
expect(mapTellKnownStateMsg).toEqual({
|
|
111
|
+
action: "known",
|
|
112
|
+
...map.core.knownState(),
|
|
113
|
+
});
|
|
114
|
+
// expect((await outRxQ.next()).value).toMatchObject(admContEx(admin.id));
|
|
115
|
+
expect((await outRxQ.next()).value).toMatchObject(groupContentEx(group));
|
|
116
|
+
const mapNewContentMsg = (await outRxQ.next()).value;
|
|
117
|
+
expect(mapNewContentMsg).toEqual({
|
|
118
|
+
action: "content",
|
|
119
|
+
id: map.core.id,
|
|
120
|
+
header: undefined,
|
|
121
|
+
new: {
|
|
122
|
+
[node.currentSessionID]: {
|
|
123
|
+
after: 1,
|
|
124
|
+
newTransactions: [
|
|
125
|
+
{
|
|
126
|
+
privacy: "trusting",
|
|
127
|
+
madeAt: map.core.sessionLogs.get(node.currentSessionID)
|
|
128
|
+
.transactions[1].madeAt,
|
|
129
|
+
changes: stableStringify([
|
|
130
|
+
{
|
|
131
|
+
op: "set",
|
|
132
|
+
key: "goodbye",
|
|
133
|
+
value: "world",
|
|
134
|
+
},
|
|
135
|
+
]),
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
lastSignature: map.core.sessionLogs.get(node.currentSessionID)
|
|
139
|
+
.lastSignature,
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
priority: getPriorityFromHeader(map.core.header),
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
test.todo("TODO: node only replies with new tx to subscribe with some known state, even in the depended on coValues");
|
|
146
|
+
test("After subscribing, node sends own known state and new txs to peer", async () => {
|
|
147
|
+
const [admin, session] = randomAnonymousAccountAndSessionID();
|
|
148
|
+
const node = new LocalNode(admin, session, Crypto);
|
|
149
|
+
const group = node.createGroup();
|
|
150
|
+
const map = group.createMap();
|
|
151
|
+
const [inRx, inTx] = newQueuePair();
|
|
152
|
+
const [outRx, outTx] = newQueuePair();
|
|
153
|
+
const outRxQ = outRx[Symbol.asyncIterator]();
|
|
154
|
+
node.syncManager.addPeer({
|
|
155
|
+
id: "test",
|
|
156
|
+
incoming: inRx,
|
|
157
|
+
outgoing: outTx,
|
|
158
|
+
role: "peer",
|
|
159
|
+
crashOnClose: true,
|
|
160
|
+
});
|
|
161
|
+
await inTx.push({
|
|
162
|
+
action: "load",
|
|
163
|
+
id: map.core.id,
|
|
164
|
+
header: false,
|
|
165
|
+
sessions: {
|
|
166
|
+
[node.currentSessionID]: 0,
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
// expect((await outRxQ.next()).value).toMatchObject(admStateEx(admin.id));
|
|
170
|
+
expect((await outRxQ.next()).value).toMatchObject(groupStateEx(group));
|
|
171
|
+
const mapTellKnownStateMsg = (await outRxQ.next()).value;
|
|
172
|
+
expect(mapTellKnownStateMsg).toEqual({
|
|
173
|
+
action: "known",
|
|
174
|
+
...map.core.knownState(),
|
|
175
|
+
});
|
|
176
|
+
// expect((await outRxQ.next()).value).toMatchObject(admContEx(admin.id));
|
|
177
|
+
expect((await outRxQ.next()).value).toMatchObject(groupContentEx(group));
|
|
178
|
+
const mapNewContentHeaderOnlyMsg = (await outRxQ.next()).value;
|
|
179
|
+
expect(mapNewContentHeaderOnlyMsg).toEqual({
|
|
180
|
+
action: "content",
|
|
181
|
+
id: map.core.id,
|
|
182
|
+
header: map.core.header,
|
|
183
|
+
new: {},
|
|
184
|
+
priority: getPriorityFromHeader(map.core.header),
|
|
185
|
+
});
|
|
186
|
+
map.set("hello", "world", "trusting");
|
|
187
|
+
const mapEditMsg1 = (await outRxQ.next()).value;
|
|
188
|
+
expect(mapEditMsg1).toEqual({
|
|
189
|
+
action: "content",
|
|
190
|
+
id: map.core.id,
|
|
191
|
+
new: {
|
|
192
|
+
[node.currentSessionID]: {
|
|
193
|
+
after: 0,
|
|
194
|
+
newTransactions: [
|
|
195
|
+
{
|
|
196
|
+
privacy: "trusting",
|
|
197
|
+
madeAt: map.core.sessionLogs.get(node.currentSessionID)
|
|
198
|
+
.transactions[0].madeAt,
|
|
199
|
+
changes: stableStringify([
|
|
200
|
+
{
|
|
201
|
+
op: "set",
|
|
202
|
+
key: "hello",
|
|
203
|
+
value: "world",
|
|
204
|
+
},
|
|
205
|
+
]),
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
lastSignature: map.core.sessionLogs.get(node.currentSessionID)
|
|
209
|
+
.lastSignature,
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
priority: getPriorityFromHeader(map.core.header),
|
|
213
|
+
});
|
|
214
|
+
map.set("goodbye", "world", "trusting");
|
|
215
|
+
const mapEditMsg2 = (await outRxQ.next()).value;
|
|
216
|
+
expect(mapEditMsg2).toEqual({
|
|
217
|
+
action: "content",
|
|
218
|
+
id: map.core.id,
|
|
219
|
+
new: {
|
|
220
|
+
[node.currentSessionID]: {
|
|
221
|
+
after: 1,
|
|
222
|
+
newTransactions: [
|
|
223
|
+
{
|
|
224
|
+
privacy: "trusting",
|
|
225
|
+
madeAt: map.core.sessionLogs.get(node.currentSessionID)
|
|
226
|
+
.transactions[1].madeAt,
|
|
227
|
+
changes: stableStringify([
|
|
228
|
+
{
|
|
229
|
+
op: "set",
|
|
230
|
+
key: "goodbye",
|
|
231
|
+
value: "world",
|
|
232
|
+
},
|
|
233
|
+
]),
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
lastSignature: map.core.sessionLogs.get(node.currentSessionID)
|
|
237
|
+
.lastSignature,
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
priority: getPriorityFromHeader(map.core.header),
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
test("Client replies with known new content to tellKnownState from server", async () => {
|
|
244
|
+
const [admin, session] = randomAnonymousAccountAndSessionID();
|
|
245
|
+
const node = new LocalNode(admin, session, Crypto);
|
|
246
|
+
const group = node.createGroup();
|
|
247
|
+
const map = group.createMap();
|
|
248
|
+
map.set("hello", "world", "trusting");
|
|
249
|
+
const [inRx, inTx] = newQueuePair();
|
|
250
|
+
const [outRx, outTx] = newQueuePair();
|
|
251
|
+
const outRxQ = outRx[Symbol.asyncIterator]();
|
|
252
|
+
node.syncManager.addPeer({
|
|
253
|
+
id: "test",
|
|
254
|
+
incoming: inRx,
|
|
255
|
+
outgoing: outTx,
|
|
256
|
+
role: "peer",
|
|
257
|
+
crashOnClose: true,
|
|
258
|
+
});
|
|
259
|
+
// expect((await outRxQ.next()).value).toMatchObject(groupStateEx(group));
|
|
260
|
+
await inTx.push({
|
|
261
|
+
action: "known",
|
|
262
|
+
id: map.core.id,
|
|
263
|
+
header: false,
|
|
264
|
+
sessions: {
|
|
265
|
+
[node.currentSessionID]: 0,
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
// expect((await outRxQ.next()).value).toMatchObject(admStateEx(admin.id));
|
|
269
|
+
expect((await outRxQ.next()).value).toMatchObject(groupStateEx(group));
|
|
270
|
+
const mapTellKnownStateMsg = (await outRxQ.next()).value;
|
|
271
|
+
expect(mapTellKnownStateMsg).toEqual({
|
|
272
|
+
action: "known",
|
|
273
|
+
...map.core.knownState(),
|
|
274
|
+
});
|
|
275
|
+
// expect((await outRxQ.next()).value).toMatchObject(admContEx(admin.id));
|
|
276
|
+
expect((await outRxQ.next()).value).toMatchObject(groupContentEx(group));
|
|
277
|
+
const mapNewContentMsg = (await outRxQ.next()).value;
|
|
278
|
+
expect(mapNewContentMsg).toEqual({
|
|
279
|
+
action: "content",
|
|
280
|
+
id: map.core.id,
|
|
281
|
+
header: map.core.header,
|
|
282
|
+
new: {
|
|
283
|
+
[node.currentSessionID]: {
|
|
284
|
+
after: 0,
|
|
285
|
+
newTransactions: [
|
|
286
|
+
{
|
|
287
|
+
privacy: "trusting",
|
|
288
|
+
madeAt: map.core.sessionLogs.get(node.currentSessionID)
|
|
289
|
+
.transactions[0].madeAt,
|
|
290
|
+
changes: stableStringify([
|
|
291
|
+
{
|
|
292
|
+
op: "set",
|
|
293
|
+
key: "hello",
|
|
294
|
+
value: "world",
|
|
295
|
+
},
|
|
296
|
+
]),
|
|
297
|
+
},
|
|
298
|
+
],
|
|
299
|
+
lastSignature: map.core.sessionLogs.get(node.currentSessionID)
|
|
300
|
+
.lastSignature,
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
priority: getPriorityFromHeader(map.core.header),
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
test("No matter the optimistic known state, node respects invalid known state messages and resyncs", async () => {
|
|
307
|
+
const [admin, session] = randomAnonymousAccountAndSessionID();
|
|
308
|
+
const node = new LocalNode(admin, session, Crypto);
|
|
309
|
+
const group = node.createGroup();
|
|
310
|
+
const map = group.createMap();
|
|
311
|
+
const [inRx, inTx] = newQueuePair();
|
|
312
|
+
const [outRx, outTx] = newQueuePair();
|
|
313
|
+
const outRxQ = outRx[Symbol.asyncIterator]();
|
|
314
|
+
node.syncManager.addPeer({
|
|
315
|
+
id: "test",
|
|
316
|
+
incoming: inRx,
|
|
317
|
+
outgoing: outTx,
|
|
318
|
+
role: "peer",
|
|
319
|
+
crashOnClose: true,
|
|
320
|
+
});
|
|
321
|
+
await inTx.push({
|
|
322
|
+
action: "load",
|
|
323
|
+
id: map.core.id,
|
|
324
|
+
header: false,
|
|
325
|
+
sessions: {
|
|
326
|
+
[node.currentSessionID]: 0,
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
// expect((await outRxQ.next()).value).toMatchObject(admStateEx(admin.id));
|
|
330
|
+
expect((await outRxQ.next()).value).toMatchObject(groupStateEx(group));
|
|
331
|
+
const mapTellKnownStateMsg = (await outRxQ.next()).value;
|
|
332
|
+
expect(mapTellKnownStateMsg).toEqual({
|
|
333
|
+
action: "known",
|
|
334
|
+
...map.core.knownState(),
|
|
335
|
+
});
|
|
336
|
+
// expect((await outRxQ.next()).value).toMatchObject(admContEx(admin.id));
|
|
337
|
+
expect((await outRxQ.next()).value).toMatchObject(groupContentEx(group));
|
|
338
|
+
const mapNewContentHeaderOnlyMsg = (await outRxQ.next()).value;
|
|
339
|
+
expect(mapNewContentHeaderOnlyMsg).toEqual({
|
|
340
|
+
action: "content",
|
|
341
|
+
id: map.core.id,
|
|
342
|
+
header: map.core.header,
|
|
343
|
+
new: {},
|
|
344
|
+
priority: getPriorityFromHeader(map.core.header),
|
|
345
|
+
});
|
|
346
|
+
map.set("hello", "world", "trusting");
|
|
347
|
+
map.set("goodbye", "world", "trusting");
|
|
348
|
+
const _mapEditMsgs = (await outRxQ.next()).value;
|
|
349
|
+
console.log("Sending correction");
|
|
350
|
+
await inTx.push({
|
|
351
|
+
action: "known",
|
|
352
|
+
isCorrection: true,
|
|
353
|
+
id: map.core.id,
|
|
354
|
+
header: true,
|
|
355
|
+
sessions: {
|
|
356
|
+
[node.currentSessionID]: 1,
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
const newContentAfterWrongAssumedState = (await outRxQ.next()).value;
|
|
360
|
+
expect(newContentAfterWrongAssumedState).toEqual({
|
|
361
|
+
action: "content",
|
|
362
|
+
id: map.core.id,
|
|
363
|
+
header: undefined,
|
|
364
|
+
new: {
|
|
365
|
+
[node.currentSessionID]: {
|
|
366
|
+
after: 1,
|
|
367
|
+
newTransactions: [
|
|
368
|
+
{
|
|
369
|
+
privacy: "trusting",
|
|
370
|
+
madeAt: map.core.sessionLogs.get(node.currentSessionID)
|
|
371
|
+
.transactions[1].madeAt,
|
|
372
|
+
changes: stableStringify([
|
|
373
|
+
{
|
|
374
|
+
op: "set",
|
|
375
|
+
key: "goodbye",
|
|
376
|
+
value: "world",
|
|
377
|
+
},
|
|
378
|
+
]),
|
|
379
|
+
},
|
|
380
|
+
],
|
|
381
|
+
lastSignature: map.core.sessionLogs.get(node.currentSessionID)
|
|
382
|
+
.lastSignature,
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
priority: getPriorityFromHeader(map.core.header),
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
test("If we add a peer, but it never subscribes to a coValue, it won't get any messages", async () => {
|
|
389
|
+
const [admin, session] = randomAnonymousAccountAndSessionID();
|
|
390
|
+
const node = new LocalNode(admin, session, Crypto);
|
|
391
|
+
const group = node.createGroup();
|
|
392
|
+
const map = group.createMap();
|
|
393
|
+
const [inRx, _inTx] = newQueuePair();
|
|
394
|
+
const [outRx, outTx] = newQueuePair();
|
|
395
|
+
const outRxQ = outRx[Symbol.asyncIterator]();
|
|
396
|
+
node.syncManager.addPeer({
|
|
397
|
+
id: "test",
|
|
398
|
+
incoming: inRx,
|
|
399
|
+
outgoing: outTx,
|
|
400
|
+
role: "peer",
|
|
401
|
+
crashOnClose: true,
|
|
402
|
+
});
|
|
403
|
+
map.set("hello", "world", "trusting");
|
|
404
|
+
const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve("neverHappened"), 100));
|
|
405
|
+
const result = await Promise.race([
|
|
406
|
+
outRxQ.next().then((value) => value.value),
|
|
407
|
+
timeoutPromise,
|
|
408
|
+
]);
|
|
409
|
+
expect(result).toEqual("neverHappened");
|
|
410
|
+
});
|
|
411
|
+
test.todo("If we add a server peer, all updates to all coValues are sent to it, even if it doesn't subscribe", async () => {
|
|
412
|
+
const [admin, session] = randomAnonymousAccountAndSessionID();
|
|
413
|
+
const node = new LocalNode(admin, session, Crypto);
|
|
414
|
+
const group = node.createGroup();
|
|
415
|
+
const map = group.createMap();
|
|
416
|
+
const [inRx, _inTx] = newQueuePair();
|
|
417
|
+
const [outRx, outTx] = newQueuePair();
|
|
418
|
+
const outRxQ = outRx[Symbol.asyncIterator]();
|
|
419
|
+
node.syncManager.addPeer({
|
|
420
|
+
id: "test",
|
|
421
|
+
incoming: inRx,
|
|
422
|
+
outgoing: outTx,
|
|
423
|
+
role: "server",
|
|
424
|
+
crashOnClose: true,
|
|
425
|
+
});
|
|
426
|
+
// expect((await outRxQ.next()).value).toMatchObject({
|
|
427
|
+
// action: "load",
|
|
428
|
+
// id: adminID,
|
|
429
|
+
// });
|
|
430
|
+
expect((await outRxQ.next()).value).toMatchObject({
|
|
431
|
+
action: "load",
|
|
432
|
+
id: group.core.id,
|
|
433
|
+
});
|
|
434
|
+
const mapSubscribeMsg = (await outRxQ.next()).value;
|
|
435
|
+
expect(mapSubscribeMsg).toEqual({
|
|
436
|
+
action: "load",
|
|
437
|
+
id: map.core.id,
|
|
438
|
+
header: true,
|
|
439
|
+
sessions: {},
|
|
440
|
+
});
|
|
441
|
+
map.set("hello", "world", "trusting");
|
|
442
|
+
// expect((await outRxQ.next()).value).toMatchObject(admContEx(admin.id));
|
|
443
|
+
expect((await outRxQ.next()).value).toMatchObject(groupContentEx(group));
|
|
444
|
+
const mapNewContentMsg = (await outRxQ.next()).value;
|
|
445
|
+
expect(mapNewContentMsg).toEqual({
|
|
446
|
+
action: "content",
|
|
447
|
+
id: map.core.id,
|
|
448
|
+
header: map.core.header,
|
|
449
|
+
new: {
|
|
450
|
+
[node.currentSessionID]: {
|
|
451
|
+
after: 0,
|
|
452
|
+
newTransactions: [
|
|
453
|
+
{
|
|
454
|
+
privacy: "trusting",
|
|
455
|
+
madeAt: map.core.sessionLogs.get(node.currentSessionID)
|
|
456
|
+
.transactions[0].madeAt,
|
|
457
|
+
changes: stableStringify([
|
|
458
|
+
{
|
|
459
|
+
op: "set",
|
|
460
|
+
key: "hello",
|
|
461
|
+
value: "world",
|
|
462
|
+
},
|
|
463
|
+
]),
|
|
464
|
+
},
|
|
465
|
+
],
|
|
466
|
+
lastSignature: map.core.sessionLogs.get(node.currentSessionID)
|
|
467
|
+
.lastSignature,
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
priority: getPriorityFromHeader(map.core.header),
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
test.skip("If we add a server peer, newly created coValues are auto-subscribed to", async () => {
|
|
474
|
+
const [admin, session] = randomAnonymousAccountAndSessionID();
|
|
475
|
+
const node = new LocalNode(admin, session, Crypto);
|
|
476
|
+
const group = node.createGroup();
|
|
477
|
+
const [inRx, _inTx] = newQueuePair();
|
|
478
|
+
const [outRx, outTx] = newQueuePair();
|
|
479
|
+
const outRxQ = outRx[Symbol.asyncIterator]();
|
|
480
|
+
node.syncManager.addPeer({
|
|
481
|
+
id: "test",
|
|
482
|
+
incoming: inRx,
|
|
483
|
+
outgoing: outTx,
|
|
484
|
+
role: "server",
|
|
485
|
+
crashOnClose: true,
|
|
486
|
+
});
|
|
487
|
+
// expect((await outRxQ.next()).value).toMatchObject({
|
|
488
|
+
// action: "load",
|
|
489
|
+
// id: admin.id,
|
|
490
|
+
// });
|
|
491
|
+
expect((await outRxQ.next()).value).toMatchObject({
|
|
492
|
+
action: "load",
|
|
493
|
+
id: group.core.id,
|
|
494
|
+
});
|
|
495
|
+
const map = group.createMap();
|
|
496
|
+
const mapSubscribeMsg = (await outRxQ.next()).value;
|
|
497
|
+
expect(mapSubscribeMsg).toEqual({
|
|
498
|
+
action: "load",
|
|
499
|
+
...map.core.knownState(),
|
|
500
|
+
});
|
|
501
|
+
// expect((await outRxQ.next()).value).toMatchObject(admContEx(adminID));
|
|
502
|
+
expect((await outRxQ.next()).value).toMatchObject(groupContentEx(group));
|
|
503
|
+
const mapContentMsg = (await outRxQ.next()).value;
|
|
504
|
+
expect(mapContentMsg).toEqual({
|
|
505
|
+
action: "content",
|
|
506
|
+
id: map.core.id,
|
|
507
|
+
header: map.core.header,
|
|
508
|
+
new: {},
|
|
509
|
+
priority: getPriorityFromHeader(map.core.header),
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
test.todo("TODO: when receiving a subscribe response that is behind our optimistic state (due to already sent content), we ignore it");
|
|
513
|
+
test("When we connect a new server peer, we try to sync all existing coValues to it", async () => {
|
|
514
|
+
const [admin, session] = randomAnonymousAccountAndSessionID();
|
|
515
|
+
const node = new LocalNode(admin, session, Crypto);
|
|
516
|
+
const group = node.createGroup();
|
|
517
|
+
const map = group.createMap();
|
|
518
|
+
const [inRx, _inTx] = newQueuePair();
|
|
519
|
+
const [outRx, outTx] = newQueuePair();
|
|
520
|
+
const outRxQ = outRx[Symbol.asyncIterator]();
|
|
521
|
+
node.syncManager.addPeer({
|
|
522
|
+
id: "test",
|
|
523
|
+
incoming: inRx,
|
|
524
|
+
outgoing: outTx,
|
|
525
|
+
role: "server",
|
|
526
|
+
crashOnClose: true,
|
|
527
|
+
});
|
|
528
|
+
// const _adminSubscribeMessage = await outRxQ.next();
|
|
529
|
+
const groupSubscribeMessage = (await outRxQ.next()).value;
|
|
530
|
+
expect(groupSubscribeMessage).toEqual({
|
|
531
|
+
action: "load",
|
|
532
|
+
...group.core.knownState(),
|
|
533
|
+
});
|
|
534
|
+
const secondMessage = (await outRxQ.next()).value;
|
|
535
|
+
expect(secondMessage).toEqual({
|
|
536
|
+
action: "load",
|
|
537
|
+
...map.core.knownState(),
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
test("When receiving a subscribe with a known state that is ahead of our own, peers should respond with a corresponding subscribe response message", async () => {
|
|
541
|
+
const [admin, session] = randomAnonymousAccountAndSessionID();
|
|
542
|
+
const node = new LocalNode(admin, session, Crypto);
|
|
543
|
+
const group = node.createGroup();
|
|
544
|
+
const map = group.createMap();
|
|
545
|
+
const [inRx, inTx] = newQueuePair();
|
|
546
|
+
const [outRx, outTx] = newQueuePair();
|
|
547
|
+
const outRxQ = outRx[Symbol.asyncIterator]();
|
|
548
|
+
node.syncManager.addPeer({
|
|
549
|
+
id: "test",
|
|
550
|
+
incoming: inRx,
|
|
551
|
+
outgoing: outTx,
|
|
552
|
+
role: "peer",
|
|
553
|
+
crashOnClose: true,
|
|
554
|
+
});
|
|
555
|
+
await inTx.push({
|
|
556
|
+
action: "load",
|
|
557
|
+
id: map.core.id,
|
|
558
|
+
header: true,
|
|
559
|
+
sessions: {
|
|
560
|
+
[node.currentSessionID]: 1,
|
|
561
|
+
},
|
|
562
|
+
});
|
|
563
|
+
// expect((await outRxQ.next()).value).toMatchObject(admStateEx(admin.id));
|
|
564
|
+
expect((await outRxQ.next()).value).toMatchObject(groupStateEx(group));
|
|
565
|
+
const mapTellKnownState = (await outRxQ.next()).value;
|
|
566
|
+
expect(mapTellKnownState).toEqual({
|
|
567
|
+
action: "known",
|
|
568
|
+
...map.core.knownState(),
|
|
569
|
+
});
|
|
570
|
+
});
|
|
571
|
+
test.skip("When replaying creation and transactions of a coValue as new content, the receiving peer integrates this information", async () => {
|
|
572
|
+
// TODO: this test is mostly correct but also slightly unrealistic, make sure we pass all messages back and forth as expected and then it should work
|
|
573
|
+
const [admin, session] = randomAnonymousAccountAndSessionID();
|
|
574
|
+
const node1 = new LocalNode(admin, session, Crypto);
|
|
575
|
+
const group = node1.createGroup();
|
|
576
|
+
const [inRx1, inTx1] = newQueuePair();
|
|
577
|
+
const [outRx1, outTx1] = newQueuePair();
|
|
578
|
+
const outRxQ1 = outRx1[Symbol.asyncIterator]();
|
|
579
|
+
node1.syncManager.addPeer({
|
|
580
|
+
id: "test2",
|
|
581
|
+
incoming: inRx1,
|
|
582
|
+
outgoing: outTx1,
|
|
583
|
+
role: "server",
|
|
584
|
+
crashOnClose: true,
|
|
585
|
+
});
|
|
586
|
+
const node2 = new LocalNode(admin, Crypto.newRandomSessionID(admin.id), Crypto);
|
|
587
|
+
const [inRx2, inTx2] = newQueuePair();
|
|
588
|
+
const [outRx2, outTx2] = newQueuePair();
|
|
589
|
+
const outRxQ2 = outRx2[Symbol.asyncIterator]();
|
|
590
|
+
node2.syncManager.addPeer({
|
|
591
|
+
id: "test1",
|
|
592
|
+
incoming: inRx2,
|
|
593
|
+
outgoing: outTx2,
|
|
594
|
+
role: "client",
|
|
595
|
+
crashOnClose: true,
|
|
596
|
+
});
|
|
597
|
+
const adminSubscribeMessage = (await outRxQ1.next()).value;
|
|
598
|
+
expect(adminSubscribeMessage).toMatchObject({
|
|
599
|
+
action: "load",
|
|
600
|
+
id: admin.id,
|
|
601
|
+
});
|
|
602
|
+
const groupSubscribeMsg = (await outRxQ1.next()).value;
|
|
603
|
+
expect(groupSubscribeMsg).toMatchObject({
|
|
604
|
+
action: "load",
|
|
605
|
+
id: group.core.id,
|
|
606
|
+
});
|
|
607
|
+
await inTx2.push(adminSubscribeMessage);
|
|
608
|
+
await inTx2.push(groupSubscribeMsg);
|
|
609
|
+
// const adminTellKnownStateMsg = (await outRxQ2.next()).value;
|
|
610
|
+
// expect(adminTellKnownStateMsg).toMatchObject(admStateEx(admin.id));
|
|
611
|
+
const groupTellKnownStateMsg = (await outRxQ2.next()).value;
|
|
612
|
+
expect(groupTellKnownStateMsg).toMatchObject(groupStateEx(group));
|
|
613
|
+
expect(node2.syncManager.peers["test1"].optimisticKnownStates.has(group.core.id)).toBeDefined();
|
|
614
|
+
// await inTx1.push(adminTellKnownStateMsg);
|
|
615
|
+
await inTx1.push(groupTellKnownStateMsg);
|
|
616
|
+
// const adminContentMsg = (await outRxQ1.next()).value;
|
|
617
|
+
// expect(adminContentMsg).toMatchObject(admContEx(admin.id));
|
|
618
|
+
const groupContentMsg = (await outRxQ1.next()).value;
|
|
619
|
+
expect(groupContentMsg).toMatchObject(groupContentEx(group));
|
|
620
|
+
// await inTx2.push(adminContentMsg);
|
|
621
|
+
await inTx2.push(groupContentMsg);
|
|
622
|
+
const map = group.createMap();
|
|
623
|
+
const mapSubscriptionMsg = (await outRxQ1.next()).value;
|
|
624
|
+
expect(mapSubscriptionMsg).toMatchObject({
|
|
625
|
+
action: "load",
|
|
626
|
+
id: map.core.id,
|
|
627
|
+
});
|
|
628
|
+
const mapNewContentMsg = (await outRxQ1.next()).value;
|
|
629
|
+
expect(mapNewContentMsg).toEqual({
|
|
630
|
+
action: "content",
|
|
631
|
+
id: map.core.id,
|
|
632
|
+
header: map.core.header,
|
|
633
|
+
new: {},
|
|
634
|
+
priority: getPriorityFromHeader(map.core.header),
|
|
635
|
+
});
|
|
636
|
+
await inTx2.push(mapSubscriptionMsg);
|
|
637
|
+
const mapTellKnownStateMsg = (await outRxQ2.next()).value;
|
|
638
|
+
expect(mapTellKnownStateMsg).toEqual({
|
|
639
|
+
action: "known",
|
|
640
|
+
id: map.core.id,
|
|
641
|
+
header: false,
|
|
642
|
+
sessions: {},
|
|
643
|
+
});
|
|
644
|
+
expect(node2.coValuesStore.get(map.core.id).state.type).toEqual("loading");
|
|
645
|
+
await inTx2.push(mapNewContentMsg);
|
|
646
|
+
map.set("hello", "world", "trusting");
|
|
647
|
+
const mapEditMsg = (await outRxQ1.next()).value;
|
|
648
|
+
await inTx2.push(mapEditMsg);
|
|
649
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
650
|
+
expect(expectMap(node2.expectCoValueLoaded(map.core.id).getCurrentContent()).get("hello")).toEqual("world");
|
|
651
|
+
});
|
|
652
|
+
test.skip("When loading a coValue on one node, the server node it is requested from replies with all the necessary depended on coValues to make it work", async () => {
|
|
653
|
+
/*
|
|
654
|
+
// TODO: this test is mostly correct but also slightly unrealistic, make sure we pass all messages back and forth as expected and then it should work
|
|
655
|
+
const [admin, session] = randomAnonymousAccountAndSessionID();
|
|
656
|
+
|
|
657
|
+
const node1 = new LocalNode(admin, session, Crypto);
|
|
658
|
+
|
|
659
|
+
const group = node1.createGroup();
|
|
660
|
+
|
|
661
|
+
const map = group.createMap();
|
|
662
|
+
map.set("hello", "world", "trusting");
|
|
663
|
+
|
|
664
|
+
const node2 = new LocalNode(admin, newRandomSessionID(admin.id), Crypto);
|
|
665
|
+
|
|
666
|
+
const [node1asPeer, node2asPeer] = connectedPeers("peer1", "peer2");
|
|
667
|
+
|
|
668
|
+
node1.syncManager.addPeer(node2asPeer);
|
|
669
|
+
node2.syncManager.addPeer(node1asPeer);
|
|
670
|
+
|
|
671
|
+
await node2.loadCoValueCore(map.core.id);
|
|
672
|
+
|
|
673
|
+
expect(
|
|
674
|
+
expectMap(
|
|
675
|
+
node2.expectCoValueLoaded(map.core.id).getCurrentContent(),
|
|
676
|
+
).get("hello"),
|
|
677
|
+
).toEqual("world");
|
|
678
|
+
*/
|
|
679
|
+
});
|
|
680
|
+
test("Can sync a coValue through a server to another client", async () => {
|
|
681
|
+
const { node: client1 } = await createConnectedTestNode();
|
|
682
|
+
const group = client1.createGroup();
|
|
683
|
+
const map = group.createMap();
|
|
684
|
+
map.set("hello", "world", "trusting");
|
|
685
|
+
const { node: client2 } = await createConnectedTestNode();
|
|
686
|
+
const mapOnClient2 = await loadCoValueOrFail(client2, map.id);
|
|
687
|
+
expect(mapOnClient2.get("hello")).toEqual("world");
|
|
688
|
+
});
|
|
689
|
+
test("Can sync a coValue with private transactions through a server to another client", async () => {
|
|
690
|
+
const { node: client1 } = await createConnectedTestNode();
|
|
691
|
+
const group = client1.createGroup();
|
|
692
|
+
const map = group.createMap();
|
|
693
|
+
map.set("hello", "world", "private");
|
|
694
|
+
group.addMember("everyone", "reader");
|
|
695
|
+
const { node: client2 } = await createConnectedTestNode();
|
|
696
|
+
const mapOnClient2 = await loadCoValueOrFail(client2, map.id);
|
|
697
|
+
expect(mapOnClient2.get("hello")).toEqual("world");
|
|
698
|
+
});
|
|
699
|
+
test.skip("When a peer's incoming/readable stream closes, we remove the peer", async () => {
|
|
700
|
+
/*
|
|
701
|
+
const [admin, session] = randomAnonymousAccountAndSessionID();
|
|
702
|
+
const node = new LocalNode(admin, session, Crypto);
|
|
703
|
+
|
|
704
|
+
const group = node.createGroup();
|
|
705
|
+
|
|
706
|
+
const [inRx, inTx] = await Effect.runPromise(newStreamPair());
|
|
707
|
+
const [outRx, outTx] = await Effect.runPromise(newStreamPair());
|
|
708
|
+
|
|
709
|
+
node.syncManager.addPeer({
|
|
710
|
+
id: "test",
|
|
711
|
+
incoming: inRx,
|
|
712
|
+
outgoing: outTx,
|
|
713
|
+
role: "server",
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
// expect(yield* Queue.take(outRxQ)).toMatchObject({
|
|
717
|
+
// action: "load",
|
|
718
|
+
// id: admin.id,
|
|
719
|
+
// });
|
|
720
|
+
expect(yield * Queue.take(outRxQ)).toMatchObject({
|
|
721
|
+
action: "load",
|
|
722
|
+
id: group.core.id,
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
const map = group.createMap();
|
|
726
|
+
|
|
727
|
+
const mapSubscribeMsg = await reader.read();
|
|
728
|
+
|
|
729
|
+
expect(mapSubscribeMsg.value).toEqual({
|
|
730
|
+
action: "load",
|
|
731
|
+
...map.core.knownState(),
|
|
732
|
+
} satisfies SyncMessage);
|
|
733
|
+
|
|
734
|
+
// expect(yield* Queue.take(outRxQ)).toMatchObject(admContEx(admin.id));
|
|
735
|
+
expect(yield * Queue.take(outRxQ)).toMatchObject(groupContentEx(group));
|
|
736
|
+
|
|
737
|
+
const mapContentMsg = await reader.read();
|
|
738
|
+
|
|
739
|
+
expect(mapContentMsg.value).toEqual({
|
|
740
|
+
action: "content",
|
|
741
|
+
id: map.core.id,
|
|
742
|
+
header: map.core.header,
|
|
743
|
+
new: {},
|
|
744
|
+
} satisfies SyncMessage);
|
|
745
|
+
|
|
746
|
+
await inTx.abort();
|
|
747
|
+
|
|
748
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
749
|
+
|
|
750
|
+
expect(node.syncManager.peers["test"]).toBeUndefined();
|
|
751
|
+
*/
|
|
752
|
+
});
|
|
753
|
+
test("If we start loading a coValue before connecting to a peer that has it, it will load it once we connect", async () => {
|
|
754
|
+
const { node: node1 } = await createConnectedTestNode();
|
|
755
|
+
const group = node1.createGroup();
|
|
756
|
+
const map = group.createMap();
|
|
757
|
+
map.set("hello", "world", "trusting");
|
|
758
|
+
const node2 = createTestNode();
|
|
759
|
+
const mapOnNode2Promise = loadCoValueOrFail(node2, map.id);
|
|
760
|
+
expect(node2.coValuesStore.get(map.core.id).state.type).toEqual("unknown");
|
|
761
|
+
connectNodeToSyncServer(node2);
|
|
762
|
+
const mapOnNode2 = await mapOnNode2Promise;
|
|
763
|
+
expect(mapOnNode2.get("hello")).toEqual("world");
|
|
764
|
+
});
|
|
765
|
+
test("should keep the peer state when the peer closes", async () => {
|
|
766
|
+
const client = createTestNode();
|
|
767
|
+
const { nodeToServerPeer, serverToNodePeer } = connectNodeToSyncServer(client);
|
|
768
|
+
const group = jazzCloud.createGroup();
|
|
769
|
+
const map = group.createMap();
|
|
770
|
+
map.set("hello", "world", "trusting");
|
|
771
|
+
await client.loadCoValueCore(map.core.id);
|
|
772
|
+
const syncManager = client.syncManager;
|
|
773
|
+
const peerState = syncManager.peers[nodeToServerPeer.id];
|
|
774
|
+
// @ts-expect-error Simulating a peer closing, leveraging the direct connection between the client/server peers
|
|
775
|
+
await serverToNodePeer.outgoing.push("Disconnected");
|
|
776
|
+
await waitFor(() => peerState?.closed);
|
|
777
|
+
expect(syncManager.peers[nodeToServerPeer.id]).not.toBeUndefined();
|
|
778
|
+
});
|
|
779
|
+
test("should delete the peer state when the peer closes if deletePeerStateOnClose is true", async () => {
|
|
780
|
+
const client = createTestNode();
|
|
781
|
+
const { nodeToServerPeer, serverToNodePeer } = connectNodeToSyncServer(client);
|
|
782
|
+
nodeToServerPeer.deletePeerStateOnClose = true;
|
|
783
|
+
const group = jazzCloud.createGroup();
|
|
784
|
+
const map = group.createMap();
|
|
785
|
+
map.set("hello", "world", "trusting");
|
|
786
|
+
await client.loadCoValueCore(map.core.id);
|
|
787
|
+
const syncManager = client.syncManager;
|
|
788
|
+
const peerState = syncManager.peers[nodeToServerPeer.id];
|
|
789
|
+
// @ts-expect-error Simulating a peer closing, leveraging the direct connection between the client/server peers
|
|
790
|
+
await serverToNodePeer.outgoing.push("Disconnected");
|
|
791
|
+
await waitFor(() => peerState?.closed);
|
|
792
|
+
expect(syncManager.peers[nodeToServerPeer.id]).toBeUndefined();
|
|
793
|
+
});
|
|
794
|
+
describe("sync - extra tests", () => {
|
|
795
|
+
test("Node handles disconnection and reconnection of a peer gracefully", async () => {
|
|
796
|
+
// Create two nodes
|
|
797
|
+
const [admin1, session1] = randomAnonymousAccountAndSessionID();
|
|
798
|
+
const node1 = new LocalNode(admin1, session1, Crypto);
|
|
799
|
+
const [admin2, session2] = randomAnonymousAccountAndSessionID();
|
|
800
|
+
const node2 = new LocalNode(admin2, session2, Crypto);
|
|
801
|
+
// Create a group and a map on node1
|
|
802
|
+
const group = node1.createGroup();
|
|
803
|
+
group.addMember("everyone", "writer");
|
|
804
|
+
const map = group.createMap();
|
|
805
|
+
map.set("key1", "value1", "trusting");
|
|
806
|
+
// Connect the nodes
|
|
807
|
+
const [node1AsPeer, node2AsPeer] = connectedPeers("node1", "node2", {
|
|
808
|
+
peer1role: "server",
|
|
809
|
+
peer2role: "client",
|
|
810
|
+
});
|
|
811
|
+
node1.syncManager.addPeer(node2AsPeer);
|
|
812
|
+
node2.syncManager.addPeer(node1AsPeer);
|
|
813
|
+
// Wait for initial sync
|
|
814
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
815
|
+
// Verify that node2 has received the map
|
|
816
|
+
const mapOnNode2 = await node2.loadCoValueCore(map.core.id);
|
|
817
|
+
if (mapOnNode2 === "unavailable") {
|
|
818
|
+
throw new Error("Map is unavailable on node2");
|
|
819
|
+
}
|
|
820
|
+
expect(expectMap(mapOnNode2.getCurrentContent()).get("key1")).toEqual("value1");
|
|
821
|
+
// Simulate disconnection
|
|
822
|
+
node1.syncManager.gracefulShutdown();
|
|
823
|
+
node2.syncManager.gracefulShutdown();
|
|
824
|
+
// Make changes on node1 while disconnected
|
|
825
|
+
map.set("key2", "value2", "trusting");
|
|
826
|
+
// Simulate reconnection
|
|
827
|
+
const [newNode1AsPeer, newNode2AsPeer] = connectedPeers("node11", "node22", {
|
|
828
|
+
peer1role: "server",
|
|
829
|
+
peer2role: "client",
|
|
830
|
+
// trace: true,
|
|
831
|
+
});
|
|
832
|
+
node1.syncManager.addPeer(newNode2AsPeer);
|
|
833
|
+
node2.syncManager.addPeer(newNode1AsPeer);
|
|
834
|
+
// Wait for re-sync
|
|
835
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
836
|
+
// Verify that node2 has received the changes made during disconnection
|
|
837
|
+
const updatedMapOnNode2 = await node2.loadCoValueCore(map.core.id);
|
|
838
|
+
if (updatedMapOnNode2 === "unavailable") {
|
|
839
|
+
throw new Error("Updated map is unavailable on node2");
|
|
840
|
+
}
|
|
841
|
+
expect(expectMap(updatedMapOnNode2.getCurrentContent()).get("key2")).toEqual("value2");
|
|
842
|
+
// Make a new change on node2 to verify two-way sync
|
|
843
|
+
const mapOnNode2ForEdit = await node2.loadCoValueCore(map.core.id);
|
|
844
|
+
if (mapOnNode2ForEdit === "unavailable") {
|
|
845
|
+
throw new Error("Updated map is unavailable on node2");
|
|
846
|
+
}
|
|
847
|
+
const success = mapOnNode2ForEdit.makeTransaction([
|
|
848
|
+
{
|
|
849
|
+
op: "set",
|
|
850
|
+
key: "key3",
|
|
851
|
+
value: "value3",
|
|
852
|
+
},
|
|
853
|
+
], "trusting");
|
|
854
|
+
if (!success) {
|
|
855
|
+
throw new Error("Failed to make transaction");
|
|
856
|
+
}
|
|
857
|
+
// Wait for sync back to node1
|
|
858
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
859
|
+
const mapOnNode1 = await node1.loadCoValueCore(map.core.id);
|
|
860
|
+
if (mapOnNode1 === "unavailable") {
|
|
861
|
+
throw new Error("Updated map is unavailable on node1");
|
|
862
|
+
}
|
|
863
|
+
// Verify that node1 has received the change from node2
|
|
864
|
+
expect(expectMap(mapOnNode1.getCurrentContent()).get("key3")).toEqual("value3");
|
|
865
|
+
});
|
|
866
|
+
test("Concurrent modifications on multiple nodes are resolved correctly", async () => {
|
|
867
|
+
// Create three nodes
|
|
868
|
+
const [admin1, session1] = randomAnonymousAccountAndSessionID();
|
|
869
|
+
const node1 = new LocalNode(admin1, session1, Crypto);
|
|
870
|
+
const [admin2, session2] = randomAnonymousAccountAndSessionID();
|
|
871
|
+
const node2 = new LocalNode(admin2, session2, Crypto);
|
|
872
|
+
const [admin3, session3] = randomAnonymousAccountAndSessionID();
|
|
873
|
+
const node3 = new LocalNode(admin3, session3, Crypto);
|
|
874
|
+
// Create a group and a map on node1
|
|
875
|
+
const group = node1.createGroup();
|
|
876
|
+
group.addMember("everyone", "writer");
|
|
877
|
+
const map = group.createMap();
|
|
878
|
+
// Connect the nodes in a triangle topology
|
|
879
|
+
const [node1AsPeerFor2, node2AsPeerFor1] = connectedPeers("node1", "node2", {
|
|
880
|
+
peer1role: "server",
|
|
881
|
+
peer2role: "client",
|
|
882
|
+
// trace: true,
|
|
883
|
+
});
|
|
884
|
+
const [node2AsPeerFor3, node3AsPeerFor2] = connectedPeers("node2", "node3", {
|
|
885
|
+
peer1role: "server",
|
|
886
|
+
peer2role: "client",
|
|
887
|
+
// trace: true,
|
|
888
|
+
});
|
|
889
|
+
const [node3AsPeerFor1, node1AsPeerFor3] = connectedPeers("node3", "node1", {
|
|
890
|
+
peer1role: "server",
|
|
891
|
+
peer2role: "client",
|
|
892
|
+
// trace: true,
|
|
893
|
+
});
|
|
894
|
+
node1.syncManager.addPeer(node2AsPeerFor1);
|
|
895
|
+
node1.syncManager.addPeer(node3AsPeerFor1);
|
|
896
|
+
node2.syncManager.addPeer(node1AsPeerFor2);
|
|
897
|
+
node2.syncManager.addPeer(node3AsPeerFor2);
|
|
898
|
+
node3.syncManager.addPeer(node1AsPeerFor3);
|
|
899
|
+
node3.syncManager.addPeer(node2AsPeerFor3);
|
|
900
|
+
// Wait for initial sync
|
|
901
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
902
|
+
// Verify that all nodes have the map
|
|
903
|
+
const mapOnNode1 = await node1.loadCoValueCore(map.core.id);
|
|
904
|
+
const mapOnNode2 = await node2.loadCoValueCore(map.core.id);
|
|
905
|
+
const mapOnNode3 = await node3.loadCoValueCore(map.core.id);
|
|
906
|
+
if (mapOnNode1 === "unavailable" ||
|
|
907
|
+
mapOnNode2 === "unavailable" ||
|
|
908
|
+
mapOnNode3 === "unavailable") {
|
|
909
|
+
throw new Error("Map is unavailable on node2 or node3");
|
|
910
|
+
}
|
|
911
|
+
// Perform concurrent modifications
|
|
912
|
+
map.set("key1", "value1", "trusting");
|
|
913
|
+
new RawCoMap(mapOnNode2).set("key2", "value2", "trusting");
|
|
914
|
+
new RawCoMap(mapOnNode3).set("key3", "value3", "trusting");
|
|
915
|
+
// Wait for sync to complete
|
|
916
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
917
|
+
// Verify that all nodes have the same final state
|
|
918
|
+
const finalStateNode1 = expectMap(mapOnNode1.getCurrentContent());
|
|
919
|
+
const finalStateNode2 = expectMap(mapOnNode2.getCurrentContent());
|
|
920
|
+
const finalStateNode3 = expectMap(mapOnNode3.getCurrentContent());
|
|
921
|
+
const expectedState = {
|
|
922
|
+
key1: "value1",
|
|
923
|
+
key2: "value2",
|
|
924
|
+
key3: "value3",
|
|
925
|
+
};
|
|
926
|
+
expect(finalStateNode1.toJSON()).toEqual(expectedState);
|
|
927
|
+
expect(finalStateNode2.toJSON()).toEqual(expectedState);
|
|
928
|
+
expect(finalStateNode3.toJSON()).toEqual(expectedState);
|
|
929
|
+
});
|
|
930
|
+
test.skip("Large coValues are synced efficiently in chunks", async () => {
|
|
931
|
+
// Create two nodes
|
|
932
|
+
const [admin1, session1] = randomAnonymousAccountAndSessionID();
|
|
933
|
+
const node1 = new LocalNode(admin1, session1, Crypto);
|
|
934
|
+
const [admin2, session2] = randomAnonymousAccountAndSessionID();
|
|
935
|
+
const node2 = new LocalNode(admin2, session2, Crypto);
|
|
936
|
+
// Create a group and a large map on node1
|
|
937
|
+
const group = node1.createGroup();
|
|
938
|
+
group.addMember("everyone", "writer");
|
|
939
|
+
const largeMap = group.createMap();
|
|
940
|
+
// Generate a large amount of data (about 10MB)
|
|
941
|
+
const dataSize = 1 * 1024 * 1024;
|
|
942
|
+
const chunkSize = 1024; // 1KB chunks
|
|
943
|
+
const chunks = dataSize / chunkSize;
|
|
944
|
+
for (let i = 0; i < chunks; i++) {
|
|
945
|
+
const key = `key${i}`;
|
|
946
|
+
const value = Buffer.alloc(chunkSize, `value${i}`).toString("base64");
|
|
947
|
+
largeMap.set(key, value, "trusting");
|
|
948
|
+
}
|
|
949
|
+
// Connect the nodes
|
|
950
|
+
const [node1AsPeer, node2AsPeer] = connectedPeers("node1", "node2", {
|
|
951
|
+
peer1role: "server",
|
|
952
|
+
peer2role: "client",
|
|
953
|
+
});
|
|
954
|
+
node1.syncManager.addPeer(node2AsPeer);
|
|
955
|
+
node2.syncManager.addPeer(node1AsPeer);
|
|
956
|
+
await new Promise((resolve) => setTimeout(resolve, 4000));
|
|
957
|
+
// Measure sync time
|
|
958
|
+
const startSync = performance.now();
|
|
959
|
+
// Load the large map on node2
|
|
960
|
+
const largeMapOnNode2 = await node2.loadCoValueCore(largeMap.core.id);
|
|
961
|
+
if (largeMapOnNode2 === "unavailable") {
|
|
962
|
+
throw new Error("Large map is unavailable on node2");
|
|
963
|
+
}
|
|
964
|
+
const endSync = performance.now();
|
|
965
|
+
const syncTime = endSync - startSync;
|
|
966
|
+
// Verify that all data was synced correctly
|
|
967
|
+
const syncedMap = new RawCoMap(largeMapOnNode2);
|
|
968
|
+
expect(Object.keys(largeMapOnNode2.getCurrentContent().toJSON() || {}).length).toBe(chunks);
|
|
969
|
+
for (let i = 0; i < chunks; i++) {
|
|
970
|
+
const key = `key${i}`;
|
|
971
|
+
const expectedValue = Buffer.alloc(chunkSize, `value${i}`).toString("base64");
|
|
972
|
+
expect(syncedMap.get(key)).toBe(expectedValue);
|
|
973
|
+
}
|
|
974
|
+
// Check that sync time is reasonable (this threshold may need adjustment)
|
|
975
|
+
const reasonableSyncTime = 10; // 30 seconds
|
|
976
|
+
expect(syncTime).toBeLessThan(reasonableSyncTime);
|
|
977
|
+
// Check memory usage (this threshold may need adjustment)
|
|
978
|
+
const memoryUsage = process.memoryUsage().heapUsed / 1024 / 1024; // in MB
|
|
979
|
+
const reasonableMemoryUsage = 1; // 500 MB
|
|
980
|
+
expect(memoryUsage).toBeLessThan(reasonableMemoryUsage);
|
|
981
|
+
});
|
|
982
|
+
test("Node correctly handles and recovers from network partitions", async () => {
|
|
983
|
+
// Create three nodes
|
|
984
|
+
const [admin1, session1] = randomAnonymousAccountAndSessionID();
|
|
985
|
+
const node1 = new LocalNode(admin1, session1, Crypto);
|
|
986
|
+
const [admin2, session2] = randomAnonymousAccountAndSessionID();
|
|
987
|
+
const node2 = new LocalNode(admin2, session2, Crypto);
|
|
988
|
+
const [admin3, session3] = randomAnonymousAccountAndSessionID();
|
|
989
|
+
const node3 = new LocalNode(admin3, session3, Crypto);
|
|
990
|
+
// Create a group and a map on node1
|
|
991
|
+
const group = node1.createGroup();
|
|
992
|
+
group.addMember("everyone", "writer");
|
|
993
|
+
const map = group.createMap();
|
|
994
|
+
map.set("initial", "value", "trusting");
|
|
995
|
+
// Connect all nodes
|
|
996
|
+
const [node1AsPeerFor2, node2AsPeerFor1] = connectedPeers("node1", "node2", {
|
|
997
|
+
peer1role: "server",
|
|
998
|
+
peer2role: "client",
|
|
999
|
+
// trace: true,
|
|
1000
|
+
});
|
|
1001
|
+
const [node2AsPeerFor3, node3AsPeerFor2] = connectedPeers("node2", "node3", {
|
|
1002
|
+
peer1role: "server",
|
|
1003
|
+
peer2role: "client",
|
|
1004
|
+
// trace: true,
|
|
1005
|
+
});
|
|
1006
|
+
const [node3AsPeerFor1, node1AsPeerFor3] = connectedPeers("node3", "node1", {
|
|
1007
|
+
peer1role: "server",
|
|
1008
|
+
peer2role: "client",
|
|
1009
|
+
// trace: true,
|
|
1010
|
+
});
|
|
1011
|
+
node1.syncManager.addPeer(node2AsPeerFor1);
|
|
1012
|
+
node1.syncManager.addPeer(node3AsPeerFor1);
|
|
1013
|
+
node2.syncManager.addPeer(node1AsPeerFor2);
|
|
1014
|
+
node2.syncManager.addPeer(node3AsPeerFor2);
|
|
1015
|
+
node3.syncManager.addPeer(node1AsPeerFor3);
|
|
1016
|
+
node3.syncManager.addPeer(node2AsPeerFor3);
|
|
1017
|
+
// Wait for initial sync
|
|
1018
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1019
|
+
// Verify initial state
|
|
1020
|
+
const mapOnNode1Core = await node1.loadCoValueCore(map.core.id);
|
|
1021
|
+
const mapOnNode2Core = await node2.loadCoValueCore(map.core.id);
|
|
1022
|
+
const mapOnNode3Core = await node3.loadCoValueCore(map.core.id);
|
|
1023
|
+
if (mapOnNode1Core === "unavailable" ||
|
|
1024
|
+
mapOnNode2Core === "unavailable" ||
|
|
1025
|
+
mapOnNode3Core === "unavailable") {
|
|
1026
|
+
throw new Error("Map is unavailable on node2 or node3");
|
|
1027
|
+
}
|
|
1028
|
+
// const mapOnNode1 = new RawCoMap(mapOnNode1Core);
|
|
1029
|
+
const mapOnNode2 = new RawCoMap(mapOnNode2Core);
|
|
1030
|
+
const mapOnNode3 = new RawCoMap(mapOnNode3Core);
|
|
1031
|
+
expect(mapOnNode2.get("initial")).toBe("value");
|
|
1032
|
+
expect(mapOnNode3.get("initial")).toBe("value");
|
|
1033
|
+
// Simulate network partition: disconnect node3 from node1 and node2
|
|
1034
|
+
node1.syncManager.peers["node3"]?.gracefulShutdown();
|
|
1035
|
+
delete node1.syncManager.peers["node3"];
|
|
1036
|
+
node2.syncManager.peers["node3"]?.gracefulShutdown();
|
|
1037
|
+
delete node2.syncManager.peers["node3"];
|
|
1038
|
+
node3.syncManager.peers["node1"]?.gracefulShutdown();
|
|
1039
|
+
delete node3.syncManager.peers["node1"];
|
|
1040
|
+
node3.syncManager.peers["node2"]?.gracefulShutdown();
|
|
1041
|
+
delete node3.syncManager.peers["node2"];
|
|
1042
|
+
// Make changes on both sides of the partition
|
|
1043
|
+
map.set("node1", "partition", "trusting");
|
|
1044
|
+
mapOnNode2.set("node2", "partition", "trusting");
|
|
1045
|
+
mapOnNode3.set("node3", "partition", "trusting");
|
|
1046
|
+
// Wait for sync between node1 and node2
|
|
1047
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1048
|
+
// Verify that node1 and node2 are in sync, but node3 is not
|
|
1049
|
+
expect(expectMap(mapOnNode1Core.getCurrentContent()).get("node1")).toBe("partition");
|
|
1050
|
+
expect(expectMap(mapOnNode1Core.getCurrentContent()).get("node2")).toBe("partition");
|
|
1051
|
+
expect(expectMap(mapOnNode1Core.getCurrentContent()).toJSON()?.node3).toBe(undefined);
|
|
1052
|
+
expect(expectMap(mapOnNode2Core.getCurrentContent()).get("node1")).toBe("partition");
|
|
1053
|
+
expect(expectMap(mapOnNode2Core.getCurrentContent()).get("node2")).toBe("partition");
|
|
1054
|
+
expect(expectMap(mapOnNode2Core.getCurrentContent()).toJSON()?.node3).toBe(undefined);
|
|
1055
|
+
expect(expectMap(mapOnNode3Core.getCurrentContent()).toJSON()?.node1).toBe(undefined);
|
|
1056
|
+
expect(expectMap(mapOnNode3Core.getCurrentContent()).toJSON()?.node2).toBe(undefined);
|
|
1057
|
+
expect(expectMap(mapOnNode3Core.getCurrentContent()).toJSON()?.node3).toBe("partition");
|
|
1058
|
+
// Restore connectivity
|
|
1059
|
+
const [newNode3AsPeerFor1, newNode1AsPeerFor3] = connectedPeers("node3", "node1", {
|
|
1060
|
+
peer1role: "server",
|
|
1061
|
+
peer2role: "client",
|
|
1062
|
+
// trace: true,
|
|
1063
|
+
});
|
|
1064
|
+
const [newNode3AsPeerFor2, newNode2AsPeerFor3] = connectedPeers("node3", "node2", {
|
|
1065
|
+
peer1role: "server",
|
|
1066
|
+
peer2role: "client",
|
|
1067
|
+
// trace: true,
|
|
1068
|
+
});
|
|
1069
|
+
node1.syncManager.addPeer(newNode3AsPeerFor1);
|
|
1070
|
+
node2.syncManager.addPeer(newNode3AsPeerFor2);
|
|
1071
|
+
node3.syncManager.addPeer(newNode1AsPeerFor3);
|
|
1072
|
+
node3.syncManager.addPeer(newNode2AsPeerFor3);
|
|
1073
|
+
// Wait for re-sync
|
|
1074
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
1075
|
+
// Verify final state: all nodes should have all changes
|
|
1076
|
+
const finalStateNode1 = expectMap(mapOnNode1Core.getCurrentContent()).toJSON();
|
|
1077
|
+
const finalStateNode2 = expectMap(mapOnNode2Core.getCurrentContent()).toJSON();
|
|
1078
|
+
const finalStateNode3 = expectMap(mapOnNode3Core.getCurrentContent()).toJSON();
|
|
1079
|
+
const expectedFinalState = {
|
|
1080
|
+
initial: "value",
|
|
1081
|
+
node1: "partition",
|
|
1082
|
+
node2: "partition",
|
|
1083
|
+
node3: "partition",
|
|
1084
|
+
};
|
|
1085
|
+
expect(finalStateNode1).toEqual(expectedFinalState);
|
|
1086
|
+
expect(finalStateNode2).toEqual(expectedFinalState);
|
|
1087
|
+
expect(finalStateNode3).toEqual(expectedFinalState);
|
|
1088
|
+
});
|
|
1089
|
+
});
|
|
1090
|
+
test("a value created on one node can be loaded on anotehr node even if not directly connected", async () => {
|
|
1091
|
+
const userA = createTestNode();
|
|
1092
|
+
const userB = createTestNode();
|
|
1093
|
+
const serverA = createTestNode();
|
|
1094
|
+
const serverB = createTestNode();
|
|
1095
|
+
const core = createTestNode();
|
|
1096
|
+
connectTwoPeers(userA, serverA, "client", "server");
|
|
1097
|
+
connectTwoPeers(userB, serverB, "client", "server");
|
|
1098
|
+
connectTwoPeers(serverA, core, "client", "server");
|
|
1099
|
+
connectTwoPeers(serverB, core, "client", "server");
|
|
1100
|
+
const group = userA.createGroup();
|
|
1101
|
+
const map = group.createMap();
|
|
1102
|
+
map.set("key1", "value1", "trusting");
|
|
1103
|
+
await map.core.waitForSync();
|
|
1104
|
+
const mapOnUserB = await loadCoValueOrFail(userB, map.id);
|
|
1105
|
+
expect(mapOnUserB.get("key1")).toBe("value1");
|
|
1106
|
+
});
|
|
1107
|
+
describe("SyncManager - knownStates vs optimisticKnownStates", () => {
|
|
1108
|
+
test("knownStates and optimisticKnownStates are the same when the coValue is fully synced", async () => {
|
|
1109
|
+
const { node: client } = await createConnectedTestNode();
|
|
1110
|
+
// Create test data
|
|
1111
|
+
const group = client.createGroup();
|
|
1112
|
+
const mapOnClient = group.createMap();
|
|
1113
|
+
mapOnClient.set("key1", "value1", "trusting");
|
|
1114
|
+
await client.syncManager.actuallySyncCoValue(mapOnClient.core);
|
|
1115
|
+
// Wait for the full sync to complete
|
|
1116
|
+
await mapOnClient.core.waitForSync();
|
|
1117
|
+
const peerStateClient = client.syncManager.getPeers()[0];
|
|
1118
|
+
const peerStateJazzCloud = jazzCloud.syncManager.getPeers()[0];
|
|
1119
|
+
// The optimisticKnownStates should be the same as the knownStates after the full sync is complete
|
|
1120
|
+
expect(peerStateClient.optimisticKnownStates.get(mapOnClient.core.id)).toEqual(peerStateClient.knownStates.get(mapOnClient.core.id));
|
|
1121
|
+
// On the other node the knownStates should be updated correctly based on the messages we received
|
|
1122
|
+
expect(peerStateJazzCloud.optimisticKnownStates.get(mapOnClient.core.id)).toEqual(peerStateJazzCloud.knownStates.get(mapOnClient.core.id));
|
|
1123
|
+
});
|
|
1124
|
+
test("optimisticKnownStates is updated as new transactions are sent, while knownStates only when the updates are acknowledged", async () => {
|
|
1125
|
+
const { node: client, nodeToServerPeer } = await createConnectedTestNode();
|
|
1126
|
+
// Create test data and sync the first change
|
|
1127
|
+
// We want that both the nodes know about the coValue so we can test
|
|
1128
|
+
// the content acknowledgement flow.
|
|
1129
|
+
const group = client.createGroup();
|
|
1130
|
+
const map = group.createMap();
|
|
1131
|
+
map.set("key1", "value1", "trusting");
|
|
1132
|
+
await client.syncManager.actuallySyncCoValue(map.core);
|
|
1133
|
+
await map.core.waitForSync();
|
|
1134
|
+
// Block the content messages
|
|
1135
|
+
// The main difference between optimisticKnownStates and knownStates is that
|
|
1136
|
+
// optimisticKnownStates is updated when the content messages are sent,
|
|
1137
|
+
// while knownStates is only updated when we receive the "known" messages
|
|
1138
|
+
// that are acknowledging the receipt of the content messages
|
|
1139
|
+
const outgoing = blockMessageTypeOnOutgoingPeer(nodeToServerPeer, "content");
|
|
1140
|
+
map.set("key2", "value2", "trusting");
|
|
1141
|
+
await client.syncManager.actuallySyncCoValue(map.core);
|
|
1142
|
+
const peerState = client.syncManager.peers[nodeToServerPeer.id];
|
|
1143
|
+
expect(peerState.optimisticKnownStates.get(map.core.id)).not.toEqual(peerState.knownStates.get(map.core.id));
|
|
1144
|
+
// Restore the implementation of push and send the blocked messages
|
|
1145
|
+
// After this the full sync can be completed and the other node will
|
|
1146
|
+
// respond with a "known" message acknowledging the receipt of the content messages
|
|
1147
|
+
outgoing.unblock();
|
|
1148
|
+
await outgoing.sendBlockedMessages();
|
|
1149
|
+
await map.core.waitForSync();
|
|
1150
|
+
expect(peerState.optimisticKnownStates.get(map.core.id)).toEqual(peerState.knownStates.get(map.core.id));
|
|
1151
|
+
});
|
|
1152
|
+
});
|
|
1153
|
+
describe("SyncManager.addPeer", () => {
|
|
1154
|
+
test("new peer gets a copy of previous peer's knownStates when replacing it", async () => {
|
|
1155
|
+
const { node: client } = await createConnectedTestNode();
|
|
1156
|
+
// Create test data
|
|
1157
|
+
const group = client.createGroup();
|
|
1158
|
+
const map = group.createMap();
|
|
1159
|
+
map.set("key1", "value1", "trusting");
|
|
1160
|
+
await client.syncManager.actuallySyncCoValue(map.core);
|
|
1161
|
+
// Wait for initial sync
|
|
1162
|
+
await map.core.waitForSync();
|
|
1163
|
+
const firstPeerState = client.syncManager.getPeers()[0];
|
|
1164
|
+
// Store the initial known states
|
|
1165
|
+
const initialKnownStates = firstPeerState.knownStates;
|
|
1166
|
+
// Create new connection with same ID
|
|
1167
|
+
const [secondPeer] = connectedPeers(firstPeerState.id, "unusedPeer", {
|
|
1168
|
+
peer1role: "server",
|
|
1169
|
+
peer2role: "client",
|
|
1170
|
+
});
|
|
1171
|
+
// Add new peer with same ID
|
|
1172
|
+
client.syncManager.addPeer(secondPeer);
|
|
1173
|
+
const newPeerState = client.syncManager.getPeers()[0];
|
|
1174
|
+
// Verify that the new peer has a copy of the previous known states
|
|
1175
|
+
const newPeerKnownStates = newPeerState.knownStates;
|
|
1176
|
+
expect(newPeerKnownStates).not.toBe(initialKnownStates); // Should be a different instance
|
|
1177
|
+
expect(newPeerKnownStates.get(map.core.id)).toEqual(initialKnownStates.get(map.core.id));
|
|
1178
|
+
});
|
|
1179
|
+
test("new peer with new ID starts with empty knownStates", async () => {
|
|
1180
|
+
const { node: client } = await createConnectedTestNode();
|
|
1181
|
+
// Create test data
|
|
1182
|
+
const group = client.createGroup();
|
|
1183
|
+
const map = group.createMap();
|
|
1184
|
+
map.set("key1", "value1", "trusting");
|
|
1185
|
+
await client.syncManager.actuallySyncCoValue(map.core);
|
|
1186
|
+
// Wait for initial sync
|
|
1187
|
+
await map.core.waitForSync();
|
|
1188
|
+
// Connect second peer with different ID
|
|
1189
|
+
const [brandNewPeer] = connectedPeers("brandNewPeer", "unusedPeer", {
|
|
1190
|
+
peer1role: "client",
|
|
1191
|
+
peer2role: "server",
|
|
1192
|
+
});
|
|
1193
|
+
// Add new peer with different ID
|
|
1194
|
+
client.syncManager.addPeer(brandNewPeer);
|
|
1195
|
+
// Verify that the new peer starts with empty known states
|
|
1196
|
+
const newPeerKnownStates = client.syncManager.peers["brandNewPeer"].knownStates;
|
|
1197
|
+
expect(newPeerKnownStates.get(map.core.id)).toBe(undefined);
|
|
1198
|
+
});
|
|
1199
|
+
test("when adding a peer with the same ID as a previous peer, the previous peer is closed", async () => {
|
|
1200
|
+
const { node: client } = await createConnectedTestNode();
|
|
1201
|
+
// Store reference to first peer
|
|
1202
|
+
const firstPeer = client.syncManager.getPeers()[0];
|
|
1203
|
+
const closeSpy = vi.spyOn(firstPeer, "gracefulShutdown");
|
|
1204
|
+
// Create and add replacement peer
|
|
1205
|
+
const [secondPeer] = connectedPeers(firstPeer.id, "unusedPeer", {
|
|
1206
|
+
peer1role: "server",
|
|
1207
|
+
peer2role: "client",
|
|
1208
|
+
});
|
|
1209
|
+
client.syncManager.addPeer(secondPeer);
|
|
1210
|
+
// Verify thet the first peer had ben closed correctly
|
|
1211
|
+
expect(closeSpy).toHaveBeenCalled();
|
|
1212
|
+
expect(firstPeer.closed).toBe(true);
|
|
1213
|
+
});
|
|
1214
|
+
test("when adding a peer with the same ID as a previous peer and the previous peer is closed, do not attempt to close it again", async () => {
|
|
1215
|
+
const { node: client } = await createConnectedTestNode();
|
|
1216
|
+
// Store reference to first peer
|
|
1217
|
+
const firstPeer = client.syncManager.getPeers()[0];
|
|
1218
|
+
firstPeer.gracefulShutdown();
|
|
1219
|
+
const closeSpy = vi.spyOn(firstPeer, "gracefulShutdown");
|
|
1220
|
+
// Create and add replacement peer
|
|
1221
|
+
const [secondPeer] = connectedPeers(firstPeer.id, "unusedPeer", {
|
|
1222
|
+
peer1role: "server",
|
|
1223
|
+
peer2role: "client",
|
|
1224
|
+
});
|
|
1225
|
+
client.syncManager.addPeer(secondPeer);
|
|
1226
|
+
// Verify thet the first peer had not been closed again
|
|
1227
|
+
expect(closeSpy).not.toHaveBeenCalled();
|
|
1228
|
+
expect(firstPeer.closed).toBe(true);
|
|
1229
|
+
});
|
|
1230
|
+
test("when adding a server peer the local coValues should be sent to it", async () => {
|
|
1231
|
+
const { node: client, addServerPeer } = await createConnectedTestNode({
|
|
1232
|
+
connected: false,
|
|
1233
|
+
});
|
|
1234
|
+
const group = client.createGroup();
|
|
1235
|
+
const map = group.createMap();
|
|
1236
|
+
map.set("key1", "value1", "trusting");
|
|
1237
|
+
addServerPeer();
|
|
1238
|
+
await map.core.waitForSync();
|
|
1239
|
+
expect(jazzCloud.coValuesStore.get(map.id).state.type).toBe("available");
|
|
1240
|
+
});
|
|
1241
|
+
});
|
|
1242
|
+
describe("loadCoValueCore with retry", () => {
|
|
1243
|
+
test("should load the value if available on the server", async () => {
|
|
1244
|
+
const { node: client } = await createConnectedTestNode();
|
|
1245
|
+
const { node: anotherClient } = await createConnectedTestNode();
|
|
1246
|
+
const group = anotherClient.createGroup();
|
|
1247
|
+
const map = group.createMap();
|
|
1248
|
+
map.set("key1", "value1", "trusting");
|
|
1249
|
+
const promise = client.loadCoValueCore(map.id);
|
|
1250
|
+
await expect(promise).resolves.not.toBe("unavailable");
|
|
1251
|
+
});
|
|
1252
|
+
test("should handle correctly two subsequent loads", async () => {
|
|
1253
|
+
const { node: client } = await createConnectedTestNode();
|
|
1254
|
+
const { node: anotherClient } = await createConnectedTestNode();
|
|
1255
|
+
const group = anotherClient.createGroup();
|
|
1256
|
+
const map = group.createMap();
|
|
1257
|
+
map.set("key1", "value1", "trusting");
|
|
1258
|
+
const promise1 = client.loadCoValueCore(map.id);
|
|
1259
|
+
const promise2 = client.loadCoValueCore(map.id);
|
|
1260
|
+
await expect(promise1).resolves.not.toBe("unavailable");
|
|
1261
|
+
await expect(promise2).resolves.not.toBe("unavailable");
|
|
1262
|
+
});
|
|
1263
|
+
});
|
|
1264
|
+
describe("waitForSyncWithPeer", () => {
|
|
1265
|
+
test("should resolve when the coValue is fully uploaded into the peer", async () => {
|
|
1266
|
+
const { node: client } = await createConnectedTestNode();
|
|
1267
|
+
// Create test data
|
|
1268
|
+
const group = client.createGroup();
|
|
1269
|
+
const map = group.createMap();
|
|
1270
|
+
map.set("key1", "value1", "trusting");
|
|
1271
|
+
await client.syncManager.actuallySyncCoValue(map.core);
|
|
1272
|
+
const peer = client.syncManager.getPeers()[0];
|
|
1273
|
+
if (!peer) {
|
|
1274
|
+
throw new Error("No peer found");
|
|
1275
|
+
}
|
|
1276
|
+
await expect(client.syncManager.waitForSyncWithPeer(peer.id, map.core.id, 100)).resolves.toBe(true);
|
|
1277
|
+
});
|
|
1278
|
+
test("should not resolve when the coValue is not synced", async () => {
|
|
1279
|
+
const { node: client } = await createConnectedTestNode();
|
|
1280
|
+
const peer = client.syncManager.getPeers()[0];
|
|
1281
|
+
if (!peer) {
|
|
1282
|
+
throw new Error("No peer found");
|
|
1283
|
+
}
|
|
1284
|
+
// Create test data
|
|
1285
|
+
const group = client.createGroup();
|
|
1286
|
+
const map = group.createMap();
|
|
1287
|
+
map.set("key1", "value1", "trusting");
|
|
1288
|
+
vi.spyOn(peer, "pushOutgoingMessage").mockImplementation(async () => {
|
|
1289
|
+
return Promise.resolve();
|
|
1290
|
+
});
|
|
1291
|
+
await client.syncManager.actuallySyncCoValue(map.core);
|
|
1292
|
+
await expect(client.syncManager.waitForSyncWithPeer(peer.id, map.core.id, 100)).rejects.toThrow("Timeout");
|
|
1293
|
+
});
|
|
1294
|
+
});
|
|
1295
|
+
test("Should not crash when syncing an unknown coValue type", async () => {
|
|
1296
|
+
const { node: client } = await createConnectedTestNode();
|
|
1297
|
+
const coValue = client.createCoValue({
|
|
1298
|
+
type: "ooops",
|
|
1299
|
+
ruleset: { type: "unsafeAllowAll" },
|
|
1300
|
+
meta: null,
|
|
1301
|
+
...Crypto.createdNowUnique(),
|
|
1302
|
+
});
|
|
1303
|
+
await coValue.waitForSync();
|
|
1304
|
+
const { node: anotherClient } = await createConnectedTestNode();
|
|
1305
|
+
const coValueOnTheOtherNode = await loadCoValueOrFail(anotherClient, coValue.getCurrentContent().id);
|
|
1306
|
+
expect(coValueOnTheOtherNode.id).toBe(coValue.id);
|
|
1307
|
+
});
|
|
1308
|
+
describe("metrics", () => {
|
|
1309
|
+
afterEach(() => {
|
|
1310
|
+
tearDownTestMetricReader();
|
|
1311
|
+
});
|
|
1312
|
+
test("should correctly track the number of connected peers", async () => {
|
|
1313
|
+
const metricReader = createTestMetricReader();
|
|
1314
|
+
const [admin, session] = randomAnonymousAccountAndSessionID();
|
|
1315
|
+
const node = new LocalNode(admin, session, Crypto);
|
|
1316
|
+
let connectedPeers = await metricReader.getMetricValue("jazz.peers", {
|
|
1317
|
+
role: "peer",
|
|
1318
|
+
});
|
|
1319
|
+
expect(connectedPeers).toBeUndefined();
|
|
1320
|
+
let connectedServerPeers = await metricReader.getMetricValue("jazz.peers", {
|
|
1321
|
+
role: "server",
|
|
1322
|
+
});
|
|
1323
|
+
expect(connectedServerPeers).toBeUndefined();
|
|
1324
|
+
// Add a first peer
|
|
1325
|
+
const [inPeer1, outPeer1] = newQueuePair();
|
|
1326
|
+
node.syncManager.addPeer({
|
|
1327
|
+
id: "peer-1",
|
|
1328
|
+
incoming: inPeer1,
|
|
1329
|
+
outgoing: outPeer1,
|
|
1330
|
+
role: "peer",
|
|
1331
|
+
crashOnClose: false,
|
|
1332
|
+
});
|
|
1333
|
+
connectedPeers = await metricReader.getMetricValue("jazz.peers", {
|
|
1334
|
+
role: "peer",
|
|
1335
|
+
});
|
|
1336
|
+
expect(connectedPeers).toBe(1);
|
|
1337
|
+
// Add another peer
|
|
1338
|
+
const [inPeer2, outPeer2] = newQueuePair();
|
|
1339
|
+
node.syncManager.addPeer({
|
|
1340
|
+
id: "peer-2",
|
|
1341
|
+
incoming: inPeer2,
|
|
1342
|
+
outgoing: outPeer2,
|
|
1343
|
+
role: "peer",
|
|
1344
|
+
crashOnClose: false,
|
|
1345
|
+
});
|
|
1346
|
+
connectedPeers = await metricReader.getMetricValue("jazz.peers", {
|
|
1347
|
+
role: "peer",
|
|
1348
|
+
});
|
|
1349
|
+
expect(connectedPeers).toBe(2);
|
|
1350
|
+
// Add a server peer
|
|
1351
|
+
const [inServer1, outServer1] = newQueuePair();
|
|
1352
|
+
node.syncManager.addPeer({
|
|
1353
|
+
id: "server-1",
|
|
1354
|
+
incoming: inServer1,
|
|
1355
|
+
outgoing: outServer1,
|
|
1356
|
+
role: "server",
|
|
1357
|
+
crashOnClose: false,
|
|
1358
|
+
});
|
|
1359
|
+
connectedServerPeers = await metricReader.getMetricValue("jazz.peers", {
|
|
1360
|
+
role: "server",
|
|
1361
|
+
});
|
|
1362
|
+
expect(connectedServerPeers).toBe(1);
|
|
1363
|
+
connectedPeers = await metricReader.getMetricValue("jazz.peers", {
|
|
1364
|
+
role: "peer",
|
|
1365
|
+
});
|
|
1366
|
+
expect(connectedPeers).toBe(2);
|
|
1367
|
+
// @ts-expect-error Simulating peer-1 closing
|
|
1368
|
+
await outPeer1.push("Disconnected");
|
|
1369
|
+
await waitFor(() => node.syncManager.peers["peer-1"]?.closed);
|
|
1370
|
+
connectedPeers = await metricReader.getMetricValue("jazz.peers", {
|
|
1371
|
+
role: "peer",
|
|
1372
|
+
});
|
|
1373
|
+
expect(connectedPeers).toBe(1);
|
|
1374
|
+
// @ts-expect-error Simulating server-1 closing
|
|
1375
|
+
await outServer1.push("Disconnected");
|
|
1376
|
+
await waitFor(() => node.syncManager.peers["server-1"]?.closed);
|
|
1377
|
+
connectedServerPeers = await metricReader.getMetricValue("jazz.peers", {
|
|
1378
|
+
role: "server",
|
|
1379
|
+
});
|
|
1380
|
+
expect(connectedServerPeers).toBe(0);
|
|
1381
|
+
});
|
|
1382
|
+
});
|
|
1383
|
+
describe("sync protocol", () => {
|
|
1384
|
+
test("should have the correct messages exchanged between client and server", async () => {
|
|
1385
|
+
// Creating the account from agent to simplify the messages exchange
|
|
1386
|
+
const { node: client, messages } = await createConnectedTestAgentNode();
|
|
1387
|
+
const group = client.createGroup();
|
|
1388
|
+
const map = group.createMap();
|
|
1389
|
+
map.set("hello", "world", "trusting");
|
|
1390
|
+
const mapOnJazzCloud = await loadCoValueOrFail(jazzCloud, map.id);
|
|
1391
|
+
expect(mapOnJazzCloud.get("hello")).toEqual("world");
|
|
1392
|
+
expect(messages).toEqual([
|
|
1393
|
+
{
|
|
1394
|
+
from: "client",
|
|
1395
|
+
msg: {
|
|
1396
|
+
action: "load",
|
|
1397
|
+
header: true,
|
|
1398
|
+
id: group.id,
|
|
1399
|
+
sessions: {
|
|
1400
|
+
[client.currentSessionID]: 3,
|
|
1401
|
+
},
|
|
1402
|
+
},
|
|
1403
|
+
},
|
|
1404
|
+
{
|
|
1405
|
+
from: "server",
|
|
1406
|
+
msg: {
|
|
1407
|
+
action: "load",
|
|
1408
|
+
header: false,
|
|
1409
|
+
id: group.id,
|
|
1410
|
+
sessions: {},
|
|
1411
|
+
},
|
|
1412
|
+
},
|
|
1413
|
+
{
|
|
1414
|
+
from: "client",
|
|
1415
|
+
msg: {
|
|
1416
|
+
action: "load",
|
|
1417
|
+
header: true,
|
|
1418
|
+
id: map.id,
|
|
1419
|
+
sessions: {
|
|
1420
|
+
[client.currentSessionID]: 1,
|
|
1421
|
+
},
|
|
1422
|
+
},
|
|
1423
|
+
},
|
|
1424
|
+
{
|
|
1425
|
+
from: "server",
|
|
1426
|
+
msg: {
|
|
1427
|
+
action: "load",
|
|
1428
|
+
header: false,
|
|
1429
|
+
id: map.id,
|
|
1430
|
+
sessions: {},
|
|
1431
|
+
},
|
|
1432
|
+
},
|
|
1433
|
+
{
|
|
1434
|
+
from: "client",
|
|
1435
|
+
msg: {
|
|
1436
|
+
action: "content",
|
|
1437
|
+
header: {
|
|
1438
|
+
createdAt: expect.any(String),
|
|
1439
|
+
meta: null,
|
|
1440
|
+
ruleset: {
|
|
1441
|
+
initialAdmin: client.account.id,
|
|
1442
|
+
type: "group",
|
|
1443
|
+
},
|
|
1444
|
+
type: "comap",
|
|
1445
|
+
uniqueness: expect.any(String),
|
|
1446
|
+
},
|
|
1447
|
+
id: group.id,
|
|
1448
|
+
new: {
|
|
1449
|
+
[client.currentSessionID]: {
|
|
1450
|
+
after: 0,
|
|
1451
|
+
lastSignature: expect.any(String),
|
|
1452
|
+
newTransactions: expect.any(Array),
|
|
1453
|
+
},
|
|
1454
|
+
},
|
|
1455
|
+
priority: 0,
|
|
1456
|
+
},
|
|
1457
|
+
},
|
|
1458
|
+
{
|
|
1459
|
+
from: "client",
|
|
1460
|
+
msg: {
|
|
1461
|
+
action: "content",
|
|
1462
|
+
header: {
|
|
1463
|
+
createdAt: expect.any(String),
|
|
1464
|
+
meta: null,
|
|
1465
|
+
ruleset: {
|
|
1466
|
+
group: group.id,
|
|
1467
|
+
type: "ownedByGroup",
|
|
1468
|
+
},
|
|
1469
|
+
type: "comap",
|
|
1470
|
+
uniqueness: expect.any(String),
|
|
1471
|
+
},
|
|
1472
|
+
id: map.id,
|
|
1473
|
+
new: {
|
|
1474
|
+
[client.currentSessionID]: {
|
|
1475
|
+
after: 0,
|
|
1476
|
+
lastSignature: expect.any(String),
|
|
1477
|
+
newTransactions: expect.any(Array),
|
|
1478
|
+
},
|
|
1479
|
+
},
|
|
1480
|
+
priority: 3,
|
|
1481
|
+
},
|
|
1482
|
+
},
|
|
1483
|
+
{
|
|
1484
|
+
from: "server",
|
|
1485
|
+
msg: {
|
|
1486
|
+
action: "known",
|
|
1487
|
+
header: true,
|
|
1488
|
+
id: group.id,
|
|
1489
|
+
sessions: {
|
|
1490
|
+
[client.currentSessionID]: 3,
|
|
1491
|
+
},
|
|
1492
|
+
},
|
|
1493
|
+
},
|
|
1494
|
+
{
|
|
1495
|
+
// TODO: This is a redundant message, we should remove it
|
|
1496
|
+
from: "client",
|
|
1497
|
+
msg: {
|
|
1498
|
+
action: "content",
|
|
1499
|
+
header: {
|
|
1500
|
+
createdAt: expect.any(String),
|
|
1501
|
+
meta: null,
|
|
1502
|
+
ruleset: {
|
|
1503
|
+
group: group.id,
|
|
1504
|
+
type: "ownedByGroup",
|
|
1505
|
+
},
|
|
1506
|
+
type: "comap",
|
|
1507
|
+
uniqueness: expect.any(String),
|
|
1508
|
+
},
|
|
1509
|
+
id: map.id,
|
|
1510
|
+
new: {
|
|
1511
|
+
[client.currentSessionID]: {
|
|
1512
|
+
after: 0,
|
|
1513
|
+
lastSignature: expect.any(String),
|
|
1514
|
+
newTransactions: expect.any(Array),
|
|
1515
|
+
},
|
|
1516
|
+
},
|
|
1517
|
+
priority: 3,
|
|
1518
|
+
},
|
|
1519
|
+
},
|
|
1520
|
+
{
|
|
1521
|
+
// TODO: This is a redundant message, we should remove it
|
|
1522
|
+
from: "server",
|
|
1523
|
+
msg: {
|
|
1524
|
+
action: "known",
|
|
1525
|
+
asDependencyOf: undefined,
|
|
1526
|
+
header: true,
|
|
1527
|
+
id: group.id,
|
|
1528
|
+
sessions: {
|
|
1529
|
+
[client.currentSessionID]: 3,
|
|
1530
|
+
},
|
|
1531
|
+
},
|
|
1532
|
+
},
|
|
1533
|
+
]);
|
|
1534
|
+
});
|
|
1535
|
+
});
|
|
1536
|
+
function groupContentEx(group) {
|
|
1537
|
+
return {
|
|
1538
|
+
action: "content",
|
|
1539
|
+
id: group.core.id,
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
function groupStateEx(group) {
|
|
1543
|
+
return {
|
|
1544
|
+
action: "known",
|
|
1545
|
+
id: group.core.id,
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
//# sourceMappingURL=sync.test.js.map
|