legend-transactional
Version:
A simple transactional, event-driven communication framework for microservices using RabbitMQ
1,205 lines (1,164 loc) • 40.8 kB
JavaScript
;
var uuid = require('uuid');
var mitt = require('mitt');
var amqplib = require('amqplib');
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
var mitt__default = /*#__PURE__*/_interopDefault(mitt);
var amqplib__default = /*#__PURE__*/_interopDefault(amqplib);
// src/@types/microservices.ts
var availableMicroservices = {
/**
* Test purpose
* Represents the "Image" test microservice.
*/
TestImage: "test-image",
/**
* Test purpose
* Represents the "Mint" test microservice.
*/
TestMint: "test-mint",
/**
* Represents the "audit-eda" microservice for event-driven architecture auditing.
* This microservice consumes audit events (audit.received, audit.processed, audit.dead_letter)
* to track event lifecycle and debugging purposes.
*/
AuditEda: "audit-eda",
/**
* Represents the "auth" microservice.
*/
Auth: "auth",
/**
* Represents the "legend-billing" microservice.
* Handles payment processing, subscriptions, and billing domain events.
*/
Billing: "billing",
/**
* Represents the "blockchain" microservice.
*/
Blockchain: "blockchain",
/**
* Represents the "legend-missions" microservice.
*/
Missions: "legend-missions",
/**
* Represents the "rankings" microservice.
*/
Rankings: "rankings",
/**
* Represents the "legend-events" microservice.
*/
Events: "legend-events",
/**
* Represents the "transactional" microservice.
*/
Transactional: "transactional",
/**
* Represents the "legend-send-email" microservice.
*/
SendEmail: "legend-send-email",
/**
* Represents the "legend-showcase" microservice.
*/
Showcase: "legend-showcase",
/**
* Represents the "social" microservice.
*/
Social: "social",
/**
* Represents the "legend-storage" microservice.
*/
Storage: "legend-storage",
/**
* Represents the "legend-game-analytics" microservice.
*/
LegendGameAnalytics: "legend-game-analytics"
};
// src/@types/event/events.ts
var gender = {
Male: "MALE",
Female: "FEMALE",
Undefined: "UNDEFINED"
};
var microserviceEvent = {
"TEST.IMAGE": "test.image",
"TEST.MINT": "test.mint",
///////////////////////////
// AUDIT EVENTS - For tracking event lifecycle
"AUDIT.PUBLISHED": "audit.published",
"AUDIT.RECEIVED": "audit.received",
"AUDIT.PROCESSED": "audit.processed",
"AUDIT.DEAD_LETTER": "audit.dead_letter",
///////////////////////////
"AUTH.DELETED_USER": "auth.deleted_user",
"AUTH.LOGOUT_USER": "auth.logout_user",
"AUTH.NEW_USER": "auth.new_user",
"AUTH.BLOCKED_USER": "auth.blocked_user",
"LEGEND_MISSIONS.NEW_MISSION_CREATED": "legend_missions.new_mission_created",
"LEGEND_MISSIONS.ONGOING_MISSION": "legend_missions.ongoing_mission",
"LEGEND_MISSIONS.MISSION_FINISHED": "legend_missions.mission_finished",
"LEGEND_MISSIONS.SEND_EMAIL_CRYPTO_MISSION_COMPLETED": "legend_missions.send_email_crypto_mission_completed",
"LEGEND_MISSIONS.SEND_EMAIL_CODE_EXCHANGE_MISSION_COMPLETED": "legend_missions.send_email_code_exchange_mission_completed",
"LEGEND_MISSIONS.SEND_EMAIL_NFT_MISSION_COMPLETED": "legend_missions.send_email_nft_mission_completed",
"LEGEND_RANKINGS.RANKINGS_FINISHED": "legend_rankings.rankings_finished",
"LEGEND_RANKINGS.NEW_RANKING_CREATED": "legend_rankings.new_ranking_created",
"LEGEND_RANKINGS.RANKING_SUBMITTED_FOR_REVIEW": "legend_rankings.ranking_submitted_for_review",
"LEGEND_RANKINGS.RANKING_APPROVED": "legend_rankings.ranking_approved",
"LEGEND_RANKINGS.RANKING_REJECTED": "legend_rankings.ranking_rejected",
"LEGEND_RANKINGS.RANKING_ACTIVATED": "legend_rankings.ranking_activated",
"LEGEND_RANKINGS.INTERMEDIATE_REWARD": "legend_rankings.intermediate_reward",
"LEGEND_RANKINGS.PARTICIPATION_REWARD": "legend_rankings.participation_reward",
"LEGEND_SHOWCASE.PRODUCT_VIRTUAL_DELETED": "legend_showcase.product_virtual_deleted",
"LEGEND_SHOWCASE.UPDATE_ALLOWED_MISSION_SUBSCRIPTION_IDS": "legend_showcase.update_allowed_mission_subscription_ids",
"LEGEND_SHOWCASE.UPDATE_ALLOWED_RANKING_SUBSCRIPTION_IDS": "legend_showcase.update_allowed_ranking_subscription_ids",
"SOCIAL.BLOCK_CHAT": "social.block_chat",
"SOCIAL.NEW_USER": "social.new_user",
"SOCIAL.UNBLOCK_CHAT": "social.unblock_chat",
"SOCIAL.UPDATED_USER": "social.updated_user",
///////////////////////////
// BILLING EVENTS
"BILLING.PAYMENT_CREATED": "billing.payment_created",
"BILLING.PAYMENT_SUCCEEDED": "billing.payment_succeeded",
"BILLING.PAYMENT_FAILED": "billing.payment_failed",
"BILLING.PAYMENT_REFUNDED": "billing.payment_refunded",
"BILLING.SUBSCRIPTION_CREATED": "billing.subscription_created",
"BILLING.SUBSCRIPTION_UPDATED": "billing.subscription_updated",
"BILLING.SUBSCRIPTION_RENEWED": "billing.subscription_renewed",
"BILLING.SUBSCRIPTION_CANCELED": "billing.subscription_canceled",
"BILLING.SUBSCRIPTION_EXPIRED": "billing.subscription_expired",
///////////////////////////
// LEGEND EVENTS
"LEGEND_EVENTS.NEW_EVENT_CREATED": "legend_events.new_event_created",
"LEGEND_EVENTS.EVENT_STARTED": "legend_events.event_started",
"LEGEND_EVENTS.EVENT_ENDED": "legend_events.event_ended",
"LEGEND_EVENTS.PLAYER_REGISTERED": "legend_events.player_registered",
"LEGEND_EVENTS.PLAYER_JOINED_WAITLIST": "legend_events.player_joined_waitlist",
"LEGEND_EVENTS.SCORE_SUBMITTED": "legend_events.score_submitted",
"LEGEND_EVENTS.EVENTS_FINISHED": "legend_events.events_finished",
"LEGEND_EVENTS.INTERMEDIATE_REWARD": "legend_events.intermediate_reward",
"LEGEND_EVENTS.PARTICIPATION_REWARD": "legend_events.participation_reward"
};
// src/@types/rabbit-mq.ts
var queue = {
/**
* Audit queue names for separate audit event types
* @constant
*/
AuditPublished: "audit_published_commands",
AuditReceived: "audit_received_commands",
AuditProcessed: "audit_processed_commands",
AuditDeadLetter: "audit_dead_letter_commands",
/**
* Queue used for sending replies in response to saga events.
*/
ReplyToSaga: "reply_to_saga",
/**
* Queue used for commencing a saga.
*/
CommenceSaga: "commence_saga"
};
var exchange = {
/**
* Audit exchange name for direct routing of audit events
*/
Audit: "audit_exchange",
/**
* Exchange dedicated to requeueing messages that require further processing in a saga process
*/
Requeue: "requeue_exchange",
/**
* Exchange for sending command messages to various consumers in a saga process
*/
Commands: "commands_exchange",
/**
* Exchange used for replying to saga events from consumers.
*/
ReplyToSaga: "reply_exchange",
/**
* Exchange used for starting a saga.
*/
CommenceSaga: "commence_saga_exchange",
/**
* Exchange used for starting a saga.
*/
Matching: "matching_exchange",
/**
* Exchange dedicated to requeueing messages that require further processing.
*/
MatchingRequeue: "matching_requeue_exchange"
};
// src/@types/saga/commands/auth.ts
var authCommands = {
/**
* Command to create a new user.
*/
CreateUser: "create_user"
};
// src/@types/saga/commands/audit-eda.ts
var auditEdaCommands = {};
// src/@types/saga/commands/blockchain.ts
var blockchainCommands = {
/**
* Saga step to transfer crypto reward to the winner of a mission.
*/
TransferMissionRewardToWinner: "crypto_reward:transfer_mission_reward_to_winner",
/**
* Saga step to make transfers to winners.
*/
TransferRewardToWinners: "crypto_reward:transfer_reward_to_winners"
};
// src/@types/saga/commands/image.ts
var testImageCommands = {
/**
* Command to create an image.
*/
CreateImage: "create_image",
/**
* Command to update a token for an image.
*/
UpdateToken: "update_token"
};
// src/@types/saga/commands/mint.ts
var testMintCommands = {
/**
* Command to mint an image.
*/
MintImage: "mint_image"
};
// src/@types/saga/commands/rankings.ts
var rankingsCommands = {};
// src/@types/saga/commands/send-email.ts
var sendEmailCommands = {};
// src/@types/saga/commands/showcase.ts
var showcaseCommands = {};
// src/@types/saga/commands/social.ts
var socialCommands = {
/**
* Command to create a new social user.
*/
CreateSocialUser: "create_social_user",
/**
* Command to update the social user's image.
*/
UpdateUserImage: "update_user:image"
};
// src/@types/saga/commands/storage.ts
var storageCommands = {
/**
* Command to store a file from base64.
*/
UploadFile: "upload_file"
};
// src/@types/saga/commands/transactional.ts
var transactionalCommands = {};
// src/@types/saga/commence.ts
var sagaTitle = {
/**
* Saga used to initiate a crypto transfer for a mission winner.
*/
TransferCryptoRewardToMissionWinner: "transfer_crypto_reward_to_mission_winner",
/**
* Saga used to initiate a crypto transfer for ranking winners.
*/
TransferCryptoRewardToRankingWinners: "transfer_crypto_reward_to_ranking_winners"
};
// src/@types/saga/sagaStep.ts
var status = {
/**
* The step is pending and hasn't been processed yet.
*/
Pending: "pending",
/**
* The step has been successfully executed.
*/
Success: "success",
/**
* The step execution has failed.
*/
Failure: "failure",
/**
* The step has been sent but not yet executed.
*/
Sent: "sent"
};
// src/constants.ts
var NACKING_DELAY_MS = 2e3;
var MAX_OCCURRENCE = 19;
var MAX_NACK_RETRIES = 20;
var nodeDataDefaults = {
payload: {},
previousPayload: {},
status: status.Pending,
isCurrentStep: false
};
// src/utils/fibonacci.ts
var fibonacci = (n) => {
if (n <= 0) return 0;
if (n === 1) return 1;
let fibPrev = 0;
let fibCurrent = 1;
for (let i = 2; i <= n; i++) {
const temp = fibCurrent;
fibCurrent += fibPrev;
fibPrev = temp;
}
return fibCurrent;
};
// src/utils/getters.ts
var getQueueName = (microservice) => {
return `${microservice}_saga_commands`;
};
var getQueueConsumer = (microservice) => {
return {
queueName: getQueueName(microservice),
exchange: exchange.Commands
};
};
var getEventKey = (event) => {
return event.toUpperCase();
};
var getEventObject = (event) => {
const key = getEventKey(event);
return {
[key]: event
};
};
// src/utils/extractMicroservice.ts
function extractMicroserviceFromQueue(queueName) {
return queueName.replace(/_match_commands$/, "").replace(/_saga_commands$/, "");
}
// src/Consumer/channels/Consume.ts
var ConsumeChannel = class {
/**
* The AMQP Channel object used for communication with RabbitMQ.
*/
channel;
/**
* The message received from RabbitMQ that this channel is currently processing.
*/
msg;
/**
* The name of the queue from which the message was consumed.
*/
queueName;
/**
* Creates a new `ConsumeChannel` instance.
*
* @param channel - The AMQP Channel for interacting with RabbitMQ.
* @param msg - The consumed message.
* @param queueName - The name of the source queue.
*/
constructor(channel, msg, queueName) {
this.channel = channel;
this.msg = msg;
this.queueName = queueName;
}
/**
* Negatively acknowledges (NACKs) the message with a specified delay and maximum retry count.
*
* This method is useful when you want to requeue the message for later processing, especially if the current attempt failed due to a temporary issue.
*
* @param delay - The delay (in milliseconds) before requeueing the message. Defaults to `NACKING_DELAY_MS`.
* @param maxRetries - The maximum number of times to requeue the message before giving up. Defaults to `undefined`, never giving up.
* @returns An object containing:
* - `count`: The current retry count.
* - `delay`: The actual delay applied to the nack.
*
* @see NACKING_DELAY_MS
*/
nackWithDelay(delay = NACKING_DELAY_MS, maxRetries) {
const { delay: delayNackRetry, count } = this.nack({ delay, maxRetries });
return { count, delay: delayNackRetry };
}
/**
* Negatively acknowledges (NACKs) the message using a Fibonacci backoff strategy.
*
* The delay before requeuing increases with each retry according to the Fibonacci sequence, helping to avoid overwhelming the system in case of repeated failures.
*
* @param maxOccurrence - The maximum number of times the Fibonacci delay is allowed to increase before being reset. Defaults to `MAX_OCCURRENCE`.
* @param maxRetries - The maximum number of times to requeue the message before giving up. Defaults to `undefined`, never giving up.
* @returns An object containing:
* - `count`: The current retry count.
* - `delay`: The calculated Fibonacci delay (in milliseconds) applied to the nack.
* - `occurrence`: The current occurrence count for the Fibonacci sequence.
* @see MAX_OCCURRENCE
*/
nackWithFibonacciStrategy(maxOccurrence = MAX_OCCURRENCE, maxRetries) {
return this.nack({ maxOccurrence, maxRetries });
}
/**
* Private helper function to handle the actual NACK logic.
*
* This method performs the NACK operation, manages retry counts and delays, and republishes the message for requeuing with appropriate headers and routing.
*
* @param nackOptions - An object specifying either:
* - `delay` and `maxRetries`: For linear backoff with a fixed delay and retry limit.
* - `maxOccurrence`: For Fibonacci backoff with a maximum occurrence count.
*/
nack({ maxRetries, maxOccurrence, delay }) {
const { msg, queueName, channel } = this;
channel.nack(msg, false, false);
let count = 0;
if (msg.properties.headers && msg.properties.headers["x-retry-count"]) {
count = Number(msg.properties.headers["x-retry-count"]);
}
count++;
let occurrence = 0;
if (msg.properties.headers && msg.properties.headers["x-occurrence"]) {
occurrence = Number(msg.properties.headers["x-occurrence"]);
if (occurrence >= (maxOccurrence ?? Infinity)) {
occurrence = 0;
}
}
occurrence++;
const nackDelay = delay ?? fibonacci(occurrence) * 1e3;
if (maxRetries && count > maxRetries) {
console.error(`MAX NACK RETRIES REACHED: ${maxRetries} - NACKING ${queueName} - ${msg.content.toString()}`);
return { count, delay: nackDelay, occurrence };
}
if (msg.properties?.headers?.["x-death"] && msg.properties.headers["x-death"].length > 1) {
const logData = {
"x-death": msg.properties.headers["x-death"],
queueName,
msg: msg.content.toString(),
headers: msg.properties.headers
};
console.warn("x-death length > 1 -> TIME TO REFACTOR", logData);
}
const xHeaders = {
"x-retry-count": count,
// 'count' and 'occurrence' are the same if the strategy is delay
"x-occurrence": occurrence
};
if (msg.fields.exchange === exchange.Matching) {
if (msg.properties?.headers?.["all-micro"]) {
delete msg.properties.headers["all-micro"];
}
channel.publish(exchange.MatchingRequeue, ``, msg.content, {
expiration: nackDelay,
headers: {
...msg.properties.headers,
// el nacking es dirigido a un microservicio en particular, el que nackeó.
micro: queueName,
...xHeaders
},
persistent: true,
// Solo se pasa lo que se necesita, se podría pasar "...msg.properties"
messageId: msg.properties.messageId,
appId: msg.properties.appId
});
} else {
channel.publish(exchange.Requeue, `${queueName}_routing_key`, msg.content, {
expiration: nackDelay,
headers: { ...msg.properties.headers, ...xHeaders },
persistent: true,
// Solo se pasa lo que se necesita, se podría pasar "...msg.properties"
messageId: msg.properties.messageId,
appId: msg.properties.appId
});
}
return { count, delay: nackDelay, occurrence };
}
};
var Consume_default = ConsumeChannel;
// src/Consumer/channels/CommenceSaga.ts
var SagaCommenceConsumeChannel = class extends Consume_default {
/**
* Method to acknowledge the message.
*/
ackMessage() {
this.channel.ack(this.msg, false);
}
};
// src/Consumer/callbacks/commenceSaga.ts
var commenceSagaConsumeCallback = (msg, channel, e, queueName) => {
if (!msg) {
console.error("NO MSG AVAILABLE");
return;
}
let saga;
try {
saga = JSON.parse(msg.content.toString());
} catch (error) {
console.error("ERROR PARSING MSG", error);
channel.nack(msg, false, false);
return;
}
const responseChannel = new SagaCommenceConsumeChannel(channel, msg, queueName);
e.emit(saga.title, { saga, channel: responseChannel });
};
// src/Broker/PublishAuditEvent.ts
async function publishAuditEvent(channel, eventType, payload) {
try {
const routingKey = eventType;
const messageBuffer = Buffer.from(JSON.stringify(payload));
channel.publish(exchange.Audit, routingKey, messageBuffer, {
contentType: "application/json",
deliveryMode: 2
// persistent
});
} catch (error) {
console.error(`Failed to publish audit event ${eventType}:`, error);
}
}
// src/Consumer/channels/Events.ts
var EventsConsumeChannel = class extends Consume_default {
/**
* The microservice name that is processing the event
*/
processorMicroservice;
/**
* The original event that was received
*/
processedEvent;
/**
* The unique event identifier (messageId) for tracking across audit events
*/
eventId;
/**
* The microservice that originally published the event
*/
publisherMicroservice;
/**
* Creates a new EventsConsumeChannel instance
*
* @param channel - The AMQP Channel
* @param msg - The consumed message
* @param queueName - The queue name
* @param processorMicroservice - The microservice name processing the event
* @param processedEvent - The event type being processed
* @param eventId - Unique event identifier (messageId) for audit tracking
* @param publisherMicroservice - The microservice that originally published the event
*/
constructor(channel, msg, queueName, processorMicroservice, processedEvent, eventId, publisherMicroservice) {
super(channel, msg, queueName);
this.processorMicroservice = processorMicroservice;
this.processedEvent = processedEvent;
this.eventId = eventId;
this.publisherMicroservice = publisherMicroservice;
}
/**
* Acknowledges the consumed saga event/command.
* Automatically emits audit.processed event after successful ACK.
*/
ackMessage() {
this.channel.ack(this.msg, false);
const timestamp = Date.now();
publishAuditEvent(this.channel, "audit.processed", {
publisher_microservice: this.publisherMicroservice,
processor_microservice: this.processorMicroservice,
processed_event: this.processedEvent,
processed_at: timestamp,
queue_name: this.queueName,
event_id: this.eventId
// UUID v7 from message properties for cross-event tracking
}).catch((error) => {
console.error("Failed to emit audit.processed event:", error);
});
}
/**
* Negatively acknowledges (NACKs) the message with a specified delay and maximum retry count.
*
* This method is useful when you want to requeue the message for later processing, especially if the current attempt failed due to a temporary issue.
*
* Additionally, this override automatically emits an `audit.dead_letter` event to track the rejection.
*
* @param delay - The delay (in milliseconds) before requeueing the message. Defaults to `NACKING_DELAY_MS`.
* @param maxRetries - The maximum number of times to requeue the message before giving up. Defaults to `undefined`, never giving up.
* @returns An object containing:
* - `count`: The current retry count.
* - `delay`: The actual delay applied to the nack.
*
* @see NACKING_DELAY_MS
*/
nackWithDelay(delay = NACKING_DELAY_MS, maxRetries) {
const parentNack = super.nackWithDelay(delay, maxRetries);
const timestamp = Date.now();
publishAuditEvent(this.channel, "audit.dead_letter", {
publisher_microservice: this.publisherMicroservice,
rejector_microservice: this.processorMicroservice,
rejected_event: this.processedEvent,
rejected_at: timestamp,
queue_name: this.queueName,
rejection_reason: "delay",
retry_count: parentNack.count,
event_id: this.eventId
// UUID v7 from message properties for cross-event tracking
}).catch((error) => {
console.error("Failed to emit audit.dead_letter event:", error);
});
return parentNack;
}
/**
* Negatively acknowledges (NACKs) the message using a Fibonacci backoff strategy.
*
* The delay before requeuing increases with each retry according to the Fibonacci sequence, helping to avoid overwhelming the system in case of repeated failures.
*
* Additionally, this override automatically emits an `audit.dead_letter` event to track the rejection.
*
* @param maxOccurrence - The maximum number of times the Fibonacci delay is allowed to increase before being reset. Defaults to `MAX_OCCURRENCE`.
* @param maxRetries - The maximum number of times to requeue the message before giving up. Defaults to `undefined`, never giving up.
* @returns An object containing:
* - `count`: The current retry count.
* - `delay`: The calculated Fibonacci delay (in milliseconds) applied to the nack.
* - `occurrence`: The current occurrence count for the Fibonacci sequence.
*
* @see MAX_OCCURRENCE
*/
nackWithFibonacciStrategy(maxOccurrence = MAX_OCCURRENCE, maxRetries) {
const parentNack = super.nackWithFibonacciStrategy(maxOccurrence, maxRetries);
const timestamp = Date.now();
publishAuditEvent(this.channel, "audit.dead_letter", {
publisher_microservice: this.publisherMicroservice,
rejector_microservice: this.processorMicroservice,
rejected_event: this.processedEvent,
rejected_at: timestamp,
queue_name: this.queueName,
rejection_reason: "fibonacci_strategy",
retry_count: parentNack.count,
event_id: this.eventId
// UUID v7 from message properties for cross-event tracking
}).catch((error) => {
console.error("Failed to emit audit.dead_letter event:", error);
});
return parentNack;
}
};
var eventCallback = (msg, channel, e, queueName) => {
if (!msg) {
console.error("mgs not AVAILABLE");
return;
}
const stringPayload = msg.content.toString();
let payload;
try {
payload = JSON.parse(stringPayload);
} catch (error) {
console.error("ERROR PARSING MSG", error);
channel.nack(msg, false, false);
return;
}
const headers = msg.properties.headers;
if (!headers || Object.values(headers).length === 0) {
console.error("headers not AVAILABLE, is a headers exchange");
channel.nack(msg, false, false);
return;
}
const allValues = Object.values(headers);
const event = [];
for (const value of allValues) {
if (typeof value === "string" && Object.values(microserviceEvent).includes(value)) {
event.push(value);
}
}
if (event.length === 0) {
console.error("Invalid header value", headers);
channel.nack(msg, false, false);
return;
}
if (event.length > 1) {
console.error(
"More then one valid header, using the first one detected, that is because the payload is typed with a particular event",
{ headersReceived: headers, eventsDetected: event }
);
}
const receiverMicroservice = extractMicroserviceFromQueue(queueName);
const receivedEvent = event[0];
const timestamp = Date.now();
let event_id = msg.properties.messageId;
if (!event_id) {
console.warn("Message is missing messageId, generating a new UUID v7 for event_id");
event_id = uuid.v7();
}
let publisherMicroservice = msg.properties.appId;
if (!publisherMicroservice) {
console.warn("Message is missing appId (publisher microservice), setting as unknown");
publisherMicroservice = "unknown";
}
publishAuditEvent(channel, "audit.received", {
publisher_microservice: publisherMicroservice,
receiver_microservice: receiverMicroservice,
received_event: receivedEvent,
received_at: timestamp,
queue_name: queueName,
event_id
}).catch((error) => {
console.error("Failed to emit audit.received event:", error);
});
const responseChannel = new EventsConsumeChannel(
channel,
msg,
queueName,
receiverMicroservice,
receivedEvent,
event_id,
publisherMicroservice
);
e.emit(event[0], { payload, channel: responseChannel });
};
// src/Consumer/channels/Step.ts
var MicroserviceConsumeChannel = class extends Consume_default {
/**
* The saga step associated with the consumed message.
*/
step;
/**
* Constructs a new instance of the ConsumeChannel class.
*
* @param {Channel} channel - The channel to interact with the message broker.
* @param {ConsumeMessage} msg - The consumed message to be processed.
* @param {string} queueName - The name of the queue from which the message was consumed.
* @param {SagaStep} step - The saga step associated with the consumed message.
*/
constructor(channel, msg, queueName, step) {
super(channel, msg, queueName);
this.step = step;
}
ackMessage(payloadForNextStep = {}) {
this.step.status = status.Success;
const previousPayload = this.step.previousPayload;
let metaData = {};
if (previousPayload) {
metaData = Object.keys(previousPayload).filter((key) => key.startsWith("__")).reduce((obj, key) => (obj[key] = previousPayload[key], obj), {});
}
this.step.payload = {
...payloadForNextStep,
...metaData
};
sendToQueue(queue.ReplyToSaga, this.step).then(() => {
this.channel.ack(this.msg, false);
}).catch((err) => {
console.error(err);
});
}
};
var SagaConsumeChannel = class extends MicroserviceConsumeChannel {
/**
* Acknowledges the consumed saga event/command.
*/
ackMessage() {
this.channel.ack(this.msg, false);
}
};
// src/Consumer/callbacks/saga.ts
var sagaConsumeCallback = (msg, channel, e, queueName) => {
if (!msg) {
console.error("NO MSG AVAILABLE");
return;
}
let currentStep;
try {
currentStep = JSON.parse(msg.content.toString());
} catch (error) {
console.error("ERROR PARSING MSG", error);
channel.nack(msg, false, false);
return;
}
const responseChannel = new SagaConsumeChannel(channel, msg, queueName, currentStep);
e.emit(currentStep.command, { step: currentStep, channel: responseChannel });
};
// src/Consumer/callbacks/sagaStep.ts
var sagaStepCallback = (msg, channel, e, queueName) => {
if (!msg) {
console.error("NO MSG AVAILABLE");
return;
}
let currentStep;
try {
currentStep = JSON.parse(msg.content.toString());
} catch (error) {
console.error("ERROR PARSING MSG", error);
channel.nack(msg, false, false);
return;
}
const { command, sagaId, previousPayload } = currentStep;
const responseChannel = new MicroserviceConsumeChannel(channel, msg, queueName, currentStep);
e.emit(command, { sagaId, payload: previousPayload, channel: responseChannel });
};
// src/Consumer/consume.ts
var consume = async (e, queueName, cb) => {
const channel = await getConsumeChannel();
await channel.prefetch(1);
await channel.consume(
queueName,
(msg) => {
cb(msg, channel, e, queueName);
},
{
exclusive: false,
// noAck means that the message will be acknowledged automatically by the broker once it is delivered.
noAck: false
}
);
};
// src/Consumer/create.ts
var createConsumers = async (consumers) => {
const channel = await getConsumeChannel();
for await (const consumer of consumers) {
const { exchange: consumerExchange, queueName } = consumer;
const requeueQueue = `${queueName}_requeue`;
const routingKey = `${queueName}_routing_key`;
await channel.assertExchange(consumerExchange, "direct", { durable: true });
await channel.assertQueue(queueName, { durable: true });
await channel.bindQueue(queueName, consumerExchange, routingKey);
saveQueueForHealthCheck(queueName);
await channel.assertExchange(exchange.Requeue, "direct", { durable: true });
await channel.assertQueue(requeueQueue, {
durable: true,
arguments: { "x-dead-letter-exchange": consumerExchange }
});
await channel.bindQueue(requeueQueue, exchange.Requeue, routingKey);
}
};
// src/Consumer/header.ts
var createHeaderConsumers = async (queueName, events) => {
const channel = await getConsumeChannel();
const requeueQueue = `${queueName}_matching_requeue`;
await channel.assertExchange(exchange.Matching, "headers", { durable: true });
await channel.assertExchange(exchange.MatchingRequeue, "headers", { durable: true });
await channel.assertQueue(queueName, { durable: true });
saveQueueForHealthCheck(queueName);
await channel.assertQueue(requeueQueue, {
durable: true,
arguments: { "x-dead-letter-exchange": exchange.Matching }
});
for (const ev of Object.values(microserviceEvent)) {
const headerEvent = getEventObject(ev);
await channel.assertExchange(ev, "headers", { durable: true });
await channel.bindExchange(ev, exchange.Matching, "", {
...headerEvent,
// key para emitir eventos a todos lo micros, todos los micros tienen el bind a este exchange "ev"
"all-micro": "yes",
"x-match": "all"
// se tienen que cumplir todos los argumentos
});
await channel.assertExchange(`${ev}_requeue`, "headers", { durable: true });
await channel.bindExchange(`${ev}_requeue`, exchange.MatchingRequeue, "", headerEvent);
const headersArgs = {
...headerEvent,
micro: queueName,
"x-match": "all"
};
if (events.includes(ev)) {
await channel.bindQueue(queueName, ev, "", headerEvent);
await channel.bindQueue(requeueQueue, `${ev}_requeue`, "", headersArgs);
await channel.assertExchange(`${ev}_${queueName}`, "headers", {
durable: true
});
await channel.bindExchange(`${ev}_${queueName}`, exchange.Matching, "", headersArgs);
await channel.bindQueue(queueName, `${ev}_${queueName}`, "", headersArgs);
} else {
await channel.unbindQueue(queueName, ev, "", headerEvent);
await channel.unbindQueue(requeueQueue, `${ev}_requeue`, "", headersArgs);
await channel.deleteExchange(`${ev}_${queueName}`, { ifUnused: false });
}
}
};
// src/Consumer/auditInfrastructure.ts
var createAuditLoggingResources = async () => {
const channel = await getConsumeChannel();
await Promise.all([
// Create direct exchange for audit events
channel.assertExchange(exchange.Audit, "direct", { durable: true }),
// Create queues for audit events
channel.assertQueue(queue.AuditPublished, { durable: true }),
channel.assertQueue(queue.AuditReceived, { durable: true }),
channel.assertQueue(queue.AuditProcessed, { durable: true }),
channel.assertQueue(queue.AuditDeadLetter, { durable: true })
]);
await Promise.all([
// Bind each queue to its specific routing key
channel.bindQueue(queue.AuditPublished, exchange.Audit, "audit.published"),
channel.bindQueue(queue.AuditReceived, exchange.Audit, "audit.received"),
channel.bindQueue(queue.AuditProcessed, exchange.Audit, "audit.processed"),
channel.bindQueue(queue.AuditDeadLetter, exchange.Audit, "audit.dead_letter")
]);
};
var conn = null;
var isTheConnectionClosed = true;
var startListeners = (c) => {
c.addListener("close", (e) => {
isTheConnectionClosed = true;
console.error("[legend_transac:__Connection closed__]", e.message);
});
c.addListener("error", (e) => {
isTheConnectionClosed = true;
console.error("[legend_transac:__Connection error__]", e.message);
});
};
var storedConfig;
var getStoredConfig = () => {
if (!storedConfig) {
throw new Error("RabbitMQ Config not initialized.");
}
return storedConfig;
};
var getRabbitMQConn = async () => {
if (conn === null) {
conn = await amqplib__default.default.connect(getStoredConfig().url);
isTheConnectionClosed = false;
startListeners(conn);
}
return conn;
};
var closeRabbitMQConn = async () => {
if (conn !== null) {
await conn.close();
conn = null;
storedConfig = void 0;
}
};
var healthCheckQueue = null;
var saveQueueForHealthCheck = (queue2) => {
healthCheckQueue = queue2;
};
var isConnectionHealthy = async () => {
let isHealthy = false;
if (isTheConnectionClosed) return isHealthy;
if (conn === null) return isHealthy;
if (healthCheckQueue === null) return isHealthy;
const queue2 = healthCheckQueue;
const closeListener = (e) => {
isTheConnectionClosed = true;
console.error("[legend_transac:health_check_listener:__Connection closed__]", e.message);
};
const errorListener = (e) => {
isTheConnectionClosed = true;
console.error("[legend_transac:health_check_listener:__Connection error__]", e.message);
};
conn.addListener("close", closeListener);
conn.addListener("error", errorListener);
const testChannel = await conn.createConfirmChannel();
try {
const testChannelPromise = new Promise((resolve, reject) => {
testChannel.checkQueue(queue2).then(() => {
isHealthy = true;
resolve();
}).catch((e) => {
console.error("[legend_transac:health_check_listener:Check failed]", e.message);
reject(e);
});
});
await testChannelPromise;
} catch (e) {
return isHealthy;
}
await testChannel.close();
conn.removeListener("close", closeListener);
conn.removeListener("error", errorListener);
return isHealthy;
};
var prepare = async (config) => {
if (storedConfig) return storedConfig;
storedConfig = config;
await getRabbitMQConn();
await getConsumeChannel();
return storedConfig;
};
var startGlobalSagaStepListener = async (url) => {
await prepare({
url,
microservice: "transactional",
events: []
});
const queueO = {
queueName: queue.ReplyToSaga,
exchange: exchange.ReplyToSaga
};
const e = mitt__default.default();
await createConsumers([queueO]);
void consume(e, queueO.queueName, sagaConsumeCallback);
return e;
};
var commenceSagaListener = async (url) => {
await prepare({
url,
microservice: "transactional",
events: []
});
const q = {
queueName: queue.CommenceSaga,
exchange: exchange.CommenceSaga
};
const e = mitt__default.default();
await createConsumers([q]);
void consume(e, q.queueName, commenceSagaConsumeCallback);
return e;
};
var transactionalInitialized = false;
var Transactional = class {
constructor(url) {
this.url = url;
if (transactionalInitialized) {
throw new Error("Transactional already initialized");
}
transactionalInitialized = true;
}
startGlobalSagaStepListener = () => {
return startGlobalSagaStepListener(this.url);
};
commenceSagaListener = () => {
return commenceSagaListener(this.url);
};
};
var connectToSagaCommandEmitter = async (config) => {
const storedConfig2 = await prepare(config);
const q = getQueueConsumer(storedConfig2.microservice);
const e = mitt__default.default();
await createConsumers([q]);
void consume(e, q.queueName, sagaStepCallback);
return e;
};
var connectToEvents = async (config) => {
const storedConfig2 = await prepare(config);
const queueName = `${storedConfig2.microservice}_match_commands`;
const e = mitt__default.default();
await createHeaderConsumers(queueName, storedConfig2.events);
await createAuditLoggingResources();
void consume(e, queueName, eventCallback);
return e;
};
var sagaInitialized = false;
var Saga = class {
constructor(conf) {
this.conf = conf;
if (sagaInitialized) {
throw new Error("Saga already initialized");
}
sagaInitialized = true;
}
connectToEvents = () => {
return connectToEvents(this.conf);
};
connectToSagaCommandEmitter = () => {
return connectToSagaCommandEmitter(this.conf);
};
};
// src/Connections/consumeChannel.ts
var consumeChannel = null;
var getConsumeChannel = async () => {
if (consumeChannel === null) {
consumeChannel = await (await getRabbitMQConn()).createChannel();
}
return consumeChannel;
};
var closeConsumeChannel = async () => {
if (consumeChannel !== null) {
await consumeChannel.close();
consumeChannel = null;
}
};
// src/Connections/stop.ts
var stopRabbitMQ = async () => {
await closeConsumeChannel();
await closeSendChannel();
await closeRabbitMQConn();
};
// src/Broker/sendChannel.ts
var sendChannel = null;
var getSendChannel = async () => {
if (sendChannel === null) {
sendChannel = await (await getRabbitMQConn()).createChannel();
}
return sendChannel;
};
var closeSendChannel = async () => {
if (sendChannel !== null) {
await sendChannel.close();
sendChannel = null;
}
};
// src/Broker/SendToQueue.ts
var sendToQueue = (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async (queueName, payload) => {
const channel = await getSendChannel();
channel.sendToQueue(queueName, Buffer.from(JSON.stringify(payload)), {
persistent: true
});
}
);
var commenceSaga = async (sagaTitle2, payload) => {
const saga = {
title: sagaTitle2,
payload
};
await sendToQueue(queue.CommenceSaga, saga);
};
var publishEvent = async (msg, event) => {
const channel = await getSendChannel();
const publisherMicroservice = getStoredConfig().microservice;
const messageId = uuid.v7();
channel.publish(exchange.Matching, ``, Buffer.from(JSON.stringify(msg)), {
headers: {
...getEventObject(event),
// key para emitir eventos a todos los micros, todos los micros tienen el bind al exchange Matching
"all-micro": "yes"
},
messageId,
appId: publisherMicroservice
});
const timestamp = Date.now();
publishAuditEvent(channel, "audit.published", {
publisher_microservice: publisherMicroservice,
published_event: event,
published_at: timestamp,
event_id: messageId
}).catch((error) => {
console.error("Failed to emit audit.published event:", error);
});
};
exports.EventsConsumeChannel = EventsConsumeChannel;
exports.MAX_NACK_RETRIES = MAX_NACK_RETRIES;
exports.MAX_OCCURRENCE = MAX_OCCURRENCE;
exports.MicroserviceConsumeChannel = MicroserviceConsumeChannel;
exports.NACKING_DELAY_MS = NACKING_DELAY_MS;
exports.Saga = Saga;
exports.SagaCommenceConsumeChannel = SagaCommenceConsumeChannel;
exports.SagaConsumeChannel = SagaConsumeChannel;
exports.Transactional = Transactional;
exports.auditEdaCommands = auditEdaCommands;
exports.authCommands = authCommands;
exports.availableMicroservices = availableMicroservices;
exports.blockchainCommands = blockchainCommands;
exports.closeConsumeChannel = closeConsumeChannel;
exports.closeRabbitMQConn = closeRabbitMQConn;
exports.closeSendChannel = closeSendChannel;
exports.commenceSaga = commenceSaga;
exports.commenceSagaConsumeCallback = commenceSagaConsumeCallback;
exports.consume = consume;
exports.createAuditLoggingResources = createAuditLoggingResources;
exports.createConsumers = createConsumers;
exports.createHeaderConsumers = createHeaderConsumers;
exports.eventCallback = eventCallback;
exports.exchange = exchange;
exports.extractMicroserviceFromQueue = extractMicroserviceFromQueue;
exports.fibonacci = fibonacci;
exports.gender = gender;
exports.getConsumeChannel = getConsumeChannel;
exports.getEventKey = getEventKey;
exports.getEventObject = getEventObject;
exports.getQueueConsumer = getQueueConsumer;
exports.getQueueName = getQueueName;
exports.getRabbitMQConn = getRabbitMQConn;
exports.getSendChannel = getSendChannel;
exports.getStoredConfig = getStoredConfig;
exports.isConnectionHealthy = isConnectionHealthy;
exports.microserviceEvent = microserviceEvent;
exports.nodeDataDefaults = nodeDataDefaults;
exports.publishEvent = publishEvent;
exports.queue = queue;
exports.rankingsCommands = rankingsCommands;
exports.sagaConsumeCallback = sagaConsumeCallback;
exports.sagaStepCallback = sagaStepCallback;
exports.sagaTitle = sagaTitle;
exports.saveQueueForHealthCheck = saveQueueForHealthCheck;
exports.sendEmailCommands = sendEmailCommands;
exports.sendToQueue = sendToQueue;
exports.showcaseCommands = showcaseCommands;
exports.socialCommands = socialCommands;
exports.status = status;
exports.stopRabbitMQ = stopRabbitMQ;
exports.storageCommands = storageCommands;
exports.testImageCommands = testImageCommands;
exports.testMintCommands = testMintCommands;
exports.transactionalCommands = transactionalCommands;