@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
@@ -1,16 +1,18 @@
1
- import { ProtonDriveEntitiesCache, Logger, ProtonDriveTelemetry } from "../../interface";
1
+ import { Logger, ProtonDriveTelemetry } from "../../interface";
2
2
  import { DriveAPIService } from "../apiService";
3
- import { DriveListener } from "./interface";
3
+ import { DriveEvent, DriveListener, EventSubscription, LatestEventIdProvider } from "./interface";
4
4
  import { EventsAPIService } from "./apiService";
5
- import { EventsCache } from "./cache";
6
5
  import { CoreEventManager } from "./coreEventManager";
7
6
  import { VolumeEventManager } from "./volumeEventManager";
7
+ import { EventManager } from "./eventManager";
8
+ import { SharesManager } from "../shares/manager";
8
9
 
9
10
  export type { DriveEvent, DriveListener } from "./interface";
10
11
  export { DriveEventType } from "./interface";
11
12
 
12
13
  const OWN_VOLUME_POLLING_INTERVAL = 30;
13
14
  const OTHER_VOLUME_POLLING_INTERVAL = 60;
15
+ const CORE_POLLING_INTERVAL = 30;
14
16
 
15
17
  /**
16
18
  * Service for listening to drive events. The service is responsible for
@@ -19,113 +21,81 @@ const OTHER_VOLUME_POLLING_INTERVAL = 60;
19
21
  */
20
22
  export class DriveEventsService {
21
23
  private apiService: EventsAPIService;
22
- private cache: EventsCache;
23
- private subscribedToRemoteDataUpdates: boolean = false;
24
- private listeners: DriveListener[] = [];
25
- private coreEvents: CoreEventManager;
26
- private volumesEvents: { [volumeId: string]: VolumeEventManager };
24
+ private coreEvents?: EventManager<DriveEvent>;
25
+ private volumeEventManagers: { [volumeId: string]: EventManager<DriveEvent> };
27
26
  private logger: Logger;
28
27
 
29
- constructor(private telemetry: ProtonDriveTelemetry, apiService: DriveAPIService, driveEntitiesCache: ProtonDriveEntitiesCache) {
28
+ constructor(private telemetry: ProtonDriveTelemetry, apiService: DriveAPIService, private shareManagement: SharesManager, private cacheEventListeners: DriveListener[] = [], private latestEventIdProvider?: LatestEventIdProvider) {
30
29
  this.telemetry = telemetry;
31
30
  this.logger = telemetry.getLogger('events');
32
31
  this.apiService = new EventsAPIService(apiService);
33
- this.cache = new EventsCache(driveEntitiesCache);
34
-
35
- // FIXME: Allow to pass own core events manager from the public interface.
36
- this.coreEvents = new CoreEventManager(this.logger, this.apiService, this.cache);
37
- this.volumesEvents = {};
32
+ this.volumeEventManagers = {};
38
33
  }
39
34
 
40
35
  /**
41
- * Loads all the subscribed volumes (including core events) from the
42
- * cache and starts listening to their events. Any additional volume
43
- * that is subscribed to later will be automatically started.
36
+ * Subscribe to drive events. The treeEventScopeId can be obtained from a node.
44
37
  */
45
- async subscribeToRemoteDataUpdates(): Promise<void> {
46
- if (this.subscribedToRemoteDataUpdates) {
47
- return;
48
- }
49
-
50
- await this.loadSubscribedVolumeEventServices();
51
- this.sendNumberOfVolumeSubscriptionsToTelemetry();
52
-
53
- this.subscribedToRemoteDataUpdates = true;
54
- await this.coreEvents.startSubscription();
55
- await Promise.all(
56
- Object.values(this.volumesEvents)
57
- .map((volumeEvents) => volumeEvents.startSubscription())
58
- );
59
- }
60
-
61
- /**
62
- * Subscribe to given volume. The volume will be polled for events
63
- * with the polling interval depending on the type of the volume.
64
- * Own volumes are polled with highest frequency, while others are
65
- * polled with lower frequency depending on the total number of
66
- * subscriptions.
67
- *
68
- * @param isOwnVolume - Owned volumes are polled with higher frequency.
69
- */
70
- async listenToVolume(volumeId: string, isOwnVolume = false): Promise<void> {
71
- await this.loadSubscribedVolumeEventServices();
72
-
73
- if (this.volumesEvents[volumeId]) {
74
- return;
75
- }
38
+ async subscribeToTreeEvents(treeEventScopeId: string, callback: DriveListener): Promise<EventSubscription> {
39
+ const volumeId = treeEventScopeId;
76
40
  this.logger.debug(`Creating volume event manager for volume ${volumeId}`);
77
- const manager = this.createVolumeEventManager(volumeId, isOwnVolume);
78
-
79
- // FIXME: Use dynamic algorithm to determine polling interval for non-own volumes.
80
- manager.setPollingInterval(isOwnVolume ? OWN_VOLUME_POLLING_INTERVAL : OTHER_VOLUME_POLLING_INTERVAL);
81
- if (this.subscribedToRemoteDataUpdates) {
82
- await manager.startSubscription();
41
+ let manager = this.volumeEventManagers[volumeId];
42
+ let started = true;
43
+ if (manager === undefined) {
44
+ manager = await this.createVolumeEventManager(volumeId);
45
+ this.volumeEventManagers[volumeId] = manager;
46
+ started = false;
83
47
  this.sendNumberOfVolumeSubscriptionsToTelemetry();
84
48
  }
49
+ const eventSubscription = manager.addListener(callback);
50
+ if (!started) {
51
+ await manager.start();
52
+ }
53
+ return eventSubscription;
85
54
  }
86
55
 
87
- private async loadSubscribedVolumeEventServices() {
88
- for (const volumeId of await this.cache.getSubscribedVolumeIds()) {
89
- if (!this.volumesEvents[volumeId]) {
90
- const isOwnVolume = await this.cache.isOwnVolume(volumeId) || false;
91
- this.createVolumeEventManager(volumeId, isOwnVolume);
56
+ // FIXME: Allow to pass own core events manager from the public interface.
57
+ async subscribeToCoreEvents(callback: DriveListener): Promise<EventSubscription> {
58
+ if (this.latestEventIdProvider === null || this.latestEventIdProvider === undefined) {
59
+ throw new Error('Cannot subscribe to events without passing a latestEventIdProvider in ProtonDriveClient initialization');
60
+ }
61
+ if (this.coreEvents === undefined) {
62
+ const coreEventManager = new CoreEventManager(this.logger, this.apiService);
63
+ const latestEventId = this.latestEventIdProvider.getLatestEventId('core') ?? null;
64
+ this.coreEvents = new EventManager(coreEventManager, CORE_POLLING_INTERVAL, latestEventId);
65
+ for (const listener of this.cacheEventListeners) {
66
+ this.coreEvents.addListener(listener);
92
67
  }
93
68
  }
69
+ const eventSubscription = this.coreEvents.addListener(callback);
70
+ await this.coreEvents.start();
71
+ return eventSubscription;
94
72
  }
95
73
 
96
74
  private sendNumberOfVolumeSubscriptionsToTelemetry() {
97
75
  this.telemetry.logEvent({
98
76
  eventName: 'volumeEventsSubscriptionsChanged',
99
- numberOfVolumeSubscriptions: Object.keys(this.volumesEvents).length,
77
+ numberOfVolumeSubscriptions: Object.keys(this.volumeEventManagers).length,
100
78
  });
101
79
  }
102
80
 
103
- private createVolumeEventManager(volumeId: string, isOwnVolume: boolean): VolumeEventManager {
104
- const manager = new VolumeEventManager(this.logger, this.apiService, this.cache, volumeId, isOwnVolume);
105
- for (const listener of this.listeners) {
106
- manager.addListener(listener);
81
+ private async createVolumeEventManager(volumeId: string): Promise<EventManager<DriveEvent>> {
82
+ if (this.latestEventIdProvider === null || this.latestEventIdProvider === undefined) {
83
+ throw new Error('Cannot subscribe to events without passing a latestEventIdProvider in ProtonDriveClient initialization');
84
+ }
85
+ const isOwnVolume = await this.shareManagement.isOwnVolume(volumeId);
86
+ const pollingInterval = this.getDefaultVolumePollingInterval(isOwnVolume);
87
+ const volumeEventManager = new VolumeEventManager(this.logger, this.apiService, volumeId);
88
+ const latestEventId = this.latestEventIdProvider.getLatestEventId(volumeId);
89
+ const eventManager = new EventManager<DriveEvent>(volumeEventManager, pollingInterval, latestEventId);
90
+ for (const listener of this.cacheEventListeners) {
91
+ eventManager.addListener(listener);
107
92
  }
108
- this.volumesEvents[volumeId] = manager;
109
- return manager;
93
+ await eventManager.start();
94
+ this.volumeEventManagers[volumeId] = eventManager;
95
+ return eventManager;
110
96
  }
111
97
 
112
- /**
113
- * Listen to the drive events. The listener will be called with the
114
- * new events as they arrive.
115
- *
116
- * One call always provides events from withing the same volume. The
117
- * second argument of the callback `fullRefreshVolumeId` is thus single
118
- * ID and if multiple volumes must be fully refreshed, client will
119
- * receive multiple calls.
120
- */
121
- addListener(callback: DriveListener): void {
122
- // Add new listener to the list for any new event manager.
123
- this.listeners.push(callback);
124
-
125
- // Add new listener to all existings managers.
126
- this.coreEvents.addListener(callback);
127
- for (const volumeEvents of Object.values(this.volumesEvents)) {
128
- volumeEvents.addListener(callback);
129
- }
98
+ private getDefaultVolumePollingInterval(isOwnVolume: boolean): number {
99
+ return isOwnVolume ? OWN_VOLUME_POLLING_INTERVAL : OTHER_VOLUME_POLLING_INTERVAL
130
100
  }
131
101
  }
@@ -1,19 +1,33 @@
1
+ import { Logger } from "../../interface";
2
+
1
3
  /**
2
4
  * Callback that accepts list of Drive events and flag whether no
3
5
  * event should be processed, but rather full cache refresh should be
4
6
  * performed.
5
- *
7
+ *
6
8
  * @param fullRefreshVolumeId - ID of the volume that should be fully refreshed.
7
9
  */
8
- export type DriveListener = (events: DriveEvent[], fullRefreshVolumeId?: string) => Promise<void>;
10
+ export type DriveListener = (event: DriveEvent) => Promise<void>;
11
+
12
+ export interface Event {
13
+ eventId: string;
14
+ }
15
+
16
+ export interface EventSubscription {
17
+ dispose(): void;
18
+ }
19
+
20
+ export interface LatestEventIdProvider {
21
+ getLatestEventId(treeEventScopeId: string): string | null;
22
+ }
9
23
 
10
24
  /**
11
25
  * Generic internal event interface representing a list of events
12
26
  * with metadata about the last event ID, whether there are more
13
27
  * events to fetch, or whether the listener should refresh its state.
14
28
  */
15
- export type Events<T> = {
16
- lastEventId: string,
29
+ export type EventsListWithStatus<T> = {
30
+ latestEventId: string,
17
31
  more: boolean,
18
32
  refresh: boolean,
19
33
  events: T[],
@@ -22,30 +36,71 @@ export type Events<T> = {
22
36
  /**
23
37
  * Internal event interface representing a list of specific Drive events.
24
38
  */
25
- export type DriveEvents = Events<DriveEvent>;
39
+ export type DriveEventsListWithStatus = EventsListWithStatus<DriveEvent>;
40
+
41
+ type NodeCruEventType = DriveEventType.NodeCreated | DriveEventType.NodeUpdated;
42
+ export type NodeEventType = NodeCruEventType | DriveEventType.NodeDeleted;
26
43
 
27
- export type DriveEvent = {
28
- type: DriveEventType.NodeCreated | DriveEventType.NodeUpdated | DriveEventType.NodeUpdatedMetadata,
44
+ export type NodeEvent = {
45
+ type: NodeCruEventType,
29
46
  nodeUid: string,
30
47
  parentNodeUid?: string,
31
48
  isTrashed: boolean,
32
49
  isShared: boolean,
33
- isOwnVolume: boolean,
50
+ treeEventScopeId: string,
51
+ eventId: string,
34
52
  } | {
35
53
  type: DriveEventType.NodeDeleted,
36
54
  nodeUid: string,
37
55
  parentNodeUid?: string,
38
- isTrashed?: boolean,
39
- isShared?: boolean,
40
- isOwnVolume: boolean,
41
- } | {
42
- type: DriveEventType.ShareWithMeUpdated,
56
+ treeEventScopeId: string,
57
+ eventId: string,
43
58
  }
44
59
 
60
+ export type FastForwardEvent = {
61
+ type: DriveEventType.FastForward,
62
+ treeEventScopeId: string,
63
+ eventId: string,
64
+ }
65
+
66
+ export type TreeRefreshEvent = {
67
+ type: DriveEventType.TreeRefresh,
68
+ treeEventScopeId: string,
69
+ eventId: string,
70
+ }
71
+
72
+ export type TreeRemovalEvent = {
73
+ type: DriveEventType.TreeRemove,
74
+ treeEventScopeId: string,
75
+ eventId: 'none',
76
+ }
77
+
78
+ export type SharedWithMeUpdated = {
79
+ type: DriveEventType.SharedWithMeUpdated,
80
+ eventId: string,
81
+ treeEventScopeId: 'core',
82
+ }
83
+
84
+ export type DriveEvent = NodeEvent | FastForwardEvent | TreeRefreshEvent | TreeRemovalEvent | FastForwardEvent | SharedWithMeUpdated;
85
+
45
86
  export enum DriveEventType {
46
87
  NodeCreated = 'node_created',
47
88
  NodeUpdated = 'node_updated',
48
- NodeUpdatedMetadata = 'node_updated_metadata',
49
89
  NodeDeleted = 'node_deleted',
50
- ShareWithMeUpdated = 'share_with_me_updated',
90
+ SharedWithMeUpdated = 'shared_with_me_updated',
91
+ TreeRefresh = 'tree_refresh',
92
+ TreeRemove = 'tree_remove',
93
+ FastForward = 'fast_forward',
94
+ }
95
+
96
+ /**
97
+ * This can happen if all shared nodes in that volume where unshared or if the
98
+ * volume was deleted.
99
+ */
100
+ export class UnsubscribeFromEventsSourceError extends Error {};
101
+
102
+ export interface EventManagerInterface<T> {
103
+ getLatestEventId(): Promise<string>;
104
+ getEvents(eventId: string): AsyncIterable<T>;
105
+ getLogger(): Logger;
51
106
  }
@@ -0,0 +1,243 @@
1
+ import { getMockLogger } from "../../tests/logger";
2
+ import { NotFoundAPIError } from "../apiService";
3
+ import { EventsAPIService } from "./apiService";
4
+ import { VolumeEventManager } from "./volumeEventManager";
5
+ import { DriveEventsListWithStatus, DriveEventType } from "./interface";
6
+
7
+ jest.mock("./apiService");
8
+
9
+ describe("VolumeEventManager", () => {
10
+ let manager: VolumeEventManager;
11
+ let mockEventsAPIService: jest.Mocked<EventsAPIService>;
12
+ const mockLogger = getMockLogger();
13
+ const volumeId = "volumeId123";
14
+
15
+ beforeEach(() => {
16
+ jest.clearAllMocks();
17
+
18
+ mockEventsAPIService = {
19
+ getVolumeLatestEventId: jest.fn(),
20
+ getVolumeEvents: jest.fn(),
21
+ getCoreLatestEventId: jest.fn(),
22
+ getCoreEvents: jest.fn(),
23
+ } as any;
24
+
25
+ manager = new VolumeEventManager(
26
+ mockLogger,
27
+ mockEventsAPIService,
28
+ volumeId
29
+ );
30
+ });
31
+
32
+ describe("getLatestEventId", () => {
33
+ it("should return the latest event ID from API", async () => {
34
+ const expectedEventId = "eventId123";
35
+ mockEventsAPIService.getVolumeLatestEventId.mockResolvedValue(expectedEventId);
36
+
37
+ const result = await manager.getLatestEventId();
38
+
39
+ expect(result).toBe(expectedEventId);
40
+ expect(mockEventsAPIService.getVolumeLatestEventId).toHaveBeenCalledWith(volumeId);
41
+ });
42
+
43
+ it("should throw UnsubscribeFromEventsSourceError when API returns NotFoundAPIError", async () => {
44
+ const notFoundError = new NotFoundAPIError("Event not found", 2501);
45
+ mockEventsAPIService.getVolumeLatestEventId.mockRejectedValue(notFoundError);
46
+
47
+ await expect(manager.getLatestEventId()).rejects.toThrow("Event not found");
48
+ });
49
+
50
+ it("should rethrow other errors", async () => {
51
+ const networkError = new Error("Network error");
52
+ mockEventsAPIService.getVolumeLatestEventId.mockRejectedValue(networkError);
53
+
54
+ await expect(manager.getLatestEventId()).rejects.toThrow("Network error");
55
+ });
56
+ });
57
+
58
+ describe("getEvents", () => {
59
+ it("should yield events from API response", async () => {
60
+ const mockEventsResponse: DriveEventsListWithStatus = {
61
+ latestEventId: "eventId456",
62
+ more: false,
63
+ refresh: false,
64
+ events: [
65
+ {
66
+ type: DriveEventType.NodeCreated,
67
+ nodeUid: "node1",
68
+ parentNodeUid: "parent1",
69
+ isTrashed: false,
70
+ isShared: false,
71
+ treeEventScopeId: volumeId,
72
+ eventId: "eventId456",
73
+ }
74
+ ],
75
+ };
76
+
77
+ mockEventsAPIService.getVolumeEvents.mockResolvedValue(mockEventsResponse);
78
+
79
+ const events = [];
80
+ for await (const event of manager.getEvents("startEventId")) {
81
+ events.push(event);
82
+ }
83
+
84
+ expect(events).toEqual(mockEventsResponse.events);
85
+ expect(mockEventsAPIService.getVolumeEvents).toHaveBeenCalledWith(volumeId, "startEventId");
86
+ });
87
+
88
+ it("should continue fetching when more events are available", async () => {
89
+ const firstResponse: DriveEventsListWithStatus = {
90
+ latestEventId: "eventId2",
91
+ more: true,
92
+ refresh: false,
93
+ events: [
94
+ {
95
+ type: DriveEventType.NodeCreated,
96
+ nodeUid: "node1",
97
+ parentNodeUid: "parent1",
98
+ isTrashed: false,
99
+ isShared: false,
100
+ treeEventScopeId: volumeId,
101
+ eventId: "eventId2",
102
+ }
103
+ ],
104
+ };
105
+
106
+ const secondResponse: DriveEventsListWithStatus = {
107
+ latestEventId: "eventId3",
108
+ more: false,
109
+ refresh: false,
110
+ events: [
111
+ {
112
+ type: DriveEventType.NodeUpdated,
113
+ nodeUid: "node2",
114
+ parentNodeUid: "parent1",
115
+ isTrashed: false,
116
+ isShared: false,
117
+ treeEventScopeId: volumeId,
118
+ eventId: "eventId3",
119
+ }
120
+ ],
121
+ };
122
+
123
+ mockEventsAPIService.getVolumeEvents
124
+ .mockResolvedValueOnce(firstResponse)
125
+ .mockResolvedValueOnce(secondResponse);
126
+
127
+ const events = [];
128
+ for await (const event of manager.getEvents("startEventId")) {
129
+ events.push(event);
130
+ }
131
+
132
+ expect(events).toHaveLength(2);
133
+ expect(events[0]).toEqual(firstResponse.events[0]);
134
+ expect(events[1]).toEqual(secondResponse.events[0]);
135
+ expect(mockEventsAPIService.getVolumeEvents).toHaveBeenCalledTimes(2);
136
+ expect(mockEventsAPIService.getVolumeEvents).toHaveBeenNthCalledWith(1, volumeId, "startEventId");
137
+ expect(mockEventsAPIService.getVolumeEvents).toHaveBeenNthCalledWith(2, volumeId, "eventId2");
138
+ });
139
+
140
+ it("should yield TreeRefresh event when refresh is true", async () => {
141
+ const mockEventsResponse: DriveEventsListWithStatus = {
142
+ latestEventId: "eventId789",
143
+ more: false,
144
+ refresh: true,
145
+ events: [],
146
+ };
147
+
148
+ mockEventsAPIService.getVolumeEvents.mockResolvedValue(mockEventsResponse);
149
+
150
+ const events = [];
151
+ for await (const event of manager.getEvents("startEventId")) {
152
+ events.push(event);
153
+ }
154
+
155
+ expect(events).toHaveLength(1);
156
+ expect(events[0]).toEqual({
157
+ type: DriveEventType.TreeRefresh,
158
+ treeEventScopeId: volumeId,
159
+ eventId: "eventId789",
160
+ });
161
+ });
162
+
163
+ it("should yield FastForward event when no events but eventId changed", async () => {
164
+ const mockEventsResponse: DriveEventsListWithStatus = {
165
+ latestEventId: "newEventId",
166
+ more: false,
167
+ refresh: false,
168
+ events: [],
169
+ };
170
+
171
+ mockEventsAPIService.getVolumeEvents.mockResolvedValue(mockEventsResponse);
172
+
173
+ const events = [];
174
+ for await (const event of manager.getEvents("oldEventId")) {
175
+ events.push(event);
176
+ }
177
+
178
+ expect(events).toHaveLength(1);
179
+ expect(events[0]).toEqual({
180
+ type: DriveEventType.FastForward,
181
+ treeEventScopeId: volumeId,
182
+ eventId: "newEventId",
183
+ });
184
+ });
185
+
186
+ it("should yield TreeRemove event when API returns NotFoundAPIError", async () => {
187
+ const notFoundError = new NotFoundAPIError("Volume not found", 2501);
188
+ mockEventsAPIService.getVolumeEvents.mockRejectedValue(notFoundError);
189
+
190
+ const events = [];
191
+ try {
192
+ for await (const event of manager.getEvents("startEventId")) {
193
+ events.push(event);
194
+ }
195
+ } catch (error) {
196
+ // The error should be re-thrown, but first it should yield a TreeRemove event
197
+ expect(error).toBe(notFoundError);
198
+ }
199
+
200
+ expect(events).toHaveLength(1);
201
+ expect(events[0]).toEqual({
202
+ type: DriveEventType.TreeRemove,
203
+ treeEventScopeId: volumeId,
204
+ eventId: 'none',
205
+ });
206
+ });
207
+
208
+ it("should rethrow non-NotFoundAPIError errors", async () => {
209
+ const networkError = new Error("Network error");
210
+ mockEventsAPIService.getVolumeEvents.mockRejectedValue(networkError);
211
+
212
+ const eventGenerator = manager.getEvents("startEventId");
213
+ const eventIterator = eventGenerator[Symbol.asyncIterator]();
214
+ await expect(eventIterator.next()).rejects.toThrow("Network error");
215
+ });
216
+
217
+ it("should not yield events when events array is empty and eventId unchanged", async () => {
218
+ const mockEventsResponse: DriveEventsListWithStatus = {
219
+ latestEventId: "sameEventId",
220
+ more: false,
221
+ refresh: false,
222
+ events: [],
223
+ };
224
+
225
+ mockEventsAPIService.getVolumeEvents.mockResolvedValue(mockEventsResponse);
226
+
227
+ const events = [];
228
+ for await (const event of manager.getEvents("sameEventId")) {
229
+ events.push(event);
230
+ }
231
+
232
+ expect(events).toHaveLength(0);
233
+ });
234
+ });
235
+
236
+ describe("getLogger", () => {
237
+ it("should return logger with prefix", () => {
238
+ const logger = manager.getLogger();
239
+ expect(logger).toBeDefined();
240
+ // The logger should be wrapped with LoggerWithPrefix, but we can't easily test the prefix
241
+ });
242
+ });
243
+ });
@@ -1,74 +1,76 @@
1
1
  import { Logger } from "../../interface";
2
2
  import { LoggerWithPrefix } from "../../telemetry";
3
3
  import { EventsAPIService } from "./apiService";
4
- import { EventsCache } from "./cache";
5
- import { DriveEvent, DriveListener } from "./interface";
6
- import { EventManager } from "./eventManager";
4
+ import { DriveEvent, DriveEventsListWithStatus, DriveEventType, EventManagerInterface, UnsubscribeFromEventsSourceError } from "./interface";
5
+ import { NotFoundAPIError } from "../apiService";
7
6
 
8
7
  /**
9
8
  * Combines API and event manager to provide a service for listening to
10
9
  * volume events. Volume events are all about nodes updates. Whenever
11
10
  * there is update to the node metadata or content, the event is emitted.
12
11
  */
13
- export class VolumeEventManager {
14
- private manager: EventManager<DriveEvent>;
12
+ export class VolumeEventManager implements EventManagerInterface<DriveEvent>{
15
13
 
16
- constructor(logger: Logger, private apiService: EventsAPIService, private cache: EventsCache, private volumeId: string, isOwnVolume: boolean) {
14
+ constructor(private logger: Logger, private apiService: EventsAPIService, private volumeId: string) {
17
15
  this.apiService = apiService;
18
16
  this.volumeId = volumeId;
19
-
20
- this.manager = new EventManager(
21
- new LoggerWithPrefix(logger, `volume ${volumeId}`),
22
- () => this.getLastEventId(),
23
- (eventId) => this.apiService.getVolumeEvents(volumeId, eventId, isOwnVolume),
24
- (lastEventId) => this.cache.setLastEventId(volumeId, {
25
- lastEventId,
26
- pollingIntervalInSeconds: this.manager.pollingIntervalInSeconds,
27
- isOwnVolume
28
- }),
29
- );
30
- this.cache.getPollingIntervalInSeconds(volumeId)
31
- .then((pollingIntervalInSeconds) => {
32
- if (pollingIntervalInSeconds) {
33
- this.manager.pollingIntervalInSeconds = pollingIntervalInSeconds;
34
- }
35
- })
36
- .catch(() => {});
37
- }
38
-
39
- private async getLastEventId(): Promise<string> {
40
- const lastEventId = await this.cache.getLastEventId(this.volumeId);
41
- if (lastEventId) {
42
- return lastEventId;
43
- }
44
- return this.apiService.getVolumeLatestEventId(this.volumeId);
45
- }
46
-
47
- /**
48
- * There is a limit how many volume subscribtions can be active at
49
- * the same time. The manager of all volume managers should set the
50
- * intervals for each volume accordingly depending on the volume
51
- * type or the total number of subscriptions.
52
- */
53
- setPollingInterval(pollingIntervalInSeconds: number): void {
54
- this.manager.pollingIntervalInSeconds = pollingIntervalInSeconds;
17
+ this.logger = new LoggerWithPrefix(logger, `volume ${volumeId}`);
55
18
  }
56
19
 
57
- async startSubscription(): Promise<void> {
58
- await this.manager.start();
20
+ getLogger(): Logger {
21
+ return this.logger;
59
22
  }
60
23
 
61
- async stopSubscription(): Promise<void> {
62
- await this.manager.stop();
24
+ async * getEvents(eventId: string): AsyncIterable<DriveEvent> {
25
+ try {
26
+ let events: DriveEventsListWithStatus;
27
+ let more = true;
28
+ while (more) {
29
+ events = await this.apiService.getVolumeEvents(this.volumeId, eventId);
30
+ more = events.more;
31
+ if (events.refresh) {
32
+ yield {
33
+ type: DriveEventType.TreeRefresh,
34
+ treeEventScopeId: this.volumeId,
35
+ eventId: events.latestEventId,
36
+ };
37
+ break;
38
+ }
39
+ // Update to the latest eventId to avoid inactive volumes from getting out of sync
40
+ if (events.events.length === 0 && events.latestEventId !== eventId) {
41
+ yield {
42
+ type: DriveEventType.FastForward,
43
+ treeEventScopeId: this.volumeId,
44
+ eventId: events.latestEventId,
45
+ };
46
+ break;
47
+ }
48
+ yield* events.events;
49
+ eventId = events.latestEventId;
50
+ }
51
+ } catch (error: unknown) {
52
+ if (error instanceof NotFoundAPIError) {
53
+ this.logger.info(`Volume events no longer accessible`);
54
+ yield {
55
+ type: DriveEventType.TreeRemove,
56
+ treeEventScopeId: this.volumeId,
57
+ // After a TreeRemoval event, polling should stop.
58
+ eventId: 'none',
59
+ };
60
+ }
61
+ throw error;
62
+ }
63
63
  }
64
64
 
65
- addListener(callback: DriveListener): void {
66
- this.manager.addListener(async (events, fullRefresh) => {
67
- if (fullRefresh) {
68
- await callback([], this.volumeId);
69
- } else {
70
- await callback(events);
65
+ async getLatestEventId(): Promise<string> {
66
+ try {
67
+ return await this.apiService.getVolumeLatestEventId(this.volumeId);
68
+ } catch (error: unknown) {
69
+ if (error instanceof NotFoundAPIError) {
70
+ this.logger.info(`Volume events no longer accessible`);
71
+ throw new UnsubscribeFromEventsSourceError(error.message);
71
72
  }
72
- });
73
+ throw error;
74
+ }
73
75
  }
74
76
  }