@protontech/drive-sdk 0.4.1 → 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.
Files changed (197) hide show
  1. package/dist/diagnostic/{sdkDiagnosticFull.d.ts → diagnostic.d.ts} +5 -4
  2. package/dist/diagnostic/{sdkDiagnosticFull.js → diagnostic.js} +13 -10
  3. package/dist/diagnostic/diagnostic.js.map +1 -0
  4. package/dist/diagnostic/index.js +2 -4
  5. package/dist/diagnostic/index.js.map +1 -1
  6. package/dist/diagnostic/interface.d.ts +22 -1
  7. package/dist/diagnostic/sdkDiagnostic.d.ts +3 -2
  8. package/dist/diagnostic/sdkDiagnostic.js +80 -8
  9. package/dist/diagnostic/sdkDiagnostic.js.map +1 -1
  10. package/dist/interface/download.d.ts +4 -4
  11. package/dist/interface/index.d.ts +1 -1
  12. package/dist/interface/index.js.map +1 -1
  13. package/dist/interface/nodes.d.ts +9 -0
  14. package/dist/interface/telemetry.d.ts +4 -1
  15. package/dist/interface/telemetry.js.map +1 -1
  16. package/dist/interface/upload.d.ts +6 -3
  17. package/dist/internal/apiService/apiService.d.ts +3 -0
  18. package/dist/internal/apiService/apiService.js +25 -2
  19. package/dist/internal/apiService/apiService.js.map +1 -1
  20. package/dist/internal/apiService/apiService.test.js +38 -0
  21. package/dist/internal/apiService/apiService.test.js.map +1 -1
  22. package/dist/internal/apiService/driveTypes.d.ts +2595 -2397
  23. package/dist/internal/apiService/errors.js +3 -0
  24. package/dist/internal/apiService/errors.js.map +1 -1
  25. package/dist/internal/apiService/errors.test.js +15 -7
  26. package/dist/internal/apiService/errors.test.js.map +1 -1
  27. package/dist/internal/asyncIteratorMap.d.ts +1 -1
  28. package/dist/internal/asyncIteratorMap.js +6 -1
  29. package/dist/internal/asyncIteratorMap.js.map +1 -1
  30. package/dist/internal/asyncIteratorMap.test.js +9 -0
  31. package/dist/internal/asyncIteratorMap.test.js.map +1 -1
  32. package/dist/internal/download/controller.d.ts +2 -0
  33. package/dist/internal/download/controller.js +15 -1
  34. package/dist/internal/download/controller.js.map +1 -1
  35. package/dist/internal/download/fileDownloader.d.ts +3 -3
  36. package/dist/internal/download/fileDownloader.js +11 -6
  37. package/dist/internal/download/fileDownloader.js.map +1 -1
  38. package/dist/internal/download/fileDownloader.test.js +8 -8
  39. package/dist/internal/download/fileDownloader.test.js.map +1 -1
  40. package/dist/internal/nodes/apiService.d.ts +6 -1
  41. package/dist/internal/nodes/apiService.js +71 -44
  42. package/dist/internal/nodes/apiService.js.map +1 -1
  43. package/dist/internal/nodes/apiService.test.js +204 -15
  44. package/dist/internal/nodes/apiService.test.js.map +1 -1
  45. package/dist/internal/nodes/debouncer.d.ts +24 -0
  46. package/dist/internal/nodes/debouncer.js +92 -0
  47. package/dist/internal/nodes/debouncer.js.map +1 -0
  48. package/dist/internal/nodes/debouncer.test.d.ts +1 -0
  49. package/dist/internal/nodes/debouncer.test.js +108 -0
  50. package/dist/internal/nodes/debouncer.test.js.map +1 -0
  51. package/dist/internal/nodes/extendedAttributes.js +2 -2
  52. package/dist/internal/nodes/extendedAttributes.js.map +1 -1
  53. package/dist/internal/nodes/index.js +1 -1
  54. package/dist/internal/nodes/index.js.map +1 -1
  55. package/dist/internal/nodes/nodesAccess.d.ts +6 -4
  56. package/dist/internal/nodes/nodesAccess.js +29 -9
  57. package/dist/internal/nodes/nodesAccess.js.map +1 -1
  58. package/dist/internal/nodes/nodesAccess.test.js +19 -7
  59. package/dist/internal/nodes/nodesAccess.test.js.map +1 -1
  60. package/dist/internal/nodes/nodesManagement.d.ts +2 -2
  61. package/dist/internal/nodes/nodesManagement.js +5 -3
  62. package/dist/internal/nodes/nodesManagement.js.map +1 -1
  63. package/dist/internal/nodes/nodesManagement.test.js +3 -1
  64. package/dist/internal/nodes/nodesManagement.test.js.map +1 -1
  65. package/dist/internal/photos/apiService.js +9 -20
  66. package/dist/internal/photos/apiService.js.map +1 -1
  67. package/dist/internal/photos/upload.d.ts +2 -1
  68. package/dist/internal/photos/upload.js +9 -3
  69. package/dist/internal/photos/upload.js.map +1 -1
  70. package/dist/internal/sharing/apiService.d.ts +1 -1
  71. package/dist/internal/sharing/apiService.js +2 -2
  72. package/dist/internal/sharing/apiService.js.map +1 -1
  73. package/dist/internal/sharing/sharingManagement.d.ts +4 -1
  74. package/dist/internal/sharing/sharingManagement.js +7 -4
  75. package/dist/internal/sharing/sharingManagement.js.map +1 -1
  76. package/dist/internal/sharingPublic/apiService.d.ts +8 -10
  77. package/dist/internal/sharingPublic/apiService.js +9 -125
  78. package/dist/internal/sharingPublic/apiService.js.map +1 -1
  79. package/dist/internal/sharingPublic/cryptoReporter.d.ts +16 -0
  80. package/dist/internal/sharingPublic/{cryptoService.js → cryptoReporter.js} +3 -16
  81. package/dist/internal/sharingPublic/cryptoReporter.js.map +1 -0
  82. package/dist/internal/sharingPublic/index.d.ts +22 -4
  83. package/dist/internal/sharingPublic/index.js +37 -12
  84. package/dist/internal/sharingPublic/index.js.map +1 -1
  85. package/dist/internal/sharingPublic/nodes.d.ts +18 -0
  86. package/dist/internal/sharingPublic/nodes.js +46 -0
  87. package/dist/internal/sharingPublic/nodes.js.map +1 -0
  88. package/dist/internal/sharingPublic/session/apiService.d.ts +7 -5
  89. package/dist/internal/sharingPublic/session/apiService.js +25 -4
  90. package/dist/internal/sharingPublic/session/apiService.js.map +1 -1
  91. package/dist/internal/sharingPublic/session/interface.d.ts +17 -0
  92. package/dist/internal/sharingPublic/session/manager.d.ts +12 -4
  93. package/dist/internal/sharingPublic/session/manager.js +14 -4
  94. package/dist/internal/sharingPublic/session/manager.js.map +1 -1
  95. package/dist/internal/sharingPublic/session/session.d.ts +7 -4
  96. package/dist/internal/sharingPublic/session/session.js +7 -3
  97. package/dist/internal/sharingPublic/session/session.js.map +1 -1
  98. package/dist/internal/sharingPublic/session/url.test.js +3 -3
  99. package/dist/internal/sharingPublic/shares.d.ts +27 -0
  100. package/dist/internal/sharingPublic/shares.js +46 -0
  101. package/dist/internal/sharingPublic/shares.js.map +1 -0
  102. package/dist/internal/upload/apiService.js +10 -1
  103. package/dist/internal/upload/apiService.js.map +1 -1
  104. package/dist/internal/upload/controller.d.ts +11 -3
  105. package/dist/internal/upload/controller.js +16 -2
  106. package/dist/internal/upload/controller.js.map +1 -1
  107. package/dist/internal/upload/fileUploader.d.ts +6 -3
  108. package/dist/internal/upload/fileUploader.js +4 -4
  109. package/dist/internal/upload/fileUploader.js.map +1 -1
  110. package/dist/internal/upload/fileUploader.test.js +23 -11
  111. package/dist/internal/upload/fileUploader.test.js.map +1 -1
  112. package/dist/internal/upload/streamUploader.d.ts +9 -4
  113. package/dist/internal/upload/streamUploader.js +67 -20
  114. package/dist/internal/upload/streamUploader.js.map +1 -1
  115. package/dist/internal/upload/streamUploader.test.js +43 -13
  116. package/dist/internal/upload/streamUploader.test.js.map +1 -1
  117. package/dist/protonDriveClient.d.ts +11 -6
  118. package/dist/protonDriveClient.js +11 -10
  119. package/dist/protonDriveClient.js.map +1 -1
  120. package/dist/protonDrivePublicLinkClient.d.ts +34 -6
  121. package/dist/protonDrivePublicLinkClient.js +52 -9
  122. package/dist/protonDrivePublicLinkClient.js.map +1 -1
  123. package/dist/tests/telemetry.d.ts +4 -2
  124. package/dist/tests/telemetry.js +3 -1
  125. package/dist/tests/telemetry.js.map +1 -1
  126. package/dist/transformers.d.ts +3 -2
  127. package/dist/transformers.js +6 -0
  128. package/dist/transformers.js.map +1 -1
  129. package/package.json +1 -1
  130. package/src/diagnostic/{sdkDiagnosticFull.ts → diagnostic.ts} +10 -6
  131. package/src/diagnostic/index.ts +3 -5
  132. package/src/diagnostic/interface.ts +39 -0
  133. package/src/diagnostic/sdkDiagnostic.ts +111 -10
  134. package/src/interface/download.ts +4 -4
  135. package/src/interface/index.ts +1 -0
  136. package/src/interface/nodes.ts +3 -0
  137. package/src/interface/telemetry.ts +5 -0
  138. package/src/interface/upload.ts +3 -3
  139. package/src/internal/apiService/apiService.test.ts +50 -0
  140. package/src/internal/apiService/apiService.ts +33 -2
  141. package/src/internal/apiService/driveTypes.ts +2713 -2561
  142. package/src/internal/apiService/errors.test.ts +10 -0
  143. package/src/internal/apiService/errors.ts +5 -1
  144. package/src/internal/asyncIteratorMap.test.ts +12 -0
  145. package/src/internal/asyncIteratorMap.ts +8 -0
  146. package/src/internal/download/controller.ts +13 -1
  147. package/src/internal/download/fileDownloader.test.ts +8 -8
  148. package/src/internal/download/fileDownloader.ts +13 -6
  149. package/src/internal/nodes/apiService.test.ts +261 -14
  150. package/src/internal/nodes/apiService.ts +99 -65
  151. package/src/internal/nodes/debouncer.test.ts +141 -0
  152. package/src/internal/nodes/debouncer.ts +109 -0
  153. package/src/internal/nodes/extendedAttributes.ts +2 -2
  154. package/src/internal/nodes/index.ts +1 -8
  155. package/src/internal/nodes/nodesAccess.test.ts +19 -7
  156. package/src/internal/nodes/nodesAccess.ts +44 -9
  157. package/src/internal/nodes/nodesManagement.test.ts +3 -1
  158. package/src/internal/nodes/nodesManagement.ts +11 -5
  159. package/src/internal/photos/apiService.ts +12 -29
  160. package/src/internal/photos/upload.ts +22 -1
  161. package/src/internal/sharing/apiService.ts +2 -2
  162. package/src/internal/sharing/sharingManagement.ts +7 -4
  163. package/src/internal/sharingPublic/apiService.ts +23 -160
  164. package/src/internal/sharingPublic/{cryptoService.ts → cryptoReporter.ts} +2 -27
  165. package/src/internal/sharingPublic/index.ts +76 -13
  166. package/src/internal/sharingPublic/nodes.ts +59 -0
  167. package/src/internal/sharingPublic/session/apiService.ts +32 -10
  168. package/src/internal/sharingPublic/session/interface.ts +20 -0
  169. package/src/internal/sharingPublic/session/manager.ts +31 -8
  170. package/src/internal/sharingPublic/session/session.ts +12 -7
  171. package/src/internal/sharingPublic/session/url.test.ts +3 -3
  172. package/src/internal/sharingPublic/shares.ts +50 -0
  173. package/src/internal/upload/apiService.ts +12 -1
  174. package/src/internal/upload/controller.ts +16 -4
  175. package/src/internal/upload/fileUploader.test.ts +25 -11
  176. package/src/internal/upload/fileUploader.ts +6 -5
  177. package/src/internal/upload/streamUploader.test.ts +56 -12
  178. package/src/internal/upload/streamUploader.ts +78 -20
  179. package/src/protonDriveClient.ts +29 -11
  180. package/src/protonDrivePublicLinkClient.ts +100 -16
  181. package/src/tests/telemetry.ts +6 -3
  182. package/src/transformers.ts +8 -0
  183. package/dist/diagnostic/sdkDiagnosticFull.js.map +0 -1
  184. package/dist/internal/sharingPublic/cryptoCache.d.ts +0 -19
  185. package/dist/internal/sharingPublic/cryptoCache.js +0 -72
  186. package/dist/internal/sharingPublic/cryptoCache.js.map +0 -1
  187. package/dist/internal/sharingPublic/cryptoService.d.ts +0 -9
  188. package/dist/internal/sharingPublic/cryptoService.js.map +0 -1
  189. package/dist/internal/sharingPublic/interface.d.ts +0 -6
  190. package/dist/internal/sharingPublic/interface.js +0 -3
  191. package/dist/internal/sharingPublic/interface.js.map +0 -1
  192. package/dist/internal/sharingPublic/manager.d.ts +0 -19
  193. package/dist/internal/sharingPublic/manager.js +0 -81
  194. package/dist/internal/sharingPublic/manager.js.map +0 -1
  195. package/src/internal/sharingPublic/cryptoCache.ts +0 -79
  196. package/src/internal/sharingPublic/interface.ts +0 -14
  197. package/src/internal/sharingPublic/manager.ts +0 -86
@@ -1,11 +1,12 @@
1
1
  import { c } from 'ttag';
2
2
 
3
- import { ProtonDriveError, ValidationError } from '../../errors';
3
+ import { NodeWithSameNameExistsValidationError, ProtonDriveError, ValidationError } from '../../errors';
4
4
  import { Logger, NodeResult } from '../../interface';
5
5
  import { MemberRole, RevisionState } from '../../interface/nodes';
6
6
  import {
7
7
  DriveAPIService,
8
8
  drivePaths,
9
+ ErrorCode,
9
10
  InvalidRequirementsAPIError,
10
11
  isCodeOk,
11
12
  nodeTypeNumberToNodeType,
@@ -128,7 +129,7 @@ export class NodeAPIService {
128
129
 
129
130
  async *iterateNodes(
130
131
  nodeUids: string[],
131
- ownVolumeId: string,
132
+ ownVolumeId: string | undefined,
132
133
  filterOptions?: FilterOptions,
133
134
  signal?: AbortSignal,
134
135
  ): AsyncGenerator<EncryptedNode> {
@@ -389,55 +390,49 @@ export class NodeAPIService {
389
390
  return makeNodeUid(volumeId, response.LinkID);
390
391
  }
391
392
 
392
- // Improvement requested: split into multiple calls for many nodes.
393
393
  async *trashNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
394
- const nodeIds = nodeUids.map(splitNodeUid);
395
- const volumeId = assertAndGetSingleVolumeId(c('Operation').t`Trashing items`, nodeIds);
396
-
397
- const response = await this.apiService.post<PostTrashNodesRequest, PostTrashNodesResponse>(
398
- `drive/v2/volumes/${volumeId}/trash_multiple`,
399
- {
400
- LinkIDs: nodeIds.map(({ nodeId }) => nodeId),
401
- },
402
- signal,
403
- );
394
+ for (const { volumeId, batchNodeIds, batchNodeUids } of groupNodeUidsByVolumeAndIteratePerBatch(nodeUids)) {
395
+ const response = await this.apiService.post<PostTrashNodesRequest, PostTrashNodesResponse>(
396
+ `drive/v2/volumes/${volumeId}/trash_multiple`,
397
+ {
398
+ LinkIDs: batchNodeIds,
399
+ },
400
+ signal,
401
+ );
404
402
 
405
- // TODO: remove `as` when backend fixes OpenAPI schema.
406
- yield* handleResponseErrors(nodeUids, volumeId, response.Responses as LinkResponse[]);
403
+ // TODO: remove `as` when backend fixes OpenAPI schema.
404
+ yield* handleResponseErrors(batchNodeUids, volumeId, response.Responses as LinkResponse[]);
405
+ }
407
406
  }
408
407
 
409
- // Improvement requested: split into multiple calls for many nodes.
410
408
  async *restoreNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
411
- const nodeIds = nodeUids.map(splitNodeUid);
412
- const volumeId = assertAndGetSingleVolumeId(c('Operation').t`Restoring items`, nodeIds);
413
-
414
- const response = await this.apiService.put<PutRestoreNodesRequest, PutRestoreNodesResponse>(
415
- `drive/v2/volumes/${volumeId}/trash/restore_multiple`,
416
- {
417
- LinkIDs: nodeIds.map(({ nodeId }) => nodeId),
418
- },
419
- signal,
420
- );
409
+ for (const { volumeId, batchNodeIds, batchNodeUids } of groupNodeUidsByVolumeAndIteratePerBatch(nodeUids)) {
410
+ const response = await this.apiService.put<PutRestoreNodesRequest, PutRestoreNodesResponse>(
411
+ `drive/v2/volumes/${volumeId}/trash/restore_multiple`,
412
+ {
413
+ LinkIDs: batchNodeIds,
414
+ },
415
+ signal,
416
+ );
421
417
 
422
- // TODO: remove `as` when backend fixes OpenAPI schema.
423
- yield* handleResponseErrors(nodeUids, volumeId, response.Responses as LinkResponse[]);
418
+ // TODO: remove `as` when backend fixes OpenAPI schema.
419
+ yield* handleResponseErrors(batchNodeUids, volumeId, response.Responses as LinkResponse[]);
420
+ }
424
421
  }
425
422
 
426
- // Improvement requested: split into multiple calls for many nodes.
427
423
  async *deleteNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<NodeResult> {
428
- const nodeIds = nodeUids.map(splitNodeUid);
429
- const volumeId = assertAndGetSingleVolumeId(c('Operation').t`Deleting items`, nodeIds);
430
-
431
- const response = await this.apiService.post<PostDeleteNodesRequest, PostDeleteNodesResponse>(
432
- `drive/v2/volumes/${volumeId}/trash/delete_multiple`,
433
- {
434
- LinkIDs: nodeIds.map(({ nodeId }) => nodeId),
435
- },
436
- signal,
437
- );
424
+ for (const { volumeId, batchNodeIds, batchNodeUids } of groupNodeUidsByVolumeAndIteratePerBatch(nodeUids)) {
425
+ const response = await this.apiService.post<PostDeleteNodesRequest, PostDeleteNodesResponse>(
426
+ `drive/v2/volumes/${volumeId}/trash/delete_multiple`,
427
+ {
428
+ LinkIDs: batchNodeIds,
429
+ },
430
+ signal,
431
+ );
438
432
 
439
- // TODO: remove `as` when backend fixes OpenAPI schema.
440
- yield* handleResponseErrors(nodeUids, volumeId, response.Responses as LinkResponse[]);
433
+ // TODO: remove `as` when backend fixes OpenAPI schema.
434
+ yield* handleResponseErrors(batchNodeUids, volumeId, response.Responses as LinkResponse[]);
435
+ }
441
436
  }
442
437
 
443
438
  async createFolder(
@@ -455,21 +450,41 @@ export class NodeAPIService {
455
450
  ): Promise<string> {
456
451
  const { volumeId, nodeId: parentId } = splitNodeUid(parentUid);
457
452
 
458
- const response = await this.apiService.post<PostCreateFolderRequest, PostCreateFolderResponse>(
459
- `drive/v2/volumes/${volumeId}/folders`,
460
- {
461
- ParentLinkID: parentId,
462
- NodeKey: newNode.armoredKey,
463
- NodeHashKey: newNode.armoredHashKey,
464
- NodePassphrase: newNode.armoredNodePassphrase,
465
- NodePassphraseSignature: newNode.armoredNodePassphraseSignature,
466
- SignatureEmail: newNode.signatureEmail,
467
- Name: newNode.encryptedName,
468
- Hash: newNode.hash,
469
- // @ts-expect-error: XAttr is optional as undefined.
470
- XAttr: newNode.armoredExtendedAttributes,
471
- },
472
- );
453
+ let response: PostCreateFolderResponse;
454
+ try {
455
+ response = await this.apiService.post<PostCreateFolderRequest, PostCreateFolderResponse>(
456
+ `drive/v2/volumes/${volumeId}/folders`,
457
+ {
458
+ ParentLinkID: parentId,
459
+ NodeKey: newNode.armoredKey,
460
+ NodeHashKey: newNode.armoredHashKey,
461
+ NodePassphrase: newNode.armoredNodePassphrase,
462
+ NodePassphraseSignature: newNode.armoredNodePassphraseSignature,
463
+ SignatureEmail: newNode.signatureEmail,
464
+ Name: newNode.encryptedName,
465
+ Hash: newNode.hash,
466
+ // @ts-expect-error: XAttr is optional as undefined.
467
+ XAttr: newNode.armoredExtendedAttributes,
468
+ },
469
+ );
470
+ } catch (error: unknown) {
471
+ if (error instanceof ValidationError) {
472
+ if (error.code === ErrorCode.ALREADY_EXISTS) {
473
+ const typedDetails = error.details as
474
+ | {
475
+ ConflictLinkID: string;
476
+ }
477
+ | undefined;
478
+
479
+ const existingNodeUid = typedDetails?.ConflictLinkID
480
+ ? makeNodeUid(volumeId, typedDetails.ConflictLinkID)
481
+ : undefined;
482
+
483
+ throw new NodeWithSameNameExistsValidationError(error.message, error.code, existingNodeUid);
484
+ }
485
+ }
486
+ throw error;
487
+ }
473
488
 
474
489
  return makeNodeUid(volumeId, response.Folder.ID);
475
490
  }
@@ -513,15 +528,6 @@ export class NodeAPIService {
513
528
  }
514
529
  }
515
530
 
516
- function assertAndGetSingleVolumeId(operationForErrorMessage: string, nodeIds: { volumeId: string }[]): string {
517
- const uniqueVolumeIds = new Set(nodeIds.map(({ volumeId }) => volumeId));
518
- if (uniqueVolumeIds.size !== 1) {
519
- throw new ValidationError(c('Error').t`${operationForErrorMessage} from multiple sections is not allowed`);
520
- }
521
- const volumeId = nodeIds[0].volumeId;
522
- return volumeId;
523
- }
524
-
525
531
  type LinkResponse = {
526
532
  LinkID: string;
527
533
  Response: {
@@ -657,6 +663,34 @@ function linkToEncryptedNode(
657
663
  throw new Error(`Unknown node type: ${link.Link.Type}`);
658
664
  }
659
665
 
666
+ export function* groupNodeUidsByVolumeAndIteratePerBatch(
667
+ nodeUids: string[],
668
+ ): Generator<{ volumeId: string; batchNodeIds: string[]; batchNodeUids: string[] }> {
669
+ const allNodeIds = nodeUids.map((nodeUid: string) => {
670
+ const { volumeId, nodeId } = splitNodeUid(nodeUid);
671
+ return { volumeId, nodeIds: { nodeId, nodeUid } };
672
+ });
673
+
674
+ const nodeIdsByVolumeId = new Map<string, { nodeId: string; nodeUid: string }[]>();
675
+ for (const { volumeId, nodeIds } of allNodeIds) {
676
+ if (!nodeIdsByVolumeId.has(volumeId)) {
677
+ nodeIdsByVolumeId.set(volumeId, []);
678
+ }
679
+ nodeIdsByVolumeId.get(volumeId)?.push(nodeIds);
680
+ }
681
+
682
+ for (const [volumeId, nodeIds] of nodeIdsByVolumeId.entries()) {
683
+ for (const nodeIdsBatch of batch(nodeIds, API_NODES_BATCH_SIZE)) {
684
+ yield {
685
+ volumeId,
686
+ batchNodeIds: nodeIdsBatch.map(({ nodeId }) => nodeId),
687
+ batchNodeUids: nodeIdsBatch.map(({ nodeUid }) => nodeUid),
688
+ };
689
+ }
690
+ }
691
+ }
692
+
693
+
660
694
  function transformRevisionResponse(
661
695
  volumeId: string,
662
696
  nodeId: string,
@@ -0,0 +1,141 @@
1
+ import { ProtonDriveTelemetry } from '../../interface';
2
+ import { getMockTelemetry } from '../../tests/telemetry';
3
+ import { NodesDebouncer } from './debouncer';
4
+
5
+ describe('NodesDebouncer', () => {
6
+ let debouncer: NodesDebouncer;
7
+ let mockTelemetry: ReturnType<typeof getMockTelemetry>;
8
+
9
+ beforeEach(() => {
10
+ mockTelemetry = getMockTelemetry();
11
+ debouncer = new NodesDebouncer(mockTelemetry);
12
+
13
+ jest.useFakeTimers();
14
+ });
15
+
16
+ afterEach(() => {
17
+ jest.useRealTimers();
18
+ debouncer.clear();
19
+ });
20
+
21
+ it('should register a node for loading and wait for it to finish', async () => {
22
+ const nodeUid = 'test-node-1';
23
+ debouncer.loadingNode(nodeUid);
24
+
25
+ // Verify that the node is registered by checking if waitForLoadingNode works
26
+ const waitPromise = debouncer.waitForLoadingNode(nodeUid);
27
+ expect(waitPromise).toBeInstanceOf(Promise);
28
+
29
+ // Finish loading to clean up
30
+ debouncer.finishedLoadingNode(nodeUid);
31
+ await waitPromise;
32
+ });
33
+
34
+ it('should allow multiple nodes to be registered', async () => {
35
+ const nodeUid1 = 'test-node-1';
36
+ const nodeUid2 = 'test-node-2';
37
+
38
+ debouncer.loadingNode(nodeUid1);
39
+ debouncer.loadingNode(nodeUid2);
40
+
41
+ const wait1 = debouncer.waitForLoadingNode(nodeUid1);
42
+ const wait2 = debouncer.waitForLoadingNode(nodeUid2);
43
+
44
+ expect(wait1).toBeInstanceOf(Promise);
45
+ expect(wait2).toBeInstanceOf(Promise);
46
+
47
+ debouncer.finishedLoadingNode(nodeUid1);
48
+ debouncer.finishedLoadingNode(nodeUid2);
49
+ await Promise.all([wait1, wait2]);
50
+ });
51
+
52
+ it('should register multiple nodes at once', async () => {
53
+ const nodeUid1 = 'test-node-1';
54
+ const nodeUid2 = 'test-node-2';
55
+
56
+ debouncer.loadingNodes([nodeUid1, nodeUid2]);
57
+
58
+ const wait1 = debouncer.waitForLoadingNode(nodeUid1);
59
+ const wait2 = debouncer.waitForLoadingNode(nodeUid2);
60
+
61
+ expect(wait1).toBeInstanceOf(Promise);
62
+ expect(wait2).toBeInstanceOf(Promise);
63
+
64
+ debouncer.finishedLoadingNode(nodeUid1);
65
+ debouncer.finishedLoadingNode(nodeUid2);
66
+ await Promise.all([wait1, wait2]);
67
+ });
68
+
69
+ it('should warn about registering the same node twice', async () => {
70
+ const nodeUid = 'test-node-1';
71
+
72
+ // Register the same node twice
73
+ debouncer.loadingNode(nodeUid);
74
+ debouncer.loadingNode(nodeUid);
75
+
76
+ expect(mockTelemetry.mockLogger.warn).toHaveBeenCalledWith(`Loading twice for: ${nodeUid}`);
77
+ });
78
+
79
+ it('should send metric when waiting for a long time', async () => {
80
+ const nodeUid = 'test-node-1';
81
+ debouncer.loadingNode(nodeUid);
82
+
83
+ const waitPromise = debouncer.waitForLoadingNode(nodeUid);
84
+ expect(mockTelemetry.recordMetric).not.toHaveBeenCalled();
85
+ jest.advanceTimersByTime(1500);
86
+ expect(mockTelemetry.recordMetric).toHaveBeenCalledWith({
87
+ eventName: 'debounceLongWait',
88
+ });
89
+
90
+ debouncer.finishedLoadingNode(nodeUid);
91
+ await waitPromise;
92
+ });
93
+
94
+ it('should timeout', async () => {
95
+ const nodeUid = 'test-node-1';
96
+ debouncer.loadingNode(nodeUid);
97
+
98
+ jest.advanceTimersByTime(6000);
99
+ expect(mockTelemetry.mockLogger.warn).toHaveBeenCalledWith(`Timeout for: ${nodeUid}`);
100
+ await expect(debouncer.waitForLoadingNode(nodeUid)).resolves.toBeUndefined();
101
+ });
102
+
103
+ describe('finishedLoadingNode', () => {
104
+ it('should handle non-existent node gracefully', async () => {
105
+ const nodeUid = 'non-existent-node';
106
+
107
+ expect(() => debouncer.finishedLoadingNode(nodeUid)).not.toThrow();
108
+ });
109
+
110
+ it('should remove node from internal map after finishing', async () => {
111
+ const nodeUid = 'test-node-1';
112
+ debouncer.loadingNode(nodeUid);
113
+ debouncer.finishedLoadingNode(nodeUid);
114
+
115
+ const waitPromise = debouncer.waitForLoadingNode(nodeUid);
116
+ await expect(waitPromise).resolves.toBe(undefined);
117
+ });
118
+ });
119
+
120
+ describe('waitForLoadingNode', () => {
121
+ it('should return immediately for non-registered node', async () => {
122
+ const nodeUid = 'non-existent-node';
123
+
124
+ const result = await debouncer.waitForLoadingNode(nodeUid);
125
+ expect(result).toBeUndefined();
126
+ expect(mockTelemetry.mockLogger.debug).not.toHaveBeenCalled();
127
+ });
128
+
129
+ it('should wait for registered node and log debug message', async () => {
130
+ const nodeUid = 'test-node-1';
131
+ debouncer.loadingNode(nodeUid);
132
+
133
+ const waitPromise = debouncer.waitForLoadingNode(nodeUid);
134
+
135
+ expect(mockTelemetry.mockLogger.debug).toHaveBeenCalledWith(`Wait for: ${nodeUid}`);
136
+
137
+ debouncer.finishedLoadingNode(nodeUid);
138
+ await waitPromise;
139
+ });
140
+ });
141
+ });
@@ -0,0 +1,109 @@
1
+ import { Logger, ProtonDriveTelemetry } from '../../interface';
2
+
3
+ /**
4
+ * The timeout for which the node is considered to be loading.
5
+ * If the node is not loaded after this timeout, it is considered to be
6
+ * loaded or failed to be loaded, and allowed other places to proceed.
7
+ *
8
+ * Decrypting many nodes in parallel can take a lot of time, so we allow
9
+ * more time for this.
10
+ */
11
+ const DEBOUNCE_TIMEOUT = 5000;
12
+
13
+ /**
14
+ * The timeout for which the node is considered to be waiting for a long time.
15
+ * After this timeout the metric is sent.
16
+ */
17
+ const DEBOUNCE_LONG_WAIT_TIMEOUT = 1000;
18
+
19
+ /**
20
+ * Helper to avoid loading the same node twice.
21
+ *
22
+ * Each place that loads a node should report it is being loaded,
23
+ * and when it is finished, it should report it is finished.
24
+ * The finish must be called even if the node fails to be loaded
25
+ * to clear the promise.
26
+ *
27
+ * Each place that loads a node from cache should first wait for
28
+ * the node to be loaded if that is the case.
29
+ */
30
+ export class NodesDebouncer {
31
+ private logger: Logger;
32
+
33
+ private promises: Map<
34
+ string,
35
+ {
36
+ promise: Promise<void>;
37
+ resolve: () => void;
38
+ timeout: NodeJS.Timeout;
39
+ }
40
+ > = new Map();
41
+
42
+ constructor(private telemetry: ProtonDriveTelemetry) {
43
+ this.logger = telemetry.getLogger('nodes-debouncer');
44
+ this.telemetry = telemetry;
45
+ }
46
+
47
+ loadingNodes(nodeUids: string[]) {
48
+ for (const nodeUid of nodeUids) {
49
+ this.loadingNode(nodeUid);
50
+ }
51
+ }
52
+
53
+ loadingNode(nodeUid: string) {
54
+ const { promise, resolve } = Promise.withResolvers<void>();
55
+ if (this.promises.has(nodeUid)) {
56
+ this.logger.warn(`Loading twice for: ${nodeUid}`);
57
+ return;
58
+ }
59
+
60
+ const timeout = setTimeout(() => {
61
+ this.logger.warn(`Timeout for: ${nodeUid}`);
62
+ this.finishedLoadingNode(nodeUid);
63
+ }, DEBOUNCE_TIMEOUT);
64
+ this.promises.set(nodeUid, { promise, resolve, timeout });
65
+ }
66
+
67
+ finishedLoadingNodes(nodeUids: string[]) {
68
+ for (const nodeUid of nodeUids) {
69
+ this.finishedLoadingNode(nodeUid);
70
+ }
71
+ }
72
+
73
+ finishedLoadingNode(nodeUid: string) {
74
+ const result = this.promises.get(nodeUid);
75
+ if (!result) {
76
+ return;
77
+ }
78
+
79
+ clearTimeout(result.timeout);
80
+ result.resolve();
81
+ this.promises.delete(nodeUid);
82
+ }
83
+
84
+ async waitForLoadingNode(nodeUid: string) {
85
+ const result = this.promises.get(nodeUid);
86
+ if (!result) {
87
+ return;
88
+ }
89
+
90
+ const metricTimeout = setTimeout(() => {
91
+ this.telemetry.recordMetric({
92
+ eventName: 'debounceLongWait',
93
+ });
94
+ }, DEBOUNCE_LONG_WAIT_TIMEOUT);
95
+
96
+ this.logger.debug(`Wait for: ${nodeUid}`);
97
+ await result.promise;
98
+
99
+ clearTimeout(metricTimeout);
100
+ }
101
+
102
+ clear() {
103
+ for (const result of this.promises.values()) {
104
+ clearTimeout(result.timeout);
105
+ result.resolve();
106
+ }
107
+ this.promises.clear();
108
+ }
109
+ }
@@ -208,11 +208,11 @@ function parseBlockSizes(
208
208
  return undefined;
209
209
  }
210
210
  if (!Array.isArray(blockSizes)) {
211
- logger.warn(`XAttr block sizes "${blockSizes}" is not valid`);
211
+ logger.warn(`XAttr block sizes "${JSON.stringify(blockSizes)}" is not valid`);
212
212
  return undefined;
213
213
  }
214
214
  if (blockSizes.some((size) => typeof size !== 'number' || size <= 0)) {
215
- logger.warn(`XAttr block sizes "${blockSizes}" is not valid`);
215
+ logger.warn(`XAttr block sizes "${JSON.stringify(blockSizes)}" is not valid`);
216
216
  return undefined;
217
217
  }
218
218
  if (blockSizes.length === 0) {
@@ -43,14 +43,7 @@ export function initNodesModule(
43
43
  const cryptoCache = new NodesCryptoCache(telemetry.getLogger('nodes-cache'), driveCryptoCache);
44
44
  const cryptoReporter = new NodesCryptoReporter(telemetry, sharesService);
45
45
  const cryptoService = new NodesCryptoService(telemetry, driveCrypto, account, cryptoReporter);
46
- const nodesAccess = new NodesAccess(
47
- telemetry.getLogger('nodes'),
48
- api,
49
- cache,
50
- cryptoCache,
51
- cryptoService,
52
- sharesService,
53
- );
46
+ const nodesAccess = new NodesAccess(telemetry, api, cache, cryptoCache, cryptoService, sharesService);
54
47
  const nodesEventHandler = new NodesEventsHandler(telemetry.getLogger('nodes-events'), cache);
55
48
  const nodesManagement = new NodesManagement(api, cryptoCache, cryptoService, nodesAccess);
56
49
  const nodesRevisions = new NodesRevisons(telemetry.getLogger('nodes'), api, cryptoService, nodesAccess);
@@ -1,4 +1,4 @@
1
- import { getMockLogger } from '../../tests/logger';
1
+ import { getMockTelemetry } from '../../tests/telemetry';
2
2
  import { PrivateKey } from '../../crypto';
3
3
  import { DecryptionError } from '../../errors';
4
4
  import { NodeAPIService } from './apiService';
@@ -50,7 +50,7 @@ describe('nodesAccess', () => {
50
50
  getSharePrivateKey: jest.fn(),
51
51
  };
52
52
 
53
- access = new NodesAccess(getMockLogger(), apiService, cache, cryptoCache, cryptoService, shareService);
53
+ access = new NodesAccess(getMockTelemetry(), apiService, cache, cryptoCache, cryptoService, shareService);
54
54
  });
55
55
 
56
56
  describe('getNode', () => {
@@ -352,7 +352,7 @@ describe('nodesAccess', () => {
352
352
  expect(cache.setFolderChildrenLoaded).not.toHaveBeenCalled();
353
353
  });
354
354
 
355
- it.only('should return only filtered nodes from API', async () => {
355
+ it('should return only filtered nodes from API', async () => {
356
356
  cache.isFolderChildrenLoaded = jest.fn().mockResolvedValue(false);
357
357
  cache.getNode = jest.fn().mockImplementation((uid: string) => {
358
358
  if (uid === parentNode.uid) {
@@ -444,7 +444,7 @@ describe('nodesAccess', () => {
444
444
  const node1 = { uid: 'volumeId~node1', isStale: false, treeEventScopeId: 'volumeId' } as DecryptedNode;
445
445
  const node2 = { uid: 'volumeId~node2', isStale: false, treeEventScopeId: 'volumeId' } as DecryptedNode;
446
446
  const node3 = { uid: 'volumeId~node3', isStale: false, treeEventScopeId: 'volumeId' } as DecryptedNode;
447
- const node4 = { uid: 'volume~node4', isStale: false, treeEventScopeId: 'volumeId' } as DecryptedNode;
447
+ const node4 = { uid: 'volumeId~node4', isStale: false, treeEventScopeId: 'volumeId' } as DecryptedNode;
448
448
 
449
449
  it('should serve fully from cache', async () => {
450
450
  cache.iterateNodes = jest.fn().mockImplementation(async function* () {
@@ -569,7 +569,11 @@ describe('nodesAccess', () => {
569
569
  it('should get share parent keys', async () => {
570
570
  shareService.getSharePrivateKey = jest.fn(() => Promise.resolve('shareKey' as any as PrivateKey));
571
571
 
572
- const result = await access.getParentKeys({ shareId: 'shareId', parentUid: undefined });
572
+ const result = await access.getParentKeys({
573
+ uid: 'volumeId~nodeId',
574
+ shareId: 'shareId',
575
+ parentUid: undefined,
576
+ });
573
577
  expect(result).toEqual({ key: 'shareKey' });
574
578
  expect(cryptoCache.getNodeKeys).not.toHaveBeenCalled();
575
579
  });
@@ -577,7 +581,11 @@ describe('nodesAccess', () => {
577
581
  it('should get node parent keys', async () => {
578
582
  cryptoCache.getNodeKeys = jest.fn(() => Promise.resolve({ key: 'parentKey' } as any as DecryptedNodeKeys));
579
583
 
580
- const result = await access.getParentKeys({ shareId: undefined, parentUid: 'volumeId~parentNodeid' });
584
+ const result = await access.getParentKeys({
585
+ uid: 'volumeId~nodeId',
586
+ shareId: undefined,
587
+ parentUid: 'volumeId~parentNodeid',
588
+ });
581
589
  expect(result).toEqual({ key: 'parentKey' });
582
590
  expect(shareService.getSharePrivateKey).not.toHaveBeenCalled();
583
591
  });
@@ -585,7 +593,11 @@ describe('nodesAccess', () => {
585
593
  it('should get node parent keys even if share is set', async () => {
586
594
  cryptoCache.getNodeKeys = jest.fn(() => Promise.resolve({ key: 'parentKey' } as any as DecryptedNodeKeys));
587
595
 
588
- const result = await access.getParentKeys({ shareId: 'shareId', parentUid: 'volume1~parentNodeid' });
596
+ const result = await access.getParentKeys({
597
+ uid: 'volume1~nodeId',
598
+ shareId: 'shareId',
599
+ parentUid: 'volume1~parentNodeid',
600
+ });
589
601
  expect(result).toEqual({ key: 'parentKey' });
590
602
  expect(shareService.getSharePrivateKey).not.toHaveBeenCalled();
591
603
  });