Skip to content

Commit 4830f1d

Browse files
committed
generic cache. local file in dev, redis in prod
1 parent 0b714d1 commit 4830f1d

2 files changed

Lines changed: 113 additions & 0 deletions

File tree

nuxt.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,10 @@ export default defineNuxtConfig({
168168
driver: 'fsLite',
169169
base: './.cache/atproto-oauth/session',
170170
},
171+
'generic-cache': {
172+
driver: 'fsLite',
173+
base: './.cache/generic',
174+
},
171175
},
172176
typescript: {
173177
tsConfig: {

server/utils/cache.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { Redis } from '@upstash/redis'
2+
3+
/**
4+
* Generic cache adapter to allow using a local cache during development and redis in production
5+
*/
6+
export interface CacheAdapter {
7+
get<T>(key: string): Promise<T | undefined>
8+
set<T>(key: string, value: T, ttl?: number): Promise<void>
9+
delete(key: string): Promise<void>
10+
}
11+
12+
/**
13+
* Local cache data entry
14+
*/
15+
interface LocalCachedEntry<T = unknown> {
16+
value: T
17+
ttl?: number
18+
cachedAt: number
19+
}
20+
21+
/**
22+
* Checks to see if a cache entry is stale locally
23+
* @param entry - The entry from the locla cache
24+
* @returns
25+
*/
26+
function isCacheEntryStale(entry: LocalCachedEntry): boolean {
27+
if (!entry.ttl) return false
28+
const now = Date.now()
29+
const expiresAt = entry.cachedAt + entry.ttl * 1000
30+
return now > expiresAt
31+
}
32+
33+
/**
34+
* Local implmentation of a cache to be used during development
35+
*/
36+
export class StorageCacheAdapter implements CacheAdapter {
37+
private readonly storage = useStorage('generic-cache')
38+
39+
async get<T>(key: string): Promise<T | undefined> {
40+
const result = await this.storage.getItem<LocalCachedEntry<T>>(key)
41+
if (!result) return
42+
if (isCacheEntryStale(result)) {
43+
await this.storage.removeItem(key)
44+
return
45+
}
46+
return result.value
47+
}
48+
49+
async set<T>(key: string, value: T, ttl?: number): Promise<void> {
50+
await this.storage.setItem(key, { value, ttl, cachedAt: Date.now() })
51+
}
52+
53+
async delete(key: string): Promise<void> {
54+
await this.storage.removeItem(key)
55+
}
56+
}
57+
58+
/**
59+
* Redis cache storage with TTL handled by redis for use in production
60+
*/
61+
export class RedisCacheAdatper implements CacheAdapter {
62+
private readonly redis: Redis
63+
private readonly prefix: string
64+
65+
formatKey(key: string): string {
66+
return `${this.prefix}:${key}`
67+
}
68+
69+
constructor(redis: Redis, prefix: string) {
70+
this.redis = redis
71+
this.prefix = prefix
72+
}
73+
74+
async get<T>(key: string): Promise<T | undefined> {
75+
const formattedKey = this.formatKey(key)
76+
const value = await this.redis.get<T>(formattedKey)
77+
if (!value) return
78+
return value
79+
}
80+
81+
async set<T>(key: string, value: T, ttl?: number): Promise<void> {
82+
const formattedKey = this.formatKey(key)
83+
if (ttl) {
84+
await this.redis.setex(formattedKey, ttl, value)
85+
} else {
86+
await this.redis.set(formattedKey, value)
87+
}
88+
}
89+
90+
async delete(key: string): Promise<void> {
91+
const formattedKey = this.formatKey(key)
92+
await this.redis.del(formattedKey)
93+
}
94+
}
95+
96+
export function getCache(prefix: string): CacheAdapter {
97+
const config = useRuntimeConfig()
98+
99+
if (!import.meta.dev && config.upstash?.redisRestUrl && config.upstash?.redisRestToken) {
100+
const redis = new Redis({
101+
url: config.upstash.redisRestUrl,
102+
token: config.upstash.redisRestToken,
103+
})
104+
return new RedisCacheAdatper(redis, prefix)
105+
}
106+
107+
console.log('using storage')
108+
return new StorageCacheAdapter()
109+
}

0 commit comments

Comments
 (0)