cojson 0.13.16 → 0.13.18

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 (168) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +16 -0
  3. package/dist/PeerState.d.ts +3 -0
  4. package/dist/PeerState.d.ts.map +1 -1
  5. package/dist/PeerState.js +9 -0
  6. package/dist/PeerState.js.map +1 -1
  7. package/dist/SyncStateManager.d.ts.map +1 -1
  8. package/dist/SyncStateManager.js +2 -3
  9. package/dist/SyncStateManager.js.map +1 -1
  10. package/dist/coValue.d.ts +6 -4
  11. package/dist/coValue.d.ts.map +1 -1
  12. package/dist/coValue.js +5 -4
  13. package/dist/coValue.js.map +1 -1
  14. package/dist/coValueCore/coValueCore.d.ts +143 -0
  15. package/dist/coValueCore/coValueCore.d.ts.map +1 -0
  16. package/dist/{coValueCore.js → coValueCore/coValueCore.js} +314 -246
  17. package/dist/coValueCore/coValueCore.js.map +1 -0
  18. package/dist/coValueCore/verifiedState.d.ts +65 -0
  19. package/dist/coValueCore/verifiedState.d.ts.map +1 -0
  20. package/dist/coValueCore/verifiedState.js +210 -0
  21. package/dist/coValueCore/verifiedState.js.map +1 -0
  22. package/dist/coValues/account.d.ts +8 -10
  23. package/dist/coValues/account.d.ts.map +1 -1
  24. package/dist/coValues/account.js +12 -13
  25. package/dist/coValues/account.js.map +1 -1
  26. package/dist/coValues/coList.d.ts +10 -6
  27. package/dist/coValues/coList.d.ts.map +1 -1
  28. package/dist/coValues/coList.js +41 -15
  29. package/dist/coValues/coList.js.map +1 -1
  30. package/dist/coValues/coMap.d.ts +4 -3
  31. package/dist/coValues/coMap.d.ts.map +1 -1
  32. package/dist/coValues/coMap.js +5 -3
  33. package/dist/coValues/coMap.js.map +1 -1
  34. package/dist/coValues/coPlainText.d.ts +2 -2
  35. package/dist/coValues/coPlainText.d.ts.map +1 -1
  36. package/dist/coValues/coPlainText.js +5 -5
  37. package/dist/coValues/coPlainText.js.map +1 -1
  38. package/dist/coValues/coStream.d.ts +5 -4
  39. package/dist/coValues/coStream.d.ts.map +1 -1
  40. package/dist/coValues/coStream.js +5 -3
  41. package/dist/coValues/coStream.js.map +1 -1
  42. package/dist/coValues/group.d.ts +7 -2
  43. package/dist/coValues/group.d.ts.map +1 -1
  44. package/dist/coValues/group.js +29 -26
  45. package/dist/coValues/group.js.map +1 -1
  46. package/dist/coreToCoValue.d.ts +4 -3
  47. package/dist/coreToCoValue.d.ts.map +1 -1
  48. package/dist/coreToCoValue.js +10 -14
  49. package/dist/coreToCoValue.js.map +1 -1
  50. package/dist/exports.d.ts +6 -5
  51. package/dist/exports.d.ts.map +1 -1
  52. package/dist/exports.js +3 -4
  53. package/dist/exports.js.map +1 -1
  54. package/dist/localNode.d.ts +30 -24
  55. package/dist/localNode.d.ts.map +1 -1
  56. package/dist/localNode.js +153 -177
  57. package/dist/localNode.js.map +1 -1
  58. package/dist/permissions.d.ts +2 -1
  59. package/dist/permissions.d.ts.map +1 -1
  60. package/dist/permissions.js +15 -11
  61. package/dist/permissions.js.map +1 -1
  62. package/dist/priority.d.ts +1 -1
  63. package/dist/priority.d.ts.map +1 -1
  64. package/dist/sync.d.ts +2 -2
  65. package/dist/sync.d.ts.map +1 -1
  66. package/dist/sync.js +86 -55
  67. package/dist/sync.js.map +1 -1
  68. package/dist/tests/coList.test.js +133 -13
  69. package/dist/tests/coList.test.js.map +1 -1
  70. package/dist/tests/coMap.test.js +43 -14
  71. package/dist/tests/coMap.test.js.map +1 -1
  72. package/dist/tests/coPlainText.test.js +9 -10
  73. package/dist/tests/coPlainText.test.js.map +1 -1
  74. package/dist/tests/coStream.test.js +49 -18
  75. package/dist/tests/coStream.test.js.map +1 -1
  76. package/dist/tests/coValueCore.test.js +22 -28
  77. package/dist/tests/coValueCore.test.js.map +1 -1
  78. package/dist/tests/coValueCoreLoadingState.test.d.ts +2 -0
  79. package/dist/tests/coValueCoreLoadingState.test.d.ts.map +1 -0
  80. package/dist/tests/coValueCoreLoadingState.test.js +227 -0
  81. package/dist/tests/coValueCoreLoadingState.test.js.map +1 -0
  82. package/dist/tests/group.test.js +42 -43
  83. package/dist/tests/group.test.js.map +1 -1
  84. package/dist/tests/messagesTestUtils.d.ts +2 -2
  85. package/dist/tests/messagesTestUtils.d.ts.map +1 -1
  86. package/dist/tests/messagesTestUtils.js +1 -1
  87. package/dist/tests/messagesTestUtils.js.map +1 -1
  88. package/dist/tests/permissions.test.js +224 -292
  89. package/dist/tests/permissions.test.js.map +1 -1
  90. package/dist/tests/priority.test.js +13 -14
  91. package/dist/tests/priority.test.js.map +1 -1
  92. package/dist/tests/sync.auth.test.d.ts +2 -0
  93. package/dist/tests/sync.auth.test.d.ts.map +1 -0
  94. package/dist/tests/sync.auth.test.js +141 -0
  95. package/dist/tests/sync.auth.test.js.map +1 -0
  96. package/dist/tests/sync.load.test.js +60 -2
  97. package/dist/tests/sync.load.test.js.map +1 -1
  98. package/dist/tests/sync.mesh.test.js +70 -10
  99. package/dist/tests/sync.mesh.test.js.map +1 -1
  100. package/dist/tests/sync.peerReconciliation.test.js +19 -19
  101. package/dist/tests/sync.peerReconciliation.test.js.map +1 -1
  102. package/dist/tests/sync.storage.test.js +20 -13
  103. package/dist/tests/sync.storage.test.js.map +1 -1
  104. package/dist/tests/sync.test.js +32 -39
  105. package/dist/tests/sync.test.js.map +1 -1
  106. package/dist/tests/sync.upload.test.js +126 -37
  107. package/dist/tests/sync.upload.test.js.map +1 -1
  108. package/dist/tests/testUtils.d.ts +24 -15
  109. package/dist/tests/testUtils.d.ts.map +1 -1
  110. package/dist/tests/testUtils.js +88 -61
  111. package/dist/tests/testUtils.js.map +1 -1
  112. package/dist/typeUtils/expectGroup.js +1 -1
  113. package/dist/typeUtils/expectGroup.js.map +1 -1
  114. package/package.json +1 -1
  115. package/src/PeerState.ts +11 -0
  116. package/src/SyncStateManager.ts +2 -3
  117. package/src/coValue.ts +14 -8
  118. package/src/{coValueCore.ts → coValueCore/coValueCore.ts} +470 -413
  119. package/src/coValueCore/verifiedState.ts +376 -0
  120. package/src/coValues/account.ts +20 -25
  121. package/src/coValues/coList.ts +63 -29
  122. package/src/coValues/coMap.ts +13 -6
  123. package/src/coValues/coPlainText.ts +10 -8
  124. package/src/coValues/coStream.ts +12 -7
  125. package/src/coValues/group.ts +50 -28
  126. package/src/coreToCoValue.ts +14 -15
  127. package/src/exports.ts +9 -7
  128. package/src/localNode.ts +248 -283
  129. package/src/permissions.ts +18 -12
  130. package/src/priority.ts +1 -1
  131. package/src/sync.ts +96 -63
  132. package/src/tests/coList.test.ts +200 -12
  133. package/src/tests/coMap.test.ts +65 -14
  134. package/src/tests/coPlainText.test.ts +12 -9
  135. package/src/tests/coStream.test.ts +80 -17
  136. package/src/tests/coValueCore.test.ts +30 -27
  137. package/src/tests/coValueCoreLoadingState.test.ts +337 -0
  138. package/src/tests/group.test.ts +44 -68
  139. package/src/tests/messagesTestUtils.ts +3 -8
  140. package/src/tests/permissions.test.ts +283 -449
  141. package/src/tests/priority.test.ts +17 -13
  142. package/src/tests/sync.auth.test.ts +188 -0
  143. package/src/tests/sync.load.test.ts +79 -2
  144. package/src/tests/sync.mesh.test.ts +89 -9
  145. package/src/tests/sync.peerReconciliation.test.ts +25 -25
  146. package/src/tests/sync.storage.test.ts +20 -13
  147. package/src/tests/sync.test.ts +43 -43
  148. package/src/tests/sync.upload.test.ts +157 -37
  149. package/src/tests/testUtils.ts +120 -74
  150. package/src/typeUtils/expectGroup.ts +1 -1
  151. package/dist/CoValuesStore.d.ts +0 -14
  152. package/dist/CoValuesStore.d.ts.map +0 -1
  153. package/dist/CoValuesStore.js +0 -32
  154. package/dist/CoValuesStore.js.map +0 -1
  155. package/dist/coValueCore.d.ts +0 -141
  156. package/dist/coValueCore.d.ts.map +0 -1
  157. package/dist/coValueCore.js.map +0 -1
  158. package/dist/coValueState.d.ts +0 -34
  159. package/dist/coValueState.d.ts.map +0 -1
  160. package/dist/coValueState.js +0 -228
  161. package/dist/coValueState.js.map +0 -1
  162. package/dist/tests/coValueState.test.d.ts +0 -2
  163. package/dist/tests/coValueState.test.d.ts.map +0 -1
  164. package/dist/tests/coValueState.test.js +0 -344
  165. package/dist/tests/coValueState.test.js.map +0 -1
  166. package/src/CoValuesStore.ts +0 -41
  167. package/src/coValueState.ts +0 -300
  168. package/src/tests/coValueState.test.ts +0 -525
@@ -1,8 +1,14 @@
1
- import { Result, err, ok } from "neverthrow";
2
- import { AnyRawCoValue, RawCoValue } from "./coValue.js";
3
- import { ControlledAccountOrAgent, RawAccountID } from "./coValues/account.js";
4
- import { RawGroup } from "./coValues/group.js";
5
- import { coreToCoValue } from "./coreToCoValue.js";
1
+ import { UpDownCounter, ValueType, metrics } from "@opentelemetry/api";
2
+ import { Result, err } from "neverthrow";
3
+ import { PeerState } from "../PeerState.js";
4
+ import { RawCoValue } from "../coValue.js";
5
+ import {
6
+ ControlledAccount,
7
+ ControlledAccountOrAgent,
8
+ RawAccountID,
9
+ } from "../coValues/account.js";
10
+ import { RawGroup } from "../coValues/group.js";
11
+ import { coreToCoValue } from "../coreToCoValue.js";
6
12
  import {
7
13
  CryptoProvider,
8
14
  Encrypted,
@@ -12,7 +18,7 @@ import {
12
18
  Signature,
13
19
  SignerID,
14
20
  StreamingHash,
15
- } from "./crypto/crypto.js";
21
+ } from "../crypto/crypto.js";
16
22
  import {
17
23
  RawCoID,
18
24
  SessionID,
@@ -20,21 +26,20 @@ import {
20
26
  getGroupDependentKeyList,
21
27
  getParentGroupId,
22
28
  isParentGroupReference,
23
- } from "./ids.js";
24
- import { Stringified, parseJSON, stableStringify } from "./jsonStringify.js";
25
- import { JsonObject, JsonValue } from "./jsonValue.js";
26
- import { LocalNode, ResolveAccountAgentError } from "./localNode.js";
27
- import { logger } from "./logger.js";
29
+ } from "../ids.js";
30
+ import { parseJSON, stableStringify } from "../jsonStringify.js";
31
+ import { JsonValue } from "../jsonValue.js";
32
+ import { LocalNode, ResolveAccountAgentError } from "../localNode.js";
33
+ import { logger } from "../logger.js";
28
34
  import {
29
- PermissionsDef as RulesetDef,
30
35
  determineValidTransactions,
31
36
  isKeyForKeyField,
32
- } from "./permissions.js";
33
- import { getPriorityFromHeader } from "./priority.js";
34
- import { CoValueKnownState, NewContentMessage } from "./sync.js";
35
- import { accountOrAgentIDfromSessionID } from "./typeUtils/accountOrAgentIDfromSessionID.js";
36
- import { expectGroup } from "./typeUtils/expectGroup.js";
37
- import { isAccountID } from "./typeUtils/isAccountID.js";
37
+ } from "../permissions.js";
38
+ import { CoValueKnownState, PeerID, emptyKnownState } from "../sync.js";
39
+ import { accountOrAgentIDfromSessionID } from "../typeUtils/accountOrAgentIDfromSessionID.js";
40
+ import { expectGroup } from "../typeUtils/expectGroup.js";
41
+ import { isAccountID } from "../typeUtils/isAccountID.js";
42
+ import { CoValueHeader, Transaction, VerifiedState } from "./verifiedState.js";
38
43
 
39
44
  /**
40
45
  In order to not block other concurrently syncing CoValues we introduce a maximum size of transactions,
@@ -45,17 +50,6 @@ import { isAccountID } from "./typeUtils/isAccountID.js";
45
50
  **/
46
51
  export const MAX_RECOMMENDED_TX_SIZE = 100 * 1024;
47
52
 
48
- export type CoValueHeader = {
49
- type: AnyRawCoValue["type"];
50
- ruleset: RulesetDef;
51
- meta: JsonObject | null;
52
- } & CoValueUniqueness;
53
-
54
- export type CoValueUniqueness = {
55
- uniqueness: JsonValue;
56
- createdAt?: `2${string}` | null;
57
- };
58
-
59
53
  export function idforHeader(
60
54
  header: CoValueHeader,
61
55
  crypto: CryptoProvider,
@@ -64,29 +58,6 @@ export function idforHeader(
64
58
  return `co_z${hash.slice("shortHash_z".length)}`;
65
59
  }
66
60
 
67
- type SessionLog = {
68
- transactions: Transaction[];
69
- lastHash?: Hash;
70
- streamingHash: StreamingHash;
71
- signatureAfter: { [txIdx: number]: Signature | undefined };
72
- lastSignature: Signature;
73
- };
74
-
75
- export type PrivateTransaction = {
76
- privacy: "private";
77
- madeAt: number;
78
- keyUsed: KeyID;
79
- encryptedChanges: Encrypted<JsonValue[], { in: RawCoID; tx: TransactionID }>;
80
- };
81
-
82
- export type TrustingTransaction = {
83
- privacy: "trusting";
84
- madeAt: number;
85
- changes: Stringified<JsonValue[]>;
86
- };
87
-
88
- export type Transaction = PrivateTransaction | TrustingTransaction;
89
-
90
61
  export type DecryptedTransaction = {
91
62
  txID: TransactionID;
92
63
  changes: JsonValue[];
@@ -95,55 +66,246 @@ export type DecryptedTransaction = {
95
66
 
96
67
  const readKeyCache = new WeakMap<CoValueCore, { [id: KeyID]: KeySecret }>();
97
68
 
69
+ export type AvailableCoValueCore = CoValueCore & { verified: VerifiedState };
70
+
71
+ export const CO_VALUE_LOADING_CONFIG = {
72
+ MAX_RETRIES: 2,
73
+ TIMEOUT: 30_000,
74
+ };
75
+
98
76
  export class CoValueCore {
77
+ // context
78
+ readonly node: LocalNode;
79
+ private readonly crypto: CryptoProvider;
80
+
81
+ // state
99
82
  id: RawCoID;
100
- node: LocalNode;
101
- crypto: CryptoProvider;
102
- header: CoValueHeader;
103
- _sessionLogs: Map<SessionID, SessionLog>;
104
- _cachedContent?: RawCoValue;
105
- listeners: Set<(content?: RawCoValue) => void> = new Set();
106
- _decryptionCache: {
83
+ private _verified: VerifiedState | null;
84
+ /** Holds the fundamental syncable content of a CoValue,
85
+ * consisting of the header (verified by hash -> RawCoID)
86
+ * and the sessions (verified by signature).
87
+ *
88
+ * It does not do any *validation* or *decryption* and as such doesn't
89
+ * depend on other CoValues or the LocalNode.
90
+ *
91
+ * `CoValueCore.verified` may be null when a CoValue is requested to be
92
+ * loaded but no content has been received from storage or peers yet.
93
+ * In this case, it acts as a centralised entry to keep track of peer loading
94
+ * state and to subscribe to its content when it does become available. */
95
+ get verified() {
96
+ return this._verified;
97
+ }
98
+ private readonly peers = new Map<
99
+ PeerID,
100
+ | { type: "unknown" | "pending" | "available" | "unavailable" }
101
+ | {
102
+ type: "errored";
103
+ error: TryAddTransactionsError;
104
+ }
105
+ >();
106
+
107
+ // cached state and listeners
108
+ private _cachedContent?: RawCoValue;
109
+ private readonly listeners: Set<
110
+ (core: CoValueCore, unsub: () => void) => void
111
+ > = new Set();
112
+ private readonly _decryptionCache: {
107
113
  [key: Encrypted<JsonValue[], JsonValue>]: JsonValue[] | undefined;
108
114
  } = {};
109
- _cachedKnownState?: CoValueKnownState;
110
- _cachedDependentOn?: RawCoID[];
111
- _cachedNewContentSinceEmpty?: NewContentMessage[] | undefined;
112
- _currentAsyncAddTransaction?: Promise<void>;
115
+ private _cachedDependentOn?: RawCoID[];
116
+ private counter: UpDownCounter;
113
117
 
114
- constructor(
115
- header: CoValueHeader,
118
+ private constructor(
119
+ init: { header: CoValueHeader } | { id: RawCoID },
116
120
  node: LocalNode,
117
- internalInitSessions: Map<SessionID, SessionLog> = new Map(),
118
121
  ) {
119
122
  this.crypto = node.crypto;
120
- this.id = idforHeader(header, node.crypto);
121
- this.header = header;
122
- this._sessionLogs = internalInitSessions;
123
+ if ("header" in init) {
124
+ this.id = idforHeader(init.header, node.crypto);
125
+ this._verified = new VerifiedState(
126
+ this.id,
127
+ node.crypto,
128
+ init.header,
129
+ new Map(),
130
+ );
131
+ } else {
132
+ this.id = init.id;
133
+ this._verified = null;
134
+ }
123
135
  this.node = node;
136
+
137
+ this.counter = metrics
138
+ .getMeter("cojson")
139
+ .createUpDownCounter("jazz.covalues.loaded", {
140
+ description: "The number of covalues in the system",
141
+ unit: "covalue",
142
+ valueType: ValueType.INT,
143
+ });
144
+
145
+ this.updateCounter(null);
146
+ }
147
+
148
+ static fromID(id: RawCoID, node: LocalNode): CoValueCore {
149
+ return new CoValueCore({ id }, node);
150
+ }
151
+
152
+ static fromHeader(
153
+ header: CoValueHeader,
154
+ node: LocalNode,
155
+ ): AvailableCoValueCore {
156
+ return new CoValueCore({ header }, node) as AvailableCoValueCore;
157
+ }
158
+
159
+ get loadingState() {
160
+ if (this.verified) {
161
+ return "available";
162
+ } else if (this.peers.size === 0) {
163
+ return "unknown";
164
+ }
165
+
166
+ for (const peer of this.peers.values()) {
167
+ if (peer.type === "pending") {
168
+ return "loading";
169
+ } else if (peer.type === "unknown") {
170
+ return "unknown";
171
+ }
172
+ }
173
+
174
+ return "unavailable";
175
+ }
176
+
177
+ isAvailable(): this is AvailableCoValueCore {
178
+ return !!this.verified;
179
+ }
180
+
181
+ isErroredInPeer(peerId: PeerID) {
182
+ return this.peers.get(peerId)?.type === "errored";
183
+ }
184
+
185
+ waitForAvailableOrUnavailable(): Promise<CoValueCore> {
186
+ return new Promise<CoValueCore>((resolve) => {
187
+ const listener = (core: CoValueCore) => {
188
+ if (core.isAvailable() || core.loadingState === "unavailable") {
189
+ resolve(core);
190
+ this.listeners.delete(listener);
191
+ }
192
+ };
193
+
194
+ this.listeners.add(listener);
195
+ listener(this);
196
+ });
197
+ }
198
+
199
+ getStateForPeer(peerId: PeerID) {
200
+ return this.peers.get(peerId);
201
+ }
202
+
203
+ private updateCounter(previousState: string | null) {
204
+ const newState = this.loadingState;
205
+
206
+ if (previousState !== newState) {
207
+ if (previousState) {
208
+ this.counter.add(-1, { state: previousState });
209
+ }
210
+ this.counter.add(1, { state: newState });
211
+ }
212
+ }
213
+
214
+ markNotFoundInPeer(peerId: PeerID) {
215
+ const previousState = this.loadingState;
216
+ this.peers.set(peerId, { type: "unavailable" });
217
+ this.updateCounter(previousState);
218
+ this.notifyUpdate("immediate");
219
+ }
220
+
221
+ // TODO: rename to "provided"
222
+ markAvailable(header: CoValueHeader, fromPeerId: PeerID) {
223
+ const previousState = this.loadingState;
224
+
225
+ if (this._verified?.sessions.size) {
226
+ throw new Error(
227
+ "CoValueCore: markAvailable called on coValue with verified sessions present!",
228
+ );
229
+ }
230
+ this._verified = new VerifiedState(
231
+ this.id,
232
+ this.node.crypto,
233
+ header,
234
+ new Map(),
235
+ );
236
+
237
+ this.peers.set(fromPeerId, { type: "available" });
238
+ this.updateCounter(previousState);
239
+ this.notifyUpdate("immediate");
240
+ }
241
+
242
+ internalMarkMagicallyAvailable(
243
+ verified: VerifiedState,
244
+ { forceOverwrite = false }: { forceOverwrite?: boolean } = {},
245
+ ) {
246
+ const previousState = this.loadingState;
247
+ this.internalShamefullyCloneVerifiedStateFrom(verified, {
248
+ forceOverwrite,
249
+ });
250
+ this.updateCounter(previousState);
251
+ this.notifyUpdate("immediate");
252
+ }
253
+
254
+ markErrored(peerId: PeerID, error: TryAddTransactionsError) {
255
+ const previousState = this.loadingState;
256
+ this.peers.set(peerId, { type: "errored", error });
257
+ this.updateCounter(previousState);
258
+ this.notifyUpdate("immediate");
259
+ }
260
+
261
+ private markPending(peerId: PeerID) {
262
+ const previousState = this.loadingState;
263
+ this.peers.set(peerId, { type: "pending" });
264
+ this.updateCounter(previousState);
265
+ this.notifyUpdate("immediate");
266
+ }
267
+
268
+ internalShamefullyCloneVerifiedStateFrom(
269
+ state: VerifiedState,
270
+ { forceOverwrite = false }: { forceOverwrite?: boolean } = {},
271
+ ) {
272
+ if (!forceOverwrite && this._verified?.sessions.size) {
273
+ throw new Error(
274
+ "CoValueCore: internalShamefullyCloneVerifiedStateFrom called on coValue with verified sessions present!",
275
+ );
276
+ }
277
+ this._verified = state.clone();
278
+ this._cachedContent = undefined;
279
+ this._cachedDependentOn = undefined;
280
+ }
281
+
282
+ internalShamefullyResetCachedContent() {
283
+ this._cachedContent = undefined;
284
+ this._cachedDependentOn = undefined;
124
285
  }
125
286
 
126
287
  groupInvalidationSubscription?: () => void;
127
288
 
128
289
  subscribeToGroupInvalidation() {
290
+ if (!this.verified) {
291
+ return;
292
+ }
293
+
129
294
  if (this.groupInvalidationSubscription) {
130
295
  return;
131
296
  }
132
297
 
133
- const header = this.header;
298
+ const header = this.verified.header;
134
299
 
135
300
  if (header.ruleset.type == "ownedByGroup") {
136
301
  const groupId = header.ruleset.group;
137
- const entry = this.node.coValuesStore.get(groupId);
302
+ const entry = this.node.getCoValue(groupId);
138
303
 
139
304
  if (entry.isAvailable()) {
140
- this.groupInvalidationSubscription = entry.core.subscribe(
141
- (_groupUpdate) => {
142
- this._cachedContent = undefined;
143
- this.notifyUpdate("immediate");
144
- },
145
- false,
146
- );
305
+ this.groupInvalidationSubscription = entry.subscribe((_groupUpdate) => {
306
+ this._cachedContent = undefined;
307
+ this.notifyUpdate("immediate");
308
+ }, false);
147
309
  } else {
148
310
  logger.error("CoValueCore: Owner group not available", {
149
311
  id: this.id,
@@ -153,64 +315,47 @@ export class CoValueCore {
153
315
  }
154
316
  }
155
317
 
156
- get sessionLogs(): Map<SessionID, SessionLog> {
157
- return this._sessionLogs;
158
- }
159
-
160
- testWithDifferentAccount(
161
- account: ControlledAccountOrAgent,
162
- currentSessionID: SessionID,
163
- ): CoValueCore {
164
- const newNode = this.node.testWithDifferentAccount(
165
- account,
166
- currentSessionID,
318
+ contentInClonedNodeWithDifferentAccount(
319
+ controlledAccountOrAgent: ControlledAccountOrAgent,
320
+ ): RawCoValue {
321
+ const newNode = this.node.cloneWithDifferentAccount(
322
+ controlledAccountOrAgent,
167
323
  );
168
324
 
169
- return newNode.expectCoValueLoaded(this.id);
325
+ return newNode.expectCoValueLoaded(this.id).getCurrentContent();
170
326
  }
171
327
 
172
328
  knownState(): CoValueKnownState {
173
- if (this._cachedKnownState) {
174
- return this._cachedKnownState;
329
+ if (this.isAvailable()) {
330
+ return this.verified.knownState();
175
331
  } else {
176
- const knownState = this.knownStateUncached();
177
- this._cachedKnownState = knownState;
178
- return knownState;
332
+ return emptyKnownState(this.id);
179
333
  }
180
334
  }
181
335
 
182
- /** @internal */
183
- knownStateUncached(): CoValueKnownState {
184
- const sessions: CoValueKnownState["sessions"] = {};
185
-
186
- for (const [sessionID, sessionLog] of this.sessionLogs.entries()) {
187
- sessions[sessionID] = sessionLog.transactions.length;
188
- }
189
-
190
- return {
191
- id: this.id,
192
- header: true,
193
- sessions,
194
- };
195
- }
196
-
197
336
  get meta(): JsonValue {
198
- return this.header?.meta ?? null;
337
+ return this.verified?.header.meta ?? null;
199
338
  }
200
339
 
201
340
  nextTransactionID(): TransactionID {
341
+ if (!this.verified) {
342
+ throw new Error(
343
+ "CoValueCore: nextTransactionID called on coValue without verified state",
344
+ );
345
+ }
346
+
202
347
  // This is an ugly hack to get a unique but stable session ID for editing the current account
203
348
  const sessionID =
204
- this.header.meta?.type === "account"
349
+ this.verified.header.meta?.type === "account"
205
350
  ? (this.node.currentSessionID.replace(
206
- this.node.account.id,
207
- this.node.account.currentAgentID(),
351
+ this.node.getCurrentAgent().id,
352
+ this.node.getCurrentAgent().currentAgentID(),
208
353
  ) as SessionID)
209
354
  : this.node.currentSessionID;
210
355
 
211
356
  return {
212
357
  sessionID,
213
- txIndex: this.sessionLogs.get(sessionID)?.transactions.length || 0,
358
+ txIndex: this.verified.sessions.get(sessionID)?.transactions.length || 0,
214
359
  };
215
360
  }
216
361
 
@@ -219,6 +364,7 @@ export class CoValueCore {
219
364
  newTransactions: Transaction[],
220
365
  givenExpectedNewHash: Hash | undefined,
221
366
  newSignature: Signature,
367
+ notifyMode: "immediate" | "deferred",
222
368
  skipVerify: boolean = false,
223
369
  givenNewStreamingHash?: StreamingHash,
224
370
  ): Result<true, TryAddTransactionsError> {
@@ -228,126 +374,45 @@ export class CoValueCore {
228
374
  "Expected to know signer of transaction",
229
375
  )
230
376
  .andThen((agent) => {
377
+ if (!this.verified) {
378
+ return err({
379
+ type: "TriedToAddTransactionsWithoutVerifiedState",
380
+ id: this.id,
381
+ } satisfies TriedToAddTransactionsWithoutVerifiedStateErrpr);
382
+ }
383
+
231
384
  const signerID = this.crypto.getAgentSignerID(agent);
232
385
 
233
- if (
234
- skipVerify === true &&
235
- givenNewStreamingHash &&
236
- givenExpectedNewHash
237
- ) {
238
- this.doAddTransactions(
239
- sessionID,
240
- newTransactions,
241
- newSignature,
242
- givenExpectedNewHash,
243
- givenNewStreamingHash,
244
- "immediate",
245
- );
246
- } else {
247
- const { expectedNewHash, newStreamingHash } =
248
- this.expectedNewHashAfter(sessionID, newTransactions);
386
+ const result = this.verified.tryAddTransactions(
387
+ sessionID,
388
+ signerID,
389
+ newTransactions,
390
+ givenExpectedNewHash,
391
+ newSignature,
392
+ skipVerify,
393
+ givenNewStreamingHash,
394
+ );
249
395
 
396
+ if (result.isOk()) {
250
397
  if (
251
- givenExpectedNewHash &&
252
- givenExpectedNewHash !== expectedNewHash
398
+ this._cachedContent &&
399
+ "processNewTransactions" in this._cachedContent &&
400
+ typeof this._cachedContent.processNewTransactions === "function"
253
401
  ) {
254
- return err({
255
- type: "InvalidHash",
256
- id: this.id,
257
- expectedNewHash,
258
- givenExpectedNewHash,
259
- } satisfies InvalidHashError);
402
+ this._cachedContent.processNewTransactions();
403
+ } else {
404
+ this._cachedContent = undefined;
260
405
  }
261
406
 
262
- if (!this.crypto.verify(newSignature, expectedNewHash, signerID)) {
263
- return err({
264
- type: "InvalidSignature",
265
- id: this.id,
266
- newSignature,
267
- sessionID,
268
- signerID,
269
- } satisfies InvalidSignatureError);
270
- }
407
+ this._cachedDependentOn = undefined;
271
408
 
272
- this.doAddTransactions(
273
- sessionID,
274
- newTransactions,
275
- newSignature,
276
- expectedNewHash,
277
- newStreamingHash,
278
- "immediate",
279
- );
409
+ this.notifyUpdate(notifyMode);
280
410
  }
281
411
 
282
- return ok(true as const);
412
+ return result;
283
413
  });
284
414
  }
285
415
 
286
- private doAddTransactions(
287
- sessionID: SessionID,
288
- newTransactions: Transaction[],
289
- newSignature: Signature,
290
- expectedNewHash: Hash,
291
- newStreamingHash: StreamingHash,
292
- notifyMode: "immediate" | "deferred",
293
- ) {
294
- if (this.node.crashed) {
295
- throw new Error("Trying to add transactions after node is crashed");
296
- }
297
- const transactions = this.sessionLogs.get(sessionID)?.transactions ?? [];
298
-
299
- for (const tx of newTransactions) {
300
- transactions.push(tx);
301
- }
302
-
303
- const signatureAfter =
304
- this.sessionLogs.get(sessionID)?.signatureAfter ?? {};
305
-
306
- const lastInbetweenSignatureIdx = Object.keys(signatureAfter).reduce(
307
- (max, idx) => (parseInt(idx) > max ? parseInt(idx) : max),
308
- -1,
309
- );
310
-
311
- const sizeOfTxsSinceLastInbetweenSignature = transactions
312
- .slice(lastInbetweenSignatureIdx + 1)
313
- .reduce(
314
- (sum, tx) =>
315
- sum +
316
- (tx.privacy === "private"
317
- ? tx.encryptedChanges.length
318
- : tx.changes.length),
319
- 0,
320
- );
321
-
322
- if (sizeOfTxsSinceLastInbetweenSignature > MAX_RECOMMENDED_TX_SIZE) {
323
- signatureAfter[transactions.length - 1] = newSignature;
324
- }
325
-
326
- this._sessionLogs.set(sessionID, {
327
- transactions,
328
- lastHash: expectedNewHash,
329
- streamingHash: newStreamingHash,
330
- lastSignature: newSignature,
331
- signatureAfter: signatureAfter,
332
- });
333
-
334
- if (
335
- this._cachedContent &&
336
- "processNewTransactions" in this._cachedContent &&
337
- typeof this._cachedContent.processNewTransactions === "function"
338
- ) {
339
- this._cachedContent.processNewTransactions();
340
- } else {
341
- this._cachedContent = undefined;
342
- }
343
-
344
- this._cachedKnownState = undefined;
345
- this._cachedDependentOn = undefined;
346
- this._cachedNewContentSinceEmpty = undefined;
347
-
348
- this.notifyUpdate(notifyMode);
349
- }
350
-
351
416
  deferredUpdates = 0;
352
417
  nextDeferredNotify: Promise<void> | undefined;
353
418
 
@@ -357,10 +422,11 @@ export class CoValueCore {
357
422
  }
358
423
 
359
424
  if (notifyMode === "immediate") {
360
- const content = this.getCurrentContent();
361
425
  for (const listener of this.listeners) {
362
426
  try {
363
- listener(content);
427
+ listener(this, () => {
428
+ this.listeners.delete(listener);
429
+ });
364
430
  } catch (e) {
365
431
  logger.error("Error in listener for coValue " + this.id, { err: e });
366
432
  }
@@ -371,10 +437,11 @@ export class CoValueCore {
371
437
  setTimeout(() => {
372
438
  this.nextDeferredNotify = undefined;
373
439
  this.deferredUpdates = 0;
374
- const content = this.getCurrentContent();
375
440
  for (const listener of this.listeners) {
376
441
  try {
377
- listener(content);
442
+ listener(this, () => {
443
+ this.listeners.delete(listener);
444
+ });
378
445
  } catch (e) {
379
446
  logger.error("Error in listener for coValue " + this.id, {
380
447
  err: e,
@@ -390,13 +457,15 @@ export class CoValueCore {
390
457
  }
391
458
 
392
459
  subscribe(
393
- listener: (content?: RawCoValue) => void,
460
+ listener: (core: CoValueCore, unsub: () => void) => void,
394
461
  immediateInvoke = true,
395
462
  ): () => void {
396
463
  this.listeners.add(listener);
397
464
 
398
465
  if (immediateInvoke) {
399
- listener(this.getCurrentContent());
466
+ listener(this, () => {
467
+ this.listeners.delete(listener);
468
+ });
400
469
  }
401
470
 
402
471
  return () => {
@@ -404,28 +473,16 @@ export class CoValueCore {
404
473
  };
405
474
  }
406
475
 
407
- expectedNewHashAfter(
408
- sessionID: SessionID,
409
- newTransactions: Transaction[],
410
- ): { expectedNewHash: Hash; newStreamingHash: StreamingHash } {
411
- const streamingHash =
412
- this.sessionLogs.get(sessionID)?.streamingHash.clone() ??
413
- new StreamingHash(this.crypto);
414
-
415
- for (const transaction of newTransactions) {
416
- streamingHash.update(transaction);
417
- }
418
-
419
- return {
420
- expectedNewHash: streamingHash.digest(),
421
- newStreamingHash: streamingHash,
422
- };
423
- }
424
-
425
476
  makeTransaction(
426
477
  changes: JsonValue[],
427
478
  privacy: "private" | "trusting",
428
479
  ): boolean {
480
+ if (!this.verified) {
481
+ throw new Error(
482
+ "CoValueCore: makeTransaction called on coValue without verified state",
483
+ );
484
+ }
485
+
429
486
  const madeAt = Date.now();
430
487
 
431
488
  let transaction: Transaction;
@@ -460,20 +517,18 @@ export class CoValueCore {
460
517
 
461
518
  // This is an ugly hack to get a unique but stable session ID for editing the current account
462
519
  const sessionID =
463
- this.header.meta?.type === "account"
520
+ this.verified.header.meta?.type === "account"
464
521
  ? (this.node.currentSessionID.replace(
465
- this.node.account.id,
466
- this.node.account.currentAgentID(),
522
+ this.node.getCurrentAgent().id,
523
+ this.node.getCurrentAgent().currentAgentID(),
467
524
  ) as SessionID)
468
525
  : this.node.currentSessionID;
469
526
 
470
- const { expectedNewHash, newStreamingHash } = this.expectedNewHashAfter(
471
- sessionID,
472
- [transaction],
473
- );
527
+ const { expectedNewHash, newStreamingHash } =
528
+ this.verified.expectedNewHashAfter(sessionID, [transaction]);
474
529
 
475
530
  const signature = this.crypto.sign(
476
- this.node.account.currentSignerSecret(),
531
+ this.node.getCurrentAgent().currentSignerSecret(),
477
532
  expectedNewHash,
478
533
  );
479
534
 
@@ -482,6 +537,7 @@ export class CoValueCore {
482
537
  [transaction],
483
538
  expectedNewHash,
484
539
  signature,
540
+ "immediate",
485
541
  true,
486
542
  newStreamingHash,
487
543
  )._unsafeUnwrap({ withStackTrace: true });
@@ -497,13 +553,19 @@ export class CoValueCore {
497
553
  getCurrentContent(options?: {
498
554
  ignorePrivateTransactions: true;
499
555
  }): RawCoValue {
556
+ if (!this.verified) {
557
+ throw new Error(
558
+ "CoValueCore: getCurrentContent called on coValue without verified state",
559
+ );
560
+ }
561
+
500
562
  if (!options?.ignorePrivateTransactions && this._cachedContent) {
501
563
  return this._cachedContent;
502
564
  }
503
565
 
504
566
  this.subscribeToGroupInvalidation();
505
567
 
506
- const newContent = coreToCoValue(this, options);
568
+ const newContent = coreToCoValue(this as AvailableCoValueCore, options);
507
569
 
508
570
  if (!options?.ignorePrivateTransactions) {
509
571
  this._cachedContent = newContent;
@@ -581,6 +643,7 @@ export class CoValueCore {
581
643
 
582
644
  getValidSortedTransactions(options?: {
583
645
  ignorePrivateTransactions: boolean;
646
+ knownTransactions: CoValueKnownState["sessions"];
584
647
  }): DecryptedTransaction[] {
585
648
  const allTransactions = this.getValidTransactions(options);
586
649
 
@@ -604,8 +667,17 @@ export class CoValueCore {
604
667
  );
605
668
  }
606
669
 
607
- getCurrentReadKey(): { secret: KeySecret | undefined; id: KeyID } {
608
- if (this.header.ruleset.type === "group") {
670
+ getCurrentReadKey(): {
671
+ secret: KeySecret | undefined;
672
+ id: KeyID;
673
+ } {
674
+ if (!this.verified) {
675
+ throw new Error(
676
+ "CoValueCore: getCurrentReadKey called on coValue without verified state",
677
+ );
678
+ }
679
+
680
+ if (this.verified.header.ruleset.type === "group") {
609
681
  const content = expectGroup(this.getCurrentContent());
610
682
 
611
683
  const currentKeyId = content.getCurrentReadKeyId();
@@ -620,9 +692,9 @@ export class CoValueCore {
620
692
  secret: secret,
621
693
  id: currentKeyId,
622
694
  };
623
- } else if (this.header.ruleset.type === "ownedByGroup") {
695
+ } else if (this.verified.header.ruleset.type === "ownedByGroup") {
624
696
  return this.node
625
- .expectCoValueLoaded(this.header.ruleset.group)
697
+ .expectCoValueLoaded(this.verified.header.ruleset.group)
626
698
  .getCurrentReadKey();
627
699
  } else {
628
700
  throw new Error(
@@ -648,19 +720,32 @@ export class CoValueCore {
648
720
  }
649
721
 
650
722
  getUncachedReadKey(keyID: KeyID): KeySecret | undefined {
651
- if (this.header.ruleset.type === "group") {
652
- const content = expectGroup(
653
- this.getCurrentContent({ ignorePrivateTransactions: true }),
723
+ if (!this.verified) {
724
+ throw new Error(
725
+ "CoValueCore: getUncachedReadKey called on coValue without verified state",
654
726
  );
727
+ }
655
728
 
729
+ if (this.verified.header.ruleset.type === "group") {
730
+ const content = expectGroup(
731
+ this.getCurrentContent({ ignorePrivateTransactions: true }), // to prevent recursion
732
+ );
656
733
  const keyForEveryone = content.get(`${keyID}_for_everyone`);
657
- if (keyForEveryone) return keyForEveryone;
734
+ if (keyForEveryone) {
735
+ return keyForEveryone;
736
+ }
658
737
 
659
738
  // Try to find key revelation for us
660
- const lookupAccountOrAgentID =
661
- this.header.meta?.type === "account"
662
- ? this.node.account.currentAgentID()
663
- : this.node.account.id;
739
+ const currentAgentOrAccountID = accountOrAgentIDfromSessionID(
740
+ this.node.currentSessionID,
741
+ );
742
+
743
+ // being careful here to avoid recursion
744
+ const lookupAccountOrAgentID = isAccountID(currentAgentOrAccountID)
745
+ ? this.id === currentAgentOrAccountID
746
+ ? this.crypto.getAgentID(this.node.agentSecret) // in accounts, the read key is revealed for the primitive agent
747
+ : currentAgentOrAccountID // current account ID
748
+ : currentAgentOrAccountID; // current agent ID
664
749
 
665
750
  const lastReadyKeyEdit = content.lastEditAt(
666
751
  `${keyID}_for_${lookupAccountOrAgentID}`,
@@ -674,7 +759,7 @@ export class CoValueCore {
674
759
 
675
760
  const secret = this.crypto.unseal(
676
761
  lastReadyKeyEdit.value,
677
- this.node.account.currentSealerSecret(),
762
+ this.crypto.getAgentSealerSecret(this.node.agentSecret), // being careful here to avoid recursion
678
763
  this.crypto.getAgentSealerID(revealerAgent),
679
764
  {
680
765
  in: this.id,
@@ -762,9 +847,9 @@ export class CoValueCore {
762
847
  }
763
848
 
764
849
  return undefined;
765
- } else if (this.header.ruleset.type === "ownedByGroup") {
850
+ } else if (this.verified.header.ruleset.type === "ownedByGroup") {
766
851
  return this.node
767
- .expectCoValueLoaded(this.header.ruleset.group)
852
+ .expectCoValueLoaded(this.verified.header.ruleset.group)
768
853
  .getReadKey(keyID);
769
854
  } else {
770
855
  throw new Error(
@@ -796,143 +881,27 @@ export class CoValueCore {
796
881
  }
797
882
 
798
883
  getGroup(): RawGroup {
799
- if (this.header.ruleset.type !== "ownedByGroup") {
884
+ if (!this.verified) {
885
+ throw new Error(
886
+ "CoValueCore: getGroup called on coValue without verified state",
887
+ );
888
+ }
889
+
890
+ if (this.verified.header.ruleset.type !== "ownedByGroup") {
800
891
  throw new Error("Only values owned by groups have groups");
801
892
  }
802
893
 
803
894
  return expectGroup(
804
895
  this.node
805
- .expectCoValueLoaded(this.header.ruleset.group)
896
+ .expectCoValueLoaded(this.verified.header.ruleset.group)
806
897
  .getCurrentContent(),
807
898
  );
808
899
  }
809
900
 
810
901
  getTx(txID: TransactionID): Transaction | undefined {
811
- return this.sessionLogs.get(txID.sessionID)?.transactions[txID.txIndex];
812
- }
813
-
814
- newContentSince(
815
- knownState: CoValueKnownState | undefined,
816
- ): NewContentMessage[] | undefined {
817
- const isKnownStateEmpty = !knownState?.header && !knownState?.sessions;
818
-
819
- if (isKnownStateEmpty && this._cachedNewContentSinceEmpty) {
820
- return this._cachedNewContentSinceEmpty;
821
- }
822
-
823
- let currentPiece: NewContentMessage = {
824
- action: "content",
825
- id: this.id,
826
- header: knownState?.header ? undefined : this.header,
827
- priority: getPriorityFromHeader(this.header),
828
- new: {},
829
- };
830
-
831
- const pieces = [currentPiece];
832
-
833
- const sentState: CoValueKnownState["sessions"] = {};
834
-
835
- let pieceSize = 0;
836
-
837
- let sessionsTodoAgain: Set<SessionID> | undefined | "first" = "first";
838
-
839
- while (sessionsTodoAgain === "first" || sessionsTodoAgain?.size || 0 > 0) {
840
- if (sessionsTodoAgain === "first") {
841
- sessionsTodoAgain = undefined;
842
- }
843
- const sessionsTodo = sessionsTodoAgain ?? this.sessionLogs.keys();
844
-
845
- for (const sessionIDKey of sessionsTodo) {
846
- const sessionID = sessionIDKey as SessionID;
847
- const log = this.sessionLogs.get(sessionID)!;
848
- const knownStateForSessionID = knownState?.sessions[sessionID];
849
- const sentStateForSessionID = sentState[sessionID];
850
- const nextKnownSignatureIdx = getNextKnownSignatureIdx(
851
- log,
852
- knownStateForSessionID,
853
- sentStateForSessionID,
854
- );
855
-
856
- const firstNewTxIdx =
857
- sentStateForSessionID ?? knownStateForSessionID ?? 0;
858
- const afterLastNewTxIdx =
859
- nextKnownSignatureIdx === undefined
860
- ? log.transactions.length
861
- : nextKnownSignatureIdx + 1;
862
-
863
- const nNewTx = Math.max(0, afterLastNewTxIdx - firstNewTxIdx);
864
-
865
- if (nNewTx === 0) {
866
- sessionsTodoAgain?.delete(sessionID);
867
- continue;
868
- }
869
-
870
- if (afterLastNewTxIdx < log.transactions.length) {
871
- if (!sessionsTodoAgain) {
872
- sessionsTodoAgain = new Set();
873
- }
874
- sessionsTodoAgain.add(sessionID);
875
- }
876
-
877
- const oldPieceSize = pieceSize;
878
- for (let txIdx = firstNewTxIdx; txIdx < afterLastNewTxIdx; txIdx++) {
879
- const tx = log.transactions[txIdx]!;
880
- pieceSize +=
881
- tx.privacy === "private"
882
- ? tx.encryptedChanges.length
883
- : tx.changes.length;
884
- }
885
-
886
- if (pieceSize >= MAX_RECOMMENDED_TX_SIZE) {
887
- currentPiece = {
888
- action: "content",
889
- id: this.id,
890
- header: undefined,
891
- new: {},
892
- priority: getPriorityFromHeader(this.header),
893
- };
894
- pieces.push(currentPiece);
895
- pieceSize = pieceSize - oldPieceSize;
896
- }
897
-
898
- let sessionEntry = currentPiece.new[sessionID];
899
- if (!sessionEntry) {
900
- sessionEntry = {
901
- after: sentStateForSessionID ?? knownStateForSessionID ?? 0,
902
- newTransactions: [],
903
- lastSignature: "WILL_BE_REPLACED" as Signature,
904
- };
905
- currentPiece.new[sessionID] = sessionEntry;
906
- }
907
-
908
- for (let txIdx = firstNewTxIdx; txIdx < afterLastNewTxIdx; txIdx++) {
909
- const tx = log.transactions[txIdx]!;
910
- sessionEntry.newTransactions.push(tx);
911
- }
912
-
913
- sessionEntry.lastSignature =
914
- nextKnownSignatureIdx === undefined
915
- ? log.lastSignature!
916
- : log.signatureAfter[nextKnownSignatureIdx]!;
917
-
918
- sentState[sessionID] =
919
- (sentStateForSessionID ?? knownStateForSessionID ?? 0) + nNewTx;
920
- }
921
- }
922
-
923
- const piecesWithContent = pieces.filter(
924
- (piece) => Object.keys(piece.new).length > 0 || piece.header,
925
- );
926
-
927
- if (piecesWithContent.length === 0) {
928
- return undefined;
929
- }
930
-
931
- if (isKnownStateEmpty) {
932
- this._cachedNewContentSinceEmpty = piecesWithContent;
933
- }
934
-
935
- return piecesWithContent;
902
+ return this.verified?.sessions.get(txID.sessionID)?.transactions[
903
+ txID.txIndex
904
+ ];
936
905
  }
937
906
 
938
907
  getDependedOnCoValues(): RawCoID[] {
@@ -947,13 +916,17 @@ export class CoValueCore {
947
916
 
948
917
  /** @internal */
949
918
  getDependedOnCoValuesUncached(): RawCoID[] {
950
- return this.header.ruleset.type === "group"
919
+ if (!this.verified) {
920
+ return [];
921
+ }
922
+
923
+ return this.verified.header.ruleset.type === "group"
951
924
  ? getGroupDependentKeyList(expectGroup(this.getCurrentContent()).keys())
952
- : this.header.ruleset.type === "ownedByGroup"
925
+ : this.verified.header.ruleset.type === "ownedByGroup"
953
926
  ? [
954
- this.header.ruleset.group,
927
+ this.verified.header.ruleset.group,
955
928
  ...new Set(
956
- [...this.sessionLogs.keys()]
929
+ [...this.verified.sessions.keys()]
957
930
  .map((sessionID) =>
958
931
  accountOrAgentIDfromSessionID(sessionID as SessionID),
959
932
  )
@@ -971,19 +944,97 @@ export class CoValueCore {
971
944
  }) {
972
945
  return this.node.syncManager.waitForSync(this.id, options?.timeout);
973
946
  }
974
- }
975
947
 
976
- function getNextKnownSignatureIdx(
977
- log: SessionLog,
978
- knownStateForSessionID?: number,
979
- sentStateForSessionID?: number,
980
- ) {
981
- return Object.keys(log.signatureAfter)
982
- .map(Number)
983
- .sort((a, b) => a - b)
984
- .find(
985
- (idx) => idx >= (sentStateForSessionID ?? knownStateForSessionID ?? -1),
986
- );
948
+ async loadFromPeers(peers: PeerState[]) {
949
+ if (peers.length === 0) {
950
+ return;
951
+ }
952
+
953
+ const peersToActuallyLoadFrom = [];
954
+ for (const peer of peers) {
955
+ const currentState = this.peers.get(peer.id);
956
+
957
+ if (
958
+ currentState?.type === "available" ||
959
+ currentState?.type === "pending"
960
+ ) {
961
+ continue;
962
+ }
963
+
964
+ if (currentState?.type === "errored") {
965
+ continue;
966
+ }
967
+
968
+ if (currentState?.type === "unavailable") {
969
+ if (peer.shouldRetryUnavailableCoValues()) {
970
+ this.markPending(peer.id);
971
+ peersToActuallyLoadFrom.push(peer);
972
+ }
973
+
974
+ continue;
975
+ }
976
+
977
+ if (!currentState || currentState?.type === "unknown") {
978
+ this.markPending(peer.id);
979
+ peersToActuallyLoadFrom.push(peer);
980
+ }
981
+ }
982
+
983
+ for (const peer of peersToActuallyLoadFrom) {
984
+ if (peer.closed) {
985
+ this.markNotFoundInPeer(peer.id);
986
+ continue;
987
+ }
988
+
989
+ peer.pushOutgoingMessage({
990
+ action: "load",
991
+ ...this.knownState(),
992
+ });
993
+ peer.trackLoadRequestSent(this.id);
994
+
995
+ /**
996
+ * Use a very long timeout for storage peers, because under pressure
997
+ * they may take a long time to consume the messages queue
998
+ *
999
+ * TODO: Track errors on storage and do not rely on timeout
1000
+ */
1001
+ const timeoutDuration =
1002
+ peer.role === "storage"
1003
+ ? CO_VALUE_LOADING_CONFIG.TIMEOUT * 10
1004
+ : CO_VALUE_LOADING_CONFIG.TIMEOUT;
1005
+
1006
+ const waitingForPeer = new Promise<void>((resolve) => {
1007
+ const markNotFound = () => {
1008
+ if (this.peers.get(peer.id)?.type === "pending") {
1009
+ this.markNotFoundInPeer(peer.id);
1010
+ }
1011
+ };
1012
+
1013
+ const timeout = setTimeout(markNotFound, timeoutDuration);
1014
+ const removeCloseListener = peer.addCloseListener(markNotFound);
1015
+
1016
+ const listener = (state: CoValueCore) => {
1017
+ const peerState = state.peers.get(peer.id);
1018
+ if (
1019
+ state.isAvailable() || // might have become available from another peer e.g. through handleNewContent
1020
+ peerState?.type === "available" ||
1021
+ peerState?.type === "errored" ||
1022
+ peerState?.type === "unavailable"
1023
+ ) {
1024
+ this.listeners.delete(listener);
1025
+ removeCloseListener();
1026
+ clearTimeout(timeout);
1027
+ resolve();
1028
+ }
1029
+ };
1030
+
1031
+ this.listeners.add(listener);
1032
+ listener(this);
1033
+ });
1034
+
1035
+ await waitingForPeer;
1036
+ }
1037
+ }
987
1038
  }
988
1039
 
989
1040
  export type InvalidHashError = {
@@ -1001,7 +1052,13 @@ export type InvalidSignatureError = {
1001
1052
  signerID: SignerID;
1002
1053
  };
1003
1054
 
1055
+ export type TriedToAddTransactionsWithoutVerifiedStateErrpr = {
1056
+ type: "TriedToAddTransactionsWithoutVerifiedState";
1057
+ id: RawCoID;
1058
+ };
1059
+
1004
1060
  export type TryAddTransactionsError =
1061
+ | TriedToAddTransactionsWithoutVerifiedStateErrpr
1005
1062
  | ResolveAccountAgentError
1006
1063
  | InvalidHashError
1007
1064
  | InvalidSignatureError;