cojson 0.19.21 → 0.20.0

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 (254) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +67 -0
  3. package/dist/CojsonMessageChannel/CojsonMessageChannel.d.ts +42 -0
  4. package/dist/CojsonMessageChannel/CojsonMessageChannel.d.ts.map +1 -0
  5. package/dist/CojsonMessageChannel/CojsonMessageChannel.js +261 -0
  6. package/dist/CojsonMessageChannel/CojsonMessageChannel.js.map +1 -0
  7. package/dist/CojsonMessageChannel/MessagePortOutgoingChannel.d.ts +18 -0
  8. package/dist/CojsonMessageChannel/MessagePortOutgoingChannel.d.ts.map +1 -0
  9. package/dist/CojsonMessageChannel/MessagePortOutgoingChannel.js +37 -0
  10. package/dist/CojsonMessageChannel/MessagePortOutgoingChannel.js.map +1 -0
  11. package/dist/CojsonMessageChannel/index.d.ts +3 -0
  12. package/dist/CojsonMessageChannel/index.d.ts.map +1 -0
  13. package/dist/CojsonMessageChannel/index.js +2 -0
  14. package/dist/CojsonMessageChannel/index.js.map +1 -0
  15. package/dist/CojsonMessageChannel/types.d.ts +149 -0
  16. package/dist/CojsonMessageChannel/types.d.ts.map +1 -0
  17. package/dist/CojsonMessageChannel/types.js +36 -0
  18. package/dist/CojsonMessageChannel/types.js.map +1 -0
  19. package/dist/GarbageCollector.d.ts +4 -2
  20. package/dist/GarbageCollector.d.ts.map +1 -1
  21. package/dist/GarbageCollector.js +5 -3
  22. package/dist/GarbageCollector.js.map +1 -1
  23. package/dist/SyncStateManager.d.ts +3 -3
  24. package/dist/SyncStateManager.d.ts.map +1 -1
  25. package/dist/SyncStateManager.js +4 -4
  26. package/dist/SyncStateManager.js.map +1 -1
  27. package/dist/coValueContentMessage.d.ts +0 -2
  28. package/dist/coValueContentMessage.d.ts.map +1 -1
  29. package/dist/coValueContentMessage.js +0 -8
  30. package/dist/coValueContentMessage.js.map +1 -1
  31. package/dist/coValueCore/SessionMap.d.ts +4 -2
  32. package/dist/coValueCore/SessionMap.d.ts.map +1 -1
  33. package/dist/coValueCore/SessionMap.js +30 -0
  34. package/dist/coValueCore/SessionMap.js.map +1 -1
  35. package/dist/coValueCore/coValueCore.d.ts +86 -4
  36. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  37. package/dist/coValueCore/coValueCore.js +318 -17
  38. package/dist/coValueCore/coValueCore.js.map +1 -1
  39. package/dist/coValueCore/verifiedState.d.ts +6 -1
  40. package/dist/coValueCore/verifiedState.d.ts.map +1 -1
  41. package/dist/coValueCore/verifiedState.js +9 -0
  42. package/dist/coValueCore/verifiedState.js.map +1 -1
  43. package/dist/coValues/coList.d.ts +3 -2
  44. package/dist/coValues/coList.d.ts.map +1 -1
  45. package/dist/coValues/coList.js.map +1 -1
  46. package/dist/coValues/group.d.ts.map +1 -1
  47. package/dist/coValues/group.js +3 -6
  48. package/dist/coValues/group.js.map +1 -1
  49. package/dist/config.d.ts +0 -6
  50. package/dist/config.d.ts.map +1 -1
  51. package/dist/config.js +0 -8
  52. package/dist/config.js.map +1 -1
  53. package/dist/crypto/NapiCrypto.d.ts +1 -2
  54. package/dist/crypto/NapiCrypto.d.ts.map +1 -1
  55. package/dist/crypto/NapiCrypto.js +19 -4
  56. package/dist/crypto/NapiCrypto.js.map +1 -1
  57. package/dist/crypto/RNCrypto.d.ts.map +1 -1
  58. package/dist/crypto/RNCrypto.js +19 -4
  59. package/dist/crypto/RNCrypto.js.map +1 -1
  60. package/dist/crypto/WasmCrypto.d.ts +11 -4
  61. package/dist/crypto/WasmCrypto.d.ts.map +1 -1
  62. package/dist/crypto/WasmCrypto.js +52 -10
  63. package/dist/crypto/WasmCrypto.js.map +1 -1
  64. package/dist/crypto/WasmCryptoEdge.d.ts +1 -0
  65. package/dist/crypto/WasmCryptoEdge.d.ts.map +1 -1
  66. package/dist/crypto/WasmCryptoEdge.js +4 -1
  67. package/dist/crypto/WasmCryptoEdge.js.map +1 -1
  68. package/dist/crypto/crypto.d.ts +3 -3
  69. package/dist/crypto/crypto.d.ts.map +1 -1
  70. package/dist/crypto/crypto.js +6 -1
  71. package/dist/crypto/crypto.js.map +1 -1
  72. package/dist/exports.d.ts +3 -2
  73. package/dist/exports.d.ts.map +1 -1
  74. package/dist/exports.js +3 -1
  75. package/dist/exports.js.map +1 -1
  76. package/dist/ids.d.ts +4 -1
  77. package/dist/ids.d.ts.map +1 -1
  78. package/dist/ids.js +4 -0
  79. package/dist/ids.js.map +1 -1
  80. package/dist/knownState.d.ts +2 -0
  81. package/dist/knownState.d.ts.map +1 -1
  82. package/dist/localNode.d.ts +13 -3
  83. package/dist/localNode.d.ts.map +1 -1
  84. package/dist/localNode.js +17 -2
  85. package/dist/localNode.js.map +1 -1
  86. package/dist/platformUtils.d.ts +3 -0
  87. package/dist/platformUtils.d.ts.map +1 -0
  88. package/dist/platformUtils.js +24 -0
  89. package/dist/platformUtils.js.map +1 -0
  90. package/dist/storage/DeletedCoValuesEraserScheduler.d.ts +30 -0
  91. package/dist/storage/DeletedCoValuesEraserScheduler.d.ts.map +1 -0
  92. package/dist/storage/DeletedCoValuesEraserScheduler.js +84 -0
  93. package/dist/storage/DeletedCoValuesEraserScheduler.js.map +1 -0
  94. package/dist/storage/sqlite/client.d.ts +3 -0
  95. package/dist/storage/sqlite/client.d.ts.map +1 -1
  96. package/dist/storage/sqlite/client.js +44 -0
  97. package/dist/storage/sqlite/client.js.map +1 -1
  98. package/dist/storage/sqlite/sqliteMigrations.d.ts.map +1 -1
  99. package/dist/storage/sqlite/sqliteMigrations.js +7 -0
  100. package/dist/storage/sqlite/sqliteMigrations.js.map +1 -1
  101. package/dist/storage/sqliteAsync/client.d.ts +3 -0
  102. package/dist/storage/sqliteAsync/client.d.ts.map +1 -1
  103. package/dist/storage/sqliteAsync/client.js +42 -0
  104. package/dist/storage/sqliteAsync/client.js.map +1 -1
  105. package/dist/storage/storageAsync.d.ts +15 -3
  106. package/dist/storage/storageAsync.d.ts.map +1 -1
  107. package/dist/storage/storageAsync.js +60 -3
  108. package/dist/storage/storageAsync.js.map +1 -1
  109. package/dist/storage/storageSync.d.ts +14 -3
  110. package/dist/storage/storageSync.d.ts.map +1 -1
  111. package/dist/storage/storageSync.js +54 -3
  112. package/dist/storage/storageSync.js.map +1 -1
  113. package/dist/storage/types.d.ts +64 -0
  114. package/dist/storage/types.d.ts.map +1 -1
  115. package/dist/storage/types.js +12 -1
  116. package/dist/storage/types.js.map +1 -1
  117. package/dist/sync.d.ts +6 -0
  118. package/dist/sync.d.ts.map +1 -1
  119. package/dist/sync.js +69 -15
  120. package/dist/sync.js.map +1 -1
  121. package/dist/tests/CojsonMessageChannel.test.d.ts +2 -0
  122. package/dist/tests/CojsonMessageChannel.test.d.ts.map +1 -0
  123. package/dist/tests/CojsonMessageChannel.test.js +236 -0
  124. package/dist/tests/CojsonMessageChannel.test.js.map +1 -0
  125. package/dist/tests/DeletedCoValuesEraserScheduler.test.d.ts +2 -0
  126. package/dist/tests/DeletedCoValuesEraserScheduler.test.d.ts.map +1 -0
  127. package/dist/tests/DeletedCoValuesEraserScheduler.test.js +149 -0
  128. package/dist/tests/DeletedCoValuesEraserScheduler.test.js.map +1 -0
  129. package/dist/tests/GarbageCollector.test.js +91 -18
  130. package/dist/tests/GarbageCollector.test.js.map +1 -1
  131. package/dist/tests/StorageApiAsync.test.js +510 -146
  132. package/dist/tests/StorageApiAsync.test.js.map +1 -1
  133. package/dist/tests/StorageApiSync.test.js +531 -130
  134. package/dist/tests/StorageApiSync.test.js.map +1 -1
  135. package/dist/tests/SyncManager.processQueues.test.js +1 -1
  136. package/dist/tests/SyncManager.processQueues.test.js.map +1 -1
  137. package/dist/tests/SyncStateManager.test.js +1 -1
  138. package/dist/tests/SyncStateManager.test.js.map +1 -1
  139. package/dist/tests/WasmCrypto.test.js +6 -3
  140. package/dist/tests/WasmCrypto.test.js.map +1 -1
  141. package/dist/tests/coPlainText.test.js +1 -1
  142. package/dist/tests/coPlainText.test.js.map +1 -1
  143. package/dist/tests/coValueCore.loadFromStorage.test.js +4 -0
  144. package/dist/tests/coValueCore.loadFromStorage.test.js.map +1 -1
  145. package/dist/tests/coValueCore.test.js +34 -13
  146. package/dist/tests/coValueCore.test.js.map +1 -1
  147. package/dist/tests/coreWasm.test.js +127 -4
  148. package/dist/tests/coreWasm.test.js.map +1 -1
  149. package/dist/tests/crypto.test.js +89 -93
  150. package/dist/tests/crypto.test.js.map +1 -1
  151. package/dist/tests/deleteCoValue.test.d.ts +2 -0
  152. package/dist/tests/deleteCoValue.test.d.ts.map +1 -0
  153. package/dist/tests/deleteCoValue.test.js +313 -0
  154. package/dist/tests/deleteCoValue.test.js.map +1 -0
  155. package/dist/tests/group.removeMember.test.js +18 -30
  156. package/dist/tests/group.removeMember.test.js.map +1 -1
  157. package/dist/tests/knownState.lazyLoading.test.js +4 -0
  158. package/dist/tests/knownState.lazyLoading.test.js.map +1 -1
  159. package/dist/tests/sync.deleted.test.d.ts +2 -0
  160. package/dist/tests/sync.deleted.test.d.ts.map +1 -0
  161. package/dist/tests/sync.deleted.test.js +214 -0
  162. package/dist/tests/sync.deleted.test.js.map +1 -0
  163. package/dist/tests/sync.garbageCollection.test.js +56 -32
  164. package/dist/tests/sync.garbageCollection.test.js.map +1 -1
  165. package/dist/tests/sync.load.test.js +3 -5
  166. package/dist/tests/sync.load.test.js.map +1 -1
  167. package/dist/tests/sync.mesh.test.js +4 -3
  168. package/dist/tests/sync.mesh.test.js.map +1 -1
  169. package/dist/tests/sync.peerReconciliation.test.js +3 -3
  170. package/dist/tests/sync.peerReconciliation.test.js.map +1 -1
  171. package/dist/tests/sync.storage.test.js +12 -11
  172. package/dist/tests/sync.storage.test.js.map +1 -1
  173. package/dist/tests/sync.storageAsync.test.js +7 -7
  174. package/dist/tests/sync.storageAsync.test.js.map +1 -1
  175. package/dist/tests/sync.test.js +3 -2
  176. package/dist/tests/sync.test.js.map +1 -1
  177. package/dist/tests/sync.tracking.test.js +35 -4
  178. package/dist/tests/sync.tracking.test.js.map +1 -1
  179. package/dist/tests/testStorage.d.ts +3 -0
  180. package/dist/tests/testStorage.d.ts.map +1 -1
  181. package/dist/tests/testStorage.js +16 -2
  182. package/dist/tests/testStorage.js.map +1 -1
  183. package/dist/tests/testUtils.d.ts +29 -4
  184. package/dist/tests/testUtils.d.ts.map +1 -1
  185. package/dist/tests/testUtils.js +84 -9
  186. package/dist/tests/testUtils.js.map +1 -1
  187. package/package.json +6 -16
  188. package/src/CojsonMessageChannel/CojsonMessageChannel.ts +332 -0
  189. package/src/CojsonMessageChannel/MessagePortOutgoingChannel.ts +52 -0
  190. package/src/CojsonMessageChannel/index.ts +9 -0
  191. package/src/CojsonMessageChannel/types.ts +200 -0
  192. package/src/GarbageCollector.ts +5 -5
  193. package/src/SyncStateManager.ts +6 -6
  194. package/src/coValueContentMessage.ts +0 -14
  195. package/src/coValueCore/SessionMap.ts +43 -1
  196. package/src/coValueCore/coValueCore.ts +430 -15
  197. package/src/coValueCore/verifiedState.ts +26 -3
  198. package/src/coValues/coList.ts +5 -3
  199. package/src/coValues/group.ts +5 -6
  200. package/src/config.ts +0 -9
  201. package/src/crypto/NapiCrypto.ts +29 -13
  202. package/src/crypto/RNCrypto.ts +29 -11
  203. package/src/crypto/WasmCrypto.ts +67 -20
  204. package/src/crypto/WasmCryptoEdge.ts +5 -1
  205. package/src/crypto/crypto.ts +16 -4
  206. package/src/exports.ts +3 -0
  207. package/src/ids.ts +11 -1
  208. package/src/localNode.ts +18 -5
  209. package/src/platformUtils.ts +26 -0
  210. package/src/storage/DeletedCoValuesEraserScheduler.ts +124 -0
  211. package/src/storage/sqlite/client.ts +77 -0
  212. package/src/storage/sqlite/sqliteMigrations.ts +7 -0
  213. package/src/storage/sqliteAsync/client.ts +75 -0
  214. package/src/storage/storageAsync.ts +77 -4
  215. package/src/storage/storageSync.ts +73 -4
  216. package/src/storage/types.ts +75 -0
  217. package/src/sync.ts +84 -15
  218. package/src/tests/CojsonMessageChannel.test.ts +306 -0
  219. package/src/tests/DeletedCoValuesEraserScheduler.test.ts +185 -0
  220. package/src/tests/GarbageCollector.test.ts +119 -22
  221. package/src/tests/StorageApiAsync.test.ts +615 -156
  222. package/src/tests/StorageApiSync.test.ts +623 -137
  223. package/src/tests/SyncManager.processQueues.test.ts +1 -1
  224. package/src/tests/SyncStateManager.test.ts +1 -1
  225. package/src/tests/WasmCrypto.test.ts +8 -3
  226. package/src/tests/coPlainText.test.ts +1 -1
  227. package/src/tests/coValueCore.loadFromStorage.test.ts +8 -0
  228. package/src/tests/coValueCore.test.ts +49 -14
  229. package/src/tests/coreWasm.test.ts +319 -10
  230. package/src/tests/crypto.test.ts +141 -150
  231. package/src/tests/deleteCoValue.test.ts +528 -0
  232. package/src/tests/group.removeMember.test.ts +35 -35
  233. package/src/tests/knownState.lazyLoading.test.ts +8 -0
  234. package/src/tests/sync.deleted.test.ts +294 -0
  235. package/src/tests/sync.garbageCollection.test.ts +69 -36
  236. package/src/tests/sync.load.test.ts +3 -5
  237. package/src/tests/sync.mesh.test.ts +6 -3
  238. package/src/tests/sync.peerReconciliation.test.ts +3 -3
  239. package/src/tests/sync.storage.test.ts +14 -11
  240. package/src/tests/sync.storageAsync.test.ts +7 -7
  241. package/src/tests/sync.test.ts +5 -2
  242. package/src/tests/sync.tracking.test.ts +54 -4
  243. package/src/tests/testStorage.ts +30 -3
  244. package/src/tests/testUtils.ts +113 -15
  245. package/dist/crypto/PureJSCrypto.d.ts +0 -77
  246. package/dist/crypto/PureJSCrypto.d.ts.map +0 -1
  247. package/dist/crypto/PureJSCrypto.js +0 -236
  248. package/dist/crypto/PureJSCrypto.js.map +0 -1
  249. package/dist/tests/PureJSCrypto.test.d.ts +0 -2
  250. package/dist/tests/PureJSCrypto.test.d.ts.map +0 -1
  251. package/dist/tests/PureJSCrypto.test.js +0 -145
  252. package/dist/tests/PureJSCrypto.test.js.map +0 -1
  253. package/src/crypto/PureJSCrypto.ts +0 -429
  254. package/src/tests/PureJSCrypto.test.ts +0 -217
@@ -0,0 +1,294 @@
1
+ import { assert, beforeEach, describe, expect, test } from "vitest";
2
+ import { expectMap } from "../coValue";
3
+ import {
4
+ SyncMessagesLog,
5
+ TEST_NODE_CONFIG,
6
+ loadCoValueOrFail,
7
+ setupTestAccount,
8
+ setupTestNode,
9
+ waitFor,
10
+ } from "./testUtils";
11
+ import { isDeleteSessionID, SessionID } from "../ids";
12
+
13
+ let jazzCloud: ReturnType<typeof setupTestNode>;
14
+
15
+ beforeEach(async () => {
16
+ // We want to simulate a real world communication that happens asynchronously
17
+ TEST_NODE_CONFIG.withAsyncPeers = true;
18
+
19
+ SyncMessagesLog.clear();
20
+ jazzCloud = setupTestNode({ isSyncServer: true });
21
+ });
22
+
23
+ describe("syncing deleted coValues", () => {
24
+ test("client loads a deleted coValue from server (tombstone-only)", async () => {
25
+ const { node: client } = setupTestNode({ connected: true });
26
+
27
+ const group = jazzCloud.node.createGroup();
28
+ const map = group.createMap();
29
+ map.set("hello", "world", "trusting");
30
+
31
+ // Delete on the server before the client loads.
32
+ map.core.deleteCoValue();
33
+ expect(map.core.isDeleted).toBe(true);
34
+
35
+ const mapOnClient = await loadCoValueOrFail(client, map.id);
36
+ const mapCoreOnClient = client.expectCoValueLoaded(map.id);
37
+
38
+ expect(mapCoreOnClient.isDeleted).toBe(true);
39
+ // Historical content should not be synced.
40
+ expect(mapOnClient.get("hello")).toBeUndefined();
41
+
42
+ expect(
43
+ SyncMessagesLog.getMessages({
44
+ Group: group.core,
45
+ Map: map.core,
46
+ }),
47
+ ).toMatchInlineSnapshot(`
48
+ [
49
+ "client -> server | LOAD Map sessions: empty",
50
+ "server -> client | CONTENT Group header: true new: After: 0 New: 3",
51
+ "server -> client | CONTENT Map header: true new: After: 0 New: 1",
52
+ "client -> server | KNOWN Group sessions: header/3",
53
+ "client -> server | KNOWN Map sessions: header/1",
54
+ ]
55
+ `);
56
+ });
57
+
58
+ test("inbound filtering: after deletion, non-delete sessions in the same content message are ignored", async () => {
59
+ const client = setupTestNode({ connected: false });
60
+
61
+ const group = jazzCloud.node.createGroup();
62
+ const map = group.createMap();
63
+ map.set("k", "v", "trusting");
64
+
65
+ const contentBeforeDelete = map.core.newContentSince(undefined)?.[0];
66
+ assert(contentBeforeDelete);
67
+
68
+ // Create a delete marker on the server, but also keep a historical session around.
69
+ map.core.deleteCoValue();
70
+
71
+ const content = map.core.newContentSince(undefined)?.[0];
72
+ assert(content);
73
+
74
+ const groupContent = group.core.newContentSince(undefined)?.[0];
75
+ assert(groupContent);
76
+
77
+ // We merge the content before delete with the content after delete to simulate an older peer that might send extra sessions in the same message
78
+ Object.assign(contentBeforeDelete.new, content.new);
79
+
80
+ client.node.syncManager.handleNewContent(groupContent, "import");
81
+ client.node.syncManager.handleNewContent(content, "import");
82
+
83
+ const coreOnClient = client.node.expectCoValueLoaded(map.id);
84
+ expect(coreOnClient.isDeleted).toBe(true);
85
+
86
+ const contentOnClient = expectMap(coreOnClient.getCurrentContent());
87
+ expect(contentOnClient.get("k")).toBeUndefined();
88
+ });
89
+
90
+ test("should wait for the dependencies to be available before processing the deleted session/transaction", async () => {
91
+ const client = setupTestNode({ connected: false });
92
+
93
+ const group = jazzCloud.node.createGroup();
94
+ const map = group.createMap();
95
+
96
+ // Create a delete marker on the server, but also keep a historical session around.
97
+ map.core.deleteCoValue();
98
+
99
+ const content = map.core.newContentSince(undefined)?.[0];
100
+ assert(content);
101
+
102
+ const groupContent = group.core.newContentSince(undefined)?.[0];
103
+ assert(groupContent);
104
+
105
+ client.node.syncManager.handleNewContent(content, "import");
106
+ client.node.syncManager.handleNewContent(groupContent, "import");
107
+
108
+ await waitFor(() => {
109
+ expect(client.node.expectCoValueLoaded(map.id).isDeleted).toBe(true);
110
+ });
111
+ });
112
+
113
+ test("outbound blocking: post-delete normal writes are ignored and do not produce content uploads", async () => {
114
+ const client = setupTestNode({ connected: true });
115
+
116
+ const group = jazzCloud.node.createGroup();
117
+ group.addMember("everyone", "writer");
118
+ const map = group.createMap();
119
+ map.set("a", 1, "trusting");
120
+
121
+ const mapOnClient = await loadCoValueOrFail(client.node, map.id);
122
+
123
+ // Delete on the server and wait for it to propagate.
124
+ map.core.deleteCoValue();
125
+ await waitFor(() => {
126
+ expect(mapOnClient.core.isDeleted).toBe(true);
127
+ });
128
+
129
+ SyncMessagesLog.clear();
130
+
131
+ mapOnClient.set("x", "y", "trusting");
132
+
133
+ await new Promise((resolve) => setTimeout(resolve, 10));
134
+
135
+ // Ensure we didn't produce outgoing content uploads as a result of the rejected write.
136
+ const messages = SyncMessagesLog.getMessages({
137
+ Group: group.core,
138
+ Map: map.core,
139
+ });
140
+ expect(messages.some((m) => m.includes("CONTENT Map"))).toBe(false);
141
+ });
142
+
143
+ test("delete should be propagated to client-to-client sync", async () => {
144
+ const alice = setupTestNode();
145
+ alice.connectToSyncServer({
146
+ ourName: "alice",
147
+ });
148
+ const bob = setupTestNode();
149
+ bob.connectToSyncServer({
150
+ ourName: "bob",
151
+ });
152
+
153
+ const group = alice.node.createGroup();
154
+ const map = group.createMap();
155
+ map.set("hello", "world", "trusting");
156
+ map.core.deleteCoValue();
157
+
158
+ await loadCoValueOrFail(bob.node, map.id);
159
+
160
+ await waitFor(() => {
161
+ expect(bob.node.expectCoValueLoaded(map.id).isDeleted).toBe(true);
162
+ });
163
+
164
+ expect(
165
+ SyncMessagesLog.getMessages({
166
+ Group: group.core,
167
+ Map: map.core,
168
+ }),
169
+ ).toMatchInlineSnapshot(`
170
+ [
171
+ "bob -> server | LOAD Map sessions: empty",
172
+ "alice -> server | CONTENT Group header: true new: After: 0 New: 3",
173
+ "alice -> server | CONTENT Map header: true new: After: 0 New: 1",
174
+ "server -> bob | KNOWN Map sessions: empty",
175
+ "server -> alice | KNOWN Group sessions: header/3",
176
+ "server -> alice | KNOWN Map sessions: header/1",
177
+ "server -> bob | CONTENT Group header: true new: After: 0 New: 3",
178
+ "server -> bob | CONTENT Map header: true new: After: 0 New: 1",
179
+ "bob -> server | KNOWN Group sessions: header/3",
180
+ "bob -> server | KNOWN Map sessions: header/1",
181
+ ]
182
+ `);
183
+ });
184
+
185
+ test("content synced after deletion should be ignored", async () => {
186
+ const alice = setupTestNode({ connected: true });
187
+ const bob = setupTestNode();
188
+
189
+ const { peerState: bobConnection } = bob.connectToSyncServer({
190
+ ourName: "bob",
191
+ });
192
+
193
+ const group = alice.node.createGroup();
194
+ group.addMember("everyone", "writer");
195
+ const map = group.createMap();
196
+ map.set("hello", "world", "trusting");
197
+
198
+ const mapOnBob = await loadCoValueOrFail(bob.node, map.id);
199
+
200
+ SyncMessagesLog.clear();
201
+
202
+ bobConnection.gracefulShutdown();
203
+
204
+ map.core.deleteCoValue();
205
+
206
+ await map.core.waitForSync();
207
+
208
+ mapOnBob.set("hello", "updated", "trusting");
209
+
210
+ bob.connectToSyncServer({
211
+ ourName: "bob",
212
+ });
213
+
214
+ await mapOnBob.core.waitForSync();
215
+
216
+ expect(
217
+ SyncMessagesLog.getMessages({
218
+ Group: group.core,
219
+ Map: map.core,
220
+ }),
221
+ ).toMatchInlineSnapshot(`
222
+ [
223
+ "client -> server | CONTENT Map header: false new: After: 0 New: 1",
224
+ "server -> client | KNOWN Map sessions: header/2",
225
+ "server -> bob | CONTENT Map header: false new: After: 0 New: 1",
226
+ "bob -> server | LOAD Group sessions: header/5",
227
+ "bob -> server | LOAD Map sessions: header/2",
228
+ "bob -> server | CONTENT Map header: false new: After: 0 New: 1",
229
+ "server -> bob | KNOWN Group sessions: header/5",
230
+ "server -> bob | CONTENT Map header: false new: After: 0 New: 1",
231
+ "server -> bob | KNOWN Map sessions: header/3",
232
+ "bob -> server | KNOWN Map sessions: header/2",
233
+ ]
234
+ `);
235
+ });
236
+
237
+ test("should handle concurrent delete operations", async () => {
238
+ const alice = await setupTestAccount();
239
+ alice.connectToSyncServer({
240
+ ourName: "alice",
241
+ });
242
+ const bob = await setupTestAccount();
243
+ bob.connectToSyncServer({
244
+ ourName: "bob",
245
+ });
246
+
247
+ const group = jazzCloud.node.createGroup();
248
+ group.addMemberInternal(alice.account, "admin");
249
+ group.addMemberInternal(bob.account, "admin");
250
+
251
+ const map = group.createMap();
252
+ map.set("counter", 0, "trusting");
253
+ map.set("counter", 1, "trusting");
254
+
255
+ const mapOnAlice = await loadCoValueOrFail(alice.node, map.id);
256
+ const mapOnBob = await loadCoValueOrFail(bob.node, map.id);
257
+
258
+ SyncMessagesLog.clear();
259
+
260
+ mapOnAlice.core.deleteCoValue();
261
+ mapOnBob.core.deleteCoValue();
262
+
263
+ await mapOnAlice.core.waitForSync();
264
+ await mapOnBob.core.waitForSync();
265
+
266
+ expect(
267
+ SyncMessagesLog.getMessages({
268
+ Group: group.core,
269
+ Map: map.core,
270
+ }),
271
+ ).toMatchInlineSnapshot(`
272
+ [
273
+ "alice -> server | CONTENT Map header: false new: After: 0 New: 1",
274
+ "bob -> server | CONTENT Map header: false new: After: 0 New: 1",
275
+ "server -> alice | KNOWN Map sessions: header/3",
276
+ "server -> bob | CONTENT Map header: false new: After: 0 New: 1",
277
+ "server -> bob | KNOWN Map sessions: header/4",
278
+ "server -> alice | CONTENT Map header: false new: After: 0 New: 1",
279
+ "bob -> server | KNOWN Map sessions: header/4",
280
+ ]
281
+ `);
282
+
283
+ expect(map.core.isDeleted).toBe(true);
284
+
285
+ const sessions = map.core.knownState().sessions;
286
+
287
+ expect(Object.keys(sessions)).toHaveLength(2);
288
+ expect(
289
+ Object.keys(sessions).every((sessionID) =>
290
+ isDeleteSessionID(sessionID as SessionID),
291
+ ),
292
+ ).toBe(true);
293
+ });
294
+ });
@@ -50,6 +50,47 @@ describe("sync after the garbage collector has run", () => {
50
50
  const mapOnClient = await loadCoValueOrFail(client.node, map.id);
51
51
  expect(mapOnClient.get("hello")).toEqual("world");
52
52
 
53
+ expect(
54
+ SyncMessagesLog.getMessages({
55
+ Group: group.core,
56
+ Map: map.core,
57
+ }),
58
+ ).toMatchInlineSnapshot(`
59
+ [
60
+ "client -> server | LOAD Map sessions: empty",
61
+ "server -> storage | LOAD Map sessions: empty",
62
+ "storage -> server | CONTENT Map header: true new: After: 0 New: 1",
63
+ "server -> client | CONTENT Group header: true new: After: 0 New: 3",
64
+ "server -> client | CONTENT Map header: true new: After: 0 New: 1",
65
+ "client -> server | KNOWN Group sessions: header/3",
66
+ "client -> server | KNOWN Map sessions: header/1",
67
+ ]
68
+ `);
69
+ });
70
+
71
+ test("loading a coValue from the sync server that was removed by the garbage collector along with its owner", async () => {
72
+ const client = setupTestNode();
73
+
74
+ client.connectToSyncServer();
75
+
76
+ const group = jazzCloud.node.createGroup();
77
+ const map = group.createMap();
78
+ map.set("hello", "world", "trusting");
79
+
80
+ await map.core.waitForSync();
81
+
82
+ // force the garbage collector to run twice to remove the map and its group
83
+ jazzCloud.node.garbageCollector?.collect();
84
+ jazzCloud.node.garbageCollector?.collect();
85
+
86
+ expect(jazzCloud.node.getCoValue(group.id).isAvailable()).toBe(false);
87
+ expect(jazzCloud.node.getCoValue(map.id).isAvailable()).toBe(false);
88
+
89
+ SyncMessagesLog.clear();
90
+
91
+ const mapOnClient = await loadCoValueOrFail(client.node, map.id);
92
+ expect(mapOnClient.get("hello")).toEqual("world");
93
+
53
94
  expect(
54
95
  SyncMessagesLog.getMessages({
55
96
  Group: group.core,
@@ -103,7 +144,6 @@ describe("sync after the garbage collector has run", () => {
103
144
  [
104
145
  "client -> server | CONTENT Map header: false new: After: 0 New: 1",
105
146
  "server -> storage | LOAD Map sessions: empty",
106
- "storage -> server | CONTENT Group header: true new: After: 0 New: 5",
107
147
  "storage -> server | CONTENT Map header: true new: After: 0 New: 1",
108
148
  "server -> client | KNOWN Map sessions: header/2",
109
149
  "server -> storage | CONTENT Map header: false new: After: 0 New: 1",
@@ -112,41 +152,30 @@ describe("sync after the garbage collector has run", () => {
112
152
  });
113
153
 
114
154
  test("syncing a coValue that was removed by the garbage collector", async () => {
115
- const edge = setupTestNode();
116
- edge.addStorage({
117
- ourName: "edge",
118
- });
119
- edge.connectToSyncServer({
120
- syncServer: jazzCloud.node,
121
- syncServerName: "server",
122
- ourName: "edge",
123
- });
124
- edge.node.enableGarbageCollector();
125
155
  const client = setupTestNode();
126
-
127
- client.connectToSyncServer({
128
- syncServer: edge.node,
129
- syncServerName: "edge",
156
+ client.addStorage({
157
+ ourName: "client",
130
158
  });
159
+ client.node.enableGarbageCollector();
131
160
 
132
- const group = edge.node.createGroup();
133
- group.addMember("everyone", "writer");
134
-
135
- await group.core.waitForSync();
136
-
161
+ const group = client.node.createGroup();
137
162
  const map = group.createMap();
138
-
139
163
  map.set("hello", "updated", "trusting");
140
164
 
141
165
  // force the garbage collector to run before the transaction is synced
142
- edge.node.garbageCollector?.collect();
143
- expect(edge.node.getCoValue(map.id).isAvailable()).toBe(false);
166
+ client.node.garbageCollector?.collect();
167
+ expect(client.node.getCoValue(map.id).isAvailable()).toBe(false);
144
168
 
145
169
  SyncMessagesLog.clear();
146
170
 
171
+ client.connectToSyncServer();
172
+
173
+ // Wait for unsynced coValues to be resumed and synced after connecting to server
174
+ await client.node.syncManager.waitForAllCoValuesSync();
175
+
147
176
  // The storage should work even after the coValue is unmounted, so the load should be successful
148
- const mapOnClient = await loadCoValueOrFail(client.node, map.id);
149
- expect(mapOnClient.get("hello")).toEqual("updated");
177
+ const mapOnServer = await loadCoValueOrFail(jazzCloud.node, map.id);
178
+ expect(mapOnServer.get("hello")).toEqual("updated");
150
179
 
151
180
  expect(
152
181
  SyncMessagesLog.getMessages({
@@ -155,18 +184,22 @@ describe("sync after the garbage collector has run", () => {
155
184
  }),
156
185
  ).toMatchInlineSnapshot(`
157
186
  [
158
- "client -> edge | LOAD Map sessions: empty",
159
- "edge -> storage | CONTENT Map header: true new: After: 0 New: 1",
160
- "edge -> server | CONTENT Map header: true new: After: 0 New: 1",
161
- "edge -> storage | LOAD Map sessions: empty",
162
- "storage -> edge | CONTENT Group header: true new: After: 0 New: 5",
163
- "storage -> edge | CONTENT Map header: true new: After: 0 New: 1",
164
- "edge -> client | CONTENT Group header: true new: After: 0 New: 5",
165
- "edge -> client | CONTENT Map header: true new: After: 0 New: 1",
166
- "server -> edge | KNOWN Map sessions: header/1",
187
+ "client -> server | LOAD Map sessions: empty",
188
+ "client -> server | LOAD Group sessions: header/3",
189
+ "client -> storage | CONTENT Group header: true new: After: 0 New: 3",
190
+ "client -> server | CONTENT Group header: true new: After: 0 New: 3",
191
+ "client -> storage | CONTENT Map header: true new: After: 0 New: 1",
192
+ "client -> server | CONTENT Map header: true new: After: 0 New: 1",
193
+ "server -> storage | LOAD Map sessions: empty",
194
+ "storage -> server | KNOWN Map sessions: empty",
195
+ "server -> client | KNOWN Map sessions: empty",
196
+ "server -> storage | GET_KNOWN_STATE Group",
197
+ "storage -> server | GET_KNOWN_STATE_RESULT Group sessions: empty",
198
+ "server -> client | KNOWN Group sessions: empty",
199
+ "server -> client | KNOWN Group sessions: header/3",
200
+ "server -> storage | CONTENT Group header: true new: After: 0 New: 3",
201
+ "server -> client | KNOWN Map sessions: header/1",
167
202
  "server -> storage | CONTENT Map header: true new: After: 0 New: 1",
168
- "client -> edge | KNOWN Group sessions: header/5",
169
- "client -> edge | KNOWN Map sessions: header/1",
170
203
  ]
171
204
  `);
172
205
  });
@@ -514,7 +514,7 @@ describe("loading coValues from server", () => {
514
514
  });
515
515
 
516
516
  // Makes the CoValues unavailable on the server
517
- jazzCloud.restart();
517
+ await jazzCloud.restart();
518
518
 
519
519
  const client = setupTestNode({
520
520
  connected: true,
@@ -1476,7 +1476,7 @@ describe("lazy storage load optimization", () => {
1476
1476
 
1477
1477
  // Restart the server to clear memory (keeping storage)
1478
1478
  // Now the server has no CoValues in memory, only in storage
1479
- jazzCloud.restart();
1479
+ await jazzCloud.restart();
1480
1480
  jazzCloud.node.setStorage(storage);
1481
1481
 
1482
1482
  SyncMessagesLog.clear();
@@ -1519,7 +1519,7 @@ describe("lazy storage load optimization", () => {
1519
1519
  await map.core.waitForSync();
1520
1520
 
1521
1521
  // Restart the server to clear memory (keeping storage)
1522
- jazzCloud.restart();
1522
+ await jazzCloud.restart();
1523
1523
  jazzCloud.node.setStorage(storage);
1524
1524
 
1525
1525
  SyncMessagesLog.clear();
@@ -1643,7 +1643,6 @@ describe("lazy storage load optimization", () => {
1643
1643
  [
1644
1644
  "client -> server | CONTENT Map header: false new: After: 0 New: 1",
1645
1645
  "server -> storage | LOAD Map sessions: empty",
1646
- "storage -> server | CONTENT Group header: true new: After: 0 New: 5",
1647
1646
  "storage -> server | CONTENT Map header: true new: After: 0 New: 1",
1648
1647
  "server -> client | KNOWN Map sessions: header/2",
1649
1648
  "server -> storage | CONTENT Map header: false new: After: 0 New: 1",
@@ -1702,7 +1701,6 @@ describe("lazy storage load optimization", () => {
1702
1701
  [
1703
1702
  "client -> server | CONTENT Map header: false new: After: 0 New: 1",
1704
1703
  "server -> storage | LOAD Map sessions: empty",
1705
- "storage -> server | CONTENT Group header: true new: After: 0 New: 5",
1706
1704
  "storage -> server | CONTENT Map header: true new: After: 0 New: 73 expectContentUntil: header/201",
1707
1705
  "server -> client | KNOWN Map sessions: header/74",
1708
1706
  "server -> storage | CONTENT Map header: false new: After: 0 New: 1",
@@ -11,7 +11,8 @@ import {
11
11
  setupTestNode,
12
12
  waitFor,
13
13
  } from "./testUtils";
14
- import { stableStringify } from "../jsonStringify";
14
+ import { Stringified } from "../jsonStringify";
15
+ import { JsonValue } from "../jsonValue";
15
16
 
16
17
  // We want to simulate a real world communication that happens asynchronously
17
18
  TEST_NODE_CONFIG.withAsyncPeers = true;
@@ -325,7 +326,9 @@ describe("multiple clients syncing with the a cloud-like server mesh", () => {
325
326
 
326
327
  msg.new[mesh.edgeFrance.node.currentSessionID]!.newTransactions.push({
327
328
  privacy: "trusting",
328
- changes: stableStringify([{ op: "set", key: "hello", value: "updated" }]),
329
+ changes: JSON.stringify([
330
+ { op: "set", key: "hello", value: "updated" },
331
+ ]) as Stringified<JsonValue[]>,
329
332
  madeAt: Date.now(),
330
333
  });
331
334
 
@@ -506,7 +509,7 @@ describe("multiple clients syncing with the a cloud-like server mesh", () => {
506
509
  ]
507
510
  `);
508
511
 
509
- edge.restart();
512
+ await edge.restart();
510
513
 
511
514
  edge.connectToSyncServer({
512
515
  syncServerName: "core",
@@ -167,7 +167,7 @@ describe("peer reconciliation", () => {
167
167
 
168
168
  await map.core.waitForSync();
169
169
 
170
- jazzCloud.restart();
170
+ await jazzCloud.restart();
171
171
  SyncMessagesLog.clear();
172
172
  client.connectToSyncServer();
173
173
 
@@ -222,7 +222,7 @@ describe("peer reconciliation", () => {
222
222
 
223
223
  await map.core.waitForSync();
224
224
 
225
- jazzCloud.restart();
225
+ await jazzCloud.restart();
226
226
  SyncMessagesLog.clear();
227
227
  client.connectToSyncServer();
228
228
 
@@ -305,7 +305,7 @@ describe("peer reconciliation", () => {
305
305
 
306
306
  await map.core.waitForSync();
307
307
 
308
- jazzCloud.restart();
308
+ await jazzCloud.restart();
309
309
 
310
310
  SyncMessagesLog.clear();
311
311
  client.connectToSyncServer();
@@ -8,6 +8,7 @@ import {
8
8
  vi,
9
9
  } from "vitest";
10
10
 
11
+ import type { JsonValue } from "../exports";
11
12
  import { cojsonInternals, emptyKnownState } from "../exports";
12
13
  import {
13
14
  SyncMessagesLog,
@@ -19,7 +20,7 @@ import {
19
20
  tearDownTestMetricReader,
20
21
  waitFor,
21
22
  } from "./testUtils";
22
- import { stableStringify } from "../jsonStringify";
23
+ import { Stringified } from "../jsonStringify";
23
24
 
24
25
  // We want to simulate a real world communication that happens asynchronously
25
26
  TEST_NODE_CONFIG.withAsyncPeers = true;
@@ -83,7 +84,7 @@ describe("client with storage syncs with server", () => {
83
84
 
84
85
  await loadCoValueOrFail(client.node, map.id);
85
86
 
86
- client.restart();
87
+ await client.restart();
87
88
 
88
89
  client.connectToSyncServer();
89
90
  client.addStorage({
@@ -167,7 +168,7 @@ describe("client with storage syncs with server", () => {
167
168
 
168
169
  await map.core.waitForSync();
169
170
 
170
- client.restart();
171
+ await client.restart();
171
172
 
172
173
  client.addStorage({
173
174
  storage,
@@ -217,7 +218,7 @@ describe("client with storage syncs with server", () => {
217
218
  branch.set("branchKey", "branchValue");
218
219
  await branch.core.waitForSync();
219
220
 
220
- client.restart();
221
+ await client.restart();
221
222
  client.addStorage({
222
223
  storage,
223
224
  });
@@ -388,7 +389,7 @@ describe("client syncs with a server with storage", () => {
388
389
 
389
390
  SyncMessagesLog.clear();
390
391
 
391
- client.restart();
392
+ await client.restart();
392
393
 
393
394
  client.connectToSyncServer({
394
395
  ourName: "client",
@@ -457,7 +458,7 @@ describe("client syncs with a server with storage", () => {
457
458
 
458
459
  expect(correctionSpy).not.toHaveBeenCalled();
459
460
 
460
- client.restart();
461
+ await client.restart();
461
462
 
462
463
  client.connectToSyncServer({
463
464
  ourName: "client",
@@ -572,7 +573,9 @@ describe("client syncs with a server with storage", () => {
572
573
  const invalidMapContent = structuredClone(mapContent);
573
574
  invalidMapContent.new[bob.node.currentSessionID]!.newTransactions.push({
574
575
  privacy: "trusting",
575
- changes: stableStringify([{ op: "set", key: "hello", value: "updated" }]),
576
+ changes: JSON.stringify([
577
+ { op: "set", key: "hello", value: "updated" },
578
+ ]) as Stringified<JsonValue[]>,
576
579
  madeAt: Date.now(),
577
580
  });
578
581
  client.node.syncManager.handleNewContent(invalidMapContent, "import");
@@ -771,7 +774,7 @@ describe("client syncs with a server with storage", () => {
771
774
 
772
775
  SyncMessagesLog.clear();
773
776
 
774
- syncServer.restart();
777
+ await syncServer.restart();
775
778
  syncServer.addStorage({
776
779
  ourName: "syncServer",
777
780
  storage,
@@ -848,7 +851,7 @@ describe("client syncs with a server with storage", () => {
848
851
  ]);
849
852
 
850
853
  // Restart to load from storage
851
- client.restart();
854
+ await client.restart();
852
855
  client.addStorage({ storage });
853
856
 
854
857
  // Load all maps concurrently from storage
@@ -892,7 +895,7 @@ describe("client syncs with a server with storage", () => {
892
895
  SyncMessagesLog.clear();
893
896
 
894
897
  // Restart client with storage
895
- client.restart();
898
+ await client.restart();
896
899
  client.connectToSyncServer();
897
900
  client.addStorage({ storage });
898
901
 
@@ -985,7 +988,7 @@ describe("client syncs with a server with storage", () => {
985
988
 
986
989
  SyncMessagesLog.clear();
987
990
 
988
- syncServer.restart();
991
+ await syncServer.restart();
989
992
  syncServer.addStorage({
990
993
  ourName: "syncServer",
991
994
  storage,