UNPKG

@azure/service-bus

Version:
410 lines • 17.2 kB
// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. import { AmqpAnnotatedMessage, Constants } from "@azure/core-amqp"; import { Buffer } from "buffer"; import Long from "long"; import { uuid_to_string, } from "rhea-promise"; import { defaultDataTransformer } from "./dataTransformer"; import { messageLogger as logger } from "./log"; import { isDefined, isObjectWithProperties } from "@azure/core-util"; import { reorderLockToken } from "./util/utils"; /** * @internal */ export var DispositionType; (function (DispositionType) { DispositionType["complete"] = "complete"; DispositionType["deadletter"] = "deadletter"; DispositionType["abandon"] = "abandon"; DispositionType["defer"] = "defer"; })(DispositionType || (DispositionType = {})); /** * @internal * Gets the error message for when a property on given message is not of expected type */ export function getMessagePropertyTypeMismatchError(msg) { if (msg.contentType != null && typeof msg.contentType !== "string") { return new TypeError("The property 'contentType' on the message must be of type 'string'"); } if (msg.subject != null && typeof msg.subject !== "string") { return new TypeError("The property 'label' on the message must be of type 'string'"); } if (msg.to != null && typeof msg.to !== "string") { return new TypeError("The property 'to' on the message must be of type 'string'"); } if (msg.replyTo != null && typeof msg.replyTo !== "string") { return new TypeError("The property 'replyTo' on the message must be of type 'string'"); } if (msg.replyToSessionId != null && typeof msg.replyToSessionId !== "string") { return new TypeError("The property 'replyToSessionId' on the message must be of type 'string'"); } if (msg.timeToLive != null && typeof msg.timeToLive !== "number") { return new TypeError("The property 'timeToLive' on the message must be of type 'number'"); } if (msg.sessionId != null && typeof msg.sessionId !== "string") { return new TypeError("The property 'sessionId' on the message must be of type 'string'"); } if (msg.messageId != null && typeof msg.messageId !== "string" && typeof msg.messageId !== "number" && !Buffer.isBuffer(msg.messageId)) { return new TypeError("The property 'messageId' on the message must be of type string, number or Buffer"); } if (msg.correlationId != null && typeof msg.correlationId !== "string" && typeof msg.correlationId !== "number" && !Buffer.isBuffer(msg.correlationId)) { return new TypeError("The property 'correlationId' on the message must be of type string, number or Buffer"); } return; } /** * @internal * Converts given ServiceBusMessage to RheaMessage */ export function toRheaMessage(msg, encoder) { let amqpMsg; if (isAmqpAnnotatedMessage(msg)) { amqpMsg = { ...AmqpAnnotatedMessage.toRheaMessage(msg), body: encoder.encode(msg.body, msg.bodyType ?? "data"), }; } else { let bodyType = "data"; if (isServiceBusReceivedMessage(msg)) { /* * TODO: this is a bit complicated. * * It seems reasonable to expect to be able to round-trip a message (ie, * receive a message, and then send it again, possibly to another queue / topic). * If the user does that we need to make sure to respect their original AMQP * type so when the message is re - encoded we don't put 'body' into the wrong spot. * * The complication is that we need to decide if we're okay with respecting a field * from the rawAmqpMessage, which up until now we've treated as just vestigial * information on send. My hope is that the use case of "alter the sb message in some * incompatible way with the underying _rawAmqpMessage.bodyType" is not common * enough for us to try to do anything more than what I'm doing here. */ bodyType = msg._rawAmqpMessage.bodyType ?? "data"; } // TODO: it seems sensible that we'd also do this for AMQPAnnotated message. const validationError = getMessagePropertyTypeMismatchError(msg); if (validationError) { throw validationError; } amqpMsg = { body: encoder.encode(msg.body, bodyType), message_annotations: {}, }; if (msg.timeToLive) { amqpMsg.ttl = Math.min(msg.timeToLive, Constants.maxUint32Value); amqpMsg.creation_time = new Date(); amqpMsg.absolute_expiry_time = new Date(Math.min(amqpMsg.creation_time.getTime() + amqpMsg.ttl, Constants.maxAbsoluteExpiryTime)); } } if (isAmqpAnnotatedMessage(msg)) { return amqpMsg; } if (msg.applicationProperties != null) { amqpMsg.application_properties = msg.applicationProperties; } if (msg.contentType != null) { amqpMsg.content_type = msg.contentType; } if (msg.sessionId != null) { if (msg.sessionId.length > Constants.maxSessionIdLength) { throw new Error("Length of 'sessionId' property on the message cannot be greater than 128 characters."); } amqpMsg.group_id = msg.sessionId; } if (msg.replyTo != null) { amqpMsg.reply_to = msg.replyTo; } if (msg.to != null) { amqpMsg.to = msg.to; } if (msg.subject != null) { amqpMsg.subject = msg.subject; } updateMessageId(amqpMsg, msg.messageId); if (msg.correlationId != null) { amqpMsg.correlation_id = msg.correlationId; } if (msg.replyToSessionId != null) { amqpMsg.reply_to_group_id = msg.replyToSessionId; } if (msg.partitionKey != null) { if (msg.partitionKey.length > Constants.maxPartitionKeyLength) { throw new Error("Length of 'partitionKey' property on the message cannot be greater than 128 characters."); } amqpMsg.message_annotations[Constants.partitionKey] = msg.partitionKey; } // Will be required later for implementing Transactions // if (msg.viaPartitionKey != null) { // if (msg.viaPartitionKey.length > Constants.maxPartitionKeyLength) { // throw new Error( // "Length of 'viaPartitionKey' property on the message cannot be greater than 128 characters." // ); // } // amqpMsg.message_annotations![Constants.viaPartitionKey] = msg.viaPartitionKey; // } updateScheduledTime(amqpMsg, msg.scheduledEnqueueTimeUtc); logger.verbose("SBMessage to RheaMessage: %O", amqpMsg); return amqpMsg; } /** @internal */ export function updateMessageId(rheaMessage, messageId) { if (messageId != null) { if (typeof messageId === "string" && messageId.length > Constants.maxMessageIdLength) { throw new Error(`Length of 'messageId' property on the message cannot be greater than ${Constants.maxMessageIdLength} characters.`); } rheaMessage.message_id = messageId; } } /** @internal */ export function updateScheduledTime(rheaMessage, scheduledEnqueuedTimeUtc) { if (scheduledEnqueuedTimeUtc != null) { rheaMessage.message_annotations = rheaMessage.message_annotations ?? {}; rheaMessage.message_annotations[Constants.scheduledEnqueueTime] = scheduledEnqueuedTimeUtc; } } /** * @internal * Converts given RheaMessage to ServiceBusReceivedMessage */ export function fromRheaMessage(rheaMessage, options) { if (!rheaMessage) { rheaMessage = { body: undefined, }; } const { skipParsingBodyAsJson, delivery, shouldReorderLockToken, skipConvertingDate = false, } = options; const { body, bodyType } = defaultDataTransformer.decodeWithType(rheaMessage.body, skipParsingBodyAsJson); const sbmsg = { body: body, }; if (rheaMessage.application_properties != null) { sbmsg.applicationProperties = skipConvertingDate ? rheaMessage.application_properties : convertDatesToNumbers(rheaMessage.application_properties); } if (rheaMessage.content_type != null) { sbmsg.contentType = rheaMessage.content_type; } if (rheaMessage.group_id != null) { sbmsg.sessionId = rheaMessage.group_id; } if (rheaMessage.reply_to != null) { sbmsg.replyTo = rheaMessage.reply_to; } if (rheaMessage.to != null) { sbmsg.to = rheaMessage.to; } if (rheaMessage.subject != null) { sbmsg.subject = rheaMessage.subject; } if (rheaMessage.message_id != null) { sbmsg.messageId = rheaMessage.message_id; } if (rheaMessage.correlation_id != null) { sbmsg.correlationId = rheaMessage.correlation_id; } if (rheaMessage.reply_to_group_id != null) { sbmsg.replyToSessionId = rheaMessage.reply_to_group_id; } if (rheaMessage.message_annotations != null) { if (rheaMessage.message_annotations[Constants.partitionKey] != null) { sbmsg.partitionKey = rheaMessage.message_annotations[Constants.partitionKey]; } // Will be required later for implementing Transactions // if (msg.message_annotations[Constants.viaPartitionKey] != null) { // sbmsg.viaPartitionKey = msg.message_annotations[Constants.viaPartitionKey]; // } if (rheaMessage.message_annotations[Constants.scheduledEnqueueTime] != null) { sbmsg.scheduledEnqueueTimeUtc = rheaMessage.message_annotations[Constants.scheduledEnqueueTime]; } } const props = { state: "active" }; if (rheaMessage.message_annotations != null) { if (rheaMessage.message_annotations[Constants.deadLetterSource] != null) { props.deadLetterSource = rheaMessage.message_annotations[Constants.deadLetterSource]; } const messageState = rheaMessage.message_annotations[Constants.messageState]; if (messageState === 1) { props.state = "deferred"; } else if (messageState === 2) { props.state = "scheduled"; } if (rheaMessage.message_annotations[Constants.enqueueSequenceNumber] != null) { props.enqueuedSequenceNumber = rheaMessage.message_annotations[Constants.enqueueSequenceNumber]; } if (rheaMessage.message_annotations[Constants.sequenceNumber] != null) { if (Buffer.isBuffer(rheaMessage.message_annotations[Constants.sequenceNumber])) { props.sequenceNumber = Long.fromBytesBE(rheaMessage.message_annotations[Constants.sequenceNumber]); } else { props.sequenceNumber = Long.fromNumber(rheaMessage.message_annotations[Constants.sequenceNumber]); } } if (rheaMessage.message_annotations[Constants.enqueuedTime] != null) { props.enqueuedTimeUtc = new Date(rheaMessage.message_annotations[Constants.enqueuedTime]); } if (rheaMessage.message_annotations[Constants.lockedUntil] != null) { props.lockedUntilUtc = new Date(rheaMessage.message_annotations[Constants.lockedUntil]); } } const rawMessage = AmqpAnnotatedMessage.fromRheaMessage(rheaMessage); rawMessage.bodyType = bodyType; if (rheaMessage.ttl == null) { rheaMessage.ttl = rawMessage.header?.timeToLive ?? Constants.maxDurationValue; } if (props.enqueuedTimeUtc) { props.expiresAtUtc = new Date(Math.min(props.enqueuedTimeUtc.getTime() + rheaMessage.ttl, Constants.maxDurationValue)); } if (rawMessage.applicationProperties) { rawMessage.applicationProperties = skipConvertingDate ? rawMessage.applicationProperties : convertDatesToNumbers(rawMessage.applicationProperties); } if (rawMessage.deliveryAnnotations) { rawMessage.deliveryAnnotations = skipConvertingDate ? rawMessage.deliveryAnnotations : convertDatesToNumbers(rawMessage.deliveryAnnotations); } if (rawMessage.messageAnnotations) { rawMessage.messageAnnotations = skipConvertingDate ? rawMessage.messageAnnotations : convertDatesToNumbers(rawMessage.messageAnnotations); } if (rawMessage.header?.timeToLive) { sbmsg.timeToLive = rawMessage.header.timeToLive; } const rcvdsbmsg = { _rawAmqpMessage: rawMessage, deliveryCount: rheaMessage.delivery_count, lockToken: delivery && delivery.tag && delivery.tag.length !== 0 ? uuid_to_string(shouldReorderLockToken === true ? reorderLockToken(typeof delivery.tag === "string" ? Buffer.from(delivery.tag) : delivery.tag) : typeof delivery.tag === "string" ? Buffer.from(delivery.tag) : delivery.tag) : undefined, ...sbmsg, ...props, deadLetterReason: sbmsg.applicationProperties?.DeadLetterReason, deadLetterErrorDescription: sbmsg.applicationProperties?.DeadLetterErrorDescription, }; logger.verbose("AmqpMessage to ServiceBusReceivedMessage: %O", rcvdsbmsg); return rcvdsbmsg; } /** * @internal */ export function isServiceBusMessage(possible) { return isObjectWithProperties(possible, ["body"]); } /** * @internal */ export function isAmqpAnnotatedMessage(possible) { return (isObjectWithProperties(possible, ["body", "bodyType"]) && possible.constructor.name !== ServiceBusMessageImpl.name); } /** * @internal */ export function isServiceBusReceivedMessage(possible) { return isServiceBusMessage(possible) && "_rawAmqpMessage" in possible; } /** * Describes the message received from Service Bus. * * @internal */ export class ServiceBusMessageImpl { /** * @internal */ constructor(msg, delivery, shouldReorderLockToken, receiveMode, skipParsingBodyAsJson, skipConvertingDate) { const { _rawAmqpMessage, ...restOfMessageProps } = fromRheaMessage(msg, { skipParsingBodyAsJson, delivery, shouldReorderLockToken, skipConvertingDate }); this._rawAmqpMessage = _rawAmqpMessage; // need to initialize _rawAmqpMessage property to make compiler happy Object.assign(this, restOfMessageProps); this.state = restOfMessageProps.state; // to suppress error TS2564: Property 'state' has no initializer and is not definitely assigned in the constructor. // Lock on a message is applicable only in peekLock mode, but the service sets // the lock token even in receiveAndDelete mode if the entity in question is partitioned. if (receiveMode === "receiveAndDelete") { this.lockToken = undefined; } this.delivery = delivery; } /** * Creates a clone of the current message to allow it to be re-sent to the queue * @returns ServiceBusMessage */ clone() { // We are returning a ServiceBusMessage object because that object can then be sent to Service Bus const clone = { body: this.body, contentType: this.contentType, correlationId: this.correlationId, subject: this.subject, messageId: this.messageId, partitionKey: this.partitionKey, replyTo: this.replyTo, replyToSessionId: this.replyToSessionId, scheduledEnqueueTimeUtc: this.scheduledEnqueueTimeUtc, sessionId: this.sessionId, timeToLive: this.timeToLive, to: this.to, applicationProperties: this.applicationProperties, // Will be required later for implementing Transactions // viaPartitionKey: this.viaPartitionKey }; return clone; } } /** * Converts any Date objects into a number representing date.getTime(). * Recursively checks for any Date objects in arrays and objects. * @internal */ function convertDatesToNumbers(thing) { // fast exit if (!isDefined(thing)) return thing; // When 'thing' is a Date, return the number representation if (typeof thing === "object" && isObjectWithProperties(thing, ["getTime"]) && typeof thing.getTime === "function") { return thing.getTime(); } /* Examples: [0, 'foo', new Date(), { nested: new Date()}] */ if (Array.isArray(thing)) { const result = []; for (const element of thing) { result.push(convertDatesToNumbers(element)); } return result; } /* Examples: { foo: new Date(), children: { nested: new Date() }} */ if (typeof thing === "object" && isDefined(thing)) { const thingShallowCopy = { ...thing }; for (const key of Object.keys(thingShallowCopy)) { thingShallowCopy[key] = convertDatesToNumbers(thingShallowCopy[key]); } return thingShallowCopy; } return thing; } //# sourceMappingURL=serviceBusMessage.js.map