myavana-bot-test-core 2.1.4 โ†’ 2.1.6

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/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // packages/core/index.js - Fixed exports with proper module loading
2
2
  const path = require('path');
3
+ const cacheManager = require('./src/cacheManager');
3
4
 
4
5
  // Helper function to safely load modules
5
6
  function safeRequire(modulePath, moduleName) {
@@ -28,6 +29,8 @@ const modules = {
28
29
  // AI and session management
29
30
  ai: safeRequire('./src/ai', 'ai'),
30
31
  DatabaseSessionStore: safeRequire('./src/session', 'DatabaseSessionStore'),
32
+ cacheManager: safeRequire('./src/cacheManager', 'cacheManager'),
33
+
31
34
 
32
35
  // User and conversation management
33
36
  getUserDetails: safeRequire('./src/user', 'user')?.getUserDetails,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myavana-bot-test-core",
3
- "version": "2.1.4",
3
+ "version": "2.1.6",
4
4
  "description": "Shared bot functionality with enhanced features",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -0,0 +1,126 @@
1
+ #!/bin/bash
2
+ # setup-cache-maintenance.sh
3
+ # Sets up automated cache maintenance via cron
4
+
5
+ echo "๐Ÿ”ง Setting up Gemini Cache Maintenance"
6
+ echo "======================================"
7
+
8
+ # Get the directory where this script is located
9
+ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
10
+ MAINTENANCE_SCRIPT="$SCRIPT_DIR/cacheMaintenanceScript.js"
11
+ LOG_DIR="$SCRIPT_DIR/logs"
12
+ LOG_FILE="$LOG_DIR/cache-maintenance.log"
13
+
14
+ # Check if Node.js is installed
15
+ if ! command -v node &> /dev/null; then
16
+ echo "โŒ Error: Node.js is not installed"
17
+ echo "Please install Node.js first: https://nodejs.org/"
18
+ exit 1
19
+ fi
20
+
21
+ echo "โœ… Node.js found: $(node --version)"
22
+
23
+ # Check if maintenance script exists
24
+ if [ ! -f "$MAINTENANCE_SCRIPT" ]; then
25
+ echo "โŒ Error: Maintenance script not found at $MAINTENANCE_SCRIPT"
26
+ exit 1
27
+ fi
28
+
29
+ echo "โœ… Maintenance script found"
30
+
31
+ # Create logs directory if it doesn't exist
32
+ if [ ! -d "$LOG_DIR" ]; then
33
+ mkdir -p "$LOG_DIR"
34
+ echo "โœ… Created logs directory: $LOG_DIR"
35
+ else
36
+ echo "โœ… Logs directory exists: $LOG_DIR"
37
+ fi
38
+
39
+ # Create log file if it doesn't exist
40
+ touch "$LOG_FILE"
41
+ echo "โœ… Log file ready: $LOG_FILE"
42
+
43
+ # Test the maintenance script
44
+ echo ""
45
+ echo "๐Ÿงช Testing maintenance script..."
46
+ node "$MAINTENANCE_SCRIPT"
47
+
48
+ if [ $? -eq 0 ]; then
49
+ echo "โœ… Maintenance script test successful"
50
+ else
51
+ echo "โŒ Maintenance script test failed"
52
+ echo "Please check the script for errors before setting up cron"
53
+ exit 1
54
+ fi
55
+
56
+ # Generate cron job entry
57
+ CRON_ENTRY="0 2 * * * /usr/bin/node $MAINTENANCE_SCRIPT >> $LOG_FILE 2>&1"
58
+
59
+ echo ""
60
+ echo "๐Ÿ“‹ Cron Job Configuration"
61
+ echo "========================="
62
+ echo "Schedule: Daily at 2:00 AM"
63
+ echo "Script: $MAINTENANCE_SCRIPT"
64
+ echo "Log: $LOG_FILE"
65
+ echo ""
66
+ echo "Cron entry:"
67
+ echo "$CRON_ENTRY"
68
+ echo ""
69
+
70
+ # Ask user if they want to add to crontab
71
+ read -p "Do you want to add this to your crontab? (y/n) " -n 1 -r
72
+ echo
73
+ if [[ $REPLY =~ ^[Yy]$ ]]; then
74
+ # Check if cron entry already exists
75
+ if crontab -l 2>/dev/null | grep -q "$MAINTENANCE_SCRIPT"; then
76
+ echo "โš ๏ธ Cron job already exists for this script"
77
+ read -p "Do you want to remove the old entry and add a new one? (y/n) " -n 1 -r
78
+ echo
79
+ if [[ $REPLY =~ ^[Yy]$ ]]; then
80
+ # Remove old entries
81
+ crontab -l 2>/dev/null | grep -v "$MAINTENANCE_SCRIPT" | crontab -
82
+ echo "โœ… Removed old cron entry"
83
+ else
84
+ echo "โŒ Setup cancelled"
85
+ exit 0
86
+ fi
87
+ fi
88
+
89
+ # Add new cron entry
90
+ (crontab -l 2>/dev/null; echo "$CRON_ENTRY") | crontab -
91
+
92
+ if [ $? -eq 0 ]; then
93
+ echo "โœ… Cron job added successfully!"
94
+ echo ""
95
+ echo "Current crontab:"
96
+ crontab -l | grep "$MAINTENANCE_SCRIPT"
97
+ else
98
+ echo "โŒ Failed to add cron job"
99
+ exit 1
100
+ fi
101
+ else
102
+ echo "โ„น๏ธ Cron job not added"
103
+ echo ""
104
+ echo "To add manually, run:"
105
+ echo "crontab -e"
106
+ echo ""
107
+ echo "Then add this line:"
108
+ echo "$CRON_ENTRY"
109
+ fi
110
+
111
+ echo ""
112
+ echo "๐ŸŽ‰ Setup Complete!"
113
+ echo ""
114
+ echo "๐Ÿ“– Next Steps:"
115
+ echo "1. Monitor logs: tail -f $LOG_FILE"
116
+ echo "2. Test manually: node $MAINTENANCE_SCRIPT"
117
+ echo "3. View cron jobs: crontab -l"
118
+ echo "4. Edit cron schedule: crontab -e"
119
+ echo ""
120
+ echo "โฐ Schedule Options:"
121
+ echo "Daily at 2 AM: 0 2 * * *"
122
+ echo "Every 12 hours: 0 */12 * * *"
123
+ echo "Every 6 hours: 0 */6 * * *"
124
+ echo "Hourly: 0 * * * *"
125
+ echo ""
126
+ echo "To change schedule: crontab -e"
package/src/ai.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // ai.js
2
- const { genkit } = require('genkit');
2
+ const { genkit } = require('genkit/beta');
3
3
  const { googleAI } = require('@genkit-ai/google-genai');
4
4
 
5
5
  const config = require('./config');
@@ -0,0 +1,256 @@
1
+ // cacheMaintenanceScript.js
2
+ // Run this script daily via cron to cleanup expired caches and monitor cache health
3
+
4
+ const cacheManager = require('./cacheManager');
5
+ const { pgClient, redisClient } = require('./database');
6
+
7
+ /**
8
+ * Comprehensive cache maintenance and monitoring script
9
+ */
10
+ async function runMaintenance() {
11
+ console.log('๐Ÿงน ===== CACHE MAINTENANCE SCRIPT START =====');
12
+ console.log('๐Ÿ“… Timestamp:', new Date().toISOString());
13
+
14
+ try {
15
+
16
+ if (!pgClient) {
17
+ throw new Error('Database connection not available');
18
+ }
19
+
20
+ console.log('โœ… Database connection established');
21
+
22
+ // Step 1: Get cache statistics BEFORE cleanup
23
+ console.log('\n๐Ÿ“Š ===== PRE-CLEANUP STATISTICS =====');
24
+ const preStats = await cacheManager.getCacheStats();
25
+
26
+ if (preStats) {
27
+ console.log('Cache Statistics:', {
28
+ totalCaches: preStats.total_caches,
29
+ activeCaches: preStats.active_caches,
30
+ expiredCaches: preStats.expired_caches,
31
+ avgAgeHours: parseFloat(preStats.avg_age_hours).toFixed(2),
32
+ hitRate: preStats.active_caches > 0
33
+ ? ((preStats.active_caches / preStats.total_caches) * 100).toFixed(2) + '%'
34
+ : 'N/A'
35
+ });
36
+ } else {
37
+ console.log('โš ๏ธ Could not retrieve cache statistics');
38
+ }
39
+
40
+ // Step 2: Cleanup expired caches
41
+ console.log('\n๐Ÿงน ===== CLEANING UP EXPIRED CACHES =====');
42
+ const cleanupStartTime = Date.now();
43
+ await cacheManager.cleanupExpiredCaches();
44
+ const cleanupTime = Date.now() - cleanupStartTime;
45
+ console.log(`โœ… Cleanup completed in ${cleanupTime}ms`);
46
+
47
+ // Step 3: Get cache statistics AFTER cleanup
48
+ console.log('\n๐Ÿ“Š ===== POST-CLEANUP STATISTICS =====');
49
+ const postStats = await cacheManager.getCacheStats();
50
+
51
+ if (postStats) {
52
+ console.log('Updated Cache Statistics:', {
53
+ totalCaches: postStats.total_caches,
54
+ activeCaches: postStats.active_caches,
55
+ expiredCaches: postStats.expired_caches,
56
+ avgAgeHours: parseFloat(postStats.avg_age_hours).toFixed(2),
57
+ hitRate: postStats.active_caches > 0
58
+ ? ((postStats.active_caches / postStats.total_caches) * 100).toFixed(2) + '%'
59
+ : 'N/A'
60
+ });
61
+
62
+ // Calculate cleanup impact
63
+ if (preStats) {
64
+ const recordsRemoved = preStats.total_caches - postStats.total_caches;
65
+ console.log('\n๐Ÿ“ˆ Cleanup Impact:', {
66
+ recordsRemoved: recordsRemoved,
67
+ percentageRemoved: preStats.total_caches > 0
68
+ ? ((recordsRemoved / preStats.total_caches) * 100).toFixed(2) + '%'
69
+ : '0%'
70
+ });
71
+ }
72
+ }
73
+
74
+ // Step 4: Get cache age distribution
75
+ console.log('\n๐Ÿ“Š ===== CACHE AGE DISTRIBUTION =====');
76
+ const ageDistribution = await getCacheAgeDistribution();
77
+ if (ageDistribution) {
78
+ console.log('Age Distribution:', ageDistribution);
79
+ }
80
+
81
+ // Step 5: Identify users with stale or missing caches
82
+ console.log('\n๐Ÿ” ===== CACHE HEALTH CHECK =====');
83
+ const healthCheck = await performCacheHealthCheck();
84
+ if (healthCheck) {
85
+ console.log('Health Check Results:', healthCheck);
86
+ }
87
+
88
+ // Step 6: Generate recommendations
89
+ console.log('\n๐Ÿ’ก ===== RECOMMENDATIONS =====');
90
+ generateRecommendations(preStats, postStats);
91
+
92
+ console.log('\nโœ… ===== CACHE MAINTENANCE COMPLETE =====');
93
+ console.log('๐Ÿ“… Completed at:', new Date().toISOString());
94
+
95
+ // Close database connection if needed
96
+
97
+ process.exit(0);
98
+
99
+ } catch (error) {
100
+ console.error('\nโŒ ===== MAINTENANCE SCRIPT FAILED =====');
101
+ console.error('Error:', {
102
+ message: error.message,
103
+ stack: error.stack,
104
+ timestamp: new Date().toISOString()
105
+ });
106
+
107
+ process.exit(1);
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Get cache age distribution
113
+ */
114
+ async function getCacheAgeDistribution() {
115
+ try {
116
+ const query = `
117
+ SELECT
118
+ CASE
119
+ WHEN TIMESTAMPDIFF(HOUR, created_at, NOW()) < 6 THEN '0-6 hours'
120
+ WHEN TIMESTAMPDIFF(HOUR, created_at, NOW()) < 12 THEN '6-12 hours'
121
+ WHEN TIMESTAMPDIFF(HOUR, created_at, NOW()) < 18 THEN '12-18 hours'
122
+ WHEN TIMESTAMPDIFF(HOUR, created_at, NOW()) < 24 THEN '18-24 hours'
123
+ ELSE '24+ hours (expired)'
124
+ END as age_range,
125
+ COUNT(*) as count
126
+ FROM user_gemini_caches
127
+ WHERE expires_at > NOW()
128
+ GROUP BY age_range
129
+ ORDER BY MIN(TIMESTAMPDIFF(HOUR, created_at, NOW()))
130
+ `;
131
+
132
+ const result = await pgClient.query(query);
133
+ return result;
134
+
135
+ } catch (error) {
136
+ console.error('Failed to get age distribution:', error.message);
137
+ return null;
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Perform cache health check
143
+ */
144
+ async function performCacheHealthCheck() {
145
+ try {
146
+ // Check for users with very old caches (approaching expiration)
147
+ const expiringQuery = `
148
+ SELECT user_id, cache_name,
149
+ TIMESTAMPDIFF(HOUR, NOW(), expires_at) as hours_until_expiration
150
+ FROM user_gemini_caches
151
+ WHERE expires_at > NOW()
152
+ AND TIMESTAMPDIFF(HOUR, NOW(), expires_at) < 2
153
+ LIMIT 10
154
+ `;
155
+
156
+ const expiringSoon = await pgClient.query(expiringQuery);
157
+
158
+ // Get total unique users who have used chat
159
+ const totalUsersQuery = `
160
+ SELECT COUNT(DISTINCT user_id) as total_users
161
+ FROM user_gemini_caches
162
+ `;
163
+
164
+ const totalUsersResult = await pgClient.query(totalUsersQuery);
165
+ const totalUsers = totalUsersResult[0].total_users;
166
+
167
+ // Get active cache count
168
+ const activeCacheQuery = `
169
+ SELECT COUNT(*) as active_count
170
+ FROM user_gemini_caches
171
+ WHERE expires_at > NOW()
172
+ `;
173
+
174
+ const activeCacheResult = await pgClient.query(activeCacheQuery);
175
+ const activeCaches = activeCacheResult[0].active_count;
176
+
177
+ return {
178
+ totalUsers: totalUsers,
179
+ usersWithActiveCache: activeCaches,
180
+ cacheHitPotential: totalUsers > 0
181
+ ? ((activeCaches / totalUsers) * 100).toFixed(2) + '%'
182
+ : 'N/A',
183
+ cachesExpiringSoon: expiringSoon.length,
184
+ expiringSoonDetails: expiringSoon.slice(0, 3) // Show first 3
185
+ };
186
+
187
+ } catch (error) {
188
+ console.error('Health check failed:', error.message);
189
+ return null;
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Generate recommendations based on statistics
195
+ */
196
+ function generateRecommendations(preStats, postStats) {
197
+ const recommendations = [];
198
+
199
+ if (!postStats) {
200
+ console.log('โš ๏ธ Cannot generate recommendations without statistics');
201
+ return;
202
+ }
203
+
204
+ // Check cache hit rate
205
+ const hitRate = postStats.active_caches > 0
206
+ ? (postStats.active_caches / postStats.total_caches) * 100
207
+ : 0;
208
+
209
+ if (hitRate < 50) {
210
+ recommendations.push('โš ๏ธ Low cache hit rate (<50%). Consider:');
211
+ recommendations.push(' - Investigating why caches are expiring quickly');
212
+ recommendations.push(' - Checking if TTL needs adjustment');
213
+ } else if (hitRate > 80) {
214
+ recommendations.push('โœ… Excellent cache hit rate (>80%)!');
215
+ }
216
+
217
+ // Check average age
218
+ const avgAge = parseFloat(postStats.avg_age_hours);
219
+ if (avgAge < 6) {
220
+ recommendations.push('โš ๏ธ Low average cache age (<6 hours). Consider:');
221
+ recommendations.push(' - Checking if caches are being invalidated prematurely');
222
+ recommendations.push(' - Verifying system time is correct');
223
+ }
224
+
225
+ // Check total cache count
226
+ if (postStats.total_caches < 10) {
227
+ recommendations.push('โ„น๏ธ Low total cache count. This is normal for new deployments.');
228
+ } else if (postStats.total_caches > 10000) {
229
+ recommendations.push('โš ๏ธ High cache count (>10k). Consider:');
230
+ recommendations.push(' - More frequent cleanup runs');
231
+ recommendations.push(' - Adding database indexes for performance');
232
+ }
233
+
234
+ // Check cleanup effectiveness
235
+ if (preStats && postStats) {
236
+ const recordsRemoved = preStats.total_caches - postStats.total_caches;
237
+ if (recordsRemoved === 0 && preStats.expired_caches > 0) {
238
+ recommendations.push('โš ๏ธ Cleanup ran but removed no records. Check cleanup logic.');
239
+ } else if (recordsRemoved > 0) {
240
+ recommendations.push(`โœ… Cleanup successfully removed ${recordsRemoved} expired records.`);
241
+ }
242
+ }
243
+
244
+ if (recommendations.length === 0) {
245
+ console.log('โœ… All systems normal. No recommendations.');
246
+ } else {
247
+ recommendations.forEach(rec => console.log(rec));
248
+ }
249
+ }
250
+
251
+ // Run the maintenance script
252
+ if (require.main === module) {
253
+ runMaintenance();
254
+ }
255
+
256
+ module.exports = { runMaintenance };
@@ -0,0 +1,262 @@
1
+ // cacheManager.js
2
+
3
+ const ai = require('./ai');
4
+ const { googleAI } = require('@genkit-ai/google-genai');
5
+ const { pgClient, redisClient } = require('./database');
6
+
7
+ /**
8
+ * Cache Manager for Gemini API
9
+ * Handles creation, retrieval, and expiration of cached system instructions
10
+ */
11
+ class CacheManager {
12
+ constructor() {
13
+ this.CACHE_TTL_HOURS = 24;
14
+ this.CACHE_TTL_SECONDS = this.CACHE_TTL_HOURS * 60 * 60; // 24 hours in seconds
15
+ }
16
+
17
+ /**
18
+ * Get or create a cache for a user's system prompt
19
+ * @param {string} userId - User identifier
20
+ * @param {string} systemPrompt - The large system instruction to cache
21
+ * @returns {Promise<{cacheName: string, isNewCache: boolean}>}
22
+ */
23
+ async getOrCreateCache(userId, systemPrompt) {
24
+ console.log('๐Ÿ” ===== CACHE MANAGER: GET OR CREATE =====');
25
+ console.log('๐Ÿ“Š Cache Request:', {
26
+ userId: userId,
27
+ systemPromptLength: systemPrompt.length,
28
+ estimatedTokens: Math.ceil(systemPrompt.length / 4),
29
+ timestamp: new Date().toISOString()
30
+ });
31
+
32
+ try {
33
+ // Try to get existing cache for user
34
+ const existingCache = await this.getUserCache(userId);
35
+
36
+ if (existingCache && !this.isCacheExpired(existingCache)) {
37
+ console.log('โœ… Using existing cache:', {
38
+ cacheName: existingCache.cache_name,
39
+ createdAt: existingCache.created_at,
40
+ expiresAt: existingCache.expires_at,
41
+ ageHours: this.getCacheAgeHours(existingCache),
42
+ remainingHours: this.getCacheRemainingHours(existingCache)
43
+ });
44
+
45
+ return {
46
+ cacheName: existingCache.cache_name,
47
+ isNewCache: false
48
+ };
49
+ }
50
+
51
+ // Create new cache
52
+ console.log('๐Ÿ†• Creating new cache for user:', userId);
53
+ const cacheStartTime = performance.now();
54
+
55
+ const cache = await ai.caches.create({
56
+ model: googleAI.model('gemini-3-flash-preview'),
57
+ config: {
58
+ systemInstruction: systemPrompt,
59
+ },
60
+ ttlSeconds: this.CACHE_TTL_SECONDS
61
+ });
62
+
63
+ const cacheCreationTime = performance.now() - cacheStartTime;
64
+ console.log('โœ… Cache created successfully:', {
65
+ cacheName: cache.name,
66
+ creationTime: cacheCreationTime.toFixed(2) + 'ms',
67
+ ttlHours: this.CACHE_TTL_HOURS,
68
+ expiresAt: new Date(Date.now() + this.CACHE_TTL_SECONDS * 1000).toISOString()
69
+ });
70
+
71
+ // Save cache info to database
72
+ await this.saveUserCache(userId, cache.name);
73
+
74
+ console.log('๐Ÿ’พ Cache info saved to database');
75
+ console.log('๐Ÿ” ===== CACHE MANAGER: COMPLETE =====');
76
+
77
+ return {
78
+ cacheName: cache.name,
79
+ isNewCache: true
80
+ };
81
+
82
+ } catch (error) {
83
+ console.error('โŒ Cache creation/retrieval failed:', {
84
+ error: error.message,
85
+ userId: userId,
86
+ timestamp: new Date().toISOString()
87
+ });
88
+
89
+ // Return null to indicate cache unavailable - caller should proceed without cache
90
+ return {
91
+ cacheName: null,
92
+ isNewCache: false,
93
+ error: error.message
94
+ };
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Get user's cache info from database
100
+ * @param {string} userId
101
+ * @returns {Promise<object|null>}
102
+ */
103
+ async getUserCache(userId) {
104
+ console.log('๐Ÿ“‚ Fetching cache info from database for user:', userId);
105
+
106
+ try {
107
+ const query = `
108
+ SELECT cache_name, created_at, expires_at
109
+ FROM user_gemini_caches
110
+ WHERE user_id = ?
111
+ ORDER BY created_at DESC
112
+ LIMIT 1
113
+ `;
114
+
115
+ const result = await pgClient.query(query, [userId]);
116
+
117
+ if (result && result.length > 0) {
118
+ console.log('โœ… Found existing cache record:', {
119
+ cacheName: result[0].cache_name,
120
+ createdAt: result[0].created_at,
121
+ expiresAt: result[0].expires_at
122
+ });
123
+ return result[0];
124
+ }
125
+
126
+ console.log('๐Ÿ“ญ No existing cache found for user');
127
+ return null;
128
+
129
+ } catch (error) {
130
+ console.error('โŒ Database query failed:', error.message);
131
+ return null;
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Save cache info to database
137
+ * @param {string} userId
138
+ * @param {string} cacheName
139
+ */
140
+ async saveUserCache(userId, cacheName) {
141
+ console.log('๐Ÿ’พ Saving cache info to database:', {
142
+ userId: userId,
143
+ cacheName: cacheName
144
+ });
145
+
146
+ try {
147
+ const expiresAt = new Date(Date.now() + this.CACHE_TTL_SECONDS * 1000);
148
+
149
+ const query = `
150
+ INSERT INTO user_gemini_caches
151
+ (user_id, cache_name, created_at, expires_at)
152
+ VALUES (?, ?, NOW(), ?)
153
+ ON DUPLICATE KEY UPDATE
154
+ cache_name = VALUES(cache_name),
155
+ created_at = NOW(),
156
+ expires_at = VALUES(expires_at)
157
+ `;
158
+
159
+ await pgClient.query(query, [userId, cacheName, expiresAt]);
160
+
161
+ console.log('โœ… Cache info saved successfully');
162
+
163
+ } catch (error) {
164
+ console.error('โŒ Failed to save cache info:', error.message);
165
+ // Don't throw - cache creation succeeded, DB save is secondary
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Check if cache is expired
171
+ * @param {object} cacheRecord
172
+ * @returns {boolean}
173
+ */
174
+ isCacheExpired(cacheRecord) {
175
+ const now = new Date();
176
+ const expiresAt = new Date(cacheRecord.expires_at);
177
+
178
+ const isExpired = now >= expiresAt;
179
+
180
+ if (isExpired) {
181
+ console.log('โฐ Cache expired:', {
182
+ expiresAt: expiresAt.toISOString(),
183
+ now: now.toISOString(),
184
+ expiredBy: ((now - expiresAt) / (1000 * 60)).toFixed(2) + ' minutes'
185
+ });
186
+ }
187
+
188
+ return isExpired;
189
+ }
190
+
191
+ /**
192
+ * Get cache age in hours
193
+ * @param {object} cacheRecord
194
+ * @returns {number}
195
+ */
196
+ getCacheAgeHours(cacheRecord) {
197
+ const now = new Date();
198
+ const createdAt = new Date(cacheRecord.created_at);
199
+ const ageMs = now - createdAt;
200
+ return (ageMs / (1000 * 60 * 60)).toFixed(2);
201
+ }
202
+
203
+ /**
204
+ * Get remaining cache lifetime in hours
205
+ * @param {object} cacheRecord
206
+ * @returns {number}
207
+ */
208
+ getCacheRemainingHours(cacheRecord) {
209
+ const now = new Date();
210
+ const expiresAt = new Date(cacheRecord.expires_at);
211
+ const remainingMs = expiresAt - now;
212
+ return Math.max(0, (remainingMs / (1000 * 60 * 60)).toFixed(2));
213
+ }
214
+
215
+ /**
216
+ * Clean up expired caches from database (maintenance task)
217
+ */
218
+ async cleanupExpiredCaches() {
219
+ console.log('๐Ÿงน Cleaning up expired caches...');
220
+
221
+ try {
222
+ const query = `
223
+ DELETE FROM user_gemini_caches
224
+ WHERE expires_at < NOW()
225
+ `;
226
+
227
+ const result = await pgClient.query(query);
228
+
229
+ console.log('โœ… Cleanup complete:', {
230
+ deletedRecords: result.affectedRows || 0
231
+ });
232
+
233
+ } catch (error) {
234
+ console.error('โŒ Cleanup failed:', error.message);
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Get cache statistics for monitoring
240
+ */
241
+ async getCacheStats() {
242
+ try {
243
+ const query = `
244
+ SELECT
245
+ COUNT(*) as total_caches,
246
+ COUNT(CASE WHEN expires_at > NOW() THEN 1 END) as active_caches,
247
+ COUNT(CASE WHEN expires_at <= NOW() THEN 1 END) as expired_caches,
248
+ AVG(TIMESTAMPDIFF(HOUR, created_at, NOW())) as avg_age_hours
249
+ FROM user_gemini_caches
250
+ `;
251
+
252
+ const result = await pgClient.query(query);
253
+ return result[0];
254
+
255
+ } catch (error) {
256
+ console.error('โŒ Failed to get cache stats:', error.message);
257
+ return null;
258
+ }
259
+ }
260
+ }
261
+
262
+ module.exports = new CacheManager();