@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
JavaScript
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