UNPKG

@fedify/fedify

Version:

An ActivityPub server framework

162 lines (161 loc) • 5.86 kB
// deno-lint-ignore-file no-explicit-any /** * Additional options for enqueuing a message in a queue. * * @since 0.5.0 */ import * as dntShim from "../_dnt.shims.js"; /** * A message queue that processes messages in the same process. * Do not use this in production as it does neither persist messages nor * distribute them across multiple processes. * * @since 0.5.0 */ export class InProcessMessageQueue { #messages; #monitors; #pollIntervalMs; /** * Constructs a new {@link InProcessMessageQueue} with the given options. * @param options Additional options for the in-process message queue. */ constructor(options = {}) { this.#messages = []; this.#monitors = {}; this.#pollIntervalMs = dntShim.Temporal.Duration.from(options.pollInterval ?? { seconds: 5 }).total("millisecond"); } enqueue(message, options) { const delay = options?.delay == null ? 0 : Math.max(options.delay.total("millisecond"), 0); if (delay > 0) { setTimeout(() => this.enqueue(message, { ...options, delay: undefined }), delay); return Promise.resolve(); } this.#messages.push(message); for (const monitorId in this.#monitors) { this.#monitors[monitorId](); } return Promise.resolve(); } enqueueMany(messages, options) { if (messages.length === 0) return Promise.resolve(); const delay = options?.delay == null ? 0 : Math.max(options.delay.total("millisecond"), 0); if (delay > 0) { setTimeout(() => this.enqueueMany(messages, { ...options, delay: undefined }), delay); return Promise.resolve(); } this.#messages.push(...messages); for (const monitorId in this.#monitors) { this.#monitors[monitorId](); } return Promise.resolve(); } async listen(handler, options = {}) { const signal = options.signal; while (signal == null || !signal.aborted) { while (this.#messages.length > 0) { const message = this.#messages.shift(); await handler(message); } await this.#wait(this.#pollIntervalMs, signal); } } #wait(ms, signal) { let timer = null; return Promise.any([ new Promise((resolve) => { signal?.addEventListener("abort", () => { if (timer != null) clearTimeout(timer); resolve(); }, { once: true }); const monitorId = dntShim.crypto.randomUUID(); this.#monitors[monitorId] = () => { delete this.#monitors[monitorId]; if (timer != null) clearTimeout(timer); resolve(); }; }), new Promise((resolve) => timer = setTimeout(resolve, ms)), ]); } } /** * A message queue that processes messages in parallel. It takes another * {@link MessageQueue}, and processes messages in parallel up to a certain * number of workers. * * Actually, it's rather a decorator than a queue itself. * * Note that the workers do not run in truly parallel, in the sense that they * are not running in separate threads or processes. They are running in the * same process, but are scheduled to run in parallel. Hence, this is useful * for I/O-bound tasks, but not for CPU-bound tasks, which is okay for Fedify's * workloads. * * @since 1.0.0 */ export class ParallelMessageQueue { queue; workers; /** * Constructs a new {@link ParallelMessageQueue} with the given queue and * number of workers. * @param queue The message queue to use under the hood. Note that * {@link ParallelMessageQueue} cannot be nested. * @param workers The number of workers to process messages in parallel. * @throws {TypeError} If the given queue is an instance of * {@link ParallelMessageQueue}. */ constructor(queue, workers) { if (queue instanceof ParallelMessageQueue) { throw new TypeError("Cannot nest ParallelMessageQueue."); } this.queue = queue; this.workers = workers; } enqueue(message, options) { return this.queue.enqueue(message, options); } async enqueueMany(messages, options) { if (this.queue.enqueueMany == null) { const results = await Promise.allSettled(messages.map((message) => this.queue.enqueue(message, options))); const errors = results .filter((r) => r.status === "rejected") .map((r) => r.reason); if (errors.length > 1) { throw new AggregateError(errors, "Failed to enqueue messages."); } else if (errors.length === 1) throw errors[0]; return; } await this.queue.enqueueMany(messages, options); } listen(handler, options = {}) { const workers = new Map(); return this.queue.listen(async (message) => { while (workers.size >= this.workers) { const consumedId = await Promise.any(workers.values()); workers.delete(consumedId); } const workerId = dntShim.crypto.randomUUID(); const promise = this.#work(workerId, handler, message); workers.set(workerId, promise); }, options); } async #work(workerId, handler, message) { await this.#sleep(0); await handler(message); return workerId; } #sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } }