@russ-b/nestjs-common-tools
Version:
NestJS utility tools
235 lines • 10.6 kB
JavaScript
;
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.OutboxService = void 0;
const common_1 = require("@nestjs/common");
const typeorm_1 = require("typeorm");
const entities_1 = require("../entities");
const enums_1 = require("../enums");
const outbox_constants_1 = require("../outbox.constants");
let OutboxService = class OutboxService {
constructor(outboxRepository, options) {
this.outboxRepository = outboxRepository;
this.options = options;
}
getOperationalPolicy() {
return this.options.operationalPolicy;
}
async createEvent(eventType, payload, manager) {
const repo = manager
? manager.getRepository(entities_1.OutboxEvent)
: this.outboxRepository;
const event = repo.create({
eventType,
payload,
status: enums_1.OutboxEventStatus.PENDING,
});
return repo.save(event);
}
async claimById(eventId) {
return this.outboxRepository.manager.transaction(async (manager) => {
const processingStartedAt = new Date();
const result = await manager
.createQueryBuilder()
.update(entities_1.OutboxEvent)
.set({
status: enums_1.OutboxEventStatus.PROCESSING,
processingStartedAt,
})
.where('id = :id', { id: eventId })
.andWhere('status = :status', { status: enums_1.OutboxEventStatus.PENDING })
.execute();
if (!result.affected) {
return null;
}
return manager.findOne(entities_1.OutboxEvent, { where: { id: eventId } });
});
}
async claimPendingEventsByTypes(eventTypes, limit = this.options.operationalPolicy.claimBatchSize) {
if (eventTypes.length === 0) {
return [];
}
return this.outboxRepository.manager.transaction(async (manager) => {
const events = await manager
.createQueryBuilder(entities_1.OutboxEvent, 'outbox')
.where('outbox.status = :status', { status: enums_1.OutboxEventStatus.PENDING })
.andWhere('outbox.eventType IN (:...eventTypes)', { eventTypes })
.orderBy('outbox.createdAt', 'ASC')
.limit(limit)
.setLock('pessimistic_write')
.setOnLocked('skip_locked')
.getMany();
if (events.length > 0) {
const eventIds = events.map((e) => e.id);
const processingStartedAt = new Date();
await manager
.createQueryBuilder()
.update(entities_1.OutboxEvent)
.set({
status: enums_1.OutboxEventStatus.PROCESSING,
processingStartedAt,
})
.whereInIds(eventIds)
.execute();
events.forEach((e) => {
e.status = enums_1.OutboxEventStatus.PROCESSING;
e.processingStartedAt = processingStartedAt;
});
}
return events;
});
}
async claimPendingEvents(eventType, limit = this.options.operationalPolicy.claimBatchSize) {
return this.outboxRepository.manager.transaction(async (manager) => {
// Fetch pending events with pessimistic lock
const events = await manager
.createQueryBuilder(entities_1.OutboxEvent, 'outbox')
.where('outbox.status = :status', { status: enums_1.OutboxEventStatus.PENDING })
.andWhere('outbox.eventType = :eventType', { eventType })
.orderBy('outbox.createdAt', 'ASC')
.limit(limit)
.setLock('pessimistic_write')
.setOnLocked('skip_locked')
.getMany();
// Immediately mark as PROCESSING while we hold the lock
if (events.length > 0) {
const eventIds = events.map((e) => e.id);
const processingStartedAt = new Date();
await manager
.createQueryBuilder()
.update(entities_1.OutboxEvent)
.set({
status: enums_1.OutboxEventStatus.PROCESSING,
processingStartedAt,
})
.whereInIds(eventIds)
.execute();
// Update the in-memory objects too
events.forEach((e) => {
e.status = enums_1.OutboxEventStatus.PROCESSING;
e.processingStartedAt = processingStartedAt;
});
}
return events;
});
}
async markAsProcessing(eventId) {
await this.outboxRepository.update(eventId, {
status: enums_1.OutboxEventStatus.PROCESSING,
processingStartedAt: new Date(),
});
}
async markAsProcessed(eventId, expectedProcessingStartedAt) {
return this.updateEvent(eventId, {
status: enums_1.OutboxEventStatus.PROCESSED,
processingStartedAt: null,
processedAt: new Date(),
}, expectedProcessingStartedAt);
}
async incrementRetry(eventId, error, maxRetries = this.options.operationalPolicy.maxRetries, expectedProcessingStartedAt) {
const event = await this.findEventForProcessingUpdate(eventId, expectedProcessingStartedAt);
if (!event) {
return false;
}
const newRetryCount = event.retryCount + 1;
if (newRetryCount >= maxRetries) {
return this.updateEvent(eventId, {
status: enums_1.OutboxEventStatus.FAILED,
lastError: error,
retryCount: newRetryCount,
processingStartedAt: null,
}, expectedProcessingStartedAt);
}
return this.updateEvent(eventId, {
status: enums_1.OutboxEventStatus.PENDING,
lastError: error,
retryCount: newRetryCount,
processingStartedAt: null,
}, expectedProcessingStartedAt);
}
async markAsFailed(eventId, error, expectedProcessingStartedAt) {
const event = await this.findEventForProcessingUpdate(eventId, expectedProcessingStartedAt);
return this.updateEvent(eventId, {
status: enums_1.OutboxEventStatus.FAILED,
lastError: error,
retryCount: event?.retryCount || 0,
processingStartedAt: null,
}, expectedProcessingStartedAt);
}
async deleteProcessed(olderThanHours = this.options.operationalPolicy
.processedEventRetentionHours) {
const threshold = new Date(Date.now() - olderThanHours * 60 * 60 * 1000);
const result = await this.outboxRepository
.createQueryBuilder()
.delete()
.where('status = :status', { status: enums_1.OutboxEventStatus.PROCESSED })
.andWhere('processed_at < :threshold', { threshold })
.execute();
return result.affected || 0;
}
async resetStaleProcessingEvents(staleMinutes = this.options.operationalPolicy.staleProcessingMinutes) {
const staleThreshold = new Date(Date.now() - staleMinutes * 60 * 1000);
const result = await this.outboxRepository
.createQueryBuilder()
.update(entities_1.OutboxEvent)
.set({
status: enums_1.OutboxEventStatus.PENDING,
processingStartedAt: null,
})
.where('status = :status', { status: enums_1.OutboxEventStatus.PROCESSING })
.andWhere('processing_started_at < :staleThreshold', { staleThreshold })
.execute();
return result.affected || 0;
}
async findEventForProcessingUpdate(eventId, expectedProcessingStartedAt) {
const query = this.outboxRepository
.createQueryBuilder('outbox')
.where('outbox.id = :eventId', { eventId });
this.addProcessingOwnershipCondition(query, expectedProcessingStartedAt);
return query.getOne();
}
async updateEvent(eventId, values, expectedProcessingStartedAt) {
const query = this.outboxRepository
.createQueryBuilder()
.update(entities_1.OutboxEvent)
.set(values)
.where('id = :eventId', { eventId });
this.addProcessingOwnershipCondition(query, expectedProcessingStartedAt);
const result = await query.execute();
return Boolean(result.affected);
}
addProcessingOwnershipCondition(query, expectedProcessingStartedAt) {
if (expectedProcessingStartedAt === undefined) {
return;
}
query.andWhere('status = :processingStatus', {
processingStatus: enums_1.OutboxEventStatus.PROCESSING,
});
if (expectedProcessingStartedAt === null) {
query.andWhere('processing_started_at IS NULL');
return;
}
query.andWhere('processing_started_at = :expectedProcessingStartedAt', {
expectedProcessingStartedAt,
});
}
};
exports.OutboxService = OutboxService;
exports.OutboxService = OutboxService = __decorate([
(0, common_1.Injectable)(),
__param(0, (0, common_1.Inject)(outbox_constants_1.OUTBOX_EVENT_REPOSITORY)),
__param(1, (0, common_1.Inject)(outbox_constants_1.OUTBOX_MODULE_OPTIONS)),
__metadata("design:paramtypes", [typeorm_1.Repository, Object])
], OutboxService);
//# sourceMappingURL=outbox.service.js.map