UNPKG

@fedify/redis

Version:

Redis drivers for Fedify

144 lines (140 loc) 4.56 kB
import { Temporal } from "@js-temporal/polyfill"; import { JsonCodec } from "./codec.js"; import { getLogger } from "@logtape/logtape"; //#region src/mq.ts const logger = getLogger([ "fedify", "redis", "mq" ]); /** * A message queue that uses Redis as the underlying storage. * * @example * ```ts ignore * import { createFederation } from "@fedify/fedify"; * import { RedisMessageQueue } from "@fedify/redis"; * import { Redis } from "ioredis"; * * const federation = createFederation({ * // ... * queue: new RedisMessageQueue(() => new Redis()), * }); * ``` */ var RedisMessageQueue = class { #redis; #subRedis; #workerId; #channelKey; #queueKey; #lockKey; #codec; #pollIntervalMs; #loopHandle; /** * Creates a new Redis message queue. * @param redis The Redis client factory. * @param options The options for the message queue. */ constructor(redis, options = {}) { this.#redis = redis(); this.#subRedis = redis(); this.#workerId = options.workerId ?? crypto.randomUUID(); this.#channelKey = options.channelKey ?? "fedify_channel"; this.#queueKey = options.queueKey ?? "fedify_queue"; this.#lockKey = options.lockKey ?? "fedify_lock"; this.#codec = options.codec ?? new JsonCodec(); this.#pollIntervalMs = Temporal.Duration.from(options.pollInterval ?? { seconds: 5 }).total("millisecond"); } async enqueue(message, options) { const ts = options?.delay == null ? 0 : Temporal.Now.instant().add(options.delay).epochMilliseconds; const encodedMessage = this.#codec.encode([crypto.randomUUID(), message]); await this.#redis.zadd(this.#queueKey, ts, encodedMessage); if (ts < 1) this.#redis.publish(this.#channelKey, ""); } async enqueueMany(messages, options) { if (messages.length === 0) return; const ts = options?.delay == null ? 0 : Temporal.Now.instant().add(options.delay).epochMilliseconds; const multi = this.#redis.multi(); for (const message of messages) { const encodedMessage = this.#codec.encode([crypto.randomUUID(), message]); multi.zadd(this.#queueKey, ts, encodedMessage); } await multi.exec(); if (ts < 1) this.#redis.publish(this.#channelKey, ""); } async #poll() { logger.debug("Polling for messages..."); const result = await this.#redis.set(this.#lockKey, this.#workerId, "EX", Math.floor(this.#pollIntervalMs / 1e3 * 2), "NX"); if (result == null) { logger.debug("Another worker is already processing messages; skipping..."); return; } logger.debug("Acquired lock; processing messages..."); const messages = await this.#redis.zrangebyscoreBuffer(this.#queueKey, 0, Temporal.Now.instant().epochMilliseconds); logger.debug("Found {messages} messages to process.", { messages: messages.length }); try { if (messages.length < 1) return; const encodedMessage = messages[0]; await this.#redis.zrem(this.#queueKey, encodedMessage); const [_, message] = this.#codec.decode(encodedMessage); return message; } finally { await this.#redis.del(this.#lockKey); } } async listen(handler, options = {}) { if (this.#loopHandle != null) throw new Error("Already listening"); const signal = options.signal; const poll = async () => { while (!signal?.aborted) { let message; try { message = await this.#poll(); } catch (error) { logger.error("Error polling for messages: {error}", { error }); return; } if (message === void 0) return; await handler(message); } }; const promise = this.#subRedis.subscribe(this.#channelKey, () => { this.#subRedis.on("message", poll); signal?.addEventListener("abort", () => { this.#subRedis.off("message", poll); }); }); signal?.addEventListener("abort", () => { for (const timeout of timeouts) clearTimeout(timeout); }); const timeouts = /* @__PURE__ */ new Set(); while (!signal?.aborted) { let timeout; await new Promise((resolve) => { signal?.addEventListener("abort", resolve); timeout = setTimeout(() => { signal?.removeEventListener("abort", resolve); resolve(0); }, this.#pollIntervalMs); timeouts.add(timeout); }); if (timeout != null) timeouts.delete(timeout); await poll(); } return await new Promise((resolve) => { signal?.addEventListener("abort", () => { promise.catch(() => resolve()).then(() => resolve()); }); promise.catch(() => resolve()).then(() => resolve()); }); } [Symbol.dispose]() { clearInterval(this.#loopHandle); this.#redis.disconnect(); this.#subRedis.disconnect(); } }; //#endregion export { RedisMessageQueue };