cojson 0.7.35-unique.2 → 0.8.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 (77) hide show
  1. package/.turbo/turbo-build.log +2 -7
  2. package/.turbo/turbo-lint.log +4 -0
  3. package/.turbo/turbo-test.log +321 -253
  4. package/CHANGELOG.md +10 -2
  5. package/dist/PeerState.js +66 -0
  6. package/dist/PeerState.js.map +1 -0
  7. package/dist/PriorityBasedMessageQueue.js +51 -0
  8. package/dist/PriorityBasedMessageQueue.js.map +1 -0
  9. package/dist/base64url.js.map +1 -1
  10. package/dist/coValue.js.map +1 -1
  11. package/dist/coValueCore.js +3 -6
  12. package/dist/coValueCore.js.map +1 -1
  13. package/dist/coValues/account.js +0 -1
  14. package/dist/coValues/account.js.map +1 -1
  15. package/dist/coValues/coList.js.map +1 -1
  16. package/dist/coValues/coMap.js.map +1 -1
  17. package/dist/coValues/coStream.js +14 -15
  18. package/dist/coValues/coStream.js.map +1 -1
  19. package/dist/coValues/group.js.map +1 -1
  20. package/dist/coreToCoValue.js.map +1 -1
  21. package/dist/crypto/PureJSCrypto.js.map +1 -1
  22. package/dist/crypto/WasmCrypto.js.map +1 -1
  23. package/dist/crypto/crypto.js +3 -0
  24. package/dist/crypto/crypto.js.map +1 -1
  25. package/dist/index.js +3 -2
  26. package/dist/index.js.map +1 -1
  27. package/dist/jsonStringify.js.map +1 -1
  28. package/dist/localNode.js +7 -7
  29. package/dist/localNode.js.map +1 -1
  30. package/dist/permissions.js.map +1 -1
  31. package/dist/priority.js +31 -0
  32. package/dist/priority.js.map +1 -0
  33. package/dist/storage/FileSystem.js.map +1 -1
  34. package/dist/storage/chunksAndKnownStates.js +2 -0
  35. package/dist/storage/chunksAndKnownStates.js.map +1 -1
  36. package/dist/storage/index.js.map +1 -1
  37. package/dist/streamUtils.js.map +1 -1
  38. package/dist/sync.js +7 -18
  39. package/dist/sync.js.map +1 -1
  40. package/dist/tests/PeerState.test.js +80 -0
  41. package/dist/tests/PeerState.test.js.map +1 -0
  42. package/dist/tests/PriorityBasedMessageQueue.test.js +97 -0
  43. package/dist/tests/PriorityBasedMessageQueue.test.js.map +1 -0
  44. package/dist/tests/account.test.js +1 -2
  45. package/dist/tests/account.test.js.map +1 -1
  46. package/dist/tests/coMap.test.js.map +1 -1
  47. package/dist/tests/coStream.test.js +34 -1
  48. package/dist/tests/coStream.test.js.map +1 -1
  49. package/dist/tests/permissions.test.js +41 -42
  50. package/dist/tests/permissions.test.js.map +1 -1
  51. package/dist/tests/priority.test.js +61 -0
  52. package/dist/tests/priority.test.js.map +1 -0
  53. package/dist/tests/sync.test.js +327 -17
  54. package/dist/tests/sync.test.js.map +1 -1
  55. package/dist/tests/testUtils.js +1 -2
  56. package/dist/tests/testUtils.js.map +1 -1
  57. package/dist/typeUtils/expectGroup.js.map +1 -1
  58. package/package.json +3 -3
  59. package/src/PeerState.ts +85 -0
  60. package/src/PriorityBasedMessageQueue.ts +77 -0
  61. package/src/coValueCore.ts +4 -9
  62. package/src/coValues/account.ts +0 -1
  63. package/src/coValues/coStream.ts +21 -18
  64. package/src/crypto/crypto.ts +5 -0
  65. package/src/index.ts +3 -3
  66. package/src/localNode.ts +6 -7
  67. package/src/priority.ts +39 -0
  68. package/src/storage/chunksAndKnownStates.ts +2 -0
  69. package/src/sync.ts +19 -34
  70. package/src/tests/PeerState.test.ts +92 -0
  71. package/src/tests/PriorityBasedMessageQueue.test.ts +111 -0
  72. package/src/tests/account.test.ts +1 -2
  73. package/src/tests/coStream.test.ts +58 -1
  74. package/src/tests/permissions.test.ts +41 -42
  75. package/src/tests/priority.test.ts +75 -0
  76. package/src/tests/sync.test.ts +488 -26
  77. package/src/tests/testUtils.ts +1 -2
@@ -0,0 +1,39 @@
1
+ import { type CoValueHeader } from "./coValueCore.js";
2
+
3
+ /**
4
+ * The priority of a `CoValue` determines how much priority is given
5
+ * to its content messages.
6
+ *
7
+ * The priority value is handled as weight in the weighed round robin algorithm
8
+ * used to determine the order in which messages are sent.
9
+ *
10
+ * Follows the HTTP urgency range and order:
11
+ * - https://www.rfc-editor.org/rfc/rfc9218.html#name-urgency
12
+ */
13
+ export const CO_VALUE_PRIORITY = {
14
+ HIGH: 0,
15
+ MEDIUM: 3,
16
+ LOW: 6,
17
+ } as const;
18
+
19
+ export type CoValuePriority = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
20
+
21
+ export function getPriorityFromHeader(header: CoValueHeader | undefined | boolean): CoValuePriority {
22
+ if (typeof header === "boolean" || !header) {
23
+ return CO_VALUE_PRIORITY.MEDIUM;
24
+ }
25
+
26
+ if (header.meta?.type === "account") {
27
+ return CO_VALUE_PRIORITY.HIGH;
28
+ }
29
+
30
+ if (header.ruleset.type === "group") {
31
+ return CO_VALUE_PRIORITY.HIGH;
32
+ }
33
+
34
+ if (header.type === "costream" && header.meta?.type === "binary") {
35
+ return CO_VALUE_PRIORITY.LOW;
36
+ }
37
+
38
+ return CO_VALUE_PRIORITY.MEDIUM;
39
+ }
@@ -1,5 +1,6 @@
1
1
  import { RawCoID, SessionID } from "../ids.js";
2
2
  import { MAX_RECOMMENDED_TX_SIZE } from "../index.js";
3
+ import { getPriorityFromHeader } from "../priority.js";
3
4
  import { CoValueKnownState, NewContentMessage } from "../sync.js";
4
5
  import { CoValueChunk } from "./index.js";
5
6
 
@@ -15,6 +16,7 @@ export function contentSinceChunk(
15
16
  action: "content",
16
17
  header: known?.header ? undefined : chunk.header,
17
18
  new: {},
19
+ priority: getPriorityFromHeader(chunk.header),
18
20
  });
19
21
 
20
22
  for (const [sessionID, sessionsEntry] of Object.entries(
package/src/sync.ts CHANGED
@@ -3,6 +3,8 @@ import { CoValueHeader, Transaction } from "./coValueCore.js";
3
3
  import { CoValueCore } from "./coValueCore.js";
4
4
  import { LocalNode, newLoadingState } from "./localNode.js";
5
5
  import { RawCoID, SessionID } from "./ids.js";
6
+ import { PeerState } from "./PeerState.js";
7
+ import { CoValuePriority } from "./priority.js";
6
8
 
7
9
  export type CoValueKnownState = {
8
10
  id: RawCoID;
@@ -38,6 +40,7 @@ export type NewContentMessage = {
38
40
  action: "content";
39
41
  id: RawCoID;
40
42
  header?: CoValueHeader;
43
+ priority: CoValuePriority;
41
44
  new: {
42
45
  [sessionID: SessionID]: SessionNewContent;
43
46
  };
@@ -76,17 +79,6 @@ export interface Peer {
76
79
  crashOnClose: boolean;
77
80
  }
78
81
 
79
- export interface PeerState {
80
- id: PeerID;
81
- optimisticKnownStates: { [id: RawCoID]: CoValueKnownState };
82
- toldKnownState: Set<RawCoID>;
83
- incoming: IncomingSyncStream;
84
- outgoing: OutgoingSyncQueue;
85
- role: "peer" | "server" | "client";
86
- priority?: number;
87
- crashOnClose: boolean;
88
- }
89
-
90
82
  export function combinedKnownStates(
91
83
  stateA: CoValueKnownState,
92
84
  stateB: CoValueKnownState,
@@ -141,7 +133,7 @@ export class SyncManager {
141
133
 
142
134
  for (const peer of eligiblePeers) {
143
135
  // console.log("loading", id, "from", peer.id);
144
- await peer.outgoing.push({
136
+ await peer.pushOutgoingMessage({
145
137
  action: "load",
146
138
  id: id,
147
139
  header: false,
@@ -227,7 +219,7 @@ export class SyncManager {
227
219
  id,
228
220
  header: false,
229
221
  sessions: {},
230
- }).catch((e) => {
222
+ }).catch((e: unknown) => {
231
223
  console.error("Error sending load", e);
232
224
  });
233
225
  return;
@@ -244,7 +236,7 @@ export class SyncManager {
244
236
  this.trySendToPeer(peer, {
245
237
  action: "load",
246
238
  ...coValue.knownState(),
247
- }).catch((e) => {
239
+ }).catch((e: unknown) => {
248
240
  console.error("Error sending load", e);
249
241
  });
250
242
  }
@@ -274,7 +266,7 @@ export class SyncManager {
274
266
  action: "known",
275
267
  asDependencyOf,
276
268
  ...coValue.knownState(),
277
- }).catch((e) => {
269
+ }).catch((e: unknown) => {
278
270
  console.error("Error sending known state", e);
279
271
  });
280
272
 
@@ -282,7 +274,10 @@ export class SyncManager {
282
274
  }
283
275
  }
284
276
 
285
- async sendNewContentIncludingDependencies(id: RawCoID, peer: PeerState) {
277
+ async sendNewContentIncludingDependencies(
278
+ id: RawCoID,
279
+ peer: PeerState,
280
+ ) {
286
281
  const coValue = this.local.expectCoValueLoaded(id);
287
282
 
288
283
  await Promise.all(
@@ -310,9 +305,11 @@ export class SyncManager {
310
305
  // } header: ${!!piece.header}`,
311
306
  // // Object.values(piece.new).map((s) => s.newTransactions)
312
307
  // );
313
- this.trySendToPeer(peer, piece).catch((e) => {
308
+
309
+ this.trySendToPeer(peer, piece).catch((e: unknown) => {
314
310
  console.error("Error sending content piece", e);
315
311
  });
312
+
316
313
  if (performance.now() - lastYield > 10) {
317
314
  await new Promise<void>((resolve) => {
318
315
  setTimeout(resolve, 0);
@@ -336,16 +333,7 @@ export class SyncManager {
336
333
  }
337
334
 
338
335
  addPeer(peer: Peer) {
339
- const peerState: PeerState = {
340
- id: peer.id,
341
- optimisticKnownStates: {},
342
- incoming: peer.incoming,
343
- outgoing: peer.outgoing,
344
- toldKnownState: new Set(),
345
- role: peer.role,
346
- priority: peer.priority,
347
- crashOnClose: peer.crashOnClose,
348
- };
336
+ const peerState = new PeerState(peer);
349
337
  this.peers[peer.id] = peerState;
350
338
 
351
339
  if (peer.role === "server") {
@@ -420,7 +408,7 @@ export class SyncManager {
420
408
  }
421
409
 
422
410
  trySendToPeer(peer: PeerState, msg: SyncMessage) {
423
- return peer.outgoing.push(msg);
411
+ return peer.pushOutgoingMessage(msg);
424
412
  }
425
413
 
426
414
  async handleLoad(msg: LoadMessage, peer: PeerState) {
@@ -679,7 +667,8 @@ export class SyncManager {
679
667
  newTransactions.length + " new transactions",
680
668
  "after: " + newContentForSession.after,
681
669
  "our last known tx idx initially: " + ourKnownTxIdx,
682
- "our last known tx idx now: " + coValue.sessionLogs.get(sessionID)?.transactions.length,
670
+ "our last known tx idx now: " +
671
+ coValue.sessionLogs.get(sessionID)?.transactions.length,
683
672
  );
684
673
  continue;
685
674
  }
@@ -774,11 +763,7 @@ export class SyncManager {
774
763
 
775
764
  gracefulShutdown() {
776
765
  for (const peer of Object.values(this.peers)) {
777
- console.debug("Gracefully closing", peer.id);
778
- peer.outgoing.close();
779
- peer.incoming = (async function* () {
780
- yield "Disconnected" as const;
781
- })();
766
+ peer.gracefulShutdown();
782
767
  }
783
768
  }
784
769
  }
@@ -0,0 +1,92 @@
1
+ import { describe, test, expect, vi } from "vitest";
2
+ import { PeerState } from "../PeerState.js";
3
+ import { Peer, SyncMessage } from "../sync.js";
4
+ import { CO_VALUE_PRIORITY } from "../priority.js";
5
+
6
+ function setup() {
7
+ const mockPeer: Peer = {
8
+ id: "test-peer",
9
+ role: "peer",
10
+ priority: 1,
11
+ crashOnClose: false,
12
+ incoming: (async function* () {})(),
13
+ outgoing: {
14
+ push: vi.fn().mockResolvedValue(undefined),
15
+ close: vi.fn(),
16
+ },
17
+ };
18
+ const peerState = new PeerState(mockPeer);
19
+ return { mockPeer, peerState };
20
+ }
21
+
22
+ describe("PeerState", () => {
23
+ test("should initialize with correct properties", () => {
24
+ const { peerState } = setup();
25
+ expect(peerState.id).toBe("test-peer");
26
+ expect(peerState.role).toBe("peer");
27
+ expect(peerState.priority).toBe(1);
28
+ expect(peerState.crashOnClose).toBe(false);
29
+ expect(peerState.closed).toBe(false);
30
+ expect(peerState.optimisticKnownStates).toEqual({});
31
+ expect(peerState.toldKnownState).toEqual(new Set());
32
+ });
33
+
34
+ test("should push outgoing message to peer", async () => {
35
+ const { mockPeer, peerState } = setup();
36
+ const message: SyncMessage = { action: "load", id: "co_ztest-id", header: false, sessions: {} };
37
+ await peerState.pushOutgoingMessage(message);
38
+ expect(mockPeer.outgoing.push).toHaveBeenCalledWith(message);
39
+ });
40
+
41
+ test("should return peer's incoming when not closed", () => {
42
+ const { mockPeer, peerState } = setup();
43
+ expect(peerState.incoming).toBe(mockPeer.incoming);
44
+ });
45
+
46
+ test("should return Disconnected when closed", async () => {
47
+ const { peerState } = setup();
48
+ peerState.gracefulShutdown();
49
+ const incomingIterator = peerState.incoming[Symbol.asyncIterator]();
50
+ const { value, done } = await incomingIterator.next();
51
+ expect(value).toBe("Disconnected");
52
+ expect(done).toBe(false);
53
+ });
54
+
55
+ test("should perform graceful shutdown", () => {
56
+ const { mockPeer, peerState } = setup();
57
+ const consoleSpy = vi.spyOn(console, "debug").mockImplementation(() => {});
58
+ peerState.gracefulShutdown();
59
+ expect(mockPeer.outgoing.close).toHaveBeenCalled();
60
+ expect(peerState.closed).toBe(true);
61
+ expect(consoleSpy).toHaveBeenCalledWith("Gracefully closing", "test-peer");
62
+ consoleSpy.mockRestore();
63
+ });
64
+
65
+ test("should schedule outgoing messages based on their priority", async () => {
66
+ const { peerState } = setup();
67
+
68
+ const loadMessage: SyncMessage = { action: "load", id: "co_zhigh", header: false, sessions: {} };
69
+ const contentMessageHigh: SyncMessage = { action: "content", id: "co_zhigh", new: {}, priority: CO_VALUE_PRIORITY.HIGH };
70
+ const contentMessageMid: SyncMessage = { action: "content", id: "co_zmid", new: {}, priority: CO_VALUE_PRIORITY.MEDIUM };
71
+ const contentMessageLow: SyncMessage = { action: "content", id: "co_zlow", new: {}, priority: CO_VALUE_PRIORITY.LOW };
72
+
73
+ const promises = [
74
+ peerState.pushOutgoingMessage(contentMessageLow),
75
+ peerState.pushOutgoingMessage(contentMessageMid),
76
+ peerState.pushOutgoingMessage(contentMessageHigh),
77
+ peerState.pushOutgoingMessage(loadMessage),
78
+ ];
79
+
80
+ await Promise.all(promises);
81
+
82
+ // The first message is pushed directly, the other three are queued because are waiting
83
+ // for the first push to be completed.
84
+ expect(peerState["peer"].outgoing.push).toHaveBeenNthCalledWith(1, contentMessageLow);
85
+
86
+ // Load message are managed as high priority messages and having the same priority as the content message
87
+ // they follow the push order.
88
+ expect(peerState["peer"].outgoing.push).toHaveBeenNthCalledWith(2, contentMessageHigh);
89
+ expect(peerState["peer"].outgoing.push).toHaveBeenNthCalledWith(3, loadMessage);
90
+ expect(peerState["peer"].outgoing.push).toHaveBeenNthCalledWith(4, contentMessageMid);
91
+ });
92
+ });
@@ -0,0 +1,111 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { PriorityBasedMessageQueue } from "../PriorityBasedMessageQueue.js";
3
+ import { SyncMessage } from "../sync.js";
4
+ import { CO_VALUE_PRIORITY } from "../priority.js";
5
+
6
+ function setup() {
7
+ const queue = new PriorityBasedMessageQueue(CO_VALUE_PRIORITY.MEDIUM);
8
+ return { queue };
9
+ }
10
+
11
+ describe("PriorityBasedMessageQueue", () => {
12
+ test("should initialize with correct properties", () => {
13
+ const { queue } = setup();
14
+ expect(queue["defaultPriority"]).toBe(CO_VALUE_PRIORITY.MEDIUM);
15
+ expect(queue["queues"].length).toBe(8);
16
+ expect(queue["queues"].every((q) => q.length === 0)).toBe(true);
17
+ });
18
+
19
+ test("should push message with default priority", async () => {
20
+ const { queue } = setup();
21
+ const message: SyncMessage = {
22
+ action: "load",
23
+ id: "co_ztest-id",
24
+ header: false,
25
+ sessions: {},
26
+ };
27
+ void queue.push(message);
28
+ const pulledEntry = queue.pull();
29
+ expect(pulledEntry?.msg).toEqual(message);
30
+ });
31
+
32
+ test("should push message with specified priority", async () => {
33
+ const { queue } = setup();
34
+ const message: SyncMessage = {
35
+ action: "content",
36
+ id: "co_zhigh",
37
+ new: {},
38
+ priority: CO_VALUE_PRIORITY.HIGH,
39
+ };
40
+ void queue.push(message);
41
+ const pulledEntry = queue.pull();
42
+ expect(pulledEntry?.msg).toEqual(message);
43
+ });
44
+
45
+ test("should pull messages in priority order", async () => {
46
+ const { queue } = setup();
47
+ const lowPriorityMsg: SyncMessage = {
48
+ action: "content",
49
+ id: "co_zlow",
50
+ new: {},
51
+ priority: CO_VALUE_PRIORITY.LOW,
52
+ };
53
+ const mediumPriorityMsg: SyncMessage = {
54
+ action: "content",
55
+ id: "co_zmedium",
56
+ new: {},
57
+ priority: CO_VALUE_PRIORITY.MEDIUM,
58
+ };
59
+ const highPriorityMsg: SyncMessage = {
60
+ action: "content",
61
+ id: "co_zhigh",
62
+ new: {},
63
+ priority: CO_VALUE_PRIORITY.HIGH,
64
+ };
65
+
66
+ void queue.push(lowPriorityMsg);
67
+ void queue.push(mediumPriorityMsg);
68
+ void queue.push(highPriorityMsg);
69
+
70
+ expect(queue.pull()?.msg).toEqual(highPriorityMsg);
71
+ expect(queue.pull()?.msg).toEqual(mediumPriorityMsg);
72
+ expect(queue.pull()?.msg).toEqual(lowPriorityMsg);
73
+ });
74
+
75
+ test("should return undefined when pulling from empty queue", () => {
76
+ const { queue } = setup();
77
+ expect(queue.pull()).toBeUndefined();
78
+ });
79
+
80
+ test("should resolve promise when message is pulled", async () => {
81
+ const { queue } = setup();
82
+ const message: SyncMessage = {
83
+ action: "load",
84
+ id: "co_ztest-id",
85
+ header: false,
86
+ sessions: {},
87
+ };
88
+ const pushPromise = queue.push(message);
89
+
90
+ const pulledEntry = queue.pull();
91
+ pulledEntry?.resolve();
92
+
93
+ await expect(pushPromise).resolves.toBeUndefined();
94
+ });
95
+
96
+ test("should reject promise when message is rejected", async () => {
97
+ const { queue } = setup();
98
+ const message: SyncMessage = {
99
+ action: "load",
100
+ id: "co_ztest-id",
101
+ header: false,
102
+ sessions: {},
103
+ };
104
+ const pushPromise = queue.push(message);
105
+
106
+ const pulledEntry = queue.pull();
107
+ pulledEntry?.reject(new Error("Test error"));
108
+
109
+ await expect(pushPromise).rejects.toThrow("Test error");
110
+ });
111
+ });
@@ -1,5 +1,4 @@
1
1
  import { expect, test } from "vitest";
2
- import { newRandomSessionID } from "../coValueCore.js";
3
2
  import { LocalNode } from "../localNode.js";
4
3
  import { connectedPeers } from "../streamUtils.js";
5
4
  import { WasmCrypto } from "../crypto/WasmCrypto.js";
@@ -65,7 +64,7 @@ test("Can create account with one node, and then load it on another", async () =
65
64
  const node2 = await LocalNode.withLoadedAccount({
66
65
  accountID,
67
66
  accountSecret,
68
- sessionID: newRandomSessionID(accountID),
67
+ sessionID: Crypto.newRandomSessionID(accountID),
69
68
  peersToLoadFrom: [node1asPeer],
70
69
  crypto: Crypto,
71
70
  });
@@ -1,4 +1,4 @@
1
- import { expect, test } from "vitest";
1
+ import { expect, test, describe } from "vitest";
2
2
  import { expectStream } from "../coValue.js";
3
3
  import { RawBinaryCoStream } from "../coValues/coStream.js";
4
4
  import { MAX_RECOMMENDED_TX_SIZE, WasmCrypto } from "../index.js";
@@ -244,3 +244,60 @@ test("When adding large transactions (bigger than MAX_RECOMMENDED_TX_SIZE), we s
244
244
  sessionEntry.lastSignature,
245
245
  );
246
246
  });
247
+
248
+ describe("isBinaryStreamEnded", () => {
249
+ function setup() {
250
+ const node = new LocalNode(
251
+ ...randomAnonymousAccountAndSessionID(),
252
+ Crypto,
253
+ );
254
+
255
+ const coValue = node.createCoValue({
256
+ type: "costream",
257
+ ruleset: { type: "unsafeAllowAll" },
258
+ meta: { type: "binary" },
259
+ ...Crypto.createdNowUnique(),
260
+ });
261
+
262
+ const content = coValue.getCurrentContent();
263
+
264
+ if (
265
+ content.type !== "costream" ||
266
+ content.headerMeta?.type !== "binary" ||
267
+ !(content instanceof RawBinaryCoStream)
268
+ ) {
269
+ throw new Error("Expected binary stream");
270
+ }
271
+
272
+ return content;
273
+ }
274
+
275
+ test("returns true when the last item is end", () => {
276
+ const stream = setup();
277
+
278
+ stream.startBinaryStream(
279
+ { mimeType: "text/plain", fileName: "test.txt" },
280
+ "trusting",
281
+ );
282
+ stream.endBinaryStream("trusting");
283
+
284
+ expect(stream.isBinaryStreamEnded()).toBe(true);
285
+ });
286
+
287
+ test("returns false if the stream isn't ended", () => {
288
+ const stream = setup();
289
+
290
+ stream.startBinaryStream(
291
+ { mimeType: "text/plain", fileName: "test.txt" },
292
+ "trusting",
293
+ );
294
+
295
+ expect(stream.isBinaryStreamEnded()).toBe(false);
296
+ });
297
+
298
+ test("returns false if the stream isn't started", () => {
299
+ const stream = setup();
300
+
301
+ expect(stream.isBinaryStreamEnded()).toBe(false);
302
+ });
303
+ });