@lazyneoaz/testfca 1.0.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.
Files changed (120) hide show
  1. package/CHANGELOG.md +229 -0
  2. package/COOKIE_LOGIN.md +208 -0
  3. package/LICENSE +3 -0
  4. package/README.md +492 -0
  5. package/index.js +2 -0
  6. package/package.json +120 -0
  7. package/scripts/build-go.mjs +54 -0
  8. package/scripts/detect-platform.mjs +36 -0
  9. package/scripts/download-prebuilt.mjs +119 -0
  10. package/scripts/package.mjs +6 -0
  11. package/scripts/postinstall.mjs +113 -0
  12. package/src/apis/addExternalModule.js +24 -0
  13. package/src/apis/addUserToGroup.js +108 -0
  14. package/src/apis/changeAdminStatus.js +148 -0
  15. package/src/apis/changeArchivedStatus.js +61 -0
  16. package/src/apis/changeAvatar.js +103 -0
  17. package/src/apis/changeBio.js +69 -0
  18. package/src/apis/changeBlockedStatus.js +54 -0
  19. package/src/apis/changeGroupImage.js +136 -0
  20. package/src/apis/changeThreadColor.js +116 -0
  21. package/src/apis/changeThreadEmoji.js +53 -0
  22. package/src/apis/comment.js +207 -0
  23. package/src/apis/createAITheme.js +129 -0
  24. package/src/apis/createNewGroup.js +79 -0
  25. package/src/apis/createPoll.js +73 -0
  26. package/src/apis/deleteMessage.js +52 -0
  27. package/src/apis/deleteThread.js +52 -0
  28. package/src/apis/e2ee.js +170 -0
  29. package/src/apis/editMessage.js +78 -0
  30. package/src/apis/emoji.js +124 -0
  31. package/src/apis/fetchThemeData.js +82 -0
  32. package/src/apis/follow.js +81 -0
  33. package/src/apis/forwardMessage.js +52 -0
  34. package/src/apis/friend.js +243 -0
  35. package/src/apis/gcmember.js +122 -0
  36. package/src/apis/gcname.js +123 -0
  37. package/src/apis/gcrule.js +119 -0
  38. package/src/apis/getAccess.js +111 -0
  39. package/src/apis/getBotInfo.js +88 -0
  40. package/src/apis/getBotInitialData.js +43 -0
  41. package/src/apis/getFriendsList.js +79 -0
  42. package/src/apis/getMessage.js +423 -0
  43. package/src/apis/getTheme.js +95 -0
  44. package/src/apis/getThemeInfo.js +116 -0
  45. package/src/apis/getThreadHistory.js +239 -0
  46. package/src/apis/getThreadInfo.js +267 -0
  47. package/src/apis/getThreadList.js +232 -0
  48. package/src/apis/getThreadPictures.js +58 -0
  49. package/src/apis/getUserID.js +117 -0
  50. package/src/apis/getUserInfo.js +513 -0
  51. package/src/apis/getUserInfoV2.js +146 -0
  52. package/src/apis/handleMessageRequest.js +50 -0
  53. package/src/apis/httpGet.js +63 -0
  54. package/src/apis/httpPost.js +89 -0
  55. package/src/apis/httpPostFormData.js +69 -0
  56. package/src/apis/listenMqtt.js +1236 -0
  57. package/src/apis/listenSpeed.js +179 -0
  58. package/src/apis/logout.js +93 -0
  59. package/src/apis/markAsDelivered.js +47 -0
  60. package/src/apis/markAsRead.js +115 -0
  61. package/src/apis/markAsReadAll.js +40 -0
  62. package/src/apis/markAsSeen.js +70 -0
  63. package/src/apis/mqttDeltaValue.js +250 -0
  64. package/src/apis/muteThread.js +45 -0
  65. package/src/apis/nickname.js +132 -0
  66. package/src/apis/notes.js +163 -0
  67. package/src/apis/pinMessage.js +150 -0
  68. package/src/apis/produceMetaTheme.js +180 -0
  69. package/src/apis/realtime.js +182 -0
  70. package/src/apis/removeUserFromGroup.js +117 -0
  71. package/src/apis/resolvePhotoUrl.js +58 -0
  72. package/src/apis/searchForThread.js +154 -0
  73. package/src/apis/sendMessage.js +346 -0
  74. package/src/apis/sendMessageMqtt.js +248 -0
  75. package/src/apis/sendTypingIndicator.js +105 -0
  76. package/src/apis/setMessageReaction.js +38 -0
  77. package/src/apis/setMessageReactionMqtt.js +61 -0
  78. package/src/apis/setThreadTheme.js +260 -0
  79. package/src/apis/setThreadThemeMqtt.js +94 -0
  80. package/src/apis/share.js +107 -0
  81. package/src/apis/shareContact.js +66 -0
  82. package/src/apis/stickers.js +257 -0
  83. package/src/apis/story.js +181 -0
  84. package/src/apis/theme.js +233 -0
  85. package/src/apis/unfriend.js +47 -0
  86. package/src/apis/unsendMessage.js +25 -0
  87. package/src/database/appStateBackup.js +298 -0
  88. package/src/database/models/index.js +56 -0
  89. package/src/database/models/thread.js +31 -0
  90. package/src/database/models/user.js +32 -0
  91. package/src/database/threadData.js +101 -0
  92. package/src/database/userData.js +90 -0
  93. package/src/e2ee/bridge.js +275 -0
  94. package/src/e2ee/index.js +60 -0
  95. package/src/engine/client.js +95 -0
  96. package/src/engine/models/buildAPI.js +152 -0
  97. package/src/engine/models/loginHelper.js +574 -0
  98. package/src/engine/models/setOptions.js +88 -0
  99. package/src/types/index.d.ts +574 -0
  100. package/src/utils/antiSuspension.js +529 -0
  101. package/src/utils/auth-helpers.js +149 -0
  102. package/src/utils/autoReLogin.js +336 -0
  103. package/src/utils/axios.js +436 -0
  104. package/src/utils/cache.js +54 -0
  105. package/src/utils/clients.js +282 -0
  106. package/src/utils/constants.js +410 -0
  107. package/src/utils/formatters/data/formatAttachment.js +370 -0
  108. package/src/utils/formatters/data/formatDelta.js +109 -0
  109. package/src/utils/formatters/index.js +159 -0
  110. package/src/utils/formatters/value/formatCookie.js +91 -0
  111. package/src/utils/formatters/value/formatDate.js +36 -0
  112. package/src/utils/formatters/value/formatID.js +16 -0
  113. package/src/utils/formatters.js +1373 -0
  114. package/src/utils/headers.js +235 -0
  115. package/src/utils/index.js +153 -0
  116. package/src/utils/monitoring.js +333 -0
  117. package/src/utils/rateLimiter.js +319 -0
  118. package/src/utils/tokenRefresh.js +680 -0
  119. package/src/utils/user-agents.js +238 -0
  120. package/src/utils/validation.js +157 -0
@@ -0,0 +1,333 @@
1
+ "use strict";
2
+
3
+ const utils = require('./index');
4
+
5
+ class ProductionMonitor {
6
+ constructor() {
7
+ this.metrics = {
8
+ requests: {
9
+ total: 0,
10
+ success: 0,
11
+ failed: 0,
12
+ byEndpoint: new Map()
13
+ },
14
+ errors: {
15
+ total: 0,
16
+ byType: new Map(),
17
+ byCode: new Map(),
18
+ recent: []
19
+ },
20
+ performance: {
21
+ avgResponseTime: 0,
22
+ slowRequests: [],
23
+ requestTimes: []
24
+ },
25
+ session: {
26
+ loginTime: null,
27
+ lastActivity: null,
28
+ tokenRefreshCount: 0,
29
+ reconnectCount: 0
30
+ },
31
+ rateLimiting: {
32
+ hitCount: 0,
33
+ cooldowns: 0,
34
+ delayedRequests: 0
35
+ }
36
+ };
37
+
38
+ this.config = {
39
+ logLevel: 'info',
40
+ enableMetrics: true,
41
+ enableErrorTracking: true,
42
+ performanceThreshold: 5000,
43
+ errorRetentionCount: 100,
44
+ metricsInterval: 60000
45
+ };
46
+
47
+ this.startTime = Date.now();
48
+ this.metricsInterval = null;
49
+ }
50
+
51
+ setConfig(options) {
52
+ Object.assign(this.config, options);
53
+ }
54
+
55
+ trackRequest(endpoint, success, responseTime, error = null) {
56
+ if (!this.config.enableMetrics) return;
57
+
58
+ this.metrics.requests.total++;
59
+ if (success) {
60
+ this.metrics.requests.success++;
61
+ } else {
62
+ this.metrics.requests.failed++;
63
+ }
64
+
65
+ if (!this.metrics.requests.byEndpoint.has(endpoint)) {
66
+ this.metrics.requests.byEndpoint.set(endpoint, {
67
+ total: 0,
68
+ success: 0,
69
+ failed: 0,
70
+ avgTime: 0
71
+ });
72
+ }
73
+
74
+ const endpointStats = this.metrics.requests.byEndpoint.get(endpoint);
75
+ endpointStats.total++;
76
+ if (success) {
77
+ endpointStats.success++;
78
+ } else {
79
+ endpointStats.failed++;
80
+ }
81
+
82
+ endpointStats.avgTime =
83
+ (endpointStats.avgTime * (endpointStats.total - 1) + responseTime) / endpointStats.total;
84
+
85
+ this.trackPerformance(endpoint, responseTime);
86
+
87
+ if (error) {
88
+ this.trackError(error, endpoint);
89
+ }
90
+
91
+ this.metrics.session.lastActivity = Date.now();
92
+ }
93
+
94
+ trackPerformance(endpoint, responseTime) {
95
+ this.metrics.performance.requestTimes.push(responseTime);
96
+
97
+ if (this.metrics.performance.requestTimes.length > 1000) {
98
+ this.metrics.performance.requestTimes.shift();
99
+ }
100
+
101
+ const sum = this.metrics.performance.requestTimes.reduce((a, b) => a + b, 0);
102
+ this.metrics.performance.avgResponseTime =
103
+ sum / this.metrics.performance.requestTimes.length;
104
+
105
+ if (responseTime > this.config.performanceThreshold) {
106
+ this.metrics.performance.slowRequests.push({
107
+ endpoint,
108
+ responseTime,
109
+ timestamp: Date.now()
110
+ });
111
+
112
+ if (this.metrics.performance.slowRequests.length > 50) {
113
+ this.metrics.performance.slowRequests.shift();
114
+ }
115
+
116
+ utils.warn("Performance", `Slow request: ${endpoint} took ${responseTime}ms`);
117
+ }
118
+ }
119
+
120
+ trackError(error, context = '') {
121
+ if (!this.config.enableErrorTracking) return;
122
+
123
+ this.metrics.errors.total++;
124
+
125
+ const errorType = error.errorType || error.name || 'UnknownError';
126
+ const errorCode = error.errorCode || error.code || 'N/A';
127
+
128
+ this.metrics.errors.byType.set(
129
+ errorType,
130
+ (this.metrics.errors.byType.get(errorType) || 0) + 1
131
+ );
132
+
133
+ this.metrics.errors.byCode.set(
134
+ errorCode,
135
+ (this.metrics.errors.byCode.get(errorCode) || 0) + 1
136
+ );
137
+
138
+ this.metrics.errors.recent.push({
139
+ type: errorType,
140
+ code: errorCode,
141
+ message: error.message,
142
+ context,
143
+ timestamp: Date.now(),
144
+ stack: error.stack
145
+ });
146
+
147
+ if (this.metrics.errors.recent.length > this.config.errorRetentionCount) {
148
+ this.metrics.errors.recent.shift();
149
+ }
150
+ }
151
+
152
+ trackRateLimit(type, threadID = null) {
153
+ this.metrics.rateLimiting.hitCount++;
154
+
155
+ if (type === 'cooldown') {
156
+ this.metrics.rateLimiting.cooldowns++;
157
+ } else if (type === 'delayed') {
158
+ this.metrics.rateLimiting.delayedRequests++;
159
+ }
160
+ }
161
+
162
+ trackTokenRefresh() {
163
+ this.metrics.session.tokenRefreshCount++;
164
+ }
165
+
166
+ trackReconnect() {
167
+ this.metrics.session.reconnectCount++;
168
+ }
169
+
170
+ setLoginTime() {
171
+ this.metrics.session.loginTime = Date.now();
172
+ }
173
+
174
+ getMetrics() {
175
+ const uptime = Date.now() - this.startTime;
176
+ const sessionDuration = this.metrics.session.loginTime
177
+ ? Date.now() - this.metrics.session.loginTime
178
+ : 0;
179
+
180
+ return {
181
+ uptime,
182
+ sessionDuration,
183
+ requests: {
184
+ ...this.metrics.requests,
185
+ byEndpoint: Object.fromEntries(this.metrics.requests.byEndpoint),
186
+ successRate: this.metrics.requests.total > 0
187
+ ? (this.metrics.requests.success / this.metrics.requests.total * 100).toFixed(2) + '%'
188
+ : 'N/A'
189
+ },
190
+ errors: {
191
+ ...this.metrics.errors,
192
+ byType: Object.fromEntries(this.metrics.errors.byType),
193
+ byCode: Object.fromEntries(this.metrics.errors.byCode),
194
+ errorRate: this.metrics.requests.total > 0
195
+ ? (this.metrics.errors.total / this.metrics.requests.total * 100).toFixed(2) + '%'
196
+ : 'N/A'
197
+ },
198
+ performance: this.metrics.performance,
199
+ session: this.metrics.session,
200
+ rateLimiting: this.metrics.rateLimiting
201
+ };
202
+ }
203
+
204
+ getHealth() {
205
+ const metrics = this.getMetrics();
206
+ const health = {
207
+ status: 'healthy',
208
+ checks: {},
209
+ timestamp: Date.now()
210
+ };
211
+
212
+ const errorRate = this.metrics.requests.total > 0
213
+ ? (this.metrics.errors.total / this.metrics.requests.total) * 100
214
+ : 0;
215
+
216
+ health.checks.errorRate = {
217
+ status: errorRate < 5 ? 'pass' : errorRate < 15 ? 'warn' : 'fail',
218
+ value: errorRate.toFixed(2) + '%',
219
+ threshold: '5%'
220
+ };
221
+
222
+ health.checks.performance = {
223
+ status: this.metrics.performance.avgResponseTime < 2000 ? 'pass' :
224
+ this.metrics.performance.avgResponseTime < 5000 ? 'warn' : 'fail',
225
+ value: Math.round(this.metrics.performance.avgResponseTime) + 'ms',
226
+ threshold: '2000ms'
227
+ };
228
+
229
+ health.checks.session = {
230
+ status: this.metrics.session.loginTime ? 'pass' : 'fail',
231
+ value: this.metrics.session.loginTime ? 'active' : 'not logged in'
232
+ };
233
+
234
+ health.checks.rateLimiting = {
235
+ status: this.metrics.rateLimiting.hitCount < 100 ? 'pass' :
236
+ this.metrics.rateLimiting.hitCount < 500 ? 'warn' : 'fail',
237
+ value: this.metrics.rateLimiting.hitCount,
238
+ threshold: '100 hits'
239
+ };
240
+
241
+ const failedChecks = Object.values(health.checks).filter(c => c.status === 'fail').length;
242
+ const warnChecks = Object.values(health.checks).filter(c => c.status === 'warn').length;
243
+
244
+ if (failedChecks > 0) {
245
+ health.status = 'unhealthy';
246
+ } else if (warnChecks > 0) {
247
+ health.status = 'degraded';
248
+ }
249
+
250
+ return health;
251
+ }
252
+
253
+ reset() {
254
+ this.metrics.requests.total = 0;
255
+ this.metrics.requests.success = 0;
256
+ this.metrics.requests.failed = 0;
257
+ this.metrics.requests.byEndpoint.clear();
258
+ this.metrics.errors.total = 0;
259
+ this.metrics.errors.byType.clear();
260
+ this.metrics.errors.byCode.clear();
261
+ this.metrics.errors.recent = [];
262
+ this.metrics.performance.slowRequests = [];
263
+ this.metrics.performance.requestTimes = [];
264
+ this.metrics.rateLimiting.hitCount = 0;
265
+ this.metrics.rateLimiting.cooldowns = 0;
266
+ this.metrics.rateLimiting.delayedRequests = 0;
267
+
268
+ utils.success("Monitoring", "All metrics have been reset successfully");
269
+ }
270
+
271
+ displayHealthStatus() {
272
+ const health = this.getHealth();
273
+ console.log('\n' + '='.repeat(60));
274
+ if (health.status === 'healthy') {
275
+ console.log('SYSTEM HEALTH: HEALTHY');
276
+ } else if (health.status === 'degraded') {
277
+ console.log('SYSTEM HEALTH: DEGRADED');
278
+ } else {
279
+ console.log('SYSTEM HEALTH: UNHEALTHY');
280
+ }
281
+ console.log('='.repeat(60));
282
+ Object.entries(health.checks).forEach(([name, check]) => {
283
+ const nameFormatted = name.charAt(0).toUpperCase() + name.slice(1);
284
+ console.log(`\n${nameFormatted}:`);
285
+ console.log(` Status: ${check.status.toUpperCase()}`);
286
+ console.log(` Value: ${check.value}`);
287
+ if (check.threshold) {
288
+ console.log(` Threshold: ${check.threshold}`);
289
+ }
290
+ });
291
+ console.log('\n' + '='.repeat(60) + '\n');
292
+ return health;
293
+ }
294
+
295
+ startPeriodicReporting(interval = 60000) {
296
+ if (this.metricsInterval) {
297
+ clearInterval(this.metricsInterval);
298
+ }
299
+
300
+ this.metricsInterval = setInterval(() => {
301
+ const metrics = this.getMetrics();
302
+ console.log('\n' + '='.repeat(60));
303
+ console.log('PERFORMANCE METRICS REPORT');
304
+ console.log('='.repeat(60));
305
+ console.log('\nRequests:');
306
+ console.log(` Total: ${metrics.requests.total}`);
307
+ console.log(` Success Rate: ${metrics.requests.successRate}`);
308
+ console.log('\nErrors:');
309
+ console.log(` Total: ${metrics.errors.total}`);
310
+ console.log(` Error Rate: ${metrics.errors.errorRate}`);
311
+ console.log('\nPerformance:');
312
+ const avgTime = Math.round(metrics.performance.avgResponseTime);
313
+ console.log(` Avg Response Time: ${avgTime}ms`);
314
+ console.log('\nRate Limiting:');
315
+ console.log(` Total Hits: ${metrics.rateLimiting.hitCount}`);
316
+ console.log('='.repeat(60) + '\n');
317
+ }, interval);
318
+ }
319
+
320
+ stopPeriodicReporting() {
321
+ if (this.metricsInterval) {
322
+ clearInterval(this.metricsInterval);
323
+ this.metricsInterval = null;
324
+ }
325
+ }
326
+ }
327
+
328
+ const globalMonitor = new ProductionMonitor();
329
+
330
+ module.exports = {
331
+ ProductionMonitor,
332
+ globalMonitor
333
+ };
@@ -0,0 +1,319 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Adaptive Rate Limiting Manager - Optimized for Performance
5
+ * Sliding-window per-minute and per-second rate limiting to prevent
6
+ * Facebook from flagging automated behaviour.
7
+ */
8
+
9
+ class RateLimiter {
10
+ constructor() {
11
+ this.threadCooldowns = new Map();
12
+ this.endpointCooldowns = new Map();
13
+ this.errorCache = new Map();
14
+
15
+ this.ERROR_CACHE_TTL = 300000;
16
+ this.COOLDOWN_DURATION = 60000;
17
+ this.MAX_REQUESTS_PER_MINUTE = 80; // Increased from 50 for better throughput
18
+ this.MAX_CONCURRENT_REQUESTS = 8; // Increased from 5
19
+
20
+ this.activeRequests = 0;
21
+
22
+ // Sliding window: store timestamps of recent requests
23
+ this._requestWindow = [];
24
+ this._WINDOW_MS = 60000;
25
+
26
+ // Per-endpoint sliding windows
27
+ this._endpointWindows = new Map();
28
+ this._MAX_PER_ENDPOINT_PER_MINUTE = 30; // Increased from 20
29
+
30
+ // Fast path cache for cooldown checks
31
+ this._cooldownCache = new Map();
32
+ this._COOLDOWN_CACHE_TTL = 500; // 500ms cache for cooldown checks
33
+ }
34
+
35
+ configure(opts = {}) {
36
+ if (typeof opts.maxConcurrentRequests === 'number' && opts.maxConcurrentRequests > 0 && opts.maxConcurrentRequests <= 50) {
37
+ this.MAX_CONCURRENT_REQUESTS = Math.floor(opts.maxConcurrentRequests);
38
+ }
39
+ if (typeof opts.maxRequestsPerMinute === 'number' && opts.maxRequestsPerMinute > 0 && opts.maxRequestsPerMinute <= 1000) {
40
+ this.MAX_REQUESTS_PER_MINUTE = Math.floor(opts.maxRequestsPerMinute);
41
+ }
42
+ if (typeof opts.requestCooldownMs === 'number' && opts.requestCooldownMs >= 0 && opts.requestCooldownMs <= 10 * 60 * 1000) {
43
+ this.COOLDOWN_DURATION = Math.floor(opts.requestCooldownMs);
44
+ }
45
+ if (typeof opts.errorCacheTtlMs === 'number' && opts.errorCacheTtlMs >= 0 && opts.errorCacheTtlMs <= 24 * 60 * 60 * 1000) {
46
+ this.ERROR_CACHE_TTL = Math.floor(opts.errorCacheTtlMs);
47
+ }
48
+ }
49
+
50
+ // ─── Thread cooldowns ─────────────────────────────────────────────────────
51
+
52
+ isThreadOnCooldown(threadID) {
53
+ const now = Date.now();
54
+ // Fast path: check cache first
55
+ const cacheKey = `t:${threadID}`;
56
+ const cached = this._cooldownCache.get(cacheKey);
57
+ if (cached && now - cached.ts < this._COOLDOWN_CACHE_TTL) {
58
+ return cached.result;
59
+ }
60
+
61
+ const cooldownEnd = this.threadCooldowns.get(threadID);
62
+ let result = false;
63
+ if (!cooldownEnd) {
64
+ result = false;
65
+ } else if (now >= cooldownEnd) {
66
+ this.threadCooldowns.delete(threadID);
67
+ result = false;
68
+ } else {
69
+ result = true;
70
+ }
71
+
72
+ this._cooldownCache.set(cacheKey, { ts: now, result });
73
+ return result;
74
+ }
75
+
76
+ setThreadCooldown(threadID, duration = null) {
77
+ this.threadCooldowns.set(threadID, Date.now() + (duration || this.COOLDOWN_DURATION));
78
+ // Invalidate cache
79
+ this._cooldownCache.delete(`t:${threadID}`);
80
+ }
81
+
82
+ // ─── Endpoint cooldowns ───────────────────────────────────────────────────
83
+
84
+ isEndpointOnCooldown(endpoint) {
85
+ const now = Date.now();
86
+ // Fast path: check cache
87
+ const cacheKey = `e:${endpoint}`;
88
+ const cached = this._cooldownCache.get(cacheKey);
89
+ if (cached && now - cached.ts < this._COOLDOWN_CACHE_TTL) {
90
+ return cached.result;
91
+ }
92
+
93
+ const cooldownEnd = this.endpointCooldowns.get(endpoint);
94
+ let result = false;
95
+ if (!cooldownEnd) {
96
+ result = false;
97
+ } else if (now >= cooldownEnd) {
98
+ this.endpointCooldowns.delete(endpoint);
99
+ result = false;
100
+ } else {
101
+ result = true;
102
+ }
103
+
104
+ this._cooldownCache.set(cacheKey, { ts: now, result });
105
+ return result;
106
+ }
107
+
108
+ setEndpointCooldown(endpoint, duration = null) {
109
+ this.endpointCooldowns.set(endpoint, Date.now() + (duration || this.COOLDOWN_DURATION));
110
+ this._cooldownCache.delete(`e:${endpoint}`);
111
+ }
112
+
113
+ // ─── Error suppression ────────────────────────────────────────────────────
114
+
115
+ shouldSuppressError(key) {
116
+ const cachedTime = this.errorCache.get(key);
117
+ if (!cachedTime) {
118
+ this.errorCache.set(key, Date.now());
119
+ return false;
120
+ }
121
+ if (Date.now() - cachedTime > this.ERROR_CACHE_TTL) {
122
+ this.errorCache.set(key, Date.now());
123
+ return false;
124
+ }
125
+ return true;
126
+ }
127
+
128
+ // ─── Sliding-window rate checking ─────────────────────────────────────────
129
+
130
+ /**
131
+ * Prune timestamps older than the window and return the current count.
132
+ * Optimized with batch pruning.
133
+ */
134
+ _pruneWindow(arr) {
135
+ const cutoff = Date.now() - this._WINDOW_MS;
136
+ // Binary search for the first valid timestamp
137
+ let left = 0, right = arr.length;
138
+ while (left < right) {
139
+ const mid = Math.floor((left + right) / 2);
140
+ if (arr[mid] < cutoff) {
141
+ left = mid + 1;
142
+ } else {
143
+ right = mid;
144
+ }
145
+ }
146
+ if (left > 0) {
147
+ arr.splice(0, left);
148
+ }
149
+ return arr.length;
150
+ }
151
+
152
+ isGloballyRateLimited() {
153
+ const count = this._pruneWindow(this._requestWindow);
154
+ return count >= this.MAX_REQUESTS_PER_MINUTE;
155
+ }
156
+
157
+ isEndpointRateLimited(endpoint) {
158
+ if (!this._endpointWindows.has(endpoint)) return false;
159
+ const count = this._pruneWindow(this._endpointWindows.get(endpoint));
160
+ return count >= this._MAX_PER_ENDPOINT_PER_MINUTE;
161
+ }
162
+
163
+ _recordRequest(endpoint) {
164
+ const now = Date.now();
165
+ this._requestWindow.push(now);
166
+ // Prune less frequently for better performance
167
+ if (this._requestWindow.length > this.MAX_REQUESTS_PER_MINUTE * 3) {
168
+ this._pruneWindow(this._requestWindow);
169
+ }
170
+ if (endpoint) {
171
+ if (!this._endpointWindows.has(endpoint)) {
172
+ this._endpointWindows.set(endpoint, []);
173
+ }
174
+ const ew = this._endpointWindows.get(endpoint);
175
+ ew.push(now);
176
+ if (ew.length > this._MAX_PER_ENDPOINT_PER_MINUTE * 3) {
177
+ this._pruneWindow(ew);
178
+ }
179
+ }
180
+ }
181
+
182
+ // ─── Adaptive delay ───────────────────────────────────────────────────────
183
+
184
+ getAdaptiveDelay(retryCount, errorCode = null) {
185
+ const baseDelays = [1000, 2500, 5000, 10000]; // Reduced base delays
186
+ const base = baseDelays[Math.min(retryCount, baseDelays.length - 1)];
187
+
188
+ if (errorCode === 1545012 || errorCode === 1675004) {
189
+ return base * 1.5; // Reduced from 2x
190
+ }
191
+ if (errorCode === 368 || errorCode === 10) {
192
+ return base * 2; // Reduced from 3x
193
+ }
194
+ return base;
195
+ }
196
+
197
+ async addHumanizedDelay(min = 100, max = 350) { // Reduced delays
198
+ const delay = Math.floor(Math.random() * (max - min + 1)) + min;
199
+ await new Promise(resolve => setTimeout(resolve, delay));
200
+ }
201
+
202
+ /**
203
+ * Check global and concurrent rate limits.
204
+ * Will wait until below limit, then record the request.
205
+ */
206
+ async checkRateLimit(skipHumanDelay = false, endpoint = null) {
207
+ // Fast path: try immediate slot acquisition
208
+ if (this.activeRequests < this.MAX_CONCURRENT_REQUESTS) {
209
+ if (!this.isGloballyRateLimited()) {
210
+ if (!endpoint || !this.isEndpointRateLimited(endpoint)) {
211
+ if (!skipHumanDelay) {
212
+ await this.addHumanizedDelay();
213
+ }
214
+ this.activeRequests++;
215
+ this._recordRequest(endpoint);
216
+ return () => {
217
+ this.activeRequests = Math.max(0, this.activeRequests - 1);
218
+ };
219
+ }
220
+ }
221
+ }
222
+
223
+ // Slow path: wait for slot
224
+ while (this.activeRequests >= this.MAX_CONCURRENT_REQUESTS) {
225
+ await new Promise(resolve => setTimeout(resolve, 50)); // Reduced from 100ms
226
+ }
227
+
228
+ // Wait for per-minute global window to clear
229
+ let waitCycles = 0;
230
+ const maxCycles = 120; // Increased from 60
231
+ while (this.isGloballyRateLimited()) {
232
+ if (waitCycles++ > maxCycles) break;
233
+ await new Promise(resolve => setTimeout(resolve, 500)); // Reduced from 1000ms
234
+ }
235
+
236
+ // Wait for per-endpoint window to clear
237
+ if (endpoint) {
238
+ let epCycles = 0;
239
+ const maxEpCycles = 60; // Increased from 30
240
+ while (this.isEndpointRateLimited(endpoint)) {
241
+ if (epCycles++ > maxEpCycles) break;
242
+ await new Promise(resolve => setTimeout(resolve, 500));
243
+ }
244
+ }
245
+
246
+ if (!skipHumanDelay) {
247
+ await this.addHumanizedDelay();
248
+ }
249
+
250
+ this.activeRequests++;
251
+ this._recordRequest(endpoint);
252
+
253
+ return () => {
254
+ this.activeRequests = Math.max(0, this.activeRequests - 1);
255
+ };
256
+ }
257
+
258
+ // ─── Cleanup ──────────────────────────────────────────────────────────────
259
+
260
+ cleanup() {
261
+ const now = Date.now();
262
+
263
+ for (const [key, time] of this.errorCache.entries()) {
264
+ if (now - time > this.ERROR_CACHE_TTL) this.errorCache.delete(key);
265
+ }
266
+ for (const [key, time] of this.threadCooldowns.entries()) {
267
+ if (now >= time) this.threadCooldowns.delete(key);
268
+ }
269
+ for (const [key, time] of this.endpointCooldowns.entries()) {
270
+ if (now >= time) this.endpointCooldowns.delete(key);
271
+ }
272
+
273
+ // Prune all endpoint windows
274
+ for (const [key, arr] of this._endpointWindows.entries()) {
275
+ this._pruneWindow(arr);
276
+ if (arr.length === 0) this._endpointWindows.delete(key);
277
+ }
278
+ this._pruneWindow(this._requestWindow);
279
+
280
+ // Clear cooldown cache
281
+ this._cooldownCache.clear();
282
+ }
283
+
284
+ getStats() {
285
+ this._pruneWindow(this._requestWindow);
286
+ return {
287
+ activeRequests: this.activeRequests,
288
+ maxConcurrentRequests: this.MAX_CONCURRENT_REQUESTS,
289
+ maxRequestsPerMinute: this.MAX_REQUESTS_PER_MINUTE,
290
+ requestsInLastMinute: this._requestWindow.length,
291
+ threadCooldowns: this.threadCooldowns.size,
292
+ endpointCooldowns: this.endpointCooldowns.size,
293
+ errorCacheSize: this.errorCache.size
294
+ };
295
+ }
296
+
297
+ getCooldownRemaining(threadID) {
298
+ const cooldownEnd = this.threadCooldowns.get(threadID);
299
+ if (!cooldownEnd) return 0;
300
+ return Math.max(0, cooldownEnd - Date.now());
301
+ }
302
+
303
+ getEndpointCooldownRemaining(endpoint) {
304
+ const cooldownEnd = this.endpointCooldowns.get(endpoint);
305
+ if (!cooldownEnd) return 0;
306
+ return Math.max(0, cooldownEnd - Date.now());
307
+ }
308
+ }
309
+
310
+ const globalRateLimiter = new RateLimiter();
311
+
312
+ setInterval(() => globalRateLimiter.cleanup(), 60000);
313
+
314
+ module.exports = {
315
+ RateLimiter,
316
+ globalRateLimiter,
317
+ configureRateLimiter: (opts) => globalRateLimiter.configure(opts),
318
+ getRateLimiterStats: () => globalRateLimiter.getStats()
319
+ };