cojson 0.1.12 → 0.2.1

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 (54) hide show
  1. package/dist/coValue.d.ts +1 -1
  2. package/dist/coValueCore.d.ts +10 -4
  3. package/dist/coValueCore.js +91 -46
  4. package/dist/coValueCore.js.map +1 -1
  5. package/dist/coValues/coList.js +2 -1
  6. package/dist/coValues/coList.js.map +1 -1
  7. package/dist/coValues/coMap.d.ts +7 -12
  8. package/dist/coValues/coMap.js +2 -1
  9. package/dist/coValues/coMap.js.map +1 -1
  10. package/dist/coValues/coStream.d.ts +11 -2
  11. package/dist/coValues/coStream.js +61 -14
  12. package/dist/coValues/coStream.js.map +1 -1
  13. package/dist/crypto.d.ts +8 -0
  14. package/dist/crypto.js +10 -3
  15. package/dist/crypto.js.map +1 -1
  16. package/dist/group.d.ts +1 -1
  17. package/dist/index.d.ts +6 -3
  18. package/dist/index.js +5 -3
  19. package/dist/index.js.map +1 -1
  20. package/dist/jsonStringify.d.ts +6 -0
  21. package/dist/{fastJsonStableStringify.js → jsonStringify.js} +10 -6
  22. package/dist/jsonStringify.js.map +1 -0
  23. package/dist/jsonValue.d.ts +1 -1
  24. package/dist/media.d.ts +8 -0
  25. package/dist/media.js +2 -0
  26. package/dist/media.js.map +1 -0
  27. package/dist/node.js +1 -1
  28. package/dist/permissions.js +4 -2
  29. package/dist/permissions.js.map +1 -1
  30. package/dist/streamUtils.js +14 -5
  31. package/dist/streamUtils.js.map +1 -1
  32. package/dist/sync.js +35 -15
  33. package/dist/sync.js.map +1 -1
  34. package/package.json +2 -2
  35. package/src/coValue.test.ts +113 -4
  36. package/src/coValue.ts +1 -1
  37. package/src/coValueCore.test.ts +11 -10
  38. package/src/coValueCore.ts +162 -75
  39. package/src/coValues/coList.ts +2 -1
  40. package/src/coValues/coMap.ts +11 -12
  41. package/src/coValues/coStream.ts +73 -21
  42. package/src/crypto.ts +22 -4
  43. package/src/group.ts +1 -1
  44. package/src/index.ts +7 -2
  45. package/src/{fastJsonStableStringify.ts → jsonStringify.ts} +23 -11
  46. package/src/jsonValue.ts +1 -1
  47. package/src/media.ts +9 -0
  48. package/src/node.ts +1 -1
  49. package/src/permissions.ts +5 -2
  50. package/src/streamUtils.ts +26 -6
  51. package/src/sync.test.ts +19 -20
  52. package/src/sync.ts +47 -26
  53. package/dist/fastJsonStableStringify.d.ts +0 -1
  54. package/dist/fastJsonStableStringify.js.map +0 -1
@@ -14,11 +14,11 @@ import {
14
14
  sign,
15
15
  verify,
16
16
  encryptForTransaction,
17
- decryptForTransaction,
18
17
  KeyID,
19
18
  decryptKeySecret,
20
19
  getAgentSignerID,
21
20
  getAgentSealerID,
21
+ decryptRawForTransaction,
22
22
  } from "./crypto.js";
23
23
  import { JsonObject, JsonValue } from "./jsonValue.js";
24
24
  import { base58 } from "@scure/base";
@@ -32,10 +32,10 @@ import { LocalNode } from "./node.js";
32
32
  import { CoValueKnownState, NewContentMessage } from "./sync.js";
33
33
  import { AgentID, RawCoID, SessionID, TransactionID } from "./ids.js";
34
34
  import { CoList } from "./coValues/coList.js";
35
- import {
36
- AccountID,
37
- GeneralizedControlledAccount,
38
- } from "./account.js";
35
+ import { AccountID, GeneralizedControlledAccount } from "./account.js";
36
+ import { Stringified, stableStringify } from "./jsonStringify.js";
37
+
38
+ export const MAX_RECOMMENDED_TX_SIZE = 100 * 1024;
39
39
 
40
40
  export type CoValueHeader = {
41
41
  type: CoValueImpl["type"];
@@ -64,6 +64,7 @@ type SessionLog = {
64
64
  transactions: Transaction[];
65
65
  lastHash?: Hash;
66
66
  streamingHash: StreamingHash;
67
+ signatureAfter: { [txIdx: number]: Signature | undefined };
67
68
  lastSignature: Signature;
68
69
  };
69
70
 
@@ -80,14 +81,14 @@ export type PrivateTransaction = {
80
81
  export type TrustingTransaction = {
81
82
  privacy: "trusting";
82
83
  madeAt: number;
83
- changes: JsonValue[];
84
+ changes: Stringified<JsonValue[]>;
84
85
  };
85
86
 
86
87
  export type Transaction = PrivateTransaction | TrustingTransaction;
87
88
 
88
89
  export type DecryptedTransaction = {
89
90
  txID: TransactionID;
90
- changes: JsonValue[];
91
+ changes: Stringified<JsonValue[]>;
91
92
  madeAt: number;
92
93
  };
93
94
 
@@ -100,7 +101,11 @@ export class CoValueCore {
100
101
  _sessions: { [key: SessionID]: SessionLog };
101
102
  _cachedContent?: CoValueImpl;
102
103
  listeners: Set<(content?: CoValueImpl) => void> = new Set();
103
- _decryptionCache: {[key: Encrypted<JsonValue[], JsonValue>]: JsonValue[] | undefined} = {}
104
+ _decryptionCache: {
105
+ [key: Encrypted<JsonValue[], JsonValue>]:
106
+ | Stringified<JsonValue[]>
107
+ | undefined;
108
+ } = {};
104
109
 
105
110
  constructor(
106
111
  header: CoValueHeader,
@@ -209,7 +214,8 @@ export class CoValueCore {
209
214
  // const beforeVerify = performance.now();
210
215
  if (!verify(newSignature, expectedNewHash, signerID)) {
211
216
  console.warn(
212
- "Invalid signature",
217
+ "Invalid signature in",
218
+ this.id,
213
219
  newSignature,
214
220
  expectedNewHash,
215
221
  signerID
@@ -222,25 +228,13 @@ export class CoValueCore {
222
228
  // afterVerify - beforeVerify
223
229
  // );
224
230
 
225
- const transactions = this.sessions[sessionID]?.transactions ?? [];
226
-
227
- transactions.push(...newTransactions);
228
-
229
- this._sessions[sessionID] = {
230
- transactions,
231
- lastHash: expectedNewHash,
232
- streamingHash: newStreamingHash,
233
- lastSignature: newSignature,
234
- };
235
-
236
- this._cachedContent = undefined;
237
-
238
- if (this.listeners.size > 0) {
239
- const content = this.getCurrentContent();
240
- for (const listener of this.listeners) {
241
- listener(content);
242
- }
243
- }
231
+ this.doAddTransactions(
232
+ sessionID,
233
+ newTransactions,
234
+ newSignature,
235
+ expectedNewHash,
236
+ newStreamingHash
237
+ );
244
238
 
245
239
  return true;
246
240
  }
@@ -269,10 +263,8 @@ export class CoValueCore {
269
263
  const nTxBefore = this.sessions[sessionID]?.transactions.length ?? 0;
270
264
 
271
265
  // const beforeHash = performance.now();
272
- const { expectedNewHash, newStreamingHash } = await this.expectedNewHashAfterAsync(
273
- sessionID,
274
- newTransactions
275
- );
266
+ const { expectedNewHash, newStreamingHash } =
267
+ await this.expectedNewHashAfterAsync(sessionID, newTransactions);
276
268
  // const afterHash = performance.now();
277
269
  // console.log(
278
270
  // "Hashing took",
@@ -283,7 +275,7 @@ export class CoValueCore {
283
275
 
284
276
  if (nTxAfter !== nTxBefore) {
285
277
  const newTransactionLengthBefore = newTransactions.length;
286
- newTransactions = newTransactions.slice((nTxAfter - nTxBefore));
278
+ newTransactions = newTransactions.slice(nTxAfter - nTxBefore);
287
279
  console.warn("Transactions changed while async hashing", {
288
280
  nTxBefore,
289
281
  nTxAfter,
@@ -303,7 +295,8 @@ export class CoValueCore {
303
295
  // const beforeVerify = performance.now();
304
296
  if (!verify(newSignature, expectedNewHash, signerID)) {
305
297
  console.warn(
306
- "Invalid signature",
298
+ "Invalid signature in",
299
+ this.id,
307
300
  newSignature,
308
301
  expectedNewHash,
309
302
  signerID
@@ -316,15 +309,61 @@ export class CoValueCore {
316
309
  // afterVerify - beforeVerify
317
310
  // );
318
311
 
319
- const transactions = this.sessions[sessionID]?.transactions ?? [];
312
+ this.doAddTransactions(
313
+ sessionID,
314
+ newTransactions,
315
+ newSignature,
316
+ expectedNewHash,
317
+ newStreamingHash
318
+ );
320
319
 
320
+ return true;
321
+ }
322
+
323
+ private doAddTransactions(
324
+ sessionID: SessionID,
325
+ newTransactions: Transaction[],
326
+ newSignature: Signature,
327
+ expectedNewHash: Hash,
328
+ newStreamingHash: StreamingHash
329
+ ) {
330
+ const transactions = this.sessions[sessionID]?.transactions ?? [];
321
331
  transactions.push(...newTransactions);
322
332
 
333
+ const signatureAfter = this.sessions[sessionID]?.signatureAfter ?? {};
334
+
335
+ const lastInbetweenSignatureIdx = Object.keys(signatureAfter).reduce(
336
+ (max, idx) => (parseInt(idx) > max ? parseInt(idx) : max),
337
+ -1
338
+ );
339
+
340
+ const sizeOfTxsSinceLastInbetweenSignature = transactions
341
+ .slice(lastInbetweenSignatureIdx + 1)
342
+ .reduce(
343
+ (sum, tx) =>
344
+ sum +
345
+ (tx.privacy === "private"
346
+ ? tx.encryptedChanges.length
347
+ : tx.changes.length),
348
+ 0
349
+ );
350
+
351
+ if (sizeOfTxsSinceLastInbetweenSignature > 100 * 1024) {
352
+ // console.log(
353
+ // "Saving inbetween signature for tx ",
354
+ // sessionID,
355
+ // transactions.length - 1,
356
+ // sizeOfTxsSinceLastInbetweenSignature
357
+ // );
358
+ signatureAfter[transactions.length - 1] = newSignature;
359
+ }
360
+
323
361
  this._sessions[sessionID] = {
324
362
  transactions,
325
363
  lastHash: expectedNewHash,
326
364
  streamingHash: newStreamingHash,
327
365
  lastSignature: newSignature,
366
+ signatureAfter: signatureAfter,
328
367
  };
329
368
 
330
369
  this._cachedContent = undefined;
@@ -335,8 +374,6 @@ export class CoValueCore {
335
374
  listener(content);
336
375
  }
337
376
  }
338
-
339
- return true;
340
377
  }
341
378
 
342
379
  subscribe(listener: (content?: CoValueImpl) => void): () => void {
@@ -376,10 +413,10 @@ export class CoValueCore {
376
413
  new StreamingHash();
377
414
  let before = performance.now();
378
415
  for (const transaction of newTransactions) {
379
- streamingHash.update(transaction)
416
+ streamingHash.update(transaction);
380
417
  const after = performance.now();
381
418
  if (after - before > 1) {
382
- console.log("Hashing blocked for", after - before);
419
+ // console.log("Hashing blocked for", after - before);
383
420
  await new Promise((resolve) => setTimeout(resolve, 0));
384
421
  before = performance.now();
385
422
  }
@@ -415,7 +452,7 @@ export class CoValueCore {
415
452
  tx: this.nextTransactionID(),
416
453
  });
417
454
 
418
- this._decryptionCache[encrypted] = changes;
455
+ this._decryptionCache[encrypted] = stableStringify(changes);
419
456
 
420
457
  transaction = {
421
458
  privacy: "private",
@@ -427,7 +464,7 @@ export class CoValueCore {
427
464
  transaction = {
428
465
  privacy: "trusting",
429
466
  madeAt,
430
- changes,
467
+ changes: stableStringify(changes),
431
468
  };
432
469
  }
433
470
 
@@ -497,10 +534,11 @@ export class CoValueCore {
497
534
  if (!readKey) {
498
535
  return undefined;
499
536
  } else {
500
- let decrytedChanges = this._decryptionCache[tx.encryptedChanges];
537
+ let decrytedChanges =
538
+ this._decryptionCache[tx.encryptedChanges];
501
539
 
502
540
  if (!decrytedChanges) {
503
- decrytedChanges = decryptForTransaction(
541
+ decrytedChanges = decryptRawForTransaction(
504
542
  tx.encryptedChanges,
505
543
  readKey,
506
544
  {
@@ -508,7 +546,8 @@ export class CoValueCore {
508
546
  tx: txID,
509
547
  }
510
548
  );
511
- this._decryptionCache[tx.encryptedChanges] = decrytedChanges;
549
+ this._decryptionCache[tx.encryptedChanges] =
550
+ decrytedChanges;
512
551
  }
513
552
 
514
553
  if (!decrytedChanges) {
@@ -680,47 +719,95 @@ export class CoValueCore {
680
719
 
681
720
  newContentSince(
682
721
  knownState: CoValueKnownState | undefined
683
- ): NewContentMessage | undefined {
684
- const newContent: NewContentMessage = {
722
+ ): NewContentMessage[] | undefined {
723
+ let currentPiece: NewContentMessage = {
685
724
  action: "content",
686
725
  id: this.id,
687
726
  header: knownState?.header ? undefined : this.header,
688
- new: Object.fromEntries(
689
- Object.entries(this.sessions)
690
- .map(([sessionID, log]) => {
691
- const newTransactions = log.transactions.slice(
692
- knownState?.sessions[sessionID as SessionID] || 0
693
- );
727
+ new: {},
728
+ };
694
729
 
695
- if (
696
- newTransactions.length === 0 ||
697
- !log.lastHash ||
698
- !log.lastSignature
699
- ) {
700
- return undefined;
701
- }
730
+ const pieces = [currentPiece];
702
731
 
703
- return [
704
- sessionID,
705
- {
706
- after:
707
- knownState?.sessions[
708
- sessionID as SessionID
709
- ] || 0,
710
- newTransactions,
711
- lastSignature: log.lastSignature,
712
- },
713
- ];
714
- })
715
- .filter((x): x is Exclude<typeof x, undefined> => !!x)
716
- ),
732
+ const sentState: CoValueKnownState["sessions"] = {
733
+ ...knownState?.sessions,
717
734
  };
718
735
 
719
- if (!newContent.header && Object.keys(newContent.new).length === 0) {
736
+ let newTxsWereAdded = true;
737
+ let pieceSize = 0;
738
+ while (newTxsWereAdded) {
739
+ newTxsWereAdded = false;
740
+
741
+ for (const [sessionID, log] of Object.entries(this.sessions) as [
742
+ SessionID,
743
+ SessionLog
744
+ ][]) {
745
+ const nextKnownSignatureIdx = Object.keys(log.signatureAfter)
746
+ .map(Number)
747
+ .sort((a, b) => a - b)
748
+ .find((idx) => idx >= (sentState[sessionID] ?? -1));
749
+
750
+ const txsToAdd = log.transactions.slice(
751
+ sentState[sessionID] ?? 0,
752
+ nextKnownSignatureIdx === undefined
753
+ ? undefined
754
+ : nextKnownSignatureIdx + 1
755
+ );
756
+
757
+ if (txsToAdd.length === 0) continue;
758
+
759
+ newTxsWereAdded = true;
760
+
761
+ const oldPieceSize = pieceSize;
762
+ pieceSize += txsToAdd.reduce(
763
+ (sum, tx) =>
764
+ sum +
765
+ (tx.privacy === "private"
766
+ ? tx.encryptedChanges.length
767
+ : tx.changes.length),
768
+ 0
769
+ );
770
+
771
+ if (pieceSize >= MAX_RECOMMENDED_TX_SIZE) {
772
+ currentPiece = {
773
+ action: "content",
774
+ id: this.id,
775
+ header: undefined,
776
+ new: {},
777
+ };
778
+ pieces.push(currentPiece);
779
+ pieceSize = pieceSize - oldPieceSize;
780
+ }
781
+
782
+ let sessionEntry = currentPiece.new[sessionID];
783
+ if (!sessionEntry) {
784
+ sessionEntry = {
785
+ after: sentState[sessionID] ?? 0,
786
+ newTransactions: [],
787
+ lastSignature: "WILL_BE_REPLACED" as Signature
788
+ };
789
+ currentPiece.new[sessionID] = sessionEntry;
790
+ }
791
+
792
+ sessionEntry.newTransactions.push(...txsToAdd);
793
+ sessionEntry.lastSignature = nextKnownSignatureIdx === undefined
794
+ ? log.lastSignature!
795
+ : log.signatureAfter[nextKnownSignatureIdx]!
796
+
797
+ sentState[sessionID] =
798
+ (sentState[sessionID] || 0) + txsToAdd.length;
799
+ }
800
+ }
801
+
802
+ const piecesWithContent = pieces.filter(
803
+ (piece) => Object.keys(piece.new).length > 0 || piece.header
804
+ );
805
+
806
+ if (piecesWithContent.length === 0) {
720
807
  return undefined;
721
808
  }
722
809
 
723
- return newContent;
810
+ return piecesWithContent;
724
811
  }
725
812
 
726
813
  getDependedOnCoValues(): RawCoID[] {
@@ -4,6 +4,7 @@ import { CoValueCore, accountOrAgentIDfromSessionID } from "../coValueCore.js";
4
4
  import { SessionID, TransactionID } from "../ids.js";
5
5
  import { Group } from "../group.js";
6
6
  import { AccountID, isAccountID } from "../account.js";
7
+ import { parseJSON } from "../jsonStringify.js";
7
8
 
8
9
  type OpID = TransactionID & { changeIdx: number };
9
10
 
@@ -98,7 +99,7 @@ export class CoList<T extends JsonValue, Meta extends JsonObject | null = null>
98
99
  changes,
99
100
  madeAt,
100
101
  } of this.core.getValidSortedTransactions()) {
101
- for (const [changeIdx, changeUntyped] of changes.entries()) {
102
+ for (const [changeIdx, changeUntyped] of parseJSON(changes).entries()) {
102
103
  const change = changeUntyped as ListOpPayload<T>;
103
104
 
104
105
  if (change.op === "pre" || change.op === "app") {
@@ -4,15 +4,16 @@ import { CoID, ReadableCoValue, WriteableCoValue } from '../coValue.js';
4
4
  import { CoValueCore, accountOrAgentIDfromSessionID } from '../coValueCore.js';
5
5
  import { AccountID, isAccountID } from '../account.js';
6
6
  import { Group } from '../group.js';
7
+ import { parseJSON } from '../jsonStringify.js';
7
8
 
8
- type MapOp<K extends string, V extends JsonValue> = {
9
+ type MapOp<K extends string, V extends JsonValue | undefined> = {
9
10
  txID: TransactionID;
10
11
  madeAt: number;
11
12
  changeIdx: number;
12
13
  } & MapOpPayload<K, V>;
13
14
  // TODO: add after TransactionID[] for conflicts/ordering
14
15
 
15
- export type MapOpPayload<K extends string, V extends JsonValue> = {
16
+ export type MapOpPayload<K extends string, V extends JsonValue | undefined> = {
16
17
  op: "set";
17
18
  key: K;
18
19
  value: V;
@@ -22,18 +23,16 @@ export type MapOpPayload<K extends string, V extends JsonValue> = {
22
23
  key: K;
23
24
  };
24
25
 
25
- export type MapK<M extends { [key: string]: JsonValue; }> = keyof M & string;
26
- export type MapV<M extends { [key: string]: JsonValue; }> = M[MapK<M>];
27
- export type MapM<M extends { [key: string]: JsonValue; }> = {
28
- [KK in MapK<M>]: M[KK];
29
- }
26
+ export type MapK<M extends { [key: string]: JsonValue | undefined; }> = keyof M & string;
27
+ export type MapV<M extends { [key: string]: JsonValue | undefined; }> = M[MapK<M>];
28
+
30
29
 
31
30
  /** A collaborative map with precise shape `M` and optional static metadata `Meta` */
32
31
  export class CoMap<
33
- M extends { [key: string]: JsonValue; },
32
+ M extends { [key: string]: JsonValue | undefined; },
34
33
  Meta extends JsonObject | null = null,
35
34
  > implements ReadableCoValue {
36
- id: CoID<CoMap<MapM<M>, Meta>>;
35
+ id: CoID<CoMap<M, Meta>>;
37
36
  type = "comap" as const;
38
37
  core: CoValueCore;
39
38
  /** @internal */
@@ -43,7 +42,7 @@ export class CoMap<
43
42
 
44
43
  /** @internal */
45
44
  constructor(core: CoValueCore) {
46
- this.id = core.id as CoID<CoMap<MapM<M>, Meta>>;
45
+ this.id = core.id as CoID<CoMap<M, Meta>>;
47
46
  this.core = core;
48
47
  this.ops = {};
49
48
 
@@ -64,7 +63,7 @@ export class CoMap<
64
63
 
65
64
  for (const { txID, changes, madeAt } of this.core.getValidSortedTransactions()) {
66
65
  for (const [changeIdx, changeUntyped] of (
67
- changes
66
+ parseJSON(changes)
68
67
  ).entries()) {
69
68
  const change = changeUntyped as MapOpPayload<MapK<M>, MapV<M>>;
70
69
  let entries = this.ops[change.key];
@@ -207,7 +206,7 @@ export class CoMap<
207
206
  }
208
207
 
209
208
  export class WriteableCoMap<
210
- M extends { [key: string]: JsonValue; },
209
+ M extends { [key: string]: JsonValue | undefined; },
211
210
  Meta extends JsonObject | null = null,
212
211
  > extends CoMap<M, Meta> implements WriteableCoValue {
213
212
  /** @internal */
@@ -1,9 +1,12 @@
1
1
  import { JsonObject, JsonValue } from "../jsonValue.js";
2
2
  import { CoID, ReadableCoValue, WriteableCoValue } from "../coValue.js";
3
- import { CoValueCore } from "../coValueCore.js";
3
+ import { CoValueCore, accountOrAgentIDfromSessionID } from "../coValueCore.js";
4
4
  import { Group } from "../group.js";
5
5
  import { SessionID } from "../ids.js";
6
6
  import { base64URLtoBytes, bytesToBase64url } from "../base64url.js";
7
+ import { AccountID } from "../index.js";
8
+ import { isAccountID } from "../account.js";
9
+ import { parseJSON } from "../jsonStringify.js";
7
10
 
8
11
  export type BinaryChunkInfo = {
9
12
  mimeType: string;
@@ -40,7 +43,7 @@ export class CoStream<
40
43
  type = "costream" as const;
41
44
  core: CoValueCore;
42
45
  items: {
43
- [key: SessionID]: T[];
46
+ [key: SessionID]: {item: T, madeAt: number}[];
44
47
  };
45
48
 
46
49
  constructor(core: CoValueCore) {
@@ -64,16 +67,17 @@ export class CoStream<
64
67
 
65
68
  for (const {
66
69
  txID,
70
+ madeAt,
67
71
  changes,
68
72
  } of this.core.getValidSortedTransactions()) {
69
- for (const changeUntyped of changes) {
73
+ for (const changeUntyped of parseJSON(changes)) {
70
74
  const change = changeUntyped as T;
71
75
  let entries = this.items[txID.sessionID];
72
76
  if (!entries) {
73
77
  entries = [];
74
78
  this.items[txID.sessionID] = entries;
75
79
  }
76
- entries.push(change);
80
+ entries.push({item: change, madeAt});
77
81
  }
78
82
  }
79
83
  }
@@ -87,13 +91,57 @@ export class CoStream<
87
91
  );
88
92
  }
89
93
 
90
- return Object.values(this.items)[0];
94
+ return Object.values(this.items)[0]?.map(item => item.item);
95
+ }
96
+
97
+ getLastItemsPerAccount(): {[account: AccountID]: T | undefined} {
98
+ const result: {[account: AccountID]: {item: T, madeAt: number} | undefined} = {};
99
+
100
+ for (const [sessionID, items] of Object.entries(this.items)) {
101
+ const account = accountOrAgentIDfromSessionID(sessionID as SessionID);
102
+ if (!isAccountID(account)) continue;
103
+ if (items.length > 0) {
104
+ const lastItemOfSession = items[items.length - 1]!;
105
+ if (!result[account] || lastItemOfSession.madeAt > result[account]!.madeAt) {
106
+ result[account] = lastItemOfSession;
107
+ }
108
+ }
109
+ }
110
+
111
+ return Object.fromEntries(Object.entries(result).map(([account, item]) =>
112
+ [account, item?.item]
113
+ ));
114
+ }
115
+
116
+ getLastItemFrom(account: AccountID): T | undefined {
117
+ let lastItem: {item: T, madeAt: number} | undefined;
118
+
119
+ for (const [sessionID, items] of Object.entries(this.items)) {
120
+ if (sessionID.startsWith(account)) {
121
+ if (items.length > 0) {
122
+ const lastItemOfSession = items[items.length - 1]!;
123
+ if (!lastItem || lastItemOfSession.madeAt > lastItem.madeAt) {
124
+ lastItem = lastItemOfSession;
125
+ }
126
+ }
127
+ }
128
+ }
129
+
130
+ return lastItem?.item;
131
+ }
132
+
133
+ getLastItemFromMe(): T | undefined {
134
+ const myAccountID = this.core.node.account.id;
135
+ if (!isAccountID(myAccountID)) return undefined;
136
+ return this.getLastItemFrom(myAccountID);
91
137
  }
92
138
 
93
139
  toJSON(): {
94
140
  [key: SessionID]: T[];
95
141
  } {
96
- return this.items;
142
+ return Object.fromEntries(Object.entries(this.items).map(([sessionID, items]) =>
143
+ [sessionID, items.map(item => item.item)]
144
+ ));
97
145
  }
98
146
 
99
147
  subscribe(listener: (coMap: CoStream<T, Meta>) => void): () => void {
@@ -121,10 +169,10 @@ export class BinaryCoStream<
121
169
  {
122
170
  id!: CoID<BinaryCoStream<Meta>>;
123
171
 
124
- getBinaryChunks():
172
+ getBinaryChunks(allowUnfinished?: boolean):
125
173
  | (BinaryChunkInfo & { chunks: Uint8Array[]; finished: boolean })
126
174
  | undefined {
127
- const before = performance.now();
175
+ // const before = performance.now();
128
176
  const items = this.getSingleStream();
129
177
 
130
178
  if (!items) return;
@@ -136,10 +184,14 @@ export class BinaryCoStream<
136
184
  return;
137
185
  }
138
186
 
187
+ const end = items[items.length - 1];
188
+
189
+ if (end?.type !== "end" && !allowUnfinished) return;
190
+
139
191
  const chunks: Uint8Array[] = [];
140
192
 
141
193
  let finished = false;
142
- let totalLength = 0;
194
+ // let totalLength = 0;
143
195
 
144
196
  for (const item of items.slice(1)) {
145
197
  if (item.type === "end") {
@@ -155,15 +207,15 @@ export class BinaryCoStream<
155
207
  const chunk = base64URLtoBytes(
156
208
  item.chunk.slice(binary_U_prefixLength)
157
209
  );
158
- totalLength += chunk.length;
210
+ // totalLength += chunk.length;
159
211
  chunks.push(chunk);
160
212
  }
161
213
 
162
- const after = performance.now();
163
- console.log(
164
- "getBinaryChunks bandwidth in MB/s",
165
- (1000 * totalLength) / (after - before) / (1024 * 1024)
166
- );
214
+ // const after = performance.now();
215
+ // console.log(
216
+ // "getBinaryChunks bandwidth in MB/s",
217
+ // (1000 * totalLength) / (after - before) / (1024 * 1024)
218
+ // );
167
219
 
168
220
  return {
169
221
  mimeType: start.mimeType,
@@ -238,7 +290,7 @@ export class WriteableBinaryCoStream<
238
290
  chunk: Uint8Array,
239
291
  privacy: "private" | "trusting" = "private"
240
292
  ) {
241
- const before = performance.now();
293
+ // const before = performance.now();
242
294
  this.push(
243
295
  {
244
296
  type: "chunk",
@@ -246,11 +298,11 @@ export class WriteableBinaryCoStream<
246
298
  } satisfies BinaryStreamChunk,
247
299
  privacy
248
300
  );
249
- const after = performance.now();
250
- console.log(
251
- "pushBinaryStreamChunk bandwidth in MB/s",
252
- (1000 * chunk.length) / (after - before) / (1024 * 1024)
253
- );
301
+ // const after = performance.now();
302
+ // console.log(
303
+ // "pushBinaryStreamChunk bandwidth in MB/s",
304
+ // (1000 * chunk.length) / (after - before) / (1024 * 1024)
305
+ // );
254
306
  }
255
307
 
256
308
  endBinaryStream(privacy: "private" | "trusting" = "private") {