cojson 0.18.6 → 0.18.7

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 (84) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +11 -0
  3. package/dist/coValueContentMessage.d.ts +2 -0
  4. package/dist/coValueContentMessage.d.ts.map +1 -1
  5. package/dist/coValueContentMessage.js +7 -0
  6. package/dist/coValueContentMessage.js.map +1 -1
  7. package/dist/coValueCore/SessionMap.d.ts +2 -2
  8. package/dist/coValueCore/SessionMap.d.ts.map +1 -1
  9. package/dist/coValueCore/SessionMap.js +2 -4
  10. package/dist/coValueCore/SessionMap.js.map +1 -1
  11. package/dist/coValueCore/branching.d.ts +31 -9
  12. package/dist/coValueCore/branching.d.ts.map +1 -1
  13. package/dist/coValueCore/branching.js +50 -100
  14. package/dist/coValueCore/branching.js.map +1 -1
  15. package/dist/coValueCore/coValueCore.d.ts +12 -8
  16. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  17. package/dist/coValueCore/coValueCore.js +93 -23
  18. package/dist/coValueCore/coValueCore.js.map +1 -1
  19. package/dist/coValueCore/verifiedState.d.ts +4 -2
  20. package/dist/coValueCore/verifiedState.d.ts.map +1 -1
  21. package/dist/coValueCore/verifiedState.js +6 -4
  22. package/dist/coValueCore/verifiedState.js.map +1 -1
  23. package/dist/coValues/coList.d.ts.map +1 -1
  24. package/dist/coValues/coList.js +10 -1
  25. package/dist/coValues/coList.js.map +1 -1
  26. package/dist/coValues/coMap.d.ts +2 -2
  27. package/dist/coValues/coMap.d.ts.map +1 -1
  28. package/dist/coValues/coMap.js +8 -8
  29. package/dist/coValues/coMap.js.map +1 -1
  30. package/dist/coValues/group.d.ts.map +1 -1
  31. package/dist/coValues/group.js +14 -1
  32. package/dist/coValues/group.js.map +1 -1
  33. package/dist/config.d.ts +6 -0
  34. package/dist/config.d.ts.map +1 -1
  35. package/dist/config.js +8 -0
  36. package/dist/config.js.map +1 -1
  37. package/dist/crypto/PureJSCrypto.d.ts.map +1 -1
  38. package/dist/crypto/PureJSCrypto.js +14 -6
  39. package/dist/crypto/PureJSCrypto.js.map +1 -1
  40. package/dist/exports.d.ts +3 -2
  41. package/dist/exports.d.ts.map +1 -1
  42. package/dist/exports.js +2 -2
  43. package/dist/exports.js.map +1 -1
  44. package/dist/localNode.d.ts.map +1 -1
  45. package/dist/localNode.js +7 -2
  46. package/dist/localNode.js.map +1 -1
  47. package/dist/storage/storageAsync.d.ts.map +1 -1
  48. package/dist/storage/storageAsync.js.map +1 -1
  49. package/dist/sync.d.ts +1 -3
  50. package/dist/sync.d.ts.map +1 -1
  51. package/dist/sync.js +8 -18
  52. package/dist/sync.js.map +1 -1
  53. package/dist/tests/branching.test.js +107 -9
  54. package/dist/tests/branching.test.js.map +1 -1
  55. package/dist/tests/coValueCore.test.js +45 -1
  56. package/dist/tests/coValueCore.test.js.map +1 -1
  57. package/dist/tests/sync.content.test.d.ts +2 -0
  58. package/dist/tests/sync.content.test.d.ts.map +1 -0
  59. package/dist/tests/sync.content.test.js +120 -0
  60. package/dist/tests/sync.content.test.js.map +1 -0
  61. package/dist/tests/sync.load.test.js +2 -2
  62. package/dist/tests/sync.storage.test.js +1 -1
  63. package/dist/tests/sync.upload.test.js +2 -2
  64. package/package.json +2 -2
  65. package/src/coValueContentMessage.ts +13 -0
  66. package/src/coValueCore/SessionMap.ts +2 -2
  67. package/src/coValueCore/branching.ts +94 -149
  68. package/src/coValueCore/coValueCore.ts +121 -27
  69. package/src/coValueCore/verifiedState.ts +8 -0
  70. package/src/coValues/coList.ts +12 -1
  71. package/src/coValues/coMap.ts +10 -12
  72. package/src/coValues/group.ts +14 -1
  73. package/src/config.ts +9 -0
  74. package/src/crypto/PureJSCrypto.ts +25 -13
  75. package/src/exports.ts +7 -1
  76. package/src/localNode.ts +8 -2
  77. package/src/storage/storageAsync.ts +0 -1
  78. package/src/sync.ts +8 -32
  79. package/src/tests/branching.test.ts +158 -9
  80. package/src/tests/coValueCore.test.ts +62 -2
  81. package/src/tests/sync.content.test.ts +153 -0
  82. package/src/tests/sync.load.test.ts +2 -2
  83. package/src/tests/sync.storage.test.ts +1 -1
  84. package/src/tests/sync.upload.test.ts +2 -2
@@ -1,10 +1,11 @@
1
- import { beforeEach, describe, expect, test } from "vitest";
1
+ import { assert, beforeEach, describe, expect, test } from "vitest";
2
2
  import {
3
3
  createTestNode,
4
4
  setupTestNode,
5
5
  loadCoValueOrFail,
6
6
  } from "./testUtils.js";
7
7
  import { expectList, expectMap, expectPlainText } from "../coValue.js";
8
+ import { RawCoMap } from "../exports.js";
8
9
 
9
10
  let jazzCloud: ReturnType<typeof setupTestNode>;
10
11
 
@@ -94,7 +95,7 @@ describe("Branching Logic", () => {
94
95
  const result = expectMap(branch.core.mergeBranch().getCurrentContent());
95
96
 
96
97
  // Verify only one merge commit was created
97
- expect(result.core.mergeCommits.length).toBe(1);
98
+ expect(branch.core.mergeCommits.length).toBe(1);
98
99
 
99
100
  // Verify source contains branch transactions
100
101
  expect(result.get("key1")).toBe("branchValue1");
@@ -154,7 +155,7 @@ describe("Branching Logic", () => {
154
155
  branch.core.mergeBranch();
155
156
 
156
157
  // Verify two merge commits exist
157
- expect(originalMap.core.mergeCommits.length).toBe(2);
158
+ expect(branch.core.mergeCommits.length).toBe(2);
158
159
 
159
160
  // Verify both changes are now in original map
160
161
  expect(originalMap.get("key1")).toBe("branchValue1");
@@ -210,6 +211,8 @@ describe("Branching Logic", () => {
210
211
  .getCurrentContent(),
211
212
  );
212
213
 
214
+ await new Promise((resolve) => setTimeout(resolve, 5));
215
+
213
216
  // Add different items to second branch
214
217
  branch2.appendItems(["apples", "oranges", "carrots"]);
215
218
 
@@ -233,18 +236,20 @@ describe("Branching Logic", () => {
233
236
 
234
237
  expect(list.toJSON()).toEqual([
235
238
  "bread",
236
- "cheese",
237
239
  "apples",
238
240
  "oranges",
239
241
  "carrots",
240
242
  "tomatoes",
241
243
  "cucumber",
244
+ "cheese",
242
245
  ]);
243
246
  });
244
247
 
245
- test("should work with co.plainText when branching from different session", async () => {
246
- const node = createTestNode();
247
- const group = node.createGroup();
248
+ test("should work with co.plainText when merging the same branch twice on different sessions", async () => {
249
+ const client = setupTestNode({
250
+ connected: true,
251
+ });
252
+ const group = client.node.createGroup();
248
253
  const plainText = group.createPlainText();
249
254
 
250
255
  plainText.insertAfter(0, "hello");
@@ -255,12 +260,28 @@ describe("Branching Logic", () => {
255
260
  .getCurrentContent(),
256
261
  );
257
262
 
263
+ branch.insertAfter("hello".length, " world");
264
+
265
+ const anotherSession = client.spawnNewSession();
266
+
267
+ const loadedBranch = await loadCoValueOrFail(
268
+ anotherSession.node,
269
+ branch.id,
270
+ );
271
+ assert(loadedBranch);
272
+
273
+ anotherSession.connectToSyncServer().peerState.gracefulShutdown();
274
+
258
275
  // Add more items to the branch
259
- branch.insertAfter("hello".length, "world");
276
+ loadedBranch.insertAfter("hello world".length, " people");
260
277
 
261
278
  branch.core.mergeBranch();
279
+ const loadedBranchMergeResult = loadedBranch.core.mergeBranch();
262
280
 
263
- expect(plainText.toString()).toEqual("helloworld");
281
+ anotherSession.connectToSyncServer();
282
+ await loadedBranchMergeResult.waitForSync();
283
+
284
+ expect(plainText.toString()).toEqual("hello world people");
264
285
  });
265
286
  });
266
287
 
@@ -527,4 +548,132 @@ describe("Branching Logic", () => {
527
548
  expect(aliceBranch.get("bob")).toBe(true);
528
549
  });
529
550
  });
551
+
552
+ test("should alias the txID when a transaction comes from a merge", async () => {
553
+ const client = setupTestNode({
554
+ connected: true,
555
+ });
556
+ const group = client.node.createGroup();
557
+ const map = group.createMap();
558
+
559
+ map.set("key", "value");
560
+
561
+ const branch = map.core
562
+ .createBranch("feature-branch", group.id)
563
+ .getCurrentContent() as RawCoMap;
564
+ branch.set("branchKey", "branchValue");
565
+
566
+ const originalTxID = branch.core
567
+ .getValidTransactions({
568
+ skipBranchSource: true,
569
+ ignorePrivateTransactions: false,
570
+ })
571
+ .at(-1)?.txID;
572
+
573
+ branch.core.mergeBranch();
574
+
575
+ map.set("key2", "value2");
576
+
577
+ const validSortedTransactions = map.core.getValidSortedTransactions();
578
+
579
+ // Only the merged transaction should have the txId changed
580
+ const mergedTransactionIdx = validSortedTransactions.findIndex(
581
+ (tx) => tx.txID.branch,
582
+ );
583
+
584
+ expect(validSortedTransactions[mergedTransactionIdx - 1]?.txID.branch).toBe(
585
+ undefined,
586
+ );
587
+ expect(validSortedTransactions[mergedTransactionIdx]?.txID).toEqual(
588
+ originalTxID,
589
+ );
590
+ expect(validSortedTransactions[mergedTransactionIdx + 1]?.txID.branch).toBe(
591
+ undefined,
592
+ );
593
+ });
594
+
595
+ describe("hasBranch", () => {
596
+ test("should work when the branch owner is the source owner", () => {
597
+ const client = setupTestNode({
598
+ connected: true,
599
+ });
600
+ const group = client.node.createGroup();
601
+ const map = group.createMap();
602
+
603
+ map.set("key", "value");
604
+
605
+ const branch = map.core.createBranch("feature-branch", group.id);
606
+
607
+ expect(map.core.hasBranch("feature-branch")).toBe(true);
608
+ expect(map.core.hasBranch("feature-branch", group.id)).toBe(true);
609
+ expect(branch.hasBranch("feature-branch")).toBe(false);
610
+ });
611
+
612
+ test("should work when the branch onwer is implicit", () => {
613
+ const client = setupTestNode({
614
+ connected: true,
615
+ });
616
+ const group = client.node.createGroup();
617
+ const map = group.createMap();
618
+
619
+ map.set("key", "value");
620
+
621
+ const branch = map.core.createBranch("feature-branch");
622
+
623
+ expect(map.core.hasBranch("feature-branch")).toBe(true);
624
+ expect(map.core.hasBranch("feature-branch", group.id)).toBe(true);
625
+ expect(branch.hasBranch("feature-branch")).toBe(false);
626
+ });
627
+
628
+ test("should return false for non-existent branch name", () => {
629
+ const client = setupTestNode({
630
+ connected: true,
631
+ });
632
+ const group = client.node.createGroup();
633
+ const map = group.createMap();
634
+
635
+ map.set("key", "value");
636
+
637
+ expect(map.core.hasBranch("non-existent-branch")).toBe(false);
638
+ });
639
+
640
+ test("should work with explicit ownerId parameter", () => {
641
+ const client = setupTestNode({
642
+ connected: true,
643
+ });
644
+ const group = client.node.createGroup();
645
+ const map = group.createMap();
646
+
647
+ map.set("key", "value");
648
+
649
+ const differentGroup = client.node.createGroup();
650
+
651
+ map.core.createBranch("feature-branch", differentGroup.id);
652
+
653
+ // Test with explicit ownerId
654
+ expect(map.core.hasBranch("feature-branch", differentGroup.id)).toBe(
655
+ true,
656
+ );
657
+ expect(map.core.hasBranch("feature-branch")).toBe(false);
658
+ });
659
+
660
+ test("should work when the transactions have not been parsed yet", async () => {
661
+ const client = setupTestNode({
662
+ connected: true,
663
+ });
664
+ const group = client.node.createGroup();
665
+ const map = group.createMap();
666
+
667
+ map.set("key", "value");
668
+
669
+ map.core.createBranch("feature-branch", group.id);
670
+
671
+ await map.core.waitForSync();
672
+
673
+ const newSession = client.spawnNewSession();
674
+ const loadedMapCore = await newSession.node.loadCoValueCore(map.core.id);
675
+
676
+ expect(loadedMapCore.hasBranch("feature-branch", group.id)).toBe(true);
677
+ });
678
+ });
530
679
  });
@@ -8,7 +8,6 @@ import {
8
8
  vi,
9
9
  } from "vitest";
10
10
  import { CoValueCore } from "../coValueCore/coValueCore.js";
11
- import { Transaction } from "../coValueCore/verifiedState.js";
12
11
  import { WasmCrypto } from "../crypto/WasmCrypto.js";
13
12
  import { stableStringify } from "../jsonStringify.js";
14
13
  import { LocalNode } from "../localNode.js";
@@ -24,7 +23,7 @@ import {
24
23
  tearDownTestMetricReader,
25
24
  } from "./testUtils.js";
26
25
  import { CO_VALUE_PRIORITY } from "../priority.js";
27
- import { determineValidTransactions } from "../permissions.js";
26
+ import { setMaxTxSizeBytes } from "../config.js";
28
27
 
29
28
  const Crypto = await WasmCrypto.create();
30
29
 
@@ -57,6 +56,7 @@ test("transactions with wrong signature are rejected", () => {
57
56
  node.getCurrentAgent(),
58
57
  [{ hello: "world" }],
59
58
  undefined,
59
+ Date.now(),
60
60
  );
61
61
 
62
62
  transaction.madeAt = Date.now() + 1000;
@@ -87,6 +87,66 @@ test("transactions with wrong signature are rejected", () => {
87
87
  expect(newEntry.getValidSortedTransactions().length).toBe(0);
88
88
  });
89
89
 
90
+ describe("transactions that exceed the byte size limit are rejected", () => {
91
+ beforeEach(() => {
92
+ setMaxTxSizeBytes(1 * 1024);
93
+ });
94
+
95
+ afterEach(() => {
96
+ setMaxTxSizeBytes(1 * 1024 * 1024);
97
+ });
98
+
99
+ test("makeTransaction should throw error when transaction exceeds byte size limit", () => {
100
+ const [agent, sessionID] = randomAgentAndSessionID();
101
+ const node = new LocalNode(agent.agentSecret, sessionID, Crypto);
102
+
103
+ const coValue = node.createCoValue({
104
+ type: "costream",
105
+ ruleset: { type: "unsafeAllowAll" },
106
+ meta: null,
107
+ ...Crypto.createdNowUnique(),
108
+ });
109
+
110
+ const largeBinaryData = "x".repeat(1024 + 100);
111
+
112
+ expect(() => {
113
+ coValue.makeTransaction(
114
+ [
115
+ {
116
+ data: largeBinaryData,
117
+ },
118
+ ],
119
+ "trusting",
120
+ );
121
+ }).toThrow(/Transaction is too large to be synced/);
122
+ });
123
+
124
+ test("makeTransaction should work for transactions under byte size limit", () => {
125
+ const [agent, sessionID] = randomAgentAndSessionID();
126
+ const node = new LocalNode(agent.agentSecret, sessionID, Crypto);
127
+
128
+ const coValue = node.createCoValue({
129
+ type: "costream",
130
+ ruleset: { type: "unsafeAllowAll" },
131
+ meta: null,
132
+ ...Crypto.createdNowUnique(),
133
+ });
134
+
135
+ const smallData = "Hello, world!";
136
+
137
+ const success = coValue.makeTransaction(
138
+ [
139
+ {
140
+ data: smallData,
141
+ },
142
+ ],
143
+ "trusting",
144
+ );
145
+
146
+ expect(success).toBe(true);
147
+ });
148
+ });
149
+
90
150
  test("New transactions in a group correctly update owned values, including subscriptions", async () => {
91
151
  const [agent, sessionID] = randomAgentAndSessionID();
92
152
  const node = new LocalNode(agent.agentSecret, sessionID, Crypto);
@@ -0,0 +1,153 @@
1
+ import { beforeEach, describe, expect, test } from "vitest";
2
+
3
+ import { SyncMessagesLog, TEST_NODE_CONFIG, setupTestNode } from "./testUtils";
4
+
5
+ // We want to simulate a real world communication that happens asynchronously
6
+ TEST_NODE_CONFIG.withAsyncPeers = true;
7
+
8
+ describe("handling content messages", () => {
9
+ beforeEach(async () => {
10
+ SyncMessagesLog.clear();
11
+ });
12
+
13
+ test("dependencies are included when syncing new content to clients", async () => {
14
+ /*
15
+ * Initial Setup:
16
+ * core <-- edge <-- client1
17
+ */
18
+ const core = setupTestNode();
19
+
20
+ const edge = setupTestNode();
21
+ edge.connectToSyncServer({
22
+ ourName: "edge",
23
+ syncServerName: "core",
24
+ syncServer: core.node,
25
+ });
26
+
27
+ const client1 = setupTestNode();
28
+ client1.connectToSyncServer({
29
+ ourName: "client1",
30
+ syncServerName: "edge",
31
+ syncServer: edge.node,
32
+ });
33
+
34
+ // Create a map with a dependency on the client.
35
+ // Everythig will sync all the way back to core.
36
+ const group = client1.node.createGroup();
37
+ const map = group.createMap();
38
+ map.set("hello", "world", "trusting");
39
+ await map.core.waitForSync();
40
+
41
+ /*
42
+ * Add another client:
43
+ *
44
+ * |-- client1
45
+ * core <-- edge <--|
46
+ * |-- client2
47
+ *
48
+ */
49
+ const client2 = setupTestNode();
50
+ const { peerStateOnServer: client2PeerStateOnEdge } =
51
+ client2.connectToSyncServer({
52
+ ourName: "client2",
53
+ syncServerName: "edge",
54
+ syncServer: edge.node,
55
+ });
56
+
57
+ client2PeerStateOnEdge.setOptimisticKnownState(map.core.id, {
58
+ id: map.core.id,
59
+ header: false,
60
+ sessions: {},
61
+ });
62
+
63
+ // Update the map on client1.
64
+ // This should sync the map AND its dependencies to the new client.
65
+ map.set("hello2", "world2", "trusting");
66
+ await map.core.waitForSync();
67
+
68
+ const syncMessages = SyncMessagesLog.getMessages({
69
+ Group: group.core,
70
+ Map: map.core,
71
+ });
72
+ expect(
73
+ syncMessages.some((msg) =>
74
+ msg.startsWith("edge -> client2 | CONTENT Map"),
75
+ ),
76
+ ).toBe(true);
77
+ expect(
78
+ syncMessages.some((msg) =>
79
+ msg.startsWith("edge -> client2 | CONTENT Group"),
80
+ ),
81
+ ).toBe(true);
82
+ });
83
+
84
+ test("dependencies are excluded when syncing new content to servers", async () => {
85
+ /*
86
+ * Initial Setup:
87
+ * core1 <-- edge <-- client
88
+ */
89
+ const core1 = setupTestNode();
90
+
91
+ const edge = setupTestNode();
92
+ edge.connectToSyncServer({
93
+ ourName: "edge",
94
+ syncServerName: "core1",
95
+ syncServer: core1.node,
96
+ });
97
+
98
+ const client = setupTestNode();
99
+ client.connectToSyncServer({
100
+ ourName: "client",
101
+ syncServerName: "edge",
102
+ syncServer: edge.node,
103
+ });
104
+
105
+ // Create a map with a dependency on the client.
106
+ // Everythig will sync all the way back to core1.
107
+ const group = client.node.createGroup();
108
+ const map = group.createMap();
109
+ map.set("hello", "world", "trusting");
110
+ await map.core.waitForSync();
111
+
112
+ /*
113
+ * Add another core:
114
+ *
115
+ * core1 <--
116
+ * |
117
+ * |-- edge <-- client
118
+ * |
119
+ * core2 <--
120
+ */
121
+ const core2 = setupTestNode();
122
+ const { peerState: core2PeerStateOnEdge } = edge.connectToSyncServer({
123
+ ourName: "edge",
124
+ syncServerName: "core2",
125
+ syncServer: core2.node,
126
+ skipReconciliation: true,
127
+ });
128
+
129
+ core2PeerStateOnEdge.setOptimisticKnownState(map.core.id, {
130
+ id: map.core.id,
131
+ header: false,
132
+ sessions: {},
133
+ });
134
+
135
+ // Update the map on the client.
136
+ // This should sync ONLY the map back to both cores.
137
+ map.set("hello2", "world2", "trusting");
138
+ await map.core.waitForSync();
139
+
140
+ const syncMessages = SyncMessagesLog.getMessages({
141
+ Group: group.core,
142
+ Map: map.core,
143
+ });
144
+ expect(
145
+ syncMessages.some((msg) => msg.startsWith("edge -> core2 | CONTENT Map")),
146
+ ).toBe(true);
147
+ expect(
148
+ syncMessages.some((msg) =>
149
+ msg.startsWith("edge -> core2 | CONTENT Group"),
150
+ ),
151
+ ).toBe(false);
152
+ });
153
+ });
@@ -171,10 +171,10 @@ describe("loading coValues from server", () => {
171
171
  [
172
172
  "client -> server | LOAD Branch sessions: empty",
173
173
  "server -> client | CONTENT Group header: true new: After: 0 New: 5",
174
- "server -> client | CONTENT Map header: true new: After: 0 New: 2",
174
+ "server -> client | CONTENT Map header: true new: After: 0 New: 3",
175
175
  "server -> client | CONTENT Branch header: true new: After: 0 New: 2",
176
176
  "client -> server | KNOWN Group sessions: header/5",
177
- "client -> server | KNOWN Map sessions: header/2",
177
+ "client -> server | KNOWN Map sessions: header/3",
178
178
  "client -> server | KNOWN Branch sessions: header/2",
179
179
  ]
180
180
  `);
@@ -238,7 +238,7 @@ describe("client with storage syncs with server", () => {
238
238
  [
239
239
  "client -> storage | LOAD Branch sessions: empty",
240
240
  "storage -> client | CONTENT Group header: true new: After: 0 New: 3",
241
- "storage -> client | CONTENT Map header: true new: After: 0 New: 2",
241
+ "storage -> client | CONTENT Map header: true new: After: 0 New: 3",
242
242
  "storage -> client | CONTENT Branch header: true new: After: 0 New: 2",
243
243
  ]
244
244
  `);
@@ -92,11 +92,11 @@ describe("client to server upload", () => {
92
92
  "server -> client | CONTENT Map header: true new: After: 0 New: 2",
93
93
  "client -> server | KNOWN Group sessions: header/5",
94
94
  "client -> server | KNOWN Map sessions: header/2",
95
- "client -> server | LOAD Branch sessions: empty",
96
- "server -> client | KNOWN Branch sessions: empty",
97
95
  "client -> server | CONTENT Branch header: true new: After: 0 New: 1",
96
+ "client -> server | CONTENT Map header: false new: After: 0 New: 1",
98
97
  "client -> server | CONTENT Branch header: false new: After: 1 New: 1",
99
98
  "server -> client | KNOWN Branch sessions: header/1",
99
+ "server -> client | KNOWN Map sessions: header/3",
100
100
  "server -> client | KNOWN Branch sessions: header/2",
101
101
  ]
102
102
  `);