UNPKG

@message-queue-toolkit/core

Version:

Useful utilities, interfaces and base classes for message queue handling. Supports AMQP and SQS with a common abstraction on top currently

136 lines 5.64 kB
import { randomUUID } from 'node:crypto'; import { isObject } from '@lokalise/node-core'; import { Fifo } from 'toad-cache'; import { objectMatches } from "../utils/matchUtils.js"; /** * Symbol that can be used in place of a message type value in `waitForMessage` or `checkForMessage` * to match messages regardless of their type. Useful when using custom message type resolvers * where the type may not be available in the message payload itself. * * @example * // Match any message with a specific ID, regardless of type * await spy.waitForMessage({ id: '123', type: ANY_MESSAGE_TYPE }) */ export const ANY_MESSAGE_TYPE = Symbol('ANY_MESSAGE_TYPE'); /** * Symbol used to indicate that the message type could not be resolved. * Typically used when message parsing failed before type resolution could occur. * * @example * // For failed message parsing * spy.addProcessedMessage({ message: null, processingResult }, messageId, TYPE_NOT_RESOLVED) */ export const TYPE_NOT_RESOLVED = Symbol('TYPE_NOT_RESOLVED'); export function isHandlerSpy(value) { return (isObject(value) && (value instanceof HandlerSpy || value.name === 'HandlerSpy')); } export class HandlerSpy { name = 'HandlerSpy'; // biome-ignore lint/suspicious/noExplicitAny: This is expected messageBuffer; messageIdField; spyPromises; constructor(params = {}) { this.messageBuffer = new Fifo(params.bufferSize ?? 100); // @ts-expect-error this.messageIdField = params.messageIdField ?? 'id'; this.spyPromises = []; } messageMatchesFilter(spyResult, fields, status) { // Handle ANY_MESSAGE_TYPE symbol - if any field value is ANY_MESSAGE_TYPE, skip matching that field const fieldsToMatch = { ...fields }; for (const [key, value] of Object.entries(fieldsToMatch)) { if (value === ANY_MESSAGE_TYPE) { delete fieldsToMatch[key]; } } return (objectMatches(fieldsToMatch, spyResult.message) && (!status || spyResult.processingResult.status === status)); } waitForMessageWithId(id, status) { return this.waitForMessage( // @ts-expect-error { [this.messageIdField]: id }, status); } checkForMessage(expectedFields, status) { return Object.values(this.messageBuffer.items).find((spyResult) => { return this.messageMatchesFilter(spyResult.value, expectedFields, status); })?.value; } waitForMessage(expectedFields, status) { const processedMessageEntry = this.checkForMessage(expectedFields, status); if (processedMessageEntry) { return Promise.resolve(processedMessageEntry); } let resolve; const spyPromise = new Promise((_resolve) => { resolve = _resolve; }); this.spyPromises.push({ promise: spyPromise, status, fields: expectedFields, // @ts-expect-error resolve, }); return spyPromise; } clear() { this.messageBuffer.clear(); } getAllReceivedMessages() { return Object.values(this.messageBuffer.items).map((item) => item.value); } /** * Add a processed message to the spy buffer. * @param processingResult - The processing result containing the message and status * @param messageId - Optional message ID override (used if message parsing failed) * @param messageType - The resolved message type, or TYPE_NOT_RESOLVED symbol if type couldn't be determined */ addProcessedMessage(processingResult, messageId, messageType) { const resolvedMessageId = processingResult.message?.[this.messageIdField] ?? messageId ?? randomUUID(); // Use provided messageType, converting TYPE_NOT_RESOLVED symbol to string for storage const resolvedMessageType = messageType === TYPE_NOT_RESOLVED ? 'TYPE_NOT_RESOLVED' : messageType; // If we failed to parse message, let's store id and type at least for debugging const resolvedProcessingResult = processingResult.message ? processingResult : { ...processingResult, message: { [this.messageIdField]: messageId, type: resolvedMessageType === 'TYPE_NOT_RESOLVED' ? 'FAILED_TO_RESOLVE' : resolvedMessageType, }, }; const cacheId = `${resolvedMessageId}-${Date.now()}-${(Math.random() + 1) .toString(36) .substring(7)}`; this.messageBuffer.set(cacheId, resolvedProcessingResult); const foundPromise = this.spyPromises.find((spyPromise) => { return this.messageMatchesFilter(resolvedProcessingResult, spyPromise.fields, spyPromise.status); }); if (foundPromise) { foundPromise.resolve(processingResult); const index = this.spyPromises.indexOf(foundPromise); if (index > -1) { // only splice array when item is found this.spyPromises.splice(index, 1); // 2nd parameter means remove one item only } } } } export function resolveHandlerSpy(queueOptions) { if (isHandlerSpy(queueOptions.handlerSpy)) { return queueOptions.handlerSpy; } if (!queueOptions.handlerSpy) { return undefined; } if (queueOptions.handlerSpy === true) { return new HandlerSpy(); } return new HandlerSpy(queueOptions.handlerSpy); } //# sourceMappingURL=HandlerSpy.js.map