Skip to content
Open
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
8 changes: 7 additions & 1 deletion packages/kv-redis/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,14 @@ export default buildConfig({
kv: redisKVAdapter({
// Redis connection URL. Defaults to process.env.REDIS_URL
redisURL: 'redis://localhost:6379',
// Optional prefix for Redis keys to isolate the store. Defaults to 'payload-kv'
// Optional prefix for Redis keys to isolate the store. Defaults to 'payload-kv:'
keyPrefix: 'kv-storage',
// Optional TTL configuration for automatic expiration by key prefix
ttl: [
{ prefix: 'session:', ttl: 3600 },
{ prefix: 'cache:', ttl: 300 },
{ prefix: 'temp:', ttl: 60 },
Comment thread
rubixvi marked this conversation as resolved.
]
}),
})
```
Expand Down
51 changes: 49 additions & 2 deletions packages/kv-redis/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,51 @@ import type { KVAdapter, KVAdapterResult, KVStoreValue } from 'payload'

import { Redis } from 'ioredis'

/**
* Configuration rule for automatic TTL-based cache invalidation.
*/
export type TTLRule = {
/**
* Key prefix to match against (e.g., 'session:', 'cache:').
*
* This prefix is checked against the full Redis key (including any adapter-level
* `keyPrefix`) before that `keyPrefix` is applied to the user-provided key.
*/
prefix: string
/**
* Time-to-live in seconds for keys matching this rule.
*
* Must be a positive integer; non-positive values disable TTL for the matching keys.
*/
ttl: number
Comment thread
rubixvi marked this conversation as resolved.
}

/**
* Collection of TTL rules mapping Redis key prefixes to their respective TTLs.
*/
export type TTLConfig = TTLRule[]

export class RedisKVAdapter implements KVAdapter {
redisClient: Redis
resolveTTL?: (redisKey: string) => number | undefined

constructor(
readonly keyPrefix: string,
redisURL: string,
ttlConfig?: TTLConfig,
) {
this.redisClient = new Redis(redisURL)

if (ttlConfig) {
this.resolveTTL = (redisKey: string) => {
for (const rule of ttlConfig) {
if (redisKey.startsWith(rule.prefix)) {
return rule.ttl
}
}
return undefined
}
Comment thread
rubixvi marked this conversation as resolved.
}
}

async clear(): Promise<void> {
Expand Down Expand Up @@ -50,7 +87,15 @@ export class RedisKVAdapter implements KVAdapter {
}

async set(key: string, data: KVStoreValue): Promise<void> {
await this.redisClient.set(`${this.keyPrefix}${key}`, JSON.stringify(data))
const redisKey = `${this.keyPrefix}${key}`
const value = JSON.stringify(data)
const ttl = this.resolveTTL?.(redisKey)

if (ttl && ttl > 0) {
await this.redisClient.set(redisKey, value, 'EX', ttl)
} else {
await this.redisClient.set(redisKey, value)
}
}
}

Expand All @@ -63,6 +108,8 @@ export type RedisKVAdapterOptions = {
keyPrefix?: string
/** Redis connection URL (e.g., 'redis://localhost:6379'). Defaults to process.env.REDIS_URL */
redisURL?: string
/** Optional TTL configuration for automatic cache invalidation */
ttl?: TTLConfig
}

export const redisKVAdapter = (options: RedisKVAdapterOptions = {}): KVAdapterResult => {
Expand All @@ -74,6 +121,6 @@ export const redisKVAdapter = (options: RedisKVAdapterOptions = {}): KVAdapterRe
}

return {
init: () => new RedisKVAdapter(keyPrefix, redisURL),
init: () => new RedisKVAdapter(keyPrefix, redisURL, options.ttl),
}
}