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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/cacheManager.js +350 -104
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myavana-bot-test-core",
3
- "version": "2.1.8",
3
+ "version": "2.2.0",
4
4
  "description": "Shared bot functionality with enhanced features",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -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 {GoogleGenAI} = require("@google/genai");
6
+ 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
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; // 24 hours in seconds
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
- * Get or create a cache for a user's system prompt
22
- * @param {string} userId - User identifier
23
- * @param {string} systemPrompt - The large system instruction to cache
24
- * @returns {Promise<{cacheName: string, isNewCache: boolean}>}
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
- // Try to get existing cache for user
37
- const existingCache = await this.getUserCache(userId);
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 for user:', userId);
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('โœ… Cache created successfully:', {
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 cache info to database
75
- 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');
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
- * Get user's cache info from database
103
- * @param {string} userId
104
- * @returns {Promise<object|null>}
187
+ * Try to get cache from any available layer
105
188
  */
106
- async getUserCache(userId) {
107
- 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...');
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
- console.log('โœ… Found existing cache record:', {
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('โŒ Database query failed:', error.message);
371
+ console.error('โŒ PostgreSQL fetch failed:', error.message);
134
372
  return null;
135
373
  }
136
374
  }
137
375
 
138
376
  /**
139
- * Save cache info to database
140
- * @param {string} userId
141
- * @param {string} cacheName
377
+ * Get from Redis
142
378
  */
143
- async saveUserCache(userId, cacheName) {
144
- console.log('๐Ÿ’พ Saving cache info to database:', {
145
- userId: userId,
146
- cacheName: cacheName
147
- });
148
-
379
+ async getUserCacheFromRedis(userId) {
380
+ if (!redisClient) return null;
381
+
149
382
  try {
150
- const expiresAt = new Date(Date.now() + this.CACHE_TTL_SECONDS * 1000);
383
+ const key = `gemini_cache:${userId}`;
384
+ const data = await redisClient.get(key);
151
385
 
152
- const query = `
153
- INSERT INTO user_gemini_caches
154
- (user_id, cache_name, created_at, expires_at)
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
- console.log('โœ… Cache info saved successfully');
390
+ return null;
165
391
 
166
392
  } catch (error) {
167
- console.error('โŒ Failed to save cache info:', error.message);
168
- // Don't throw - cache creation succeeded, DB save is secondary
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 database (maintenance task)
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('โŒ 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
+ }
238
464
  }
465
+
466
+ console.log('โœ… Cleanup results:', results);
467
+ return results;
239
468
  }
240
469
 
241
470
  /**
242
- * Get cache statistics for monitoring
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(TIMESTAMPDIFF(HOUR, created_at, NOW())) as avg_age_hours
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
- return result[0];
257
-
492
+ stats.postgresql = result.rows[0];
258
493
  } catch (error) {
259
- console.error('โŒ Failed to get cache stats:', error.message);
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