@protontech/drive-sdk 0.0.12 → 0.1.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 (274) hide show
  1. package/dist/cache/index.d.ts +1 -0
  2. package/dist/cache/index.js +3 -1
  3. package/dist/cache/index.js.map +1 -1
  4. package/dist/cache/memoryCache.d.ts +1 -1
  5. package/dist/cache/nullCache.d.ts +14 -0
  6. package/dist/cache/nullCache.js +37 -0
  7. package/dist/cache/nullCache.js.map +1 -0
  8. package/dist/config.d.ts +16 -1
  9. package/dist/config.js +1 -1
  10. package/dist/config.js.map +1 -1
  11. package/dist/crypto/openPGPCrypto.js +2 -0
  12. package/dist/crypto/openPGPCrypto.js.map +1 -1
  13. package/dist/diagnostic/eventsGenerator.d.ts +14 -0
  14. package/dist/diagnostic/eventsGenerator.js +49 -0
  15. package/dist/diagnostic/eventsGenerator.js.map +1 -0
  16. package/dist/diagnostic/httpClient.d.ts +16 -0
  17. package/dist/diagnostic/httpClient.js +81 -0
  18. package/dist/diagnostic/httpClient.js.map +1 -0
  19. package/dist/diagnostic/index.d.ts +10 -0
  20. package/dist/diagnostic/index.js +35 -0
  21. package/dist/diagnostic/index.js.map +1 -0
  22. package/dist/diagnostic/integrityVerificationStream.d.ts +21 -0
  23. package/dist/diagnostic/integrityVerificationStream.js +56 -0
  24. package/dist/diagnostic/integrityVerificationStream.js.map +1 -0
  25. package/dist/diagnostic/interface.d.ts +102 -0
  26. package/dist/diagnostic/interface.js +3 -0
  27. package/dist/diagnostic/interface.js.map +1 -0
  28. package/dist/diagnostic/sdkDiagnostic.d.ts +22 -0
  29. package/dist/diagnostic/sdkDiagnostic.js +216 -0
  30. package/dist/diagnostic/sdkDiagnostic.js.map +1 -0
  31. package/dist/diagnostic/sdkDiagnosticFull.d.ts +18 -0
  32. package/dist/diagnostic/sdkDiagnosticFull.js +35 -0
  33. package/dist/diagnostic/sdkDiagnosticFull.js.map +1 -0
  34. package/dist/diagnostic/telemetry.d.ts +25 -0
  35. package/dist/diagnostic/telemetry.js +70 -0
  36. package/dist/diagnostic/telemetry.js.map +1 -0
  37. package/dist/diagnostic/zipGenerators.d.ts +9 -0
  38. package/dist/diagnostic/zipGenerators.js +64 -0
  39. package/dist/diagnostic/zipGenerators.js.map +1 -0
  40. package/dist/diagnostic/zipGenerators.test.js +144 -0
  41. package/dist/diagnostic/zipGenerators.test.js.map +1 -0
  42. package/dist/errors.d.ts +8 -3
  43. package/dist/errors.js +11 -4
  44. package/dist/errors.js.map +1 -1
  45. package/dist/interface/config.d.ts +26 -0
  46. package/dist/interface/config.js +3 -0
  47. package/dist/interface/config.js.map +1 -0
  48. package/dist/interface/download.d.ts +2 -2
  49. package/dist/interface/events.d.ts +60 -20
  50. package/dist/interface/events.js +11 -1
  51. package/dist/interface/events.js.map +1 -1
  52. package/dist/interface/httpClient.d.ts +0 -14
  53. package/dist/interface/index.d.ts +9 -5
  54. package/dist/interface/index.js +2 -1
  55. package/dist/interface/index.js.map +1 -1
  56. package/dist/interface/nodes.d.ts +21 -1
  57. package/dist/interface/nodes.js +11 -0
  58. package/dist/interface/nodes.js.map +1 -1
  59. package/dist/interface/sharing.d.ts +1 -0
  60. package/dist/interface/upload.d.ts +57 -3
  61. package/dist/internal/apiService/driveTypes.d.ts +1341 -465
  62. package/dist/internal/apiService/errors.js +2 -2
  63. package/dist/internal/apiService/errors.js.map +1 -1
  64. package/dist/internal/apiService/transformers.js +2 -0
  65. package/dist/internal/apiService/transformers.js.map +1 -1
  66. package/dist/internal/asyncIteratorMap.d.ts +15 -0
  67. package/dist/internal/asyncIteratorMap.js +59 -0
  68. package/dist/internal/asyncIteratorMap.js.map +1 -0
  69. package/dist/internal/asyncIteratorMap.test.js +120 -0
  70. package/dist/internal/asyncIteratorMap.test.js.map +1 -0
  71. package/dist/internal/download/apiService.js +32 -31
  72. package/dist/internal/download/apiService.js.map +1 -1
  73. package/dist/internal/download/fileDownloader.d.ts +2 -2
  74. package/dist/internal/download/fileDownloader.js.map +1 -1
  75. package/dist/internal/events/apiService.d.ts +4 -6
  76. package/dist/internal/events/apiService.js +15 -22
  77. package/dist/internal/events/apiService.js.map +1 -1
  78. package/dist/internal/events/coreEventManager.d.ts +7 -10
  79. package/dist/internal/events/coreEventManager.js +19 -36
  80. package/dist/internal/events/coreEventManager.js.map +1 -1
  81. package/dist/internal/events/coreEventManager.test.d.ts +1 -0
  82. package/dist/internal/events/coreEventManager.test.js +87 -0
  83. package/dist/internal/events/coreEventManager.test.js.map +1 -0
  84. package/dist/internal/events/eventManager.d.ts +11 -36
  85. package/dist/internal/events/eventManager.js +59 -105
  86. package/dist/internal/events/eventManager.js.map +1 -1
  87. package/dist/internal/events/eventManager.test.js +167 -82
  88. package/dist/internal/events/eventManager.test.js.map +1 -1
  89. package/dist/internal/events/index.d.ts +13 -33
  90. package/dist/internal/events/index.js +56 -72
  91. package/dist/internal/events/index.js.map +1 -1
  92. package/dist/internal/events/interface.d.ts +59 -14
  93. package/dist/internal/events/interface.js +13 -3
  94. package/dist/internal/events/interface.js.map +1 -1
  95. package/dist/internal/events/volumeEventManager.d.ts +7 -17
  96. package/dist/internal/events/volumeEventManager.js +58 -45
  97. package/dist/internal/events/volumeEventManager.js.map +1 -1
  98. package/dist/internal/events/volumeEventManager.test.d.ts +1 -0
  99. package/dist/internal/events/volumeEventManager.test.js +203 -0
  100. package/dist/internal/events/volumeEventManager.test.js.map +1 -0
  101. package/dist/internal/nodes/apiService.d.ts +2 -2
  102. package/dist/internal/nodes/apiService.js +16 -6
  103. package/dist/internal/nodes/apiService.js.map +1 -1
  104. package/dist/internal/nodes/apiService.test.js +30 -8
  105. package/dist/internal/nodes/apiService.test.js.map +1 -1
  106. package/dist/internal/nodes/cache.d.ts +10 -1
  107. package/dist/internal/nodes/cache.js +18 -0
  108. package/dist/internal/nodes/cache.js.map +1 -1
  109. package/dist/internal/nodes/cache.test.js +1 -0
  110. package/dist/internal/nodes/cache.test.js.map +1 -1
  111. package/dist/internal/nodes/cryptoService.d.ts +1 -1
  112. package/dist/internal/nodes/cryptoService.js.map +1 -1
  113. package/dist/internal/nodes/cryptoService.test.js +34 -0
  114. package/dist/internal/nodes/cryptoService.test.js.map +1 -1
  115. package/dist/internal/nodes/events.d.ts +7 -83
  116. package/dist/internal/nodes/events.js +43 -217
  117. package/dist/internal/nodes/events.js.map +1 -1
  118. package/dist/internal/nodes/events.test.js +27 -277
  119. package/dist/internal/nodes/events.test.js.map +1 -1
  120. package/dist/internal/nodes/index.d.ts +3 -4
  121. package/dist/internal/nodes/index.js +5 -5
  122. package/dist/internal/nodes/index.js.map +1 -1
  123. package/dist/internal/nodes/interface.d.ts +3 -1
  124. package/dist/internal/nodes/nodesAccess.d.ts +15 -0
  125. package/dist/internal/nodes/nodesAccess.js +65 -7
  126. package/dist/internal/nodes/nodesAccess.js.map +1 -1
  127. package/dist/internal/nodes/nodesAccess.test.js +132 -93
  128. package/dist/internal/nodes/nodesAccess.test.js.map +1 -1
  129. package/dist/internal/nodes/nodesManagement.d.ts +1 -3
  130. package/dist/internal/nodes/nodesManagement.js +12 -26
  131. package/dist/internal/nodes/nodesManagement.js.map +1 -1
  132. package/dist/internal/nodes/nodesManagement.test.js +35 -14
  133. package/dist/internal/nodes/nodesManagement.test.js.map +1 -1
  134. package/dist/internal/shares/cache.d.ts +2 -0
  135. package/dist/internal/shares/cache.js +2 -0
  136. package/dist/internal/shares/cache.js.map +1 -1
  137. package/dist/internal/shares/manager.d.ts +1 -0
  138. package/dist/internal/shares/manager.js +3 -0
  139. package/dist/internal/shares/manager.js.map +1 -1
  140. package/dist/internal/sharing/apiService.js +20 -2
  141. package/dist/internal/sharing/apiService.js.map +1 -1
  142. package/dist/internal/sharing/cryptoService.js +1 -0
  143. package/dist/internal/sharing/cryptoService.js.map +1 -1
  144. package/dist/internal/sharing/events.d.ts +23 -55
  145. package/dist/internal/sharing/events.js +46 -138
  146. package/dist/internal/sharing/events.js.map +1 -1
  147. package/dist/internal/sharing/events.test.js +77 -180
  148. package/dist/internal/sharing/events.test.js.map +1 -1
  149. package/dist/internal/sharing/index.d.ts +4 -5
  150. package/dist/internal/sharing/index.js +5 -5
  151. package/dist/internal/sharing/index.js.map +1 -1
  152. package/dist/internal/sharing/interface.d.ts +3 -0
  153. package/dist/internal/sharing/sharingManagement.d.ts +2 -3
  154. package/dist/internal/sharing/sharingManagement.js +7 -9
  155. package/dist/internal/sharing/sharingManagement.js.map +1 -1
  156. package/dist/internal/sharing/sharingManagement.test.js +9 -39
  157. package/dist/internal/sharing/sharingManagement.test.js.map +1 -1
  158. package/dist/internal/upload/apiService.d.ts +2 -3
  159. package/dist/internal/upload/apiService.js +7 -4
  160. package/dist/internal/upload/apiService.js.map +1 -1
  161. package/dist/internal/upload/fileUploader.d.ts +49 -53
  162. package/dist/internal/upload/fileUploader.js +91 -395
  163. package/dist/internal/upload/fileUploader.js.map +1 -1
  164. package/dist/internal/upload/fileUploader.test.js +38 -292
  165. package/dist/internal/upload/fileUploader.test.js.map +1 -1
  166. package/dist/internal/upload/index.d.ts +5 -5
  167. package/dist/internal/upload/index.js +23 -44
  168. package/dist/internal/upload/index.js.map +1 -1
  169. package/dist/internal/upload/interface.d.ts +2 -0
  170. package/dist/internal/upload/manager.d.ts +6 -6
  171. package/dist/internal/upload/manager.js +32 -66
  172. package/dist/internal/upload/manager.js.map +1 -1
  173. package/dist/internal/upload/manager.test.js +100 -117
  174. package/dist/internal/upload/manager.test.js.map +1 -1
  175. package/dist/internal/upload/streamUploader.d.ts +62 -0
  176. package/dist/internal/upload/streamUploader.js +440 -0
  177. package/dist/internal/upload/streamUploader.js.map +1 -0
  178. package/dist/internal/upload/streamUploader.test.d.ts +1 -0
  179. package/dist/internal/upload/streamUploader.test.js +358 -0
  180. package/dist/internal/upload/streamUploader.test.js.map +1 -0
  181. package/dist/protonDriveClient.d.ts +22 -165
  182. package/dist/protonDriveClient.js +27 -191
  183. package/dist/protonDriveClient.js.map +1 -1
  184. package/dist/protonDrivePhotosClient.js +3 -2
  185. package/dist/protonDrivePhotosClient.js.map +1 -1
  186. package/package.json +4 -4
  187. package/src/cache/index.ts +1 -0
  188. package/src/cache/memoryCache.ts +1 -1
  189. package/src/cache/nullCache.ts +38 -0
  190. package/src/config.ts +17 -2
  191. package/src/crypto/openPGPCrypto.ts +2 -0
  192. package/src/diagnostic/eventsGenerator.ts +48 -0
  193. package/src/diagnostic/httpClient.ts +80 -0
  194. package/src/diagnostic/index.ts +38 -0
  195. package/src/diagnostic/integrityVerificationStream.ts +56 -0
  196. package/src/diagnostic/interface.ts +158 -0
  197. package/src/diagnostic/sdkDiagnostic.ts +238 -0
  198. package/src/diagnostic/sdkDiagnosticFull.ts +40 -0
  199. package/src/diagnostic/telemetry.ts +71 -0
  200. package/src/diagnostic/zipGenerators.test.ts +177 -0
  201. package/src/diagnostic/zipGenerators.ts +70 -0
  202. package/src/errors.ts +13 -4
  203. package/src/interface/config.ts +28 -0
  204. package/src/interface/download.ts +2 -2
  205. package/src/interface/events.ts +66 -21
  206. package/src/interface/httpClient.ts +0 -16
  207. package/src/interface/index.ts +9 -5
  208. package/src/interface/nodes.ts +32 -12
  209. package/src/interface/sharing.ts +1 -0
  210. package/src/interface/upload.ts +59 -3
  211. package/src/internal/apiService/driveTypes.ts +1341 -465
  212. package/src/internal/apiService/errors.ts +3 -2
  213. package/src/internal/apiService/transformers.ts +2 -0
  214. package/src/internal/asyncIteratorMap.test.ts +150 -0
  215. package/src/internal/asyncIteratorMap.ts +64 -0
  216. package/src/internal/download/apiService.ts +11 -8
  217. package/src/internal/download/fileDownloader.ts +2 -2
  218. package/src/internal/events/apiService.ts +25 -28
  219. package/src/internal/events/coreEventManager.test.ts +101 -0
  220. package/src/internal/events/coreEventManager.ts +20 -45
  221. package/src/internal/events/eventManager.test.ts +201 -88
  222. package/src/internal/events/eventManager.ts +69 -115
  223. package/src/internal/events/index.ts +54 -84
  224. package/src/internal/events/interface.ts +70 -15
  225. package/src/internal/events/volumeEventManager.test.ts +243 -0
  226. package/src/internal/events/volumeEventManager.ts +55 -53
  227. package/src/internal/nodes/apiService.test.ts +36 -7
  228. package/src/internal/nodes/apiService.ts +19 -7
  229. package/src/internal/nodes/cache.test.ts +1 -0
  230. package/src/internal/nodes/cache.ts +21 -2
  231. package/src/internal/nodes/cryptoService.test.ts +38 -0
  232. package/src/internal/nodes/cryptoService.ts +1 -1
  233. package/src/internal/nodes/events.test.ts +29 -335
  234. package/src/internal/nodes/events.ts +45 -253
  235. package/src/internal/nodes/index.ts +6 -8
  236. package/src/internal/nodes/interface.ts +6 -3
  237. package/src/internal/nodes/nodesAccess.test.ts +133 -91
  238. package/src/internal/nodes/nodesAccess.ts +70 -8
  239. package/src/internal/nodes/nodesManagement.test.ts +39 -15
  240. package/src/internal/nodes/nodesManagement.ts +12 -30
  241. package/src/internal/shares/cache.ts +4 -2
  242. package/src/internal/shares/manager.ts +9 -5
  243. package/src/internal/sharing/apiService.ts +25 -2
  244. package/src/internal/sharing/cache.ts +1 -1
  245. package/src/internal/sharing/cryptoService.ts +1 -0
  246. package/src/internal/sharing/events.test.ts +89 -195
  247. package/src/internal/sharing/events.ts +42 -156
  248. package/src/internal/sharing/index.ts +6 -9
  249. package/src/internal/sharing/interface.ts +6 -2
  250. package/src/internal/sharing/sharingManagement.test.ts +10 -40
  251. package/src/internal/sharing/sharingManagement.ts +7 -11
  252. package/src/internal/upload/apiService.ts +5 -6
  253. package/src/internal/upload/fileUploader.test.ts +46 -376
  254. package/src/internal/upload/fileUploader.ts +114 -494
  255. package/src/internal/upload/index.ts +30 -54
  256. package/src/internal/upload/interface.ts +2 -0
  257. package/src/internal/upload/manager.test.ts +107 -124
  258. package/src/internal/upload/manager.ts +48 -80
  259. package/src/internal/upload/streamUploader.test.ts +468 -0
  260. package/src/internal/upload/streamUploader.ts +550 -0
  261. package/src/protonDriveClient.ts +80 -248
  262. package/src/protonDrivePhotosClient.ts +4 -3
  263. package/dist/internal/events/cache.d.ts +0 -28
  264. package/dist/internal/events/cache.js +0 -67
  265. package/dist/internal/events/cache.js.map +0 -1
  266. package/dist/internal/events/cache.test.js +0 -43
  267. package/dist/internal/events/cache.test.js.map +0 -1
  268. package/dist/internal/nodes/index.test.js +0 -112
  269. package/dist/internal/nodes/index.test.js.map +0 -1
  270. package/src/internal/events/cache.test.ts +0 -47
  271. package/src/internal/events/cache.ts +0 -80
  272. package/src/internal/nodes/index.test.ts +0 -135
  273. /package/dist/{internal/events/cache.test.d.ts → diagnostic/zipGenerators.test.d.ts} +0 -0
  274. /package/dist/internal/{nodes/index.test.d.ts → asyncIteratorMap.test.d.ts} +0 -0
@@ -22,7 +22,7 @@ describe('nodesAccess', () => {
22
22
  apiService = {
23
23
  getNode: jest.fn(),
24
24
  iterateNodes: jest.fn().mockImplementation(async function* (uids: string[]) {
25
- yield* uids.map((uid => ({ uid, parentUid: 'parentUid' } as EncryptedNode)));
25
+ yield* uids.map((uid => ({ uid, parentUid: 'volumeId~parentNodeId' } as EncryptedNode)));
26
26
  }),
27
27
  iterateChildrenNodeUids: jest.fn(),
28
28
  }
@@ -46,6 +46,7 @@ describe('nodesAccess', () => {
46
46
  }
47
47
  // @ts-expect-error No need to implement all methods for mocking
48
48
  shareService = {
49
+ getMyFilesIDs: jest.fn().mockResolvedValue({ volumeId: 'volumeId' }),
49
50
  getSharePrivateKey: jest.fn(),
50
51
  };
51
52
 
@@ -54,49 +55,51 @@ describe('nodesAccess', () => {
54
55
 
55
56
  describe('getNode', () => {
56
57
  it('should get node from cache', async () => {
57
- const node = { uid: 'nodeId', isStale: false } as DecryptedNode;
58
+ const node = { uid: 'volumeId~nodeId', isStale: false } as DecryptedNode;
58
59
  cache.getNode = jest.fn(() => Promise.resolve(node));
59
60
 
60
- const result = await access.getNode('nodeId');
61
+ const result = await access.getNode('volumeId~nodeId');
61
62
  expect(result).toBe(node);
62
63
  expect(apiService.getNode).not.toHaveBeenCalled();
63
64
  });
64
65
 
65
- it('should get node from API when cahce is stale', async () => {
66
- const encryptedNode = { uid: 'nodeId', parentUid: 'parentUid' } as EncryptedNode;
67
- const decryptedUnparsedNode = { uid: 'nodeId', parentUid: 'parentUid', name: { ok: true, value: 'name' } } as DecryptedUnparsedNode;
66
+ it('should get node from API when cache is stale', async () => {
67
+ const encryptedNode = { uid: 'volumeId~nodeId', parentUid: 'volumeId~parentNodeid' } as EncryptedNode;
68
+ const decryptedUnparsedNode = { uid: 'volumeId~nodeId', parentUid: 'volumeId~parentNodeid', name: { ok: true, value: 'name' } } as DecryptedUnparsedNode;
68
69
  const decryptedNode = {
69
70
  ...decryptedUnparsedNode,
70
71
  name: { ok: true, value: 'name' },
71
72
  isStale: false,
72
73
  activeRevision: undefined,
73
74
  folder: undefined,
75
+ treeEventScopeId: "volumeId",
74
76
  } as DecryptedNode;
75
77
  const decryptedKeys = { key: 'key' } as any as DecryptedNodeKeys;
76
78
 
77
- cache.getNode = jest.fn(() => Promise.resolve({ uid: 'nodeId', isStale: true } as DecryptedNode));
79
+ cache.getNode = jest.fn(() => Promise.resolve({ uid: 'volumeId~nodeId', isStale: true } as DecryptedNode));
78
80
  apiService.getNode = jest.fn(() => Promise.resolve(encryptedNode));
79
81
  cryptoCache.getNodeKeys = jest.fn(() => Promise.resolve({ key: 'parentKey' } as any as DecryptedNodeKeys));
80
82
  cryptoService.decryptNode = jest.fn(() => Promise.resolve({ node: decryptedUnparsedNode, keys: decryptedKeys }));
81
83
 
82
- const result = await access.getNode('nodeId');
84
+ const result = await access.getNode('volumeId~nodeId');
83
85
  expect(result).toEqual(decryptedNode);
84
- expect(apiService.getNode).toHaveBeenCalledWith('nodeId');
85
- expect(cryptoCache.getNodeKeys).toHaveBeenCalledWith('parentUid');
86
+ expect(apiService.getNode).toHaveBeenCalledWith('volumeId~nodeId', 'volumeId');
87
+ expect(cryptoCache.getNodeKeys).toHaveBeenCalledWith('volumeId~parentNodeid');
86
88
  expect(cryptoService.decryptNode).toHaveBeenCalledWith(encryptedNode, 'parentKey');
87
89
  expect(cache.setNode).toHaveBeenCalledWith(decryptedNode);
88
- expect(cryptoCache.setNodeKeys).toHaveBeenCalledWith('nodeId', decryptedKeys);
90
+ expect(cryptoCache.setNodeKeys).toHaveBeenCalledWith('volumeId~nodeId', decryptedKeys);
89
91
  });
90
92
 
91
93
  it('should get node from API missing cache', async () => {
92
- const encryptedNode = { uid: 'nodeId', parentUid: 'parentUid' } as EncryptedNode;
93
- const decryptedUnparsedNode = { uid: 'nodeId', parentUid: 'parentUid', name: { ok: true, value: 'name' } } as DecryptedUnparsedNode;
94
+ const encryptedNode = { uid: 'volumeId~nodeId', parentUid: 'volumeId~parentNodeid' } as EncryptedNode;
95
+ const decryptedUnparsedNode = { uid: 'volumeId~nodeId', parentUid: 'volumeId~parentNodeid', name: { ok: true, value: 'name' } } as DecryptedUnparsedNode;
94
96
  const decryptedNode = {
95
97
  ...decryptedUnparsedNode,
96
98
  name: { ok: true, value: 'name' },
97
99
  isStale: false,
98
100
  activeRevision: undefined,
99
101
  folder: undefined,
102
+ treeEventScopeId: 'volumeId',
100
103
  } as DecryptedNode;
101
104
  const decryptedKeys = { key: 'key' } as any as DecryptedNodeKeys;
102
105
 
@@ -105,21 +108,22 @@ describe('nodesAccess', () => {
105
108
  cryptoCache.getNodeKeys = jest.fn(() => Promise.resolve({ key: 'parentKey' } as any as DecryptedNodeKeys));
106
109
  cryptoService.decryptNode = jest.fn(() => Promise.resolve({ node: decryptedUnparsedNode, keys: decryptedKeys }));
107
110
 
108
- const result = await access.getNode('nodeId');
111
+ const result = await access.getNode('volumeId~nodeId');
109
112
  expect(result).toEqual(decryptedNode);
110
- expect(apiService.getNode).toHaveBeenCalledWith('nodeId');
111
- expect(cryptoCache.getNodeKeys).toHaveBeenCalledWith('parentUid');
113
+ expect(apiService.getNode).toHaveBeenCalledWith('volumeId~nodeId', 'volumeId');
114
+ expect(cryptoCache.getNodeKeys).toHaveBeenCalledWith('volumeId~parentNodeid');
112
115
  expect(cryptoService.decryptNode).toHaveBeenCalledWith(encryptedNode, 'parentKey');
113
116
  expect(cache.setNode).toHaveBeenCalledWith(decryptedNode);
114
- expect(cryptoCache.setNodeKeys).toHaveBeenCalledWith('nodeId', decryptedKeys);
117
+ expect(cryptoCache.setNodeKeys).toHaveBeenCalledWith('volumeId~nodeId', decryptedKeys);
115
118
  });
116
119
 
117
120
  it('should validate node name', async () => {
118
- const encryptedNode = { uid: 'nodeId', parentUid: 'parentUid' } as EncryptedNode;
119
- const decryptedUnparsedNode = { uid: 'nodeId', parentUid: 'parentUid', name: { ok: true, value: 'foo/bar' } } as DecryptedUnparsedNode;
121
+ const encryptedNode = { uid: 'volumeId~nodeId', parentUid: 'volumeId~parentNodeid' } as EncryptedNode;
122
+ const decryptedUnparsedNode = { uid: 'volumeId~nodeId', parentUid: 'volumeId~parentNodeid', name: { ok: true, value: 'foo/bar' } } as DecryptedUnparsedNode;
120
123
  const decryptedNode = {
121
124
  ...decryptedUnparsedNode,
122
125
  name: { ok: false, error: { name: 'foo/bar', error: "Name must not contain the character '/'" } },
126
+ treeEventScopeId: 'volumeId',
123
127
  } as DecryptedNode;
124
128
  const decryptedKeys = { key: 'key' } as any as DecryptedNodeKeys;
125
129
 
@@ -128,7 +132,7 @@ describe('nodesAccess', () => {
128
132
  cryptoCache.getNodeKeys = jest.fn(() => Promise.resolve({ key: 'parentKey' } as any as DecryptedNodeKeys));
129
133
  cryptoService.decryptNode = jest.fn(() => Promise.resolve({ node: decryptedUnparsedNode, keys: decryptedKeys }));
130
134
 
131
- const result = await access.getNode('nodeId');
135
+ const result = await access.getNode('volumeId~nodeId');
132
136
  expect(result).toMatchObject(decryptedNode);
133
137
  });
134
138
  });
@@ -143,11 +147,11 @@ describe('nodesAccess', () => {
143
147
  });
144
148
 
145
149
  describe('iterateChildren', () => {
146
- const parentNode = { uid: 'parentUid', isStale: false } as DecryptedNode;
147
- const node1 = { uid: 'node1', isStale: false } as DecryptedNode;
148
- const node2 = { uid: 'node2', isStale: false } as DecryptedNode;
149
- const node3 = { uid: 'node3', isStale: false } as DecryptedNode;
150
- const node4 = { uid: 'node4', isStale: false } as DecryptedNode;
150
+ const parentNode = { uid: 'volumeId~parentNodeid', isStale: false } as DecryptedNode;
151
+ const node1 = { uid: 'volumeId~node1', isStale: false } as DecryptedNode;
152
+ const node2 = { uid: 'volumeId~node2', isStale: false } as DecryptedNode;
153
+ const node3 = { uid: 'volumeId~node3', isStale: false } as DecryptedNode;
154
+ const node4 = { uid: 'volumeId~node4', isStale: false } as DecryptedNode;
151
155
 
152
156
  beforeEach(() => {
153
157
  cache.getNode = jest.fn().mockResolvedValue(parentNode);
@@ -162,7 +166,7 @@ describe('nodesAccess', () => {
162
166
  yield { ok: true, node: node4 };
163
167
  });
164
168
 
165
- const result = await Array.fromAsync(access.iterateFolderChildren('parentUid'));
169
+ const result = await Array.fromAsync(access.iterateFolderChildren('volumeId~parentNodeid'));
166
170
  expect(result).toMatchObject([node1, node2, node3, node4]);
167
171
  expect(apiService.iterateChildrenNodeUids).not.toHaveBeenCalled();
168
172
  expect(apiService.iterateNodes).not.toHaveBeenCalled();
@@ -177,9 +181,9 @@ describe('nodesAccess', () => {
177
181
  yield { ok: true, uid: node4.uid, node: node4 };
178
182
  });
179
183
 
180
- const result = await Array.fromAsync(access.iterateFolderChildren('parentUid'));
184
+ const result = await Array.fromAsync(access.iterateFolderChildren('volumeId~parentNodeid'));
181
185
  expect(result).toMatchObject([node1, node4, node2, node3]);
182
- expect(apiService.iterateNodes).toHaveBeenCalledWith(['node2', 'node3'], undefined);
186
+ expect(apiService.iterateNodes).toHaveBeenCalledWith([node2.uid, node3.uid], 'volumeId', undefined);
183
187
  expect(cryptoService.decryptNode).toHaveBeenCalledTimes(2);
184
188
  expect(cache.setNode).toHaveBeenCalledTimes(2);
185
189
  expect(cryptoCache.setNodeKeys).toHaveBeenCalledTimes(2);
@@ -187,26 +191,26 @@ describe('nodesAccess', () => {
187
191
 
188
192
  it('should load children uids and serve nodes from cache', async () => {
189
193
  apiService.iterateChildrenNodeUids = jest.fn().mockImplementation(async function* () {
190
- yield 'node1';
191
- yield 'node2';
192
- yield 'node3';
193
- yield 'node4';
194
+ yield node1.uid;
195
+ yield node2.uid;
196
+ yield node3.uid;
197
+ yield node4.uid;
194
198
  });
195
199
  cache.getNode = jest.fn().mockImplementation((uid: string) => ({ uid, isStale: false }));
196
200
 
197
- const result = await Array.fromAsync(access.iterateFolderChildren('parentUid'));
201
+ const result = await Array.fromAsync(access.iterateFolderChildren('volumeId~parentNodeid'));
198
202
  expect(result).toMatchObject([node1, node2, node3, node4]);
199
- expect(apiService.iterateChildrenNodeUids).toHaveBeenCalledWith('parentUid', undefined);
203
+ expect(apiService.iterateChildrenNodeUids).toHaveBeenCalledWith('volumeId~parentNodeid', undefined);
200
204
  expect(apiService.iterateNodes).not.toHaveBeenCalled();
201
- expect(cache.setFolderChildrenLoaded).toHaveBeenCalledWith('parentUid');
205
+ expect(cache.setFolderChildrenLoaded).toHaveBeenCalledWith('volumeId~parentNodeid');
202
206
  });
203
207
 
204
208
  it('should load from API', async () => {
205
209
  apiService.iterateChildrenNodeUids = jest.fn().mockImplementation(async function* () {
206
- yield 'node1';
207
- yield 'node2';
208
- yield 'node3';
209
- yield 'node4';
210
+ yield node1.uid;
211
+ yield node2.uid;
212
+ yield node3.uid;
213
+ yield node4.uid;
210
214
  });
211
215
  cache.getNode = jest.fn().mockImplementation((uid: string) => {
212
216
  if (uid === parentNode.uid) {
@@ -215,21 +219,21 @@ describe('nodesAccess', () => {
215
219
  throw new Error('Entity not found');
216
220
  });
217
221
 
218
- const result = await Array.fromAsync(access.iterateFolderChildren('parentUid'));
222
+ const result = await Array.fromAsync(access.iterateFolderChildren('volumeId~parentNodeid'));
219
223
  expect(result).toMatchObject([node1, node2, node3, node4]);
220
- expect(apiService.iterateChildrenNodeUids).toHaveBeenCalledWith('parentUid', undefined);
221
- expect(apiService.iterateNodes).toHaveBeenCalledWith(['node1', 'node2', 'node3', 'node4'], undefined);
224
+ expect(apiService.iterateChildrenNodeUids).toHaveBeenCalledWith('volumeId~parentNodeid', undefined);
225
+ expect(apiService.iterateNodes).toHaveBeenCalledWith(['volumeId~node1', 'volumeId~node2', 'volumeId~node3', 'volumeId~node4'], 'volumeId', undefined);
222
226
  expect(cryptoService.decryptNode).toHaveBeenCalledTimes(4);
223
227
  expect(cache.setNode).toHaveBeenCalledTimes(4);
224
228
  expect(cryptoCache.setNodeKeys).toHaveBeenCalledTimes(4);
225
- expect(cache.setFolderChildrenLoaded).toHaveBeenCalledWith('parentUid');
229
+ expect(cache.setFolderChildrenLoaded).toHaveBeenCalledWith('volumeId~parentNodeid');
226
230
  });
227
231
 
228
232
  it('should remove from cache if missing on API', async () => {
229
233
  apiService.iterateChildrenNodeUids = jest.fn().mockImplementation(async function* () {
230
- yield 'node1';
231
- yield 'node2';
232
- yield 'node3';
234
+ yield node1.uid;
235
+ yield node2.uid;
236
+ yield node3.uid;
233
237
  });
234
238
  cache.getNode = jest.fn().mockImplementation((uid: string) => {
235
239
  if (uid === parentNode.uid) {
@@ -242,16 +246,16 @@ describe('nodesAccess', () => {
242
246
  yield* uids.slice(1).map((uid) => ({ uid, parentUid: parentNode.uid } as EncryptedNode));
243
247
  });
244
248
 
245
- const result = await Array.fromAsync(access.iterateFolderChildren('parentUid'));
249
+ const result = await Array.fromAsync(access.iterateFolderChildren('volumeId~parentNodeid'));
246
250
  expect(result).toMatchObject([node2, node3]);
247
- expect(cache.removeNodes).toHaveBeenCalledWith(['node1']);
251
+ expect(cache.removeNodes).toHaveBeenCalledWith([node1.uid]);
248
252
  });
249
253
 
250
254
  it('should yield all decryptable children before throwing error', async () => {
251
255
  apiService.iterateChildrenNodeUids = jest.fn().mockImplementation(async function* () {
252
- yield 'node1';
253
- yield 'node2';
254
- yield 'node3';
256
+ yield 'volumeId~node1';
257
+ yield 'volumeId~node2';
258
+ yield 'volumeId~node3';
255
259
  });
256
260
  cache.getNode = jest.fn().mockImplementation((uid: string) => {
257
261
  if (uid === parentNode.uid) {
@@ -260,7 +264,7 @@ describe('nodesAccess', () => {
260
264
  throw new Error('Entity not found');
261
265
  });
262
266
  cryptoService.decryptNode = jest.fn().mockImplementation((encryptedNode: EncryptedNode) => {
263
- if (encryptedNode.uid === 'node2') {
267
+ if (encryptedNode.uid === 'volumeId~node2') {
264
268
  throw new DecryptionError('Decryption failed');
265
269
  }
266
270
  return Promise.resolve({
@@ -269,11 +273,11 @@ describe('nodesAccess', () => {
269
273
  });
270
274
  });
271
275
 
272
- const generator = access.iterateFolderChildren('parentUid');
276
+ const generator = access.iterateFolderChildren('volumeId~parentNodeid');
273
277
  const node1 = await generator.next();
274
- expect(node1.value).toMatchObject({ uid: 'node1' });
278
+ expect(node1.value).toMatchObject({ uid: 'volumeId~node1' });
275
279
  const node2 = await generator.next();
276
- expect(node2.value).toMatchObject({ uid: 'node3' });
280
+ expect(node2.value).toMatchObject({ uid: 'volumeId~node3' });
277
281
  const node3 = generator.next();
278
282
  await expect(node3).rejects.toThrow('Failed to decrypt some nodes');
279
283
  try {
@@ -288,10 +292,10 @@ describe('nodesAccess', () => {
288
292
 
289
293
  describe('iterateTrashedNodes', () => {
290
294
  const volumeId = 'volumeId';
291
- const node1 = { uid: 'node1', isStale: false } as DecryptedNode;
292
- const node2 = { uid: 'node2', isStale: false } as DecryptedNode;
293
- const node3 = { uid: 'node3', isStale: false } as DecryptedNode;
294
- const node4 = { uid: 'node4', isStale: false } as DecryptedNode;
295
+ const node1 = { uid: 'volumeId~node1', isStale: false } as DecryptedNode;
296
+ const node2 = { uid: 'volumeId~node2', isStale: false } as DecryptedNode;
297
+ const node3 = { uid: 'volumeId~node3', isStale: false } as DecryptedNode;
298
+ const node4 = { uid: 'volumeId~node4', isStale: false } as DecryptedNode;
295
299
 
296
300
  beforeEach(() => {
297
301
  shareService.getMyFilesIDs = jest.fn().mockResolvedValue({ volumeId });
@@ -320,7 +324,7 @@ describe('nodesAccess', () => {
320
324
  const result = await Array.fromAsync(access.iterateTrashedNodes());
321
325
  expect(result).toMatchObject([node1, node2, node3, node4]);
322
326
  expect(apiService.iterateTrashedNodeUids).toHaveBeenCalledWith(volumeId, undefined);
323
- expect(apiService.iterateNodes).toHaveBeenCalledWith(['node1', 'node2', 'node3', 'node4'], undefined);
327
+ expect(apiService.iterateNodes).toHaveBeenCalledWith(['volumeId~node1', 'volumeId~node2', 'volumeId~node3', 'volumeId~node4'], volumeId, undefined);
324
328
  expect(cryptoService.decryptNode).toHaveBeenCalledTimes(4);
325
329
  expect(cache.setNode).toHaveBeenCalledTimes(4);
326
330
  expect(cryptoCache.setNodeKeys).toHaveBeenCalledTimes(4);
@@ -332,20 +336,20 @@ describe('nodesAccess', () => {
332
336
  });
333
337
  apiService.iterateNodes = jest.fn().mockImplementation(async function* (uids: string[]) {
334
338
  // Skip first node - make it missing.
335
- yield* uids.slice(1).map((uid) => ({ uid, parentUid: 'parentUid' } as EncryptedNode));
339
+ yield* uids.slice(1).map((uid) => ({ uid, parentUid: 'volumeId~parentNodeid' } as EncryptedNode));
336
340
  });
337
341
 
338
342
  const result = await Array.fromAsync(access.iterateTrashedNodes());
339
343
  expect(result).toMatchObject([node2, node3, node4]);
340
- expect(cache.removeNodes).toHaveBeenCalledWith(['node1']);
344
+ expect(cache.removeNodes).toHaveBeenCalledWith(['volumeId~node1']);
341
345
  });
342
346
  });
343
347
 
344
348
  describe('iterateNodes', () => {
345
- const node1 = { uid: 'node1', isStale: false } as DecryptedNode;
346
- const node2 = { uid: 'node2', isStale: false } as DecryptedNode;
347
- const node3 = { uid: 'node3', isStale: false } as DecryptedNode;
348
- const node4 = { uid: 'node4', isStale: false } as DecryptedNode;
349
+ const node1 = { uid: 'volumeId~node1', isStale: false, treeEventScopeId: 'volumeId' } as DecryptedNode;
350
+ const node2 = { uid: 'volumeId~node2', isStale: false, treeEventScopeId: 'volumeId' } as DecryptedNode;
351
+ const node3 = { uid: 'volumeId~node3', isStale: false, treeEventScopeId: 'volumeId' } as DecryptedNode;
352
+ const node4 = { uid: 'volume~node4', isStale: false, treeEventScopeId: 'volumeId' } as DecryptedNode;
349
353
 
350
354
  it('should serve fully from cache', async () => {
351
355
  cache.iterateNodes = jest.fn().mockImplementation(async function* () {
@@ -355,7 +359,7 @@ describe('nodesAccess', () => {
355
359
  yield { ok: true, node: node4 };
356
360
  });
357
361
 
358
- const result = await Array.fromAsync(access.iterateNodes(['node1', 'node2', 'node3', 'node4']));
362
+ const result = await Array.fromAsync(access.iterateNodes(['volumeId~node1', 'volumeId~node2', 'volumeId~node3', 'volumeId~node4']));
359
363
  expect(result).toMatchObject([node1, node2, node3, node4]);
360
364
  expect(apiService.iterateNodes).not.toHaveBeenCalled();
361
365
  });
@@ -363,52 +367,55 @@ describe('nodesAccess', () => {
363
367
  it('should load from API', async () => {
364
368
  cache.iterateNodes = jest.fn().mockImplementation(async function* () {
365
369
  yield { ok: true, node: node1 };
366
- yield { ok: false, uid: 'node2' };
367
- yield { ok: false, uid: 'node3' };
370
+ yield { ok: false, uid: 'volumeId~node2' };
371
+ yield { ok: false, uid: 'volumeId~node3' };
368
372
  yield { ok: true, node: node4 };
369
373
  });
370
374
 
371
- const result = await Array.fromAsync(access.iterateNodes(['node1', 'node2', 'node3', 'node4']));
375
+ const result = await Array.fromAsync(access.iterateNodes(['volumeId~node1', 'volumeId~node2', 'volumeId~node3', 'volumeId~node4']));
372
376
  expect(result).toMatchObject([node1, node4, node2, node3]);
373
- expect(apiService.iterateNodes).toHaveBeenCalledWith(['node2', 'node3'], undefined);
377
+ expect(apiService.iterateNodes).toHaveBeenCalledWith(['volumeId~node2', 'volumeId~node3'], 'volumeId', undefined);
374
378
  });
375
379
 
376
380
  it('should remove from cache if missing on API and return back to caller', async () => {
377
381
  cache.iterateNodes = jest.fn().mockImplementation(async function* () {
378
- yield { ok: false, uid: 'node1' };
379
- yield { ok: false, uid: 'node2' };
380
- yield { ok: false, uid: 'node3' };
382
+ yield { ok: false, uid: 'volumeId~node1' };
383
+ yield { ok: false, uid: 'volumeId~node2' };
384
+ yield { ok: false, uid: 'volumeId~node3' };
381
385
  });
382
386
  apiService.iterateNodes = jest.fn().mockImplementation(async function* (uids: string[]) {
383
387
  // Skip first node - make it missing.
384
- yield* uids.slice(1).map((uid) => ({ uid, parentUid: 'parentUid' } as EncryptedNode));
388
+ yield* uids.slice(1).map((uid) => ({ uid, parentUid: 'volumeId~parentNodeid' } as EncryptedNode));
385
389
  });
386
390
 
387
- const result = await Array.fromAsync(access.iterateNodes(['node1', 'node2', 'node3']));
388
- expect(result).toMatchObject([node2, node3, {missingUid: 'node1'}]);
389
- expect(cache.removeNodes).toHaveBeenCalledWith(['node1']);
391
+ const result = await Array.fromAsync(access.iterateNodes(['volumeId~node1', 'volumeId~node2', 'volumeId~node3']));
392
+ expect(result).toMatchObject([node2, node3, {missingUid: 'volumeId~node1'}]);
393
+ expect(cache.removeNodes).toHaveBeenCalledWith(['volumeId~node1']);
390
394
  });
391
395
 
392
396
  it('should return degraded node if parent cannot be decrypted', async () => {
393
397
  cache.iterateNodes = jest.fn().mockImplementation(async function* () {
394
- yield { ok: false, uid: 'node1' };
395
- yield { ok: false, uid: 'node2' };
396
- yield { ok: false, uid: 'node3' };
398
+ yield { ok: false, uid: 'volumeId~node1' };
399
+ yield { ok: false, uid: 'volumeId~node2' };
400
+ yield { ok: false, uid: 'volumeId~node3' };
397
401
  });
398
402
  const encryptedCrypto = {
399
403
  signatureEmail: 'signatureEmail',
400
404
  nameSignatureEmail: 'nameSignatureEmail',
401
405
  };
402
406
  apiService.iterateNodes = jest.fn().mockImplementation(async function* (uids: string[]) {
403
- yield* uids.map((uid) => ({
407
+ yield* uids.map((uid) => {
408
+ const parentUid = uid.replace('node', 'parentOfNode');
409
+ return {
404
410
  uid,
405
- parentUid: `parentUidFor:${uid}`,
411
+ parentUid,
406
412
  encryptedCrypto,
407
- } as EncryptedNode));
413
+ } as EncryptedNode
414
+ });
408
415
  });
409
416
  const decryptionError = new DecryptionError('Parent cannot be decrypted');
410
417
  jest.spyOn(access, 'getParentKeys').mockImplementation(async ({ parentUid }) => {
411
- if (parentUid === 'parentUidFor:node1') {
418
+ if (parentUid === 'volumeId~parentOfNode1') {
412
419
  throw decryptionError;
413
420
  }
414
421
  return {
@@ -416,12 +423,12 @@ describe('nodesAccess', () => {
416
423
  } as any;
417
424
  } );
418
425
 
419
- const result = await Array.fromAsync(access.iterateNodes(['node1', 'node2', 'node3']));
426
+ const result = await Array.fromAsync(access.iterateNodes(['volumeId~node1', 'volumeId~node2', 'volumeId~node3']));
420
427
  expect(result).toEqual([
421
428
  {
422
429
  ...node1,
423
430
  encryptedCrypto,
424
- parentUid: 'parentUidFor:node1',
431
+ parentUid: 'volumeId~parentOfNode1',
425
432
  name: { ok: false, error: decryptionError },
426
433
  keyAuthor: { ok: false, error: { claimedAuthor: 'signatureEmail', error: decryptionError.message } },
427
434
  nameAuthor: { ok: false, error: { claimedAuthor: 'nameSignatureEmail', error: decryptionError.message } },
@@ -456,7 +463,7 @@ describe('nodesAccess', () => {
456
463
  it('should get node parent keys', async () => {
457
464
  cryptoCache.getNodeKeys = jest.fn(() => Promise.resolve({ key: 'parentKey' } as any as DecryptedNodeKeys));
458
465
 
459
- const result = await access.getParentKeys({ shareId: undefined, parentUid: 'parentUid' });
466
+ const result = await access.getParentKeys({ shareId: undefined, parentUid: 'volumeId~parentNodeid' });
460
467
  expect(result).toEqual({ key: 'parentKey' });
461
468
  expect(shareService.getSharePrivateKey).not.toHaveBeenCalled();
462
469
  });
@@ -464,7 +471,7 @@ describe('nodesAccess', () => {
464
471
  it('should get node parent keys even if share is set', async () => {
465
472
  cryptoCache.getNodeKeys = jest.fn(() => Promise.resolve({ key: 'parentKey' } as any as DecryptedNodeKeys));
466
473
 
467
- const result = await access.getParentKeys({ shareId: 'shareId', parentUid: 'parentUid' });
474
+ const result = await access.getParentKeys({ shareId: 'shareId', parentUid: 'volume1~parentNodeid' });
468
475
  expect(result).toEqual({ key: 'parentKey' });
469
476
  expect(shareService.getSharePrivateKey).not.toHaveBeenCalled();
470
477
  });
@@ -476,7 +483,7 @@ describe('nodesAccess', () => {
476
483
  apiService.getNode = jest.fn(() => Promise.reject(new Error('API called')));
477
484
 
478
485
  try {
479
- await access.getNodeKeys('nodeId');
486
+ await access.getNodeKeys('volumeId~nodeId');
480
487
  throw new Error('Expected error');
481
488
  } catch (error: unknown) {
482
489
  expect(`${error}`).toBe('Error: API called');
@@ -489,7 +496,7 @@ describe('nodesAccess', () => {
489
496
  const nodeUid = 'nodeUid';
490
497
  const node = {
491
498
  uid: nodeUid,
492
- parentUid: 'parentUid',
499
+ parentUid: 'volume1~parentNodeid',
493
500
  encryptedName: 'encryptedName',
494
501
  } as DecryptedNode;
495
502
 
@@ -552,4 +559,39 @@ describe('nodesAccess', () => {
552
559
  expect(result).toBe('https://drive.proton.me/shareId/folder/nodeId');
553
560
  });
554
561
  });
562
+
563
+ describe('notifyNodeChanged', () => {
564
+ it('should mark node as stale', async () => {
565
+ const node = { uid: 'volumeId~nodeId', isStale: false } as DecryptedNode;
566
+ cache.getNode = jest.fn(() => Promise.resolve(node));
567
+ cache.setNode = jest.fn();
568
+ await access.notifyNodeChanged(node.uid);
569
+ expect(cache.getNode).toHaveBeenCalledWith(node.uid);
570
+ expect(cache.setNode).toHaveBeenCalledWith({...node, isStale: true});
571
+ });
572
+ it('should update parent if needed', async () => {
573
+ const node = { uid: 'volumeId~nodeId', parentUid: 'v1~pn1', isStale: false } as DecryptedNode;
574
+ cache.getNode = jest.fn(() => Promise.resolve(node));
575
+ cache.setNode = jest.fn();
576
+ await access.notifyNodeChanged(node.uid, 'v1~pn2');
577
+ expect(cache.getNode).toHaveBeenCalledWith(node.uid);
578
+ expect(cache.setNode).toHaveBeenCalledWith({...node, parentUid: 'v1~pn2', isStale: true});
579
+ });
580
+ });
581
+
582
+ describe('notifyChildCreated', () => {
583
+ it('should reset parent listing', async () => {
584
+ const nodeUid = 'VolumeId1~NodeId1';
585
+ cache.resetFolderChildrenLoaded = jest.fn();
586
+ await access.notifyChildCreated(nodeUid);
587
+ expect(cache.resetFolderChildrenLoaded).toHaveBeenCalledWith(nodeUid);
588
+ });
589
+ });
590
+
591
+ describe('notifyNodeDeleted', () => {
592
+ it('should reset parent listing', async () => {
593
+ await access.notifyNodeDeleted('v1~n1');
594
+ expect(cache.removeNodes).toHaveBeenCalledWith(['v1~n1']);
595
+ });
596
+ });
555
597
  });
@@ -3,6 +3,7 @@ import { c } from 'ttag';
3
3
  import { PrivateKey, SessionKey } from "../../crypto";
4
4
  import { InvalidNameError, Logger, MissingNode, NodeType, Result, resultError, resultOk } from "../../interface";
5
5
  import { DecryptionError, ProtonDriveError } from "../../errors";
6
+ import { asyncIteratorMap } from '../asyncIteratorMap';
6
7
  import { getErrorMessage } from '../errors';
7
8
  import { BatchLoading } from "../batchLoading";
8
9
  import { makeNodeUid, splitNodeUid } from "../uids";
@@ -15,9 +16,19 @@ import { SharesService, EncryptedNode, DecryptedUnparsedNode, DecryptedNode, Dec
15
16
  import { validateNodeName } from "./validations";
16
17
  import { isProtonDocument, isProtonSheet } from './mediaTypes';
17
18
 
19
+ // This is the number of nodes that are loaded in parallel.
20
+ // It is a trade-off between initial wait time and overhead of API calls.
21
+ const BATCH_LOADING_SIZE = 30;
22
+
23
+ // This is the number of nodes that are decrypted in parallel.
24
+ // It is a trade-off between performance and memory usage.
25
+ // Higher number means more memory usage, but faster decryption.
26
+ // Lower number means less memory usage, but slower decryption.
27
+ const DECRYPTION_CONCURRENCY = 15;
28
+
18
29
  /**
19
30
  * Provides access to node metadata.
20
- *
31
+ *
21
32
  * The node access module is responsible for fetching, decrypting and caching
22
33
  * nodes metadata.
23
34
  */
@@ -64,7 +75,7 @@ export class NodesAccess {
64
75
  // Ensure the parent is loaded and up-to-date.
65
76
  const parentNode = await this.getNode(parentNodeUid);
66
77
 
67
- const batchLoading = new BatchLoading<string, DecryptedNode>({ iterateItems: (nodeUids) => this.loadNodes(nodeUids, signal) });
78
+ const batchLoading = new BatchLoading<string, DecryptedNode>({ iterateItems: (nodeUids) => this.loadNodes(nodeUids, signal), batchSize: BATCH_LOADING_SIZE });
68
79
 
69
80
  const areChildrenCached = await this.cache.isFolderChildrenLoaded(parentNodeUid);
70
81
  if (areChildrenCached) {
@@ -100,7 +111,7 @@ export class NodesAccess {
100
111
  // Improvement requested: keep status of loaded trash and leverage cache.
101
112
  async *iterateTrashedNodes(signal?: AbortSignal): AsyncGenerator<DecryptedNode> {
102
113
  const { volumeId } = await this.shareService.getMyFilesIDs();
103
- const batchLoading = new BatchLoading<string, DecryptedNode>({ iterateItems: (nodeUids) => this.loadNodes(nodeUids, signal) });
114
+ const batchLoading = new BatchLoading<string, DecryptedNode>({ iterateItems: (nodeUids) => this.loadNodes(nodeUids, signal), batchSize: BATCH_LOADING_SIZE });
104
115
  for await (const nodeUid of this.apiService.iterateTrashedNodeUids(volumeId, signal)) {
105
116
  let node;
106
117
  try {
@@ -118,7 +129,7 @@ export class NodesAccess {
118
129
  }
119
130
 
120
131
  async *iterateNodes(nodeUids: string[], signal?: AbortSignal): AsyncGenerator<DecryptedNode | MissingNode> {
121
- const batchLoading = new BatchLoading<string, DecryptedNode | MissingNode>({ iterateItems: (nodeUids) => this.loadNodesWithMissingReport(nodeUids, signal) });
132
+ const batchLoading = new BatchLoading<string, DecryptedNode | MissingNode>({ iterateItems: (nodeUids) => this.loadNodesWithMissingReport(nodeUids, signal), batchSize: BATCH_LOADING_SIZE });
122
133
  for await (const result of this.cache.iterateNodes(nodeUids)) {
123
134
  if (result.ok && !result.node.isStale) {
124
135
  yield result.node;
@@ -129,8 +140,45 @@ export class NodesAccess {
129
140
  yield* batchLoading.loadRest();
130
141
  }
131
142
 
143
+ /**
144
+ * Call to invalidate the folder listing cache. This should be refactored into a clean
145
+ * cache layer once the cache is split off.
146
+ */
147
+ async notifyChildCreated(nodeUid: string): Promise<void> {
148
+ await this.cache.resetFolderChildrenLoaded(nodeUid);
149
+ }
150
+
151
+ /**
152
+ * Call to invalidate the node cache when a node changes. Parent can be set after a move
153
+ * to ensure parent listing of new parent is up to date if cached.
154
+ * This should be refactored into a clean cache layer once the cache is split off.
155
+ */
156
+ async notifyNodeChanged(nodeUid: string, newParentUid?: string): Promise<void> {
157
+ try {
158
+ const node = await this.cache.getNode(nodeUid);
159
+ if (node.isStale && newParentUid === null) {
160
+ return;
161
+ }
162
+ node.isStale = true;
163
+ if (newParentUid) {
164
+ node.parentUid = newParentUid;
165
+ }
166
+ await this.cache.setNode(node);
167
+ } catch (error: unknown) {
168
+ this.logger.warn(`Failed to set node ${nodeUid} as stale after sharing: ${error}`);
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Call to remove a node from cache. This should be refactored when the cache is split off.
174
+ */
175
+ async notifyNodeDeleted(nodeUid: string): Promise<void> {
176
+ await this.cache.removeNodes([nodeUid]);
177
+ }
178
+
132
179
  private async loadNode(nodeUid: string): Promise<{ node: DecryptedNode, keys?: DecryptedNodeKeys }> {
133
- const encryptedNode = await this.apiService.getNode(nodeUid);
180
+ const { volumeId: ownVolumeId } = await this.shareService.getMyFilesIDs();
181
+ const encryptedNode = await this.apiService.getNode(nodeUid, ownVolumeId);
134
182
  return this.decryptNode(encryptedNode);
135
183
  }
136
184
 
@@ -147,13 +195,24 @@ export class NodesAccess {
147
195
  const returnedNodeUids: string[] = [];
148
196
  const errors = [];
149
197
 
150
- for await (const encryptedNode of this.apiService.iterateNodes(nodeUids, signal)) {
198
+ const { volumeId: ownVolumeId } = await this.shareService.getMyFilesIDs();
199
+
200
+ const encryptedNodesIterator = this.apiService.iterateNodes(nodeUids, ownVolumeId, signal);
201
+ const decryptNodeMapper = async (encryptedNode: EncryptedNode): Promise<Result<DecryptedNode, unknown>> => {
151
202
  returnedNodeUids.push(encryptedNode.uid);
152
203
  try {
153
204
  const { node } = await this.decryptNode(encryptedNode);
154
- yield node;
205
+ return resultOk(node);
155
206
  } catch (error: unknown) {
156
- errors.push(error);
207
+ return resultError(error);
208
+ }
209
+ };
210
+ const decryptedNodesIterator = asyncIteratorMap(encryptedNodesIterator, decryptNodeMapper, DECRYPTION_CONCURRENCY);
211
+ for await (const node of decryptedNodesIterator) {
212
+ if (node.ok) {
213
+ yield node.value;
214
+ } else {
215
+ errors.push(node.error);
157
216
  }
158
217
  }
159
218
 
@@ -194,6 +253,7 @@ export class NodesAccess {
194
253
  error: getErrorMessage(error),
195
254
  }),
196
255
  errors: [error],
256
+ treeEventScopeId: splitNodeUid(encryptedNode.uid).volumeId,
197
257
  },
198
258
  };
199
259
  }
@@ -249,6 +309,7 @@ export class NodesAccess {
249
309
  ...extendedAttributes,
250
310
  }),
251
311
  folder: undefined,
312
+ treeEventScopeId: splitNodeUid(unparsedNode.uid).volumeId,
252
313
  }
253
314
  }
254
315
 
@@ -263,6 +324,7 @@ export class NodesAccess {
263
324
  folder: extendedAttributes ? {
264
325
  ...extendedAttributes,
265
326
  } : undefined,
327
+ treeEventScopeId: splitNodeUid(unparsedNode.uid).volumeId,
266
328
  }
267
329
  }
268
330