cojson 0.20.7 → 0.20.8

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 (209) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +18 -0
  3. package/dist/SyncStateManager.d.ts.map +1 -1
  4. package/dist/SyncStateManager.js +0 -2
  5. package/dist/SyncStateManager.js.map +1 -1
  6. package/dist/base64url.d.ts +15 -0
  7. package/dist/base64url.d.ts.map +1 -1
  8. package/dist/base64url.js +101 -5
  9. package/dist/base64url.js.map +1 -1
  10. package/dist/base64url.test.js +76 -1
  11. package/dist/base64url.test.js.map +1 -1
  12. package/dist/coValue.d.ts +2 -1
  13. package/dist/coValue.d.ts.map +1 -1
  14. package/dist/coValue.js.map +1 -1
  15. package/dist/coValueCore/coValueCore.d.ts +9 -11
  16. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  17. package/dist/coValueCore/coValueCore.js +92 -65
  18. package/dist/coValueCore/coValueCore.js.map +1 -1
  19. package/dist/coValueCore/verifiedState.d.ts +38 -7
  20. package/dist/coValueCore/verifiedState.d.ts.map +1 -1
  21. package/dist/coValueCore/verifiedState.js +226 -30
  22. package/dist/coValueCore/verifiedState.js.map +1 -1
  23. package/dist/coValues/binaryCoStream.d.ts +63 -0
  24. package/dist/coValues/binaryCoStream.d.ts.map +1 -0
  25. package/dist/coValues/binaryCoStream.js +125 -0
  26. package/dist/coValues/binaryCoStream.js.map +1 -0
  27. package/dist/coValues/coList.d.ts +3 -1
  28. package/dist/coValues/coList.d.ts.map +1 -1
  29. package/dist/coValues/coList.js +15 -6
  30. package/dist/coValues/coList.js.map +1 -1
  31. package/dist/coValues/coMap.d.ts +1 -1
  32. package/dist/coValues/coMap.d.ts.map +1 -1
  33. package/dist/coValues/coMap.js +2 -2
  34. package/dist/coValues/coMap.js.map +1 -1
  35. package/dist/coValues/coStream.d.ts +0 -38
  36. package/dist/coValues/coStream.d.ts.map +1 -1
  37. package/dist/coValues/coStream.js +0 -86
  38. package/dist/coValues/coStream.js.map +1 -1
  39. package/dist/coValues/group.d.ts +44 -6
  40. package/dist/coValues/group.d.ts.map +1 -1
  41. package/dist/coValues/group.js +198 -17
  42. package/dist/coValues/group.js.map +1 -1
  43. package/dist/coreToCoValue.d.ts +2 -1
  44. package/dist/coreToCoValue.d.ts.map +1 -1
  45. package/dist/coreToCoValue.js +2 -1
  46. package/dist/coreToCoValue.js.map +1 -1
  47. package/dist/crypto/NapiCrypto.d.ts +18 -24
  48. package/dist/crypto/NapiCrypto.d.ts.map +1 -1
  49. package/dist/crypto/NapiCrypto.js +98 -60
  50. package/dist/crypto/NapiCrypto.js.map +1 -1
  51. package/dist/crypto/RNCrypto.d.ts +16 -3
  52. package/dist/crypto/RNCrypto.d.ts.map +1 -1
  53. package/dist/crypto/RNCrypto.js +117 -54
  54. package/dist/crypto/RNCrypto.js.map +1 -1
  55. package/dist/crypto/WasmCrypto.d.ts +18 -24
  56. package/dist/crypto/WasmCrypto.d.ts.map +1 -1
  57. package/dist/crypto/WasmCrypto.js +100 -61
  58. package/dist/crypto/WasmCrypto.js.map +1 -1
  59. package/dist/crypto/crypto.d.ts +55 -19
  60. package/dist/crypto/crypto.d.ts.map +1 -1
  61. package/dist/crypto/crypto.js +14 -3
  62. package/dist/crypto/crypto.js.map +1 -1
  63. package/dist/exports.d.ts +7 -3
  64. package/dist/exports.d.ts.map +1 -1
  65. package/dist/exports.js +4 -2
  66. package/dist/exports.js.map +1 -1
  67. package/dist/localNode.d.ts +3 -1
  68. package/dist/localNode.d.ts.map +1 -1
  69. package/dist/localNode.js +10 -3
  70. package/dist/localNode.js.map +1 -1
  71. package/dist/media.d.ts +1 -1
  72. package/dist/media.d.ts.map +1 -1
  73. package/dist/permissions.d.ts +2 -1
  74. package/dist/permissions.d.ts.map +1 -1
  75. package/dist/permissions.js +19 -3
  76. package/dist/permissions.js.map +1 -1
  77. package/dist/storage/sqliteAsync/client.d.ts +24 -12
  78. package/dist/storage/sqliteAsync/client.d.ts.map +1 -1
  79. package/dist/storage/sqliteAsync/client.js +70 -58
  80. package/dist/storage/sqliteAsync/client.js.map +1 -1
  81. package/dist/storage/sqliteAsync/types.d.ts +1 -1
  82. package/dist/storage/sqliteAsync/types.d.ts.map +1 -1
  83. package/dist/storage/types.d.ts +1 -0
  84. package/dist/storage/types.d.ts.map +1 -1
  85. package/dist/sync.d.ts.map +1 -1
  86. package/dist/sync.js +7 -1
  87. package/dist/sync.js.map +1 -1
  88. package/dist/tests/CojsonMessageChannel.test.js +2 -2
  89. package/dist/tests/SQLiteClientAsync.test.d.ts +2 -0
  90. package/dist/tests/SQLiteClientAsync.test.d.ts.map +1 -0
  91. package/dist/tests/SQLiteClientAsync.test.js +64 -0
  92. package/dist/tests/SQLiteClientAsync.test.js.map +1 -0
  93. package/dist/tests/StorageApiAsync.test.js +2 -8
  94. package/dist/tests/StorageApiAsync.test.js.map +1 -1
  95. package/dist/tests/SyncStateManager.test.js +2 -2
  96. package/dist/tests/WasmCrypto.test.js +1 -15
  97. package/dist/tests/WasmCrypto.test.js.map +1 -1
  98. package/dist/tests/coList.test.js +24 -5
  99. package/dist/tests/coList.test.js.map +1 -1
  100. package/dist/tests/coStream.test.js +4 -3
  101. package/dist/tests/coStream.test.js.map +1 -1
  102. package/dist/tests/coValueCore.initTransaction.test.d.ts +2 -0
  103. package/dist/tests/coValueCore.initTransaction.test.d.ts.map +1 -0
  104. package/dist/tests/coValueCore.initTransaction.test.js +438 -0
  105. package/dist/tests/coValueCore.initTransaction.test.js.map +1 -0
  106. package/dist/tests/coValueCore.test.js +11 -19
  107. package/dist/tests/coValueCore.test.js.map +1 -1
  108. package/dist/tests/crypto.test.js +83 -0
  109. package/dist/tests/crypto.test.js.map +1 -1
  110. package/dist/tests/deleteCoValue.test.js +5 -5
  111. package/dist/tests/deleteCoValue.test.js.map +1 -1
  112. package/dist/tests/group.inheritance.test.js +11 -0
  113. package/dist/tests/group.inheritance.test.js.map +1 -1
  114. package/dist/tests/group.test.js +24 -1
  115. package/dist/tests/group.test.js.map +1 -1
  116. package/dist/tests/groupSealer.test.d.ts +2 -0
  117. package/dist/tests/groupSealer.test.d.ts.map +1 -0
  118. package/dist/tests/groupSealer.test.js +913 -0
  119. package/dist/tests/groupSealer.test.js.map +1 -0
  120. package/dist/tests/setup.js +5 -0
  121. package/dist/tests/setup.js.map +1 -1
  122. package/dist/tests/sync.auth.test.js +10 -10
  123. package/dist/tests/sync.concurrentLoad.test.js +12 -12
  124. package/dist/tests/sync.deleted.test.js +8 -8
  125. package/dist/tests/sync.garbageCollection.test.js +10 -10
  126. package/dist/tests/sync.invite.test.js +12 -12
  127. package/dist/tests/sync.known.test.js +2 -2
  128. package/dist/tests/sync.load.test.js +107 -107
  129. package/dist/tests/sync.mesh.test.js +164 -46
  130. package/dist/tests/sync.mesh.test.js.map +1 -1
  131. package/dist/tests/sync.multipleServers.test.js +43 -43
  132. package/dist/tests/sync.peerReconciliation.test.js +29 -29
  133. package/dist/tests/sync.sharding.test.js +3 -3
  134. package/dist/tests/sync.storage.test.js +104 -104
  135. package/dist/tests/sync.storage.test.js.map +1 -1
  136. package/dist/tests/sync.storageAsync.test.js +56 -56
  137. package/dist/tests/sync.upload.test.js +22 -22
  138. package/dist/tests/testStorage.d.ts +2 -0
  139. package/dist/tests/testStorage.d.ts.map +1 -1
  140. package/dist/tests/testStorage.js +30 -6
  141. package/dist/tests/testStorage.js.map +1 -1
  142. package/dist/typeUtils/isCoValue.js +1 -1
  143. package/dist/typeUtils/isCoValue.js.map +1 -1
  144. package/package.json +4 -4
  145. package/src/SyncStateManager.ts +0 -2
  146. package/src/base64url.test.ts +89 -1
  147. package/src/base64url.ts +134 -6
  148. package/src/coValue.ts +2 -1
  149. package/src/coValueCore/coValueCore.ts +126 -84
  150. package/src/coValueCore/verifiedState.ts +335 -53
  151. package/src/coValues/binaryCoStream.ts +217 -0
  152. package/src/coValues/coList.ts +21 -8
  153. package/src/coValues/coMap.ts +3 -0
  154. package/src/coValues/coStream.ts +0 -170
  155. package/src/coValues/group.ts +270 -21
  156. package/src/coreToCoValue.ts +2 -1
  157. package/src/crypto/NapiCrypto.ts +198 -95
  158. package/src/crypto/RNCrypto.ts +229 -102
  159. package/src/crypto/WasmCrypto.ts +201 -95
  160. package/src/crypto/crypto.ts +118 -45
  161. package/src/exports.ts +11 -5
  162. package/src/localNode.ts +17 -1
  163. package/src/media.ts +1 -1
  164. package/src/permissions.ts +30 -7
  165. package/src/storage/sqliteAsync/client.ts +136 -115
  166. package/src/storage/sqliteAsync/types.ts +3 -1
  167. package/src/storage/types.ts +4 -0
  168. package/src/sync.ts +10 -1
  169. package/src/tests/CojsonMessageChannel.test.ts +2 -2
  170. package/src/tests/SQLiteClientAsync.test.ts +75 -0
  171. package/src/tests/StorageApiAsync.test.ts +4 -9
  172. package/src/tests/SyncStateManager.test.ts +2 -2
  173. package/src/tests/WasmCrypto.test.ts +1 -25
  174. package/src/tests/coList.test.ts +39 -5
  175. package/src/tests/coStream.test.ts +4 -5
  176. package/src/tests/coValueCore.initTransaction.test.ts +836 -0
  177. package/src/tests/coValueCore.test.ts +11 -22
  178. package/src/tests/crypto.test.ts +107 -0
  179. package/src/tests/deleteCoValue.test.ts +5 -5
  180. package/src/tests/group.inheritance.test.ts +16 -0
  181. package/src/tests/group.test.ts +29 -1
  182. package/src/tests/groupSealer.test.ts +1473 -0
  183. package/src/tests/setup.ts +6 -0
  184. package/src/tests/sync.auth.test.ts +10 -10
  185. package/src/tests/sync.concurrentLoad.test.ts +12 -12
  186. package/src/tests/sync.deleted.test.ts +8 -8
  187. package/src/tests/sync.garbageCollection.test.ts +10 -10
  188. package/src/tests/sync.invite.test.ts +12 -12
  189. package/src/tests/sync.known.test.ts +2 -2
  190. package/src/tests/sync.load.test.ts +107 -107
  191. package/src/tests/sync.mesh.test.ts +189 -46
  192. package/src/tests/sync.multipleServers.test.ts +43 -43
  193. package/src/tests/sync.peerReconciliation.test.ts +29 -29
  194. package/src/tests/sync.sharding.test.ts +3 -3
  195. package/src/tests/sync.storage.test.ts +104 -104
  196. package/src/tests/sync.storageAsync.test.ts +56 -56
  197. package/src/tests/sync.upload.test.ts +22 -22
  198. package/src/tests/testStorage.ts +39 -9
  199. package/src/typeUtils/isCoValue.ts +1 -1
  200. package/dist/coValueCore/SessionMap.d.ts +0 -55
  201. package/dist/coValueCore/SessionMap.d.ts.map +0 -1
  202. package/dist/coValueCore/SessionMap.js +0 -206
  203. package/dist/coValueCore/SessionMap.js.map +0 -1
  204. package/dist/tests/coreWasm.test.d.ts +0 -2
  205. package/dist/tests/coreWasm.test.d.ts.map +0 -1
  206. package/dist/tests/coreWasm.test.js +0 -203
  207. package/dist/tests/coreWasm.test.js.map +0 -1
  208. package/src/coValueCore/SessionMap.ts +0 -394
  209. package/src/tests/coreWasm.test.ts +0 -452
@@ -0,0 +1,836 @@
1
+ import { beforeEach, describe, expect, test, vi } from "vitest";
2
+ import { WasmCrypto } from "../crypto/WasmCrypto.js";
3
+ import {
4
+ createTwoConnectedNodes,
5
+ importContentIntoNode,
6
+ loadCoValueOrFail,
7
+ setupTestNode,
8
+ waitFor,
9
+ } from "./testUtils.js";
10
+
11
+ beforeEach(() => {
12
+ setupTestNode({ isSyncServer: true });
13
+ });
14
+
15
+ describe("init transaction meta", () => {
16
+ test("a transaction with init meta is parsed correctly", () => {
17
+ const client = setupTestNode();
18
+ const group = client.node.createGroup();
19
+ const map = group.createMap();
20
+
21
+ map.core.makeTransaction(
22
+ [{ op: "set", key: "hello", value: "world" }],
23
+ "trusting",
24
+ { fww: "init" },
25
+ );
26
+
27
+ const transactions = map.core.getValidSortedTransactions();
28
+ expect(transactions).toHaveLength(1);
29
+ expect(transactions[0]?.meta).toEqual({ fww: "init" });
30
+ });
31
+
32
+ test("first-init-wins: only the first init transaction is valid", () => {
33
+ const client = setupTestNode();
34
+ const group = client.node.createGroup();
35
+ const map = group.createMap();
36
+
37
+ // Make two init transactions with different timestamps
38
+ // The first one (earlier madeAt) should win
39
+ const earlierTime = Date.now();
40
+ map.core.makeTransaction(
41
+ [{ op: "set", key: "version", value: "first" }],
42
+ "trusting",
43
+ { fww: "init" },
44
+ earlierTime,
45
+ );
46
+
47
+ const laterTime = earlierTime + 100;
48
+ map.core.makeTransaction(
49
+ [{ op: "set", key: "version", value: "second" }],
50
+ "trusting",
51
+ { fww: "init" },
52
+ laterTime,
53
+ );
54
+
55
+ const validTransactions = map.core.getValidSortedTransactions();
56
+
57
+ // Only the first init transaction should be valid
58
+ expect(validTransactions).toHaveLength(1);
59
+ expect(validTransactions[0]?.meta).toEqual({ fww: "init" });
60
+
61
+ // The first transaction (earlier madeAt) should be the valid one
62
+ expect(validTransactions[0]?.madeAt).toBe(earlierTime);
63
+ });
64
+
65
+ test("first-init-wins: transactions without init meta are not affected", () => {
66
+ const client = setupTestNode();
67
+ const group = client.node.createGroup();
68
+ const map = group.createMap();
69
+
70
+ // Make an init transaction
71
+ map.core.makeTransaction(
72
+ [{ op: "set", key: "version", value: "init" }],
73
+ "trusting",
74
+ { fww: "init" },
75
+ );
76
+
77
+ // Make a regular transaction (no init meta)
78
+ map.core.makeTransaction(
79
+ [{ op: "set", key: "hello", value: "world" }],
80
+ "trusting",
81
+ );
82
+
83
+ const validTransactions = map.core.getValidSortedTransactions();
84
+
85
+ // Both transactions should be valid
86
+ expect(validTransactions).toHaveLength(2);
87
+ });
88
+
89
+ test("late-arriving winner triggers content rebuild", async () => {
90
+ const client = setupTestNode({ connected: true });
91
+ const clientSession2 = client.spawnNewSession();
92
+ const group = client.node.createGroup();
93
+ const map = group.createMap();
94
+ const mapOnClientSession2 = await loadCoValueOrFail(
95
+ clientSession2.node,
96
+ map.id,
97
+ );
98
+
99
+ // Make an init transaction with a later timestamp
100
+ const laterTime = Date.now() + 1000;
101
+ map.core.makeTransaction(
102
+ [{ op: "set", key: "version", value: "later" }],
103
+ "trusting",
104
+ { fww: "init" },
105
+ laterTime,
106
+ );
107
+
108
+ expect(map.get("version")).toBe("later");
109
+
110
+ const rebuildSpy = vi.spyOn(map, "rebuildFromCore");
111
+
112
+ // Now make an init transaction with an earlier timestamp (this should win)
113
+ const earlierTime = laterTime - 500;
114
+ mapOnClientSession2.core.makeTransaction(
115
+ [{ op: "set", key: "version", value: "earlier" }],
116
+ "trusting",
117
+ { fww: "init" },
118
+ earlierTime,
119
+ );
120
+
121
+ await waitFor(() => {
122
+ expect(map.core.knownState()).toEqual(
123
+ mapOnClientSession2.core.knownState(),
124
+ );
125
+ });
126
+
127
+ // The content should have been rebuilt
128
+ expect(rebuildSpy).toHaveBeenCalled();
129
+
130
+ expect(map.get("version")).toBe("earlier");
131
+ });
132
+
133
+ test("two init transactions coming together do not trigger content rebuild", async () => {
134
+ const alice = setupTestNode({ connected: true });
135
+ const aliceSession2 = alice.spawnNewSession();
136
+ const bob = setupTestNode({ connected: false });
137
+ const group = alice.node.createGroup();
138
+ group.addMember("everyone", "writer");
139
+ const map = group.createMap();
140
+
141
+ importContentIntoNode(group.core, bob.node);
142
+ importContentIntoNode(map.core, bob.node);
143
+
144
+ const mapOnBob = bob.node.getCoValue(map.id);
145
+ const rebuildSpy = vi.spyOn(mapOnBob, "scheduleContentRebuild");
146
+
147
+ const mapOnAliceSession2 = await loadCoValueOrFail(
148
+ aliceSession2.node,
149
+ map.id,
150
+ );
151
+
152
+ // Make an init transaction with a later timestamp
153
+ const laterTime = Date.now() + 1000;
154
+ map.core.makeTransaction(
155
+ [{ op: "set", key: "version", value: "later" }],
156
+ "trusting",
157
+ { fww: "init" },
158
+ laterTime,
159
+ );
160
+
161
+ expect(map.get("version")).toBe("later");
162
+
163
+ // Now make an init transaction with an earlier timestamp (this should win)
164
+ const earlierTime = laterTime - 500;
165
+ mapOnAliceSession2.core.makeTransaction(
166
+ [{ op: "set", key: "version", value: "earlier" }],
167
+ "trusting",
168
+ { fww: "init" },
169
+ earlierTime,
170
+ );
171
+
172
+ await waitFor(() => {
173
+ expect(map.core.knownState()).toEqual(
174
+ mapOnAliceSession2.core.knownState(),
175
+ );
176
+ });
177
+
178
+ importContentIntoNode(map.core, bob.node);
179
+
180
+ // The content should have been rebuilt
181
+ expect(rebuildSpy).not.toHaveBeenCalled();
182
+
183
+ expect(mapOnBob.getCurrentContent().toJSON()).toEqual({
184
+ version: "earlier",
185
+ });
186
+ expect(map.get("version")).toBe("earlier");
187
+ });
188
+
189
+ test("content reflects the winning init transaction after rebuild", async () => {
190
+ const client = setupTestNode({ connected: true });
191
+ const clientSession2 = client.spawnNewSession();
192
+ const group = client.node.createGroup();
193
+ const map = group.createMap();
194
+ const mapOnClientSession2 = await loadCoValueOrFail(
195
+ clientSession2.node,
196
+ map.id,
197
+ );
198
+
199
+ // Make an init transaction with a later timestamp
200
+ const laterTime = Date.now() + 1000;
201
+ map.core.makeTransaction(
202
+ [{ op: "set", key: "version", value: "later" }],
203
+ "trusting",
204
+ { fww: "init" },
205
+ laterTime,
206
+ );
207
+
208
+ // Verify initial content
209
+ expect(map.get("version")).toBe("later");
210
+
211
+ // Now make an init transaction with an earlier timestamp (this should win)
212
+ const earlierTime = laterTime - 500;
213
+ mapOnClientSession2.core.makeTransaction(
214
+ [{ op: "set", key: "version", value: "earlier" }],
215
+ "trusting",
216
+ { fww: "init" },
217
+ earlierTime,
218
+ );
219
+
220
+ await waitFor(() => {
221
+ expect(map.core.knownState()).toEqual(
222
+ mapOnClientSession2.core.knownState(),
223
+ );
224
+ });
225
+
226
+ // The content should reflect the earlier (winning) init transaction
227
+ expect(map.get("version")).toBe("earlier");
228
+ });
229
+
230
+ test("losing init transaction is marked as invalid (different sessions)", async () => {
231
+ const client = setupTestNode({ connected: true });
232
+ const clientSession2 = client.spawnNewSession();
233
+ const group = client.node.createGroup();
234
+ const map = group.createMap();
235
+
236
+ const mapOnClientSession2 = await loadCoValueOrFail(
237
+ clientSession2.node,
238
+ map.id,
239
+ );
240
+
241
+ const earlierTime = Date.now();
242
+ map.core.makeTransaction(
243
+ [{ op: "set", key: "version", value: "first" }],
244
+ "trusting",
245
+ { fww: "init" },
246
+ earlierTime,
247
+ );
248
+
249
+ const laterTime = earlierTime + 100;
250
+ mapOnClientSession2.core.makeTransaction(
251
+ [{ op: "set", key: "version", value: "second" }],
252
+ "trusting",
253
+ { fww: "init" },
254
+ laterTime,
255
+ );
256
+
257
+ await waitFor(() => {
258
+ expect(map.core.knownState()).toEqual(
259
+ mapOnClientSession2.core.knownState(),
260
+ );
261
+ });
262
+
263
+ // Check the raw verified transactions
264
+ const allTransactions = map.core.verifiedTransactions;
265
+ expect(allTransactions).toHaveLength(2);
266
+
267
+ // The first transaction should be valid
268
+ const firstTx = allTransactions.find((tx) => tx.madeAt === earlierTime);
269
+ expect(firstTx?.isValid).toBe(true);
270
+
271
+ // The second transaction should be invalid
272
+ const secondTx = allTransactions.find((tx) => tx.madeAt === laterTime);
273
+ expect(secondTx?.isValid).toBe(false);
274
+ expect(secondTx?.validationErrorMessage).toBe(
275
+ `Transaction is not the first writer for fww key "init"`,
276
+ );
277
+ });
278
+
279
+ test("losing init transaction is marked as invalid (same session)", () => {
280
+ const client = setupTestNode();
281
+ const group = client.node.createGroup();
282
+ const map = group.createMap();
283
+
284
+ const earlierTime = Date.now();
285
+ map.core.makeTransaction(
286
+ [{ op: "set", key: "version", value: "first" }],
287
+ "trusting",
288
+ { fww: "init" },
289
+ earlierTime,
290
+ );
291
+
292
+ const laterTime = earlierTime + 100;
293
+ map.core.makeTransaction(
294
+ [{ op: "set", key: "version", value: "second" }],
295
+ "trusting",
296
+ { fww: "init" },
297
+ laterTime,
298
+ );
299
+
300
+ // Check the raw verified transactions
301
+ const allTransactions = map.core.verifiedTransactions;
302
+ expect(allTransactions).toHaveLength(2);
303
+
304
+ // The first transaction should be valid
305
+ const firstTx = allTransactions.find((tx) => tx.madeAt === earlierTime);
306
+ expect(firstTx?.isValid).toBe(true);
307
+
308
+ // The second transaction should be invalid
309
+ const secondTx = allTransactions.find((tx) => tx.madeAt === laterTime);
310
+ expect(secondTx?.isValid).toBe(false);
311
+ expect(secondTx?.validationErrorMessage).toBe(
312
+ `Transaction is not the first writer for fww key "init"`,
313
+ );
314
+ });
315
+
316
+ test("validity change on processed transaction dispatches rebuild", async () => {
317
+ const client = setupTestNode({ connected: true });
318
+ const clientSession2 = client.spawnNewSession();
319
+ const group = client.node.createGroup();
320
+ const map = group.createMap();
321
+
322
+ const mapOnClientSession2 = await loadCoValueOrFail(
323
+ clientSession2.node,
324
+ map.id,
325
+ );
326
+
327
+ // Make an init transaction and process it
328
+ const laterTime = Date.now() + 1000;
329
+ map.core.makeTransaction(
330
+ [{ op: "set", key: "version", value: "later" }],
331
+ "trusting",
332
+ { fww: "init" },
333
+ laterTime,
334
+ );
335
+
336
+ // Check the transaction is marked as processed
337
+ const laterTx = map.core.verifiedTransactions.find(
338
+ (tx) => tx.madeAt === laterTime,
339
+ );
340
+ expect(laterTx?.stage).toBe("processed");
341
+ expect(laterTx?.isValid).toBe(true);
342
+
343
+ // Add a new init transaction with an earlier timestamp
344
+ const earlierTime = laterTime - 500;
345
+ mapOnClientSession2.core.makeTransaction(
346
+ [{ op: "set", key: "version", value: "earlier" }],
347
+ "trusting",
348
+ { fww: "init" },
349
+ earlierTime,
350
+ );
351
+
352
+ await waitFor(() => {
353
+ expect(map.core.knownState()).toEqual(
354
+ mapOnClientSession2.core.knownState(),
355
+ );
356
+ });
357
+
358
+ // The later transaction should now be invalid
359
+ expect(laterTx?.isValid).toBe(false);
360
+ });
361
+
362
+ test("synced init transactions resolve correctly across nodes", async () => {
363
+ const { node1, node2 } = await createTwoConnectedNodes("server", "server");
364
+
365
+ const group = node1.node.createGroup();
366
+ group.addMember("everyone", "writer");
367
+
368
+ const map = group.createMap();
369
+
370
+ // Make an init transaction on node1
371
+ map.core.makeTransaction(
372
+ [{ op: "set", key: "version", value: "node1" }],
373
+ "trusting",
374
+ { fww: "init" },
375
+ );
376
+
377
+ await map.core.waitForSync();
378
+
379
+ // Load the map on node2
380
+ const mapOnNode2 = await loadCoValueOrFail(node2.node, map.id);
381
+
382
+ await waitFor(() => {
383
+ expect(mapOnNode2.get("version")).toBe("node1");
384
+ });
385
+
386
+ // Both nodes should have the same valid transaction
387
+ const node1ValidTxs = map.core.getValidSortedTransactions();
388
+ const node2ValidTxs = mapOnNode2.core.getValidSortedTransactions();
389
+
390
+ expect(node1ValidTxs).toHaveLength(1);
391
+ expect(node2ValidTxs).toHaveLength(1);
392
+ expect(node1ValidTxs[0]?.madeAt).toBe(node2ValidTxs[0]?.madeAt);
393
+ });
394
+
395
+ test("concurrent init transactions from different nodes resolve deterministically", async () => {
396
+ const { node1, node2 } = await createTwoConnectedNodes("server", "server");
397
+
398
+ const group = node1.node.createGroup();
399
+ group.addMember("everyone", "writer");
400
+
401
+ const map = group.createMap();
402
+
403
+ await map.core.waitForSync();
404
+
405
+ // Load the map on node2
406
+ const mapOnNode2 = await loadCoValueOrFail(node2.node, map.id);
407
+
408
+ // Make init transactions on both nodes with different timestamps
409
+ const node1Time = Date.now();
410
+ const node2Time = node1Time + 1000; // node2 is later
411
+
412
+ map.core.makeTransaction(
413
+ [{ op: "set", key: "version", value: "node1" }],
414
+ "trusting",
415
+ { fww: "init" },
416
+ node1Time,
417
+ );
418
+
419
+ mapOnNode2.core.makeTransaction(
420
+ [{ op: "set", key: "version", value: "node2" }],
421
+ "trusting",
422
+ { fww: "init" },
423
+ node2Time,
424
+ );
425
+
426
+ // Wait for sync
427
+ await map.core.waitForSync();
428
+ await mapOnNode2.core.waitForSync();
429
+
430
+ // Wait for microtasks
431
+ await new Promise<void>((resolve) => queueMicrotask(resolve));
432
+
433
+ // Both nodes should converge to the same winner (node1 with earlier timestamp)
434
+ await waitFor(() => {
435
+ expect(map.get("version")).toBe("node1");
436
+ expect(mapOnNode2.get("version")).toBe("node1");
437
+ });
438
+ });
439
+
440
+ test("subscription is notified when init transaction changes the content", async () => {
441
+ const client = setupTestNode();
442
+ const group = client.node.createGroup();
443
+ const map = group.createMap();
444
+
445
+ // Make an init transaction with a later timestamp
446
+ const laterTime = Date.now() + 1000;
447
+ map.core.makeTransaction(
448
+ [{ op: "set", key: "version", value: "later" }],
449
+ "trusting",
450
+ { fww: "init" },
451
+ laterTime,
452
+ );
453
+
454
+ // Subscribe to changes
455
+ const subscriptionSpy = vi.fn();
456
+ const unsubscribe = map.subscribe(subscriptionSpy);
457
+
458
+ subscriptionSpy.mockClear();
459
+
460
+ // Add a new init transaction with an earlier timestamp
461
+ const earlierTime = laterTime - 500;
462
+ map.core.makeTransaction(
463
+ [{ op: "set", key: "version", value: "earlier" }],
464
+ "trusting",
465
+ { fww: "init" },
466
+ earlierTime,
467
+ );
468
+
469
+ // Wait for notifications
470
+ await waitFor(() => {
471
+ expect(subscriptionSpy).toHaveBeenCalled();
472
+ });
473
+
474
+ unsubscribe();
475
+ });
476
+
477
+ test("getValidTransactions returns discarded init transactions when includeInvalidMetaTransactions is true", async () => {
478
+ const { node1, node2 } = await createTwoConnectedNodes("server", "server");
479
+
480
+ const group = node1.node.createGroup();
481
+ group.addMember("everyone", "writer");
482
+
483
+ const map = group.createMap();
484
+
485
+ const earlierTime = Date.now();
486
+ const laterTime = earlierTime + 1000;
487
+
488
+ map.core.makeTransaction(
489
+ [{ op: "set", key: "version", value: "later" }],
490
+ "trusting",
491
+ { fww: "init" },
492
+ laterTime,
493
+ );
494
+
495
+ await map.core.waitForSync();
496
+
497
+ const mapOnNode2 = await loadCoValueOrFail(node2.node, map.id);
498
+
499
+ await waitFor(() => {
500
+ expect(mapOnNode2.get("version")).toBe("later");
501
+ });
502
+
503
+ mapOnNode2.core.makeTransaction(
504
+ [{ op: "set", key: "version", value: "earlier" }],
505
+ "trusting",
506
+ { fww: "init" },
507
+ earlierTime,
508
+ );
509
+
510
+ await waitFor(() => {
511
+ expect(map.core.knownState()).toEqual(mapOnNode2.core.knownState());
512
+ });
513
+
514
+ // Without flag: only valid transactions
515
+ const validOnly = map.core.getValidSortedTransactions();
516
+ expect(validOnly).toHaveLength(1);
517
+ expect(validOnly[0]?.madeAt).toBe(earlierTime);
518
+
519
+ // With flag: includes invalid init transactions
520
+ const withInvalid = map.core.getValidSortedTransactions({
521
+ includeInvalidMetaTransactions: true,
522
+ ignorePrivateTransactions: false,
523
+ });
524
+ expect(withInvalid).toHaveLength(2);
525
+ expect(withInvalid.filter((tx) => tx.isValid)).toHaveLength(1);
526
+ expect(withInvalid.filter((tx) => !tx.isValid)).toHaveLength(1);
527
+ });
528
+
529
+ test("getValidTransactions({includeInvalidMetaTransactions: true}) does not return permission-invalid transactions", async () => {
530
+ const { node1, node2 } = await createTwoConnectedNodes("server", "server");
531
+
532
+ const group = node1.node.createGroup();
533
+ const map = group.createMap();
534
+
535
+ group.addMember(node2.node.getCurrentAgent(), "reader");
536
+
537
+ map.set("key", "admin-value", "trusting");
538
+
539
+ await map.core.waitForSync();
540
+
541
+ const mapOnReader = await loadCoValueOrFail(node2.node, map.id);
542
+
543
+ await waitFor(() => {
544
+ expect(mapOnReader.get("key")).toBe("admin-value");
545
+ });
546
+
547
+ mapOnReader.set("key", "reader-value", "trusting");
548
+
549
+ // Permission-invalid transactions are never included
550
+ const allTx = mapOnReader.core.getValidSortedTransactions({
551
+ includeInvalidMetaTransactions: true,
552
+ ignorePrivateTransactions: false,
553
+ });
554
+
555
+ expect(allTx).toHaveLength(1);
556
+ expect(mapOnReader.get("key")).toBe("admin-value");
557
+ });
558
+
559
+ test("different FWW keys are independent", () => {
560
+ const client = setupTestNode();
561
+ const group = client.node.createGroup();
562
+ const map = group.createMap();
563
+
564
+ // Make two FWW transactions with different keys
565
+ map.core.makeTransaction(
566
+ [{ op: "set", key: "keyA", value: "valueA" }],
567
+ "trusting",
568
+ { fww: "keyA" },
569
+ );
570
+
571
+ map.core.makeTransaction(
572
+ [{ op: "set", key: "keyB", value: "valueB" }],
573
+ "trusting",
574
+ { fww: "keyB" },
575
+ );
576
+
577
+ const validTransactions = map.core.getValidSortedTransactions();
578
+
579
+ // Both transactions should be valid since they have different FWW keys
580
+ expect(validTransactions).toHaveLength(2);
581
+ expect(validTransactions[0]?.meta).toEqual({ fww: "keyA" });
582
+ expect(validTransactions[1]?.meta).toEqual({ fww: "keyB" });
583
+ });
584
+
585
+ test("FWW at non-zero txIndex is valid", () => {
586
+ const client = setupTestNode();
587
+ const group = client.node.createGroup();
588
+ const map = group.createMap();
589
+
590
+ // Make a regular transaction first (no FWW meta)
591
+ map.core.makeTransaction(
592
+ [{ op: "set", key: "setup", value: "regular" }],
593
+ "trusting",
594
+ );
595
+
596
+ // Make an FWW transaction at txIndex > 0
597
+ map.core.makeTransaction(
598
+ [{ op: "set", key: "version", value: "init" }],
599
+ "trusting",
600
+ { fww: "init" },
601
+ );
602
+
603
+ const validTransactions = map.core.getValidSortedTransactions();
604
+
605
+ // Both transactions should be valid
606
+ expect(validTransactions).toHaveLength(2);
607
+
608
+ // The FWW transaction should be valid even though it's not at txIndex 0
609
+ const fwwTx = validTransactions.find((tx) => tx.meta?.fww === "init");
610
+ expect(fwwTx).toBeDefined();
611
+ expect(fwwTx?.isValid).toBe(true);
612
+ });
613
+
614
+ test("multiple FWW keys with different winners", async () => {
615
+ const client = setupTestNode({ connected: true });
616
+ const clientSession2 = client.spawnNewSession();
617
+ const group = client.node.createGroup();
618
+ const map = group.createMap();
619
+
620
+ const mapOnClientSession2 = await loadCoValueOrFail(
621
+ clientSession2.node,
622
+ map.id,
623
+ );
624
+
625
+ const earlierTime = Date.now();
626
+ const laterTime = earlierTime + 100;
627
+
628
+ // Session 1: Make FWW transaction for "keyA" with earlier time
629
+ map.core.makeTransaction(
630
+ [{ op: "set", key: "keyA", value: "session1" }],
631
+ "trusting",
632
+ { fww: "keyA" },
633
+ earlierTime,
634
+ );
635
+
636
+ // Session 1: Make FWW transaction for "keyB" with later time
637
+ map.core.makeTransaction(
638
+ [{ op: "set", key: "keyB", value: "session1" }],
639
+ "trusting",
640
+ { fww: "keyB" },
641
+ laterTime,
642
+ );
643
+
644
+ // Session 2: Make FWW transaction for "keyA" with later time
645
+ mapOnClientSession2.core.makeTransaction(
646
+ [{ op: "set", key: "keyA", value: "session2" }],
647
+ "trusting",
648
+ { fww: "keyA" },
649
+ laterTime,
650
+ );
651
+
652
+ // Session 2: Make FWW transaction for "keyB" with earlier time
653
+ mapOnClientSession2.core.makeTransaction(
654
+ [{ op: "set", key: "keyB", value: "session2" }],
655
+ "trusting",
656
+ { fww: "keyB" },
657
+ earlierTime,
658
+ );
659
+
660
+ await waitFor(() => {
661
+ expect(map.core.knownState()).toEqual(
662
+ mapOnClientSession2.core.knownState(),
663
+ );
664
+ });
665
+
666
+ // Each key should independently pick its winner
667
+ // keyA: session1 wins (earlier time)
668
+ // keyB: session2 wins (earlier time)
669
+ const validTransactions = map.core.getValidSortedTransactions();
670
+ expect(validTransactions).toHaveLength(2);
671
+
672
+ const keyAWinner = validTransactions.find((tx) => tx.meta?.fww === "keyA");
673
+ const keyBWinner = validTransactions.find((tx) => tx.meta?.fww === "keyB");
674
+
675
+ expect(keyAWinner).toBeDefined();
676
+ expect(keyBWinner).toBeDefined();
677
+
678
+ // Verify the correct values are in the map
679
+ expect(map.get("keyA")).toBe("session1");
680
+ expect(map.get("keyB")).toBe("session2");
681
+ });
682
+
683
+ test("multiple FWW keys resolve independently across synced nodes", async () => {
684
+ const { node1, node2 } = await createTwoConnectedNodes("server", "server");
685
+
686
+ const group = node1.node.createGroup();
687
+ group.addMember("everyone", "writer");
688
+
689
+ const map = group.createMap();
690
+
691
+ await map.core.waitForSync();
692
+
693
+ // Load the map on node2
694
+ const mapOnNode2 = await loadCoValueOrFail(node2.node, map.id);
695
+
696
+ const earlierTime = Date.now();
697
+ const laterTime = earlierTime + 1000;
698
+
699
+ // On node1: create FWW transaction for "keyA" with earlier time, "keyB" with later time
700
+ map.core.makeTransaction(
701
+ [{ op: "set", key: "keyA", value: "node1" }],
702
+ "trusting",
703
+ { fww: "keyA" },
704
+ earlierTime,
705
+ );
706
+
707
+ map.core.makeTransaction(
708
+ [{ op: "set", key: "keyB", value: "node1" }],
709
+ "trusting",
710
+ { fww: "keyB" },
711
+ laterTime,
712
+ );
713
+
714
+ // On node2: create FWW transaction for "keyA" with later time, "keyB" with earlier time
715
+ mapOnNode2.core.makeTransaction(
716
+ [{ op: "set", key: "keyA", value: "node2" }],
717
+ "trusting",
718
+ { fww: "keyA" },
719
+ laterTime,
720
+ );
721
+
722
+ mapOnNode2.core.makeTransaction(
723
+ [{ op: "set", key: "keyB", value: "node2" }],
724
+ "trusting",
725
+ { fww: "keyB" },
726
+ earlierTime,
727
+ );
728
+
729
+ // Wait for sync
730
+ await map.core.waitForSync();
731
+ await mapOnNode2.core.waitForSync();
732
+
733
+ // Wait for microtasks
734
+ await new Promise<void>((resolve) => queueMicrotask(resolve));
735
+
736
+ // Both nodes should converge to the same state
737
+ // keyA: node1 wins (earlier timestamp)
738
+ // keyB: node2 wins (earlier timestamp)
739
+ await waitFor(() => {
740
+ expect(map.get("keyA")).toBe("node1");
741
+ expect(map.get("keyB")).toBe("node2");
742
+ expect(mapOnNode2.get("keyA")).toBe("node1");
743
+ expect(mapOnNode2.get("keyB")).toBe("node2");
744
+ });
745
+
746
+ // Verify both nodes have exactly 2 valid transactions (one per key)
747
+ const node1ValidTxs = map.core.getValidSortedTransactions();
748
+ const node2ValidTxs = mapOnNode2.core.getValidSortedTransactions();
749
+
750
+ expect(node1ValidTxs).toHaveLength(2);
751
+ expect(node2ValidTxs).toHaveLength(2);
752
+ });
753
+
754
+ test("late-arriving winner for one key does not affect other keys", async () => {
755
+ const client = setupTestNode({ connected: true });
756
+ const clientSession2 = client.spawnNewSession();
757
+ const group = client.node.createGroup();
758
+ const map = group.createMap();
759
+
760
+ const mapOnClientSession2 = await loadCoValueOrFail(
761
+ clientSession2.node,
762
+ map.id,
763
+ );
764
+
765
+ const earlierTime = Date.now();
766
+ const laterTime = earlierTime + 1000;
767
+
768
+ // Session 1: Create FWW transaction for "keyA" with later time
769
+ map.core.makeTransaction(
770
+ [{ op: "set", key: "keyA", value: "session1" }],
771
+ "trusting",
772
+ { fww: "keyA" },
773
+ laterTime,
774
+ );
775
+
776
+ // Session 1: Create FWW transaction for "keyB" (any time)
777
+ map.core.makeTransaction(
778
+ [{ op: "set", key: "keyB", value: "session1" }],
779
+ "trusting",
780
+ { fww: "keyB" },
781
+ earlierTime,
782
+ );
783
+
784
+ // Verify initial state
785
+ expect(map.get("keyA")).toBe("session1");
786
+ expect(map.get("keyB")).toBe("session1");
787
+
788
+ // Session 2: Create FWW transaction for "keyA" with earlier time (this should win)
789
+ mapOnClientSession2.core.makeTransaction(
790
+ [{ op: "set", key: "keyA", value: "session2" }],
791
+ "trusting",
792
+ { fww: "keyA" },
793
+ earlierTime,
794
+ );
795
+
796
+ await waitFor(() => {
797
+ expect(map.core.knownState()).toEqual(
798
+ mapOnClientSession2.core.knownState(),
799
+ );
800
+ });
801
+
802
+ // After sync:
803
+ // keyA: session2's transaction wins (earlier timestamp), session1's is invalidated
804
+ // keyB: session1's transaction remains valid (unaffected)
805
+ expect(map.get("keyA")).toBe("session2");
806
+ expect(map.get("keyB")).toBe("session1");
807
+
808
+ // Check that both keys have exactly one valid transaction each
809
+ const allTransactions = map.core.getValidSortedTransactions({
810
+ includeInvalidMetaTransactions: true,
811
+ ignorePrivateTransactions: false,
812
+ });
813
+
814
+ // Should have 3 total transactions: 2 for keyA (one invalid), 1 for keyB (valid)
815
+ expect(allTransactions).toHaveLength(3);
816
+
817
+ // keyA transactions
818
+ const keyATransactions = allTransactions.filter(
819
+ (tx) => tx.meta?.fww === "keyA",
820
+ );
821
+ expect(keyATransactions).toHaveLength(2);
822
+ expect(keyATransactions.filter((tx) => tx.isValid)).toHaveLength(1);
823
+ expect(keyATransactions.filter((tx) => !tx.isValid)).toHaveLength(1);
824
+
825
+ // The winning keyA transaction should be from session2 (earlier time)
826
+ const validKeyA = keyATransactions.find((tx) => tx.isValid);
827
+ expect(validKeyA?.madeAt).toBe(earlierTime);
828
+
829
+ // keyB transaction should be valid (unaffected by keyA's late-arriving winner)
830
+ const keyBTransactions = allTransactions.filter(
831
+ (tx) => tx.meta?.fww === "keyB",
832
+ );
833
+ expect(keyBTransactions).toHaveLength(1);
834
+ expect(keyBTransactions[0]?.isValid).toBe(true);
835
+ });
836
+ });