an app to share curated trails
sidetrail.app
1import "server-only";
2import Redis from "ioredis";
3
4let redis: Redis | null = null;
5
6function getRedis(): Redis {
7 if (!redis) {
8 redis = new Redis(process.env.REDIS_URL || "redis://localhost:6379", {
9 maxRetriesPerRequest: 3,
10 lazyConnect: true,
11 });
12 redis.on("error", (err) => {
13 console.error("[redis] connection error:", err.message);
14 });
15 }
16 return redis;
17}
18
19export type SimpleStoreRedisOptions = {
20 keyPrefix: string;
21 ttlSeconds: number;
22};
23
24/**
25 * Simple Redis-backed store compatible with @atproto SimpleStore interface.
26 * Used for OAuth caches that should survive server restarts.
27 */
28export class SimpleStoreRedis<V> {
29 private keyPrefix: string;
30 private ttlSeconds: number;
31
32 constructor(options: SimpleStoreRedisOptions) {
33 this.keyPrefix = options.keyPrefix;
34 this.ttlSeconds = options.ttlSeconds;
35 }
36
37 async get(key: string): Promise<V | undefined> {
38 try {
39 const value = await getRedis().get(this.keyPrefix + key);
40 if (!value) return undefined;
41 return JSON.parse(value) as V;
42 } catch (err) {
43 console.error("[redis-store] get error:", err);
44 return undefined;
45 }
46 }
47
48 async set(key: string, value: V): Promise<void> {
49 try {
50 await getRedis().set(this.keyPrefix + key, JSON.stringify(value), "EX", this.ttlSeconds);
51 } catch (err) {
52 console.error("[redis-store] set error:", err);
53 }
54 }
55
56 async del(key: string): Promise<void> {
57 try {
58 await getRedis().del(this.keyPrefix + key);
59 } catch (err) {
60 console.error("[redis-store] del error:", err);
61 }
62 }
63}