@wildix/xbees-conversations-utils 1.1.78 → 1.1.80

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.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_COOLDOWN_SLEEP_CHUNK_MS = exports.BUDGET_MINIMAL_DELAY_MAX_MS = exports.BUDGET_MINIMAL_DELAY_MIN_MS = exports.BUDGET_RELAXED_DELAY_MAX_MS = exports.BUDGET_RELAXED_DELAY_MIN_MS = exports.BUDGET_RELAXED_USAGE_THRESHOLD = exports.BUDGET_CLEAR_USAGE_THRESHOLD = 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 = exports.STREAM_SYNC_METHOD_NAMES = exports.STREAM_SYNC_CHANNEL_RESULT_METHOD_NAMES = exports.STREAM_METHOD_NAMES = exports.STREAM_CHANNEL_RESULT_METHOD_NAMES = exports.CHANNEL_SYNC_PROXY_RESULT_METHOD_NAMES = exports.CHANNEL_SYNC_METHOD_NAMES = exports.CHANNEL_METHOD_NAMES = void 0;
3
+ exports.DEFAULT_OPTIONS = 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_COOLDOWN_SLEEP_CHUNK_MS = exports.BUDGET_MINIMAL_DELAY_MAX_MS = exports.BUDGET_MINIMAL_DELAY_MIN_MS = exports.BUDGET_RELAXED_DELAY_MAX_MS = exports.BUDGET_RELAXED_DELAY_MIN_MS = exports.BUDGET_RELAXED_USAGE_THRESHOLD = exports.BUDGET_CLEAR_USAGE_THRESHOLD = exports.BUDGET_DELAY_MAX_MS = exports.BUDGET_DELAY_MIN_MS = exports.BUDGET_THRESHOLD = exports.STREAM_BUDGET_COOLDOWN_KEY = exports.STREAM_RATE_LIMIT_METADATA_KEY_SUFFIX = 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 = exports.STREAM_SYNC_METHOD_NAMES = exports.STREAM_SYNC_CHANNEL_RESULT_METHOD_NAMES = exports.STREAM_METHOD_NAMES = exports.STREAM_CHANNEL_RESULT_METHOD_NAMES = exports.CHANNEL_SYNC_PROXY_RESULT_METHOD_NAMES = exports.CHANNEL_SYNC_METHOD_NAMES = exports.CHANNEL_METHOD_NAMES = void 0;
4
4
  var generatedMethodNames_1 = require("./generatedMethodNames");
5
5
  Object.defineProperty(exports, "CHANNEL_METHOD_NAMES", { enumerable: true, get: function () { return generatedMethodNames_1.CHANNEL_METHOD_NAMES; } });
6
6
  Object.defineProperty(exports, "CHANNEL_SYNC_METHOD_NAMES", { enumerable: true, get: function () { return generatedMethodNames_1.CHANNEL_SYNC_METHOD_NAMES; } });
@@ -17,6 +17,7 @@ exports.STREAM_RATE_LIMIT_COOLDOWN_KEY_PREFIX = 'stream-rate-limit-cooldown:';
17
17
  exports.STREAM_BUDGET_COOLDOWN_KEY_PREFIX = 'stream-budget-cooldown:';
18
18
  exports.STREAM_RATE_LIMIT_OPTIONS_MARKER = '__xbsRateLimitOptions';
19
19
  exports.STREAM_RATE_LIMIT_SOFT_COOLDOWN_KEY = 'stream-rate-limit-soft-global';
20
+ exports.STREAM_RATE_LIMIT_METADATA_KEY_SUFFIX = ':meta';
20
21
  exports.STREAM_BUDGET_COOLDOWN_KEY = 'stream-budget-global';
21
22
  exports.BUDGET_THRESHOLD = 0.8;
22
23
  exports.BUDGET_DELAY_MIN_MS = 30000;
@@ -72,6 +72,92 @@ class RedisCooldown {
72
72
  });
73
73
  }
74
74
  }
75
+ static async getValue(cooldownKey, context, options) {
76
+ const { redis, logger } = context;
77
+ try {
78
+ const value = await redis.get(RedisCooldown.resolveRedisKey(cooldownKey, options));
79
+ if (typeof value === 'string' && value.length > 0) {
80
+ return value;
81
+ }
82
+ }
83
+ catch (error) {
84
+ logger.warn('Failed to read stream rate-limit metadata from Redis', {
85
+ cooldownKey,
86
+ error,
87
+ });
88
+ }
89
+ }
90
+ static async setValueWithRemainingMs(cooldownKey, value, cooldownMs, context, options) {
91
+ if (value.length === 0 || cooldownMs <= 0) {
92
+ return;
93
+ }
94
+ const { redis, logger } = context;
95
+ try {
96
+ await redis.eval(`
97
+ local key = KEYS[1]
98
+ local value = ARGV[1]
99
+ local new_ttl = tonumber(ARGV[2])
100
+ local current_ttl = redis.call('PTTL', key)
101
+
102
+ if current_ttl == -2 then
103
+ redis.call('SET', key, value, 'PX', new_ttl)
104
+ return 1
105
+ end
106
+
107
+ if current_ttl == -1 or current_ttl < new_ttl then
108
+ redis.call('SET', key, value, 'PX', new_ttl)
109
+ return 2
110
+ end
111
+
112
+ redis.call('SET', key, value, 'KEEPTTL')
113
+ return 0
114
+ `, 1, RedisCooldown.resolveRedisKey(cooldownKey, options), value, cooldownMs.toString());
115
+ }
116
+ catch (error) {
117
+ logger.warn('Failed to write stream rate-limit metadata to Redis', {
118
+ cooldownKey,
119
+ cooldownMs,
120
+ error,
121
+ });
122
+ }
123
+ }
124
+ static async setRemainingMsWithValue(cooldownKey, valueKey, value, cooldownMs, context, options) {
125
+ if (value.length === 0 || cooldownMs <= 0) {
126
+ return;
127
+ }
128
+ const { redis, logger } = context;
129
+ const cooldownRedisKey = RedisCooldown.resolveRedisKey(cooldownKey, options);
130
+ const valueRedisKey = RedisCooldown.resolveRedisKey(valueKey, options);
131
+ try {
132
+ await redis.eval(`
133
+ local cooldown_key = KEYS[1]
134
+ local value_key = KEYS[2]
135
+ local value = ARGV[1]
136
+ local new_ttl = tonumber(ARGV[2])
137
+ local current_ttl = redis.call('PTTL', cooldown_key)
138
+ local final_ttl = new_ttl
139
+
140
+ if current_ttl == -2 then
141
+ redis.call('SET', cooldown_key, '1', 'PX', new_ttl)
142
+ elseif current_ttl == -1 or current_ttl < new_ttl then
143
+ redis.call('PEXPIRE', cooldown_key, new_ttl)
144
+ else
145
+ final_ttl = current_ttl
146
+ end
147
+
148
+ redis.call('SET', value_key, value, 'PX', final_ttl)
149
+ return final_ttl
150
+ `, 2, cooldownRedisKey, valueRedisKey, value, cooldownMs.toString());
151
+ }
152
+ catch (error) {
153
+ logger.warn('Failed to write stream rate-limit cooldown and metadata to Redis', {
154
+ cooldownKey,
155
+ valueKey,
156
+ cooldownMs,
157
+ error,
158
+ });
159
+ }
160
+ }
75
161
  static async clear(cooldownKey, context, options) {
76
162
  const { redis, logger } = context;
77
163
  try {
@@ -4,6 +4,7 @@ exports.executeWithRateLimitHandling = void 0;
4
4
  const promises_1 = require("node:timers/promises");
5
5
  const constants_1 = require("../constants");
6
6
  const createRateLimitExceededException_1 = require("./createRateLimitExceededException");
7
+ const rateLimitSnapshot_1 = require("./rateLimitSnapshot");
7
8
  const processStreamRateLimitException_1 = require("./processStreamRateLimitException");
8
9
  const RedisCooldown_1 = require("./RedisCooldown");
9
10
  function calculateRateLimitDelayMs(error, maxDelayMs) {
@@ -20,6 +21,34 @@ function calculateRateLimitDelayMs(error, maxDelayMs) {
20
21
  }
21
22
  return Math.min(maxDelayMs, constants_1.DEFAULT_MAX_DELAY_MS);
22
23
  }
24
+ function getRateLimitMetadataKey(operation) {
25
+ return `${operation}${constants_1.STREAM_RATE_LIMIT_METADATA_KEY_SUFFIX}`;
26
+ }
27
+ function toCooldownRateLimit(snapshot, cooldownMs) {
28
+ return {
29
+ limit: snapshot.limit,
30
+ remaining: snapshot.remaining,
31
+ reset: Math.max(snapshot.reset, Date.now() + cooldownMs),
32
+ };
33
+ }
34
+ function toRateLimitSnapshot(error) {
35
+ const limit = Number(error.rateLimit);
36
+ const remaining = error.rateLimitRemaining;
37
+ const resetMs = error.rateLimitReset;
38
+ if (!Number.isFinite(limit) ||
39
+ typeof remaining !== 'number' ||
40
+ !Number.isFinite(remaining) ||
41
+ typeof resetMs !== 'number' ||
42
+ !Number.isFinite(resetMs) ||
43
+ resetMs <= 0) {
44
+ return;
45
+ }
46
+ return {
47
+ limit,
48
+ remaining,
49
+ reset: Date.now() + resetMs,
50
+ };
51
+ }
23
52
  async function executeWithRateLimitHandling(execute, operation, context, options) {
24
53
  const { logger, redis } = context;
25
54
  const { enableCooldown, maxAttempts, maxDelayMs, maxRetryableDelayMs } = options;
@@ -27,11 +56,17 @@ async function executeWithRateLimitHandling(execute, operation, context, options
27
56
  if (enableCooldown) {
28
57
  const cooldownMs = await RedisCooldown_1.RedisCooldown.getRemainingMs(operation, { redis, logger });
29
58
  if (cooldownMs > 0) {
59
+ const rateLimitSnapshot = (0, rateLimitSnapshot_1.parseRateLimitSnapshot)(await RedisCooldown_1.RedisCooldown.getValue(getRateLimitMetadataKey(operation), { redis, logger }));
30
60
  logger.warn('Skipping stream request due to active rate-limit cooldown', {
31
61
  operation,
32
62
  cooldown: cooldownMs,
63
+ rateLimitSnapshot,
64
+ });
65
+ throw (0, createRateLimitExceededException_1.createRateLimitExceededException)({
66
+ operation,
67
+ retryAfterMs: cooldownMs,
68
+ rateLimit: rateLimitSnapshot ? toCooldownRateLimit(rateLimitSnapshot, cooldownMs) : undefined,
33
69
  });
34
- throw (0, createRateLimitExceededException_1.createRateLimitExceededException)({ operation, retryAfterMs: cooldownMs });
35
70
  }
36
71
  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 });
37
72
  if (softCooldownMs > 0) {
@@ -70,7 +105,13 @@ async function executeWithRateLimitHandling(execute, operation, context, options
70
105
  }
71
106
  const retryAfterMs = calculateRateLimitDelayMs(rateLimitError, maxDelayMs);
72
107
  if (enableCooldown) {
73
- await RedisCooldown_1.RedisCooldown.setRemainingMs(operation, retryAfterMs, { redis, logger });
108
+ const rateLimitSnapshot = toRateLimitSnapshot(rateLimitError);
109
+ if (rateLimitSnapshot) {
110
+ await RedisCooldown_1.RedisCooldown.setRemainingMsWithValue(operation, getRateLimitMetadataKey(operation), (0, rateLimitSnapshot_1.serializeRateLimitSnapshot)(rateLimitSnapshot), retryAfterMs, { redis, logger });
111
+ }
112
+ else {
113
+ await RedisCooldown_1.RedisCooldown.setRemainingMs(operation, retryAfterMs, { redis, logger });
114
+ }
74
115
  }
75
116
  if (attempt < maxAttempts && retryAfterMs <= maxRetryableDelayMs) {
76
117
  logger.warn('Retrying stream request after rate limit', {
@@ -45,7 +45,6 @@ function isStreamHttpRateLimitError(error) {
45
45
  function processStreamRateLimitException(error, logger) {
46
46
  if (isStreamHttpRateLimitError(error)) {
47
47
  const headers = error.response?.headers ?? {};
48
- logger.warn(`rate limit headers`, { headers });
49
48
  const rateLimit = getRateLimitFromHeaders(headers);
50
49
  const retryAfterMs = parseRetryAfterMs(headers);
51
50
  const message = error.response?.data?.message || error.message || 'Rate limit exceeded';
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseRateLimitSnapshot = exports.serializeRateLimitSnapshot = void 0;
4
+ const RATE_LIMIT_SNAPSHOT_PARTS_COUNT = 3;
5
+ function serializeRateLimitSnapshot(snapshot) {
6
+ return `${snapshot.limit},${snapshot.remaining},${snapshot.reset}`;
7
+ }
8
+ exports.serializeRateLimitSnapshot = serializeRateLimitSnapshot;
9
+ function parseRateLimitSnapshot(snapshot) {
10
+ if (typeof snapshot !== 'string' || snapshot.length === 0) {
11
+ return;
12
+ }
13
+ const parts = snapshot.split(',');
14
+ if (parts.length !== RATE_LIMIT_SNAPSHOT_PARTS_COUNT) {
15
+ return;
16
+ }
17
+ const [limitRaw, remainingRaw, resetRaw] = parts;
18
+ const limit = Number(limitRaw);
19
+ const remaining = Number(remainingRaw);
20
+ const reset = Number(resetRaw);
21
+ if (!Number.isFinite(limit) ||
22
+ !Number.isFinite(remaining) ||
23
+ !Number.isFinite(reset) ||
24
+ limit < 0 ||
25
+ remaining < 0 ||
26
+ reset <= 0) {
27
+ return;
28
+ }
29
+ return { limit, remaining, reset };
30
+ }
31
+ exports.parseRateLimitSnapshot = parseRateLimitSnapshot;
@@ -7,6 +7,7 @@ export const STREAM_RATE_LIMIT_COOLDOWN_KEY_PREFIX = 'stream-rate-limit-cooldown
7
7
  export const STREAM_BUDGET_COOLDOWN_KEY_PREFIX = 'stream-budget-cooldown:';
8
8
  export const STREAM_RATE_LIMIT_OPTIONS_MARKER = '__xbsRateLimitOptions';
9
9
  export const STREAM_RATE_LIMIT_SOFT_COOLDOWN_KEY = 'stream-rate-limit-soft-global';
10
+ export const STREAM_RATE_LIMIT_METADATA_KEY_SUFFIX = ':meta';
10
11
  export const STREAM_BUDGET_COOLDOWN_KEY = 'stream-budget-global';
11
12
  export const BUDGET_THRESHOLD = 0.8;
12
13
  export const BUDGET_DELAY_MIN_MS = 30000;
@@ -69,6 +69,92 @@ export class RedisCooldown {
69
69
  });
70
70
  }
71
71
  }
72
+ static async getValue(cooldownKey, context, options) {
73
+ const { redis, logger } = context;
74
+ try {
75
+ const value = await redis.get(RedisCooldown.resolveRedisKey(cooldownKey, options));
76
+ if (typeof value === 'string' && value.length > 0) {
77
+ return value;
78
+ }
79
+ }
80
+ catch (error) {
81
+ logger.warn('Failed to read stream rate-limit metadata from Redis', {
82
+ cooldownKey,
83
+ error,
84
+ });
85
+ }
86
+ }
87
+ static async setValueWithRemainingMs(cooldownKey, value, cooldownMs, context, options) {
88
+ if (value.length === 0 || cooldownMs <= 0) {
89
+ return;
90
+ }
91
+ const { redis, logger } = context;
92
+ try {
93
+ await redis.eval(`
94
+ local key = KEYS[1]
95
+ local value = ARGV[1]
96
+ local new_ttl = tonumber(ARGV[2])
97
+ local current_ttl = redis.call('PTTL', key)
98
+
99
+ if current_ttl == -2 then
100
+ redis.call('SET', key, value, 'PX', new_ttl)
101
+ return 1
102
+ end
103
+
104
+ if current_ttl == -1 or current_ttl < new_ttl then
105
+ redis.call('SET', key, value, 'PX', new_ttl)
106
+ return 2
107
+ end
108
+
109
+ redis.call('SET', key, value, 'KEEPTTL')
110
+ return 0
111
+ `, 1, RedisCooldown.resolveRedisKey(cooldownKey, options), value, cooldownMs.toString());
112
+ }
113
+ catch (error) {
114
+ logger.warn('Failed to write stream rate-limit metadata to Redis', {
115
+ cooldownKey,
116
+ cooldownMs,
117
+ error,
118
+ });
119
+ }
120
+ }
121
+ static async setRemainingMsWithValue(cooldownKey, valueKey, value, cooldownMs, context, options) {
122
+ if (value.length === 0 || cooldownMs <= 0) {
123
+ return;
124
+ }
125
+ const { redis, logger } = context;
126
+ const cooldownRedisKey = RedisCooldown.resolveRedisKey(cooldownKey, options);
127
+ const valueRedisKey = RedisCooldown.resolveRedisKey(valueKey, options);
128
+ try {
129
+ await redis.eval(`
130
+ local cooldown_key = KEYS[1]
131
+ local value_key = KEYS[2]
132
+ local value = ARGV[1]
133
+ local new_ttl = tonumber(ARGV[2])
134
+ local current_ttl = redis.call('PTTL', cooldown_key)
135
+ local final_ttl = new_ttl
136
+
137
+ if current_ttl == -2 then
138
+ redis.call('SET', cooldown_key, '1', 'PX', new_ttl)
139
+ elseif current_ttl == -1 or current_ttl < new_ttl then
140
+ redis.call('PEXPIRE', cooldown_key, new_ttl)
141
+ else
142
+ final_ttl = current_ttl
143
+ end
144
+
145
+ redis.call('SET', value_key, value, 'PX', final_ttl)
146
+ return final_ttl
147
+ `, 2, cooldownRedisKey, valueRedisKey, value, cooldownMs.toString());
148
+ }
149
+ catch (error) {
150
+ logger.warn('Failed to write stream rate-limit cooldown and metadata to Redis', {
151
+ cooldownKey,
152
+ valueKey,
153
+ cooldownMs,
154
+ error,
155
+ });
156
+ }
157
+ }
72
158
  static async clear(cooldownKey, context, options) {
73
159
  const { redis, logger } = context;
74
160
  try {
@@ -1,6 +1,7 @@
1
1
  import { setTimeout as sleep } from 'node:timers/promises';
2
- import { BUDGET_COOLDOWN_SLEEP_CHUNK_MS, BUDGET_DELAY_MAX_MS, 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';
2
+ import { BUDGET_COOLDOWN_SLEEP_CHUNK_MS, BUDGET_DELAY_MAX_MS, 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_METADATA_KEY_SUFFIX, STREAM_RATE_LIMIT_SOFT_COOLDOWN_KEY, } from '../constants';
3
3
  import { createRateLimitExceededException } from './createRateLimitExceededException';
4
+ import { parseRateLimitSnapshot, serializeRateLimitSnapshot } from './rateLimitSnapshot';
4
5
  import { processStreamRateLimitException } from './processStreamRateLimitException';
5
6
  import { RedisCooldown } from './RedisCooldown';
6
7
  function calculateRateLimitDelayMs(error, maxDelayMs) {
@@ -17,6 +18,34 @@ function calculateRateLimitDelayMs(error, maxDelayMs) {
17
18
  }
18
19
  return Math.min(maxDelayMs, DEFAULT_MAX_DELAY_MS);
19
20
  }
21
+ function getRateLimitMetadataKey(operation) {
22
+ return `${operation}${STREAM_RATE_LIMIT_METADATA_KEY_SUFFIX}`;
23
+ }
24
+ function toCooldownRateLimit(snapshot, cooldownMs) {
25
+ return {
26
+ limit: snapshot.limit,
27
+ remaining: snapshot.remaining,
28
+ reset: Math.max(snapshot.reset, Date.now() + cooldownMs),
29
+ };
30
+ }
31
+ function toRateLimitSnapshot(error) {
32
+ const limit = Number(error.rateLimit);
33
+ const remaining = error.rateLimitRemaining;
34
+ const resetMs = error.rateLimitReset;
35
+ if (!Number.isFinite(limit) ||
36
+ typeof remaining !== 'number' ||
37
+ !Number.isFinite(remaining) ||
38
+ typeof resetMs !== 'number' ||
39
+ !Number.isFinite(resetMs) ||
40
+ resetMs <= 0) {
41
+ return;
42
+ }
43
+ return {
44
+ limit,
45
+ remaining,
46
+ reset: Date.now() + resetMs,
47
+ };
48
+ }
20
49
  export async function executeWithRateLimitHandling(execute, operation, context, options) {
21
50
  const { logger, redis } = context;
22
51
  const { enableCooldown, maxAttempts, maxDelayMs, maxRetryableDelayMs } = options;
@@ -24,11 +53,17 @@ export async function executeWithRateLimitHandling(execute, operation, context,
24
53
  if (enableCooldown) {
25
54
  const cooldownMs = await RedisCooldown.getRemainingMs(operation, { redis, logger });
26
55
  if (cooldownMs > 0) {
56
+ const rateLimitSnapshot = parseRateLimitSnapshot(await RedisCooldown.getValue(getRateLimitMetadataKey(operation), { redis, logger }));
27
57
  logger.warn('Skipping stream request due to active rate-limit cooldown', {
28
58
  operation,
29
59
  cooldown: cooldownMs,
60
+ rateLimitSnapshot,
61
+ });
62
+ throw createRateLimitExceededException({
63
+ operation,
64
+ retryAfterMs: cooldownMs,
65
+ rateLimit: rateLimitSnapshot ? toCooldownRateLimit(rateLimitSnapshot, cooldownMs) : undefined,
30
66
  });
31
- throw createRateLimitExceededException({ operation, retryAfterMs: cooldownMs });
32
67
  }
33
68
  const softCooldownMs = await RedisCooldown.getRemainingMs(STREAM_RATE_LIMIT_SOFT_COOLDOWN_KEY, { redis, logger }, { keyPrefix: STREAM_RATE_LIMIT_COOLDOWN_KEY_PREFIX });
34
69
  if (softCooldownMs > 0) {
@@ -67,7 +102,13 @@ export async function executeWithRateLimitHandling(execute, operation, context,
67
102
  }
68
103
  const retryAfterMs = calculateRateLimitDelayMs(rateLimitError, maxDelayMs);
69
104
  if (enableCooldown) {
70
- await RedisCooldown.setRemainingMs(operation, retryAfterMs, { redis, logger });
105
+ const rateLimitSnapshot = toRateLimitSnapshot(rateLimitError);
106
+ if (rateLimitSnapshot) {
107
+ await RedisCooldown.setRemainingMsWithValue(operation, getRateLimitMetadataKey(operation), serializeRateLimitSnapshot(rateLimitSnapshot), retryAfterMs, { redis, logger });
108
+ }
109
+ else {
110
+ await RedisCooldown.setRemainingMs(operation, retryAfterMs, { redis, logger });
111
+ }
71
112
  }
72
113
  if (attempt < maxAttempts && retryAfterMs <= maxRetryableDelayMs) {
73
114
  logger.warn('Retrying stream request after rate limit', {
@@ -42,7 +42,6 @@ function isStreamHttpRateLimitError(error) {
42
42
  export function processStreamRateLimitException(error, logger) {
43
43
  if (isStreamHttpRateLimitError(error)) {
44
44
  const headers = error.response?.headers ?? {};
45
- logger.warn(`rate limit headers`, { headers });
46
45
  const rateLimit = getRateLimitFromHeaders(headers);
47
46
  const retryAfterMs = parseRetryAfterMs(headers);
48
47
  const message = error.response?.data?.message || error.message || 'Rate limit exceeded';
@@ -0,0 +1,26 @@
1
+ const RATE_LIMIT_SNAPSHOT_PARTS_COUNT = 3;
2
+ export function serializeRateLimitSnapshot(snapshot) {
3
+ return `${snapshot.limit},${snapshot.remaining},${snapshot.reset}`;
4
+ }
5
+ export function parseRateLimitSnapshot(snapshot) {
6
+ if (typeof snapshot !== 'string' || snapshot.length === 0) {
7
+ return;
8
+ }
9
+ const parts = snapshot.split(',');
10
+ if (parts.length !== RATE_LIMIT_SNAPSHOT_PARTS_COUNT) {
11
+ return;
12
+ }
13
+ const [limitRaw, remainingRaw, resetRaw] = parts;
14
+ const limit = Number(limitRaw);
15
+ const remaining = Number(remainingRaw);
16
+ const reset = Number(resetRaw);
17
+ if (!Number.isFinite(limit) ||
18
+ !Number.isFinite(remaining) ||
19
+ !Number.isFinite(reset) ||
20
+ limit < 0 ||
21
+ remaining < 0 ||
22
+ reset <= 0) {
23
+ return;
24
+ }
25
+ return { limit, remaining, reset };
26
+ }
@@ -8,6 +8,7 @@ export declare const STREAM_RATE_LIMIT_COOLDOWN_KEY_PREFIX = "stream-rate-limit-
8
8
  export declare const STREAM_BUDGET_COOLDOWN_KEY_PREFIX = "stream-budget-cooldown:";
9
9
  export declare const STREAM_RATE_LIMIT_OPTIONS_MARKER = "__xbsRateLimitOptions";
10
10
  export declare const STREAM_RATE_LIMIT_SOFT_COOLDOWN_KEY = "stream-rate-limit-soft-global";
11
+ export declare const STREAM_RATE_LIMIT_METADATA_KEY_SUFFIX = ":meta";
11
12
  export declare const STREAM_BUDGET_COOLDOWN_KEY = "stream-budget-global";
12
13
  export declare const BUDGET_THRESHOLD = 0.8;
13
14
  export declare const BUDGET_DELAY_MIN_MS = 30000;
@@ -4,5 +4,8 @@ export declare class RedisCooldown {
4
4
  static getRemainingMs(cooldownKey: string, context: RedisContext & LoggerContext, options?: RedisCooldownOptions): Promise<number>;
5
5
  static setRemainingMs(cooldownKey: string, cooldownMs: number, context: RedisContext & LoggerContext, options?: RedisCooldownOptions): Promise<void>;
6
6
  static setRemainingMsExact(cooldownKey: string, cooldownMs: number, context: RedisContext & LoggerContext, options?: RedisCooldownOptions): Promise<void>;
7
+ static getValue(cooldownKey: string, context: RedisContext & LoggerContext, options?: RedisCooldownOptions): Promise<string | undefined>;
8
+ static setValueWithRemainingMs(cooldownKey: string, value: string, cooldownMs: number, context: RedisContext & LoggerContext, options?: RedisCooldownOptions): Promise<void>;
9
+ static setRemainingMsWithValue(cooldownKey: string, valueKey: string, value: string, cooldownMs: number, context: RedisContext & LoggerContext, options?: RedisCooldownOptions): Promise<void>;
7
10
  static clear(cooldownKey: string, context: RedisContext & LoggerContext, options?: RedisCooldownOptions): Promise<void>;
8
11
  }
@@ -0,0 +1,3 @@
1
+ import { StreamRateLimitSnapshot } from '../types';
2
+ export declare function serializeRateLimitSnapshot(snapshot: StreamRateLimitSnapshot): string;
3
+ export declare function parseRateLimitSnapshot(snapshot: string | null | undefined): StreamRateLimitSnapshot | undefined;
@@ -36,6 +36,11 @@ export type StreamBudgetHeaders = {
36
36
  remainingMs?: number;
37
37
  usedMs?: number;
38
38
  };
39
+ export type StreamRateLimitSnapshot = {
40
+ limit: number;
41
+ remaining: number;
42
+ reset: number;
43
+ };
39
44
  /** Axios response shape used by budget interceptor. */
40
45
  export type StreamBudgetInterceptorResponse = {
41
46
  headers?: StreamHeaders;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wildix/xbees-conversations-utils",
3
- "version": "1.1.78",
3
+ "version": "1.1.80",
4
4
  "description": "",
5
5
  "main": "./dist-cjs/index.js",
6
6
  "module": "./dist-es/index.js",