@protontech/drive-sdk 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/dist/crypto/driveCrypto.d.ts +11 -0
  2. package/dist/crypto/driveCrypto.js +20 -7
  3. package/dist/crypto/driveCrypto.js.map +1 -1
  4. package/dist/crypto/interface.d.ts +10 -1
  5. package/dist/crypto/openPGPCrypto.d.ts +18 -2
  6. package/dist/crypto/openPGPCrypto.js +25 -6
  7. package/dist/crypto/openPGPCrypto.js.map +1 -1
  8. package/dist/diagnostic/telemetry.d.ts +1 -1
  9. package/dist/diagnostic/telemetry.js +1 -1
  10. package/dist/diagnostic/telemetry.js.map +1 -1
  11. package/dist/interface/download.d.ts +46 -0
  12. package/dist/interface/index.d.ts +2 -2
  13. package/dist/interface/index.js.map +1 -1
  14. package/dist/interface/nodes.d.ts +26 -1
  15. package/dist/interface/nodes.js.map +1 -1
  16. package/dist/interface/telemetry.d.ts +5 -2
  17. package/dist/interface/telemetry.js.map +1 -1
  18. package/dist/internal/apiService/apiService.js +1 -1
  19. package/dist/internal/apiService/apiService.js.map +1 -1
  20. package/dist/internal/apiService/driveTypes.d.ts +78 -165
  21. package/dist/internal/apiService/index.d.ts +1 -1
  22. package/dist/internal/apiService/index.js +2 -2
  23. package/dist/internal/apiService/index.js.map +1 -1
  24. package/dist/internal/apiService/transformers.d.ts +1 -1
  25. package/dist/internal/apiService/transformers.js +2 -2
  26. package/dist/internal/apiService/transformers.js.map +1 -1
  27. package/dist/internal/download/blockIndex.d.ts +11 -0
  28. package/dist/internal/download/blockIndex.js +35 -0
  29. package/dist/internal/download/blockIndex.js.map +1 -0
  30. package/dist/internal/download/blockIndex.test.d.ts +1 -0
  31. package/dist/internal/download/blockIndex.test.js +147 -0
  32. package/dist/internal/download/blockIndex.test.js.map +1 -0
  33. package/dist/internal/download/fileDownloader.d.ts +6 -2
  34. package/dist/internal/download/fileDownloader.js +83 -6
  35. package/dist/internal/download/fileDownloader.js.map +1 -1
  36. package/dist/internal/download/fileDownloader.test.js +69 -4
  37. package/dist/internal/download/fileDownloader.test.js.map +1 -1
  38. package/dist/internal/download/interface.d.ts +4 -4
  39. package/dist/internal/download/seekableStream.d.ts +80 -0
  40. package/dist/internal/download/seekableStream.js +163 -0
  41. package/dist/internal/download/seekableStream.js.map +1 -0
  42. package/dist/internal/download/seekableStream.test.d.ts +1 -0
  43. package/dist/internal/download/seekableStream.test.js +149 -0
  44. package/dist/internal/download/seekableStream.test.js.map +1 -0
  45. package/dist/internal/download/telemetry.js +1 -1
  46. package/dist/internal/download/telemetry.js.map +1 -1
  47. package/dist/internal/download/telemetry.test.js +7 -7
  48. package/dist/internal/download/telemetry.test.js.map +1 -1
  49. package/dist/internal/errors.d.ts +1 -1
  50. package/dist/internal/errors.js +7 -1
  51. package/dist/internal/errors.js.map +1 -1
  52. package/dist/internal/errors.test.js +44 -10
  53. package/dist/internal/errors.test.js.map +1 -1
  54. package/dist/internal/events/eventManager.d.ts +1 -0
  55. package/dist/internal/events/eventManager.js +9 -0
  56. package/dist/internal/events/eventManager.js.map +1 -1
  57. package/dist/internal/events/eventManager.test.js +53 -38
  58. package/dist/internal/events/eventManager.test.js.map +1 -1
  59. package/dist/internal/events/index.d.ts +4 -3
  60. package/dist/internal/events/index.js +38 -32
  61. package/dist/internal/events/index.js.map +1 -1
  62. package/dist/internal/nodes/apiService.js +16 -3
  63. package/dist/internal/nodes/apiService.js.map +1 -1
  64. package/dist/internal/nodes/apiService.test.js +43 -7
  65. package/dist/internal/nodes/apiService.test.js.map +1 -1
  66. package/dist/internal/nodes/cache.js +9 -2
  67. package/dist/internal/nodes/cache.js.map +1 -1
  68. package/dist/internal/nodes/cache.test.js +6 -1
  69. package/dist/internal/nodes/cache.test.js.map +1 -1
  70. package/dist/internal/nodes/cryptoService.d.ts +4 -1
  71. package/dist/internal/nodes/cryptoService.js +66 -16
  72. package/dist/internal/nodes/cryptoService.js.map +1 -1
  73. package/dist/internal/nodes/cryptoService.test.js +129 -46
  74. package/dist/internal/nodes/cryptoService.test.js.map +1 -1
  75. package/dist/internal/nodes/events.js +7 -7
  76. package/dist/internal/nodes/events.js.map +1 -1
  77. package/dist/internal/nodes/extendedAttributes.d.ts +2 -1
  78. package/dist/internal/nodes/extendedAttributes.js +27 -1
  79. package/dist/internal/nodes/extendedAttributes.js.map +1 -1
  80. package/dist/internal/nodes/extendedAttributes.test.js +59 -6
  81. package/dist/internal/nodes/extendedAttributes.test.js.map +1 -1
  82. package/dist/internal/nodes/index.test.js +1 -1
  83. package/dist/internal/nodes/index.test.js.map +1 -1
  84. package/dist/internal/nodes/interface.d.ts +18 -2
  85. package/dist/internal/nodes/nodesAccess.js +11 -1
  86. package/dist/internal/nodes/nodesAccess.js.map +1 -1
  87. package/dist/internal/nodes/nodesManagement.js +1 -1
  88. package/dist/internal/nodes/nodesManagement.js.map +1 -1
  89. package/dist/internal/nodes/nodesRevisions.d.ts +4 -3
  90. package/dist/internal/nodes/nodesRevisions.js +2 -2
  91. package/dist/internal/nodes/nodesRevisions.js.map +1 -1
  92. package/dist/internal/shares/cryptoService.js +7 -4
  93. package/dist/internal/shares/cryptoService.js.map +1 -1
  94. package/dist/internal/shares/cryptoService.test.js +5 -3
  95. package/dist/internal/shares/cryptoService.test.js.map +1 -1
  96. package/dist/internal/sharing/apiService.js +5 -5
  97. package/dist/internal/sharing/apiService.js.map +1 -1
  98. package/dist/internal/sharing/cache.d.ts +1 -0
  99. package/dist/internal/sharing/cache.js +9 -0
  100. package/dist/internal/sharing/cache.js.map +1 -1
  101. package/dist/internal/sharing/cryptoService.js +8 -5
  102. package/dist/internal/sharing/cryptoService.js.map +1 -1
  103. package/dist/internal/sharing/cryptoService.test.js +7 -4
  104. package/dist/internal/sharing/cryptoService.test.js.map +1 -1
  105. package/dist/internal/sharing/events.d.ts +1 -0
  106. package/dist/internal/sharing/events.js +28 -18
  107. package/dist/internal/sharing/events.js.map +1 -1
  108. package/dist/internal/sharing/events.test.js +98 -84
  109. package/dist/internal/sharing/events.test.js.map +1 -1
  110. package/dist/internal/upload/interface.d.ts +1 -0
  111. package/dist/internal/upload/manager.d.ts +1 -1
  112. package/dist/internal/upload/manager.js +8 -4
  113. package/dist/internal/upload/manager.js.map +1 -1
  114. package/dist/internal/upload/manager.test.js +7 -10
  115. package/dist/internal/upload/manager.test.js.map +1 -1
  116. package/dist/internal/upload/streamUploader.js +1 -1
  117. package/dist/internal/upload/streamUploader.js.map +1 -1
  118. package/dist/internal/upload/streamUploader.test.js +1 -1
  119. package/dist/internal/upload/streamUploader.test.js.map +1 -1
  120. package/dist/internal/upload/telemetry.js +2 -2
  121. package/dist/internal/upload/telemetry.js.map +1 -1
  122. package/dist/internal/upload/telemetry.test.js +7 -7
  123. package/dist/internal/upload/telemetry.test.js.map +1 -1
  124. package/dist/protonDriveClient.js +2 -2
  125. package/dist/protonDriveClient.js.map +1 -1
  126. package/dist/telemetry.d.ts +2 -2
  127. package/dist/telemetry.js +2 -2
  128. package/dist/telemetry.js.map +1 -1
  129. package/dist/tests/telemetry.js +1 -1
  130. package/dist/tests/telemetry.js.map +1 -1
  131. package/dist/transformers.d.ts +1 -1
  132. package/dist/transformers.js +3 -1
  133. package/dist/transformers.js.map +1 -1
  134. package/package.json +1 -1
  135. package/src/crypto/driveCrypto.ts +70 -25
  136. package/src/crypto/interface.ts +15 -0
  137. package/src/crypto/openPGPCrypto.ts +37 -5
  138. package/src/diagnostic/telemetry.ts +1 -1
  139. package/src/interface/download.ts +46 -0
  140. package/src/interface/index.ts +2 -1
  141. package/src/interface/nodes.ts +28 -1
  142. package/src/interface/telemetry.ts +6 -1
  143. package/src/internal/apiService/apiService.ts +1 -1
  144. package/src/internal/apiService/driveTypes.ts +78 -165
  145. package/src/internal/apiService/index.ts +1 -1
  146. package/src/internal/apiService/transformers.ts +1 -1
  147. package/src/internal/download/blockIndex.test.ts +158 -0
  148. package/src/internal/download/blockIndex.ts +36 -0
  149. package/src/internal/download/fileDownloader.test.ts +100 -7
  150. package/src/internal/download/fileDownloader.ts +109 -9
  151. package/src/internal/download/interface.ts +4 -4
  152. package/src/internal/download/seekableStream.test.ts +187 -0
  153. package/src/internal/download/seekableStream.ts +182 -0
  154. package/src/internal/download/telemetry.test.ts +7 -7
  155. package/src/internal/download/telemetry.ts +1 -1
  156. package/src/internal/errors.test.ts +45 -11
  157. package/src/internal/errors.ts +8 -0
  158. package/src/internal/events/eventManager.test.ts +61 -40
  159. package/src/internal/events/eventManager.ts +10 -0
  160. package/src/internal/events/index.ts +53 -35
  161. package/src/internal/nodes/apiService.test.ts +59 -15
  162. package/src/internal/nodes/apiService.ts +21 -4
  163. package/src/internal/nodes/cache.test.ts +6 -1
  164. package/src/internal/nodes/cache.ts +9 -2
  165. package/src/internal/nodes/cryptoService.test.ts +139 -47
  166. package/src/internal/nodes/cryptoService.ts +94 -9
  167. package/src/internal/nodes/events.ts +6 -7
  168. package/src/internal/nodes/extendedAttributes.test.ts +60 -7
  169. package/src/internal/nodes/extendedAttributes.ts +37 -1
  170. package/src/internal/nodes/index.test.ts +1 -1
  171. package/src/internal/nodes/interface.ts +19 -2
  172. package/src/internal/nodes/nodesAccess.ts +15 -1
  173. package/src/internal/nodes/nodesManagement.ts +1 -1
  174. package/src/internal/nodes/nodesRevisions.ts +14 -5
  175. package/src/internal/shares/cryptoService.test.ts +5 -3
  176. package/src/internal/shares/cryptoService.ts +7 -4
  177. package/src/internal/sharing/apiService.ts +6 -6
  178. package/src/internal/sharing/cache.ts +9 -0
  179. package/src/internal/sharing/cryptoService.test.ts +7 -4
  180. package/src/internal/sharing/cryptoService.ts +8 -5
  181. package/src/internal/sharing/events.test.ts +104 -89
  182. package/src/internal/sharing/events.ts +33 -18
  183. package/src/internal/upload/interface.ts +1 -0
  184. package/src/internal/upload/manager.test.ts +7 -10
  185. package/src/internal/upload/manager.ts +7 -4
  186. package/src/internal/upload/streamUploader.test.ts +1 -1
  187. package/src/internal/upload/streamUploader.ts +1 -1
  188. package/src/internal/upload/telemetry.test.ts +7 -7
  189. package/src/internal/upload/telemetry.ts +2 -2
  190. package/src/protonDriveClient.ts +2 -2
  191. package/src/telemetry.ts +2 -2
  192. package/src/tests/telemetry.ts +1 -1
  193. package/src/transformers.ts +6 -2
@@ -0,0 +1,182 @@
1
+ interface UnderlyingSeekableSource extends UnderlyingDefaultSource<Uint8Array> {
2
+ seek: (position: number) => void | Promise<void>;
3
+ }
4
+
5
+ /**
6
+ * A seekable readable stream that can be used to seek to a specific position
7
+ * in the stream.
8
+ *
9
+ * This is useful for downloading the file in chunks or jumping to a specific
10
+ * position in the file when streaming a video.
11
+ *
12
+ * Example to get next chunk of data from the stream at position 100:
13
+ *
14
+ * ```
15
+ * const stream = new SeekableReadableStream(underlyingSource);
16
+ * const reader = stream.getReader();
17
+ * await stream.seek(100);
18
+ * const data = await stream.read();
19
+ * console.log(data);
20
+ * ```
21
+ */
22
+ export class SeekableReadableStream extends ReadableStream<Uint8Array> {
23
+ private seekCallback: (position: number) => void | Promise<void>;
24
+
25
+ constructor({ seek, ...underlyingSource }: UnderlyingSeekableSource, queuingStrategy?: QueuingStrategy<Uint8Array>) {
26
+ super(underlyingSource, queuingStrategy);
27
+ this.seekCallback = seek;
28
+ }
29
+
30
+ seek(position: number): void | Promise<void> {
31
+ return this.seekCallback(position);
32
+ }
33
+ }
34
+
35
+ /**
36
+ * A buffered seekable stream that allows to seek and read specific number of
37
+ * bytes from the stream.
38
+ *
39
+ * This is useful for reading specific range of data from the stream. Example
40
+ * being video player buffering the next several bytes.
41
+ *
42
+ * The underlying source can chunk the data into various sizes. To ensure that
43
+ * every read operation is for the correct location, the SeekableStream is not
44
+ * queueing the data upfront. Instead, it will read the data and buffer it for
45
+ * the next read operation. If seek is called, the internal buffer is updated
46
+ * accordingly.
47
+ *
48
+ * Example to read 10 bytes from the stream at position 100:
49
+ *
50
+ * ```
51
+ * const stream = new BufferedSeekableStream(underlyingSource);
52
+ * await stream.seek(100);
53
+ * const data = await stream.read(10);
54
+ * console.log(data);
55
+ * ```
56
+ */
57
+ export class BufferedSeekableStream extends SeekableReadableStream {
58
+ private buffer: Uint8Array = new Uint8Array(0);
59
+ private bufferPosition: number = 0;
60
+ private reader: ReadableStreamDefaultReader<Uint8Array> | null = null;
61
+ private streamClosed: boolean = false;
62
+ private currentPosition: number = 0;
63
+
64
+ constructor(underlyingSource: UnderlyingSeekableSource, queuingStrategy?: QueuingStrategy<Uint8Array>) {
65
+ // highWaterMark means that the stream will buffer up to this many
66
+ // bytes. We do not want to buffer anything
67
+ if (queuingStrategy && queuingStrategy.highWaterMark !== 0) {
68
+ throw new Error('highWaterMark must be 0');
69
+ }
70
+
71
+ super(underlyingSource, {
72
+ ...queuingStrategy,
73
+ highWaterMark: 0,
74
+ });
75
+
76
+ this.reader = super.getReader();
77
+ }
78
+
79
+ /**
80
+ * Read a specific number of bytes from the stream.
81
+ *
82
+ * When the underlying source provides more bytes than requested, the
83
+ * remaining bytes are buffered and used for the next read operation.
84
+ *
85
+ * @param numBytes - Number of bytes to read
86
+ * @returns Promise<Uint8Array> The read bytes
87
+ */
88
+ async read(numBytes: number): Promise<{ value: Uint8Array; done: boolean }> {
89
+ if (numBytes <= 0) {
90
+ throw new Error('Invalid number of bytes to read');
91
+ }
92
+
93
+ await this.ensureBufferSize(numBytes);
94
+
95
+ const result = this.buffer.slice(this.bufferPosition, this.bufferPosition + numBytes);
96
+ this.bufferPosition += numBytes;
97
+ this.currentPosition += numBytes;
98
+ return {
99
+ value: result,
100
+ done: this.streamClosed,
101
+ };
102
+ }
103
+
104
+ private async ensureBufferSize(minBytes: number): Promise<void> {
105
+ const availableBytes = this.buffer.length - this.bufferPosition;
106
+ const neededBytes = minBytes - availableBytes;
107
+
108
+ if (neededBytes <= 0 || this.streamClosed) {
109
+ return;
110
+ }
111
+
112
+ const chunks: Uint8Array[] = [];
113
+ let totalBytesRead = 0;
114
+
115
+ while (totalBytesRead < neededBytes && !this.streamClosed) {
116
+ if (!this.reader) {
117
+ throw new Error('Stream reader is not available');
118
+ }
119
+
120
+ const { done, value } = await this.reader.read();
121
+
122
+ if (done) {
123
+ this.streamClosed = true;
124
+ break;
125
+ }
126
+
127
+ if (value) {
128
+ chunks.push(value);
129
+ totalBytesRead += value.length;
130
+ }
131
+ }
132
+
133
+ if (chunks.length > 0) {
134
+ // Create new buffer with existing unused data plus new chunks
135
+ const unusedBufferData = this.buffer.slice(this.bufferPosition);
136
+ const newTotalLength = unusedBufferData.length + totalBytesRead;
137
+ const newBuffer = new Uint8Array(newTotalLength);
138
+
139
+ newBuffer.set(unusedBufferData, 0);
140
+ let offset = unusedBufferData.length;
141
+ for (const chunk of chunks) {
142
+ newBuffer.set(chunk, offset);
143
+ offset += chunk.length;
144
+ }
145
+
146
+ this.buffer = newBuffer;
147
+ this.bufferPosition = 0;
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Seek to the given position in the stream.
153
+ *
154
+ * If the position is outside of internally buffered data, the buffer is
155
+ * cleared. If the position is seeked back, the buffer is read again from
156
+ * the underlying source.
157
+ *
158
+ * @param position - The position to seek to in bytes.
159
+ */
160
+ async seek(position: number): Promise<void> {
161
+ const endOfBufferPosition = this.currentPosition + (this.buffer.length - this.bufferPosition);
162
+
163
+ if (position > endOfBufferPosition) {
164
+ this.buffer = new Uint8Array(0);
165
+ this.bufferPosition = 0;
166
+ } else if (position < this.currentPosition) {
167
+ this.buffer = new Uint8Array(0);
168
+ this.bufferPosition = 0;
169
+ } else {
170
+ this.bufferPosition += position - this.currentPosition;
171
+ }
172
+
173
+ await super.seek(position);
174
+
175
+ if (this.reader) {
176
+ this.reader.releaseLock();
177
+ }
178
+ this.reader = super.getReader();
179
+ this.streamClosed = false;
180
+ this.currentPosition = position;
181
+ }
182
+ }
@@ -14,7 +14,7 @@ describe('DownloadTelemetry', () => {
14
14
 
15
15
  beforeEach(() => {
16
16
  mockTelemetry = {
17
- logEvent: jest.fn(),
17
+ recordMetric: jest.fn(),
18
18
  getLogger: jest.fn().mockReturnValue({
19
19
  info: jest.fn(),
20
20
  warn: jest.fn(),
@@ -34,7 +34,7 @@ describe('DownloadTelemetry', () => {
34
34
  const error = new Error('Failed');
35
35
  await downloadTelemetry.downloadInitFailed(nodeUid, error);
36
36
 
37
- expect(mockTelemetry.logEvent).toHaveBeenCalledWith({
37
+ expect(mockTelemetry.recordMetric).toHaveBeenCalledWith({
38
38
  eventName: 'download',
39
39
  volumeType: 'own_volume',
40
40
  downloadedSize: 0,
@@ -47,7 +47,7 @@ describe('DownloadTelemetry', () => {
47
47
  const error = new Error('Failed');
48
48
  await downloadTelemetry.downloadFailed(revisionUid, error, 123, 456);
49
49
 
50
- expect(mockTelemetry.logEvent).toHaveBeenCalledWith({
50
+ expect(mockTelemetry.recordMetric).toHaveBeenCalledWith({
51
51
  eventName: 'download',
52
52
  volumeType: 'own_volume',
53
53
  downloadedSize: 123,
@@ -60,7 +60,7 @@ describe('DownloadTelemetry', () => {
60
60
  it('should log successful download (excludes error)', async () => {
61
61
  await downloadTelemetry.downloadFinished(revisionUid, 500);
62
62
 
63
- expect(mockTelemetry.logEvent).toHaveBeenCalledWith({
63
+ expect(mockTelemetry.recordMetric).toHaveBeenCalledWith({
64
64
  eventName: 'download',
65
65
  volumeType: 'own_volume',
66
66
  downloadedSize: 500,
@@ -70,7 +70,7 @@ describe('DownloadTelemetry', () => {
70
70
 
71
71
  describe('detect error category', () => {
72
72
  const verifyErrorCategory = (error: string) => {
73
- expect(mockTelemetry.logEvent).toHaveBeenCalledWith(
73
+ expect(mockTelemetry.recordMetric).toHaveBeenCalledWith(
74
74
  expect.objectContaining({
75
75
  error,
76
76
  }),
@@ -80,7 +80,7 @@ describe('DownloadTelemetry', () => {
80
80
  it('should ignore ValidationError', async () => {
81
81
  const error = new ValidationError('Validation error');
82
82
  await downloadTelemetry.downloadFailed(revisionUid, error, 100, 200);
83
- expect(mockTelemetry.logEvent).not.toHaveBeenCalled();
83
+ expect(mockTelemetry.recordMetric).not.toHaveBeenCalled();
84
84
  });
85
85
 
86
86
  it('should ignore AbortError', async () => {
@@ -88,7 +88,7 @@ describe('DownloadTelemetry', () => {
88
88
  error.name = 'AbortError';
89
89
  await downloadTelemetry.downloadFailed(revisionUid, error, 100, 200);
90
90
 
91
- expect(mockTelemetry.logEvent).not.toHaveBeenCalled();
91
+ expect(mockTelemetry.recordMetric).not.toHaveBeenCalled();
92
92
  });
93
93
 
94
94
  it('should detect "rate_limited" error for RateLimitedError', async () => {
@@ -80,7 +80,7 @@ export class DownloadTelemetry {
80
80
  this.logger.error('Failed to get metric volume type', error);
81
81
  }
82
82
 
83
- this.telemetry.logEvent({
83
+ this.telemetry.recordMetric({
84
84
  eventName: 'download',
85
85
  volumeType,
86
86
  ...options,
@@ -2,20 +2,54 @@ import { VERIFICATION_STATUS } from '../crypto';
2
2
  import { getVerificationMessage } from './errors';
3
3
 
4
4
  describe('getVerificationMessage', () => {
5
- const testCases: [VERIFICATION_STATUS, string | undefined, boolean, string][] = [
6
- [VERIFICATION_STATUS.NOT_SIGNED, 'type', false, 'Missing signature for type'],
7
- [VERIFICATION_STATUS.NOT_SIGNED, undefined, false, 'Missing signature'],
8
- [VERIFICATION_STATUS.NOT_SIGNED, 'type', true, 'Missing signature for type'],
9
- [VERIFICATION_STATUS.NOT_SIGNED, undefined, true, 'Missing signature'],
10
- [VERIFICATION_STATUS.SIGNED_AND_INVALID, 'type', false, 'Signature verification for type failed'],
11
- [VERIFICATION_STATUS.SIGNED_AND_INVALID, undefined, false, 'Signature verification failed'],
12
- [VERIFICATION_STATUS.SIGNED_AND_INVALID, 'type', true, 'Verification keys for type are not available'],
13
- [VERIFICATION_STATUS.SIGNED_AND_INVALID, undefined, true, 'Verification keys are not available'],
5
+ const testCases: [VERIFICATION_STATUS, Error[] | undefined, string | undefined, boolean, string][] = [
6
+ [VERIFICATION_STATUS.NOT_SIGNED, undefined, 'type', false, 'Missing signature for type'],
7
+ [VERIFICATION_STATUS.NOT_SIGNED, undefined, undefined, false, 'Missing signature'],
8
+ [VERIFICATION_STATUS.NOT_SIGNED, undefined, 'type', true, 'Missing signature for type'],
9
+ [VERIFICATION_STATUS.NOT_SIGNED, undefined, undefined, true, 'Missing signature'],
10
+ [VERIFICATION_STATUS.SIGNED_AND_INVALID, undefined, 'type', false, 'Signature verification for type failed'],
11
+ [VERIFICATION_STATUS.SIGNED_AND_INVALID, undefined, undefined, false, 'Signature verification failed'],
12
+ [
13
+ VERIFICATION_STATUS.SIGNED_AND_INVALID,
14
+ undefined,
15
+ 'type',
16
+ true,
17
+ 'Verification keys for type are not available',
18
+ ],
19
+ [VERIFICATION_STATUS.SIGNED_AND_INVALID, undefined, undefined, true, 'Verification keys are not available'],
20
+ [
21
+ VERIFICATION_STATUS.SIGNED_AND_INVALID,
22
+ [new Error('error1'), new Error('error2')],
23
+ undefined,
24
+ false,
25
+ 'Signature verification failed: error1, error2',
26
+ ],
27
+ [
28
+ VERIFICATION_STATUS.SIGNED_AND_INVALID,
29
+ [new Error('error1'), new Error('error2')],
30
+ 'type',
31
+ false,
32
+ 'Signature verification for type failed: error1, error2',
33
+ ],
34
+ [
35
+ VERIFICATION_STATUS.SIGNED_AND_INVALID,
36
+ [new Error('error1'), new Error('error2')],
37
+ undefined,
38
+ true,
39
+ 'Verification keys are not available',
40
+ ],
41
+ [
42
+ VERIFICATION_STATUS.SIGNED_AND_INVALID,
43
+ [new Error('error1'), new Error('error2')],
44
+ 'type',
45
+ true,
46
+ 'Verification keys for type are not available',
47
+ ],
14
48
  ];
15
49
 
16
- for (const [status, type, notAvailable, expected] of testCases) {
50
+ for (const [status, errors, type, notAvailable, expected] of testCases) {
17
51
  it(`returns correct message for status ${status} with type ${type} and notAvailable ${notAvailable}`, () => {
18
- expect(getVerificationMessage(status, type, notAvailable)).toBe(expected);
52
+ expect(getVerificationMessage(status, errors, type, notAvailable)).toBe(expected);
19
53
  });
20
54
  }
21
55
  });
@@ -11,6 +11,7 @@ export function getErrorMessage(error: unknown): string {
11
11
  */
12
12
  export function getVerificationMessage(
13
13
  verified: VERIFICATION_STATUS,
14
+ verificationErrors?: Error[],
14
15
  signatureType?: string,
15
16
  notAvailableVerificationKeys = false,
16
17
  ): string {
@@ -24,6 +25,13 @@ export function getVerificationMessage(
24
25
  : c('Error').t`Verification keys are not available`;
25
26
  }
26
27
 
28
+ if (verificationErrors) {
29
+ const errorMessage = verificationErrors?.map((e) => e.message).join(', ');
30
+ return signatureType
31
+ ? c('Error').t`Signature verification for ${signatureType} failed: ${errorMessage}`
32
+ : c('Error').t`Signature verification failed: ${errorMessage}`;
33
+ }
34
+
27
35
  return signatureType
28
36
  ? c('Error').t`Signature verification for ${signatureType} failed`
29
37
  : c('Error').t`Signature verification failed`;
@@ -9,20 +9,16 @@ const POLLING_INTERVAL = 1;
9
9
  describe('EventManager', () => {
10
10
  let manager: EventManager<DriveEvent>;
11
11
 
12
- const getLatestEventIdMock = jest.fn();
13
- const getEventsMock = jest.fn();
14
12
  const listenerMock = jest.fn();
15
- const mockLogger = getMockLogger();
16
13
  const subscriptions: EventSubscription[] = [];
14
+ const mockEventManager = {
15
+ getLogger: () => getMockLogger(),
16
+ getLatestEventId: jest.fn(),
17
+ getEvents: jest.fn(),
18
+ };
17
19
 
18
20
  beforeEach(() => {
19
- const mockEventManager = {
20
- getLogger: () => mockLogger,
21
- getLatestEventId: getLatestEventIdMock,
22
- getEvents: getEventsMock,
23
- };
24
-
25
- manager = new EventManager(mockEventManager as any, POLLING_INTERVAL, null);
21
+ manager = new EventManager(mockEventManager, POLLING_INTERVAL, null);
26
22
  const subscription = manager.addListener(listenerMock);
27
23
  subscriptions.push(subscription);
28
24
  });
@@ -37,7 +33,7 @@ describe('EventManager', () => {
37
33
  });
38
34
 
39
35
  it('should start polling when started', async () => {
40
- getLatestEventIdMock.mockResolvedValue('EventId1');
36
+ mockEventManager.getLatestEventId.mockResolvedValue('EventId1');
41
37
 
42
38
  const mockEvents: DriveEvent[][] = [
43
39
  [
@@ -56,7 +52,7 @@ describe('EventManager', () => {
56
52
  ],
57
53
  ];
58
54
 
59
- getEventsMock
55
+ mockEventManager.getEvents
60
56
  .mockImplementationOnce(async function* () {
61
57
  yield* mockEvents[0];
62
58
  })
@@ -65,22 +61,23 @@ describe('EventManager', () => {
65
61
  })
66
62
  .mockImplementationOnce(async function* () {});
67
63
 
68
- expect(getLatestEventIdMock).toHaveBeenCalledTimes(0);
69
- expect(getEventsMock).toHaveBeenCalledTimes(0);
64
+ expect(mockEventManager.getLatestEventId).toHaveBeenCalledTimes(0);
65
+ expect(mockEventManager.getEvents).toHaveBeenCalledTimes(0);
70
66
 
71
67
  expect(await manager.start()).toBeUndefined();
68
+ await jest.runOnlyPendingTimersAsync();
72
69
 
73
- expect(getLatestEventIdMock).toHaveBeenCalledTimes(1);
74
- expect(getEventsMock).toHaveBeenCalledWith('EventId1');
70
+ expect(mockEventManager.getLatestEventId).toHaveBeenCalledTimes(1);
71
+ expect(mockEventManager.getEvents).toHaveBeenCalledWith('EventId1');
75
72
 
76
73
  await jest.runOnlyPendingTimersAsync();
77
- expect(getEventsMock).toHaveBeenCalledTimes(2);
78
- expect(getEventsMock).toHaveBeenCalledWith('EventId2');
74
+ expect(mockEventManager.getEvents).toHaveBeenCalledTimes(2);
75
+ expect(mockEventManager.getEvents).toHaveBeenCalledWith('EventId2');
79
76
  });
80
77
 
81
78
  it('should stop polling when stopped', async () => {
82
- getLatestEventIdMock.mockResolvedValue('eventId1');
83
- getEventsMock.mockImplementation(async function* () {
79
+ mockEventManager.getLatestEventId.mockResolvedValue('eventId1');
80
+ mockEventManager.getEvents.mockImplementation(async function* () {
84
81
  yield {
85
82
  type: DriveEventType.FastForward,
86
83
  treeEventScopeId: 'volume1',
@@ -91,16 +88,16 @@ describe('EventManager', () => {
91
88
  await manager.start();
92
89
  await jest.runOnlyPendingTimersAsync();
93
90
 
94
- const callsBeforeStop = getEventsMock.mock.calls.length;
91
+ const callsBeforeStop = mockEventManager.getEvents.mock.calls.length;
95
92
  await manager.stop();
96
93
  await jest.runOnlyPendingTimersAsync();
97
94
 
98
95
  // Should not have made additional calls after stopping
99
- expect(getEventsMock).toHaveBeenCalledTimes(callsBeforeStop);
96
+ expect(mockEventManager.getEvents).toHaveBeenCalledTimes(callsBeforeStop);
100
97
  });
101
98
 
102
99
  it('should notify all listeners when getting events', async () => {
103
- getLatestEventIdMock.mockResolvedValue('eventId1');
100
+ mockEventManager.getLatestEventId.mockResolvedValue('eventId1');
104
101
 
105
102
  const mockEvents: DriveEvent[] = [
106
103
  {
@@ -114,7 +111,7 @@ describe('EventManager', () => {
114
111
  },
115
112
  ];
116
113
 
117
- getEventsMock
114
+ mockEventManager.getEvents
118
115
  .mockImplementationOnce(async function* () {
119
116
  yield* mockEvents;
120
117
  })
@@ -127,19 +124,19 @@ describe('EventManager', () => {
127
124
  });
128
125
 
129
126
  it('should propagate unsubscription errors', async () => {
130
- getLatestEventIdMock.mockImplementation(() => {
127
+ mockEventManager.getLatestEventId.mockImplementation(() => {
131
128
  throw new UnsubscribeFromEventsSourceError('Not found');
132
129
  });
133
130
 
134
131
  await expect(manager.start()).rejects.toThrow(UnsubscribeFromEventsSourceError);
135
132
 
136
- expect(getLatestEventIdMock).toHaveBeenCalledTimes(1);
133
+ expect(mockEventManager.getLatestEventId).toHaveBeenCalledTimes(1);
137
134
  expect(listenerMock).toHaveBeenCalledTimes(0);
138
- expect(getEventsMock).toHaveBeenCalledTimes(0);
135
+ expect(mockEventManager.getEvents).toHaveBeenCalledTimes(0);
139
136
  });
140
137
 
141
138
  it('should continue processing multiple events', async () => {
142
- getLatestEventIdMock.mockResolvedValue('eventId1');
139
+ mockEventManager.getLatestEventId.mockResolvedValue('eventId1');
143
140
 
144
141
  const mockEvents: DriveEvent[] = [
145
142
  {
@@ -162,7 +159,7 @@ describe('EventManager', () => {
162
159
  },
163
160
  ];
164
161
 
165
- getEventsMock
162
+ mockEventManager.getEvents
166
163
  .mockImplementationOnce(async function* () {
167
164
  yield* mockEvents;
168
165
  })
@@ -177,7 +174,7 @@ describe('EventManager', () => {
177
174
  expect(listenerMock).toHaveBeenNthCalledWith(1, mockEvents[0]);
178
175
  expect(listenerMock).toHaveBeenNthCalledWith(2, mockEvents[1]);
179
176
 
180
- getEventsMock.mockImplementationOnce(async function* () {
177
+ mockEventManager.getEvents.mockImplementationOnce(async function* () {
181
178
  yield* mockEvents;
182
179
  });
183
180
  await jest.runOnlyPendingTimersAsync();
@@ -187,10 +184,10 @@ describe('EventManager', () => {
187
184
  });
188
185
 
189
186
  it('should retry on error with exponential backoff', async () => {
190
- getLatestEventIdMock.mockResolvedValue('eventId1');
187
+ mockEventManager.getLatestEventId.mockResolvedValue('eventId1');
191
188
 
192
189
  let callCount = 0;
193
- getEventsMock.mockImplementation(async function* () {
190
+ mockEventManager.getEvents.mockImplementation(async function* () {
194
191
  callCount++;
195
192
  if (callCount <= 3) {
196
193
  throw new Error('Network error');
@@ -205,11 +202,12 @@ describe('EventManager', () => {
205
202
  expect(manager['retryIndex']).toEqual(0);
206
203
 
207
204
  expect(await manager.start()).toBeUndefined();
208
- expect(getEventsMock).toHaveBeenCalledTimes(1);
205
+ await jest.runOnlyPendingTimersAsync();
206
+ expect(mockEventManager.getEvents).toHaveBeenCalledTimes(1);
209
207
  expect(manager['retryIndex']).toEqual(1);
210
208
 
211
209
  await jest.runOnlyPendingTimersAsync();
212
- expect(getEventsMock).toHaveBeenCalledTimes(2);
210
+ expect(mockEventManager.getEvents).toHaveBeenCalledTimes(2);
213
211
  expect(manager['retryIndex']).toEqual(2);
214
212
 
215
213
  await jest.runOnlyPendingTimersAsync();
@@ -224,8 +222,8 @@ describe('EventManager', () => {
224
222
  });
225
223
 
226
224
  it('should stop polling when stopped immediately', async () => {
227
- getLatestEventIdMock.mockResolvedValue('eventId1');
228
- getEventsMock.mockImplementation(async function* () {
225
+ mockEventManager.getLatestEventId.mockResolvedValue('eventId1');
226
+ mockEventManager.getEvents.mockImplementation(async function* () {
229
227
  yield {
230
228
  type: DriveEventType.FastForward,
231
229
  treeEventScopeId: 'volume1',
@@ -234,18 +232,19 @@ describe('EventManager', () => {
234
232
  });
235
233
 
236
234
  expect(await manager.start()).toBeUndefined();
237
- expect(getEventsMock).toHaveBeenCalledTimes(1);
235
+ await jest.runOnlyPendingTimersAsync();
236
+ expect(mockEventManager.getEvents).toHaveBeenCalledTimes(1);
238
237
  await manager.stop();
239
238
  await jest.runOnlyPendingTimersAsync();
240
239
 
241
240
  // getEvents should have been called once during start, but not again after stop
242
- expect(getEventsMock).toHaveBeenCalledTimes(1);
241
+ expect(mockEventManager.getEvents).toHaveBeenCalledTimes(1);
243
242
  });
244
243
 
245
244
  it('should handle empty event streams', async () => {
246
- getLatestEventIdMock.mockResolvedValue('eventId1');
245
+ mockEventManager.getLatestEventId.mockResolvedValue('eventId1');
247
246
 
248
- getEventsMock.mockImplementation(async function* () {
247
+ mockEventManager.getEvents.mockImplementation(async function* () {
249
248
  // Empty generator - no events
250
249
  });
251
250
 
@@ -254,4 +253,26 @@ describe('EventManager', () => {
254
253
 
255
254
  expect(listenerMock).toHaveBeenCalledTimes(0);
256
255
  });
256
+
257
+ it('should poll right away after start if latestEventId is passed', async () => {
258
+ manager = new EventManager(mockEventManager, POLLING_INTERVAL, 'eventId1');
259
+
260
+ await manager.start();
261
+
262
+ // Right after the start it is called.
263
+ expect(mockEventManager.getEvents).toHaveBeenCalledTimes(1);
264
+ });
265
+
266
+ it('should not poll right away after start if latestEventId is not passed', async () => {
267
+ manager = new EventManager(mockEventManager, POLLING_INTERVAL, null);
268
+
269
+ await manager.start();
270
+
271
+ // Right after the start it is not called.
272
+ expect(mockEventManager.getEvents).not.toHaveBeenCalled();
273
+
274
+ // But it is scheduled to be called after the polling interval.
275
+ await jest.runOnlyPendingTimersAsync();
276
+ expect(mockEventManager.getEvents).toHaveBeenCalledTimes(1);
277
+ });
257
278
  });
@@ -38,8 +38,11 @@ export class EventManager<T extends Event> {
38
38
  }
39
39
 
40
40
  async start(): Promise<void> {
41
+ this.logger.info(`Starting event manager with latestEventId: ${this.latestEventId}`);
41
42
  if (this.latestEventId === undefined) {
42
43
  this.latestEventId = await this.specializedEventManager.getLatestEventId();
44
+ this.scheduleNextPoll();
45
+ return;
43
46
  }
44
47
  this.processPromise = this.processEvents();
45
48
  }
@@ -107,6 +110,13 @@ export class EventManager<T extends Event> {
107
110
  throw listenerError;
108
111
  }
109
112
 
113
+ this.scheduleNextPoll();
114
+ }
115
+
116
+ private scheduleNextPoll() {
117
+ if (this.timeoutHandle) {
118
+ clearTimeout(this.timeoutHandle);
119
+ }
110
120
  this.timeoutHandle = setTimeout(() => {
111
121
  this.processPromise = this.processEvents();
112
122
  }, this.nextPollTimeout);