@wildix/xbees-conversations-utils 1.1.73 → 1.1.75

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 = 500;
20
+ exports.SOFT_THROTTLE_DELAY_MEDIUM_MS = 1500;
21
+ exports.SOFT_THROTTLE_DELAY_HIGH_MS = 3000;
15
22
  exports.STREAM_SYNC_CHANNEL_RESULT_METHOD_NAMES = [
16
23
  'channel',
17
24
  'getChannelById',
@@ -6,9 +6,9 @@ const constants_1 = require("../constants");
6
6
  function toPositiveMilliseconds(milliseconds) {
7
7
  return Math.max(0, Math.ceil(milliseconds));
8
8
  }
9
- function toUnixEpochSecondsFromDelayMs(delayMs) {
9
+ function toHttpDateFromDelayMs(delayMs) {
10
10
  const positiveDelayMs = toPositiveMilliseconds(delayMs);
11
- return Math.ceil((Date.now() + positiveDelayMs) / 1000);
11
+ return new Date(Date.now() + positiveDelayMs).toUTCString();
12
12
  }
13
13
  function createRateLimitExceededException(options = {}) {
14
14
  const { rateLimit, message, operation, retryAfterMs = constants_1.DEFAULT_RETRY_AFTER_MS } = options;
@@ -16,7 +16,7 @@ function createRateLimitExceededException(options = {}) {
16
16
  const retryAfterFromResetMs = rateLimit?.reset ? rateLimit.reset - Date.now() : undefined;
17
17
  const effectiveRetryAfterMs = retryAfterFromResetMs ?? retryAfterMs;
18
18
  const retryAfterMilliseconds = toPositiveMilliseconds(effectiveRetryAfterMs);
19
- const retryAfterUnixEpochSeconds = toUnixEpochSecondsFromDelayMs(effectiveRetryAfterMs);
19
+ const retryAfterHttpDate = toHttpDateFromDelayMs(effectiveRetryAfterMs);
20
20
  const rateLimitResetMilliseconds = rateLimit?.reset
21
21
  ? toPositiveMilliseconds(retryAfterFromResetMs ?? 0)
22
22
  : retryAfterMilliseconds;
@@ -24,7 +24,7 @@ function createRateLimitExceededException(options = {}) {
24
24
  $metadata: {},
25
25
  message: message || defaultMessage,
26
26
  rateLimitRemaining: rateLimit?.remaining || 0,
27
- retryAfter: retryAfterUnixEpochSeconds,
27
+ retryAfter: retryAfterHttpDate,
28
28
  rateLimit: rateLimit?.limit?.toString() || 'unknown',
29
29
  rateLimitReset: rateLimitResetMilliseconds,
30
30
  });
@@ -7,6 +7,20 @@ const createRateLimitExceededException_1 = require("./createRateLimitExceededExc
7
7
  const processStreamRateLimitException_1 = require("./processStreamRateLimitException");
8
8
  const RedisCooldown_1 = require("./RedisCooldown");
9
9
  function normalizeRetryAfterToDelayMs(retryAfter) {
10
+ if (typeof retryAfter === 'string') {
11
+ const asDateMs = Date.parse(retryAfter);
12
+ if (!Number.isNaN(asDateMs)) {
13
+ return Math.max(0, Math.round(asDateMs - Date.now()));
14
+ }
15
+ const asSeconds = Number(retryAfter);
16
+ if (Number.isFinite(asSeconds) && asSeconds > 0) {
17
+ return Math.max(0, Math.round(asSeconds * 1000));
18
+ }
19
+ return 0;
20
+ }
21
+ if (typeof retryAfter !== 'number' || !Number.isFinite(retryAfter) || retryAfter <= 0) {
22
+ return 0;
23
+ }
10
24
  if (retryAfter > 1000000000) {
11
25
  return Math.max(0, Math.round(retryAfter * 1000 - Date.now()));
12
26
  }
@@ -16,8 +30,8 @@ function normalizeRetryAfterToDelayMs(retryAfter) {
16
30
  return Math.max(0, Math.round(retryAfter * 1000));
17
31
  }
18
32
  function calculateRateLimitDelayMs(error, maxDelayMs) {
19
- const hasServerRetryAfter = typeof error.retryAfter === 'number' && error.retryAfter > 0;
20
- const retryAfterMsFromError = hasServerRetryAfter ? normalizeRetryAfterToDelayMs(error.retryAfter) : 0;
33
+ const retryAfterMsFromError = normalizeRetryAfterToDelayMs(error.retryAfter);
34
+ const hasServerRetryAfter = retryAfterMsFromError > 0;
21
35
  const baseDelayMs = hasServerRetryAfter ? retryAfterMsFromError + constants_1.RETRY_AFTER_BUFFER_MS : constants_1.DEFAULT_RETRY_AFTER_MS;
22
36
  if (baseDelayMs > 0) {
23
37
  const jitterMs = Math.floor(Math.random() * constants_1.DEFAULT_JITTER_MS);
@@ -34,6 +48,14 @@ async function executeWithRateLimitHandling(execute, operation, context, options
34
48
  const { enableCooldown, maxAttempts, maxDelayMs, maxRetryableDelayMs } = options;
35
49
  let attempt = 1;
36
50
  if (enableCooldown) {
51
+ 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 });
52
+ if (softCooldownMs > 0) {
53
+ logger.debug('Delaying stream request due to active soft rate-limit cooldown', {
54
+ operation,
55
+ cooldown: softCooldownMs,
56
+ });
57
+ await (0, promises_1.setTimeout)(softCooldownMs);
58
+ }
37
59
  const budgetCooldownMs = await RedisCooldown_1.RedisCooldown.getRemainingMs(constants_1.STREAM_BUDGET_COOLDOWN_KEY, { redis, logger }, { keyPrefix: constants_1.STREAM_BUDGET_COOLDOWN_KEY_PREFIX });
38
60
  if (budgetCooldownMs > 0) {
39
61
  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 = 500;
17
+ export const SOFT_THROTTLE_DELAY_MEDIUM_MS = 1500;
18
+ export const SOFT_THROTTLE_DELAY_HIGH_MS = 3000;
12
19
  export const STREAM_SYNC_CHANNEL_RESULT_METHOD_NAMES = [
13
20
  'channel',
14
21
  'getChannelById',
@@ -3,9 +3,9 @@ import { DEFAULT_RETRY_AFTER_MS } from '../constants';
3
3
  function toPositiveMilliseconds(milliseconds) {
4
4
  return Math.max(0, Math.ceil(milliseconds));
5
5
  }
6
- function toUnixEpochSecondsFromDelayMs(delayMs) {
6
+ function toHttpDateFromDelayMs(delayMs) {
7
7
  const positiveDelayMs = toPositiveMilliseconds(delayMs);
8
- return Math.ceil((Date.now() + positiveDelayMs) / 1000);
8
+ return new Date(Date.now() + positiveDelayMs).toUTCString();
9
9
  }
10
10
  export function createRateLimitExceededException(options = {}) {
11
11
  const { rateLimit, message, operation, retryAfterMs = DEFAULT_RETRY_AFTER_MS } = options;
@@ -13,7 +13,7 @@ export function createRateLimitExceededException(options = {}) {
13
13
  const retryAfterFromResetMs = rateLimit?.reset ? rateLimit.reset - Date.now() : undefined;
14
14
  const effectiveRetryAfterMs = retryAfterFromResetMs ?? retryAfterMs;
15
15
  const retryAfterMilliseconds = toPositiveMilliseconds(effectiveRetryAfterMs);
16
- const retryAfterUnixEpochSeconds = toUnixEpochSecondsFromDelayMs(effectiveRetryAfterMs);
16
+ const retryAfterHttpDate = toHttpDateFromDelayMs(effectiveRetryAfterMs);
17
17
  const rateLimitResetMilliseconds = rateLimit?.reset
18
18
  ? toPositiveMilliseconds(retryAfterFromResetMs ?? 0)
19
19
  : retryAfterMilliseconds;
@@ -21,7 +21,7 @@ export function createRateLimitExceededException(options = {}) {
21
21
  $metadata: {},
22
22
  message: message || defaultMessage,
23
23
  rateLimitRemaining: rateLimit?.remaining || 0,
24
- retryAfter: retryAfterUnixEpochSeconds,
24
+ retryAfter: retryAfterHttpDate,
25
25
  rateLimit: rateLimit?.limit?.toString() || 'unknown',
26
26
  rateLimitReset: rateLimitResetMilliseconds,
27
27
  });
@@ -1,9 +1,23 @@
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';
6
6
  function normalizeRetryAfterToDelayMs(retryAfter) {
7
+ if (typeof retryAfter === 'string') {
8
+ const asDateMs = Date.parse(retryAfter);
9
+ if (!Number.isNaN(asDateMs)) {
10
+ return Math.max(0, Math.round(asDateMs - Date.now()));
11
+ }
12
+ const asSeconds = Number(retryAfter);
13
+ if (Number.isFinite(asSeconds) && asSeconds > 0) {
14
+ return Math.max(0, Math.round(asSeconds * 1000));
15
+ }
16
+ return 0;
17
+ }
18
+ if (typeof retryAfter !== 'number' || !Number.isFinite(retryAfter) || retryAfter <= 0) {
19
+ return 0;
20
+ }
7
21
  if (retryAfter > 1000000000) {
8
22
  return Math.max(0, Math.round(retryAfter * 1000 - Date.now()));
9
23
  }
@@ -13,8 +27,8 @@ function normalizeRetryAfterToDelayMs(retryAfter) {
13
27
  return Math.max(0, Math.round(retryAfter * 1000));
14
28
  }
15
29
  function calculateRateLimitDelayMs(error, maxDelayMs) {
16
- const hasServerRetryAfter = typeof error.retryAfter === 'number' && error.retryAfter > 0;
17
- const retryAfterMsFromError = hasServerRetryAfter ? normalizeRetryAfterToDelayMs(error.retryAfter) : 0;
30
+ const retryAfterMsFromError = normalizeRetryAfterToDelayMs(error.retryAfter);
31
+ const hasServerRetryAfter = retryAfterMsFromError > 0;
18
32
  const baseDelayMs = hasServerRetryAfter ? retryAfterMsFromError + RETRY_AFTER_BUFFER_MS : DEFAULT_RETRY_AFTER_MS;
19
33
  if (baseDelayMs > 0) {
20
34
  const jitterMs = Math.floor(Math.random() * DEFAULT_JITTER_MS);
@@ -31,6 +45,14 @@ export async function executeWithRateLimitHandling(execute, operation, context,
31
45
  const { enableCooldown, maxAttempts, maxDelayMs, maxRetryableDelayMs } = options;
32
46
  let attempt = 1;
33
47
  if (enableCooldown) {
48
+ const softCooldownMs = await RedisCooldown.getRemainingMs(STREAM_RATE_LIMIT_SOFT_COOLDOWN_KEY, { redis, logger }, { keyPrefix: STREAM_RATE_LIMIT_COOLDOWN_KEY_PREFIX });
49
+ if (softCooldownMs > 0) {
50
+ logger.debug('Delaying stream request due to active soft rate-limit cooldown', {
51
+ operation,
52
+ cooldown: softCooldownMs,
53
+ });
54
+ await sleep(softCooldownMs);
55
+ }
34
56
  const budgetCooldownMs = await RedisCooldown.getRemainingMs(STREAM_BUDGET_COOLDOWN_KEY, { redis, logger }, { keyPrefix: STREAM_BUDGET_COOLDOWN_KEY_PREFIX });
35
57
  if (budgetCooldownMs > 0) {
36
58
  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 = 500;
18
+ export declare const SOFT_THROTTLE_DELAY_MEDIUM_MS = 1500;
19
+ export declare const SOFT_THROTTLE_DELAY_HIGH_MS = 3000;
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.75",
4
4
  "description": "",
5
5
  "main": "./dist-cjs/index.js",
6
6
  "module": "./dist-es/index.js",