@wildix/xbees-conversations-utils 1.1.73 → 1.1.74

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,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DEFAULT_OPTIONS = exports.CHANNEL_SYNC_PROXY_RESULT_METHOD_NAMES = exports.CHANNEL_SYNC_METHOD_NAMES = exports.STREAM_SYNC_METHOD_NAMES = exports.STREAM_SYNC_CHANNEL_RESULT_METHOD_NAMES = exports.BUDGET_DELAY_MAX_MS = exports.BUDGET_DELAY_MIN_MS = exports.BUDGET_THRESHOLD = exports.STREAM_BUDGET_COOLDOWN_KEY = exports.STREAM_RATE_LIMIT_OPTIONS_MARKER = exports.STREAM_BUDGET_COOLDOWN_KEY_PREFIX = exports.STREAM_RATE_LIMIT_COOLDOWN_KEY_PREFIX = exports.RETRY_AFTER_BUFFER_MS = exports.DEFAULT_JITTER_MS = exports.DEFAULT_RETRY_AFTER_MS = exports.DEFAULT_MAX_DELAY_MS = void 0;
3
+ exports.DEFAULT_OPTIONS = exports.CHANNEL_SYNC_PROXY_RESULT_METHOD_NAMES = exports.CHANNEL_SYNC_METHOD_NAMES = exports.STREAM_SYNC_METHOD_NAMES = exports.STREAM_SYNC_CHANNEL_RESULT_METHOD_NAMES = exports.SOFT_THROTTLE_DELAY_HIGH_MS = exports.SOFT_THROTTLE_DELAY_MEDIUM_MS = exports.SOFT_THROTTLE_DELAY_LOW_MS = exports.SOFT_THROTTLE_USAGE_HIGH = exports.SOFT_THROTTLE_USAGE_MEDIUM = exports.SOFT_THROTTLE_USAGE_LOW = exports.BUDGET_DELAY_MAX_MS = exports.BUDGET_DELAY_MIN_MS = exports.BUDGET_THRESHOLD = exports.STREAM_BUDGET_COOLDOWN_KEY = exports.STREAM_RATE_LIMIT_SOFT_COOLDOWN_KEY = exports.STREAM_RATE_LIMIT_OPTIONS_MARKER = exports.STREAM_BUDGET_COOLDOWN_KEY_PREFIX = exports.STREAM_RATE_LIMIT_COOLDOWN_KEY_PREFIX = exports.RETRY_AFTER_BUFFER_MS = exports.DEFAULT_JITTER_MS = exports.DEFAULT_RETRY_AFTER_MS = exports.DEFAULT_MAX_DELAY_MS = void 0;
4
4
  exports.DEFAULT_MAX_DELAY_MS = 5000;
5
5
  exports.DEFAULT_RETRY_AFTER_MS = 10000;
6
6
  exports.DEFAULT_JITTER_MS = 150;
@@ -8,10 +8,17 @@ exports.RETRY_AFTER_BUFFER_MS = 1000;
8
8
  exports.STREAM_RATE_LIMIT_COOLDOWN_KEY_PREFIX = 'stream-rate-limit-cooldown:';
9
9
  exports.STREAM_BUDGET_COOLDOWN_KEY_PREFIX = 'stream-budget-cooldown:';
10
10
  exports.STREAM_RATE_LIMIT_OPTIONS_MARKER = '__xbsRateLimitOptions';
11
+ exports.STREAM_RATE_LIMIT_SOFT_COOLDOWN_KEY = 'stream-rate-limit-soft-global';
11
12
  exports.STREAM_BUDGET_COOLDOWN_KEY = 'stream-budget-global';
12
13
  exports.BUDGET_THRESHOLD = 0.8;
13
14
  exports.BUDGET_DELAY_MIN_MS = 30000;
14
15
  exports.BUDGET_DELAY_MAX_MS = 60000;
16
+ exports.SOFT_THROTTLE_USAGE_LOW = 0.7;
17
+ exports.SOFT_THROTTLE_USAGE_MEDIUM = 0.85;
18
+ exports.SOFT_THROTTLE_USAGE_HIGH = 0.95;
19
+ exports.SOFT_THROTTLE_DELAY_LOW_MS = 150;
20
+ exports.SOFT_THROTTLE_DELAY_MEDIUM_MS = 500;
21
+ exports.SOFT_THROTTLE_DELAY_HIGH_MS = 1200;
15
22
  exports.STREAM_SYNC_CHANNEL_RESULT_METHOD_NAMES = [
16
23
  'channel',
17
24
  'getChannelById',
@@ -34,6 +34,14 @@ async function executeWithRateLimitHandling(execute, operation, context, options
34
34
  const { enableCooldown, maxAttempts, maxDelayMs, maxRetryableDelayMs } = options;
35
35
  let attempt = 1;
36
36
  if (enableCooldown) {
37
+ const softCooldownMs = await RedisCooldown_1.RedisCooldown.getRemainingMs(constants_1.STREAM_RATE_LIMIT_SOFT_COOLDOWN_KEY, { redis, logger }, { keyPrefix: constants_1.STREAM_RATE_LIMIT_COOLDOWN_KEY_PREFIX });
38
+ if (softCooldownMs > 0) {
39
+ logger.debug('Delaying stream request due to active soft rate-limit cooldown', {
40
+ operation,
41
+ cooldown: softCooldownMs,
42
+ });
43
+ await (0, promises_1.setTimeout)(softCooldownMs);
44
+ }
37
45
  const budgetCooldownMs = await RedisCooldown_1.RedisCooldown.getRemainingMs(constants_1.STREAM_BUDGET_COOLDOWN_KEY, { redis, logger }, { keyPrefix: constants_1.STREAM_BUDGET_COOLDOWN_KEY_PREFIX });
38
46
  if (budgetCooldownMs > 0) {
39
47
  logger.warn('Delaying stream request due to active budget cooldown', {
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.processStreamRateLimitHeaders = void 0;
4
+ const constants_1 = require("../constants");
5
+ const RedisCooldown_1 = require("./RedisCooldown");
6
+ function parseHeaderNumber(value) {
7
+ if (typeof value === 'number' && Number.isFinite(value)) {
8
+ return value;
9
+ }
10
+ if (typeof value === 'string') {
11
+ const parsed = Number(value);
12
+ if (Number.isFinite(parsed)) {
13
+ return parsed;
14
+ }
15
+ }
16
+ }
17
+ function getHeaderCaseInsensitive(headers, key) {
18
+ if (!headers) {
19
+ return;
20
+ }
21
+ const direct = headers[key];
22
+ if (direct !== undefined) {
23
+ return direct;
24
+ }
25
+ const normalizedKey = key.toLowerCase();
26
+ for (const [headerKey, headerValue] of Object.entries(headers)) {
27
+ if (headerKey.toLowerCase() === normalizedKey) {
28
+ return headerValue;
29
+ }
30
+ }
31
+ }
32
+ function computeSoftThrottleDelayMs(limit, remaining) {
33
+ if (limit <= 0) {
34
+ return 0;
35
+ }
36
+ const usage = 1 - remaining / limit;
37
+ if (usage >= constants_1.SOFT_THROTTLE_USAGE_HIGH) {
38
+ return constants_1.SOFT_THROTTLE_DELAY_HIGH_MS;
39
+ }
40
+ if (usage >= constants_1.SOFT_THROTTLE_USAGE_MEDIUM) {
41
+ return constants_1.SOFT_THROTTLE_DELAY_MEDIUM_MS;
42
+ }
43
+ if (usage >= constants_1.SOFT_THROTTLE_USAGE_LOW) {
44
+ return constants_1.SOFT_THROTTLE_DELAY_LOW_MS;
45
+ }
46
+ return 0;
47
+ }
48
+ async function processStreamRateLimitHeaders(headers, context) {
49
+ const { logger } = context;
50
+ const limit = parseHeaderNumber(getHeaderCaseInsensitive(headers, 'x-ratelimit-limit'));
51
+ const remaining = parseHeaderNumber(getHeaderCaseInsensitive(headers, 'x-ratelimit-remaining'));
52
+ if (limit === undefined || remaining === undefined) {
53
+ return;
54
+ }
55
+ if (limit <= 0 || remaining < 0) {
56
+ return;
57
+ }
58
+ const delayMs = computeSoftThrottleDelayMs(limit, remaining);
59
+ if (delayMs > 0) {
60
+ await RedisCooldown_1.RedisCooldown.setRemainingMs(constants_1.STREAM_RATE_LIMIT_SOFT_COOLDOWN_KEY, delayMs, context, {
61
+ keyPrefix: constants_1.STREAM_RATE_LIMIT_COOLDOWN_KEY_PREFIX,
62
+ });
63
+ logger.debug('Enabling soft stream rate-limit cooldown', {
64
+ rateLimit: limit,
65
+ rateLimitRemaining: remaining,
66
+ cooldownMs: delayMs,
67
+ });
68
+ return;
69
+ }
70
+ await RedisCooldown_1.RedisCooldown.clear(constants_1.STREAM_RATE_LIMIT_SOFT_COOLDOWN_KEY, context, {
71
+ keyPrefix: constants_1.STREAM_RATE_LIMIT_COOLDOWN_KEY_PREFIX,
72
+ });
73
+ }
74
+ exports.processStreamRateLimitHeaders = processStreamRateLimitHeaders;
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.registerStreamBudgetInterceptors = void 0;
4
4
  const processStreamBudgetHeaders_1 = require("./processStreamBudgetHeaders");
5
+ const processStreamRateLimitHeaders_1 = require("./processStreamRateLimitHeaders");
5
6
  const attachedInterceptors = new WeakSet();
6
7
  function hasResponseInterceptors(value) {
7
8
  return (typeof value === 'object' &&
@@ -35,9 +36,11 @@ function registerStreamBudgetInterceptors(stream, context) {
35
36
  }
36
37
  axiosInstance.interceptors.response.use(async (response) => {
37
38
  await (0, processStreamBudgetHeaders_1.processStreamBudgetHeaders)(response?.headers, context);
39
+ await (0, processStreamRateLimitHeaders_1.processStreamRateLimitHeaders)(response?.headers, context);
38
40
  return response;
39
41
  }, async (error) => {
40
42
  await (0, processStreamBudgetHeaders_1.processStreamBudgetHeaders)(error?.response?.headers, context);
43
+ await (0, processStreamRateLimitHeaders_1.processStreamRateLimitHeaders)(error?.response?.headers, context);
41
44
  return Promise.reject(error);
42
45
  });
43
46
  attachedInterceptors.add(axiosInstance);
@@ -5,10 +5,17 @@ export const RETRY_AFTER_BUFFER_MS = 1000;
5
5
  export const STREAM_RATE_LIMIT_COOLDOWN_KEY_PREFIX = 'stream-rate-limit-cooldown:';
6
6
  export const STREAM_BUDGET_COOLDOWN_KEY_PREFIX = 'stream-budget-cooldown:';
7
7
  export const STREAM_RATE_LIMIT_OPTIONS_MARKER = '__xbsRateLimitOptions';
8
+ export const STREAM_RATE_LIMIT_SOFT_COOLDOWN_KEY = 'stream-rate-limit-soft-global';
8
9
  export const STREAM_BUDGET_COOLDOWN_KEY = 'stream-budget-global';
9
10
  export const BUDGET_THRESHOLD = 0.8;
10
11
  export const BUDGET_DELAY_MIN_MS = 30000;
11
12
  export const BUDGET_DELAY_MAX_MS = 60000;
13
+ export const SOFT_THROTTLE_USAGE_LOW = 0.7;
14
+ export const SOFT_THROTTLE_USAGE_MEDIUM = 0.85;
15
+ export const SOFT_THROTTLE_USAGE_HIGH = 0.95;
16
+ export const SOFT_THROTTLE_DELAY_LOW_MS = 150;
17
+ export const SOFT_THROTTLE_DELAY_MEDIUM_MS = 500;
18
+ export const SOFT_THROTTLE_DELAY_HIGH_MS = 1200;
12
19
  export const STREAM_SYNC_CHANNEL_RESULT_METHOD_NAMES = [
13
20
  'channel',
14
21
  'getChannelById',
@@ -1,5 +1,5 @@
1
1
  import { setTimeout as sleep } from 'node:timers/promises';
2
- import { DEFAULT_JITTER_MS, DEFAULT_MAX_DELAY_MS, DEFAULT_RETRY_AFTER_MS, RETRY_AFTER_BUFFER_MS, STREAM_BUDGET_COOLDOWN_KEY, STREAM_BUDGET_COOLDOWN_KEY_PREFIX, } from '../constants';
2
+ import { DEFAULT_JITTER_MS, DEFAULT_MAX_DELAY_MS, DEFAULT_RETRY_AFTER_MS, RETRY_AFTER_BUFFER_MS, STREAM_BUDGET_COOLDOWN_KEY, STREAM_BUDGET_COOLDOWN_KEY_PREFIX, STREAM_RATE_LIMIT_COOLDOWN_KEY_PREFIX, STREAM_RATE_LIMIT_SOFT_COOLDOWN_KEY, } from '../constants';
3
3
  import { createRateLimitExceededException } from './createRateLimitExceededException';
4
4
  import { processStreamRateLimitException } from './processStreamRateLimitException';
5
5
  import { RedisCooldown } from './RedisCooldown';
@@ -31,6 +31,14 @@ export async function executeWithRateLimitHandling(execute, operation, context,
31
31
  const { enableCooldown, maxAttempts, maxDelayMs, maxRetryableDelayMs } = options;
32
32
  let attempt = 1;
33
33
  if (enableCooldown) {
34
+ const softCooldownMs = await RedisCooldown.getRemainingMs(STREAM_RATE_LIMIT_SOFT_COOLDOWN_KEY, { redis, logger }, { keyPrefix: STREAM_RATE_LIMIT_COOLDOWN_KEY_PREFIX });
35
+ if (softCooldownMs > 0) {
36
+ logger.debug('Delaying stream request due to active soft rate-limit cooldown', {
37
+ operation,
38
+ cooldown: softCooldownMs,
39
+ });
40
+ await sleep(softCooldownMs);
41
+ }
34
42
  const budgetCooldownMs = await RedisCooldown.getRemainingMs(STREAM_BUDGET_COOLDOWN_KEY, { redis, logger }, { keyPrefix: STREAM_BUDGET_COOLDOWN_KEY_PREFIX });
35
43
  if (budgetCooldownMs > 0) {
36
44
  logger.warn('Delaying stream request due to active budget cooldown', {
@@ -0,0 +1,70 @@
1
+ import { SOFT_THROTTLE_DELAY_HIGH_MS, SOFT_THROTTLE_DELAY_LOW_MS, SOFT_THROTTLE_DELAY_MEDIUM_MS, SOFT_THROTTLE_USAGE_HIGH, SOFT_THROTTLE_USAGE_LOW, SOFT_THROTTLE_USAGE_MEDIUM, STREAM_RATE_LIMIT_COOLDOWN_KEY_PREFIX, STREAM_RATE_LIMIT_SOFT_COOLDOWN_KEY, } from '../constants';
2
+ import { RedisCooldown } from './RedisCooldown';
3
+ function parseHeaderNumber(value) {
4
+ if (typeof value === 'number' && Number.isFinite(value)) {
5
+ return value;
6
+ }
7
+ if (typeof value === 'string') {
8
+ const parsed = Number(value);
9
+ if (Number.isFinite(parsed)) {
10
+ return parsed;
11
+ }
12
+ }
13
+ }
14
+ function getHeaderCaseInsensitive(headers, key) {
15
+ if (!headers) {
16
+ return;
17
+ }
18
+ const direct = headers[key];
19
+ if (direct !== undefined) {
20
+ return direct;
21
+ }
22
+ const normalizedKey = key.toLowerCase();
23
+ for (const [headerKey, headerValue] of Object.entries(headers)) {
24
+ if (headerKey.toLowerCase() === normalizedKey) {
25
+ return headerValue;
26
+ }
27
+ }
28
+ }
29
+ function computeSoftThrottleDelayMs(limit, remaining) {
30
+ if (limit <= 0) {
31
+ return 0;
32
+ }
33
+ const usage = 1 - remaining / limit;
34
+ if (usage >= SOFT_THROTTLE_USAGE_HIGH) {
35
+ return SOFT_THROTTLE_DELAY_HIGH_MS;
36
+ }
37
+ if (usage >= SOFT_THROTTLE_USAGE_MEDIUM) {
38
+ return SOFT_THROTTLE_DELAY_MEDIUM_MS;
39
+ }
40
+ if (usage >= SOFT_THROTTLE_USAGE_LOW) {
41
+ return SOFT_THROTTLE_DELAY_LOW_MS;
42
+ }
43
+ return 0;
44
+ }
45
+ export async function processStreamRateLimitHeaders(headers, context) {
46
+ const { logger } = context;
47
+ const limit = parseHeaderNumber(getHeaderCaseInsensitive(headers, 'x-ratelimit-limit'));
48
+ const remaining = parseHeaderNumber(getHeaderCaseInsensitive(headers, 'x-ratelimit-remaining'));
49
+ if (limit === undefined || remaining === undefined) {
50
+ return;
51
+ }
52
+ if (limit <= 0 || remaining < 0) {
53
+ return;
54
+ }
55
+ const delayMs = computeSoftThrottleDelayMs(limit, remaining);
56
+ if (delayMs > 0) {
57
+ await RedisCooldown.setRemainingMs(STREAM_RATE_LIMIT_SOFT_COOLDOWN_KEY, delayMs, context, {
58
+ keyPrefix: STREAM_RATE_LIMIT_COOLDOWN_KEY_PREFIX,
59
+ });
60
+ logger.debug('Enabling soft stream rate-limit cooldown', {
61
+ rateLimit: limit,
62
+ rateLimitRemaining: remaining,
63
+ cooldownMs: delayMs,
64
+ });
65
+ return;
66
+ }
67
+ await RedisCooldown.clear(STREAM_RATE_LIMIT_SOFT_COOLDOWN_KEY, context, {
68
+ keyPrefix: STREAM_RATE_LIMIT_COOLDOWN_KEY_PREFIX,
69
+ });
70
+ }
@@ -1,4 +1,5 @@
1
1
  import { processStreamBudgetHeaders } from './processStreamBudgetHeaders';
2
+ import { processStreamRateLimitHeaders } from './processStreamRateLimitHeaders';
2
3
  const attachedInterceptors = new WeakSet();
3
4
  function hasResponseInterceptors(value) {
4
5
  return (typeof value === 'object' &&
@@ -32,9 +33,11 @@ export function registerStreamBudgetInterceptors(stream, context) {
32
33
  }
33
34
  axiosInstance.interceptors.response.use(async (response) => {
34
35
  await processStreamBudgetHeaders(response?.headers, context);
36
+ await processStreamRateLimitHeaders(response?.headers, context);
35
37
  return response;
36
38
  }, async (error) => {
37
39
  await processStreamBudgetHeaders(error?.response?.headers, context);
40
+ await processStreamRateLimitHeaders(error?.response?.headers, context);
38
41
  return Promise.reject(error);
39
42
  });
40
43
  attachedInterceptors.add(axiosInstance);
@@ -6,10 +6,17 @@ export declare const RETRY_AFTER_BUFFER_MS = 1000;
6
6
  export declare const STREAM_RATE_LIMIT_COOLDOWN_KEY_PREFIX = "stream-rate-limit-cooldown:";
7
7
  export declare const STREAM_BUDGET_COOLDOWN_KEY_PREFIX = "stream-budget-cooldown:";
8
8
  export declare const STREAM_RATE_LIMIT_OPTIONS_MARKER = "__xbsRateLimitOptions";
9
+ export declare const STREAM_RATE_LIMIT_SOFT_COOLDOWN_KEY = "stream-rate-limit-soft-global";
9
10
  export declare const STREAM_BUDGET_COOLDOWN_KEY = "stream-budget-global";
10
11
  export declare const BUDGET_THRESHOLD = 0.8;
11
12
  export declare const BUDGET_DELAY_MIN_MS = 30000;
12
13
  export declare const BUDGET_DELAY_MAX_MS = 60000;
14
+ export declare const SOFT_THROTTLE_USAGE_LOW = 0.7;
15
+ export declare const SOFT_THROTTLE_USAGE_MEDIUM = 0.85;
16
+ export declare const SOFT_THROTTLE_USAGE_HIGH = 0.95;
17
+ export declare const SOFT_THROTTLE_DELAY_LOW_MS = 150;
18
+ export declare const SOFT_THROTTLE_DELAY_MEDIUM_MS = 500;
19
+ export declare const SOFT_THROTTLE_DELAY_HIGH_MS = 1200;
13
20
  export declare const STREAM_SYNC_CHANNEL_RESULT_METHOD_NAMES: readonly ["channel", "getChannelById", "getChannelByMembers", "hydrateActiveChannels"];
14
21
  export declare const STREAM_SYNC_METHOD_NAMES: readonly ["_addChannelConfig", "_buildWSPayload", "_callClientListeners", "_deleteUserMessageReference", "_enrichAxiosOptions", "_getConnectionID", "_getToken", "_handleClientEvent", "_handleUserEvent", "_hasConnectionID", "_isUsingServerAuth", "_logApiError", "_logApiRequest", "_logApiResponse", "_muteStatus", "_normalizeDate", "_normalizeExpiration", "_sayHi", "_setUser", "_startCleaning", "_updateMemberWatcherReferences", "_updateUserMessageReferences", "_updateUserReferences", "_validateAndGetMessageId", "createAbortControllerForNextRequest", "createToken", "devToken", "dispatchEvent", "errorFromResponse", "getAuthType", "getUserAgent", "handleEvent", "handleResponse", "off", "on", "setBaseURL", "setLocalDevice", "setUserAgent", "userMuteStatus", "verifyWebhook"];
15
22
  export declare const CHANNEL_SYNC_METHOD_NAMES: readonly ["_channelURL", "countUnread", "countUnreadMentions", "getConfig", "lastMessage", "lastRead", "muteStatus", "off", "on"];
@@ -0,0 +1,6 @@
1
+ import { LoggerContext, RedisContext, StreamHeaders } from '../types';
2
+ /**
3
+ * Applies soft global throttle from x-ratelimit-* headers to smooth bursts
4
+ * before hard 429 throttling starts.
5
+ */
6
+ export declare function processStreamRateLimitHeaders(headers: StreamHeaders | undefined, context: LoggerContext & RedisContext): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wildix/xbees-conversations-utils",
3
- "version": "1.1.73",
3
+ "version": "1.1.74",
4
4
  "description": "",
5
5
  "main": "./dist-cjs/index.js",
6
6
  "module": "./dist-es/index.js",