cojson 0.18.11 → 0.18.13

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 (71) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +17 -0
  3. package/dist/coValueCore/branching.d.ts +5 -0
  4. package/dist/coValueCore/branching.d.ts.map +1 -1
  5. package/dist/coValueCore/branching.js +26 -8
  6. package/dist/coValueCore/branching.js.map +1 -1
  7. package/dist/coValueCore/coValueCore.d.ts +3 -6
  8. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  9. package/dist/coValueCore/coValueCore.js +22 -17
  10. package/dist/coValueCore/coValueCore.js.map +1 -1
  11. package/dist/coValues/account.d.ts +0 -5
  12. package/dist/coValues/account.d.ts.map +1 -1
  13. package/dist/coValues/account.js +0 -20
  14. package/dist/coValues/account.js.map +1 -1
  15. package/dist/crypto/PureJSCrypto.d.ts +0 -5
  16. package/dist/crypto/PureJSCrypto.d.ts.map +1 -1
  17. package/dist/crypto/PureJSCrypto.js +13 -30
  18. package/dist/crypto/PureJSCrypto.js.map +1 -1
  19. package/dist/crypto/WasmCrypto.d.ts +0 -4
  20. package/dist/crypto/WasmCrypto.d.ts.map +1 -1
  21. package/dist/crypto/WasmCrypto.js +1 -14
  22. package/dist/crypto/WasmCrypto.js.map +1 -1
  23. package/dist/crypto/crypto.d.ts +1 -39
  24. package/dist/crypto/crypto.d.ts.map +1 -1
  25. package/dist/crypto/crypto.js +11 -53
  26. package/dist/crypto/crypto.js.map +1 -1
  27. package/dist/exports.d.ts +3 -2
  28. package/dist/exports.d.ts.map +1 -1
  29. package/dist/exports.js +3 -2
  30. package/dist/exports.js.map +1 -1
  31. package/dist/knownState.d.ts +3 -0
  32. package/dist/knownState.d.ts.map +1 -0
  33. package/dist/knownState.js +8 -0
  34. package/dist/knownState.js.map +1 -0
  35. package/dist/localNode.d.ts.map +1 -1
  36. package/dist/localNode.js +4 -0
  37. package/dist/localNode.js.map +1 -1
  38. package/dist/sync.d.ts.map +1 -1
  39. package/dist/sync.js +8 -1
  40. package/dist/sync.js.map +1 -1
  41. package/dist/tests/branching.test.js +36 -29
  42. package/dist/tests/branching.test.js.map +1 -1
  43. package/dist/tests/coValueCore.test.js +31 -4
  44. package/dist/tests/coValueCore.test.js.map +1 -1
  45. package/dist/tests/coValueCoreLoadingState.test.js +15 -5
  46. package/dist/tests/coValueCoreLoadingState.test.js.map +1 -1
  47. package/dist/tests/crypto.test.js +0 -41
  48. package/dist/tests/crypto.test.js.map +1 -1
  49. package/dist/tests/sync.upload.test.js +20 -1
  50. package/dist/tests/sync.upload.test.js.map +1 -1
  51. package/package.json +2 -2
  52. package/src/coValueCore/branching.ts +37 -16
  53. package/src/coValueCore/coValueCore.ts +30 -21
  54. package/src/coValues/account.ts +0 -25
  55. package/src/crypto/PureJSCrypto.ts +14 -43
  56. package/src/crypto/WasmCrypto.ts +0 -19
  57. package/src/crypto/crypto.ts +12 -94
  58. package/src/exports.ts +2 -2
  59. package/src/knownState.ts +17 -0
  60. package/src/localNode.ts +5 -0
  61. package/src/sync.ts +11 -2
  62. package/src/tests/branching.test.ts +45 -34
  63. package/src/tests/coValueCore.test.ts +40 -4
  64. package/src/tests/coValueCoreLoadingState.test.ts +17 -5
  65. package/src/tests/crypto.test.ts +0 -53
  66. package/src/tests/sync.upload.test.ts +28 -1
  67. package/dist/tests/cryptoImpl.test.d.ts +0 -2
  68. package/dist/tests/cryptoImpl.test.d.ts.map +0 -1
  69. package/dist/tests/cryptoImpl.test.js +0 -144
  70. package/dist/tests/cryptoImpl.test.js.map +0 -1
  71. package/src/tests/cryptoImpl.test.ts +0 -213
@@ -24,7 +24,6 @@ import {
24
24
  Signature,
25
25
  SignerID,
26
26
  SignerSecret,
27
- StreamingHash,
28
27
  textDecoder,
29
28
  textEncoder,
30
29
  } from "./crypto.js";
@@ -68,14 +67,6 @@ export class PureJSCrypto extends CryptoProvider<Blake3State> {
68
67
  return new PureJSCrypto();
69
68
  }
70
69
 
71
- emptyBlake3State(): Blake3State {
72
- return blake3.create({});
73
- }
74
-
75
- cloneBlake3State(state: Blake3State): Blake3State {
76
- return state.clone();
77
- }
78
-
79
70
  blake3HashOnce(data: Uint8Array) {
80
71
  return blake3(data);
81
72
  }
@@ -87,14 +78,6 @@ export class PureJSCrypto extends CryptoProvider<Blake3State> {
87
78
  return blake3.create({}).update(context).update(data).digest();
88
79
  }
89
80
 
90
- blake3IncrementalUpdate(state: Blake3State, data: Uint8Array) {
91
- return state.update(data);
92
- }
93
-
94
- blake3DigestForState(state: Blake3State): Uint8Array {
95
- return state.clone().digest();
96
- }
97
-
98
81
  generateNonce(input: Uint8Array): Uint8Array {
99
82
  return this.blake3HashOnce(input).slice(0, 24);
100
83
  }
@@ -241,7 +224,7 @@ export class PureJSSessionLog implements SessionLogImpl {
241
224
  private readonly signerID: SignerID | undefined,
242
225
  private readonly crypto: PureJSCrypto,
243
226
  ) {
244
- this.streamingHash = this.crypto.emptyBlake3State();
227
+ this.streamingHash = blake3.create({});
245
228
  }
246
229
 
247
230
  clone(): SessionLogImpl {
@@ -253,7 +236,7 @@ export class PureJSSessionLog implements SessionLogImpl {
253
236
  );
254
237
  newLog.transactions = this.transactions.slice();
255
238
  newLog.lastSignature = this.lastSignature;
256
- newLog.streamingHash = this.crypto.cloneBlake3State(this.streamingHash);
239
+ newLog.streamingHash = this.streamingHash.clone();
257
240
  return newLog;
258
241
  }
259
242
 
@@ -274,29 +257,29 @@ export class PureJSSessionLog implements SessionLogImpl {
274
257
  newSignature: Signature,
275
258
  skipVerify: boolean,
276
259
  ) {
260
+ for (const tx of transactions) {
261
+ this.streamingHash.update(textEncoder.encode(tx));
262
+ }
263
+
277
264
  if (!skipVerify) {
278
265
  if (!this.signerID) {
279
266
  throw new Error("Tried to add transactions without signer ID");
280
267
  }
281
268
 
282
- const checkHasher = this.crypto.cloneBlake3State(this.streamingHash);
283
-
284
- for (const tx of transactions) {
285
- checkHasher.update(textEncoder.encode(tx));
286
- }
287
- const newHash = checkHasher.digest();
269
+ const newHash = this.streamingHash.clone().digest();
288
270
  const newHashEncoded = `hash_z${base58.encode(newHash)}`;
289
271
 
290
272
  if (!this.crypto.verify(newSignature, newHashEncoded, this.signerID)) {
273
+ // Rebuild the streaming hash to the original state
274
+ this.streamingHash = blake3.create({});
275
+ for (const tx of this.transactions) {
276
+ this.streamingHash.update(textEncoder.encode(tx));
277
+ }
291
278
  throw new Error("Signature verification failed");
292
279
  }
293
280
  }
294
281
 
295
282
  for (const tx of transactions) {
296
- this.crypto.blake3IncrementalUpdate(
297
- this.streamingHash,
298
- textEncoder.encode(tx),
299
- );
300
283
  this.transactions.push(tx);
301
284
  }
302
285
 
@@ -305,24 +288,12 @@ export class PureJSSessionLog implements SessionLogImpl {
305
288
  return newSignature;
306
289
  }
307
290
 
308
- expectedHashAfter(transactionsJson: string[]): string {
309
- const hasher = this.crypto.cloneBlake3State(this.streamingHash);
310
- for (const tx of transactionsJson) {
311
- hasher.update(textEncoder.encode(tx));
312
- }
313
- const newHash = hasher.digest();
314
- return `hash_z${base58.encode(newHash)}`;
315
- }
316
-
317
291
  internalAddNewTransaction(
318
292
  transaction: string,
319
293
  signerAgent: ControlledAccountOrAgent,
320
294
  ) {
321
- this.crypto.blake3IncrementalUpdate(
322
- this.streamingHash,
323
- textEncoder.encode(transaction),
324
- );
325
- const newHash = this.crypto.blake3DigestForState(this.streamingHash);
295
+ this.streamingHash.update(textEncoder.encode(transaction));
296
+ const newHash = this.streamingHash.clone().digest();
326
297
  const newHashEncoded = `hash_z${base58.encode(newHash)}`;
327
298
  const signature = this.crypto.sign(
328
299
  signerAgent.currentSignerSecret(),
@@ -2,7 +2,6 @@ import {
2
2
  SessionLog,
3
3
  initialize,
4
4
  Blake3Hasher,
5
- blake3_empty_state,
6
5
  blake3_hash_once,
7
6
  blake3_hash_once_with_context,
8
7
  decrypt,
@@ -25,7 +24,6 @@ import { PureJSCrypto } from "./PureJSCrypto.js";
25
24
  import {
26
25
  CryptoProvider,
27
26
  Encrypted,
28
- Hash,
29
27
  KeyID,
30
28
  KeySecret,
31
29
  Sealed,
@@ -73,14 +71,6 @@ export class WasmCrypto extends CryptoProvider<Blake3State> {
73
71
  return new WasmCrypto();
74
72
  }
75
73
 
76
- emptyBlake3State(): Blake3State {
77
- return blake3_empty_state();
78
- }
79
-
80
- cloneBlake3State(state: Blake3State): Blake3State {
81
- return state.clone();
82
- }
83
-
84
74
  blake3HashOnce(data: Uint8Array) {
85
75
  return blake3_hash_once(data);
86
76
  }
@@ -92,15 +82,6 @@ export class WasmCrypto extends CryptoProvider<Blake3State> {
92
82
  return blake3_hash_once_with_context(data, context);
93
83
  }
94
84
 
95
- blake3IncrementalUpdate(state: Blake3State, data: Uint8Array): Blake3State {
96
- state.update(data);
97
- return state;
98
- }
99
-
100
- blake3DigestForState(state: Blake3State): Uint8Array {
101
- return state.finalize();
102
- }
103
-
104
85
  newEd25519SigningKey(): Uint8Array {
105
86
  return new_ed25519_signing_key();
106
87
  }
@@ -40,14 +40,6 @@ export abstract class CryptoProvider<Blake3State = any> {
40
40
  return `signerSecret_z${base58.encode(this.newEd25519SigningKey())}`;
41
41
  }
42
42
 
43
- signerSecretToBytes(secret: SignerSecret): Uint8Array {
44
- return base58.decode(secret.substring("signerSecret_z".length));
45
- }
46
-
47
- signerSecretFromBytes(bytes: Uint8Array): SignerSecret {
48
- return `signerSecret_z${base58.encode(bytes)}`;
49
- }
50
-
51
43
  abstract getSignerID(secret: SignerSecret): SignerID;
52
44
 
53
45
  abstract sign(secret: SignerSecret, message: JsonValue): Signature;
@@ -64,39 +56,25 @@ export abstract class CryptoProvider<Blake3State = any> {
64
56
  return `sealerSecret_z${base58.encode(this.newX25519StaticSecret())}`;
65
57
  }
66
58
 
67
- sealerSecretToBytes(secret: SealerSecret): Uint8Array {
68
- return base58.decode(secret.substring("sealerSecret_z".length));
69
- }
70
-
71
- sealerSecretFromBytes(bytes: Uint8Array): SealerSecret {
72
- return `sealerSecret_z${base58.encode(bytes)}`;
73
- }
74
-
75
59
  abstract getSealerID(secret: SealerSecret): SealerID;
76
60
 
77
61
  newRandomAgentSecret(): AgentSecret {
78
62
  return `${this.newRandomSealer()}/${this.newRandomSigner()}`;
79
63
  }
80
64
 
81
- agentSecretToBytes(secret: AgentSecret): Uint8Array {
82
- const [sealerSecret, signerSecret] = secret.split("/");
83
- return new Uint8Array([
84
- ...this.sealerSecretToBytes(sealerSecret as SealerSecret),
85
- ...this.signerSecretToBytes(signerSecret as SignerSecret),
86
- ]);
87
- }
88
-
89
- agentSecretFromBytes(bytes: Uint8Array): AgentSecret {
90
- const sealerSecret = this.sealerSecretFromBytes(bytes.slice(0, 32));
91
- const signerSecret = this.signerSecretFromBytes(bytes.slice(32));
92
- return `${sealerSecret}/${signerSecret}`;
93
- }
94
-
65
+ agentIdCache = new Map<string, AgentID>();
95
66
  getAgentID(secret: AgentSecret): AgentID {
96
- const [sealerSecret, signerSecret] = secret.split("/");
97
- return `${this.getSealerID(
98
- sealerSecret as SealerSecret,
99
- )}/${this.getSignerID(signerSecret as SignerSecret)}`;
67
+ const cacheKey = secret;
68
+ let agentId = this.agentIdCache.get(cacheKey);
69
+ if (!agentId) {
70
+ const [sealerSecret, signerSecret] = secret.split("/") as [
71
+ SealerSecret,
72
+ SignerSecret,
73
+ ];
74
+ agentId = `${this.getSealerID(sealerSecret)}/${this.getSignerID(signerSecret)}`;
75
+ this.agentIdCache.set(cacheKey, agentId);
76
+ }
77
+ return agentId;
100
78
  }
101
79
 
102
80
  getAgentSignerID(agentId: AgentID): SignerID {
@@ -115,18 +93,11 @@ export abstract class CryptoProvider<Blake3State = any> {
115
93
  return agentSecret.split("/")[0] as SealerSecret;
116
94
  }
117
95
 
118
- abstract emptyBlake3State(): Blake3State;
119
- abstract cloneBlake3State(state: Blake3State): Blake3State;
120
96
  abstract blake3HashOnce(data: Uint8Array): Uint8Array;
121
97
  abstract blake3HashOnceWithContext(
122
98
  data: Uint8Array,
123
99
  { context }: { context: Uint8Array },
124
100
  ): Uint8Array;
125
- abstract blake3IncrementalUpdate(
126
- state: Blake3State,
127
- data: Uint8Array,
128
- ): Blake3State;
129
- abstract blake3DigestForState(state: Blake3State): Uint8Array;
130
101
 
131
102
  secureHash(value: JsonValue): Hash {
132
103
  return `hash_z${base58.encode(
@@ -149,14 +120,6 @@ export abstract class CryptoProvider<Blake3State = any> {
149
120
  nOnceMaterial: N,
150
121
  ): Encrypted<T, N>;
151
122
 
152
- encryptForTransaction<T extends JsonValue>(
153
- value: T,
154
- keySecret: KeySecret,
155
- nOnceMaterial: { in: RawCoID; tx: TransactionID },
156
- ): Encrypted<T, { in: RawCoID; tx: TransactionID }> {
157
- return this.encrypt(value, keySecret, nOnceMaterial);
158
- }
159
-
160
123
  abstract decryptRaw<T extends JsonValue, N extends JsonValue>(
161
124
  encrypted: Encrypted<T, N>,
162
125
  keySecret: KeySecret,
@@ -183,22 +146,6 @@ export abstract class CryptoProvider<Blake3State = any> {
183
146
  };
184
147
  }
185
148
 
186
- decryptRawForTransaction<T extends JsonValue>(
187
- encrypted: Encrypted<T, { in: RawCoID; tx: TransactionID }>,
188
- keySecret: KeySecret,
189
- nOnceMaterial: { in: RawCoID; tx: TransactionID },
190
- ): Stringified<T> | undefined {
191
- return this.decryptRaw(encrypted, keySecret, nOnceMaterial);
192
- }
193
-
194
- decryptForTransaction<T extends JsonValue>(
195
- encrypted: Encrypted<T, { in: RawCoID; tx: TransactionID }>,
196
- keySecret: KeySecret,
197
- nOnceMaterial: { in: RawCoID; tx: TransactionID },
198
- ): T | undefined {
199
- return this.decrypt(encrypted, keySecret, nOnceMaterial);
200
- }
201
-
202
149
  encryptKeySecret(keys: {
203
150
  toEncrypt: { id: KeyID; secret: KeySecret };
204
151
  encrypting: { id: KeyID; secret: KeySecret };
@@ -311,35 +258,6 @@ export abstract class CryptoProvider<Blake3State = any> {
311
258
  }
312
259
 
313
260
  export type Hash = `hash_z${string}`;
314
-
315
- export class StreamingHash {
316
- state: Uint8Array;
317
- crypto: CryptoProvider;
318
-
319
- constructor(crypto: CryptoProvider, fromClone?: Uint8Array) {
320
- this.state = fromClone || crypto.emptyBlake3State();
321
- this.crypto = crypto;
322
- }
323
-
324
- update(value: JsonValue): Uint8Array {
325
- const encoded = textEncoder.encode(stableStringify(value));
326
- this.state = this.crypto.blake3IncrementalUpdate(this.state, encoded);
327
- return encoded;
328
- }
329
-
330
- digest(): Hash {
331
- const hash = this.crypto.blake3DigestForState(this.state);
332
- return `hash_z${base58.encode(hash)}`;
333
- }
334
-
335
- clone(): StreamingHash {
336
- return new StreamingHash(
337
- this.crypto,
338
- this.crypto.cloneBlake3State(this.state),
339
- );
340
- }
341
- }
342
-
343
261
  export type ShortHash = `shortHash_z${string}`;
344
262
  export const shortHashLength = 19;
345
263
 
package/src/exports.ts CHANGED
@@ -27,7 +27,6 @@ import { EVERYONE, RawGroup } from "./coValues/group.js";
27
27
  import type { Everyone } from "./coValues/group.js";
28
28
  import {
29
29
  CryptoProvider,
30
- StreamingHash,
31
30
  secretSeedLength,
32
31
  shortHashLength,
33
32
  } from "./crypto/crypto.js";
@@ -85,6 +84,7 @@ import {
85
84
  import { LogLevel, logger } from "./logger.js";
86
85
  import { CO_VALUE_PRIORITY, getPriorityFromHeader } from "./priority.js";
87
86
  import { getDependedOnCoValues } from "./storage/syncUtils.js";
87
+ import { canBeBranched } from "./coValueCore/branching.js";
88
88
 
89
89
  type Value = JsonValue | AnyRawCoValue;
90
90
 
@@ -107,7 +107,6 @@ export const cojsonInternals = {
107
107
  isAccountID,
108
108
  accountHeaderForInitialAgentSecret,
109
109
  idforHeader,
110
- StreamingHash,
111
110
  getPriorityFromHeader,
112
111
  getGroupDependentKeyList,
113
112
  getGroupDependentKey,
@@ -124,6 +123,7 @@ export const cojsonInternals = {
124
123
  getContentMessageSize,
125
124
  TRANSACTION_CONFIG,
126
125
  setMaxRecommendedTxSize,
126
+ canBeBranched,
127
127
  };
128
128
 
129
129
  export {
@@ -0,0 +1,17 @@
1
+ import type { SessionID } from "./exports";
2
+ import type { CoValueKnownState } from "./sync";
3
+
4
+ export function combineKnownStateSessions(
5
+ a: CoValueKnownState["sessions"],
6
+ b: CoValueKnownState["sessions"],
7
+ ): CoValueKnownState["sessions"] {
8
+ const sessionStates: CoValueKnownState["sessions"] = {};
9
+
10
+ for (const sessionID of Object.keys(a).concat(
11
+ Object.keys(b),
12
+ ) as SessionID[]) {
13
+ sessionStates[sessionID] = Math.max(a[sessionID] || 0, b[sessionID] || 0);
14
+ }
15
+
16
+ return sessionStates;
17
+ }
package/src/localNode.ts CHANGED
@@ -39,6 +39,7 @@ import { StorageAPI } from "./storage/index.js";
39
39
  import { Peer, PeerID, SyncManager } from "./sync.js";
40
40
  import { accountOrAgentIDfromSessionID } from "./typeUtils/accountOrAgentIDfromSessionID.js";
41
41
  import { expectGroup } from "./typeUtils/expectGroup.js";
42
+ import { canBeBranched } from "./coValueCore/branching.js";
42
43
 
43
44
  /** A `LocalNode` represents a local view of a set of loaded `CoValue`s, from the perspective of a particular account (or primitive cryptographic agent).
44
45
 
@@ -477,6 +478,10 @@ export class LocalNode {
477
478
  return "unavailable";
478
479
  }
479
480
 
481
+ if (!canBeBranched(source)) {
482
+ return source.getCurrentContent() as T;
483
+ }
484
+
480
485
  const branch = source.getBranch(branchName, branchOwnerID);
481
486
 
482
487
  if (branch.isAvailable()) {
package/src/sync.ts CHANGED
@@ -594,13 +594,22 @@ export class SyncManager {
594
594
  }
595
595
  }
596
596
 
597
- peer?.updateHeader(msg.id, true);
598
- coValue.provideHeader(
597
+ const success = coValue.provideHeader(
599
598
  msg.header,
600
599
  peer?.id ?? "storage",
601
600
  msg.expectContentUntil,
602
601
  );
603
602
 
603
+ if (!success) {
604
+ logger.error("Failed to provide header", {
605
+ id: msg.id,
606
+ header: msg.header,
607
+ });
608
+ return;
609
+ }
610
+
611
+ peer?.updateHeader(msg.id, true);
612
+
604
613
  if (msg.expectContentUntil) {
605
614
  peer?.combineWith(msg.id, {
606
615
  id: msg.id,
@@ -92,14 +92,20 @@ describe("Branching Logic", () => {
92
92
 
93
93
  // Merge branch twice - second merge should not create new commit
94
94
  expectMap(branch.core.mergeBranch().getCurrentContent());
95
- const result = expectMap(branch.core.mergeBranch().getCurrentContent());
96
95
 
97
- // Verify only one merge commit was created
98
- expect(branch.core.mergeCommits.length).toBe(1);
96
+ const knownState = originalMap.core.knownState();
97
+
98
+ expectMap(branch.core.mergeBranch().getCurrentContent());
99
+
100
+ // Verify the known state is the same
101
+ expect(originalMap.core.knownState()).toEqual(knownState);
99
102
 
100
103
  // Verify source contains branch transactions
101
- expect(result.get("key1")).toBe("branchValue1");
102
- expect(result.get("key2")).toBe("value2");
104
+ expect(originalMap.get("key1")).toBe("branchValue1");
105
+ expect(originalMap.get("key2")).toBe("value2");
106
+
107
+ // Verify only one merge commit was created
108
+ expect(originalMap.core.mergeCommits.length).toBe(1);
103
109
  });
104
110
 
105
111
  test("should not create merge commit when merging empty branch", () => {
@@ -118,10 +124,10 @@ describe("Branching Logic", () => {
118
124
  );
119
125
 
120
126
  // Merge empty branch
121
- const result = expectMap(branch.core.mergeBranch().getCurrentContent());
127
+ expectMap(branch.core.mergeBranch().getCurrentContent());
122
128
 
123
129
  // Verify no merge commit was created
124
- expect(result.core.mergeCommits.length).toBe(0);
130
+ expect(originalMap.core.mergeCommits.length).toBe(0);
125
131
  });
126
132
 
127
133
  test("should merge only new changes from branch after previous merge", () => {
@@ -155,7 +161,7 @@ describe("Branching Logic", () => {
155
161
  branch.core.mergeBranch();
156
162
 
157
163
  // Verify two merge commits exist
158
- expect(branch.core.mergeCommits.length).toBe(2);
164
+ expect(originalMap.core.mergeCommits.length).toBe(2);
159
165
 
160
166
  // Verify both changes are now in original map
161
167
  expect(originalMap.get("key1")).toBe("branchValue1");
@@ -496,56 +502,61 @@ describe("Branching Logic", () => {
496
502
 
497
503
  describe("Branch Conflict Resolution", () => {
498
504
  test("should successfully handle concurrent branch creation on different nodes", async () => {
499
- const bob = setupTestNode();
500
- const { peer: bobPeer } = bob.connectToSyncServer();
501
505
  const alice = setupTestNode({
502
506
  connected: true,
503
507
  });
504
508
 
505
- const client = setupTestNode();
509
+ const bob = setupTestNode();
510
+ let { peerState: bobPeer } = bob.connectToSyncServer();
511
+
506
512
  const group = jazzCloud.node.createGroup();
507
513
  group.addMember("everyone", "writer");
508
514
 
509
515
  // Create map without any branches
510
- const originalMap = group.createMap();
516
+ const map = group.createMap();
511
517
 
512
- const originalMapOnBob = await loadCoValueOrFail(
513
- bob.node,
514
- originalMap.id,
515
- );
518
+ const bobMap = await loadCoValueOrFail(bob.node, map.id);
516
519
 
517
520
  // Disconnect bob from sync server to create isolation
518
- bobPeer.outgoing.close();
521
+ bobPeer.gracefulShutdown();
522
+
523
+ bobMap.set("bob", "main");
524
+ map.set("alice", "main");
519
525
 
520
526
  // Create branches on different nodes
521
527
  const aliceBranch = await alice.node.checkoutBranch(
522
- originalMap.id,
528
+ map.id,
523
529
  "feature-branch",
524
530
  );
525
- const bobBranch = expectMap(
526
- originalMapOnBob.core
527
- .createBranch("feature-branch", group.id)
528
- .getCurrentContent(),
529
- );
531
+ const bobBranch = await bob.node.checkoutBranch(map.id, "feature-branch");
530
532
 
531
- if (aliceBranch === "unavailable") {
532
- throw new Error("Alice branch is unavailable");
533
+ if (aliceBranch === "unavailable" || bobBranch === "unavailable") {
534
+ throw new Error("Alice or bob branch is unavailable");
533
535
  }
534
536
 
535
- // Add different data to each branch
536
- bobBranch.set("bob", true);
537
- aliceBranch.set("alice", true);
537
+ // The branch start should be different
538
+ expect(aliceBranch.get("alice")).toBe("main");
539
+ expect(aliceBranch.get("bob")).toBe(undefined);
540
+ expect(bobBranch.get("alice")).toBe(undefined);
541
+ expect(bobBranch.get("bob")).toBe("main");
542
+
543
+ expect(aliceBranch.core.branchStart).not.toEqual(
544
+ bobBranch.core.branchStart,
545
+ );
538
546
 
539
547
  // Reconnect bob to sync server
540
- bob.connectToSyncServer();
548
+ bobPeer = bob.connectToSyncServer().peerState;
541
549
 
542
- // Wait for sync to complete
543
- await bobBranch.core.waitForSync();
544
550
  await aliceBranch.core.waitForSync();
551
+ await bobBranch.core.waitForSync();
552
+
553
+ // The branch start from both branches should be aligned now
554
+ expect(aliceBranch.get("alice")).toBe("main");
555
+ expect(aliceBranch.get("bob")).toBe("main");
556
+ expect(bobBranch.get("alice")).toBe("main");
557
+ expect(bobBranch.get("bob")).toBe("main");
545
558
 
546
- // Verify both branches now contain data from the other
547
- expect(bobBranch.get("alice")).toBe(true);
548
- expect(aliceBranch.get("bob")).toBe(true);
559
+ expect(aliceBranch.core.branchStart).toEqual(bobBranch.core.branchStart);
549
560
  });
550
561
  });
551
562
 
@@ -7,7 +7,7 @@ import {
7
7
  test,
8
8
  vi,
9
9
  } from "vitest";
10
- import { CoValueCore } from "../coValueCore/coValueCore.js";
10
+ import { CoValueCore, idforHeader } from "../coValueCore/coValueCore.js";
11
11
  import { WasmCrypto } from "../crypto/WasmCrypto.js";
12
12
  import { stableStringify } from "../jsonStringify.js";
13
13
  import { LocalNode } from "../localNode.js";
@@ -519,10 +519,46 @@ describe("markErrored and isErroredInPeer", () => {
519
519
  // Mark peer as unavailable
520
520
  coValue.markNotFoundInPeer(peerId);
521
521
  expect(coValue.isErroredInPeer(peerId)).toBe(false);
522
+ });
522
523
 
523
- // Mark peer as available
524
- coValue.provideHeader({} as any, peerId);
525
- expect(coValue.isErroredInPeer(peerId)).toBe(false);
524
+ test("provideHeader should work", () => {
525
+ const [agent, sessionID] = randomAgentAndSessionID();
526
+ const node = new LocalNode(agent.agentSecret, sessionID, Crypto);
527
+
528
+ const header = {
529
+ type: "costream",
530
+ ruleset: { type: "unsafeAllowAll" },
531
+ meta: null,
532
+ ...Crypto.createdNowUnique(),
533
+ } as const;
534
+
535
+ const coValue = node.getCoValue(idforHeader(header, Crypto));
536
+
537
+ expect(coValue.isAvailable()).toBe(false);
538
+
539
+ const success = coValue.provideHeader(header, "peerId");
540
+ expect(success).toBe(true);
541
+ expect(coValue.isAvailable()).toBe(true);
542
+ });
543
+
544
+ test("provideHeader should return false if the header hash doesn't match the coValue id", () => {
545
+ const [agent, sessionID] = randomAgentAndSessionID();
546
+ const node = new LocalNode(agent.agentSecret, sessionID, Crypto);
547
+
548
+ const header = {
549
+ type: "costream",
550
+ ruleset: { type: "unsafeAllowAll" },
551
+ meta: null,
552
+ ...Crypto.createdNowUnique(),
553
+ } as const;
554
+
555
+ const coValue = node.getCoValue("co_ztest123");
556
+
557
+ expect(coValue.isAvailable()).toBe(false);
558
+
559
+ const success = coValue.provideHeader(header, "peerId");
560
+ expect(success).toBe(false);
561
+ expect(coValue.isAvailable()).toBe(false);
526
562
  });
527
563
 
528
564
  test("markErrored should work with multiple peers", () => {
@@ -1,11 +1,12 @@
1
1
  import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
2
2
  import { PeerState } from "../PeerState";
3
- import { CoValueCore } from "../coValueCore/coValueCore";
3
+ import { CoValueCore, idforHeader } from "../coValueCore/coValueCore";
4
4
  import { CoValueHeader, VerifiedState } from "../coValueCore/verifiedState";
5
5
  import { RawCoID } from "../ids";
6
6
  import { LocalNode } from "../localNode";
7
7
  import { Peer } from "../sync";
8
8
  import { createTestMetricReader, tearDownTestMetricReader } from "./testUtils";
9
+ import { WasmCrypto } from "../crypto/WasmCrypto";
9
10
 
10
11
  let metricReader: ReturnType<typeof createTestMetricReader>;
11
12
 
@@ -17,10 +18,21 @@ afterEach(() => {
17
18
  tearDownTestMetricReader();
18
19
  });
19
20
 
20
- const mockNode = {} as LocalNode;
21
+ const mockNode = {
22
+ crypto: await WasmCrypto.create(),
23
+ } as unknown as LocalNode;
24
+
25
+ const testCoValueHeader = {
26
+ type: "comap",
27
+ ruleset: { type: "ownedByGroup", group: "co_ztest123" },
28
+ meta: null,
29
+ uniqueness: null,
30
+ } as CoValueHeader;
31
+
32
+ const testCoValueId = idforHeader(testCoValueHeader, mockNode.crypto);
21
33
 
22
34
  describe("CoValueCore loading state", () => {
23
- const mockCoValueId = "co_test123" as RawCoID;
35
+ const mockCoValueId = testCoValueId;
24
36
 
25
37
  test("should create unknown state", async () => {
26
38
  const state = CoValueCore.fromID(mockCoValueId, mockNode);
@@ -195,7 +207,7 @@ describe("CoValueCore loading state", () => {
195
207
  role: "server",
196
208
  },
197
209
  async () => {
198
- state.provideHeader({} as CoValueHeader, "peer1");
210
+ state.provideHeader(testCoValueHeader, "peer1");
199
211
  },
200
212
  );
201
213
  const peer2 = createMockPeerState(
@@ -248,7 +260,7 @@ describe("CoValueCore loading state", () => {
248
260
  role: "server",
249
261
  },
250
262
  async () => {
251
- state.provideHeader({} as CoValueHeader, "peer2");
263
+ state.provideHeader(testCoValueHeader, "peer2");
252
264
  },
253
265
  );
254
266