cojson 0.18.32 → 0.18.34

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 (120) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +18 -0
  3. package/dist/SyncStateManager.d.ts.map +1 -1
  4. package/dist/SyncStateManager.js +2 -2
  5. package/dist/SyncStateManager.js.map +1 -1
  6. package/dist/coValueCore/SessionMap.d.ts +1 -0
  7. package/dist/coValueCore/SessionMap.d.ts.map +1 -1
  8. package/dist/coValueCore/SessionMap.js +22 -12
  9. package/dist/coValueCore/SessionMap.js.map +1 -1
  10. package/dist/coValueCore/coValueCore.d.ts +14 -9
  11. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  12. package/dist/coValueCore/coValueCore.js +65 -51
  13. package/dist/coValueCore/coValueCore.js.map +1 -1
  14. package/dist/coValueCore/verifiedState.d.ts +5 -3
  15. package/dist/coValueCore/verifiedState.d.ts.map +1 -1
  16. package/dist/coValueCore/verifiedState.js +93 -76
  17. package/dist/coValueCore/verifiedState.js.map +1 -1
  18. package/dist/coValues/group.d.ts +1 -0
  19. package/dist/coValues/group.d.ts.map +1 -1
  20. package/dist/coValues/group.js +24 -4
  21. package/dist/coValues/group.js.map +1 -1
  22. package/dist/knownState.d.ts +9 -1
  23. package/dist/knownState.d.ts.map +1 -1
  24. package/dist/knownState.js +29 -3
  25. package/dist/knownState.js.map +1 -1
  26. package/dist/localNode.d.ts.map +1 -1
  27. package/dist/localNode.js +3 -3
  28. package/dist/localNode.js.map +1 -1
  29. package/dist/queue/LocalTransactionsSyncQueue.d.ts +10 -9
  30. package/dist/queue/LocalTransactionsSyncQueue.d.ts.map +1 -1
  31. package/dist/queue/LocalTransactionsSyncQueue.js +53 -32
  32. package/dist/queue/LocalTransactionsSyncQueue.js.map +1 -1
  33. package/dist/storage/knownState.js +2 -2
  34. package/dist/storage/knownState.js.map +1 -1
  35. package/dist/sync.d.ts +1 -2
  36. package/dist/sync.d.ts.map +1 -1
  37. package/dist/sync.js +8 -3
  38. package/dist/sync.js.map +1 -1
  39. package/dist/tests/PureJSCrypto.test.js +1 -1
  40. package/dist/tests/PureJSCrypto.test.js.map +1 -1
  41. package/dist/tests/StorageApiAsync.test.js +11 -11
  42. package/dist/tests/StorageApiAsync.test.js.map +1 -1
  43. package/dist/tests/StorageApiSync.test.js +3 -3
  44. package/dist/tests/StorageApiSync.test.js.map +1 -1
  45. package/dist/tests/WasmCrypto.test.js +1 -1
  46. package/dist/tests/WasmCrypto.test.js.map +1 -1
  47. package/dist/tests/coPlainText.test.js +13 -14
  48. package/dist/tests/coPlainText.test.js.map +1 -1
  49. package/dist/tests/coStream.test.js +12 -12
  50. package/dist/tests/coStream.test.js.map +1 -1
  51. package/dist/tests/coValueCore.isCompletelyDownloaded.test.d.ts +2 -0
  52. package/dist/tests/coValueCore.isCompletelyDownloaded.test.d.ts.map +1 -0
  53. package/dist/tests/coValueCore.isCompletelyDownloaded.test.js +422 -0
  54. package/dist/tests/coValueCore.isCompletelyDownloaded.test.js.map +1 -0
  55. package/dist/tests/coValueCore.isStreaming.test.d.ts +2 -0
  56. package/dist/tests/coValueCore.isStreaming.test.d.ts.map +1 -0
  57. package/dist/tests/coValueCore.isStreaming.test.js +232 -0
  58. package/dist/tests/coValueCore.isStreaming.test.js.map +1 -0
  59. package/dist/tests/coValueCore.newContentSince.test.d.ts +2 -0
  60. package/dist/tests/coValueCore.newContentSince.test.d.ts.map +1 -0
  61. package/dist/tests/coValueCore.newContentSince.test.js +808 -0
  62. package/dist/tests/coValueCore.newContentSince.test.js.map +1 -0
  63. package/dist/tests/coreWasm.test.js +2 -2
  64. package/dist/tests/coreWasm.test.js.map +1 -1
  65. package/dist/tests/group.childKeyRotation.test.d.ts +2 -0
  66. package/dist/tests/group.childKeyRotation.test.d.ts.map +1 -0
  67. package/dist/tests/group.childKeyRotation.test.js +261 -0
  68. package/dist/tests/group.childKeyRotation.test.js.map +1 -0
  69. package/dist/tests/group.removeMember.test.js +1 -114
  70. package/dist/tests/group.removeMember.test.js.map +1 -1
  71. package/dist/tests/knownState.test.js +83 -11
  72. package/dist/tests/knownState.test.js.map +1 -1
  73. package/dist/tests/sync.auth.test.js +6 -6
  74. package/dist/tests/sync.load.test.js +67 -4
  75. package/dist/tests/sync.load.test.js.map +1 -1
  76. package/dist/tests/sync.mesh.test.js +41 -40
  77. package/dist/tests/sync.mesh.test.js.map +1 -1
  78. package/dist/tests/sync.peerReconciliation.test.js +1 -1
  79. package/dist/tests/sync.storage.test.js +29 -28
  80. package/dist/tests/sync.storage.test.js.map +1 -1
  81. package/dist/tests/sync.storageAsync.test.js +26 -25
  82. package/dist/tests/sync.storageAsync.test.js.map +1 -1
  83. package/dist/tests/sync.upload.test.js +96 -40
  84. package/dist/tests/sync.upload.test.js.map +1 -1
  85. package/dist/tests/testUtils.d.ts +12 -8
  86. package/dist/tests/testUtils.d.ts.map +1 -1
  87. package/dist/tests/testUtils.js +39 -8
  88. package/dist/tests/testUtils.js.map +1 -1
  89. package/package.json +3 -3
  90. package/src/SyncStateManager.ts +5 -2
  91. package/src/coValueCore/SessionMap.ts +39 -12
  92. package/src/coValueCore/coValueCore.ts +81 -66
  93. package/src/coValueCore/verifiedState.ts +139 -109
  94. package/src/coValues/group.ts +27 -4
  95. package/src/knownState.ts +49 -5
  96. package/src/localNode.ts +7 -5
  97. package/src/queue/LocalTransactionsSyncQueue.ts +77 -68
  98. package/src/storage/knownState.ts +2 -2
  99. package/src/sync.ts +7 -3
  100. package/src/tests/PureJSCrypto.test.ts +1 -2
  101. package/src/tests/StorageApiAsync.test.ts +11 -11
  102. package/src/tests/StorageApiSync.test.ts +3 -3
  103. package/src/tests/WasmCrypto.test.ts +1 -2
  104. package/src/tests/coPlainText.test.ts +13 -14
  105. package/src/tests/coStream.test.ts +12 -12
  106. package/src/tests/coValueCore.isCompletelyDownloaded.test.ts +590 -0
  107. package/src/tests/coValueCore.isStreaming.test.ts +353 -0
  108. package/src/tests/coValueCore.newContentSince.test.ts +966 -0
  109. package/src/tests/coreWasm.test.ts +2 -2
  110. package/src/tests/group.childKeyRotation.test.ts +431 -0
  111. package/src/tests/group.removeMember.test.ts +1 -184
  112. package/src/tests/knownState.test.ts +108 -11
  113. package/src/tests/sync.auth.test.ts +6 -6
  114. package/src/tests/sync.load.test.ts +79 -4
  115. package/src/tests/sync.mesh.test.ts +41 -40
  116. package/src/tests/sync.peerReconciliation.test.ts +1 -1
  117. package/src/tests/sync.storage.test.ts +29 -28
  118. package/src/tests/sync.storageAsync.test.ts +26 -25
  119. package/src/tests/sync.upload.test.ts +106 -40
  120. package/src/tests/testUtils.ts +43 -9
@@ -254,63 +254,77 @@ export class CoValueCore {
254
254
  return this.hasVerifiedContent();
255
255
  }
256
256
 
257
- /**
258
- * True if the coValue is completely downloaded:
259
- * - the current coValue is available and not streaming
260
- * - the group is available and not streaming
261
- * - TODO: all the parent groups are available and not streaming
262
- */
263
- isCompletelyDownloaded(): this is AvailableCoValueCore {
257
+ isCompletelyDownloaded(): boolean {
264
258
  if (!this.hasVerifiedContent()) {
265
259
  return false;
266
260
  }
267
261
 
268
- if (this.verified.isStreaming()) {
262
+ if (this.isStreaming()) {
269
263
  return false;
270
264
  }
271
265
 
272
- const group = this.safeGetGroup();
273
-
274
- // TODO: Group coValues should be completely downloaded when all their parent groups are completely downloaded
275
- if (!group) {
276
- return true;
266
+ if (this.incompleteDependencies.size > 0) {
267
+ return false;
277
268
  }
278
269
 
279
- return group.core.isCompletelyDownloaded();
270
+ return true;
271
+ }
272
+
273
+ isStreaming() {
274
+ return this.verified?.isStreaming() ?? false;
280
275
  }
281
276
 
282
277
  hasVerifiedContent(): this is AvailableCoValueCore {
283
278
  return !!this.verified;
284
279
  }
285
280
 
281
+ /**
282
+ * Returns the CoValue data as NewContentMessage objects, excluding the transactions that are part of the given known state.
283
+ *
284
+ * Used to serialize the CoValue data to send it to peers and storage.
285
+ */
286
+ newContentSince(
287
+ knownState?: CoValueKnownState,
288
+ ): NewContentMessage[] | undefined {
289
+ return this.verified?.newContentSince(knownState);
290
+ }
291
+
286
292
  isErroredInPeer(peerId: PeerID) {
287
293
  return this.getLoadingStateForPeer(peerId) === "errored";
288
294
  }
289
295
 
290
- waitFor(callback: (value: CoValueCore) => boolean) {
296
+ waitFor(opts: {
297
+ predicate: (value: CoValueCore) => boolean;
298
+ onSuccess: (value: CoValueCore) => void;
299
+ }) {
300
+ const { predicate, onSuccess } = opts;
301
+ this.subscribe((core, unsubscribe) => {
302
+ if (predicate(core)) {
303
+ unsubscribe();
304
+ onSuccess(core);
305
+ }
306
+ }, true);
307
+ }
308
+
309
+ waitForAsync(callback: (value: CoValueCore) => boolean) {
291
310
  return new Promise<CoValueCore>((resolve) => {
292
- this.subscribe((core, unsubscribe) => {
293
- if (callback(core)) {
294
- unsubscribe();
295
- resolve(core);
296
- }
297
- }, true);
311
+ this.waitFor({ predicate: callback, onSuccess: resolve });
298
312
  });
299
313
  }
300
314
 
301
315
  waitForAvailableOrUnavailable(): Promise<CoValueCore> {
302
- return this.waitFor(
316
+ return this.waitForAsync(
303
317
  (core) => core.isAvailable() || core.loadingState === "unavailable",
304
318
  );
305
319
  }
306
320
 
307
321
  waitForAvailable(): Promise<CoValueCore> {
308
- return this.waitFor((core) => core.isAvailable());
322
+ return this.waitForAsync((core) => core.isAvailable());
309
323
  }
310
324
 
311
325
  waitForFullStreaming(): Promise<CoValueCore> {
312
- return this.waitFor(
313
- (core) => core.isAvailable() && !core.verified.isStreaming(),
326
+ return this.waitForAsync(
327
+ (core) => core.isAvailable() && !core.isStreaming(),
314
328
  );
315
329
  }
316
330
 
@@ -368,7 +382,7 @@ export class CoValueCore {
368
382
 
369
383
  missingDependencies = new Set<RawCoID>();
370
384
 
371
- isCircularMissingDependency(dependency: CoValueCore) {
385
+ isCircularDependency(dependency: CoValueCore) {
372
386
  const visited = new Set<RawCoID>();
373
387
  const stack = [dependency];
374
388
 
@@ -381,7 +395,7 @@ export class CoValueCore {
381
395
 
382
396
  visited.add(current.id);
383
397
 
384
- for (const dependency of current.missingDependencies) {
398
+ for (const dependency of current.dependencies) {
385
399
  if (dependency === this.id) {
386
400
  return true;
387
401
  }
@@ -395,14 +409,6 @@ export class CoValueCore {
395
409
  return false;
396
410
  }
397
411
 
398
- markDependencyAvailable(dependency: RawCoID) {
399
- this.missingDependencies.delete(dependency);
400
-
401
- if (this.missingDependencies.size === 0) {
402
- this.scheduleNotifyUpdate();
403
- }
404
- }
405
-
406
412
  newContentQueue: {
407
413
  msg: NewContentMessage;
408
414
  from: PeerState | "storage" | "import";
@@ -422,17 +428,16 @@ export class CoValueCore {
422
428
  return;
423
429
  }
424
430
 
425
- this.subscribe((core, unsubscribe) => {
426
- if (!core.hasMissingDependencies()) {
427
- unsubscribe();
428
-
431
+ this.waitFor({
432
+ predicate: (core) => !core.hasMissingDependencies(),
433
+ onSuccess: () => {
429
434
  const enqueuedNewContent = this.newContentQueue;
430
435
  this.newContentQueue = [];
431
436
 
432
437
  for (const { msg, from } of enqueuedNewContent) {
433
438
  this.node.syncManager.handleNewContent(msg, from);
434
439
  }
435
- }
440
+ },
436
441
  });
437
442
  }
438
443
 
@@ -764,6 +769,8 @@ export class CoValueCore {
764
769
 
765
770
  let result: { signature: Signature; transaction: Transaction };
766
771
 
772
+ const knownStateBefore = this.knownState();
773
+
767
774
  if (privacy === "private") {
768
775
  const { secret: keySecret, id: keyID } = this.getCurrentReadKey();
769
776
 
@@ -790,7 +797,7 @@ export class CoValueCore {
790
797
  );
791
798
  }
792
799
 
793
- const { transaction, signature } = result;
800
+ const { transaction } = result;
794
801
 
795
802
  // Assign pre-parsed meta and changes to skip the parse/decrypt operation when loading
796
803
  // this transaction in the current content
@@ -798,9 +805,6 @@ export class CoValueCore {
798
805
 
799
806
  this.node.syncManager.recordTransactionsSize([transaction], "local");
800
807
 
801
- const session = this.verified.sessions.get(sessionID);
802
- const txIdx = session ? session.transactions.length - 1 : 0;
803
-
804
808
  this.resetKnownStateCache();
805
809
  this.processNewTransactions();
806
810
  this.addDependenciesFromNewTransaction(transaction);
@@ -808,13 +812,7 @@ export class CoValueCore {
808
812
  // force immediate notification because local updates may come from the UI
809
813
  // where we need synchronous updates
810
814
  this.notifyUpdate();
811
- this.node.syncManager.syncLocalTransaction(
812
- this.verified,
813
- transaction,
814
- sessionID,
815
- signature,
816
- txIdx,
817
- );
815
+ this.node.syncManager.syncLocalTransaction(this.verified, knownStateBefore);
818
816
 
819
817
  return true;
820
818
  }
@@ -1163,30 +1161,47 @@ export class CoValueCore {
1163
1161
  }
1164
1162
 
1165
1163
  dependencies: Set<RawCoID> = new Set();
1164
+ incompleteDependencies: Set<RawCoID> = new Set();
1166
1165
  private addDependency(dependency: RawCoID) {
1167
- if (this.dependencies.has(dependency)) {
1168
- return true;
1169
- }
1170
-
1171
- this.dependencies.add(dependency);
1172
-
1173
1166
  const dependencyCoValue = this.node.getCoValue(dependency);
1174
1167
 
1175
- if (this.isCircularMissingDependency(dependencyCoValue)) {
1176
- return true;
1168
+ if (
1169
+ this.isCircularDependency(dependencyCoValue) ||
1170
+ this.dependencies.has(dependency)
1171
+ ) {
1172
+ return;
1177
1173
  }
1178
1174
 
1175
+ this.dependencies.add(dependency);
1179
1176
  dependencyCoValue.addDependant(this.id);
1180
1177
 
1178
+ if (!dependencyCoValue.isCompletelyDownloaded()) {
1179
+ this.incompleteDependencies.add(dependencyCoValue.id);
1180
+ dependencyCoValue.waitFor({
1181
+ predicate: (dependencyCoValue) =>
1182
+ dependencyCoValue.isCompletelyDownloaded(),
1183
+ onSuccess: () => {
1184
+ this.incompleteDependencies.delete(dependencyCoValue.id);
1185
+ if (this.incompleteDependencies.size === 0) {
1186
+ // We want this to propagate immediately in the dependency chain
1187
+ this.notifyUpdate();
1188
+ }
1189
+ },
1190
+ });
1191
+ }
1192
+
1181
1193
  if (!dependencyCoValue.isAvailable()) {
1182
- this.missingDependencies.add(dependency);
1183
- dependencyCoValue.subscribe((dependencyCoValue, unsubscribe) => {
1184
- if (dependencyCoValue.isAvailable()) {
1185
- unsubscribe();
1186
- this.markDependencyAvailable(dependency);
1187
- }
1194
+ this.missingDependencies.add(dependencyCoValue.id);
1195
+ dependencyCoValue.waitFor({
1196
+ predicate: (dependencyCoValue) => dependencyCoValue.isAvailable(),
1197
+ onSuccess: () => {
1198
+ this.missingDependencies.delete(dependencyCoValue.id);
1199
+
1200
+ if (this.missingDependencies.size === 0) {
1201
+ this.notifyUpdate(); // We want this to propagate immediately
1202
+ }
1203
+ },
1188
1204
  });
1189
- return false;
1190
1205
  }
1191
1206
  }
1192
1207
 
@@ -4,6 +4,8 @@ import {
4
4
  createContentMessage,
5
5
  exceedsRecommendedSize,
6
6
  getTransactionSize,
7
+ addTransactionToContentMessage,
8
+ knownStateFromContent,
7
9
  } from "../coValueContentMessage.js";
8
10
  import {
9
11
  CryptoProvider,
@@ -19,9 +21,13 @@ import { JsonObject, JsonValue } from "../jsonValue.js";
19
21
  import { PermissionsDef as RulesetDef } from "../permissions.js";
20
22
  import { NewContentMessage } from "../sync.js";
21
23
  import { TryAddTransactionsError } from "./coValueCore.js";
22
- import { SessionLog, SessionMap } from "./SessionMap.js";
24
+ import { SessionMap } from "./SessionMap.js";
23
25
  import { ControlledAccountOrAgent } from "../coValues/account.js";
24
- import { cloneKnownState, CoValueKnownState } from "../knownState.js";
26
+ import {
27
+ CoValueKnownState,
28
+ getKnownStateToSend,
29
+ KnownStateSessions,
30
+ } from "../knownState.js";
25
31
 
26
32
  export type CoValueHeader = {
27
33
  type: AnyRawCoValue["type"];
@@ -55,7 +61,6 @@ export class VerifiedState {
55
61
  readonly crypto: CryptoProvider;
56
62
  readonly header: CoValueHeader;
57
63
  readonly sessions: SessionMap;
58
- private _cachedNewContentSinceEmpty: NewContentMessage[] | undefined;
59
64
  public lastAccessed: number | undefined;
60
65
  public branchSourceId?: RawCoID;
61
66
  public branchName?: string;
@@ -98,10 +103,6 @@ export class VerifiedState {
98
103
  skipVerify,
99
104
  );
100
105
 
101
- if (result.isOk()) {
102
- this._cachedNewContentSinceEmpty = undefined;
103
- }
104
-
105
106
  return result;
106
107
  }
107
108
 
@@ -120,8 +121,6 @@ export class VerifiedState {
120
121
  madeAt,
121
122
  );
122
123
 
123
- this._cachedNewContentSinceEmpty = undefined;
124
-
125
124
  return result;
126
125
  }
127
126
 
@@ -144,8 +143,6 @@ export class VerifiedState {
144
143
  madeAt,
145
144
  );
146
145
 
147
- this._cachedNewContentSinceEmpty = undefined;
148
-
149
146
  return result;
150
147
  }
151
148
 
@@ -160,119 +157,157 @@ export class VerifiedState {
160
157
  );
161
158
  }
162
159
 
160
+ setStreamingKnownState(streamingKnownState: KnownStateSessions) {
161
+ this.sessions.setStreamingKnownState(streamingKnownState);
162
+ }
163
+
163
164
  newContentSince(
164
165
  knownState: CoValueKnownState | undefined,
166
+ opts?: {
167
+ skipExpectContentUntil?: boolean;
168
+ },
165
169
  ): NewContentMessage[] | undefined {
166
- const isKnownStateEmpty = !knownState?.header && !knownState?.sessions;
167
-
168
- if (isKnownStateEmpty && this._cachedNewContentSinceEmpty) {
169
- return this._cachedNewContentSinceEmpty;
170
- }
171
-
172
170
  let currentPiece: NewContentMessage = createContentMessage(
173
171
  this.id,
174
172
  this.header,
175
- !knownState?.header,
173
+ false,
176
174
  );
177
-
178
- const pieces = [currentPiece];
179
-
180
- const sentState: CoValueKnownState["sessions"] = {};
181
-
175
+ const pieces: NewContentMessage[] = [currentPiece];
182
176
  let pieceSize = 0;
183
177
 
184
- let sessionsTodoAgain: Set<SessionID> | undefined | "first" = "first";
185
-
186
- while (sessionsTodoAgain === "first" || sessionsTodoAgain?.size || 0 > 0) {
187
- if (sessionsTodoAgain === "first") {
188
- sessionsTodoAgain = undefined;
178
+ const startNewPiece = () => {
179
+ currentPiece = createContentMessage(this.id, this.header, false);
180
+ pieces.push(currentPiece);
181
+ pieceSize = 0;
182
+ };
183
+
184
+ const moveSessionContentToNewPiece = (sessionID: SessionID) => {
185
+ const sessionContent = currentPiece.new[sessionID];
186
+
187
+ if (!sessionContent) {
188
+ throw new Error("Session content not found", {
189
+ cause: {
190
+ sessionID,
191
+ currentPiece,
192
+ },
193
+ });
189
194
  }
190
- const sessionsTodo = sessionsTodoAgain ?? this.sessions.keys();
191
-
192
- for (const sessionIDKey of sessionsTodo) {
193
- const sessionID = sessionIDKey as SessionID;
194
- const log = this.sessions.get(sessionID)!;
195
- const knownStateForSessionID = knownState?.sessions[sessionID];
196
- const sentStateForSessionID = sentState[sessionID];
197
- const nextKnownSignatureIdx = getNextKnownSignatureIdx(
198
- log,
199
- knownStateForSessionID,
200
- sentStateForSessionID,
201
- );
202
-
203
- const firstNewTxIdx =
204
- sentStateForSessionID ?? knownStateForSessionID ?? 0;
205
- const afterLastNewTxIdx =
206
- nextKnownSignatureIdx === undefined
207
- ? log.transactions.length
208
- : nextKnownSignatureIdx + 1;
209
-
210
- const nNewTx = Math.max(0, afterLastNewTxIdx - firstNewTxIdx);
211
-
212
- if (nNewTx === 0) {
213
- sessionsTodoAgain?.delete(sessionID);
214
- continue;
215
- }
216
195
 
217
- if (afterLastNewTxIdx < log.transactions.length) {
218
- if (!sessionsTodoAgain) {
219
- sessionsTodoAgain = new Set();
196
+ delete currentPiece.new[sessionID];
197
+
198
+ const newPiece = createContentMessage(this.id, this.header, false);
199
+ newPiece.new[sessionID] = sessionContent;
200
+
201
+ // Insert the new piece before the current piece, to ensure that the order of the new transactions is preserved
202
+ pieces.splice(pieces.length - 1, 0, newPiece);
203
+ };
204
+
205
+ const sessionSent = knownState?.sessions;
206
+
207
+ for (const [sessionID, log] of this.sessions.sessions) {
208
+ const startFrom = sessionSent?.[sessionID] ?? 0;
209
+
210
+ let currentSessionSize = 0;
211
+
212
+ for (let txIdx = startFrom; txIdx < log.transactions.length; txIdx++) {
213
+ const isLastItem = txIdx === log.transactions.length - 1;
214
+ const tx = log.transactions[txIdx]!;
215
+
216
+ currentSessionSize += getTransactionSize(tx);
217
+
218
+ const signatureAfter = log.signatureAfter[txIdx];
219
+
220
+ if (signatureAfter) {
221
+ addTransactionToContentMessage(
222
+ currentPiece,
223
+ tx,
224
+ sessionID,
225
+ signatureAfter,
226
+ txIdx,
227
+ );
228
+ // When we meet a signatureAfter it means that the transaction log exceeds the recommended size
229
+ // so we move the session content to a dedicated piece, because it must be sent in a standalone piece
230
+ moveSessionContentToNewPiece(sessionID);
231
+ currentSessionSize = 0;
232
+ } else if (isLastItem) {
233
+ if (!log.lastSignature) {
234
+ throw new Error(
235
+ "All the SessionLogs sent must have a lastSignature",
236
+ {
237
+ cause: log,
238
+ },
239
+ );
220
240
  }
221
- sessionsTodoAgain.add(sessionID);
222
- }
223
-
224
- const oldPieceSize = pieceSize;
225
- for (let txIdx = firstNewTxIdx; txIdx < afterLastNewTxIdx; txIdx++) {
226
- const tx = log.transactions[txIdx]!;
227
- pieceSize += getTransactionSize(tx);
228
- }
229
241
 
230
- if (exceedsRecommendedSize(pieceSize)) {
231
- if (!currentPiece.expectContentUntil && pieces.length === 1) {
232
- currentPiece.expectContentUntil =
233
- this.knownStateWithStreaming().sessions;
242
+ addTransactionToContentMessage(
243
+ currentPiece,
244
+ tx,
245
+ sessionID,
246
+ log.lastSignature,
247
+ txIdx,
248
+ );
249
+
250
+ // If the current session size already exceeds the recommended size, we move the session content to a dedicated piece
251
+ if (exceedsRecommendedSize(currentSessionSize)) {
252
+ assertLastSignature(sessionID, currentPiece);
253
+ moveSessionContentToNewPiece(sessionID);
254
+ } else if (exceedsRecommendedSize(pieceSize, currentSessionSize)) {
255
+ assertLastSignature(sessionID, currentPiece);
256
+ startNewPiece();
257
+ } else {
258
+ pieceSize += currentSessionSize;
234
259
  }
235
-
236
- currentPiece = createContentMessage(this.id, this.header, false);
237
- pieces.push(currentPiece);
238
- pieceSize = pieceSize - oldPieceSize;
260
+ } else {
261
+ // Unsafely add the transaction to the content message, without a signature because we don't have one for this session
262
+ // Checks and assertions are enforced in this function to avoid that a content message gets out without a signature
263
+ const signature = undefined as Signature | undefined;
264
+ addTransactionToContentMessage(
265
+ currentPiece,
266
+ tx,
267
+ sessionID,
268
+ signature!,
269
+ txIdx,
270
+ );
239
271
  }
272
+ }
240
273
 
241
- let sessionEntry = currentPiece.new[sessionID];
242
- if (!sessionEntry) {
243
- sessionEntry = {
244
- after: sentStateForSessionID ?? knownStateForSessionID ?? 0,
245
- newTransactions: [],
246
- lastSignature: "WILL_BE_REPLACED" as Signature,
247
- };
248
- currentPiece.new[sessionID] = sessionEntry;
249
- }
274
+ assertLastSignature(sessionID, currentPiece);
275
+ }
250
276
 
251
- for (let txIdx = firstNewTxIdx; txIdx < afterLastNewTxIdx; txIdx++) {
252
- const tx = log.transactions[txIdx]!;
253
- sessionEntry.newTransactions.push(tx);
254
- }
277
+ const firstPiece = pieces[0];
255
278
 
256
- sessionEntry.lastSignature =
257
- nextKnownSignatureIdx === undefined
258
- ? log.lastSignature!
259
- : log.signatureAfter[nextKnownSignatureIdx]!;
279
+ if (!firstPiece) {
280
+ throw new Error("First piece not found", {
281
+ cause: pieces,
282
+ });
283
+ }
260
284
 
261
- sentState[sessionID] =
262
- (sentStateForSessionID ?? knownStateForSessionID ?? 0) + nNewTx;
263
- }
285
+ const includeHeader = !knownState?.header;
286
+
287
+ if (includeHeader) {
288
+ firstPiece.header = this.header;
264
289
  }
265
290
 
266
291
  const piecesWithContent = pieces.filter(
267
- (piece) => Object.keys(piece.new).length > 0 || piece.header,
292
+ (piece) => piece.header || Object.keys(piece.new).length > 0,
268
293
  );
269
294
 
270
- if (piecesWithContent.length === 0) {
271
- return undefined;
295
+ if (piecesWithContent.length > 1 || this.isStreaming()) {
296
+ // Flag that more content is coming
297
+ if (knownState) {
298
+ firstPiece.expectContentUntil = getKnownStateToSend(
299
+ this.knownStateWithStreaming().sessions,
300
+ knownState.sessions,
301
+ );
302
+ } else {
303
+ firstPiece.expectContentUntil = {
304
+ ...this.knownStateWithStreaming().sessions,
305
+ };
306
+ }
272
307
  }
273
308
 
274
- if (isKnownStateEmpty) {
275
- this._cachedNewContentSinceEmpty = piecesWithContent;
309
+ if (piecesWithContent.length === 0) {
310
+ return undefined;
276
311
  }
277
312
 
278
313
  return piecesWithContent;
@@ -307,15 +342,10 @@ export class VerifiedState {
307
342
  }
308
343
  }
309
344
 
310
- function getNextKnownSignatureIdx(
311
- log: SessionLog,
312
- knownStateForSessionID?: number,
313
- sentStateForSessionID?: number,
314
- ) {
315
- return Object.keys(log.signatureAfter)
316
- .map(Number)
317
- .sort((a, b) => a - b)
318
- .find(
319
- (idx) => idx >= (sentStateForSessionID ?? knownStateForSessionID ?? -1),
320
- );
345
+ function assertLastSignature(sessionID: SessionID, content: NewContentMessage) {
346
+ if (content.new[sessionID] && !content.new[sessionID].lastSignature) {
347
+ throw new Error("The SessionContent sent must have a lastSignature", {
348
+ cause: content.new[sessionID],
349
+ });
350
+ }
321
351
  }
@@ -123,6 +123,13 @@ function healMissingKeyForEveryone(group: RawGroup) {
123
123
  }
124
124
 
125
125
  function needsKeyRotation(group: RawGroup) {
126
+ const myRole = group.myRole();
127
+
128
+ // Checking only direct membership because inside the migrations we can't navigate the parent groups
129
+ if (myRole !== "admin" && myRole !== "manager") {
130
+ return false;
131
+ }
132
+
126
133
  const currentReadKeyId = group.get("readKey");
127
134
 
128
135
  if (!currentReadKeyId) {
@@ -190,11 +197,27 @@ export class RawGroup<
190
197
  ) {
191
198
  super(core, options);
192
199
  this.crypto = core.node.crypto;
200
+ this.migrate();
201
+ }
193
202
 
194
- // Checks if this is not an account
195
- if (core.isGroup() && !core.verified.isStreaming()) {
196
- // rotateReadKeyIfNeeded(this);
203
+ migrate() {
204
+ if (!this.core.isGroup()) {
205
+ return;
206
+ }
207
+
208
+ const runMigrations = () => {
209
+ rotateReadKeyIfNeeded(this);
197
210
  healMissingKeyForEveryone(this);
211
+ };
212
+
213
+ // We need the group and their parents to be completely downloaded to correctly handle the migrations
214
+ if (!this.core.isCompletelyDownloaded()) {
215
+ this.core.waitFor({
216
+ predicate: (core) => core.isCompletelyDownloaded(),
217
+ onSuccess: runMigrations,
218
+ });
219
+ } else {
220
+ runMigrations();
198
221
  }
199
222
  }
200
223
 
@@ -318,7 +341,7 @@ export class RawGroup<
318
341
  * @category 1. Role reading
319
342
  */
320
343
  myRole(): Role | undefined {
321
- return this.roleOfInternal(this.core.node.getCurrentAgent().id);
344
+ return this.roleOfInternal(this.core.node.getCurrentAccountOrAgentID());
322
345
  }
323
346
 
324
347
  /**