cojson 0.13.17 → 0.13.20

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