cojson 0.4.13 → 0.5.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.
@@ -1,4 +1,3 @@
1
- import { randomBytes } from "@noble/hashes/utils";
2
1
  import { AnyCoValue, CoValue } from "./coValue.js";
3
2
  import {
4
3
  Encrypted,
@@ -28,10 +27,7 @@ import { Group } from "./coValues/group.js";
28
27
  import { LocalNode } from "./localNode.js";
29
28
  import { CoValueKnownState, NewContentMessage } from "./sync.js";
30
29
  import { AgentID, RawCoID, SessionID, TransactionID } from "./ids.js";
31
- import {
32
- AccountID,
33
- GeneralizedControlledAccount,
34
- } from "./coValues/account.js";
30
+ import { AccountID, GeneralizedControlledAccount } from "./coValues/account.js";
35
31
  import { Stringified, parseJSON, stableStringify } from "./jsonStringify.js";
36
32
  import { coreToCoValue } from "./coreToCoValue.js";
37
33
  import { expectGroup } from "./typeUtils/expectGroup.js";
@@ -54,7 +50,10 @@ export function idforHeader(header: CoValueHeader): RawCoID {
54
50
  }
55
51
 
56
52
  export function newRandomSessionID(accountID: AccountID | AgentID): SessionID {
57
- return `${accountID}_session_z${base58.encode(randomBytes(8))}`;
53
+ return `${accountID}_session_z${base58.encode(
54
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
55
+ (globalThis as any).crypto.getRandomValues(new Uint8Array(8))
56
+ )}`;
58
57
  }
59
58
 
60
59
  type SessionLog = {
@@ -95,23 +94,25 @@ export class CoValueCore {
95
94
  id: RawCoID;
96
95
  node: LocalNode;
97
96
  header: CoValueHeader;
98
- _sessions: { [key: SessionID]: SessionLog };
97
+ _sessionLogs: Map<SessionID, SessionLog>;
99
98
  _cachedContent?: CoValue;
100
99
  listeners: Set<(content?: CoValue) => void> = new Set();
101
100
  _decryptionCache: {
102
- [key: Encrypted<JsonValue[], JsonValue>]:
103
- | JsonValue[]
104
- | undefined;
101
+ [key: Encrypted<JsonValue[], JsonValue>]: JsonValue[] | undefined;
105
102
  } = {};
103
+ currentlyAsyncApplyingTxDone?: Promise<void>;
104
+ _cachedKnownState?: CoValueKnownState;
105
+ _cachedDependentOn?: RawCoID[];
106
+ _cachedNewContentSinceEmpty?: NewContentMessage[] | undefined;
106
107
 
107
108
  constructor(
108
109
  header: CoValueHeader,
109
110
  node: LocalNode,
110
- internalInitSessions: { [key: SessionID]: SessionLog } = {}
111
+ internalInitSessions: Map<SessionID, SessionLog> = new Map()
111
112
  ) {
112
113
  this.id = idforHeader(header);
113
114
  this.header = header;
114
- this._sessions = internalInitSessions;
115
+ this._sessionLogs = internalInitSessions;
115
116
  this.node = node;
116
117
 
117
118
  if (header.ruleset.type == "ownedByGroup") {
@@ -127,8 +128,8 @@ export class CoValueCore {
127
128
  }
128
129
  }
129
130
 
130
- get sessions(): Readonly<{ [key: SessionID]: SessionLog }> {
131
- return this._sessions;
131
+ get sessionLogs(): Map<SessionID, SessionLog> {
132
+ return this._sessionLogs;
132
133
  }
133
134
 
134
135
  testWithDifferentAccount(
@@ -144,11 +145,22 @@ export class CoValueCore {
144
145
  }
145
146
 
146
147
  knownState(): CoValueKnownState {
148
+ if (this._cachedKnownState) {
149
+ return this._cachedKnownState;
150
+ } else {
151
+ const knownState = this.knownStateUncached();
152
+ this._cachedKnownState = knownState;
153
+ return knownState;
154
+ }
155
+ }
156
+
157
+ /** @internal */
158
+ knownStateUncached(): CoValueKnownState {
147
159
  return {
148
160
  id: this.id,
149
161
  header: true,
150
162
  sessions: Object.fromEntries(
151
- Object.entries(this.sessions).map(([k, v]) => [
163
+ [...this.sessionLogs.entries()].map(([k, v]) => [
152
164
  k,
153
165
  v.transactions.length,
154
166
  ])
@@ -172,7 +184,7 @@ export class CoValueCore {
172
184
 
173
185
  return {
174
186
  sessionID,
175
- txIndex: this.sessions[sessionID]?.transactions.length || 0,
187
+ txIndex: this.sessionLogs.get(sessionID)?.transactions.length || 0,
176
188
  };
177
189
  }
178
190
 
@@ -250,8 +262,17 @@ export class CoValueCore {
250
262
  givenExpectedNewHash: Hash | undefined,
251
263
  newSignature: Signature
252
264
  ): Promise<boolean> {
265
+ if (this.currentlyAsyncApplyingTxDone) {
266
+ await this.currentlyAsyncApplyingTxDone;
267
+ }
268
+ let resolveDone!: () => void;
269
+
270
+ this.currentlyAsyncApplyingTxDone = new Promise((resolve) => {
271
+ resolveDone = resolve;
272
+ });
273
+
253
274
  const signerID = getAgentSignerID(
254
- this.node.resolveAccountAgent(
275
+ await this.node.resolveAccountAgentAsync(
255
276
  accountOrAgentIDfromSessionID(sessionID),
256
277
  "Expected to know signer of transaction"
257
278
  )
@@ -262,10 +283,11 @@ export class CoValueCore {
262
283
  "Unknown agent",
263
284
  accountOrAgentIDfromSessionID(sessionID)
264
285
  );
286
+ resolveDone();
265
287
  return false;
266
288
  }
267
289
 
268
- const nTxBefore = this.sessions[sessionID]?.transactions.length ?? 0;
290
+ const nTxBefore = this.sessionLogs.get(sessionID)?.transactions.length ?? 0;
269
291
 
270
292
  // const beforeHash = performance.now();
271
293
  const { expectedNewHash, newStreamingHash } =
@@ -276,7 +298,7 @@ export class CoValueCore {
276
298
  // afterHash - beforeHash
277
299
  // );
278
300
 
279
- const nTxAfter = this.sessions[sessionID]?.transactions.length ?? 0;
301
+ const nTxAfter = this.sessionLogs.get(sessionID)?.transactions.length ?? 0;
280
302
 
281
303
  if (nTxAfter !== nTxBefore) {
282
304
  const newTransactionLengthBefore = newTransactions.length;
@@ -294,6 +316,7 @@ export class CoValueCore {
294
316
  expectedNewHash,
295
317
  givenExpectedNewHash,
296
318
  });
319
+ resolveDone();
297
320
  return false;
298
321
  }
299
322
 
@@ -306,6 +329,7 @@ export class CoValueCore {
306
329
  expectedNewHash,
307
330
  signerID
308
331
  );
332
+ resolveDone();
309
333
  return false;
310
334
  }
311
335
  // const afterVerify = performance.now();
@@ -322,6 +346,7 @@ export class CoValueCore {
322
346
  newStreamingHash
323
347
  );
324
348
 
349
+ resolveDone();
325
350
  return true;
326
351
  }
327
352
 
@@ -332,10 +357,10 @@ export class CoValueCore {
332
357
  expectedNewHash: Hash,
333
358
  newStreamingHash: StreamingHash
334
359
  ) {
335
- const transactions = this.sessions[sessionID]?.transactions ?? [];
360
+ const transactions = this.sessionLogs.get(sessionID)?.transactions ?? [];
336
361
  transactions.push(...newTransactions);
337
362
 
338
- const signatureAfter = this.sessions[sessionID]?.signatureAfter ?? {};
363
+ const signatureAfter = this.sessionLogs.get(sessionID)?.signatureAfter ?? {};
339
364
 
340
365
  const lastInbetweenSignatureIdx = Object.keys(signatureAfter).reduce(
341
366
  (max, idx) => (parseInt(idx) > max ? parseInt(idx) : max),
@@ -363,15 +388,18 @@ export class CoValueCore {
363
388
  signatureAfter[transactions.length - 1] = newSignature;
364
389
  }
365
390
 
366
- this._sessions[sessionID] = {
391
+ this._sessionLogs.set(sessionID, {
367
392
  transactions,
368
393
  lastHash: expectedNewHash,
369
394
  streamingHash: newStreamingHash,
370
395
  lastSignature: newSignature,
371
396
  signatureAfter: signatureAfter,
372
- };
397
+ });
373
398
 
374
399
  this._cachedContent = undefined;
400
+ this._cachedKnownState = undefined;
401
+ this._cachedDependentOn = undefined;
402
+ this._cachedNewContentSinceEmpty = undefined;
375
403
 
376
404
  if (this.listeners.size > 0) {
377
405
  const content = this.getCurrentContent();
@@ -395,7 +423,7 @@ export class CoValueCore {
395
423
  newTransactions: Transaction[]
396
424
  ): { expectedNewHash: Hash; newStreamingHash: StreamingHash } {
397
425
  const streamingHash =
398
- this.sessions[sessionID]?.streamingHash.clone() ??
426
+ this.sessionLogs.get(sessionID)?.streamingHash.clone() ??
399
427
  new StreamingHash();
400
428
  for (const transaction of newTransactions) {
401
429
  streamingHash.update(transaction);
@@ -414,7 +442,7 @@ export class CoValueCore {
414
442
  newTransactions: Transaction[]
415
443
  ): Promise<{ expectedNewHash: Hash; newStreamingHash: StreamingHash }> {
416
444
  const streamingHash =
417
- this.sessions[sessionID]?.streamingHash.clone() ??
445
+ this.sessionLogs.get(sessionID)?.streamingHash.clone() ??
418
446
  new StreamingHash();
419
447
  let before = performance.now();
420
448
  for (const transaction of newTransactions) {
@@ -552,8 +580,9 @@ export class CoValueCore {
552
580
  in: this.id,
553
581
  tx: txID,
554
582
  }
555
- )
556
- decrytedChanges = decryptedString && parseJSON(decryptedString);
583
+ );
584
+ decrytedChanges =
585
+ decryptedString && parseJSON(decryptedString);
557
586
  this._decryptionCache[tx.encryptedChanges] =
558
587
  decrytedChanges;
559
588
  }
@@ -725,12 +754,18 @@ export class CoValueCore {
725
754
  }
726
755
 
727
756
  getTx(txID: TransactionID): Transaction | undefined {
728
- return this.sessions[txID.sessionID]?.transactions[txID.txIndex];
757
+ return this.sessionLogs.get(txID.sessionID)?.transactions[txID.txIndex];
729
758
  }
730
759
 
731
760
  newContentSince(
732
761
  knownState: CoValueKnownState | undefined
733
762
  ): NewContentMessage[] | undefined {
763
+ const isKnownStateEmpty = !knownState?.header && !knownState?.sessions;
764
+
765
+ if (isKnownStateEmpty && this._cachedNewContentSinceEmpty) {
766
+ return this._cachedNewContentSinceEmpty;
767
+ }
768
+
734
769
  let currentPiece: NewContentMessage = {
735
770
  action: "content",
736
771
  id: this.id,
@@ -740,44 +775,55 @@ export class CoValueCore {
740
775
 
741
776
  const pieces = [currentPiece];
742
777
 
743
- const sentState: CoValueKnownState["sessions"] = {
744
- ...knownState?.sessions,
745
- };
778
+ const sentState: CoValueKnownState["sessions"] = {};
746
779
 
747
- let newTxsWereAdded = true;
748
780
  let pieceSize = 0;
749
- while (newTxsWereAdded) {
750
- newTxsWereAdded = false;
751
-
752
- for (const [sessionID, log] of Object.entries(this.sessions) as [
753
- SessionID,
754
- SessionLog
755
- ][]) {
756
- const nextKnownSignatureIdx = Object.keys(log.signatureAfter)
757
- .map(Number)
758
- .sort((a, b) => a - b)
759
- .find((idx) => idx >= (sentState[sessionID] ?? -1));
760
-
761
- const txsToAdd = log.transactions.slice(
762
- sentState[sessionID] ?? 0,
763
- nextKnownSignatureIdx === undefined
764
- ? undefined
765
- : nextKnownSignatureIdx + 1
781
+
782
+ let sessionsTodoAgain: Set<SessionID> | undefined | "first" = "first";
783
+
784
+ while (sessionsTodoAgain === "first" || (sessionsTodoAgain?.size || 0 > 0)) {
785
+ if (sessionsTodoAgain === "first") {
786
+ sessionsTodoAgain = undefined;
787
+ }
788
+ const sessionsTodo = sessionsTodoAgain ?? this.sessionLogs.keys();
789
+
790
+ for (const sessionIDKey of sessionsTodo) {
791
+ const sessionID = sessionIDKey as SessionID;
792
+ const log = this.sessionLogs.get(sessionID)!;
793
+ const knownStateForSessionID = knownState?.sessions[sessionID];
794
+ const sentStateForSessionID = sentState[sessionID];
795
+ const nextKnownSignatureIdx = getNextKnownSignatureIdx(
796
+ log,
797
+ knownStateForSessionID,
798
+ sentStateForSessionID
766
799
  );
767
800
 
768
- if (txsToAdd.length === 0) continue;
801
+ const firstNewTxIdx = sentStateForSessionID ?? knownStateForSessionID ?? 0;
802
+ const afterLastNewTxIdx = nextKnownSignatureIdx === undefined
803
+ ? log.transactions.length
804
+ : nextKnownSignatureIdx + 1;
769
805
 
770
- newTxsWereAdded = true;
806
+ const nNewTx = Math.max(0, afterLastNewTxIdx - firstNewTxIdx);
807
+
808
+ if (nNewTx === 0) {
809
+ sessionsTodoAgain?.delete(sessionID);
810
+ continue;
811
+ }
812
+
813
+ if (afterLastNewTxIdx < log.transactions.length) {
814
+ if (!sessionsTodoAgain) {
815
+ sessionsTodoAgain = new Set();
816
+ }
817
+ sessionsTodoAgain.add(sessionID);
818
+ }
771
819
 
772
820
  const oldPieceSize = pieceSize;
773
- pieceSize += txsToAdd.reduce(
774
- (sum, tx) =>
775
- sum +
776
- (tx.privacy === "private"
777
- ? tx.encryptedChanges.length
778
- : tx.changes.length),
779
- 0
780
- );
821
+ for (let txIdx = firstNewTxIdx; txIdx < afterLastNewTxIdx; txIdx++) {
822
+ const tx = log.transactions[txIdx]!;
823
+ pieceSize += (tx.privacy === "private"
824
+ ? tx.encryptedChanges.length
825
+ : tx.changes.length);
826
+ }
781
827
 
782
828
  if (pieceSize >= MAX_RECOMMENDED_TX_SIZE) {
783
829
  currentPiece = {
@@ -793,21 +839,26 @@ export class CoValueCore {
793
839
  let sessionEntry = currentPiece.new[sessionID];
794
840
  if (!sessionEntry) {
795
841
  sessionEntry = {
796
- after: sentState[sessionID] ?? 0,
842
+ after: sentStateForSessionID ?? knownStateForSessionID ?? 0,
797
843
  newTransactions: [],
798
844
  lastSignature: "WILL_BE_REPLACED" as Signature,
799
845
  };
800
846
  currentPiece.new[sessionID] = sessionEntry;
801
847
  }
802
848
 
803
- sessionEntry.newTransactions.push(...txsToAdd);
849
+ for (let txIdx = firstNewTxIdx; txIdx < afterLastNewTxIdx; txIdx++) {
850
+ const tx = log.transactions[txIdx]!;
851
+ sessionEntry.newTransactions.push(tx);
852
+ }
853
+
854
+
804
855
  sessionEntry.lastSignature =
805
856
  nextKnownSignatureIdx === undefined
806
857
  ? log.lastSignature!
807
858
  : log.signatureAfter[nextKnownSignatureIdx]!;
808
859
 
809
860
  sentState[sessionID] =
810
- (sentState[sessionID] || 0) + txsToAdd.length;
861
+ (sentStateForSessionID ?? knownStateForSessionID ?? 0) + nNewTx;
811
862
  }
812
863
  }
813
864
 
@@ -819,10 +870,25 @@ export class CoValueCore {
819
870
  return undefined;
820
871
  }
821
872
 
873
+ if (isKnownStateEmpty) {
874
+ this._cachedNewContentSinceEmpty = piecesWithContent;
875
+ }
876
+
822
877
  return piecesWithContent;
823
878
  }
824
879
 
825
880
  getDependedOnCoValues(): RawCoID[] {
881
+ if (this._cachedDependentOn) {
882
+ return this._cachedDependentOn;
883
+ } else {
884
+ const dependentOn = this.getDependedOnCoValuesUncached();
885
+ this._cachedDependentOn = dependentOn;
886
+ return dependentOn;
887
+ }
888
+ }
889
+
890
+ /** @internal */
891
+ getDependedOnCoValuesUncached(): RawCoID[] {
826
892
  return this.header.ruleset.type === "group"
827
893
  ? expectGroup(this.getCurrentContent())
828
894
  .keys()
@@ -831,7 +897,7 @@ export class CoValueCore {
831
897
  ? [
832
898
  this.header.ruleset.group,
833
899
  ...new Set(
834
- Object.keys(this._sessions)
900
+ [...this.sessionLogs.keys()]
835
901
  .map((sessionID) =>
836
902
  accountOrAgentIDfromSessionID(
837
903
  sessionID as SessionID
@@ -846,3 +912,14 @@ export class CoValueCore {
846
912
  : [];
847
913
  }
848
914
  }
915
+
916
+ function getNextKnownSignatureIdx(
917
+ log: SessionLog,
918
+ knownStateForSessionID?: number,
919
+ sentStateForSessionID?: number,
920
+ ) {
921
+ return Object.keys(log.signatureAfter)
922
+ .map(Number)
923
+ .sort((a, b) => a - b)
924
+ .find((idx) => idx >= (sentStateForSessionID ?? knownStateForSessionID ?? -1));
925
+ }
@@ -75,6 +75,13 @@ export class CoListView<
75
75
  /** @category 6. Meta */
76
76
  readonly _item!: Item;
77
77
 
78
+ /** @internal */
79
+ _cachedEntries?: {
80
+ value: Item;
81
+ madeAt: number;
82
+ opID: OpID;
83
+ }[];
84
+
78
85
  /** @internal */
79
86
  constructor(core: CoValueCore) {
80
87
  this.id = core.id as CoID<this>;
@@ -126,10 +133,11 @@ export class CoListView<
126
133
  change.before.txIndex
127
134
  ]?.[change.before.changeIdx];
128
135
  if (!beforeEntry) {
129
- throw new Error(
130
- "Not yet implemented: insertion before missing op " +
131
- change.before
132
- );
136
+ // console.error(
137
+ // "Insertion before missing op " +
138
+ // change.before
139
+ // );
140
+ continue;
133
141
  }
134
142
  beforeEntry.predecessors.splice(0, 0, {
135
143
  ...txID,
@@ -148,10 +156,10 @@ export class CoListView<
148
156
  change.after.txIndex
149
157
  ]?.[change.after.changeIdx];
150
158
  if (!afterEntry) {
151
- throw new Error(
152
- "Not yet implemented: insertion after missing op " +
153
- change.after
154
- );
159
+ // console.error(
160
+ // "Insertion after missing op " + change.after
161
+ // );
162
+ continue;
155
163
  }
156
164
  afterEntry.successors.push({
157
165
  ...txID,
@@ -241,6 +249,20 @@ export class CoListView<
241
249
  value: Item;
242
250
  madeAt: number;
243
251
  opID: OpID;
252
+ }[] {
253
+ if (this._cachedEntries) {
254
+ return this._cachedEntries;
255
+ }
256
+ const arr = this.entriesUncached();
257
+ this._cachedEntries = arr;
258
+ return arr;
259
+ }
260
+
261
+ /** @internal */
262
+ entriesUncached(): {
263
+ value: Item;
264
+ madeAt: number;
265
+ opID: OpID;
244
266
  }[] {
245
267
  const arr: {
246
268
  value: Item;
@@ -542,6 +564,7 @@ export class MutableCoList<
542
564
  this.beforeEnd = listAfter.beforeEnd;
543
565
  this.insertions = listAfter.insertions;
544
566
  this.deletionsByInsertion = listAfter.deletionsByInsertion;
567
+ this._cachedEntries = undefined;
545
568
  }
546
569
 
547
570
  /** Prepends `item` before the item currently at index `before`.
@@ -567,6 +590,7 @@ export class MutableCoList<
567
590
  this.beforeEnd = listAfter.beforeEnd;
568
591
  this.insertions = listAfter.insertions;
569
592
  this.deletionsByInsertion = listAfter.deletionsByInsertion;
593
+ this._cachedEntries = undefined;
570
594
  }
571
595
 
572
596
  /** Deletes the item at index `at` from the list.
@@ -587,5 +611,6 @@ export class MutableCoList<
587
611
  this.beforeEnd = listAfter.beforeEnd;
588
612
  this.insertions = listAfter.insertions;
589
613
  this.deletionsByInsertion = listAfter.deletionsByInsertion;
614
+ this._cachedEntries = undefined;
590
615
  }
591
616
  }