cojson 0.8.12 → 0.8.17

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