cojson 0.18.6 → 0.18.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 (86) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +18 -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 +1 -0
  45. package/dist/localNode.d.ts.map +1 -1
  46. package/dist/localNode.js +10 -2
  47. package/dist/localNode.js.map +1 -1
  48. package/dist/storage/storageAsync.d.ts.map +1 -1
  49. package/dist/storage/storageAsync.js.map +1 -1
  50. package/dist/sync.d.ts +3 -3
  51. package/dist/sync.d.ts.map +1 -1
  52. package/dist/sync.js +29 -19
  53. package/dist/sync.js.map +1 -1
  54. package/dist/tests/branching.test.js +107 -9
  55. package/dist/tests/branching.test.js.map +1 -1
  56. package/dist/tests/coValueCore.test.js +45 -1
  57. package/dist/tests/coValueCore.test.js.map +1 -1
  58. package/dist/tests/sync.content.test.d.ts +2 -0
  59. package/dist/tests/sync.content.test.d.ts.map +1 -0
  60. package/dist/tests/sync.content.test.js +120 -0
  61. package/dist/tests/sync.content.test.js.map +1 -0
  62. package/dist/tests/sync.load.test.js +15 -2
  63. package/dist/tests/sync.load.test.js.map +1 -1
  64. package/dist/tests/sync.storage.test.js +1 -1
  65. package/dist/tests/sync.upload.test.js +2 -2
  66. package/package.json +2 -2
  67. package/src/coValueContentMessage.ts +13 -0
  68. package/src/coValueCore/SessionMap.ts +2 -2
  69. package/src/coValueCore/branching.ts +94 -149
  70. package/src/coValueCore/coValueCore.ts +121 -27
  71. package/src/coValueCore/verifiedState.ts +8 -0
  72. package/src/coValues/coList.ts +12 -1
  73. package/src/coValues/coMap.ts +10 -12
  74. package/src/coValues/group.ts +14 -1
  75. package/src/config.ts +9 -0
  76. package/src/crypto/PureJSCrypto.ts +25 -13
  77. package/src/exports.ts +7 -1
  78. package/src/localNode.ts +12 -2
  79. package/src/storage/storageAsync.ts +0 -1
  80. package/src/sync.ts +37 -33
  81. package/src/tests/branching.test.ts +158 -9
  82. package/src/tests/coValueCore.test.ts +62 -2
  83. package/src/tests/sync.content.test.ts +153 -0
  84. package/src/tests/sync.load.test.ts +19 -2
  85. package/src/tests/sync.storage.test.ts +1 -1
  86. package/src/tests/sync.upload.test.ts +2 -2
@@ -49,10 +49,17 @@ export class RawCoMapView<
49
49
  latest: {
50
50
  [Key in keyof Shape & string]?: MapOp<Key, Shape[Key]>;
51
51
  };
52
+
52
53
  /** @internal */
53
- latestTxMadeAt: number;
54
+ get latestTxMadeAt(): number {
55
+ return this.core.latestTxMadeAt;
56
+ }
57
+
54
58
  /** @internal */
55
- earliestTxMadeAt: number | null;
59
+ get earliestTxMadeAt(): number {
60
+ return this.core.earliestTxMadeAt;
61
+ }
62
+
56
63
  /** @internal */
57
64
  ops: {
58
65
  [Key in keyof Shape & string]?: MapOp<Key, Shape[Key]>[];
@@ -80,8 +87,7 @@ export class RawCoMapView<
80
87
  ) {
81
88
  this.id = core.id as CoID<this>;
82
89
  this.core = core;
83
- this.latestTxMadeAt = 0;
84
- this.earliestTxMadeAt = null;
90
+
85
91
  this.ignorePrivateTransactions =
86
92
  options?.ignorePrivateTransactions ?? false;
87
93
  this.ops = {};
@@ -105,10 +111,6 @@ export class RawCoMapView<
105
111
  return;
106
112
  }
107
113
 
108
- if (this.earliestTxMadeAt === null && newValidTransactions[0]) {
109
- this.earliestTxMadeAt = newValidTransactions[0].madeAt;
110
- }
111
-
112
114
  const { ops } = this;
113
115
 
114
116
  const changedEntries = new Map<
@@ -117,10 +119,6 @@ export class RawCoMapView<
117
119
  >();
118
120
 
119
121
  for (const { txID, changes, madeAt, tx } of newValidTransactions) {
120
- if (madeAt > this.latestTxMadeAt) {
121
- this.latestTxMadeAt = madeAt;
122
- }
123
-
124
122
  for (let changeIdx = 0; changeIdx < changes.length; changeIdx++) {
125
123
  const change = changes[changeIdx] as MapOpPayload<
126
124
  keyof Shape & string,
@@ -1070,6 +1070,9 @@ export class RawGroup<
1070
1070
 
1071
1071
  if (init) {
1072
1072
  map.assign(init, initPrivacy);
1073
+ } else if (!uniqueness.createdAt) {
1074
+ // If the createdAt is not set, we need to make a trusting transaction to set the createdAt
1075
+ map.core.makeTransaction([], "trusting");
1073
1076
  }
1074
1077
 
1075
1078
  return map;
@@ -1101,6 +1104,9 @@ export class RawGroup<
1101
1104
 
1102
1105
  if (init?.length) {
1103
1106
  list.appendItems(init, undefined, initPrivacy);
1107
+ } else if (!uniqueness.createdAt) {
1108
+ // If the createdAt is not set, we need to make a trusting transaction to set the createdAt
1109
+ list.core.makeTransaction([], "trusting");
1104
1110
  }
1105
1111
 
1106
1112
  return list;
@@ -1141,7 +1147,7 @@ export class RawGroup<
1141
1147
  meta?: C["headerMeta"],
1142
1148
  uniqueness: CoValueUniqueness = this.crypto.createdNowUnique(),
1143
1149
  ): C {
1144
- return this.core.node
1150
+ const stream = this.core.node
1145
1151
  .createCoValue({
1146
1152
  type: "costream",
1147
1153
  ruleset: {
@@ -1152,6 +1158,13 @@ export class RawGroup<
1152
1158
  ...uniqueness,
1153
1159
  })
1154
1160
  .getCurrentContent() as C;
1161
+
1162
+ if (!uniqueness.createdAt) {
1163
+ // If the createdAt is not set, we need to make a trusting transaction to set the createdAt
1164
+ stream.core.makeTransaction([], "trusting");
1165
+ }
1166
+
1167
+ return stream;
1155
1168
  }
1156
1169
 
1157
1170
  /** @category 3. Value creation */
package/src/config.ts CHANGED
@@ -7,12 +7,21 @@
7
7
  **/
8
8
  export const TRANSACTION_CONFIG = {
9
9
  MAX_RECOMMENDED_TX_SIZE: 100 * 1024,
10
+ /**
11
+ * Messages larger than this will be rejected when creating a transaction.
12
+ * The current limit is set at 1MB because that's the limit imposed by Cloudflare to Websocket messages.
13
+ */
14
+ MAX_TX_SIZE_BYTES: 1 * 1024 * 1024,
10
15
  };
11
16
 
12
17
  export function setMaxRecommendedTxSize(size: number) {
13
18
  TRANSACTION_CONFIG.MAX_RECOMMENDED_TX_SIZE = size;
14
19
  }
15
20
 
21
+ export function setMaxTxSizeBytes(size: number) {
22
+ TRANSACTION_CONFIG.MAX_TX_SIZE_BYTES = size;
23
+ }
24
+
16
25
  export const CO_VALUE_LOADING_CONFIG = {
17
26
  MAX_RETRIES: 1,
18
27
  TIMEOUT: 30_000,
@@ -32,6 +32,29 @@ import { ControlledAccountOrAgent } from "../coValues/account.js";
32
32
 
33
33
  type Blake3State = ReturnType<typeof blake3.create>;
34
34
 
35
+ const x25519SharedSecretCache = new Map<string, Uint8Array>();
36
+
37
+ function getx25519SharedSecret(
38
+ privateKeyA: SealerSecret,
39
+ publicKeyB: SealerID,
40
+ ): Uint8Array {
41
+ const cacheKey = `${privateKeyA}-${publicKeyB}`;
42
+ let sharedSecret = x25519SharedSecretCache.get(cacheKey);
43
+
44
+ if (!sharedSecret) {
45
+ const privateKeyABytes = base58.decode(
46
+ privateKeyA.substring("sealerSecret_z".length),
47
+ );
48
+ const publicKeyBBytes = base58.decode(
49
+ publicKeyB.substring("sealer_z".length),
50
+ );
51
+ sharedSecret = x25519.getSharedSecret(privateKeyABytes, publicKeyBBytes);
52
+ x25519SharedSecretCache.set(cacheKey, sharedSecret);
53
+ }
54
+
55
+ return sharedSecret;
56
+ }
57
+
35
58
  /**
36
59
  * Pure JavaScript implementation of the CryptoProvider interface using noble-curves and noble-ciphers libraries.
37
60
  * This provides a fallback implementation that doesn't require WebAssembly, offering:
@@ -164,16 +187,10 @@ export class PureJSCrypto extends CryptoProvider<Blake3State> {
164
187
  to: SealerID;
165
188
  nOnceMaterial: { in: RawCoID; tx: TransactionID };
166
189
  }): Sealed<T> {
190
+ const sharedSecret = getx25519SharedSecret(from, to);
167
191
  const nOnce = this.generateJsonNonce(nOnceMaterial);
168
-
169
- const sealerPub = base58.decode(to.substring("sealer_z".length));
170
-
171
- const senderPriv = base58.decode(from.substring("sealerSecret_z".length));
172
-
173
192
  const plaintext = textEncoder.encode(stableStringify(message));
174
193
 
175
- const sharedSecret = x25519.getSharedSecret(senderPriv, sealerPub);
176
-
177
194
  const sealedBytes = xsalsa20poly1305(sharedSecret, nOnce).encrypt(
178
195
  plaintext,
179
196
  );
@@ -189,14 +206,9 @@ export class PureJSCrypto extends CryptoProvider<Blake3State> {
189
206
  ): T | undefined {
190
207
  const nOnce = this.generateJsonNonce(nOnceMaterial);
191
208
 
192
- const sealerPriv = base58.decode(sealer.substring("sealerSecret_z".length));
193
-
194
- const senderPub = base58.decode(from.substring("sealer_z".length));
195
-
209
+ const sharedSecret = getx25519SharedSecret(sealer, from);
196
210
  const sealedBytes = base64URLtoBytes(sealed.substring("sealed_U".length));
197
211
 
198
- const sharedSecret = x25519.getSharedSecret(sealerPriv, senderPub);
199
-
200
212
  const plaintext = xsalsa20poly1305(sharedSecret, nOnce).decrypt(
201
213
  sealedBytes,
202
214
  );
package/src/exports.ts CHANGED
@@ -63,7 +63,12 @@ import type { JsonObject, JsonValue } from "./jsonValue.js";
63
63
  import type * as Media from "./media.js";
64
64
  import { disablePermissionErrors } from "./permissions.js";
65
65
  import type { Peer, SyncMessage } from "./sync.js";
66
- import { DisconnectedError, SyncManager, emptyKnownState } from "./sync.js";
66
+ import {
67
+ DisconnectedError,
68
+ SyncManager,
69
+ emptyKnownState,
70
+ hwrServerPeerSelector,
71
+ } from "./sync.js";
67
72
 
68
73
  import {
69
74
  getContentMessageSize,
@@ -163,6 +168,7 @@ export {
163
168
  LogLevel,
164
169
  base64URLtoBytes,
165
170
  bytesToBase64url,
171
+ hwrServerPeerSelector,
166
172
  };
167
173
 
168
174
  export type {
package/src/localNode.ts CHANGED
@@ -97,6 +97,10 @@ export class LocalNode {
97
97
  this.storage = undefined;
98
98
  }
99
99
 
100
+ hasCoValue(id: RawCoID) {
101
+ return this.coValues.has(id);
102
+ }
103
+
100
104
  getCoValue(id: RawCoID) {
101
105
  let entry = this.coValues.get(id);
102
106
 
@@ -479,8 +483,14 @@ export class LocalNode {
479
483
  return branch.getCurrentContent() as T;
480
484
  }
481
485
 
482
- // Passing skipRetry to true because otherwise creating a new branch would always take 1 retry delay
483
- await this.loadCoValueCore(branch.id, undefined, true);
486
+ // Do a synchronous check to see if the branch exists, if not we don't need to try to load the branch
487
+ if (!source.hasBranch(branchName, branchOwnerID)) {
488
+ return source
489
+ .createBranch(branchName, branchOwnerID)
490
+ .getCurrentContent() as T;
491
+ }
492
+
493
+ await this.loadCoValueCore(branch.id);
484
494
 
485
495
  if (!branch.isAvailable()) {
486
496
  return source
@@ -1,7 +1,6 @@
1
1
  import {
2
2
  createContentMessage,
3
3
  exceedsRecommendedSize,
4
- getTransactionSize,
5
4
  } from "../coValueContentMessage.js";
6
5
  import {
7
6
  type CoValueCore,
package/src/sync.ts CHANGED
@@ -139,6 +139,13 @@ export class SyncManager {
139
139
  // the transactions have already been verified by the [trusted] peer that sent them.
140
140
  private skipVerify: boolean = false;
141
141
 
142
+ // When true, coValues that arrive from server peers will be ignored if they had not
143
+ // explicitly been requested via a load message.
144
+ private _ignoreUnknownCoValuesFromServers: boolean = false;
145
+ ignoreUnknownCoValuesFromServers() {
146
+ this._ignoreUnknownCoValuesFromServers = true;
147
+ }
148
+
142
149
  peersCounter = metrics.getMeter("cojson").createUpDownCounter("jazz.peers", {
143
150
  description: "Amount of connected peers",
144
151
  valueType: ValueType.INT,
@@ -191,7 +198,28 @@ export class SyncManager {
191
198
  msg,
192
199
  });
193
200
  return;
194
- } else if (this.local.getCoValue(msg.id).isErroredInPeer(peer.id)) {
201
+ }
202
+
203
+ // Prevent core shards from storing content that belongs to other shards.
204
+ //
205
+ // This can happen because a covalue "miss" on a core shard will cause a load message to
206
+ // be sent to the original unsharded core. The original core, treating the peer as a client,
207
+ // will respond with the covalue and its dependencies. Those dependencies might not belong
208
+ // to this shard, so they should be ignored.
209
+ //
210
+ // TODO: remove once core has been sharded.
211
+ if (
212
+ peer.role === "server" &&
213
+ this._ignoreUnknownCoValuesFromServers &&
214
+ !this.local.hasCoValue(msg.id)
215
+ ) {
216
+ logger.warn(
217
+ `Ignoring message ${msg.action} on unknown coValue ${msg.id} from peer ${peer.id}`,
218
+ );
219
+ return;
220
+ }
221
+
222
+ if (this.local.getCoValue(msg.id).isErroredInPeer(peer.id)) {
195
223
  logger.warn(
196
224
  `Skipping message ${msg.action} on errored coValue ${msg.id} from peer ${peer.id}`,
197
225
  );
@@ -219,28 +247,7 @@ export class SyncManager {
219
247
  }
220
248
  }
221
249
 
222
- sendNewContentIncludingDependencies(
223
- id: RawCoID,
224
- peer: PeerState,
225
- seen: Set<RawCoID> = new Set(),
226
- ) {
227
- this.sendNewContent(id, peer, seen, true);
228
- }
229
-
230
- sendNewContentWithoutDependencies(
231
- id: RawCoID,
232
- peer: PeerState,
233
- seen: Set<RawCoID> = new Set(),
234
- ) {
235
- this.sendNewContent(id, peer, seen, false);
236
- }
237
-
238
- private sendNewContent(
239
- id: RawCoID,
240
- peer: PeerState,
241
- seen: Set<RawCoID> = new Set(),
242
- includeDependencies: boolean,
243
- ) {
250
+ sendNewContent(id: RawCoID, peer: PeerState, seen: Set<RawCoID> = new Set()) {
244
251
  if (seen.has(id)) {
245
252
  return;
246
253
  }
@@ -253,9 +260,10 @@ export class SyncManager {
253
260
  return;
254
261
  }
255
262
 
263
+ const includeDependencies = peer.role !== "server";
256
264
  if (includeDependencies) {
257
265
  for (const dependency of coValue.getDependedOnCoValues()) {
258
- this.sendNewContentIncludingDependencies(dependency, peer, seen);
266
+ this.sendNewContent(dependency, peer, seen);
259
267
  }
260
268
  }
261
269
 
@@ -434,7 +442,7 @@ export class SyncManager {
434
442
  const coValue = this.local.getCoValue(msg.id);
435
443
 
436
444
  if (coValue.isAvailable()) {
437
- this.sendNewContentIncludingDependencies(msg.id, peer);
445
+ this.sendNewContent(msg.id, peer);
438
446
  return;
439
447
  }
440
448
 
@@ -444,7 +452,7 @@ export class SyncManager {
444
452
 
445
453
  const handleLoadResult = () => {
446
454
  if (coValue.isAvailable()) {
447
- this.sendNewContentIncludingDependencies(msg.id, peer);
455
+ this.sendNewContent(msg.id, peer);
448
456
  return;
449
457
  }
450
458
 
@@ -478,11 +486,7 @@ export class SyncManager {
478
486
  }
479
487
 
480
488
  if (coValue.isAvailable()) {
481
- if (peer.role === "server") {
482
- this.sendNewContentWithoutDependencies(msg.id, peer);
483
- } else {
484
- this.sendNewContentIncludingDependencies(msg.id, peer);
485
- }
489
+ this.sendNewContent(msg.id, peer);
486
490
  }
487
491
  }
488
492
 
@@ -777,7 +781,7 @@ export class SyncManager {
777
781
 
778
782
  // We directly forward the new content to peers that have an active subscription
779
783
  if (peer.optimisticKnownStates.has(coValue.id)) {
780
- this.sendNewContentIncludingDependencies(coValue.id, peer);
784
+ this.sendNewContent(coValue.id, peer);
781
785
  syncedPeers.push(peer);
782
786
  } else if (
783
787
  peer.role === "server" &&
@@ -808,7 +812,7 @@ export class SyncManager {
808
812
  handleCorrection(msg: KnownStateMessage, peer: PeerState) {
809
813
  peer.setKnownState(msg.id, knownStateIn(msg));
810
814
 
811
- return this.sendNewContentIncludingDependencies(msg.id, peer);
815
+ return this.sendNewContent(msg.id, peer);
812
816
  }
813
817
 
814
818
  private syncQueue = new LocalTransactionsSyncQueue((content) =>
@@ -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);