@fedify/fedify
Version:
An ActivityPub server framework
162 lines (161 loc) • 5.86 kB
JavaScript
// 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));
}
}