UNPKG

@arturwojnar/hermes-postgresql

Version:

Production-Ready TypeScript Outbox Pattern for PostgreSQL

115 lines 3.67 kB
import { literalObject } from '@arturwojnar/hermes'; import assert from 'assert'; const OutboxConsumerStatuses = [ 'INITIAL', 'CREATED', 'SUCCESS_PUBLISH', 'FAILED_PUBLISH', 'DELETED', ]; class OutboxConsumerStore { _sql; _consumerName; _partitionKey; _consumer = null; constructor(_sql, _consumerName, _partitionKey = 'default') { this._sql = _sql; this._consumerName = _consumerName; this._partitionKey = _partitionKey; } async load() { const [consumer] = (await this._sql ` SELECT "id", "lastProcessedLsn", "status", "failedNextLsn", "nextLsnRedeliveryCount", "createdAt", "lastUpdatedAt" FROM "outboxConsumer" WHERE "consumerName"=${this._consumerName} AND "partitionKey"=${this._partitionKey} `); this._consumer = consumer; return consumer; } async createOrLoad(data) { await this._sql ` INSERT INTO "outboxConsumer" ( "consumerName", "partitionKey", "lastProcessedLsn", "createdAt" ) VALUES ( ${data.consumerName}, ${data.partitionKey}, '0/00000000', ${data.createdAt} ) ON CONFLICT ("consumerName") DO NOTHING; `; return await this.load(); } async update(consumerName, change) { assert(this._consumer); const skipKeys = ['id', 'createdAt', 'consumerName', 'partitionKey']; const updates = Object.entries(change) .filter(([key]) => !skipKeys.includes(key)) .map(([key, value]) => `"${key}" = ${value}`); if (updates.length === 0) { return this._consumer; } await this._sql ` UPDATE "outboxConsumer" SET ${this._sql.unsafe(updates.join(', '))} WHERE "consumerName" = ${consumerName}; `; Object.assign(this._consumer, updates); return this._consumer; } get consumer() { return this._consumer; } get consumerName() { return this._consumerName; } } class OutboxConsumerState { _store; constructor(_store) { this._store = _store; } async createOrLoad(partitionKey = 'default') { return await this._store.createOrLoad(literalObject({ status: 'CREATED', id: 0, consumerName: this._store.consumerName, partitionKey, createdAt: new Date(), })); } async moveFurther(lastProcessedLsn) { const { consumer } = this._store; assert(consumer); if (consumer.status === 'DELETED' || consumer.status === 'INITIAL') { return; } await this._store.update(this._store.consumerName, literalObject({ status: 'SUCCESS_PUBLISH', lastUpdatedAt: new Date(), lastProcessedLsn, })); } async reportFailedDelivery(failedNextLsn) { const { consumer } = this._store; assert(consumer); if (consumer.status === 'DELETED' || consumer.status === 'INITIAL') { return; } const nextLsnRedeliveryCount = consumer.status === 'CREATED' || consumer.status === 'SUCCESS_PUBLISH' ? 1 : consumer.nextLsnRedeliveryCount + 1; await this._store.update(this._store.consumerName, literalObject({ status: 'FAILED_PUBLISH', lastUpdatedAt: new Date(), failedNextLsn, nextLsnRedeliveryCount, })); } get data() { return this._store.consumer; } } export { OutboxConsumerState, OutboxConsumerStatuses, OutboxConsumerStore }; //# sourceMappingURL=OutboxConsumerModel.js.map