import { ChannelStore } from '@pikku/core/channel' import type { Channel } from '@pikku/core/channel' import { Redis, type RedisOptions } from 'ioredis' /** * Redis-based implementation of ChannelStore * * Stores channel state in Redis with configurable key prefix. * * @example * ```typescript * const redis = new Redis({ host: 'localhost', port: 6379 }) * const store = new RedisChannelStore(redis, 'myapp') * ``` */ export class RedisChannelStore extends ChannelStore { private redis: Redis private keyPrefix: string private ownsConnection: boolean /** * @param connectionOrConfig - ioredis Redis instance or RedisOptions config * @param keyPrefix - Redis key prefix (default: 'pikku') */ constructor( connectionOrConfig: Redis | RedisOptions | string, keyPrefix = 'pikku' ) { super() this.keyPrefix = keyPrefix // Check if it's a Redis instance or config options if (connectionOrConfig instanceof Redis) { this.redis = connectionOrConfig this.ownsConnection = false } else if (typeof connectionOrConfig === 'string') { // It's a connection string this.redis = new Redis(connectionOrConfig) this.ownsConnection = true } else { // It's a config object this.redis = new Redis(connectionOrConfig) this.ownsConnection = true } } /** * Initialize the store (no-op for Redis, always ready) */ public async init(): Promise { // Redis doesn't require schema initialization // Just verify connection works await this.redis.ping() } private channelKey(channelId: string): string { return `${this.keyPrefix}:channel:${channelId}` } public async addChannel({ channelId, channelName, openingData, }: Channel): Promise { const key = this.channelKey(channelId) await this.redis.hmset( key, 'channelId', channelId, 'channelName', channelName, 'openingData', JSON.stringify(openingData || {}), 'createdAt', Date.now().toString() ) } public async removeChannels(channelIds: string[]): Promise { if (channelIds.length === 0) { return } const pipeline = this.redis.pipeline() for (const channelId of channelIds) { const key = this.channelKey(channelId) pipeline.del(key) // Also remove from subscriptions pipeline.del(`${this.keyPrefix}:subs:${channelId}`) } await pipeline.exec() } public async setPikkuUserId( channelId: string, pikkuUserId: string | null ): Promise { const key = this.channelKey(channelId) if (pikkuUserId) { await this.redis.hset(key, 'pikkuUserId', pikkuUserId) } else { await this.redis.hdel(key, 'pikkuUserId') } } public async getChannel( channelId: string ): Promise { const key = this.channelKey(channelId) const data = await this.redis.hgetall(key) if (!data.channelId) { throw new Error(`Channel not found: ${channelId}`) } return { channelId: data.channelId!, channelName: data.channelName!, openingData: data.openingData ? JSON.parse(data.openingData) : {}, pikkuUserId: data.pikkuUserId || undefined, } } public async setState(channelId: string, state: unknown): Promise { const key = this.channelKey(channelId) await this.redis.hset(key, 'state', JSON.stringify(state ?? null)) } public async getState(channelId: string): Promise { const key = this.channelKey(channelId) const raw = await this.redis.hget(key, 'state') if (!raw) return undefined try { const parsed = JSON.parse(raw) return parsed ?? undefined } catch { return undefined } } public async clearState(channelId: string): Promise { const key = this.channelKey(channelId) await this.redis.hdel(key, 'state') } /** * Close the Redis connection if owned by this store */ public async close(): Promise { if (this.ownsConnection) { await this.redis.quit() } } }