cojson 0.1.11 → 0.1.12

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/src/crypto.ts CHANGED
@@ -2,12 +2,39 @@ import { ed25519, x25519 } from "@noble/curves/ed25519";
2
2
  import { xsalsa20_poly1305, xsalsa20 } from "@noble/ciphers/salsa";
3
3
  import { JsonValue } from "./jsonValue.js";
4
4
  import { base58 } from "@scure/base";
5
- import stableStringify from "fast-json-stable-stringify";
6
- import { blake3 } from "@noble/hashes/blake3";
7
5
  import { randomBytes } from "@noble/ciphers/webcrypto/utils";
8
6
  import { AgentID, RawCoID, TransactionID } from "./ids.js";
9
7
  import { base64URLtoBytes, bytesToBase64url } from "./base64url.js";
10
8
 
9
+ import { createBLAKE3 } from 'hash-wasm';
10
+ import { stableStringify } from "./fastJsonStableStringify.js";
11
+
12
+ let blake3Instance: Awaited<ReturnType<typeof createBLAKE3>>;
13
+ let blake3HashOnce: (data: Uint8Array) => Uint8Array;
14
+ let blake3HashOnceWithContext: (data: Uint8Array, {context}: {context: Uint8Array}) => Uint8Array;
15
+ let blake3incrementalUpdateSLOW_WITH_DEVTOOLS: (state: Uint8Array, data: Uint8Array) => Uint8Array;
16
+ let blake3digestForState: (state: Uint8Array) => Uint8Array;
17
+
18
+ export const cryptoReady = new Promise<void>((resolve) => {
19
+ createBLAKE3().then(bl3 => {
20
+ blake3Instance = bl3;
21
+ blake3HashOnce = (data) => {
22
+ return bl3.init().update(data).digest('binary');
23
+ }
24
+ blake3HashOnceWithContext = (data, {context}) => {
25
+ return bl3.init().update(context).update(data).digest('binary');
26
+ }
27
+ blake3incrementalUpdateSLOW_WITH_DEVTOOLS = (state, data) => {
28
+ bl3.load(state).update(data);
29
+ return bl3.save();
30
+ }
31
+ blake3digestForState = (state) => {
32
+ return bl3.load(state).digest('binary');
33
+ }
34
+ resolve();
35
+ })
36
+ });
37
+
11
38
  export type SignerSecret = `signerSecret_z${string}`;
12
39
  export type SignerID = `signer_z${string}`;
13
40
  export type Signature = `signature_z${string}`;
@@ -128,7 +155,7 @@ export function seal<T extends JsonValue>(
128
155
  to: SealerID,
129
156
  nOnceMaterial: { in: RawCoID; tx: TransactionID }
130
157
  ): Sealed<T> {
131
- const nOnce = blake3(
158
+ const nOnce = blake3HashOnce(
132
159
  textEncoder.encode(stableStringify(nOnceMaterial))
133
160
  ).slice(0, 24);
134
161
 
@@ -153,7 +180,7 @@ export function unseal<T extends JsonValue>(
153
180
  from: SealerID,
154
181
  nOnceMaterial: { in: RawCoID; tx: TransactionID }
155
182
  ): T | undefined {
156
- const nOnce = blake3(
183
+ const nOnce = blake3HashOnce(
157
184
  textEncoder.encode(stableStringify(nOnceMaterial))
158
185
  ).slice(0, 24);
159
186
 
@@ -181,28 +208,32 @@ export type Hash = `hash_z${string}`;
181
208
 
182
209
  export function secureHash(value: JsonValue): Hash {
183
210
  return `hash_z${base58.encode(
184
- blake3(textEncoder.encode(stableStringify(value)))
211
+ blake3HashOnce(textEncoder.encode(stableStringify(value)))
185
212
  )}`;
186
213
  }
187
214
 
188
215
  export class StreamingHash {
189
- state: ReturnType<typeof blake3.create>;
216
+ state: Uint8Array;
190
217
 
191
- constructor(fromClone?: ReturnType<typeof blake3.create>) {
192
- this.state = fromClone || blake3.create({});
218
+ constructor(fromClone?: Uint8Array) {
219
+ this.state = fromClone || blake3Instance.init().save();
193
220
  }
194
221
 
195
222
  update(value: JsonValue) {
196
- this.state.update(textEncoder.encode(stableStringify(value)));
223
+ const encoded = textEncoder.encode(stableStringify(value))
224
+ // const before = performance.now();
225
+ this.state = blake3incrementalUpdateSLOW_WITH_DEVTOOLS(this.state, encoded);
226
+ // const after = performance.now();
227
+ // console.log(`Hashing throughput in MB/s`, 1000 * (encoded.length / (after - before)) / (1024 * 1024));
197
228
  }
198
229
 
199
230
  digest(): Hash {
200
- const hash = this.state.digest();
231
+ const hash = blake3digestForState(this.state);
201
232
  return `hash_z${base58.encode(hash)}`;
202
233
  }
203
234
 
204
235
  clone(): StreamingHash {
205
- return new StreamingHash(this.state.clone());
236
+ return new StreamingHash(new Uint8Array(this.state));
206
237
  }
207
238
  }
208
239
 
@@ -211,7 +242,7 @@ export const shortHashLength = 19;
211
242
 
212
243
  export function shortHash(value: JsonValue): ShortHash {
213
244
  return `shortHash_z${base58.encode(
214
- blake3(textEncoder.encode(stableStringify(value))).slice(
245
+ blake3HashOnce(textEncoder.encode(stableStringify(value))).slice(
215
246
  0,
216
247
  shortHashLength
217
248
  )
@@ -241,7 +272,7 @@ function encrypt<T extends JsonValue, N extends JsonValue>(
241
272
  const keySecretBytes = base58.decode(
242
273
  keySecret.substring("keySecret_z".length)
243
274
  );
244
- const nOnce = blake3(
275
+ const nOnce = blake3HashOnce(
245
276
  textEncoder.encode(stableStringify(nOnceMaterial))
246
277
  ).slice(0, 24);
247
278
 
@@ -293,7 +324,7 @@ function decrypt<T extends JsonValue, N extends JsonValue>(
293
324
  const keySecretBytes = base58.decode(
294
325
  keySecret.substring("keySecret_z".length)
295
326
  );
296
- const nOnce = blake3(
327
+ const nOnce = blake3HashOnce(
297
328
  textEncoder.encode(stableStringify(nOnceMaterial))
298
329
  ).slice(0, 24);
299
330
 
@@ -365,11 +396,11 @@ export function agentSecretFromSecretSeed(secretSeed: Uint8Array): AgentSecret {
365
396
  }
366
397
 
367
398
  return `sealerSecret_z${base58.encode(
368
- blake3(secretSeed, {
399
+ blake3HashOnceWithContext(secretSeed, {
369
400
  context: textEncoder.encode("seal"),
370
401
  })
371
402
  )}/signerSecret_z${base58.encode(
372
- blake3(secretSeed, {
403
+ blake3HashOnceWithContext(secretSeed, {
373
404
  context: textEncoder.encode("sign"),
374
405
  })
375
406
  )}`;
@@ -0,0 +1,54 @@
1
+ // adapted from fast-json-stable-stringify (https://github.com/epoberezkin/fast-json-stable-stringify)
2
+
3
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4
+ export function stableStringify(data: any): string | undefined {
5
+ const cycles = false;
6
+
7
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
8
+ const seen: any[] = [];
9
+ let node = data;
10
+
11
+ if (node && node.toJSON && typeof node.toJSON === "function") {
12
+ node = node.toJSON();
13
+ }
14
+
15
+ if (node === undefined) return;
16
+ if (typeof node == "number") return isFinite(node) ? "" + node : "null";
17
+ if (typeof node !== "object") {
18
+ if (typeof node === "string" && (node.startsWith("encrypted_U") || node.startsWith("binary_U"))) {
19
+ return `"${node}"`;
20
+ }
21
+ return JSON.stringify(node);
22
+ }
23
+
24
+ let i, out;
25
+ if (Array.isArray(node)) {
26
+ out = "[";
27
+ for (i = 0; i < node.length; i++) {
28
+ if (i) out += ",";
29
+ out += stableStringify(node[i]) || "null";
30
+ }
31
+ return out + "]";
32
+ }
33
+
34
+ if (node === null) return "null";
35
+
36
+ if (seen.indexOf(node) !== -1) {
37
+ if (cycles) return JSON.stringify("__cycle__");
38
+ throw new TypeError("Converting circular structure to JSON");
39
+ }
40
+
41
+ const seenIndex = seen.push(node) - 1;
42
+ const keys = Object.keys(node).sort();
43
+ out = "";
44
+ for (i = 0; i < keys.length; i++) {
45
+ const key = keys[i]!;
46
+ const value = stableStringify(node[key]);
47
+
48
+ if (!value) continue;
49
+ if (out) out += ",";
50
+ out += JSON.stringify(key) + ":" + value;
51
+ }
52
+ seen.splice(seenIndex, 1);
53
+ return "{" + out + "}";
54
+ }
package/src/group.test.ts CHANGED
@@ -1,6 +1,10 @@
1
- import { LocalNode, CoMap, CoList, CoStream, BinaryCoStream } from "./index";
1
+ import { LocalNode, CoMap, CoList, CoStream, BinaryCoStream, cojsonReady } from "./index";
2
2
  import { randomAnonymousAccountAndSessionID } from "./testUtils";
3
3
 
4
+ beforeEach(async () => {
5
+ await cojsonReady;
6
+ });
7
+
4
8
  test("Can create a CoMap in a group", () => {
5
9
  const node = new LocalNode(...randomAnonymousAccountAndSessionID());
6
10
 
package/src/index.ts CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  agentSecretFromSecretSeed,
19
19
  secretSeedLength,
20
20
  shortHashLength,
21
+ cryptoReady
21
22
  } from "./crypto.js";
22
23
  import { connectedPeers } from "./streamUtils.js";
23
24
  import { AnonymousControlledAccount, ControlledAccount } from "./account.js";
@@ -69,6 +70,7 @@ export {
69
70
  CoValueCore,
70
71
  AnonymousControlledAccount,
71
72
  ControlledAccount,
73
+ cryptoReady as cojsonReady,
72
74
  };
73
75
 
74
76
  export type {
@@ -17,7 +17,11 @@ import {
17
17
  groupWithTwoAdmins,
18
18
  groupWithTwoAdminsHighLevel,
19
19
  } from "./testUtils.js";
20
- import { AnonymousControlledAccount } from "./index.js";
20
+ import { AnonymousControlledAccount, cojsonReady } from "./index.js";
21
+
22
+ beforeEach(async () => {
23
+ await cojsonReady;
24
+ });
21
25
 
22
26
  test("Initial admin can add another admin to a group", () => {
23
27
  groupWithTwoAdmins();
package/src/sync.test.ts CHANGED
@@ -1,14 +1,9 @@
1
1
  import { newRandomSessionID } from "./coValueCore.js";
2
2
  import { LocalNode } from "./node.js";
3
- import { Peer, PeerID, SyncMessage } from "./sync.js";
3
+ import { SyncMessage } from "./sync.js";
4
4
  import { expectMap } from "./coValue.js";
5
5
  import { MapOpPayload } from "./coValues/coMap.js";
6
6
  import { Group } from "./group.js";
7
- import {
8
- ReadableStream,
9
- WritableStream,
10
- TransformStream,
11
- } from "isomorphic-streams";
12
7
  import {
13
8
  randomAnonymousAccountAndSessionID,
14
9
  shouldNotResolve,
@@ -18,6 +13,11 @@ import {
18
13
  newStreamPair
19
14
  } from "./streamUtils.js";
20
15
  import { AccountID } from "./account.js";
16
+ import { cojsonReady } from "./index.js";
17
+
18
+ beforeEach(async () => {
19
+ await cojsonReady;
20
+ });
21
21
 
22
22
  test("Node replies with initial tx and header to empty subscribe", async () => {
23
23
  const [admin, session] = randomAnonymousAccountAndSessionID();
package/src/sync.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  WritableStreamDefaultWriter,
10
10
  } from "isomorphic-streams";
11
11
  import { RawCoID, SessionID } from "./ids.js";
12
+ import { stableStringify } from "./fastJsonStableStringify.js";
12
13
 
13
14
  export type CoValueKnownState = {
14
15
  id: RawCoID;
@@ -445,12 +446,26 @@ export class SyncManager {
445
446
  const newTransactions =
446
447
  newContentForSession.newTransactions.slice(alreadyKnownOffset);
447
448
 
448
- const success = coValue.tryAddTransactions(
449
+ const before = performance.now();
450
+ const success = await coValue.tryAddTransactionsAsync(
449
451
  sessionID,
450
452
  newTransactions,
451
453
  undefined,
452
454
  newContentForSession.lastSignature
453
455
  );
456
+ const after = performance.now();
457
+ if (after - before > 10) {
458
+ const totalTxLength = newTransactions.map(t => stableStringify(t)!.length).reduce((a, b) => a + b, 0);
459
+ console.log(
460
+ "Adding incoming transactions took",
461
+ after - before,
462
+ "ms",
463
+ totalTxLength,
464
+ "bytes = ",
465
+ "bandwidth: MB/s",
466
+ (1000 * totalTxLength / (after - before)) / (1024 * 1024)
467
+ );
468
+ }
454
469
 
455
470
  if (!success) {
456
471
  console.error("Failed to add transactions", newTransactions);