@wildix/xbees-conversations-utils 1.1.85 → 1.1.86

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 (56) hide show
  1. package/README.md +118 -1
  2. package/dist-cjs/index.js +1 -1
  3. package/dist-cjs/streamRateLimitHandlingProxy/constants.js +43 -0
  4. package/dist-cjs/streamRateLimitHandlingProxy/createRateLimitedStreamProxy.js +92 -0
  5. package/dist-cjs/streamRateLimitHandlingProxy/generatedMethodNames.js +320 -0
  6. package/dist-cjs/streamRateLimitHandlingProxy/helpers/RedisCooldown.js +174 -0
  7. package/dist-cjs/streamRateLimitHandlingProxy/helpers/createRateLimitExceededException.js +32 -0
  8. package/dist-cjs/streamRateLimitHandlingProxy/helpers/executeWithRateLimitHandling.js +144 -0
  9. package/dist-cjs/streamRateLimitHandlingProxy/helpers/headerUtils.js +40 -0
  10. package/dist-cjs/streamRateLimitHandlingProxy/helpers/isStreamChannelLike.js +23 -0
  11. package/dist-cjs/streamRateLimitHandlingProxy/helpers/processStreamBudgetHeaders.js +93 -0
  12. package/dist-cjs/streamRateLimitHandlingProxy/helpers/processStreamRateLimitException.js +63 -0
  13. package/dist-cjs/streamRateLimitHandlingProxy/helpers/processStreamRateLimitHeaders.js +72 -0
  14. package/dist-cjs/streamRateLimitHandlingProxy/helpers/rateLimitSnapshot.js +31 -0
  15. package/dist-cjs/streamRateLimitHandlingProxy/helpers/registerStreamInterceptors.js +58 -0
  16. package/dist-cjs/streamRateLimitHandlingProxy/helpers/splitCallArgsAndOptions.js +21 -0
  17. package/dist-cjs/streamRateLimitHandlingProxy/index.js +5 -0
  18. package/dist-cjs/streamRateLimitHandlingProxy/types.js +3 -0
  19. package/dist-cjs/streamRateLimitHandlingProxy/withStreamRateLimitOptions.js +10 -0
  20. package/dist-es/index.js +1 -1
  21. package/dist-es/streamRateLimitHandlingProxy/constants.js +33 -0
  22. package/dist-es/streamRateLimitHandlingProxy/createRateLimitedStreamProxy.js +88 -0
  23. package/dist-es/streamRateLimitHandlingProxy/generatedMethodNames.js +317 -0
  24. package/dist-es/streamRateLimitHandlingProxy/helpers/RedisCooldown.js +170 -0
  25. package/dist-es/streamRateLimitHandlingProxy/helpers/createRateLimitExceededException.js +28 -0
  26. package/dist-es/streamRateLimitHandlingProxy/helpers/executeWithRateLimitHandling.js +140 -0
  27. package/dist-es/streamRateLimitHandlingProxy/helpers/headerUtils.js +34 -0
  28. package/dist-es/streamRateLimitHandlingProxy/helpers/isStreamChannelLike.js +19 -0
  29. package/dist-es/streamRateLimitHandlingProxy/helpers/processStreamBudgetHeaders.js +89 -0
  30. package/dist-es/streamRateLimitHandlingProxy/helpers/processStreamRateLimitException.js +59 -0
  31. package/dist-es/streamRateLimitHandlingProxy/helpers/processStreamRateLimitHeaders.js +68 -0
  32. package/dist-es/streamRateLimitHandlingProxy/helpers/rateLimitSnapshot.js +26 -0
  33. package/dist-es/streamRateLimitHandlingProxy/helpers/registerStreamInterceptors.js +54 -0
  34. package/dist-es/streamRateLimitHandlingProxy/helpers/splitCallArgsAndOptions.js +17 -0
  35. package/dist-es/streamRateLimitHandlingProxy/index.js +2 -0
  36. package/dist-es/streamRateLimitHandlingProxy/types.js +1 -0
  37. package/dist-es/streamRateLimitHandlingProxy/withStreamRateLimitOptions.js +6 -0
  38. package/dist-types/index.d.ts +1 -1
  39. package/dist-types/streamRateLimitHandlingProxy/constants.d.ts +29 -0
  40. package/dist-types/streamRateLimitHandlingProxy/createRateLimitedStreamProxy.d.ts +11 -0
  41. package/dist-types/streamRateLimitHandlingProxy/generatedMethodNames.d.ts +11 -0
  42. package/dist-types/streamRateLimitHandlingProxy/helpers/RedisCooldown.d.ts +11 -0
  43. package/dist-types/streamRateLimitHandlingProxy/helpers/createRateLimitExceededException.d.ts +10 -0
  44. package/dist-types/streamRateLimitHandlingProxy/helpers/executeWithRateLimitHandling.d.ts +2 -0
  45. package/dist-types/streamRateLimitHandlingProxy/helpers/headerUtils.d.ts +4 -0
  46. package/dist-types/streamRateLimitHandlingProxy/helpers/isStreamChannelLike.d.ts +6 -0
  47. package/dist-types/streamRateLimitHandlingProxy/helpers/processStreamBudgetHeaders.d.ts +9 -0
  48. package/dist-types/streamRateLimitHandlingProxy/helpers/processStreamRateLimitException.d.ts +3 -0
  49. package/dist-types/streamRateLimitHandlingProxy/helpers/processStreamRateLimitHeaders.d.ts +6 -0
  50. package/dist-types/streamRateLimitHandlingProxy/helpers/rateLimitSnapshot.d.ts +3 -0
  51. package/dist-types/streamRateLimitHandlingProxy/helpers/registerStreamInterceptors.d.ts +2 -0
  52. package/dist-types/streamRateLimitHandlingProxy/helpers/splitCallArgsAndOptions.d.ts +6 -0
  53. package/dist-types/streamRateLimitHandlingProxy/index.d.ts +2 -0
  54. package/dist-types/streamRateLimitHandlingProxy/types.d.ts +68 -0
  55. package/dist-types/streamRateLimitHandlingProxy/withStreamRateLimitOptions.d.ts +6 -0
  56. package/package.json +1 -1
@@ -0,0 +1,174 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RedisCooldown = void 0;
4
+ const constants_1 = require("../constants");
5
+ class RedisCooldown {
6
+ static resolveRedisKey(cooldownKey, options) {
7
+ const keyPrefix = options?.keyPrefix ?? constants_1.STREAM_RATE_LIMIT_COOLDOWN_KEY_PREFIX;
8
+ return `${keyPrefix}${cooldownKey}`;
9
+ }
10
+ static async getRemainingMs(cooldownKey, context, options) {
11
+ const { redis, logger } = context;
12
+ try {
13
+ const ttlMs = await redis.pttl(RedisCooldown.resolveRedisKey(cooldownKey, options));
14
+ if (ttlMs > 0) {
15
+ return ttlMs;
16
+ }
17
+ }
18
+ catch (error) {
19
+ logger.warn('Failed to read stream rate-limit cooldown from Redis', {
20
+ cooldownKey,
21
+ error,
22
+ });
23
+ }
24
+ return 0;
25
+ }
26
+ static async setRemainingMs(cooldownKey, cooldownMs, context, options) {
27
+ if (cooldownMs <= 0) {
28
+ return;
29
+ }
30
+ const { redis, logger } = context;
31
+ const redisKey = RedisCooldown.resolveRedisKey(cooldownKey, options);
32
+ try {
33
+ await redis.eval(`
34
+ local key = KEYS[1]
35
+ local new_ttl = tonumber(ARGV[1])
36
+ local current_ttl = redis.call('PTTL', key)
37
+
38
+ if current_ttl == -2 then
39
+ redis.call('SET', key, '1', 'PX', new_ttl)
40
+ return 1
41
+ end
42
+
43
+ if current_ttl == -1 or current_ttl < new_ttl then
44
+ redis.call('PEXPIRE', key, new_ttl)
45
+ return 2
46
+ end
47
+
48
+ return 0
49
+ `, 1, redisKey, cooldownMs.toString());
50
+ }
51
+ catch (error) {
52
+ logger.warn('Failed to write stream rate-limit cooldown to Redis', {
53
+ cooldownKey,
54
+ cooldownMs,
55
+ error,
56
+ });
57
+ }
58
+ }
59
+ static async setRemainingMsExact(cooldownKey, cooldownMs, context, options) {
60
+ if (cooldownMs <= 0) {
61
+ return;
62
+ }
63
+ const { redis, logger } = context;
64
+ try {
65
+ await redis.set(RedisCooldown.resolveRedisKey(cooldownKey, options), '1', 'PX', cooldownMs);
66
+ }
67
+ catch (error) {
68
+ logger.warn('Failed to write exact stream rate-limit cooldown to Redis', {
69
+ cooldownKey,
70
+ cooldownMs,
71
+ error,
72
+ });
73
+ }
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
+ }
161
+ static async clear(cooldownKey, context, options) {
162
+ const { redis, logger } = context;
163
+ try {
164
+ await redis.del(RedisCooldown.resolveRedisKey(cooldownKey, options));
165
+ }
166
+ catch (error) {
167
+ logger.warn('Failed to clear stream rate-limit cooldown from Redis', {
168
+ cooldownKey,
169
+ error,
170
+ });
171
+ }
172
+ }
173
+ }
174
+ exports.RedisCooldown = RedisCooldown;
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createRateLimitExceededException = void 0;
4
+ const xbees_conversations_client_1 = require("@wildix/xbees-conversations-client");
5
+ const constants_1 = require("../constants");
6
+ const RETRY_AFTER_HEADER_PADDING_MS = 3000;
7
+ function toPositiveMilliseconds(milliseconds) {
8
+ return Math.max(0, Math.ceil(milliseconds));
9
+ }
10
+ function toEpochSecondsFromNow(delayMs) {
11
+ return Math.ceil((Date.now() + toPositiveMilliseconds(delayMs)) / 1000);
12
+ }
13
+ function createRateLimitExceededException(options = {}) {
14
+ const { rateLimit, message, operation, retryAfterMs = constants_1.DEFAULT_RETRY_AFTER_MS } = options;
15
+ const defaultMessage = operation ? `Rate limit exceeded for ${operation}` : 'Rate limit exceeded';
16
+ const retryAfterFromResetMs = rateLimit?.reset ? rateLimit.reset - Date.now() : undefined;
17
+ const effectiveRetryAfterMs = retryAfterFromResetMs ?? retryAfterMs;
18
+ const retryAfterMilliseconds = toPositiveMilliseconds(effectiveRetryAfterMs);
19
+ const rateLimitResetMilliseconds = rateLimit?.reset
20
+ ? toPositiveMilliseconds(retryAfterFromResetMs ?? 0)
21
+ : retryAfterMilliseconds;
22
+ const retryAfterEpochSeconds = toEpochSecondsFromNow(effectiveRetryAfterMs + RETRY_AFTER_HEADER_PADDING_MS);
23
+ return new xbees_conversations_client_1.RateLimitExceededException({
24
+ $metadata: {},
25
+ message: message || defaultMessage,
26
+ rateLimitRemaining: rateLimit?.remaining || 0,
27
+ retryAfter: retryAfterEpochSeconds,
28
+ rateLimit: rateLimit?.limit?.toString() || 'unknown',
29
+ rateLimitReset: rateLimitResetMilliseconds,
30
+ });
31
+ }
32
+ exports.createRateLimitExceededException = createRateLimitExceededException;
@@ -0,0 +1,144 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.executeWithRateLimitHandling = void 0;
4
+ const promises_1 = require("node:timers/promises");
5
+ const constants_1 = require("../constants");
6
+ const createRateLimitExceededException_1 = require("./createRateLimitExceededException");
7
+ const processStreamRateLimitException_1 = require("./processStreamRateLimitException");
8
+ const rateLimitSnapshot_1 = require("./rateLimitSnapshot");
9
+ const RedisCooldown_1 = require("./RedisCooldown");
10
+ function calculateRateLimitDelayMs(error, maxDelayMs) {
11
+ const retryAfterMs = typeof error.rateLimitReset === 'number' && error.rateLimitReset > 0 ? error.rateLimitReset : 0;
12
+ const hasServerRetryAfter = retryAfterMs > 0;
13
+ const baseDelayMs = hasServerRetryAfter ? retryAfterMs + constants_1.RETRY_AFTER_BUFFER_MS : constants_1.DEFAULT_RETRY_AFTER_MS;
14
+ if (baseDelayMs > 0) {
15
+ const jitterMs = Math.floor(Math.random() * constants_1.DEFAULT_JITTER_MS);
16
+ const computedDelayMs = baseDelayMs + jitterMs;
17
+ if (hasServerRetryAfter) {
18
+ return computedDelayMs;
19
+ }
20
+ return Math.min(maxDelayMs, computedDelayMs);
21
+ }
22
+ return Math.min(maxDelayMs, constants_1.DEFAULT_MAX_DELAY_MS);
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
+ }
52
+ async function executeWithRateLimitHandling(execute, operation, context, options) {
53
+ const { logger, redis } = context;
54
+ const { enableCooldown, maxAttempts, maxDelayMs, maxRetryableDelayMs } = options;
55
+ let attempt = 1;
56
+ if (enableCooldown) {
57
+ const cooldownMs = await RedisCooldown_1.RedisCooldown.getRemainingMs(operation, { redis, logger });
58
+ if (cooldownMs > 0) {
59
+ const rateLimitSnapshot = (0, rateLimitSnapshot_1.parseRateLimitSnapshot)(await RedisCooldown_1.RedisCooldown.getValue(getRateLimitMetadataKey(operation), { redis, logger }));
60
+ logger.warn('Skipping stream request due to active rate-limit cooldown', {
61
+ operation,
62
+ cooldown: cooldownMs,
63
+ rateLimitSnapshot,
64
+ });
65
+ throw (0, createRateLimitExceededException_1.createRateLimitExceededException)({
66
+ operation,
67
+ retryAfterMs: cooldownMs,
68
+ rateLimit: rateLimitSnapshot ? toCooldownRateLimit(rateLimitSnapshot, cooldownMs) : undefined,
69
+ });
70
+ }
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 });
72
+ if (softCooldownMs > 0) {
73
+ logger.debug('Delaying stream request due to active soft rate-limit cooldown', {
74
+ operation,
75
+ cooldown: softCooldownMs,
76
+ });
77
+ await (0, promises_1.setTimeout)(softCooldownMs);
78
+ }
79
+ const budgetCooldownMs = await RedisCooldown_1.RedisCooldown.getRemainingMs(constants_1.STREAM_BUDGET_COOLDOWN_KEY, { redis, logger }, { keyPrefix: constants_1.STREAM_BUDGET_COOLDOWN_KEY_PREFIX });
80
+ if (budgetCooldownMs > 0) {
81
+ logger.warn('Delaying stream request due to active budget cooldown', {
82
+ operation,
83
+ cooldown: budgetCooldownMs,
84
+ });
85
+ let totalBudgetWaitMs = 0;
86
+ for (;;) {
87
+ const waitMs = await RedisCooldown_1.RedisCooldown.getRemainingMs(constants_1.STREAM_BUDGET_COOLDOWN_KEY, { redis, logger }, { keyPrefix: constants_1.STREAM_BUDGET_COOLDOWN_KEY_PREFIX });
88
+ if (waitMs <= 0 || totalBudgetWaitMs >= constants_1.BUDGET_DELAY_MAX_MS) {
89
+ break;
90
+ }
91
+ const chunkMs = Math.min(waitMs, constants_1.BUDGET_COOLDOWN_SLEEP_CHUNK_MS);
92
+ await (0, promises_1.setTimeout)(chunkMs);
93
+ totalBudgetWaitMs += chunkMs;
94
+ }
95
+ }
96
+ }
97
+ for (;;) {
98
+ try {
99
+ return await execute();
100
+ }
101
+ catch (error) {
102
+ const rateLimitError = (0, processStreamRateLimitException_1.processStreamRateLimitException)(error, logger);
103
+ if (!rateLimitError) {
104
+ throw error;
105
+ }
106
+ const retryAfterMs = calculateRateLimitDelayMs(rateLimitError, maxDelayMs);
107
+ if (enableCooldown) {
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
+ }
115
+ }
116
+ if (attempt < maxAttempts && retryAfterMs <= maxRetryableDelayMs) {
117
+ logger.warn('Retrying stream request after rate limit', {
118
+ operation,
119
+ attempt,
120
+ maxAttempts,
121
+ retryAfterMs,
122
+ rateLimit: rateLimitError.rateLimit,
123
+ rateLimitRemaining: rateLimitError.rateLimitRemaining,
124
+ rateLimitReset: rateLimitError.rateLimitReset,
125
+ });
126
+ await (0, promises_1.setTimeout)(retryAfterMs);
127
+ attempt++;
128
+ continue;
129
+ }
130
+ logger.warn('Propagating stream rate limit w/o retry', {
131
+ operation,
132
+ attempt,
133
+ maxAttempts,
134
+ retryAfterMs,
135
+ maxRetryableDelayMs,
136
+ rateLimit: rateLimitError.rateLimit,
137
+ rateLimitRemaining: rateLimitError.rateLimitRemaining,
138
+ rateLimitReset: rateLimitError.rateLimitReset,
139
+ });
140
+ throw rateLimitError;
141
+ }
142
+ }
143
+ }
144
+ exports.executeWithRateLimitHandling = executeWithRateLimitHandling;
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizeResetToEpochMs = exports.parseHeaderNumber = exports.getHeaderCaseInsensitive = void 0;
4
+ function getHeaderCaseInsensitive(headers, key) {
5
+ if (!headers) {
6
+ return;
7
+ }
8
+ const direct = headers[key];
9
+ if (direct !== undefined) {
10
+ return direct;
11
+ }
12
+ const normalizedKey = key.toLowerCase();
13
+ for (const [headerKey, headerValue] of Object.entries(headers)) {
14
+ if (headerKey.toLowerCase() === normalizedKey) {
15
+ return headerValue;
16
+ }
17
+ }
18
+ }
19
+ exports.getHeaderCaseInsensitive = getHeaderCaseInsensitive;
20
+ function parseHeaderNumber(headers, key) {
21
+ const raw = getHeaderCaseInsensitive(headers, key);
22
+ if (typeof raw === 'number' && Number.isFinite(raw)) {
23
+ return raw;
24
+ }
25
+ if (typeof raw === 'string') {
26
+ const parsed = Number(raw);
27
+ if (Number.isFinite(parsed)) {
28
+ return parsed;
29
+ }
30
+ }
31
+ }
32
+ exports.parseHeaderNumber = parseHeaderNumber;
33
+ function normalizeResetToEpochMs(reset) {
34
+ if (reset < 10000)
35
+ return Date.now() + Math.round(reset * 1000);
36
+ if (reset < 10000000000)
37
+ return Math.round(reset * 1000);
38
+ return Math.round(reset);
39
+ }
40
+ exports.normalizeResetToEpochMs = normalizeResetToEpochMs;
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isStreamChannelLike = void 0;
4
+ const stream_chat_1 = require("stream-chat");
5
+ function hasFunctionProperty(value, key) {
6
+ return key in value && typeof value[key] === 'function';
7
+ }
8
+ function hasStringProperty(value, key) {
9
+ return key in value && typeof value[key] === 'string';
10
+ }
11
+ function isStreamChannelLike(value) {
12
+ if (typeof value !== 'object' || value === null) {
13
+ return false;
14
+ }
15
+ if (value instanceof stream_chat_1.Channel) {
16
+ return true;
17
+ }
18
+ return (hasStringProperty(value, 'cid') &&
19
+ hasFunctionProperty(value, 'sendMessage') &&
20
+ hasFunctionProperty(value, 'queryMembers') &&
21
+ hasFunctionProperty(value, 'updatePartial'));
22
+ }
23
+ exports.isStreamChannelLike = isStreamChannelLike;
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.processStreamBudgetHeaders = void 0;
4
+ const constants_1 = require("../constants");
5
+ const headerUtils_1 = require("./headerUtils");
6
+ const RedisCooldown_1 = require("./RedisCooldown");
7
+ function extractBudgetHeaders(headers) {
8
+ return {
9
+ limitMs: (0, headerUtils_1.parseHeaderNumber)(headers ?? {}, 'x-budget-limit-ms'),
10
+ remainingMs: (0, headerUtils_1.parseHeaderNumber)(headers ?? {}, 'x-budget-remaining-ms'),
11
+ usedMs: (0, headerUtils_1.parseHeaderNumber)(headers ?? {}, 'x-budget-used-ms'),
12
+ };
13
+ }
14
+ function isBudgetThresholdReached(limitMs, remainingMs, usedMs) {
15
+ if (limitMs <= 0) {
16
+ return false;
17
+ }
18
+ if (typeof usedMs === 'number' && usedMs / limitMs >= constants_1.BUDGET_THRESHOLD) {
19
+ return true;
20
+ }
21
+ if (typeof remainingMs === 'number') {
22
+ const remainingThreshold = Math.ceil((1 - constants_1.BUDGET_THRESHOLD) * limitMs);
23
+ return remainingMs <= remainingThreshold;
24
+ }
25
+ return false;
26
+ }
27
+ function getRandomDelayInRange(minMs, maxMs) {
28
+ return Math.round(minMs + Math.random() * (maxMs - minMs));
29
+ }
30
+ function getBudgetUsage(limitMs, remainingMs, usedMs) {
31
+ if (limitMs <= 0) {
32
+ return 0;
33
+ }
34
+ if (typeof usedMs === 'number' && Number.isFinite(usedMs) && usedMs >= 0) {
35
+ return usedMs / limitMs;
36
+ }
37
+ if (typeof remainingMs === 'number' && Number.isFinite(remainingMs)) {
38
+ return Math.max(0, 1 - remainingMs / limitMs);
39
+ }
40
+ return 0;
41
+ }
42
+ async function processStreamBudgetHeaders(headers, context) {
43
+ const { logger } = context;
44
+ const { limitMs, remainingMs, usedMs } = extractBudgetHeaders(headers);
45
+ if (limitMs === undefined && remainingMs === undefined && usedMs === undefined) {
46
+ return;
47
+ }
48
+ if (typeof limitMs !== 'number' || !Number.isFinite(limitMs)) {
49
+ return;
50
+ }
51
+ const usage = getBudgetUsage(limitMs, remainingMs, usedMs);
52
+ const thresholdReached = isBudgetThresholdReached(limitMs, remainingMs, usedMs);
53
+ if (thresholdReached) {
54
+ const delayMs = getRandomDelayInRange(constants_1.BUDGET_DELAY_MIN_MS, constants_1.BUDGET_DELAY_MAX_MS);
55
+ await RedisCooldown_1.RedisCooldown.setRemainingMsExact(constants_1.STREAM_BUDGET_COOLDOWN_KEY, delayMs, context, {
56
+ keyPrefix: constants_1.STREAM_BUDGET_COOLDOWN_KEY_PREFIX,
57
+ });
58
+ logger.warn('Stream budget threshold reached, enabling cooldown', {
59
+ budgetLimitMs: limitMs,
60
+ budgetRemainingMs: remainingMs,
61
+ budgetUsedMs: usedMs,
62
+ budgetUsage: usage,
63
+ cooldownMs: delayMs,
64
+ });
65
+ return;
66
+ }
67
+ if (usage < constants_1.BUDGET_CLEAR_USAGE_THRESHOLD) {
68
+ await RedisCooldown_1.RedisCooldown.clear(constants_1.STREAM_BUDGET_COOLDOWN_KEY, context, {
69
+ keyPrefix: constants_1.STREAM_BUDGET_COOLDOWN_KEY_PREFIX,
70
+ });
71
+ logger.info('Stream budget is low, clearing cooldown', {
72
+ budgetLimitMs: limitMs,
73
+ budgetRemainingMs: remainingMs,
74
+ budgetUsedMs: usedMs,
75
+ budgetUsage: usage,
76
+ });
77
+ return;
78
+ }
79
+ const delayMs = usage >= constants_1.BUDGET_RELAXED_USAGE_THRESHOLD
80
+ ? getRandomDelayInRange(constants_1.BUDGET_RELAXED_DELAY_MIN_MS, constants_1.BUDGET_RELAXED_DELAY_MAX_MS)
81
+ : getRandomDelayInRange(constants_1.BUDGET_MINIMAL_DELAY_MIN_MS, constants_1.BUDGET_MINIMAL_DELAY_MAX_MS);
82
+ await RedisCooldown_1.RedisCooldown.setRemainingMsExact(constants_1.STREAM_BUDGET_COOLDOWN_KEY, delayMs, context, {
83
+ keyPrefix: constants_1.STREAM_BUDGET_COOLDOWN_KEY_PREFIX,
84
+ });
85
+ logger.info('Stream budget is below threshold, reducing cooldown', {
86
+ budgetLimitMs: limitMs,
87
+ budgetRemainingMs: remainingMs,
88
+ budgetUsedMs: usedMs,
89
+ budgetUsage: usage,
90
+ cooldownMs: delayMs,
91
+ });
92
+ }
93
+ exports.processStreamBudgetHeaders = processStreamBudgetHeaders;
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.processStreamRateLimitException = void 0;
4
+ const createRateLimitExceededException_1 = require("./createRateLimitExceededException");
5
+ const headerUtils_1 = require("./headerUtils");
6
+ function parseRetryAfterMs(headers) {
7
+ const retryAfter = (0, headerUtils_1.getHeaderCaseInsensitive)(headers, 'retry-after');
8
+ if (typeof retryAfter === 'number' && Number.isFinite(retryAfter) && retryAfter >= 0) {
9
+ return Math.round(retryAfter * 1000);
10
+ }
11
+ if (typeof retryAfter !== 'string') {
12
+ return;
13
+ }
14
+ const asSeconds = Number(retryAfter);
15
+ if (Number.isFinite(asSeconds) && asSeconds >= 0) {
16
+ return Math.round(asSeconds * 1000);
17
+ }
18
+ const asDateMs = Date.parse(retryAfter);
19
+ if (!Number.isNaN(asDateMs)) {
20
+ return Math.max(0, asDateMs - Date.now());
21
+ }
22
+ }
23
+ function getRateLimitFromHeaders(headers) {
24
+ const limit = (0, headerUtils_1.parseHeaderNumber)(headers, 'x-ratelimit-limit');
25
+ const remaining = (0, headerUtils_1.parseHeaderNumber)(headers, 'x-ratelimit-remaining');
26
+ const resetRaw = (0, headerUtils_1.parseHeaderNumber)(headers, 'x-ratelimit-reset');
27
+ if (limit === undefined || remaining === undefined || resetRaw === undefined) {
28
+ return;
29
+ }
30
+ return {
31
+ limit,
32
+ remaining,
33
+ reset: (0, headerUtils_1.normalizeResetToEpochMs)(resetRaw),
34
+ };
35
+ }
36
+ function isStreamHttpRateLimitError(error) {
37
+ return (typeof error === 'object' &&
38
+ error !== null &&
39
+ (('status' in error && error.status === 429) ||
40
+ ('response' in error &&
41
+ typeof error.response === 'object' &&
42
+ error.response !== null &&
43
+ error.response?.status === 429)));
44
+ }
45
+ function processStreamRateLimitException(error, logger) {
46
+ if (isStreamHttpRateLimitError(error)) {
47
+ const headers = error.response?.headers ?? {};
48
+ const rateLimit = getRateLimitFromHeaders(headers);
49
+ const retryAfterMs = parseRetryAfterMs(headers);
50
+ const message = error.response?.data?.message || error.message || 'Rate limit exceeded';
51
+ logger.warn('Stream rate limit exceeded [HTTP 429]', {
52
+ status: error.status ?? error.response?.status,
53
+ retryAfterMs,
54
+ rateLimit,
55
+ });
56
+ return (0, createRateLimitExceededException_1.createRateLimitExceededException)({
57
+ rateLimit,
58
+ retryAfterMs,
59
+ message,
60
+ });
61
+ }
62
+ }
63
+ exports.processStreamRateLimitException = processStreamRateLimitException;
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.processStreamRateLimitHeaders = void 0;
4
+ const constants_1 = require("../constants");
5
+ const headerUtils_1 = require("./headerUtils");
6
+ const RedisCooldown_1 = require("./RedisCooldown");
7
+ function computeSoftThrottleUsage(limit, remaining) {
8
+ if (limit <= 0) {
9
+ return 0;
10
+ }
11
+ return Math.max(0, 1 - remaining / limit);
12
+ }
13
+ function toUsagePercent(usage) {
14
+ return Math.round(usage * 10000) / 100;
15
+ }
16
+ function computeSoftThrottleDelayMs(limit, remaining) {
17
+ const usage = computeSoftThrottleUsage(limit, remaining);
18
+ if (usage >= constants_1.SOFT_THROTTLE_USAGE_HIGH) {
19
+ return constants_1.SOFT_THROTTLE_DELAY_HIGH_MS;
20
+ }
21
+ if (usage >= constants_1.SOFT_THROTTLE_USAGE_MEDIUM) {
22
+ return constants_1.SOFT_THROTTLE_DELAY_MEDIUM_MS;
23
+ }
24
+ if (usage >= constants_1.SOFT_THROTTLE_USAGE_LOW) {
25
+ return constants_1.SOFT_THROTTLE_DELAY_LOW_MS;
26
+ }
27
+ return 0;
28
+ }
29
+ async function processStreamRateLimitHeaders(headers, context) {
30
+ const { logger } = context;
31
+ const limit = (0, headerUtils_1.parseHeaderNumber)(headers ?? {}, 'x-ratelimit-limit');
32
+ const remaining = (0, headerUtils_1.parseHeaderNumber)(headers ?? {}, 'x-ratelimit-remaining');
33
+ if (limit === undefined || remaining === undefined) {
34
+ return;
35
+ }
36
+ if (limit <= 0 || remaining < 0) {
37
+ return;
38
+ }
39
+ const usage = computeSoftThrottleUsage(limit, remaining);
40
+ const usagePercent = toUsagePercent(usage);
41
+ const delayMs = computeSoftThrottleDelayMs(limit, remaining);
42
+ const currentCooldownMs = await RedisCooldown_1.RedisCooldown.getRemainingMs(constants_1.STREAM_RATE_LIMIT_SOFT_COOLDOWN_KEY, context, {
43
+ keyPrefix: constants_1.STREAM_RATE_LIMIT_COOLDOWN_KEY_PREFIX,
44
+ });
45
+ if (delayMs > 0) {
46
+ await RedisCooldown_1.RedisCooldown.setRemainingMs(constants_1.STREAM_RATE_LIMIT_SOFT_COOLDOWN_KEY, delayMs, context, {
47
+ keyPrefix: constants_1.STREAM_RATE_LIMIT_COOLDOWN_KEY_PREFIX,
48
+ });
49
+ logger.debug(currentCooldownMs > 0 ? 'Updating soft stream rate-limit cooldown' : 'Enabling soft stream rate-limit cooldown', {
50
+ rateLimit: limit,
51
+ rateLimitRemaining: remaining,
52
+ rateLimitUsage: usage,
53
+ rateLimitUsagePercent: usagePercent,
54
+ cooldownMs: delayMs,
55
+ previousCooldownMs: currentCooldownMs || undefined,
56
+ });
57
+ return;
58
+ }
59
+ if (currentCooldownMs > 0) {
60
+ await RedisCooldown_1.RedisCooldown.clear(constants_1.STREAM_RATE_LIMIT_SOFT_COOLDOWN_KEY, context, {
61
+ keyPrefix: constants_1.STREAM_RATE_LIMIT_COOLDOWN_KEY_PREFIX,
62
+ });
63
+ logger.debug('Clearing soft stream rate-limit cooldown', {
64
+ rateLimit: limit,
65
+ rateLimitRemaining: remaining,
66
+ rateLimitUsage: usage,
67
+ rateLimitUsagePercent: usagePercent,
68
+ previousCooldownMs: currentCooldownMs,
69
+ });
70
+ }
71
+ }
72
+ exports.processStreamRateLimitHeaders = processStreamRateLimitHeaders;
@@ -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;