@wildix/xbees-conversations-utils 1.1.79 → 1.1.81
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/rateLimits/createRateLimitedStreamProxy.js +8 -3
- package/dist-cjs/rateLimits/helpers/RedisCooldown.js +37 -0
- package/dist-cjs/rateLimits/helpers/executeWithRateLimitHandling.js +5 -3
- package/dist-cjs/rateLimits/helpers/registerStreamInterceptors.js +15 -5
- package/dist-es/rateLimits/createRateLimitedStreamProxy.js +8 -3
- package/dist-es/rateLimits/helpers/RedisCooldown.js +37 -0
- package/dist-es/rateLimits/helpers/executeWithRateLimitHandling.js +5 -3
- package/dist-es/rateLimits/helpers/registerStreamInterceptors.js +15 -5
- package/dist-types/rateLimits/helpers/RedisCooldown.d.ts +1 -0
- package/package.json +2 -2
|
@@ -55,13 +55,15 @@ function wrapTarget(target, dependencies) {
|
|
|
55
55
|
}
|
|
56
56
|
if (STREAM_SYNC_CHANNEL_RESULT_METHODS.has(property)) {
|
|
57
57
|
return (...args) => {
|
|
58
|
-
const
|
|
58
|
+
const { callArgs } = (0, splitCallArgsAndOptions_1.splitCallArgsAndOptions)(args);
|
|
59
|
+
const result = Reflect.apply(value, originalTarget, callArgs);
|
|
59
60
|
return normalizeStreamCallResult(result, dependencies, true);
|
|
60
61
|
};
|
|
61
62
|
}
|
|
62
63
|
if (CHANNEL_SYNC_PROXY_RESULT_METHODS.has(property)) {
|
|
63
64
|
return (...args) => {
|
|
64
|
-
const
|
|
65
|
+
const { callArgs } = (0, splitCallArgsAndOptions_1.splitCallArgsAndOptions)(args);
|
|
66
|
+
const result = Reflect.apply(value, originalTarget, callArgs);
|
|
65
67
|
if (typeof result === 'object' && result !== null) {
|
|
66
68
|
return wrapTarget(result, dependencies);
|
|
67
69
|
}
|
|
@@ -69,7 +71,10 @@ function wrapTarget(target, dependencies) {
|
|
|
69
71
|
};
|
|
70
72
|
}
|
|
71
73
|
if (STREAM_SYNC_METHODS.has(property) || CHANNEL_SYNC_METHODS.has(property)) {
|
|
72
|
-
return (...args) =>
|
|
74
|
+
return (...args) => {
|
|
75
|
+
const { callArgs } = (0, splitCallArgsAndOptions_1.splitCallArgsAndOptions)(args);
|
|
76
|
+
return Reflect.apply(value, originalTarget, callArgs);
|
|
77
|
+
};
|
|
73
78
|
}
|
|
74
79
|
return createRateLimitedMethodWrapper(originalTarget, value, property, dependencies, STREAM_CHANNEL_RESULT_METHODS.has(property));
|
|
75
80
|
},
|
|
@@ -121,6 +121,43 @@ class RedisCooldown {
|
|
|
121
121
|
});
|
|
122
122
|
}
|
|
123
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
|
+
}
|
|
124
161
|
static async clear(cooldownKey, context, options) {
|
|
125
162
|
const { redis, logger } = context;
|
|
126
163
|
try {
|
|
@@ -4,8 +4,8 @@ exports.executeWithRateLimitHandling = void 0;
|
|
|
4
4
|
const promises_1 = require("node:timers/promises");
|
|
5
5
|
const constants_1 = require("../constants");
|
|
6
6
|
const createRateLimitExceededException_1 = require("./createRateLimitExceededException");
|
|
7
|
-
const rateLimitSnapshot_1 = require("./rateLimitSnapshot");
|
|
8
7
|
const processStreamRateLimitException_1 = require("./processStreamRateLimitException");
|
|
8
|
+
const rateLimitSnapshot_1 = require("./rateLimitSnapshot");
|
|
9
9
|
const RedisCooldown_1 = require("./RedisCooldown");
|
|
10
10
|
function calculateRateLimitDelayMs(error, maxDelayMs) {
|
|
11
11
|
const retryAfterMs = typeof error.rateLimitReset === 'number' && error.rateLimitReset > 0 ? error.rateLimitReset : 0;
|
|
@@ -106,9 +106,11 @@ async function executeWithRateLimitHandling(execute, operation, context, options
|
|
|
106
106
|
const retryAfterMs = calculateRateLimitDelayMs(rateLimitError, maxDelayMs);
|
|
107
107
|
if (enableCooldown) {
|
|
108
108
|
const rateLimitSnapshot = toRateLimitSnapshot(rateLimitError);
|
|
109
|
-
await RedisCooldown_1.RedisCooldown.setRemainingMs(operation, retryAfterMs, { redis, logger });
|
|
110
109
|
if (rateLimitSnapshot) {
|
|
111
|
-
await RedisCooldown_1.RedisCooldown.
|
|
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 });
|
|
112
114
|
}
|
|
113
115
|
}
|
|
114
116
|
if (attempt < maxAttempts && retryAfterMs <= maxRetryableDelayMs) {
|
|
@@ -34,15 +34,25 @@ function registerStreamInterceptors(stream, context) {
|
|
|
34
34
|
if (attachedInterceptors.has(axiosInstance)) {
|
|
35
35
|
return;
|
|
36
36
|
}
|
|
37
|
+
attachedInterceptors.add(axiosInstance);
|
|
37
38
|
axiosInstance.interceptors.response.use(async (response) => {
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
try {
|
|
40
|
+
await (0, processStreamBudgetHeaders_1.processStreamBudgetHeaders)(response?.headers, context);
|
|
41
|
+
await (0, processStreamRateLimitHeaders_1.processStreamRateLimitHeaders)(response?.headers, context);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
logger.warn('Failed to process Stream response headers in interceptor', { error });
|
|
45
|
+
}
|
|
40
46
|
return response;
|
|
41
47
|
}, async (error) => {
|
|
42
|
-
|
|
43
|
-
|
|
48
|
+
try {
|
|
49
|
+
await (0, processStreamBudgetHeaders_1.processStreamBudgetHeaders)(error?.response?.headers, context);
|
|
50
|
+
await (0, processStreamRateLimitHeaders_1.processStreamRateLimitHeaders)(error?.response?.headers, context);
|
|
51
|
+
}
|
|
52
|
+
catch (interceptorError) {
|
|
53
|
+
logger.warn('Failed to process Stream error headers in interceptor', { error: interceptorError });
|
|
54
|
+
}
|
|
44
55
|
return Promise.reject(error);
|
|
45
56
|
});
|
|
46
|
-
attachedInterceptors.add(axiosInstance);
|
|
47
57
|
}
|
|
48
58
|
exports.registerStreamInterceptors = registerStreamInterceptors;
|
|
@@ -52,13 +52,15 @@ function wrapTarget(target, dependencies) {
|
|
|
52
52
|
}
|
|
53
53
|
if (STREAM_SYNC_CHANNEL_RESULT_METHODS.has(property)) {
|
|
54
54
|
return (...args) => {
|
|
55
|
-
const
|
|
55
|
+
const { callArgs } = splitCallArgsAndOptions(args);
|
|
56
|
+
const result = Reflect.apply(value, originalTarget, callArgs);
|
|
56
57
|
return normalizeStreamCallResult(result, dependencies, true);
|
|
57
58
|
};
|
|
58
59
|
}
|
|
59
60
|
if (CHANNEL_SYNC_PROXY_RESULT_METHODS.has(property)) {
|
|
60
61
|
return (...args) => {
|
|
61
|
-
const
|
|
62
|
+
const { callArgs } = splitCallArgsAndOptions(args);
|
|
63
|
+
const result = Reflect.apply(value, originalTarget, callArgs);
|
|
62
64
|
if (typeof result === 'object' && result !== null) {
|
|
63
65
|
return wrapTarget(result, dependencies);
|
|
64
66
|
}
|
|
@@ -66,7 +68,10 @@ function wrapTarget(target, dependencies) {
|
|
|
66
68
|
};
|
|
67
69
|
}
|
|
68
70
|
if (STREAM_SYNC_METHODS.has(property) || CHANNEL_SYNC_METHODS.has(property)) {
|
|
69
|
-
return (...args) =>
|
|
71
|
+
return (...args) => {
|
|
72
|
+
const { callArgs } = splitCallArgsAndOptions(args);
|
|
73
|
+
return Reflect.apply(value, originalTarget, callArgs);
|
|
74
|
+
};
|
|
70
75
|
}
|
|
71
76
|
return createRateLimitedMethodWrapper(originalTarget, value, property, dependencies, STREAM_CHANNEL_RESULT_METHODS.has(property));
|
|
72
77
|
},
|
|
@@ -118,6 +118,43 @@ export class RedisCooldown {
|
|
|
118
118
|
});
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
|
+
static async setRemainingMsWithValue(cooldownKey, valueKey, value, cooldownMs, context, options) {
|
|
122
|
+
if (value.length === 0 || cooldownMs <= 0) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const { redis, logger } = context;
|
|
126
|
+
const cooldownRedisKey = RedisCooldown.resolveRedisKey(cooldownKey, options);
|
|
127
|
+
const valueRedisKey = RedisCooldown.resolveRedisKey(valueKey, options);
|
|
128
|
+
try {
|
|
129
|
+
await redis.eval(`
|
|
130
|
+
local cooldown_key = KEYS[1]
|
|
131
|
+
local value_key = KEYS[2]
|
|
132
|
+
local value = ARGV[1]
|
|
133
|
+
local new_ttl = tonumber(ARGV[2])
|
|
134
|
+
local current_ttl = redis.call('PTTL', cooldown_key)
|
|
135
|
+
local final_ttl = new_ttl
|
|
136
|
+
|
|
137
|
+
if current_ttl == -2 then
|
|
138
|
+
redis.call('SET', cooldown_key, '1', 'PX', new_ttl)
|
|
139
|
+
elseif current_ttl == -1 or current_ttl < new_ttl then
|
|
140
|
+
redis.call('PEXPIRE', cooldown_key, new_ttl)
|
|
141
|
+
else
|
|
142
|
+
final_ttl = current_ttl
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
redis.call('SET', value_key, value, 'PX', final_ttl)
|
|
146
|
+
return final_ttl
|
|
147
|
+
`, 2, cooldownRedisKey, valueRedisKey, value, cooldownMs.toString());
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
logger.warn('Failed to write stream rate-limit cooldown and metadata to Redis', {
|
|
151
|
+
cooldownKey,
|
|
152
|
+
valueKey,
|
|
153
|
+
cooldownMs,
|
|
154
|
+
error,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
121
158
|
static async clear(cooldownKey, context, options) {
|
|
122
159
|
const { redis, logger } = context;
|
|
123
160
|
try {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
2
2
|
import { BUDGET_COOLDOWN_SLEEP_CHUNK_MS, BUDGET_DELAY_MAX_MS, DEFAULT_JITTER_MS, DEFAULT_MAX_DELAY_MS, DEFAULT_RETRY_AFTER_MS, RETRY_AFTER_BUFFER_MS, STREAM_BUDGET_COOLDOWN_KEY, STREAM_BUDGET_COOLDOWN_KEY_PREFIX, STREAM_RATE_LIMIT_COOLDOWN_KEY_PREFIX, STREAM_RATE_LIMIT_METADATA_KEY_SUFFIX, STREAM_RATE_LIMIT_SOFT_COOLDOWN_KEY, } from '../constants';
|
|
3
3
|
import { createRateLimitExceededException } from './createRateLimitExceededException';
|
|
4
|
-
import { parseRateLimitSnapshot, serializeRateLimitSnapshot } from './rateLimitSnapshot';
|
|
5
4
|
import { processStreamRateLimitException } from './processStreamRateLimitException';
|
|
5
|
+
import { parseRateLimitSnapshot, serializeRateLimitSnapshot } from './rateLimitSnapshot';
|
|
6
6
|
import { RedisCooldown } from './RedisCooldown';
|
|
7
7
|
function calculateRateLimitDelayMs(error, maxDelayMs) {
|
|
8
8
|
const retryAfterMs = typeof error.rateLimitReset === 'number' && error.rateLimitReset > 0 ? error.rateLimitReset : 0;
|
|
@@ -103,9 +103,11 @@ export async function executeWithRateLimitHandling(execute, operation, context,
|
|
|
103
103
|
const retryAfterMs = calculateRateLimitDelayMs(rateLimitError, maxDelayMs);
|
|
104
104
|
if (enableCooldown) {
|
|
105
105
|
const rateLimitSnapshot = toRateLimitSnapshot(rateLimitError);
|
|
106
|
-
await RedisCooldown.setRemainingMs(operation, retryAfterMs, { redis, logger });
|
|
107
106
|
if (rateLimitSnapshot) {
|
|
108
|
-
await RedisCooldown.
|
|
107
|
+
await RedisCooldown.setRemainingMsWithValue(operation, getRateLimitMetadataKey(operation), serializeRateLimitSnapshot(rateLimitSnapshot), retryAfterMs, { redis, logger });
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
await RedisCooldown.setRemainingMs(operation, retryAfterMs, { redis, logger });
|
|
109
111
|
}
|
|
110
112
|
}
|
|
111
113
|
if (attempt < maxAttempts && retryAfterMs <= maxRetryableDelayMs) {
|
|
@@ -31,14 +31,24 @@ export function registerStreamInterceptors(stream, context) {
|
|
|
31
31
|
if (attachedInterceptors.has(axiosInstance)) {
|
|
32
32
|
return;
|
|
33
33
|
}
|
|
34
|
+
attachedInterceptors.add(axiosInstance);
|
|
34
35
|
axiosInstance.interceptors.response.use(async (response) => {
|
|
35
|
-
|
|
36
|
-
|
|
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
|
+
}
|
|
37
43
|
return response;
|
|
38
44
|
}, async (error) => {
|
|
39
|
-
|
|
40
|
-
|
|
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
|
+
}
|
|
41
52
|
return Promise.reject(error);
|
|
42
53
|
});
|
|
43
|
-
attachedInterceptors.add(axiosInstance);
|
|
44
54
|
}
|
|
@@ -6,5 +6,6 @@ export declare class RedisCooldown {
|
|
|
6
6
|
static setRemainingMsExact(cooldownKey: string, cooldownMs: number, context: RedisContext & LoggerContext, options?: RedisCooldownOptions): Promise<void>;
|
|
7
7
|
static getValue(cooldownKey: string, context: RedisContext & LoggerContext, options?: RedisCooldownOptions): Promise<string | undefined>;
|
|
8
8
|
static setValueWithRemainingMs(cooldownKey: string, value: string, cooldownMs: number, context: RedisContext & LoggerContext, options?: RedisCooldownOptions): Promise<void>;
|
|
9
|
+
static setRemainingMsWithValue(cooldownKey: string, valueKey: string, value: string, cooldownMs: number, context: RedisContext & LoggerContext, options?: RedisCooldownOptions): Promise<void>;
|
|
9
10
|
static clear(cooldownKey: string, context: RedisContext & LoggerContext, options?: RedisCooldownOptions): Promise<void>;
|
|
10
11
|
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wildix/xbees-conversations-utils",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.81",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "./dist-cjs/index.js",
|
|
6
6
|
"module": "./dist-es/index.js",
|
|
7
7
|
"types": "./dist-types/index.d.ts",
|
|
8
8
|
"scripts": {
|
|
9
|
+
"prebuild": "node ./scripts/generateRateLimitMethodNames.mjs",
|
|
9
10
|
"build": "concurrently 'yarn:build:cjs' 'yarn:build:es' 'yarn:build:types'",
|
|
10
11
|
"build:cjs": "tsc -p tsconfig.cjs.json",
|
|
11
12
|
"build:es": "tsc -p tsconfig.es.json",
|
|
@@ -14,7 +15,6 @@
|
|
|
14
15
|
"build:types:downlevel": "downlevel-dts dist-types dist-types/ts3.4",
|
|
15
16
|
"build:api-extractor": "mkdir -p etc && api-extractor run --local --verbose",
|
|
16
17
|
"check-types": "tsc --noemit",
|
|
17
|
-
"generate:rate-limit-method-names": "node ./scripts/generateRateLimitMethodNames.mjs",
|
|
18
18
|
"clean": "rimraf ./dist-* && rimraf *.tsbuildinfo",
|
|
19
19
|
"lint": "eslint . && npm run check-types",
|
|
20
20
|
"lint:fix": "eslint . --fix",
|