Skip to content

Instantly share code, notes, and snippets.

@tomfa
Last active December 29, 2021 11:43
Show Gist options
  • Save tomfa/6d1c94fa46460f6c4cbc2b98bf8aa2c5 to your computer and use it in GitHub Desktop.
Save tomfa/6d1c94fa46460f6c4cbc2b98bf8aa2c5 to your computer and use it in GitHub Desktop.
Caching in Node with Redis and/or in-memory storage + Browser with localStorage
import { ICache } from './types';
import { MemoryClient } from './memoryClient';
import { RedisClient } from './redisClient';
export class CacheClient implements ICache {
private client: ICache;
constructor({ redisUrl }: { redisUrl?: string }) {
if (redisUrl) {
this.client = new RedisClient({ redisUrl });
} else {
console.log(`CacheClient is missing redisUrl. Falling back to MemoryCache`);
this.client = new MemoryClient();
}
}
get(key: string): Promise<string | null> {
return this.client.get(key);
}
set(key: string, value: string, cacheDurationSeconds: number): Promise<void> {
return this.client.set(key, value, cacheDurationSeconds);
}
}
const client = new CacheClient({ redisUrl: process.env.REDIS_URL });
export default client;
import { ICache } from './types';
export class LocalStoreClient implements ICache {
private localStore: Storage;
defaultExpiryTimeSeconds: number;
private prefix: string;
constructor({ defaultExpiryTimeSeconds = 3600, prefix = 'cache-' }: { defaultExpiryTimeSeconds: number, prefix: string } = {}) {
if (window !== undefined) {
this.localStore = window.localStorage
}
this.defaultExpiryTimeSeconds = defaultExpiryTimeSeconds
this.prefix = prefix
}
async get(key: string): Promise<string | null> {
const value = this.localStore.getItem(this.prefix + key);
if (value) {
const data = JSON.parse(value);
const hasExpired = data.expiry <= Date.now();
if (hasExpired) {
this.localStore.removeItem(this.prefix + key);
return null;
}
return data.data;
}
return null;
}
async set(key: string, value: string, cacheDurationSeconds?: number): Promise<void> {
const expiry = Date.now() + (cacheDurationSeconds || this.defaultExpiryTimeSeconds) * 1000;
this.localStore.setItem(this.prefix + key, JSON.stringify({ expiry, data: value }));
}
}
import { ICache, InternalCacheValue } from './types';
export class MemoryClient implements ICache {
private cachedData: Record<string, InternalCacheValue> = {};
async get(key: string): Promise<string | null> {
const value = this.cachedData[key];
if (value) {
const hasExpired = value.expiry <= Date.now();
if (hasExpired) {
delete this.cachedData[key];
return null;
}
return value.data;
}
return null;
}
async set(key: string, value: string, cacheDurationSeconds: number): Promise<void> {
const expiry = Date.now() + cacheDurationSeconds * 1000;
this.cachedData[key] = { expiry, data: value };
}
}
/*
* This will use redis cache if process.env.REDIS_URL is
* present. Otherwise, it'll use a simple in-memory cache.
*/
import cache from './cache';
type HeavyDataStructure = { message: string }
const cachedAPICall = async () => {
const cachedValue = await cache.get('key');
if (cachedValue) {
return JSON.parse(cachedValue) as HeavyDataStructure;
}
const data: HeavyDataStructure = await someSlowFunction();
await cache.set('key', JSON.stringify(data));
return data;
}
import redis from 'redis';
import { ICache } from './types';
export class RedisClient implements ICache {
private redisClient: redis.RedisClient;
constructor(args: { redisUrl: string }) {
this.redisClient = redis.createClient({
url: args.redisUrl,
});
}
async get(key: string): Promise<string | null> {
return new Promise((resolve, reject) => {
this.redisClient.get(key, (err, value) => {
if (!err) {
resolve(value);
} else {
reject(err);
}
});
});
}
async set(key: string, value: string, cachingDurationSeconds: number): Promise<void> {
this.redisClient.setex(key, cachingDurationSeconds, value);
}
}
export interface ICache {
get(key: string): Promise<string | null>;
set(key: string, value: string, cacheDurationSeconds: number): Promise<void>;
}
type InternalCacheValue = { expiry: number; data: string };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment