UNPKG

@azure/service-bus

Version:
1,012 lines • 50 kB
// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. import Long from "long"; import { message as RheaMessageUtil, generate_uuid, string_to_uuid, types, ReceiverEvents, } from "rhea-promise"; import { ConditionErrorNameMapper, Constants, defaultCancellableLock, RequestResponseLink, } from "@azure/core-amqp"; import { DispositionType, ServiceBusMessageImpl, toRheaMessage, fromRheaMessage, updateScheduledTime, updateMessageId, } from "../serviceBusMessage"; import { LinkEntity } from "./linkEntity"; import { managementClientLogger, receiverLogger, senderLogger } from "../log"; import { toBuffer, waitForSendable } from "../util/utils"; import { InvalidMaxMessageCountError, throwErrorIfConnectionClosed, throwTypeErrorIfParameterIsEmptyString, throwTypeErrorIfParameterMissing, throwTypeErrorIfParameterNotLong, throwTypeErrorIfParameterTypeMismatch, } from "../util/errors"; import { max32BitNumber } from "../util/constants"; import { Buffer } from "buffer"; import { AbortController } from "@azure/abort-controller"; import { translateServiceBusError } from "../serviceBusError"; import { defaultDataTransformer, tryToJsonDecode } from "../dataTransformer"; import { delay, isDefined, isObjectWithProperties } from "@azure/core-util"; /** * @internal */ const sqlRuleProperties = ["sqlExpression"]; function isSqlRuleFilter(obj) { if (obj) { return sqlRuleProperties.some((validProperty) => isObjectWithProperties(obj, [validProperty])); } return false; } /** * @internal */ const correlationProperties = [ "correlationId", "messageId", "to", "replyTo", "subject", "sessionId", "replyToSessionId", "contentType", "applicationProperties", ]; function isCorrelationRuleFilter(obj) { if (obj) { return correlationProperties.some((validProperty) => isObjectWithProperties(obj, [validProperty])); } return false; } /** * @internal * Describes the ServiceBus Management Client that talks * to the $management endpoint over AMQP connection. */ export class ManagementClient extends LinkEntity { /** * Instantiates the management client. * @param context - The connection context * @param entityPath - The name/path of the entity (queue/topic/subscription name) * for which the management request needs to be made. * @param options - Options to be provided for creating the * "$management" client. */ constructor(context, entityPath, options) { super(`${entityPath}/$management`, entityPath, context, "mgmt", managementClientLogger, { address: options && options.address ? options.address : Constants.management, audience: options && options.audience ? options.audience : `${context.config.endpoint}${entityPath}/$management`, }); /** * The reply to Guid for the management client. */ this.replyTo = generate_uuid(); /** * Provides the sequence number of the last peeked message. */ this._lastPeekedSequenceNumber = Long.ZERO; /** * lock token for init operation */ this._initLock = `initMgmtLink-${generate_uuid()}`; this._context = context; } /** * initialize link with unique this.replyTo address. * @param options - * @returns updated options bag that has adjusted `timeoutInMs` to account for init time */ async initWithUniqueReplyTo(options = {}) { const retryTimeoutInMs = options.timeoutInMs ?? Constants.defaultOperationTimeoutInMs; const initOperationStartTime = Date.now(); return defaultCancellableLock.acquire(this._initLock, async () => { managementClientLogger.verbose(`${this.logPrefix} lock acquired for initializing replyTo address and link`); if (!this.isOpen()) { this.replyTo = generate_uuid(); managementClientLogger.verbose(`${this.logPrefix} new replyTo address: ${this.replyTo} generated`); } const { abortSignal } = options ?? {}; const aborter = new AbortController(); const { signal } = new AbortController([ aborter.signal, ...(abortSignal ? [abortSignal] : []), ]); if (!this.isOpen()) { await Promise.race([ this._init(signal), delay(retryTimeoutInMs, { abortSignal: aborter.signal }).then(() => { throw { name: "OperationTimeoutError", message: "The management request timed out. Please try again later.", }; }), ]).finally(() => aborter.abort()); } // time taken by the init operation const timeTakenByInit = Date.now() - initOperationStartTime; return { ...options, // Left over time timeoutInMs: retryTimeoutInMs - timeTakenByInit, }; }, { abortSignal: options.abortSignal, timeoutInMs: retryTimeoutInMs, }); } async _init(abortSignal) { throwErrorIfConnectionClosed(this._context); try { const rxopt = { source: { address: this.address }, name: this.replyTo, target: { address: this.replyTo }, onSessionError: (context) => { const sbError = translateServiceBusError(context.session.error); managementClientLogger.logError(sbError, `${this.logPrefix} An error occurred on the session for request/response links for $management`); }, }; const sropt = { target: { address: this.address }, onError: (context) => { const ehError = translateServiceBusError(context.sender.error); managementClientLogger.logError(ehError, `${this.logPrefix} An error occurred on the $management sender link`); }, }; // Even if multiple parallel requests reach here, the initLink secures a lock // to ensure there won't be multiple initializations await this.initLink({ senderOptions: sropt, receiverOptions: rxopt, }, abortSignal); } catch (err) { const translatedError = translateServiceBusError(err); managementClientLogger.logError(translatedError, `${this.logPrefix} An error occurred while establishing the $management links`); throw translatedError; } } async createRheaLink( // eslint-disable-next-line @azure/azure-sdk/ts-naming-options options) { const rheaLink = await RequestResponseLink.create(this._context.connection, options.senderOptions, options.receiverOptions); // Attach listener for the `receiver_error` events to log the errors. // "message" event listener is added in core-amqp. // "rhea" doesn't allow setting only the "onError" handler in the options if it is not accompanied by an "onMessage" handler. // Hence, not passing onError handler in the receiver options, adding a handler below. rheaLink.receiver.on(ReceiverEvents.receiverError, (context) => { const ehError = translateServiceBusError(context.receiver.error); managementClientLogger.logError(ehError, `${this.logPrefix} An error occurred on the $management receiver link`); }); return rheaLink; } /** * Given array of typed values, returns the element in given index */ _safelyGetTypedValueFromArray(data, index) { return Array.isArray(data) && data.length > index && data[index] ? data[index].value : undefined; } _decodeApplicationPropertiesMap(obj) { if (!types.is_map(obj)) { throw new Error("object to decode is not of Map types"); } const array = obj.value; const result = {}; for (let i = 0; i < array.length; i += 2) { const key = array[i].value; result[key] = array[i + 1].value; } return result; } async _makeManagementRequest(request, internalLogger, sendRequestOptions = {}) { if (request.message_id === undefined) { request.message_id = generate_uuid(); } try { const { timeoutInMs } = sendRequestOptions; await waitForSendable(internalLogger, this.logPrefix, this.name, timeoutInMs ?? Constants.defaultOperationTimeoutInMs, this.link?.sender, this.link?.session?.outgoing?.available()); return await this.link.sendRequest(request, sendRequestOptions); } catch (err) { const translatedError = translateServiceBusError(err); internalLogger.logError(translatedError, "%s An error occurred during send on management request-response link with address '%s'", this.logPrefix, this.address); throw translatedError; } } /** * Closes the AMQP management session to the ServiceBus namespace for this client, * returning a promise that will be resolved when disconnection is completed. */ async close() { try { // Always clear the timeout, as the isOpen check may report // false without ever having cleared the timeout otherwise. // NOTE: management link currently doesn't have a separate concept of "detaching" like // the other links do. When we add handling of this (via the onDetached call, like other links) // we can change this back to closeLink("permanent"). await this.closeLink(); managementClientLogger.verbose("Successfully closed the management session."); } catch (err) { managementClientLogger.logError(err, `${this.logPrefix} An error occurred while closing the management session`); throw err; } } /** * Fetches the next batch of active messages. The first call to `peek()` fetches the first * active message for this client. Each subsequent call fetches the subsequent message in the * entity. * * Unlike a `received` message, `peeked` message will not have lock token associated with it, * and hence it cannot be `Completed/Abandoned/Deferred/Deadlettered/Renewed`. This method will * also fetch even Deferred messages (but not Deadlettered message). * * @param messageCount - The number of messages to retrieve. Default value `1`. * @param omitMessageBody - Whether to omit message body when peeking. Default value `false`. */ async peek(messageCount, omitMessageBody, options) { throwErrorIfConnectionClosed(this._context); return this.peekBySequenceNumber(this._lastPeekedSequenceNumber.add(1), messageCount, undefined, omitMessageBody, options); } /** * Fetches the next batch of active messages in the current MessageSession. The first call to * `peek()` fetches the first active message for this client. Each subsequent call fetches the * subsequent message in the entity. * * Unlike a `received` message, `peeked` message will not have lock token associated with it, * and hence it cannot be `Completed/Abandoned/Deferred/Deadlettered/Renewed`. This method will * also fetch even Deferred messages (but not Deadlettered message). * * @param sessionId - The sessionId from which messages need to be peeked. * @param messageCount - The number of messages to retrieve. Default value `1`. * @param omitMessageBody - Whether to omit message body when peeking Default value `false`. */ async peekMessagesBySession(sessionId, messageCount, omitMessageBody, options) { throwErrorIfConnectionClosed(this._context); return this.peekBySequenceNumber(this._lastPeekedSequenceNumber.add(1), messageCount, sessionId, omitMessageBody, options); } /** * Peeks the desired number of messages from the specified sequence number. * * @param fromSequenceNumber - The sequence number from where to read the message. * @param messageCount - The number of messages to retrieve. Default value `1`. * @param sessionId - The sessionId from which messages need to be peeked. * @param omitMessageBody - Whether to omit message body when peeking. Default value `false`. */ async peekBySequenceNumber(fromSequenceNumber, maxMessageCount, sessionId, omitMessageBody, options) { throwErrorIfConnectionClosed(this._context); const connId = this._context.connectionId; // Checks for fromSequenceNumber throwTypeErrorIfParameterMissing(connId, "fromSequenceNumber", fromSequenceNumber); throwTypeErrorIfParameterNotLong(connId, "fromSequenceNumber", fromSequenceNumber); // Checks for maxMessageCount throwTypeErrorIfParameterMissing(this._context.connectionId, "maxMessageCount", maxMessageCount); throwTypeErrorIfParameterTypeMismatch(this._context.connectionId, "maxMessageCount", maxMessageCount, "number"); if (isNaN(maxMessageCount) || maxMessageCount < 1) { throw new TypeError(InvalidMaxMessageCountError); } const messageList = []; try { const messageBody = {}; messageBody[Constants.fromSequenceNumber] = types.wrap_long(Buffer.from(fromSequenceNumber.toBytesBE())); messageBody[Constants.messageCount] = types.wrap_int(maxMessageCount); if (isDefined(sessionId)) { messageBody[Constants.sessionIdMapKey] = sessionId; } if (isDefined(omitMessageBody)) { const omitMessageBodyKey = "omit-message-body"; // TODO: Service Bus specific. Put it somewhere messageBody[omitMessageBodyKey] = types.wrap_boolean(omitMessageBody); } const updatedOptions = await this.initWithUniqueReplyTo(options); const request = { body: messageBody, reply_to: this.replyTo, application_properties: { operation: Constants.operations.peekMessage, }, }; if (updatedOptions?.associatedLinkName) { request.application_properties[Constants.associatedLinkName] = updatedOptions?.associatedLinkName; } request.application_properties[Constants.trackingId] = generate_uuid(); // TODO: it'd be nice to attribute this peek request to the actual receiver that made it. So have them pass in a // log prefix rather than just falling back to the management links. receiverLogger.verbose("%s Peek by sequence number request body: %O.", this.logPrefix, request.body); const result = await this._makeManagementRequest(request, receiverLogger, updatedOptions); if (result.application_properties.statusCode !== 204) { const messages = result.body.messages; for (const msg of messages) { const decodedMessage = RheaMessageUtil.decode(msg.message); const message = fromRheaMessage(decodedMessage, { skipParsingBodyAsJson: updatedOptions?.skipParsingBodyAsJson ?? false, skipConvertingDate: updatedOptions?.skipConvertingDate ?? false, }); messageList.push(message); this._lastPeekedSequenceNumber = message.sequenceNumber; } } } catch (err) { const error = translateServiceBusError(err); receiverLogger.logError(error, `${this.logPrefix} An error occurred while sending the request to peek messages to $management endpoint`); // statusCode == 404 then do not throw if (error.code !== ConditionErrorNameMapper["com.microsoft:message-not-found"]) { throw error; } } return messageList; } /** * Renews the lock on the message. The lock will be renewed based on the setting specified on * the queue. * * When a message is received in `PeekLock` mode, the message is locked on the server for this * receiver instance for a duration as specified during the Queue/Subscription creation * (LockDuration). If processing of the message requires longer than this duration, the * lock needs to be renewed. For each renewal, it resets the time the message is locked by the * LockDuration set on the Entity. * * @param lockToken - Lock token of the message * @param options - Options that can be set while sending the request. * @returns New lock token expiry date and time in UTC format. */ // eslint-disable-next-line @azure/azure-sdk/ts-naming-options async renewLock(lockToken, options) { throwErrorIfConnectionClosed(this._context); if (!options) options = {}; if (options.timeoutInMs == null) options.timeoutInMs = 5000; try { const messageBody = {}; messageBody[Constants.lockTokens] = types.wrap_array([string_to_uuid(lockToken)], 0x98, undefined); const updatedOptions = await this.initWithUniqueReplyTo(options); const request = { body: messageBody, reply_to: this.replyTo, application_properties: { operation: Constants.operations.renewLock, }, }; request.application_properties[Constants.trackingId] = generate_uuid(); if (updatedOptions.associatedLinkName) { request.application_properties[Constants.associatedLinkName] = updatedOptions.associatedLinkName; } receiverLogger.verbose("[%s] Renew message Lock request: %O.", this._context.connectionId, request); const result = await this._makeManagementRequest(request, receiverLogger, { abortSignal: updatedOptions?.abortSignal, requestName: "renewLock", }); const lockedUntilUtc = new Date(result.body.expirations[0]); return lockedUntilUtc; } catch (err) { const error = translateServiceBusError(err); receiverLogger.logError(error, `${this.logPrefix} An error occurred while sending the renew lock request to $management endpoint`); throw error; } } /** * Schedules an array of messages to appear on Service Bus at a later time. * * @param scheduledEnqueueTimeUtc - The UTC time at which the messages should be enqueued. * @param messages - An array of messages that needs to be scheduled. * @returns The sequence numbers of messages that were scheduled. */ async scheduleMessages(scheduledEnqueueTimeUtc, messages, options) { throwErrorIfConnectionClosed(this._context); if (!messages.length) { return []; } const messageBody = []; for (let i = 0; i < messages.length; i++) { const item = messages[i]; try { const rheaMessage = toRheaMessage(item, defaultDataTransformer); updateMessageId(rheaMessage, rheaMessage.message_id || generate_uuid()); updateScheduledTime(rheaMessage, scheduledEnqueueTimeUtc); const entry = { message: RheaMessageUtil.encode(rheaMessage), "message-id": rheaMessage.message_id, }; if (rheaMessage.group_id) { entry[Constants.sessionIdMapKey] = rheaMessage.group_id; } if (rheaMessage.message_annotations?.[Constants.partitionKey]) { entry["partition-key"] = rheaMessage.message_annotations[Constants.partitionKey]; } // Will be required later for implementing Transactions // if (item.viaPartitionKey) { // entry["via-partition-key"] = item.viaPartitionKey; // } const wrappedEntry = types.wrap_map(entry); messageBody.push(wrappedEntry); } catch (err) { const error = translateServiceBusError(err); senderLogger.logError(error, `${this.logPrefix} An error occurred while encoding the item at position ${i} in the messages array`); throw error; } } const updatedOptions = await this.initWithUniqueReplyTo(options); try { const request = { body: { messages: messageBody }, reply_to: this.replyTo, application_properties: { operation: Constants.operations.scheduleMessage, }, }; if (updatedOptions?.associatedLinkName) { request.application_properties[Constants.associatedLinkName] = updatedOptions?.associatedLinkName; } request.application_properties[Constants.trackingId] = generate_uuid(); senderLogger.verbose("%s Schedule messages request body: %O.", this.logPrefix, request.body); const result = await this._makeManagementRequest(request, senderLogger, updatedOptions); const sequenceNumbers = result.body[Constants.sequenceNumbers]; const sequenceNumbersAsLong = []; for (let i = 0; i < sequenceNumbers.length; i++) { if (typeof sequenceNumbers[i] === "number") { sequenceNumbersAsLong.push(Long.fromNumber(sequenceNumbers[i])); } else { sequenceNumbersAsLong.push(Long.fromBytesBE(sequenceNumbers[i])); } } return sequenceNumbersAsLong; } catch (err) { const error = translateServiceBusError(err); senderLogger.logError(error, `${this.logPrefix} An error occurred while sending the request to schedule messages to $management endpoint`); throw error; } } /** * Cancels an array of messages that were scheduled. * @param sequenceNumbers - An Array of sequence numbers of the message to be cancelled. */ async cancelScheduledMessages(sequenceNumbers, options) { throwErrorIfConnectionClosed(this._context); if (!sequenceNumbers.length) { return; } const messageBody = {}; messageBody[Constants.sequenceNumbers] = []; for (let i = 0; i < sequenceNumbers.length; i++) { const sequenceNumber = sequenceNumbers[i]; try { messageBody[Constants.sequenceNumbers].push(Buffer.from(sequenceNumber.toBytesBE())); } catch (err) { const error = translateServiceBusError(err); senderLogger.logError(error, `${this.logPrefix} An error occurred while encoding the item at position ${i} in the sequenceNumbers array`); throw error; } } try { messageBody[Constants.sequenceNumbers] = types.wrap_array(messageBody[Constants.sequenceNumbers], 0x81, undefined); const updatedOptions = await this.initWithUniqueReplyTo(options); const request = { body: messageBody, reply_to: this.replyTo, application_properties: { operation: Constants.operations.cancelScheduledMessage, }, }; if (updatedOptions?.associatedLinkName) { request.application_properties[Constants.associatedLinkName] = updatedOptions?.associatedLinkName; } request.application_properties[Constants.trackingId] = generate_uuid(); senderLogger.verbose("%s Cancel scheduled messages request body: %O.", this.logPrefix, request.body); await this._makeManagementRequest(request, senderLogger, updatedOptions); return; } catch (err) { const error = translateServiceBusError(err); senderLogger.logError(error, `${this.logPrefix} An error occurred while sending the request to cancel the scheduled message to $management endpoint`); throw error; } } /** * Receives a list of deferred messages identified by `sequenceNumbers`. * * @param sequenceNumbers - A list containing the sequence numbers to receive. * @param receiveMode - The mode in which the receiver was created. * @returns a list of messages identified by the given sequenceNumbers or an empty list if no messages are found. * - Throws an error if the messages have not been deferred. */ async receiveDeferredMessages(sequenceNumbers, receiveMode, sessionId, options) { throwErrorIfConnectionClosed(this._context); if (!sequenceNumbers.length) { return []; } const messageList = []; const messageBody = {}; messageBody[Constants.sequenceNumbers] = []; for (let i = 0; i < sequenceNumbers.length; i++) { const sequenceNumber = sequenceNumbers[i]; try { messageBody[Constants.sequenceNumbers].push(Buffer.from(sequenceNumber.toBytesBE())); } catch (err) { const error = translateServiceBusError(err); receiverLogger.logError(error, `${this.logPrefix} An error occurred while encoding the item at position ${i} in the sequenceNumbers array`); throw error; } } try { messageBody[Constants.sequenceNumbers] = types.wrap_array(messageBody[Constants.sequenceNumbers], 0x81, undefined); const receiverSettleMode = receiveMode === "receiveAndDelete" ? 0 : 1; messageBody[Constants.receiverSettleMode] = types.wrap_uint(receiverSettleMode); if (sessionId != null) { messageBody[Constants.sessionIdMapKey] = sessionId; } const updatedOptions = await this.initWithUniqueReplyTo(options); const request = { body: messageBody, reply_to: this.replyTo, application_properties: { operation: Constants.operations.receiveBySequenceNumber, }, }; if (updatedOptions?.associatedLinkName) { request.application_properties[Constants.associatedLinkName] = updatedOptions?.associatedLinkName; } request.application_properties[Constants.trackingId] = generate_uuid(); receiverLogger.verbose("%s Receive deferred messages request body: %O.", this.logPrefix, request.body); const result = await this._makeManagementRequest(request, receiverLogger, updatedOptions); const messages = result.body.messages; for (const msg of messages) { const decodedMessage = RheaMessageUtil.decode(msg.message); const message = new ServiceBusMessageImpl(decodedMessage, { tag: msg["lock-token"] }, false, receiveMode, updatedOptions?.skipParsingBodyAsJson ?? false, false); messageList.push(message); } return messageList; } catch (err) { const error = translateServiceBusError(err); receiverLogger.logError(error, `${this.logPrefix} An error occurred while sending the request to receive deferred messages to $management endpoint`); throw error; } } /** * Updates the disposition status of deferred messages. * * @param lockTokens - Message lock tokens to update disposition status. * @param dispositionStatus - The disposition status to be set * @param options - Optional parameters that can be provided while updating the disposition status. */ async updateDispositionStatus(lockToken, dispositionType, // TODO: mgmt link retry<> will come in the next PR. options) { throwErrorIfConnectionClosed(this._context); if (!options) options = {}; try { let dispositionStatus; if (dispositionType === DispositionType.abandon) dispositionStatus = "abandoned"; else if (dispositionType === DispositionType.complete) dispositionStatus = "completed"; else if (dispositionType === DispositionType.defer) dispositionStatus = "defered"; else if (dispositionType === DispositionType.deadletter) dispositionStatus = "suspended"; else throw new Error(`Provided "dispositionType" - ${dispositionType} is invalid`); const messageBody = {}; const lockTokenBuffer = []; lockTokenBuffer.push(string_to_uuid(lockToken)); messageBody[Constants.lockTokens] = types.wrap_array(lockTokenBuffer, 0x98, undefined); messageBody[Constants.dispositionStatus] = dispositionStatus; if (options.deadLetterDescription != null) { messageBody[Constants.deadLetterDescription] = options.deadLetterDescription; } if (options.deadLetterReason != null) { messageBody[Constants.deadLetterReason] = options.deadLetterReason; } if (options.propertiesToModify != null) { messageBody[Constants.propertiesToModify] = options.propertiesToModify; } if (options.sessionId != null) { messageBody[Constants.sessionIdMapKey] = options.sessionId; } const updatedOptions = await this.initWithUniqueReplyTo(options); const request = { body: messageBody, reply_to: this.replyTo, application_properties: { operation: Constants.operations.updateDisposition, }, }; if (updatedOptions.associatedLinkName) { request.application_properties[Constants.associatedLinkName] = updatedOptions.associatedLinkName; } request.application_properties[Constants.trackingId] = generate_uuid(); receiverLogger.verbose("%s Update disposition status request body: %O.", this.logPrefix, request.body); await this._makeManagementRequest(request, receiverLogger, updatedOptions); } catch (err) { const error = translateServiceBusError(err); receiverLogger.logError(error, `${this.logPrefix} An error occurred while sending the request to update disposition status to $management endpoint`); throw error; } } /** * Renews the lock for the specified session. * * @param sessionId - Id of the session for which the lock needs to be renewed * @param options - Options that can be set while sending the request. * @returns New lock token expiry date and time in UTC format. */ async renewSessionLock(sessionId, options) { throwErrorIfConnectionClosed(this._context); try { const messageBody = {}; messageBody[Constants.sessionIdMapKey] = sessionId; const updatedOptions = await this.initWithUniqueReplyTo(options); const request = { body: messageBody, reply_to: this.replyTo, application_properties: { operation: Constants.operations.renewSessionLock, }, }; request.application_properties[Constants.trackingId] = generate_uuid(); if (updatedOptions?.associatedLinkName) { request.application_properties[Constants.associatedLinkName] = updatedOptions?.associatedLinkName; } receiverLogger.verbose("%s Renew Session Lock request body: %O.", this.logPrefix, request.body); const result = await this._makeManagementRequest(request, receiverLogger, updatedOptions); const lockedUntilUtc = new Date(result.body.expiration); receiverLogger.verbose("%s Lock for session '%s' will expire at %s.", this.logPrefix, sessionId, lockedUntilUtc.toString()); return lockedUntilUtc; } catch (err) { const error = translateServiceBusError(err); receiverLogger.logError(error, `${this.logPrefix} An error occurred while sending the renew lock request to $management endpoint`); throw error; } } /** * Sets the state of the specified session. * * @param sessionId - The session for which the state needs to be set * @param state - The state that needs to be set. */ async setSessionState(sessionId, state, options) { throwErrorIfConnectionClosed(this._context); try { const messageBody = {}; messageBody[Constants.sessionIdMapKey] = sessionId; messageBody["session-state"] = toBuffer(state); const updatedOptions = await this.initWithUniqueReplyTo(options); const request = { body: messageBody, reply_to: this.replyTo, application_properties: { operation: Constants.operations.setSessionState, }, }; if (updatedOptions?.associatedLinkName) { request.application_properties[Constants.associatedLinkName] = updatedOptions?.associatedLinkName; } request.application_properties[Constants.trackingId] = generate_uuid(); receiverLogger.verbose("%s Set Session state request body: %O.", this.logPrefix, request.body); await this._makeManagementRequest(request, receiverLogger, updatedOptions); } catch (err) { const error = translateServiceBusError(err); receiverLogger.logError(error, `${this.logPrefix} An error occurred while sending the renew lock request to $management endpoint`); throw error; } } /** * Gets the state of the specified session. * * @param sessionId - The session for which the state needs to be retrieved. * @returns The state of that session */ async getSessionState(sessionId, options) { throwErrorIfConnectionClosed(this._context); try { const messageBody = {}; messageBody[Constants.sessionIdMapKey] = sessionId; const updatedOptions = await this.initWithUniqueReplyTo(options); const request = { body: messageBody, reply_to: this.replyTo, application_properties: { operation: Constants.operations.getSessionState, }, }; if (updatedOptions?.associatedLinkName) { request.application_properties[Constants.associatedLinkName] = updatedOptions?.associatedLinkName; } request.application_properties[Constants.trackingId] = generate_uuid(); receiverLogger.verbose("%s Get session state request body: %O.", this.logPrefix, request.body); const result = await this._makeManagementRequest(request, receiverLogger, updatedOptions); return result.body["session-state"] ? tryToJsonDecode(result.body["session-state"]) : result.body["session-state"]; } catch (err) { const error = translateServiceBusError(err); receiverLogger.logError(error, `${this.logPrefix} An error occurred while sending the renew lock request to $management endpoint`); throw error; } } /** * Lists the sessions on the ServiceBus Queue/Topic. * @param lastUpdateTime - Filter to include only sessions updated after a given time. * @param skip - The number of sessions to skip * @param top - Maximum numer of sessions. * @returns A list of session ids. */ async listMessageSessions(skip, top, lastUpdatedTime, options) { throwErrorIfConnectionClosed(this._context); const defaultLastUpdatedTimeForListingSessions = 259200000; // 3 * 24 * 3600 * 1000 if (typeof skip !== "number") { throw new Error("'skip' is a required parameter and must be of type 'number'."); } if (typeof top !== "number") { throw new Error("'top' is a required parameter and must be of type 'number'."); } if (lastUpdatedTime && !(lastUpdatedTime instanceof Date)) { throw new Error("'lastUpdatedTime' must be of type 'Date'."); } if (!lastUpdatedTime) { lastUpdatedTime = new Date(Date.now() - defaultLastUpdatedTimeForListingSessions); } try { const messageBody = {}; messageBody["last-updated-time"] = lastUpdatedTime; messageBody["skip"] = types.wrap_int(skip); messageBody["top"] = types.wrap_int(top); const updatedOptions = await this.initWithUniqueReplyTo(options); const request = { body: messageBody, reply_to: this.replyTo, application_properties: { operation: Constants.operations.enumerateSessions, }, }; request.application_properties[Constants.trackingId] = generate_uuid(); managementClientLogger.verbose("%s List sessions request body: %O.", this.logPrefix, request.body); const response = await this._makeManagementRequest(request, managementClientLogger, updatedOptions); return (response && response.body && response.body["sessions-ids"]) || []; } catch (err) { const error = translateServiceBusError(err); managementClientLogger.logError(error, `${this.logPrefix} An error occurred while sending the renew lock request to $management endpoint`); throw error; } } /** * Get all the rules on the Subscription. * @returns A list of rules. */ async getRules(options) { throwErrorIfConnectionClosed(this._context); try { const updatedOptions = (await this.initWithUniqueReplyTo(options)); const request = { body: { top: updatedOptions?.maxCount ? types.wrap_int(updatedOptions.maxCount) : types.wrap_int(max32BitNumber), skip: updatedOptions?.skip ? types.wrap_int(updatedOptions.skip) : types.wrap_int(0), }, reply_to: this.replyTo, application_properties: { operation: Constants.operations.enumerateRules, }, }; request.application_properties[Constants.trackingId] = generate_uuid(); managementClientLogger.verbose("%s Get rules request body: %O.", this.logPrefix, request.body); const response = await this._makeManagementRequest(request, managementClientLogger, updatedOptions); if (response.application_properties.statusCode === 204 || !response.body || !Array.isArray(response.body.rules)) { return []; } // Reference: https://docs.microsoft.com/azure/service-bus-messaging/service-bus-amqp-request-response#response-11 const result = response.body.rules || []; const rules = []; result.forEach((x) => { const ruleDescriptor = x["rule-description"]; let filter; // We use the first three elements of the `ruleDescriptor.value` to get filter, action, name if (!ruleDescriptor || !ruleDescriptor.descriptor || ruleDescriptor.descriptor.value !== Constants.descriptorCodes.ruleDescriptionList || !Array.isArray(ruleDescriptor.value) || ruleDescriptor.value.length < 3) { return; } const filtersRawData = ruleDescriptor.value[0]; const actionsRawData = ruleDescriptor.value[1]; let sqlRuleAction; if (actionsRawData.descriptor.value === Constants.descriptorCodes.sqlRuleActionList && Array.isArray(actionsRawData.value) && actionsRawData.value.length) { sqlRuleAction = { sqlExpression: this._safelyGetTypedValueFromArray(actionsRawData.value, 0), }; } else { sqlRuleAction = {}; } switch (filtersRawData.descriptor.value) { case Constants.descriptorCodes.trueFilterList: filter = { sqlExpression: "1=1", }; break; case Constants.descriptorCodes.falseFilterList: filter = { sqlExpression: "1=0", }; break; case Constants.descriptorCodes.sqlFilterList: filter = { sqlExpression: this._safelyGetTypedValueFromArray(filtersRawData.value, 0), }; break; case Constants.descriptorCodes.correlationFilterList: filter = { correlationId: this._safelyGetTypedValueFromArray(filtersRawData.value, 0), messageId: this._safelyGetTypedValueFromArray(filtersRawData.value, 1), to: this._safelyGetTypedValueFromArray(filtersRawData.value, 2), replyTo: this._safelyGetTypedValueFromArray(filtersRawData.value, 3), subject: this._safelyGetTypedValueFromArray(filtersRawData.value, 4), sessionId: this._safelyGetTypedValueFromArray(filtersRawData.value, 5), replyToSessionId: this._safelyGetTypedValueFromArray(filtersRawData.value, 6), contentType: this._safelyGetTypedValueFromArray(filtersRawData.value, 7), applicationProperties: Array.isArray(filtersRawData.value) && filtersRawData.value.length > 8 && filtersRawData.value[8] ? this._decodeApplicationPropertiesMap(filtersRawData.value[8]) : undefined, }; break; default: throw new Error(`${this.logPrefix} Found unexpected descriptor code for the filter: ${filtersRawData.descriptor.value}`); } const rule = { name: ruleDescriptor.value[2].value, filter, action: sqlRuleAction, }; rules.push(rule); }); return rules; } catch (err) { const error = translateServiceBusError(err); managementClientLogger.logError(error, `${this.logPrefix} An error occurred while sending the get rules request to $management endpoint`); throw error; } } /** * Removes the rule on the Subscription identified by the given rule name. */ async removeRule(ruleName, options) { throwErrorIfConnectionClosed(this._context); throwTypeErrorIfParameterMissing(this._context.connectionId, "ruleName", ruleName); ruleName = String(ruleName); throwTypeErrorIfParameterIsEmptyString(this._context.connectionId, "ruleName", ruleName); try { const updatedOptions = await this.initWithUniqueReplyTo(options); const request = { body: { "rule-name": types.wrap_string(ruleName), }, reply_to: this.replyTo, application_properties: { operation: Constants.operations.removeRule, }, }; request.application_properties[Constants.trackingId] = generate_uuid(); managementClientLogger.verbose("%s Remove Rule request body: %O.", this.logPrefix, request.body); await this._makeManagementRequest(request, managementClientLogger, updatedOptions); } catch (err) { const error = translateServiceBusError(err); managementClientLogger.logError(error, `${this.logPrefix} An error occurred while sending the remove rule request to $management endpoint`); throw error; } } /** * Adds a rule on the subscription as defined by the given rule name, filter and action * @param ruleName - Name of the rule * @param filter - A Boolean, SQL expression or a Correlation filter * @param sqlRuleActionExpression - Action to perform if the message satisfies the filtering expression */ async addRule(ruleName, filter, sqlRuleActionExpression, options) { throwErrorIfConnectionClosed(this._context); throwTypeErrorIfParameterMissing(this._context.connectionId, "ruleName", ruleName); ruleName = String(ruleName); throwTypeErrorIfParameterIsEmptyString(this._context.connectionId, "ruleName", ruleName); throwTypeErrorIfParameterMissing(this._context.connectionId, "filter", filter); if (!isSqlRuleFilter(filter) && !isCorrelationRuleFilter(filter)) { throw new TypeError(`The parameter "filter" should implement either the SqlRuleFilter or the CorrelationRuleFilter interface.`); } try { const ruleDescription = {}; if (isSqlRuleFilter(filter)) { ruleDescription["sql-filter"] = { expression: filter.sqlExpression, }; } else { ruleDescription["correlation-filter"] = { "correlation-id": filter.correlationId, "message-id": filter.messageId, to: filter.to, "reply-to": filter.replyTo, label: filter.subject, "session-id": filter.sessionId, "reply-to-session-id": filter.replyToSessionId, "content-type": filter.contentType, properties: filter.applicationProperties, }; } if (sqlRuleActionExpression !== undefined) { ruleDescription["sql-rule-action"] = { expression: String(sqlRuleActionExpression), }; } const updatedOptions = await this.initWithUniqueReplyTo(options); const request = { body: { "rule-name": types.wrap_string(ruleName), "rule-description": types.wrap_map(ruleDescription), }, reply_to: this.replyTo, application_properties: { operation: Constants.operations.addRule, }, }; request.application_properties[Constants.trackingId] = generate_uuid(); managementClientLogger.verbose("%s Add Rule request body: %O.", this.logPrefix, request.body); await this._makeManagementRequest(request, managementClientLogger, updatedOptions); } catch (err) { const error = translateServiceBusError(err); managementClientLogger.logError(error, `${this.logPrefix} An error occurred while sending the Add rule request to $management endpoint`); throw error; } } removeLinkFromContext() { delete this._context.managementClients[this.name]; } } /** * Converts an AmqpAnnotatedMessage or ServiceBusMessage into a properly formatted * message for sending to the mgmt link for scheduling. * * @internal * @hidden */ export function toScheduleableMessage(item, scheduledEnqueueTimeUtc) { const rheaMessage = toRheaMessage(item, defaultDataTransformer); updateMessageId(rheaMessage, rheaMessage.message_id || generate_uuid()); updateScheduledTime(rheaMessage, scheduledEnqueueTimeUtc); const entry = { message: RheaMessageUtil.encode(rheaMessage), "message-id": rheaMessage.message_id, }; rheaMessage.message_annotations = { ...rheaMessage.message_annotations, [Constants.scheduledEnqueueTime]: scheduledEnqueueTimeUtc, }; if (rheaMessage.group_id) { entry[Constants.sessionIdMapKey] = rheaMessage.group_id; } const partitionKey = rheaMessage.message_annotations && rheaMessage.message_annotations[Constants.partitionKey]; if (partitionKey) { entry["partition-key"] = partitionKey; } return entry; } //# sourceMappingURL=managementClient.js.map