UNPKG

pg-trx-outbox

Version:

Transactional outbox of Postgres for Node.js with little Event Sourcing

192 lines (191 loc) 8.96 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Transfer = void 0; const throw_1 = __importDefault(require("throw")); const ts_pattern_1 = require("ts-pattern"); const node_util_1 = require("node:util"); const debug_1 = __importDefault(require("debug")); const app_name_ts_1 = require("./app-name.js"); class Transfer { logger = (0, debug_1.default)(`pg-trx-outbox:${app_name_ts_1.appName}`); queue; options; pg; adapter; es; constructor(options, pg, adapter, es) { this.logger.log = console.log.bind(console); this.options = options; this.pg = pg; this.adapter = adapter; this.es = es; Promise.resolve().then(() => __importStar(require('p-queue'))).then(({ default: PQueue }) => (this.queue = new PQueue({ concurrency: options.outboxOptions?.concurrency ? Infinity : 1 }))); } async start() { } async stop() { this.queue.clear(); await this.queue.onIdle(); } async transferMessages() { await this.queue.add(async () => this.doTransferMessages('event'), { priority: 1 }); await this.queue.add(async () => this.doTransferMessages('command')); } async doTransferMessages(mode) { let messages = []; const client = await this.pg.getClient(); try { await client.query('begin'); messages = await this.fetchPgMessages(client, mode); if (messages.length) { const results = await this.adapter.send(messages); const ids = []; const responses = []; const errors = []; const metas = []; const processed = []; const attempts = []; const sinceAt = []; const errorApproved = []; for (const [i, resp] of results.entries()) { const message = messages[i] ?? (0, throw_1.default)(new Error('Message not exists for result')); ids.push(message.id); metas.push(resp.meta ?? null); responses.push(resp.status === 'fulfilled' ? typeof resp.value === 'string' || Array.isArray(resp.value) ? { r: resp.value } : resp.value : null); errors.push((resp.error ? this.normalizeError(resp.error) : null) ?? (resp.status === 'rejected' ? this.normalizeError(resp.reason) : null)); const needRetry = resp.status === 'rejected' && this.options.outboxOptions?.retryError?.(resp.reason) && message.attempts < (this.options.outboxOptions?.retryMaxAttempts ?? 5); processed.push(!needRetry); attempts.push(message.attempts + (needRetry ? 1 : 0)); sinceAt.push(needRetry ? new Date(Date.now() + (this.options.outboxOptions?.retryDelay ?? 5) * 1000) : message.since_at); errorApproved.push((resp.error?.isApproved ? true : false) || (resp.status === 'rejected' && resp.reason.isApproved) ? true : false); } await this.updateToProcessed(client, ids, responses, errors, metas, processed, attempts, sinceAt, errorApproved); if (mode === 'event') { this.es.setLastEventId(messages.at(-1)?.id ?? '0'); } } } catch (e) { if (e.code !== '55P03') { if (messages.length) { await this.updateToProcessed(client, messages.map(r => r.id), messages.map(() => null), messages.map(() => this.normalizeError(e)), messages.map(() => null), messages.map(() => true), messages.map(m => m.attempts), messages.map(m => m.since_at), messages.map(() => false)); } throw e; } } finally { await client.query('commit'); client.release(); } await this.adapter.onHandled(messages); } normalizeError(error) { return (0, ts_pattern_1.match)(error) .with(ts_pattern_1.P.instanceOf(Error), ({ cause, message, stack }) => (stack ?? message) + (cause instanceof Error ? `\nCause: ${cause.stack ?? cause.message}` : cause != null ? `\nCause: ${(0, node_util_1.inspect)(cause)}` : '')) .with(ts_pattern_1.P.string, err => err) .otherwise(err => (0, node_util_1.inspect)(err)); } async fetchPgMessages(client, mode) { const limit = this.options.outboxOptions?.limit ?? 50; const lastEventId = this.es.getLastEventId(); this.logger('fetching of messages, limit %d, from event id %d', limit, lastEventId); const resp = await client.query(` select id, topic, key, value, context_id, error, attempts, since_at, is_event from pg_trx_outbox${this.options.outboxOptions?.partition == null ? '' : `_${this.options.outboxOptions?.partition}`} where ${mode === 'command' ? `(is_event = false and processed = false and (since_at is null or now() > since_at)) ${this.options.outboxOptions?.topicFilter?.length ? 'and topic = any($2)' : ''}` : `(is_event = true and id > $2) ${this.options.outboxOptions?.topicFilter?.length ? 'and topic = any($3)' : ''}`} order by id limit $1 ${mode === 'command' ? `for update ${this.options.outboxOptions?.concurrency ? 'skip locked' : ''}` : ''} `, [ limit, ...(mode === 'event' ? [lastEventId] : []), ...(this.options.outboxOptions?.topicFilter?.length ? [this.options.outboxOptions?.topicFilter] : []), ]); this.logger('received messages with ids %o', resp.rows.map(r => r.id)); return resp.rows; } async updateToProcessed(client, ids, responses, errors, meta, done, attempts, sinceAt, errorApproved) { await client.query(` with info as ( select * from unnest($1::bigint[], $2::jsonb[], $3::text[], $4::jsonb[], $5::boolean[], $6::smallint[], $7::timestamptz[], $8::boolean[]) x(id, resp, err, meta, processed, attempts, since_at, error_approved)) update pg_trx_outbox${this.options.outboxOptions?.partition == null ? '' : `_${this.options.outboxOptions?.partition}`} p set processed = (select processed from info where info.id = p.id limit 1), updated_at = now(), response = (select resp from info where info.id = p.id limit 1), error = (select err from info where info.id = p.id limit 1), meta = (select meta from info where info.id = p.id limit 1), attempts = (select attempts from info where info.id = p.id limit 1), since_at = (select since_at from info where info.id = p.id limit 1), error_approved = (select error_approved from info where info.id = p.id limit 1) where p.id = any($1) `, [ids, responses, errors, meta, done, attempts, sinceAt, errorApproved]); } } exports.Transfer = Transfer;