@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,139 +1,252 @@
1
1
  import { getMockLogger } from "../../tests/logger";
2
- import { NotFoundAPIError } from "../apiService";
3
2
  import { EventManager } from "./eventManager";
3
+ import { DriveEvent, DriveEventType, EventSubscription, UnsubscribeFromEventsSourceError } from "./interface";
4
4
 
5
5
  jest.useFakeTimers();
6
6
 
7
+ const POLLING_INTERVAL = 1;
8
+
7
9
  describe("EventManager", () => {
8
- let manager: EventManager<string>;
9
-
10
- const getLastEventIdMock = jest.fn();
10
+ let manager: EventManager<DriveEvent>;
11
+
12
+ const getLatestEventIdMock = jest.fn();
11
13
  const getEventsMock = jest.fn();
12
- const updateLatestEventIdMock = jest.fn();
13
14
  const listenerMock = jest.fn();
15
+ const mockLogger = getMockLogger();
16
+ const subscriptions: EventSubscription[] = [];
14
17
 
15
18
  beforeEach(() => {
16
- jest.clearAllMocks();
17
-
18
- getLastEventIdMock.mockImplementation(() => Promise.resolve("eventId1"));
19
- getEventsMock.mockImplementation(() => Promise.resolve({
20
- lastEventId: "eventId2",
21
- more: false,
22
- refresh: false,
23
- events: ["event1", "event2"],
24
- }));
19
+ const mockEventManager = {
20
+ getLogger: () => mockLogger,
21
+ getLatestEventId: getLatestEventIdMock,
22
+ getEvents: getEventsMock,
23
+ };
25
24
 
26
25
  manager = new EventManager(
27
- getMockLogger(),
28
- getLastEventIdMock,
29
- getEventsMock,
30
- updateLatestEventIdMock,
26
+ mockEventManager as any,
27
+ POLLING_INTERVAL,
28
+ null,
31
29
  );
32
- manager.addListener(listenerMock);
30
+ const subscription = manager.addListener(listenerMock);
31
+ subscriptions.push(subscription);
33
32
  });
34
33
 
35
34
  afterEach(async () => {
36
35
  await manager.stop();
36
+ while (subscriptions.length > 0) {
37
+ const subscription = subscriptions.pop();
38
+ subscription?.dispose();
39
+ }
40
+ jest.clearAllMocks();
37
41
  });
38
42
 
39
- it("should get latest event ID on first run only", async () => {
40
- await manager.start();
41
- expect(getLastEventIdMock).toHaveBeenCalledTimes(1);
42
- expect(getEventsMock).toHaveBeenCalledTimes(0);
43
- expect(listenerMock).toHaveBeenCalledTimes(0);
44
- expect(updateLatestEventIdMock).toHaveBeenCalledTimes(1);
45
- expect(updateLatestEventIdMock).toHaveBeenCalledWith('eventId1');
46
- });
43
+ it("should start polling when started", async () => {
44
+ getLatestEventIdMock.mockResolvedValue('EventId1');
45
+
46
+ const mockEvents: DriveEvent[][] = [
47
+ [{
48
+ type: DriveEventType.FastForward,
49
+ treeEventScopeId: 'volume1',
50
+ eventId: 'EventId2',
51
+ }],
52
+ [{
53
+ type: DriveEventType.FastForward,
54
+ treeEventScopeId: 'volume1',
55
+ eventId: 'EventId3',
56
+ }],
57
+ ];
58
+
59
+ getEventsMock.mockImplementationOnce(async function* () {
60
+ yield* mockEvents[0];
61
+ }).mockImplementationOnce(async function* () {
62
+ yield* mockEvents[1];
63
+ }).mockImplementationOnce(async function* () {
64
+ });
47
65
 
48
- it("should notify about events in the next run", async () => {
49
- await manager.start();
50
- expect(getLastEventIdMock).toHaveBeenCalledTimes(1);
66
+ expect(getLatestEventIdMock).toHaveBeenCalledTimes(0);
51
67
  expect(getEventsMock).toHaveBeenCalledTimes(0);
52
- expect(listenerMock).toHaveBeenCalledTimes(0);
53
- expect(updateLatestEventIdMock).toHaveBeenCalledTimes(1);
54
- expect(updateLatestEventIdMock).toHaveBeenCalledWith('eventId1');
55
- updateLatestEventIdMock.mockClear();
68
+
69
+ expect(await manager.start()).toBeUndefined();
70
+
71
+ expect(getLatestEventIdMock).toHaveBeenCalledTimes(1);
72
+ expect(getEventsMock).toHaveBeenCalledWith('EventId1');
73
+
56
74
  await jest.runOnlyPendingTimersAsync();
57
- expect(getEventsMock).toHaveBeenCalledTimes(1);
58
- expect(listenerMock).toHaveBeenCalledTimes(1);
59
- expect(updateLatestEventIdMock).toHaveBeenCalledTimes(1);
60
- expect(updateLatestEventIdMock).toHaveBeenCalledWith('eventId2');
75
+ expect(getEventsMock).toHaveBeenCalledTimes(2);
76
+ expect(getEventsMock).toHaveBeenCalledWith('EventId2');
61
77
  });
62
78
 
63
- it("should continue with more events", async () => {
64
- getEventsMock.mockImplementation((lastEventId: string) => Promise.resolve({
65
- lastEventId: lastEventId === "eventId1" ? "eventId2" : "eventId3",
66
- more: lastEventId === "eventId1" ? true : false,
67
- refresh: false,
68
- events: lastEventId === "eventId1" ? ["event1", "event2"] : ["event3"],
69
- }));
79
+ it("should stop polling when stopped", async () => {
80
+ getLatestEventIdMock.mockResolvedValue('eventId1');
81
+ getEventsMock.mockImplementation(async function* () {
82
+ yield {
83
+ type: DriveEventType.FastForward,
84
+ treeEventScopeId: 'volume1',
85
+ eventId: 'eventId1',
86
+ };
87
+ });
88
+
70
89
  await manager.start();
71
90
  await jest.runOnlyPendingTimersAsync();
72
- expect(getEventsMock).toHaveBeenCalledTimes(2);
73
- expect(listenerMock).toHaveBeenCalledTimes(2);
74
- expect(listenerMock).toHaveBeenCalledWith(["event1", "event2"], false);
75
- expect(listenerMock).toHaveBeenCalledWith(["event3"], false);
76
- expect(updateLatestEventIdMock).toHaveBeenCalledTimes(3);
77
- expect(updateLatestEventIdMock).toHaveBeenCalledWith('eventId1');
78
- expect(updateLatestEventIdMock).toHaveBeenCalledWith('eventId2');
79
- expect(updateLatestEventIdMock).toHaveBeenCalledWith('eventId3');
91
+
92
+ const callsBeforeStop = getEventsMock.mock.calls.length;
93
+ await manager.stop();
94
+ await jest.runOnlyPendingTimersAsync();
95
+
96
+ // Should not have made additional calls after stopping
97
+ expect(getEventsMock).toHaveBeenCalledTimes(callsBeforeStop);
80
98
  });
81
99
 
82
- it("should refresh if event does not exist", async () => {
83
- getEventsMock.mockImplementation(() => Promise.reject(new NotFoundAPIError('Event not found', 2501)));
84
- await manager.start();
100
+ it("should notify all listeners when getting events", async () => {
101
+ getLatestEventIdMock.mockResolvedValue('eventId1');
102
+
103
+ const mockEvents: DriveEvent[] = [
104
+ {
105
+ type: DriveEventType.NodeCreated,
106
+ nodeUid: 'node1',
107
+ parentNodeUid: 'parent1',
108
+ isTrashed: false,
109
+ isShared: false,
110
+ treeEventScopeId: 'volume1',
111
+ eventId: 'eventId2',
112
+ },
113
+ ];
114
+
115
+ getEventsMock.mockImplementationOnce(async function* () {
116
+ yield* mockEvents;
117
+ }).mockImplementation(async function* () {
118
+ });
119
+
120
+ expect(await manager.start()).toBeUndefined();
85
121
  await jest.runOnlyPendingTimersAsync();
86
- expect(getLastEventIdMock).toHaveBeenCalledTimes(2);
87
122
  expect(listenerMock).toHaveBeenCalledTimes(1);
88
- expect(listenerMock).toHaveBeenCalledWith([], true);
89
- expect(updateLatestEventIdMock).toHaveBeenCalledTimes(1);
90
- expect(updateLatestEventIdMock).toHaveBeenCalledWith('eventId1');
123
+ expect(listenerMock).toHaveBeenNthCalledWith(1, mockEvents[0]);
91
124
  });
92
125
 
93
- it("should retry on error", async () => {
94
- let index = 0;
95
- getEventsMock.mockImplementation(() => {
96
- index++;
97
- if (index <= 3) {
98
- return Promise.reject(new Error("Error"));
126
+ it("should propagate unsubscription errors", async () => {
127
+ getLatestEventIdMock.mockImplementation(() => {
128
+ throw new UnsubscribeFromEventsSourceError("Not found");
129
+ });
130
+
131
+ await expect(manager.start()).rejects.toThrow(UnsubscribeFromEventsSourceError);
132
+
133
+ expect(getLatestEventIdMock).toHaveBeenCalledTimes(1);
134
+ expect(listenerMock).toHaveBeenCalledTimes(0);
135
+ expect(getEventsMock).toHaveBeenCalledTimes(0);
136
+ });
137
+
138
+ it("should continue processing multiple events", async () => {
139
+ getLatestEventIdMock.mockResolvedValue('eventId1');
140
+
141
+ const mockEvents: DriveEvent[] = [
142
+ {
143
+ type: DriveEventType.NodeCreated,
144
+ nodeUid: 'node1',
145
+ parentNodeUid: 'parent1',
146
+ isTrashed: false,
147
+ isShared: false,
148
+ treeEventScopeId: 'volume1',
149
+ eventId: 'eventId2',
150
+ },
151
+ {
152
+ type: DriveEventType.NodeCreated,
153
+ nodeUid: 'node2',
154
+ parentNodeUid: 'parent1',
155
+ isTrashed: false,
156
+ isShared: false,
157
+ treeEventScopeId: 'volume1',
158
+ eventId: 'eventId3',
99
159
  }
100
- return Promise.resolve({
101
- lastEventId: "eventId2",
102
- more: false,
103
- refresh: false,
104
- events: ["event1", "event2"],
105
- });
160
+ ];
161
+
162
+ getEventsMock.mockImplementationOnce(async function* () {
163
+ yield* mockEvents;
164
+ }).mockImplementation(async function* () {
165
+ // Empty generator for subsequent calls
106
166
  });
167
+
107
168
  await manager.start();
108
- updateLatestEventIdMock.mockClear();
169
+ await jest.runOnlyPendingTimersAsync();
109
170
 
110
- // First failure.
171
+ expect(listenerMock).toHaveBeenCalledTimes(2);
172
+ expect(listenerMock).toHaveBeenNthCalledWith(1, mockEvents[0]);
173
+ expect(listenerMock).toHaveBeenNthCalledWith(2, mockEvents[1]);
174
+
175
+ getEventsMock.mockImplementationOnce(async function* () {
176
+ yield* mockEvents;
177
+ })
111
178
  await jest.runOnlyPendingTimersAsync();
112
- expect(listenerMock).toHaveBeenCalledTimes(0);
113
- expect(manager.nextPollTimeout).toBe(30000);
179
+ expect(listenerMock).toHaveBeenCalledTimes(4);
180
+ expect(listenerMock).toHaveBeenNthCalledWith(1, mockEvents[0]);
181
+ expect(listenerMock).toHaveBeenNthCalledWith(2, mockEvents[1]);
182
+ });
183
+
184
+ it("should retry on error with exponential backoff", async () => {
185
+ getLatestEventIdMock.mockResolvedValue('eventId1');
186
+
187
+ let callCount = 0;
188
+ getEventsMock.mockImplementation(async function* () {
189
+ callCount++;
190
+ if (callCount <= 3) {
191
+ throw new Error("Network error");
192
+ }
193
+ yield {
194
+ type: DriveEventType.FastForward,
195
+ treeEventScopeId: 'volume1',
196
+ eventId: 'eventId3',
197
+ };
198
+ });
199
+
200
+ expect(manager['retryIndex']).toEqual(0);
201
+
202
+ expect(await manager.start()).toBeUndefined();
203
+ expect(getEventsMock).toHaveBeenCalledTimes(1);
204
+ expect(manager['retryIndex']).toEqual(1);
114
205
 
115
- // Second failure.
116
206
  await jest.runOnlyPendingTimersAsync();
117
- expect(listenerMock).toHaveBeenCalledTimes(0);
118
- expect(manager.nextPollTimeout).toBe(60000);
207
+ expect(getEventsMock).toHaveBeenCalledTimes(2);
208
+ expect(manager['retryIndex']).toEqual(2);
119
209
 
120
- // Third failure.
121
210
  await jest.runOnlyPendingTimersAsync();
211
+ expect(manager['retryIndex']).toEqual(3);
212
+
122
213
  expect(listenerMock).toHaveBeenCalledTimes(0);
123
- expect(manager.nextPollTimeout).toBe(90000);
124
214
 
125
- // And now it passes.
126
215
  await jest.runOnlyPendingTimersAsync();
127
216
  expect(listenerMock).toHaveBeenCalledTimes(1);
128
- expect(listenerMock).toHaveBeenCalledWith(["event1", "event2"], false);
129
- expect(updateLatestEventIdMock).toHaveBeenCalledTimes(1);
130
- expect(updateLatestEventIdMock).toHaveBeenCalledWith('eventId2');
217
+ // After success, retry index should reset
218
+ expect(manager['retryIndex']).toEqual(0);
131
219
  });
132
220
 
133
- it("should stop polling", async () => {
134
- await manager.start();
221
+ it("should stop polling when stopped immediately", async () => {
222
+ getLatestEventIdMock.mockResolvedValue('eventId1');
223
+ getEventsMock.mockImplementation(async function* () {
224
+ yield {
225
+ type: DriveEventType.FastForward,
226
+ treeEventScopeId: 'volume1',
227
+ eventId: 'eventId1',
228
+ };
229
+ });
230
+
231
+ expect(await manager.start()).toBeUndefined();
232
+ expect(getEventsMock).toHaveBeenCalledTimes(1);
135
233
  await manager.stop();
136
234
  await jest.runOnlyPendingTimersAsync();
137
- expect(getEventsMock).toHaveBeenCalledTimes(0);
235
+
236
+ // getEvents should have been called once during start, but not again after stop
237
+ expect(getEventsMock).toHaveBeenCalledTimes(1);
238
+ });
239
+
240
+ it("should handle empty event streams", async () => {
241
+ getLatestEventIdMock.mockResolvedValue('eventId1');
242
+
243
+ getEventsMock.mockImplementation(async function* () {
244
+ // Empty generator - no events
245
+ });
246
+
247
+ await manager.start();
248
+ await jest.runOnlyPendingTimersAsync();
249
+
250
+ expect(listenerMock).toHaveBeenCalledTimes(0);
138
251
  });
139
252
  });
@@ -1,168 +1,122 @@
1
1
  import { Logger } from "../../interface";
2
- import { NotFoundAPIError } from "../apiService";
3
- import { Events } from "./interface";
2
+ import { EventManagerInterface, Event, EventSubscription } from "./interface";
4
3
 
5
- const DEFAULT_POLLING_INTERVAL_IN_SECONDS = 30;
6
4
  const FIBONACCI_LIST = [1, 1, 2, 3, 5, 8, 13];
7
5
 
8
- /**
9
- * `fullRefresh` is true when the event manager has requested a full
10
- * refresh of the data. That can happen if there is too many events
11
- * to be processed or the last event ID is too old.
12
- */
13
- type Listener<T> = (events: T[], fullRefresh: boolean) => Promise<void>;
6
+ type Listener<T> = (event: T) => Promise<void>;
7
+
14
8
 
15
9
  /**
16
10
  * Event manager general helper that is responsible for fetching events
17
11
  * from the server and notifying listeners about the events.
18
- *
12
+ *
19
13
  * The specific implementation of fetching the events from the API must
20
14
  * be passed as dependency and can be used for any type of events that
21
15
  * supports the same structure.
22
- *
16
+ *
23
17
  * The manager will not start fetching events until the `start` method is
24
18
  * called. Once started, the manager will fetch events in a loop with
25
19
  * a timeout between each fetch. The default timeout is 30 seconds and
26
20
  * additional jitter is used in case of failure.
27
- *
28
- * Example of usage:
29
- *
30
- * ```typescript
31
- * const manager = new EventManager(
32
- * logger,
33
- * () => apiService.getLatestEventId(),
34
- * (eventId) => apiService.getEvents(eventId),
35
- * );
36
- *
37
- * manager.addListener((events, fullRefresh) => {
38
- * // Process the events
39
- * });
40
- *
41
- * manager.start();
42
- * ```
43
21
  */
44
- export class EventManager<T> {
22
+ export class EventManager<T extends Event> {
23
+ private logger: Logger;
45
24
  private latestEventId?: string;
46
25
  private timeoutHandle?: ReturnType<typeof setTimeout>;
47
26
  private processPromise?: Promise<void>;
48
27
  private listeners: Listener<T>[] = [];
49
28
  private retryIndex: number = 0;
50
29
 
51
- pollingIntervalInSeconds = DEFAULT_POLLING_INTERVAL_IN_SECONDS;
52
-
53
30
  constructor(
54
- private logger: Logger,
55
- private getLatestEventId: () => Promise<string>,
56
- private getEvents: (eventId: string) => Promise<Events<T>>,
57
- private updateLatestEventId: (lastEventId: string) => Promise<void>,
31
+ private specializedEventManager: EventManagerInterface<T>,
32
+ private pollingIntervalInSeconds: number,
33
+ latestEventId: string | null,
58
34
  ) {
59
- this.logger = logger;
60
- this.getLatestEventId = getLatestEventId;
61
- this.getEvents = getEvents;
62
- this.updateLatestEventId = updateLatestEventId;
35
+ if (latestEventId !== null) {
36
+ this.latestEventId = latestEventId;
37
+ }
38
+ this.logger = specializedEventManager.getLogger();
63
39
  }
64
40
 
65
- addListener(callback: Listener<T>): void {
41
+ async start(): Promise<void> {
42
+ if (this.latestEventId === undefined) {
43
+ this.latestEventId = await this.specializedEventManager.getLatestEventId();
44
+ }
45
+ this.processPromise = this.processEvents();
46
+ }
47
+
48
+ addListener(callback: Listener<T>): EventSubscription {
66
49
  this.listeners.push(callback);
50
+ return {
51
+ dispose: (): void => {
52
+ const index = this.listeners.indexOf(callback);
53
+ this.listeners.splice(index, 1);
54
+ },
55
+ };
67
56
  }
68
57
 
69
- async start(): Promise<void> {
70
- this.logger.info(`Starting event manager with polling interval ${this.pollingIntervalInSeconds} seconds`);
71
- await this.stop();
72
- this.processPromise = this.processEvents();
58
+ setPollingInterval(pollingIntervalInSeconds: number): void {
59
+ this.pollingIntervalInSeconds = pollingIntervalInSeconds;
60
+ }
61
+
62
+ async stop(): Promise<void> {
63
+ if (this.processPromise) {
64
+ this.logger.info(`Stopping event manager`);
65
+ try {
66
+ await this.processPromise;
67
+ } catch (error) {
68
+ this.logger.warn(`Failed to stop cleanly: ${error instanceof Error ? error.message : error}`);
69
+ }
70
+ }
71
+
72
+ if (!this.timeoutHandle) {
73
+ return;
74
+ }
75
+
76
+ clearTimeout(this.timeoutHandle);
77
+ this.timeoutHandle = undefined;
78
+ }
79
+
80
+ private async notifyListeners(event: T): Promise<void> {
81
+ for (const listener of this.listeners) {
82
+ await listener(event);
83
+ }
73
84
  }
74
85
 
75
86
  private async processEvents() {
87
+ let listenerError;
76
88
  try {
77
- if (!this.latestEventId) {
78
- this.latestEventId = await this.getLatestEventId();
79
- await this.updateLatestEventId(this.latestEventId);
80
- } else {
81
- while (true) {
82
- let result;
83
- try {
84
- result = await this.getEvents(this.latestEventId);
85
- } catch (error: unknown) {
86
- // If last event ID is not found, we need to refresh the data.
87
- // Caller is notified via standard event update with refresh flag.
88
- if (error instanceof NotFoundAPIError) {
89
- this.logger.warn(`Last event ID not found, refreshing data`);
90
- result = {
91
- lastEventId: await this.getLatestEventId(),
92
- more: false,
93
- refresh: true,
94
- events: [],
95
- };
96
- } else {
97
- // Any other error is considered as a failure and we will retry
98
- // with backoff policy.
99
- throw error;
100
- }
101
- }
102
- await this.notifyListeners(result);
103
- if (result.lastEventId !== this.latestEventId) {
104
- await this.updateLatestEventId(result.lastEventId);
105
- this.latestEventId = result.lastEventId;
106
- }
107
- if (!result.more) {
108
- break;
109
- }
89
+ const events = this.specializedEventManager.getEvents(this.latestEventId!);
90
+ for await (const event of events) {
91
+ try {
92
+ await this.notifyListeners(event);
93
+ } catch (internalListenerError) {
94
+ listenerError = internalListenerError;
95
+ break;
110
96
  }
97
+ this.latestEventId = event.eventId;
111
98
  }
112
99
  this.retryIndex = 0;
113
100
  } catch (error: unknown) {
101
+ // This could be improved to catch api specific errors and let the listener errors bubble up directly
114
102
  this.logger.error(`Failed to process events: ${error instanceof Error ? error.message : error} (retry ${this.retryIndex}, last event ID: ${this.latestEventId})`);
115
103
  this.retryIndex++;
116
104
  }
105
+ if (listenerError) {
106
+ throw listenerError;
107
+ }
117
108
 
118
109
  this.timeoutHandle = setTimeout(() => {
119
110
  this.processPromise = this.processEvents();
120
111
  }, this.nextPollTimeout);
121
112
  };
122
113
 
123
- private async notifyListeners(result: Events<T>): Promise<void> {
124
- if (result.events.length === 0 && !result.refresh) {
125
- return;
126
- }
127
- if (!this.listeners.length) {
128
- return;
129
- }
130
-
131
- this.logger.debug(`Notifying listeners about ${result.events.length} events`);
132
-
133
- for (const listener of this.listeners) {
134
- try {
135
- await listener(result.events, result.refresh);
136
- } catch (error: unknown) {
137
- this.logger.error(`Failed to process events: ${error instanceof Error ? error.message : error} (last event ID: ${result.lastEventId}, refresh: ${result.refresh})`);
138
- throw error;
139
- }
140
- }
141
- }
142
-
143
114
  /**
144
115
  * Polling timeout is using exponential backoff with Fibonacci sequence.
145
- *
146
- * The timeout is public for testing purposes only.
147
116
  */
148
- get nextPollTimeout(): number {
117
+ private get nextPollTimeout(): number {
149
118
  const retryIndex = Math.min(this.retryIndex, FIBONACCI_LIST.length - 1);
119
+ // FIXME jitter
150
120
  return this.pollingIntervalInSeconds * 1000 * FIBONACCI_LIST[retryIndex];
151
121
  }
152
-
153
- async stop(): Promise<void> {
154
- if (this.processPromise) {
155
- this.logger.info(`Stopping event manager`);
156
- try {
157
- await this.processPromise;
158
- } catch {}
159
- }
160
-
161
- if (!this.timeoutHandle) {
162
- return;
163
- }
164
-
165
- clearTimeout(this.timeoutHandle);
166
- this.timeoutHandle = undefined;
167
- }
168
122
  }