@fedify/redis
Version:
Redis drivers for Fedify
116 lines (112 loc) • 3.62 kB
JavaScript
import { Temporal } from "@js-temporal/polyfill";
import { JsonCodec } from "./codec.js";
import { Buffer } from "node:buffer";
//#region src/kv.ts
/**
* A key–value store that uses Redis as the underlying storage.
*
* @example
* ```ts ignore
* import { createFederation } from "@fedify/fedify";
* import { RedisKvStore } from "@fedify/redis";
* import { Redis, Cluster } from "ioredis";
*
* // Using a standalone Redis instance:
* const federation = createFederation({
* // ...
* kv: new RedisKvStore(new Redis()),
* });
*
* // Using a Redis Cluster:
* const cluster = new Cluster([
* { host: "127.0.0.1", port: 7000 },
* { host: "127.0.0.1", port: 7001 },
* { host: "127.0.0.1", port: 7002 },
* ]);
* const federation = createFederation({
* // ...
* kv: new RedisKvStore(cluster),
* });
* ```
*/
var RedisKvStore = class {
#redis;
#keyPrefix;
#keyPrefixStr;
#codec;
#textEncoder = new TextEncoder();
/**
* Creates a new Redis key–value store.
* @param redis The Redis client (standalone or cluster) to use.
* @param options The options for the key–value store.
*/
constructor(redis, options = {}) {
this.#redis = redis;
this.#keyPrefix = options.keyPrefix ?? "fedify::";
this.#keyPrefixStr = typeof this.#keyPrefix === "string" ? this.#keyPrefix : new TextDecoder().decode(new Uint8Array(this.#keyPrefix));
this.#codec = options.codec ?? new JsonCodec();
}
#serializeKey(key) {
const suffix = key.map((part) => part.replaceAll(":", "_:")).join("::");
if (typeof this.#keyPrefix === "string") return `${this.#keyPrefix}${suffix}`;
const suffixBytes = this.#textEncoder.encode(suffix);
return Buffer.concat([new Uint8Array(this.#keyPrefix), suffixBytes]);
}
async get(key) {
const serializedKey = this.#serializeKey(key);
const encodedValue = await this.#redis.getBuffer(serializedKey);
if (encodedValue == null) return void 0;
return this.#codec.decode(encodedValue);
}
async set(key, value, options) {
const serializedKey = this.#serializeKey(key);
const encodedValue = this.#codec.encode(value);
if (options?.ttl != null) await this.#redis.setex(serializedKey, options.ttl.total("second"), encodedValue);
else await this.#redis.set(serializedKey, encodedValue);
}
async delete(key) {
const serializedKey = this.#serializeKey(key);
await this.#redis.del(serializedKey);
}
#deserializeKey(redisKey) {
const suffix = redisKey.slice(this.#keyPrefixStr.length);
return suffix.split("::").map((p) => p.replaceAll("_:", ":"));
}
/**
* {@inheritDoc KvStore.list}
* @since 1.10.0
*/
async *list(prefix) {
let pattern;
let exactKey = null;
if (prefix == null || prefix.length === 0) pattern = `${this.#keyPrefixStr}*`;
else {
const prefixKey = this.#serializeKey(prefix);
const prefixKeyFullStr = typeof prefixKey === "string" ? prefixKey : new TextDecoder().decode(new Uint8Array(prefixKey));
exactKey = prefixKey;
pattern = `${prefixKeyFullStr}::*`;
}
if (exactKey != null) {
const exactValue = await this.#redis.getBuffer(exactKey);
if (exactValue != null) yield {
key: prefix,
value: this.#codec.decode(exactValue)
};
}
let cursor = "0";
do {
const [nextCursor, keys] = await this.#redis.scan(cursor, "MATCH", pattern, "COUNT", 100);
cursor = nextCursor;
for (const key of keys) {
const encodedValue = await this.#redis.getBuffer(key);
if (encodedValue == null) continue;
yield {
key: this.#deserializeKey(key),
value: this.#codec.decode(encodedValue)
};
}
} while (cursor !== "0");
}
};
//#endregion
export { RedisKvStore };