UNPKG

alepha

Version:

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

505 lines (504 loc) 14.9 kB
import { $env, $hook, $inject, $module, Alepha, AlephaError, z } from "alepha"; import { $logger } from "alepha/logger"; import { RESP_TYPES, createClient } from "@redis/client"; //#region ../../src/redis/providers/RedisProvider.ts /** * Abstract Redis provider interface. * * This abstract class defines the common interface for Redis operations. * Implementations include: * - {@link NodeRedisProvider} - Uses `@redis/client` for Node.js runtime * - {@link BunRedisProvider} - Uses Bun's native `RedisClient` for Bun runtime * * @example * ```ts * // Inject the abstract provider - runtime selects the implementation * const redis = alepha.inject(RedisProvider); * * // Use common operations * await redis.set("key", "value"); * const value = await redis.get("key"); * ``` */ var RedisProvider = class {}; //#endregion //#region ../../src/redis/providers/BunRedisProvider.ts const envSchema$1 = z.object({ REDIS_URL: z.text({ default: "redis://localhost:6379", description: "Redis connection URL" }) }); /** * Bun Redis client provider using Bun's native Redis client. * * This provider uses Bun's built-in `RedisClient` class for Redis connections, * which provides excellent performance (7.9x faster than ioredis) on the Bun 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: BunRedisProvider, * }); * ``` */ var BunRedisProvider = class extends RedisProvider { log = $logger(); alepha = $inject(Alepha); env = $env(envSchema$1); client; get publisher() { if (!this.client?.connected) throw new AlephaError("Redis client is not ready"); return this.client; } get isReady() { return this.client?.connected ?? false; } start = $hook({ on: "start", handler: () => this.connect() }); stop = $hook({ on: "stop", handler: () => this.close() }); /** * Connect to the Redis server. */ async connect() { if (!this.alepha.isBun()) throw new AlephaError("BunRedisProvider requires the Bun runtime. Use NodeRedisProvider for Node.js."); this.log.debug("Connecting..."); this.client = new Bun.RedisClient(this.getUrl(), { autoReconnect: true, enableAutoPipelining: true }); this.client.onconnect = () => { this.log.trace("Redis connected"); }; this.client.onclose = (error) => { if (this.alepha.isStarted() && error) this.log.error("Redis connection closed", error); }; await this.client.connect(); this.log.info("Connection OK"); } /** * Close the connection to the Redis server. */ async close() { if (this.client) { this.log.debug("Closing connection..."); this.client.close(); this.client = void 0; this.log.info("Connection closed"); } } /** * Create a duplicate connection for pub/sub or other isolated operations. */ async duplicate() { if (typeof Bun === "undefined") throw new AlephaError("BunRedisProvider requires the Bun runtime."); const client = new Bun.RedisClient(this.getUrl(), { autoReconnect: true, enableAutoPipelining: true }); client.onclose = (error) => { if (this.alepha.isStarted() && error) this.log.error("Redis duplicate connection closed", error); }; await client.connect(); return client; } async get(key) { this.log.trace(`Getting key ${key}`); const resp = await this.publisher.getBuffer(key); if (resp === null) return; return Buffer.from(resp); } async set(key, value, options) { const buf = Buffer.isBuffer(value) ? value : Buffer.from(value, "utf-8"); const args = [key, buf.toString("binary")]; if (options?.expiration) if (options.expiration.type === "KEEPTTL") args.push("KEEPTTL"); else args.push(options.expiration.type, String(options.expiration.value)); if (options?.EX !== void 0) args.push("EX", String(options.EX)); if (options?.PX !== void 0) args.push("PX", String(options.PX)); if (options?.EXAT !== void 0) args.push("EXAT", String(options.EXAT)); if (options?.PXAT !== void 0) args.push("PXAT", String(options.PXAT)); if (options?.KEEPTTL) args.push("KEEPTTL"); if (options?.condition === "NX") args.push("NX"); else if (options?.condition === "XX") args.push("XX"); if (options?.NX) args.push("NX"); if (options?.XX) args.push("XX"); if (options?.GET) args.push("GET"); const resp = args.length === 2 ? await this.publisher.set(key, buf) : await this.publisher.send("SET", args); if (resp == null || resp === "OK") return buf; return Buffer.isBuffer(resp) ? resp : Buffer.from(String(resp), "binary"); } async has(key) { return this.publisher.exists(key); } async keys(pattern) { const keys = await this.publisher.send("KEYS", [pattern]); if (!Array.isArray(keys)) return []; return keys.map((key) => key instanceof Uint8Array ? Buffer.from(key).toString() : String(key)); } async del(keys) { if (keys.length === 0) return; await this.publisher.send("DEL", keys); } async lpush(key, value) { await this.publisher.send("LPUSH", [key, value]); } async rpop(key) { const value = await this.publisher.send("RPOP", [key]); if (value == null) return; if (value instanceof Uint8Array) return Buffer.from(value).toString(); return String(value); } async publish(channel, message) { await this.publisher.publish(channel, message); } async incr(key, amount) { const result = await this.publisher.send("INCRBY", [key, String(amount)]); return Number(result); } /** * Get the Redis connection URL. */ getUrl() { return this.env.REDIS_URL; } }; //#endregion //#region ../../src/redis/providers/RedisSubscriberProvider.ts /** * Abstract Redis subscriber provider interface. * * This abstract class defines the common interface for Redis pub/sub subscriptions. * Implementations include: * - {@link NodeRedisSubscriberProvider} - Uses `@redis/client` for Node.js runtime * - {@link BunRedisSubscriberProvider} - Uses Bun's native `RedisClient` for Bun runtime * * Redis requires separate connections for pub/sub operations, so this provider * creates a dedicated connection for subscriptions. * * @example * ```ts * // Inject the abstract provider - runtime selects the implementation * const subscriber = alepha.inject(RedisSubscriberProvider); * * // Subscribe to a channel * await subscriber.subscribe("my-channel", (message, channel) => { * console.log(`Received: ${message} on ${channel}`); * }); * ``` */ var RedisSubscriberProvider = class {}; //#endregion //#region ../../src/redis/providers/BunRedisSubscriberProvider.ts /** * Bun Redis subscriber provider for pub/sub operations. * * This provider creates a dedicated Redis connection for subscriptions, * as Redis requires separate connections for pub/sub operations. * * @example * ```ts * const subscriber = alepha.inject(RedisSubscriberProvider); * await subscriber.subscribe("channel", (message, channel) => { * console.log(`Received: ${message} on ${channel}`); * }); * ``` */ var BunRedisSubscriberProvider = class extends RedisSubscriberProvider { log = $logger(); alepha = $inject(Alepha); redisProvider = $inject(BunRedisProvider); client; get subscriber() { if (!this.client?.connected) throw new AlephaError("Redis subscriber client is not ready"); return this.client; } get isReady() { return this.client?.connected ?? false; } start = $hook({ on: "start", handler: () => this.connect() }); stop = $hook({ on: "stop", handler: () => this.close() }); /** * Connect to the Redis server for subscriptions. */ async connect() { this.log.debug("Connecting subscriber..."); this.client = await this.redisProvider.duplicate(); this.log.info("Subscriber connection OK"); } /** * Close the subscriber connection. */ async close() { if (this.client) { this.log.debug("Closing subscriber connection..."); this.client.close(); this.client = void 0; this.log.info("Subscriber connection closed"); } } async subscribe(channel, callback) { await this.subscriber.subscribe(channel, (message, ch) => { callback(typeof message === "object" && message !== null ? Buffer.from(message).toString() : String(message), ch); }); } async unsubscribe(channel, _callback) { await this.subscriber.unsubscribe(channel); } }; //#endregion //#region ../../src/redis/providers/NodeRedisProvider.ts const envSchema = z.object({ REDIS_URL: z.text({ default: "redis://localhost:6379", description: "Redis connection URL" }) }); /** * 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, * }); * ``` */ var NodeRedisProvider = class extends RedisProvider { log = $logger(); alepha = $inject(Alepha); env = $env(envSchema); client = this.createClient(); get publisher() { if (!this.client.isReady) throw new AlephaError("Redis client is not ready"); return this.client; } get isReady() { return this.client.isReady; } start = $hook({ on: "start", handler: () => this.connect() }); stop = $hook({ on: "stop", handler: () => this.close() }); /** * Connect to the Redis server. */ async connect() { this.log.debug("Connecting..."); await this.client.connect(); this.log.info("Connection OK"); } /** * Close the connection to the Redis server. */ async close() { this.log.debug("Closing connection..."); await this.client.close(); this.log.info("Connection closed"); } duplicate(options) { return this.client.duplicate({ ...options, RESP: 3 }).withTypeMapping({ [RESP_TYPES.BLOB_STRING]: Buffer }); } async get(key) { this.log.trace(`Getting key ${key}`); const resp = await this.publisher.get(key); if (resp === null) return; return Buffer.from(resp); } async set(key, value, options) { const buf = Buffer.isBuffer(value) ? value : Buffer.from(value, "utf-8"); const setOptions = {}; if (options?.expiration) if (options.expiration.type === "KEEPTTL") setOptions.KEEPTTL = true; else setOptions[options.expiration.type] = options.expiration.value; if (options?.EX !== void 0) setOptions.EX = options.EX; if (options?.PX !== void 0) setOptions.PX = options.PX; if (options?.EXAT !== void 0) setOptions.EXAT = options.EXAT; if (options?.PXAT !== void 0) setOptions.PXAT = options.PXAT; if (options?.KEEPTTL) setOptions.KEEPTTL = true; if (options?.condition === "NX") setOptions.NX = true; else if (options?.condition === "XX") setOptions.XX = true; 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 : void 0); if (resp === "OK" || !resp) return buf; return Buffer.from(resp); } async has(key) { return await this.publisher.exists(key) > 0; } async keys(pattern) { return (await this.publisher.keys(pattern)).map((key) => key.toString()); } async del(keys) { if (keys.length === 0) return; await this.publisher.del(keys); } async lpush(key, value) { await this.publisher.LPUSH(key, value); } async rpop(key) { const value = await this.publisher.RPOP(key); if (value == null) return; return String(value); } async publish(channel, message) { await this.publisher.publish(channel, message); } async incr(key, amount) { return this.publisher.INCRBY(key, amount); } /** * Get the Redis connection URL. */ getUrl() { return this.env.REDIS_URL; } /** * Redis client factory method. */ createClient() { 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; } }; //#endregion //#region ../../src/redis/providers/NodeRedisSubscriberProvider.ts /** * Node.js Redis subscriber provider using `@redis/client`. * * This provider creates a dedicated Redis connection for subscriptions, * as Redis requires separate connections for pub/sub operations. * * @example * ```ts * const subscriber = alepha.inject(RedisSubscriberProvider); * await subscriber.subscribe("channel", (message, channel) => { * console.log(`Received: ${message} on ${channel}`); * }); * ``` */ var NodeRedisSubscriberProvider = class extends RedisSubscriberProvider { log = $logger(); alepha = $inject(Alepha); redisProvider = $inject(NodeRedisProvider); client = this.createClient(); get subscriber() { if (!this.client.isReady) throw new AlephaError("Redis subscriber client is not ready"); return this.client; } get isReady() { return this.client.isReady; } start = $hook({ on: "start", handler: () => this.connect() }); stop = $hook({ on: "stop", handler: () => this.close() }); async connect() { this.log.debug("Connecting subscriber..."); await this.client.connect(); this.log.info("Subscriber connection OK"); } async close() { if (!this.client.isReady) { this.log.debug("Subscriber client not ready, skipping close"); return; } this.log.debug("Closing subscriber connection..."); await this.client.close(); this.log.info("Subscriber connection closed"); } async subscribe(channel, callback) { await this.subscriber.subscribe(channel, callback); } async unsubscribe(channel, callback) { await this.subscriber.unsubscribe(channel, callback); } /** * Redis subscriber client factory method. */ createClient() { const client = this.redisProvider.duplicate(); client.on("error", (error) => { if (this.alepha.isStarted()) this.log.error(error); }); return client; } }; //#endregion //#region ../../src/redis/index.ts /** * Redis client wrapper. * * **Features:** * - Connection pooling * - Automatic reconnection * - Command pipelining * - Pub/sub support * * @module alepha.redis */ const AlephaRedis = $module({ name: "alepha.redis", services: [RedisProvider, RedisSubscriberProvider], variants: [ NodeRedisProvider, NodeRedisSubscriberProvider, BunRedisProvider, BunRedisSubscriberProvider ], register: (alepha) => { if (alepha.isBun()) alepha.with({ provide: RedisProvider, use: BunRedisProvider }).with({ provide: RedisSubscriberProvider, use: BunRedisSubscriberProvider }); else alepha.with({ provide: RedisProvider, use: NodeRedisProvider }).with({ provide: RedisSubscriberProvider, use: NodeRedisSubscriberProvider }); } }); //#endregion export { AlephaRedis, BunRedisProvider, BunRedisSubscriberProvider, NodeRedisProvider, NodeRedisSubscriberProvider, RedisProvider, RedisSubscriberProvider }; //# sourceMappingURL=index.js.map