@wildix/xbees-conversations-utils 1.1.57 → 1.1.58

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.
Files changed (50) hide show
  1. package/dist-cjs/index.js +2 -2
  2. package/dist-cjs/rateLimits/RedisCooldown.js +45 -35
  3. package/dist-cjs/rateLimits/constants.js +6 -13
  4. package/dist-cjs/rateLimits/createRateLimitedStreamProxy.js +30 -73
  5. package/dist-cjs/rateLimits/exception/createRateLimitExceededException.js +17 -14
  6. package/dist-cjs/rateLimits/exception/processStreamRateLimitException.js +39 -23
  7. package/dist-cjs/rateLimits/executeStreamRequestWithRateLimitHandling.js +86 -67
  8. package/dist-cjs/rateLimits/helpers/RedisCooldown.js +56 -0
  9. package/dist-cjs/rateLimits/helpers/createRateLimitExceededException.js +18 -0
  10. package/dist-cjs/rateLimits/helpers/executeWithRateLimitHandling.js +72 -0
  11. package/dist-cjs/rateLimits/helpers/isStreamChannelLike.js +15 -0
  12. package/dist-cjs/rateLimits/helpers/processStreamRateLimitException.js +31 -0
  13. package/dist-cjs/rateLimits/helpers/splitCallArgsAndOptions.js +21 -0
  14. package/dist-cjs/rateLimits/index.js +1 -0
  15. package/dist-cjs/rateLimits/types.js +1 -0
  16. package/dist-cjs/rateLimits/withStreamRateLimitOptions.js +10 -0
  17. package/dist-es/index.js +2 -2
  18. package/dist-es/rateLimits/RedisCooldown.js +43 -33
  19. package/dist-es/rateLimits/constants.js +5 -12
  20. package/dist-es/rateLimits/createRateLimitedStreamProxy.js +30 -72
  21. package/dist-es/rateLimits/exception/createRateLimitExceededException.js +15 -12
  22. package/dist-es/rateLimits/exception/processStreamRateLimitException.js +32 -22
  23. package/dist-es/rateLimits/executeStreamRequestWithRateLimitHandling.js +87 -65
  24. package/dist-es/rateLimits/helpers/RedisCooldown.js +52 -0
  25. package/dist-es/rateLimits/helpers/createRateLimitExceededException.js +14 -0
  26. package/dist-es/rateLimits/helpers/executeWithRateLimitHandling.js +68 -0
  27. package/dist-es/rateLimits/helpers/isStreamChannelLike.js +11 -0
  28. package/dist-es/rateLimits/helpers/processStreamRateLimitException.js +27 -0
  29. package/dist-es/rateLimits/helpers/splitCallArgsAndOptions.js +17 -0
  30. package/dist-es/rateLimits/index.js +1 -0
  31. package/dist-es/rateLimits/types.js +1 -1
  32. package/dist-es/rateLimits/withStreamRateLimitOptions.js +6 -0
  33. package/dist-types/index.d.ts +2 -2
  34. package/dist-types/rateLimits/RedisCooldown.d.ts +5 -3
  35. package/dist-types/rateLimits/constants.d.ts +3 -5
  36. package/dist-types/rateLimits/createRateLimitedStreamProxy.d.ts +6 -13
  37. package/dist-types/rateLimits/exception/createRateLimitExceededException.d.ts +8 -6
  38. package/dist-types/rateLimits/exception/processStreamRateLimitException.d.ts +8 -3
  39. package/dist-types/rateLimits/executeStreamRequestWithRateLimitHandling.d.ts +8 -2
  40. package/dist-types/rateLimits/helpers/RedisCooldown.d.ts +5 -0
  41. package/dist-types/rateLimits/helpers/createRateLimitExceededException.d.ts +10 -0
  42. package/dist-types/rateLimits/helpers/executeWithRateLimitHandling.d.ts +2 -0
  43. package/dist-types/rateLimits/helpers/isStreamChannelLike.d.ts +6 -0
  44. package/dist-types/rateLimits/helpers/processStreamRateLimitException.d.ts +2 -0
  45. package/dist-types/rateLimits/helpers/splitCallArgsAndOptions.d.ts +6 -0
  46. package/dist-types/rateLimits/index.d.ts +1 -0
  47. package/dist-types/rateLimits/types.d.ts +15 -3
  48. package/dist-types/rateLimits/withStreamRateLimitOptions.d.ts +6 -0
  49. package/dist-types/types.d.ts +1 -1
  50. package/package.json +6 -1
@@ -1,71 +1,93 @@
1
- import { setTimeout as sleep } from 'node:timers/promises';
2
- import { DEFAULT_FIXED_RATE_LIMIT_DELAY_MS, DEFAULT_JITTER_MS, DEFAULT_OPTS_1_ATTEMPT_NO_COOLDOWN, DEFAULT_RETRY_AFTER_MS, RETRY_AFTER_BUFFER_MS, } from './constants';
3
- import { createRateLimitExceededException } from './exception/createRateLimitExceededException';
4
- import { processStreamRateLimitException } from './exception/processStreamRateLimitException';
5
- import { RedisCooldown } from './RedisCooldown';
1
+ import {setTimeout as sleep} from 'node:timers/promises';
2
+
3
+ import {
4
+ DEFAULT_FIXED_RATE_LIMIT_DELAY_MS,
5
+ DEFAULT_JITTER_MS,
6
+ DEFAULT_OPTS_1_ATTEMPT_NO_COOLDOWN,
7
+ DEFAULT_RETRY_AFTER_MS,
8
+ RETRY_AFTER_BUFFER_MS,
9
+ } from './constants';
10
+ import {createRateLimitExceededException} from './exception/createRateLimitExceededException';
11
+ import {processStreamRateLimitException} from './exception/processStreamRateLimitException';
12
+ import {RedisCooldown} from './RedisCooldown';
13
+
14
+ /** Computes actual backoff delay from rate-limit error and configured caps. */
6
15
  function calculateRateLimitDelayMs(error, maxDelayMs) {
7
- const retryAfterMs = typeof error.retryAfter === 'number' ? error.retryAfter + RETRY_AFTER_BUFFER_MS : DEFAULT_RETRY_AFTER_MS;
8
- if (retryAfterMs > 0) {
9
- const jitterMs = Math.floor(Math.random() * DEFAULT_JITTER_MS);
10
- return Math.min(maxDelayMs, retryAfterMs + jitterMs);
11
- }
12
- return Math.min(maxDelayMs, DEFAULT_FIXED_RATE_LIMIT_DELAY_MS);
16
+ const retryAfterMs =
17
+ typeof error.retryAfter === 'number' ? error.retryAfter + RETRY_AFTER_BUFFER_MS : DEFAULT_RETRY_AFTER_MS;
18
+
19
+ if (retryAfterMs > 0) {
20
+ const jitterMs = Math.floor(Math.random() * DEFAULT_JITTER_MS);
21
+
22
+ return Math.min(maxDelayMs, retryAfterMs + jitterMs);
23
+ }
24
+
25
+ return Math.min(maxDelayMs, DEFAULT_FIXED_RATE_LIMIT_DELAY_MS);
13
26
  }
27
+
28
+ /** Executes Stream request with cooldown check, retry policy and rate-limit normalization. */
14
29
  export async function executeStreamRequestWithRateLimitHandling(execute, operation, context, options = {}) {
15
- const { logger, redis } = context;
16
- const { enableCooldown, maxAttempts, maxDelayMs, maxRetryableDelayMs } = {
17
- ...DEFAULT_OPTS_1_ATTEMPT_NO_COOLDOWN,
18
- ...options,
19
- };
20
- let attempt = 1;
21
- if (enableCooldown) {
22
- const cooldownMs = await RedisCooldown.getRemainingMs(operation, { redis, logger });
23
- if (cooldownMs > 0) {
24
- logger.warn('Skipping stream request due to active rate-limit cooldown', {
25
- operation,
26
- cooldown: cooldownMs,
27
- });
28
- throw createRateLimitExceededException({ operation, retryAfterMs: cooldownMs });
29
- }
30
+ const {redis} = context;
31
+ const {enableCooldown, maxAttempts, maxDelayMs, maxRetryableDelayMs} = {
32
+ ...DEFAULT_OPTS_1_ATTEMPT_NO_COOLDOWN,
33
+ ...options,
34
+ };
35
+ let attempt = 1;
36
+
37
+ if (enableCooldown) {
38
+ const cooldownMs = await RedisCooldown.getRemainingMs(operation, {redis});
39
+
40
+ if (cooldownMs > 0) {
41
+ console.warn('Skipping stream request due to active rate-limit cooldown', {
42
+ operation,
43
+ cooldown: cooldownMs,
44
+ });
45
+ throw createRateLimitExceededException({operation, retryAfterMs: cooldownMs});
30
46
  }
31
- for (;;) {
32
- try {
33
- return await execute();
34
- }
35
- catch (error) {
36
- const rateLimitError = processStreamRateLimitException(error, logger);
37
- if (!rateLimitError) {
38
- throw error;
39
- }
40
- const retryAfterMs = calculateRateLimitDelayMs(rateLimitError, maxDelayMs);
41
- if (enableCooldown) {
42
- await RedisCooldown.setRemainingMs(operation, retryAfterMs, { redis, logger });
43
- }
44
- if (attempt < maxAttempts && retryAfterMs <= maxRetryableDelayMs) {
45
- logger.warn('Retrying stream request after rate limit', {
46
- operation,
47
- attempt,
48
- maxAttempts,
49
- retryAfterMs,
50
- rateLimit: rateLimitError.rateLimit,
51
- rateLimitRemaining: rateLimitError.rateLimitRemaining,
52
- rateLimitReset: rateLimitError.rateLimitReset,
53
- });
54
- await sleep(retryAfterMs);
55
- attempt++;
56
- continue;
57
- }
58
- logger.warn('Propagating stream rate limit w/o retry', {
59
- operation,
60
- attempt,
61
- maxAttempts,
62
- retryAfterMs,
63
- maxRetryableDelayMs,
64
- rateLimit: rateLimitError.rateLimit,
65
- rateLimitRemaining: rateLimitError.rateLimitRemaining,
66
- rateLimitReset: rateLimitError.rateLimitReset,
67
- });
68
- throw rateLimitError;
69
- }
47
+ }
48
+
49
+ for (;;) {
50
+ try {
51
+ return await execute();
52
+ } catch (error) {
53
+ const rateLimitError = processStreamRateLimitException(error);
54
+
55
+ if (!rateLimitError) {
56
+ throw error;
57
+ }
58
+
59
+ const retryAfterMs = calculateRateLimitDelayMs(rateLimitError, maxDelayMs);
60
+
61
+ if (enableCooldown) {
62
+ await RedisCooldown.setRemainingMs(operation, retryAfterMs, {redis});
63
+ }
64
+
65
+ if (attempt < maxAttempts && retryAfterMs <= maxRetryableDelayMs) {
66
+ console.warn('Retrying stream request after rate limit', {
67
+ operation,
68
+ attempt,
69
+ maxAttempts,
70
+ retryAfterMs,
71
+ rateLimit: rateLimitError.rateLimit,
72
+ rateLimitRemaining: rateLimitError.rateLimitRemaining,
73
+ rateLimitReset: rateLimitError.rateLimitReset,
74
+ });
75
+ await sleep(retryAfterMs);
76
+ attempt++;
77
+ continue;
78
+ }
79
+
80
+ console.warn('Propagating stream rate limit w/o retry', {
81
+ operation,
82
+ attempt,
83
+ maxAttempts,
84
+ retryAfterMs,
85
+ maxRetryableDelayMs,
86
+ rateLimit: rateLimitError.rateLimit,
87
+ rateLimitRemaining: rateLimitError.rateLimitRemaining,
88
+ rateLimitReset: rateLimitError.rateLimitReset,
89
+ });
90
+ throw rateLimitError;
70
91
  }
92
+ }
71
93
  }
@@ -0,0 +1,52 @@
1
+ import { STREAM_RATE_LIMIT_COOLDOWN_KEY_PREFIX } from '../constants';
2
+ export class RedisCooldown {
3
+ static async getRemainingMs(cooldownKey, context) {
4
+ const { redis } = context;
5
+ try {
6
+ const ttlMs = await redis.pttl(`${STREAM_RATE_LIMIT_COOLDOWN_KEY_PREFIX}${cooldownKey}`);
7
+ if (ttlMs > 0) {
8
+ return ttlMs;
9
+ }
10
+ }
11
+ catch (error) {
12
+ console.warn('Failed to read stream rate-limit cooldown from Redis', {
13
+ cooldownKey,
14
+ error,
15
+ });
16
+ }
17
+ return 0;
18
+ }
19
+ static async setRemainingMs(cooldownKey, cooldownMs, context) {
20
+ if (cooldownMs <= 0) {
21
+ return;
22
+ }
23
+ const { redis } = context;
24
+ const redisKey = `${STREAM_RATE_LIMIT_COOLDOWN_KEY_PREFIX}${cooldownKey}`;
25
+ try {
26
+ await redis.eval(`
27
+ local key = KEYS[1]
28
+ local new_ttl = tonumber(ARGV[1])
29
+ local current_ttl = redis.call('PTTL', key)
30
+
31
+ if current_ttl == -2 then
32
+ redis.call('SET', key, '1', 'PX', new_ttl)
33
+ return 1
34
+ end
35
+
36
+ if current_ttl == -1 or current_ttl < new_ttl then
37
+ redis.call('PEXPIRE', key, new_ttl)
38
+ return 2
39
+ end
40
+
41
+ return 0
42
+ `, 1, redisKey, cooldownMs.toString());
43
+ }
44
+ catch (error) {
45
+ console.warn('Failed to write stream rate-limit cooldown to Redis', {
46
+ cooldownKey,
47
+ cooldownMs,
48
+ error,
49
+ });
50
+ }
51
+ }
52
+ }
@@ -0,0 +1,14 @@
1
+ import { RateLimitExceededException } from '@wildix/xbees-conversations-client';
2
+ import { DEFAULT_RETRY_AFTER_MS } from '../constants';
3
+ export function createRateLimitExceededException(options = {}) {
4
+ const { rateLimit, message, operation, retryAfterMs = DEFAULT_RETRY_AFTER_MS } = options;
5
+ const defaultMessage = operation ? `Rate limit exceeded for ${operation}` : 'Rate limit exceeded';
6
+ return new RateLimitExceededException({
7
+ $metadata: {},
8
+ message: message || defaultMessage,
9
+ rateLimitRemaining: rateLimit?.remaining || 0,
10
+ retryAfter: rateLimit?.reset ? rateLimit.reset - Date.now() : retryAfterMs,
11
+ rateLimit: rateLimit?.limit?.toString() || 'unknown',
12
+ rateLimitReset: rateLimit?.reset || Date.now() + retryAfterMs,
13
+ });
14
+ }
@@ -0,0 +1,68 @@
1
+ import { setTimeout as sleep } from 'node:timers/promises';
2
+ import { DEFAULT_FIXED_RATE_LIMIT_DELAY_MS, DEFAULT_JITTER_MS, DEFAULT_RETRY_AFTER_MS, RETRY_AFTER_BUFFER_MS, } from '../constants';
3
+ import { createRateLimitExceededException } from './createRateLimitExceededException';
4
+ import { processStreamRateLimitException } from './processStreamRateLimitException';
5
+ import { RedisCooldown } from './RedisCooldown';
6
+ function calculateRateLimitDelayMs(error, maxDelayMs) {
7
+ const retryAfterMs = typeof error.retryAfter === 'number' ? error.retryAfter + RETRY_AFTER_BUFFER_MS : DEFAULT_RETRY_AFTER_MS;
8
+ if (retryAfterMs > 0) {
9
+ const jitterMs = Math.floor(Math.random() * DEFAULT_JITTER_MS);
10
+ return Math.min(maxDelayMs, retryAfterMs + jitterMs);
11
+ }
12
+ return Math.min(maxDelayMs, DEFAULT_FIXED_RATE_LIMIT_DELAY_MS);
13
+ }
14
+ export async function executeWithRateLimitHandling(execute, operation, context, options) {
15
+ const { redis } = context;
16
+ const { enableCooldown, maxAttempts, maxDelayMs, maxRetryableDelayMs } = options;
17
+ let attempt = 1;
18
+ if (enableCooldown) {
19
+ const cooldownMs = await RedisCooldown.getRemainingMs(operation, { redis });
20
+ if (cooldownMs > 0) {
21
+ console.warn('Skipping stream request due to active rate-limit cooldown', {
22
+ operation,
23
+ cooldown: cooldownMs,
24
+ });
25
+ throw createRateLimitExceededException({ operation, retryAfterMs: cooldownMs });
26
+ }
27
+ }
28
+ for (;;) {
29
+ try {
30
+ return await execute();
31
+ }
32
+ catch (error) {
33
+ const rateLimitError = processStreamRateLimitException(error);
34
+ if (!rateLimitError) {
35
+ throw error;
36
+ }
37
+ const retryAfterMs = calculateRateLimitDelayMs(rateLimitError, maxDelayMs);
38
+ if (enableCooldown) {
39
+ await RedisCooldown.setRemainingMs(operation, retryAfterMs, { redis });
40
+ }
41
+ if (attempt < maxAttempts && retryAfterMs <= maxRetryableDelayMs) {
42
+ console.warn('Retrying stream request after rate limit', {
43
+ operation,
44
+ attempt,
45
+ maxAttempts,
46
+ retryAfterMs,
47
+ rateLimit: rateLimitError.rateLimit,
48
+ rateLimitRemaining: rateLimitError.rateLimitRemaining,
49
+ rateLimitReset: rateLimitError.rateLimitReset,
50
+ });
51
+ await sleep(retryAfterMs);
52
+ attempt++;
53
+ continue;
54
+ }
55
+ console.warn('Propagating stream rate limit w/o retry', {
56
+ operation,
57
+ attempt,
58
+ maxAttempts,
59
+ retryAfterMs,
60
+ maxRetryableDelayMs,
61
+ rateLimit: rateLimitError.rateLimit,
62
+ rateLimitRemaining: rateLimitError.rateLimitRemaining,
63
+ rateLimitReset: rateLimitError.rateLimitReset,
64
+ });
65
+ throw rateLimitError;
66
+ }
67
+ }
68
+ }
@@ -0,0 +1,11 @@
1
+ function hasFunctionProperty(value, key) {
2
+ return key in value && typeof value[key] === 'function';
3
+ }
4
+ export function isStreamChannelLike(value) {
5
+ if (typeof value !== 'object' || value === null) {
6
+ return false;
7
+ }
8
+ return (hasFunctionProperty(value, 'sendMessage') &&
9
+ hasFunctionProperty(value, 'queryMembers') &&
10
+ hasFunctionProperty(value, 'updatePartial'));
11
+ }
@@ -0,0 +1,27 @@
1
+ import { createRateLimitExceededException } from './createRateLimitExceededException';
2
+ function isStreamException(error) {
3
+ return (typeof error === 'object' &&
4
+ error !== null &&
5
+ 'metadata' in error &&
6
+ typeof error.metadata === 'object' &&
7
+ error.metadata !== null &&
8
+ 'responseCode' in error.metadata);
9
+ }
10
+ function isStreamAPIException(error) {
11
+ return typeof error === 'object' && !!error.code;
12
+ }
13
+ export function processStreamRateLimitException(error) {
14
+ if (isStreamException(error) && error.metadata?.responseCode === 429) {
15
+ const rateLimit = error.metadata?.rateLimit;
16
+ console.warn('Stream rate limit exceeded [StreamException]', { rateLimit });
17
+ return createRateLimitExceededException({ rateLimit, message: error.message || 'Rate limit exceeded' });
18
+ }
19
+ if (isStreamAPIException(error) && (error.code === 429 || error.StatusCode === 429)) {
20
+ console.warn('Stream rate limit exceeded [StreamAPIException]', { code: error.code, StatusCode: error.StatusCode });
21
+ return createRateLimitExceededException({ message: error.message || 'Rate limit exceeded' });
22
+ }
23
+ if (typeof error === 'object' && error !== null && 'status' in error && error.status === 429) {
24
+ console.warn('Stream rate limit exceeded [error.status === 429]');
25
+ return createRateLimitExceededException({ message: 'Rate limit exceeded' });
26
+ }
27
+ }
@@ -0,0 +1,17 @@
1
+ import { STREAM_RATE_LIMIT_OPTIONS_MARKER } from '../constants';
2
+ function isStreamRateLimitOptionsToken(value) {
3
+ return typeof value === 'object' && value !== null && STREAM_RATE_LIMIT_OPTIONS_MARKER in value;
4
+ }
5
+ export function splitCallArgsAndOptions(args) {
6
+ if (args.length === 0) {
7
+ return { callArgs: args, callOptions: {} };
8
+ }
9
+ const lastArg = args[args.length - 1];
10
+ if (isStreamRateLimitOptionsToken(lastArg)) {
11
+ return {
12
+ callArgs: args.slice(0, -1),
13
+ callOptions: lastArg[STREAM_RATE_LIMIT_OPTIONS_MARKER],
14
+ };
15
+ }
16
+ return { callArgs: args, callOptions: {} };
17
+ }
@@ -1 +1,2 @@
1
1
  export * from './createRateLimitedStreamProxy';
2
+ export * from './withStreamRateLimitOptions';
@@ -1 +1 @@
1
- export {};
1
+ import { STREAM_RATE_LIMIT_OPTIONS_MARKER } from './constants';
@@ -0,0 +1,6 @@
1
+ import { STREAM_RATE_LIMIT_OPTIONS_MARKER } from './constants';
2
+ export function withStreamRateLimitOptions(options) {
3
+ return {
4
+ [STREAM_RATE_LIMIT_OPTIONS_MARKER]: options,
5
+ };
6
+ }
@@ -1,4 +1,4 @@
1
- export * from './types';
2
1
  export * from './normalization';
3
- export * from './stream';
4
2
  export * from './rateLimits';
3
+ export * from './stream';
4
+ export * from './types';
@@ -1,5 +1,7 @@
1
- import { LoggerContext, RedisContext } from "./types";
1
+ import {LoggerContext, RedisContext} from './types';
2
+
2
3
  export declare class RedisCooldown {
3
- static getRemainingMs(cooldownKey: string, context: RedisContext & LoggerContext): Promise<number>;
4
- static setRemainingMs(cooldownKey: string, cooldownMs: number, context: RedisContext & LoggerContext): Promise<void>;
4
+ static getRemainingMs(cooldownKey: string, context: RedisContext & LoggerContext): Promise<number>;
5
+
6
+ static setRemainingMs(cooldownKey: string, cooldownMs: number, context: RedisContext & LoggerContext): Promise<void>;
5
7
  }
@@ -1,10 +1,8 @@
1
1
  import { StreamRateLimitRetryOptions } from './types';
2
2
  export declare const DEFAULT_FIXED_RATE_LIMIT_DELAY_MS = 15000;
3
- export declare const DEFAULT_MAX_DELAY_MS = 15000;
4
- export declare const DEFAULT_MAX_RETRYABLE_DELAY_MS = 5000;
5
3
  export declare const DEFAULT_JITTER_MS = 150;
6
4
  export declare const RETRY_AFTER_BUFFER_MS = 1000;
7
- export declare const STREAM_RATE_LIMIT_COOLDOWN_KEY_PREFIX = "stream-rate-limit-cooldown:";
8
5
  export declare const DEFAULT_RETRY_AFTER_MS = 10000;
9
- export declare const DEFAULT_OPTS_1_ATTEMPT_NO_COOLDOWN: StreamRateLimitRetryOptions;
10
- export declare const DEFAULT_OPTS_3_ATTEMPTS_WITH_COOLDOWN: StreamRateLimitRetryOptions;
6
+ export declare const STREAM_RATE_LIMIT_COOLDOWN_KEY_PREFIX = "stream-rate-limit-cooldown:";
7
+ export declare const STREAM_RATE_LIMIT_OPTIONS_MARKER = "__xbsRateLimitOptions";
8
+ export declare const DEFAULT_OPTIONS: StreamRateLimitRetryOptions;
@@ -1,15 +1,8 @@
1
1
  import { StreamChat } from 'stream-chat';
2
- import { LoggerContext, RedisContext } from './types';
3
- import { StreamRateLimitRetryOptions } from './types';
4
- import { StreamType } from "../stream";
5
- declare const STREAM_RATE_LIMIT_OPTIONS_MARKER = "__xbsRateLimitOptions";
6
- type StreamRateLimitOptionsToken = {
7
- [STREAM_RATE_LIMIT_OPTIONS_MARKER]: Partial<StreamRateLimitRetryOptions>;
8
- };
9
- type MethodWithRateLimitOptions<T> = T extends (...args: infer Args) => infer Result ? T & ((...args: [...Args, StreamRateLimitOptionsToken?]) => Result) : T;
10
- export type RateLimited<T extends object> = {
11
- [Key in keyof T]: MethodWithRateLimitOptions<T[Key]>;
12
- };
13
- export declare function withStreamRateLimitOptions(options: Partial<StreamRateLimitRetryOptions>): StreamRateLimitOptionsToken;
2
+ import { StreamType } from '../stream';
3
+ import { LoggerContext, RateLimited, RedisContext } from './types';
4
+ /**
5
+ * Creates a proxy around a Stream client that transparently applies rate-limit handling
6
+ * to method calls and recursively wraps returned channel objects with the same behavior.
7
+ */
14
8
  export declare function createRateLimitedStreamProxy(stream: StreamChat<StreamType>, context: LoggerContext & RedisContext): RateLimited<StreamChat<StreamType>>;
15
- export {};
@@ -1,10 +1,12 @@
1
- import { RateLimitExceededException } from '@wildix/xbees-conversations-client';
2
- import { RateLimitsInfo } from 'stream-chat';
1
+ import {RateLimitExceededException} from '@wildix/xbees-conversations-client';
2
+
3
+ import {RateLimitsInfo} from 'stream-chat';
4
+
3
5
  type Options = {
4
- rateLimit?: RateLimitsInfo;
5
- message?: string;
6
- operation?: string;
7
- retryAfterMs?: number;
6
+ rateLimit?: RateLimitsInfo;
7
+ message?: string;
8
+ operation?: string;
9
+ retryAfterMs?: number;
8
10
  };
9
11
  export declare function createRateLimitExceededException(options?: Options): RateLimitExceededException;
10
12
  export {};
@@ -1,3 +1,8 @@
1
- import { RateLimitExceededException } from '@wildix/xbees-conversations-client';
2
- import { Logger } from '@aws-lambda-powertools/logger';
3
- export declare function processStreamRateLimitException(error: unknown, logger: Logger): RateLimitExceededException | undefined;
1
+ import {RateLimitExceededException} from '@wildix/xbees-conversations-client';
2
+
3
+ import {Logger} from '@aws-lambda-powertools/logger';
4
+
5
+ export declare function processStreamRateLimitException(
6
+ error: unknown,
7
+ logger: Logger,
8
+ ): RateLimitExceededException | undefined;
@@ -1,2 +1,8 @@
1
- import { LoggerContext, RedisContext, StreamOperationType, StreamRateLimitRetryOptions } from './types';
2
- export declare function executeStreamRequestWithRateLimitHandling<T>(execute: () => Promise<T>, operation: StreamOperationType, context: LoggerContext & RedisContext, options?: Partial<StreamRateLimitRetryOptions>): Promise<T>;
1
+ import {LoggerContext, RedisContext, StreamOperationType, StreamRateLimitRetryOptions} from './types';
2
+
3
+ export declare function executeStreamRequestWithRateLimitHandling<T>(
4
+ execute: () => Promise<T>,
5
+ operation: StreamOperationType,
6
+ context: LoggerContext & RedisContext,
7
+ options?: Partial<StreamRateLimitRetryOptions>,
8
+ ): Promise<T>;
@@ -0,0 +1,5 @@
1
+ import { RedisContext } from '../types';
2
+ export declare class RedisCooldown {
3
+ static getRemainingMs(cooldownKey: string, context: RedisContext): Promise<number>;
4
+ static setRemainingMs(cooldownKey: string, cooldownMs: number, context: RedisContext): Promise<void>;
5
+ }
@@ -0,0 +1,10 @@
1
+ import { RateLimitExceededException } from '@wildix/xbees-conversations-client';
2
+ import { RateLimitsInfo } from 'stream-chat';
3
+ type Options = {
4
+ rateLimit?: RateLimitsInfo;
5
+ message?: string;
6
+ operation?: string;
7
+ retryAfterMs?: number;
8
+ };
9
+ export declare function createRateLimitExceededException(options?: Options): RateLimitExceededException;
10
+ export {};
@@ -0,0 +1,2 @@
1
+ import { LoggerContext, RedisContext, StreamOperationType, StreamRateLimitRetryOptions } from '../types';
2
+ export declare function executeWithRateLimitHandling<T>(execute: () => Promise<T>, operation: StreamOperationType, context: LoggerContext & RedisContext, options: StreamRateLimitRetryOptions): Promise<T>;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Detects Stream channel-like objects.
3
+ * These objects should be correctly wrapped to keep rate-limit behavior
4
+ * on methods returned from other proxied calls.
5
+ */
6
+ export declare function isStreamChannelLike(value: unknown): value is object;
@@ -0,0 +1,2 @@
1
+ import { RateLimitExceededException } from '@wildix/xbees-conversations-client';
2
+ export declare function processStreamRateLimitException(error: unknown): RateLimitExceededException | undefined;
@@ -0,0 +1,6 @@
1
+ import { ArgsAndOptions } from '../types';
2
+ /**
3
+ * Splits original call args and optional rate-limit options token.
4
+ * If no token is provided, options fallback to an empty object.
5
+ */
6
+ export declare function splitCallArgsAndOptions(args: unknown[]): ArgsAndOptions;
@@ -1 +1,2 @@
1
1
  export * from './createRateLimitedStreamProxy';
2
+ export * from './withStreamRateLimitOptions';
@@ -1,7 +1,8 @@
1
- import { StreamChat } from 'stream-chat';
2
- import { StreamChannel, StreamType } from "../stream";
3
1
  import Redis from 'ioredis';
4
- import { Logger } from "@aws-lambda-powertools/logger";
2
+ import { StreamChat } from 'stream-chat';
3
+ import { StreamChannel, StreamType } from '../stream';
4
+ import { STREAM_RATE_LIMIT_OPTIONS_MARKER } from './constants';
5
+ import { Logger } from '@aws-lambda-powertools/logger';
5
6
  export type StreamOperationType = keyof StreamChat<StreamType> | keyof StreamChannel;
6
7
  export interface StreamRateLimitRetryOptions {
7
8
  maxAttempts: number;
@@ -9,9 +10,20 @@ export interface StreamRateLimitRetryOptions {
9
10
  maxRetryableDelayMs: number;
10
11
  enableCooldown: boolean;
11
12
  }
13
+ export type StreamRateLimitOptionsToken = {
14
+ [STREAM_RATE_LIMIT_OPTIONS_MARKER]: Partial<StreamRateLimitRetryOptions>;
15
+ };
16
+ export type MethodWithRateLimitOptions<T> = T extends (...args: infer Args) => infer Result ? T & ((...args: [...Args, StreamRateLimitOptionsToken?]) => Result) : T;
17
+ export type RateLimited<T extends object> = {
18
+ [Key in keyof T]: MethodWithRateLimitOptions<T[Key]>;
19
+ };
12
20
  export type RedisContext = {
13
21
  redis: Redis;
14
22
  };
15
23
  export type LoggerContext = {
16
24
  logger: Logger;
17
25
  };
26
+ export type ArgsAndOptions = {
27
+ callArgs: unknown[];
28
+ callOptions: Partial<StreamRateLimitRetryOptions>;
29
+ };
@@ -0,0 +1,6 @@
1
+ import { StreamRateLimitOptionsToken, StreamRateLimitRetryOptions } from './types';
2
+ /**
3
+ * Creates a marker token that can be passed as the last argument to proxied Stream methods.
4
+ * The proxy strips this token from original method args and uses it as per-call retry options.
5
+ */
6
+ export declare function withStreamRateLimitOptions(options: Partial<StreamRateLimitRetryOptions>): StreamRateLimitOptionsToken;
@@ -1,4 +1,4 @@
1
- import { Channel, ChannelMemberRole, ChannelMember, Message, Reaction, User } from "@wildix/xbees-conversations-client";
1
+ import { Channel, ChannelMember, ChannelMemberRole, Message, Reaction, User } from '@wildix/xbees-conversations-client';
2
2
  export declare enum MessageDirection {
3
3
  INCOMING = "incoming",
4
4
  OUTGOING = "outgoing"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wildix/xbees-conversations-utils",
3
- "version": "1.1.57",
3
+ "version": "1.1.58",
4
4
  "description": "",
5
5
  "main": "./dist-cjs/index.js",
6
6
  "module": "./dist-es/index.js",
@@ -13,7 +13,10 @@
13
13
  "build:types": "tsc -p tsconfig.types.json",
14
14
  "build:types:downlevel": "downlevel-dts dist-types dist-types/ts3.4",
15
15
  "build:api-extractor": "mkdir -p etc && api-extractor run --local --verbose",
16
+ "check-types": "tsc --noemit",
16
17
  "clean": "rimraf ./dist-* && rimraf *.tsbuildinfo",
18
+ "lint": "eslint . && npm run check-types",
19
+ "lint:fix": "eslint . --fix",
17
20
  "postpublish": "node ../../scripts/postpublish.js"
18
21
  },
19
22
  "engines": {
@@ -30,8 +33,10 @@
30
33
  "devDependencies": {
31
34
  "@tsconfig/node18": "18.2.4",
32
35
  "@tsconfig/recommended": "1.0.1",
36
+ "@wildix/eslint-config-style-guide": "1.2.1",
33
37
  "concurrently": "7.0.0",
34
38
  "downlevel-dts": "0.10.1",
39
+ "eslint": "8.57.1",
35
40
  "rimraf": "^3.0.0",
36
41
  "typescript": "~5.2.2"
37
42
  },