UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

271 lines (231 loc) 6.47 kB
import { createClient, RESP_TYPES, type RedisClientType, type SetOptions, } from "@redis/client"; import { $env, $hook, $inject, Alepha, AlephaError, type Static, z, } from "alepha"; import { $logger } from "alepha/logger"; import { RedisProvider, type RedisSetOptions } from "./RedisProvider.ts"; const envSchema = z.object({ REDIS_URL: z.text({ default: "redis://localhost:6379", description: "Redis connection URL", }), }); declare module "alepha" { interface Env extends Partial<Static<typeof envSchema>> {} } export type NodeRedisClient = RedisClientType< {}, {}, {}, 3, { 36: BufferConstructor } >; export type NodeRedisClientOptions = Parameters<typeof createClient>[0]; /** * Node.js Redis client provider using `@redis/client`. * * This provider uses the official Redis client for Node.js runtime. * * @example * ```ts * // Set REDIS_URL environment variable (default: redis://localhost:6379) * // REDIS_URL=redis://:password@myredis.example.com:6379 * * // Or configure programmatically * alepha.with({ * provide: RedisProvider, * use: NodeRedisProvider, * }); * ``` */ export class NodeRedisProvider extends RedisProvider { protected readonly log = $logger(); protected readonly alepha = $inject(Alepha); protected readonly env = $env(envSchema); protected readonly client = this.createClient(); public get publisher(): NodeRedisClient { if (!this.client.isReady) { throw new AlephaError("Redis client is not ready"); } return this.client; } public override get isReady(): boolean { return this.client.isReady; } protected readonly start = $hook({ on: "start", handler: () => this.connect(), }); protected readonly stop = $hook({ on: "stop", handler: () => this.close(), }); /** * Connect to the Redis server. */ public override async connect(): Promise<void> { this.log.debug("Connecting..."); await this.client.connect(); this.log.info("Connection OK"); } /** * Close the connection to the Redis server. */ public override async close(): Promise<void> { this.log.debug("Closing connection..."); await this.client.close(); this.log.info("Connection closed"); } public duplicate(options?: Partial<NodeRedisClientOptions>): NodeRedisClient { return this.client .duplicate({ ...options, RESP: 3, }) .withTypeMapping({ [RESP_TYPES.BLOB_STRING]: Buffer, }); } public override async get(key: string): Promise<Buffer | undefined> { this.log.trace(`Getting key ${key}`); const resp = await this.publisher.get(key); if (resp === null) { return undefined; } return Buffer.from(resp); } public override async set( key: string, value: Buffer | string, options?: RedisSetOptions, ): Promise<Buffer> { const buf = Buffer.isBuffer(value) ? value : Buffer.from(value, "utf-8"); // Convert RedisSetOptions to @redis/client SetOptions const setOptions: SetOptions = {}; // Handle expiration object format (from alepha/cache-redis, alepha/lock-redis) if (options?.expiration) { if (options.expiration.type === "KEEPTTL") { setOptions.KEEPTTL = true; } else { setOptions[options.expiration.type] = options.expiration.value; } } // Handle direct expiration properties if (options?.EX !== undefined) { setOptions.EX = options.EX; } if (options?.PX !== undefined) { setOptions.PX = options.PX; } if (options?.EXAT !== undefined) { setOptions.EXAT = options.EXAT; } if (options?.PXAT !== undefined) { setOptions.PXAT = options.PXAT; } if (options?.KEEPTTL) { setOptions.KEEPTTL = true; } // Handle condition object format if (options?.condition === "NX") { setOptions.NX = true; } else if (options?.condition === "XX") { setOptions.XX = true; } // Handle direct condition properties if (options?.NX) { setOptions.NX = true; } if (options?.XX) { setOptions.XX = true; } if (options?.GET) { setOptions.GET = true; } const resp = await this.publisher.set( key, buf, Object.keys(setOptions).length > 0 ? setOptions : undefined, ); if (resp === "OK" || !resp) { return buf; } return Buffer.from(resp); } public override async has(key: string): Promise<boolean> { const resp = await this.publisher.exists(key); return resp > 0; } public override async keys(pattern: string): Promise<string[]> { const keys = await this.publisher.keys(pattern); return keys.map((key) => key.toString()); } public override async del(keys: string[]): Promise<void> { if (keys.length === 0) { return; } await this.publisher.del(keys); } // --------------------------------------------------------- // Queue operations // --------------------------------------------------------- public override async lpush(key: string, value: string): Promise<void> { await this.publisher.LPUSH(key, value); } public override async rpop(key: string): Promise<string | undefined> { const value = await this.publisher.RPOP(key); if (value == null) { return undefined; } return String(value); } // --------------------------------------------------------- // Pub/Sub operations // --------------------------------------------------------- public override async publish( channel: string, message: string, ): Promise<void> { await this.publisher.publish(channel, message); } // --------------------------------------------------------- // Counter operations // --------------------------------------------------------- public override async incr(key: string, amount: number): Promise<number> { return this.publisher.INCRBY(key, amount); } /** * Get the Redis connection URL. */ protected getUrl(): string { return this.env.REDIS_URL; } /** * Redis client factory method. */ protected createClient(): NodeRedisClient { const client = createClient({ url: this.getUrl(), RESP: 3, }).withTypeMapping({ [RESP_TYPES.BLOB_STRING]: Buffer, }); client.on("error", (error) => { if (this.alepha.isStarted()) { this.log.error(error); } }); return client; } }