@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,436 @@
1
+ /* eslint-disable no-prototype-builtins */
2
+ "use strict";
3
+
4
+ const axios = require("axios");
5
+ const { CookieJar } = require("tough-cookie");
6
+ const { wrapper } = require("axios-cookiejar-support");
7
+ const FormData = require("form-data");
8
+ const { getHeaders } = require("./headers");
9
+ const { getType } = require("./constants");
10
+ const { globalRateLimiter } = require("./rateLimiter");
11
+
12
+ const jar = new CookieJar();
13
+ const client = wrapper(axios.create({ jar }));
14
+
15
+ let proxyConfig = {};
16
+
17
+ const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
18
+
19
+ // Fast path for simple delays - use setImmediate for 0ms delays
20
+ const fastDelay = (ms) => {
21
+ if (ms <= 0) return Promise.resolve();
22
+ if (ms <= 1) return new Promise(resolve => setImmediate(resolve));
23
+ return new Promise(resolve => setTimeout(resolve, ms));
24
+ };
25
+
26
+ function adaptResponse(res) {
27
+ const response = res.response || res;
28
+ return {
29
+ ...response,
30
+ body: response.data,
31
+ statusCode: response.status,
32
+ request: {
33
+ uri: new URL(response.config.url),
34
+ headers: response.config.headers,
35
+ method: response.config.method.toUpperCase(),
36
+ form: response.config.data,
37
+ formData: response.config.data
38
+ },
39
+ };
40
+ }
41
+
42
+ /**
43
+ * Inspects an API response body for signs of session expiry or Facebook
44
+ * bot-detection checkpoints and emits the appropriate signals on ctx.
45
+ *
46
+ * Returns true if the response looks like a valid authenticated response,
47
+ * false if it signals logout / checkpoint.
48
+ *
49
+ * When a logout is detected and ctx.performAutoLogin is available the
50
+ * function fires it (non-blocking) and throws so the caller knows the
51
+ * original response is unusable.
52
+ */
53
+ async function inspectResponseForSessionIssues(adapted, ctx) {
54
+ if (!ctx || ctx._skipSessionInspect) return;
55
+
56
+ const body = adapted.body;
57
+ if (!body) return;
58
+
59
+ const bodyStr = typeof body === 'string' ? body : JSON.stringify(body);
60
+
61
+ // Facebook bot-detection checkpoint IDs
62
+ const isCheckpoint282 = bodyStr.includes('1501092823525282');
63
+ const isCheckpoint956 = bodyStr.includes('828281030927956');
64
+ const isScrapingWarning = bodyStr.includes('XCheckpointFBScrapingWarningController');
65
+
66
+ if (isCheckpoint282) {
67
+ const err = new Error('Bot checkpoint 282 detected. Please verify the account.');
68
+ err.error = 'checkpoint_282';
69
+ err.res = body;
70
+ if (ctx._emitter && typeof ctx._emitter.emit === 'function') {
71
+ ctx._emitter.emit('checkpoint_282', { res: body });
72
+ }
73
+ throw err;
74
+ }
75
+
76
+ if (isCheckpoint956) {
77
+ const err = new Error('Bot checkpoint 956 detected. Please verify the account.');
78
+ err.error = 'checkpoint_956';
79
+ err.res = body;
80
+ if (ctx._emitter && typeof ctx._emitter.emit === 'function') {
81
+ ctx._emitter.emit('checkpoint_956', { res: body });
82
+ }
83
+ throw err;
84
+ }
85
+
86
+ if (isScrapingWarning) {
87
+ const err = new Error('Facebook scraping warning checkpoint detected.');
88
+ err.error = 'checkpoint_scraping';
89
+ err.res = body;
90
+ if (ctx._emitter && typeof ctx._emitter.emit === 'function') {
91
+ ctx._emitter.emit('checkpoint', { type: 'scraping_warning', res: body });
92
+ }
93
+ throw err;
94
+ }
95
+
96
+ // Detect session expiry / forced logout.
97
+ // IMPORTANT: Facebook's authenticated homepage also contains login.php links
98
+ // in its nav/footer and has <title>Facebook</title> — any broad HTML-content
99
+ // check will produce false positives and break valid sessions.
100
+ // We ONLY flag a response as a login redirect when:
101
+ // • The page contains the actual login form elements, OR
102
+ // • A parsed JSON body explicitly redirects to login.php via a "redirect" field.
103
+ const isLoginRedirect =
104
+ bodyStr.includes('<form id="login_form"') ||
105
+ bodyStr.includes('id="loginbutton"') ||
106
+ bodyStr.includes('"login_page"') ||
107
+ // JSON responses that carry an explicit redirect to the login page.
108
+ // "next" appears alongside login.php only in unauthenticated redirect payloads.
109
+ (bodyStr.includes('login.php') && bodyStr.includes('"next":"'));
110
+
111
+ const isLoginBlocked =
112
+ typeof body === 'object' && body !== null && body.error === 1357001;
113
+
114
+ if (isLoginBlocked) {
115
+ const err = new Error('Facebook blocked the login.');
116
+ err.error = 'login_blocked';
117
+ err.res = body;
118
+ throw err;
119
+ }
120
+
121
+ if (isLoginRedirect) {
122
+ if (ctx._emitter && typeof ctx._emitter.emit === 'function') {
123
+ ctx._emitter.emit('sessionExpired', { res: body });
124
+ }
125
+
126
+ if (!ctx.auto_login && typeof ctx.performAutoLogin === 'function') {
127
+ ctx.auto_login = true;
128
+ // Safety: reset the flag after 2 minutes no matter what so future
129
+ // session expiries are never silently swallowed.
130
+ const autoLoginSafetyTimer = setTimeout(() => { ctx.auto_login = false; }, 120000);
131
+ try {
132
+ const ok = await ctx.performAutoLogin();
133
+ clearTimeout(autoLoginSafetyTimer);
134
+ ctx.auto_login = false;
135
+ if (!ok) {
136
+ const err = new Error('Not logged in. Auto re-login failed.');
137
+ err.error = 'Not logged in.';
138
+ err.res = body;
139
+ throw err;
140
+ }
141
+ } catch (autoErr) {
142
+ clearTimeout(autoLoginSafetyTimer);
143
+ ctx.auto_login = false;
144
+ throw autoErr;
145
+ }
146
+ } else {
147
+ const err = new Error('Not logged in. Session has expired.');
148
+ err.error = 'Not logged in.';
149
+ err.res = body;
150
+ throw err;
151
+ }
152
+ }
153
+ }
154
+
155
+ async function requestWithRetry(requestFunction, retries = 5, endpoint = '', threadID = '', ctx = null) {
156
+ // Fast path for simple requests
157
+ const isSimpleRequest = !endpoint && !threadID;
158
+
159
+ // Acquire rate limit slot with optimized path
160
+ let releaseSlot = null;
161
+ if (!isSimpleRequest) {
162
+ releaseSlot = await globalRateLimiter.checkRateLimit(false, endpoint);
163
+ }
164
+
165
+ // Check cooldowns efficiently
166
+ if (!isSimpleRequest) {
167
+ if (globalRateLimiter.isEndpointOnCooldown("__GLOBAL__")) {
168
+ const cooldown = globalRateLimiter.getEndpointCooldownRemaining("__GLOBAL__");
169
+ if (cooldown > 0) {
170
+ console.warn(`Global cooldown active. Waiting ${cooldown}ms...`);
171
+ await delay(cooldown);
172
+ }
173
+ }
174
+
175
+ if (endpoint && globalRateLimiter.isEndpointOnCooldown(endpoint)) {
176
+ const cooldown = globalRateLimiter.getEndpointCooldownRemaining(endpoint);
177
+ if (cooldown > 0) {
178
+ console.warn(`Endpoint ${endpoint} on cooldown. Waiting ${cooldown}ms...`);
179
+ await delay(cooldown);
180
+ }
181
+ }
182
+
183
+ if (threadID && globalRateLimiter.isThreadOnCooldown(threadID)) {
184
+ const cooldown = globalRateLimiter.getCooldownRemaining(threadID);
185
+ if (cooldown > 0) {
186
+ console.warn(`Thread ${threadID} on cooldown. Waiting ${cooldown}ms...`);
187
+ await delay(cooldown);
188
+ }
189
+ }
190
+ }
191
+
192
+ const checkAndApplyRateLimitCooldowns = (responseBody) => {
193
+ const ERROR_COOLDOWNS = {
194
+ 1545012: 60000,
195
+ 1675004: 30000,
196
+ 368: 120000,
197
+ 404: 5000,
198
+ 500: 10000,
199
+ 503: 30000
200
+ };
201
+
202
+ const applyCooldown = (errorCode) => {
203
+ if (errorCode && ERROR_COOLDOWNS[errorCode]) {
204
+ if (threadID) {
205
+ globalRateLimiter.setThreadCooldown(threadID, ERROR_COOLDOWNS[errorCode]);
206
+ }
207
+ if (endpoint) {
208
+ globalRateLimiter.setEndpointCooldown(endpoint, ERROR_COOLDOWNS[errorCode]);
209
+ }
210
+ console.warn(`Rate limit detected (error ${errorCode}). Applied cooldown.`);
211
+ return true;
212
+ }
213
+ return false;
214
+ };
215
+
216
+ if (!responseBody || typeof responseBody !== 'object') {
217
+ return false;
218
+ }
219
+
220
+ if (applyCooldown(responseBody.error)) {
221
+ return true;
222
+ }
223
+
224
+ if (Array.isArray(responseBody)) {
225
+ for (const item of responseBody) {
226
+ if (item && typeof item === 'object') {
227
+ if (applyCooldown(item.error)) return true;
228
+ if (item.errors && Array.isArray(item.errors)) {
229
+ for (const err of item.errors) {
230
+ const code = err.code || err.extensions?.code;
231
+ if (applyCooldown(code)) return true;
232
+ }
233
+ }
234
+ }
235
+ }
236
+ }
237
+
238
+ if (responseBody.errors && Array.isArray(responseBody.errors)) {
239
+ for (const err of responseBody.errors) {
240
+ const code = err.code || err.extensions?.code;
241
+ if (applyCooldown(code)) return true;
242
+ }
243
+ }
244
+
245
+ return false;
246
+ };
247
+
248
+ try {
249
+ for (let i = 0; i < retries; i++) {
250
+ try {
251
+ const res = await requestFunction();
252
+ const adapted = adaptResponse(res);
253
+
254
+ checkAndApplyRateLimitCooldowns(adapted.body);
255
+
256
+ // Inspect for session expiry / bot-detection checkpoints
257
+ await inspectResponseForSessionIssues(adapted, ctx);
258
+
259
+ return adapted;
260
+ } catch (error) {
261
+ // If this is a session/checkpoint error we already raised, propagate immediately
262
+ if (error.error === 'Not logged in.' ||
263
+ error.error === 'checkpoint_282' ||
264
+ error.error === 'checkpoint_956' ||
265
+ error.error === 'checkpoint_scraping' ||
266
+ error.error === 'login_blocked') {
267
+ throw error;
268
+ }
269
+
270
+ // Abort immediately on invalid header characters - retrying won't help
271
+ if (error.code === 'ERR_INVALID_CHAR' ||
272
+ (error.message && error.message.includes('Invalid character in header'))) {
273
+ const e = new Error('Invalid header content detected. Request aborted.');
274
+ e.error = 'invalid_header';
275
+ e.code = 'ERR_INVALID_CHAR';
276
+ e.originalError = error;
277
+ throw e;
278
+ }
279
+
280
+ // Network errors - might be transient
281
+ const isNetworkError = error.code && [
282
+ 'ECONNRESET', 'ETIMEDOUT', 'ECONNREFUSED', 'ENETUNREACH',
283
+ 'EHOSTUNREACH', 'EAI_AGAIN', 'ENOTFOUND', 'ESOCKETTIMEDOUT'
284
+ ].includes(error.code);
285
+
286
+ if (error.response) {
287
+ const adapted = adaptResponse(error.response);
288
+ checkAndApplyRateLimitCooldowns(adapted.body);
289
+ }
290
+
291
+ if (i === retries - 1) {
292
+ console.error(`Request failed after ${retries} attempts:`, error.message);
293
+ if (error.response) {
294
+ return adaptResponse(error.response);
295
+ }
296
+ throw error;
297
+ }
298
+
299
+ // Adaptive backoff: network errors get shorter delays
300
+ const baseMultiplier = isNetworkError ? 0.5 : 1;
301
+ const backoffTime = Math.min(
302
+ (Math.pow(2, i) * 1000 * baseMultiplier) + Math.floor(Math.random() * 1000),
303
+ 30000 // Cap at 30 seconds
304
+ );
305
+
306
+ if (backoffTime > 100) {
307
+ console.warn(`Request attempt ${i + 1} failed. Retrying in ${Math.round(backoffTime)}ms...`);
308
+ await delay(backoffTime);
309
+ } else {
310
+ // Fast path for very short delays
311
+ await fastDelay(backoffTime);
312
+ }
313
+ }
314
+ }
315
+ } finally {
316
+ // Always release the concurrency slot regardless of outcome.
317
+ if (typeof releaseSlot === 'function') releaseSlot();
318
+ }
319
+ }
320
+
321
+ function setProxy(proxyUrl) {
322
+ if (proxyUrl) {
323
+ try {
324
+ const parsedProxy = new URL(proxyUrl);
325
+ proxyConfig = {
326
+ proxy: {
327
+ host: parsedProxy.hostname,
328
+ port: parsedProxy.port,
329
+ protocol: parsedProxy.protocol.replace(":", ""),
330
+ auth: parsedProxy.username && parsedProxy.password ? {
331
+ username: parsedProxy.username,
332
+ password: parsedProxy.password,
333
+ } : undefined,
334
+ },
335
+ };
336
+ } catch (e) {
337
+ console.error("Invalid proxy URL. Please use a full URL format (e.g., http://user:pass@host:port).");
338
+ proxyConfig = {};
339
+ }
340
+ } else {
341
+ proxyConfig = {};
342
+ }
343
+ }
344
+
345
+ function cleanGet(url) {
346
+ const fn = () => client.get(url, { timeout: 60000, ...proxyConfig });
347
+ return requestWithRetry(fn);
348
+ }
349
+
350
+ async function get(url, reqJar, qs, options, ctx, customHeader) {
351
+ const config = {
352
+ headers: getHeaders(url, options, ctx, customHeader),
353
+ timeout: 60000,
354
+ params: qs,
355
+ ...proxyConfig,
356
+ validateStatus: (status) => status >= 200 && status < 600,
357
+ // Enable response compression for faster transfers
358
+ decompress: true,
359
+ // Optimize for performance
360
+ maxContentLength: 50 * 1024 * 1024, // 50MB max
361
+ maxBodyLength: 50 * 1024 * 1024
362
+ };
363
+ const endpoint = new URL(url).pathname;
364
+ const threadHint = ctx && ctx.requestThreadID ? String(ctx.requestThreadID) : '';
365
+ return requestWithRetry(async () => await client.get(url, config), 3, endpoint, threadHint, ctx);
366
+ }
367
+
368
+ async function post(url, reqJar, form, options, ctx, customHeader) {
369
+ const headers = getHeaders(url, options, ctx, customHeader, 'xhr');
370
+ let data = form;
371
+ let contentType = headers['Content-Type'] || 'application/x-www-form-urlencoded';
372
+
373
+ if (contentType.includes('json')) {
374
+ data = JSON.stringify(form);
375
+ } else {
376
+ // Use URLSearchParams for better performance on large forms
377
+ const transformedForm = new URLSearchParams();
378
+ for (const key in form) {
379
+ if (form.hasOwnProperty(key)) {
380
+ let value = form[key];
381
+ if (getType(value) === "Object") {
382
+ value = JSON.stringify(value);
383
+ }
384
+ transformedForm.append(key, value);
385
+ }
386
+ }
387
+ data = transformedForm.toString();
388
+ }
389
+
390
+ headers['Content-Type'] = contentType;
391
+
392
+ const config = {
393
+ headers,
394
+ timeout: 60000,
395
+ ...proxyConfig,
396
+ validateStatus: (status) => status >= 200 && status < 600,
397
+ decompress: true,
398
+ maxContentLength: 50 * 1024 * 1024,
399
+ maxBodyLength: 50 * 1024 * 1024
400
+ };
401
+ const endpoint = new URL(url).pathname;
402
+ const threadHint = ctx && ctx.requestThreadID ? String(ctx.requestThreadID) : '';
403
+ return requestWithRetry(async () => await client.post(url, data, config), 3, endpoint, threadHint, ctx);
404
+ }
405
+
406
+ async function postFormData(url, reqJar, form, qs, options, ctx) {
407
+ const formData = new FormData();
408
+ for (const key in form) {
409
+ if (form.hasOwnProperty(key)) {
410
+ formData.append(key, form[key]);
411
+ }
412
+ }
413
+
414
+ const customHeader = { "Content-Type": `multipart/form-data; boundary=${formData.getBoundary()}` };
415
+
416
+ const config = {
417
+ headers: getHeaders(url, options, ctx, customHeader, 'xhr'),
418
+ timeout: 60000,
419
+ params: qs,
420
+ ...proxyConfig,
421
+ validateStatus: (status) => status >= 200 && status < 600,
422
+ };
423
+ const endpoint = new URL(url).pathname;
424
+ const threadHint = ctx && ctx.requestThreadID ? String(ctx.requestThreadID) : '';
425
+ return requestWithRetry(async () => await client.post(url, formData, config), 3, endpoint, threadHint, ctx);
426
+ }
427
+
428
+ module.exports = {
429
+ cleanGet,
430
+ get,
431
+ post,
432
+ postFormData,
433
+ getJar: () => jar,
434
+ setProxy,
435
+ requestWithRetry
436
+ };
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+
3
+ class SimpleCache {
4
+ constructor(defaultTTL = 300000) { // 5 minutes default
5
+ this.cache = new Map();
6
+ this.defaultTTL = defaultTTL;
7
+ }
8
+
9
+ set(key, value, ttl = this.defaultTTL) {
10
+ const expiresAt = Date.now() + ttl;
11
+ this.cache.set(key, { value, expiresAt });
12
+
13
+ // Clean up expired entries occasionally
14
+ if (Math.random() < 0.01) { // 1% chance
15
+ this.cleanup();
16
+ }
17
+ }
18
+
19
+ get(key) {
20
+ const item = this.cache.get(key);
21
+ if (!item) return null;
22
+
23
+ if (Date.now() > item.expiresAt) {
24
+ this.cache.delete(key);
25
+ return null;
26
+ }
27
+
28
+ return item.value;
29
+ }
30
+
31
+ delete(key) {
32
+ return this.cache.delete(key);
33
+ }
34
+
35
+ clear() {
36
+ this.cache.clear();
37
+ }
38
+
39
+ cleanup() {
40
+ const now = Date.now();
41
+ for (const [key, item] of this.cache.entries()) {
42
+ if (now > item.expiresAt) {
43
+ this.cache.delete(key);
44
+ }
45
+ }
46
+ }
47
+
48
+ size() {
49
+ this.cleanup();
50
+ return this.cache.size;
51
+ }
52
+ }
53
+
54
+ module.exports = SimpleCache;