cojson 0.12.1 → 0.13.2

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 (40) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +22 -0
  3. package/dist/PeerState.d.ts +1 -7
  4. package/dist/PeerState.d.ts.map +1 -1
  5. package/dist/PeerState.js +5 -3
  6. package/dist/PeerState.js.map +1 -1
  7. package/dist/PriorityBasedMessageQueue.d.ts +17 -3
  8. package/dist/PriorityBasedMessageQueue.d.ts.map +1 -1
  9. package/dist/PriorityBasedMessageQueue.js +57 -29
  10. package/dist/PriorityBasedMessageQueue.js.map +1 -1
  11. package/dist/coValueCore.d.ts +1 -5
  12. package/dist/coValueCore.d.ts.map +1 -1
  13. package/dist/coValueCore.js +29 -43
  14. package/dist/coValueCore.js.map +1 -1
  15. package/dist/coValueState.d.ts.map +1 -1
  16. package/dist/coValueState.js +18 -4
  17. package/dist/coValueState.js.map +1 -1
  18. package/dist/crypto/crypto.d.ts.map +1 -1
  19. package/dist/crypto/crypto.js +3 -1
  20. package/dist/crypto/crypto.js.map +1 -1
  21. package/dist/priority.d.ts +2 -2
  22. package/dist/priority.d.ts.map +1 -1
  23. package/dist/priority.js +1 -1
  24. package/dist/sync.d.ts.map +1 -1
  25. package/dist/sync.js +21 -28
  26. package/dist/sync.js.map +1 -1
  27. package/dist/tests/PriorityBasedMessageQueue.test.js +86 -22
  28. package/dist/tests/PriorityBasedMessageQueue.test.js.map +1 -1
  29. package/dist/tests/sync.test.js +51 -12
  30. package/dist/tests/sync.test.js.map +1 -1
  31. package/package.json +2 -3
  32. package/src/PeerState.ts +11 -7
  33. package/src/PriorityBasedMessageQueue.ts +75 -35
  34. package/src/coValueCore.ts +50 -60
  35. package/src/coValueState.ts +23 -4
  36. package/src/crypto/crypto.ts +4 -1
  37. package/src/priority.ts +2 -2
  38. package/src/sync.ts +21 -38
  39. package/src/tests/PriorityBasedMessageQueue.test.ts +127 -34
  40. package/src/tests/sync.test.ts +64 -21
@@ -176,11 +176,12 @@ export class CoValueState {
176
176
  async loadFromPeers(peers: PeerState[]) {
177
177
  const state = this.state;
178
178
 
179
- if (state.type !== "unknown" && state.type !== "unavailable") {
179
+ if (state.type === "loading" || state.type === "available") {
180
180
  return;
181
181
  }
182
182
 
183
183
  if (peers.length === 0) {
184
+ this.moveToState(new CoValueUnavailableState());
184
185
  return;
185
186
  }
186
187
 
@@ -192,7 +193,11 @@ export class CoValueState {
192
193
 
193
194
  // If we are in the loading state we move to a new loading state
194
195
  // to reset all the loading promises
195
- if (this.state.type === "loading" || this.state.type === "unknown") {
196
+ if (
197
+ this.state.type === "loading" ||
198
+ this.state.type === "unknown" ||
199
+ this.state.type === "unavailable"
200
+ ) {
196
201
  this.moveToState(
197
202
  new CoValueLoadingState(peersWithoutErrors.map((p) => p.id)),
198
203
  );
@@ -308,6 +313,19 @@ async function loadCoValueFromPeers(
308
313
  }
309
314
 
310
315
  if (coValueEntry.state.type === "loading") {
316
+ const { promise, resolve } = createResolvablePromise<void>();
317
+
318
+ /**
319
+ * Use a very long timeout for storage peers, because under pressure
320
+ * they may take a long time to consume the messages queue
321
+ *
322
+ * TODO: Track errors on storage and do not rely on timeout
323
+ */
324
+ const timeoutDuration =
325
+ peer.role === "storage"
326
+ ? CO_VALUE_LOADING_CONFIG.TIMEOUT * 10
327
+ : CO_VALUE_LOADING_CONFIG.TIMEOUT;
328
+
311
329
  const timeout = setTimeout(() => {
312
330
  if (coValueEntry.state.type === "loading") {
313
331
  logger.warn("Failed to load coValue from peer", {
@@ -319,9 +337,10 @@ async function loadCoValueFromPeers(
319
337
  type: "not-found-in-peer",
320
338
  peerId: peer.id,
321
339
  });
340
+ resolve();
322
341
  }
323
- }, CO_VALUE_LOADING_CONFIG.TIMEOUT);
324
- await coValueEntry.state.waitForPeer(peer.id);
342
+ }, timeoutDuration);
343
+ await Promise.race([promise, coValueEntry.state.waitForPeer(peer.id)]);
325
344
  clearTimeout(timeout);
326
345
  }
327
346
  }
@@ -1,4 +1,3 @@
1
- import { randomBytes } from "@noble/ciphers/webcrypto/utils";
2
1
  import { base58 } from "@scure/base";
3
2
  import { RawAccountID } from "../coValues/account.js";
4
3
  import { AgentID, RawCoID, TransactionID } from "../ids.js";
@@ -7,6 +6,10 @@ import { Stringified, parseJSON, stableStringify } from "../jsonStringify.js";
7
6
  import { JsonValue } from "../jsonValue.js";
8
7
  import { logger } from "../logger.js";
9
8
 
9
+ function randomBytes(bytesLength = 32): Uint8Array {
10
+ return crypto.getRandomValues(new Uint8Array(bytesLength));
11
+ }
12
+
10
13
  export type SignerSecret = `signerSecret_z${string}`;
11
14
  export type SignerID = `signer_z${string}`;
12
15
  export type Signature = `signature_z${string}`;
package/src/priority.ts CHANGED
@@ -7,7 +7,7 @@ import { type CoValueHeader } from "./coValueCore.js";
7
7
  * The priority value is handled as weight in the weighed round robin algorithm
8
8
  * used to determine the order in which messages are sent.
9
9
  *
10
- * Follows the HTTP urgency range and order:
10
+ * Loosely follows the HTTP urgency range and order, but limited to 3 values:
11
11
  * - https://www.rfc-editor.org/rfc/rfc9218.html#name-urgency
12
12
  */
13
13
  export const CO_VALUE_PRIORITY = {
@@ -16,7 +16,7 @@ export const CO_VALUE_PRIORITY = {
16
16
  LOW: 6,
17
17
  } as const;
18
18
 
19
- export type CoValuePriority = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
19
+ export type CoValuePriority = 0 | 3 | 6;
20
20
 
21
21
  export function getPriorityFromHeader(
22
22
  header: CoValueHeader | undefined | boolean,
package/src/sync.ts CHANGED
@@ -23,10 +23,6 @@ export function emptyKnownState(id: RawCoID): CoValueKnownState {
23
23
  };
24
24
  }
25
25
 
26
- function getErrorMessage(e: unknown) {
27
- return e instanceof Error ? e.message : "Unknown error";
28
- }
29
-
30
26
  export type SyncMessage =
31
27
  | LoadMessage
32
28
  | KnownStateMessage
@@ -415,7 +411,20 @@ export class SyncManager {
415
411
  entry.loadFromPeers([peer]).catch((e) => {
416
412
  logger.error("Error loading coValue in handleLoad", { err: e });
417
413
  });
414
+ } else {
415
+ // We don't have any eligible peers to load the coValue from
416
+ // so we send a known state back to the sender to let it know
417
+ // that the coValue is unavailable
418
+ this.trySendToPeer(peer, {
419
+ action: "known",
420
+ id: msg.id,
421
+ header: false,
422
+ sessions: {},
423
+ }).catch((e) => {
424
+ logger.error("Error sending known state back", { err: e });
425
+ });
418
426
  }
427
+
419
428
  return;
420
429
  } else {
421
430
  this.local.loadCoValueCore(msg.id, peer.id).catch((e) => {
@@ -460,6 +469,13 @@ export class SyncManager {
460
469
  err: e,
461
470
  });
462
471
  });
472
+ } else if (entry.state.type === "unavailable") {
473
+ this.trySendToPeer(peer, {
474
+ action: "known",
475
+ id: msg.id,
476
+ header: false,
477
+ sessions: {},
478
+ });
463
479
  }
464
480
 
465
481
  if (entry.state.type === "available") {
@@ -604,39 +620,12 @@ export class SyncManager {
604
620
  continue;
605
621
  }
606
622
 
607
- const before = performance.now();
608
- // eslint-disable-next-line neverthrow/must-use-result
609
623
  const result = coValue.tryAddTransactions(
610
624
  sessionID,
611
625
  newTransactions,
612
626
  undefined,
613
627
  newContentForSession.lastSignature,
614
628
  );
615
- const after = performance.now();
616
- if (after - before > 80) {
617
- const totalTxLength = newTransactions
618
- .map((t) =>
619
- t.privacy === "private"
620
- ? t.encryptedChanges.length
621
- : t.changes.length,
622
- )
623
- .reduce((a, b) => a + b, 0);
624
- logger.debug(
625
- `Adding incoming transactions took ${(after - before).toFixed(
626
- 2,
627
- )}ms for ${totalTxLength} bytes = bandwidth: ${(
628
- (1000 * totalTxLength) / (after - before) / (1024 * 1024)
629
- ).toFixed(2)} MB/s`,
630
- );
631
- }
632
-
633
- // const theirTotalnTxs = Object.values(
634
- // peer.optimisticKnownStates[msg.id]?.sessions || {},
635
- // ).reduce((sum, nTxs) => sum + nTxs, 0);
636
- // const ourTotalnTxs = [...coValue.sessionLogs.values()].reduce(
637
- // (sum, session) => sum + session.transactions.length,
638
- // 0,
639
- // );
640
629
 
641
630
  if (result.isErr()) {
642
631
  logger.error("Failed to add transactions", {
@@ -735,16 +724,10 @@ export class SyncManager {
735
724
  }
736
725
 
737
726
  async actuallySyncCoValue(coValue: CoValueCore) {
738
- // let blockingSince = performance.now();
739
727
  for (const peer of this.peersInPriorityOrder()) {
740
728
  if (peer.closed) continue;
741
729
  if (peer.erroredCoValues.has(coValue.id)) continue;
742
- // if (performance.now() - blockingSince > 5) {
743
- // await new Promise<void>((resolve) => {
744
- // setTimeout(resolve, 0);
745
- // });
746
- // blockingSince = performance.now();
747
- // }
730
+
748
731
  if (peer.optimisticKnownStates.has(coValue.id)) {
749
732
  await this.tellUntoldKnownStateIncludingDependencies(coValue.id, peer);
750
733
  await this.sendNewContentIncludingDependencies(coValue.id, peer);
@@ -7,9 +7,9 @@ import {
7
7
  tearDownTestMetricReader,
8
8
  } from "./testUtils.js";
9
9
 
10
- function setup() {
10
+ function setup(attrs?: Record<string, string | number>) {
11
11
  const metricReader = createTestMetricReader();
12
- const queue = new PriorityBasedMessageQueue(CO_VALUE_PRIORITY.MEDIUM);
12
+ const queue = new PriorityBasedMessageQueue(CO_VALUE_PRIORITY.MEDIUM, attrs);
13
13
  return { queue, metricReader };
14
14
  }
15
15
 
@@ -18,10 +18,133 @@ describe("PriorityBasedMessageQueue", () => {
18
18
  tearDownTestMetricReader();
19
19
  });
20
20
 
21
+ describe("meteredQueue", () => {
22
+ test("should corretly count pushes", async () => {
23
+ const { queue, metricReader } = setup();
24
+ const message: SyncMessage = {
25
+ action: "load",
26
+ id: "co_ztest-id",
27
+ header: false,
28
+ sessions: {},
29
+ };
30
+
31
+ expect(
32
+ await metricReader.getMetricValue("jazz.messagequeue.pushed", {
33
+ priority: CO_VALUE_PRIORITY.MEDIUM,
34
+ }),
35
+ ).toBe(0);
36
+
37
+ void queue.push(message);
38
+ expect(
39
+ await metricReader.getMetricValue("jazz.messagequeue.pushed", {
40
+ priority: CO_VALUE_PRIORITY.MEDIUM,
41
+ }),
42
+ ).toBe(1);
43
+
44
+ void queue.push(message);
45
+ expect(
46
+ await metricReader.getMetricValue("jazz.messagequeue.pushed", {
47
+ priority: CO_VALUE_PRIORITY.MEDIUM,
48
+ }),
49
+ ).toBe(2);
50
+ });
51
+
52
+ test("should corretly count pulls", async () => {
53
+ const { queue, metricReader } = setup();
54
+ const message: SyncMessage = {
55
+ action: "load",
56
+ id: "co_ztest-id",
57
+ header: false,
58
+ sessions: {},
59
+ };
60
+
61
+ expect(
62
+ await metricReader.getMetricValue("jazz.messagequeue.pulled", {
63
+ priority: CO_VALUE_PRIORITY.MEDIUM,
64
+ }),
65
+ ).toBe(0);
66
+
67
+ void queue.push(message);
68
+ expect(
69
+ await metricReader.getMetricValue("jazz.messagequeue.pulled", {
70
+ priority: CO_VALUE_PRIORITY.MEDIUM,
71
+ }),
72
+ ).toBe(0);
73
+
74
+ void queue.pull();
75
+
76
+ expect(
77
+ await metricReader.getMetricValue("jazz.messagequeue.pulled", {
78
+ priority: CO_VALUE_PRIORITY.MEDIUM,
79
+ }),
80
+ ).toBe(1);
81
+
82
+ // We only have one item in the queue, so this should not change the metric value
83
+ void queue.pull();
84
+ expect(
85
+ await metricReader.getMetricValue("jazz.messagequeue.pulled", {
86
+ priority: CO_VALUE_PRIORITY.MEDIUM,
87
+ }),
88
+ ).toBe(1);
89
+ });
90
+
91
+ test("should corretly set custom attributes to the metrics", async () => {
92
+ const { queue, metricReader } = setup({ role: "server" });
93
+ const message: SyncMessage = {
94
+ action: "load",
95
+ id: "co_ztest-id",
96
+ header: false,
97
+ sessions: {},
98
+ };
99
+
100
+ expect(
101
+ await metricReader.getMetricValue("jazz.messagequeue.pushed", {
102
+ priority: CO_VALUE_PRIORITY.MEDIUM,
103
+ role: "server",
104
+ }),
105
+ ).toBe(0);
106
+ expect(
107
+ await metricReader.getMetricValue("jazz.messagequeue.pushed", {
108
+ priority: CO_VALUE_PRIORITY.MEDIUM,
109
+ role: "client",
110
+ }),
111
+ ).toBeUndefined();
112
+
113
+ void queue.push(message);
114
+ expect(
115
+ await metricReader.getMetricValue("jazz.messagequeue.pushed", {
116
+ priority: CO_VALUE_PRIORITY.MEDIUM,
117
+ role: "server",
118
+ }),
119
+ ).toBe(1);
120
+ expect(
121
+ await metricReader.getMetricValue("jazz.messagequeue.pulled", {
122
+ priority: CO_VALUE_PRIORITY.MEDIUM,
123
+ role: "server",
124
+ }),
125
+ ).toBe(0);
126
+
127
+ void queue.pull();
128
+
129
+ expect(
130
+ await metricReader.getMetricValue("jazz.messagequeue.pushed", {
131
+ priority: CO_VALUE_PRIORITY.MEDIUM,
132
+ role: "server",
133
+ }),
134
+ ).toBe(1);
135
+ expect(
136
+ await metricReader.getMetricValue("jazz.messagequeue.pulled", {
137
+ priority: CO_VALUE_PRIORITY.MEDIUM,
138
+ role: "server",
139
+ }),
140
+ ).toBe(1);
141
+ });
142
+ });
143
+
21
144
  test("should initialize with correct properties", () => {
22
145
  const { queue } = setup();
23
146
  expect(queue["defaultPriority"]).toBe(CO_VALUE_PRIORITY.MEDIUM);
24
- expect(queue["queues"].length).toBe(8);
147
+ expect(queue["queues"].length).toBe(3);
25
148
  expect(queue["queues"].every((q) => !q.length)).toBe(true);
26
149
  });
27
150
 
@@ -52,7 +175,7 @@ describe("PriorityBasedMessageQueue", () => {
52
175
  });
53
176
 
54
177
  test("should pull messages in priority order", async () => {
55
- const { queue, metricReader } = setup();
178
+ const { queue } = setup();
56
179
  const lowPriorityMsg: SyncMessage = {
57
180
  action: "content",
58
181
  id: "co_zlow",
@@ -73,42 +196,12 @@ describe("PriorityBasedMessageQueue", () => {
73
196
  };
74
197
 
75
198
  void queue.push(lowPriorityMsg);
76
- expect(
77
- await metricReader.getMetricValue("jazz.messagequeue.size", {
78
- priority: lowPriorityMsg.priority,
79
- }),
80
- ).toBe(1);
81
199
  void queue.push(mediumPriorityMsg);
82
- expect(
83
- await metricReader.getMetricValue("jazz.messagequeue.size", {
84
- priority: mediumPriorityMsg.priority,
85
- }),
86
- ).toBe(1);
87
200
  void queue.push(highPriorityMsg);
88
- expect(
89
- await metricReader.getMetricValue("jazz.messagequeue.size", {
90
- priority: highPriorityMsg.priority,
91
- }),
92
- ).toBe(1);
93
201
 
94
202
  expect(queue.pull()?.msg).toEqual(highPriorityMsg);
95
- expect(
96
- await metricReader.getMetricValue("jazz.messagequeue.size", {
97
- priority: highPriorityMsg.priority,
98
- }),
99
- ).toBe(0);
100
203
  expect(queue.pull()?.msg).toEqual(mediumPriorityMsg);
101
- expect(
102
- await metricReader.getMetricValue("jazz.messagequeue.size", {
103
- priority: mediumPriorityMsg.priority,
104
- }),
105
- ).toBe(0);
106
204
  expect(queue.pull()?.msg).toEqual(lowPriorityMsg);
107
- expect(
108
- await metricReader.getMetricValue("jazz.messagequeue.size", {
109
- priority: lowPriorityMsg.priority,
110
- }),
111
- ).toBe(0);
112
205
  });
113
206
 
114
207
  test("should return undefined when pulling from empty queue", () => {
@@ -956,27 +956,6 @@ test.skip("When a peer's incoming/readable stream closes, we remove the peer", a
956
956
  */
957
957
  });
958
958
 
959
- test("If we start loading a coValue before connecting to a peer that has it, it will load it once we connect", async () => {
960
- const { node: node1 } = await createConnectedTestNode();
961
-
962
- const group = node1.createGroup();
963
-
964
- const map = group.createMap();
965
- map.set("hello", "world", "trusting");
966
-
967
- const node2 = createTestNode();
968
-
969
- const mapOnNode2Promise = loadCoValueOrFail(node2, map.id);
970
-
971
- expect(node2.coValuesStore.get(map.core.id).state.type).toEqual("unknown");
972
-
973
- connectNodeToSyncServer(node2);
974
-
975
- const mapOnNode2 = await mapOnNode2Promise;
976
-
977
- expect(mapOnNode2.get("hello")).toEqual("world");
978
- });
979
-
980
959
  test("should keep the peer state when the peer closes", async () => {
981
960
  const client = createTestNode();
982
961
 
@@ -1726,6 +1705,45 @@ describe("loadCoValueCore with retry", () => {
1726
1705
  await expect(promise1).resolves.not.toBe("unavailable");
1727
1706
  await expect(promise2).resolves.not.toBe("unavailable");
1728
1707
  });
1708
+
1709
+ test("should load unavailable coValues after they are synced", async () => {
1710
+ const bob = createTestNode();
1711
+ const alice = createTestNode();
1712
+
1713
+ // Create a group and map on anotherClient
1714
+ const group = alice.createGroup();
1715
+ const map = group.createMap();
1716
+ map.set("key1", "value1", "trusting");
1717
+
1718
+ // Start loading before syncing
1719
+ const result = await bob.loadCoValueCore(map.id);
1720
+
1721
+ expect(result).toBe("unavailable");
1722
+
1723
+ connectTwoPeers(alice, bob, "server", "server");
1724
+
1725
+ const result2 = await bob.loadCoValueCore(map.id);
1726
+
1727
+ expect(result2).not.toBe("unavailable");
1728
+ });
1729
+
1730
+ test("should successfully mark a coValue as unavailable if the server does not have it", async () => {
1731
+ const bob = createTestNode();
1732
+ const alice = createTestNode();
1733
+ const charlie = createTestNode();
1734
+
1735
+ connectTwoPeers(bob, charlie, "client", "server");
1736
+
1737
+ // Create a group and map on anotherClient
1738
+ const group = alice.createGroup();
1739
+ const map = group.createMap();
1740
+ map.set("key1", "value1", "trusting");
1741
+
1742
+ // Start loading before syncing
1743
+ const result = await bob.loadCoValueCore(map.id);
1744
+
1745
+ expect(result).toBe("unavailable");
1746
+ });
1729
1747
  });
1730
1748
 
1731
1749
  describe("waitForSyncWithPeer", () => {
@@ -1894,6 +1912,8 @@ describe("sync protocol", () => {
1894
1912
  const map = group.createMap();
1895
1913
  map.set("hello", "world", "trusting");
1896
1914
 
1915
+ await map.core.waitForSync();
1916
+
1897
1917
  const mapOnJazzCloud = await loadCoValueOrFail(jazzCloud, map.id);
1898
1918
  expect(mapOnJazzCloud.get("hello")).toEqual("world");
1899
1919
 
@@ -2038,6 +2058,29 @@ describe("sync protocol", () => {
2038
2058
  },
2039
2059
  },
2040
2060
  },
2061
+ {
2062
+ from: "server",
2063
+ msg: {
2064
+ action: "known",
2065
+ header: true,
2066
+ id: map.id,
2067
+ sessions: {
2068
+ [client.currentSessionID]: 1,
2069
+ },
2070
+ },
2071
+ },
2072
+ {
2073
+ from: "server",
2074
+ msg: {
2075
+ action: "known",
2076
+ asDependencyOf: undefined,
2077
+ header: true,
2078
+ id: map.id,
2079
+ sessions: {
2080
+ [client.currentSessionID]: 1,
2081
+ },
2082
+ },
2083
+ },
2041
2084
  ]);
2042
2085
  });
2043
2086
  });