@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.
@@ -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
- constructor(message: string, status: number);
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
  }
@@ -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
- constructor(message, status) {
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
  }
@@ -264,99 +264,29 @@ class Logger {
264
264
  const NULL_LOGGER = new Logger({}, true);
265
265
 
266
266
  /**
267
- * The Cache class provides caching capabilities that can be used across your integration.
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
- * @see {@link Cache.create}
269
+ * @param redisUrl - The redis url to connect to (optional).
270
+ * @returns A cache instance.
271
271
  */
272
- class Cache {
273
- cacheInstance;
274
- constructor(cacheInstance) {
275
- this.cacheInstance = cacheInstance;
276
- }
277
- /**
278
- * Get or fetch a value
279
- *
280
- * @param key The key of the value to get
281
- * @param ttl The time to live of the value in seconds.
282
- * @param fetchFn The function that can retrieve the original value
283
- * @param lockTtl Global distributed lock TTL (in seconds) protecting fetching.
284
- * If undefined, 0 or falsy, locking is not preformed
285
- * @param shouldCacheError A callback being passed errors, controlling whether
286
- * to cache or not errors. Defaults to never cache.
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
- constructor(message, status) {
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
  }
@@ -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';
@@ -5,6 +5,9 @@ function onError(err, _req, res, next) {
5
5
  }
6
6
  let error;
7
7
  if (err instanceof HttpError) {
8
+ if (err.retryAfter) {
9
+ res.setHeader('Retry-After', String(err.retryAfter));
10
+ }
8
11
  error = {
9
12
  code: err.status.toString(),
10
13
  message: err.message,
@@ -1,67 +1,12 @@
1
- import { FetchingFunction, CachableValue } from 'cachette';
1
+ import { CacheInstance } from 'cachette';
2
2
  /**
3
- * The Cache class provides caching capabilities that can be used across your integration.
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
- * @see {@link Cache.create}
5
+ * @param redisUrl - The redis url to connect to (optional).
6
+ * @returns A cache instance.
7
7
  */
8
- export declare class Cache {
9
- private cacheInstance;
10
- private constructor();
11
- /**
12
- * Get or fetch a value
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
- * The Cache class provides caching capabilities that can be used across your integration.
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
- * @see {@link Cache.create}
7
+ * @param redisUrl - The redis url to connect to (optional).
8
+ * @returns A cache instance.
9
9
  */
10
- export class Cache {
11
- cacheInstance;
12
- constructor(cacheInstance) {
13
- this.cacheInstance = cacheInstance;
14
- }
15
- /**
16
- * Get or fetch a value
17
- *
18
- * @param key The key of the value to get
19
- * @param ttl The time to live of the value in seconds.
20
- * @param fetchFn The function that can retrieve the original value
21
- * @param lockTtl Global distributed lock TTL (in seconds) protecting fetching.
22
- * If undefined, 0 or falsy, locking is not preformed
23
- * @param shouldCacheError A callback being passed errors, controlling whether
24
- * to cache or not errors. Defaults to never cache.
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 Cache);
10
- assert.ok(cache['cacheInstance'] instanceof LocalCache);
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.');