@message-queue-toolkit/sns
Version:
SNS adapter for message-queue-toolkit
167 lines • 7.31 kB
JavaScript
import { PublishCommand } from '@aws-sdk/client-sns';
import { InternalError } from '@lokalise/node-core';
import { DeduplicationRequesterEnum, } from '@message-queue-toolkit/core';
import { resolveOutgoingMessageAttributes } from '@message-queue-toolkit/sqs';
import { calculateOutgoingMessageSize, validateFifoTopicName } from "../utils/snsUtils.js";
import { AbstractSnsService } from "./AbstractSnsService.js";
export class AbstractSnsPublisher extends AbstractSnsService {
messageSchemaContainer;
isDeduplicationEnabled;
isFifoTopic;
messageGroupIdField;
defaultMessageGroupId;
initPromise;
constructor(dependencies, options) {
super(dependencies, options);
this.messageSchemaContainer = this.resolvePublisherMessageSchemaContainer(options);
this.isDeduplicationEnabled = !!options.enablePublisherDeduplication;
this.isFifoTopic = options.fifoTopic ?? false;
this.messageGroupIdField = options.messageGroupIdField;
this.defaultMessageGroupId = options.defaultMessageGroupId;
}
async init() {
await super.init();
// Validate FIFO topic naming conventions
if (this.isFifoTopic) {
const topicName = this.locatorConfig?.topicName ?? this.creationConfig?.topic?.Name;
if (topicName) {
validateFifoTopicName(topicName, this.isFifoTopic);
}
}
}
async publish(message, options = {}) {
const messageSchemaResult = this.resolveSchema(message);
if (messageSchemaResult.error) {
throw messageSchemaResult.error;
}
// If it's not initted yet, do the lazy init
if (!this.isInitted) {
// avoid multiple concurrent inits
if (!this.initPromise) {
this.initPromise = this.init();
}
await this.initPromise;
this.initPromise = undefined;
}
try {
const messageProcessingStartTimestamp = Date.now();
const parsedMessage = messageSchemaResult.result.parse(message);
const topicName = this.locatorConfig?.topicName ?? this.creationConfig?.topic?.Name ?? 'unknown';
if (this.logMessages) {
// @ts-expect-error
const resolvedLogMessage = this.resolveMessageLog(message, message[this.messageTypeField]);
this.logMessage(resolvedLogMessage);
}
const updatedMessage = this.updateInternalProperties(message);
// Resolve FIFO options from original message BEFORE offloading
// (offloaded payload won't have user fields needed for messageGroupIdField)
const resolvedOptions = this.resolveFifoOptions(updatedMessage, options);
const maybeOffloadedPayloadMessage = await this.offloadMessagePayloadIfNeeded(updatedMessage, () => calculateOutgoingMessageSize(updatedMessage));
if (this.isDeduplicationEnabledForMessage(parsedMessage) &&
(await this.deduplicateMessage(parsedMessage, DeduplicationRequesterEnum.Publisher))
.isDuplicated) {
this.handleMessageProcessed({
message: parsedMessage,
processingResult: { status: 'published', skippedAsDuplicate: true },
messageProcessingStartTimestamp,
queueName: topicName,
});
return;
}
await this.sendMessage(maybeOffloadedPayloadMessage, resolvedOptions);
this.handleMessageProcessed({
message: parsedMessage,
processingResult: { status: 'published' },
messageProcessingStartTimestamp,
queueName: topicName,
});
}
catch (error) {
const err = error;
this.handleError(err);
throw new InternalError({
message: `Error while publishing to SNS: ${err.message}`,
errorCode: 'SNS_PUBLISH_ERROR',
details: {
publisher: this.constructor.name,
topic: this.topicArn,
// @ts-expect-error
messageType: message[this.messageTypeField] ?? 'unknown',
},
cause: err,
});
}
}
resolveMessage() {
throw new Error('Not implemented for publisher');
}
resolveSchema(message) {
return this.messageSchemaContainer.resolveSchema(message);
}
/* c8 ignore start */
resolveNextFunction() {
throw new Error('Not implemented for publisher');
}
processPrehandlers() {
throw new Error('Not implemented for publisher');
}
preHandlerBarrier() {
throw new Error('Not implemented for publisher');
}
processMessage() {
throw new Error('Not implemented for publisher');
}
isDeduplicationEnabledForMessage(message) {
return this.isDeduplicationEnabled && super.isDeduplicationEnabledForMessage(message);
}
async sendMessage(payload, options) {
const attributes = resolveOutgoingMessageAttributes(payload);
const command = new PublishCommand({
Message: JSON.stringify(payload),
MessageAttributes: attributes,
TopicArn: this.topicArn,
...options,
});
await this.snsClient.send(command);
}
/**
* Resolves FIFO-specific options (MessageGroupId) from the message payload.
* This must be called BEFORE payload offloading, as the offloaded payload
* won't contain the user fields needed for messageGroupIdField resolution.
*
* @param payload - The original (non-offloaded) message payload
* @param options - The SNS message options to augment with FIFO settings
* @returns The options with resolved MessageGroupId for FIFO topics
*/
resolveFifoOptions(payload, options) {
if (!this.isFifoTopic) {
return options;
}
const resolvedOptions = { ...options };
// Resolve MessageGroupId if not provided
if (!resolvedOptions.MessageGroupId) {
if (this.messageGroupIdField) {
const messageGroupId = payload[this.messageGroupIdField];
if (typeof messageGroupId === 'string') {
resolvedOptions.MessageGroupId = messageGroupId;
}
}
// Fallback to default if still not set
if (!resolvedOptions.MessageGroupId && this.defaultMessageGroupId) {
resolvedOptions.MessageGroupId = this.defaultMessageGroupId;
}
}
// Validate MessageGroupId is present for FIFO topics
if (!resolvedOptions.MessageGroupId) {
throw new InternalError({
message: 'MessageGroupId is required for FIFO topics. Provide it in publish options, configure messageGroupIdField, or set defaultMessageGroupId.',
errorCode: 'FIFO_MESSAGE_GROUP_ID_REQUIRED',
details: {
topicArn: this.topicArn,
},
});
}
return resolvedOptions;
}
}
//# sourceMappingURL=AbstractSnsPublisher.js.map