cojson 0.7.0-alpha.29 → 0.7.0-alpha.36

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.
@@ -250,7 +250,8 @@ export class CoValueCore {
250
250
  newTransactions,
251
251
  newSignature,
252
252
  expectedNewHash,
253
- newStreamingHash
253
+ newStreamingHash,
254
+ "immediate"
254
255
  );
255
256
 
256
257
  return true;
@@ -287,7 +288,8 @@ export class CoValueCore {
287
288
  return false;
288
289
  }
289
290
 
290
- const nTxBefore = this.sessionLogs.get(sessionID)?.transactions.length ?? 0;
291
+ const nTxBefore =
292
+ this.sessionLogs.get(sessionID)?.transactions.length ?? 0;
291
293
 
292
294
  // const beforeHash = performance.now();
293
295
  const { expectedNewHash, newStreamingHash } =
@@ -298,7 +300,8 @@ export class CoValueCore {
298
300
  // afterHash - beforeHash
299
301
  // );
300
302
 
301
- const nTxAfter = this.sessionLogs.get(sessionID)?.transactions.length ?? 0;
303
+ const nTxAfter =
304
+ this.sessionLogs.get(sessionID)?.transactions.length ?? 0;
302
305
 
303
306
  if (nTxAfter !== nTxBefore) {
304
307
  const newTransactionLengthBefore = newTransactions.length;
@@ -320,7 +323,7 @@ export class CoValueCore {
320
323
  return false;
321
324
  }
322
325
 
323
- // const beforeVerify = performance.now();
326
+ performance.mark("verifyStart" + this.id);
324
327
  if (!verify(newSignature, expectedNewHash, signerID)) {
325
328
  console.warn(
326
329
  "Invalid signature in",
@@ -332,18 +335,20 @@ export class CoValueCore {
332
335
  resolveDone();
333
336
  return false;
334
337
  }
335
- // const afterVerify = performance.now();
336
- // console.log(
337
- // "Verify took",
338
- // afterVerify - beforeVerify
339
- // );
338
+ performance.mark("verifyEnd" + this.id);
339
+ performance.measure(
340
+ "verify" + this.id,
341
+ "verifyStart" + this.id,
342
+ "verifyEnd" + this.id
343
+ );
340
344
 
341
345
  this.doAddTransactions(
342
346
  sessionID,
343
347
  newTransactions,
344
348
  newSignature,
345
349
  expectedNewHash,
346
- newStreamingHash
350
+ newStreamingHash,
351
+ "deferred"
347
352
  );
348
353
 
349
354
  resolveDone();
@@ -355,12 +360,15 @@ export class CoValueCore {
355
360
  newTransactions: Transaction[],
356
361
  newSignature: Signature,
357
362
  expectedNewHash: Hash,
358
- newStreamingHash: StreamingHash
363
+ newStreamingHash: StreamingHash,
364
+ notifyMode: "immediate" | "deferred"
359
365
  ) {
360
- const transactions = this.sessionLogs.get(sessionID)?.transactions ?? [];
366
+ const transactions =
367
+ this.sessionLogs.get(sessionID)?.transactions ?? [];
361
368
  transactions.push(...newTransactions);
362
369
 
363
- const signatureAfter = this.sessionLogs.get(sessionID)?.signatureAfter ?? {};
370
+ const signatureAfter =
371
+ this.sessionLogs.get(sessionID)?.signatureAfter ?? {};
364
372
 
365
373
  const lastInbetweenSignatureIdx = Object.keys(signatureAfter).reduce(
366
374
  (max, idx) => (parseInt(idx) > max ? parseInt(idx) : max),
@@ -402,13 +410,33 @@ export class CoValueCore {
402
410
  this._cachedNewContentSinceEmpty = undefined;
403
411
 
404
412
  if (this.listeners.size > 0) {
405
- const content = this.getCurrentContent();
406
- for (const listener of this.listeners) {
407
- listener(content);
413
+ if (notifyMode === "immediate") {
414
+ const content = this.getCurrentContent();
415
+ for (const listener of this.listeners) {
416
+ listener(content);
417
+ }
418
+ } else {
419
+ if (!this.nextDeferredNotify) {
420
+ this.nextDeferredNotify = new Promise((resolve) => {
421
+ setTimeout(() => {
422
+ this.nextDeferredNotify = undefined;
423
+ this.deferredUpdates = 0;
424
+ const content = this.getCurrentContent();
425
+ for (const listener of this.listeners) {
426
+ listener(content);
427
+ }
428
+ resolve();
429
+ }, 0);
430
+ });
431
+ }
432
+ this.deferredUpdates++;
408
433
  }
409
434
  }
410
435
  }
411
436
 
437
+ deferredUpdates = 0;
438
+ nextDeferredNotify: Promise<void> | undefined;
439
+
412
440
  subscribe(listener: (content?: RawCoValue) => void): () => void {
413
441
  this.listeners.add(listener);
414
442
  listener(this.getCurrentContent());
@@ -533,7 +561,9 @@ export class CoValueCore {
533
561
  return success;
534
562
  }
535
563
 
536
- getCurrentContent(options?: { ignorePrivateTransactions: true }): RawCoValue {
564
+ getCurrentContent(options?: {
565
+ ignorePrivateTransactions: true;
566
+ }): RawCoValue {
537
567
  if (!options?.ignorePrivateTransactions && this._cachedContent) {
538
568
  return this._cachedContent;
539
569
  }
@@ -781,7 +811,11 @@ export class CoValueCore {
781
811
 
782
812
  let sessionsTodoAgain: Set<SessionID> | undefined | "first" = "first";
783
813
 
784
- while (sessionsTodoAgain === "first" || (sessionsTodoAgain?.size || 0 > 0)) {
814
+ while (
815
+ sessionsTodoAgain === "first" ||
816
+ sessionsTodoAgain?.size ||
817
+ 0 > 0
818
+ ) {
785
819
  if (sessionsTodoAgain === "first") {
786
820
  sessionsTodoAgain = undefined;
787
821
  }
@@ -798,10 +832,12 @@ export class CoValueCore {
798
832
  sentStateForSessionID
799
833
  );
800
834
 
801
- const firstNewTxIdx = sentStateForSessionID ?? knownStateForSessionID ?? 0;
802
- const afterLastNewTxIdx = nextKnownSignatureIdx === undefined
803
- ? log.transactions.length
804
- : nextKnownSignatureIdx + 1;
835
+ const firstNewTxIdx =
836
+ sentStateForSessionID ?? knownStateForSessionID ?? 0;
837
+ const afterLastNewTxIdx =
838
+ nextKnownSignatureIdx === undefined
839
+ ? log.transactions.length
840
+ : nextKnownSignatureIdx + 1;
805
841
 
806
842
  const nNewTx = Math.max(0, afterLastNewTxIdx - firstNewTxIdx);
807
843
 
@@ -818,11 +854,16 @@ export class CoValueCore {
818
854
  }
819
855
 
820
856
  const oldPieceSize = pieceSize;
821
- for (let txIdx = firstNewTxIdx; txIdx < afterLastNewTxIdx; txIdx++) {
857
+ for (
858
+ let txIdx = firstNewTxIdx;
859
+ txIdx < afterLastNewTxIdx;
860
+ txIdx++
861
+ ) {
822
862
  const tx = log.transactions[txIdx]!;
823
- pieceSize += (tx.privacy === "private"
824
- ? tx.encryptedChanges.length
825
- : tx.changes.length);
863
+ pieceSize +=
864
+ tx.privacy === "private"
865
+ ? tx.encryptedChanges.length
866
+ : tx.changes.length;
826
867
  }
827
868
 
828
869
  if (pieceSize >= MAX_RECOMMENDED_TX_SIZE) {
@@ -839,26 +880,33 @@ export class CoValueCore {
839
880
  let sessionEntry = currentPiece.new[sessionID];
840
881
  if (!sessionEntry) {
841
882
  sessionEntry = {
842
- after: sentStateForSessionID ?? knownStateForSessionID ?? 0,
883
+ after:
884
+ sentStateForSessionID ??
885
+ knownStateForSessionID ??
886
+ 0,
843
887
  newTransactions: [],
844
888
  lastSignature: "WILL_BE_REPLACED" as Signature,
845
889
  };
846
890
  currentPiece.new[sessionID] = sessionEntry;
847
891
  }
848
892
 
849
- for (let txIdx = firstNewTxIdx; txIdx < afterLastNewTxIdx; txIdx++) {
893
+ for (
894
+ let txIdx = firstNewTxIdx;
895
+ txIdx < afterLastNewTxIdx;
896
+ txIdx++
897
+ ) {
850
898
  const tx = log.transactions[txIdx]!;
851
899
  sessionEntry.newTransactions.push(tx);
852
900
  }
853
901
 
854
-
855
902
  sessionEntry.lastSignature =
856
903
  nextKnownSignatureIdx === undefined
857
904
  ? log.lastSignature!
858
905
  : log.signatureAfter[nextKnownSignatureIdx]!;
859
906
 
860
907
  sentState[sessionID] =
861
- (sentStateForSessionID ?? knownStateForSessionID ?? 0) + nNewTx;
908
+ (sentStateForSessionID ?? knownStateForSessionID ?? 0) +
909
+ nNewTx;
862
910
  }
863
911
  }
864
912
 
@@ -894,32 +942,35 @@ export class CoValueCore {
894
942
  .keys()
895
943
  .filter((k): k is AccountID => k.startsWith("co_"))
896
944
  : this.header.ruleset.type === "ownedByGroup"
897
- ? [
898
- this.header.ruleset.group,
899
- ...new Set(
900
- [...this.sessionLogs.keys()]
901
- .map((sessionID) =>
902
- accountOrAgentIDfromSessionID(
903
- sessionID as SessionID
904
- )
905
- )
906
- .filter(
907
- (session): session is AccountID =>
908
- isAccountID(session) && session !== this.id
909
- )
910
- ),
911
- ]
912
- : [];
945
+ ? [
946
+ this.header.ruleset.group,
947
+ ...new Set(
948
+ [...this.sessionLogs.keys()]
949
+ .map((sessionID) =>
950
+ accountOrAgentIDfromSessionID(
951
+ sessionID as SessionID
952
+ )
953
+ )
954
+ .filter(
955
+ (session): session is AccountID =>
956
+ isAccountID(session) && session !== this.id
957
+ )
958
+ ),
959
+ ]
960
+ : [];
913
961
  }
914
962
  }
915
963
 
916
964
  function getNextKnownSignatureIdx(
917
965
  log: SessionLog,
918
966
  knownStateForSessionID?: number,
919
- sentStateForSessionID?: number,
967
+ sentStateForSessionID?: number
920
968
  ) {
921
969
  return Object.keys(log.signatureAfter)
922
970
  .map(Number)
923
971
  .sort((a, b) => a - b)
924
- .find((idx) => idx >= (sentStateForSessionID ?? knownStateForSessionID ?? -1));
972
+ .find(
973
+ (idx) =>
974
+ idx >= (sentStateForSessionID ?? knownStateForSessionID ?? -1)
975
+ );
925
976
  }
package/src/crypto.ts CHANGED
@@ -276,7 +276,7 @@ export class StreamingHash {
276
276
  this.state = fromClone || blake3Instance.init().save();
277
277
  }
278
278
 
279
- update(value: JsonValue) {
279
+ update(value: JsonValue): Uint8Array {
280
280
  const encoded = textEncoder.encode(stableStringify(value));
281
281
  // const before = performance.now();
282
282
  this.state = blake3incrementalUpdateSLOW_WITH_DEVTOOLS(
@@ -285,6 +285,7 @@ export class StreamingHash {
285
285
  );
286
286
  // const after = performance.now();
287
287
  // console.log(`Hashing throughput in MB/s`, 1000 * (encoded.length / (after - before)) / (1024 * 1024));
288
+ return encoded;
288
289
  }
289
290
 
290
291
  digest(): Hash {
package/src/index.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  secretSeedLength,
21
21
  shortHashLength,
22
22
  cryptoReady,
23
+ StreamingHash
23
24
  } from "./crypto.js";
24
25
  import { connectedPeers } from "./streamUtils.js";
25
26
  import { ControlledAgent, RawControlledAccount } from "./coValues/account.js";
@@ -56,6 +57,14 @@ import type * as Media from "./media.js";
56
57
 
57
58
  type Value = JsonValue | AnyRawCoValue;
58
59
 
60
+ import {
61
+ LSMStorage,
62
+ FSErr,
63
+ BlockFilename,
64
+ WalFilename,
65
+ } from "./storage/index.js";
66
+ import { FileSystem } from "./storage/FileSystem.js";
67
+
59
68
  /** @hidden */
60
69
  export const cojsonInternals = {
61
70
  agentSecretFromBytes,
@@ -78,6 +87,7 @@ export const cojsonInternals = {
78
87
  isAccountID,
79
88
  accountHeaderForInitialAgentSecret,
80
89
  idforHeader,
90
+ StreamingHash
81
91
  };
82
92
 
83
93
  export {
@@ -113,7 +123,12 @@ export {
113
123
  AgentSecret,
114
124
  InviteSecret,
115
125
  SyncMessage,
116
- isRawCoID
126
+ isRawCoID,
127
+ FileSystem,
128
+ LSMStorage,
129
+ FSErr,
130
+ BlockFilename,
131
+ WalFilename,
117
132
  };
118
133
 
119
134
  export type { Value };
package/src/localNode.ts CHANGED
@@ -534,7 +534,7 @@ export class LocalNode {
534
534
  );
535
535
  }
536
536
 
537
- return new RawAccount(coValue).currentAgentID();
537
+ return (coValue.getCurrentContent() as RawAccount).currentAgentID();
538
538
  }
539
539
 
540
540
  async resolveAccountAgentAsync(
@@ -569,7 +569,7 @@ export class LocalNode {
569
569
  );
570
570
  }
571
571
 
572
- return new RawAccount(coValue).currentAgentID();
572
+ return (coValue.getCurrentContent() as RawAccount).currentAgentID();
573
573
  }
574
574
 
575
575
  /**
@@ -0,0 +1,151 @@
1
+ import { Effect } from "effect";
2
+ import { CoValueChunk } from "./index.js";
3
+ import { RawCoID } from "../ids.js";
4
+ import { StreamingHash } from "../crypto.js";
5
+
6
+ export type BlockFilename =
7
+ `${string}-L${number}-H${number}.jsonl`;
8
+
9
+ export type BlockHeader = { id: RawCoID; start: number; length: number }[];
10
+
11
+ export type WalEntry = { id: RawCoID } & CoValueChunk;
12
+
13
+ export type WalFilename = `wal-${number}.jsonl`;
14
+
15
+ export type FSErr = {
16
+ type: "fileSystemError";
17
+ error: Error;
18
+ };
19
+
20
+ export interface FileSystem<WriteHandle, ReadHandle> {
21
+ createFile(filename: string): Effect.Effect<WriteHandle, FSErr>;
22
+ append(handle: WriteHandle, data: Uint8Array): Effect.Effect<void, FSErr>;
23
+ close(
24
+ handle: ReadHandle | WriteHandle,
25
+ ): Effect.Effect<void, FSErr>;
26
+ closeAndRename(
27
+ handle: WriteHandle,
28
+ filename: BlockFilename
29
+ ): Effect.Effect<void, FSErr>;
30
+ openToRead(
31
+ filename: string
32
+ ): Effect.Effect<{ handle: ReadHandle; size: number; }, FSErr>;
33
+ read(
34
+ handle: ReadHandle,
35
+ offset: number,
36
+ length: number
37
+ ): Effect.Effect<Uint8Array, FSErr>;
38
+ listFiles(): Effect.Effect<string[], FSErr>;
39
+ removeFile(filename: BlockFilename | WalFilename): Effect.Effect<void, FSErr>;
40
+ }
41
+
42
+ export const textEncoder = new TextEncoder();
43
+ export const textDecoder = new TextDecoder();
44
+
45
+ export function readChunk<RH, FS extends FileSystem<unknown, RH>>(
46
+ handle: RH,
47
+ header: { start: number; length: number; },
48
+ fs: FS
49
+ ): Effect.Effect<CoValueChunk, FSErr> {
50
+ return Effect.gen(function* ($) {
51
+ const chunkBytes = yield* $(
52
+ fs.read(handle, header.start, header.length)
53
+ );
54
+
55
+ const chunk = JSON.parse(textDecoder.decode(chunkBytes));
56
+ return chunk;
57
+ });
58
+ }
59
+
60
+ export function readHeader<RH, FS extends FileSystem<unknown, RH>>(
61
+ filename: string,
62
+ handle: RH,
63
+ size: number,
64
+ fs: FS
65
+ ): Effect.Effect<BlockHeader, FSErr> {
66
+ return Effect.gen(function* ($) {
67
+
68
+
69
+ const headerLength = Number(filename.match(/-H(\d+)\.jsonl$/)![1]!);
70
+
71
+ const headerBytes = yield* $(
72
+ fs.read(handle, size - headerLength, headerLength)
73
+ );
74
+
75
+ const header = JSON.parse(textDecoder.decode(headerBytes));
76
+ return header;
77
+ });
78
+ }
79
+
80
+ export function writeBlock<WH, RH, FS extends FileSystem<WH, RH>>(
81
+ chunks: Map<RawCoID, CoValueChunk>,
82
+ level: number,
83
+ fs: FS
84
+ ): Effect.Effect<void, FSErr> {
85
+ if (chunks.size === 0) {
86
+ return Effect.die(new Error("No chunks to write"));
87
+ }
88
+
89
+ return Effect.gen(function* ($) {
90
+ const blockHeader: BlockHeader = [];
91
+
92
+ let offset = 0;
93
+
94
+ const file = yield* $(
95
+ fs.createFile(
96
+ "wipBlock" +
97
+ Math.random().toString(36).substring(7) +
98
+ ".tmp.jsonl"
99
+ )
100
+ );
101
+ const hash = new StreamingHash();
102
+
103
+ const chunksSortedById = Array.from(chunks).sort(([id1], [id2]) => id1.localeCompare(id2)
104
+ );
105
+
106
+ for (const [id, chunk] of chunksSortedById) {
107
+ const encodedBytes = hash.update(chunk);
108
+ const encodedBytesWithNewline = new Uint8Array(
109
+ encodedBytes.length + 1
110
+ );
111
+ encodedBytesWithNewline.set(encodedBytes);
112
+ encodedBytesWithNewline[encodedBytes.length] = 10;
113
+ yield* $(fs.append(file, encodedBytesWithNewline));
114
+ const length = encodedBytesWithNewline.length;
115
+ blockHeader.push({ id, start: offset, length });
116
+ offset += length;
117
+ }
118
+
119
+ const headerBytes = textEncoder.encode(JSON.stringify(blockHeader));
120
+ yield* $(fs.append(file, headerBytes));
121
+
122
+ console.log(
123
+ "full file",
124
+ yield* $(
125
+ fs.read(file as unknown as RH, 0, offset + headerBytes.length)
126
+ )
127
+ );
128
+
129
+ const filename: BlockFilename = `${hash.digest()}-L${level}-H${headerBytes.length}.jsonl`;
130
+ console.log("renaming to" + filename);
131
+ yield* $(fs.closeAndRename(file, filename));
132
+
133
+ console.log("Wrote block", filename, blockHeader);
134
+ });
135
+ }
136
+
137
+ export function writeToWal<WH, RH, FS extends FileSystem<WH, RH>>(
138
+ handle: WH,
139
+ fs: FS,
140
+ id: RawCoID,
141
+ chunk: CoValueChunk
142
+ ): Effect.Effect<void, FSErr> {
143
+ return Effect.gen(function* ($) {
144
+ const walEntry: WalEntry = {
145
+ id,
146
+ ...chunk,
147
+ };
148
+ const bytes = textEncoder.encode(JSON.stringify(walEntry) + "\n");
149
+ yield* $(fs.append(handle, bytes));
150
+ });
151
+ }
@@ -0,0 +1,132 @@
1
+ import { Either } from 'effect';
2
+ import { RawCoID, SessionID } from '../ids.js';
3
+ import { MAX_RECOMMENDED_TX_SIZE } from '../index.js';
4
+ import { CoValueKnownState, NewContentMessage } from "../sync.js";
5
+ import { CoValueChunk } from "./index.js";
6
+
7
+ export function contentSinceChunk(
8
+ id: RawCoID,
9
+ chunk: CoValueChunk,
10
+ known?: CoValueKnownState
11
+ ): NewContentMessage[] {
12
+ const newContentPieces: NewContentMessage[] = [];
13
+
14
+ newContentPieces.push({
15
+ id: id,
16
+ action: "content",
17
+ header: known?.header ? undefined : chunk.header,
18
+ new: {},
19
+ });
20
+
21
+ for (const [sessionID, sessionsEntry] of Object.entries(
22
+ chunk.sessionEntries
23
+ )) {
24
+ for (const entry of sessionsEntry) {
25
+ const knownStart = known?.sessions[sessionID as SessionID] || 0;
26
+
27
+ if (entry.after + entry.transactions.length <= knownStart) {
28
+ continue;
29
+ }
30
+
31
+ const actuallyNewTransactions = entry.transactions.slice(
32
+ Math.max(0, knownStart - entry.after)
33
+ );
34
+
35
+ const newAfter = entry.after +
36
+ (actuallyNewTransactions.length - entry.transactions.length);
37
+
38
+ let newContentEntry = newContentPieces[0]?.new[sessionID as SessionID];
39
+
40
+ if (!newContentEntry) {
41
+ newContentEntry = {
42
+ after: newAfter,
43
+ lastSignature: entry.lastSignature,
44
+ newTransactions: actuallyNewTransactions,
45
+ };
46
+ newContentPieces[0]!.new[sessionID as SessionID] =
47
+ newContentEntry;
48
+ } else {
49
+ newContentEntry.newTransactions.push(
50
+ ...actuallyNewTransactions
51
+ );
52
+ newContentEntry.lastSignature = entry.lastSignature;
53
+ }
54
+ }
55
+ }
56
+
57
+ return newContentPieces;
58
+ }
59
+
60
+ export function chunkToKnownState(id: RawCoID, chunk: CoValueChunk) {
61
+ const ourKnown: CoValueKnownState = {
62
+ id,
63
+ header: !!chunk.header,
64
+ sessions: {},
65
+ };
66
+
67
+ for (const [sessionID, sessionEntries] of Object.entries(
68
+ chunk.sessionEntries
69
+ )) {
70
+ for (const entry of sessionEntries) {
71
+ ourKnown.sessions[sessionID as SessionID] =
72
+ entry.after + entry.transactions.length;
73
+ }
74
+ }
75
+ return ourKnown;
76
+ }
77
+
78
+ export function mergeChunks(
79
+ chunkA: CoValueChunk,
80
+ chunkB: CoValueChunk
81
+ ): Either.Either<"nonContigous", CoValueChunk> {
82
+ const header = chunkA.header || chunkB.header;
83
+
84
+ const newSessions = { ...chunkA.sessionEntries };
85
+ for (const sessionID in chunkB.sessionEntries) {
86
+ // figure out if we can merge the chunks
87
+ const sessionEntriesA = chunkA.sessionEntries[sessionID];
88
+ const sessionEntriesB = chunkB.sessionEntries[sessionID]!;
89
+
90
+ if (!sessionEntriesA) {
91
+ newSessions[sessionID] = sessionEntriesB;
92
+ continue;
93
+ }
94
+
95
+ const lastEntryOfA = sessionEntriesA[sessionEntriesA.length - 1]!;
96
+ const firstEntryOfB = sessionEntriesB[0]!;
97
+
98
+ if (lastEntryOfA.after + lastEntryOfA.transactions.length ===
99
+ firstEntryOfB.after) {
100
+ const newEntries = [];
101
+ let bytesSinceLastSignature = 0;
102
+ for (const entry of sessionEntriesA.concat(sessionEntriesB)) {
103
+ const entryByteLength = entry.transactions.reduce(
104
+ (sum, tx) => sum +
105
+ (tx.privacy === "private"
106
+ ? tx.encryptedChanges.length
107
+ : tx.changes.length),
108
+ 0
109
+ );
110
+ if (newEntries.length === 0 ||
111
+ bytesSinceLastSignature + entryByteLength >
112
+ MAX_RECOMMENDED_TX_SIZE) {
113
+ newEntries.push({
114
+ after: entry.after,
115
+ lastSignature: entry.lastSignature,
116
+ transactions: entry.transactions,
117
+ });
118
+ bytesSinceLastSignature = 0;
119
+ } else {
120
+ const lastNewEntry = newEntries[newEntries.length - 1]!;
121
+ lastNewEntry.transactions.push(...entry.transactions);
122
+
123
+ bytesSinceLastSignature += entry.transactions.length;
124
+ }
125
+ }
126
+ } else {
127
+ return Either.right("nonContigous" as const);
128
+ }
129
+ }
130
+
131
+ return Either.left({ header, sessionEntries: newSessions });
132
+ }