UNPKG

@fedify/postgres

Version:

PostgreSQL drivers for Fedify

186 lines (182 loc) 6.06 kB
const { Temporal } = require("@js-temporal/polyfill"); const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs'); const require_utils = require('./utils.cjs'); const __logtape_logtape = require_rolldown_runtime.__toESM(require("@logtape/logtape")); const postgres = require_rolldown_runtime.__toESM(require("postgres")); //#region src/mq.ts const logger = (0, __logtape_logtape.getLogger)([ "fedify", "postgres", "mq" ]); /** * A message queue that uses PostgreSQL as the underlying storage. * * @example * ```ts * import { createFederation } from "@fedify/fedify"; * import { PostgresKvStore, PostgresMessageQueue } from "@fedify/postgres"; * import postgres from "postgres"; * * const sql = postgres("postgres://user:pass@localhost/db"); * * const federation = createFederation({ * kv: new PostgresKvStore(sql), * queue: new PostgresMessageQueue(sql), * }); * ``` */ var PostgresMessageQueue = class { #sql; #tableName; #channelName; #pollIntervalMs; #initialized; #driverSerializesJson = false; constructor(sql, options = {}) { this.#sql = sql; this.#tableName = options?.tableName ?? "fedify_message_v2"; this.#channelName = options?.channelName ?? "fedify_channel"; this.#pollIntervalMs = Temporal.Duration.from(options?.pollInterval ?? { seconds: 5 }).total("millisecond"); this.#initialized = options?.initialized ?? false; } async enqueue(message, options) { await this.initialize(); const delay = options?.delay ?? Temporal.Duration.from({ seconds: 0 }); if (options?.delay) logger.debug("Enqueuing a message with a delay of {delay}...", { delay, message }); else logger.debug("Enqueuing a message...", { message }); await this.#sql` INSERT INTO ${this.#sql(this.#tableName)} (message, delay) VALUES ( ${this.#json(message)}, ${delay.toString()} ); `; logger.debug("Enqueued a message.", { message }); await this.#sql.notify(this.#channelName, delay.toString()); logger.debug("Notified the message queue channel {channelName}.", { channelName: this.#channelName, message }); } async enqueueMany(messages, options) { if (messages.length === 0) return; await this.initialize(); const delay = options?.delay ?? Temporal.Duration.from({ seconds: 0 }); if (options?.delay) logger.debug("Enqueuing messages with a delay of {delay}...", { delay, messages }); else logger.debug("Enqueuing messages...", { messages }); for (const message of messages) await this.#sql` INSERT INTO ${this.#sql(this.#tableName)} (message, delay) VALUES ( ${this.#json(message)}, ${delay.toString()} ); `; logger.debug("Enqueued messages.", { messages }); await this.#sql.notify(this.#channelName, delay.toString()); logger.debug("Notified the message queue channel {channelName}.", { channelName: this.#channelName, messages }); } async listen(handler, options = {}) { await this.initialize(); const { signal } = options; const poll = async () => { while (!signal?.aborted) { const query = this.#sql` DELETE FROM ${this.#sql(this.#tableName)} WHERE id = ( SELECT id FROM ${this.#sql(this.#tableName)} WHERE created + delay < CURRENT_TIMESTAMP ORDER BY created LIMIT 1 ) RETURNING message; `.execute(); const cancel = query.cancel.bind(query); signal?.addEventListener("abort", cancel); let i = 0; for (const message of await query) { if (signal?.aborted) return; await handler(message.message); i++; } signal?.removeEventListener("abort", cancel); if (i < 1) break; } }; const timeouts = /* @__PURE__ */ new Set(); const listen = await this.#sql.listen(this.#channelName, async (delay) => { const duration = Temporal.Duration.from(delay); const durationMs = duration.total("millisecond"); if (durationMs < 1) await poll(); else timeouts.add(setTimeout(poll, durationMs)); }, poll); signal?.addEventListener("abort", () => { listen.unlisten(); for (const timeout of timeouts) clearTimeout(timeout); }); 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(); } await new Promise((resolve) => { signal?.addEventListener("abort", () => resolve()); if (signal?.aborted) return resolve(); }); } /** * Initializes the message queue table if it does not already exist. */ async initialize() { if (this.#initialized) return; logger.debug("Initializing the message queue table {tableName}...", { tableName: this.#tableName }); try { await this.#sql` CREATE TABLE IF NOT EXISTS ${this.#sql(this.#tableName)} ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), message jsonb NOT NULL, delay interval DEFAULT '0 seconds', created timestamp with time zone DEFAULT CURRENT_TIMESTAMP ); `; } catch (error) { if (!(error instanceof postgres.default.PostgresError && error.constraint_name === "pg_type_typname_nsp_index")) { logger.error("Failed to initialize the message queue table: {error}", { error }); throw error; } } this.#driverSerializesJson = await require_utils.driverSerializesJson(this.#sql); this.#initialized = true; logger.debug("Initialized the message queue table {tableName}.", { tableName: this.#tableName }); } /** * Drops the message queue table if it exists. */ async drop() { await this.#sql`DROP TABLE IF EXISTS ${this.#sql(this.#tableName)};`; } #json(value) { if (this.#driverSerializesJson) return this.#sql.json(value); return this.#sql.json(JSON.stringify(value)); } }; //#endregion exports.PostgresMessageQueue = PostgresMessageQueue;