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,14 +1,15 @@
1
- import { err, ok } from "neverthrow";
2
- import { coreToCoValue } from "./coreToCoValue.js";
3
- import { StreamingHash, } from "./crypto/crypto.js";
4
- import { getGroupDependentKeyList, getParentGroupId, isParentGroupReference, } from "./ids.js";
5
- import { parseJSON, stableStringify } from "./jsonStringify.js";
6
- import { logger } from "./logger.js";
7
- import { determineValidTransactions, isKeyForKeyField, } from "./permissions.js";
8
- import { getPriorityFromHeader } from "./priority.js";
9
- import { accountOrAgentIDfromSessionID } from "./typeUtils/accountOrAgentIDfromSessionID.js";
10
- import { expectGroup } from "./typeUtils/expectGroup.js";
11
- import { isAccountID } from "./typeUtils/isAccountID.js";
1
+ import { ValueType, metrics } from "@opentelemetry/api";
2
+ import { err } from "neverthrow";
3
+ import { coreToCoValue } from "../coreToCoValue.js";
4
+ import { getGroupDependentKeyList, getParentGroupId, isParentGroupReference, } from "../ids.js";
5
+ import { parseJSON, stableStringify } from "../jsonStringify.js";
6
+ import { logger } from "../logger.js";
7
+ import { determineValidTransactions, isKeyForKeyField, } from "../permissions.js";
8
+ import { emptyKnownState } from "../sync.js";
9
+ import { accountOrAgentIDfromSessionID } from "../typeUtils/accountOrAgentIDfromSessionID.js";
10
+ import { expectGroup } from "../typeUtils/expectGroup.js";
11
+ import { isAccountID } from "../typeUtils/isAccountID.js";
12
+ import { VerifiedState } from "./verifiedState.js";
12
13
  /**
13
14
  In order to not block other concurrently syncing CoValues we introduce a maximum size of transactions,
14
15
  since they are the smallest unit of progress that can be synced within a CoValue.
@@ -22,27 +23,164 @@ export function idforHeader(header, crypto) {
22
23
  return `co_z${hash.slice("shortHash_z".length)}`;
23
24
  }
24
25
  const readKeyCache = new WeakMap();
26
+ export const CO_VALUE_LOADING_CONFIG = {
27
+ MAX_RETRIES: 2,
28
+ TIMEOUT: 30000,
29
+ };
25
30
  export class CoValueCore {
26
- constructor(header, node, internalInitSessions = new Map()) {
31
+ /** Holds the fundamental syncable content of a CoValue,
32
+ * consisting of the header (verified by hash -> RawCoID)
33
+ * and the sessions (verified by signature).
34
+ *
35
+ * It does not do any *validation* or *decryption* and as such doesn't
36
+ * depend on other CoValues or the LocalNode.
37
+ *
38
+ * `CoValueCore.verified` may be null when a CoValue is requested to be
39
+ * loaded but no content has been received from storage or peers yet.
40
+ * In this case, it acts as a centralised entry to keep track of peer loading
41
+ * state and to subscribe to its content when it does become available. */
42
+ get verified() {
43
+ return this._verified;
44
+ }
45
+ constructor(init, node) {
46
+ this.peers = new Map();
27
47
  this.listeners = new Set();
28
48
  this._decryptionCache = {};
29
49
  this.deferredUpdates = 0;
30
50
  this.crypto = node.crypto;
31
- this.id = idforHeader(header, node.crypto);
32
- this.header = header;
33
- this._sessionLogs = internalInitSessions;
51
+ if ("header" in init) {
52
+ this.id = idforHeader(init.header, node.crypto);
53
+ this._verified = new VerifiedState(this.id, node.crypto, init.header, new Map());
54
+ }
55
+ else {
56
+ this.id = init.id;
57
+ this._verified = null;
58
+ }
34
59
  this.node = node;
60
+ this.counter = metrics
61
+ .getMeter("cojson")
62
+ .createUpDownCounter("jazz.covalues.loaded", {
63
+ description: "The number of covalues in the system",
64
+ unit: "covalue",
65
+ valueType: ValueType.INT,
66
+ });
67
+ this.updateCounter(null);
68
+ }
69
+ static fromID(id, node) {
70
+ return new CoValueCore({ id }, node);
71
+ }
72
+ static fromHeader(header, node) {
73
+ return new CoValueCore({ header }, node);
74
+ }
75
+ get loadingState() {
76
+ if (this.verified) {
77
+ return "available";
78
+ }
79
+ else if (this.peers.size === 0) {
80
+ return "unknown";
81
+ }
82
+ for (const peer of this.peers.values()) {
83
+ if (peer.type === "pending") {
84
+ return "loading";
85
+ }
86
+ else if (peer.type === "unknown") {
87
+ return "unknown";
88
+ }
89
+ }
90
+ return "unavailable";
91
+ }
92
+ isAvailable() {
93
+ return !!this.verified;
94
+ }
95
+ isErroredInPeer(peerId) {
96
+ return this.peers.get(peerId)?.type === "errored";
97
+ }
98
+ waitForAvailableOrUnavailable() {
99
+ return new Promise((resolve) => {
100
+ const listener = (core) => {
101
+ if (core.isAvailable() || core.loadingState === "unavailable") {
102
+ resolve(core);
103
+ this.listeners.delete(listener);
104
+ }
105
+ };
106
+ this.listeners.add(listener);
107
+ listener(this);
108
+ });
109
+ }
110
+ getStateForPeer(peerId) {
111
+ return this.peers.get(peerId);
112
+ }
113
+ updateCounter(previousState) {
114
+ const newState = this.loadingState;
115
+ if (previousState !== newState) {
116
+ if (previousState) {
117
+ this.counter.add(-1, { state: previousState });
118
+ }
119
+ this.counter.add(1, { state: newState });
120
+ }
121
+ }
122
+ markNotFoundInPeer(peerId) {
123
+ const previousState = this.loadingState;
124
+ this.peers.set(peerId, { type: "unavailable" });
125
+ this.updateCounter(previousState);
126
+ this.notifyUpdate("immediate");
127
+ }
128
+ // TODO: rename to "provided"
129
+ markAvailable(header, fromPeerId) {
130
+ const previousState = this.loadingState;
131
+ if (this._verified?.sessions.size) {
132
+ throw new Error("CoValueCore: markAvailable called on coValue with verified sessions present!");
133
+ }
134
+ this._verified = new VerifiedState(this.id, this.node.crypto, header, new Map());
135
+ this.peers.set(fromPeerId, { type: "available" });
136
+ this.updateCounter(previousState);
137
+ this.notifyUpdate("immediate");
138
+ }
139
+ internalMarkMagicallyAvailable(verified, { forceOverwrite = false } = {}) {
140
+ const previousState = this.loadingState;
141
+ this.internalShamefullyCloneVerifiedStateFrom(verified, {
142
+ forceOverwrite,
143
+ });
144
+ this.updateCounter(previousState);
145
+ this.notifyUpdate("immediate");
146
+ }
147
+ markErrored(peerId, error) {
148
+ const previousState = this.loadingState;
149
+ this.peers.set(peerId, { type: "errored", error });
150
+ this.updateCounter(previousState);
151
+ this.notifyUpdate("immediate");
152
+ }
153
+ markPending(peerId) {
154
+ const previousState = this.loadingState;
155
+ this.peers.set(peerId, { type: "pending" });
156
+ this.updateCounter(previousState);
157
+ this.notifyUpdate("immediate");
158
+ }
159
+ internalShamefullyCloneVerifiedStateFrom(state, { forceOverwrite = false } = {}) {
160
+ if (!forceOverwrite && this._verified?.sessions.size) {
161
+ throw new Error("CoValueCore: internalShamefullyCloneVerifiedStateFrom called on coValue with verified sessions present!");
162
+ }
163
+ this._verified = state.clone();
164
+ this._cachedContent = undefined;
165
+ this._cachedDependentOn = undefined;
166
+ }
167
+ internalShamefullyResetCachedContent() {
168
+ this._cachedContent = undefined;
169
+ this._cachedDependentOn = undefined;
35
170
  }
36
171
  subscribeToGroupInvalidation() {
172
+ if (!this.verified) {
173
+ return;
174
+ }
37
175
  if (this.groupInvalidationSubscription) {
38
176
  return;
39
177
  }
40
- const header = this.header;
178
+ const header = this.verified.header;
41
179
  if (header.ruleset.type == "ownedByGroup") {
42
180
  const groupId = header.ruleset.group;
43
- const entry = this.node.coValuesStore.get(groupId);
181
+ const entry = this.node.getCoValue(groupId);
44
182
  if (entry.isAvailable()) {
45
- this.groupInvalidationSubscription = entry.core.subscribe((_groupUpdate) => {
183
+ this.groupInvalidationSubscription = entry.subscribe((_groupUpdate) => {
46
184
  this._cachedContent = undefined;
47
185
  this.notifyUpdate("immediate");
48
186
  }, false);
@@ -55,131 +193,71 @@ export class CoValueCore {
55
193
  }
56
194
  }
57
195
  }
58
- get sessionLogs() {
59
- return this._sessionLogs;
60
- }
61
- testWithDifferentAccount(account, currentSessionID) {
62
- const newNode = this.node.testWithDifferentAccount(account, currentSessionID);
63
- return newNode.expectCoValueLoaded(this.id);
196
+ contentInClonedNodeWithDifferentAccount(controlledAccountOrAgent) {
197
+ const newNode = this.node.cloneWithDifferentAccount(controlledAccountOrAgent);
198
+ return newNode.expectCoValueLoaded(this.id).getCurrentContent();
64
199
  }
65
200
  knownState() {
66
- if (this._cachedKnownState) {
67
- return this._cachedKnownState;
201
+ if (this.isAvailable()) {
202
+ return this.verified.knownState();
68
203
  }
69
204
  else {
70
- const knownState = this.knownStateUncached();
71
- this._cachedKnownState = knownState;
72
- return knownState;
205
+ return emptyKnownState(this.id);
73
206
  }
74
207
  }
75
- /** @internal */
76
- knownStateUncached() {
77
- const sessions = {};
78
- for (const [sessionID, sessionLog] of this.sessionLogs.entries()) {
79
- sessions[sessionID] = sessionLog.transactions.length;
80
- }
81
- return {
82
- id: this.id,
83
- header: true,
84
- sessions,
85
- };
86
- }
87
208
  get meta() {
88
- return this.header?.meta ?? null;
209
+ return this.verified?.header.meta ?? null;
89
210
  }
90
211
  nextTransactionID() {
212
+ if (!this.verified) {
213
+ throw new Error("CoValueCore: nextTransactionID called on coValue without verified state");
214
+ }
91
215
  // This is an ugly hack to get a unique but stable session ID for editing the current account
92
- const sessionID = this.header.meta?.type === "account"
93
- ? this.node.currentSessionID.replace(this.node.account.id, this.node.account.currentAgentID())
216
+ const sessionID = this.verified.header.meta?.type === "account"
217
+ ? this.node.currentSessionID.replace(this.node.getCurrentAgent().id, this.node.getCurrentAgent().currentAgentID())
94
218
  : this.node.currentSessionID;
95
219
  return {
96
220
  sessionID,
97
- txIndex: this.sessionLogs.get(sessionID)?.transactions.length || 0,
221
+ txIndex: this.verified.sessions.get(sessionID)?.transactions.length || 0,
98
222
  };
99
223
  }
100
- tryAddTransactions(sessionID, newTransactions, givenExpectedNewHash, newSignature, skipVerify = false, givenNewStreamingHash) {
224
+ tryAddTransactions(sessionID, newTransactions, givenExpectedNewHash, newSignature, notifyMode, skipVerify = false, givenNewStreamingHash) {
101
225
  return this.node
102
226
  .resolveAccountAgent(accountOrAgentIDfromSessionID(sessionID), "Expected to know signer of transaction")
103
227
  .andThen((agent) => {
104
- const signerID = this.crypto.getAgentSignerID(agent);
105
- if (skipVerify === true &&
106
- givenNewStreamingHash &&
107
- givenExpectedNewHash) {
108
- this.doAddTransactions(sessionID, newTransactions, newSignature, givenExpectedNewHash, givenNewStreamingHash, "immediate");
228
+ if (!this.verified) {
229
+ return err({
230
+ type: "TriedToAddTransactionsWithoutVerifiedState",
231
+ id: this.id,
232
+ });
109
233
  }
110
- else {
111
- const { expectedNewHash, newStreamingHash } = this.expectedNewHashAfter(sessionID, newTransactions);
112
- if (givenExpectedNewHash &&
113
- givenExpectedNewHash !== expectedNewHash) {
114
- return err({
115
- type: "InvalidHash",
116
- id: this.id,
117
- expectedNewHash,
118
- givenExpectedNewHash,
119
- });
234
+ const signerID = this.crypto.getAgentSignerID(agent);
235
+ const result = this.verified.tryAddTransactions(sessionID, signerID, newTransactions, givenExpectedNewHash, newSignature, skipVerify, givenNewStreamingHash);
236
+ if (result.isOk()) {
237
+ if (this._cachedContent &&
238
+ "processNewTransactions" in this._cachedContent &&
239
+ typeof this._cachedContent.processNewTransactions === "function") {
240
+ this._cachedContent.processNewTransactions();
120
241
  }
121
- if (!this.crypto.verify(newSignature, expectedNewHash, signerID)) {
122
- return err({
123
- type: "InvalidSignature",
124
- id: this.id,
125
- newSignature,
126
- sessionID,
127
- signerID,
128
- });
242
+ else {
243
+ this._cachedContent = undefined;
129
244
  }
130
- this.doAddTransactions(sessionID, newTransactions, newSignature, expectedNewHash, newStreamingHash, "immediate");
245
+ this._cachedDependentOn = undefined;
246
+ this.notifyUpdate(notifyMode);
131
247
  }
132
- return ok(true);
248
+ return result;
133
249
  });
134
250
  }
135
- doAddTransactions(sessionID, newTransactions, newSignature, expectedNewHash, newStreamingHash, notifyMode) {
136
- if (this.node.crashed) {
137
- throw new Error("Trying to add transactions after node is crashed");
138
- }
139
- const transactions = this.sessionLogs.get(sessionID)?.transactions ?? [];
140
- for (const tx of newTransactions) {
141
- transactions.push(tx);
142
- }
143
- const signatureAfter = this.sessionLogs.get(sessionID)?.signatureAfter ?? {};
144
- const lastInbetweenSignatureIdx = Object.keys(signatureAfter).reduce((max, idx) => (parseInt(idx) > max ? parseInt(idx) : max), -1);
145
- const sizeOfTxsSinceLastInbetweenSignature = transactions
146
- .slice(lastInbetweenSignatureIdx + 1)
147
- .reduce((sum, tx) => sum +
148
- (tx.privacy === "private"
149
- ? tx.encryptedChanges.length
150
- : tx.changes.length), 0);
151
- if (sizeOfTxsSinceLastInbetweenSignature > MAX_RECOMMENDED_TX_SIZE) {
152
- signatureAfter[transactions.length - 1] = newSignature;
153
- }
154
- this._sessionLogs.set(sessionID, {
155
- transactions,
156
- lastHash: expectedNewHash,
157
- streamingHash: newStreamingHash,
158
- lastSignature: newSignature,
159
- signatureAfter: signatureAfter,
160
- });
161
- if (this._cachedContent &&
162
- "processNewTransactions" in this._cachedContent &&
163
- typeof this._cachedContent.processNewTransactions === "function") {
164
- this._cachedContent.processNewTransactions();
165
- }
166
- else {
167
- this._cachedContent = undefined;
168
- }
169
- this._cachedKnownState = undefined;
170
- this._cachedDependentOn = undefined;
171
- this._cachedNewContentSinceEmpty = undefined;
172
- this.notifyUpdate(notifyMode);
173
- }
174
251
  notifyUpdate(notifyMode) {
175
252
  if (this.listeners.size === 0) {
176
253
  return;
177
254
  }
178
255
  if (notifyMode === "immediate") {
179
- const content = this.getCurrentContent();
180
256
  for (const listener of this.listeners) {
181
257
  try {
182
- listener(content);
258
+ listener(this, () => {
259
+ this.listeners.delete(listener);
260
+ });
183
261
  }
184
262
  catch (e) {
185
263
  logger.error("Error in listener for coValue " + this.id, { err: e });
@@ -192,10 +270,11 @@ export class CoValueCore {
192
270
  setTimeout(() => {
193
271
  this.nextDeferredNotify = undefined;
194
272
  this.deferredUpdates = 0;
195
- const content = this.getCurrentContent();
196
273
  for (const listener of this.listeners) {
197
274
  try {
198
- listener(content);
275
+ listener(this, () => {
276
+ this.listeners.delete(listener);
277
+ });
199
278
  }
200
279
  catch (e) {
201
280
  logger.error("Error in listener for coValue " + this.id, {
@@ -213,24 +292,18 @@ export class CoValueCore {
213
292
  subscribe(listener, immediateInvoke = true) {
214
293
  this.listeners.add(listener);
215
294
  if (immediateInvoke) {
216
- listener(this.getCurrentContent());
295
+ listener(this, () => {
296
+ this.listeners.delete(listener);
297
+ });
217
298
  }
218
299
  return () => {
219
300
  this.listeners.delete(listener);
220
301
  };
221
302
  }
222
- expectedNewHashAfter(sessionID, newTransactions) {
223
- const streamingHash = this.sessionLogs.get(sessionID)?.streamingHash.clone() ??
224
- new StreamingHash(this.crypto);
225
- for (const transaction of newTransactions) {
226
- streamingHash.update(transaction);
227
- }
228
- return {
229
- expectedNewHash: streamingHash.digest(),
230
- newStreamingHash: streamingHash,
231
- };
232
- }
233
303
  makeTransaction(changes, privacy) {
304
+ if (!this.verified) {
305
+ throw new Error("CoValueCore: makeTransaction called on coValue without verified state");
306
+ }
234
307
  const madeAt = Date.now();
235
308
  let transaction;
236
309
  if (privacy === "private") {
@@ -258,12 +331,12 @@ export class CoValueCore {
258
331
  };
259
332
  }
260
333
  // This is an ugly hack to get a unique but stable session ID for editing the current account
261
- const sessionID = this.header.meta?.type === "account"
262
- ? this.node.currentSessionID.replace(this.node.account.id, this.node.account.currentAgentID())
334
+ const sessionID = this.verified.header.meta?.type === "account"
335
+ ? this.node.currentSessionID.replace(this.node.getCurrentAgent().id, this.node.getCurrentAgent().currentAgentID())
263
336
  : this.node.currentSessionID;
264
- const { expectedNewHash, newStreamingHash } = this.expectedNewHashAfter(sessionID, [transaction]);
265
- const signature = this.crypto.sign(this.node.account.currentSignerSecret(), expectedNewHash);
266
- const success = this.tryAddTransactions(sessionID, [transaction], expectedNewHash, signature, true, newStreamingHash)._unsafeUnwrap({ withStackTrace: true });
337
+ const { expectedNewHash, newStreamingHash } = this.verified.expectedNewHashAfter(sessionID, [transaction]);
338
+ const signature = this.crypto.sign(this.node.getCurrentAgent().currentSignerSecret(), expectedNewHash);
339
+ const success = this.tryAddTransactions(sessionID, [transaction], expectedNewHash, signature, "immediate", true, newStreamingHash)._unsafeUnwrap({ withStackTrace: true });
267
340
  if (success) {
268
341
  this.node.syncManager.recordTransactionsSize([transaction], "local");
269
342
  void this.node.syncManager.requestCoValueSync(this);
@@ -271,6 +344,9 @@ export class CoValueCore {
271
344
  return success;
272
345
  }
273
346
  getCurrentContent(options) {
347
+ if (!this.verified) {
348
+ throw new Error("CoValueCore: getCurrentContent called on coValue without verified state");
349
+ }
274
350
  if (!options?.ignorePrivateTransactions && this._cachedContent) {
275
351
  return this._cachedContent;
276
352
  }
@@ -332,16 +408,19 @@ export class CoValueCore {
332
408
  return allTransactions;
333
409
  }
334
410
  compareTransactions(a, b) {
335
- return (a.madeAt - b.madeAt ||
336
- (a.txID.sessionID === b.txID.sessionID
337
- ? 0
338
- : a.txID.sessionID < b.txID.sessionID
339
- ? -1
340
- : 1) ||
341
- a.txID.txIndex - b.txID.txIndex);
411
+ if (a.madeAt !== b.madeAt) {
412
+ return a.madeAt - b.madeAt;
413
+ }
414
+ if (a.txID.sessionID === b.txID.sessionID) {
415
+ return a.txID.txIndex - b.txID.txIndex;
416
+ }
417
+ return 0;
342
418
  }
343
419
  getCurrentReadKey() {
344
- if (this.header.ruleset.type === "group") {
420
+ if (!this.verified) {
421
+ throw new Error("CoValueCore: getCurrentReadKey called on coValue without verified state");
422
+ }
423
+ if (this.verified.header.ruleset.type === "group") {
345
424
  const content = expectGroup(this.getCurrentContent());
346
425
  const currentKeyId = content.getCurrentReadKeyId();
347
426
  if (!currentKeyId) {
@@ -353,9 +432,9 @@ export class CoValueCore {
353
432
  id: currentKeyId,
354
433
  };
355
434
  }
356
- else if (this.header.ruleset.type === "ownedByGroup") {
435
+ else if (this.verified.header.ruleset.type === "ownedByGroup") {
357
436
  return this.node
358
- .expectCoValueLoaded(this.header.ruleset.group)
437
+ .expectCoValueLoaded(this.verified.header.ruleset.group)
359
438
  .getCurrentReadKey();
360
439
  }
361
440
  else {
@@ -378,22 +457,31 @@ export class CoValueCore {
378
457
  return key;
379
458
  }
380
459
  getUncachedReadKey(keyID) {
381
- if (this.header.ruleset.type === "group") {
460
+ if (!this.verified) {
461
+ throw new Error("CoValueCore: getUncachedReadKey called on coValue without verified state");
462
+ }
463
+ if (this.verified.header.ruleset.type === "group") {
382
464
  const content = expectGroup(this.getCurrentContent({ ignorePrivateTransactions: true }));
383
465
  const keyForEveryone = content.get(`${keyID}_for_everyone`);
384
- if (keyForEveryone)
466
+ if (keyForEveryone) {
385
467
  return keyForEveryone;
468
+ }
386
469
  // Try to find key revelation for us
387
- const lookupAccountOrAgentID = this.header.meta?.type === "account"
388
- ? this.node.account.currentAgentID()
389
- : this.node.account.id;
470
+ const currentAgentOrAccountID = accountOrAgentIDfromSessionID(this.node.currentSessionID);
471
+ // being careful here to avoid recursion
472
+ const lookupAccountOrAgentID = isAccountID(currentAgentOrAccountID)
473
+ ? this.id === currentAgentOrAccountID
474
+ ? this.crypto.getAgentID(this.node.agentSecret) // in accounts, the read key is revealed for the primitive agent
475
+ : currentAgentOrAccountID // current account ID
476
+ : currentAgentOrAccountID; // current agent ID
390
477
  const lastReadyKeyEdit = content.lastEditAt(`${keyID}_for_${lookupAccountOrAgentID}`);
391
478
  if (lastReadyKeyEdit?.value) {
392
479
  const revealer = lastReadyKeyEdit.by;
393
480
  const revealerAgent = this.node
394
481
  .resolveAccountAgent(revealer, "Expected to know revealer")
395
482
  ._unsafeUnwrap({ withStackTrace: true });
396
- const secret = this.crypto.unseal(lastReadyKeyEdit.value, this.node.account.currentSealerSecret(), this.crypto.getAgentSealerID(revealerAgent), {
483
+ const secret = this.crypto.unseal(lastReadyKeyEdit.value, this.crypto.getAgentSealerSecret(this.node.agentSecret), // being careful here to avoid recursion
484
+ this.crypto.getAgentSealerID(revealerAgent), {
397
485
  in: this.id,
398
486
  tx: lastReadyKeyEdit.tx,
399
487
  });
@@ -449,9 +537,9 @@ export class CoValueCore {
449
537
  }
450
538
  return undefined;
451
539
  }
452
- else if (this.header.ruleset.type === "ownedByGroup") {
540
+ else if (this.verified.header.ruleset.type === "ownedByGroup") {
453
541
  return this.node
454
- .expectCoValueLoaded(this.header.ruleset.group)
542
+ .expectCoValueLoaded(this.verified.header.ruleset.group)
455
543
  .getReadKey(keyID);
456
544
  }
457
545
  else {
@@ -476,106 +564,18 @@ export class CoValueCore {
476
564
  return validParentKeys;
477
565
  }
478
566
  getGroup() {
479
- if (this.header.ruleset.type !== "ownedByGroup") {
567
+ if (!this.verified) {
568
+ throw new Error("CoValueCore: getGroup called on coValue without verified state");
569
+ }
570
+ if (this.verified.header.ruleset.type !== "ownedByGroup") {
480
571
  throw new Error("Only values owned by groups have groups");
481
572
  }
482
573
  return expectGroup(this.node
483
- .expectCoValueLoaded(this.header.ruleset.group)
574
+ .expectCoValueLoaded(this.verified.header.ruleset.group)
484
575
  .getCurrentContent());
485
576
  }
486
577
  getTx(txID) {
487
- return this.sessionLogs.get(txID.sessionID)?.transactions[txID.txIndex];
488
- }
489
- newContentSince(knownState) {
490
- const isKnownStateEmpty = !knownState?.header && !knownState?.sessions;
491
- if (isKnownStateEmpty && this._cachedNewContentSinceEmpty) {
492
- return this._cachedNewContentSinceEmpty;
493
- }
494
- let currentPiece = {
495
- action: "content",
496
- id: this.id,
497
- header: knownState?.header ? undefined : this.header,
498
- priority: getPriorityFromHeader(this.header),
499
- new: {},
500
- };
501
- const pieces = [currentPiece];
502
- const sentState = {};
503
- let pieceSize = 0;
504
- let sessionsTodoAgain = "first";
505
- while (sessionsTodoAgain === "first" || sessionsTodoAgain?.size || 0 > 0) {
506
- if (sessionsTodoAgain === "first") {
507
- sessionsTodoAgain = undefined;
508
- }
509
- const sessionsTodo = sessionsTodoAgain ?? this.sessionLogs.keys();
510
- for (const sessionIDKey of sessionsTodo) {
511
- const sessionID = sessionIDKey;
512
- const log = this.sessionLogs.get(sessionID);
513
- const knownStateForSessionID = knownState?.sessions[sessionID];
514
- const sentStateForSessionID = sentState[sessionID];
515
- const nextKnownSignatureIdx = getNextKnownSignatureIdx(log, knownStateForSessionID, sentStateForSessionID);
516
- const firstNewTxIdx = sentStateForSessionID ?? knownStateForSessionID ?? 0;
517
- const afterLastNewTxIdx = nextKnownSignatureIdx === undefined
518
- ? log.transactions.length
519
- : nextKnownSignatureIdx + 1;
520
- const nNewTx = Math.max(0, afterLastNewTxIdx - firstNewTxIdx);
521
- if (nNewTx === 0) {
522
- sessionsTodoAgain?.delete(sessionID);
523
- continue;
524
- }
525
- if (afterLastNewTxIdx < log.transactions.length) {
526
- if (!sessionsTodoAgain) {
527
- sessionsTodoAgain = new Set();
528
- }
529
- sessionsTodoAgain.add(sessionID);
530
- }
531
- const oldPieceSize = pieceSize;
532
- for (let txIdx = firstNewTxIdx; txIdx < afterLastNewTxIdx; txIdx++) {
533
- const tx = log.transactions[txIdx];
534
- pieceSize +=
535
- tx.privacy === "private"
536
- ? tx.encryptedChanges.length
537
- : tx.changes.length;
538
- }
539
- if (pieceSize >= MAX_RECOMMENDED_TX_SIZE) {
540
- currentPiece = {
541
- action: "content",
542
- id: this.id,
543
- header: undefined,
544
- new: {},
545
- priority: getPriorityFromHeader(this.header),
546
- };
547
- pieces.push(currentPiece);
548
- pieceSize = pieceSize - oldPieceSize;
549
- }
550
- let sessionEntry = currentPiece.new[sessionID];
551
- if (!sessionEntry) {
552
- sessionEntry = {
553
- after: sentStateForSessionID ?? knownStateForSessionID ?? 0,
554
- newTransactions: [],
555
- lastSignature: "WILL_BE_REPLACED",
556
- };
557
- currentPiece.new[sessionID] = sessionEntry;
558
- }
559
- for (let txIdx = firstNewTxIdx; txIdx < afterLastNewTxIdx; txIdx++) {
560
- const tx = log.transactions[txIdx];
561
- sessionEntry.newTransactions.push(tx);
562
- }
563
- sessionEntry.lastSignature =
564
- nextKnownSignatureIdx === undefined
565
- ? log.lastSignature
566
- : log.signatureAfter[nextKnownSignatureIdx];
567
- sentState[sessionID] =
568
- (sentStateForSessionID ?? knownStateForSessionID ?? 0) + nNewTx;
569
- }
570
- }
571
- const piecesWithContent = pieces.filter((piece) => Object.keys(piece.new).length > 0 || piece.header);
572
- if (piecesWithContent.length === 0) {
573
- return undefined;
574
- }
575
- if (isKnownStateEmpty) {
576
- this._cachedNewContentSinceEmpty = piecesWithContent;
577
- }
578
- return piecesWithContent;
578
+ return this.verified?.sessions.get(txID.sessionID)?.transactions[txID.txIndex];
579
579
  }
580
580
  getDependedOnCoValues() {
581
581
  if (this._cachedDependentOn) {
@@ -589,12 +589,15 @@ export class CoValueCore {
589
589
  }
590
590
  /** @internal */
591
591
  getDependedOnCoValuesUncached() {
592
- return this.header.ruleset.type === "group"
592
+ if (!this.verified) {
593
+ return [];
594
+ }
595
+ return this.verified.header.ruleset.type === "group"
593
596
  ? getGroupDependentKeyList(expectGroup(this.getCurrentContent()).keys())
594
- : this.header.ruleset.type === "ownedByGroup"
597
+ : this.verified.header.ruleset.type === "ownedByGroup"
595
598
  ? [
596
- this.header.ruleset.group,
597
- ...new Set([...this.sessionLogs.keys()]
599
+ this.verified.header.ruleset.group,
600
+ ...new Set([...this.verified.sessions.keys()]
598
601
  .map((sessionID) => accountOrAgentIDfromSessionID(sessionID))
599
602
  .filter((session) => isAccountID(session) && session !== this.id)),
600
603
  ]
@@ -603,11 +606,80 @@ export class CoValueCore {
603
606
  waitForSync(options) {
604
607
  return this.node.syncManager.waitForSync(this.id, options?.timeout);
605
608
  }
606
- }
607
- function getNextKnownSignatureIdx(log, knownStateForSessionID, sentStateForSessionID) {
608
- return Object.keys(log.signatureAfter)
609
- .map(Number)
610
- .sort((a, b) => a - b)
611
- .find((idx) => idx >= (sentStateForSessionID ?? knownStateForSessionID ?? -1));
609
+ async loadFromPeers(peers) {
610
+ if (peers.length === 0) {
611
+ return;
612
+ }
613
+ const peersToActuallyLoadFrom = [];
614
+ for (const peer of peers) {
615
+ const currentState = this.peers.get(peer.id);
616
+ if (currentState?.type === "available" ||
617
+ currentState?.type === "pending") {
618
+ continue;
619
+ }
620
+ if (currentState?.type === "errored") {
621
+ continue;
622
+ }
623
+ if (currentState?.type === "unavailable") {
624
+ if (peer.shouldRetryUnavailableCoValues()) {
625
+ this.markPending(peer.id);
626
+ peersToActuallyLoadFrom.push(peer);
627
+ }
628
+ continue;
629
+ }
630
+ if (!currentState || currentState?.type === "unknown") {
631
+ this.markPending(peer.id);
632
+ peersToActuallyLoadFrom.push(peer);
633
+ }
634
+ }
635
+ for (const peer of peersToActuallyLoadFrom) {
636
+ if (peer.closed) {
637
+ this.markNotFoundInPeer(peer.id);
638
+ continue;
639
+ }
640
+ peer.pushOutgoingMessage({
641
+ action: "load",
642
+ ...this.knownState(),
643
+ });
644
+ peer.trackLoadRequestSent(this.id);
645
+ /**
646
+ * Use a very long timeout for storage peers, because under pressure
647
+ * they may take a long time to consume the messages queue
648
+ *
649
+ * TODO: Track errors on storage and do not rely on timeout
650
+ */
651
+ const timeoutDuration = peer.role === "storage"
652
+ ? CO_VALUE_LOADING_CONFIG.TIMEOUT * 10
653
+ : CO_VALUE_LOADING_CONFIG.TIMEOUT;
654
+ const waitingForPeer = new Promise((resolve) => {
655
+ const markNotFound = () => {
656
+ if (this.peers.get(peer.id)?.type === "pending") {
657
+ logger.warn("Timeout waiting for peer to load coValue", {
658
+ id: this.id,
659
+ peerID: peer.id,
660
+ });
661
+ this.markNotFoundInPeer(peer.id);
662
+ }
663
+ };
664
+ const timeout = setTimeout(markNotFound, timeoutDuration);
665
+ const removeCloseListener = peer.addCloseListener(markNotFound);
666
+ const listener = (state) => {
667
+ const peerState = state.peers.get(peer.id);
668
+ if (state.isAvailable() || // might have become available from another peer e.g. through handleNewContent
669
+ peerState?.type === "available" ||
670
+ peerState?.type === "errored" ||
671
+ peerState?.type === "unavailable") {
672
+ this.listeners.delete(listener);
673
+ removeCloseListener();
674
+ clearTimeout(timeout);
675
+ resolve();
676
+ }
677
+ };
678
+ this.listeners.add(listener);
679
+ listener(this);
680
+ });
681
+ await waitingForPeer;
682
+ }
683
+ }
612
684
  }
613
685
  //# sourceMappingURL=coValueCore.js.map