cojson 0.13.15 → 0.13.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 (62) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +13 -0
  3. package/dist/coValue.d.ts +2 -0
  4. package/dist/coValue.d.ts.map +1 -1
  5. package/dist/coValue.js +1 -0
  6. package/dist/coValue.js.map +1 -1
  7. package/dist/coValueCore.d.ts +1 -0
  8. package/dist/coValueCore.d.ts.map +1 -1
  9. package/dist/coValueCore.js.map +1 -1
  10. package/dist/coValueState.d.ts.map +1 -1
  11. package/dist/coValueState.js +58 -96
  12. package/dist/coValueState.js.map +1 -1
  13. package/dist/coValues/coList.d.ts +7 -3
  14. package/dist/coValues/coList.d.ts.map +1 -1
  15. package/dist/coValues/coList.js +35 -12
  16. package/dist/coValues/coList.js.map +1 -1
  17. package/dist/coValues/coMap.d.ts +1 -0
  18. package/dist/coValues/coMap.d.ts.map +1 -1
  19. package/dist/coValues/coMap.js +2 -0
  20. package/dist/coValues/coMap.js.map +1 -1
  21. package/dist/coValues/coPlainText.d.ts.map +1 -1
  22. package/dist/coValues/coPlainText.js +1 -1
  23. package/dist/coValues/coPlainText.js.map +1 -1
  24. package/dist/coValues/coStream.d.ts +2 -1
  25. package/dist/coValues/coStream.d.ts.map +1 -1
  26. package/dist/coValues/coStream.js +2 -0
  27. package/dist/coValues/coStream.js.map +1 -1
  28. package/dist/coreToCoValue.d.ts +2 -1
  29. package/dist/coreToCoValue.d.ts.map +1 -1
  30. package/dist/localNode.d.ts.map +1 -1
  31. package/dist/localNode.js +21 -14
  32. package/dist/localNode.js.map +1 -1
  33. package/dist/sync.js +3 -3
  34. package/dist/sync.js.map +1 -1
  35. package/dist/tests/coList.test.js +119 -2
  36. package/dist/tests/coList.test.js.map +1 -1
  37. package/dist/tests/coMap.test.js +32 -2
  38. package/dist/tests/coMap.test.js.map +1 -1
  39. package/dist/tests/coStream.test.js +28 -2
  40. package/dist/tests/coStream.test.js.map +1 -1
  41. package/dist/tests/coValueState.test.js +10 -143
  42. package/dist/tests/coValueState.test.js.map +1 -1
  43. package/dist/tests/sync.load.test.js +59 -1
  44. package/dist/tests/sync.load.test.js.map +1 -1
  45. package/dist/tests/sync.mesh.test.js +48 -1
  46. package/dist/tests/sync.mesh.test.js.map +1 -1
  47. package/package.json +1 -1
  48. package/src/coValue.ts +3 -0
  49. package/src/coValueCore.ts +1 -0
  50. package/src/coValueState.ts +67 -122
  51. package/src/coValues/coList.ts +51 -23
  52. package/src/coValues/coMap.ts +4 -0
  53. package/src/coValues/coPlainText.ts +1 -2
  54. package/src/coValues/coStream.ts +3 -1
  55. package/src/localNode.ts +29 -18
  56. package/src/sync.ts +3 -3
  57. package/src/tests/coList.test.ts +184 -2
  58. package/src/tests/coMap.test.ts +54 -2
  59. package/src/tests/coStream.test.ts +56 -2
  60. package/src/tests/coValueState.test.ts +9 -207
  61. package/src/tests/sync.load.test.ts +78 -1
  62. package/src/tests/sync.mesh.test.ts +67 -0
@@ -2,6 +2,7 @@ import { CoID, RawCoValue } from "../coValue.js";
2
2
  import { CoValueCore } from "../coValueCore.js";
3
3
  import { AgentID, SessionID, TransactionID } from "../ids.js";
4
4
  import { JsonObject, JsonValue } from "../jsonValue.js";
5
+ import { CoValueKnownState } from "../sync.js";
5
6
  import { accountOrAgentIDfromSessionID } from "../typeUtils/accountOrAgentIDfromSessionID.js";
6
7
  import { isCoValue } from "../typeUtils/isCoValue.js";
7
8
  import { RawAccountID } from "./account.js";
@@ -41,7 +42,7 @@ type DeletionEntry = {
41
42
  deletionID: OpID;
42
43
  } & DeletionOpPayload;
43
44
 
44
- export class RawCoListView<
45
+ export class RawCoList<
45
46
  Item extends JsonValue = JsonValue,
46
47
  Meta extends JsonObject | null = null,
47
48
  > implements RawCoValue
@@ -81,26 +82,52 @@ export class RawCoListView<
81
82
  madeAt: number;
82
83
  opID: OpID;
83
84
  }[];
85
+ /** @internal */
86
+ totalValidTransactions = 0;
87
+ knownTransactions: CoValueKnownState["sessions"] = {};
88
+ lastValidTransaction: number | undefined;
84
89
 
85
90
  /** @internal */
86
91
  constructor(core: CoValueCore) {
87
92
  this.id = core.id as CoID<this>;
88
93
  this.core = core;
89
- this.afterStart = [];
90
- this.beforeEnd = [];
91
- this.insertions = {};
92
- this.deletionsByInsertion = {};
93
94
 
94
95
  this.insertions = {};
95
96
  this.deletionsByInsertion = {};
96
97
  this.afterStart = [];
97
98
  this.beforeEnd = [];
99
+ this.knownTransactions = {};
100
+
101
+ this.processNewTransactions();
102
+ }
103
+
104
+ processNewTransactions() {
105
+ const transactions = this.core.getValidSortedTransactions({
106
+ ignorePrivateTransactions: false,
107
+ knownTransactions: this.knownTransactions,
108
+ });
109
+
110
+ if (transactions.length === 0) {
111
+ return;
112
+ }
113
+
114
+ this.totalValidTransactions += transactions.length;
115
+ let lastValidTransaction: number | undefined = undefined;
116
+ let oldestValidTransaction: number | undefined = undefined;
117
+ this._cachedEntries = undefined;
118
+
119
+ for (const { txID, changes, madeAt } of transactions) {
120
+ lastValidTransaction = Math.max(lastValidTransaction ?? 0, madeAt);
121
+ oldestValidTransaction = Math.min(
122
+ oldestValidTransaction ?? Infinity,
123
+ madeAt,
124
+ );
125
+
126
+ this.knownTransactions[txID.sessionID] = Math.max(
127
+ this.knownTransactions[txID.sessionID] ?? 0,
128
+ txID.txIndex,
129
+ );
98
130
 
99
- for (const {
100
- txID,
101
- changes,
102
- madeAt,
103
- } of this.core.getValidSortedTransactions()) {
104
131
  for (const [changeIdx, changeUntyped] of changes.entries()) {
105
132
  const change = changeUntyped as ListOpPayload<Item>;
106
133
 
@@ -193,6 +220,16 @@ export class RawCoListView<
193
220
  }
194
221
  }
195
222
  }
223
+
224
+ if (
225
+ this.lastValidTransaction &&
226
+ oldestValidTransaction &&
227
+ oldestValidTransaction < this.lastValidTransaction
228
+ ) {
229
+ this.rebuildFromCore();
230
+ } else {
231
+ this.lastValidTransaction = lastValidTransaction;
232
+ }
196
233
  }
197
234
 
198
235
  /** @category 6. Meta */
@@ -407,15 +444,7 @@ export class RawCoListView<
407
444
  listener(content as this);
408
445
  });
409
446
  }
410
- }
411
447
 
412
- export class RawCoList<
413
- Item extends JsonValue = JsonValue,
414
- Meta extends JsonObject | null = JsonObject | null,
415
- >
416
- extends RawCoListView<Item, Meta>
417
- implements RawCoValue
418
- {
419
448
  /** Appends `item` after the item currently at index `after`.
420
449
  *
421
450
  * If `privacy` is `"private"` **(default)**, `item` is encrypted in the transaction, only readable by other members of the group this `CoList` belongs to. Not even sync servers can see the content in plaintext.
@@ -480,8 +509,7 @@ export class RawCoList<
480
509
  }
481
510
 
482
511
  this.core.makeTransaction(changes, privacy);
483
-
484
- this.rebuildFromCore();
512
+ this.processNewTransactions();
485
513
  }
486
514
 
487
515
  /**
@@ -528,7 +556,7 @@ export class RawCoList<
528
556
  privacy,
529
557
  );
530
558
 
531
- this.rebuildFromCore();
559
+ this.processNewTransactions();
532
560
  }
533
561
 
534
562
  /** Deletes the item at index `at`.
@@ -555,7 +583,7 @@ export class RawCoList<
555
583
  privacy,
556
584
  );
557
585
 
558
- this.rebuildFromCore();
586
+ this.processNewTransactions();
559
587
  }
560
588
 
561
589
  replace(
@@ -583,7 +611,7 @@ export class RawCoList<
583
611
  ],
584
612
  privacy,
585
613
  );
586
- this.rebuildFromCore();
614
+ this.processNewTransactions();
587
615
  }
588
616
 
589
617
  /** @internal */
@@ -60,6 +60,8 @@ export class RawCoMapView<
60
60
  /** @category 6. Meta */
61
61
  readonly _shape!: Shape;
62
62
 
63
+ totalValidTransactions = 0;
64
+
63
65
  /** @internal */
64
66
  constructor(
65
67
  core: CoValueCore,
@@ -133,6 +135,8 @@ export class RawCoMapView<
133
135
  }
134
136
  }
135
137
 
138
+ this.totalValidTransactions += newValidTransactions.length;
139
+
136
140
  for (const entries of changedEntries.values()) {
137
141
  entries.sort(this.core.compareTransactions);
138
142
  }
@@ -186,7 +186,6 @@ export class RawCoPlainText<
186
186
  idx = nextIdx;
187
187
  }
188
188
  this.core.makeTransaction(ops, privacy);
189
-
190
- this.rebuildFromCore();
189
+ this.processNewTransactions();
191
190
  }
192
191
  }
@@ -56,6 +56,7 @@ export class RawCoStreamView<
56
56
  };
57
57
  /** @internal */
58
58
  knownTransactions: CoValueKnownState["sessions"];
59
+ totalValidTransactions = 0;
59
60
  readonly _item!: Item;
60
61
 
61
62
  constructor(core: CoValueCore) {
@@ -96,7 +97,7 @@ export class RawCoStreamView<
96
97
  }
97
98
 
98
99
  /** @internal */
99
- protected processNewTransactions() {
100
+ processNewTransactions() {
100
101
  const changeEntries = new Set<CoStreamItem<Item>[]>();
101
102
 
102
103
  const newValidTransactions = this.core.getValidTransactions({
@@ -109,6 +110,7 @@ export class RawCoStreamView<
109
110
  }
110
111
 
111
112
  for (const { txID, madeAt, changes } of newValidTransactions) {
113
+ this.totalValidTransactions++;
112
114
  for (const changeUntyped of changes) {
113
115
  const change = changeUntyped as Item;
114
116
  let entries = this.items[txID.sessionID];
package/src/localNode.ts CHANGED
@@ -266,29 +266,40 @@ export class LocalNode {
266
266
  });
267
267
  }
268
268
 
269
- const entry = this.coValuesStore.get(id);
269
+ let retries = 0;
270
270
 
271
- if (
272
- entry.highLevelState === "unknown" ||
273
- entry.highLevelState === "unavailable"
274
- ) {
275
- const peers =
276
- this.syncManager.getServerAndStoragePeers(skipLoadingFromPeer);
271
+ while (true) {
272
+ const entry = this.coValuesStore.get(id);
277
273
 
278
- if (peers.length === 0) {
279
- return "unavailable";
280
- }
274
+ if (
275
+ entry.highLevelState === "unknown" ||
276
+ entry.highLevelState === "unavailable"
277
+ ) {
278
+ const peers =
279
+ this.syncManager.getServerAndStoragePeers(skipLoadingFromPeer);
281
280
 
282
- await entry.loadFromPeers(peers).catch((e) => {
283
- logger.error("Error loading from peers", {
284
- id,
285
- err: e,
281
+ if (peers.length === 0) {
282
+ return "unavailable";
283
+ }
284
+
285
+ entry.loadFromPeers(peers).catch((e) => {
286
+ logger.error("Error loading from peers", {
287
+ id,
288
+ err: e,
289
+ });
286
290
  });
287
- });
288
- }
291
+ }
292
+
293
+ const result = await entry.getCoValue();
289
294
 
290
- // TODO: What if the loading fails because in the previous loadCoValueCore call the Peer with the covalue was skipped?
291
- return entry.getCoValue();
295
+ if (result !== "unavailable" || retries >= 1) {
296
+ return result;
297
+ }
298
+
299
+ await new Promise((resolve) => setTimeout(resolve, 300));
300
+
301
+ retries++;
302
+ }
292
303
  }
293
304
 
294
305
  /**
package/src/sync.ts CHANGED
@@ -163,13 +163,13 @@ export class SyncManager {
163
163
  `Skipping message ${msg.action} on errored coValue ${msg.id} from peer ${peer.id}`,
164
164
  );
165
165
  return;
166
- } else if (msg.id === undefined) {
167
- logger.info("Received sync message with undefined id", {
166
+ } else if (msg.id === undefined || msg.id === null) {
167
+ logger.warn("Received sync message with undefined id", {
168
168
  msg,
169
169
  });
170
170
  return;
171
171
  } else if (!msg.id.startsWith("co_z")) {
172
- logger.info("Received sync message with invalid id", {
172
+ logger.warn("Received sync message with invalid id", {
173
173
  msg,
174
174
  });
175
175
  return;
@@ -1,11 +1,20 @@
1
- import { expect, test } from "vitest";
1
+ import { beforeEach, expect, test } from "vitest";
2
2
  import { expectList } from "../coValue.js";
3
3
  import { WasmCrypto } from "../crypto/WasmCrypto.js";
4
4
  import { LocalNode } from "../localNode.js";
5
- import { randomAnonymousAccountAndSessionID } from "./testUtils.js";
5
+ import {
6
+ loadCoValueOrFail,
7
+ randomAnonymousAccountAndSessionID,
8
+ setupTestNode,
9
+ waitFor,
10
+ } from "./testUtils.js";
6
11
 
7
12
  const Crypto = await WasmCrypto.create();
8
13
 
14
+ beforeEach(async () => {
15
+ setupTestNode({ isSyncServer: true });
16
+ });
17
+
9
18
  test("Empty CoList works", () => {
10
19
  const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
11
20
 
@@ -221,3 +230,176 @@ test("Items prepended to start appear with latest first", () => {
221
230
 
222
231
  expect(content.toJSON()).toEqual(["third", "second", "first"]);
223
232
  });
233
+
234
+ test("mixing prepend and append", () => {
235
+ const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
236
+
237
+ const coValue = node.createCoValue({
238
+ type: "colist",
239
+ ruleset: { type: "unsafeAllowAll" },
240
+ meta: null,
241
+ ...Crypto.createdNowUnique(),
242
+ });
243
+
244
+ const list = expectList(coValue.getCurrentContent());
245
+
246
+ list.append(2, undefined, "trusting");
247
+ list.prepend(1, undefined, "trusting");
248
+ list.append(3, undefined, "trusting");
249
+
250
+ expect(list.toJSON()).toEqual([1, 2, 3]);
251
+ });
252
+
253
+ test("Items appended to start", () => {
254
+ const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
255
+
256
+ const coValue = node.createCoValue({
257
+ type: "colist",
258
+ ruleset: { type: "unsafeAllowAll" },
259
+ meta: null,
260
+ ...Crypto.createdNowUnique(),
261
+ });
262
+
263
+ const content = expectList(coValue.getCurrentContent());
264
+
265
+ content.append("first", 0, "trusting");
266
+ content.append("second", 0, "trusting");
267
+ content.append("third", 0, "trusting");
268
+
269
+ // This result is correct because "third" is appended after "first"
270
+ // Using the Array methods this would be the same as doing content.splice(1, 0, "third")
271
+ expect(content.toJSON()).toEqual(["first", "third", "second"]);
272
+ });
273
+
274
+ test("syncing appends with an older timestamp", async () => {
275
+ const client = setupTestNode({
276
+ connected: true,
277
+ });
278
+ const otherClient = setupTestNode({});
279
+
280
+ const otherClientConnection = otherClient.connectToSyncServer();
281
+
282
+ const coValue = client.node.createCoValue({
283
+ type: "colist",
284
+ ruleset: { type: "unsafeAllowAll" },
285
+ meta: null,
286
+ ...Crypto.createdNowUnique(),
287
+ });
288
+
289
+ const list = expectList(coValue.getCurrentContent());
290
+
291
+ list.append(1, undefined, "trusting");
292
+ list.append(2, undefined, "trusting");
293
+
294
+ const listOnOtherClient = await loadCoValueOrFail(otherClient.node, list.id);
295
+
296
+ otherClientConnection.peerState.gracefulShutdown();
297
+
298
+ listOnOtherClient.append(3, undefined, "trusting");
299
+
300
+ await new Promise((resolve) => setTimeout(resolve, 50));
301
+
302
+ list.append(4, undefined, "trusting");
303
+
304
+ await new Promise((resolve) => setTimeout(resolve, 50));
305
+
306
+ listOnOtherClient.append(5, undefined, "trusting");
307
+
308
+ await new Promise((resolve) => setTimeout(resolve, 50));
309
+
310
+ list.append(6, undefined, "trusting");
311
+
312
+ otherClient.connectToSyncServer();
313
+
314
+ await waitFor(() => {
315
+ expect(list.toJSON()).toEqual([1, 2, 4, 6, 3, 5]);
316
+ });
317
+
318
+ expect(listOnOtherClient.toJSON()).toEqual(list.toJSON());
319
+ });
320
+
321
+ test("syncing prepends with an older timestamp", async () => {
322
+ const client = setupTestNode({
323
+ connected: true,
324
+ });
325
+ const otherClient = setupTestNode({});
326
+
327
+ const otherClientConnection = otherClient.connectToSyncServer();
328
+
329
+ const coValue = client.node.createCoValue({
330
+ type: "colist",
331
+ ruleset: { type: "unsafeAllowAll" },
332
+ meta: null,
333
+ ...Crypto.createdNowUnique(),
334
+ });
335
+
336
+ const list = expectList(coValue.getCurrentContent());
337
+
338
+ list.prepend(1, undefined, "trusting");
339
+ list.prepend(2, undefined, "trusting");
340
+
341
+ const listOnOtherClient = await loadCoValueOrFail(otherClient.node, list.id);
342
+
343
+ otherClientConnection.peerState.gracefulShutdown();
344
+
345
+ listOnOtherClient.prepend(3, undefined, "trusting");
346
+
347
+ await new Promise((resolve) => setTimeout(resolve, 50));
348
+
349
+ list.prepend(4, undefined, "trusting");
350
+
351
+ await new Promise((resolve) => setTimeout(resolve, 50));
352
+
353
+ listOnOtherClient.prepend(5, undefined, "trusting");
354
+
355
+ await new Promise((resolve) => setTimeout(resolve, 50));
356
+
357
+ list.prepend(6, undefined, "trusting");
358
+
359
+ otherClient.connectToSyncServer();
360
+
361
+ await waitFor(() => {
362
+ expect(list.toJSON()).toEqual([6, 4, 5, 3, 2, 1]);
363
+ });
364
+
365
+ expect(listOnOtherClient.toJSON()).toEqual(list.toJSON());
366
+ });
367
+
368
+ test("totalValidTransactions should return the number of valid transactions processed", async () => {
369
+ const client = setupTestNode({
370
+ connected: true,
371
+ });
372
+ const otherClient = setupTestNode({});
373
+
374
+ const otherClientConnection = otherClient.connectToSyncServer();
375
+
376
+ const group = client.node.createGroup();
377
+ group.addMember("everyone", "reader");
378
+
379
+ const list = group.createList([1, 2]);
380
+
381
+ const listOnOtherClient = await loadCoValueOrFail(otherClient.node, list.id);
382
+
383
+ otherClientConnection.peerState.gracefulShutdown();
384
+
385
+ group.addMember("everyone", "writer");
386
+
387
+ await new Promise((resolve) => setTimeout(resolve, 50));
388
+
389
+ listOnOtherClient.append(3, undefined, "trusting");
390
+
391
+ expect(listOnOtherClient.toJSON()).toEqual([1, 2]);
392
+ expect(listOnOtherClient.totalValidTransactions).toEqual(1);
393
+
394
+ otherClient.connectToSyncServer();
395
+
396
+ await waitFor(() => {
397
+ expect(listOnOtherClient.core.getCurrentContent().toJSON()).toEqual([
398
+ 1, 2, 3,
399
+ ]);
400
+ });
401
+
402
+ expect(
403
+ listOnOtherClient.core.getCurrentContent().totalValidTransactions,
404
+ ).toEqual(2);
405
+ });
@@ -1,13 +1,23 @@
1
- import { expect, test } from "vitest";
1
+ import { beforeEach, expect, test } from "vitest";
2
2
  import { expectMap } from "../coValue.js";
3
3
  import { operationToEditEntry } from "../coValues/coMap.js";
4
4
  import { WasmCrypto } from "../crypto/WasmCrypto.js";
5
5
  import { LocalNode } from "../localNode.js";
6
6
  import { accountOrAgentIDfromSessionID } from "../typeUtils/accountOrAgentIDfromSessionID.js";
7
- import { hotSleep, randomAnonymousAccountAndSessionID } from "./testUtils.js";
7
+ import {
8
+ hotSleep,
9
+ loadCoValueOrFail,
10
+ randomAnonymousAccountAndSessionID,
11
+ setupTestNode,
12
+ waitFor,
13
+ } from "./testUtils.js";
8
14
 
9
15
  const Crypto = await WasmCrypto.create();
10
16
 
17
+ beforeEach(async () => {
18
+ setupTestNode({ isSyncServer: true });
19
+ });
20
+
11
21
  test("Empty CoMap works", () => {
12
22
  const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
13
23
 
@@ -207,3 +217,45 @@ test("Can set items in bulk with assign", () => {
207
217
  key3: "assign3",
208
218
  });
209
219
  });
220
+
221
+ test("totalValidTransactions should return the number of valid transactions processed", async () => {
222
+ const client = setupTestNode({
223
+ connected: true,
224
+ });
225
+ const otherClient = setupTestNode({});
226
+
227
+ const otherClientConnection = otherClient.connectToSyncServer();
228
+
229
+ const group = client.node.createGroup();
230
+ group.addMember("everyone", "reader");
231
+
232
+ const map = group.createMap({ fromClient: true });
233
+
234
+ const mapOnOtherClient = await loadCoValueOrFail(otherClient.node, map.id);
235
+
236
+ otherClientConnection.peerState.gracefulShutdown();
237
+
238
+ group.addMember("everyone", "writer");
239
+
240
+ await new Promise((resolve) => setTimeout(resolve, 50));
241
+
242
+ mapOnOtherClient.set("fromOtherClient", true, "trusting");
243
+
244
+ expect(mapOnOtherClient.totalValidTransactions).toEqual(1);
245
+ expect(mapOnOtherClient.toJSON()).toEqual({
246
+ fromClient: true,
247
+ });
248
+
249
+ otherClient.connectToSyncServer();
250
+
251
+ await waitFor(() => {
252
+ expect(mapOnOtherClient.core.getCurrentContent().toJSON()).toEqual({
253
+ fromClient: true,
254
+ fromOtherClient: true,
255
+ });
256
+ });
257
+
258
+ expect(
259
+ mapOnOtherClient.core.getCurrentContent().totalValidTransactions,
260
+ ).toEqual(2);
261
+ });
@@ -1,4 +1,4 @@
1
- import { describe, expect, test } from "vitest";
1
+ import { beforeEach, describe, expect, test } from "vitest";
2
2
  import { expectStream } from "../coValue.js";
3
3
  import { MAX_RECOMMENDED_TX_SIZE } from "../coValueCore.js";
4
4
  import {
@@ -10,10 +10,19 @@ import {
10
10
  import { WasmCrypto } from "../crypto/WasmCrypto.js";
11
11
  import { SessionID } from "../ids.js";
12
12
  import { LocalNode } from "../localNode.js";
13
- import { randomAnonymousAccountAndSessionID } from "./testUtils.js";
13
+ import {
14
+ loadCoValueOrFail,
15
+ randomAnonymousAccountAndSessionID,
16
+ setupTestNode,
17
+ waitFor,
18
+ } from "./testUtils.js";
14
19
 
15
20
  const Crypto = await WasmCrypto.create();
16
21
 
22
+ beforeEach(async () => {
23
+ setupTestNode({ isSyncServer: true });
24
+ });
25
+
17
26
  test("Empty CoStream works", () => {
18
27
  const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
19
28
 
@@ -252,6 +261,51 @@ test("When adding large transactions (bigger than MAX_RECOMMENDED_TX_SIZE), we s
252
261
  );
253
262
  });
254
263
 
264
+ test("totalValidTransactions should return the number of valid transactions processed", async () => {
265
+ const client = setupTestNode({
266
+ connected: true,
267
+ });
268
+ const otherClient = setupTestNode({});
269
+
270
+ const otherClientConnection = otherClient.connectToSyncServer();
271
+
272
+ const group = client.node.createGroup();
273
+ group.addMember("everyone", "reader");
274
+
275
+ const stream = group.createStream();
276
+ stream.push(1, "trusting");
277
+
278
+ const streamOnOtherClient = await loadCoValueOrFail(
279
+ otherClient.node,
280
+ stream.id,
281
+ );
282
+
283
+ otherClientConnection.peerState.gracefulShutdown();
284
+
285
+ group.addMember("everyone", "writer");
286
+
287
+ await new Promise((resolve) => setTimeout(resolve, 50));
288
+
289
+ streamOnOtherClient.push(2, "trusting");
290
+
291
+ expect(streamOnOtherClient.totalValidTransactions).toEqual(1);
292
+ expect(Object.keys(streamOnOtherClient.toJSON()).length).toEqual(1);
293
+
294
+ otherClient.connectToSyncServer();
295
+
296
+ await waitFor(() => {
297
+ expect(
298
+ Object.keys(
299
+ streamOnOtherClient.core.getCurrentContent().toJSON() as object,
300
+ ).length,
301
+ ).toEqual(2);
302
+ });
303
+
304
+ expect(
305
+ streamOnOtherClient.core.getCurrentContent().totalValidTransactions,
306
+ ).toEqual(2);
307
+ });
308
+
255
309
  describe("isBinaryStreamEnded", () => {
256
310
  function setup() {
257
311
  const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);