@azure/service-bus
Version:
Azure Service Bus SDK for JavaScript
1,434 lines (1,422 loc) • 486 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var coreAmqp = require('@azure/core-amqp');
var coreAuth = require('@azure/core-auth');
var coreClient = require('@azure/core-client');
var coreRestPipeline = require('@azure/core-rest-pipeline');
var logger$1 = require('@azure/logger');
var coreUtil = require('@azure/core-util');
var coreXml = require('@azure/core-xml');
var buffer = require('buffer');
var url = require('url');
var Long = require('long');
var rheaPromise = require('rhea-promise');
var isBuffer = require('is-buffer');
var abortController = require('@azure/abort-controller');
var crypto = require('crypto');
var coreTracing = require('@azure/core-tracing');
var os = require('os');
var corePaging = require('@azure/core-paging');
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var os__namespace = /*#__PURE__*/_interopNamespaceDefault(os);
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
/**
* The `@azure/logger` configuration for this package.
* This will output logs using the `azure:service-bus` namespace prefix.
* @internal
*/
const logger = createServiceBusLogger("service-bus");
/**
* Logging for ServiceBusReceivers of any type (session, non-session)
* @internal
*/
const receiverLogger = createServiceBusLogger("service-bus:receiver");
/**
* Logging for ServiceBusSenders
* @internal
*/
const senderLogger = createServiceBusLogger("service-bus:sender");
/**
* Logging for ServiceBusRuleManagers
* @internal
*/
const ruleManagerLogger = createServiceBusLogger("service-bus:rulemanager");
/**
* Logging for connection management
* @internal
*/
const connectionLogger = createServiceBusLogger("service-bus:connection");
/**
* Logging for the ServiceBusAdministrationClient
* @internal
*/
const administrationLogger = createServiceBusLogger("service-bus:administration");
/**
* Logging related to message encoding/decoding.
* @internal
*/
const messageLogger = createServiceBusLogger("service-bus:messages");
/**
* Logging related to message encoding/decoding.
* @internal
*/
const managementClientLogger = createServiceBusLogger("service-bus:management");
/**
* Logs the error's stack trace to "verbose" if a stack trace is available.
* @param error - Error containing a stack trace.
* @internal
*/
function logErrorStackTrace(_logger, error) {
if (coreUtil.isObjectWithProperties(error, ["stack"]) && error.stack) {
_logger.verbose(error.stack);
}
}
/**
* Creates an AzureLogger with any additional methods for standardized logging (for example, with errors)
* @internal
*/
function createServiceBusLogger(namespace) {
const _logger = logger$1.createClientLogger(namespace);
_logger["logError"] = (err, ...args) => {
let l;
// abort errors are user initiated so we don't have to treat them as warnings, like we
// would with other errors.
if (isError(err) && err.name === "AbortError") {
l = _logger.info;
}
else {
l = _logger.warning;
}
// tack on the error object so it also gets logged.
args.push(":", err);
// let the normal formatting work and include the error at the end.
l(...args);
// optionally log the stack trace if it's available but this always goes to verbose
if (err && err.stack) {
_logger.verbose(err.stack);
}
};
return _logger;
}
/**
* @internal
*/
function isError(err) {
return err != null && err.name != null;
}
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
/**
* @internal
*/
const packageJsonInfo = {
name: "@azure/service-bus",
version: "7.9.5",
};
/**
* The amount of time in milliseconds that a receiver
* will wait while draining credits before returning.
* @internal
*/
const receiveDrainTimeoutInMs = 200;
/**
* @internal
*/
const max32BitNumber = Math.pow(2, 31) - 1;
/**
* Queue name identifier
* @internal
*/
const QUEUE_NAME = "QueueName";
/**
* Topic name identifier
* @internal
*/
const TOPIC_NAME = "TopicName";
/**
* Subscription name identifier
* @internal
*/
const SUBSCRIPTION_NAME = "SubscriptionName";
/**
* Accessed at field
* @internal
*/
const ACCESSED_AT = "AccessedAt";
/**
* Updated at field
* @internal
*/
const UPDATED_AT = "UpdatedAt";
/**
* Created at field
* @internal
*/
const CREATED_AT = "CreatedAt";
/**
* Authorization rules on the entity
* @internal
*/
const AUTHORIZATION_RULES = "AuthorizationRules";
/**
* Entity Availability Status field
* @internal
*/
const ENTITY_AVAILABILITY_STATUS = "EntityAvailabilityStatus";
/**
* Enable express option
* @internal
*/
const ENABLE_EXPRESS = "EnableExpress";
/**
* The entity's size in bytes.
*
* @internal
*/
const SIZE_IN_BYTES = "SizeInBytes";
/**
* The entity's message count.
*
* @internal
*/
const MESSAGE_COUNT = "MessageCount";
/**
* The topic's subscription count.
*
* @internal
*/
const SUBSCRIPTION_COUNT = "SubscriptionCount";
/**
* The topic / subscription's count details.
*
* @internal
*/
const COUNT_DETAILS = "CountDetails";
/**
* Max idle time before entity is deleted.
* This is specified in ISO-8601 duration format such as "PT1M" for 1 minute, "PT5S" for 5 seconds.
* @internal
*/
const AUTO_DELETE_ON_IDLE = "AutoDeleteOnIdle";
/**
* The status information on response
*
* @internal
*/
const STATUS = "Status";
/**
* The URL of Service Bus entity to forward messages to.
*
* @internal
*/
const FORWARD_TO = "ForwardTo";
/**
* The user meta data information
*
* @internal
*/
const USER_METADATA = "UserMetadata";
/**
* The maximum size in megabytes.
*
* @internal
*/
const MAX_SIZE_IN_MEGABYTES = "MaxSizeInMegabytes";
/**
* The maximum size in kilobytes.
*
* @internal
*/
const MAX_MESSAGE_SIZE_IN_KILOBYTES = "MaxMessageSizeInKilobytes";
/**
* The default message time to live.
* This is specified in ISO-8601 duration format such as "PT1M" for 1 minute, "PT5S" for 5 seconds.
* @internal
*/
const DEFAULT_MESSAGE_TIME_TO_LIVE = "DefaultMessageTimeToLive";
/**
* The lock duration.
* This is specified in ISO-8601 duration format such as "PT1M" for 1 minute, "PT5S" for 5 seconds.
* @internal
*/
const LOCK_DURATION = "LockDuration";
/**
* The indication if session is required or not.
*
* @internal
*/
const REQUIRES_SESSION = "RequiresSession";
/**
* The indication if duplicate detection is required or not.
*
* @internal
*/
const REQUIRES_DUPLICATE_DETECTION = "RequiresDuplicateDetection";
/**
* The indication if dead lettering on message expiration. If it is enabled and a message expires,
* the Service Bus moves the message from the queue into the entity dead-letter sub-queue.
* If disabled, message will be permanently deleted from the main entity.
* Settable only at entity creation time.
*
* @internal
*/
const DEAD_LETTERING_ON_MESSAGE_EXPIRATION = "DeadLetteringOnMessageExpiration";
/**
* The indication if dead lettering on filter evaluation exceptions.
*
* @internal
*/
const DEAD_LETTERING_ON_FILTER_EVALUATION_EXCEPTIONS = "DeadLetteringOnFilterEvaluationExceptions";
/**
* The history time window for duplicate detection.
* This is specified in ISO-8601 duration format such as "PT1M" for 1 minute, "PT5S" for 5 seconds.
* @internal
*/
const DUPLICATE_DETECTION_HISTORY_TIME_WINDOW = "DuplicateDetectionHistoryTimeWindow";
/**
* The maximum delivery count of messages after which if it is still not settled, gets moved to the dead-letter sub-queue.
*
* @internal
*/
const MAX_DELIVERY_COUNT = "MaxDeliveryCount";
/**
* Indicates if the queue has enabled batch operations.
*
* @internal
*/
const ENABLE_BATCHED_OPERATIONS = "EnableBatchedOperations";
/**
* Indicates whether the topic can be ordered
*
* @internal
*/
const SUPPORT_ORDERING = "SupportOrdering";
/**
* Indicates whether the topic/queue should be split across multiple partitions
*
* @internal
*/
const ENABLE_PARTITIONING = "EnablePartitioning";
/**
* The URL of Service Bus entity to forward deadlettered messages to.
*
* @internal
*/
const FORWARD_DEADLETTERED_MESSAGES_TO = "ForwardDeadLetteredMessagesTo";
/**
* Query string parameter to set Service Bus API version
*
* @internal
*/
const API_VERSION_QUERY_KEY = "api-version";
/**
* Current API version being sent to service bus
*
* @internal
*/
const CURRENT_API_VERSION = "2021-05";
/**
* Marker for atom metadata.
*
* @internal
*/
const XML_METADATA_MARKER = "$";
/**
* Marker for atom value.
*
* @internal
*/
const XML_VALUE_MARKER = "_";
/**
* Constant representing the property where the atom default elements are stored.
*
* @internal
*/
const ATOM_METADATA_MARKER = "_";
/**
* Known HTTP status codes as documented and referenced in ATOM based management API feature
* https://docs.microsoft.com/dotnet/api/system.net.httpstatuscode?view=netframework-4.8
* @internal
*/
const HttpResponseCodes = {
100: "Continue",
101: "SwitchingProtocols",
200: "Ok",
201: "Created",
202: "Accepted",
203: "NonAuthoritativeInformation",
204: "NoContent",
205: "ResetContent",
206: "PartialContent",
300: "MultipleChoices",
301: "Moved",
302: "Redirect",
303: "RedirectMethod",
304: "NotModified",
305: "UseProxy",
306: "Unused",
400: "BadRequest",
401: "Unauthorized",
402: "PaymentRequired",
403: "Forbidden",
404: "NotFound",
405: "MethodNotAllowed",
406: "NotAcceptable",
407: "ProxyAuthenticationRequired",
409: "Conflict",
410: "Gone",
411: "LengthRequired",
412: "PreconditionFailed",
413: "RequestEntityTooLarge",
414: "RequestUriTooLong",
415: "UnsupportedMediaType",
416: "RequestRangeNotSatisfiable",
417: "ExpectationFailed",
426: "UpgradeRequired",
500: "InternalServerError",
501: "NotImplemented",
502: "BadGateway",
503: "ServiceUnavailable",
504: "GatewayTimeout",
505: "HttpVersionNotSupported",
};
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
/**
* @internal
*/
/**
* @internal
*/
const parseURL = (rawUrl) => {
return new url.URL(rawUrl);
};
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
/**
* A collection of HttpHeaders that can be sent with a HTTP request.
*/
function getHeaderKey(headerName) {
return headerName.toLowerCase();
}
/**
* A collection of HTTP header key/value pairs.
*/
class HttpHeaders {
constructor(rawHeaders) {
this._headersMap = {};
if (rawHeaders) {
for (const headerName in rawHeaders) {
this.set(headerName, rawHeaders[headerName]);
}
}
}
/**
* Set a header in this collection with the provided name and value. The name is
* case-insensitive.
* @param headerName - The name of the header to set. This value is case-insensitive.
* @param headerValue - The value of the header to set.
*/
set(headerName, headerValue) {
this._headersMap[getHeaderKey(headerName)] = {
name: headerName,
value: headerValue.toString(),
};
}
/**
* Get the header value for the provided header name, or undefined if no header exists in this
* collection with the provided name.
* @param headerName - The name of the header.
*/
get(headerName) {
const header = this._headersMap[getHeaderKey(headerName)];
return !header ? undefined : header.value;
}
/**
* Get whether or not this header collection contains a header entry for the provided header name.
*/
contains(headerName) {
return !!this._headersMap[getHeaderKey(headerName)];
}
/**
* Remove the header with the provided headerName. Return whether or not the header existed and
* was removed.
* @param headerName - The name of the header to remove.
*/
remove(headerName) {
const result = this.contains(headerName);
delete this._headersMap[getHeaderKey(headerName)];
return result;
}
/**
* Get the headers that are contained this collection as an object.
*/
rawHeaders() {
return this.toJson({ preserveCase: true });
}
/**
* Get the headers that are contained in this collection as an array.
*/
headersArray() {
const headers = [];
for (const headerKey in this._headersMap) {
headers.push(this._headersMap[headerKey]);
}
return headers;
}
/**
* Get the header names that are contained in this collection.
*/
headerNames() {
const headerNames = [];
const headers = this.headersArray();
for (let i = 0; i < headers.length; ++i) {
headerNames.push(headers[i].name);
}
return headerNames;
}
/**
* Get the header values that are contained in this collection.
*/
headerValues() {
const headerValues = [];
const headers = this.headersArray();
for (let i = 0; i < headers.length; ++i) {
headerValues.push(headers[i].value);
}
return headerValues;
}
/**
* Get the JSON object representation of this HTTP header collection.
*/
toJson(options = {}) {
const result = {};
if (options.preserveCase) {
for (const headerKey in this._headersMap) {
const header = this._headersMap[headerKey];
result[header.name] = header.value;
}
}
else {
for (const headerKey in this._headersMap) {
const header = this._headersMap[headerKey];
result[getHeaderKey(header.name)] = header.value;
}
}
return result;
}
/**
* Get the string representation of this HTTP header collection.
*/
toString() {
return JSON.stringify(this.toJson({ preserveCase: true }));
}
/**
* Create a deep clone/copy of this HttpHeaders collection.
*/
clone() {
const resultPreservingCasing = {};
for (const headerKey in this._headersMap) {
const header = this._headersMap[headerKey];
resultPreservingCasing[header.name] = header.value;
}
return new HttpHeaders(resultPreservingCasing);
}
}
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
function toHttpHeaderLike(headers) {
return new HttpHeaders(headers.toJSON({ preserveCase: true }));
}
function toWebResourceLike(request) {
return {
url: request.url,
method: request.method,
headers: toHttpHeaderLike(request.headers),
withCredentials: request.withCredentials,
timeout: request.timeout,
requestId: request.headers.get("x-ms-client-request-id") || "",
};
}
/**
* Helper to transform PipelineResponse to slimmed-down HttpResponse used in Service Bus.
*/
function toHttpResponse(response) {
return {
request: toWebResourceLike(response.request),
status: response.status,
headers: toHttpHeaderLike(response.headers),
};
}
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
/**
* Translation between the MessagingErrorCodes into a ServiceBusCode
*
* @internal
*/
const wellKnownMessageCodesToServiceBusCodes = new Map([
["MessagingEntityNotFoundError", "MessagingEntityNotFound"],
["MessageLockLostError", "MessageLockLost"],
["MessageNotFoundError", "MessageNotFound"],
["MessageTooLargeError", "MessageSizeExceeded"],
["MessagingEntityAlreadyExistsError", "MessagingEntityAlreadyExists"],
["MessagingEntityDisabledError", "MessagingEntityDisabled"],
["QuotaExceededError", "QuotaExceeded"],
["ServerBusyError", "ServiceBusy"],
["OperationTimeoutError", "ServiceTimeout"],
["ServiceUnavailableError", "ServiceTimeout"],
["ServiceCommunicationError", "ServiceCommunicationProblem"],
["SessionCannotBeLockedError", "SessionCannotBeLocked"],
["SessionLockLostError", "SessionLockLost"],
["UnauthorizedError", "UnauthorizedAccess"],
]);
/**
* Errors that occur within Service Bus.
*/
class ServiceBusError extends coreAmqp.MessagingError {
constructor(messageOrError, code) {
const message = typeof messageOrError === "string" ? messageOrError : messageOrError.message;
super(message);
if (typeof messageOrError === "string") {
this.code = code ?? "GeneralError";
}
else {
for (const prop in messageOrError) {
this[prop] = messageOrError[prop];
}
this.code = ServiceBusError.normalizeMessagingCode(messageOrError.code);
// For GeneralErrors, prefix the error message with the MessagingError code to provide
// more context to the user.
if (this.code === "GeneralError" && messageOrError.code) {
this.message = `${messageOrError.code}: ${this.message}`;
}
}
this.name = "ServiceBusError";
}
static normalizeMessagingCode(oldCode) {
if (oldCode == null || !wellKnownMessageCodesToServiceBusCodes.has(oldCode)) {
return "GeneralError";
}
return wellKnownMessageCodesToServiceBusCodes.get(oldCode);
}
}
/**
* Translates an error into either an Error or a ServiceBusError which provides a `reason` code that
* can be used by clients to programmatically react to errors.
*
* If you are calling `@azure/core-amqp/translate` you should swap to using this function instead since it provides
* Service Bus specific handling of the error (falling back to default translate behavior otherwise).
*
* @internal
*/
function translateServiceBusError(err) {
if (isServiceBusError(err)) {
return err;
}
const translatedError = coreAmqp.translate(err);
if (coreAmqp.isMessagingError(translatedError)) {
return new ServiceBusError(translatedError);
}
return translatedError;
}
/**
* Determines if an error is of type `ServiceBusError`
*
* @param err - An error to check to see if it's of type ServiceBusError
*/
function isServiceBusError(err) {
return coreUtil.isObjectWithProperties(err, ["name"]) && err.name === "ServiceBusError";
}
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
/**
* @internal
* Provides a uniue name by appending a string guid to the given string in the following format:
* `{name}-{uuid}`.
* @param name - The nme of the entity
*/
function getUniqueName(name) {
return `${name}-${rheaPromise.generate_uuid()}`;
}
/**
* @internal
* Returns the passed identifier if it is not undefined or empty;
* otherwise generate and returns a unique one in the following format;
* `{prefix}-{uuid}`.
* @param prefix - The prefix used to generate identifier
* @param identifier - an identifier name
*/
function ensureValidIdentifier(prefix, identifier) {
return identifier ? identifier : getUniqueName(prefix);
}
/**
* @internal
* If you try to turn a Guid into a Buffer in .NET, the bytes of the first three groups get
* flipped within the group, but the last two groups don't get flipped, so we end up with a
* different byte order. This is the order of bytes needed to make Service Bus recognize the token.
*
* @param lockToken - The lock token whose bytes need to be reorded.
* @returns Buffer representing reordered bytes.
*/
function reorderLockToken(lockTokenBytes) {
if (!lockTokenBytes || !buffer.Buffer.isBuffer(lockTokenBytes)) {
return lockTokenBytes;
}
return buffer.Buffer.from([
lockTokenBytes[3],
lockTokenBytes[2],
lockTokenBytes[1],
lockTokenBytes[0],
lockTokenBytes[5],
lockTokenBytes[4],
lockTokenBytes[7],
lockTokenBytes[6],
lockTokenBytes[8],
lockTokenBytes[9],
lockTokenBytes[10],
lockTokenBytes[11],
lockTokenBytes[12],
lockTokenBytes[13],
lockTokenBytes[14],
lockTokenBytes[15],
]);
}
/**
* @internal
* Provides the time in milliseconds after which the lock renewal should occur.
* @param lockedUntilUtc - The time until which the message is locked.
*/
function calculateRenewAfterDuration(lockedUntilUtc) {
const now = Date.now();
const lockedUntil = lockedUntilUtc.getTime();
const remainingTime = lockedUntil - now;
receiverLogger.verbose("Locked until utc : %d", lockedUntil);
receiverLogger.verbose("Current time is : %d", now);
receiverLogger.verbose("Remaining time is : %d", remainingTime);
if (remainingTime < 1000) {
return 0;
}
const buffer = Math.min(remainingTime / 2, 10000); // 10 seconds
const renewAfter = remainingTime - buffer;
receiverLogger.verbose("Renew after : %d", renewAfter);
return renewAfter;
}
/**
* @internal
* Converts the .net ticks to a JS Date object.
*
* - The epoch for the DateTimeOffset type is `0000-01-01`, while the epoch for JS Dates is
* `1970-01-01`.
* - The DateTimeOffset ticks value for the date `1970-01-01` is `621355968000000000`.
* - Hence, to convert it to the JS epoch; we `subtract` the delta from the given value.
* - Ticks in DateTimeOffset is `1/10000000` second, while ticks in JS Date is `1/1000` second.
* - Thus, we `divide` the value by `10000` to convert it to JS Date ticks.
*
* @param buf - Input as a Buffer
* @returns The JS Date object.
*/
function convertTicksToDate(buf) {
const epochMicroDiff = 621355968000000000;
const longValue = Long.fromBytesBE(buf);
const timeInMS = longValue.sub(epochMicroDiff).div(10000).toNumber();
const result = new Date(timeInMS);
logger.verbose("The converted date is: %s", result.toString());
return result;
}
/**
* @internal
* Converts any given input to a Buffer.
* @param input - The input that needs to be converted to a Buffer.
*/
function toBuffer(input) {
let result;
messageLogger.verbose("[utils.toBuffer] The given message body that needs to be converted to buffer is: ", input);
if (isBuffer(input)) {
result = input;
}
else {
// string, undefined, null, boolean, array, object, number should end up here
// coercing undefined to null as that will ensure that null value will be given to the
// customer on receive.
if (input === undefined)
input = null;
try {
const inputStr = JSON.stringify(input);
result = buffer.Buffer.from(inputStr, "utf8");
}
catch (err) {
const msg = `An error occurred while executing JSON.stringify() on the given input ` +
input +
`${err instanceof Error ? err.stack : JSON.stringify(err)}`;
messageLogger.warning("[utils.toBuffer] " + msg);
throw err instanceof Error ? err : new Error(msg);
}
}
messageLogger.verbose("[utils.toBuffer] The converted buffer is: %O.", result);
return result;
}
/**
* @internal
* Helper utility to retrieve `string` value from given string,
* or throws error if undefined.
*/
function getString(value, nameOfProperty) {
const result = getStringOrUndefined(value);
if (result === undefined) {
throw new Error(`"${nameOfProperty}" received from service expected to be a string value and not undefined.`);
}
return result;
}
/**
* @internal
* Helper utility to retrieve `string` value from given input,
* or undefined if not passed in.
*/
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
function getStringOrUndefined(value) {
if (!coreUtil.isDefined(value)) {
return undefined;
}
return value.toString();
}
/**
* @internal
* Helper utility to retrieve `integer` value from given string,
* or throws error if undefined.
*/
function getInteger(value, nameOfProperty) {
const result = getIntegerOrUndefined(value);
if (result === undefined) {
throw new Error(`"${nameOfProperty}" received from service expected to be a number value and not undefined.`);
}
return result;
}
/**
* @internal
* Helper utility to retrieve `integer` value from given string,
* or undefined if not passed in.
*/
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
function getIntegerOrUndefined(value) {
if (!coreUtil.isDefined(value)) {
return undefined;
}
const result = parseInt(value.toString());
return isNaN(result) ? undefined : result;
}
/**
* @internal
* Helper utility to convert ISO-8601 time into Date type.
*/
function getDate(value, nameOfProperty) {
return new Date(getString(value, nameOfProperty));
}
/**
* @internal
* Helper utility to retrieve `boolean` value from given string,
* or throws error if undefined.
*/
function getBoolean(value, nameOfProperty) {
const result = getBooleanOrUndefined(value);
if (result === undefined) {
throw new Error(`"${nameOfProperty}" received from service expected to be a boolean value and not undefined.`);
}
return result;
}
/**
* @internal
* Helper utility to retrieve `boolean` value from given string,
* or undefined if not passed in.
*/
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
function getBooleanOrUndefined(value) {
if (!coreUtil.isDefined(value)) {
return undefined;
}
return value.toString().trim().toLowerCase() === "true";
}
/**
* @internal
* Helps in differentiating JSON like objects from other kinds of objects.
*/
const EMPTY_JSON_OBJECT_CONSTRUCTOR = {}.constructor;
/**
* @internal
* Returns `true` if given input is a JSON like object.
*/
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
function isJSONLikeObject(value) {
// `value.constructor === {}.constructor` differentiates among the "object"s,
// would filter the JSON objects and won't match any array or other kinds of objects
// -------------------------------------------------------------------------------
// Few examples | typeof obj ==="object" | obj.constructor==={}.constructor
// -------------------------------------------------------------------------------
// {abc:1} | true | true
// ["a","b"] | true | false
// [{"a":1},{"b":2}] | true | false
// new Date() | true | false
// 123 | false | false
// -------------------------------------------------------------------------------
return typeof value === "object" && value.constructor === EMPTY_JSON_OBJECT_CONSTRUCTOR;
}
/**
* @internal
* Helper utility to retrieve message count details from given input,
*/
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
function getMessageCountDetails(value) {
const xmlnsPrefix = getXMLNSPrefix(value);
if (!coreUtil.isDefined(value)) {
value = {};
}
return {
activeMessageCount: parseInt(value[`${xmlnsPrefix}:ActiveMessageCount`]) || 0,
deadLetterMessageCount: parseInt(value[`${xmlnsPrefix}:DeadLetterMessageCount`]) || 0,
scheduledMessageCount: parseInt(value[`${xmlnsPrefix}:ScheduledMessageCount`]) || 0,
transferMessageCount: parseInt(value[`${xmlnsPrefix}:TransferMessageCount`]) || 0,
transferDeadLetterMessageCount: parseInt(value[`${xmlnsPrefix}:TransferDeadLetterMessageCount`]) || 0,
};
}
/**
* @internal
* Gets the xmlns prefix from the root of the objects that are part of the parsed response body.
*/
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
function getXMLNSPrefix(value) {
if (!value[XML_METADATA_MARKER]) {
throw new Error(`Error occurred while parsing the response body - cannot find the XML_METADATA_MARKER "$" on the object ${JSON.stringify(value)}`);
}
const keys = Object.keys(value[XML_METADATA_MARKER]);
if (keys.length !== 1) {
throw new Error(`Error occurred while parsing the response body - unexpected number of "xmlns:\${prefix}" keys at ${JSON.stringify(value[XML_METADATA_MARKER])}`);
}
if (!keys[0].startsWith("xmlns:")) {
throw new Error(`Error occurred while parsing the response body - unexpected key at ${JSON.stringify(value[XML_METADATA_MARKER])}`);
}
// Pick the substring that's after "xmlns:"
const xmlnsPrefix = keys[0].substring(6);
if (!xmlnsPrefix) {
throw new Error(`Error occurred while parsing the response body - unexpected xmlns prefix at ${JSON.stringify(value[XML_METADATA_MARKER])}`);
}
return xmlnsPrefix;
}
/**
* @internal
* Helper utility to retrieve array of `AuthorizationRule` from given input,
* or undefined if not passed in.
*/
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
function getAuthorizationRulesOrUndefined(value) {
const authorizationRules = [];
// Ignore special case as Service Bus treats "" as a valid value for authorization rules
if (typeof value === "string" && value.trim() === "") {
return undefined;
}
if (!coreUtil.isDefined(value)) {
return undefined;
}
const rawAuthorizationRules = value.AuthorizationRule;
if (Array.isArray(rawAuthorizationRules)) {
for (let i = 0; i < rawAuthorizationRules.length; i++) {
authorizationRules.push(buildAuthorizationRule(rawAuthorizationRules[i]));
}
}
else {
authorizationRules.push(buildAuthorizationRule(rawAuthorizationRules));
}
return authorizationRules;
}
/**
* @internal
* Helper utility to build an instance of parsed authorization rule as `AuthorizationRule` from given input.
*/
function buildAuthorizationRule(value) {
let accessRights;
if (coreUtil.isDefined(value["Rights"])) {
accessRights = value["Rights"]["AccessRights"];
}
const authorizationRule = {
claimType: value["ClaimType"],
accessRights,
keyName: value["KeyName"],
primaryKey: value["PrimaryKey"],
secondaryKey: value["SecondaryKey"],
};
if (authorizationRule.accessRights && !Array.isArray(authorizationRule.accessRights)) {
authorizationRule.accessRights = [authorizationRule.accessRights];
}
return authorizationRule;
}
/**
* @internal
* Helper utility to extract output containing array of `RawAuthorizationRule` instances from given input,
* or undefined if not passed in.
*/
function getRawAuthorizationRules(authorizationRules) {
if (!coreUtil.isDefined(authorizationRules)) {
return undefined;
}
if (!Array.isArray(authorizationRules)) {
throw new TypeError(`authorizationRules must be an array of AuthorizationRule objects or undefined, but received ${JSON.stringify(authorizationRules, undefined, 2)}`);
}
const rawAuthorizationRules = [];
for (let i = 0; i < authorizationRules.length; i++) {
rawAuthorizationRules.push(buildRawAuthorizationRule(authorizationRules[i]));
}
return { AuthorizationRule: rawAuthorizationRules };
}
/**
* @internal
* Helper utility to build an instance of raw authorization rule as RawAuthorizationRule from given `AuthorizationRule` input.
* @param authorizationRule - parsed Authorization Rule instance
*/
function buildRawAuthorizationRule(authorizationRule) {
if (!isJSONLikeObject(authorizationRule) || authorizationRule === null) {
throw new TypeError(`Expected authorizationRule input to be a JS object value, but received ${JSON.stringify(authorizationRule, undefined, 2)}`);
}
const rawAuthorizationRule = {
ClaimType: authorizationRule.claimType,
// ClaimValue is not settable by the users, but service expects the value for PUT requests
ClaimValue: "None",
Rights: {
AccessRights: authorizationRule.accessRights,
},
KeyName: authorizationRule.keyName,
PrimaryKey: authorizationRule.primaryKey,
SecondaryKey: authorizationRule.secondaryKey,
};
rawAuthorizationRule[XML_METADATA_MARKER] = {
"p5:type": "SharedAccessAuthorizationRule",
"xmlns:p5": "http://www.w3.org/2001/XMLSchema-instance",
};
return rawAuthorizationRule;
}
/**
* @internal
* Helper utility to check if given string is an absolute URL
*/
function isAbsoluteUrl(url) {
const _url = url.toLowerCase();
return _url.startsWith("sb://") || _url.startsWith("http://") || _url.startsWith("https://");
}
/**
* An executor for a function that returns a Promise that obeys both a timeout and an
* optional AbortSignal.
* @param timeoutMs - The number of milliseconds to allow before throwing an OperationTimeoutError.
* @param timeoutMessage - The message to place in the .description field for the thrown exception for Timeout.
* @param abortSignal - The abortSignal associated with containing operation.
* @param abortErrorMsg - The abort error message associated with containing operation.
* @param value - The value to be resolved with after a timeout of t milliseconds.
*
* @internal
*/
async function waitForTimeoutOrAbortOrResolve(args) {
if (args.abortSignal && args.abortSignal.aborted) {
throw new abortController.AbortError(coreAmqp.StandardAbortMessage);
}
let timer = undefined;
let clearAbortSignal = undefined;
const clearAbortSignalAndTimer = () => {
(args.timeoutFunctions?.clearTimeoutFn ?? clearTimeout)(timer);
if (clearAbortSignal) {
clearAbortSignal();
}
};
// eslint-disable-next-line promise/param-names
const abortOrTimeoutPromise = new Promise((_resolve, reject) => {
clearAbortSignal = checkAndRegisterWithAbortSignal(reject, args.abortSignal);
timer = (args.timeoutFunctions?.setTimeoutFn ?? setTimeout)(() => {
reject(new rheaPromise.OperationTimeoutError(args.timeoutMessage));
}, args.timeoutMs);
});
try {
return await Promise.race([abortOrTimeoutPromise, args.actionFn()]);
}
finally {
clearAbortSignalAndTimer();
}
}
/**
* Registers listener to the abort event on the abortSignal to call your abortFn and
* returns a function that will clear the same listener.
*
* If abort signal is already aborted, then throws an AbortError and returns a function that does nothing
*
* @returns A function that removes any of our attached event listeners on the abort signal or an empty function if
* the abortSignal was not defined.
*
* @internal
*/
function checkAndRegisterWithAbortSignal(onAbortFn, abortSignal) {
if (abortSignal == null) {
return () => {
/** Nothing to do here, no abort signal */
};
}
if (abortSignal.aborted) {
throw new abortController.AbortError(coreAmqp.StandardAbortMessage);
}
const onAbort = () => {
abortSignal.removeEventListener("abort", onAbort);
onAbortFn(new abortController.AbortError(coreAmqp.StandardAbortMessage));
};
abortSignal.addEventListener("abort", onAbort);
return () => abortSignal.removeEventListener("abort", onAbort);
}
/**
* @internal
* The user agent prefix string for the ServiceBus client.
* See guideline at https://azure.github.io/azure-sdk/general_azurecore.html#telemetry-policy
*/
const libInfo = `azsdk-js-azureservicebus/${packageJsonInfo.version}`;
/**
* @internal
* Returns the formatted prefix by removing the spaces, by appending the libInfo.
*/
function formatUserAgentPrefix(prefix) {
let userAgentPrefix = `${(prefix || "").replace(" ", "")}`;
userAgentPrefix = userAgentPrefix.length > 0 ? userAgentPrefix + " " : "";
return `${userAgentPrefix}${libInfo}`;
}
/**
* @internal
* Helper method which returns `HttpResponse` from an object of shape `PipelineResponse`.
* TODO: remove this and use toHttpResponse() directly
*/
const getHttpResponseOnly = (pipelineResponse) => toHttpResponse(pipelineResponse);
/**
* @internal
* Waits for one second if a sender is not sendable then check again. Throws
* SenderBusyError if it is still not sendable.
* Only waits when operation timeout is greater than one second.
* @returns the actual waiting time.
*/
async function waitForSendable(sendLogger, logPrefix, name, timeout, sender, outgoingAvaiable) {
let waitTimeForSendable = 1000;
if (!sender?.sendable() && timeout > waitTimeForSendable) {
sendLogger.verbose("%s Sender '%s', waiting for 1 second for sender to become sendable", logPrefix, name);
await coreAmqp.delay(waitTimeForSendable);
sendLogger.verbose("%s Sender '%s' after waiting for a second, credit: %d available: %d", logPrefix, name, sender?.credit, outgoingAvaiable);
}
else {
waitTimeForSendable = 0;
}
if (!sender?.sendable()) {
// let us retry to send the message after some time.
const msg = `[${logPrefix}] Sender "${name}", ` + `cannot send the message right now. Please try later.`;
sendLogger.warning(msg);
const amqpError = {
condition: coreAmqp.ErrorNameConditionMapper.SenderBusyError,
description: msg,
};
throw translateServiceBusError(amqpError);
}
return waitTimeForSendable;
}
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
/**
applies options to the pipeline request.
*/
function applyRequestOptions(request, options) {
if (options.headers) {
const headers = options.headers;
for (const headerName of Object.keys(headers)) {
request.headers.set(headerName, headers[headerName]);
}
}
request.onDownloadProgress = options.onDownloadProgress;
request.onUploadProgress = options.onUploadProgress;
request.abortSignal = options.abortSignal;
request.timeout = options.timeout;
if (options.tracingOptions) {
request.tracingOptions = options.tracingOptions;
}
}
/**
* @internal
* Utility to execute Atom XML operations as HTTP requests
*/
async function executeAtomXmlOperation(serviceBusAtomManagementClient, request, serializer, operationOptions, requestObject) {
if (requestObject) {
request.body = coreXml.stringifyXML(serializer.serialize(requestObject), { rootName: "entry" });
if (request.method === "PUT") {
request.headers.set("content-length", buffer.Buffer.byteLength(request.body));
}
}
administrationLogger.verbose(`Executing ATOM based HTTP request: ${request.body}`);
const reqPrepareOptions = {
headers: operationOptions.requestOptions?.customHeaders,
onUploadProgress: operationOptions.requestOptions?.onUploadProgress,
onDownloadProgress: operationOptions.requestOptions?.onDownloadProgress,
abortSignal: operationOptions.abortSignal,
tracingOptions: operationOptions.tracingOptions,
disableJsonStringifyOnBody: true,
timeout: operationOptions.requestOptions?.timeout || 0,
};
applyRequestOptions(request, reqPrepareOptions);
const response = await serviceBusAtomManagementClient.sendRequest(request);
administrationLogger.verbose(`Received ATOM based HTTP response: ${response.bodyAsText}`);
try {
if (response.bodyAsText) {
response.parsedBody = await coreXml.parseXML(response.bodyAsText, {
includeRoot: true,
});
}
}
catch (err) {
const error = new coreRestPipeline.RestError(`Error occurred while parsing the response body - expected the service to return valid xml content.`, {
code: coreRestPipeline.RestError.PARSE_ERROR,
statusCode: response.status,
request: response.request,
response,
});
administrationLogger.logError(err, "Error parsing response body from Service");
throw error;
}
return serializer.deserialize(response);
}
/**
* @internal
* The key-value pairs having undefined/null as the values would lead to the empty tags in the serialized XML request.
* Empty tags in the request body is problematic because of the following reasons.
* - ATOM based management operations throw a "Bad Request" error if empty tags are included in the XML request body at top level.
* - At the inner levels, Service assigns the empty strings as values to the empty tags instead of throwing an error.
*
* This method recursively removes the key-value pairs with undefined/null as the values from the request object that is to be serialized.
*
*/
function sanitizeSerializableObject(resource) {
Object.keys(resource).forEach(function (property) {
if (!coreUtil.isDefined(resource[property])) {
delete resource[property];
}
else if (isJSONLikeObject(resource[property])) {
sanitizeSerializableObject(resource[property]);
}
});
}
/**
* @internal
* Serializes input information to construct the Atom XML request
* @param resourceName - Name of the resource to be serialized like `QueueDescription`
* @param resource - The entity details
* @param allowedProperties - The set of properties that are allowed by the service for the
* associated operation(s);
*/
function serializeToAtomXmlRequest(resourceName, resource) {
const content = {};
content[resourceName] = Object.assign({}, resource);
sanitizeSerializableObject(content[resourceName]);
content[resourceName][XML_METADATA_MARKER] = {
xmlns: "http://schemas.microsoft.com/netservices/2010/10/servicebus/connect",
"xmlns:i": "http://www.w3.org/2001/XMLSchema-instance",
};
content[XML_METADATA_MARKER] = { type: "application/xml" };
const requestDetails = {
updated: new Date().toISOString(),
content: content,
};
requestDetails[XML_METADATA_MARKER] = {
xmlns: "http://www.w3.org/2005/Atom",
};
return requestDetails;
}
/**
* @internal
* Transforms response to contain the parsed data.
* @param nameProperties - The set of 'name' properties to be constructed on the
* resultant object e.g., QueueName, TopicName, etc.
*/
async function deserializeAtomXmlResponse(nameProperties, response) {
// If received data is a non-valid HTTP response, the body is expected to contain error information
if (response.status < 200 || response.status >= 300) {
throw buildError(response);
}
parseAtomResult(response, nameProperties);
return response;
}
/**
* @internal
* Utility to deserialize the given JSON content in response body based on
* if it's a single `entry` or `feed` and updates the `response.parsedBody` to hold the evaluated output.
* @param response - Response containing the JSON value in `response.parsedBody`
* @param nameProperties - The set of 'name' properties to be constructed on the
* resultant object e.g., QueueName, TopicName, etc.
* */
function parseAtomResult(response, nameProperties) {
const atomResponseInJson = response.parsedBody;
let result;
if (!atomResponseInJson) {
response.parsedBody = undefined;
return;
}
if (atomResponseInJson.feed) {
result = parseFeedResult(atomResponseInJson.feed);
}
else if (atomResponseInJson.entry) {
result = parseEntryResult(atomResponseInJson.entry);
}
if (result) {
if (Array.isArray(result)) {
result.forEach((entry) => {
setName(entry, nameProperties);
});
}
else {
setName(result, nameProperties);
}
response.parsedBody = result;
return;
}
administrationLogger.warning("Failure in parsing response body from service. Expected response to be in Atom XML format and have either feed or entry components, but received - %0", atomResponseInJson);
throw new coreRestPipeline.RestError("Error occurred while parsing the response body - expected the service to return atom xml content with either feed or entry elements.", {
code: coreRestPipeline.RestError.PARSE_ERROR,
statusCode: response.status,
request: response.request,
response,
});
}
/**
* @internal
* Utility to help parse given `entry` result
*/
function parseEntryResult(entry) {
let result;
if (typeof entry !== "object" ||
entry == null ||
typeof entry.content !== "object" ||
entry.content == null) {
return undefined;
}
const contentElementNames = Object.keys(entry.content).filter(function (key) {
return key !== XML_METADATA_MARKER;
});
if (contentElementNames && contentElementNames[0]) {
const contentRootElementName = contentElementNames[0];
delete entry.content[contentRootElementName][XML_METADATA_MARKER];
result = entry.content[contentRootElementName];
if (result) {
if (entry[XML_METADATA_MARKER]) {
result[ATOM_METADATA_MARKER] = entry[XML_METADATA_MARKER];
}
else {
result[ATOM_METADATA_MARKER] = {};
}
result[ATOM_METADATA_MARKER]["ContentRootElement"] = contentRootElementName;
Object.keys(entry).forEach((property) => {
if (property !== "content" && property !== XML_METADATA_MARKER) {
result[ATOM_METADATA_MARKER][property] = entry[property];
}
});
return result;
}
}
return undefined;
}
/**
* @internal
* Utility to help parse link info from the given `feed` result
*/
function parseLinkInfo(feedLink, relationship) {
if (!feedLink || !Array.isArray(feedLink)) {
return undefined;
}
for (const linkInfo of feedLink) {
if (linkInfo[XML_METADATA_MARKER].rel === relationship) {
return linkInfo[XML_METADATA_MARKER].href;
}
}
return undefined;
}
/**
* @internal
* Utility to help parse given `feed` result
*/
function parseFeedResult(feed) {
const result = [];
if (typeof feed === "object" && feed != null && feed.entry) {
if (Array.isArray(feed.entry)) {
feed.entry.forEach((entry) => {
const parsedEntryResult = parseEntryResult(entry);
if (parsedEntryResult) {
result.push(parsedEntryResult);
}
});
}
else {
const parsedEntryResult = parseEntryResult(feed.entry);
if (parsedEntryResult) {
result.push(parsedEntryResult);
}
}
result.nextLink = parseLinkInfo(feed.link, "next");
}
return result;
}
/**
* @internal
*/
function isKnownResponseCode(statusCode) {
return !!HttpResponseCodes[statusCode];
}
/**
* @internal
* Extracts the applicable entity name(s) from the URL based on the known structure
* and instantiates the corresponding name properties to the deserialized response
*
* The pattern matching checks to extract entity names are based on following
* constraints dictated by the service
* - '/' is allowed in Queue and Topic names
* - '/' is not allowed in Namespace, Subscription and Rule names
* - Valid pathname URL structures used in the ATOM based management API are
* - `<namespace-component>/<topic-name>/Subscriptions/<subscription-name>/Rules/<rule-name>`
* - `<namespace-component>/<topic-name>/Subscriptions/<subscription-name>`
* - `<namespace-component>/<any-entity-name>`
*
*/
function setName(entry, nameProperties) {
if (entry[ATOM_METADATA_MARKER]) {
let rawUrl = entry[ATOM_METADATA_MARKER].id;
// The parsedUrl gets constructed differently for browser vs Node.
// It is specifically behaves different for some of the Atom based management API where
// the received URL in "id" element is of type "sb:// ... " and not a standard HTTP one
// Hence, normalizing the URL for parsing to work as expected in browser
if (rawUrl.startsWith("sb://")) {
rawUrl = "https://" + rawUrl.substring(5);
}
const parsedUrl = parseURL(rawUrl);
const pathname = parsedUrl.pathname;
const firstIndexOfDelimiter = pathname.indexOf("/");
if (pathname.match("(.*)/(.*)/Subscriptions/(.*)/Rules/(.*)")) {
const lastIndexOfSubscriptionsDelimiter = pathname.lastIndexOf("/Subscriptions/");
const firstIndexOfRulesDelimiter = pathname.indexOf("/Rules/");
entry[nameProperties[0]] = pathname.substring(firstIndexOfDelimiter + 1, lastIndexOfSubscriptionsDelimiter);
entry[nameProperties[1]] = pathname.substring(lastIndexOfSubscriptionsDelimiter + 15, firstIndexOfRulesDelimiter);
entry[nameProperties[2]] = pathname.substring(firstIndexOfRulesDelimiter + 7);
}
else if (pathname.match("(.*)/(.*)/Subscriptions/(.*)")) {
const lastIndexOfSubscriptionsDelimiter = pathname.lastIndexOf("/Subscriptions/");
entry[nameProperties[0]] = pathname.substring(firstIndexOfDelimiter + 1, lastIndexOfSubscriptionsDelimiter);
entry[nameProperties[1]] = pathname.substring(lastIndexOfSubscriptionsDelimiter + 15);
}
else if (pathname.match("(.*)/(