qase-javascript-commons 2.4.10 → 2.4.12

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/changelog.md CHANGED
@@ -1,3 +1,15 @@
1
+ # qase-javascript-commons@2.4.12
2
+
3
+ ## What's new
4
+
5
+ Improved the retry mechanism for uploading attachments.
6
+
7
+ # qase-javascript-commons@2.4.11
8
+
9
+ ## What's new
10
+
11
+ Added retry mechanism for uploading attachments.
12
+
1
13
  # qase-javascript-commons@2.4.10
2
14
 
3
15
  ## What's new
@@ -21,6 +21,27 @@ export declare class ClientV1 implements IClient {
21
21
  private getErrorMessage;
22
22
  uploadAttachment(attachment: Attachment): Promise<string>;
23
23
  protected uploadAttachments(attachments: Attachment[]): Promise<string[]>;
24
+ /**
25
+ * Upload attachment with retry logic for 429 errors
26
+ * @param project Project code
27
+ * @param data Attachment data
28
+ * @param attachmentName Attachment name for logging
29
+ * @param maxRetries Maximum number of retry attempts
30
+ * @param initialDelay Initial delay in milliseconds
31
+ * @returns Promise with upload response
32
+ */
33
+ private uploadAttachmentWithRetry;
34
+ /**
35
+ * Extract Retry-After header value from response or return null
36
+ * @param error Axios error
37
+ * @returns Retry-After value in milliseconds or null
38
+ */
39
+ private getRetryAfter;
40
+ /**
41
+ * Delay execution for specified milliseconds
42
+ * @param ms Milliseconds to delay
43
+ */
44
+ private delay;
24
45
  private prepareAttachmentData;
25
46
  private getEnvironmentId;
26
47
  private prepareRunObject;
@@ -145,11 +145,21 @@ class ClientV1 {
145
145
  return [];
146
146
  }
147
147
  const uploadedHashes = [];
148
- for (const attachment of attachments) {
148
+ // Add initial random delay to spread out requests from different workers/shard
149
+ // This helps prevent all workers from hitting the API at the same time
150
+ if (attachments.length > 0) {
151
+ const initialJitter = Math.random() * 500; // 0-500ms random delay
152
+ await this.delay(initialJitter);
153
+ }
154
+ for (let i = 0; i < attachments.length; i++) {
155
+ const attachment = attachments[i];
156
+ if (!attachment) {
157
+ continue;
158
+ }
149
159
  try {
150
160
  this.logger.logDebug(`Uploading attachment: ${attachment.file_path ?? attachment.file_name}`);
151
161
  const data = this.prepareAttachmentData(attachment);
152
- const response = await this.attachmentClient.uploadAttachment(this.config.project, [data]);
162
+ const response = await this.uploadAttachmentWithRetry(this.config.project, [data], attachment.file_path ?? attachment.file_name);
153
163
  const hash = response.data.result?.[0]?.hash;
154
164
  if (hash) {
155
165
  uploadedHashes.push(hash);
@@ -158,9 +168,96 @@ class ClientV1 {
158
168
  catch (error) {
159
169
  this.logger.logError('Cannot upload attachment:', error);
160
170
  }
171
+ // Add delay between requests to avoid rate limiting
172
+ // Skip delay after the last attachment
173
+ if (i < attachments.length - 1) {
174
+ // Increased delay with random jitter to prevent synchronization
175
+ const baseDelay = 1000; // 1000ms (1 second) base delay
176
+ const jitter = Math.random() * 300; // 0-300ms random jitter
177
+ await this.delay(baseDelay + jitter);
178
+ }
161
179
  }
162
180
  return uploadedHashes;
163
181
  }
182
+ /**
183
+ * Upload attachment with retry logic for 429 errors
184
+ * @param project Project code
185
+ * @param data Attachment data
186
+ * @param attachmentName Attachment name for logging
187
+ * @param maxRetries Maximum number of retry attempts
188
+ * @param initialDelay Initial delay in milliseconds
189
+ * @returns Promise with upload response
190
+ */
191
+ async uploadAttachmentWithRetry(project, data, attachmentName, maxRetries = 5, initialDelay = 1000) {
192
+ let lastError;
193
+ let delay = initialDelay;
194
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
195
+ try {
196
+ return await this.attachmentClient.uploadAttachment(project, data);
197
+ }
198
+ catch (error) {
199
+ lastError = error;
200
+ // Check if it's a 429 error (Too Many Requests)
201
+ if ((0, is_axios_error_1.isAxiosError)(error)) {
202
+ if (error.response?.status === 429) {
203
+ if (attempt < maxRetries) {
204
+ const retryAfter = this.getRetryAfter(error);
205
+ const baseWaitTime = retryAfter ?? delay;
206
+ // Add jitter (random delay) to prevent all workers from retrying simultaneously
207
+ // Jitter is 10-30% of the wait time to spread out retry attempts
208
+ const jitterPercent = 0.1 + Math.random() * 0.2; // 10-30%
209
+ const jitter = baseWaitTime * jitterPercent;
210
+ const waitTime = Math.floor(baseWaitTime + jitter);
211
+ this.logger.logDebug(`Rate limit exceeded (429) for attachment "${attachmentName}". ` +
212
+ `Retrying in ${waitTime}ms (attempt ${attempt + 1}/${maxRetries})`);
213
+ await this.delay(waitTime);
214
+ // Exponential backoff: double the delay for next attempt
215
+ delay = Math.min(delay * 2, 30000); // Cap at 30 seconds
216
+ }
217
+ else {
218
+ this.logger.logError(`Failed to upload attachment "${attachmentName}" after ${maxRetries} retries due to rate limiting`);
219
+ }
220
+ }
221
+ else {
222
+ // For non-429 errors, throw immediately
223
+ throw error;
224
+ }
225
+ }
226
+ else {
227
+ // For non-Axios errors, throw immediately
228
+ throw error;
229
+ }
230
+ }
231
+ }
232
+ // If we exhausted all retries, throw the last error
233
+ throw lastError;
234
+ }
235
+ /**
236
+ * Extract Retry-After header value from response or return null
237
+ * @param error Axios error
238
+ * @returns Retry-After value in milliseconds or null
239
+ */
240
+ getRetryAfter(error) {
241
+ const headers = error.response?.headers;
242
+ if (!headers) {
243
+ return null;
244
+ }
245
+ const retryAfterHeader = headers['retry-after'];
246
+ if (retryAfterHeader && typeof retryAfterHeader === 'string') {
247
+ const retryAfterSeconds = parseInt(retryAfterHeader, 10);
248
+ if (!isNaN(retryAfterSeconds)) {
249
+ return retryAfterSeconds * 1000; // Convert to milliseconds
250
+ }
251
+ }
252
+ return null;
253
+ }
254
+ /**
255
+ * Delay execution for specified milliseconds
256
+ * @param ms Milliseconds to delay
257
+ */
258
+ delay(ms) {
259
+ return new Promise(resolve => setTimeout(resolve, ms));
260
+ }
164
261
  prepareAttachmentData(attachment) {
165
262
  if (attachment.file_path) {
166
263
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qase-javascript-commons",
3
- "version": "2.4.10",
3
+ "version": "2.4.12",
4
4
  "description": "Qase JS Reporters",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -24,31 +24,32 @@
24
24
  "author": "Qase Team <support@qase.io>",
25
25
  "license": "Apache-2.0",
26
26
  "dependencies": {
27
- "ajv": "^8.12.0",
27
+ "ajv": "^8.17.1",
28
28
  "async-mutex": "~0.5.0",
29
29
  "chalk": "^4.1.2",
30
- "env-schema": "^5.2.0",
31
- "form-data": "^4.0.4",
30
+ "env-schema": "^5.2.1",
31
+ "form-data": "^4.0.5",
32
32
  "lodash.get": "^4.4.2",
33
33
  "lodash.merge": "^4.6.2",
34
34
  "lodash.mergewith": "^4.6.2",
35
- "mime-types": "^2.1.33",
35
+ "mime-types": "^2.1.35",
36
36
  "qase-api-client": "~1.1.0",
37
- "qase-api-v2-client": "~1.0.1",
37
+ "qase-api-v2-client": "~1.0.3",
38
38
  "strip-ansi": "^6.0.1",
39
- "uuid": "^9.0.0"
39
+ "uuid": "^9.0.1"
40
40
  },
41
41
  "devDependencies": {
42
- "@jest/globals": "^29.5.0",
43
- "@types/jest": "^29.5.2",
44
- "@types/lodash.get": "^4.4.7",
45
- "@types/lodash.merge": "^4.6.7",
46
- "@types/lodash.mergewith": "^4.6.7",
42
+ "@jest/globals": "^29.7.0",
43
+ "@types/jest": "^29.5.14",
44
+ "@types/lodash.get": "^4.4.9",
45
+ "@types/lodash.merge": "^4.6.9",
46
+ "@types/lodash.mergewith": "^4.6.9",
47
47
  "@types/mime-types": "^2.1.4",
48
- "@types/node": "^20.12.5",
49
- "@types/uuid": "^9.0.1",
50
- "axios": "^1.12.0",
51
- "jest": "^29.5.0",
52
- "ts-jest": "^29.1.0"
48
+ "@types/minimatch": "^6.0.0",
49
+ "@types/node": "^20.19.25",
50
+ "@types/uuid": "^9.0.8",
51
+ "axios": "^1.13.2",
52
+ "jest": "^29.7.0",
53
+ "ts-jest": "^29.4.5"
53
54
  }
54
55
  }
@@ -2,7 +2,9 @@
2
2
  "extends": "./tsconfig.json",
3
3
 
4
4
  "compilerOptions": {
5
- "noEmit": false
5
+ "noEmit": false,
6
+ "skipLibCheck": true,
7
+ "types": []
6
8
  },
7
9
 
8
10
  "include": ["./src/**/*.ts"]