@elisra-devops/docgen-data-provider 1.18.0 → 1.19.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.
@@ -1,16 +1,34 @@
1
- import axios from 'axios';
1
+ import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
2
2
  import logger from '../utils/logger';
3
3
 
4
4
  export class TFSServices {
5
+ // Connection pooling
6
+ private static connectionPool = {
7
+ httpAgent: new (require('http').Agent)({
8
+ keepAlive: true,
9
+ maxSockets: 50, // Allow more connections
10
+ keepAliveMsecs: 30000, // Keep connections alive longer
11
+ }),
12
+ httpsAgent: new (require('https').Agent)({
13
+ keepAlive: true,
14
+ maxSockets: 50,
15
+ keepAliveMsecs: 30000,
16
+ }),
17
+ };
18
+
19
+ // Axios instance with connection reuse
20
+ private static axiosInstance: AxiosInstance = axios.create({
21
+ httpAgent: this.connectionPool.httpAgent,
22
+ httpsAgent: this.connectionPool.httpsAgent,
23
+ });
24
+
5
25
  public static async downloadZipFile(url: string, pat: string): Promise<any> {
6
26
  try {
7
- let res = await axios.request({
27
+ const res = await this.axiosInstance.request({
8
28
  url: url,
9
29
  headers: { 'Content-Type': 'application/zip' },
10
- auth: {
11
- username: '',
12
- password: pat,
13
- },
30
+ auth: { username: '', password: pat },
31
+ timeout: 15000, // Increased timeout for large files
14
32
  });
15
33
  return res;
16
34
  } catch (e) {
@@ -27,52 +45,22 @@ export class TFSServices {
27
45
  customHeaders: any = {},
28
46
  printError: boolean = true
29
47
  ): Promise<any> {
30
- let config: any = {
48
+ const config: AxiosRequestConfig = {
31
49
  headers: customHeaders,
32
50
  method: requestMethod,
33
- auth: {
34
- username: '',
35
- password: pat,
36
- },
51
+ auth: { username: '', password: pat },
37
52
  data: data,
38
53
  responseType: 'arraybuffer', // Important for binary data
39
- timeout: 5000, // Set timeout to 5 seconds
54
+ timeout: 8000, // Increased timeout for images
40
55
  };
41
- let json;
42
- let attempts = 0;
43
- const maxAttempts = 3;
44
-
45
- logger.silly(`making request:
46
- url: ${url}
47
- config: ${JSON.stringify(config)}`);
48
56
 
49
- while (attempts < maxAttempts) {
50
- try {
51
- const response = await axios(url, config);
52
-
53
- // Convert binary data to Base64
54
- const base64String = Buffer.from(response.data, 'binary').toString('base64');
55
- const contentType = response.headers['content-type']; // e.g., "image/png; api-version=7.1"
56
- const mimeType = contentType.split(';')[0].trim(); // Extracts "image/png"
57
- return `data:${mimeType};base64,${base64String}`;
58
- } catch (e: any) {
59
- attempts++;
60
- if (e.message.includes('ETIMEDOUT') && attempts < maxAttempts) {
61
- logger.warn(`Request timed out. Retrying attempt ${attempts} of ${maxAttempts}...`);
62
- continue;
63
- }
64
- if (printError) {
65
- if (e.response) {
66
- logger.error(`Error fetching image from Azure DevOps at ${url}: ${e.message}`);
67
- logger.error(`Status: ${e.response.status}`);
68
- logger.error(`Response Data: ${JSON.stringify(e.response.data)}`);
69
- } else {
70
- logger.error(`Error fetching image from Azure DevOps at ${url}: ${e.message}`);
71
- }
72
- }
73
- throw e;
74
- }
75
- }
57
+ return this.executeWithRetry(url, config, printError, (response) => {
58
+ // Convert binary data to Base64
59
+ const base64String = Buffer.from(response.data, 'binary').toString('base64');
60
+ const contentType = response.headers['content-type'] || 'application/octet-stream';
61
+ const mimeType = contentType.split(';')[0].trim();
62
+ return `data:${mimeType};base64,${base64String}`;
63
+ });
76
64
  }
77
65
 
78
66
  public static async getItemContent(
@@ -83,87 +71,37 @@ export class TFSServices {
83
71
  customHeaders: any = {},
84
72
  printError: boolean = true
85
73
  ): Promise<any> {
86
- let config: any = {
74
+ // Clean URL
75
+ const cleanUrl = url.replace(/ /g, '%20');
76
+
77
+ const config: AxiosRequestConfig = {
87
78
  headers: customHeaders,
88
79
  method: requestMethod,
89
- auth: {
90
- username: '',
91
- password: pat,
92
- },
80
+ auth: { username: '', password: pat },
93
81
  data: data,
94
- timeout: 3000, // Set timeout to 3 seconds
82
+ timeout: 10000, // More reasonable timeout
95
83
  };
96
- let json;
97
- let attempts = 0;
98
- const maxAttempts = 3;
99
- url = url.replace(/ /g, '%20');
100
- logger.silly(`making request:
101
- url: ${url}
102
- config: ${JSON.stringify(config)}`);
103
-
104
- while (attempts < maxAttempts) {
105
- try {
106
- let result = await axios(url, config);
107
- json = JSON.parse(JSON.stringify(result.data));
108
- return json;
109
- } catch (e: any) {
110
- logger.warn(`error fetching item content from azure devops at ${url}`);
111
- logger.warn(`error: ${JSON.stringify(e.response?.data?.message)}`);
112
- if (e.response?.data?.message.includes('could not be found')) {
113
- logger.info(`File does not exist, or you do not have permissions to read it.`);
114
- return undefined;
115
- }
116
84
 
117
- attempts++;
118
- if (
119
- e.message.includes('ETIMEDOUT') ||
120
- e.message.includes('timeout') ||
121
- (e.response?.data?.message?.includes('timeout') && attempts < maxAttempts)
122
- ) {
123
- logger.warn(`Request timed out. Retrying attempt ${attempts} of ${maxAttempts}...`);
124
- continue;
125
- }
126
- if (printError) {
127
- if (e.response) {
128
- // Log detailed error information including the URL
129
- logger.error(`Error making request to Azure DevOps at ${url}: ${e.message}`);
130
- logger.error(`Status: ${e.response.status}`);
131
- logger.error(`Response Data: ${JSON.stringify(e.response.data?.message)}`);
132
- } else {
133
- // Handle other errors (network, etc.)
134
- logger.error(`Error making request to Azure DevOps at ${url}: ${e.message}`);
135
- }
136
- }
137
- throw e;
138
- }
139
- }
85
+ return this.executeWithRetry(cleanUrl, config, printError, (response) => {
86
+ // Direct return of data without extra JSON parsing
87
+ return response.data;
88
+ });
140
89
  }
141
90
 
142
91
  public static async getJfrogRequest(url: string, header?: any) {
143
- let config: any = {
92
+ const config: AxiosRequestConfig = {
144
93
  method: 'GET',
94
+ headers: header,
95
+ timeout: 8000, // Reasonable timeout
145
96
  };
146
- if (header) {
147
- config['headers'] = header;
148
- }
149
97
 
150
- let json;
151
98
  try {
152
- let result = await axios(url, config);
153
- json = JSON.parse(JSON.stringify(result.data));
99
+ const result = await this.axiosInstance.request(config);
100
+ return result.data;
154
101
  } catch (e: any) {
155
- if (e.response) {
156
- // Log detailed error information including the URL
157
- logger.error(`Error making request Jfrog at ${url}: ${e.message}`);
158
- logger.error(`Status: ${e.response.status}`);
159
- logger.error(`Response Data: ${JSON.stringify(e.response.data)}`);
160
- } else {
161
- // Handle other errors (network, etc.)
162
- logger.error(`Error making request to Jfrog at ${url}: ${e.message}`);
163
- }
102
+ this.logDetailedError(e, url);
164
103
  throw e;
165
104
  }
166
- return json;
167
105
  }
168
106
 
169
107
  public static async postRequest(
@@ -173,32 +111,129 @@ export class TFSServices {
173
111
  data: any,
174
112
  customHeaders: any = { headers: { 'Content-Type': 'application/json' } }
175
113
  ): Promise<any> {
176
- let config: any = {
114
+ const config: AxiosRequestConfig = {
177
115
  headers: customHeaders,
178
116
  method: requestMethod,
179
- auth: {
180
- username: '',
181
- password: pat,
182
- },
117
+ auth: { username: '', password: pat },
183
118
  data: data,
119
+ timeout: 10000, // More reasonable timeout
184
120
  };
185
- let result;
186
- logger.silly(`making request:
187
- url: ${url}
188
- config: ${JSON.stringify(config)}`);
121
+
122
+ // Use shorter log format for better performance
123
+ logger.silly(`Request: ${url} [${requestMethod}]`);
124
+
189
125
  try {
190
- result = await axios(url, config);
126
+ const result = await this.axiosInstance.request(config);
127
+ return result;
191
128
  } catch (e: any) {
192
- if (e.response) {
193
- // Log detailed error information including the URL
194
- logger.error(`Error making request to Azure DevOps at ${url}: ${e.message}`);
195
- logger.error(`Status: ${e.response.status}`);
196
- logger.error(`Response Data: ${JSON.stringify(e.response.data?.message)}`);
197
- } else {
198
- // Handle other errors (network, etc.)
199
- logger.error(`Error making request to Azure DevOps at ${url}: ${e.message}`);
129
+ this.logDetailedError(e, url);
130
+ throw e;
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Execute a request with intelligent retry logic
136
+ */
137
+ private static async executeWithRetry(
138
+ url: string,
139
+ config: AxiosRequestConfig,
140
+ printError: boolean,
141
+ responseProcessor: (response: any) => any
142
+ ): Promise<any> {
143
+ let attempts = 0;
144
+ const maxAttempts = 3;
145
+ const baseDelay = 500; // Start with 500ms delay
146
+
147
+ while (true) {
148
+ try {
149
+ const result = await this.axiosInstance.request({ ...config, url });
150
+ return responseProcessor(result);
151
+ } catch (e: any) {
152
+ attempts++;
153
+ const errorMessage = this.getErrorMessage(e);
154
+
155
+ // Handle not found errors
156
+ if (errorMessage.includes('could not be found')) {
157
+ logger.info(`File does not exist, or you do not have permissions to read it.`);
158
+ return undefined;
159
+ }
160
+
161
+ // Check if we should retry
162
+ if (attempts < maxAttempts && this.isRetryableError(e)) {
163
+ // Calculate exponential backoff with jitter
164
+ const jitter = Math.random() * 0.3 + 0.85; // Between 0.85 and 1.15
165
+ const delay = Math.min(baseDelay * Math.pow(2, attempts - 1) * jitter, 5000);
166
+
167
+ logger.warn(`Request failed. Retrying in ${Math.round(delay)}ms (${attempts}/${maxAttempts})`);
168
+ await new Promise((resolve) => setTimeout(resolve, delay));
169
+ continue;
170
+ }
171
+
172
+ // Log error if needed
173
+ if (printError) {
174
+ this.logDetailedError(e, url);
175
+ }
176
+
177
+ throw e;
200
178
  }
201
179
  }
202
- return result;
180
+ }
181
+
182
+ /**
183
+ * Check if an error is retryable
184
+ */
185
+ private static isRetryableError(error: any): boolean {
186
+ // Network errors
187
+ if (error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT' || error.message.includes('timeout')) {
188
+ return true;
189
+ }
190
+
191
+ // Server errors (5xx)
192
+ if (error.response?.status >= 500) {
193
+ return true;
194
+ }
195
+
196
+ // Rate limiting (429)
197
+ if (error.response?.status === 429) {
198
+ return true;
199
+ }
200
+
201
+ return false;
202
+ }
203
+
204
+ /**
205
+ * Log detailed error information
206
+ */
207
+ private static logDetailedError(error: any, url: string): void {
208
+ if (error.response) {
209
+ logger.error(`Error for ${url}: ${error.message}`);
210
+ logger.error(`Status: ${error.response.status}`);
211
+
212
+ if (error.response.data) {
213
+ if (typeof error.response.data === 'string') {
214
+ logger.error(`Response: ${error.response.data.substring(0, 200)}`);
215
+ } else {
216
+ const dataMessage =
217
+ error.response.data.message || JSON.stringify(error.response.data).substring(0, 200);
218
+ logger.error(`Response: ${dataMessage}`);
219
+ }
220
+ }
221
+ } else {
222
+ logger.error(`Error for ${url}: ${error.message}`);
223
+ }
224
+ }
225
+
226
+ private static getErrorMessage(error: any): string {
227
+ if (error.response?.data?.message) {
228
+ return JSON.stringify(error.response.data.message);
229
+ } else if (error.response?.data) {
230
+ return JSON.stringify(error.response.data);
231
+ } else if (error.response) {
232
+ return `HTTP ${error.response.status}`;
233
+ } else if (error.message) {
234
+ return error.message;
235
+ } else {
236
+ return 'Unknown error occurred';
237
+ }
203
238
  }
204
239
  }