@wildix/xbees-conversations-utils 1.1.92 → 1.2.0
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.
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/streamRateLimitHandlingProxy/constants.js +1 -2
- package/dist-cjs/streamRateLimitHandlingProxy/helpers/createRateLimitExceededException.js +2 -5
- package/dist-cjs/streamRateLimitHandlingProxy/helpers/executeWithRateLimitHandling.js +31 -23
- package/dist-cjs/streamRateLimitHandlingProxy/helpers/headerUtils.js +12 -4
- package/dist-cjs/streamRateLimitHandlingProxy/helpers/processStreamBudgetHeaders.js +2 -2
- package/dist-cjs/streamRateLimitHandlingProxy/helpers/processStreamRateLimitHeaders.js +0 -3
- package/dist-cjs/streamRateLimitHandlingProxy/helpers/registerStreamInterceptors.js +25 -19
- package/dist-es/index.js +1 -1
- package/dist-es/streamRateLimitHandlingProxy/constants.js +0 -1
- package/dist-es/streamRateLimitHandlingProxy/helpers/createRateLimitExceededException.js +2 -5
- package/dist-es/streamRateLimitHandlingProxy/helpers/executeWithRateLimitHandling.js +32 -24
- package/dist-es/streamRateLimitHandlingProxy/helpers/headerUtils.js +12 -4
- package/dist-es/streamRateLimitHandlingProxy/helpers/processStreamBudgetHeaders.js +2 -2
- package/dist-es/streamRateLimitHandlingProxy/helpers/processStreamRateLimitHeaders.js +0 -3
- package/dist-es/streamRateLimitHandlingProxy/helpers/registerStreamInterceptors.js +25 -19
- package/dist-types/index.d.ts +1 -1
- package/dist-types/streamRateLimitHandlingProxy/constants.d.ts +0 -1
- package/package.json +1 -1
package/dist-cjs/index.js
CHANGED
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const tslib_1 = require("tslib");
|
|
4
4
|
tslib_1.__exportStar(require("./normalization"), exports);
|
|
5
|
-
tslib_1.__exportStar(require("./streamRateLimitHandlingProxy"), exports);
|
|
6
5
|
tslib_1.__exportStar(require("./stream"), exports);
|
|
6
|
+
tslib_1.__exportStar(require("./streamRateLimitHandlingProxy"), exports);
|
|
7
7
|
tslib_1.__exportStar(require("./types"), exports);
|
|
@@ -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_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.
|
|
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.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; } });
|
|
@@ -9,7 +9,6 @@ Object.defineProperty(exports, "STREAM_CHANNEL_RESULT_METHOD_NAMES", { enumerabl
|
|
|
9
9
|
Object.defineProperty(exports, "STREAM_METHOD_NAMES", { enumerable: true, get: function () { return generatedMethodNames_1.STREAM_METHOD_NAMES; } });
|
|
10
10
|
Object.defineProperty(exports, "STREAM_SYNC_CHANNEL_RESULT_METHOD_NAMES", { enumerable: true, get: function () { return generatedMethodNames_1.STREAM_SYNC_CHANNEL_RESULT_METHOD_NAMES; } });
|
|
11
11
|
Object.defineProperty(exports, "STREAM_SYNC_METHOD_NAMES", { enumerable: true, get: function () { return generatedMethodNames_1.STREAM_SYNC_METHOD_NAMES; } });
|
|
12
|
-
exports.DEFAULT_MAX_DELAY_MS = 5000;
|
|
13
12
|
exports.DEFAULT_RETRY_AFTER_MS = 10000;
|
|
14
13
|
exports.DEFAULT_JITTER_MS = 150;
|
|
15
14
|
exports.RETRY_AFTER_BUFFER_MS = 1000;
|
|
@@ -13,12 +13,9 @@ function toEpochSecondsFromNow(delayMs) {
|
|
|
13
13
|
function createRateLimitExceededException(options = {}) {
|
|
14
14
|
const { rateLimit, message, operation, retryAfterMs = constants_1.DEFAULT_RETRY_AFTER_MS } = options;
|
|
15
15
|
const defaultMessage = operation ? `Rate limit exceeded for ${operation}` : 'Rate limit exceeded';
|
|
16
|
-
const retryAfterFromResetMs = rateLimit?.reset ? rateLimit.reset - Date.now() : undefined;
|
|
16
|
+
const retryAfterFromResetMs = rateLimit?.reset != null ? rateLimit.reset - Date.now() : undefined;
|
|
17
17
|
const effectiveRetryAfterMs = retryAfterFromResetMs ?? retryAfterMs;
|
|
18
18
|
const retryAfterMilliseconds = toPositiveMilliseconds(effectiveRetryAfterMs);
|
|
19
|
-
const rateLimitResetMilliseconds = rateLimit?.reset
|
|
20
|
-
? toPositiveMilliseconds(retryAfterFromResetMs ?? 0)
|
|
21
|
-
: retryAfterMilliseconds;
|
|
22
19
|
const retryAfterEpochSeconds = toEpochSecondsFromNow(effectiveRetryAfterMs + RETRY_AFTER_HEADER_PADDING_MS);
|
|
23
20
|
return new xbees_conversations_client_1.RateLimitExceededException({
|
|
24
21
|
$metadata: {},
|
|
@@ -26,7 +23,7 @@ function createRateLimitExceededException(options = {}) {
|
|
|
26
23
|
rateLimitRemaining: rateLimit?.remaining || 0,
|
|
27
24
|
retryAfter: retryAfterEpochSeconds,
|
|
28
25
|
rateLimit: rateLimit?.limit?.toString() || 'unknown',
|
|
29
|
-
rateLimitReset:
|
|
26
|
+
rateLimitReset: retryAfterMilliseconds,
|
|
30
27
|
});
|
|
31
28
|
}
|
|
32
29
|
exports.createRateLimitExceededException = createRateLimitExceededException;
|
|
@@ -11,15 +11,12 @@ function calculateRateLimitDelayMs(error, maxDelayMs) {
|
|
|
11
11
|
const retryAfterMs = typeof error.rateLimitReset === 'number' && error.rateLimitReset > 0 ? error.rateLimitReset : 0;
|
|
12
12
|
const hasServerRetryAfter = retryAfterMs > 0;
|
|
13
13
|
const baseDelayMs = hasServerRetryAfter ? retryAfterMs + constants_1.RETRY_AFTER_BUFFER_MS : constants_1.DEFAULT_RETRY_AFTER_MS;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return computedDelayMs;
|
|
19
|
-
}
|
|
20
|
-
return Math.min(maxDelayMs, computedDelayMs);
|
|
14
|
+
const jitterMs = Math.floor(Math.random() * constants_1.DEFAULT_JITTER_MS);
|
|
15
|
+
const computedDelayMs = baseDelayMs + jitterMs;
|
|
16
|
+
if (hasServerRetryAfter) {
|
|
17
|
+
return computedDelayMs;
|
|
21
18
|
}
|
|
22
|
-
return Math.min(maxDelayMs,
|
|
19
|
+
return Math.min(maxDelayMs, computedDelayMs);
|
|
23
20
|
}
|
|
24
21
|
function getRateLimitMetadataKey(operation) {
|
|
25
22
|
return `${operation}${constants_1.STREAM_RATE_LIMIT_METADATA_KEY_SUFFIX}`;
|
|
@@ -49,25 +46,31 @@ function toRateLimitSnapshot(error) {
|
|
|
49
46
|
reset: Date.now() + resetMs,
|
|
50
47
|
};
|
|
51
48
|
}
|
|
49
|
+
async function throwIfOperationCooldownActive(operation, context) {
|
|
50
|
+
const { logger, redis } = context;
|
|
51
|
+
const cooldownMs = await RedisCooldown_1.RedisCooldown.getRemainingMs(operation, { redis, logger });
|
|
52
|
+
if (cooldownMs <= 0) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const rateLimitSnapshot = (0, rateLimitSnapshot_1.parseRateLimitSnapshot)(await RedisCooldown_1.RedisCooldown.getValue(getRateLimitMetadataKey(operation), { redis, logger }));
|
|
56
|
+
logger.warn('Skipping stream request due to active rate-limit cooldown', {
|
|
57
|
+
operation,
|
|
58
|
+
cooldown: cooldownMs,
|
|
59
|
+
rateLimitSnapshot,
|
|
60
|
+
});
|
|
61
|
+
throw (0, createRateLimitExceededException_1.createRateLimitExceededException)({
|
|
62
|
+
operation,
|
|
63
|
+
retryAfterMs: cooldownMs,
|
|
64
|
+
rateLimit: rateLimitSnapshot ? toCooldownRateLimit(rateLimitSnapshot, cooldownMs) : undefined,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
52
67
|
async function executeWithRateLimitHandling(execute, operation, context, options) {
|
|
53
68
|
const { logger, redis } = context;
|
|
54
69
|
const { enableCooldown, maxAttempts, maxDelayMs, maxRetryableDelayMs } = options;
|
|
55
70
|
let attempt = 1;
|
|
56
71
|
if (enableCooldown) {
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
}
|
|
72
|
+
await throwIfOperationCooldownActive(operation, { logger, redis });
|
|
73
|
+
let waitedForSharedCooldown = false;
|
|
71
74
|
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
75
|
if (softCooldownMs > 0) {
|
|
73
76
|
logger.info('Delaying stream request due to active soft rate-limit cooldown', {
|
|
@@ -75,6 +78,7 @@ async function executeWithRateLimitHandling(execute, operation, context, options
|
|
|
75
78
|
cooldown: softCooldownMs,
|
|
76
79
|
});
|
|
77
80
|
await (0, promises_1.setTimeout)(softCooldownMs);
|
|
81
|
+
waitedForSharedCooldown = true;
|
|
78
82
|
}
|
|
79
83
|
const budgetCooldownMs = await RedisCooldown_1.RedisCooldown.getRemainingMs(constants_1.STREAM_BUDGET_COOLDOWN_KEY, { redis, logger }, { keyPrefix: constants_1.STREAM_BUDGET_COOLDOWN_KEY_PREFIX });
|
|
80
84
|
if (budgetCooldownMs > 0) {
|
|
@@ -88,11 +92,15 @@ async function executeWithRateLimitHandling(execute, operation, context, options
|
|
|
88
92
|
if (waitMs <= 0 || totalBudgetWaitMs >= constants_1.BUDGET_DELAY_MAX_MS) {
|
|
89
93
|
break;
|
|
90
94
|
}
|
|
91
|
-
const chunkMs = Math.min(waitMs, constants_1.BUDGET_COOLDOWN_SLEEP_CHUNK_MS);
|
|
95
|
+
const chunkMs = Math.min(waitMs, constants_1.BUDGET_COOLDOWN_SLEEP_CHUNK_MS, constants_1.BUDGET_DELAY_MAX_MS - totalBudgetWaitMs);
|
|
92
96
|
await (0, promises_1.setTimeout)(chunkMs);
|
|
93
97
|
totalBudgetWaitMs += chunkMs;
|
|
98
|
+
waitedForSharedCooldown = true;
|
|
94
99
|
}
|
|
95
100
|
}
|
|
101
|
+
if (waitedForSharedCooldown) {
|
|
102
|
+
await throwIfOperationCooldownActive(operation, { logger, redis });
|
|
103
|
+
}
|
|
96
104
|
}
|
|
97
105
|
for (;;) {
|
|
98
106
|
try {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.normalizeResetToEpochMs = exports.parseHeaderNumber = exports.getHeaderCaseInsensitive = void 0;
|
|
4
|
+
const EPOCH_SECONDS_PAST_SKEW_SECONDS = 60;
|
|
5
|
+
const EPOCH_SECONDS_FUTURE_SKEW_SECONDS = 24 * 60 * 60;
|
|
4
6
|
function getHeaderCaseInsensitive(headers, key) {
|
|
5
7
|
if (!headers) {
|
|
6
8
|
return;
|
|
@@ -31,10 +33,16 @@ function parseHeaderNumber(headers, key) {
|
|
|
31
33
|
}
|
|
32
34
|
exports.parseHeaderNumber = parseHeaderNumber;
|
|
33
35
|
function normalizeResetToEpochMs(reset) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
const nowMs = Date.now();
|
|
37
|
+
if (!Number.isFinite(reset)) {
|
|
38
|
+
return nowMs;
|
|
39
|
+
}
|
|
40
|
+
if (reset >= 10000000000)
|
|
41
|
+
return Math.round(reset);
|
|
42
|
+
const nowSeconds = nowMs / 1000;
|
|
43
|
+
const looksLikeEpochSeconds = reset >= nowSeconds - EPOCH_SECONDS_PAST_SKEW_SECONDS && reset <= nowSeconds + EPOCH_SECONDS_FUTURE_SKEW_SECONDS;
|
|
44
|
+
if (looksLikeEpochSeconds)
|
|
37
45
|
return Math.round(reset * 1000);
|
|
38
|
-
return Math.round(reset);
|
|
46
|
+
return nowMs + Math.round(reset * 1000);
|
|
39
47
|
}
|
|
40
48
|
exports.normalizeResetToEpochMs = normalizeResetToEpochMs;
|
|
@@ -19,7 +19,7 @@ function isBudgetThresholdReached(limitMs, remainingMs, usedMs) {
|
|
|
19
19
|
return true;
|
|
20
20
|
}
|
|
21
21
|
if (typeof remainingMs === 'number') {
|
|
22
|
-
const remainingThreshold =
|
|
22
|
+
const remainingThreshold = (1 - constants_1.BUDGET_THRESHOLD) * limitMs;
|
|
23
23
|
return remainingMs <= remainingThreshold;
|
|
24
24
|
}
|
|
25
25
|
return false;
|
|
@@ -45,7 +45,7 @@ async function processStreamBudgetHeaders(headers, context) {
|
|
|
45
45
|
if (limitMs === undefined && remainingMs === undefined && usedMs === undefined) {
|
|
46
46
|
return;
|
|
47
47
|
}
|
|
48
|
-
if (typeof limitMs !== 'number' || !Number.isFinite(limitMs)) {
|
|
48
|
+
if (typeof limitMs !== 'number' || !Number.isFinite(limitMs) || limitMs <= 0) {
|
|
49
49
|
return;
|
|
50
50
|
}
|
|
51
51
|
const usage = getBudgetUsage(limitMs, remainingMs, usedMs);
|
|
@@ -10,9 +10,6 @@ function computeSoftThrottleUsage(limit, remaining) {
|
|
|
10
10
|
}
|
|
11
11
|
return Math.max(0, 1 - remaining / limit);
|
|
12
12
|
}
|
|
13
|
-
function toUsagePercent(usage) {
|
|
14
|
-
return Math.round(usage * 10000) / 100;
|
|
15
|
-
}
|
|
16
13
|
function computeSoftThrottleDelayMs(limit, remaining) {
|
|
17
14
|
const usage = computeSoftThrottleUsage(limit, remaining);
|
|
18
15
|
if (usage >= constants_1.SOFT_THROTTLE_USAGE_HIGH) {
|
|
@@ -35,24 +35,30 @@ function registerStreamInterceptors(stream, context) {
|
|
|
35
35
|
return;
|
|
36
36
|
}
|
|
37
37
|
attachedInterceptors.add(axiosInstance);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
38
|
+
try {
|
|
39
|
+
axiosInstance.interceptors.response.use(async (response) => {
|
|
40
|
+
try {
|
|
41
|
+
await (0, processStreamBudgetHeaders_1.processStreamBudgetHeaders)(response?.headers, context);
|
|
42
|
+
await (0, processStreamRateLimitHeaders_1.processStreamRateLimitHeaders)(response?.headers, context);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
logger.warn('Failed to process Stream response headers in interceptor', { error });
|
|
46
|
+
}
|
|
47
|
+
return response;
|
|
48
|
+
}, async (error) => {
|
|
49
|
+
try {
|
|
50
|
+
await (0, processStreamBudgetHeaders_1.processStreamBudgetHeaders)(error?.response?.headers, context);
|
|
51
|
+
await (0, processStreamRateLimitHeaders_1.processStreamRateLimitHeaders)(error?.response?.headers, context);
|
|
52
|
+
}
|
|
53
|
+
catch (interceptorError) {
|
|
54
|
+
logger.warn('Failed to process Stream error headers in interceptor', { error: interceptorError });
|
|
55
|
+
}
|
|
56
|
+
return Promise.reject(error);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
attachedInterceptors.delete(axiosInstance);
|
|
61
|
+
logger.warn('Failed to register Stream response interceptors', { error });
|
|
62
|
+
}
|
|
57
63
|
}
|
|
58
64
|
exports.registerStreamInterceptors = registerStreamInterceptors;
|
package/dist-es/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export { CHANNEL_METHOD_NAMES, CHANNEL_SYNC_METHOD_NAMES, CHANNEL_SYNC_PROXY_RESULT_METHOD_NAMES, STREAM_CHANNEL_RESULT_METHOD_NAMES, STREAM_METHOD_NAMES, STREAM_SYNC_CHANNEL_RESULT_METHOD_NAMES, STREAM_SYNC_METHOD_NAMES, } from './generatedMethodNames';
|
|
2
|
-
export const DEFAULT_MAX_DELAY_MS = 5000;
|
|
3
2
|
export const DEFAULT_RETRY_AFTER_MS = 10000;
|
|
4
3
|
export const DEFAULT_JITTER_MS = 150;
|
|
5
4
|
export const RETRY_AFTER_BUFFER_MS = 1000;
|
|
@@ -10,12 +10,9 @@ function toEpochSecondsFromNow(delayMs) {
|
|
|
10
10
|
export function createRateLimitExceededException(options = {}) {
|
|
11
11
|
const { rateLimit, message, operation, retryAfterMs = DEFAULT_RETRY_AFTER_MS } = options;
|
|
12
12
|
const defaultMessage = operation ? `Rate limit exceeded for ${operation}` : 'Rate limit exceeded';
|
|
13
|
-
const retryAfterFromResetMs = rateLimit?.reset ? rateLimit.reset - Date.now() : undefined;
|
|
13
|
+
const retryAfterFromResetMs = rateLimit?.reset != null ? rateLimit.reset - Date.now() : undefined;
|
|
14
14
|
const effectiveRetryAfterMs = retryAfterFromResetMs ?? retryAfterMs;
|
|
15
15
|
const retryAfterMilliseconds = toPositiveMilliseconds(effectiveRetryAfterMs);
|
|
16
|
-
const rateLimitResetMilliseconds = rateLimit?.reset
|
|
17
|
-
? toPositiveMilliseconds(retryAfterFromResetMs ?? 0)
|
|
18
|
-
: retryAfterMilliseconds;
|
|
19
16
|
const retryAfterEpochSeconds = toEpochSecondsFromNow(effectiveRetryAfterMs + RETRY_AFTER_HEADER_PADDING_MS);
|
|
20
17
|
return new RateLimitExceededException({
|
|
21
18
|
$metadata: {},
|
|
@@ -23,6 +20,6 @@ export function createRateLimitExceededException(options = {}) {
|
|
|
23
20
|
rateLimitRemaining: rateLimit?.remaining || 0,
|
|
24
21
|
retryAfter: retryAfterEpochSeconds,
|
|
25
22
|
rateLimit: rateLimit?.limit?.toString() || 'unknown',
|
|
26
|
-
rateLimitReset:
|
|
23
|
+
rateLimitReset: retryAfterMilliseconds,
|
|
27
24
|
});
|
|
28
25
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
2
|
-
import { BUDGET_COOLDOWN_SLEEP_CHUNK_MS, BUDGET_DELAY_MAX_MS, DEFAULT_JITTER_MS,
|
|
2
|
+
import { BUDGET_COOLDOWN_SLEEP_CHUNK_MS, BUDGET_DELAY_MAX_MS, DEFAULT_JITTER_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
4
|
import { processStreamRateLimitException } from './processStreamRateLimitException';
|
|
5
5
|
import { parseRateLimitSnapshot, serializeRateLimitSnapshot } from './rateLimitSnapshot';
|
|
@@ -8,15 +8,12 @@ function calculateRateLimitDelayMs(error, maxDelayMs) {
|
|
|
8
8
|
const retryAfterMs = typeof error.rateLimitReset === 'number' && error.rateLimitReset > 0 ? error.rateLimitReset : 0;
|
|
9
9
|
const hasServerRetryAfter = retryAfterMs > 0;
|
|
10
10
|
const baseDelayMs = hasServerRetryAfter ? retryAfterMs + RETRY_AFTER_BUFFER_MS : DEFAULT_RETRY_AFTER_MS;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return computedDelayMs;
|
|
16
|
-
}
|
|
17
|
-
return Math.min(maxDelayMs, computedDelayMs);
|
|
11
|
+
const jitterMs = Math.floor(Math.random() * DEFAULT_JITTER_MS);
|
|
12
|
+
const computedDelayMs = baseDelayMs + jitterMs;
|
|
13
|
+
if (hasServerRetryAfter) {
|
|
14
|
+
return computedDelayMs;
|
|
18
15
|
}
|
|
19
|
-
return Math.min(maxDelayMs,
|
|
16
|
+
return Math.min(maxDelayMs, computedDelayMs);
|
|
20
17
|
}
|
|
21
18
|
function getRateLimitMetadataKey(operation) {
|
|
22
19
|
return `${operation}${STREAM_RATE_LIMIT_METADATA_KEY_SUFFIX}`;
|
|
@@ -46,25 +43,31 @@ function toRateLimitSnapshot(error) {
|
|
|
46
43
|
reset: Date.now() + resetMs,
|
|
47
44
|
};
|
|
48
45
|
}
|
|
46
|
+
async function throwIfOperationCooldownActive(operation, context) {
|
|
47
|
+
const { logger, redis } = context;
|
|
48
|
+
const cooldownMs = await RedisCooldown.getRemainingMs(operation, { redis, logger });
|
|
49
|
+
if (cooldownMs <= 0) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const rateLimitSnapshot = parseRateLimitSnapshot(await RedisCooldown.getValue(getRateLimitMetadataKey(operation), { redis, logger }));
|
|
53
|
+
logger.warn('Skipping stream request due to active rate-limit cooldown', {
|
|
54
|
+
operation,
|
|
55
|
+
cooldown: cooldownMs,
|
|
56
|
+
rateLimitSnapshot,
|
|
57
|
+
});
|
|
58
|
+
throw createRateLimitExceededException({
|
|
59
|
+
operation,
|
|
60
|
+
retryAfterMs: cooldownMs,
|
|
61
|
+
rateLimit: rateLimitSnapshot ? toCooldownRateLimit(rateLimitSnapshot, cooldownMs) : undefined,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
49
64
|
export async function executeWithRateLimitHandling(execute, operation, context, options) {
|
|
50
65
|
const { logger, redis } = context;
|
|
51
66
|
const { enableCooldown, maxAttempts, maxDelayMs, maxRetryableDelayMs } = options;
|
|
52
67
|
let attempt = 1;
|
|
53
68
|
if (enableCooldown) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const rateLimitSnapshot = parseRateLimitSnapshot(await RedisCooldown.getValue(getRateLimitMetadataKey(operation), { redis, logger }));
|
|
57
|
-
logger.warn('Skipping stream request due to active rate-limit cooldown', {
|
|
58
|
-
operation,
|
|
59
|
-
cooldown: cooldownMs,
|
|
60
|
-
rateLimitSnapshot,
|
|
61
|
-
});
|
|
62
|
-
throw createRateLimitExceededException({
|
|
63
|
-
operation,
|
|
64
|
-
retryAfterMs: cooldownMs,
|
|
65
|
-
rateLimit: rateLimitSnapshot ? toCooldownRateLimit(rateLimitSnapshot, cooldownMs) : undefined,
|
|
66
|
-
});
|
|
67
|
-
}
|
|
69
|
+
await throwIfOperationCooldownActive(operation, { logger, redis });
|
|
70
|
+
let waitedForSharedCooldown = false;
|
|
68
71
|
const softCooldownMs = await RedisCooldown.getRemainingMs(STREAM_RATE_LIMIT_SOFT_COOLDOWN_KEY, { redis, logger }, { keyPrefix: STREAM_RATE_LIMIT_COOLDOWN_KEY_PREFIX });
|
|
69
72
|
if (softCooldownMs > 0) {
|
|
70
73
|
logger.info('Delaying stream request due to active soft rate-limit cooldown', {
|
|
@@ -72,6 +75,7 @@ export async function executeWithRateLimitHandling(execute, operation, context,
|
|
|
72
75
|
cooldown: softCooldownMs,
|
|
73
76
|
});
|
|
74
77
|
await sleep(softCooldownMs);
|
|
78
|
+
waitedForSharedCooldown = true;
|
|
75
79
|
}
|
|
76
80
|
const budgetCooldownMs = await RedisCooldown.getRemainingMs(STREAM_BUDGET_COOLDOWN_KEY, { redis, logger }, { keyPrefix: STREAM_BUDGET_COOLDOWN_KEY_PREFIX });
|
|
77
81
|
if (budgetCooldownMs > 0) {
|
|
@@ -85,11 +89,15 @@ export async function executeWithRateLimitHandling(execute, operation, context,
|
|
|
85
89
|
if (waitMs <= 0 || totalBudgetWaitMs >= BUDGET_DELAY_MAX_MS) {
|
|
86
90
|
break;
|
|
87
91
|
}
|
|
88
|
-
const chunkMs = Math.min(waitMs, BUDGET_COOLDOWN_SLEEP_CHUNK_MS);
|
|
92
|
+
const chunkMs = Math.min(waitMs, BUDGET_COOLDOWN_SLEEP_CHUNK_MS, BUDGET_DELAY_MAX_MS - totalBudgetWaitMs);
|
|
89
93
|
await sleep(chunkMs);
|
|
90
94
|
totalBudgetWaitMs += chunkMs;
|
|
95
|
+
waitedForSharedCooldown = true;
|
|
91
96
|
}
|
|
92
97
|
}
|
|
98
|
+
if (waitedForSharedCooldown) {
|
|
99
|
+
await throwIfOperationCooldownActive(operation, { logger, redis });
|
|
100
|
+
}
|
|
93
101
|
}
|
|
94
102
|
for (;;) {
|
|
95
103
|
try {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const EPOCH_SECONDS_PAST_SKEW_SECONDS = 60;
|
|
2
|
+
const EPOCH_SECONDS_FUTURE_SKEW_SECONDS = 24 * 60 * 60;
|
|
1
3
|
export function getHeaderCaseInsensitive(headers, key) {
|
|
2
4
|
if (!headers) {
|
|
3
5
|
return;
|
|
@@ -26,9 +28,15 @@ export function parseHeaderNumber(headers, key) {
|
|
|
26
28
|
}
|
|
27
29
|
}
|
|
28
30
|
export function normalizeResetToEpochMs(reset) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
const nowMs = Date.now();
|
|
32
|
+
if (!Number.isFinite(reset)) {
|
|
33
|
+
return nowMs;
|
|
34
|
+
}
|
|
35
|
+
if (reset >= 10000000000)
|
|
36
|
+
return Math.round(reset);
|
|
37
|
+
const nowSeconds = nowMs / 1000;
|
|
38
|
+
const looksLikeEpochSeconds = reset >= nowSeconds - EPOCH_SECONDS_PAST_SKEW_SECONDS && reset <= nowSeconds + EPOCH_SECONDS_FUTURE_SKEW_SECONDS;
|
|
39
|
+
if (looksLikeEpochSeconds)
|
|
32
40
|
return Math.round(reset * 1000);
|
|
33
|
-
return Math.round(reset);
|
|
41
|
+
return nowMs + Math.round(reset * 1000);
|
|
34
42
|
}
|
|
@@ -16,7 +16,7 @@ function isBudgetThresholdReached(limitMs, remainingMs, usedMs) {
|
|
|
16
16
|
return true;
|
|
17
17
|
}
|
|
18
18
|
if (typeof remainingMs === 'number') {
|
|
19
|
-
const remainingThreshold =
|
|
19
|
+
const remainingThreshold = (1 - BUDGET_THRESHOLD) * limitMs;
|
|
20
20
|
return remainingMs <= remainingThreshold;
|
|
21
21
|
}
|
|
22
22
|
return false;
|
|
@@ -42,7 +42,7 @@ export async function processStreamBudgetHeaders(headers, context) {
|
|
|
42
42
|
if (limitMs === undefined && remainingMs === undefined && usedMs === undefined) {
|
|
43
43
|
return;
|
|
44
44
|
}
|
|
45
|
-
if (typeof limitMs !== 'number' || !Number.isFinite(limitMs)) {
|
|
45
|
+
if (typeof limitMs !== 'number' || !Number.isFinite(limitMs) || limitMs <= 0) {
|
|
46
46
|
return;
|
|
47
47
|
}
|
|
48
48
|
const usage = getBudgetUsage(limitMs, remainingMs, usedMs);
|
|
@@ -7,9 +7,6 @@ function computeSoftThrottleUsage(limit, remaining) {
|
|
|
7
7
|
}
|
|
8
8
|
return Math.max(0, 1 - remaining / limit);
|
|
9
9
|
}
|
|
10
|
-
function toUsagePercent(usage) {
|
|
11
|
-
return Math.round(usage * 10000) / 100;
|
|
12
|
-
}
|
|
13
10
|
function computeSoftThrottleDelayMs(limit, remaining) {
|
|
14
11
|
const usage = computeSoftThrottleUsage(limit, remaining);
|
|
15
12
|
if (usage >= SOFT_THROTTLE_USAGE_HIGH) {
|
|
@@ -32,23 +32,29 @@ export function registerStreamInterceptors(stream, context) {
|
|
|
32
32
|
return;
|
|
33
33
|
}
|
|
34
34
|
attachedInterceptors.add(axiosInstance);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
35
|
+
try {
|
|
36
|
+
axiosInstance.interceptors.response.use(async (response) => {
|
|
37
|
+
try {
|
|
38
|
+
await processStreamBudgetHeaders(response?.headers, context);
|
|
39
|
+
await processStreamRateLimitHeaders(response?.headers, context);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
logger.warn('Failed to process Stream response headers in interceptor', { error });
|
|
43
|
+
}
|
|
44
|
+
return response;
|
|
45
|
+
}, async (error) => {
|
|
46
|
+
try {
|
|
47
|
+
await processStreamBudgetHeaders(error?.response?.headers, context);
|
|
48
|
+
await processStreamRateLimitHeaders(error?.response?.headers, context);
|
|
49
|
+
}
|
|
50
|
+
catch (interceptorError) {
|
|
51
|
+
logger.warn('Failed to process Stream error headers in interceptor', { error: interceptorError });
|
|
52
|
+
}
|
|
53
|
+
return Promise.reject(error);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
attachedInterceptors.delete(axiosInstance);
|
|
58
|
+
logger.warn('Failed to register Stream response interceptors', { error });
|
|
59
|
+
}
|
|
54
60
|
}
|
package/dist-types/index.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { StreamRateLimitRetryOptions } from './types';
|
|
2
2
|
export { CHANNEL_METHOD_NAMES, CHANNEL_SYNC_METHOD_NAMES, CHANNEL_SYNC_PROXY_RESULT_METHOD_NAMES, STREAM_CHANNEL_RESULT_METHOD_NAMES, STREAM_METHOD_NAMES, STREAM_SYNC_CHANNEL_RESULT_METHOD_NAMES, STREAM_SYNC_METHOD_NAMES, } from './generatedMethodNames';
|
|
3
|
-
export declare const DEFAULT_MAX_DELAY_MS = 5000;
|
|
4
3
|
export declare const DEFAULT_RETRY_AFTER_MS = 10000;
|
|
5
4
|
export declare const DEFAULT_JITTER_MS = 150;
|
|
6
5
|
export declare const RETRY_AFTER_BUFFER_MS = 1000;
|