myavana-bot-test-core 2.1.9 โ†’ 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/cacheManager.js +290 -141
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myavana-bot-test-core",
3
- "version": "2.1.9",
3
+ "version": "2.2.0",
4
4
  "description": "Shared bot functionality with enhanced features",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -1,4 +1,4 @@
1
- // cacheManager.js - OPTIMIZED VERSION
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');
@@ -7,48 +7,75 @@ const { GoogleGenAI } = require("@google/genai");
7
7
  const config = require('./config');
8
8
 
9
9
  /**
10
- * Cache Manager for Gemini API
11
- * Handles creation, retrieval, and expiration of cached system instructions
12
- * OPTIMIZED: Check cache validity before expensive prompt construction
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)
13
15
  */
14
16
  const cacheAi = new GoogleGenAI({ apiKey: config.genkitApiKey });
15
17
 
16
18
  class CacheManager {
17
19
  constructor() {
18
20
  this.CACHE_TTL_HOURS = 24;
19
- this.CACHE_TTL_SECONDS = this.CACHE_TTL_HOURS * 60 * 60; // 24 hours in seconds
20
- this.CACHE_REFRESH_THRESHOLD_HOURS = 1; // Refresh if cache expires in < 1 hour
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');
21
28
  }
22
29
 
23
30
  /**
24
- * NEW: Check if user has a valid cache WITHOUT constructing prompt
25
- * This is fast and should be called BEFORE expensive prompt construction
26
- * @param {string} userId - User identifier
27
- * @returns {Promise<{hasValidCache: boolean, cacheName: string|null, needsRefresh: boolean}>}
31
+ * Check cache validity across all storage layers
28
32
  */
29
33
  async checkCacheValidity(userId) {
30
34
  console.log('๐Ÿ” Quick cache check for user:', userId);
35
+ const checkStart = performance.now();
31
36
 
32
37
  try {
33
- const existingCache = await this.getUserCache(userId);
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
+ }
34
59
 
35
60
  if (!existingCache) {
36
- console.log('๐Ÿ“ญ No cache exists - will need to create one');
61
+ console.log('๐Ÿ“ญ No cache exists (checked:', (performance.now() - checkStart).toFixed(0), 'ms)');
37
62
  return {
38
63
  hasValidCache: false,
39
64
  cacheName: null,
40
65
  needsRefresh: false,
41
- reason: 'no_cache'
66
+ reason: 'no_cache',
67
+ source: 'none'
42
68
  };
43
69
  }
44
70
 
45
71
  if (this.isCacheExpired(existingCache)) {
46
- console.log('โฐ Cache expired - will need to create new one');
72
+ console.log('โฐ Cache expired (from:', source, ')');
47
73
  return {
48
74
  hasValidCache: false,
49
75
  cacheName: null,
50
76
  needsRefresh: true,
51
- reason: 'expired'
77
+ reason: 'expired',
78
+ source: source
52
79
  };
53
80
  }
54
81
 
@@ -56,9 +83,11 @@ class CacheManager {
56
83
  const needsRefresh = remainingHours < this.CACHE_REFRESH_THRESHOLD_HOURS;
57
84
 
58
85
  console.log('โœ… Valid cache found:', {
86
+ source: source,
59
87
  cacheName: existingCache.cache_name,
60
88
  remainingHours: remainingHours,
61
- needsRefresh: needsRefresh
89
+ needsRefresh: needsRefresh,
90
+ checkTime: (performance.now() - checkStart).toFixed(0) + 'ms'
62
91
  });
63
92
 
64
93
  return {
@@ -66,7 +95,8 @@ class CacheManager {
66
95
  cacheName: existingCache.cache_name,
67
96
  needsRefresh: needsRefresh,
68
97
  remainingHours: remainingHours,
69
- reason: 'valid'
98
+ reason: 'valid',
99
+ source: source
70
100
  };
71
101
 
72
102
  } catch (error) {
@@ -75,17 +105,14 @@ class CacheManager {
75
105
  hasValidCache: false,
76
106
  cacheName: null,
77
107
  needsRefresh: false,
78
- reason: 'error'
108
+ reason: 'error',
109
+ source: 'none'
79
110
  };
80
111
  }
81
112
  }
82
113
 
83
114
  /**
84
- * Get or create a cache for a user's system prompt
85
- * NOW: Can be called conditionally based on checkCacheValidity result
86
- * @param {string} userId - User identifier
87
- * @param {string} systemPrompt - The large system instruction to cache
88
- * @returns {Promise<{cacheName: string, isNewCache: boolean}>}
115
+ * Get or create cache
89
116
  */
90
117
  async getOrCreateCache(userId, systemPrompt) {
91
118
  console.log('๐Ÿ” ===== CACHE MANAGER: GET OR CREATE =====');
@@ -97,18 +124,11 @@ class CacheManager {
97
124
  });
98
125
 
99
126
  try {
100
- // Try to get existing cache for user
101
- const existingCache = await this.getUserCache(userId);
127
+ // Quick check across all layers
128
+ const existingCache = await this.getUserCacheFromAnyLayer(userId);
102
129
 
103
130
  if (existingCache && !this.isCacheExpired(existingCache)) {
104
- console.log('โœ… Using existing cache:', {
105
- cacheName: existingCache.cache_name,
106
- createdAt: existingCache.created_at,
107
- expiresAt: existingCache.expires_at,
108
- ageHours: this.getCacheAgeHours(existingCache),
109
- remainingHours: this.getCacheRemainingHours(existingCache)
110
- });
111
-
131
+ console.log('โœ… Using existing cache:', existingCache.cache_name);
112
132
  return {
113
133
  cacheName: existingCache.cache_name,
114
134
  isNewCache: false
@@ -116,7 +136,7 @@ class CacheManager {
116
136
  }
117
137
 
118
138
  // Create new cache
119
- console.log('๐Ÿ†• Creating new cache for user:', userId);
139
+ console.log('๐Ÿ†• Creating new Gemini cache...');
120
140
  const cacheStartTime = performance.now();
121
141
  const modelName = "gemini-3-flash-preview";
122
142
 
@@ -129,17 +149,18 @@ class CacheManager {
129
149
  });
130
150
 
131
151
  const cacheCreationTime = performance.now() - cacheStartTime;
132
- console.log('โœ… Cache created successfully:', {
152
+ console.log('โœ… Gemini cache created:', {
133
153
  cacheName: cache.name,
134
154
  creationTime: cacheCreationTime.toFixed(2) + 'ms',
135
155
  ttlHours: this.CACHE_TTL_HOURS,
136
156
  expiresAt: new Date(Date.now() + this.CACHE_TTL_SECONDS * 1000).toISOString()
137
157
  });
138
158
 
139
- // Save cache info to database
140
- await this.saveUserCache(userId, cache.name);
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');
141
163
 
142
- console.log('๐Ÿ’พ Cache info saved to database');
143
164
  console.log('๐Ÿ” ===== CACHE MANAGER: COMPLETE =====');
144
165
 
145
166
  return {
@@ -154,7 +175,6 @@ class CacheManager {
154
175
  timestamp: new Date().toISOString()
155
176
  });
156
177
 
157
- // Return null to indicate cache unavailable - caller should proceed without cache
158
178
  return {
159
179
  cacheName: null,
160
180
  isNewCache: false,
@@ -164,14 +184,172 @@ class CacheManager {
164
184
  }
165
185
 
166
186
  /**
167
- * Get user's cache info from database
168
- * FIXED: Using correct PostgreSQL syntax (not MySQL)
169
- * @param {string} userId
170
- * @returns {Promise<object|null>}
187
+ * Try to get cache from any available layer
171
188
  */
172
- async getUserCache(userId) {
173
- console.log('๐Ÿ“‚ Fetching cache info from database for user:', userId);
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...');
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);
174
345
 
346
+ return true;
347
+ }
348
+
349
+ /**
350
+ * Get from PostgreSQL
351
+ */
352
+ async getUserCache(userId) {
175
353
  try {
176
354
  const query = `
177
355
  SELECT cache_name, created_at, expires_at
@@ -184,87 +362,57 @@ class CacheManager {
184
362
  const result = await pgClient.query(query, [userId]);
185
363
 
186
364
  if (result && result.rows && result.rows.length > 0) {
187
- console.log('โœ… Found existing cache record:', {
188
- cacheName: result.rows[0].cache_name,
189
- createdAt: result.rows[0].created_at,
190
- expiresAt: result.rows[0].expires_at
191
- });
192
365
  return result.rows[0];
193
366
  }
194
367
 
195
- console.log('๐Ÿ“ญ No existing cache found for user');
196
368
  return null;
197
369
 
198
370
  } catch (error) {
199
- console.error('โŒ Database query failed:', error.message);
371
+ console.error('โŒ PostgreSQL fetch failed:', error.message);
200
372
  return null;
201
373
  }
202
374
  }
203
375
 
204
376
  /**
205
- * Save cache info to database
206
- * FIXED: Using PostgreSQL syntax instead of MySQL
207
- * @param {string} userId
208
- * @param {string} cacheName
377
+ * Get from Redis
209
378
  */
210
- async saveUserCache(userId, cacheName) {
211
- console.log('๐Ÿ’พ Saving cache info to database:', {
212
- userId: userId,
213
- cacheName: cacheName
214
- });
215
-
379
+ async getUserCacheFromRedis(userId) {
380
+ if (!redisClient) return null;
381
+
216
382
  try {
217
- const expiresAt = new Date(Date.now() + this.CACHE_TTL_SECONDS * 1000);
383
+ const key = `gemini_cache:${userId}`;
384
+ const data = await redisClient.get(key);
218
385
 
219
- // PostgreSQL syntax with UPSERT (ON CONFLICT)
220
- const query = `
221
- INSERT INTO user_gemini_caches
222
- (user_id, cache_name, created_at, expires_at)
223
- VALUES ($1, $2, NOW(), $3)
224
- ON CONFLICT (user_id)
225
- DO UPDATE SET
226
- cache_name = EXCLUDED.cache_name,
227
- created_at = NOW(),
228
- expires_at = EXCLUDED.expires_at
229
- `;
230
-
231
- await pgClient.query(query, [userId, cacheName, expiresAt]);
386
+ if (data) {
387
+ return JSON.parse(data);
388
+ }
232
389
 
233
- console.log('โœ… Cache info saved successfully');
390
+ return null;
234
391
 
235
392
  } catch (error) {
236
- console.error('โŒ Failed to save cache info:', error.message);
237
- console.error('โŒ Error details:', error);
238
- // Don't throw - cache creation succeeded, DB save is secondary
393
+ console.error('โŒ Redis fetch failed:', error.message);
394
+ return null;
239
395
  }
240
396
  }
241
397
 
398
+ /**
399
+ * Get from Memory
400
+ */
401
+ getUserCacheFromMemory(userId) {
402
+ return this.memoryCache.get(userId) || null;
403
+ }
404
+
242
405
  /**
243
406
  * Check if cache is expired
244
- * @param {object} cacheRecord
245
- * @returns {boolean}
246
407
  */
247
408
  isCacheExpired(cacheRecord) {
248
409
  const now = new Date();
249
410
  const expiresAt = new Date(cacheRecord.expires_at);
250
-
251
- const isExpired = now >= expiresAt;
252
-
253
- if (isExpired) {
254
- console.log('โฐ Cache expired:', {
255
- expiresAt: expiresAt.toISOString(),
256
- now: now.toISOString(),
257
- expiredBy: ((now - expiresAt) / (1000 * 60)).toFixed(2) + ' minutes'
258
- });
259
- }
260
-
261
- return isExpired;
411
+ return now >= expiresAt;
262
412
  }
263
413
 
264
414
  /**
265
415
  * Get cache age in hours
266
- * @param {object} cacheRecord
267
- * @returns {number}
268
416
  */
269
417
  getCacheAgeHours(cacheRecord) {
270
418
  const now = new Date();
@@ -275,8 +423,6 @@ class CacheManager {
275
423
 
276
424
  /**
277
425
  * Get remaining cache lifetime in hours
278
- * @param {object} cacheRecord
279
- * @returns {number}
280
426
  */
281
427
  getCacheRemainingHours(cacheRecord) {
282
428
  const now = new Date();
@@ -286,34 +432,52 @@ class CacheManager {
286
432
  }
287
433
 
288
434
  /**
289
- * Clean up expired caches from database (maintenance task)
290
- * FIXED: PostgreSQL syntax
435
+ * Clean up expired caches from all layers
291
436
  */
292
437
  async cleanupExpiredCaches() {
293
- 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
+ };
294
445
 
446
+ // PostgreSQL cleanup
295
447
  try {
296
- const query = `
297
- DELETE FROM user_gemini_caches
298
- WHERE expires_at < NOW()
299
- `;
300
-
448
+ const query = `DELETE FROM user_gemini_caches WHERE expires_at < NOW()`;
301
449
  const result = await pgClient.query(query);
302
-
303
- console.log('โœ… Cleanup complete:', {
304
- deletedRecords: result.rowCount || 0
305
- });
306
-
450
+ results.postgresql = result.rowCount || 0;
307
451
  } catch (error) {
308
- console.error('โŒ Cleanup failed:', error.message);
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
+ }
309
464
  }
465
+
466
+ console.log('โœ… Cleanup results:', results);
467
+ return results;
310
468
  }
311
469
 
312
470
  /**
313
- * Get cache statistics for monitoring
314
- * FIXED: PostgreSQL syntax with EXTRACT instead of TIMESTAMPDIFF
471
+ * Get cache statistics
315
472
  */
316
473
  async getCacheStats() {
474
+ const stats = {
475
+ memory_caches: this.memoryCache.size,
476
+ postgresql: null,
477
+ redis: null
478
+ };
479
+
480
+ // PostgreSQL stats
317
481
  try {
318
482
  const query = `
319
483
  SELECT
@@ -325,37 +489,22 @@ class CacheManager {
325
489
  `;
326
490
 
327
491
  const result = await pgClient.query(query);
328
- return result.rows[0];
329
-
492
+ stats.postgresql = result.rows[0];
330
493
  } catch (error) {
331
- console.error('โŒ Failed to get cache stats:', error.message);
332
- return null;
494
+ console.error('โŒ PostgreSQL stats failed:', error.message);
333
495
  }
334
- }
335
-
336
- /**
337
- * NEW: Proactively refresh cache in background if it's about to expire
338
- * This ensures users always have a fresh cache without waiting
339
- * @param {string} userId
340
- * @param {function} getSystemPromptFn - Function that returns the system prompt
341
- */
342
- async refreshCacheIfNeeded(userId, getSystemPromptFn) {
343
- const cacheStatus = await this.checkCacheValidity(userId);
344
496
 
345
- if (cacheStatus.needsRefresh) {
346
- console.log('๐Ÿ”„ Cache needs refresh - starting background refresh');
347
-
348
- // Do this asynchronously - don't wait
349
- setImmediate(async () => {
350
- try {
351
- const systemPrompt = await getSystemPromptFn();
352
- await this.getOrCreateCache(userId, systemPrompt);
353
- console.log('โœ… Background cache refresh completed');
354
- } catch (error) {
355
- console.error('โŒ Background cache refresh failed:', error.message);
356
- }
357
- });
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
+ }
358
505
  }
506
+
507
+ return stats;
359
508
  }
360
509
  }
361
510