@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.
- package/dist/crypto/driveCrypto.d.ts +11 -0
- package/dist/crypto/driveCrypto.js +20 -7
- package/dist/crypto/driveCrypto.js.map +1 -1
- package/dist/crypto/interface.d.ts +10 -1
- package/dist/crypto/openPGPCrypto.d.ts +18 -2
- package/dist/crypto/openPGPCrypto.js +25 -6
- package/dist/crypto/openPGPCrypto.js.map +1 -1
- package/dist/diagnostic/telemetry.d.ts +1 -1
- package/dist/diagnostic/telemetry.js +1 -1
- package/dist/diagnostic/telemetry.js.map +1 -1
- package/dist/interface/download.d.ts +46 -0
- package/dist/interface/index.d.ts +2 -2
- package/dist/interface/index.js.map +1 -1
- package/dist/interface/nodes.d.ts +26 -1
- package/dist/interface/nodes.js.map +1 -1
- package/dist/interface/telemetry.d.ts +5 -2
- package/dist/interface/telemetry.js.map +1 -1
- package/dist/internal/apiService/apiService.js +1 -1
- package/dist/internal/apiService/apiService.js.map +1 -1
- package/dist/internal/apiService/driveTypes.d.ts +78 -165
- package/dist/internal/apiService/index.d.ts +1 -1
- package/dist/internal/apiService/index.js +2 -2
- package/dist/internal/apiService/index.js.map +1 -1
- package/dist/internal/apiService/transformers.d.ts +1 -1
- package/dist/internal/apiService/transformers.js +2 -2
- package/dist/internal/apiService/transformers.js.map +1 -1
- package/dist/internal/download/blockIndex.d.ts +11 -0
- package/dist/internal/download/blockIndex.js +35 -0
- package/dist/internal/download/blockIndex.js.map +1 -0
- package/dist/internal/download/blockIndex.test.d.ts +1 -0
- package/dist/internal/download/blockIndex.test.js +147 -0
- package/dist/internal/download/blockIndex.test.js.map +1 -0
- package/dist/internal/download/fileDownloader.d.ts +6 -2
- package/dist/internal/download/fileDownloader.js +83 -6
- package/dist/internal/download/fileDownloader.js.map +1 -1
- package/dist/internal/download/fileDownloader.test.js +69 -4
- package/dist/internal/download/fileDownloader.test.js.map +1 -1
- package/dist/internal/download/interface.d.ts +4 -4
- package/dist/internal/download/seekableStream.d.ts +80 -0
- package/dist/internal/download/seekableStream.js +163 -0
- package/dist/internal/download/seekableStream.js.map +1 -0
- package/dist/internal/download/seekableStream.test.d.ts +1 -0
- package/dist/internal/download/seekableStream.test.js +149 -0
- package/dist/internal/download/seekableStream.test.js.map +1 -0
- package/dist/internal/download/telemetry.js +1 -1
- package/dist/internal/download/telemetry.js.map +1 -1
- package/dist/internal/download/telemetry.test.js +7 -7
- package/dist/internal/download/telemetry.test.js.map +1 -1
- package/dist/internal/errors.d.ts +1 -1
- package/dist/internal/errors.js +7 -1
- package/dist/internal/errors.js.map +1 -1
- package/dist/internal/errors.test.js +44 -10
- package/dist/internal/errors.test.js.map +1 -1
- package/dist/internal/events/eventManager.d.ts +1 -0
- package/dist/internal/events/eventManager.js +9 -0
- package/dist/internal/events/eventManager.js.map +1 -1
- package/dist/internal/events/eventManager.test.js +53 -38
- package/dist/internal/events/eventManager.test.js.map +1 -1
- package/dist/internal/events/index.d.ts +4 -3
- package/dist/internal/events/index.js +38 -32
- package/dist/internal/events/index.js.map +1 -1
- package/dist/internal/nodes/apiService.js +16 -3
- package/dist/internal/nodes/apiService.js.map +1 -1
- package/dist/internal/nodes/apiService.test.js +43 -7
- package/dist/internal/nodes/apiService.test.js.map +1 -1
- package/dist/internal/nodes/cache.js +9 -2
- package/dist/internal/nodes/cache.js.map +1 -1
- package/dist/internal/nodes/cache.test.js +6 -1
- package/dist/internal/nodes/cache.test.js.map +1 -1
- package/dist/internal/nodes/cryptoService.d.ts +4 -1
- package/dist/internal/nodes/cryptoService.js +66 -16
- package/dist/internal/nodes/cryptoService.js.map +1 -1
- package/dist/internal/nodes/cryptoService.test.js +129 -46
- package/dist/internal/nodes/cryptoService.test.js.map +1 -1
- package/dist/internal/nodes/events.js +7 -7
- package/dist/internal/nodes/events.js.map +1 -1
- package/dist/internal/nodes/extendedAttributes.d.ts +2 -1
- package/dist/internal/nodes/extendedAttributes.js +27 -1
- package/dist/internal/nodes/extendedAttributes.js.map +1 -1
- package/dist/internal/nodes/extendedAttributes.test.js +59 -6
- package/dist/internal/nodes/extendedAttributes.test.js.map +1 -1
- package/dist/internal/nodes/index.test.js +1 -1
- package/dist/internal/nodes/index.test.js.map +1 -1
- package/dist/internal/nodes/interface.d.ts +18 -2
- package/dist/internal/nodes/nodesAccess.js +11 -1
- package/dist/internal/nodes/nodesAccess.js.map +1 -1
- package/dist/internal/nodes/nodesManagement.js +1 -1
- package/dist/internal/nodes/nodesManagement.js.map +1 -1
- package/dist/internal/nodes/nodesRevisions.d.ts +4 -3
- package/dist/internal/nodes/nodesRevisions.js +2 -2
- package/dist/internal/nodes/nodesRevisions.js.map +1 -1
- package/dist/internal/shares/cryptoService.js +7 -4
- package/dist/internal/shares/cryptoService.js.map +1 -1
- package/dist/internal/shares/cryptoService.test.js +5 -3
- package/dist/internal/shares/cryptoService.test.js.map +1 -1
- package/dist/internal/sharing/apiService.js +5 -5
- package/dist/internal/sharing/apiService.js.map +1 -1
- package/dist/internal/sharing/cache.d.ts +1 -0
- package/dist/internal/sharing/cache.js +9 -0
- package/dist/internal/sharing/cache.js.map +1 -1
- package/dist/internal/sharing/cryptoService.js +8 -5
- package/dist/internal/sharing/cryptoService.js.map +1 -1
- package/dist/internal/sharing/cryptoService.test.js +7 -4
- package/dist/internal/sharing/cryptoService.test.js.map +1 -1
- package/dist/internal/sharing/events.d.ts +1 -0
- package/dist/internal/sharing/events.js +28 -18
- package/dist/internal/sharing/events.js.map +1 -1
- package/dist/internal/sharing/events.test.js +98 -84
- package/dist/internal/sharing/events.test.js.map +1 -1
- package/dist/internal/upload/interface.d.ts +1 -0
- package/dist/internal/upload/manager.d.ts +1 -1
- package/dist/internal/upload/manager.js +8 -4
- package/dist/internal/upload/manager.js.map +1 -1
- package/dist/internal/upload/manager.test.js +7 -10
- package/dist/internal/upload/manager.test.js.map +1 -1
- package/dist/internal/upload/streamUploader.js +1 -1
- package/dist/internal/upload/streamUploader.js.map +1 -1
- package/dist/internal/upload/streamUploader.test.js +1 -1
- package/dist/internal/upload/streamUploader.test.js.map +1 -1
- package/dist/internal/upload/telemetry.js +2 -2
- package/dist/internal/upload/telemetry.js.map +1 -1
- package/dist/internal/upload/telemetry.test.js +7 -7
- package/dist/internal/upload/telemetry.test.js.map +1 -1
- package/dist/protonDriveClient.js +2 -2
- package/dist/protonDriveClient.js.map +1 -1
- package/dist/telemetry.d.ts +2 -2
- package/dist/telemetry.js +2 -2
- package/dist/telemetry.js.map +1 -1
- package/dist/tests/telemetry.js +1 -1
- package/dist/tests/telemetry.js.map +1 -1
- package/dist/transformers.d.ts +1 -1
- package/dist/transformers.js +3 -1
- package/dist/transformers.js.map +1 -1
- package/package.json +1 -1
- package/src/crypto/driveCrypto.ts +70 -25
- package/src/crypto/interface.ts +15 -0
- package/src/crypto/openPGPCrypto.ts +37 -5
- package/src/diagnostic/telemetry.ts +1 -1
- package/src/interface/download.ts +46 -0
- package/src/interface/index.ts +2 -1
- package/src/interface/nodes.ts +28 -1
- package/src/interface/telemetry.ts +6 -1
- package/src/internal/apiService/apiService.ts +1 -1
- package/src/internal/apiService/driveTypes.ts +78 -165
- package/src/internal/apiService/index.ts +1 -1
- package/src/internal/apiService/transformers.ts +1 -1
- package/src/internal/download/blockIndex.test.ts +158 -0
- package/src/internal/download/blockIndex.ts +36 -0
- package/src/internal/download/fileDownloader.test.ts +100 -7
- package/src/internal/download/fileDownloader.ts +109 -9
- package/src/internal/download/interface.ts +4 -4
- package/src/internal/download/seekableStream.test.ts +187 -0
- package/src/internal/download/seekableStream.ts +182 -0
- package/src/internal/download/telemetry.test.ts +7 -7
- package/src/internal/download/telemetry.ts +1 -1
- package/src/internal/errors.test.ts +45 -11
- package/src/internal/errors.ts +8 -0
- package/src/internal/events/eventManager.test.ts +61 -40
- package/src/internal/events/eventManager.ts +10 -0
- package/src/internal/events/index.ts +53 -35
- package/src/internal/nodes/apiService.test.ts +59 -15
- package/src/internal/nodes/apiService.ts +21 -4
- package/src/internal/nodes/cache.test.ts +6 -1
- package/src/internal/nodes/cache.ts +9 -2
- package/src/internal/nodes/cryptoService.test.ts +139 -47
- package/src/internal/nodes/cryptoService.ts +94 -9
- package/src/internal/nodes/events.ts +6 -7
- package/src/internal/nodes/extendedAttributes.test.ts +60 -7
- package/src/internal/nodes/extendedAttributes.ts +37 -1
- package/src/internal/nodes/index.test.ts +1 -1
- package/src/internal/nodes/interface.ts +19 -2
- package/src/internal/nodes/nodesAccess.ts +15 -1
- package/src/internal/nodes/nodesManagement.ts +1 -1
- package/src/internal/nodes/nodesRevisions.ts +14 -5
- package/src/internal/shares/cryptoService.test.ts +5 -3
- package/src/internal/shares/cryptoService.ts +7 -4
- package/src/internal/sharing/apiService.ts +6 -6
- package/src/internal/sharing/cache.ts +9 -0
- package/src/internal/sharing/cryptoService.test.ts +7 -4
- package/src/internal/sharing/cryptoService.ts +8 -5
- package/src/internal/sharing/events.test.ts +104 -89
- package/src/internal/sharing/events.ts +33 -18
- package/src/internal/upload/interface.ts +1 -0
- package/src/internal/upload/manager.test.ts +7 -10
- package/src/internal/upload/manager.ts +7 -4
- package/src/internal/upload/streamUploader.test.ts +1 -1
- package/src/internal/upload/streamUploader.ts +1 -1
- package/src/internal/upload/telemetry.test.ts +7 -7
- package/src/internal/upload/telemetry.ts +2 -2
- package/src/protonDriveClient.ts +2 -2
- package/src/telemetry.ts +2 -2
- package/src/tests/telemetry.ts +1 -1
- 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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
91
|
+
expect(mockTelemetry.recordMetric).not.toHaveBeenCalled();
|
|
92
92
|
});
|
|
93
93
|
|
|
94
94
|
it('should detect "rate_limited" error for RateLimitedError', async () => {
|
|
@@ -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
|
-
[
|
|
13
|
-
|
|
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
|
});
|
package/src/internal/errors.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
69
|
-
expect(
|
|
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(
|
|
74
|
-
expect(
|
|
70
|
+
expect(mockEventManager.getLatestEventId).toHaveBeenCalledTimes(1);
|
|
71
|
+
expect(mockEventManager.getEvents).toHaveBeenCalledWith('EventId1');
|
|
75
72
|
|
|
76
73
|
await jest.runOnlyPendingTimersAsync();
|
|
77
|
-
expect(
|
|
78
|
-
expect(
|
|
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
|
-
|
|
83
|
-
|
|
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 =
|
|
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(
|
|
96
|
+
expect(mockEventManager.getEvents).toHaveBeenCalledTimes(callsBeforeStop);
|
|
100
97
|
});
|
|
101
98
|
|
|
102
99
|
it('should notify all listeners when getting events', async () => {
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
133
|
+
expect(mockEventManager.getLatestEventId).toHaveBeenCalledTimes(1);
|
|
137
134
|
expect(listenerMock).toHaveBeenCalledTimes(0);
|
|
138
|
-
expect(
|
|
135
|
+
expect(mockEventManager.getEvents).toHaveBeenCalledTimes(0);
|
|
139
136
|
});
|
|
140
137
|
|
|
141
138
|
it('should continue processing multiple events', async () => {
|
|
142
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
187
|
+
mockEventManager.getLatestEventId.mockResolvedValue('eventId1');
|
|
191
188
|
|
|
192
189
|
let callCount = 0;
|
|
193
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
228
|
-
|
|
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
|
-
|
|
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(
|
|
241
|
+
expect(mockEventManager.getEvents).toHaveBeenCalledTimes(1);
|
|
243
242
|
});
|
|
244
243
|
|
|
245
244
|
it('should handle empty event streams', async () => {
|
|
246
|
-
|
|
245
|
+
mockEventManager.getLatestEventId.mockResolvedValue('eventId1');
|
|
247
246
|
|
|
248
|
-
|
|
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);
|