@cap-js-community/event-queue
Version:
An event queue that enables secure transactional processing of asynchronous and periodic events, featuring instant event processing with Redis Pub/Sub and load distribution across all application instances.
112 lines (100 loc) • 4.42 kB
JavaScript
;
const { CronExpressionParser } = require("cron-parser");
const config = require("./config");
const common = require("./shared/common");
const EventQueueError = require("./EventQueueError");
const openTelemetry = require("./shared/openTelemetry");
/**
* Asynchronously publishes a series of events to the event queue.
*
* @param {Transaction} tx - The transaction object to be used for database operations.
* @param {Array|Object} events - An array of event objects or a single event object. Each event object should match the Event table structure:
* {
* type: String, // Event type. This is a required field.
* subType: String, // Event subtype. This is a required field.
* referenceEntity: String, // Reference entity associated with the event.
* referenceEntityKey: UUID, // UUID key of the reference entity.
* status: Status, // Status of the event, defaults to 0.
* payload: LargeString, // Payload of the event.
* attempts: Integer, // The number of attempts made, defaults to 0.
* lastAttemptTimestamp: Timestamp, // Timestamp of the last attempt.
* createdAt: Timestamp, // Timestamp of event creation. This field is automatically set on insert.
* startAfter: Timestamp, // Timestamp indicating when the event should start after.
* }
* @param {Object} [options] - Optional settings.
* @param {Boolean} [options.skipBroadcast=false] - If set to true, event broadcasting will be skipped. Defaults to false.
* @param {Boolean} [options.skipInsertEventsBeforeCommit=false] - If set to true, events will not be inserted before the transaction commit. Defaults to false.
* @throws {EventQueueError} Throws an error if the configuration is not initialized.
* @throws {EventQueueError} Throws an error if the event type is unknown.
* @throws {EventQueueError} Throws an error if the startAfter field is not a valid date.
* @returns {Promise<*>} Returns a promise which resolves to the result of the database insert operation.
*/
const publishEvent = async (
tx,
events,
{
skipBroadcast = false,
skipInsertEventsBeforeCommit = false,
addTraceContext = true,
allowNotExistingConfiguration = false,
} = {}
) => {
if (!config.initialized) {
throw EventQueueError.notInitialized();
}
const eventsForProcessing = Array.isArray(events) ? events : [events];
for (const event of eventsForProcessing) {
const { type, subType, startAfter, namespace } = event;
const eventConfig = config.getEventConfig(type, subType, namespace);
if (!eventConfig && !allowNotExistingConfiguration) {
throw EventQueueError.unknownEventType(type, subType);
}
if (startAfter && !common.isValidDate(startAfter)) {
throw EventQueueError.malformedDate(startAfter);
}
if (eventConfig?.isPeriodic) {
throw EventQueueError.manuelPeriodicEventInsert(type, subType);
}
if (typeof event.payload !== "string") {
event.payload = JSON.stringify(event.payload);
}
if (addTraceContext) {
event.context = JSON.stringify({ traceContext: openTelemetry.getCurrentTraceContext() });
}
if (eventConfig?.timeBucket && !(startAfter in event)) {
event.startAfter = CronExpressionParser.parse(eventConfig.timeBucket).next().toISOString();
}
if (event.namespace === undefined) {
event.namespace = config.namespace;
}
}
if (config.insertEventsBeforeCommit && !skipInsertEventsBeforeCommit) {
_registerHandlerAndAddEvents(tx, events, skipBroadcast);
} else {
let result;
tx._skipEventQueueBroadcast = skipBroadcast;
result = await tx.run(INSERT.into(config.tableNameEventQueue).entries(events));
tx._skipEventQueueBroadcast = false;
return result;
}
};
const _registerHandlerAndAddEvents = (tx, events, skipBroadcast) => {
tx._eventQueue ??= { events: [], handlerRegistered: false };
tx._eventQueue.events = tx._eventQueue.events.concat(events);
if (tx._eventQueue.handlerRegistered) {
return;
}
tx._eventQueue.handlerRegistered = true;
tx.context.before("commit", async () => {
if (!tx._eventQueue.events?.length) {
return;
}
tx._skipEventQueueBroadcast = skipBroadcast;
await tx.run(INSERT.into(config.tableNameEventQueue).entries(tx._eventQueue.events));
tx._skipEventQueueBroadcast = false;
tx._eventQueue = null;
});
};
module.exports = {
publishEvent,
};