cojson 0.8.11 → 0.8.16

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