alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
505 lines (504 loc) • 14.9 kB
JavaScript
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