@wildix/xbees-conversations-utils 1.1.93 → 1.2.1
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/normalization.js +0 -2
- package/dist-es/normalization.js +0 -2
- package/dist-types/types.d.ts +0 -1
- package/package.json +1 -1
- package/dist-cjs/rateLimits/RedisCooldown.js +0 -66
- package/dist-cjs/rateLimits/constants.js +0 -43
- package/dist-cjs/rateLimits/createRateLimitedStreamProxy.js +0 -92
- package/dist-cjs/rateLimits/exception/createRateLimitExceededException.js +0 -21
- package/dist-cjs/rateLimits/exception/processStreamRateLimitException.js +0 -47
- package/dist-cjs/rateLimits/executeStreamRequestWithRateLimitHandling.js +0 -94
- package/dist-cjs/rateLimits/generatedMethodNames.js +0 -320
- package/dist-cjs/rateLimits/helpers/RedisCooldown.js +0 -174
- package/dist-cjs/rateLimits/helpers/createRateLimitExceededException.js +0 -32
- package/dist-cjs/rateLimits/helpers/executeWithRateLimitHandling.js +0 -144
- package/dist-cjs/rateLimits/helpers/headerUtils.js +0 -40
- package/dist-cjs/rateLimits/helpers/isStreamChannelLike.js +0 -23
- package/dist-cjs/rateLimits/helpers/processStreamBudgetHeaders.js +0 -93
- package/dist-cjs/rateLimits/helpers/processStreamRateLimitException.js +0 -63
- package/dist-cjs/rateLimits/helpers/processStreamRateLimitHeaders.js +0 -50
- package/dist-cjs/rateLimits/helpers/rateLimitSnapshot.js +0 -31
- package/dist-cjs/rateLimits/helpers/registerStreamBudgetInterceptors.js +0 -48
- package/dist-cjs/rateLimits/helpers/registerStreamInterceptors.js +0 -58
- package/dist-cjs/rateLimits/helpers/splitCallArgsAndOptions.js +0 -21
- package/dist-cjs/rateLimits/index.js +0 -7
- package/dist-cjs/rateLimits/types.js +0 -3
- package/dist-cjs/rateLimits/withStreamRateLimitOptions.js +0 -10
- package/dist-es/rateLimits/RedisCooldown.js +0 -62
- package/dist-es/rateLimits/constants.js +0 -33
- package/dist-es/rateLimits/createRateLimitedStreamProxy.js +0 -88
- package/dist-es/rateLimits/exception/createRateLimitExceededException.js +0 -17
- package/dist-es/rateLimits/exception/processStreamRateLimitException.js +0 -37
- package/dist-es/rateLimits/executeStreamRequestWithRateLimitHandling.js +0 -93
- package/dist-es/rateLimits/generatedMethodNames.js +0 -317
- package/dist-es/rateLimits/helpers/RedisCooldown.js +0 -170
- package/dist-es/rateLimits/helpers/createRateLimitExceededException.js +0 -28
- package/dist-es/rateLimits/helpers/executeWithRateLimitHandling.js +0 -140
- package/dist-es/rateLimits/helpers/headerUtils.js +0 -34
- package/dist-es/rateLimits/helpers/isStreamChannelLike.js +0 -19
- package/dist-es/rateLimits/helpers/processStreamBudgetHeaders.js +0 -89
- package/dist-es/rateLimits/helpers/processStreamRateLimitException.js +0 -59
- package/dist-es/rateLimits/helpers/processStreamRateLimitHeaders.js +0 -46
- package/dist-es/rateLimits/helpers/rateLimitSnapshot.js +0 -26
- package/dist-es/rateLimits/helpers/registerStreamBudgetInterceptors.js +0 -44
- package/dist-es/rateLimits/helpers/registerStreamInterceptors.js +0 -54
- package/dist-es/rateLimits/helpers/splitCallArgsAndOptions.js +0 -17
- package/dist-es/rateLimits/index.js +0 -4
- package/dist-es/rateLimits/types.js +0 -1
- package/dist-es/rateLimits/withStreamRateLimitOptions.js +0 -6
- package/dist-types/rateLimits/RedisCooldown.d.ts +0 -7
- package/dist-types/rateLimits/constants.d.ts +0 -29
- package/dist-types/rateLimits/createRateLimitedStreamProxy.d.ts +0 -11
- package/dist-types/rateLimits/exception/createRateLimitExceededException.d.ts +0 -12
- package/dist-types/rateLimits/exception/processStreamRateLimitException.d.ts +0 -8
- package/dist-types/rateLimits/executeStreamRequestWithRateLimitHandling.d.ts +0 -8
- package/dist-types/rateLimits/generatedMethodNames.d.ts +0 -11
- package/dist-types/rateLimits/helpers/RedisCooldown.d.ts +0 -11
- package/dist-types/rateLimits/helpers/createRateLimitExceededException.d.ts +0 -10
- package/dist-types/rateLimits/helpers/executeWithRateLimitHandling.d.ts +0 -2
- package/dist-types/rateLimits/helpers/headerUtils.d.ts +0 -4
- package/dist-types/rateLimits/helpers/isStreamChannelLike.d.ts +0 -6
- package/dist-types/rateLimits/helpers/processStreamBudgetHeaders.d.ts +0 -9
- package/dist-types/rateLimits/helpers/processStreamRateLimitException.d.ts +0 -3
- package/dist-types/rateLimits/helpers/processStreamRateLimitHeaders.d.ts +0 -6
- package/dist-types/rateLimits/helpers/rateLimitSnapshot.d.ts +0 -3
- package/dist-types/rateLimits/helpers/registerStreamBudgetInterceptors.d.ts +0 -2
- package/dist-types/rateLimits/helpers/registerStreamInterceptors.d.ts +0 -2
- package/dist-types/rateLimits/helpers/splitCallArgsAndOptions.d.ts +0 -6
- package/dist-types/rateLimits/index.d.ts +0 -4
- package/dist-types/rateLimits/types.d.ts +0 -68
- package/dist-types/rateLimits/withStreamRateLimitOptions.d.ts +0 -6
|
@@ -1,140 +0,0 @@
|
|
|
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_METADATA_KEY_SUFFIX, STREAM_RATE_LIMIT_SOFT_COOLDOWN_KEY, } from '../constants';
|
|
3
|
-
import { createRateLimitExceededException } from './createRateLimitExceededException';
|
|
4
|
-
import { processStreamRateLimitException } from './processStreamRateLimitException';
|
|
5
|
-
import { parseRateLimitSnapshot, serializeRateLimitSnapshot } from './rateLimitSnapshot';
|
|
6
|
-
import { RedisCooldown } from './RedisCooldown';
|
|
7
|
-
function calculateRateLimitDelayMs(error, maxDelayMs) {
|
|
8
|
-
const retryAfterMs = typeof error.rateLimitReset === 'number' && error.rateLimitReset > 0 ? error.rateLimitReset : 0;
|
|
9
|
-
const hasServerRetryAfter = retryAfterMs > 0;
|
|
10
|
-
const baseDelayMs = hasServerRetryAfter ? retryAfterMs + RETRY_AFTER_BUFFER_MS : DEFAULT_RETRY_AFTER_MS;
|
|
11
|
-
if (baseDelayMs > 0) {
|
|
12
|
-
const jitterMs = Math.floor(Math.random() * DEFAULT_JITTER_MS);
|
|
13
|
-
const computedDelayMs = baseDelayMs + jitterMs;
|
|
14
|
-
if (hasServerRetryAfter) {
|
|
15
|
-
return computedDelayMs;
|
|
16
|
-
}
|
|
17
|
-
return Math.min(maxDelayMs, computedDelayMs);
|
|
18
|
-
}
|
|
19
|
-
return Math.min(maxDelayMs, DEFAULT_MAX_DELAY_MS);
|
|
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
|
-
}
|
|
49
|
-
export async function executeWithRateLimitHandling(execute, operation, context, options) {
|
|
50
|
-
const { logger, redis } = context;
|
|
51
|
-
const { enableCooldown, maxAttempts, maxDelayMs, maxRetryableDelayMs } = options;
|
|
52
|
-
let attempt = 1;
|
|
53
|
-
if (enableCooldown) {
|
|
54
|
-
const cooldownMs = await RedisCooldown.getRemainingMs(operation, { redis, logger });
|
|
55
|
-
if (cooldownMs > 0) {
|
|
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
|
-
}
|
|
68
|
-
const softCooldownMs = await RedisCooldown.getRemainingMs(STREAM_RATE_LIMIT_SOFT_COOLDOWN_KEY, { redis, logger }, { keyPrefix: STREAM_RATE_LIMIT_COOLDOWN_KEY_PREFIX });
|
|
69
|
-
if (softCooldownMs > 0) {
|
|
70
|
-
logger.debug('Delaying stream request due to active soft rate-limit cooldown', {
|
|
71
|
-
operation,
|
|
72
|
-
cooldown: softCooldownMs,
|
|
73
|
-
});
|
|
74
|
-
await sleep(softCooldownMs);
|
|
75
|
-
}
|
|
76
|
-
const budgetCooldownMs = await RedisCooldown.getRemainingMs(STREAM_BUDGET_COOLDOWN_KEY, { redis, logger }, { keyPrefix: STREAM_BUDGET_COOLDOWN_KEY_PREFIX });
|
|
77
|
-
if (budgetCooldownMs > 0) {
|
|
78
|
-
logger.warn('Delaying stream request due to active budget cooldown', {
|
|
79
|
-
operation,
|
|
80
|
-
cooldown: budgetCooldownMs,
|
|
81
|
-
});
|
|
82
|
-
let totalBudgetWaitMs = 0;
|
|
83
|
-
for (;;) {
|
|
84
|
-
const waitMs = await RedisCooldown.getRemainingMs(STREAM_BUDGET_COOLDOWN_KEY, { redis, logger }, { keyPrefix: STREAM_BUDGET_COOLDOWN_KEY_PREFIX });
|
|
85
|
-
if (waitMs <= 0 || totalBudgetWaitMs >= BUDGET_DELAY_MAX_MS) {
|
|
86
|
-
break;
|
|
87
|
-
}
|
|
88
|
-
const chunkMs = Math.min(waitMs, BUDGET_COOLDOWN_SLEEP_CHUNK_MS);
|
|
89
|
-
await sleep(chunkMs);
|
|
90
|
-
totalBudgetWaitMs += chunkMs;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
for (;;) {
|
|
95
|
-
try {
|
|
96
|
-
return await execute();
|
|
97
|
-
}
|
|
98
|
-
catch (error) {
|
|
99
|
-
const rateLimitError = processStreamRateLimitException(error, logger);
|
|
100
|
-
if (!rateLimitError) {
|
|
101
|
-
throw error;
|
|
102
|
-
}
|
|
103
|
-
const retryAfterMs = calculateRateLimitDelayMs(rateLimitError, maxDelayMs);
|
|
104
|
-
if (enableCooldown) {
|
|
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
|
-
}
|
|
112
|
-
}
|
|
113
|
-
if (attempt < maxAttempts && retryAfterMs <= maxRetryableDelayMs) {
|
|
114
|
-
logger.warn('Retrying stream request after rate limit', {
|
|
115
|
-
operation,
|
|
116
|
-
attempt,
|
|
117
|
-
maxAttempts,
|
|
118
|
-
retryAfterMs,
|
|
119
|
-
rateLimit: rateLimitError.rateLimit,
|
|
120
|
-
rateLimitRemaining: rateLimitError.rateLimitRemaining,
|
|
121
|
-
rateLimitReset: rateLimitError.rateLimitReset,
|
|
122
|
-
});
|
|
123
|
-
await sleep(retryAfterMs);
|
|
124
|
-
attempt++;
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
logger.warn('Propagating stream rate limit w/o retry', {
|
|
128
|
-
operation,
|
|
129
|
-
attempt,
|
|
130
|
-
maxAttempts,
|
|
131
|
-
retryAfterMs,
|
|
132
|
-
maxRetryableDelayMs,
|
|
133
|
-
rateLimit: rateLimitError.rateLimit,
|
|
134
|
-
rateLimitRemaining: rateLimitError.rateLimitRemaining,
|
|
135
|
-
rateLimitReset: rateLimitError.rateLimitReset,
|
|
136
|
-
});
|
|
137
|
-
throw rateLimitError;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
export function getHeaderCaseInsensitive(headers, key) {
|
|
2
|
-
if (!headers) {
|
|
3
|
-
return;
|
|
4
|
-
}
|
|
5
|
-
const direct = headers[key];
|
|
6
|
-
if (direct !== undefined) {
|
|
7
|
-
return direct;
|
|
8
|
-
}
|
|
9
|
-
const normalizedKey = key.toLowerCase();
|
|
10
|
-
for (const [headerKey, headerValue] of Object.entries(headers)) {
|
|
11
|
-
if (headerKey.toLowerCase() === normalizedKey) {
|
|
12
|
-
return headerValue;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
export function parseHeaderNumber(headers, key) {
|
|
17
|
-
const raw = getHeaderCaseInsensitive(headers, key);
|
|
18
|
-
if (typeof raw === 'number' && Number.isFinite(raw)) {
|
|
19
|
-
return raw;
|
|
20
|
-
}
|
|
21
|
-
if (typeof raw === 'string') {
|
|
22
|
-
const parsed = Number(raw);
|
|
23
|
-
if (Number.isFinite(parsed)) {
|
|
24
|
-
return parsed;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
export function normalizeResetToEpochMs(reset) {
|
|
29
|
-
if (reset < 10000)
|
|
30
|
-
return Date.now() + Math.round(reset * 1000);
|
|
31
|
-
if (reset < 10000000000)
|
|
32
|
-
return Math.round(reset * 1000);
|
|
33
|
-
return Math.round(reset);
|
|
34
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { Channel } from 'stream-chat';
|
|
2
|
-
function hasFunctionProperty(value, key) {
|
|
3
|
-
return key in value && typeof value[key] === 'function';
|
|
4
|
-
}
|
|
5
|
-
function hasStringProperty(value, key) {
|
|
6
|
-
return key in value && typeof value[key] === 'string';
|
|
7
|
-
}
|
|
8
|
-
export function isStreamChannelLike(value) {
|
|
9
|
-
if (typeof value !== 'object' || value === null) {
|
|
10
|
-
return false;
|
|
11
|
-
}
|
|
12
|
-
if (value instanceof Channel) {
|
|
13
|
-
return true;
|
|
14
|
-
}
|
|
15
|
-
return (hasStringProperty(value, 'cid') &&
|
|
16
|
-
hasFunctionProperty(value, 'sendMessage') &&
|
|
17
|
-
hasFunctionProperty(value, 'queryMembers') &&
|
|
18
|
-
hasFunctionProperty(value, 'updatePartial'));
|
|
19
|
-
}
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { BUDGET_CLEAR_USAGE_THRESHOLD, BUDGET_DELAY_MAX_MS, BUDGET_DELAY_MIN_MS, BUDGET_MINIMAL_DELAY_MAX_MS, BUDGET_MINIMAL_DELAY_MIN_MS, BUDGET_RELAXED_DELAY_MAX_MS, BUDGET_RELAXED_DELAY_MIN_MS, BUDGET_RELAXED_USAGE_THRESHOLD, BUDGET_THRESHOLD, STREAM_BUDGET_COOLDOWN_KEY, STREAM_BUDGET_COOLDOWN_KEY_PREFIX, } from '../constants';
|
|
2
|
-
import { parseHeaderNumber } from './headerUtils';
|
|
3
|
-
import { RedisCooldown } from './RedisCooldown';
|
|
4
|
-
function extractBudgetHeaders(headers) {
|
|
5
|
-
return {
|
|
6
|
-
limitMs: parseHeaderNumber(headers ?? {}, 'x-budget-limit-ms'),
|
|
7
|
-
remainingMs: parseHeaderNumber(headers ?? {}, 'x-budget-remaining-ms'),
|
|
8
|
-
usedMs: parseHeaderNumber(headers ?? {}, 'x-budget-used-ms'),
|
|
9
|
-
};
|
|
10
|
-
}
|
|
11
|
-
function isBudgetThresholdReached(limitMs, remainingMs, usedMs) {
|
|
12
|
-
if (limitMs <= 0) {
|
|
13
|
-
return false;
|
|
14
|
-
}
|
|
15
|
-
if (typeof usedMs === 'number' && usedMs / limitMs >= BUDGET_THRESHOLD) {
|
|
16
|
-
return true;
|
|
17
|
-
}
|
|
18
|
-
if (typeof remainingMs === 'number') {
|
|
19
|
-
const remainingThreshold = Math.ceil((1 - BUDGET_THRESHOLD) * limitMs);
|
|
20
|
-
return remainingMs <= remainingThreshold;
|
|
21
|
-
}
|
|
22
|
-
return false;
|
|
23
|
-
}
|
|
24
|
-
function getRandomDelayInRange(minMs, maxMs) {
|
|
25
|
-
return Math.round(minMs + Math.random() * (maxMs - minMs));
|
|
26
|
-
}
|
|
27
|
-
function getBudgetUsage(limitMs, remainingMs, usedMs) {
|
|
28
|
-
if (limitMs <= 0) {
|
|
29
|
-
return 0;
|
|
30
|
-
}
|
|
31
|
-
if (typeof usedMs === 'number' && Number.isFinite(usedMs) && usedMs >= 0) {
|
|
32
|
-
return usedMs / limitMs;
|
|
33
|
-
}
|
|
34
|
-
if (typeof remainingMs === 'number' && Number.isFinite(remainingMs)) {
|
|
35
|
-
return Math.max(0, 1 - remainingMs / limitMs);
|
|
36
|
-
}
|
|
37
|
-
return 0;
|
|
38
|
-
}
|
|
39
|
-
export async function processStreamBudgetHeaders(headers, context) {
|
|
40
|
-
const { logger } = context;
|
|
41
|
-
const { limitMs, remainingMs, usedMs } = extractBudgetHeaders(headers);
|
|
42
|
-
if (limitMs === undefined && remainingMs === undefined && usedMs === undefined) {
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
if (typeof limitMs !== 'number' || !Number.isFinite(limitMs)) {
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
const usage = getBudgetUsage(limitMs, remainingMs, usedMs);
|
|
49
|
-
const thresholdReached = isBudgetThresholdReached(limitMs, remainingMs, usedMs);
|
|
50
|
-
if (thresholdReached) {
|
|
51
|
-
const delayMs = getRandomDelayInRange(BUDGET_DELAY_MIN_MS, BUDGET_DELAY_MAX_MS);
|
|
52
|
-
await RedisCooldown.setRemainingMsExact(STREAM_BUDGET_COOLDOWN_KEY, delayMs, context, {
|
|
53
|
-
keyPrefix: STREAM_BUDGET_COOLDOWN_KEY_PREFIX,
|
|
54
|
-
});
|
|
55
|
-
logger.warn('Stream budget threshold reached, enabling cooldown', {
|
|
56
|
-
budgetLimitMs: limitMs,
|
|
57
|
-
budgetRemainingMs: remainingMs,
|
|
58
|
-
budgetUsedMs: usedMs,
|
|
59
|
-
budgetUsage: usage,
|
|
60
|
-
cooldownMs: delayMs,
|
|
61
|
-
});
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
if (usage < BUDGET_CLEAR_USAGE_THRESHOLD) {
|
|
65
|
-
await RedisCooldown.clear(STREAM_BUDGET_COOLDOWN_KEY, context, {
|
|
66
|
-
keyPrefix: STREAM_BUDGET_COOLDOWN_KEY_PREFIX,
|
|
67
|
-
});
|
|
68
|
-
logger.info('Stream budget is low, clearing cooldown', {
|
|
69
|
-
budgetLimitMs: limitMs,
|
|
70
|
-
budgetRemainingMs: remainingMs,
|
|
71
|
-
budgetUsedMs: usedMs,
|
|
72
|
-
budgetUsage: usage,
|
|
73
|
-
});
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
const delayMs = usage >= BUDGET_RELAXED_USAGE_THRESHOLD
|
|
77
|
-
? getRandomDelayInRange(BUDGET_RELAXED_DELAY_MIN_MS, BUDGET_RELAXED_DELAY_MAX_MS)
|
|
78
|
-
: getRandomDelayInRange(BUDGET_MINIMAL_DELAY_MIN_MS, BUDGET_MINIMAL_DELAY_MAX_MS);
|
|
79
|
-
await RedisCooldown.setRemainingMsExact(STREAM_BUDGET_COOLDOWN_KEY, delayMs, context, {
|
|
80
|
-
keyPrefix: STREAM_BUDGET_COOLDOWN_KEY_PREFIX,
|
|
81
|
-
});
|
|
82
|
-
logger.info('Stream budget is below threshold, reducing cooldown', {
|
|
83
|
-
budgetLimitMs: limitMs,
|
|
84
|
-
budgetRemainingMs: remainingMs,
|
|
85
|
-
budgetUsedMs: usedMs,
|
|
86
|
-
budgetUsage: usage,
|
|
87
|
-
cooldownMs: delayMs,
|
|
88
|
-
});
|
|
89
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { createRateLimitExceededException } from './createRateLimitExceededException';
|
|
2
|
-
import { getHeaderCaseInsensitive, normalizeResetToEpochMs, parseHeaderNumber } from './headerUtils';
|
|
3
|
-
function parseRetryAfterMs(headers) {
|
|
4
|
-
const retryAfter = getHeaderCaseInsensitive(headers, 'retry-after');
|
|
5
|
-
if (typeof retryAfter === 'number' && Number.isFinite(retryAfter) && retryAfter >= 0) {
|
|
6
|
-
return Math.round(retryAfter * 1000);
|
|
7
|
-
}
|
|
8
|
-
if (typeof retryAfter !== 'string') {
|
|
9
|
-
return;
|
|
10
|
-
}
|
|
11
|
-
const asSeconds = Number(retryAfter);
|
|
12
|
-
if (Number.isFinite(asSeconds) && asSeconds >= 0) {
|
|
13
|
-
return Math.round(asSeconds * 1000);
|
|
14
|
-
}
|
|
15
|
-
const asDateMs = Date.parse(retryAfter);
|
|
16
|
-
if (!Number.isNaN(asDateMs)) {
|
|
17
|
-
return Math.max(0, asDateMs - Date.now());
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
function getRateLimitFromHeaders(headers) {
|
|
21
|
-
const limit = parseHeaderNumber(headers, 'x-ratelimit-limit');
|
|
22
|
-
const remaining = parseHeaderNumber(headers, 'x-ratelimit-remaining');
|
|
23
|
-
const resetRaw = parseHeaderNumber(headers, 'x-ratelimit-reset');
|
|
24
|
-
if (limit === undefined || remaining === undefined || resetRaw === undefined) {
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
return {
|
|
28
|
-
limit,
|
|
29
|
-
remaining,
|
|
30
|
-
reset: normalizeResetToEpochMs(resetRaw),
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
function isStreamHttpRateLimitError(error) {
|
|
34
|
-
return (typeof error === 'object' &&
|
|
35
|
-
error !== null &&
|
|
36
|
-
(('status' in error && error.status === 429) ||
|
|
37
|
-
('response' in error &&
|
|
38
|
-
typeof error.response === 'object' &&
|
|
39
|
-
error.response !== null &&
|
|
40
|
-
error.response?.status === 429)));
|
|
41
|
-
}
|
|
42
|
-
export function processStreamRateLimitException(error, logger) {
|
|
43
|
-
if (isStreamHttpRateLimitError(error)) {
|
|
44
|
-
const headers = error.response?.headers ?? {};
|
|
45
|
-
const rateLimit = getRateLimitFromHeaders(headers);
|
|
46
|
-
const retryAfterMs = parseRetryAfterMs(headers);
|
|
47
|
-
const message = error.response?.data?.message || error.message || 'Rate limit exceeded';
|
|
48
|
-
logger.warn('Stream rate limit exceeded [HTTP 429]', {
|
|
49
|
-
status: error.status ?? error.response?.status,
|
|
50
|
-
retryAfterMs,
|
|
51
|
-
rateLimit,
|
|
52
|
-
});
|
|
53
|
-
return createRateLimitExceededException({
|
|
54
|
-
rateLimit,
|
|
55
|
-
retryAfterMs,
|
|
56
|
-
message,
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
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 { parseHeaderNumber } from './headerUtils';
|
|
3
|
-
import { RedisCooldown } from './RedisCooldown';
|
|
4
|
-
function computeSoftThrottleDelayMs(limit, remaining) {
|
|
5
|
-
if (limit <= 0) {
|
|
6
|
-
return 0;
|
|
7
|
-
}
|
|
8
|
-
const usage = 1 - remaining / limit;
|
|
9
|
-
if (usage >= SOFT_THROTTLE_USAGE_HIGH) {
|
|
10
|
-
return SOFT_THROTTLE_DELAY_HIGH_MS;
|
|
11
|
-
}
|
|
12
|
-
if (usage >= SOFT_THROTTLE_USAGE_MEDIUM) {
|
|
13
|
-
return SOFT_THROTTLE_DELAY_MEDIUM_MS;
|
|
14
|
-
}
|
|
15
|
-
if (usage >= SOFT_THROTTLE_USAGE_LOW) {
|
|
16
|
-
return SOFT_THROTTLE_DELAY_LOW_MS;
|
|
17
|
-
}
|
|
18
|
-
return 0;
|
|
19
|
-
}
|
|
20
|
-
export async function processStreamRateLimitHeaders(headers, context) {
|
|
21
|
-
const { logger } = context;
|
|
22
|
-
const limit = parseHeaderNumber(headers ?? {}, 'x-ratelimit-limit');
|
|
23
|
-
const remaining = parseHeaderNumber(headers ?? {}, 'x-ratelimit-remaining');
|
|
24
|
-
if (limit === undefined || remaining === undefined) {
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
if (limit <= 0 || remaining < 0) {
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
const delayMs = computeSoftThrottleDelayMs(limit, remaining);
|
|
31
|
-
if (delayMs > 0) {
|
|
32
|
-
await RedisCooldown.setRemainingMs(STREAM_RATE_LIMIT_SOFT_COOLDOWN_KEY, delayMs, context, {
|
|
33
|
-
keyPrefix: STREAM_RATE_LIMIT_COOLDOWN_KEY_PREFIX,
|
|
34
|
-
});
|
|
35
|
-
logger.debug('Enabling soft stream rate-limit cooldown', {
|
|
36
|
-
rateLimit: limit,
|
|
37
|
-
rateLimitRemaining: remaining,
|
|
38
|
-
cooldownMs: delayMs,
|
|
39
|
-
});
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
await RedisCooldown.clear(STREAM_RATE_LIMIT_SOFT_COOLDOWN_KEY, context, {
|
|
43
|
-
keyPrefix: STREAM_RATE_LIMIT_COOLDOWN_KEY_PREFIX,
|
|
44
|
-
});
|
|
45
|
-
logger.debug('Clearing soft stream rate-limit cooldown');
|
|
46
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { processStreamBudgetHeaders } from './processStreamBudgetHeaders';
|
|
2
|
-
import { processStreamRateLimitHeaders } from './processStreamRateLimitHeaders';
|
|
3
|
-
const attachedInterceptors = new WeakSet();
|
|
4
|
-
function hasResponseInterceptors(value) {
|
|
5
|
-
return (typeof value === 'object' &&
|
|
6
|
-
value !== null &&
|
|
7
|
-
typeof value.interceptors?.response?.use === 'function');
|
|
8
|
-
}
|
|
9
|
-
function resolveAxiosInstance(stream) {
|
|
10
|
-
if (hasResponseInterceptors(stream)) {
|
|
11
|
-
return stream;
|
|
12
|
-
}
|
|
13
|
-
if (typeof stream !== 'object' || stream === null) {
|
|
14
|
-
return;
|
|
15
|
-
}
|
|
16
|
-
const maybeStream = stream;
|
|
17
|
-
if (hasResponseInterceptors(maybeStream.axiosInstance)) {
|
|
18
|
-
return maybeStream.axiosInstance;
|
|
19
|
-
}
|
|
20
|
-
if (hasResponseInterceptors(maybeStream._client?.axiosInstance)) {
|
|
21
|
-
return maybeStream._client?.axiosInstance;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
export function registerStreamBudgetInterceptors(stream, context) {
|
|
25
|
-
const { logger } = context;
|
|
26
|
-
const axiosInstance = resolveAxiosInstance(stream);
|
|
27
|
-
if (!axiosInstance || !axiosInstance.interceptors?.response?.use) {
|
|
28
|
-
logger.warn('Unable to register stream budget interceptor: axios instance not found');
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
if (attachedInterceptors.has(axiosInstance)) {
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
axiosInstance.interceptors.response.use(async (response) => {
|
|
35
|
-
await processStreamBudgetHeaders(response?.headers, context);
|
|
36
|
-
await processStreamRateLimitHeaders(response?.headers, context);
|
|
37
|
-
return response;
|
|
38
|
-
}, async (error) => {
|
|
39
|
-
await processStreamBudgetHeaders(error?.response?.headers, context);
|
|
40
|
-
await processStreamRateLimitHeaders(error?.response?.headers, context);
|
|
41
|
-
return Promise.reject(error);
|
|
42
|
-
});
|
|
43
|
-
attachedInterceptors.add(axiosInstance);
|
|
44
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { processStreamBudgetHeaders } from './processStreamBudgetHeaders';
|
|
2
|
-
import { processStreamRateLimitHeaders } from './processStreamRateLimitHeaders';
|
|
3
|
-
const attachedInterceptors = new WeakSet();
|
|
4
|
-
function hasResponseInterceptors(value) {
|
|
5
|
-
return (typeof value === 'object' &&
|
|
6
|
-
value !== null &&
|
|
7
|
-
typeof value.interceptors?.response?.use === 'function');
|
|
8
|
-
}
|
|
9
|
-
function resolveAxiosInstance(stream) {
|
|
10
|
-
if (hasResponseInterceptors(stream)) {
|
|
11
|
-
return stream;
|
|
12
|
-
}
|
|
13
|
-
if (typeof stream !== 'object' || stream === null) {
|
|
14
|
-
return;
|
|
15
|
-
}
|
|
16
|
-
const maybeStream = stream;
|
|
17
|
-
if (hasResponseInterceptors(maybeStream.axiosInstance)) {
|
|
18
|
-
return maybeStream.axiosInstance;
|
|
19
|
-
}
|
|
20
|
-
if (hasResponseInterceptors(maybeStream._client?.axiosInstance)) {
|
|
21
|
-
return maybeStream._client?.axiosInstance;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
export function registerStreamInterceptors(stream, context) {
|
|
25
|
-
const { logger } = context;
|
|
26
|
-
const axiosInstance = resolveAxiosInstance(stream);
|
|
27
|
-
if (!axiosInstance || !axiosInstance.interceptors?.response?.use) {
|
|
28
|
-
logger.warn('Unable to register stream budget & rate limit interceptors: axios instance not found');
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
if (attachedInterceptors.has(axiosInstance)) {
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
attachedInterceptors.add(axiosInstance);
|
|
35
|
-
axiosInstance.interceptors.response.use(async (response) => {
|
|
36
|
-
try {
|
|
37
|
-
await processStreamBudgetHeaders(response?.headers, context);
|
|
38
|
-
await processStreamRateLimitHeaders(response?.headers, context);
|
|
39
|
-
}
|
|
40
|
-
catch (error) {
|
|
41
|
-
logger.warn('Failed to process Stream response headers in interceptor', { error });
|
|
42
|
-
}
|
|
43
|
-
return response;
|
|
44
|
-
}, async (error) => {
|
|
45
|
-
try {
|
|
46
|
-
await processStreamBudgetHeaders(error?.response?.headers, context);
|
|
47
|
-
await processStreamRateLimitHeaders(error?.response?.headers, context);
|
|
48
|
-
}
|
|
49
|
-
catch (interceptorError) {
|
|
50
|
-
logger.warn('Failed to process Stream error headers in interceptor', { error: interceptorError });
|
|
51
|
-
}
|
|
52
|
-
return Promise.reject(error);
|
|
53
|
-
});
|
|
54
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { STREAM_RATE_LIMIT_OPTIONS_MARKER } from '../constants';
|
|
2
|
-
function isStreamRateLimitOptionsToken(value) {
|
|
3
|
-
return typeof value === 'object' && value !== null && STREAM_RATE_LIMIT_OPTIONS_MARKER in value;
|
|
4
|
-
}
|
|
5
|
-
export function splitCallArgsAndOptions(args) {
|
|
6
|
-
if (args.length === 0) {
|
|
7
|
-
return { callArgs: args, callOptions: {} };
|
|
8
|
-
}
|
|
9
|
-
const lastArg = args[args.length - 1];
|
|
10
|
-
if (isStreamRateLimitOptionsToken(lastArg)) {
|
|
11
|
-
return {
|
|
12
|
-
callArgs: args.slice(0, -1),
|
|
13
|
-
callOptions: lastArg[STREAM_RATE_LIMIT_OPTIONS_MARKER],
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
return { callArgs: args, callOptions: {} };
|
|
17
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import { STREAM_RATE_LIMIT_OPTIONS_MARKER } from './constants';
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import {LoggerContext, RedisContext} from './types';
|
|
2
|
-
|
|
3
|
-
export declare class RedisCooldown {
|
|
4
|
-
static getRemainingMs(cooldownKey: string, context: RedisContext & LoggerContext): Promise<number>;
|
|
5
|
-
|
|
6
|
-
static setRemainingMs(cooldownKey: string, cooldownMs: number, context: RedisContext & LoggerContext): Promise<void>;
|
|
7
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { StreamRateLimitRetryOptions } from './types';
|
|
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
|
-
export declare const DEFAULT_RETRY_AFTER_MS = 10000;
|
|
5
|
-
export declare const DEFAULT_JITTER_MS = 150;
|
|
6
|
-
export declare const RETRY_AFTER_BUFFER_MS = 1000;
|
|
7
|
-
export declare const STREAM_RATE_LIMIT_COOLDOWN_KEY_PREFIX = "stream-rate-limit-cooldown:";
|
|
8
|
-
export declare const STREAM_BUDGET_COOLDOWN_KEY_PREFIX = "stream-budget-cooldown:";
|
|
9
|
-
export declare const STREAM_RATE_LIMIT_OPTIONS_MARKER = "__xbsRateLimitOptions";
|
|
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";
|
|
12
|
-
export declare const STREAM_BUDGET_COOLDOWN_KEY = "stream-budget-global";
|
|
13
|
-
export declare const BUDGET_THRESHOLD = 0.8;
|
|
14
|
-
export declare const BUDGET_DELAY_MIN_MS = 30000;
|
|
15
|
-
export declare const BUDGET_DELAY_MAX_MS = 60000;
|
|
16
|
-
export declare const BUDGET_CLEAR_USAGE_THRESHOLD = 0.6;
|
|
17
|
-
export declare const BUDGET_RELAXED_USAGE_THRESHOLD = 0.7;
|
|
18
|
-
export declare const BUDGET_RELAXED_DELAY_MIN_MS = 5000;
|
|
19
|
-
export declare const BUDGET_RELAXED_DELAY_MAX_MS = 10000;
|
|
20
|
-
export declare const BUDGET_MINIMAL_DELAY_MIN_MS = 1000;
|
|
21
|
-
export declare const BUDGET_MINIMAL_DELAY_MAX_MS = 2000;
|
|
22
|
-
export declare const BUDGET_COOLDOWN_SLEEP_CHUNK_MS = 3000;
|
|
23
|
-
export declare const SOFT_THROTTLE_USAGE_LOW = 0.7;
|
|
24
|
-
export declare const SOFT_THROTTLE_USAGE_MEDIUM = 0.85;
|
|
25
|
-
export declare const SOFT_THROTTLE_USAGE_HIGH = 0.95;
|
|
26
|
-
export declare const SOFT_THROTTLE_DELAY_LOW_MS = 500;
|
|
27
|
-
export declare const SOFT_THROTTLE_DELAY_MEDIUM_MS = 1500;
|
|
28
|
-
export declare const SOFT_THROTTLE_DELAY_HIGH_MS = 3000;
|
|
29
|
-
export declare const DEFAULT_OPTIONS: StreamRateLimitRetryOptions;
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { StreamChat } from 'stream-chat';
|
|
2
|
-
import { StreamType } from '../stream';
|
|
3
|
-
import { LoggerContext, MethodWithRateLimitOptions, RedisContext } from './types';
|
|
4
|
-
export type RateLimited<T extends object> = {
|
|
5
|
-
[Key in keyof T]: MethodWithRateLimitOptions<T[Key]>;
|
|
6
|
-
};
|
|
7
|
-
/**
|
|
8
|
-
* Creates a proxy around a Stream client that transparently applies rate-limit handling
|
|
9
|
-
* to method calls and recursively wraps returned channel objects with the same behavior.
|
|
10
|
-
*/
|
|
11
|
-
export declare function createRateLimitedStreamProxy(stream: StreamChat<StreamType>, context: LoggerContext & RedisContext): RateLimited<StreamChat<StreamType>>;
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import {RateLimitExceededException} from '@wildix/xbees-conversations-client';
|
|
2
|
-
|
|
3
|
-
import {RateLimitsInfo} from 'stream-chat';
|
|
4
|
-
|
|
5
|
-
type Options = {
|
|
6
|
-
rateLimit?: RateLimitsInfo;
|
|
7
|
-
message?: string;
|
|
8
|
-
operation?: string;
|
|
9
|
-
retryAfterMs?: number;
|
|
10
|
-
};
|
|
11
|
-
export declare function createRateLimitExceededException(options?: Options): RateLimitExceededException;
|
|
12
|
-
export {};
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import {RateLimitExceededException} from '@wildix/xbees-conversations-client';
|
|
2
|
-
|
|
3
|
-
import {Logger} from '@aws-lambda-powertools/logger';
|
|
4
|
-
|
|
5
|
-
export declare function processStreamRateLimitException(
|
|
6
|
-
error: unknown,
|
|
7
|
-
logger: Logger,
|
|
8
|
-
): RateLimitExceededException | undefined;
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import {LoggerContext, RedisContext, StreamOperationType, StreamRateLimitRetryOptions} from './types';
|
|
2
|
-
|
|
3
|
-
export declare function executeStreamRequestWithRateLimitHandling<T>(
|
|
4
|
-
execute: () => Promise<T>,
|
|
5
|
-
operation: StreamOperationType,
|
|
6
|
-
context: LoggerContext & RedisContext,
|
|
7
|
-
options?: Partial<StreamRateLimitRetryOptions>,
|
|
8
|
-
): Promise<T>;
|