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