cojson 0.18.6 → 0.18.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +11 -0
  3. package/dist/coValueContentMessage.d.ts +2 -0
  4. package/dist/coValueContentMessage.d.ts.map +1 -1
  5. package/dist/coValueContentMessage.js +7 -0
  6. package/dist/coValueContentMessage.js.map +1 -1
  7. package/dist/coValueCore/SessionMap.d.ts +2 -2
  8. package/dist/coValueCore/SessionMap.d.ts.map +1 -1
  9. package/dist/coValueCore/SessionMap.js +2 -4
  10. package/dist/coValueCore/SessionMap.js.map +1 -1
  11. package/dist/coValueCore/branching.d.ts +31 -9
  12. package/dist/coValueCore/branching.d.ts.map +1 -1
  13. package/dist/coValueCore/branching.js +50 -100
  14. package/dist/coValueCore/branching.js.map +1 -1
  15. package/dist/coValueCore/coValueCore.d.ts +12 -8
  16. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  17. package/dist/coValueCore/coValueCore.js +93 -23
  18. package/dist/coValueCore/coValueCore.js.map +1 -1
  19. package/dist/coValueCore/verifiedState.d.ts +4 -2
  20. package/dist/coValueCore/verifiedState.d.ts.map +1 -1
  21. package/dist/coValueCore/verifiedState.js +6 -4
  22. package/dist/coValueCore/verifiedState.js.map +1 -1
  23. package/dist/coValues/coList.d.ts.map +1 -1
  24. package/dist/coValues/coList.js +10 -1
  25. package/dist/coValues/coList.js.map +1 -1
  26. package/dist/coValues/coMap.d.ts +2 -2
  27. package/dist/coValues/coMap.d.ts.map +1 -1
  28. package/dist/coValues/coMap.js +8 -8
  29. package/dist/coValues/coMap.js.map +1 -1
  30. package/dist/coValues/group.d.ts.map +1 -1
  31. package/dist/coValues/group.js +14 -1
  32. package/dist/coValues/group.js.map +1 -1
  33. package/dist/config.d.ts +6 -0
  34. package/dist/config.d.ts.map +1 -1
  35. package/dist/config.js +8 -0
  36. package/dist/config.js.map +1 -1
  37. package/dist/crypto/PureJSCrypto.d.ts.map +1 -1
  38. package/dist/crypto/PureJSCrypto.js +14 -6
  39. package/dist/crypto/PureJSCrypto.js.map +1 -1
  40. package/dist/exports.d.ts +3 -2
  41. package/dist/exports.d.ts.map +1 -1
  42. package/dist/exports.js +2 -2
  43. package/dist/exports.js.map +1 -1
  44. package/dist/localNode.d.ts.map +1 -1
  45. package/dist/localNode.js +7 -2
  46. package/dist/localNode.js.map +1 -1
  47. package/dist/storage/storageAsync.d.ts.map +1 -1
  48. package/dist/storage/storageAsync.js.map +1 -1
  49. package/dist/sync.d.ts +1 -3
  50. package/dist/sync.d.ts.map +1 -1
  51. package/dist/sync.js +8 -18
  52. package/dist/sync.js.map +1 -1
  53. package/dist/tests/branching.test.js +107 -9
  54. package/dist/tests/branching.test.js.map +1 -1
  55. package/dist/tests/coValueCore.test.js +45 -1
  56. package/dist/tests/coValueCore.test.js.map +1 -1
  57. package/dist/tests/sync.content.test.d.ts +2 -0
  58. package/dist/tests/sync.content.test.d.ts.map +1 -0
  59. package/dist/tests/sync.content.test.js +120 -0
  60. package/dist/tests/sync.content.test.js.map +1 -0
  61. package/dist/tests/sync.load.test.js +2 -2
  62. package/dist/tests/sync.storage.test.js +1 -1
  63. package/dist/tests/sync.upload.test.js +2 -2
  64. package/package.json +2 -2
  65. package/src/coValueContentMessage.ts +13 -0
  66. package/src/coValueCore/SessionMap.ts +2 -2
  67. package/src/coValueCore/branching.ts +94 -149
  68. package/src/coValueCore/coValueCore.ts +121 -27
  69. package/src/coValueCore/verifiedState.ts +8 -0
  70. package/src/coValues/coList.ts +12 -1
  71. package/src/coValues/coMap.ts +10 -12
  72. package/src/coValues/group.ts +14 -1
  73. package/src/config.ts +9 -0
  74. package/src/crypto/PureJSCrypto.ts +25 -13
  75. package/src/exports.ts +7 -1
  76. package/src/localNode.ts +8 -2
  77. package/src/storage/storageAsync.ts +0 -1
  78. package/src/sync.ts +8 -32
  79. package/src/tests/branching.test.ts +158 -9
  80. package/src/tests/coValueCore.test.ts +62 -2
  81. package/src/tests/sync.content.test.ts +153 -0
  82. package/src/tests/sync.load.test.ts +2 -2
  83. package/src/tests/sync.storage.test.ts +1 -1
  84. package/src/tests/sync.upload.test.ts +2 -2
@@ -5,10 +5,10 @@ import type { RawCoValue } from "../coValue.js";
5
5
  import type { ControlledAccountOrAgent } from "../coValues/account.js";
6
6
  import type { RawGroup } from "../coValues/group.js";
7
7
  import { CO_VALUE_LOADING_CONFIG } from "../config.js";
8
+ import { validateTxSizeLimitInBytes } from "../coValueContentMessage.js";
8
9
  import { coreToCoValue } from "../coreToCoValue.js";
9
10
  import {
10
11
  CryptoProvider,
11
- Encrypted,
12
12
  Hash,
13
13
  KeyID,
14
14
  KeySecret,
@@ -17,7 +17,6 @@ import {
17
17
  } from "../crypto/crypto.js";
18
18
  import { AgentID, RawCoID, SessionID, TransactionID } from "../ids.js";
19
19
  import { JsonObject, JsonValue } from "../jsonValue.js";
20
- import { parseJSON, safeParseJSON } from "../jsonStringify.js";
21
20
  import { LocalNode, ResolveAccountAgentError } from "../localNode.js";
22
21
  import { logger } from "../logger.js";
23
22
  import { determineValidTransactions } from "../permissions.js";
@@ -29,10 +28,14 @@ import { CoValueHeader, Transaction, VerifiedState } from "./verifiedState.js";
29
28
  import { SessionMap } from "./SessionMap.js";
30
29
  import {
31
30
  MergeCommit,
31
+ BranchPointerCommit,
32
+ MergedTransactionMetadata,
32
33
  createBranch,
33
34
  getBranchId,
35
+ getBranchOwnerId,
34
36
  getBranchSource,
35
37
  mergeBranch,
38
+ BranchStartCommit,
36
39
  } from "./branching.js";
37
40
  import { type RawAccountID } from "../coValues/account.js";
38
41
  import { decodeTransactionChangesAndMeta } from "./decodeTransactionChangesAndMeta.js";
@@ -70,6 +73,9 @@ export type VerifiedTransaction = {
70
73
 
71
74
  // True if the meta information has been parsed and loaded in the CoValueCore
72
75
  hasMetaBeenParsed: boolean;
76
+
77
+ // The previous verified transaction for the same session
78
+ previous: VerifiedTransaction | undefined;
73
79
  };
74
80
 
75
81
  export type DecryptedTransaction = {
@@ -600,6 +606,7 @@ export class CoValueCore {
600
606
  changes: JsonValue[],
601
607
  privacy: "private" | "trusting",
602
608
  meta?: JsonObject,
609
+ madeAt?: number,
603
610
  ): boolean {
604
611
  if (!this.verified) {
605
612
  throw new Error(
@@ -607,6 +614,8 @@ export class CoValueCore {
607
614
  );
608
615
  }
609
616
 
617
+ validateTxSizeLimitInBytes(changes);
618
+
610
619
  // This is an ugly hack to get a unique but stable session ID for editing the current account
611
620
  const sessionID =
612
621
  this.verified.header.meta?.type === "account"
@@ -634,6 +643,7 @@ export class CoValueCore {
634
643
  keyID,
635
644
  keySecret,
636
645
  meta,
646
+ madeAt ?? Date.now(),
637
647
  );
638
648
  } else {
639
649
  result = this.verified.makeNewTrustingTransaction(
@@ -641,6 +651,7 @@ export class CoValueCore {
641
651
  signerAgent,
642
652
  changes,
643
653
  meta,
654
+ madeAt ?? Date.now(),
644
655
  );
645
656
  }
646
657
 
@@ -689,12 +700,13 @@ export class CoValueCore {
689
700
  }
690
701
 
691
702
  // The starting point of the branch, in case this CoValue is a branch
692
- branchStart:
693
- | { branch: CoValueKnownState["sessions"]; madeAt: number }
694
- | undefined;
703
+ branchStart: { from: BranchStartCommit["from"]; madeAt: number } | undefined;
695
704
 
696
705
  // The list of merge commits that have been made
697
- mergeCommits: { commit: MergeCommit; madeAt: number }[] = [];
706
+ mergeCommits: MergeCommit[] = [];
707
+ branches: BranchPointerCommit[] = [];
708
+ earliestTxMadeAt: number = Number.MAX_SAFE_INTEGER;
709
+ latestTxMadeAt: number = 0;
698
710
 
699
711
  // Reset the parsed transactions and branches, to validate them again from scratch when the group is updated
700
712
  resetParsedTransactions() {
@@ -710,6 +722,11 @@ export class CoValueCore {
710
722
  verifiedTransactions: VerifiedTransaction[] = [];
711
723
  private verifiedTransactionsKnownSessions: CoValueKnownState["sessions"] = {};
712
724
 
725
+ private lastVerifiedTransactionBySessionID: Record<
726
+ SessionID,
727
+ VerifiedTransaction
728
+ > = {};
729
+
713
730
  /**
714
731
  * Loads the new transaction from the SessionMap into verifiedTransactions as a VerifiedTransaction.
715
732
  *
@@ -752,7 +769,7 @@ export class CoValueCore {
752
769
  txIndex,
753
770
  };
754
771
 
755
- this.verifiedTransactions.push({
772
+ const verifiedTransaction = {
756
773
  author: accountOrAgentIDfromSessionID(sessionID),
757
774
  txID,
758
775
  madeAt: tx.madeAt,
@@ -764,7 +781,20 @@ export class CoValueCore {
764
781
  hasInvalidMeta: false,
765
782
  hasMetaBeenParsed: false,
766
783
  tx,
767
- });
784
+ previous: this.lastVerifiedTransactionBySessionID[sessionID],
785
+ };
786
+
787
+ if (verifiedTransaction.madeAt > this.latestTxMadeAt) {
788
+ this.latestTxMadeAt = verifiedTransaction.madeAt;
789
+ }
790
+
791
+ if (verifiedTransaction.madeAt < this.earliestTxMadeAt) {
792
+ this.earliestTxMadeAt = verifiedTransaction.madeAt;
793
+ }
794
+
795
+ this.verifiedTransactions.push(verifiedTransaction);
796
+ this.lastVerifiedTransactionBySessionID[sessionID] =
797
+ verifiedTransaction;
768
798
  });
769
799
 
770
800
  this.verifiedTransactionsKnownSessions[sessionID] =
@@ -793,23 +823,54 @@ export class CoValueCore {
793
823
 
794
824
  transaction.hasMetaBeenParsed = true;
795
825
 
796
- if (
797
- transaction.meta?.["branch"] &&
798
- (!this.branchStart || transaction.madeAt < this.branchStart.madeAt)
799
- ) {
800
- this.branchStart = {
801
- branch: transaction.meta.branch as CoValueKnownState["sessions"],
802
- madeAt: transaction.madeAt,
803
- };
826
+ // Branch related meta information
827
+ if (this.isBranch()) {
828
+ // Check if the transaction is a branch start
829
+ if ("from" in transaction.meta) {
830
+ if (!this.branchStart || transaction.madeAt < this.branchStart.madeAt) {
831
+ const commit = transaction.meta as BranchStartCommit;
832
+
833
+ this.branchStart = {
834
+ from: commit.from,
835
+ madeAt: transaction.madeAt,
836
+ };
837
+ }
838
+ }
839
+
840
+ // Check if the transaction is a merged checkpoint for a branch
841
+ if ("merged" in transaction.meta) {
842
+ const mergeCommit = transaction.meta as MergeCommit;
843
+ this.mergeCommits.push(mergeCommit);
844
+ }
804
845
  }
805
846
 
806
- if (transaction.meta?.["merge"]) {
807
- const mergeCommit = transaction.meta as MergeCommit;
847
+ // Check if the transaction is a branch pointer
848
+ if ("branch" in transaction.meta) {
849
+ const branch = transaction.meta as BranchPointerCommit;
808
850
 
809
- this.mergeCommits.push({
810
- commit: mergeCommit,
811
- madeAt: transaction.madeAt,
812
- });
851
+ this.branches.push(branch);
852
+ }
853
+
854
+ // Check if the transaction has been merged from a branch
855
+ if ("mi" in transaction.meta) {
856
+ const meta = transaction.meta as MergedTransactionMetadata;
857
+
858
+ // Check if the transaction is a merge commit
859
+ const previousTransaction = transaction.previous?.txID;
860
+ const sessionID = meta.s ?? previousTransaction?.sessionID;
861
+
862
+ if (sessionID) {
863
+ transaction.txID = {
864
+ sessionID,
865
+ txIndex: meta.mi,
866
+ branch: meta.b ?? previousTransaction?.branch,
867
+ };
868
+ } else {
869
+ logger.error("Merge commit without session ID", {
870
+ txID: transaction.txID,
871
+ prev: previousTransaction ?? null,
872
+ });
873
+ }
813
874
  }
814
875
  }
815
876
 
@@ -876,7 +937,9 @@ export class CoValueCore {
876
937
  const { txID } = transaction;
877
938
 
878
939
  const from = options?.from?.[txID.sessionID] ?? -1;
879
- const to = options?.to?.[txID.sessionID] ?? Infinity;
940
+
941
+ // Load the to filter index. Sessions that are not in the to filter will be skipped
942
+ const to = options?.to ? (options.to[txID.sessionID] ?? -1) : Infinity;
880
943
 
881
944
  // The txIndex starts at 0 and from/to are referring to the count of transactions
882
945
  if (from > txID.txIndex || to < txID.txIndex) {
@@ -889,7 +952,7 @@ export class CoValueCore {
889
952
  // If this is a branch, we load the valid transactions from the source
890
953
  if (source && this.branchStart && !options?.skipBranchSource) {
891
954
  const sourceTransactions = source.getValidTransactions({
892
- to: this.branchStart.branch,
955
+ to: this.branchStart.from,
893
956
  ignorePrivateTransactions: options?.ignorePrivateTransactions ?? false,
894
957
  knownTransactions: options?.knownTransactions,
895
958
  });
@@ -915,15 +978,46 @@ export class CoValueCore {
915
978
  }
916
979
 
917
980
  getCurrentBranchName() {
918
- return this.verified?.header.meta?.branch as string | undefined;
981
+ return this.verified?.branchName;
919
982
  }
920
983
 
921
984
  getCurrentBranchSourceId() {
922
- return this.verified?.header.meta?.source as RawCoID | undefined;
985
+ return this.verified?.branchSourceId;
923
986
  }
924
987
 
925
988
  isBranch() {
926
- return Boolean(this.getCurrentBranchSourceId());
989
+ return Boolean(this.verified?.branchSourceId);
990
+ }
991
+
992
+ hasBranch(name: string, ownerId?: RawCoID) {
993
+ // This function requires the meta information to be parsed, which might not be the case
994
+ // if the value content hasn't been loaded yet
995
+ this.parseNewTransactions(false);
996
+
997
+ const currentOwnerId = getBranchOwnerId(this);
998
+ return this.branches.some((item) => {
999
+ if (item.branch !== name) {
1000
+ return false;
1001
+ }
1002
+
1003
+ if (item.ownerId === ownerId) {
1004
+ return true;
1005
+ }
1006
+
1007
+ if (!ownerId) {
1008
+ return item.ownerId === currentOwnerId;
1009
+ }
1010
+
1011
+ if (!item.ownerId) {
1012
+ return ownerId === currentOwnerId;
1013
+ }
1014
+ });
1015
+ }
1016
+
1017
+ getMergeCommits() {
1018
+ this.parseNewTransactions(false);
1019
+
1020
+ return this.mergeCommits;
927
1021
  }
928
1022
 
929
1023
  getValidSortedTransactions(options?: {
@@ -59,6 +59,8 @@ export class VerifiedState {
59
59
  private _cachedNewContentSinceEmpty: NewContentMessage[] | undefined;
60
60
  private streamingKnownState?: CoValueKnownState["sessions"];
61
61
  public lastAccessed: number | undefined;
62
+ public branchSourceId?: RawCoID;
63
+ public branchName?: string;
62
64
 
63
65
  constructor(
64
66
  id: RawCoID,
@@ -74,6 +76,8 @@ export class VerifiedState {
74
76
  this.streamingKnownState = streamingKnownState
75
77
  ? { ...streamingKnownState }
76
78
  : undefined;
79
+ this.branchSourceId = header.meta?.source as RawCoID | undefined;
80
+ this.branchName = header.meta?.branch as string | undefined;
77
81
  }
78
82
 
79
83
  clone(): VerifiedState {
@@ -114,12 +118,14 @@ export class VerifiedState {
114
118
  signerAgent: ControlledAccountOrAgent,
115
119
  changes: JsonValue[],
116
120
  meta: JsonObject | undefined,
121
+ madeAt: number,
117
122
  ) {
118
123
  const result = this.sessions.makeNewTrustingTransaction(
119
124
  sessionID,
120
125
  signerAgent,
121
126
  changes,
122
127
  meta,
128
+ madeAt,
123
129
  );
124
130
 
125
131
  this._cachedNewContentSinceEmpty = undefined;
@@ -135,6 +141,7 @@ export class VerifiedState {
135
141
  keyID: KeyID,
136
142
  keySecret: KeySecret,
137
143
  meta: JsonObject | undefined,
144
+ madeAt: number,
138
145
  ) {
139
146
  const result = this.sessions.makeNewPrivateTransaction(
140
147
  sessionID,
@@ -143,6 +150,7 @@ export class VerifiedState {
143
150
  keyID,
144
151
  keySecret,
145
152
  meta,
153
+ madeAt,
146
154
  );
147
155
 
148
156
  this._cachedNewContentSinceEmpty = undefined;
@@ -138,7 +138,13 @@ export class RawCoList<
138
138
  sessionEntry[opID.txIndex] = txEntry;
139
139
  }
140
140
 
141
+ // Check if the change index already exists, may be the case of double merges
142
+ if (txEntry[opID.changeIdx]) {
143
+ return false;
144
+ }
145
+
141
146
  txEntry[opID.changeIdx] = value;
147
+ return true;
142
148
  }
143
149
 
144
150
  private isDeleted(opID: OpID) {
@@ -216,13 +222,18 @@ export class RawCoList<
216
222
  };
217
223
 
218
224
  if (change.op === "pre" || change.op === "app") {
219
- this.createInsertionsEntry(opID, {
225
+ const created = this.createInsertionsEntry(opID, {
220
226
  madeAt,
221
227
  predecessors: [],
222
228
  successors: [],
223
229
  change,
224
230
  });
225
231
 
232
+ // If the change index already exists, we don't need to process it again
233
+ if (!created) {
234
+ continue;
235
+ }
236
+
226
237
  if (change.op === "pre") {
227
238
  if (change.before === "end") {
228
239
  this.beforeEnd.push(opID);
@@ -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
@@ -479,8 +479,14 @@ export class LocalNode {
479
479
  return branch.getCurrentContent() as T;
480
480
  }
481
481
 
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);
482
+ // Do a synchronous check to see if the branch exists, if not we don't need to try to load the branch
483
+ if (!source.hasBranch(branchName, branchOwnerID)) {
484
+ return source
485
+ .createBranch(branchName, branchOwnerID)
486
+ .getCurrentContent() as T;
487
+ }
488
+
489
+ await this.loadCoValueCore(branch.id);
484
490
 
485
491
  if (!branch.isAvailable()) {
486
492
  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
@@ -219,28 +219,7 @@ export class SyncManager {
219
219
  }
220
220
  }
221
221
 
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
- ) {
222
+ sendNewContent(id: RawCoID, peer: PeerState, seen: Set<RawCoID> = new Set()) {
244
223
  if (seen.has(id)) {
245
224
  return;
246
225
  }
@@ -253,9 +232,10 @@ export class SyncManager {
253
232
  return;
254
233
  }
255
234
 
235
+ const includeDependencies = peer.role !== "server";
256
236
  if (includeDependencies) {
257
237
  for (const dependency of coValue.getDependedOnCoValues()) {
258
- this.sendNewContentIncludingDependencies(dependency, peer, seen);
238
+ this.sendNewContent(dependency, peer, seen);
259
239
  }
260
240
  }
261
241
 
@@ -434,7 +414,7 @@ export class SyncManager {
434
414
  const coValue = this.local.getCoValue(msg.id);
435
415
 
436
416
  if (coValue.isAvailable()) {
437
- this.sendNewContentIncludingDependencies(msg.id, peer);
417
+ this.sendNewContent(msg.id, peer);
438
418
  return;
439
419
  }
440
420
 
@@ -444,7 +424,7 @@ export class SyncManager {
444
424
 
445
425
  const handleLoadResult = () => {
446
426
  if (coValue.isAvailable()) {
447
- this.sendNewContentIncludingDependencies(msg.id, peer);
427
+ this.sendNewContent(msg.id, peer);
448
428
  return;
449
429
  }
450
430
 
@@ -478,11 +458,7 @@ export class SyncManager {
478
458
  }
479
459
 
480
460
  if (coValue.isAvailable()) {
481
- if (peer.role === "server") {
482
- this.sendNewContentWithoutDependencies(msg.id, peer);
483
- } else {
484
- this.sendNewContentIncludingDependencies(msg.id, peer);
485
- }
461
+ this.sendNewContent(msg.id, peer);
486
462
  }
487
463
  }
488
464
 
@@ -777,7 +753,7 @@ export class SyncManager {
777
753
 
778
754
  // We directly forward the new content to peers that have an active subscription
779
755
  if (peer.optimisticKnownStates.has(coValue.id)) {
780
- this.sendNewContentIncludingDependencies(coValue.id, peer);
756
+ this.sendNewContent(coValue.id, peer);
781
757
  syncedPeers.push(peer);
782
758
  } else if (
783
759
  peer.role === "server" &&
@@ -808,7 +784,7 @@ export class SyncManager {
808
784
  handleCorrection(msg: KnownStateMessage, peer: PeerState) {
809
785
  peer.setKnownState(msg.id, knownStateIn(msg));
810
786
 
811
- return this.sendNewContentIncludingDependencies(msg.id, peer);
787
+ return this.sendNewContent(msg.id, peer);
812
788
  }
813
789
 
814
790
  private syncQueue = new LocalTransactionsSyncQueue((content) =>