myavana-bot-test-core 2.1.8 โ 2.2.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.
- package/package.json +1 -1
- package/src/cacheManager.js +350 -104
package/package.json
CHANGED
package/src/cacheManager.js
CHANGED
|
@@ -1,27 +1,118 @@
|
|
|
1
|
-
// cacheManager.js
|
|
1
|
+
// cacheManager.js - PRODUCTION VERSION WITH MULTI-LAYER FALLBACK
|
|
2
2
|
|
|
3
3
|
const ai = require('./ai');
|
|
4
4
|
const { googleAI } = require('@genkit-ai/google-genai');
|
|
5
5
|
const { pgClient, redisClient } = require('./database');
|
|
6
|
-
const
|
|
6
|
+
const { GoogleGenAI } = require("@google/genai");
|
|
7
7
|
const config = require('./config');
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Cache Manager for Gemini API
|
|
11
|
-
*
|
|
10
|
+
* Cache Manager for Gemini API with Multi-Layer Storage
|
|
11
|
+
* Storage Layers (in order of preference):
|
|
12
|
+
* 1. PostgreSQL (persistent, shared across instances)
|
|
13
|
+
* 2. Redis (fast, shared across instances)
|
|
14
|
+
* 3. Memory (fastest, single instance only)
|
|
12
15
|
*/
|
|
13
16
|
const cacheAi = new GoogleGenAI({ apiKey: config.genkitApiKey });
|
|
17
|
+
|
|
14
18
|
class CacheManager {
|
|
15
19
|
constructor() {
|
|
16
20
|
this.CACHE_TTL_HOURS = 24;
|
|
17
|
-
this.CACHE_TTL_SECONDS = this.CACHE_TTL_HOURS * 60 * 60;
|
|
21
|
+
this.CACHE_TTL_SECONDS = this.CACHE_TTL_HOURS * 60 * 60;
|
|
22
|
+
this.CACHE_REFRESH_THRESHOLD_HOURS = 1;
|
|
23
|
+
|
|
24
|
+
// In-memory fallback (last resort)
|
|
25
|
+
this.memoryCache = new Map();
|
|
26
|
+
|
|
27
|
+
console.log('๐ง Cache Manager initialized with multi-layer storage');
|
|
18
28
|
}
|
|
19
29
|
|
|
20
30
|
/**
|
|
21
|
-
*
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
31
|
+
* Check cache validity across all storage layers
|
|
32
|
+
*/
|
|
33
|
+
async checkCacheValidity(userId) {
|
|
34
|
+
console.log('๐ Quick cache check for user:', userId);
|
|
35
|
+
const checkStart = performance.now();
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
// Try all layers in order of preference
|
|
39
|
+
let existingCache = null;
|
|
40
|
+
let source = 'none';
|
|
41
|
+
|
|
42
|
+
// Layer 1: PostgreSQL (preferred)
|
|
43
|
+
existingCache = await this.getUserCache(userId);
|
|
44
|
+
if (existingCache) {
|
|
45
|
+
source = 'postgresql';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Layer 2: Redis (if PostgreSQL failed)
|
|
49
|
+
if (!existingCache && redisClient) {
|
|
50
|
+
existingCache = await this.getUserCacheFromRedis(userId);
|
|
51
|
+
if (existingCache) source = 'redis';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Layer 3: Memory (if both failed)
|
|
55
|
+
if (!existingCache) {
|
|
56
|
+
existingCache = this.getUserCacheFromMemory(userId);
|
|
57
|
+
if (existingCache) source = 'memory';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!existingCache) {
|
|
61
|
+
console.log('๐ญ No cache exists (checked:', (performance.now() - checkStart).toFixed(0), 'ms)');
|
|
62
|
+
return {
|
|
63
|
+
hasValidCache: false,
|
|
64
|
+
cacheName: null,
|
|
65
|
+
needsRefresh: false,
|
|
66
|
+
reason: 'no_cache',
|
|
67
|
+
source: 'none'
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (this.isCacheExpired(existingCache)) {
|
|
72
|
+
console.log('โฐ Cache expired (from:', source, ')');
|
|
73
|
+
return {
|
|
74
|
+
hasValidCache: false,
|
|
75
|
+
cacheName: null,
|
|
76
|
+
needsRefresh: true,
|
|
77
|
+
reason: 'expired',
|
|
78
|
+
source: source
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const remainingHours = this.getCacheRemainingHours(existingCache);
|
|
83
|
+
const needsRefresh = remainingHours < this.CACHE_REFRESH_THRESHOLD_HOURS;
|
|
84
|
+
|
|
85
|
+
console.log('โ
Valid cache found:', {
|
|
86
|
+
source: source,
|
|
87
|
+
cacheName: existingCache.cache_name,
|
|
88
|
+
remainingHours: remainingHours,
|
|
89
|
+
needsRefresh: needsRefresh,
|
|
90
|
+
checkTime: (performance.now() - checkStart).toFixed(0) + 'ms'
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
hasValidCache: true,
|
|
95
|
+
cacheName: existingCache.cache_name,
|
|
96
|
+
needsRefresh: needsRefresh,
|
|
97
|
+
remainingHours: remainingHours,
|
|
98
|
+
reason: 'valid',
|
|
99
|
+
source: source
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error('โ Cache check failed:', error.message);
|
|
104
|
+
return {
|
|
105
|
+
hasValidCache: false,
|
|
106
|
+
cacheName: null,
|
|
107
|
+
needsRefresh: false,
|
|
108
|
+
reason: 'error',
|
|
109
|
+
source: 'none'
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Get or create cache
|
|
25
116
|
*/
|
|
26
117
|
async getOrCreateCache(userId, systemPrompt) {
|
|
27
118
|
console.log('๐ ===== CACHE MANAGER: GET OR CREATE =====');
|
|
@@ -33,18 +124,11 @@ class CacheManager {
|
|
|
33
124
|
});
|
|
34
125
|
|
|
35
126
|
try {
|
|
36
|
-
//
|
|
37
|
-
const existingCache = await this.
|
|
127
|
+
// Quick check across all layers
|
|
128
|
+
const existingCache = await this.getUserCacheFromAnyLayer(userId);
|
|
38
129
|
|
|
39
130
|
if (existingCache && !this.isCacheExpired(existingCache)) {
|
|
40
|
-
console.log('โ
Using existing cache:',
|
|
41
|
-
cacheName: existingCache.cache_name,
|
|
42
|
-
createdAt: existingCache.created_at,
|
|
43
|
-
expiresAt: existingCache.expires_at,
|
|
44
|
-
ageHours: this.getCacheAgeHours(existingCache),
|
|
45
|
-
remainingHours: this.getCacheRemainingHours(existingCache)
|
|
46
|
-
});
|
|
47
|
-
|
|
131
|
+
console.log('โ
Using existing cache:', existingCache.cache_name);
|
|
48
132
|
return {
|
|
49
133
|
cacheName: existingCache.cache_name,
|
|
50
134
|
isNewCache: false
|
|
@@ -52,9 +136,10 @@ class CacheManager {
|
|
|
52
136
|
}
|
|
53
137
|
|
|
54
138
|
// Create new cache
|
|
55
|
-
console.log('๐ Creating new cache
|
|
139
|
+
console.log('๐ Creating new Gemini cache...');
|
|
56
140
|
const cacheStartTime = performance.now();
|
|
57
|
-
const modelName = "gemini-3-flash-preview"
|
|
141
|
+
const modelName = "gemini-3-flash-preview";
|
|
142
|
+
|
|
58
143
|
const cache = await cacheAi.caches.create({
|
|
59
144
|
model: modelName,
|
|
60
145
|
config: {
|
|
@@ -64,17 +149,18 @@ class CacheManager {
|
|
|
64
149
|
});
|
|
65
150
|
|
|
66
151
|
const cacheCreationTime = performance.now() - cacheStartTime;
|
|
67
|
-
console.log('โ
|
|
152
|
+
console.log('โ
Gemini cache created:', {
|
|
68
153
|
cacheName: cache.name,
|
|
69
154
|
creationTime: cacheCreationTime.toFixed(2) + 'ms',
|
|
70
155
|
ttlHours: this.CACHE_TTL_HOURS,
|
|
71
156
|
expiresAt: new Date(Date.now() + this.CACHE_TTL_SECONDS * 1000).toISOString()
|
|
72
157
|
});
|
|
73
158
|
|
|
74
|
-
// Save
|
|
75
|
-
|
|
159
|
+
// Save to all available storage layers
|
|
160
|
+
const saveStart = performance.now();
|
|
161
|
+
await this.saveUserCacheMultiLayer(userId, cache.name);
|
|
162
|
+
console.log('๐พ Multi-layer save completed:', (performance.now() - saveStart).toFixed(0), 'ms');
|
|
76
163
|
|
|
77
|
-
console.log('๐พ Cache info saved to database');
|
|
78
164
|
console.log('๐ ===== CACHE MANAGER: COMPLETE =====');
|
|
79
165
|
|
|
80
166
|
return {
|
|
@@ -89,7 +175,6 @@ class CacheManager {
|
|
|
89
175
|
timestamp: new Date().toISOString()
|
|
90
176
|
});
|
|
91
177
|
|
|
92
|
-
// Return null to indicate cache unavailable - caller should proceed without cache
|
|
93
178
|
return {
|
|
94
179
|
cacheName: null,
|
|
95
180
|
isNewCache: false,
|
|
@@ -99,166 +184,327 @@ class CacheManager {
|
|
|
99
184
|
}
|
|
100
185
|
|
|
101
186
|
/**
|
|
102
|
-
*
|
|
103
|
-
* @param {string} userId
|
|
104
|
-
* @returns {Promise<object|null>}
|
|
187
|
+
* Try to get cache from any available layer
|
|
105
188
|
*/
|
|
106
|
-
async
|
|
107
|
-
|
|
189
|
+
async getUserCacheFromAnyLayer(userId) {
|
|
190
|
+
// Try PostgreSQL
|
|
191
|
+
let cache = await this.getUserCache(userId);
|
|
192
|
+
if (cache) return cache;
|
|
193
|
+
|
|
194
|
+
// Try Redis
|
|
195
|
+
if (redisClient) {
|
|
196
|
+
cache = await this.getUserCacheFromRedis(userId);
|
|
197
|
+
if (cache) return cache;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Try Memory
|
|
201
|
+
return this.getUserCacheFromMemory(userId);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Save cache to all available layers (don't fail if one fails)
|
|
206
|
+
*/
|
|
207
|
+
async saveUserCacheMultiLayer(userId, cacheName) {
|
|
208
|
+
const expiresAt = new Date(Date.now() + this.CACHE_TTL_SECONDS * 1000);
|
|
209
|
+
const cacheData = {
|
|
210
|
+
cache_name: cacheName,
|
|
211
|
+
created_at: new Date(),
|
|
212
|
+
expires_at: expiresAt
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const results = {
|
|
216
|
+
postgresql: false,
|
|
217
|
+
redis: false,
|
|
218
|
+
memory: false
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// Layer 1: PostgreSQL (primary)
|
|
222
|
+
try {
|
|
223
|
+
await this.saveUserCacheToPostgres(userId, cacheName, expiresAt);
|
|
224
|
+
results.postgresql = true;
|
|
225
|
+
} catch (error) {
|
|
226
|
+
console.error('โ PostgreSQL save failed:', error.message);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Layer 2: Redis (fast fallback)
|
|
230
|
+
if (redisClient) {
|
|
231
|
+
try {
|
|
232
|
+
await this.saveUserCacheToRedis(userId, cacheData);
|
|
233
|
+
results.redis = true;
|
|
234
|
+
} catch (error) {
|
|
235
|
+
console.error('โ Redis save failed:', error.message);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Layer 3: Memory (always succeeds)
|
|
240
|
+
try {
|
|
241
|
+
this.saveUserCacheToMemory(userId, cacheData);
|
|
242
|
+
results.memory = true;
|
|
243
|
+
} catch (error) {
|
|
244
|
+
console.error('โ Memory save failed:', error.message);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
console.log('๐พ Multi-layer save results:', results);
|
|
248
|
+
|
|
249
|
+
// At least one layer must succeed
|
|
250
|
+
if (!results.postgresql && !results.redis && !results.memory) {
|
|
251
|
+
throw new Error('All cache storage layers failed');
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Save to PostgreSQL with automatic fallback to DELETE+INSERT
|
|
257
|
+
*/
|
|
258
|
+
async saveUserCacheToPostgres(userId, cacheName, expiresAt) {
|
|
259
|
+
console.log('๐พ Saving to PostgreSQL...');
|
|
108
260
|
|
|
261
|
+
try {
|
|
262
|
+
// First try UPSERT (requires PRIMARY KEY or UNIQUE constraint)
|
|
263
|
+
try {
|
|
264
|
+
const query = `
|
|
265
|
+
INSERT INTO user_gemini_caches (user_id, cache_name, created_at, expires_at)
|
|
266
|
+
VALUES ($1, $2, NOW(), $3)
|
|
267
|
+
ON CONFLICT (user_id)
|
|
268
|
+
DO UPDATE SET
|
|
269
|
+
cache_name = EXCLUDED.cache_name,
|
|
270
|
+
created_at = NOW(),
|
|
271
|
+
expires_at = EXCLUDED.expires_at
|
|
272
|
+
`;
|
|
273
|
+
|
|
274
|
+
await pgClient.query(query, [userId, cacheName, expiresAt]);
|
|
275
|
+
console.log('โ
PostgreSQL save successful (UPSERT)');
|
|
276
|
+
return true;
|
|
277
|
+
|
|
278
|
+
} catch (upsertError) {
|
|
279
|
+
// If UPSERT fails (error code 42P10 = no constraint), use DELETE + INSERT
|
|
280
|
+
if (upsertError.code === '42P10') {
|
|
281
|
+
console.log('โ ๏ธ No constraint found, using DELETE+INSERT fallback');
|
|
282
|
+
|
|
283
|
+
// Delete old record
|
|
284
|
+
await pgClient.query(
|
|
285
|
+
`DELETE FROM user_gemini_caches WHERE user_id = $1`,
|
|
286
|
+
[userId]
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
// Insert new record
|
|
290
|
+
await pgClient.query(
|
|
291
|
+
`INSERT INTO user_gemini_caches (user_id, cache_name, created_at, expires_at)
|
|
292
|
+
VALUES ($1, $2, NOW(), $3)`,
|
|
293
|
+
[userId, cacheName, expiresAt]
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
console.log('โ
PostgreSQL save successful (DELETE+INSERT)');
|
|
297
|
+
return true;
|
|
298
|
+
} else {
|
|
299
|
+
throw upsertError; // Re-throw if it's a different error
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
} catch (error) {
|
|
304
|
+
console.error('โ PostgreSQL save completely failed:', error.message);
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Save to Redis
|
|
311
|
+
*/
|
|
312
|
+
async saveUserCacheToRedis(userId, cacheData) {
|
|
313
|
+
console.log('๐พ Saving to Redis...');
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
const key = `gemini_cache:${userId}`;
|
|
317
|
+
await redisClient.setex(
|
|
318
|
+
key,
|
|
319
|
+
this.CACHE_TTL_SECONDS,
|
|
320
|
+
JSON.stringify(cacheData)
|
|
321
|
+
);
|
|
322
|
+
console.log('โ
Redis save successful');
|
|
323
|
+
return true;
|
|
324
|
+
} catch (error) {
|
|
325
|
+
console.error('โ Redis save failed:', error.message);
|
|
326
|
+
throw error;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Save to Memory (always succeeds)
|
|
332
|
+
*/
|
|
333
|
+
saveUserCacheToMemory(userId, cacheData) {
|
|
334
|
+
console.log('๐พ Saving to memory...');
|
|
335
|
+
this.memoryCache.set(userId, cacheData);
|
|
336
|
+
console.log('โ
Memory save successful');
|
|
337
|
+
|
|
338
|
+
// Auto-cleanup after TTL
|
|
339
|
+
setTimeout(() => {
|
|
340
|
+
if (this.memoryCache.has(userId)) {
|
|
341
|
+
this.memoryCache.delete(userId);
|
|
342
|
+
console.log('๐งน Auto-removed expired cache from memory:', userId);
|
|
343
|
+
}
|
|
344
|
+
}, this.CACHE_TTL_SECONDS * 1000);
|
|
345
|
+
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Get from PostgreSQL
|
|
351
|
+
*/
|
|
352
|
+
async getUserCache(userId) {
|
|
109
353
|
try {
|
|
110
354
|
const query = `
|
|
111
355
|
SELECT cache_name, created_at, expires_at
|
|
112
356
|
FROM user_gemini_caches
|
|
113
|
-
WHERE user_id =
|
|
357
|
+
WHERE user_id = $1
|
|
114
358
|
ORDER BY created_at DESC
|
|
115
359
|
LIMIT 1
|
|
116
360
|
`;
|
|
117
361
|
|
|
118
362
|
const result = await pgClient.query(query, [userId]);
|
|
119
363
|
|
|
120
|
-
if (result && result.length > 0) {
|
|
121
|
-
|
|
122
|
-
cacheName: result[0].cache_name,
|
|
123
|
-
createdAt: result[0].created_at,
|
|
124
|
-
expiresAt: result[0].expires_at
|
|
125
|
-
});
|
|
126
|
-
return result[0];
|
|
364
|
+
if (result && result.rows && result.rows.length > 0) {
|
|
365
|
+
return result.rows[0];
|
|
127
366
|
}
|
|
128
367
|
|
|
129
|
-
console.log('๐ญ No existing cache found for user');
|
|
130
368
|
return null;
|
|
131
369
|
|
|
132
370
|
} catch (error) {
|
|
133
|
-
console.error('โ
|
|
371
|
+
console.error('โ PostgreSQL fetch failed:', error.message);
|
|
134
372
|
return null;
|
|
135
373
|
}
|
|
136
374
|
}
|
|
137
375
|
|
|
138
376
|
/**
|
|
139
|
-
*
|
|
140
|
-
* @param {string} userId
|
|
141
|
-
* @param {string} cacheName
|
|
377
|
+
* Get from Redis
|
|
142
378
|
*/
|
|
143
|
-
async
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
cacheName: cacheName
|
|
147
|
-
});
|
|
148
|
-
|
|
379
|
+
async getUserCacheFromRedis(userId) {
|
|
380
|
+
if (!redisClient) return null;
|
|
381
|
+
|
|
149
382
|
try {
|
|
150
|
-
const
|
|
383
|
+
const key = `gemini_cache:${userId}`;
|
|
384
|
+
const data = await redisClient.get(key);
|
|
151
385
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
VALUES (?, ?, NOW(), ?)
|
|
156
|
-
ON DUPLICATE KEY UPDATE
|
|
157
|
-
cache_name = VALUES(cache_name),
|
|
158
|
-
created_at = NOW(),
|
|
159
|
-
expires_at = VALUES(expires_at)
|
|
160
|
-
`;
|
|
161
|
-
|
|
162
|
-
await pgClient.query(query, [userId, cacheName, expiresAt]);
|
|
386
|
+
if (data) {
|
|
387
|
+
return JSON.parse(data);
|
|
388
|
+
}
|
|
163
389
|
|
|
164
|
-
|
|
390
|
+
return null;
|
|
165
391
|
|
|
166
392
|
} catch (error) {
|
|
167
|
-
console.error('โ
|
|
168
|
-
|
|
393
|
+
console.error('โ Redis fetch failed:', error.message);
|
|
394
|
+
return null;
|
|
169
395
|
}
|
|
170
396
|
}
|
|
171
397
|
|
|
398
|
+
/**
|
|
399
|
+
* Get from Memory
|
|
400
|
+
*/
|
|
401
|
+
getUserCacheFromMemory(userId) {
|
|
402
|
+
return this.memoryCache.get(userId) || null;
|
|
403
|
+
}
|
|
404
|
+
|
|
172
405
|
/**
|
|
173
406
|
* Check if cache is expired
|
|
174
|
-
* @param {object} cacheRecord
|
|
175
|
-
* @returns {boolean}
|
|
176
407
|
*/
|
|
177
408
|
isCacheExpired(cacheRecord) {
|
|
178
409
|
const now = new Date();
|
|
179
410
|
const expiresAt = new Date(cacheRecord.expires_at);
|
|
180
|
-
|
|
181
|
-
const isExpired = now >= expiresAt;
|
|
182
|
-
|
|
183
|
-
if (isExpired) {
|
|
184
|
-
console.log('โฐ Cache expired:', {
|
|
185
|
-
expiresAt: expiresAt.toISOString(),
|
|
186
|
-
now: now.toISOString(),
|
|
187
|
-
expiredBy: ((now - expiresAt) / (1000 * 60)).toFixed(2) + ' minutes'
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return isExpired;
|
|
411
|
+
return now >= expiresAt;
|
|
192
412
|
}
|
|
193
413
|
|
|
194
414
|
/**
|
|
195
415
|
* Get cache age in hours
|
|
196
|
-
* @param {object} cacheRecord
|
|
197
|
-
* @returns {number}
|
|
198
416
|
*/
|
|
199
417
|
getCacheAgeHours(cacheRecord) {
|
|
200
418
|
const now = new Date();
|
|
201
419
|
const createdAt = new Date(cacheRecord.created_at);
|
|
202
420
|
const ageMs = now - createdAt;
|
|
203
|
-
return (ageMs / (1000 * 60 * 60)).toFixed(2);
|
|
421
|
+
return parseFloat((ageMs / (1000 * 60 * 60)).toFixed(2));
|
|
204
422
|
}
|
|
205
423
|
|
|
206
424
|
/**
|
|
207
425
|
* Get remaining cache lifetime in hours
|
|
208
|
-
* @param {object} cacheRecord
|
|
209
|
-
* @returns {number}
|
|
210
426
|
*/
|
|
211
427
|
getCacheRemainingHours(cacheRecord) {
|
|
212
428
|
const now = new Date();
|
|
213
429
|
const expiresAt = new Date(cacheRecord.expires_at);
|
|
214
430
|
const remainingMs = expiresAt - now;
|
|
215
|
-
return Math.max(0, (remainingMs / (1000 * 60 * 60)).toFixed(2));
|
|
431
|
+
return Math.max(0, parseFloat((remainingMs / (1000 * 60 * 60)).toFixed(2)));
|
|
216
432
|
}
|
|
217
433
|
|
|
218
434
|
/**
|
|
219
|
-
* Clean up expired caches from
|
|
435
|
+
* Clean up expired caches from all layers
|
|
220
436
|
*/
|
|
221
437
|
async cleanupExpiredCaches() {
|
|
222
|
-
console.log('๐งน Cleaning up expired caches...');
|
|
438
|
+
console.log('๐งน Cleaning up expired caches from all layers...');
|
|
439
|
+
|
|
440
|
+
const results = {
|
|
441
|
+
postgresql: 0,
|
|
442
|
+
redis: 0,
|
|
443
|
+
memory: 0
|
|
444
|
+
};
|
|
223
445
|
|
|
446
|
+
// PostgreSQL cleanup
|
|
224
447
|
try {
|
|
225
|
-
const query = `
|
|
226
|
-
DELETE FROM user_gemini_caches
|
|
227
|
-
WHERE expires_at < NOW()
|
|
228
|
-
`;
|
|
229
|
-
|
|
448
|
+
const query = `DELETE FROM user_gemini_caches WHERE expires_at < NOW()`;
|
|
230
449
|
const result = await pgClient.query(query);
|
|
231
|
-
|
|
232
|
-
console.log('โ
Cleanup complete:', {
|
|
233
|
-
deletedRecords: result.affectedRows || 0
|
|
234
|
-
});
|
|
235
|
-
|
|
450
|
+
results.postgresql = result.rowCount || 0;
|
|
236
451
|
} catch (error) {
|
|
237
|
-
console.error('โ
|
|
452
|
+
console.error('โ PostgreSQL cleanup failed:', error.message);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Redis cleanup (would need to scan all keys - expensive, skip for now)
|
|
456
|
+
// Redis auto-expires with TTL, so not critical
|
|
457
|
+
|
|
458
|
+
// Memory cleanup
|
|
459
|
+
for (const [userId, cacheData] of this.memoryCache.entries()) {
|
|
460
|
+
if (this.isCacheExpired(cacheData)) {
|
|
461
|
+
this.memoryCache.delete(userId);
|
|
462
|
+
results.memory++;
|
|
463
|
+
}
|
|
238
464
|
}
|
|
465
|
+
|
|
466
|
+
console.log('โ
Cleanup results:', results);
|
|
467
|
+
return results;
|
|
239
468
|
}
|
|
240
469
|
|
|
241
470
|
/**
|
|
242
|
-
* Get cache statistics
|
|
471
|
+
* Get cache statistics
|
|
243
472
|
*/
|
|
244
473
|
async getCacheStats() {
|
|
474
|
+
const stats = {
|
|
475
|
+
memory_caches: this.memoryCache.size,
|
|
476
|
+
postgresql: null,
|
|
477
|
+
redis: null
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
// PostgreSQL stats
|
|
245
481
|
try {
|
|
246
482
|
const query = `
|
|
247
483
|
SELECT
|
|
248
484
|
COUNT(*) as total_caches,
|
|
249
485
|
COUNT(CASE WHEN expires_at > NOW() THEN 1 END) as active_caches,
|
|
250
486
|
COUNT(CASE WHEN expires_at <= NOW() THEN 1 END) as expired_caches,
|
|
251
|
-
AVG(
|
|
487
|
+
AVG(EXTRACT(EPOCH FROM (NOW() - created_at)) / 3600) as avg_age_hours
|
|
252
488
|
FROM user_gemini_caches
|
|
253
489
|
`;
|
|
254
490
|
|
|
255
491
|
const result = await pgClient.query(query);
|
|
256
|
-
|
|
257
|
-
|
|
492
|
+
stats.postgresql = result.rows[0];
|
|
258
493
|
} catch (error) {
|
|
259
|
-
console.error('โ
|
|
260
|
-
return null;
|
|
494
|
+
console.error('โ PostgreSQL stats failed:', error.message);
|
|
261
495
|
}
|
|
496
|
+
|
|
497
|
+
// Redis stats (would need dbsize command)
|
|
498
|
+
if (redisClient) {
|
|
499
|
+
try {
|
|
500
|
+
const keys = await redisClient.keys('gemini_cache:*');
|
|
501
|
+
stats.redis = { total_keys: keys.length };
|
|
502
|
+
} catch (error) {
|
|
503
|
+
console.error('โ Redis stats failed:', error.message);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return stats;
|
|
262
508
|
}
|
|
263
509
|
}
|
|
264
510
|
|