alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
141 lines (116 loc) • 3.78 kB
text/typescript
import { $atom, $inject, $state, Alepha, type Static, z } from "alepha";
import { CacheProvider } from "alepha/cache";
import { $logger } from "alepha/logger";
import { RedisProvider } from "alepha/redis";
// ---------------------------------------------------------------------------------------------------------------------
/**
* Redis cache configuration atom.
*/
export const redisCacheOptions = $atom({
name: "alepha.cache.redis.options",
schema: z.object({
prefix: z
.text({
description:
"Prefix for all cache keys in Redis. Useful for testing or multi-tenant applications.",
})
.optional(),
}),
default: {},
});
export type RedisCacheOptions = Static<typeof redisCacheOptions.schema>;
declare module "alepha" {
interface State {
[redisCacheOptions.key]: RedisCacheOptions;
}
}
// ---------------------------------------------------------------------------------------------------------------------
export class RedisCacheProvider extends CacheProvider {
protected readonly log = $logger();
protected readonly redisProvider = $inject(RedisProvider);
protected readonly options = $state(redisCacheOptions);
protected readonly alepha = $inject(Alepha);
public async get(name: string, key: string): Promise<Uint8Array | undefined> {
if (!this.alepha.isStarted()) {
return;
}
const keyWithPrefix = this.prefix(name, key);
const buffer = await this.redisProvider.get(keyWithPrefix);
if (!buffer) {
return;
}
this.log.debug(`Cache hit`, {
size: buffer.byteLength,
key: keyWithPrefix,
});
return new Uint8Array(buffer);
}
public async set(
name: string,
key: string,
value: Uint8Array | string,
ttl?: number,
): Promise<Uint8Array> {
if (!this.alepha.isReady()) {
return new Uint8Array(Buffer.from(value));
}
const buffer = Buffer.from(value);
const prefix = this.prefix(name, key);
if (ttl) {
return new Uint8Array(
await this.redisProvider.set(prefix, buffer, {
expiration: { type: "PX", value: ttl },
}),
);
}
return new Uint8Array(await this.redisProvider.set(prefix, buffer));
}
public async del(name: string, ...keys: string[]): Promise<void> {
const nameKey = this.prefix(name);
if (keys.length === 0) {
const matched = await this.redisProvider.keys(`${nameKey}:*`);
if (matched.length > 0) {
await this.redisProvider.del(matched);
}
return;
}
const prefixed = keys.map((key) =>
key.startsWith(nameKey) ? key : this.prefix(name, key),
);
if (prefixed.length > 0) {
await this.redisProvider.del(prefixed);
}
}
public async has(name: string, key: string): Promise<boolean> {
return this.redisProvider.has(this.prefix(name, key));
}
public async keys(name: string, filter?: string): Promise<string[]> {
if (filter) {
return await this.redisProvider.keys(`${this.prefix(name)}:${filter}*`);
}
return this.redisProvider.keys(`${this.prefix(name)}:*`);
}
public async clear(): Promise<void> {
this.log.debug("Clearing all cache");
const pattern = `${this.prefix()}:*`;
const keys = await this.redisProvider.keys(pattern);
if (keys.length > 0) {
await this.redisProvider.del(keys);
}
}
public async incr(
name: string,
key: string,
amount: number,
): Promise<number> {
const keyWithPrefix = this.prefix(name, key);
return this.redisProvider.incr(keyWithPrefix, amount);
}
protected prefix(...path: string[]): string {
const parts = ["cache", ...path];
if (this.options.prefix) {
parts.unshift(this.options.prefix);
}
return parts.join(":");
}
}