cojson 0.18.12 → 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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +11 -0
- package/dist/coValueCore/branching.d.ts +5 -0
- package/dist/coValueCore/branching.d.ts.map +1 -1
- package/dist/coValueCore/branching.js +26 -8
- package/dist/coValueCore/branching.js.map +1 -1
- package/dist/coValueCore/coValueCore.d.ts +3 -6
- package/dist/coValueCore/coValueCore.d.ts.map +1 -1
- package/dist/coValueCore/coValueCore.js +22 -17
- package/dist/coValueCore/coValueCore.js.map +1 -1
- package/dist/coValues/account.d.ts +0 -5
- package/dist/coValues/account.d.ts.map +1 -1
- package/dist/coValues/account.js +0 -20
- package/dist/coValues/account.js.map +1 -1
- package/dist/crypto/PureJSCrypto.d.ts +0 -5
- package/dist/crypto/PureJSCrypto.d.ts.map +1 -1
- package/dist/crypto/PureJSCrypto.js +13 -30
- package/dist/crypto/PureJSCrypto.js.map +1 -1
- package/dist/crypto/WasmCrypto.d.ts +0 -4
- package/dist/crypto/WasmCrypto.d.ts.map +1 -1
- package/dist/crypto/WasmCrypto.js +1 -14
- package/dist/crypto/WasmCrypto.js.map +1 -1
- package/dist/crypto/crypto.d.ts +1 -39
- package/dist/crypto/crypto.d.ts.map +1 -1
- package/dist/crypto/crypto.js +11 -53
- package/dist/crypto/crypto.js.map +1 -1
- package/dist/exports.d.ts +3 -2
- package/dist/exports.d.ts.map +1 -1
- package/dist/exports.js +3 -2
- package/dist/exports.js.map +1 -1
- package/dist/knownState.d.ts +3 -0
- package/dist/knownState.d.ts.map +1 -0
- package/dist/knownState.js +8 -0
- package/dist/knownState.js.map +1 -0
- package/dist/localNode.d.ts.map +1 -1
- package/dist/localNode.js +4 -0
- package/dist/localNode.js.map +1 -1
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +8 -1
- package/dist/sync.js.map +1 -1
- package/dist/tests/branching.test.js +36 -29
- package/dist/tests/branching.test.js.map +1 -1
- package/dist/tests/coValueCore.test.js +31 -4
- package/dist/tests/coValueCore.test.js.map +1 -1
- package/dist/tests/coValueCoreLoadingState.test.js +15 -5
- package/dist/tests/coValueCoreLoadingState.test.js.map +1 -1
- package/dist/tests/crypto.test.js +0 -41
- package/dist/tests/crypto.test.js.map +1 -1
- package/dist/tests/sync.upload.test.js +20 -1
- package/dist/tests/sync.upload.test.js.map +1 -1
- package/package.json +2 -2
- package/src/coValueCore/branching.ts +37 -16
- package/src/coValueCore/coValueCore.ts +30 -21
- package/src/coValues/account.ts +0 -25
- package/src/crypto/PureJSCrypto.ts +14 -43
- package/src/crypto/WasmCrypto.ts +0 -19
- package/src/crypto/crypto.ts +12 -94
- package/src/exports.ts +2 -2
- package/src/knownState.ts +17 -0
- package/src/localNode.ts +5 -0
- package/src/sync.ts +11 -2
- package/src/tests/branching.test.ts +45 -34
- package/src/tests/coValueCore.test.ts +40 -4
- package/src/tests/coValueCoreLoadingState.test.ts +17 -5
- package/src/tests/crypto.test.ts +0 -53
- package/src/tests/sync.upload.test.ts +28 -1
- package/dist/tests/cryptoImpl.test.d.ts +0 -2
- package/dist/tests/cryptoImpl.test.d.ts.map +0 -1
- package/dist/tests/cryptoImpl.test.js +0 -144
- package/dist/tests/cryptoImpl.test.js.map +0 -1
- 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 =
|
|
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.
|
|
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
|
|
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.
|
|
322
|
-
|
|
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(),
|
package/src/crypto/WasmCrypto.ts
CHANGED
|
@@ -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
|
}
|
package/src/crypto/crypto.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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(
|
|
102
|
-
expect(
|
|
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
|
-
|
|
127
|
+
expectMap(branch.core.mergeBranch().getCurrentContent());
|
|
122
128
|
|
|
123
129
|
// Verify no merge commit was created
|
|
124
|
-
expect(
|
|
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(
|
|
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
|
|
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
|
|
516
|
+
const map = group.createMap();
|
|
511
517
|
|
|
512
|
-
const
|
|
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.
|
|
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
|
-
|
|
528
|
+
map.id,
|
|
523
529
|
"feature-branch",
|
|
524
530
|
);
|
|
525
|
-
const bobBranch =
|
|
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
|
-
//
|
|
536
|
-
|
|
537
|
-
aliceBranch.
|
|
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
|
-
|
|
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
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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 = {
|
|
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 =
|
|
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(
|
|
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(
|
|
263
|
+
state.provideHeader(testCoValueHeader, "peer2");
|
|
252
264
|
},
|
|
253
265
|
);
|
|
254
266
|
|