@wildix/xbees-conversations-utils 1.1.75 → 1.1.77

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 (31) hide show
  1. package/dist-cjs/rateLimits/constants.js +17 -62
  2. package/dist-cjs/rateLimits/createRateLimitedStreamProxy.js +20 -7
  3. package/dist-cjs/rateLimits/generatedMethodNames.js +320 -0
  4. package/dist-cjs/rateLimits/helpers/RedisCooldown.js +16 -0
  5. package/dist-cjs/rateLimits/helpers/createRateLimitExceededException.js +5 -5
  6. package/dist-cjs/rateLimits/helpers/executeWithRateLimitHandling.js +21 -35
  7. package/dist-cjs/rateLimits/helpers/headerUtils.js +40 -0
  8. package/dist-cjs/rateLimits/helpers/isStreamChannelLike.js +9 -1
  9. package/dist-cjs/rateLimits/helpers/processStreamBudgetHeaders.js +41 -35
  10. package/dist-cjs/rateLimits/helpers/processStreamRateLimitException.js +7 -18
  11. package/dist-cjs/rateLimits/helpers/processStreamRateLimitHeaders.js +4 -28
  12. package/dist-cjs/rateLimits/helpers/registerStreamInterceptors.js +48 -0
  13. package/dist-es/rateLimits/constants.js +9 -61
  14. package/dist-es/rateLimits/createRateLimitedStreamProxy.js +21 -8
  15. package/dist-es/rateLimits/generatedMethodNames.js +317 -0
  16. package/dist-es/rateLimits/helpers/RedisCooldown.js +16 -0
  17. package/dist-es/rateLimits/helpers/createRateLimitExceededException.js +5 -5
  18. package/dist-es/rateLimits/helpers/executeWithRateLimitHandling.js +22 -36
  19. package/dist-es/rateLimits/helpers/headerUtils.js +34 -0
  20. package/dist-es/rateLimits/helpers/isStreamChannelLike.js +9 -1
  21. package/dist-es/rateLimits/helpers/processStreamBudgetHeaders.js +42 -36
  22. package/dist-es/rateLimits/helpers/processStreamRateLimitException.js +5 -16
  23. package/dist-es/rateLimits/helpers/processStreamRateLimitHeaders.js +4 -28
  24. package/dist-es/rateLimits/helpers/registerStreamInterceptors.js +44 -0
  25. package/dist-types/rateLimits/constants.d.ts +8 -4
  26. package/dist-types/rateLimits/generatedMethodNames.d.ts +11 -0
  27. package/dist-types/rateLimits/helpers/RedisCooldown.d.ts +1 -0
  28. package/dist-types/rateLimits/helpers/headerUtils.d.ts +4 -0
  29. package/dist-types/rateLimits/helpers/processStreamBudgetHeaders.d.ts +2 -1
  30. package/dist-types/rateLimits/helpers/registerStreamInterceptors.d.ts +2 -0
  31. package/package.json +2 -1
@@ -1,31 +1,6 @@
1
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';
2
3
  import { RedisCooldown } from './RedisCooldown';
3
- function parseHeaderNumber(value) {
4
- if (typeof value === 'number' && Number.isFinite(value)) {
5
- return value;
6
- }
7
- if (typeof value === 'string') {
8
- const parsed = Number(value);
9
- if (Number.isFinite(parsed)) {
10
- return parsed;
11
- }
12
- }
13
- }
14
- function getHeaderCaseInsensitive(headers, key) {
15
- if (!headers) {
16
- return;
17
- }
18
- const direct = headers[key];
19
- if (direct !== undefined) {
20
- return direct;
21
- }
22
- const normalizedKey = key.toLowerCase();
23
- for (const [headerKey, headerValue] of Object.entries(headers)) {
24
- if (headerKey.toLowerCase() === normalizedKey) {
25
- return headerValue;
26
- }
27
- }
28
- }
29
4
  function computeSoftThrottleDelayMs(limit, remaining) {
30
5
  if (limit <= 0) {
31
6
  return 0;
@@ -44,8 +19,8 @@ function computeSoftThrottleDelayMs(limit, remaining) {
44
19
  }
45
20
  export async function processStreamRateLimitHeaders(headers, context) {
46
21
  const { logger } = context;
47
- const limit = parseHeaderNumber(getHeaderCaseInsensitive(headers, 'x-ratelimit-limit'));
48
- const remaining = parseHeaderNumber(getHeaderCaseInsensitive(headers, 'x-ratelimit-remaining'));
22
+ const limit = parseHeaderNumber(headers ?? {}, 'x-ratelimit-limit');
23
+ const remaining = parseHeaderNumber(headers ?? {}, 'x-ratelimit-remaining');
49
24
  if (limit === undefined || remaining === undefined) {
50
25
  return;
51
26
  }
@@ -67,4 +42,5 @@ export async function processStreamRateLimitHeaders(headers, context) {
67
42
  await RedisCooldown.clear(STREAM_RATE_LIMIT_SOFT_COOLDOWN_KEY, context, {
68
43
  keyPrefix: STREAM_RATE_LIMIT_COOLDOWN_KEY_PREFIX,
69
44
  });
45
+ logger.debug('Clearing soft stream rate-limit cooldown');
70
46
  }
@@ -0,0 +1,44 @@
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
+ 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,4 +1,5 @@
1
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';
2
3
  export declare const DEFAULT_MAX_DELAY_MS = 5000;
3
4
  export declare const DEFAULT_RETRY_AFTER_MS = 10000;
4
5
  export declare const DEFAULT_JITTER_MS = 150;
@@ -11,14 +12,17 @@ export declare const STREAM_BUDGET_COOLDOWN_KEY = "stream-budget-global";
11
12
  export declare const BUDGET_THRESHOLD = 0.8;
12
13
  export declare const BUDGET_DELAY_MIN_MS = 30000;
13
14
  export declare const BUDGET_DELAY_MAX_MS = 60000;
15
+ export declare const BUDGET_CLEAR_USAGE_THRESHOLD = 0.6;
16
+ export declare const BUDGET_RELAXED_USAGE_THRESHOLD = 0.7;
17
+ export declare const BUDGET_RELAXED_DELAY_MIN_MS = 5000;
18
+ export declare const BUDGET_RELAXED_DELAY_MAX_MS = 10000;
19
+ export declare const BUDGET_MINIMAL_DELAY_MIN_MS = 1000;
20
+ export declare const BUDGET_MINIMAL_DELAY_MAX_MS = 2000;
21
+ export declare const BUDGET_COOLDOWN_SLEEP_CHUNK_MS = 3000;
14
22
  export declare const SOFT_THROTTLE_USAGE_LOW = 0.7;
15
23
  export declare const SOFT_THROTTLE_USAGE_MEDIUM = 0.85;
16
24
  export declare const SOFT_THROTTLE_USAGE_HIGH = 0.95;
17
25
  export declare const SOFT_THROTTLE_DELAY_LOW_MS = 500;
18
26
  export declare const SOFT_THROTTLE_DELAY_MEDIUM_MS = 1500;
19
27
  export declare const SOFT_THROTTLE_DELAY_HIGH_MS = 3000;
20
- export declare const STREAM_SYNC_CHANNEL_RESULT_METHOD_NAMES: readonly ["channel", "getChannelById", "getChannelByMembers", "hydrateActiveChannels"];
21
- export declare const STREAM_SYNC_METHOD_NAMES: readonly ["_addChannelConfig", "_buildWSPayload", "_callClientListeners", "_deleteUserMessageReference", "_enrichAxiosOptions", "_getConnectionID", "_getToken", "_handleClientEvent", "_handleUserEvent", "_hasConnectionID", "_isUsingServerAuth", "_logApiError", "_logApiRequest", "_logApiResponse", "_muteStatus", "_normalizeDate", "_normalizeExpiration", "_sayHi", "_setUser", "_startCleaning", "_updateMemberWatcherReferences", "_updateUserMessageReferences", "_updateUserReferences", "_validateAndGetMessageId", "createAbortControllerForNextRequest", "createToken", "devToken", "dispatchEvent", "errorFromResponse", "getAuthType", "getUserAgent", "handleEvent", "handleResponse", "off", "on", "setBaseURL", "setLocalDevice", "setUserAgent", "userMuteStatus", "verifyWebhook"];
22
- export declare const CHANNEL_SYNC_METHOD_NAMES: readonly ["_channelURL", "countUnread", "countUnreadMentions", "getConfig", "lastMessage", "lastRead", "muteStatus", "off", "on"];
23
- export declare const CHANNEL_SYNC_PROXY_RESULT_METHOD_NAMES: readonly ["getClient"];
24
28
  export declare const DEFAULT_OPTIONS: StreamRateLimitRetryOptions;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * This file is auto-generated by `scripts/generateRateLimitMethodNames.mjs`.
3
+ * Do not edit manually.
4
+ */
5
+ export declare const STREAM_METHOD_NAMES: readonly ["_addChannelConfig", "_buildWSPayload", "_callClientListeners", "_createImport", "_createImportURL", "_deleteUserMessageReference", "_enrichAxiosOptions", "_getConnectionID", "_getImport", "_getToken", "_handleClientEvent", "_handleUserEvent", "_hasConnectionID", "_isUsingServerAuth", "_listImports", "_logApiError", "_logApiRequest", "_logApiResponse", "_muteStatus", "_normalizeDate", "_normalizeExpiration", "_queryFlagReports", "_queryFlags", "_reviewFlagReport", "_sayHi", "_setToken", "_setupConnection", "_setUser", "_startCleaning", "_unblockMessage", "_updateMemberWatcherReferences", "_updateUserMessageReferences", "_updateUserReferences", "_validateAndGetMessageId", "addDevice", "banUser", "channel", "closeConnection", "commitMessage", "connect", "connectAnonymousUser", "connectUser", "createAbortControllerForNextRequest", "createBlockList", "createCampaign", "createChannelType", "createCommand", "createPermission", "createRole", "createSegment", "createToken", "deactivateUser", "deactivateUsers", "delete", "deleteBlockList", "deleteCampaign", "deleteChannels", "deleteChannelType", "deleteCommand", "deleteMessage", "deletePermission", "deletePushProvider", "deleteRole", "deleteSegment", "deleteUser", "deleteUsers", "devToken", "disconnect", "disconnectUser", "dispatchEvent", "doAxiosRequest", "enrichURL", "errorFromResponse", "exportChannel", "exportChannels", "exportUser", "exportUsers", "flagMessage", "flagUser", "get", "getAppSettings", "getAuthType", "getBlockList", "getCallToken", "getChannelById", "getChannelByMembers", "getChannelType", "getCommand", "getDevices", "getExportChannelStatus", "getMessage", "getPermission", "getRateLimits", "getTask", "getUnreadCount", "getUserAgent", "handleEvent", "handleResponse", "hydrateActiveChannels", "listBlockLists", "listChannelTypes", "listCommands", "listPermissions", "listPushProviders", "listRoles", "markAllRead", "markChannelsRead", "muteUser", "off", "on", "openConnection", "partialUpdateMessage", "partialUpdateUser", "partialUpdateUsers", "patch", "pinMessage", "post", "put", "queryBannedUsers", "queryCampaigns", "queryChannels", "queryMessageFlags", "queryRecipients", "querySegments", "queryUsers", "reactivateUser", "reactivateUsers", "recoverState", "removeDevice", "removeShadowBan", "restoreUsers", "resumeCampaign", "revokeTokens", "revokeUsersToken", "revokeUserToken", "scheduleCampaign", "search", "sendFile", "sendUserCustomEvent", "setAnonymousUser", "setBaseURL", "setGuestUser", "setLocalDevice", "setUser", "setUserAgent", "shadowBan", "stopCampaign", "sync", "testCampaign", "testPushSettings", "testSQSSettings", "translateMessage", "unbanUser", "unblockMessage", "unflagMessage", "unflagUser", "unmuteUser", "unpinMessage", "updateAppSettings", "updateBlockList", "updateCampaign", "updateChannelType", "updateCommand", "updateMessage", "updatePermission", "updateSegment", "updateUser", "updateUsers", "upsertPushProvider", "upsertUser", "upsertUsers", "userMuteStatus", "verifyWebhook"];
6
+ export declare const STREAM_SYNC_METHOD_NAMES: readonly ["_addChannelConfig", "_buildWSPayload", "_callClientListeners", "_deleteUserMessageReference", "_enrichAxiosOptions", "_getConnectionID", "_getToken", "_handleClientEvent", "_handleUserEvent", "_hasConnectionID", "_isUsingServerAuth", "_logApiError", "_logApiRequest", "_logApiResponse", "_muteStatus", "_normalizeDate", "_normalizeExpiration", "_sayHi", "_setUser", "_startCleaning", "_updateMemberWatcherReferences", "_updateUserMessageReferences", "_updateUserReferences", "_validateAndGetMessageId", "createAbortControllerForNextRequest", "createToken", "devToken", "dispatchEvent", "errorFromResponse", "getAuthType", "getUserAgent", "handleEvent", "handleResponse", "off", "on", "setBaseURL", "setLocalDevice", "setUserAgent", "userMuteStatus", "verifyWebhook"];
7
+ export declare const STREAM_CHANNEL_RESULT_METHOD_NAMES: readonly ["channel", "getChannelById", "getChannelByMembers", "hydrateActiveChannels", "queryChannels"];
8
+ export declare const STREAM_SYNC_CHANNEL_RESULT_METHOD_NAMES: readonly ["channel", "getChannelById", "getChannelByMembers", "hydrateActiveChannels"];
9
+ export declare const CHANNEL_METHOD_NAMES: readonly ["_callChannelListeners", "_channelURL", "_checkInitialized", "_countMessageAsUnread", "_disconnect", "_extendEventWithOwnReactions", "_handleChannelEvent", "_initializeState", "_update", "acceptInvite", "addMembers", "addModerators", "assignRoles", "banUser", "clean", "countUnread", "countUnreadMentions", "create", "createCall", "delete", "deleteFile", "deleteImage", "deleteReaction", "demoteModerators", "disableSlowMode", "enableSlowMode", "getClient", "getConfig", "getMessagesById", "getPinnedMessages", "getReactions", "getReplies", "hide", "inviteMembers", "keystroke", "lastMessage", "lastRead", "markRead", "markUnread", "mute", "muteStatus", "off", "on", "query", "queryMembers", "rejectInvite", "removeMembers", "removeShadowBan", "search", "sendAction", "sendEvent", "sendFile", "sendImage", "sendMessage", "sendReaction", "shadowBan", "show", "stopTyping", "stopWatching", "truncate", "unbanUser", "unmute", "update", "updatePartial", "watch"];
10
+ export declare const CHANNEL_SYNC_METHOD_NAMES: readonly ["_callChannelListeners", "_channelURL", "_checkInitialized", "_countMessageAsUnread", "_disconnect", "_extendEventWithOwnReactions", "_handleChannelEvent", "_initializeState", "clean", "countUnread", "countUnreadMentions", "getConfig", "lastMessage", "lastRead", "muteStatus", "off", "on"];
11
+ export declare const CHANNEL_SYNC_PROXY_RESULT_METHOD_NAMES: readonly ["getClient"];
@@ -3,5 +3,6 @@ export declare class RedisCooldown {
3
3
  private static resolveRedisKey;
4
4
  static getRemainingMs(cooldownKey: string, context: RedisContext & LoggerContext, options?: RedisCooldownOptions): Promise<number>;
5
5
  static setRemainingMs(cooldownKey: string, cooldownMs: number, context: RedisContext & LoggerContext, options?: RedisCooldownOptions): Promise<void>;
6
+ static setRemainingMsExact(cooldownKey: string, cooldownMs: number, context: RedisContext & LoggerContext, options?: RedisCooldownOptions): Promise<void>;
6
7
  static clear(cooldownKey: string, context: RedisContext & LoggerContext, options?: RedisCooldownOptions): Promise<void>;
7
8
  }
@@ -0,0 +1,4 @@
1
+ import { StreamHeaders } from '../types';
2
+ export declare function getHeaderCaseInsensitive(headers: StreamHeaders | Record<string, unknown> | undefined, key: string): unknown;
3
+ export declare function parseHeaderNumber(headers: StreamHeaders | Record<string, unknown>, key: string): number | undefined;
4
+ export declare function normalizeResetToEpochMs(reset: number): number;
@@ -2,7 +2,8 @@ import { LoggerContext, RedisContext, StreamHeaders } from '../types';
2
2
  /**
3
3
  * Updates app-wide Stream budget cooldown in Redis based on x-budget-* headers.
4
4
  * - If budget usage reaches the threshold (>=80%), enables cooldown for 30-60s.
5
- * - If headers indicate usage is below threshold, disables existing cooldown.
5
+ * - If usage is between 60% and 80%, keeps a shorter cooldown.
6
+ * - If usage drops below 60%, clears cooldown.
6
7
  * - If headers are missing/invalid, leaves current cooldown unchanged.
7
8
  */
8
9
  export declare function processStreamBudgetHeaders(headers: StreamHeaders | undefined, context: LoggerContext & RedisContext): Promise<void>;
@@ -0,0 +1,2 @@
1
+ import { LoggerContext, RedisContext } from '../types';
2
+ export declare function registerStreamInterceptors(stream: unknown, context: LoggerContext & RedisContext): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wildix/xbees-conversations-utils",
3
- "version": "1.1.75",
3
+ "version": "1.1.77",
4
4
  "description": "",
5
5
  "main": "./dist-cjs/index.js",
6
6
  "module": "./dist-es/index.js",
@@ -14,6 +14,7 @@
14
14
  "build:types:downlevel": "downlevel-dts dist-types dist-types/ts3.4",
15
15
  "build:api-extractor": "mkdir -p etc && api-extractor run --local --verbose",
16
16
  "check-types": "tsc --noemit",
17
+ "generate:rate-limit-method-names": "node ./scripts/generateRateLimitMethodNames.mjs",
17
18
  "clean": "rimraf ./dist-* && rimraf *.tsbuildinfo",
18
19
  "lint": "eslint . && npm run check-types",
19
20
  "lint:fix": "eslint . --fix",