@unito/integration-sdk 2.3.14 → 2.4.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/src/httpErrors.d.ts +11 -3
- package/dist/src/httpErrors.js +8 -5
- package/dist/src/index.cjs +62 -95
- package/dist/src/index.d.ts +1 -1
- package/dist/src/middlewares/errors.js +3 -0
- package/dist/src/resources/cache.d.ts +9 -64
- package/dist/src/resources/cache.js +20 -90
- package/dist/src/resources/provider.d.ts +0 -1
- package/dist/src/resources/provider.js +31 -0
- package/dist/test/middlewares/errors.test.js +24 -1
- package/dist/test/resources/cache.test.js +2 -3
- package/dist/test/resources/provider.test.js +224 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/httpErrors.ts +8 -5
- package/src/index.ts +1 -1
- package/src/middlewares/errors.ts +4 -0
- package/src/resources/cache.ts +27 -108
- package/src/resources/provider.ts +39 -1
- package/test/middlewares/errors.test.ts +28 -1
- package/test/resources/cache.test.ts +2 -3
- package/test/resources/provider.test.ts +269 -0
package/dist/src/httpErrors.d.ts
CHANGED
|
@@ -4,10 +4,14 @@
|
|
|
4
4
|
*
|
|
5
5
|
* @field message - The error message
|
|
6
6
|
* @field status - The HTTP status code to return
|
|
7
|
+
* @field retryAfter - The minimum number of seconds to wait before retrying the request.
|
|
7
8
|
*/
|
|
8
9
|
export declare class HttpError extends Error {
|
|
9
10
|
readonly status: number;
|
|
10
|
-
|
|
11
|
+
readonly retryAfter: number | undefined;
|
|
12
|
+
constructor(message: string, status: number, options?: {
|
|
13
|
+
retryAfter?: number;
|
|
14
|
+
});
|
|
11
15
|
}
|
|
12
16
|
/**
|
|
13
17
|
* Used to generate a 400 Bad Request. Usually used when something is missing to properly handle the request.
|
|
@@ -57,12 +61,16 @@ export declare class UnprocessableEntityError extends HttpError {
|
|
|
57
61
|
* Used to generate a 423 Provider Instance Locked.
|
|
58
62
|
*/
|
|
59
63
|
export declare class ProviderInstanceLockedError extends HttpError {
|
|
60
|
-
constructor(message?: string
|
|
64
|
+
constructor(message?: string, options?: {
|
|
65
|
+
retryAfter?: number;
|
|
66
|
+
});
|
|
61
67
|
}
|
|
62
68
|
/**
|
|
63
69
|
* Used to generate a 429 Rate Limit Exceeded. Usually used when an operation triggers or would trigger a rate limit
|
|
64
70
|
* error on the provider's side.
|
|
65
71
|
*/
|
|
66
72
|
export declare class RateLimitExceededError extends HttpError {
|
|
67
|
-
constructor(message?: string
|
|
73
|
+
constructor(message?: string, options?: {
|
|
74
|
+
retryAfter?: number;
|
|
75
|
+
});
|
|
68
76
|
}
|
package/dist/src/httpErrors.js
CHANGED
|
@@ -4,13 +4,16 @@
|
|
|
4
4
|
*
|
|
5
5
|
* @field message - The error message
|
|
6
6
|
* @field status - The HTTP status code to return
|
|
7
|
+
* @field retryAfter - The minimum number of seconds to wait before retrying the request.
|
|
7
8
|
*/
|
|
8
9
|
export class HttpError extends Error {
|
|
9
10
|
status;
|
|
10
|
-
|
|
11
|
+
retryAfter = undefined;
|
|
12
|
+
constructor(message, status, options = {}) {
|
|
11
13
|
super(message);
|
|
12
14
|
this.status = status;
|
|
13
15
|
this.name = this.constructor.name;
|
|
16
|
+
this.retryAfter = options.retryAfter;
|
|
14
17
|
}
|
|
15
18
|
}
|
|
16
19
|
/**
|
|
@@ -75,8 +78,8 @@ export class UnprocessableEntityError extends HttpError {
|
|
|
75
78
|
* Used to generate a 423 Provider Instance Locked.
|
|
76
79
|
*/
|
|
77
80
|
export class ProviderInstanceLockedError extends HttpError {
|
|
78
|
-
constructor(message) {
|
|
79
|
-
super(message || 'Provider instance locked or unavailable', 423);
|
|
81
|
+
constructor(message, options = {}) {
|
|
82
|
+
super(message || 'Provider instance locked or unavailable', 423, options);
|
|
80
83
|
}
|
|
81
84
|
}
|
|
82
85
|
/**
|
|
@@ -84,7 +87,7 @@ export class ProviderInstanceLockedError extends HttpError {
|
|
|
84
87
|
* error on the provider's side.
|
|
85
88
|
*/
|
|
86
89
|
export class RateLimitExceededError extends HttpError {
|
|
87
|
-
constructor(message) {
|
|
88
|
-
super(message || 'Rate Limit Exceeded', 429);
|
|
90
|
+
constructor(message, options = {}) {
|
|
91
|
+
super(message || 'Rate Limit Exceeded', 429, options);
|
|
89
92
|
}
|
|
90
93
|
}
|
package/dist/src/index.cjs
CHANGED
|
@@ -264,99 +264,29 @@ class Logger {
|
|
|
264
264
|
const NULL_LOGGER = new Logger({}, true);
|
|
265
265
|
|
|
266
266
|
/**
|
|
267
|
-
*
|
|
268
|
-
* It can be backed by a Redis instance (by passing it a URL to the instance) or a local cache.
|
|
267
|
+
* Initializes a Cache backed by the Redis instance at the provided url if present, or a LocalCache otherwise.
|
|
269
268
|
*
|
|
270
|
-
* @
|
|
269
|
+
* @param redisUrl - The redis url to connect to (optional).
|
|
270
|
+
* @returns A cache instance.
|
|
271
271
|
*/
|
|
272
|
-
|
|
273
|
-
cacheInstance;
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
* @returns The cached or fetched value
|
|
289
|
-
*/
|
|
290
|
-
getOrFetchValue(key, ttl, fetcher, lockTtl, shouldCacheError) {
|
|
291
|
-
return this.cacheInstance.getOrFetchValue(key, ttl, fetcher, lockTtl, shouldCacheError);
|
|
292
|
-
}
|
|
293
|
-
/**
|
|
294
|
-
* Get a value from the cache.
|
|
295
|
-
*
|
|
296
|
-
* @param key The key of the value to get.
|
|
297
|
-
*
|
|
298
|
-
* @return The value associated with the key, or undefined if
|
|
299
|
-
* no such value exists.
|
|
300
|
-
*/
|
|
301
|
-
getValue(key) {
|
|
302
|
-
return this.cacheInstance.getValue(key);
|
|
303
|
-
}
|
|
304
|
-
/**
|
|
305
|
-
* Set a value in the cache.
|
|
306
|
-
*
|
|
307
|
-
* @param key The key of the value to set.
|
|
308
|
-
* @param value The value to set.
|
|
309
|
-
* @param ttl The time to live of the value in seconds.
|
|
310
|
-
* By default, the value will not expire
|
|
311
|
-
*
|
|
312
|
-
* @return true if the value was stored, false otherwise.
|
|
313
|
-
*/
|
|
314
|
-
setValue(key, value, ttl) {
|
|
315
|
-
return this.cacheInstance.setValue(key, value, ttl);
|
|
316
|
-
}
|
|
317
|
-
/**
|
|
318
|
-
* Delete a value from the cache.
|
|
319
|
-
* @param key — The key of the value to set.
|
|
320
|
-
*/
|
|
321
|
-
delValue(key) {
|
|
322
|
-
return this.cacheInstance.delValue(key);
|
|
323
|
-
}
|
|
324
|
-
/**
|
|
325
|
-
* Get the TTL of an entry, in ms
|
|
326
|
-
*
|
|
327
|
-
* @param key The key of the entry whose ttl to retrieve
|
|
328
|
-
*
|
|
329
|
-
* @return The remaining TTL on the entry, in ms.
|
|
330
|
-
* undefined if the entry does not exist.
|
|
331
|
-
* 0 if the entry does not expire.
|
|
332
|
-
*/
|
|
333
|
-
getTtl(key) {
|
|
334
|
-
return this.cacheInstance.getTtl(key);
|
|
335
|
-
}
|
|
336
|
-
/**
|
|
337
|
-
* Initializes a Cache backed by the Redis instance at the provided url if present, or a LocalCache otherwise.
|
|
338
|
-
*
|
|
339
|
-
* @param redisUrl - The redis url to connect to (optional).
|
|
340
|
-
* @returns A cache instance.
|
|
341
|
-
*/
|
|
342
|
-
static create(redisUrl) {
|
|
343
|
-
const cacheInstance = redisUrl ? new cachette.RedisCache(redisUrl) : new cachette.LocalCache();
|
|
344
|
-
// Intended: the correlation id will be the same for all logs of Cachette.
|
|
345
|
-
const correlationId = crypto.randomUUID();
|
|
346
|
-
const logger = new Logger({ correlation_id: correlationId });
|
|
347
|
-
cacheInstance
|
|
348
|
-
.on('info', message => {
|
|
349
|
-
logger.info(message);
|
|
350
|
-
})
|
|
351
|
-
.on('warn', message => {
|
|
352
|
-
logger.warn(message);
|
|
353
|
-
})
|
|
354
|
-
.on('error', message => {
|
|
355
|
-
logger.error(message);
|
|
356
|
-
});
|
|
357
|
-
return new Cache(cacheInstance);
|
|
358
|
-
}
|
|
272
|
+
function create(redisUrl) {
|
|
273
|
+
const cacheInstance = redisUrl ? new cachette.RedisCache(redisUrl) : new cachette.LocalCache();
|
|
274
|
+
// Intended: the correlation id will be the same for all logs of Cachette.
|
|
275
|
+
const correlationId = crypto.randomUUID();
|
|
276
|
+
const logger = new Logger({ correlation_id: correlationId });
|
|
277
|
+
cacheInstance
|
|
278
|
+
.on('info', message => {
|
|
279
|
+
logger.info(message);
|
|
280
|
+
})
|
|
281
|
+
.on('warn', message => {
|
|
282
|
+
logger.warn(message);
|
|
283
|
+
})
|
|
284
|
+
.on('error', message => {
|
|
285
|
+
logger.error(message);
|
|
286
|
+
});
|
|
287
|
+
return cacheInstance;
|
|
359
288
|
}
|
|
289
|
+
const Cache = { create };
|
|
360
290
|
|
|
361
291
|
/**
|
|
362
292
|
* Error class meant to be returned by integrations in case of exceptions. These errors will be caught and handled
|
|
@@ -364,13 +294,16 @@ class Cache {
|
|
|
364
294
|
*
|
|
365
295
|
* @field message - The error message
|
|
366
296
|
* @field status - The HTTP status code to return
|
|
297
|
+
* @field retryAfter - The minimum number of seconds to wait before retrying the request.
|
|
367
298
|
*/
|
|
368
299
|
class HttpError extends Error {
|
|
369
300
|
status;
|
|
370
|
-
|
|
301
|
+
retryAfter = undefined;
|
|
302
|
+
constructor(message, status, options = {}) {
|
|
371
303
|
super(message);
|
|
372
304
|
this.status = status;
|
|
373
305
|
this.name = this.constructor.name;
|
|
306
|
+
this.retryAfter = options.retryAfter;
|
|
374
307
|
}
|
|
375
308
|
}
|
|
376
309
|
/**
|
|
@@ -435,8 +368,8 @@ class UnprocessableEntityError extends HttpError {
|
|
|
435
368
|
* Used to generate a 423 Provider Instance Locked.
|
|
436
369
|
*/
|
|
437
370
|
class ProviderInstanceLockedError extends HttpError {
|
|
438
|
-
constructor(message) {
|
|
439
|
-
super(message || 'Provider instance locked or unavailable', 423);
|
|
371
|
+
constructor(message, options = {}) {
|
|
372
|
+
super(message || 'Provider instance locked or unavailable', 423, options);
|
|
440
373
|
}
|
|
441
374
|
}
|
|
442
375
|
/**
|
|
@@ -444,8 +377,8 @@ class ProviderInstanceLockedError extends HttpError {
|
|
|
444
377
|
* error on the provider's side.
|
|
445
378
|
*/
|
|
446
379
|
class RateLimitExceededError extends HttpError {
|
|
447
|
-
constructor(message) {
|
|
448
|
-
super(message || 'Rate Limit Exceeded', 429);
|
|
380
|
+
constructor(message, options = {}) {
|
|
381
|
+
super(message || 'Rate Limit Exceeded', 429, options);
|
|
449
382
|
}
|
|
450
383
|
}
|
|
451
384
|
|
|
@@ -876,6 +809,9 @@ function onError(err, _req, res, next) {
|
|
|
876
809
|
}
|
|
877
810
|
let error;
|
|
878
811
|
if (err instanceof HttpError) {
|
|
812
|
+
if (err.retryAfter) {
|
|
813
|
+
res.setHeader('Retry-After', String(err.retryAfter));
|
|
814
|
+
}
|
|
879
815
|
error = {
|
|
880
816
|
code: err.status.toString(),
|
|
881
817
|
message: err.message,
|
|
@@ -1369,6 +1305,21 @@ class Provider {
|
|
|
1369
1305
|
request.on('error', error => {
|
|
1370
1306
|
reject(this.handleError(400, `Error while calling the provider: "${error}"`, options));
|
|
1371
1307
|
});
|
|
1308
|
+
if (options.signal) {
|
|
1309
|
+
const abortHandler = () => {
|
|
1310
|
+
request.destroy();
|
|
1311
|
+
reject(this.handleError(408, 'Timeout', options));
|
|
1312
|
+
};
|
|
1313
|
+
if (options.signal.aborted) {
|
|
1314
|
+
abortHandler();
|
|
1315
|
+
}
|
|
1316
|
+
options.signal.addEventListener('abort', abortHandler);
|
|
1317
|
+
request.on('close', () => {
|
|
1318
|
+
if (options.signal) {
|
|
1319
|
+
options.signal.removeEventListener('abort', abortHandler);
|
|
1320
|
+
}
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1372
1323
|
form.pipe(request);
|
|
1373
1324
|
}
|
|
1374
1325
|
catch (error) {
|
|
@@ -1423,6 +1374,7 @@ class Provider {
|
|
|
1423
1374
|
path: urlObj.pathname + urlObj.search,
|
|
1424
1375
|
method: 'POST',
|
|
1425
1376
|
headers,
|
|
1377
|
+
timeout: 0,
|
|
1426
1378
|
};
|
|
1427
1379
|
const request = https.request(requestOptions, response => {
|
|
1428
1380
|
response.setEncoding('utf8');
|
|
@@ -1462,6 +1414,21 @@ class Provider {
|
|
|
1462
1414
|
request.destroy();
|
|
1463
1415
|
safeReject(this.handleError(500, `Stream error: "${error}"`, options));
|
|
1464
1416
|
});
|
|
1417
|
+
if (options.signal) {
|
|
1418
|
+
const abortHandler = () => {
|
|
1419
|
+
request.destroy();
|
|
1420
|
+
safeReject(this.handleError(408, 'Timeout', options));
|
|
1421
|
+
};
|
|
1422
|
+
if (options.signal.aborted) {
|
|
1423
|
+
abortHandler();
|
|
1424
|
+
}
|
|
1425
|
+
options.signal.addEventListener('abort', abortHandler);
|
|
1426
|
+
request.on('close', () => {
|
|
1427
|
+
if (options.signal) {
|
|
1428
|
+
options.signal.removeEventListener('abort', abortHandler);
|
|
1429
|
+
}
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1465
1432
|
// Stream the data directly without buffering
|
|
1466
1433
|
stream.pipe(request);
|
|
1467
1434
|
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export * as Api from '@unito/integration-api';
|
|
2
|
-
export { Cache } from './resources/cache.js';
|
|
2
|
+
export { Cache, type CacheInstance } from './resources/cache.js';
|
|
3
3
|
export { default as Integration } from './integration.js';
|
|
4
4
|
export * from './handler.js';
|
|
5
5
|
export { Provider, type Response as ProviderResponse, type RequestOptions as ProviderRequestOptions, type RateLimiter, } from './resources/provider.js';
|
|
@@ -1,67 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CacheInstance } from 'cachette';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
* It can be backed by a Redis instance (by passing it a URL to the instance) or a local cache.
|
|
3
|
+
* Initializes a Cache backed by the Redis instance at the provided url if present, or a LocalCache otherwise.
|
|
5
4
|
*
|
|
6
|
-
* @
|
|
5
|
+
* @param redisUrl - The redis url to connect to (optional).
|
|
6
|
+
* @returns A cache instance.
|
|
7
7
|
*/
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
*
|
|
14
|
-
* @param key The key of the value to get
|
|
15
|
-
* @param ttl The time to live of the value in seconds.
|
|
16
|
-
* @param fetchFn The function that can retrieve the original value
|
|
17
|
-
* @param lockTtl Global distributed lock TTL (in seconds) protecting fetching.
|
|
18
|
-
* If undefined, 0 or falsy, locking is not preformed
|
|
19
|
-
* @param shouldCacheError A callback being passed errors, controlling whether
|
|
20
|
-
* to cache or not errors. Defaults to never cache.
|
|
21
|
-
*
|
|
22
|
-
* @returns The cached or fetched value
|
|
23
|
-
*/
|
|
24
|
-
getOrFetchValue<F extends FetchingFunction = FetchingFunction>(key: string, ttl: number, fetcher: F, lockTtl?: number, shouldCacheError?: (err: Error) => boolean): Promise<ReturnType<F>>;
|
|
25
|
-
/**
|
|
26
|
-
* Get a value from the cache.
|
|
27
|
-
*
|
|
28
|
-
* @param key The key of the value to get.
|
|
29
|
-
*
|
|
30
|
-
* @return The value associated with the key, or undefined if
|
|
31
|
-
* no such value exists.
|
|
32
|
-
*/
|
|
33
|
-
getValue(key: string): Promise<CachableValue>;
|
|
34
|
-
/**
|
|
35
|
-
* Set a value in the cache.
|
|
36
|
-
*
|
|
37
|
-
* @param key The key of the value to set.
|
|
38
|
-
* @param value The value to set.
|
|
39
|
-
* @param ttl The time to live of the value in seconds.
|
|
40
|
-
* By default, the value will not expire
|
|
41
|
-
*
|
|
42
|
-
* @return true if the value was stored, false otherwise.
|
|
43
|
-
*/
|
|
44
|
-
setValue(key: string, value: CachableValue, ttl?: number): Promise<boolean>;
|
|
45
|
-
/**
|
|
46
|
-
* Delete a value from the cache.
|
|
47
|
-
* @param key — The key of the value to set.
|
|
48
|
-
*/
|
|
49
|
-
delValue(key: string): Promise<void>;
|
|
50
|
-
/**
|
|
51
|
-
* Get the TTL of an entry, in ms
|
|
52
|
-
*
|
|
53
|
-
* @param key The key of the entry whose ttl to retrieve
|
|
54
|
-
*
|
|
55
|
-
* @return The remaining TTL on the entry, in ms.
|
|
56
|
-
* undefined if the entry does not exist.
|
|
57
|
-
* 0 if the entry does not expire.
|
|
58
|
-
*/
|
|
59
|
-
getTtl(key: string): Promise<number | undefined>;
|
|
60
|
-
/**
|
|
61
|
-
* Initializes a Cache backed by the Redis instance at the provided url if present, or a LocalCache otherwise.
|
|
62
|
-
*
|
|
63
|
-
* @param redisUrl - The redis url to connect to (optional).
|
|
64
|
-
* @returns A cache instance.
|
|
65
|
-
*/
|
|
66
|
-
static create(redisUrl?: string): Cache;
|
|
67
|
-
}
|
|
8
|
+
declare function create(redisUrl?: string): CacheInstance;
|
|
9
|
+
export declare const Cache: {
|
|
10
|
+
create: typeof create;
|
|
11
|
+
};
|
|
12
|
+
export type { CacheInstance };
|
|
@@ -2,96 +2,26 @@ import { LocalCache, RedisCache } from 'cachette';
|
|
|
2
2
|
import crypto from 'crypto';
|
|
3
3
|
import Logger from './logger.js';
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
* It can be backed by a Redis instance (by passing it a URL to the instance) or a local cache.
|
|
5
|
+
* Initializes a Cache backed by the Redis instance at the provided url if present, or a LocalCache otherwise.
|
|
7
6
|
*
|
|
8
|
-
* @
|
|
7
|
+
* @param redisUrl - The redis url to connect to (optional).
|
|
8
|
+
* @returns A cache instance.
|
|
9
9
|
*/
|
|
10
|
-
|
|
11
|
-
cacheInstance;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
* @returns The cached or fetched value
|
|
27
|
-
*/
|
|
28
|
-
getOrFetchValue(key, ttl, fetcher, lockTtl, shouldCacheError) {
|
|
29
|
-
return this.cacheInstance.getOrFetchValue(key, ttl, fetcher, lockTtl, shouldCacheError);
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Get a value from the cache.
|
|
33
|
-
*
|
|
34
|
-
* @param key The key of the value to get.
|
|
35
|
-
*
|
|
36
|
-
* @return The value associated with the key, or undefined if
|
|
37
|
-
* no such value exists.
|
|
38
|
-
*/
|
|
39
|
-
getValue(key) {
|
|
40
|
-
return this.cacheInstance.getValue(key);
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Set a value in the cache.
|
|
44
|
-
*
|
|
45
|
-
* @param key The key of the value to set.
|
|
46
|
-
* @param value The value to set.
|
|
47
|
-
* @param ttl The time to live of the value in seconds.
|
|
48
|
-
* By default, the value will not expire
|
|
49
|
-
*
|
|
50
|
-
* @return true if the value was stored, false otherwise.
|
|
51
|
-
*/
|
|
52
|
-
setValue(key, value, ttl) {
|
|
53
|
-
return this.cacheInstance.setValue(key, value, ttl);
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Delete a value from the cache.
|
|
57
|
-
* @param key — The key of the value to set.
|
|
58
|
-
*/
|
|
59
|
-
delValue(key) {
|
|
60
|
-
return this.cacheInstance.delValue(key);
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Get the TTL of an entry, in ms
|
|
64
|
-
*
|
|
65
|
-
* @param key The key of the entry whose ttl to retrieve
|
|
66
|
-
*
|
|
67
|
-
* @return The remaining TTL on the entry, in ms.
|
|
68
|
-
* undefined if the entry does not exist.
|
|
69
|
-
* 0 if the entry does not expire.
|
|
70
|
-
*/
|
|
71
|
-
getTtl(key) {
|
|
72
|
-
return this.cacheInstance.getTtl(key);
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Initializes a Cache backed by the Redis instance at the provided url if present, or a LocalCache otherwise.
|
|
76
|
-
*
|
|
77
|
-
* @param redisUrl - The redis url to connect to (optional).
|
|
78
|
-
* @returns A cache instance.
|
|
79
|
-
*/
|
|
80
|
-
static create(redisUrl) {
|
|
81
|
-
const cacheInstance = redisUrl ? new RedisCache(redisUrl) : new LocalCache();
|
|
82
|
-
// Intended: the correlation id will be the same for all logs of Cachette.
|
|
83
|
-
const correlationId = crypto.randomUUID();
|
|
84
|
-
const logger = new Logger({ correlation_id: correlationId });
|
|
85
|
-
cacheInstance
|
|
86
|
-
.on('info', message => {
|
|
87
|
-
logger.info(message);
|
|
88
|
-
})
|
|
89
|
-
.on('warn', message => {
|
|
90
|
-
logger.warn(message);
|
|
91
|
-
})
|
|
92
|
-
.on('error', message => {
|
|
93
|
-
logger.error(message);
|
|
94
|
-
});
|
|
95
|
-
return new Cache(cacheInstance);
|
|
96
|
-
}
|
|
10
|
+
function create(redisUrl) {
|
|
11
|
+
const cacheInstance = redisUrl ? new RedisCache(redisUrl) : new LocalCache();
|
|
12
|
+
// Intended: the correlation id will be the same for all logs of Cachette.
|
|
13
|
+
const correlationId = crypto.randomUUID();
|
|
14
|
+
const logger = new Logger({ correlation_id: correlationId });
|
|
15
|
+
cacheInstance
|
|
16
|
+
.on('info', message => {
|
|
17
|
+
logger.info(message);
|
|
18
|
+
})
|
|
19
|
+
.on('warn', message => {
|
|
20
|
+
logger.warn(message);
|
|
21
|
+
})
|
|
22
|
+
.on('error', message => {
|
|
23
|
+
logger.error(message);
|
|
24
|
+
});
|
|
25
|
+
return cacheInstance;
|
|
97
26
|
}
|
|
27
|
+
export const Cache = { create };
|
|
@@ -17,7 +17,6 @@ import Logger from '../resources/logger.js';
|
|
|
17
17
|
* @param targetFunction - The function to call the provider.
|
|
18
18
|
* @returns The response from the provider.
|
|
19
19
|
* @throws RateLimitExceededError when the rate limit is exceeded.
|
|
20
|
-
* @throws WouldExceedRateLimitError when the next call would exceed the rate limit.
|
|
21
20
|
* @throws HttpError when the provider returns an error.
|
|
22
21
|
*/
|
|
23
22
|
export type RateLimiter = <T>(options: {
|
|
@@ -150,6 +150,21 @@ export class Provider {
|
|
|
150
150
|
request.on('error', error => {
|
|
151
151
|
reject(this.handleError(400, `Error while calling the provider: "${error}"`, options));
|
|
152
152
|
});
|
|
153
|
+
if (options.signal) {
|
|
154
|
+
const abortHandler = () => {
|
|
155
|
+
request.destroy();
|
|
156
|
+
reject(this.handleError(408, 'Timeout', options));
|
|
157
|
+
};
|
|
158
|
+
if (options.signal.aborted) {
|
|
159
|
+
abortHandler();
|
|
160
|
+
}
|
|
161
|
+
options.signal.addEventListener('abort', abortHandler);
|
|
162
|
+
request.on('close', () => {
|
|
163
|
+
if (options.signal) {
|
|
164
|
+
options.signal.removeEventListener('abort', abortHandler);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
153
168
|
form.pipe(request);
|
|
154
169
|
}
|
|
155
170
|
catch (error) {
|
|
@@ -204,6 +219,7 @@ export class Provider {
|
|
|
204
219
|
path: urlObj.pathname + urlObj.search,
|
|
205
220
|
method: 'POST',
|
|
206
221
|
headers,
|
|
222
|
+
timeout: 0,
|
|
207
223
|
};
|
|
208
224
|
const request = https.request(requestOptions, response => {
|
|
209
225
|
response.setEncoding('utf8');
|
|
@@ -243,6 +259,21 @@ export class Provider {
|
|
|
243
259
|
request.destroy();
|
|
244
260
|
safeReject(this.handleError(500, `Stream error: "${error}"`, options));
|
|
245
261
|
});
|
|
262
|
+
if (options.signal) {
|
|
263
|
+
const abortHandler = () => {
|
|
264
|
+
request.destroy();
|
|
265
|
+
safeReject(this.handleError(408, 'Timeout', options));
|
|
266
|
+
};
|
|
267
|
+
if (options.signal.aborted) {
|
|
268
|
+
abortHandler();
|
|
269
|
+
}
|
|
270
|
+
options.signal.addEventListener('abort', abortHandler);
|
|
271
|
+
request.on('close', () => {
|
|
272
|
+
if (options.signal) {
|
|
273
|
+
options.signal.removeEventListener('abort', abortHandler);
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
}
|
|
246
277
|
// Stream the data directly without buffering
|
|
247
278
|
stream.pipe(request);
|
|
248
279
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import { describe, it } from 'node:test';
|
|
3
3
|
import onError from '../../src/middlewares/errors.js';
|
|
4
|
-
import { HttpError } from '../../src/httpErrors.js';
|
|
4
|
+
import { HttpError, RateLimitExceededError } from '../../src/httpErrors.js';
|
|
5
5
|
describe('errors middleware', () => {
|
|
6
6
|
it('headers sent, do nothing', () => {
|
|
7
7
|
let actualStatus;
|
|
@@ -41,6 +41,29 @@ describe('errors middleware', () => {
|
|
|
41
41
|
assert.strictEqual(actualStatus, 429);
|
|
42
42
|
assert.deepEqual(actualJson, { code: '429', message: 'httpError' });
|
|
43
43
|
});
|
|
44
|
+
it('handles retry-after header', () => {
|
|
45
|
+
let actualStatus;
|
|
46
|
+
let actualJson;
|
|
47
|
+
const response = {
|
|
48
|
+
headersSent: false,
|
|
49
|
+
status: (code) => {
|
|
50
|
+
actualStatus = code;
|
|
51
|
+
return {
|
|
52
|
+
json: (json) => {
|
|
53
|
+
actualJson = json;
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
locals: { logger: { error: () => undefined } },
|
|
58
|
+
setHeader: (name, value) => {
|
|
59
|
+
assert.strictEqual(name, 'Retry-After');
|
|
60
|
+
assert.strictEqual(value, '1234');
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
onError(new RateLimitExceededError('httpError', { retryAfter: 1234 }), {}, response, () => { });
|
|
64
|
+
assert.strictEqual(actualStatus, 429);
|
|
65
|
+
assert.deepEqual(actualJson, { code: '429', message: 'httpError' });
|
|
66
|
+
});
|
|
44
67
|
it('handles other error', () => {
|
|
45
68
|
let actualStatus;
|
|
46
69
|
let actualJson;
|
|
@@ -6,9 +6,8 @@ describe('Cache', () => {
|
|
|
6
6
|
describe('initializeCache', () => {
|
|
7
7
|
it('no redis url returns Cache with a inner LocalCache', async () => {
|
|
8
8
|
const cache = Cache.create();
|
|
9
|
-
assert.ok(cache instanceof
|
|
10
|
-
|
|
11
|
-
await cache['cacheInstance'].quit();
|
|
9
|
+
assert.ok(cache instanceof LocalCache);
|
|
10
|
+
await cache.quit();
|
|
12
11
|
});
|
|
13
12
|
it('redis url tries to return a RedisCache', () => {
|
|
14
13
|
assert.throws(() => Cache.create('fakeredis'), Error, 'Invalid redis url fakereis.');
|