@protontech/drive-sdk 0.1.2 → 0.2.0

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 (153) hide show
  1. package/dist/crypto/driveCrypto.d.ts +11 -0
  2. package/dist/crypto/driveCrypto.js +20 -7
  3. package/dist/crypto/driveCrypto.js.map +1 -1
  4. package/dist/crypto/interface.d.ts +10 -1
  5. package/dist/crypto/openPGPCrypto.d.ts +18 -2
  6. package/dist/crypto/openPGPCrypto.js +25 -6
  7. package/dist/crypto/openPGPCrypto.js.map +1 -1
  8. package/dist/diagnostic/telemetry.d.ts +1 -1
  9. package/dist/diagnostic/telemetry.js +1 -1
  10. package/dist/diagnostic/telemetry.js.map +1 -1
  11. package/dist/interface/download.d.ts +46 -0
  12. package/dist/interface/index.d.ts +2 -2
  13. package/dist/interface/index.js.map +1 -1
  14. package/dist/interface/nodes.d.ts +26 -1
  15. package/dist/interface/nodes.js.map +1 -1
  16. package/dist/interface/telemetry.d.ts +5 -2
  17. package/dist/interface/telemetry.js.map +1 -1
  18. package/dist/internal/apiService/apiService.js +1 -1
  19. package/dist/internal/apiService/apiService.js.map +1 -1
  20. package/dist/internal/apiService/driveTypes.d.ts +78 -165
  21. package/dist/internal/apiService/index.d.ts +1 -1
  22. package/dist/internal/apiService/index.js +2 -2
  23. package/dist/internal/apiService/index.js.map +1 -1
  24. package/dist/internal/apiService/transformers.d.ts +1 -1
  25. package/dist/internal/apiService/transformers.js +2 -2
  26. package/dist/internal/apiService/transformers.js.map +1 -1
  27. package/dist/internal/download/blockIndex.d.ts +11 -0
  28. package/dist/internal/download/blockIndex.js +35 -0
  29. package/dist/internal/download/blockIndex.js.map +1 -0
  30. package/dist/internal/download/blockIndex.test.d.ts +1 -0
  31. package/dist/internal/download/blockIndex.test.js +147 -0
  32. package/dist/internal/download/blockIndex.test.js.map +1 -0
  33. package/dist/internal/download/fileDownloader.d.ts +6 -2
  34. package/dist/internal/download/fileDownloader.js +83 -6
  35. package/dist/internal/download/fileDownloader.js.map +1 -1
  36. package/dist/internal/download/fileDownloader.test.js +69 -4
  37. package/dist/internal/download/fileDownloader.test.js.map +1 -1
  38. package/dist/internal/download/interface.d.ts +4 -4
  39. package/dist/internal/download/seekableStream.d.ts +80 -0
  40. package/dist/internal/download/seekableStream.js +163 -0
  41. package/dist/internal/download/seekableStream.js.map +1 -0
  42. package/dist/internal/download/seekableStream.test.d.ts +1 -0
  43. package/dist/internal/download/seekableStream.test.js +149 -0
  44. package/dist/internal/download/seekableStream.test.js.map +1 -0
  45. package/dist/internal/download/telemetry.js +1 -1
  46. package/dist/internal/download/telemetry.js.map +1 -1
  47. package/dist/internal/download/telemetry.test.js +7 -7
  48. package/dist/internal/download/telemetry.test.js.map +1 -1
  49. package/dist/internal/errors.d.ts +1 -1
  50. package/dist/internal/errors.js +7 -1
  51. package/dist/internal/errors.js.map +1 -1
  52. package/dist/internal/errors.test.js +44 -10
  53. package/dist/internal/errors.test.js.map +1 -1
  54. package/dist/internal/events/index.js +1 -1
  55. package/dist/internal/events/index.js.map +1 -1
  56. package/dist/internal/nodes/apiService.js +16 -3
  57. package/dist/internal/nodes/apiService.js.map +1 -1
  58. package/dist/internal/nodes/apiService.test.js +43 -7
  59. package/dist/internal/nodes/apiService.test.js.map +1 -1
  60. package/dist/internal/nodes/cache.js +9 -2
  61. package/dist/internal/nodes/cache.js.map +1 -1
  62. package/dist/internal/nodes/cache.test.js +6 -1
  63. package/dist/internal/nodes/cache.test.js.map +1 -1
  64. package/dist/internal/nodes/cryptoService.d.ts +4 -1
  65. package/dist/internal/nodes/cryptoService.js +66 -16
  66. package/dist/internal/nodes/cryptoService.js.map +1 -1
  67. package/dist/internal/nodes/cryptoService.test.js +129 -46
  68. package/dist/internal/nodes/cryptoService.test.js.map +1 -1
  69. package/dist/internal/nodes/extendedAttributes.d.ts +2 -1
  70. package/dist/internal/nodes/extendedAttributes.js +27 -1
  71. package/dist/internal/nodes/extendedAttributes.js.map +1 -1
  72. package/dist/internal/nodes/extendedAttributes.test.js +59 -6
  73. package/dist/internal/nodes/extendedAttributes.test.js.map +1 -1
  74. package/dist/internal/nodes/index.test.js +1 -1
  75. package/dist/internal/nodes/index.test.js.map +1 -1
  76. package/dist/internal/nodes/interface.d.ts +18 -2
  77. package/dist/internal/nodes/nodesAccess.js +11 -1
  78. package/dist/internal/nodes/nodesAccess.js.map +1 -1
  79. package/dist/internal/nodes/nodesManagement.js +1 -1
  80. package/dist/internal/nodes/nodesManagement.js.map +1 -1
  81. package/dist/internal/nodes/nodesRevisions.d.ts +4 -3
  82. package/dist/internal/nodes/nodesRevisions.js +2 -2
  83. package/dist/internal/nodes/nodesRevisions.js.map +1 -1
  84. package/dist/internal/shares/cryptoService.js +7 -4
  85. package/dist/internal/shares/cryptoService.js.map +1 -1
  86. package/dist/internal/shares/cryptoService.test.js +5 -3
  87. package/dist/internal/shares/cryptoService.test.js.map +1 -1
  88. package/dist/internal/sharing/apiService.js +5 -5
  89. package/dist/internal/sharing/apiService.js.map +1 -1
  90. package/dist/internal/sharing/cryptoService.js +8 -5
  91. package/dist/internal/sharing/cryptoService.js.map +1 -1
  92. package/dist/internal/sharing/cryptoService.test.js +7 -4
  93. package/dist/internal/sharing/cryptoService.test.js.map +1 -1
  94. package/dist/internal/upload/telemetry.js +2 -2
  95. package/dist/internal/upload/telemetry.js.map +1 -1
  96. package/dist/internal/upload/telemetry.test.js +7 -7
  97. package/dist/internal/upload/telemetry.test.js.map +1 -1
  98. package/dist/telemetry.d.ts +2 -2
  99. package/dist/telemetry.js +2 -2
  100. package/dist/telemetry.js.map +1 -1
  101. package/dist/tests/telemetry.js +1 -1
  102. package/dist/tests/telemetry.js.map +1 -1
  103. package/dist/transformers.d.ts +1 -1
  104. package/dist/transformers.js +2 -1
  105. package/dist/transformers.js.map +1 -1
  106. package/package.json +1 -1
  107. package/src/crypto/driveCrypto.ts +70 -25
  108. package/src/crypto/interface.ts +15 -0
  109. package/src/crypto/openPGPCrypto.ts +37 -5
  110. package/src/diagnostic/telemetry.ts +1 -1
  111. package/src/interface/download.ts +46 -0
  112. package/src/interface/index.ts +2 -1
  113. package/src/interface/nodes.ts +28 -1
  114. package/src/interface/telemetry.ts +6 -1
  115. package/src/internal/apiService/apiService.ts +1 -1
  116. package/src/internal/apiService/driveTypes.ts +78 -165
  117. package/src/internal/apiService/index.ts +1 -1
  118. package/src/internal/apiService/transformers.ts +1 -1
  119. package/src/internal/download/blockIndex.test.ts +158 -0
  120. package/src/internal/download/blockIndex.ts +36 -0
  121. package/src/internal/download/fileDownloader.test.ts +100 -7
  122. package/src/internal/download/fileDownloader.ts +109 -9
  123. package/src/internal/download/interface.ts +4 -4
  124. package/src/internal/download/seekableStream.test.ts +187 -0
  125. package/src/internal/download/seekableStream.ts +182 -0
  126. package/src/internal/download/telemetry.test.ts +7 -7
  127. package/src/internal/download/telemetry.ts +1 -1
  128. package/src/internal/errors.test.ts +45 -11
  129. package/src/internal/errors.ts +8 -0
  130. package/src/internal/events/index.ts +1 -1
  131. package/src/internal/nodes/apiService.test.ts +59 -15
  132. package/src/internal/nodes/apiService.ts +21 -4
  133. package/src/internal/nodes/cache.test.ts +6 -1
  134. package/src/internal/nodes/cache.ts +9 -2
  135. package/src/internal/nodes/cryptoService.test.ts +139 -47
  136. package/src/internal/nodes/cryptoService.ts +94 -9
  137. package/src/internal/nodes/extendedAttributes.test.ts +60 -7
  138. package/src/internal/nodes/extendedAttributes.ts +37 -1
  139. package/src/internal/nodes/index.test.ts +1 -1
  140. package/src/internal/nodes/interface.ts +19 -2
  141. package/src/internal/nodes/nodesAccess.ts +15 -1
  142. package/src/internal/nodes/nodesManagement.ts +1 -1
  143. package/src/internal/nodes/nodesRevisions.ts +14 -5
  144. package/src/internal/shares/cryptoService.test.ts +5 -3
  145. package/src/internal/shares/cryptoService.ts +7 -4
  146. package/src/internal/sharing/apiService.ts +6 -6
  147. package/src/internal/sharing/cryptoService.test.ts +7 -4
  148. package/src/internal/sharing/cryptoService.ts +8 -5
  149. package/src/internal/upload/telemetry.test.ts +7 -7
  150. package/src/internal/upload/telemetry.ts +2 -2
  151. package/src/telemetry.ts +2 -2
  152. package/src/tests/telemetry.ts +1 -1
  153. package/src/transformers.ts +4 -2
@@ -1,9 +1,9 @@
1
- import { Revision } from '../../interface';
1
+ import { APIHTTPError, HTTPErrorCode } from '../apiService';
2
+ import { DecryptedRevision } from '../nodes';
2
3
  import { FileDownloader } from './fileDownloader';
3
4
  import { DownloadTelemetry } from './telemetry';
4
5
  import { DownloadAPIService } from './apiService';
5
6
  import { DownloadCryptoService } from './cryptoService';
6
- import { APIHTTPError, HTTPErrorCode } from '../apiService';
7
7
 
8
8
  function mockBlockDownload(_: string, token: string, onProgress: (downloadedBytes: number) => void) {
9
9
  const index = parseInt(token.slice(5, 6));
@@ -21,7 +21,7 @@ describe('FileDownloader', () => {
21
21
  let apiService: DownloadAPIService;
22
22
  let cryptoService: DownloadCryptoService;
23
23
  let nodeKey: { key: object; contentKeyPacketSessionKey: string };
24
- let revision: Revision;
24
+ let revision: DecryptedRevision;
25
25
 
26
26
  beforeEach(() => {
27
27
  // @ts-expect-error No need to implement all methods for mocking
@@ -74,7 +74,8 @@ describe('FileDownloader', () => {
74
74
  revision = {
75
75
  uid: 'revisionUid',
76
76
  claimedSize: 1024,
77
- } as Revision;
77
+ claimedBlockSizes: [16, 16, 16, 16],
78
+ } as DecryptedRevision;
78
79
  });
79
80
 
80
81
  describe('writeToStream', () => {
@@ -262,7 +263,7 @@ describe('FileDownloader', () => {
262
263
  await verifyOnProgress([1, 2, 3]);
263
264
  });
264
265
 
265
- it('should handle failure when veryfing block', async () => {
266
+ it('should handle failure when verifying block', async () => {
266
267
  cryptoService.verifyBlockIntegrity = jest.fn().mockImplementation(async function () {
267
268
  throw new Error('Failed to verify block');
268
269
  });
@@ -270,7 +271,7 @@ describe('FileDownloader', () => {
270
271
  await verifyFailure('Failed to verify block', undefined);
271
272
  });
272
273
 
273
- it('should handle one time-off failure when veryfing block', async () => {
274
+ it('should handle one time-off failure when verifying block', async () => {
274
275
  let count = 0;
275
276
  cryptoService.verifyBlockIntegrity = jest.fn().mockImplementation(async function () {
276
277
  if (count === 0) {
@@ -335,7 +336,7 @@ describe('FileDownloader', () => {
335
336
  await verifyOnProgress([1, 2, 3]);
336
337
  });
337
338
 
338
- it('should handle failure when veryfing manifest', async () => {
339
+ it('should handle failure when verifying manifest', async () => {
339
340
  cryptoService.verifyManifest = jest.fn().mockImplementation(async function () {
340
341
  throw new Error('Failed to verify manifest');
341
342
  });
@@ -394,4 +395,96 @@ describe('FileDownloader', () => {
394
395
  expect(onFinish).toHaveBeenCalledTimes(1);
395
396
  });
396
397
  });
398
+
399
+ describe('getSeekableStream', () => {
400
+ let onFinish: () => void;
401
+ let downloader: FileDownloader;
402
+
403
+ beforeEach(() => {
404
+ apiService.downloadBlock = jest.fn().mockImplementation(async function (_, token) {
405
+ const index = parseInt(token.slice(5, 6)) - 1;
406
+ const data = new Uint8Array(16);
407
+ for (let i = 0; i < data.length; i++) {
408
+ data[i] = index * 16 + i;
409
+ }
410
+ return data;
411
+ });
412
+
413
+ onFinish = jest.fn();
414
+
415
+ downloader = new FileDownloader(
416
+ telemetry,
417
+ apiService,
418
+ cryptoService,
419
+ nodeKey as any,
420
+ revision,
421
+ undefined,
422
+ onFinish,
423
+ );
424
+ });
425
+
426
+ it('should read the stream', async () => {
427
+ const stream = downloader.getSeekableStream();
428
+
429
+ const data = await stream.read(32);
430
+ expect(data.value).toEqual(
431
+ new Uint8Array([
432
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26,
433
+ 27, 28, 29, 30, 31,
434
+ ]),
435
+ );
436
+ expect(data.done).toEqual(false);
437
+
438
+ const data2 = await stream.read(32);
439
+ expect(data2.value).toEqual(
440
+ new Uint8Array([
441
+ 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56,
442
+ 57, 58, 59, 60, 61, 62, 63,
443
+ ]),
444
+ );
445
+ expect(data2.done).toEqual(false);
446
+
447
+ const data3 = await stream.read(32);
448
+ expect(data3.value).toEqual(new Uint8Array([]));
449
+ expect(data3.done).toEqual(true);
450
+
451
+ expect(cryptoService.decryptBlock).toHaveBeenCalledTimes(4);
452
+ expect(cryptoService.decryptBlock).toHaveBeenCalledWith(expect.anything(), {
453
+ key: 'privateKey',
454
+ contentKeyPacketSessionKey: 'contentSessionKey',
455
+ verificationKeys: 'verificationKeys',
456
+ });
457
+ });
458
+
459
+ it('should read the stream with seeking', async () => {
460
+ const stream = downloader.getSeekableStream();
461
+
462
+ const data1 = await stream.read(5);
463
+ expect(data1.value).toEqual(new Uint8Array([0, 1, 2, 3, 4]));
464
+ expect(data1.done).toEqual(false);
465
+ expect(cryptoService.decryptBlock).toHaveBeenCalledTimes(1);
466
+
467
+ await stream.seek(10);
468
+
469
+ // Seek withing first block, so no new block is downloaded.
470
+ const data2 = await stream.read(5);
471
+ expect(data2.value).toEqual(new Uint8Array([10, 11, 12, 13, 14]));
472
+ expect(data2.done).toEqual(false);
473
+ expect(cryptoService.decryptBlock).toHaveBeenCalledTimes(1);
474
+
475
+ // Seek and read from second and third blocks.
476
+ await stream.seek(30);
477
+
478
+ const data3 = await stream.read(5);
479
+ expect(data3.value).toEqual(new Uint8Array([30, 31, 32, 33, 34]));
480
+ expect(data3.done).toEqual(false);
481
+ expect(cryptoService.decryptBlock).toHaveBeenCalledTimes(3);
482
+
483
+ expect(cryptoService.decryptBlock).toHaveBeenCalledWith(expect.anything(), {
484
+ key: 'privateKey',
485
+ contentKeyPacketSessionKey: 'contentSessionKey',
486
+ verificationKeys: 'verificationKeys',
487
+ });
488
+ });
489
+ });
397
490
  });
@@ -1,11 +1,14 @@
1
1
  import { PrivateKey, SessionKey, base64StringToUint8Array } from '../../crypto';
2
- import { Logger, Revision } from '../../interface';
2
+ import { Logger } from '../../interface';
3
3
  import { LoggerWithPrefix } from '../../telemetry';
4
4
  import { APIHTTPError, HTTPErrorCode } from '../apiService';
5
+ import { DecryptedRevision } from '../nodes';
5
6
  import { DownloadAPIService } from './apiService';
7
+ import { getBlockIndex } from './blockIndex';
6
8
  import { DownloadController } from './controller';
7
9
  import { DownloadCryptoService } from './cryptoService';
8
10
  import { BlockMetadata, RevisionKeys } from './interface';
11
+ import { BufferedSeekableStream } from './seekableStream';
9
12
  import { DownloadTelemetry } from './telemetry';
10
13
 
11
14
  /**
@@ -33,7 +36,7 @@ export class FileDownloader {
33
36
  private apiService: DownloadAPIService,
34
37
  private cryptoService: DownloadCryptoService,
35
38
  private nodeKey: { key: PrivateKey; contentKeyPacketSessionKey: SessionKey },
36
- private revision: Revision,
39
+ private revision: DecryptedRevision,
37
40
  private signal?: AbortSignal,
38
41
  private onFinish?: () => void,
39
42
  ) {
@@ -52,6 +55,89 @@ export class FileDownloader {
52
55
  return this.revision.claimedSize;
53
56
  }
54
57
 
58
+ getSeekableStream(): BufferedSeekableStream {
59
+ let position = 0;
60
+ let cryptoKeys: RevisionKeys;
61
+
62
+ const logger = new LoggerWithPrefix(this.logger, `seekable stream`);
63
+
64
+ const claimedBlockSizes = this.revision.claimedBlockSizes;
65
+ if (!claimedBlockSizes) {
66
+ // Old nodes will not have claimed block sizes. One option is to
67
+ // use default block size, but old clients didn't use the same
68
+ // size (4 MiB vs 4 MB, for example).
69
+ // Ideally, we should throw error that client can easily handle,
70
+ // at the same time, new nodes shouldn't have this issue.
71
+ // For now, we throw general error that client must handle as any
72
+ // error from download - do not support seeking and ask user to
73
+ // download the whole file instead.
74
+ // In the future, we might either change this error, or have some
75
+ // clever way to detect block sizes from the first block and work
76
+ // around this issue.
77
+ throw new Error('Revision does not have defined claimed block sizes');
78
+ }
79
+
80
+ const stream = new BufferedSeekableStream({
81
+ start: async () => {
82
+ logger.debug(`Starting`);
83
+ cryptoKeys = await this.cryptoService.getRevisionKeys(this.nodeKey, this.revision);
84
+ },
85
+ pull: async (controller) => {
86
+ logger.debug(`Pulling at position ${position}`);
87
+
88
+ const result = await this.downloadDataFromPosition(claimedBlockSizes, position, cryptoKeys);
89
+ if (result instanceof Error) {
90
+ logger.error('Download failed', result);
91
+ controller.error(result);
92
+ return;
93
+ }
94
+ if (!result) {
95
+ logger.debug(`Download finished at position ${position}`);
96
+ controller.close();
97
+ return;
98
+ }
99
+ controller.enqueue(result);
100
+ position += result.length;
101
+ },
102
+ cancel: (reason?: unknown) => {
103
+ logger.info(`Cancelled: ${reason}`);
104
+ this.onFinish?.();
105
+ },
106
+ seek: async (newPosition) => {
107
+ logger.info(`Seeking to position ${newPosition}`);
108
+ position = newPosition;
109
+ },
110
+ });
111
+ return stream;
112
+ }
113
+
114
+ private async downloadDataFromPosition(
115
+ claimedBlockSizes: number[],
116
+ position: number,
117
+ cryptoKeys: RevisionKeys,
118
+ ): Promise<Uint8Array | Error | undefined> {
119
+ const { value, done } = getBlockIndex(claimedBlockSizes, position);
120
+ if (done) {
121
+ return;
122
+ }
123
+
124
+ this.logger.info(`Downloading data from block ${value.blockIndex} at offset ${value.blockOffset}`);
125
+
126
+ try {
127
+ const { blockIndex, blockOffset } = value;
128
+ const blockMetadata = await this.apiService.getRevisionBlockToken(
129
+ this.revision.uid,
130
+ blockIndex,
131
+ this.signal,
132
+ );
133
+
134
+ const blockData = await this.downloadBlockData(blockMetadata, true, cryptoKeys);
135
+ return blockData.slice(blockOffset);
136
+ } catch (error: unknown) {
137
+ return error instanceof Error ? error : new Error(`Unknown error: ${error}`);
138
+ }
139
+ }
140
+
55
141
  writeToStream(stream: WritableStream, onProgress?: (downloadedBytes: number) => void): DownloadController {
56
142
  if (this.controller.promise) {
57
143
  throw new Error(`Download already started`);
@@ -71,7 +157,7 @@ export class FileDownloader {
71
157
 
72
158
  private async downloadToStream(
73
159
  stream: WritableStream,
74
- onProgress?: (writtenBytes: number) => void,
160
+ onProgress?: (downloadedBytes: number) => void,
75
161
  ignoreIntegrityErrors = false,
76
162
  ): Promise<void> {
77
163
  const writer = stream.getWriter();
@@ -113,12 +199,16 @@ export class FileDownloader {
113
199
  this.ongoingDownloads.set(blockMetadata.index, { downloadPromise });
114
200
 
115
201
  await this.waitForDownloadCapacity();
116
- await this.flushCompletedBlocks(writer);
202
+ await this.flushCompletedBlocks(async (chunk) => {
203
+ await writer.write(chunk);
204
+ });
117
205
  }
118
206
 
119
207
  this.logger.debug(`All blocks downloading, waiting for them to finish`);
120
208
  await Promise.all(this.downloadPromises);
121
- await this.flushCompletedBlocks(writer);
209
+ await this.flushCompletedBlocks(async (chunk) => {
210
+ await writer.write(chunk);
211
+ });
122
212
 
123
213
  if (this.ongoingDownloads.size > 0) {
124
214
  this.logger.error(`Some blocks were not downloaded: ${this.ongoingDownloads.keys()}`);
@@ -158,6 +248,16 @@ export class FileDownloader {
158
248
  cryptoKeys: RevisionKeys,
159
249
  onProgress: (downloadedBytes: number) => void,
160
250
  ) {
251
+ const blockData = await this.downloadBlockData(blockMetadata, ignoreIntegrityErrors, cryptoKeys, onProgress);
252
+ this.ongoingDownloads.get(blockMetadata.index)!.decryptedBufferedBlock = blockData;
253
+ }
254
+
255
+ private async downloadBlockData(
256
+ blockMetadata: BlockMetadata,
257
+ ignoreIntegrityErrors: boolean,
258
+ cryptoKeys: RevisionKeys,
259
+ onProgress?: (downloadedBytes: number) => void,
260
+ ): Promise<Uint8Array> {
161
261
  const logger = new LoggerWithPrefix(this.logger, `block ${blockMetadata.index}`);
162
262
  logger.info(`Download started`);
163
263
 
@@ -219,8 +319,8 @@ export class FileDownloader {
219
319
  }
220
320
  }
221
321
 
222
- this.ongoingDownloads.get(blockMetadata.index)!.decryptedBufferedBlock = decryptedBlock;
223
322
  logger.info(`Downloaded`);
323
+ return decryptedBlock;
224
324
  }
225
325
 
226
326
  private async waitForDownloadCapacity() {
@@ -251,16 +351,16 @@ export class FileDownloader {
251
351
  }
252
352
  }
253
353
 
254
- private async flushCompletedBlocks(writer: WritableStreamDefaultWriter<Uint8Array>) {
354
+ private async flushCompletedBlocks(write: (chunk: Uint8Array) => void | Promise<void>) {
255
355
  this.logger.debug(`Flushing completed blocks`);
256
356
  while (this.isNextBlockDownloaded) {
257
357
  const decryptedBlock = this.ongoingDownloads.get(this.nextBlockIndex)!.decryptedBufferedBlock!;
258
358
  this.logger.info(`Flushing completed block ${this.nextBlockIndex}`);
259
359
  try {
260
- await writer.write(decryptedBlock);
360
+ await write(decryptedBlock);
261
361
  } catch (error) {
262
362
  this.logger.error(`Failed to write block, retrying once`, error);
263
- await writer.write(decryptedBlock);
363
+ await write(decryptedBlock);
264
364
  }
265
365
  this.ongoingDownloads.delete(this.nextBlockIndex);
266
366
  this.nextBlockIndex++;
@@ -1,6 +1,6 @@
1
1
  import { PrivateKey, PublicKey, SessionKey } from '../../crypto';
2
- import { NodeType, Result, Revision, MissingNode, MetricVolumeType } from '../../interface';
3
- import { DecryptedNode } from '../nodes';
2
+ import { NodeType, Result, MissingNode, MetricVolumeType } from '../../interface';
3
+ import { DecryptedNode, DecryptedRevision } from '../nodes';
4
4
 
5
5
  export type BlockMetadata = {
6
6
  index: number;
@@ -29,9 +29,9 @@ export interface NodesService {
29
29
  export interface NodesServiceNode {
30
30
  uid: string;
31
31
  type: NodeType;
32
- activeRevision?: Result<Revision, Error>;
32
+ activeRevision?: Result<DecryptedRevision, Error>;
33
33
  }
34
34
 
35
35
  export interface RevisionsService {
36
- getRevision(nodeRevisionUid: string): Promise<Revision>;
36
+ getRevision(nodeRevisionUid: string): Promise<DecryptedRevision>;
37
37
  }
@@ -0,0 +1,187 @@
1
+ import { SeekableReadableStream, BufferedSeekableStream } from './seekableStream';
2
+
3
+ describe('SeekableReadableStream', () => {
4
+ it('should call the seek callback when seek is called', async () => {
5
+ const mockSeek = jest.fn().mockResolvedValue(undefined);
6
+ const mockStart = jest.fn();
7
+
8
+ const stream = new SeekableReadableStream({
9
+ start: mockStart,
10
+ seek: mockSeek,
11
+ });
12
+
13
+ await stream.seek(100);
14
+
15
+ expect(mockSeek).toHaveBeenCalledWith(100);
16
+ expect(mockSeek).toHaveBeenCalledTimes(1);
17
+ });
18
+
19
+ it('should handle synchronous seek callback', async () => {
20
+ const mockSeek = jest.fn().mockReturnValue(undefined);
21
+
22
+ const stream = new SeekableReadableStream({
23
+ seek: mockSeek,
24
+ });
25
+
26
+ await stream.seek(250);
27
+
28
+ expect(mockSeek).toHaveBeenCalledWith(250);
29
+ });
30
+ });
31
+
32
+ describe('BufferedSeekableStream', () => {
33
+ let startWithCloseMock: jest.Mock;
34
+ let pullMock: jest.Mock;
35
+
36
+ const data1 = new Uint8Array([1, 2, 3, 4, 5]);
37
+ const data2 = new Uint8Array([6, 7, 8, 9, 10]);
38
+
39
+ beforeEach(() => {
40
+ startWithCloseMock = jest.fn().mockImplementation((controller) => {
41
+ controller.enqueue(data1);
42
+ controller.close();
43
+ });
44
+
45
+ let readIndex = 0;
46
+ pullMock = jest.fn().mockImplementation((controller) => {
47
+ if (readIndex === 0) {
48
+ controller.enqueue(data1);
49
+ } else if (readIndex === 1) {
50
+ controller.enqueue(data2);
51
+ } else {
52
+ controller.close();
53
+ }
54
+ readIndex++;
55
+ });
56
+ });
57
+
58
+ it('should throw error if highWaterMark is not 0', () => {
59
+ expect(() => {
60
+ new BufferedSeekableStream({ seek: jest.fn() }, { highWaterMark: 1 });
61
+ }).toThrow('highWaterMark must be 0');
62
+ });
63
+
64
+ it('should throw error when reading invalid number of bytes', async () => {
65
+ const stream = new BufferedSeekableStream({
66
+ seek: jest.fn(),
67
+ });
68
+
69
+ await expect(stream.read(0)).rejects.toThrow('Invalid number of bytes to read');
70
+ await expect(stream.read(-1)).rejects.toThrow('Invalid number of bytes to read');
71
+ });
72
+
73
+ it('should read exact number of bytes when underlying source provides exact amount', async () => {
74
+ const stream = new BufferedSeekableStream({
75
+ start: startWithCloseMock,
76
+ seek: jest.fn(),
77
+ });
78
+
79
+ const result = await stream.read(5);
80
+
81
+ expect(result).toEqual({ value: data1, done: false });
82
+ });
83
+
84
+ it('should buffer extra bytes when underlying source provides more than requested', async () => {
85
+ const stream = new BufferedSeekableStream({
86
+ pull: pullMock,
87
+ seek: jest.fn(),
88
+ });
89
+
90
+ const result1 = await stream.read(3);
91
+ expect(result1).toEqual({ value: new Uint8Array([1, 2, 3]), done: false });
92
+ expect(pullMock).toHaveBeenCalledTimes(1);
93
+
94
+ const result2 = await stream.read(2);
95
+ expect(result2).toEqual({ value: new Uint8Array([4, 5]), done: false });
96
+ expect(pullMock).toHaveBeenCalledTimes(1);
97
+ });
98
+
99
+ it('should use buffered data and read more when buffer is not enough for next read', async () => {
100
+ const stream = new BufferedSeekableStream({
101
+ pull: pullMock,
102
+ seek: jest.fn(),
103
+ });
104
+
105
+ const result1 = await stream.read(3);
106
+ expect(result1).toEqual({ value: new Uint8Array([1, 2, 3]), done: false });
107
+ expect(pullMock).toHaveBeenCalledTimes(1);
108
+
109
+ const result2 = await stream.read(5);
110
+ expect(result2).toEqual({ value: new Uint8Array([4, 5, 6, 7, 8]), done: false });
111
+ expect(pullMock).toHaveBeenCalledTimes(2);
112
+ });
113
+
114
+ it('should handle end of file gracefully when not enough data available', async () => {
115
+ const stream = new BufferedSeekableStream({
116
+ start: startWithCloseMock,
117
+ seek: jest.fn(),
118
+ });
119
+
120
+ const result = await stream.read(10);
121
+ expect(result).toEqual({ value: data1, done: true });
122
+ });
123
+
124
+ it('should clear buffer when seeking back', async () => {
125
+ const stream = new BufferedSeekableStream({
126
+ pull: pullMock,
127
+ seek: jest.fn(),
128
+ });
129
+
130
+ const result1 = await stream.read(2);
131
+ expect(result1).toEqual({ value: new Uint8Array([1, 2]), done: false });
132
+
133
+ await stream.seek(0);
134
+
135
+ const result2 = await stream.read(3);
136
+ expect(result2).toEqual({ value: new Uint8Array([6, 7, 8]), done: false });
137
+ });
138
+
139
+ it('should clear buffer when seeking past buffer end', async () => {
140
+ const stream = new BufferedSeekableStream({
141
+ pull: pullMock,
142
+ seek: jest.fn(),
143
+ });
144
+
145
+ const result1 = await stream.read(2);
146
+ expect(result1).toEqual({ value: new Uint8Array([1, 2]), done: false });
147
+
148
+ await stream.seek(100);
149
+
150
+ const result2 = await stream.read(3);
151
+ expect(result2).toEqual({ value: new Uint8Array([6, 7, 8]), done: false });
152
+ });
153
+
154
+ it('should update buffer correctly when seeking within buffer range', async () => {
155
+ const stream = new BufferedSeekableStream({
156
+ pull: pullMock,
157
+ seek: jest.fn(),
158
+ });
159
+
160
+ const result1 = await stream.read(1);
161
+ expect(result1).toEqual({ value: new Uint8Array([1]), done: false });
162
+
163
+ await stream.seek(3);
164
+
165
+ const result2 = await stream.read(3);
166
+ expect(result2).toEqual({ value: new Uint8Array([4, 5, 6]), done: false });
167
+ });
168
+
169
+ it('should handle multiple read operations correctly', async () => {
170
+ const stream = new BufferedSeekableStream({
171
+ pull: pullMock,
172
+ seek: jest.fn(),
173
+ });
174
+
175
+ const result1 = await stream.read(2);
176
+ expect(result1).toEqual({ value: new Uint8Array([1, 2]), done: false });
177
+
178
+ const result2 = await stream.read(4);
179
+ expect(result2).toEqual({ value: new Uint8Array([3, 4, 5, 6]), done: false });
180
+
181
+ const result3 = await stream.read(3);
182
+ expect(result3).toEqual({ value: new Uint8Array([7, 8, 9]), done: false });
183
+
184
+ const result4 = await stream.read(2);
185
+ expect(result4).toEqual({ value: new Uint8Array([10]), done: true });
186
+ });
187
+ });