@arturwojnar/hermes-postgresql
Version:
Production-Ready TypeScript Outbox Pattern for PostgreSQL
115 lines • 3.67 kB
JavaScript
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