Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions packages/server/REDIS_CACHE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Redis Cache Implementation

This implementation adds Redis support to the UAAA server cache system.

## Configuration

Add the following to your configuration file to use Redis cache:

```json
{
"cacheType": "redis",
"redisUrl": "redis://localhost:6379"
}
```

Configuration options:
- `cacheType`: Either "mongo" (default) or "redis"
- `redisUrl`: Redis connection URL (optional, defaults to `redis://localhost:6379` or `REDIS_URL` environment variable)

## Environment Variables

You can also configure Redis using environment variables:
- `REDIS_URL`: Redis connection URL

## Example Configuration

For development with Docker Compose:
```json
{
"appId": "uaaa.local",
"mongoUri": "mongodb://localhost:27017/uaaa",
"cacheType": "redis",
"redisUrl": "redis://localhost:6379",
"plugins": [],
"port": 3000,
"deploymentUrl": "http://localhost:3000",
"jwtTimeout": "1h",
"refreshTimeout": "7d",
"tokenTimeout": "10m"
}
```

For production:
```json
{
"appId": "uaaa.prod",
"mongoUri": "mongodb://mongo:27017/uaaa",
"cacheType": "redis",
"redisUrl": "redis://redis:6379",
"plugins": [],
"port": 3000,
"deploymentUrl": "https://your-domain.com",
"jwtTimeout": "1h",
"refreshTimeout": "7d",
"tokenTimeout": "10m"
}
```
1 change: 1 addition & 0 deletions packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"nanoid": "^5.1.5",
"nodemailer": "^7.0.5",
"pino": "^9.9.0",
"redis": "^5.8.2",
"totp-generator": "^1.0.0",
"typanion": "^3.14.0"
},
Expand Down
12 changes: 11 additions & 1 deletion packages/server/src/cache/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Hookable } from 'hookable'
import type { App } from '../index.js'
import { CacheImpl } from './_common.js'
import { MongoCache } from './mongo.js'
import { RedisCache } from './redis.js'

export interface CacheManager extends Omit<CacheImpl, 'init'> {}

Expand All @@ -13,7 +14,15 @@ export class CacheManager extends Hookable {
}

async initCache() {
this.impl ??= new MongoCache(this)
if (!this.impl) {
const cacheType = this.app.config.get('cacheType') || 'mongo'
if (cacheType === 'redis') {
const redisUrl = this.app.config.get('redisUrl')
this.impl = new RedisCache(this, redisUrl)
} else {
this.impl = new MongoCache(this)
}
}
await this.impl.init?.()
this.set = this.impl.set.bind(this.impl)
this.setx = this.impl.setx.bind(this.impl)
Expand All @@ -37,3 +46,4 @@ export class CacheManager extends Hookable {

export * from './_common.js'
export * from './mongo.js'
export * from './redis.js'
93 changes: 93 additions & 0 deletions packages/server/src/cache/redis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { createClient, type RedisClientType } from 'redis'
import { CacheImpl } from './_common.js'
import type { CacheManager } from './index.js'

export class RedisCache extends CacheImpl {
client!: RedisClientType

constructor(manager: CacheManager, private connectionUrl?: string) {
super(manager)
}

async init(): Promise<void> {
this.client = createClient({
url: this.connectionUrl || process.env.REDIS_URL || 'redis://localhost:6379'
})

this.client.on('error', (err) => {
console.error('Redis Client Error', err)
})

await this.client.connect()
}

async disconnect(): Promise<void> {
if (this.client) {
await this.client.disconnect()
}
}

override async set(key: string, value: string, expiresIn: number): Promise<void> {
await this.client.setEx(key, Math.ceil(expiresIn / 1000), value)
}

override async get(key: string): Promise<string | null> {
return await this.client.get(key)
}

override async del(key: string): Promise<void> {
await this.client.del(key)
}

override async ttl(key: string): Promise<number> {
const ttlSeconds = await this.client.ttl(key)
if (ttlSeconds === -2) return -2 // Key does not exist
if (ttlSeconds === -1) return -1 // Key exists but has no expiration
return ttlSeconds * 1000 // Convert to milliseconds
}

override async clear(): Promise<void> {
await this.client.flushDb()
}

override async setn(key: string, value: number, expiresIn: number): Promise<void> {
await this.client.setEx(key, Math.ceil(expiresIn / 1000), value.toString())
}

override async getn(key: string): Promise<number> {
const value = await this.client.get(key)
if (value === null) return 0
const parsed = parseInt(value, 10)
return isNaN(parsed) ? 0 : parsed
}

override async incr(key: string, amount: number, expiresIn: number): Promise<number> {
// Use a transaction to ensure atomicity
const multi = this.client.multi()
multi.incrBy(key, amount)
multi.expire(key, Math.ceil(expiresIn / 1000))
const results = await multi.exec()

if (!results || results.length < 1) return 0
const result = results[0] as unknown
return typeof result === 'number' ? result : 0
}

override async expire(key: string, timeout: number): Promise<void> {
await this.client.expire(key, Math.ceil(timeout / 1000))
}

override async setx<T>(key: string, value: T, expiresIn: number): Promise<void> {
await this.client.setEx(key, Math.ceil(expiresIn / 1000), JSON.stringify(value))
}

override async getx<T>(key: string): Promise<T | null> {
const value = await this.client.get(key)
if (value === null) return null
try {
return JSON.parse(value) as T
} catch {
return null
}
}
}
4 changes: 3 additions & 1 deletion packages/server/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ const tAppConfig = type({
'trustedUpstreamIssuers?': 'string[]',
'openidClaimConfig?': type.Record('string', type({ alias: 'string', 'verifiable?': 'boolean' })),
'openidAdditionalClaims?': 'Record<string,string>',
'uiPath?': 'string'
'uiPath?': 'string',
'cacheType?': '"mongo" | "redis"',
'redisUrl?': 'string'
})

type IAppConfig = typeof tAppConfig.infer
Expand Down
61 changes: 60 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3540,6 +3540,51 @@ __metadata:
languageName: node
linkType: hard

"@redis/bloom@npm:5.8.2":
version: 5.8.2
resolution: "@redis/bloom@npm:5.8.2"
peerDependencies:
"@redis/client": ^5.8.2
checksum: 10/99ec4f127b6ce86a62d8ddde9a2a3631feb0c68abe8c4c2e4cea143c1d1e2cec5fe78f0ad79b8cabc2bc0007f4718d6bf3b253526c704bc0e2111c9ef3b28bf9
languageName: node
linkType: hard

"@redis/client@npm:5.8.2":
version: 5.8.2
resolution: "@redis/client@npm:5.8.2"
dependencies:
cluster-key-slot: "npm:1.1.2"
checksum: 10/653ba2d0efb97b4479b3e9d2c1038da1993191a863f8c0427975f63a66354638b02befc2375977a0d0906a5bd70c0f57982e905063079c0c294944c519d87c88
languageName: node
linkType: hard

"@redis/json@npm:5.8.2":
version: 5.8.2
resolution: "@redis/json@npm:5.8.2"
peerDependencies:
"@redis/client": ^5.8.2
checksum: 10/2877cd93b717344f1d1215474932bf2a662a069970356e781dc0f45f2fb4d1941c49401db33a34e9ab9347c485bba22bc97b57ddf2185e83f53ee8320968dedf
languageName: node
linkType: hard

"@redis/search@npm:5.8.2":
version: 5.8.2
resolution: "@redis/search@npm:5.8.2"
peerDependencies:
"@redis/client": ^5.8.2
checksum: 10/6cc0499a11e86ea28451c02dc5b4b3133fc32e53447e4aebf0d100b0820ba2bcccacab9f344192b6a672b56851a4053705099482f26e4639f8fc3031f87a1cc6
languageName: node
linkType: hard

"@redis/time-series@npm:5.8.2":
version: 5.8.2
resolution: "@redis/time-series@npm:5.8.2"
peerDependencies:
"@redis/client": ^5.8.2
checksum: 10/a7758173807b1464433bb3c18714b53acbc26f7a9c91d4cb3530bb8cfd85bdb60bcb4e67057ec9b053f5c64408beab4d4d3440689e6850762f9edc7e2e527231
languageName: node
linkType: hard

"@rolldown/pluginutils@npm:1.0.0-beta.29":
version: 1.0.0-beta.29
resolution: "@rolldown/pluginutils@npm:1.0.0-beta.29"
Expand Down Expand Up @@ -4990,6 +5035,7 @@ __metadata:
nanoid: "npm:^5.1.5"
nodemailer: "npm:^7.0.5"
pino: "npm:^9.9.0"
redis: "npm:^5.8.2"
totp-generator: "npm:^1.0.0"
typanion: "npm:^3.14.0"
typescript: "npm:^5.9.2"
Expand Down Expand Up @@ -6730,7 +6776,7 @@ __metadata:
languageName: node
linkType: hard

"cluster-key-slot@npm:^1.1.0":
"cluster-key-slot@npm:1.1.2, cluster-key-slot@npm:^1.1.0":
version: 1.1.2
resolution: "cluster-key-slot@npm:1.1.2"
checksum: 10/516ed8b5e1a14d9c3a9c96c72ef6de2d70dfcdbaa0ec3a90bc7b9216c5457e39c09a5775750c272369070308542e671146120153062ab5f2f481bed5de2c925f
Expand Down Expand Up @@ -12089,6 +12135,19 @@ __metadata:
languageName: node
linkType: hard

"redis@npm:^5.8.2":
version: 5.8.2
resolution: "redis@npm:5.8.2"
dependencies:
"@redis/bloom": "npm:5.8.2"
"@redis/client": "npm:5.8.2"
"@redis/json": "npm:5.8.2"
"@redis/search": "npm:5.8.2"
"@redis/time-series": "npm:5.8.2"
checksum: 10/a7635cedf25f449ecf4ee667067cff655b6d462a93225a6e93217c3fdb3a050cf503f6add9ccaedb6caaced5e80c36ff754bea8ed4b270f2a1006add041bd219
languageName: node
linkType: hard

"regex-recursion@npm:^6.0.2":
version: 6.0.2
resolution: "regex-recursion@npm:6.0.2"
Expand Down