diff --git a/src/RedisCache.ts b/src/RedisCache.ts index fb112ae..72b8c52 100644 --- a/src/RedisCache.ts +++ b/src/RedisCache.ts @@ -4,12 +4,30 @@ import Redis, { RedisOptions } from 'ioredis'; import Selector from './model/Selector'; class RedisCache implements Cache { - private static DEFAULT_TTL = 60 * 60 * 24 * 7; + // 1 hour TTL to limit stale data window if cache invalidation fails during Redis issues + private static DEFAULT_TTL = 60 * 60; private redis_: Redis; constructor(options: RedisOptions) { - this.redis_ = new Redis(options); + this.redis_ = new Redis({ + ...options, + connectTimeout: 500, // 500ms to connect + commandTimeout: 200, // 200ms per command + // retryStrategy controls retries for initial connection and reconnection attempts + // maxRetriesPerRequest controls retries for individual commands (GET, SET, etc) + retryStrategy: (times) => { + // Stop reconnection attempts after 2 tries + if (times > 2) return null; + return 50; // Wait only 50ms between reconnection attempts + }, + maxRetriesPerRequest: 0, // Don't retry commands - fail immediately + enableOfflineQueue: false, // Don't queue commands when disconnected + }); + + this.redis_.on('error', (err) => { + console.error('Redis error (app will continue without cache):', err.message); + }); } private static key_ = ({ collection, id }: Selector): string => { @@ -17,23 +35,34 @@ class RedisCache implements Cache { }; async get(selector: Selector): Promise { - const data = await this.redis_.get(RedisCache.key_(selector)); - if (!data) return null; - - return JSON.parse(data); + try { + const data = await this.redis_.get(RedisCache.key_(selector)); + if (!data) return null; + return JSON.parse(data); + } catch (err) { + console.error('Redis GET failed, continuing without cache:', err); + return null; + } } async set(selector: Selector, value: object | null): Promise { - if (!value) { + // Cache-aside pattern: Only populate cache on reads, not writes + // This prevents stale data if cache write fails but DB write succeeds + // Instead, we just invalidate the cache entry on writes + try { await this.redis_.del(RedisCache.key_(selector)); - return; + } catch (err) { + console.error('Redis cache invalidation failed, continuing without cache:', err); + // Best effort - if invalidation fails, TTL will eventually clear stale data } - - await this.redis_.setex(RedisCache.key_(selector), RedisCache.DEFAULT_TTL, JSON.stringify(value)); } async remove(selector: Selector): Promise { - await this.redis_.del(RedisCache.key_(selector)); + try { + await this.redis_.del(RedisCache.key_(selector)); + } catch (err) { + console.error('Redis DEL failed, continuing without cache:', err); + } } } diff --git a/src/db.ts b/src/db.ts index c3ebd04..ae7ce9f 100644 --- a/src/db.ts +++ b/src/db.ts @@ -60,16 +60,12 @@ class Db { const cacheSelector: Selector = { ...selector, collection: fCollection }; - try { - const cached = await this.cache_.get(cacheSelector); + const cached = await this.cache_.get(cacheSelector); - if (cached) return { - type: 'success', - value: cached as T, - }; - } catch (e) { - console.error(e); - } + if (cached) return { + type: 'success', + value: cached as T, + }; const doc = await firestore .collection(fCollection) @@ -82,11 +78,12 @@ class Db { message: `Document "${selector.id}" not found.`, }; + // Populate the cache with fresh data from Firestore (cache-aside pattern) try { - // Update the cache await this.cache_.set(cacheSelector, doc.data()); } catch (e) { - console.error(e); + console.error('Failed to populate cache after read:', e); + // Continue - cache is optional } return { @@ -120,12 +117,7 @@ class Db { await docRef.set(value, { mergeFields: keysToReplace }); } else { await docRef.set(value); - - try { - await this.cache_.set(cacheSelector, value); - } catch (e) { - console.error(e); - } + await this.cache_.set(cacheSelector, value); } return { @@ -143,11 +135,7 @@ class Db { const cacheSelector: Selector = { ...selector, collection: fCollection }; - try { - await this.cache_.remove(cacheSelector); - } catch (e) { - console.error(e); - } + await this.cache_.remove(cacheSelector); await firestore .collection(fCollection)