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