diff --git a/.travis.yml b/.travis.yml index 8d14912..10f6834 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,4 +2,16 @@ language: node_js sudo: false node_js: - "8" - - "6" \ No newline at end of file + - "6" + + before_install: + - cd client + - npm install + - cd ../processor + - npm install + + script: + - cd client + - npm test + - cd ../processor + - npm test \ No newline at end of file diff --git a/README.md b/README.md index d814aff..5dd05f4 100644 --- a/README.md +++ b/README.md @@ -1,178 +1,5 @@ -azure-event-hubs -================ +# Azure Event Hubs for node.js -_This SDK is currently in preview._ - -- **Node.js version: 6.x or higher.** We would encourage you to install the latest available LTS version from https://nodejs.org. - -## Installation ## -```bash -npm install azure-event-hubs -``` - -### Client creation -The simplest usage is to use the static factory method `EventHubClient.createFromConnectionString(_connection-string_, _event-hub-path_)`. Once you have a client, you can use it for: - -### Sending events -- You can send a single event using `client.send()` method. -- You can even batch multiple events together using `client.sendBatch()` method. - -### Receiving events -- You can use `await client.receiveBatch(...)` to receive desired number of events for specified amount of time. **Note this is a blocking call**. -That is it will return an array of EventData objects once it receives the desired number of events or the max wait time occurs (which ever happens first). -This is very useful when you want to know how the received events look like or for testing/debugging purposes. -- For production we would expect customers would simply want to receive events and process them. Hence we have a `client.receive(. . .)` method on the receiver. -This message takes the `messageHandler()` and the `errorHandler()` amongst other parameters and registers them to the receiver. -This method returns a `ReceiverHandler` that can be used to stop receiving further events `await receiverHandler.stop()` - -## IDE ## -This sdk has been developed in [TypeScript](https://typescriptlang.org) and has good source code documentation. It is highly recommended to use [vscode](https://code.visualstudio.com) or any other IDE that provides better intellisense and exposes the full power of source code documentation. - -## Debug logs ## - -You can set the following environment variable to get the debug logs. -- Getting the debug logs from the Event Hub SDK -``` -export DEBUG=azure* -``` -- Getting the debug logs from the Event Hub SDK and the protocol level library. -``` -export DEBUG=azure*,rhea* -``` - -## Examples ## - -Please take a look at the [examples](https://github.com/Azure/azure-event-hubs-node/tree/master/examples) directory for detailed examples. - -## Example 1 - Get the partition IDs. - -```js -const { EventHubClient } = require('azure-event-hubs'); - -const client = EventHubClient.createFromConnectionString(process.env["EVENTHUB_CONNECTION_STRING"], process.env["EVENTHUB_NAME"]); - -async function main() { - const partitionIds = await client.getPartitionIds(); -} - -main().catch((err) => { - console.log(err); -}); -``` - -## Example 2.1 - Receive events with handlers -This mechanism is useful for receiving events for a longer duration. - -Receive events from partition ID 1 after the current time. -```js -const { EventHubClient, EventPosition } = require('azure-event-hubs'); - -const client = EventHubClient.createFromConnectionString(process.env["EVENTHUB_CONNECTION_STRING"], process.env["EVENTHUB_NAME"]); - -async function main() { - const onError = (err) => { - console.log("An error occurred on the receiver ", err); - }; - - const onMessage = (eventData) => { - console.log(eventData.body); - const enqueuedTime = eventData.annotations["x-opt-enqueued-time"]; - console.log("Enqueued Time: ", enqueuedTime); - }; - - const receiveHandler = client.receive("1", onMessage, onError, { eventPosition: EventPosition.fromEnqueuedTime(Date.now()) }); - - // To stop receiving events later on... - await receiveHandler.stop(); -} - -main().catch((err) => { - console.log(err); -}); -``` - -## Example 2.2 - Receive specified number of events for a given time -This mechanism is useful when you want to see how the received events look like. It can also be useful for debugging purpose. - -Receive events from partitionId `"1"` after the current time. -```js -const { EventHubClient, EventPosition } = require('azure-event-hubs'); - -const client = EventHubClient.createFromConnectionString(process.env["EVENTHUB_CONNECTION_STRING"], process.env["EVENTHUB_NAME"]); - -async function main() { - const datas = await client.receiveBatch("1", 100 /*number of events*/, 20 /*amount of time in seconds the receiver should run. Default 60 seconds.*/, { eventPosition: EventPosition.fromEnqueuedTime(Date.now()) }); - console.log("Array of EventData objects", datas); -} - -main().catch((err) => { - console.log(err); -}); -``` - -## Example 3 - Send an event with partition key. - -Send an event with a given "partition-key" which is then hashed to a partition ID (so all events with the same key will go to the same ID, but load is balanced between partitions). - -```js -const { EventHubClient, EventPosition } = require('azure-event-hubs'); - -const client = EventHubClient.createFromConnectionString(process.env["EVENTHUB_CONNECTION_STRING"], process.env["EVENTHUB_NAME"]); - -async function main() { - const eventData: EventData = { body: "Hello World", partitionKey: "pk12345"}; - const delivery = await client.send(eventData); - console.log("message sent successfully."); -} - -main().catch((err) => { - console.log(err); -}); -``` - -## Example 4 - Send an event to a specific partition id. - -Send an event to a specific partition ID if needed. If not specified then EventHub will store the events in the partition in a round-robin pattern. - -```js -const { EventHubClient, EventPosition } = require('azure-event-hubs'); - -const client = EventHubClient.createFromConnectionString(process.env["EVENTHUB_CONNECTION_STRING"], process.env["EVENTHUB_NAME"]); - -async function main() { - const data: EventData = { body: "Hello World 1", message_id: "343-0909-5454-23423-54543" }; - const delivery = await client.send(data, "1"); - console.log("message sent successfully."); -} - -main().catch((err) => { - console.log(err); -}); -``` - -## Example 5 - Send multiple events as a batch. - -Send multiple events grouped together. - -```js -const { EventHubClient, EventPosition } = require('azure-event-hubs'); - -const client = EventHubClient.createFromConnectionString(process.env["EVENTHUB_CONNECTION_STRING"], process.env["EVENTHUB_NAME"]); - -async function main() { - const datas = [ - { body: "Hello World 1", applicationProperties: { id: "Some id" }, partitionKey: "pk786" }, - { body: "Hello World 2" }, - { body: "Hello World 3" } - ]; - const delivery = await client.sendBatch(datas); - console.log("message sent successfully."); -} - -main().catch((err) => { - console.log(err); -}); -``` - -## AMQP Dependencies ## -It depends on [rhea](https://github.com/amqp/rhea) library for managing connections, sending and receiving events over the [AMQP](http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-complete-v1.0-os.pdf) protocol. +We have two client libraries for Azure Event Hubs +- `azure-event-hubs` - The Event Hubs client for sending and receiving messages. You can find more info over [here](./client). +- `azure-event-processor-host` - The Event Processor Host for efficient receiving of messages. You can find more info over [here](./processor). \ No newline at end of file diff --git a/.npmignore b/client/.npmignore similarity index 100% rename from .npmignore rename to client/.npmignore diff --git a/client/README.md b/client/README.md new file mode 100644 index 0000000..d814aff --- /dev/null +++ b/client/README.md @@ -0,0 +1,178 @@ +azure-event-hubs +================ + +_This SDK is currently in preview._ + +- **Node.js version: 6.x or higher.** We would encourage you to install the latest available LTS version from https://nodejs.org. + +## Installation ## +```bash +npm install azure-event-hubs +``` + +### Client creation +The simplest usage is to use the static factory method `EventHubClient.createFromConnectionString(_connection-string_, _event-hub-path_)`. Once you have a client, you can use it for: + +### Sending events +- You can send a single event using `client.send()` method. +- You can even batch multiple events together using `client.sendBatch()` method. + +### Receiving events +- You can use `await client.receiveBatch(...)` to receive desired number of events for specified amount of time. **Note this is a blocking call**. +That is it will return an array of EventData objects once it receives the desired number of events or the max wait time occurs (which ever happens first). +This is very useful when you want to know how the received events look like or for testing/debugging purposes. +- For production we would expect customers would simply want to receive events and process them. Hence we have a `client.receive(. . .)` method on the receiver. +This message takes the `messageHandler()` and the `errorHandler()` amongst other parameters and registers them to the receiver. +This method returns a `ReceiverHandler` that can be used to stop receiving further events `await receiverHandler.stop()` + +## IDE ## +This sdk has been developed in [TypeScript](https://typescriptlang.org) and has good source code documentation. It is highly recommended to use [vscode](https://code.visualstudio.com) or any other IDE that provides better intellisense and exposes the full power of source code documentation. + +## Debug logs ## + +You can set the following environment variable to get the debug logs. +- Getting the debug logs from the Event Hub SDK +``` +export DEBUG=azure* +``` +- Getting the debug logs from the Event Hub SDK and the protocol level library. +``` +export DEBUG=azure*,rhea* +``` + +## Examples ## + +Please take a look at the [examples](https://github.com/Azure/azure-event-hubs-node/tree/master/examples) directory for detailed examples. + +## Example 1 - Get the partition IDs. + +```js +const { EventHubClient } = require('azure-event-hubs'); + +const client = EventHubClient.createFromConnectionString(process.env["EVENTHUB_CONNECTION_STRING"], process.env["EVENTHUB_NAME"]); + +async function main() { + const partitionIds = await client.getPartitionIds(); +} + +main().catch((err) => { + console.log(err); +}); +``` + +## Example 2.1 - Receive events with handlers +This mechanism is useful for receiving events for a longer duration. + +Receive events from partition ID 1 after the current time. +```js +const { EventHubClient, EventPosition } = require('azure-event-hubs'); + +const client = EventHubClient.createFromConnectionString(process.env["EVENTHUB_CONNECTION_STRING"], process.env["EVENTHUB_NAME"]); + +async function main() { + const onError = (err) => { + console.log("An error occurred on the receiver ", err); + }; + + const onMessage = (eventData) => { + console.log(eventData.body); + const enqueuedTime = eventData.annotations["x-opt-enqueued-time"]; + console.log("Enqueued Time: ", enqueuedTime); + }; + + const receiveHandler = client.receive("1", onMessage, onError, { eventPosition: EventPosition.fromEnqueuedTime(Date.now()) }); + + // To stop receiving events later on... + await receiveHandler.stop(); +} + +main().catch((err) => { + console.log(err); +}); +``` + +## Example 2.2 - Receive specified number of events for a given time +This mechanism is useful when you want to see how the received events look like. It can also be useful for debugging purpose. + +Receive events from partitionId `"1"` after the current time. +```js +const { EventHubClient, EventPosition } = require('azure-event-hubs'); + +const client = EventHubClient.createFromConnectionString(process.env["EVENTHUB_CONNECTION_STRING"], process.env["EVENTHUB_NAME"]); + +async function main() { + const datas = await client.receiveBatch("1", 100 /*number of events*/, 20 /*amount of time in seconds the receiver should run. Default 60 seconds.*/, { eventPosition: EventPosition.fromEnqueuedTime(Date.now()) }); + console.log("Array of EventData objects", datas); +} + +main().catch((err) => { + console.log(err); +}); +``` + +## Example 3 - Send an event with partition key. + +Send an event with a given "partition-key" which is then hashed to a partition ID (so all events with the same key will go to the same ID, but load is balanced between partitions). + +```js +const { EventHubClient, EventPosition } = require('azure-event-hubs'); + +const client = EventHubClient.createFromConnectionString(process.env["EVENTHUB_CONNECTION_STRING"], process.env["EVENTHUB_NAME"]); + +async function main() { + const eventData: EventData = { body: "Hello World", partitionKey: "pk12345"}; + const delivery = await client.send(eventData); + console.log("message sent successfully."); +} + +main().catch((err) => { + console.log(err); +}); +``` + +## Example 4 - Send an event to a specific partition id. + +Send an event to a specific partition ID if needed. If not specified then EventHub will store the events in the partition in a round-robin pattern. + +```js +const { EventHubClient, EventPosition } = require('azure-event-hubs'); + +const client = EventHubClient.createFromConnectionString(process.env["EVENTHUB_CONNECTION_STRING"], process.env["EVENTHUB_NAME"]); + +async function main() { + const data: EventData = { body: "Hello World 1", message_id: "343-0909-5454-23423-54543" }; + const delivery = await client.send(data, "1"); + console.log("message sent successfully."); +} + +main().catch((err) => { + console.log(err); +}); +``` + +## Example 5 - Send multiple events as a batch. + +Send multiple events grouped together. + +```js +const { EventHubClient, EventPosition } = require('azure-event-hubs'); + +const client = EventHubClient.createFromConnectionString(process.env["EVENTHUB_CONNECTION_STRING"], process.env["EVENTHUB_NAME"]); + +async function main() { + const datas = [ + { body: "Hello World 1", applicationProperties: { id: "Some id" }, partitionKey: "pk786" }, + { body: "Hello World 2" }, + { body: "Hello World 3" } + ]; + const delivery = await client.sendBatch(datas); + console.log("message sent successfully."); +} + +main().catch((err) => { + console.log(err); +}); +``` + +## AMQP Dependencies ## +It depends on [rhea](https://github.com/amqp/rhea) library for managing connections, sending and receiving events over the [AMQP](http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-complete-v1.0-os.pdf) protocol. diff --git a/changelog.md b/client/changelog.md similarity index 75% rename from changelog.md rename to client/changelog.md index dc82310..d740fe8 100644 --- a/changelog.md +++ b/client/changelog.md @@ -1,3 +1,10 @@ +## 2018-05-02 0.2.0 +- Added functionality to encode/decode the messages sent and received. +- Created an options object in the `client.createFromConnectionString()` and the `EventHubClient` constructor. This is a breaking change. However moving to an options object design reduces the chances of breaking changes in the future. + This options object will: + - have the existing optional `tokenProvider` property + - and a new an optional property named `dataTransformer`. You can provide your own transformer. If not provided then we will use the [DefaultDataTransformer](./client/lib/dataTransformer.ts). This should be applicable for majority of the scenarios and will ensure that messages are interoperable between different Azure services. It fixes issue #60. + ## 2018-04-26 0.1.2 - Added missing dependency for `uuid` package and nit fixes in the README.md diff --git a/examples/batchReceive.ts b/client/examples/batchReceive.ts similarity index 100% rename from examples/batchReceive.ts rename to client/examples/batchReceive.ts diff --git a/examples/batchSendReceive.ts b/client/examples/batchSendReceive.ts similarity index 100% rename from examples/batchSendReceive.ts rename to client/examples/batchSendReceive.ts diff --git a/examples/createEpochReceiver.ts b/client/examples/createEpochReceiver.ts similarity index 100% rename from examples/createEpochReceiver.ts rename to client/examples/createEpochReceiver.ts diff --git a/examples/getHubRuntimeInfo.ts b/client/examples/getHubRuntimeInfo.ts similarity index 100% rename from examples/getHubRuntimeInfo.ts rename to client/examples/getHubRuntimeInfo.ts diff --git a/examples/js/examples/batchReceive.js b/client/examples/js/examples/batchReceive.js similarity index 100% rename from examples/js/examples/batchReceive.js rename to client/examples/js/examples/batchReceive.js diff --git a/examples/js/examples/batchSendReceive.js b/client/examples/js/examples/batchSendReceive.js similarity index 100% rename from examples/js/examples/batchSendReceive.js rename to client/examples/js/examples/batchSendReceive.js diff --git a/examples/js/examples/createEpochReceiver.js b/client/examples/js/examples/createEpochReceiver.js similarity index 100% rename from examples/js/examples/createEpochReceiver.js rename to client/examples/js/examples/createEpochReceiver.js diff --git a/examples/js/examples/getHubRuntimeInfo.js b/client/examples/js/examples/getHubRuntimeInfo.js similarity index 100% rename from examples/js/examples/getHubRuntimeInfo.js rename to client/examples/js/examples/getHubRuntimeInfo.js diff --git a/examples/js/examples/sendReceiveWithInteractiveAuth.js b/client/examples/js/examples/sendReceiveWithInteractiveAuth.js similarity index 100% rename from examples/js/examples/sendReceiveWithInteractiveAuth.js rename to client/examples/js/examples/sendReceiveWithInteractiveAuth.js diff --git a/examples/js/examples/sendReceiveWithSPAuth.js b/client/examples/js/examples/sendReceiveWithSPAuth.js similarity index 100% rename from examples/js/examples/sendReceiveWithSPAuth.js rename to client/examples/js/examples/sendReceiveWithSPAuth.js diff --git a/examples/js/examples/simpleSendReceive.js b/client/examples/js/examples/simpleSendReceive.js similarity index 100% rename from examples/js/examples/simpleSendReceive.js rename to client/examples/js/examples/simpleSendReceive.js diff --git a/examples/js/examples/simpleSender.js b/client/examples/js/examples/simpleSender.js similarity index 100% rename from examples/js/examples/simpleSender.js rename to client/examples/js/examples/simpleSender.js diff --git a/examples/js/examples/streamingReceive.js b/client/examples/js/examples/streamingReceive.js similarity index 100% rename from examples/js/examples/streamingReceive.js rename to client/examples/js/examples/streamingReceive.js diff --git a/client/examples/js/lib/auth/aad.js b/client/examples/js/lib/auth/aad.js new file mode 100644 index 0000000..3c11008 --- /dev/null +++ b/client/examples/js/lib/auth/aad.js @@ -0,0 +1,66 @@ +"use strict"; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +Object.defineProperty(exports, "__esModule", { value: true }); +const ms_rest_azure_1 = require("ms-rest-azure"); +const token_1 = require("./token"); +const Constants = require("../util/constants"); +/** + * Defines the AAD (Azure ActiveDirectory) TokenProvider. + * @class AadTokenProvider + */ +class AadTokenProvider { + constructor(credentials) { + /** + * @property {number} tokenRenewalMarginInSeconds - The number of seconds within which it is + * good to renew the token. A constant set to 270 seconds (4.5 minutes). Adal has a set window of 5 minutes + * when it refreshes the token from its token cache. + */ + this.tokenRenewalMarginInSeconds = 270; + /** + * @property {number} tokenValidTimeInSeconds - The number of seconds for which the + * token is valid. A constant set to 3599 seconds (~1 hour). Adal has a set valid time of + * 1 hour (3600 seconds) when it refreshes the access token. + */ + this.tokenValidTimeInSeconds = 3599; + if (!credentials || + (credentials && + !(credentials instanceof ms_rest_azure_1.ApplicationTokenCredentials || + credentials instanceof ms_rest_azure_1.UserTokenCredentials || + credentials instanceof ms_rest_azure_1.DeviceTokenCredentials || + credentials instanceof ms_rest_azure_1.MSITokenCredentials))) { + throw new Error("'credentials' is a required parameter and must be an instance of ApplicationTokenCredentials | UserTokenCredentials | DeviceTokenCredentials | MSITokenCredentials."); + } + if (credentials instanceof ms_rest_azure_1.MSITokenCredentials) { + credentials.resource = Constants.aadEventHubsAudience; + } + this.credentials = credentials; + } + /** + * Gets the jwt token for the specified audience + * @param {string} [audience] - The audience for which the token is desired. If not + * provided then the Endpoint from the connection string will be applied. + */ + getToken(audience) { + const self = this; + return new Promise((resolve, reject) => { + self.credentials.getToken((err, result) => { + if (err) { + reject(err); + } + let expiresOn = Date.now(); + if (result.expiresOn && result.expiresOn instanceof Date) { + expiresOn = result.expiresOn.getTime(); + } + const expiry = Math.floor(expiresOn / 1000) + self.tokenValidTimeInSeconds - 5; + const tokenObj = { + expiry: expiry, + tokenType: token_1.TokenType.CbsTokenTypeJwt, + token: result.accessToken + }; + resolve(tokenObj); + }); + }); + } +} +exports.AadTokenProvider = AadTokenProvider; diff --git a/client/examples/js/lib/auth/sas.js b/client/examples/js/lib/auth/sas.js new file mode 100644 index 0000000..8a9e072 --- /dev/null +++ b/client/examples/js/lib/auth/sas.js @@ -0,0 +1,76 @@ +"use strict"; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const crypto = require("crypto"); +const utils_1 = require("../util/utils"); +const token_1 = require("./token"); +/** + * @class SasTokenProvider + * Defines the SasTokenProvider. + */ +class SasTokenProvider { + /** + * Initializes a new isntance of SasTokenProvider + * @constructor + * @param {string} namespace - The namespace of the EventHub instance. + * @param {string} keyName - The name of the EventHub key. + * @param {string} key - The secret value associated with the above EventHub key + */ + constructor(namespace, keyName, key, tokenValidTimeInSeconds, tokenRenewalMarginInSeconds) { + this.namespace = namespace; + this.keyName = keyName; + this.key = key; + this.tokenValidTimeInSeconds = tokenValidTimeInSeconds || 3600; + this.tokenRenewalMarginInSeconds = tokenRenewalMarginInSeconds || 900; + if (this.tokenValidTimeInSeconds <= this.tokenRenewalMarginInSeconds) { + throw new Error('tokenRenewalMarginInSeconds must be less than tokenValidTimeInSeconds'); + } + } + /** + * Gets the sas token for the specified audience + * @param {string} [audience] - The audience for which the token is desired. If not + * provided then the Endpoint from the connection string will be applied. + */ + getToken(audience) { + return __awaiter(this, void 0, void 0, function* () { + return this._createToken(Math.floor(Date.now() / 1000) + this.tokenValidTimeInSeconds, audience); + }); + } + /** + * Creates the sas token based on the provided information + * @param {string | number} expiry - The time period in unix time after which the token will expire. + * @param {string} [audience] - The audience for which the token is desired. If not + * provided then the Endpoint from the connection string will be applied. + */ + _createToken(expiry, audience) { + if (!audience) + audience = this.namespace; + audience = encodeURIComponent(audience); + const keyName = encodeURIComponent(this.keyName); + const stringToSign = audience + '\n' + expiry; + const sig = encodeURIComponent(crypto.createHmac('sha256', this.key).update(stringToSign, 'utf8').digest('base64')); + return { + token: `SharedAccessSignature sr=${audience}&sig=${sig}&se=${expiry}&skn=${keyName}`, + tokenType: token_1.TokenType.CbsTokenTypeSas, + expiry: expiry + }; + } + /** + * + * @param {string} connectionString - The EventHub connection string + */ + static fromConnectionString(connectionString) { + const parsed = utils_1.parseConnectionString(connectionString); + return new SasTokenProvider(parsed.Endpoint, parsed.SharedAccessKeyName, parsed.SharedAccessKey); + } +} +exports.SasTokenProvider = SasTokenProvider; diff --git a/client/examples/js/lib/auth/token.js b/client/examples/js/lib/auth/token.js new file mode 100644 index 0000000..7da22d5 --- /dev/null +++ b/client/examples/js/lib/auth/token.js @@ -0,0 +1,20 @@ +"use strict"; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +Object.defineProperty(exports, "__esModule", { value: true }); +/** + * Describes the type of supported tokens. + * @readonly + * @enum TokenType + */ +var TokenType; +(function (TokenType) { + /** + * The "jwt" token type. Used with AADTokenProvider. + */ + TokenType["CbsTokenTypeJwt"] = "jwt"; + /** + * The sas token type. Used with SasTokenProvider. + */ + TokenType["CbsTokenTypeSas"] = "servicebus.windows.net:sastoken"; +})(TokenType = exports.TokenType || (exports.TokenType = {})); diff --git a/client/examples/js/lib/batchingReceiver.js b/client/examples/js/lib/batchingReceiver.js new file mode 100644 index 0000000..da495c2 --- /dev/null +++ b/client/examples/js/lib/batchingReceiver.js @@ -0,0 +1,142 @@ +"use strict"; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +Object.defineProperty(exports, "__esModule", { value: true }); +const debugModule = require("debug"); +const _1 = require("."); +const eventHubReceiver_1 = require("./eventHubReceiver"); +const errors_1 = require("./errors"); +const Constants = require("./util/constants"); +const debug = debugModule("azure:event-hubs:receiverbatching"); +/** + * Describes the batching receiver where the user can receive a specified number of messages for a predefined time. + * @class BatchingReceiver + * @extends EventHubReceiver + */ +class BatchingReceiver extends eventHubReceiver_1.EventHubReceiver { + /** + * Instantiate a new receiver from the AMQP `Receiver`. Used by `EventHubClient`. + * + * @constructor + * @param {EventHubClient} client The EventHub client. + * @param {string} partitionId Partition ID from which to receive. + * @param {ReceiveOptions} [options] Options for how you'd like to connect. + * @param {string} [options.consumerGroup] Consumer group from which to receive. + * @param {number} [options.prefetchCount] The upper limit of events this receiver will + * actively receive regardless of whether a receive operation is pending. + * @param {boolean} [options.enableReceiverRuntimeMetric] Provides the approximate receiver runtime information + * for a logical partition of an Event Hub if the value is true. Default false. + * @param {number} [options.epoch] The epoch value that this receiver is currently + * using for partition ownership. A value of undefined means this receiver is not an epoch-based receiver. + * @param {EventPosition} [options.eventPosition] The position of EventData in the EventHub parition from + * where the receiver should start receiving. Only one of offset, sequenceNumber, enqueuedTime, customFilter can be specified. + * `EventPosition.withCustomFilter()` should be used if you want more fine-grained control of the filtering. + * See https://github.com/Azure/amqpnetlite/wiki/Azure%20Service%20Bus%20Event%20Hubs for details. + */ + constructor(context, partitionId, options) { + super(context, partitionId, options); + } + /** + * Receive a batch of EventData objects from an EventHub partition for a given count and a given max wait time in seconds, whichever + * happens first. This method can be used directly after creating the receiver object and **MUST NOT** be used along with the `start()` method. + * + * @param {number} maxMessageCount The maximum message count. Must be a value greater than 0. + * @param {number} [maxWaitTimeInSeconds] The maximum wait time in seconds for which the Receiver should wait + * to receiver the said amount of messages. If not provided, it defaults to 60 seconds. + * @returns {Promise} A promise that resolves with an array of EventData objects. + */ + receive(maxMessageCount, maxWaitTimeInSeconds) { + if (!maxMessageCount || (maxMessageCount && typeof maxMessageCount !== 'number')) { + throw new Error("'maxMessageCount' is a required parameter of type number with a value greater than 0."); + } + if (maxWaitTimeInSeconds == undefined) { + maxWaitTimeInSeconds = Constants.defaultOperationTimeoutInSeconds; + } + const eventDatas = []; + let timeOver = false; + return new Promise((resolve, reject) => { + let onReceiveMessage; + let onReceiveError; + let waitTimer; + let actionAfterWaitTimeout; + // Final action to be performed after maxMessageCount is reached or the maxWaitTime is over. + const finalAction = (timeOver, data) => { + // Resetting the mode. Now anyone can call start() or receive() again. + this._receiver.removeListener(Constants.receiverError, onReceiveError); + this._receiver.removeListener(Constants.message, onReceiveMessage); + if (!data) { + data = eventDatas.length ? eventDatas[eventDatas.length - 1] : undefined; + } + if (!timeOver) { + clearTimeout(waitTimer); + } + if (this.receiverRuntimeMetricEnabled && data) { + this.runtimeInfo.lastSequenceNumber = data.lastSequenceNumber; + this.runtimeInfo.lastEnqueuedTimeUtc = data.lastEnqueuedTime; + this.runtimeInfo.lastEnqueuedOffset = data.lastEnqueuedOffset; + this.runtimeInfo.retrievalTime = data.retrievalTime; + } + resolve(eventDatas); + }; + // Action to be performed after the max wait time is over. + actionAfterWaitTimeout = () => { + timeOver = true; + return finalAction(timeOver); + }; + // Action to be performed on the "message" event. + onReceiveMessage = (context) => { + const data = _1.EventData.fromAmqpMessage(context.message); + if (eventDatas.length <= maxMessageCount) { + eventDatas.push(data); + } + if (eventDatas.length === maxMessageCount) { + finalAction(timeOver, data); + } + }; + // Action to be taken when an error is received. + onReceiveError = (context) => { + this._receiver.removeListener(Constants.receiverError, onReceiveError); + this._receiver.removeListener(Constants.message, onReceiveMessage); + const error = errors_1.translate(context.receiver.error); + debug("[%s] Receiver '%s' received an error:\n%O", this._context.connectionId, this.name, error); + if (waitTimer) { + clearTimeout(waitTimer); + } + reject(error); + }; + const addCreditAndSetTimer = (reuse) => { + debug("[%s] Receiver '%s', adding credit for receiving %d messages.", this._context.connectionId, this.name, maxMessageCount); + this._receiver.add_credit(maxMessageCount); + let msg = "[%s] Setting the wait timer for %d seconds for receiver '%s'."; + if (reuse) + msg += " Receiver link already present, hence reusing it."; + debug(msg, this._context.connectionId, maxWaitTimeInSeconds, this.name); + waitTimer = setTimeout(actionAfterWaitTimeout, maxWaitTimeInSeconds * 1000); + }; + if (!this._isOpen()) { + debug("[%s] Receiver '%s', setting the prefetch count to 0.", this._context.connectionId, this.name); + this.prefetchCount = 0; + this._init(onReceiveMessage, onReceiveError).then(() => addCreditAndSetTimer()).catch(reject); + } + else { + addCreditAndSetTimer(true); + this._receiver.on(Constants.message, onReceiveMessage); + this._receiver.on(Constants.receiverError, onReceiveError); + } + }); + } + /** + * Creates a batching receiver. + * @static + * + * @param {ConnectionContext} context The connection context. + * @param {string | number} partitionId The partitionId to receive events from. + * @param {ReceiveOptions} [options] Receive options. + */ + static create(context, partitionId, options) { + const bReceiver = new BatchingReceiver(context, partitionId, options); + context.receivers[bReceiver.name] = bReceiver; + return bReceiver; + } +} +exports.BatchingReceiver = BatchingReceiver; diff --git a/client/examples/js/lib/cbs.js b/client/examples/js/lib/cbs.js new file mode 100644 index 0000000..7d73ded --- /dev/null +++ b/client/examples/js/lib/cbs.js @@ -0,0 +1,122 @@ +"use strict"; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const rpc_1 = require("./rpc"); +const rheaPromise = require("./rhea-promise"); +const uuid = require("uuid/v4"); +const Constants = require("./util/constants"); +const debugModule = require("debug"); +const errors_1 = require("./errors"); +const debug = debugModule("azure:event-hubs:cbs"); +/** + * @class CbsClient + * Describes the EventHub Cbs client that talks to the $cbs endopint over AMQP connection. + */ +class CbsClient { + constructor() { + /** + * @property {string} endpoint CBS endpoint - "$cbs" + */ + this.endpoint = Constants.cbsEndpoint; + /** + * @property {string} replyTo CBS replyTo - The reciever link name that the service should reply to. + */ + this.replyTo = `${Constants.cbsReplyTo}-${uuid()}`; + /** + * @property {string} cbsLock The unqiue lock name per $cbs session per connection that is used to + * acquire the lock for establishing a cbs session if one does not exist for an aqmp connection. + */ + this.cbsLock = `${Constants.negotiateCbsKey}-${uuid()}`; + } + /** + * Creates a singleton instance of the CBS session if it hasn't been initialized previously on the given connection. + * @param {any} connection The AMQP connection object on which the CBS session needs to be initialized. + */ + init(connection) { + return __awaiter(this, void 0, void 0, function* () { + if (!this._cbsSenderReceiverLink) { + const rxOpt = { + source: { + address: this.endpoint + }, + name: this.replyTo + }; + this._cbsSenderReceiverLink = yield rpc_1.createRequestResponseLink(connection, { target: { address: this.endpoint } }, rxOpt); + this._cbsSenderReceiverLink.sender.on("sender_error", (context) => { + const ehError = errors_1.translate(context.sender.error); + debug("An error occurred on the cbs sender link.. %O", ehError); + }); + this._cbsSenderReceiverLink.receiver.on("receiver_error", (context) => { + const ehError = errors_1.translate(context.receiver.error); + debug("An error occurred on the cbs receiver link.. %O", ehError); + }); + debug("[%s] Successfully created the cbs sender '%s' and receiver '%s' links over cbs session.", connection.options.id, this._cbsSenderReceiverLink.sender.name, this._cbsSenderReceiverLink.receiver.name); + } + else { + debug("[%s] CBS session is already present. Reusing the cbs sender '%s' and receiver '%s' links over cbs session.", connection.options.id, this._cbsSenderReceiverLink.sender.name, this._cbsSenderReceiverLink.receiver.name); + } + }); + } + /** + * Negotiates the CBS claim with the EventHub Service. + * @param {string} audience The audience for which the token is requested. + * @param {any} connection The underlying AMQP connection. + * @param {TokenInfo} tokenObject The token object that needs to be sent in the put-token request. + * @return {Promise} Returns a Promise that resolves when $cbs authentication is successful + * and rejects when an error occurs during $cbs authentication. + */ + negotiateClaim(audience, connection, tokenObject) { + return __awaiter(this, void 0, void 0, function* () { + try { + const request = { + body: tokenObject.token, + message_id: uuid(), + reply_to: this.replyTo, + to: this.endpoint, + application_properties: { + operation: Constants.operationPutToken, + name: audience, + type: tokenObject.tokenType + } + }; + const response = yield rpc_1.sendRequest(connection, this._cbsSenderReceiverLink, request); + return response; + } + catch (err) { + debug("[%s]An error occurred while negotating the cbs claim: %O", connection.options.id, err); + throw err; + } + }); + } + /** + * Closes the AMQP cbs session to the Event Hub for this client, + * returning a promise that will be resolved when disconnection is completed. + * @return {Promise} + */ + close() { + return __awaiter(this, void 0, void 0, function* () { + try { + if (this._cbsSenderReceiverLink) { + yield rheaPromise.closeSession(this._cbsSenderReceiverLink.session); + debug("Successfully closed the cbs session."); + this._cbsSenderReceiverLink = undefined; + } + } + catch (err) { + const msg = `An error occurred while closing the cbs session: ${JSON.stringify(err)} `; + debug(msg); + throw new Error(msg); + } + }); + } +} +exports.CbsClient = CbsClient; diff --git a/client/examples/js/lib/connectionConfig.js b/client/examples/js/lib/connectionConfig.js new file mode 100644 index 0000000..e5ce69d --- /dev/null +++ b/client/examples/js/lib/connectionConfig.js @@ -0,0 +1,54 @@ +"use strict"; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +Object.defineProperty(exports, "__esModule", { value: true }); +const utils_1 = require("./util/utils"); +var ConnectionConfig; +(function (ConnectionConfig) { + /** + * Creates the connection config. + * @param {string} connectionString - The event hub connection string + * @param {string} [path] - The name/path of the entity (hub name) to which the connection needs to happen + */ + function create(connectionString, path) { + if (!connectionString || (connectionString && typeof connectionString !== "string")) { + throw new Error("'connectionString' is a required parameter and must be of type: 'string'."); + } + const parsedCS = utils_1.parseConnectionString(connectionString); + if (!path && !parsedCS.EntityPath) { + throw new Error(`Either provide "path" or the "connectionString": "${connectionString}", must contain EntityPath="".`); + } + const result = { + connectionString: connectionString, + endpoint: parsedCS.Endpoint, + host: (parsedCS && parsedCS.Endpoint) ? (parsedCS.Endpoint.match('sb://([^/]*)') || [])[1] : "", + entityPath: path || parsedCS.EntityPath, + sharedAccessKeyName: parsedCS.SharedAccessKeyName, + sharedAccessKey: parsedCS.SharedAccessKey + }; + return result; + } + ConnectionConfig.create = create; + /** + * Validates the properties of connection config. + * @param {ConnectionConfig} config The connection config to be validated. + */ + function validate(config) { + if (!config || (config && typeof config !== "object")) { + throw new Error("'config' is a required parameter and must be of type: 'object'."); + } + if (!config.endpoint || (config.endpoint && typeof config.endpoint !== "string")) { + throw new Error("'endpoint' is a required property of the ConnectionConfig."); + } + if (!config.entityPath || (config.entityPath && typeof config.entityPath !== "string")) { + throw new Error("'entityPath' is a required property of the ConnectionConfig."); + } + if (!config.sharedAccessKeyName || (config.sharedAccessKeyName && typeof config.sharedAccessKeyName !== "string")) { + throw new Error("'sharedAccessKeyName' is a required property of the ConnectionConfig."); + } + if (!config.sharedAccessKey || (config.sharedAccessKey && typeof config.sharedAccessKey !== "string")) { + throw new Error("'sharedAccessKey' is a required property of the ConnectionConfig."); + } + } + ConnectionConfig.validate = validate; +})(ConnectionConfig = exports.ConnectionConfig || (exports.ConnectionConfig = {})); diff --git a/client/examples/js/lib/connectionContext.js b/client/examples/js/lib/connectionContext.js new file mode 100644 index 0000000..69a229e --- /dev/null +++ b/client/examples/js/lib/connectionContext.js @@ -0,0 +1,35 @@ +"use strict"; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +Object.defineProperty(exports, "__esModule", { value: true }); +const debugModule = require("debug"); +const uuid = require("uuid/v4"); +const Constants = require("./util/constants"); +const _1 = require("."); +const managementClient_1 = require("./managementClient"); +const cbs_1 = require("./cbs"); +const sas_1 = require("./auth/sas"); +const debug = debugModule("azure:event-hubs:connectionContext"); +var ConnectionContext; +(function (ConnectionContext) { + /** + * @property {string} userAgent The user agent string for the event hub client. Constant value: "/js-event-hubs". + */ + ConnectionContext.userAgent = "/js-event-hubs"; + function create(config, tokenProvider) { + _1.ConnectionConfig.validate(config); + const context = { + connectionLock: `${Constants.establishConnection}-${uuid()}`, + negotiateClaimLock: `${Constants.negotiateClaim}-${uuid()}`, + config: config, + tokenProvider: tokenProvider || new sas_1.SasTokenProvider(config.endpoint, config.sharedAccessKeyName, config.sharedAccessKey), + cbsSession: new cbs_1.CbsClient(), + managementSession: new managementClient_1.ManagementClient(config.entityPath), + senders: {}, + receivers: {} + }; + debug("Created connection context: %O", context); + return context; + } + ConnectionContext.create = create; +})(ConnectionContext = exports.ConnectionContext || (exports.ConnectionContext = {})); diff --git a/client/examples/js/lib/errors.js b/client/examples/js/lib/errors.js new file mode 100644 index 0000000..1faf8c3 --- /dev/null +++ b/client/examples/js/lib/errors.js @@ -0,0 +1,378 @@ +"use strict"; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +Object.defineProperty(exports, "__esModule", { value: true }); +/** + * Maps the conditions to the numeric AMQP Response status codes. + * @enum {ConditionStatusMapper} + */ +var ConditionStatusMapper; +(function (ConditionStatusMapper) { + ConditionStatusMapper[ConditionStatusMapper["com.microsoft:timeout"] = 408] = "com.microsoft:timeout"; + ConditionStatusMapper[ConditionStatusMapper["amqp:not-found"] = 404] = "amqp:not-found"; + ConditionStatusMapper[ConditionStatusMapper["amqp:not-implemented"] = 501] = "amqp:not-implemented"; + ConditionStatusMapper[ConditionStatusMapper["com.microsoft:entity-already-exists"] = 409] = "com.microsoft:entity-already-exists"; + ConditionStatusMapper[ConditionStatusMapper["com.microsoft:session-lock-lost"] = 410] = "com.microsoft:session-lock-lost"; + ConditionStatusMapper[ConditionStatusMapper["com.microsoft:no-matching-subscription"] = 500] = "com.microsoft:no-matching-subscription"; + ConditionStatusMapper[ConditionStatusMapper["amqp:link:message-size-exceeded"] = 403] = "amqp:link:message-size-exceeded"; + ConditionStatusMapper[ConditionStatusMapper["com.microsoft:server-busy"] = 503] = "com.microsoft:server-busy"; + ConditionStatusMapper[ConditionStatusMapper["com.microsoft:argument-error"] = 400] = "com.microsoft:argument-error"; + ConditionStatusMapper[ConditionStatusMapper["com.microsoft:argument-out-of-range"] = 400] = "com.microsoft:argument-out-of-range"; + ConditionStatusMapper[ConditionStatusMapper["com.microsoft:store-lock-lost"] = 410] = "com.microsoft:store-lock-lost"; + ConditionStatusMapper[ConditionStatusMapper["com.microsoft:session-cannot-be-locked"] = 410] = "com.microsoft:session-cannot-be-locked"; + ConditionStatusMapper[ConditionStatusMapper["com.microsoft:partition-not-owned"] = 410] = "com.microsoft:partition-not-owned"; + ConditionStatusMapper[ConditionStatusMapper["com.microsoft:entity-disabled"] = 400] = "com.microsoft:entity-disabled"; + ConditionStatusMapper[ConditionStatusMapper["com.microsoft:publisher-revoked"] = 401] = "com.microsoft:publisher-revoked"; + ConditionStatusMapper[ConditionStatusMapper["amqp:link:stolen"] = 410] = "amqp:link:stolen"; + ConditionStatusMapper[ConditionStatusMapper["amqp:not-allowed"] = 400] = "amqp:not-allowed"; + ConditionStatusMapper[ConditionStatusMapper["amqp:unauthorized-access"] = 401] = "amqp:unauthorized-access"; + ConditionStatusMapper[ConditionStatusMapper["amqp:resource-limit-exceeded"] = 403] = "amqp:resource-limit-exceeded"; + ConditionStatusMapper[ConditionStatusMapper["com.microsoft:message-lock-lost"] = 410] = "com.microsoft:message-lock-lost"; +})(ConditionStatusMapper = exports.ConditionStatusMapper || (exports.ConditionStatusMapper = {})); +/** + * Maps the conditions to the Error names. + * @enum {ConditionErrorNameMapper} + */ +var ConditionErrorNameMapper; +(function (ConditionErrorNameMapper) { + /** + * Error is thrown when an internal server error occured. You may have found a bug? + */ + ConditionErrorNameMapper["amqp:internal-error"] = "InternalServerError"; + /** + * Error for signaling general communication errors related to messaging operations. + */ + ConditionErrorNameMapper["amqp:not-found"] = "EventHubsCommunicationError"; + /** + * Error is thrown when a feature is not implemented yet but the placeholder is present. + */ + ConditionErrorNameMapper["amqp:not-implemented"] = "NotImplementedError"; + /** + * Error is thrown when an operation is attempted but is not allowed. + */ + ConditionErrorNameMapper["amqp:not-allowed"] = "InvalidOperationError"; + /** + * Error is thrown the the Azure Event Hub quota has been exceeded. + * Quotas are reset periodically, this operation will have to wait until then. + * The messaging entity has reached its maximum allowable size. + * This can happen if the maximum number of receivers (which is 5) has already + * been opened on a per-consumer group level. + */ + ConditionErrorNameMapper["amqp:resource-limit-exceeded"] = "QuotaExceededError"; + /** + * Error is thrown when the connection parameters are wrong and the server refused the connection. + */ + ConditionErrorNameMapper["amqp:unauthorized-access"] = "UnauthorizedError"; + /** + * Error is thrown when the service is unavailable. The operation should be retried. + */ + ConditionErrorNameMapper["com.microsoft:timeout"] = "ServiceUnavailableError"; + /** + * Error is thrown when an argument has a value that is out of the admissible range. + */ + ConditionErrorNameMapper["com.microsoft:argument-out-of-range"] = "ArgumentOutOfRangeError"; + /** + * Error is thrown when a condition that should have been met in order to execute an operation was not. + */ + ConditionErrorNameMapper["amqp:precondition-failed"] = "PreconditionFailedError"; + /** + * Error is thrown when data could not be decoded. + */ + ConditionErrorNameMapper["amqp:decode-error"] = "DecodeError"; + /** + * Error is thrown when an invalid field was passed in a frame body, and the operation could not proceed. + */ + ConditionErrorNameMapper["amqp:invalid-field"] = "InvalidFieldError"; + /** + * Error is thrown when the client attempted to work with a server entity to which it + * has no access because another client is working with it. + */ + ConditionErrorNameMapper["amqp:resource-locked"] = "ResourceLockedError"; + /** + * Error is thrown when a server entity the client is working with has been deleted. + */ + ConditionErrorNameMapper["amqp:resource-deleted"] = "ResourceDeletedError"; + /** + * Error is thrown when the peer sent a frame that is not permitted in the current state. + */ + ConditionErrorNameMapper["amqp:illegal-state"] = "IllegalStateError"; + /** + * Error is thrown when the peer cannot send a frame because the smallest encoding of + * the performative with the currently valid values would be too large to fit within + * a frame of the agreed maximum frame size. + */ + ConditionErrorNameMapper["amqp:frame-size-too-small"] = "FrameSizeTooSmallError"; + /** + * Error is thrown when an operator intervened to detach for some reason. + */ + ConditionErrorNameMapper["amqp:link:detach-forced"] = "DetachForcedError"; + /** + * Error is thrown when the peer sent more message transfers than currently allowed on the link. + */ + ConditionErrorNameMapper["amqp:link:transfer-limit-exceeded"] = "TransferLimitExceededError"; + /** + * Error is thrown when the message sent is too large: the maximum size is 256Kb. + */ + ConditionErrorNameMapper["amqp:link:message-size-exceeded"] = "MessageTooLargeError"; + /** + * Error is thrown when the address provided cannot be resolved to a terminus at the current container. + */ + ConditionErrorNameMapper["amqp:link:redirect"] = "LinkRedirectError"; + /** + * Error is thrown when two or more instances connect to the same partition + * with different epoch values. + */ + ConditionErrorNameMapper["amqp:link:stolen"] = "ReceiverDisconnectedError"; + /** + * Error is thrown when the peer violated incoming window for the session. + */ + ConditionErrorNameMapper["amqp:session:window-violation"] = "SessionWindowViolationError"; + /** + * Error is thrown when input was received for a link that was detached with an error. + */ + ConditionErrorNameMapper["amqp:session:errant-link"] = "ErrantLinkError"; + /** + * Error is thrown when an attach was received using a handle that is already in use for an attached link. + */ + ConditionErrorNameMapper["amqp:session:handle-in-use"] = "HanldeInUseError"; + /** + * Error is thrown when a frame (other than attach) was received referencing a handle which is not + * currently in use of an attached link. + */ + ConditionErrorNameMapper["amqp:session:unattached-handle"] = "UnattachedHandleError"; + /** + * Error is thrown when an operator intervened to close the connection for some reason. + */ + ConditionErrorNameMapper["amqp:connection:forced"] = "ConnectionForcedError"; + /** + * Error is thrown when a valid frame header cannot be formed from the incoming byte stream. + */ + ConditionErrorNameMapper["amqp:connection:framing-error"] = "FramingError"; + /** + * Error is thrown when the container is no longer available on the current connection. + */ + ConditionErrorNameMapper["amqp:connection:redirect"] = "ConnectionRedirectError"; + /** + * Error is thrown when the server is busy. Callers should wait a while and retry the operation. + */ + ConditionErrorNameMapper["com.microsoft:server-busy"] = "ServerBusyError"; + /** + * Error is thrown when an incorrect argument was received. + */ + ConditionErrorNameMapper["com.microsoft:argument-error"] = "ArgumentError"; +})(ConditionErrorNameMapper = exports.ConditionErrorNameMapper || (exports.ConditionErrorNameMapper = {})); +/** + * Maps the conditions to the Error names. + * @enum {ErrorNameConditionMapper} + */ +var ErrorNameConditionMapper; +(function (ErrorNameConditionMapper) { + /** + * Error is thrown when an internal server error occured. You may have found a bug? + */ + ErrorNameConditionMapper["InternalServerError"] = "amqp:internal-error"; + /** + * Error for signaling general communication errors related to messaging operations. + */ + ErrorNameConditionMapper["EventHubsCommunicationError"] = "amqp:not-found"; + /** + * Error is thrown when a feature is not implemented yet but the placeholder is present. + */ + ErrorNameConditionMapper["NotImplementedError"] = "amqp:not-implemented"; + /** + * Error is thrown when an operation is attempted but is not allowed. + */ + ErrorNameConditionMapper["InvalidOperationError"] = "amqp:not-allowed"; + /** + * Error is thrown the the Azure Event Hub quota has been exceeded. + * Quotas are reset periodically, this operation will have to wait until then. + * The messaging entity has reached its maximum allowable size. + * This can happen if the maximum number of receivers (which is 5) has already + * been opened on a per-consumer group level. + */ + ErrorNameConditionMapper["QuotaExceededError"] = "amqp:resource-limit-exceeded"; + /** + * Error is thrown when the connection parameters are wrong and the server refused the connection. + */ + ErrorNameConditionMapper["UnauthorizedError"] = "amqp:unauthorized-access"; + /** + * Error is thrown when the service is unavailable. The operation should be retried. + */ + ErrorNameConditionMapper["ServiceUnavailableError"] = "com.microsoft:timeout"; + /** + * Error is thrown when an argument has a value that is out of the admissible range. + */ + ErrorNameConditionMapper["ArgumentOutOfRangeError"] = "com.microsoft:argument-out-of-range"; + /** + * Error is thrown when a condition that should have been met in order to execute an operation was not. + */ + ErrorNameConditionMapper["PreconditionFailedError"] = "amqp:precondition-failed"; + /** + * Error is thrown when data could not be decoded. + */ + ErrorNameConditionMapper["DecodeError"] = "amqp:decode-error"; + /** + * Error is thrown when an invalid field was passed in a frame body, and the operation could not proceed. + */ + ErrorNameConditionMapper["InvalidFieldError"] = "amqp:invalid-field"; + /** + * Error is thrown when the client attempted to work with a server entity to which it + * has no access because another client is working with it. + */ + ErrorNameConditionMapper["ResourceLockedError"] = "amqp:resource-locked"; + /** + * Error is thrown when a server entity the client is working with has been deleted. + */ + ErrorNameConditionMapper["ResourceDeletedError"] = "amqp:resource-deleted"; + /** + * Error is thrown when the peer sent a frame that is not permitted in the current state. + */ + ErrorNameConditionMapper["IllegalStateError"] = "amqp:illegal-state"; + /** + * Error is thrown when the peer cannot send a frame because the smallest encoding of + * the performative with the currently valid values would be too large to fit within + * a frame of the agreed maximum frame size. + */ + ErrorNameConditionMapper["FrameSizeTooSmallError"] = "amqp:frame-size-too-small"; + /** + * Error is thrown when an operator intervened to detach for some reason. + */ + ErrorNameConditionMapper["DetachForcedError"] = "amqp:link:detach-forced"; + /** + * Error is thrown when the peer sent more message transfers than currently allowed on the link. + */ + ErrorNameConditionMapper["TransferLimitExceededError"] = "amqp:link:transfer-limit-exceeded"; + /** + * Error is thrown when the message sent is too large: the maximum size is 256Kb. + */ + ErrorNameConditionMapper["MessageTooLargeError"] = "amqp:link:message-size-exceeded"; + /** + * Error is thrown when the address provided cannot be resolved to a terminus at the current container. + */ + ErrorNameConditionMapper["LinkRedirectError"] = "amqp:link:redirect"; + /** + * Error is thrown when two or more instances connect to the same partition + * with different epoch values. + */ + ErrorNameConditionMapper["ReceiverDisconnectedError"] = "amqp:link:stolen"; + /** + * Error is thrown when the peer violated incoming window for the session. + */ + ErrorNameConditionMapper["SessionWindowViolationError"] = "amqp:session:window-violation"; + /** + * Error is thrown when input was received for a link that was detached with an error. + */ + ErrorNameConditionMapper["ErrantLinkError"] = "amqp:session:errant-link"; + /** + * Error is thrown when an attach was received using a handle that is already in use for an attached link. + */ + ErrorNameConditionMapper["HanldeInUseError"] = "amqp:session:handle-in-use"; + /** + * Error is thrown when a frame (other than attach) was received referencing a handle which is not + * currently in use of an attached link. + */ + ErrorNameConditionMapper["UnattachedHandleError"] = "amqp:session:unattached-handle"; + /** + * Error is thrown when an operator intervened to close the connection for some reason. + */ + ErrorNameConditionMapper["ConnectionForcedError"] = "amqp:connection:forced"; + /** + * Error is thrown when a valid frame header cannot be formed from the incoming byte stream. + */ + ErrorNameConditionMapper["FramingError"] = "amqp:connection:framing-error"; + /** + * Error is thrown when the container is no longer available on the current connection. + */ + ErrorNameConditionMapper["ConnectionRedirectError"] = "amqp:connection:redirect"; + /** + * Error is thrown when the server is busy. Callers should wait a while and retry the operation. + */ + ErrorNameConditionMapper["ServerBusyError"] = "com.microsoft:server-busy"; + /** + * Error is thrown when an incorrect argument was received. + */ + ErrorNameConditionMapper["ArgumentError"] = "com.microsoft:argument-error"; +})(ErrorNameConditionMapper = exports.ErrorNameConditionMapper || (exports.ErrorNameConditionMapper = {})); +/** + * Describes the base class for an EventHub Error. + * @class {EventHubsError} + * @extends Error + */ +class EventHubsError extends Error { + /** + * @param {string} message The error message that provides more information about the error. + */ + constructor(message) { + super(message); + /** + * @property {string} name The error name. Default value: "EventHubsError". + */ + this.name = "EventHubsError"; + /** + * @property {boolean} translated Has the error been translated. Default: true. + */ + this.translated = true; + /** + * + * @param {boolean} retryable Describes whether the error is retryable. Default: false. + */ + this.retryable = false; + } +} +exports.EventHubsError = EventHubsError; +/** + * Determines whether the given error object is like an AmqpError object. + * @param err The AmqpError object + */ +function isAmqpError(err) { + if (!err || typeof err !== "object") { + throw new Error("err is a required parameter and must be of type 'object'."); + } + let result = false; + if (((err.condition && typeof err.condition === "string") && (err.description && typeof err.description === "string")) + || (err.value && Array.isArray(err.value)) + || (err.constructor && err.constructor.name === "c")) { + result = true; + } + return result; +} +/** + * Translates the AQMP error received at the protocol layer or a generic Error into an EventHubsError. + * + * @param {AmqpError} err The amqp error that was received. + * @returns {EventHubsError} EventHubsError object. + */ +function translate(err) { + if (err.translated) { // already translated + return err; + } + else if (isAmqpError(err)) { // translate + const condition = err.condition; + const description = err.description; + const error = new EventHubsError(description); + error.condition = condition; + if (condition) { + if (condition === "com.microsoft:precondition-failed") + error.name = "PreconditionFailedError"; + else + error.name = ConditionErrorNameMapper[condition] || "EventHubsError"; + } + if (description && + (description.includes("status-code: 404") || + description.match(/The messaging entity .* could not be found.*/i) !== null)) { + error.name = "MessagingEntityNotFoundError"; + } + if (error.name === "InternalServerError" + || error.name === "ServerBusyError" + || error.name === "ServiceUnavailableError") { + error.retryable = true; + } + return error; + } + else { + // Translate a generic error into EventHubsError. + const error = new EventHubsError(err.message); + return error; + } +} +exports.translate = translate; diff --git a/client/examples/js/lib/eventData.js b/client/examples/js/lib/eventData.js new file mode 100644 index 0000000..34efcaa --- /dev/null +++ b/client/examples/js/lib/eventData.js @@ -0,0 +1,109 @@ +"use strict"; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +Object.defineProperty(exports, "__esModule", { value: true }); +const Constants = require("./util/constants"); +exports.messageProperties = [ + "message_id", "reply_to", "to", "correlation_id", "content_type", "absolute_expiry_time", + "group_id", "group_sequence", "reply_to_group_id", "content_encoding", "creation_time" +]; +/** + * Describes the methods on the EventData interface. + * @module EventData + */ +var EventData; +(function (EventData) { + /** + * Converts the AMQP message to an EventData. + * @param {AmqpMessage} msg The AMQP message that needs to be converted to EventData. + */ + function fromAmqpMessage(msg) { + // TODO: Look at how other sdks are encoding their payloads and copy them. This will ensure consistency across all the sdks. + const data = { + body: msg.body, + _raw_amqp_mesage: msg + }; + if (msg.message_annotations) { + data.annotations = msg.message_annotations; + if (msg.message_annotations[Constants.partitionKey]) + data.partitionKey = msg.message_annotations[Constants.partitionKey]; + if (msg.message_annotations[Constants.sequenceNumber]) + data.sequenceNumber = msg.message_annotations[Constants.sequenceNumber]; + if (msg.message_annotations[Constants.enqueuedTime]) + data.enqueuedTimeUtc = new Date(msg.message_annotations[Constants.enqueuedTime]); + if (msg.message_annotations[Constants.offset]) + data.offset = msg.message_annotations[Constants.offset]; + } + // Since rhea expects message properties as top level properties we will look for them and unflatten them inside properties. + for (const prop of exports.messageProperties) { + if (msg[prop]) { + if (!data.properties) { + data.properties = {}; + } + data.properties[prop] = msg[prop]; + } + } + if (msg.application_properties) { + data.applicationProperties = msg.application_properties; + } + if (msg.delivery_annotations) { + data.lastEnqueuedOffset = msg.delivery_annotations.last_enqueued_offset; + data.lastSequenceNumber = msg.delivery_annotations.last_enqueued_sequence_number; + data.lastEnqueuedTime = new Date(msg.delivery_annotations.last_enqueued_time_utc); + data.retrievalTime = new Date(msg.delivery_annotations.runtime_info_retrieval_time_utc); + } + return data; + } + EventData.fromAmqpMessage = fromAmqpMessage; + /** + * Converts an EventData object to an AMQP message. + * @param {EventData} data The EventData object that needs to be converted to an AMQP message. + */ + function toAmqpMessage(data) { + const msg = { + body: data.body, + }; + // As per the AMQP 1.0 spec If the message-annotations or delivery-annotations section is omitted, + // it is equivalent to a message-annotations section containing anempty map of annotations. + msg.message_annotations = {}; + msg.delivery_annotations = {}; + if (data.annotations) { + msg.message_annotations = data.annotations; + } + if (data.properties) { + // Set amqp message properties as top level properties, since rhea sends them as top level properties. + for (const prop in data.properties) { + msg[prop] = data.properties[prop]; + } + } + if (data.applicationProperties) { + msg.application_properties = data.applicationProperties; + } + if (data.partitionKey) { + msg.message_annotations[Constants.partitionKey] = data.partitionKey; + } + if (data.sequenceNumber != undefined) { + msg.message_annotations[Constants.sequenceNumber] = data.sequenceNumber; + } + if (data.enqueuedTimeUtc) { + msg.message_annotations[Constants.enqueuedTime] = data.enqueuedTimeUtc.getTime(); + } + if (data.offset != undefined) { + msg.message_annotations[Constants.offset] = data.offset; + } + if (data.lastEnqueuedOffset != undefined) { + msg.delivery_annotations.last_enqueued_offset = data.lastEnqueuedOffset; + } + if (data.lastSequenceNumber != undefined) { + msg.delivery_annotations.last_enqueued_sequence_number = data.lastSequenceNumber; + } + if (data.lastEnqueuedTime) { + msg.delivery_annotations.last_enqueued_time_utc = data.lastEnqueuedTime.getTime(); + } + if (data.retrievalTime) { + msg.delivery_annotations.runtime_info_retrieval_time_utc = data.retrievalTime.getTime(); + } + return msg; + } + EventData.toAmqpMessage = toAmqpMessage; +})(EventData = exports.EventData || (exports.EventData = {})); diff --git a/client/examples/js/lib/eventHubClient.js b/client/examples/js/lib/eventHubClient.js new file mode 100644 index 0000000..13b604e --- /dev/null +++ b/client/examples/js/lib/eventHubClient.js @@ -0,0 +1,295 @@ +"use strict"; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const debugModule = require("debug"); +const rhea_promise_1 = require("./rhea-promise"); +const ms_rest_azure_1 = require("ms-rest-azure"); +const _1 = require("."); +const rpc = require("./rpc"); +const connectionContext_1 = require("./connectionContext"); +const aad_1 = require("./auth/aad"); +const eventHubSender_1 = require("./eventHubSender"); +const streamingReceiver_1 = require("./streamingReceiver"); +const batchingReceiver_1 = require("./batchingReceiver"); +const debug = debugModule("azure:event-hubs:client"); +/** + * @class EventHubClient + * Describes the EventHub client. + */ +class EventHubClient { + /** + * Instantiate a client pointing to the Event Hub given by this configuration. + * + * @constructor + * @param {ConnectionConfig} config - The connection configuration to create the EventHub Client. + * @param {TokenProvider} [tokenProvider] - The token provider that provides the token for authentication. + * Default value: SasTokenProvider. + */ + constructor(config, tokenProvider) { + this._context = connectionContext_1.ConnectionContext.create(config, tokenProvider); + } + /** + * Closes the AMQP connection to the Event Hub for this client, + * returning a promise that will be resolved when disconnection is completed. + * @method close + * @returns {Promise} + */ + close() { + return __awaiter(this, void 0, void 0, function* () { + try { + if (this._context.connection) { + // Close all the senders. + for (const sender of Object.values(this._context.senders)) { + yield sender.close(); + } + // Close all the receivers. + for (const receiver of Object.values(this._context.receivers)) { + yield receiver.close(); + } + // Close the cbs session; + yield this._context.cbsSession.close(); + // Close the management session + yield this._context.managementSession.close(); + yield rhea_promise_1.closeConnection(this._context.connection); + debug("Closed the amqp connection '%s' on the client.", this._context.connectionId); + this._context.connection = undefined; + } + } + catch (err) { + const msg = `An error occurred while closing the connection "${this._context.connectionId}": ${JSON.stringify(err)}`; + debug(msg); + throw new Error(msg); + } + }); + } + /** + * Sends the given message to the EventHub. + * + * @method send + * @param {any} data Message to send. Will be sent as UTF8-encoded JSON string. + * @param {string|number} [partitionId] Partition ID to which the event data needs to be sent. This should only be specified + * if you intend to send the event to a specific partition. When not specified EventHub will store the messages in a round-robin + * fashion amongst the different partitions in the EventHub. + * + * @returns {Promise} Promise + */ + send(data, partitionId) { + return __awaiter(this, void 0, void 0, function* () { + const sender = eventHubSender_1.EventHubSender.create(this._context, partitionId); + return yield sender.send(data); + }); + } + /** + * Send a batch of EventData to the EventHub. The "message_annotations", "application_properties" and "properties" + * of the first message will be set as that of the envelope (batch message). + * + * @method sendBatch + * @param {Array} datas An array of EventData objects to be sent in a Batch message. + * @param {string|number} [partitionId] Partition ID to which the event data needs to be sent. This should only be specified + * if you intend to send the event to a specific partition. When not specified EventHub will store the messages in a round-robin + * fashion amongst the different partitions in the EventHub. + * + * @return {Promise} Promise + */ + sendBatch(datas, partitionId) { + return __awaiter(this, void 0, void 0, function* () { + const sender = eventHubSender_1.EventHubSender.create(this._context, partitionId); + return yield sender.sendBatch(datas); + }); + } + /** + * Starts the receiver by establishing an AMQP session and an AMQP receiver link on the session. Messages will be passed to + * the provided onMessage handler and error will be passes to the provided onError handler. + * + * @param {string|number} partitionId Partition ID from which to receive. + * @param {OnMessage} onMessage The message handler to receive event data objects. + * @param {OnError} onError The error handler to receive an error that occurs + * while receiving messages. + * @param {ReceiveOptions} [options] Options for how you'd like to connect. + * @param {string} [name] The name of the receiver. If not provided + * then we will set a GUID by default. + * @param {string} [options.consumerGroup] Consumer group from which to receive. + * @param {number} [options.prefetchCount] The upper limit of events this receiver will + * actively receive regardless of whether a receive operation is pending. + * @param {boolean} [options.enableReceiverRuntimeMetric] Provides the approximate receiver runtime information + * for a logical partition of an Event Hub if the value is true. Default false. + * @param {number} [options.epoch] The epoch value that this receiver is currently + * using for partition ownership. A value of undefined means this receiver is not an epoch-based receiver. + * @param {EventPosition} [options.eventPosition] The position of EventData in the EventHub parition from + * where the receiver should start receiving. Only one of offset, sequenceNumber, enqueuedTime, customFilter can be specified. + * `EventPosition.withCustomFilter()` should be used if you want more fine-grained control of the filtering. + * See https://github.com/Azure/amqpnetlite/wiki/Azure%20Service%20Bus%20Event%20Hubs for details. + * + * @returns {ReceiveHandler} ReceiveHandler - An object that provides a mechanism to stop receiving more messages. + */ + receiveOnMessage(partitionId, onMessage, onError, options) { + if (!partitionId || (partitionId && typeof partitionId !== "string" && typeof partitionId !== "number")) { + throw new Error("'partitionId' is a required parameter and must be of type: 'string' | 'number'."); + } + const sReceiver = streamingReceiver_1.StreamingReceiver.create(this._context, partitionId, options); + this._context.receivers[sReceiver.name] = sReceiver; + sReceiver.receiveOnMessage(onMessage, onError); + return new streamingReceiver_1.ReceiveHandler(sReceiver); + } + /** + * Receives a batch of EventData objects from an EventHub partition for a given count and a given max wait time in seconds, whichever + * happens first. This method can be used directly after creating the receiver object and **MUST NOT** be used along with the `start()` method. + * + * @param {string|number} partitionId Partition ID from which to receive. + * @param {number} maxMessageCount The maximum message count. Must be a value greater than 0. + * @param {number} [maxWaitTimeInSeconds] The maximum wait time in seconds for which the Receiver should wait + * to receiver the said amount of messages. If not provided, it defaults to 60 seconds. + * @param {ReceiveOptions} [options] Options for how you'd like to connect. + * @param {string} [name] The name of the receiver. If not provided + * then we will set a GUID by default. + * @param {string} [options.consumerGroup] Consumer group from which to receive. + * @param {number} [options.prefetchCount] The upper limit of events this receiver will + * actively receive regardless of whether a receive operation is pending. + * @param {boolean} [options.enableReceiverRuntimeMetric] Provides the approximate receiver runtime information + * for a logical partition of an Event Hub if the value is true. Default false. + * @param {number} [options.epoch] The epoch value that this receiver is currently + * using for partition ownership. A value of undefined means this receiver is not an epoch-based receiver. + * @param {EventPosition} [options.eventPosition] The position of EventData in the EventHub parition from + * where the receiver should start receiving. Only one of offset, sequenceNumber, enqueuedTime, customFilter can be specified. + * `EventPosition.withCustomFilter()` should be used if you want more fine-grained control of the filtering. + * See https://github.com/Azure/amqpnetlite/wiki/Azure%20Service%20Bus%20Event%20Hubs for details. + * + * @returns {Promise} A promise that resolves with an array of EventData objects. + */ + receiveBatch(partitionId, maxMessageCount, maxWaitTimeInSeconds, options) { + return __awaiter(this, void 0, void 0, function* () { + if (!partitionId || (partitionId && typeof partitionId !== "string" && typeof partitionId !== "number")) { + throw new Error("'partitionId' is a required parameter and must be of type: 'string' | 'number'."); + } + const bReceiver = batchingReceiver_1.BatchingReceiver.create(this._context, partitionId, options); + let error; + let result = []; + try { + result = yield bReceiver.receive(maxMessageCount, maxWaitTimeInSeconds); + } + catch (err) { + error = err; + debug("[%s] Receiver '%s', an error occurred while receiving %d messages for %d max time:\n %O", this._context.connectionId, bReceiver.name, maxMessageCount, maxWaitTimeInSeconds, err); + } + try { + yield bReceiver.close(); + } + catch (err) { + // do nothing about it. + } + if (error) { + throw error; + } + return result; + }); + } + /** + * Provides the eventhub runtime information. + * @method getHubRuntimeInformation + * @returns {Promise} + */ + getHubRuntimeInformation() { + return __awaiter(this, void 0, void 0, function* () { + try { + yield rpc.open(this._context); + return yield this._context.managementSession.getHubRuntimeInformation(this._context.connection); + } + catch (err) { + debug("An error occurred while getting the hub runtime information: %O", err); + throw err; + } + }); + } + /** + * Provides an array of partitionIds. + * @method getPartitionIds + * @returns {Promise>} + */ + getPartitionIds() { + return __awaiter(this, void 0, void 0, function* () { + try { + const runtimeInfo = yield this.getHubRuntimeInformation(); + return runtimeInfo.partitionIds; + } + catch (err) { + debug("An error occurred while getting the partition ids: %O", err); + throw err; + } + }); + } + /** + * Provides information about the specified partition. + * @method getPartitionInformation + * @param {(string|number)} partitionId Partition ID for which partition information is required. + */ + getPartitionInformation(partitionId) { + return __awaiter(this, void 0, void 0, function* () { + if (!partitionId || (partitionId && typeof partitionId !== "string" && typeof partitionId !== "number")) { + throw new Error("'partitionId' is a required parameter and must be of type: 'string' | 'number'."); + } + try { + yield rpc.open(this._context); + return yield this._context.managementSession.getPartitionInformation(this._context.connection, partitionId); + } + catch (err) { + debug("An error occurred while getting the partition information: %O", err); + throw err; + } + }); + } + /** + * Creates an EventHub Client from connection string. + * @method createFromConnectionString + * @param {string} connectionString - Connection string of the form 'Endpoint=sb://my-servicebus-namespace.servicebus.windows.net/;SharedAccessKeyName=my-SA-name;SharedAccessKey=my-SA-key' + * @param {string} [path] - EventHub path of the form 'my-event-hub-name' + * @param {TokenProvider} [tokenProvider] - An instance of the token provider that provides the token for authentication. Default value: SasTokenProvider. + * @returns {EventHubClient} - An instance of the eventhub client. + */ + static createFromConnectionString(connectionString, path, tokenProvider) { + if (!connectionString || (connectionString && typeof connectionString !== "string")) { + throw new Error("'connectionString' is a required parameter and must be of type: 'string'."); + } + const config = _1.ConnectionConfig.create(connectionString, path); + if (!config.entityPath) { + throw new Error(`Either the connectionString must have "EntityPath=" or you must provide "path", while creating the client`); + } + return new EventHubClient(config, tokenProvider); + } + /** + * Creates an EventHub Client from AADTokenCredentials. + * @method + * @param {string} host - Fully qualified domain name for Event Hubs. Most likely, {yournamespace}.servicebus.windows.net + * @param {string} entityPath - EventHub path of the form 'my-event-hub-name' + * @param {TokenCredentials} credentials - The AAD Token credentials. It can be one of the following: ApplicationTokenCredentials | UserTokenCredentials | DeviceTokenCredentials | MSITokenCredentials. + */ + static createFromAadTokenCredentials(host, entityPath, credentials) { + if (!host || (host && typeof host !== "string")) { + throw new Error("'host' is a required parameter and must be of type: 'string'."); + } + if (!entityPath || (entityPath && typeof entityPath !== "string")) { + throw new Error("'entityPath' is a required parameter and must be of type: 'string'."); + } + if (!credentials || + !(credentials instanceof ms_rest_azure_1.ApplicationTokenCredentials || + credentials instanceof ms_rest_azure_1.UserTokenCredentials || + credentials instanceof ms_rest_azure_1.DeviceTokenCredentials || + credentials instanceof ms_rest_azure_1.MSITokenCredentials)) { + throw new Error("'credentials' is a required parameter and must be an instance of ApplicationTokenCredentials | UserTokenCredentials | DeviceTokenCredentials | MSITokenCredentials."); + } + if (!host.endsWith("/")) + host += "/"; + const connectionString = `Endpoint=sb://${host};SharedAccessKeyName=defaultKeyName;SharedAccessKey=defaultKeyValue`; + const tokenProvider = new aad_1.AadTokenProvider(credentials); + return EventHubClient.createFromConnectionString(connectionString, entityPath, tokenProvider); + } +} +exports.EventHubClient = EventHubClient; diff --git a/client/examples/js/lib/eventHubReceiver.js b/client/examples/js/lib/eventHubReceiver.js new file mode 100644 index 0000000..3f44697 --- /dev/null +++ b/client/examples/js/lib/eventHubReceiver.js @@ -0,0 +1,257 @@ +"use strict"; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const rhea = require("rhea"); +const debugModule = require("debug"); +const uuid = require("uuid/v4"); +const rheaPromise = require("./rhea-promise"); +const rpc = require("./rpc"); +const errors_1 = require("./errors"); +const Constants = require("./util/constants"); +const _1 = require("."); +const utils_1 = require("./util/utils"); +const debug = debugModule("azure:event-hubs:receiver"); +/** + * Describes the EventHubReceiver that will receive event data from EventHub. + * @class EventHubReceiver + */ +class EventHubReceiver { + /** + * Instantiate a new receiver from the AMQP `Receiver`. Used by `EventHubClient`. + * + * @constructor + * @param {EventHubClient} client The EventHub client. + * @param {string} partitionId Partition ID from which to receive. + * @param {ReceiveOptions} [options] Options for how you'd like to connect. + * @param {string} [options.consumerGroup] Consumer group from which to receive. + * @param {number} [options.prefetchCount] The upper limit of events this receiver will + * actively receive regardless of whether a receive operation is pending. + * @param {boolean} [options.enableReceiverRuntimeMetric] Provides the approximate receiver runtime information + * for a logical partition of an Event Hub if the value is true. Default false. + * @param {number} [options.epoch] The epoch value that this receiver is currently + * using for partition ownership. A value of undefined means this receiver is not an epoch-based receiver. + * @param {EventPosition} [options.eventPosition] The position of EventData in the EventHub parition from + * where the receiver should start receiving. Only one of offset, sequenceNumber, enqueuedTime, customFilter can be specified. + * `EventPosition.withCustomFilter()` should be used if you want more fine-grained control of the filtering. + * See https://github.com/Azure/amqpnetlite/wiki/Azure%20Service%20Bus%20Event%20Hubs for details. + */ + constructor(context, partitionId, options) { + /** + * @property {number} [prefetchCount] The number of messages that the receiver can fetch/receive initially. Defaults to 1000. + */ + this.prefetchCount = Constants.defaultPrefetchCount; + /** + * @property {boolean} receiverRuntimeMetricEnabled Indicates whether receiver runtime metric is enabled. Default: false. + */ + this.receiverRuntimeMetricEnabled = false; + if (!options) + options = {}; + this._context = context; + this.name = options.name || uuid(); + this.partitionId = partitionId; + this.consumerGroup = options.consumerGroup ? options.consumerGroup : Constants.defaultConsumerGroup; + this.address = `${this._context.config.entityPath}/ConsumerGroups/${this.consumerGroup}/Partitions/${this.partitionId}`; + this.audience = `${this._context.config.endpoint}${this.address}`; + this.prefetchCount = options.prefetchCount !== undefined && options.prefetchCount !== null ? options.prefetchCount : Constants.defaultPrefetchCount; + this.epoch = options.epoch; + this.identifier = options.identifier; + this.options = options; + this.receiverRuntimeMetricEnabled = options.enableReceiverRuntimeMetric || false; + this.runtimeInfo = { + partitionId: `${partitionId}` + }; + this._onAmqpMessage = (context) => { + const evData = _1.EventData.fromAmqpMessage(context.message); + if (this.receiverRuntimeMetricEnabled && evData) { + this.runtimeInfo.lastSequenceNumber = evData.lastSequenceNumber; + this.runtimeInfo.lastEnqueuedTimeUtc = evData.lastEnqueuedTime; + this.runtimeInfo.lastEnqueuedOffset = evData.lastEnqueuedOffset; + this.runtimeInfo.retrievalTime = evData.retrievalTime; + } + this._onMessage(evData); + }; + this._onAmqpError = (context) => { + const ehError = errors_1.translate(context.receiver.error); + // TODO: Should we retry before calling user's error method? + debug("[%s] An error occurred for Receiver '%s': %O.", this._context.connectionId, this.name, ehError); + this._onError(ehError); + }; + } + /** + * Closes the underlying AMQP receiver. + * @param {boolean} [preserveInContext] Should the receiver be preserved in context. Default value false. + */ + close() { + return __awaiter(this, void 0, void 0, function* () { + if (this._receiver) { + try { + // TODO: should I call _receiver.detach() or _receiver.close()? + // should I also call this._session.close() after closing the reciver + // or can I directly close the session which will take care of closing the receiver as well. + yield rheaPromise.closeReceiver(this._receiver); + // Resetting the mode. + debug("[%s] Deleted the receiver '%s' from the client cache.", this._context.connectionId, this.name); + this._receiver = undefined; + this._session = undefined; + clearTimeout(this._tokenRenewalTimer); + debug("[%s] Receiver '%s', has been closed.", this._context.connectionId, this.name); + } + catch (err) { + debug("An error occurred while closing the receiver %s %O", this.name, errors_1.translate(err)); + } + } + }); + } + /** + * Creates a new AMQP receiver under a new AMQP session. + * @returns {Promise} + */ + _init(onAmqpMessage, onAmqpError) { + return __awaiter(this, void 0, void 0, function* () { + try { + // Acquire the lock and establish an amqp connection if it does not exist. + if (!this._context.connection) { + debug("[%s] EH Receiver '%s' establishing AMQP connection.", this._context.connectionId, this.name); + yield utils_1.defaultLock.acquire(this._context.connectionLock, () => { return rpc.open(this._context); }); + } + if (!this._isOpen()) { + yield this._negotiateClaim(); + if (!onAmqpMessage) { + onAmqpMessage = this._onAmqpMessage; + } + if (!onAmqpError) { + onAmqpError = this._onAmqpError; + } + this._session = yield rheaPromise.createSession(this._context.connection); + debug("[%s] Trying to create receiver '%s'...", this._context.connectionId, this.name); + const rcvrOptions = this._createReceiverOptions(); + this._receiver = yield rheaPromise.createReceiverWithHandlers(this._session, onAmqpMessage, onAmqpError, rcvrOptions); + debug("Promise to create the receiver resolved. Created receiver with name: ", this.name); + debug("[%s] Receiver '%s' created with receiver options: %O", this._context.connectionId, this.name, rcvrOptions); + // It is possible for someone to close the receiver and then start it again. + // Thus make sure that the receiver is present in the client cache. + if (!this._context.receivers[this.name]) + this._context.receivers[this.name] = this; + yield this._ensureTokenRenewal(); + } + } + catch (err) { + err = errors_1.translate(err); + debug("[%s] An error occured while creating the receiver '%s': %O", this._context.connectionId, this.name, err); + throw err; + } + }); + } + /** + * Determines whether the AMQP receiver link is open. If open then returns true else returns false. + * @protected + * + * @return {boolean} boolean + */ + _isOpen() { + let result = false; + if (this._session && this._receiver) { + if (this._receiver.is_open && this._receiver.is_open()) { + result = true; + } + } + return result; + } + /** + * Creates the options that need to be specified while creating an AMQP receiver link. + * @private + */ + _createReceiverOptions() { + const rcvrOptions = { + name: this.name, + autoaccept: true, + source: { + address: this.address + }, + credit_window: this.prefetchCount, + }; + if (this.epoch !== undefined && this.epoch !== null) { + if (!rcvrOptions.properties) + rcvrOptions.properties = {}; + rcvrOptions.properties[Constants.attachEpoch] = rhea.types.wrap_long(this.epoch); + } + if (this.identifier) { + if (!rcvrOptions.properties) + rcvrOptions.properties = {}; + rcvrOptions.properties[Constants.receiverIdentifierName] = this.identifier; + } + if (this.receiverRuntimeMetricEnabled) { + rcvrOptions.desired_capabilities = Constants.enableReceiverRuntimeMetricName; + } + if (this.options && this.options.eventPosition) { + // Set filter on the receiver if event position is specified. + const filterClause = this.options.eventPosition.getExpression(); + if (filterClause) { + rcvrOptions.source.filter = { + "apache.org:selector-filter:string": rhea.types.wrap_described(filterClause, 0x468C00000004) + }; + } + } + return rcvrOptions; + } + /** + * Negotiates the cbs claim for the EventHubReceiver. + * @private + * @param {boolean} [setTokenRenewal] Set the token renewal timer. Default false. + * @return {Promise} Promise + */ + _negotiateClaim(setTokenRenewal) { + return __awaiter(this, void 0, void 0, function* () { + // Acquire the lock and establish a cbs session if it does not exist on the connection. Although node.js + // is single threaded, we need a locking mechanism to ensure that a race condition does not happen while + // creating a shared resource (in this case the cbs session, since we want to have exactly 1 cbs session + // per connection). + debug("Acquiring lock: '%s' for creating the cbs session while creating the receiver: ${this.name}.", this._context.connectionId, this._context.cbsSession.cbsLock, this.name); + // Acquire the lock and establish a cbs session if it does not exist on the connection. + yield utils_1.defaultLock.acquire(this._context.cbsSession.cbsLock, () => { return this._context.cbsSession.init(this._context.connection); }); + const tokenObject = yield this._context.tokenProvider.getToken(this.audience); + debug("[%s] EH Receiver '%s': calling negotiateClaim for audience '%s'.", this._context.connectionId, this.audience); + // Acquire the lock to negotiate the CBS claim. + debug("[%s] Acquiring lock: '%s' for cbs auth for receiver: '%s'.", this._context.connectionId, this._context.negotiateClaimLock, this.name); + yield utils_1.defaultLock.acquire(this._context.negotiateClaimLock, () => { + return this._context.cbsSession.negotiateClaim(this.audience, this._context.connection, tokenObject); + }); + debug("[%s] Negotiated claim for receiver '%s' with with partition '%s'", this._context.connectionId, this.name, this.partitionId); + if (setTokenRenewal) { + yield this._ensureTokenRenewal(); + } + }); + } + /** + * Ensures that the token is renewed within the predefined renewal margin. + * @private + * @return {Promise} Promise + */ + _ensureTokenRenewal() { + return __awaiter(this, void 0, void 0, function* () { + const tokenValidTimeInSeconds = this._context.tokenProvider.tokenValidTimeInSeconds; + const tokenRenewalMarginInSeconds = this._context.tokenProvider.tokenRenewalMarginInSeconds; + const nextRenewalTimeout = (tokenValidTimeInSeconds - tokenRenewalMarginInSeconds) * 1000; + this._tokenRenewalTimer = setTimeout(() => __awaiter(this, void 0, void 0, function* () { + try { + yield this._negotiateClaim(true); + } + catch (err) { + // TODO: May be add some retries over here before emitting the error. + debug("[%s] Receiver '%s', an error occurred while renewing the token: %O", this._context.connectionId, this.name, errors_1.translate(err)); + } + }), nextRenewalTimeout); + debug("[%s]Receiver '%s', has next token renewal in %d seconds @(%s).", this._context.connectionId, this.name, nextRenewalTimeout / 1000, new Date(Date.now() + nextRenewalTimeout).toString()); + }); + } +} +exports.EventHubReceiver = EventHubReceiver; diff --git a/client/examples/js/lib/eventHubSender.js b/client/examples/js/lib/eventHubSender.js new file mode 100644 index 0000000..f648c79 --- /dev/null +++ b/client/examples/js/lib/eventHubSender.js @@ -0,0 +1,365 @@ +"use strict"; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const rhea = require("rhea"); +const debugModule = require("debug"); +const uuid = require("uuid/v4"); +const errors_1 = require("./errors"); +const rpc = require("./rpc"); +const rheaPromise = require("./rhea-promise"); +const eventData_1 = require("./eventData"); +const utils_1 = require("./util/utils"); +const retry_1 = require("./retry"); +const debug = debugModule("azure:event-hubs:sender"); +/** + * Instantiates a new sender from the AMQP `Sender`. Used by `EventHubClient`. + * + * @param {any} session - The amqp session on which the amqp sender link was created. + * @param {any} sender - The amqp sender link. + * @constructor + */ +class EventHubSender { + /** + * Creates a new EventHubSender instance. + * @constructor + * @param {EventHubClient} client The EventHub client. + * @param {string|number} [partitionId] The EventHub partition id to which the sender + * wants to send the event data. + */ + constructor(context, partitionId, name) { + this.senderLock = `sender-${uuid()}`; + this._context = context; + this.name = name || uuid(); + this.address = this._context.config.entityPath; + this.partitionId = partitionId; + if (this.partitionId !== null && this.partitionId !== undefined) { + this.address += `/Partitions/${this.partitionId}`; + } + this.audience = `${this._context.config.endpoint}${this.address}`; + } + /** + * Sends the given message, with the given options on this link + * + * @method send + * @param {any} data Message to send. Will be sent as UTF8-encoded JSON string. + * @returns {Promise} Promise + */ + send(data) { + return __awaiter(this, void 0, void 0, function* () { + try { + if (!data || (data && typeof data !== "object")) { + throw new Error("data is required and it must be of type object."); + } + if (!this._isOpen()) { + debug("Acquiring lock %s for initializing the session, sender and " + + "possibly the connection.", this.senderLock); + yield utils_1.defaultLock.acquire(this.senderLock, () => { return this._init(); }); + } + const message = eventData_1.EventData.toAmqpMessage(data); + return yield this._trySend(message); + } + catch (err) { + debug("An error occurred while sending the message %O", err); + throw err; + } + }); + } + /** + * Send a batch of EventData to the EventHub. The "message_annotations", + * "application_properties" and "properties" of the first message will be set as that + * of the envelope (batch message). + * @param {Array} datas An array of EventData objects to be sent in a Batch message. + * @return {Promise} Promise + */ + sendBatch(datas) { + return __awaiter(this, void 0, void 0, function* () { + try { + if (!datas || (datas && !Array.isArray(datas))) { + throw new Error("data is required and it must be an Array."); + } + if (!this._isOpen()) { + debug("Acquiring lock %s for initializing the session, sender and " + + "possibly the connection.", this.senderLock); + yield utils_1.defaultLock.acquire(this.senderLock, () => { return this._init(); }); + } + debug("[%s] Sender '%s', trying to send EventData[]: %O", this._context.connectionId, this.name, datas); + const messages = []; + // Convert EventData to AmqpMessage. + for (let i = 0; i < datas.length; i++) { + const message = eventData_1.EventData.toAmqpMessage(datas[i]); + messages[i] = message; + } + // Encode every amqp message and then convert every encoded message to amqp data section + const batchMessage = { + body: rhea.message.data_sections(messages.map(rhea.message.encode)) + }; + // Set message_annotations, application_properties and properties of the first message as + // that of the envelope (batch message). + if (messages[0].message_annotations) { + batchMessage.message_annotations = messages[0].message_annotations; + } + if (messages[0].application_properties) { + batchMessage.application_properties = messages[0].application_properties; + } + for (const prop of eventData_1.messageProperties) { + if (messages[0][prop]) { + batchMessage[prop] = messages[0][prop]; + } + } + // Finally encode the envelope (batch message). + const encodedBatchMessage = rhea.message.encode(batchMessage); + debug("[%s]Sender '%s', sending encoded batch message.", this._context.connectionId, this.name, encodedBatchMessage); + return yield this._trySend(encodedBatchMessage, undefined, 0x80013700); + } + catch (err) { + debug("An error occurred while sending the batch message %O", err); + throw err; + } + }); + } + /** + * "Unlink" this sender, closing the link and resolving when that operation is complete. + * Leaves the underlying connection open. + * @method close + * @return {Promise} Promise + */ + close() { + return __awaiter(this, void 0, void 0, function* () { + if (this._sender) { + try { + yield rheaPromise.closeSender(this._sender); + delete this._context.senders[this.name]; + debug("[%s] Deleted the sender '%s' from the client cache.", this._context.connectionId, this.name); + this._sender = undefined; + this._session = undefined; + clearTimeout(this._tokenRenewalTimer); + debug("[%s]Sender '%s' closed.", this._context.connectionId, this.name); + } + catch (err) { + debug("An error occurred while closing the sender %O", err); + throw err; + } + } + }); + } + _createSenderOptions() { + const options = { + name: this.name, + target: { + address: this.address + } + }; + debug("Creating sender with options: %O", options); + return options; + } + /** + * Tries to send the message to EventHub if there is enough credit to send them + * and the circular buffer has available space to settle the message after sending them. + * + * We have implemented a synchronous send over here in the sense that we shall be waiting + * for the message to be accepted or rejected and accordingly resolve or reject the promise. + * + * @param message The message to be sent to EventHub. + * @return {Promise} Promise + */ + _trySend(message, tag, format) { + const sendEventPromise = new Promise((resolve, reject) => { + debug("[%s] Sender '%s', credit: %d available: %d", this._context.connectionId, this.name, this._sender.credit, this._sender.session.outgoing.available()); + if (this._sender.sendable()) { + debug("[%s] Sender '%s', sending message: %O", this._context.connectionId, this.name, message); + let onRejected; + let onReleased; + let onModified; + let onAccepted; + const removeListeners = () => { + this._sender.removeListener("rejected", onRejected); + this._sender.removeListener("accepted", onAccepted); + this._sender.removeListener("released", onReleased); + this._sender.removeListener("modified", onModified); + }; + onAccepted = (context) => { + // Since we will be adding listener for accepted and rejected event every time + // we send a message, we need to remove listener for both the events. + // This will ensure duplicate listeners are not added for the same event. + removeListeners(); + debug("[%s] Sender '%s', got event accepted.", this._context.connectionId, this.name); + resolve(context.delivery); + }; + onRejected = (context) => { + removeListeners(); + debug("[%s] Sender '%s', got event rejected.", this._context.connectionId, this.name); + reject(errors_1.translate(context.delivery.remote_state.error)); + }; + onReleased = (context) => { + removeListeners(); + debug("[%s] Sender '%s', got event released.", this._context.connectionId, this.name); + let err; + if (context.delivery.remote_state.error) { + err = errors_1.translate(context.delivery.remote_state.error); + } + else { + err = new Error(`[${this._context.connectionId}] Sender '${this.name}', ` + + `received a release disposition. Hence we are rejecting the promise.`); + } + reject(err); + }; + onModified = (context) => { + removeListeners(); + debug("[%s] Sender '%s', got event modified.", this._context.connectionId, this.name); + let err; + if (context.delivery.remote_state.error) { + err = errors_1.translate(context.delivery.remote_state.error); + } + else { + err = new Error(`[${this._context.connectionId}] Sender "${this.name}", ` + + `received a modified disposition. Hence we are rejecting the promise.`); + } + reject(err); + }; + this._sender.on("accepted", onAccepted); + this._sender.on("rejected", onRejected); + this._sender.on("modified", onModified); + this._sender.on("released", onReleased); + const delivery = this._sender.send(message, tag, format); + debug("[%s] Sender '%s', sent message with delivery id: %d", this._context.connectionId, this.name, delivery.id); + } + else { + const msg = `[${this._context.connectionId}] Sender "${this.name}", ` + + `cannot send the message right now. Please try later.`; + debug(msg); + reject(new Error(msg)); + } + }); + return retry_1.retry(() => sendEventPromise); + } + /** + * Determines whether the AMQP sender link is open. If open then returns true else returns false. + * @private + * + * @return {boolean} boolean + */ + _isOpen() { + let result = false; + if (this._session && this._sender) { + if (this._sender.is_open && this._sender.is_open()) { + result = true; + } + } + return result; + } + /** + * Initializes the sender session on the connection. + * @returns {Promise} + */ + _init() { + return __awaiter(this, void 0, void 0, function* () { + try { + // Acquire the lock and establish an amqp connection if it does not exist. + if (!this._context.connection) { + debug("[%s] EH Sender '%s' establishing an AMQP connection.", this._context.connectionId, this.name); + yield utils_1.defaultLock.acquire(this._context.connectionLock, () => { return rpc.open(this._context); }); + } + if (!this._session && !this._sender) { + yield this._negotiateClaim(); + const onAmqpError = (context) => { + const senderError = errors_1.translate(context.sender.error); + // TODO: Should we retry before calling user's error method? + debug("[%s] An error occurred for sender '%s': %O.", this._context.connectionId, this.name, senderError); + }; + this._session = yield rheaPromise.createSession(this._context.connection); + debug("[%s] Trying to create sender '%s'...", this._context.connectionId, this.name); + const options = this._createSenderOptions(); + this._sender = yield rheaPromise.createSenderWithHandlers(this._session, onAmqpError, options); + debug("[%s] Promise to create the sender resolved. Created sender with name: %s", this._context.connectionId, this.name); + debug("[%s] Sender '%s' created with sender options: %O", this._context.connectionId, this.name, options); + // It is possible for someone to close the sender and then start it again. + // Thus make sure that the sender is present in the client cache. + if (!this._context.senders[this.address]) + this._context.senders[this.address] = this; + yield this._ensureTokenRenewal(); + } + } + catch (err) { + err = errors_1.translate(err); + debug("[%s] An error occurred while creating the sender %s", this._context.connectionId, this.name, err); + throw err; + } + }); + } + /** + * Negotiates the cbs claim for the EventHub Sender. + * @private + * @param {boolean} [setTokenRenewal] Set the token renewal timer. Default false. + * @return {Promise} Promise + */ + _negotiateClaim(setTokenRenewal) { + return __awaiter(this, void 0, void 0, function* () { + // Acquire the lock and establish a cbs session if it does not exist on the connection. + // Although node.js is single threaded, we need a locking mechanism to ensure that a + // race condition does not happen while creating a shared resource (in this case the + // cbs session, since we want to have exactly 1 cbs session per connection). + debug("[%s] Acquiring lock: '%s' for creating the cbs session while creating the sender: '%s'.", this._context.connectionId, this._context.cbsSession.cbsLock, this.name); + yield utils_1.defaultLock.acquire(this._context.cbsSession.cbsLock, () => { return this._context.cbsSession.init(this._context.connection); }); + const tokenObject = yield this._context.tokenProvider.getToken(this.audience); + debug("[%s] EH Sender: calling negotiateClaim for audience '%s'.", this._context.connectionId, this.audience); + // Acquire the lock to negotiate the CBS claim. + debug("[%s] Acquiring lock: '%s' for cbs auth for sender: '%s'.", this._context.connectionId, this._context.negotiateClaimLock, this.name); + yield utils_1.defaultLock.acquire(this._context.negotiateClaimLock, () => { + return this._context.cbsSession.negotiateClaim(this.audience, this._context.connection, tokenObject); + }); + debug("[%s] Negotiated claim for sender '%s' with with partition: %s", this._context.connectionId, this.partitionId); + if (setTokenRenewal) { + yield this._ensureTokenRenewal(); + } + }); + } + /** + * Ensures that the token is renewed within the predefined renewal margin. + * @private + * @returns {void} + */ + _ensureTokenRenewal() { + return __awaiter(this, void 0, void 0, function* () { + const tokenValidTimeInSeconds = this._context.tokenProvider.tokenValidTimeInSeconds; + const tokenRenewalMarginInSeconds = this._context.tokenProvider.tokenRenewalMarginInSeconds; + const nextRenewalTimeout = (tokenValidTimeInSeconds - tokenRenewalMarginInSeconds) * 1000; + this._tokenRenewalTimer = setTimeout(() => __awaiter(this, void 0, void 0, function* () { + try { + yield this._negotiateClaim(true); + } + catch (err) { + // TODO: May be add some retries over here before emitting the error. + debug("[%s] Sender '%s', an error occurred while renewing the token: %O", this._context.connectionId, this.name, err); + } + }), nextRenewalTimeout); + debug("[%s]Sender '%s', has next token renewal in %d seconds @(%s).", nextRenewalTimeout / 1000, new Date(Date.now() + nextRenewalTimeout).toString()); + }); + } + /** + * Creates a new sender to the given event hub, and optionally to a given partition if it is + * not present in the context or returns the one present in the context. + * @static + * @param {(string|number)} [partitionId] Partition ID to which it will send event data. + * @returns {Promise} + */ + static create(context, partitionId) { + if (partitionId && typeof partitionId !== "string" && typeof partitionId !== "number") { + throw new Error("'partitionId' must be of type: 'string' | 'number'."); + } + const ehSender = new EventHubSender(context, partitionId); + if (!context.senders[ehSender.address]) { + context.senders[ehSender.address] = ehSender; + } + return context.senders[ehSender.address]; + } +} +exports.EventHubSender = EventHubSender; diff --git a/client/examples/js/lib/eventPosition.js b/client/examples/js/lib/eventPosition.js new file mode 100644 index 0000000..ed812bb --- /dev/null +++ b/client/examples/js/lib/eventPosition.js @@ -0,0 +1,116 @@ +"use strict"; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +Object.defineProperty(exports, "__esModule", { value: true }); +const Constants = require("./util/constants"); +const errors_1 = require("./errors"); +const _1 = require("."); +/** + * Represents options can be set during the creation of a event hub receiver. + * Defines a position of an @link~EventData in the event hub partition. + * @class EventPosition + */ +class EventPosition { + constructor(options) { + /** + * @property {boolean} isInclusive Indicates if the current event at the specified offset is + * included or not. It is only applicable if offset is set. Default value: false. + */ + this.isInclusive = false; + if (options) { + this.offset = options.offset; + this.enqueuedTime = options.enqueuedTime; + this.sequenceNumber = options.sequenceNumber; + this.isInclusive = options.isInclusive || false; + this.customFilter = options.customFilter; + } + } + /** + * Gets the expression (filter clause) that needs to be set on the source. + * @return {string} filterExpression + */ + getExpression() { + let result; + // order of preference + if (this.offset) { + result = this.isInclusive ? + `${Constants.offsetAnnotation} >= '${this.offset}'` : + `${Constants.offsetAnnotation} > '${this.offset}'`; + } + else if (this.sequenceNumber) { + result = this.isInclusive ? + `${Constants.sequenceNumberAnnotation} >= '${this.sequenceNumber}'` : + `${Constants.sequenceNumberAnnotation} > '${this.sequenceNumber}'`; + } + else if (this.enqueuedTime) { + const time = (this.enqueuedTime instanceof Date) ? this.enqueuedTime.getTime() : this.enqueuedTime; + result = `${Constants.enqueuedTimeAnnotation} > '${time}'`; + } + else if (this.customFilter) { + result = this.customFilter; + } + if (!result) { + throw errors_1.translate({ condition: _1.ErrorNameConditionMapper.ArgumentError, description: "No starting position was set in the EventPosition." }); + } + return result; + } + /** + * Creates a position at the given offset. + * @param {string} offset The offset of the data relative to the Event Hub partition stream. + * The offset is a marker or identifier for an event within the Event Hubs stream. + * The identifier is unique within a partition of the Event Hubs stream. + * @param {boolean} isInclusive If true, the specified event is included; + * otherwise the next event is returned. Default: false. + * @return {EventPosition} EventPosition + */ + static fromOffset(offset, isInclusive) { + return new EventPosition({ offset: offset, isInclusive: isInclusive }); + } + /** + * Creates a position at the given sequence number. + * @param {number} sequenceNumber The logical sequence number of the event within the partition stream of the Event Hub. + * @param {boolean} isInclusive If true, the specified event is included; + * otherwise the next event is returned. Default false. + * @return {EventPosition} EventPosition + */ + static fromSequenceNumber(sequenceNumber, isInclusive) { + return new EventPosition({ sequenceNumber: sequenceNumber, isInclusive: isInclusive }); + } + /** + * Creates a position at the given enqueued time. + * @param {Date | number} enqueuedTime The enqueue time. This value represents the actual time of enqueuing the message. + * @param {boolean} isInclusive If true, the specified event is included; otherwise the next event is returned. + * @return {EventPosition} EventPosition + */ + static fromEnqueuedTime(enqueuedTime) { + return new EventPosition({ enqueuedTime: enqueuedTime }); + } + /** + * Creates a position based on the given custom filter. + * @param {string} customFilter The cutom filter expression that needs to be applied on the receiver. This should be used + * only when one of the other methods `fromOffset()`, `fromSequenceNumber()`, `fromEnqueuedTime()` is not applicable for + * your scenario. + */ + static withCustomFilter(customFilter) { + return new EventPosition({ customFilter: customFilter }); + } + /** + * Returns the position for the start of a stream. Provide this position in receiver creation to + * start receiving from the first available event in the partition. + * @return {EventPosition} EventPosition + */ + static fromStart() { + return EventPosition.fromOffset(EventPosition.startOfStream); + } + /** + * Returns the position for the end of a stream. Provide this position in receiver creation to + * start receiving from the next available event in the partition after the receiver is created. + * @return {EventPosition} EventPosition + */ + static fromEnd() { + return EventPosition.fromOffset(EventPosition.endOfStream); + } +} +EventPosition.startOfStream = "-1"; +EventPosition.endOfStream = "@latest"; +exports.EventPosition = EventPosition; diff --git a/client/examples/js/lib/index.js b/client/examples/js/lib/index.js new file mode 100644 index 0000000..768aeef --- /dev/null +++ b/client/examples/js/lib/index.js @@ -0,0 +1,23 @@ +"use strict"; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +Object.defineProperty(exports, "__esModule", { value: true }); +var eventData_1 = require("./eventData"); +exports.EventData = eventData_1.EventData; +var connectionConfig_1 = require("./connectionConfig"); +exports.ConnectionConfig = connectionConfig_1.ConnectionConfig; +var errors_1 = require("./errors"); +exports.EventHubsError = errors_1.EventHubsError; +exports.ErrorNameConditionMapper = errors_1.ErrorNameConditionMapper; +exports.ConditionStatusMapper = errors_1.ConditionStatusMapper; +exports.ConditionErrorNameMapper = errors_1.ConditionErrorNameMapper; +var eventHubClient_1 = require("./eventHubClient"); +exports.EventHubClient = eventHubClient_1.EventHubClient; +var eventPosition_1 = require("./eventPosition"); +exports.EventPosition = eventPosition_1.EventPosition; +var token_1 = require("./auth/token"); +exports.TokenType = token_1.TokenType; +var constants_1 = require("./util/constants"); +exports.aadEventHubsAudience = constants_1.aadEventHubsAudience; +var utils_1 = require("./util/utils"); +exports.delay = utils_1.delay; diff --git a/client/examples/js/lib/managementClient.js b/client/examples/js/lib/managementClient.js new file mode 100644 index 0000000..b74c031 --- /dev/null +++ b/client/examples/js/lib/managementClient.js @@ -0,0 +1,163 @@ +"use strict"; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const uuid = require("uuid/v4"); +const rheaPromise = require("./rhea-promise"); +const Constants = require("./util/constants"); +const debugModule = require("debug"); +const rpc_1 = require("./rpc"); +const utils_1 = require("./util/utils"); +const Buffer = require("buffer/").Buffer; +const debug = debugModule("azure:event-hubs:management"); +/** + * @class ManagementClient + * Descibes the EventHubs Management Client that talks + * to the $management endpoint over AMQP connection. + */ +class ManagementClient { + /** + * @constructor + * Instantiates the management client. + * @param entityPath - The name/path of the entity (hub name) for which the management request needs to be made. + */ + constructor(entityPath) { + this.entityPath = entityPath; + this.managementLock = `${Constants.managementRequestKey}-${uuid()}`; + this.entityPath = entityPath; + } + /** + * Provides the eventhub runtime information. + * @method getHubRuntimeInformation + * @param {Connection} connection - The established amqp connection + * @returns {Promise} + */ + getHubRuntimeInformation(connection) { + return __awaiter(this, void 0, void 0, function* () { + const info = yield this._makeManagementRequest(connection, Constants.eventHub); + const runtimeInfo = { + path: info.name, + createdAt: new Date(info.created_at), + partitionCount: info.partition_count, + partitionIds: info.partition_ids, + type: info.type + }; + debug("[%s] The hub runtime info is: %O", connection.options.id, runtimeInfo); + return runtimeInfo; + }); + } + /** + * Provides an array of partitionIds. + * @method getPartitionIds + * @param {Connection} connection - The established amqp connection + * @returns {Promise>} + */ + getPartitionIds(connection) { + return __awaiter(this, void 0, void 0, function* () { + const runtimeInfo = yield this.getHubRuntimeInformation(connection); + return runtimeInfo.partitionIds; + }); + } + /** + * Provides information about the specified partition. + * @method getPartitionInformation + * @param {Connection} connection - The established amqp connection + * @param {(string|number)} partitionId Partition ID for which partition information is required. + */ + getPartitionInformation(connection, partitionId) { + return __awaiter(this, void 0, void 0, function* () { + if (!partitionId || (partitionId && typeof partitionId !== "string" && typeof partitionId !== "number")) { + throw new Error("'partitionId' is a required parameter and must be of type: 'string' | 'number'."); + } + const info = yield this._makeManagementRequest(connection, Constants.partition, partitionId); + const partitionInfo = { + beginningSequenceNumber: info.begin_sequence_number, + hubPath: info.name, + lastEnqueuedOffset: info.last_enqueued_offset, + lastEnqueuedTimeUtc: new Date(info.last_enqueued_time_utc), + lastSequenceNumber: info.last_enqueued_sequence_number, + partitionId: info.partition, + type: info.type + }; + debug("[%s] The partition info is: %O.", connection.options.id, partitionInfo); + return partitionInfo; + }); + } + /** + * Closes the AMQP management session to the Event Hub for this client, + * returning a promise that will be resolved when disconnection is completed. + * @return {Promise} + */ + close() { + return __awaiter(this, void 0, void 0, function* () { + try { + if (this._mgmtReqResLink) { + yield rheaPromise.closeSession(this._mgmtReqResLink.session); + debug("Successfully closed the management session."); + this._mgmtReqResLink = undefined; + } + } + catch (err) { + const msg = `An error occurred while closing the management session: ${err}`; + debug(msg); + throw new Error(msg); + } + }); + } + _init(connection, endpoint, replyTo) { + return __awaiter(this, void 0, void 0, function* () { + if (!this._mgmtReqResLink) { + const rxopt = { source: { address: endpoint }, name: replyTo, target: { address: replyTo } }; + debug("Creating a session for $management endpoint"); + this._mgmtReqResLink = yield rpc_1.createRequestResponseLink(connection, { target: { address: endpoint } }, rxopt); + debug("[%s] Created sender '%s' and receiver '%s' links for $management endpoint.", connection.options.id, this._mgmtReqResLink.sender.name, this._mgmtReqResLink.receiver.name); + } + }); + } + /** + * @private + * Helper method to make the management request + * @param {Connection} connection - The established amqp connection + * @param {string} type - The type of entity requested for. Valid values are "eventhub", "partition" + * @param {string | number} [partitionId] - The partitionId. Required only when type is "partition". + */ + _makeManagementRequest(connection, type, partitionId) { + return __awaiter(this, void 0, void 0, function* () { + if (partitionId && typeof partitionId !== "string" && typeof partitionId !== "number") { + throw new Error("'partitionId' is a required parameter and must be of type: 'string' | 'number'."); + } + try { + const endpoint = Constants.management; + const replyTo = uuid(); + const request = { + body: Buffer.from(JSON.stringify([])), + message_id: uuid(), + reply_to: replyTo, + application_properties: { + operation: Constants.readOperation, + name: this.entityPath, + type: `${Constants.vendorString}:${type}` + } + }; + if (partitionId && type === Constants.partition) { + request.application_properties.partition = partitionId; + } + yield utils_1.defaultLock.acquire(this.managementLock, () => { return this._init(connection, endpoint, replyTo); }); + return rpc_1.sendRequest(connection, this._mgmtReqResLink, request); + } + catch (err) { + debug("An error occurred while making the request to $management endpoint: %O", err); + throw err; + } + }); + } +} +exports.ManagementClient = ManagementClient; diff --git a/client/examples/js/lib/retry.js b/client/examples/js/lib/retry.js new file mode 100644 index 0000000..949a4c3 --- /dev/null +++ b/client/examples/js/lib/retry.js @@ -0,0 +1,89 @@ +"use strict"; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const errors_1 = require("./errors"); +const _1 = require("."); +const debugModule = require("debug"); +const debug = debugModule("azure:event-hubs:retry"); +function isDelivery(obj) { + let result = false; + if (obj && typeof obj.id === "number" && typeof obj.settled === "boolean" && + typeof obj.remote_settled === "boolean" && typeof obj.format === "number") { + result = true; + } + return result; +} +/** + * It will attempt to linearly retry an operation specified number of times with a specified + * delay in between each retry. The retries will only happen if the error is retryable. + * + * @param {Promise} operation The operation that needs to be retried. + * @param {number} [times] Number of times the operation needs to be retried in case of error. Default: 3. + * @param {number} [delayInSeconds] Amount of time to wait in seconds before making the next attempt. Default: 15. + * + * @return {Promise} Promise. + */ +function retry(operation, times, delayInSeconds) { + return __awaiter(this, void 0, void 0, function* () { + if (!operation || typeof operation !== "function") { + throw new Error("'operation' is a required parameter and must be of type 'function'."); + } + if (times && typeof times !== "number") { + throw new Error("'times' must be of type 'number'."); + } + if (delayInSeconds && typeof delayInSeconds !== "number") { + throw new Error("'delayInSeconds' must be of type 'number'."); + } + if (!times) + times = 3; + if (!delayInSeconds) + delayInSeconds = 15; + let lastError; + let result; + let success = false; + for (let i = 0; i < times; i++) { + const j = i + 1; + debug("Retry attempt number: %d", j); + try { + result = yield operation(); + success = true; + debug("Success, after attempt number: %d.", j); + if (!isDelivery(result)) { + debug("Success result: %O", result); + } + break; + } + catch (err) { + if (!err.translated) { + err = errors_1.translate(err); + } + lastError = err; + debug("Error occured in attempt number %d: %O", j, err); + if (lastError.retryable) { + debug("Sleeping for %d seconds.", delayInSeconds); + yield _1.delay(delayInSeconds * 1000); + continue; + } + else { + break; + } + } + } + if (success) { + return result; + } + else { + throw lastError; + } + }); +} +exports.retry = retry; diff --git a/client/examples/js/lib/rhea-promise/index.js b/client/examples/js/lib/rhea-promise/index.js new file mode 100644 index 0000000..867509a --- /dev/null +++ b/client/examples/js/lib/rhea-promise/index.js @@ -0,0 +1,436 @@ +"use strict"; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +Object.defineProperty(exports, "__esModule", { value: true }); +const rhea = require("rhea"); +const debugModule = require("debug"); +const debug = debugModule("rhea-promise"); +/** + * Establishes an amqp connection. + * @param {ConnectionOptions} [options] Options to be provided for establishing an amqp connection. + * @return {Promise} Promise + * - **Resolves** the promise with the Connection object when rhea emits the "connection_open" event. + * - **Rejects** the promise with an AmqpError when rhea emits the "connection_close" event while trying + * to establish an amqp connection. + */ +function connect(options) { + return new Promise((resolve, reject) => { + const connection = rhea.connect(options); + function removeListeners(connection) { + connection.removeListener("connection_open", onOpen); + connection.removeListener("connection_close", onClose); + connection.removeListener("disconnected", onClose); + } + function onOpen(context) { + removeListeners(connection); + process.nextTick(() => { + debug("Resolving the promise with amqp connection."); + resolve(connection); + }); + } + function onClose(context) { + removeListeners(connection); + debug(`Error occurred while establishing amqp connection.`, context.connection.error); + reject(context.connection.error); + } + connection.once("connection_open", onOpen); + connection.once("connection_close", onClose); + connection.once("disconnected", onClose); + }); +} +exports.connect = connect; +/** + * Closes the amqp connection. + * @param {Connection} connection The amqp connection that needs to be closed. + * @return {Promise} Promise + * - **Resolves** the promise when rhea emits the "connection_close" event. + * - **Rejects** the promise with an AmqpError when rhea emits the "connection_error" event while trying + * to close an amqp connection. + */ +function closeConnection(connection) { + if (!connection || (connection && typeof connection !== "object")) { + throw new Error("connection is a required parameter and must be of type 'object'."); + } + return new Promise((resolve, reject) => { + if (connection.is_open()) { + function onClose(context) { + connection.removeListener("connection_close", onClose); + process.nextTick(() => { + debug("Resolving the promise as the connection has been successfully closed."); + resolve(); + }); + } + function onError(context) { + connection.removeListener("connection_error", onError); + debug(`Error occurred while closing amqp connection.`, context.connection.error); + reject(context.connection.error); + } + connection.once("connection_close", onClose); + connection.once("connection_error", onError); + connection.close(); + } + else { + resolve(); + } + }); +} +exports.closeConnection = closeConnection; +/** + * Creates an amqp session on the provided amqp connection. + * @param {Connection} connection The amqp connection object + * @return {Promise} Promise + * - **Resolves** the promise with the Session object when rhea emits the "session_open" event. + * - **Rejects** the promise with an AmqpError when rhea emits the "session_close" event while trying + * to create an amqp session. + */ +function createSession(connection) { + if (!connection || (connection && typeof connection !== "object")) { + throw new Error("connection is a required parameter and must be of type 'object'."); + } + return new Promise((resolve, reject) => { + const session = connection.create_session(); + function removeListeners(session) { + session.removeListener("session_open", onOpen); + session.removeListener("session_close", onClose); + } + function onOpen(context) { + removeListeners(session); + process.nextTick(() => { + debug("Resolving the promise with amqp session."); + resolve(session); + }); + } + function onClose(context) { + removeListeners(session); + debug(`Error occurred while establishing a session over amqp connection.`, context.session.error); + reject(context.session.error); + } + session.once("session_open", onOpen); + session.once("session_close", onClose); + debug("Calling amqp session.begin()."); + session.begin(); + }); +} +exports.createSession = createSession; +/** + * Closes the amqp session. + * @param {Session} session The amqp session that needs to be closed. + * @return {Promise} Promise + * - **Resolves** the promise when rhea emits the "session_close" event. + * - **Rejects** the promise with an AmqpError when rhea emits the "session_error" event while trying + * to close an amqp session. + */ +function closeSession(session) { + if (!session || (session && typeof session !== "object")) { + throw new Error("session is a required parameter and must be of type 'object'."); + } + return new Promise((resolve, reject) => { + if (session.is_open()) { + function onClose(context) { + session.removeListener("session_close", onClose); + process.nextTick(() => { + debug("Resolving the promise as the amqp session has been closed."); + resolve(); + }); + } + function onError(context) { + session.removeListener("session_error", onError); + debug(`Error occurred while closing amqp session.`, context.session.error); + reject(context.session.error); + } + session.once("session_close", onClose); + session.once("session_error", onError); + session.close(); + } + else { + resolve(); + } + }); +} +exports.closeSession = closeSession; +/** + * Creates an amqp sender on the provided amqp session. + * @param {Session} session The amqp session object on which the sender link needs to be established. + * @param {SenderOptions} [options] Options that can be provided while creating an amqp sender. + * @return {Promise} Promise + * - **Resolves** the promise with the Sender object when rhea emits the "sender_open" event. + * - **Rejects** the promise with an AmqpError when rhea emits the "sender_close" event while trying + * to create an amqp sender. + */ +function createSender(session, options) { + if (!session || (session && typeof session !== "object")) { + throw new Error("session is a required parameter and must be of type 'object'."); + } + return new Promise((resolve, reject) => { + const sender = session.attach_sender(options); + function removeListeners(session) { + sender.removeListener("sendable", onOpen); + sender.removeListener("sender_close", onClose); + } + function onOpen(context) { + removeListeners(session); + process.nextTick(() => { + debug(`Resolving the promise with amqp sender "${sender.name}".`); + resolve(sender); + }); + } + function onClose(context) { + removeListeners(session); + debug(`Error occurred while creating a sender over amqp connection.`, context.sender.error); + reject(context.sender.error); + } + sender.once("sendable", onOpen); + sender.once("sender_close", onClose); + }); +} +exports.createSender = createSender; +/** + * Creates an amqp sender on the provided amqp session. + * @param {Session} session The amqp session object on which the sender link needs to be established. + * @param {OnAmqpEvent} onError The event handler for the "error" event for the sender. + * @param {SenderOptions} [options] Options that can be provided while creating an amqp sender. + * @return {Promise} Promise + * - **Resolves** the promise with the Sender object when rhea emits the "sender_open" event. + * - **Rejects** the promise with an AmqpError when rhea emits the "sender_close" event while trying + * to create an amqp sender. + */ +function createSenderWithHandlers(session, onError, options) { + if (!session || (session && typeof session !== "object")) { + throw new Error("session is a required parameter and must be of type 'object'."); + } + return new Promise((resolve, reject) => { + const sender = session.attach_sender(options); + sender.on("sender_error", onError); + function removeListeners(session) { + sender.removeListener("sendable", onOpen); + sender.removeListener("sender_close", onClose); + } + function onOpen(context) { + removeListeners(session); + process.nextTick(() => { + debug(`Resolving the promise with amqp sender "${sender.name}".`); + resolve(sender); + }); + } + function onClose(context) { + removeListeners(session); + debug(`Error occurred while creating a sender over amqp connection.`, context.sender.error); + reject(context.sender.error); + } + sender.once("sendable", onOpen); + sender.once("sender_close", onClose); + }); +} +exports.createSenderWithHandlers = createSenderWithHandlers; +/** + * Closes the amqp sender. + * @param {Sender} sender The amqp sender that needs to be closed. + * @return {Promise} Promise + * - **Resolves** the promise when rhea emits the "sender_close" event. + * - **Rejects** the promise with an AmqpError when rhea emits the + * "sender_error" event while trying to close an amqp sender. + */ +function closeSender(sender) { + if (!sender || (sender && typeof sender !== "object")) { + throw new Error("sender is a required parameter and must be of type 'object'."); + } + return new Promise((resolve, reject) => { + if (sender.is_open()) { + function onClose(context) { + sender.removeListener("sender_close", onClose); + process.nextTick(() => { + debug("Resolving the promise as the amqp sender has been closed."); + resolve(); + }); + } + function onError(context) { + sender.removeListener("sender_error", onError); + debug(`Error occurred while closing amqp sender.`, context.sender.error); + reject(context.sender.error); + } + sender.once("sender_close", onClose); + sender.once("sender_error", onError); + sender.close(); + } + else { + resolve(); + } + }); +} +exports.closeSender = closeSender; +/** + * Creates an amqp receiver on the provided amqp session. This method should be used when you will be + * sending a request and waiting for a response from the service. For example: This method is useful + * while creating request/response links for $management or $cbs endpoint. + * @param {Session} session The amqp session object on which the receiver link needs to be established. + * @param {ReceiverOptions} [options] Options that can be provided while creating an amqp receiver. + * @return {Promise} Promise + * - **Resolves** the promise with the Receiver object when rhea emits the "receiver_open" event. + * - **Rejects** the promise with an AmqpError when rhea emits the "receiver_close" event while trying + * to create an amqp receiver. + */ +function createReceiver(session, options) { + if (!session || (session && typeof session !== "object")) { + throw new Error("session is a required parameter and must be of type 'object'."); + } + return new Promise((resolve, reject) => { + const receiver = session.attach_receiver(options); + function removeListeners(receiver) { + receiver.removeListener("receiver_open", onOpen); + receiver.removeListener("receiver_close", onClose); + } + function onOpen(context) { + removeListeners(receiver); + process.nextTick(() => { + debug(`Resolving the promise with amqp receiver "${receiver.name}".`); + resolve(receiver); + }); + } + function onClose(context) { + removeListeners(receiver); + debug(`Error occurred while creating a receiver over amqp connection.`, context.receiver.error); + reject(context.receiver.error); + } + receiver.once("receiver_open", onOpen); + receiver.once("receiver_close", onClose); + }); +} +exports.createReceiver = createReceiver; +/** + * Creates an amqp receiver with provided message and error event handlers on the provided amqp session. + * This method should be used when you want to ensure that no messages are lost. For example: This method + * is useful for creating EventHub Receivers where you want to start receiving ASAP. + * @param {Session} session The amqp session object on which the receiver link needs to be established. + * @param {OnAmqpEvent} onMessage The event handler for the "message" event for the receiver. + * @param {OnAmqpEvent} onError The event handler for the "error" event for the receiver. + * @param {ReceiverOptions} [options] Options that can be provided while creating an amqp receiver. + * @return {Promise} Promise + * - **Resolves** the promise with the Receiver object when rhea emits the "receiver_open" event. + * - **Rejects** the promise with an AmqpError when rhea emits the "receiver_close" event while trying + * to create an amqp receiver. + */ +function createReceiverWithHandlers(session, onMessage, onError, options) { + if (!session || (session && typeof session !== "object")) { + throw new Error("session is a required parameter and must be of type 'object'."); + } + if (!onMessage || (onMessage && typeof onMessage !== "function")) { + throw new Error("onMessage is a required parameter and must be of type 'function'."); + } + if (!onError || (onError && typeof onError !== "function")) { + throw new Error("onError is a required parameter and must be of type 'function'."); + } + return new Promise((resolve, reject) => { + const receiver = session.attach_receiver(options); + receiver.on("message", onMessage); + receiver.on("receiver_error", onError); + function removeListeners(receiver) { + receiver.removeListener("receiver_open", onOpen); + receiver.removeListener("receiver_close", onClose); + } + function onOpen(context) { + removeListeners(receiver); + process.nextTick(() => { + debug(`Resolving the promise with amqp receiver "${receiver.name}".`); + resolve(receiver); + }); + } + function onClose(context) { + removeListeners(receiver); + debug(`Error occurred while creating a receiver over amqp connection.`, context.receiver.error); + reject(context.receiver.error); + } + receiver.once("receiver_open", onOpen); + receiver.once("receiver_close", onClose); + }); +} +exports.createReceiverWithHandlers = createReceiverWithHandlers; +/** + * Closes the amqp receiver. + * @param {Receiver} receiver The amqp receiver that needs to be closed. + * @return {Promise} Promise + * - **Resolves** the promise when rhea emits the "receiver_close" event. + * - **Rejects** the promise with an AmqpError when rhea emits the + * "receiver_error" event while trying to close an amqp receiver. + */ +function closeReceiver(receiver) { + if (!receiver || (receiver && typeof receiver !== "object")) { + throw new Error("receiver is a required parameter and must be of type 'object'."); + } + return new Promise((resolve, reject) => { + if (receiver.is_open()) { + function onClose(context) { + receiver.removeListener("receiver_close", onClose); + process.nextTick(() => { + debug("Resolving the promise as the amqp receiver has been closed."); + resolve(); + }); + } + function onError(context) { + receiver.removeListener("receiver_error", onError); + debug(`Error occurred while closing amqp receiver.`, context.receiver.error); + reject(context.receiver.error); + } + receiver.once("receiver_close", onClose); + receiver.once("receiver_error", onError); + receiver.close(); + } + else { + resolve(); + } + }); +} +exports.closeReceiver = closeReceiver; +/** + * Defines a mapping for Http like response status codes for different status-code values provided by an AMQP broker. + * @enum AmqpResponseStatusCode + */ +var AmqpResponseStatusCode; +(function (AmqpResponseStatusCode) { + AmqpResponseStatusCode[AmqpResponseStatusCode["Continue"] = 100] = "Continue"; + AmqpResponseStatusCode[AmqpResponseStatusCode["SwitchingProtocols"] = 101] = "SwitchingProtocols"; + AmqpResponseStatusCode[AmqpResponseStatusCode["OK"] = 200] = "OK"; + AmqpResponseStatusCode[AmqpResponseStatusCode["Created"] = 201] = "Created"; + AmqpResponseStatusCode[AmqpResponseStatusCode["Accepted"] = 202] = "Accepted"; + AmqpResponseStatusCode[AmqpResponseStatusCode["NonAuthoritativeInformation"] = 203] = "NonAuthoritativeInformation"; + AmqpResponseStatusCode[AmqpResponseStatusCode["NoContent"] = 204] = "NoContent"; + AmqpResponseStatusCode[AmqpResponseStatusCode["ResetContent"] = 205] = "ResetContent"; + AmqpResponseStatusCode[AmqpResponseStatusCode["PartialContent"] = 206] = "PartialContent"; + AmqpResponseStatusCode[AmqpResponseStatusCode["Ambiguous"] = 300] = "Ambiguous"; + AmqpResponseStatusCode[AmqpResponseStatusCode["MultipleChoices"] = 300] = "MultipleChoices"; + AmqpResponseStatusCode[AmqpResponseStatusCode["Moved"] = 301] = "Moved"; + AmqpResponseStatusCode[AmqpResponseStatusCode["MovedPermanently"] = 301] = "MovedPermanently"; + AmqpResponseStatusCode[AmqpResponseStatusCode["Found"] = 302] = "Found"; + AmqpResponseStatusCode[AmqpResponseStatusCode["Redirect"] = 302] = "Redirect"; + AmqpResponseStatusCode[AmqpResponseStatusCode["RedirectMethod"] = 303] = "RedirectMethod"; + AmqpResponseStatusCode[AmqpResponseStatusCode["SeeOther"] = 303] = "SeeOther"; + AmqpResponseStatusCode[AmqpResponseStatusCode["NotModified"] = 304] = "NotModified"; + AmqpResponseStatusCode[AmqpResponseStatusCode["UseProxy"] = 305] = "UseProxy"; + AmqpResponseStatusCode[AmqpResponseStatusCode["Unused"] = 306] = "Unused"; + AmqpResponseStatusCode[AmqpResponseStatusCode["RedirectKeepVerb"] = 307] = "RedirectKeepVerb"; + AmqpResponseStatusCode[AmqpResponseStatusCode["TemporaryRedirect"] = 307] = "TemporaryRedirect"; + AmqpResponseStatusCode[AmqpResponseStatusCode["BadRequest"] = 400] = "BadRequest"; + AmqpResponseStatusCode[AmqpResponseStatusCode["Unauthorized"] = 401] = "Unauthorized"; + AmqpResponseStatusCode[AmqpResponseStatusCode["PaymentRequired"] = 402] = "PaymentRequired"; + AmqpResponseStatusCode[AmqpResponseStatusCode["Forbidden"] = 403] = "Forbidden"; + AmqpResponseStatusCode[AmqpResponseStatusCode["NotFound"] = 404] = "NotFound"; + AmqpResponseStatusCode[AmqpResponseStatusCode["MethodNotAllowed"] = 405] = "MethodNotAllowed"; + AmqpResponseStatusCode[AmqpResponseStatusCode["NotAcceptable"] = 406] = "NotAcceptable"; + AmqpResponseStatusCode[AmqpResponseStatusCode["ProxyAuthenticationRequired"] = 407] = "ProxyAuthenticationRequired"; + AmqpResponseStatusCode[AmqpResponseStatusCode["RequestTimeout"] = 408] = "RequestTimeout"; + AmqpResponseStatusCode[AmqpResponseStatusCode["Conflict"] = 409] = "Conflict"; + AmqpResponseStatusCode[AmqpResponseStatusCode["Gone"] = 410] = "Gone"; + AmqpResponseStatusCode[AmqpResponseStatusCode["LengthRequired"] = 411] = "LengthRequired"; + AmqpResponseStatusCode[AmqpResponseStatusCode["PreconditionFailed"] = 412] = "PreconditionFailed"; + AmqpResponseStatusCode[AmqpResponseStatusCode["RequestEntityTooLarge"] = 413] = "RequestEntityTooLarge"; + AmqpResponseStatusCode[AmqpResponseStatusCode["RequestUriTooLong"] = 414] = "RequestUriTooLong"; + AmqpResponseStatusCode[AmqpResponseStatusCode["UnsupportedMediaType"] = 415] = "UnsupportedMediaType"; + AmqpResponseStatusCode[AmqpResponseStatusCode["RequestedRangeNotSatisfiable"] = 416] = "RequestedRangeNotSatisfiable"; + AmqpResponseStatusCode[AmqpResponseStatusCode["ExpectationFailed"] = 417] = "ExpectationFailed"; + AmqpResponseStatusCode[AmqpResponseStatusCode["UpgradeRequired"] = 426] = "UpgradeRequired"; + AmqpResponseStatusCode[AmqpResponseStatusCode["InternalServerError"] = 500] = "InternalServerError"; + AmqpResponseStatusCode[AmqpResponseStatusCode["NotImplemented"] = 501] = "NotImplemented"; + AmqpResponseStatusCode[AmqpResponseStatusCode["BadGateway"] = 502] = "BadGateway"; + AmqpResponseStatusCode[AmqpResponseStatusCode["ServiceUnavailable"] = 503] = "ServiceUnavailable"; + AmqpResponseStatusCode[AmqpResponseStatusCode["GatewayTimeout"] = 504] = "GatewayTimeout"; + AmqpResponseStatusCode[AmqpResponseStatusCode["HttpVersionNotSupported"] = 505] = "HttpVersionNotSupported"; +})(AmqpResponseStatusCode = exports.AmqpResponseStatusCode || (exports.AmqpResponseStatusCode = {})); diff --git a/client/examples/js/lib/rpc.js b/client/examples/js/lib/rpc.js new file mode 100644 index 0000000..75eadd3 --- /dev/null +++ b/client/examples/js/lib/rpc.js @@ -0,0 +1,249 @@ +"use strict"; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const os = require("os"); +const process = require("process"); +const debugModule = require("debug"); +const uuid = require("uuid/v4"); +const utils_1 = require("./util/utils"); +const rhea_promise_1 = require("./rhea-promise"); +const Constants = require("./util/constants"); +const connectionContext_1 = require("./connectionContext"); +const errors_1 = require("./errors"); +const retry_1 = require("./retry"); +const debug = debugModule("azure:event-hubs:rpc"); +function createRequestResponseLink(connection, senderOptions, receiverOptions) { + return __awaiter(this, void 0, void 0, function* () { + if (!connection) { + throw new Error(`Please provide a connection to create the sender/receiver link on the same session.`); + } + if (!senderOptions) { + throw new Error(`Please provide sender options.`); + } + if (!receiverOptions) { + throw new Error(`Please provide receiver options.`); + } + const session = yield rhea_promise_1.createSession(connection); + const [sender, receiver] = yield Promise.all([ + rhea_promise_1.createSender(session, senderOptions), + rhea_promise_1.createReceiver(session, receiverOptions) + ]); + debug("[%s] Successfully created the sender and receiver links on the same session.", connection.options.id); + return { + session: session, + sender: sender, + receiver: receiver + }; + }); +} +exports.createRequestResponseLink = createRequestResponseLink; +function createReceiverLink(connection, receiverOptions) { + return __awaiter(this, void 0, void 0, function* () { + if (!connection) { + throw new Error(`Please provide a connection to create the receiver link on a session.`); + } + if (!receiverOptions) { + throw new Error(`Please provide receiver options.`); + } + const session = yield rhea_promise_1.createSession(connection); + const receiver = yield rhea_promise_1.createReceiver(session, receiverOptions); + debug("[%s] Successfully created the receiver link on a dedicated session for it.", connection.options.id); + return { + session: session, + receiver: receiver + }; + }); +} +exports.createReceiverLink = createReceiverLink; +function createReceiverLinkWithHandlers(options) { + return __awaiter(this, void 0, void 0, function* () { + if (!options.connection) { + throw new Error(`Please provide a connection to create the receiver link on a session.`); + } + if (!options.receiverOptions) { + throw new Error(`Please provide receiver options.`); + } + if (!options.onError) { + throw new Error(`Please provide onError.`); + } + if (!options.onMessage) { + throw new Error(`Please provide onMessage.`); + } + const session = yield rhea_promise_1.createSession(options.connection); + const receiver = yield rhea_promise_1.createReceiverWithHandlers(session, options.onMessage, options.onError, options.receiverOptions); + debug("[%s] Successfully created the receiver link on a dedicated session for it.", options.connection.options.id); + return { + session: session, + receiver: receiver + }; + }); +} +exports.createReceiverLinkWithHandlers = createReceiverLinkWithHandlers; +function createSenderLink(connection, senderOptions) { + return __awaiter(this, void 0, void 0, function* () { + if (!connection) { + throw new Error(`Please provide a connection to create the sender link on a session.`); + } + if (!senderOptions) { + throw new Error(`Please provide sender options.`); + } + const session = yield rhea_promise_1.createSession(connection); + const sender = yield rhea_promise_1.createSender(session, senderOptions); + debug("[%s] Successfully created the sender link on a dedicated session for it.", connection.options.id); + return { + session: session, + sender: sender + }; + }); +} +exports.createSenderLink = createSenderLink; +function createSenderLinkWithHandlers(options) { + return __awaiter(this, void 0, void 0, function* () { + if (!options.connection) { + throw new Error(`Please provide a connection to create the sender link on a session.`); + } + if (!options.senderOptions) { + throw new Error(`Please provide sender options.`); + } + if (!options.onError) { + throw new Error(`Please provide onError.`); + } + const session = yield rhea_promise_1.createSession(options.connection); + const sender = yield rhea_promise_1.createSenderWithHandlers(session, options.onError, options.senderOptions); + debug("[%s] Successfully created the sender link on a dedicated session for it.", options.connection.options.id); + return { + session: session, + sender: sender + }; + }); +} +exports.createSenderLinkWithHandlers = createSenderLinkWithHandlers; +function sendRequest(connection, link, request, timeoutInSeconds) { + if (!connection) { + throw new Error("connection is a required parameter and must be of type 'object'."); + } + if (!link) { + throw new Error("link is a required parameter and must be of type 'object'."); + } + if (!request) { + throw new Error("request is a required parameter and must be of type 'object'."); + } + if (!request.message_id) + request.message_id = uuid(); + if (!timeoutInSeconds) { + timeoutInSeconds = 10; + } + const sendRequestPromise = new Promise((resolve, reject) => { + let waitTimer; + let timeOver = false; + const messageCallback = (context) => { + // remove the event listener as this will be registered next time when someone makes a request. + link.receiver.removeListener(Constants.message, messageCallback); + const code = context.message.application_properties[Constants.statusCode]; + const desc = context.message.application_properties[Constants.statusDescription]; + const errorCondition = context.message.application_properties[Constants.errorCondition]; + const responseCorrelationId = context.message.correlation_id; + debug("[%s] %s response: ", connection.options.id, request.to || "$management", context.message); + if (code > 199 && code < 300) { + if (request.message_id === responseCorrelationId || request.correlation_id === responseCorrelationId) { + if (!timeOver) { + clearTimeout(waitTimer); + } + debug("[%s] request-messageId | '%s' == '%s' | response-correlationId.", connection.options.id, request.message_id, responseCorrelationId); + return resolve(context.message.body); + } + else { + debug("[%s] request-messageId | '%s' != '%s' | response-correlationId. Hence dropping this response and waiting for the next one.", connection.options.id, request.message_id, responseCorrelationId); + } + } + else { + const condition = errorCondition || errors_1.ConditionStatusMapper[code] || "amqp:internal-error"; + const e = { + condition: condition, + description: desc + }; + return reject(errors_1.translate(e)); + } + }; + const actionAfterTimeout = () => { + timeOver = true; + link.receiver.removeListener(Constants.message, messageCallback); + const address = link.receiver.options && link.receiver.options.source && link.receiver.options.source.address + ? link.receiver.options.source.address + : "address"; + const desc = `The request with message_id "${request.message_id}" to "${address}" ` + + `endpoint timed out. Please try again later.`; + const e = { + condition: errors_1.ConditionStatusMapper[408], + description: desc + }; + return reject(errors_1.translate(e)); + }; + link.receiver.on(Constants.message, messageCallback); + waitTimer = setTimeout(actionAfterTimeout, timeoutInSeconds * 1000); + debug("[%s] %s request sent: %O", connection.options.id, request.to || "$managment", request); + link.sender.send(request); + }); + return retry_1.retry(() => sendRequestPromise); +} +exports.sendRequest = sendRequest; +/** + * Opens the AMQP connection to the Event Hub for this client, returning a promise + * that will be resolved when the connection is completed. + * + * @param {ConnectionContext} context The connection context. + * @param {boolean} [useSaslPlain] True for using sasl plain mode for authentication, false otherwise. + * @returns {Promise} + */ +function open(context, useSaslPlain) { + return __awaiter(this, void 0, void 0, function* () { + try { + yield utils_1.defaultLock.acquire("connect", () => { return _open(context, useSaslPlain); }); + } + catch (err) { + debug(err); + throw err; + } + }); +} +exports.open = open; +function _open(context, useSaslPlain) { + return __awaiter(this, void 0, void 0, function* () { + if (useSaslPlain && typeof useSaslPlain !== "boolean") { + throw new Error("'useSaslPlain' must be of type 'boolean'."); + } + if (!context.connection) { + const connectOptions = { + transport: Constants.TLS, + host: context.config.host, + hostname: context.config.host, + username: context.config.sharedAccessKeyName, + port: 5671, + reconnect_limit: Constants.reconnectLimit, + properties: { + product: "MSJSClient", + version: Constants.packageJsonInfo.version || "0.1.0", + platform: `(${os.arch()}-${os.type()}-${os.release()})`, + framework: `Node/${process.version}`, + "user-agent": connectionContext_1.ConnectionContext.userAgent + } + }; + if (useSaslPlain) { + connectOptions.password = context.config.sharedAccessKey; + } + debug("Dialing the amqp connection with options.", connectOptions); + context.connection = yield rhea_promise_1.connect(connectOptions); + context.connectionId = context.connection.options.id; + debug("Successfully established the amqp connection '%s'.", context.connectionId); + } + }); +} diff --git a/client/examples/js/lib/streamingReceiver.js b/client/examples/js/lib/streamingReceiver.js new file mode 100644 index 0000000..15e6b47 --- /dev/null +++ b/client/examples/js/lib/streamingReceiver.js @@ -0,0 +1,126 @@ +"use strict"; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const debugModule = require("debug"); +const eventHubReceiver_1 = require("./eventHubReceiver"); +const Constants = require("./util/constants"); +const debug = debugModule("azure:event-hubs:receiverstreaming"); +class ReceiveHandler { + /** + * Creates an instance of the ReceiveHandler. + * @constructor + * @param {EventHubReceiver} receiver The underlying EventHubReceiver. + */ + constructor(receiver) { + this._receiver = receiver; + this.name = receiver ? receiver.name : "ReceiveHandler"; + } + /** + * @property {ReceiverRuntimeInfo} runtimeInfo The receiver runtime info. This property will only + * be enabled when `enableReceiverRuntimeMetric` option is set to true in the + * `client.receiveOnMessage()` method. + * @readonly + */ + get runtimeInfo() { + return this._receiver ? this._receiver.runtimeInfo : undefined; + } + /** + * Stops the underlying EventHubReceiver from receiving more messages. + * @return {Promise} Promise + */ + stop() { + return __awaiter(this, void 0, void 0, function* () { + if (this._receiver) { + yield this._receiver.close(); + } + }); + } +} +exports.ReceiveHandler = ReceiveHandler; +/** + * Describes the streaming receiver where the user can receive the message + * by providing handler functions. + * @class StreamingReceiver + * @extends EventHubReceiver + */ +class StreamingReceiver extends eventHubReceiver_1.EventHubReceiver { + /** + * Instantiate a new receiver from the AMQP `Receiver`. Used by `EventHubClient`. + * + * @constructor + * @param {EventHubClient} client The EventHub client. + * @param {string} partitionId Partition ID from which to receive. + * @param {ReceiveOptions} [options] Options for how you'd like to connect. + * @param {string} [options.consumerGroup] Consumer group from which to receive. + * @param {number} [options.prefetchCount] The upper limit of events this receiver will + * actively receive regardless of whether a receive operation is pending. + * @param {boolean} [options.enableReceiverRuntimeMetric] Provides the approximate receiver runtime information + * for a logical partition of an Event Hub if the value is true. Default false. + * @param {number} [options.epoch] The epoch value that this receiver is currently + * using for partition ownership. A value of undefined means this receiver is not an epoch-based receiver. + * @param {EventPosition} [options.eventPosition] The position of EventData in the EventHub parition from + * where the receiver should start receiving. Only one of offset, sequenceNumber, enqueuedTime, customFilter can be specified. + * `EventPosition.withCustomFilter()` should be used if you want more fine-grained control of the filtering. + * See https://github.com/Azure/amqpnetlite/wiki/Azure%20Service%20Bus%20Event%20Hubs for details. + */ + constructor(context, partitionId, options) { + super(context, partitionId, options); + } + /** + * Starts the receiver by establishing an AMQP session and an AMQP receiver link on the session. + * + * @param {OnMessage} onMessage The message handler to receive event data objects. + * @param {OnError} onError The error handler to receive an error that occurs while receivin messages. + */ + receiveOnMessage(onMessage, onError) { + if (!onMessage || typeof onMessage !== "function") { + throw new Error("'onMessage' is a required parameter and must be of type 'function'."); + } + if (!onError || typeof onError !== "function") { + throw new Error("'onError' is a required parameter and must be of type 'function'."); + } + this._onMessage = onMessage; + this._onError = onError; + if (!this._isOpen()) { + this._init().catch((err) => { + this._onError(err); + }); + } + else { + // It is possible that the receiver link has been established due to a previous receive() call. If that + // is the case then add message and error event handlers to the receiver. When the receiver will be closed + // these handlers will be automatically removed. + debug("[%s] Receiver link is already present for '%s' due to previous receive() calls. " + + "Hence reusing it and attaching message and error handlers.", this._context.connectionId, this.name); + this._receiver.on(Constants.message, this._onAmqpMessage); + this._receiver.on(Constants.receiverError, this._onAmqpError); + this._receiver.set_credit_window(Constants.defaultPrefetchCount); + this._receiver.add_credit(Constants.defaultPrefetchCount); + debug("[%s] Receiver '%s', set the prefetch count to 1000 and " + + "providing a credit of the same amount.", this._context.connectionId, this.name); + } + } + /** + * Creates a streaming receiver. + * @static + * + * @param {ConnectionContext} context The connection context. + * @param {string | number} partitionId The partitionId to receive events from. + * @param {ReceiveOptions} [options] Receive options. + */ + static create(context, partitionId, options) { + const sReceiver = new StreamingReceiver(context, partitionId, options); + context.receivers[sReceiver.name] = sReceiver; + return sReceiver; + } +} +exports.StreamingReceiver = StreamingReceiver; diff --git a/client/examples/js/lib/util/constants.js b/client/examples/js/lib/util/constants.js new file mode 100644 index 0000000..483b301 --- /dev/null +++ b/client/examples/js/lib/util/constants.js @@ -0,0 +1,48 @@ +"use strict"; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +Object.defineProperty(exports, "__esModule", { value: true }); +exports.partitionKey = "x-opt-partition-key"; +exports.sequenceNumber = "x-opt-sequence-number"; +exports.enqueuedTime = "x-opt-enqueued-time"; +exports.offset = "x-opt-offset"; +exports.enqueuedTimeAnnotation = `amqp.annotation.${exports.enqueuedTime}`; +exports.offsetAnnotation = `amqp.annotation.${exports.offset}`; +exports.sequenceNumberAnnotation = `amqp.annotation.${exports.sequenceNumber}`; +exports.message = "message"; +exports.error = "error"; +exports.statusCode = "status-code"; +exports.statusDescription = "status-description"; +exports.errorCondition = "error-condition"; +exports.management = "$management"; +exports.partition = "partition"; +exports.partitionId = "partitionId"; +exports.readOperation = "READ"; +exports.TLS = "tls"; +exports.establishConnection = "establishConnection"; +exports.defaultConsumerGroup = "$default"; +exports.eventHub = "eventhub"; +exports.cbsEndpoint = "$cbs"; +exports.cbsReplyTo = "cbs"; +exports.operationPutToken = "put-token"; +exports.aadEventHubsAudience = "https://eventhubs.azure.net/"; +exports.maxUserAgentLength = 128; +exports.vendorString = "com.microsoft"; +exports.attachEpoch = `${exports.vendorString}:epoch`; +exports.receiverIdentifierName = `${exports.vendorString}:receiver-name`; +exports.enableReceiverRuntimeMetricName = `${exports.vendorString}:enable-receiver-runtime-metric`; +exports.receiverError = "receiver_error"; +exports.senderError = "sender_error"; +exports.sessionError = "session_error"; +exports.connectionError = "connection_error"; +exports.defaultOperationTimeoutInSeconds = 60; +exports.managementRequestKey = "managementRequest"; +exports.negotiateCbsKey = "negotiateCbs"; +exports.negotiateClaim = "negotiateClaim"; +exports.ensureContainerAndBlob = "ensureContainerAndBlob"; +exports.defaultPrefetchCount = 1000; +exports.reconnectLimit = 100; +exports.packageJsonInfo = { + name: "azure-event-hubs-js", + version: "0.1.0" +}; diff --git a/client/examples/js/lib/util/utils.js b/client/examples/js/lib/util/utils.js new file mode 100644 index 0000000..7fb833b --- /dev/null +++ b/client/examples/js/lib/util/utils.js @@ -0,0 +1,34 @@ +"use strict"; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +Object.defineProperty(exports, "__esModule", { value: true }); +const AsyncLock = require("async-lock"); +function parseConnectionString(connectionString) { + return connectionString.split(';').reduce((acc, part) => { + const splitIndex = part.indexOf('='); + return Object.assign({}, acc, { [part.substring(0, splitIndex)]: part.substring(splitIndex + 1) }); + }, {}); +} +exports.parseConnectionString = parseConnectionString; +/** + * Gets a new instance of the async lock with desired settings. + * @param {AsyncLockOptions} [options] The async lock options. + */ +function getNewAsyncLock(options) { + return new AsyncLock(options); +} +exports.getNewAsyncLock = getNewAsyncLock; +/** + * @constant {AsyncLock} defaultLock The async lock instance with default settings. + */ +exports.defaultLock = new AsyncLock(); +/** + * A wrapper for setTimeout that resolves a promise after t milliseconds. + * @param {number} t - The number of milliseconds to be delayed. + * @param {T} value - The value to be resolved with after a timeout of t milliseconds. + * @returns {Promise} - Resolved promise + */ +function delay(t, value) { + return new Promise((resolve) => setTimeout(() => resolve(value), t)); +} +exports.delay = delay; diff --git a/examples/package.json b/client/examples/package.json similarity index 100% rename from examples/package.json rename to client/examples/package.json diff --git a/examples/sendReceiveWithInteractiveAuth.ts b/client/examples/sendReceiveWithInteractiveAuth.ts similarity index 100% rename from examples/sendReceiveWithInteractiveAuth.ts rename to client/examples/sendReceiveWithInteractiveAuth.ts diff --git a/examples/sendReceiveWithSPAuth.ts b/client/examples/sendReceiveWithSPAuth.ts similarity index 100% rename from examples/sendReceiveWithSPAuth.ts rename to client/examples/sendReceiveWithSPAuth.ts diff --git a/examples/simpleSendReceive.ts b/client/examples/simpleSendReceive.ts similarity index 100% rename from examples/simpleSendReceive.ts rename to client/examples/simpleSendReceive.ts diff --git a/examples/simpleSender.ts b/client/examples/simpleSender.ts similarity index 100% rename from examples/simpleSender.ts rename to client/examples/simpleSender.ts diff --git a/examples/streamingReceive.ts b/client/examples/streamingReceive.ts similarity index 100% rename from examples/streamingReceive.ts rename to client/examples/streamingReceive.ts diff --git a/examples/tsconfig.json b/client/examples/tsconfig.json similarity index 100% rename from examples/tsconfig.json rename to client/examples/tsconfig.json diff --git a/lib/auth/aad.ts b/client/lib/auth/aad.ts similarity index 100% rename from lib/auth/aad.ts rename to client/lib/auth/aad.ts diff --git a/lib/auth/sas.ts b/client/lib/auth/sas.ts similarity index 100% rename from lib/auth/sas.ts rename to client/lib/auth/sas.ts diff --git a/lib/auth/token.ts b/client/lib/auth/token.ts similarity index 100% rename from lib/auth/token.ts rename to client/lib/auth/token.ts diff --git a/lib/batchingReceiver.ts b/client/lib/batchingReceiver.ts similarity index 100% rename from lib/batchingReceiver.ts rename to client/lib/batchingReceiver.ts diff --git a/lib/cbs.ts b/client/lib/cbs.ts similarity index 100% rename from lib/cbs.ts rename to client/lib/cbs.ts diff --git a/lib/connectionConfig.ts b/client/lib/connectionConfig.ts similarity index 100% rename from lib/connectionConfig.ts rename to client/lib/connectionConfig.ts diff --git a/lib/connectionContext.ts b/client/lib/connectionContext.ts similarity index 100% rename from lib/connectionContext.ts rename to client/lib/connectionContext.ts diff --git a/lib/dataTransformer.ts b/client/lib/dataTransformer.ts similarity index 100% rename from lib/dataTransformer.ts rename to client/lib/dataTransformer.ts diff --git a/lib/errors.ts b/client/lib/errors.ts similarity index 100% rename from lib/errors.ts rename to client/lib/errors.ts diff --git a/lib/eventData.ts b/client/lib/eventData.ts similarity index 100% rename from lib/eventData.ts rename to client/lib/eventData.ts diff --git a/lib/eventHubClient.ts b/client/lib/eventHubClient.ts similarity index 100% rename from lib/eventHubClient.ts rename to client/lib/eventHubClient.ts diff --git a/lib/eventHubReceiver.ts b/client/lib/eventHubReceiver.ts similarity index 100% rename from lib/eventHubReceiver.ts rename to client/lib/eventHubReceiver.ts diff --git a/lib/eventHubSender.ts b/client/lib/eventHubSender.ts similarity index 100% rename from lib/eventHubSender.ts rename to client/lib/eventHubSender.ts diff --git a/lib/eventPosition.ts b/client/lib/eventPosition.ts similarity index 100% rename from lib/eventPosition.ts rename to client/lib/eventPosition.ts diff --git a/lib/index.ts b/client/lib/index.ts similarity index 81% rename from lib/index.ts rename to client/lib/index.ts index 1979aa9..df1b6cb 100644 --- a/lib/index.ts +++ b/client/lib/index.ts @@ -7,14 +7,11 @@ export { export { Delivery } from "./rhea-promise"; export { ConnectionConfig } from "./connectionConfig"; export { ReceiverRuntimeInfo, OnMessage, OnError } from "./eventHubReceiver"; +export { ReceiveHandler } from "./streamingReceiver"; export { EventHubsError, ErrorNameConditionMapper, ConditionStatusMapper, ConditionErrorNameMapper } from "./errors"; -export { EventHubClient, ReceiveOptions } from "./eventHubClient"; -export { - EventProcessorHost, OnEphClose, OnEphMessage, OnEphOpen, PartitionFilter, - ConnectionStringBasedOptions, EventProcessorOptions -} from "./eph"; +export { EventHubClient, ReceiveOptions, ClientOptionsBase, ClientOptions } from "./eventHubClient"; export { EventPosition } from "./eventPosition"; export { DataTransformer, DefaultDataTransformer } from "./dataTransformer"; export { EventHubPartitionRuntimeInformation, EventHubRuntimeInformation } from "./managementClient"; diff --git a/lib/managementClient.ts b/client/lib/managementClient.ts similarity index 100% rename from lib/managementClient.ts rename to client/lib/managementClient.ts diff --git a/lib/retry.ts b/client/lib/retry.ts similarity index 100% rename from lib/retry.ts rename to client/lib/retry.ts diff --git a/lib/rhea-promise/global.d.ts b/client/lib/rhea-promise/global.d.ts similarity index 100% rename from lib/rhea-promise/global.d.ts rename to client/lib/rhea-promise/global.d.ts diff --git a/lib/rhea-promise/index.ts b/client/lib/rhea-promise/index.ts similarity index 100% rename from lib/rhea-promise/index.ts rename to client/lib/rhea-promise/index.ts diff --git a/lib/rpc.ts b/client/lib/rpc.ts similarity index 100% rename from lib/rpc.ts rename to client/lib/rpc.ts diff --git a/lib/streamingReceiver.ts b/client/lib/streamingReceiver.ts similarity index 100% rename from lib/streamingReceiver.ts rename to client/lib/streamingReceiver.ts diff --git a/lib/util/constants.ts b/client/lib/util/constants.ts similarity index 99% rename from lib/util/constants.ts rename to client/lib/util/constants.ts index 8fa7831..fe1bc4a 100644 --- a/lib/util/constants.ts +++ b/client/lib/util/constants.ts @@ -43,5 +43,5 @@ export const defaultPrefetchCount = 1000; export const reconnectLimit = 100; export const packageJsonInfo = { name: "azure-event-hubs-js", - version: "0.1.1" + version: "0.2.0" }; diff --git a/lib/util/utils.ts b/client/lib/util/utils.ts similarity index 100% rename from lib/util/utils.ts rename to client/lib/util/utils.ts diff --git a/package-lock.json b/client/package-lock.json similarity index 91% rename from package-lock.json rename to client/package-lock.json index bbbae7a..da5027f 100644 --- a/package-lock.json +++ b/client/package-lock.json @@ -1,6 +1,6 @@ { "name": "azure-event-hubs", - "version": "0.1.2", + "version": "0.2.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -186,31 +186,6 @@ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" }, - "azure-storage": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/azure-storage/-/azure-storage-2.8.2.tgz", - "integrity": "sha1-GctjKx5WMK6uVEUaZNrBYLFlyEg=", - "requires": { - "browserify-mime": "~1.2.9", - "extend": "~1.2.1", - "json-edm-parser": "0.1.2", - "md5.js": "1.3.4", - "readable-stream": "~2.0.0", - "request": "~2.83.0", - "underscore": "~1.8.3", - "uuid": "^3.0.0", - "validator": "~9.4.1", - "xml2js": "0.2.8", - "xmlbuilder": "0.4.3" - }, - "dependencies": { - "extend": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/extend/-/extend-1.2.1.tgz", - "integrity": "sha1-oPX9bPyDpf5J72mNYOyKYk3UV2w=" - } - } - }, "babel-code-frame": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", @@ -281,11 +256,6 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, - "browserify-mime": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/browserify-mime/-/browserify-mime-1.2.9.tgz", - "integrity": "sha1-rrGvKN5sDXpqLOQK22j/GEIq8x8=" - }, "buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -617,15 +587,6 @@ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true }, - "hash-base": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", - "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, "hawk": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", @@ -671,7 +632,8 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true }, "is-buffer": { "version": "2.0.2", @@ -688,11 +650,6 @@ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -720,14 +677,6 @@ "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", "optional": true }, - "json-edm-parser": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/json-edm-parser/-/json-edm-parser-0.1.2.tgz", - "integrity": "sha1-HmCw/vG8CvZ7wNFG393lSGzWFbQ=", - "requires": { - "jsonparse": "~1.2.0" - } - }, "json-schema": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", @@ -743,11 +692,6 @@ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, - "jsonparse": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.2.0.tgz", - "integrity": "sha1-XAxWhRBxYOcv50ib3eoLRMK8Z70=" - }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -791,15 +735,6 @@ "integrity": "sha512-0Dab5btKVPhibSalc9QGXb559ED7G7iLjFXBaj9Wq8O3vorueR5K5jaE3hkG6ZQINyhA/JgG6Qk4qdFQjsYV6g==", "dev": true }, - "md5.js": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", - "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=", - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" - } - }, "mime-db": { "version": "1.33.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", @@ -977,11 +912,6 @@ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, - "process-nextick-args": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" - }, "punycode": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", @@ -992,19 +922,6 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" }, - "readable-stream": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", - "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "~1.0.0", - "process-nextick-args": "~1.0.6", - "string_decoder": "~0.10.x", - "util-deprecate": "~1.0.1" - } - }, "request": { "version": "2.83.0", "resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz", @@ -1056,11 +973,6 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" }, - "sax": { - "version": "0.5.8", - "resolved": "https://registry.npmjs.org/sax/-/sax-0.5.8.tgz", - "integrity": "sha1-1HLbIo6zMcJQaw6MFVJK25OdEsE=" - }, "semver": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", @@ -1111,11 +1023,6 @@ "tweetnacl": "~0.14.0" } }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - }, "stringstream": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", @@ -1244,21 +1151,11 @@ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=" }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, "uuid": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==" }, - "validator": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/validator/-/validator-9.4.1.tgz", - "integrity": "sha512-YV5KjzvRmSyJ1ee/Dm5UED0G+1L4GZnLN3w6/T+zZm8scVua4sOhYKWTUrKa0H/tMiJyO9QLHMPN+9mB/aMunA==" - }, "verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", @@ -1275,19 +1172,6 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, - "xml2js": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.2.8.tgz", - "integrity": "sha1-m4FpCTFjH/CdGVdUn69U9PmAs8I=", - "requires": { - "sax": "0.5.x" - } - }, - "xmlbuilder": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-0.4.3.tgz", - "integrity": "sha1-xGFLp04K0ZbmCcknLNnh3bKKilg=" - }, "xmldom": { "version": "0.1.27", "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz", diff --git a/package.json b/client/package.json similarity index 96% rename from package.json rename to client/package.json index c738764..fc16aaf 100644 --- a/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "azure-event-hubs", - "version": "0.1.2", + "version": "0.2.0", "description": "Azure Event Hubs SDK for Node.js", "author": "Microsoft Corporation", "license": "MIT", @@ -13,7 +13,6 @@ "ms-rest": "^2.3.3", "ms-rest-azure": "^2.5.5", "rhea": "^0.2.12", - "azure-storage": "^2.8.2", "uuid": "^3.2.1" }, "devDependencies": { @@ -49,4 +48,4 @@ "bugs": { "url": "http://github.com/Azure/azure-event-hubs-node/issues" } -} \ No newline at end of file +} diff --git a/tests/client.spec.ts b/client/tests/client.spec.ts similarity index 100% rename from tests/client.spec.ts rename to client/tests/client.spec.ts diff --git a/tests/config.spec.ts b/client/tests/config.spec.ts similarity index 100% rename from tests/config.spec.ts rename to client/tests/config.spec.ts diff --git a/tests/dataTransformer.spec.ts b/client/tests/dataTransformer.spec.ts similarity index 98% rename from tests/dataTransformer.spec.ts rename to client/tests/dataTransformer.spec.ts index 0777030..f582076 100644 --- a/tests/dataTransformer.spec.ts +++ b/client/tests/dataTransformer.spec.ts @@ -5,8 +5,6 @@ import * as chai from "chai"; const should = chai.should(); import * as assert from "assert"; const isBuffer = require("is-buffer"); -// import * as debugModule from "debug"; -// const debug = debugModule("azure:event-hubs:datatransformer-spec"); import { DefaultDataTransformer } from "../lib"; describe("DataTransformer", function () { diff --git a/tests/errors.spec.ts b/client/tests/errors.spec.ts similarity index 100% rename from tests/errors.spec.ts rename to client/tests/errors.spec.ts diff --git a/tests/eventdata.spec.ts b/client/tests/eventdata.spec.ts similarity index 100% rename from tests/eventdata.spec.ts rename to client/tests/eventdata.spec.ts diff --git a/tests/hubruntime.spec.ts b/client/tests/hubruntime.spec.ts similarity index 100% rename from tests/hubruntime.spec.ts rename to client/tests/hubruntime.spec.ts diff --git a/tests/misc.spec.ts b/client/tests/misc.spec.ts similarity index 100% rename from tests/misc.spec.ts rename to client/tests/misc.spec.ts diff --git a/tests/receiver.spec.ts b/client/tests/receiver.spec.ts similarity index 100% rename from tests/receiver.spec.ts rename to client/tests/receiver.spec.ts diff --git a/tests/retry.spec.ts b/client/tests/retry.spec.ts similarity index 100% rename from tests/retry.spec.ts rename to client/tests/retry.spec.ts diff --git a/tests/sender.spec.ts b/client/tests/sender.spec.ts similarity index 100% rename from tests/sender.spec.ts rename to client/tests/sender.spec.ts diff --git a/client/tsconfig.json b/client/tsconfig.json new file mode 100644 index 0000000..2a2803e --- /dev/null +++ b/client/tsconfig.json @@ -0,0 +1,40 @@ +{ + "compilerOptions": { + "module": "commonjs", + "preserveConstEnums": true, + "sourceMap": true, + "newLine": "LF", + "target": "es6", + "moduleResolution": "node", + "noImplicitReturns": true, + "outDir": "dist/lib", + "allowJs": false, + "noUnusedLocals":true, + "strict": true, + "strictPropertyInitialization": true, + "declaration": true, + "declarationDir": "./typings/lib", + "lib": [ + "es2015", + "dom", + "dom.iterable", + "es5", + "es6", + "es7", + "esnext", + "esnext.asynciterable", + "es2015.iterable" + ] + }, + "compileOnSave": true, + "exclude": [ + "node_modules", + "testhub", + "typings/**", + "examples/**", + "processor" + ], + "include": [ + "./lib/**/*.ts" + ] +} \ No newline at end of file diff --git a/client/tslint.json b/client/tslint.json new file mode 100644 index 0000000..d80aa27 --- /dev/null +++ b/client/tslint.json @@ -0,0 +1,114 @@ +{ + "rules": { + "member-ordering": [ + true, + { + "order": "instance-sandwich" + } + ], + "typedef": [ + true, + "call-signature", + "parameter", + "property-declaration", + "member-variable-declaration" + ], + "typedef-whitespace": [ + true, + { + "call-signature": "nospace", + "index-signature": "nospace", + "parameter": "nospace", + "property-declaration": "nospace", + "variable-declaration": "nospace" + }, + { + "call-signature": "onespace", + "index-signature": "onespace", + "parameter": "onespace", + "property-declaration": "onespace", + "variable-declaration": "onespace" + } + ], + "prefer-const": true, + "no-construct": true, + "no-duplicate-variable": true, + "no-empty": true, + "no-invalid-this": true, + "no-null-keyword": true, + "no-string-literal": true, + "no-switch-case-fall-through": true, + "no-unsafe-finally": true, + "no-unused-expression": true, + "no-unused-variable": true, + "no-use-before-declare": true, + "no-var-keyword": true, + "no-floating-promises": true, + "triple-equals": [ + true, + "allow-null-check", + "allow-undefined-check" + ], + "use-isnan": true, + "eofline": true, + "indent": [ + true, + "spaces" + ], + "no-trailing-whitespace": true, + "arrow-parens": true, + "class-name": true, + "comment-format": [ + true, + "check-space" + ], + "interface-name": [ + true, + "never-prefix" + ], + "jsdoc-format": true, + "new-parens": true, + "one-line": [ + true, + "check-catch", + "check-finally", + "check-else", + "check-open-brace" + ], + "one-variable-per-declaration": [ + true, + "ignore-for-loop" + ], + "semicolon": [ + true, + "always" + ], + "curly": [ + true, + "ignore-same-line" + ], + "variable-name": [ + true, + "check-format", + "allow-leading-underscore", + "ban-keywords" + ], + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-operator", + "check-module", + "check-seperator", + "check-type" + ] + }, + "cliOptions": { + "exclude": [ + "examples", + "tests", + "testhub/*.ts", + "processor" + ] + } +} \ No newline at end of file diff --git a/processor/.npmignore b/processor/.npmignore new file mode 100644 index 0000000..b2759b3 --- /dev/null +++ b/processor/.npmignore @@ -0,0 +1,43 @@ +# git # +.git* + +# Node # +node_modules/ + +# Test folder # +tests/ +testhub/ + +# Tools and dev scripts # +tools/ +tasks/ +scripts/ +gruntfile.js +gulpfile.js + +# Nuget packages # +.nuget/ +packages/ +packages.config + +# VS # +.ntvs_analysis.* +*.sln +*.obj + +# Examples # +examples/ +# vscode # +.vscode/ +.env +sample.env + +# misc # +contribute.md +lib/ +tsconfig.json +tslint.json +webpack.config.ts +.idea/ +changelog.md +samples/ diff --git a/processor/README.md b/processor/README.md new file mode 100644 index 0000000..da34450 --- /dev/null +++ b/processor/README.md @@ -0,0 +1,68 @@ +azure-event-processor-host +================ + +_This SDK is currently in preview._ + +Azure Event Processor Host helps you efficiently receive events from an EventHub. It will create EventHub Receivers +across all the partitions in the consumer group of an EventHub and provide you messages received across +all the partitions. It will checkpoint metadata about the received messages at regular interval in an +Azure Storage Blob. This makes it easy to continue receiving messages from where you left at a later time. + +- **Node.js version: 6.x or higher.** We would encourage you to install the latest available LTS version from https://nodejs.org. + +## Installation ## +```bash +npm install azure-event-processor-host +``` + +## Examples +- Examples can be found over [here](./examples). + +## Usage + +```js +const { EventProcessorHost, delay } = require("azure-event-processor-host"); + +const storageConnectionString = "STORAGE_CONNECTION_STRING"; +const ehconnectionString = "EVENTHUB_CONNECTION_STRING"; +const entityPath = "EVENTHUB_NAME"; +const path = process.env[entityPath] || ""; +const storageCS = process.env[storageConnectionString]; +const ehCS = process.env[ehconnectionString]; + +async function main() { + // Create the Event Processo Host + const eph = EventProcessorHost.createFromConnectionString( + EventProcessorHost.createHostName("my-host"), + storageCS!, + ehCS!, + { + eventHubPath: path + } + ); + // Message event handler + const onMessage = (context/*PartitionContext*/, data /*EventData*/) => { + console.log(">>>>> Rx message from '%s': '%s'", context.partitionId, data.body); + return context.checkpoint(); + }; + // Error event handler + const onError = (error) => { + console.log(">>>>> Received Error: %O", error); + }; + // Register the event handlers + eph.on(EventProcessorHost.message, onMessage); + eph.on(EventProcessorHost.error, onError); + // start the EPH + await eph.start(); + // After some time let' say 2 minutes + await delay(120000); + // This will stop the EPH. + await eph.stop(); +} + +main().catch((err) => { + console.log(err); +}); +``` + + diff --git a/processor/changelog.md b/processor/changelog.md new file mode 100644 index 0000000..48370fd --- /dev/null +++ b/processor/changelog.md @@ -0,0 +1,6 @@ +## 2018-05-02 0.1.0 +- First version of `azure-event-processor-host` based on the new `azure-event-hubs` sdk. +- This client library makes it easier to manage receivers for an EventHub. +- You can checkpoint the received data to an Azure Storage Blob. The processor does checkpointing +on your behalf at regular intervals. This makes it easy to start receiving events from the point you +left at a later time. \ No newline at end of file diff --git a/processor/examples/eph.ts b/processor/examples/eph.ts new file mode 100644 index 0000000..abbfc1d --- /dev/null +++ b/processor/examples/eph.ts @@ -0,0 +1,42 @@ +import { EventProcessorHost, OnEphMessage, EventData, PartitionContext, OnEphError, delay } from "../lib"; + +const storageConnectionString = "STORAGE_CONNECTION_STRING"; +const ehconnectionString = "EVENTHUB_CONNECTION_STRING"; +const entityPath = "EVENTHUB_NAME"; +const path = process.env[entityPath] || ""; +const storageCS = process.env[storageConnectionString]; +const ehCS = process.env[ehconnectionString]; + +async function main() { + // Create the Event Processo Host + const eph = EventProcessorHost.createFromConnectionString( + EventProcessorHost.createHostName("my-host"), + storageCS!, + ehCS!, + { + eventHubPath: path + } + ); + // Message event handler + const onMessage: OnEphMessage = (context: PartitionContext, data: EventData) => { + console.log(">>>>> Rx message from '%s': '%s'", context.partitionId, data.body); + return context.checkpoint(); + }; + // Error event handler + const onError: OnEphError = (error) => { + console.log(">>>>> Received Error: %O", error); + }; + // Register the event handlers + eph.on(EventProcessorHost.message, onMessage); + eph.on(EventProcessorHost.error, onError); + // start the EPH + await eph.start(); + // After some time let' say 2 minutes + await delay(120000); + // This will stop the EPH. + await eph.stop(); +} + +main().catch((err) => { + console.log(err); +}); diff --git a/processor/examples/package.json b/processor/examples/package.json new file mode 100644 index 0000000..363dab9 --- /dev/null +++ b/processor/examples/package.json @@ -0,0 +1,11 @@ +{ + "name": "example-eh", + "private": true, + "version": "0.0.1", + "dependencies": { + "azure-event-processor-host": "*" + }, + "scripts": { + "build": "tsc -p ./tsconfig.json --baseUrl ../" + } +} \ No newline at end of file diff --git a/processor/examples/tsconfig.json b/processor/examples/tsconfig.json new file mode 100644 index 0000000..81363fa --- /dev/null +++ b/processor/examples/tsconfig.json @@ -0,0 +1,39 @@ +{ + "compilerOptions": { + "module": "commonjs", + "preserveConstEnums": true, + "sourceMap": false, + "newLine": "LF", + "target": "es6", + "moduleResolution": "node", + "noImplicitReturns": true, + "outDir": "js", + "allowJs": false, + "noUnusedLocals": true, + "strict": true, + "strictPropertyInitialization": true, + "declaration": false, + "baseUrl": "../", + "paths": { + "azure-event-processor-host": ["./lib/"] + }, + "lib": [ + "es2015", + "dom", + "dom.iterable", + "es5", + "es6", + "es7", + "esnext", + "esnext.asynciterable", + "es2015.iterable" + ] + }, + "compileOnSave": true, + "exclude": [ + "node_modules", + ], + "include": [ + "./*.ts" + ] +} \ No newline at end of file diff --git a/lib/eph/blobLease.ts b/processor/lib/blobLease.ts similarity index 98% rename from lib/eph/blobLease.ts rename to processor/lib/blobLease.ts index 654fc18..61204c8 100644 --- a/lib/eph/blobLease.ts +++ b/processor/lib/blobLease.ts @@ -3,8 +3,8 @@ import * as debugModule from "debug"; import { createBlobService, BlobService, ServiceResponse } from "azure-storage"; -import * as Constants from "../util/constants"; -import { defaultLock } from "../util/utils"; +import * as Constants from "azure-event-hubs/lib/util/constants"; +import { defaultLock } from "azure-event-hubs/lib/util/utils"; const debug = debugModule("azure:event-hubs:eph:lease"); diff --git a/lib/eph/blobLeaseManager.ts b/processor/lib/blobLeaseManager.ts similarity index 99% rename from lib/eph/blobLeaseManager.ts rename to processor/lib/blobLeaseManager.ts index d115a6c..927692a 100644 --- a/lib/eph/blobLeaseManager.ts +++ b/processor/lib/blobLeaseManager.ts @@ -4,7 +4,7 @@ import { EventEmitter } from "events"; import * as debugModule from "debug"; import { BlobLease, Lease } from "./blobLease"; -import { Dictionary } from "../eventData"; +import { Dictionary } from "azure-event-hubs"; const debug = debugModule("azure:event-hubs:eph:lease-manager"); /** diff --git a/lib/eph/checkpointInfo.ts b/processor/lib/checkpointInfo.ts similarity index 100% rename from lib/eph/checkpointInfo.ts rename to processor/lib/checkpointInfo.ts diff --git a/lib/eph/eventProcessorHost.ts b/processor/lib/eventProcessorHost.ts similarity index 97% rename from lib/eph/eventProcessorHost.ts rename to processor/lib/eventProcessorHost.ts index b715dd1..b9476a2 100644 --- a/lib/eph/eventProcessorHost.ts +++ b/processor/lib/eventProcessorHost.ts @@ -6,18 +6,17 @@ import * as debugModule from "debug"; import { BlobLeaseManager, LeaseManager } from "./blobLeaseManager"; import { BlobLease, Lease } from "./blobLease"; import { PartitionContext } from "./partitionContext"; -import { EventHubClient, ClientOptions } from "../eventHubClient"; import { EventEmitter } from "events"; import { TokenProvider, EventHubRuntimeInformation, EventHubPartitionRuntimeInformation, - ReceiveOptions, EventPosition, OnMessage, OnError, EventHubsError -} from ".."; -import { Dictionary, EventData } from "../eventData"; + ReceiveOptions, EventPosition, OnMessage, OnError, EventHubsError, EventHubClient, ClientOptions, + Dictionary, EventData, ReceiveHandler, ClientOptionsBase +} from "azure-event-hubs"; import { ApplicationTokenCredentials, UserTokenCredentials, DeviceTokenCredentials, MSITokenCredentials } from "ms-rest-azure"; -import { ReceiveHandler } from "../streamingReceiver"; + const debug = debugModule("azure:event-hubs:eph:host"); /** @@ -54,7 +53,7 @@ export interface StartEPHOptions { * Describes the optional parameters that can be provided for creating an EventProcessorHost. * @interface EventProcessorOptions */ -export interface EventProcessorOptions { +export interface EventProcessorOptions extends ClientOptionsBase { /** * @property {EventPosition} initialOffset This is only used when then receiver is being created * for the very first time and there is no checkpoint data in the blob. For this option to be @@ -71,7 +70,8 @@ export interface EventProcessorOptions { */ leaseManager?: LeaseManager; /** - * @property {string} [leasecontainerName] Azure Storage container name for use by built-in lease and checkpoint manager. + * @property {string} [leasecontainerName] Azure Storage container name for use by built-in + * lease and checkpoint manager. */ leasecontainerName?: string; /** @@ -415,7 +415,8 @@ export class EventProcessorHost extends EventEmitter { options?: ConnectionStringBasedOptions): EventProcessorHost { if (!options) options = {}; const ehCOptions: ClientOptions = { - tokenProvider: options.tokenProvider + tokenProvider: options.tokenProvider, + dataTransformer: options.dataTransformer }; return new EventProcessorHost(hostName, storageConnectionString, EventHubClient.createFromConnectionString(eventHubConnectionString, options.eventHubPath, @@ -448,7 +449,11 @@ export class EventProcessorHost extends EventEmitter { eventHubPath: string, credentials: ApplicationTokenCredentials | UserTokenCredentials | DeviceTokenCredentials | MSITokenCredentials, options?: EventProcessorOptions): EventProcessorHost { + if (!options) options = {}; + const ehcOptions: ClientOptionsBase = { + dataTransformer: options.dataTransformer + }; return new EventProcessorHost(hostName, storageConnectionString, - EventHubClient.createFromAadTokenCredentials(namespace, eventHubPath, credentials), options); + EventHubClient.createFromAadTokenCredentials(namespace, eventHubPath, credentials, ehcOptions), options); } } diff --git a/lib/eph/index.ts b/processor/lib/index.ts similarity index 87% rename from lib/eph/index.ts rename to processor/lib/index.ts index 175d348..5b907f7 100644 --- a/lib/eph/index.ts +++ b/processor/lib/index.ts @@ -9,3 +9,4 @@ export { PartitionContext } from "./partitionContext"; export { CheckpointInfo } from "./checkpointInfo"; export { Lease } from "./blobLease"; export { LeaseManager, LeaseWithDuration } from "./blobLeaseManager"; +export { delay, EventData, OnError as OnEphError } from "azure-event-hubs"; diff --git a/lib/eph/partitionContext.ts b/processor/lib/partitionContext.ts similarity index 97% rename from lib/eph/partitionContext.ts rename to processor/lib/partitionContext.ts index d275346..7beaeea 100644 --- a/lib/eph/partitionContext.ts +++ b/processor/lib/partitionContext.ts @@ -4,8 +4,8 @@ import * as debugModule from "debug"; const debug = debugModule("azure:event-hubs:eph:partition"); import * as uuid from "uuid/v4"; -import { EventData } from "../"; -import * as Constants from "../util/constants"; +import { EventData } from "azure-event-hubs"; +import * as Constants from "azure-event-hubs/lib/util/constants"; import { BlobLease } from "./blobLease"; import { CheckpointInfo } from "./checkpointInfo"; diff --git a/processor/package-lock.json b/processor/package-lock.json new file mode 100644 index 0000000..1cd4016 --- /dev/null +++ b/processor/package-lock.json @@ -0,0 +1,2072 @@ +{ + "name": "azure-event-processor-host", + "version": "0.1.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/chai": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.1.2.tgz", + "integrity": "sha512-D8uQwKYUw2KESkorZ27ykzXgvkDJYXVEihGklgfp5I4HUP8D6IxtcdLTMB1emjQiWzV7WZ5ihm1cxIzVwjoleQ==", + "dev": true + }, + "@types/chai-as-promised": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.0.tgz", + "integrity": "sha512-MFiW54UOSt+f2bRw8J7LgQeIvE/9b4oGvwU7XW30S9QGAiHGnU/fmiOprsyMkdmH2rl8xSPc0/yrQw8juXU6bQ==", + "dev": true, + "requires": { + "@types/chai": "*" + } + }, + "@types/debug": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-0.0.30.tgz", + "integrity": "sha512-orGL5LXERPYsLov6CWs3Fh6203+dXzJkR7OnddIr2514Hsecwc8xRpzCapshBbKFImCsvS/mk6+FWiN5LyZJAQ==", + "dev": true + }, + "@types/dotenv": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/dotenv/-/dotenv-4.0.3.tgz", + "integrity": "sha512-mmhpINC/HcLGQK5ikFJlLXINVvcxhlrV+ZOUJSN7/ottYl+8X4oSXzS9lBtDkmWAl96EGyGyLrNvk9zqdSH8Fw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/mocha": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.0.0.tgz", + "integrity": "sha512-ZS0vBV7Jn5Z/Q4T3VXauEKMDCV8nWOtJJg90OsDylkYJiQwcWtKuLzohWzrthBkerUF7DLMmJcwOPEP0i/AOXw==", + "dev": true + }, + "@types/node": { + "version": "8.9.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.9.4.tgz", + "integrity": "sha512-dSvD36qnQs78G1BPsrZFdPpvLgMW/dnvr5+nTW2csMs5TiP9MOXrjUbnMZOEwnIuBklXtn7b6TPA2Cuq07bDHA==", + "dev": true + }, + "@types/uuid": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.3.tgz", + "integrity": "sha512-5fRLCYhLtDb3hMWqQyH10qtF+Ud2JnNCXTCZ+9ktNdCcgslcuXkDTkFcJNk++MT29yDntDnlF1+jD+uVGumsbw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "requires": { + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" + } + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", + "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" + }, + "azure-event-hubs": { + "version": "file:../client", + "requires": { + "async-lock": "^1.1.2", + "debug": "^3.1.0", + "is-buffer": "2.0.2", + "ms-rest": "^2.3.3", + "ms-rest-azure": "^2.5.5", + "rhea": "^0.2.12", + "uuid": "^3.2.1" + }, + "dependencies": { + "@types/async-lock": { + "version": "1.1.0", + "bundled": true + }, + "@types/caseless": { + "version": "0.12.1", + "bundled": true + }, + "@types/chai": { + "version": "4.1.2", + "bundled": true + }, + "@types/chai-as-promised": { + "version": "7.1.0", + "bundled": true, + "requires": { + "@types/chai": "*" + } + }, + "@types/debug": { + "version": "0.0.30", + "bundled": true + }, + "@types/dotenv": { + "version": "4.0.3", + "bundled": true, + "requires": { + "@types/node": "*" + } + }, + "@types/form-data": { + "version": "2.2.1", + "bundled": true, + "requires": { + "@types/node": "*" + } + }, + "@types/mocha": { + "version": "5.0.0", + "bundled": true + }, + "@types/node": { + "version": "8.9.4", + "bundled": true + }, + "@types/request": { + "version": "2.47.0", + "bundled": true, + "requires": { + "@types/caseless": "*", + "@types/form-data": "*", + "@types/node": "*", + "@types/tough-cookie": "*" + } + }, + "@types/tough-cookie": { + "version": "2.3.2", + "bundled": true + }, + "@types/uuid": { + "version": "3.4.3", + "bundled": true, + "requires": { + "@types/node": "*" + } + }, + "adal-node": { + "version": "0.1.28", + "bundled": true, + "requires": { + "@types/node": "^8.0.47", + "async": ">=0.6.0", + "date-utils": "*", + "jws": "3.x.x", + "request": ">= 2.52.0", + "underscore": ">= 1.3.1", + "uuid": "^3.1.0", + "xmldom": ">= 0.1.x", + "xpath.js": "~1.1.0" + } + }, + "ajv": { + "version": "5.5.2", + "bundled": true, + "requires": { + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" + } + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true + }, + "ansi-styles": { + "version": "2.2.1", + "bundled": true + }, + "argparse": { + "version": "1.0.10", + "bundled": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "arrify": { + "version": "1.0.1", + "bundled": true + }, + "asn1": { + "version": "0.2.3", + "bundled": true + }, + "assert-plus": { + "version": "1.0.0", + "bundled": true + }, + "assertion-error": { + "version": "1.1.0", + "bundled": true + }, + "async": { + "version": "2.6.0", + "bundled": true, + "requires": { + "lodash": "^4.14.0" + } + }, + "async-lock": { + "version": "1.1.2", + "bundled": true + }, + "asynckit": { + "version": "0.4.0", + "bundled": true + }, + "aws-sign2": { + "version": "0.7.0", + "bundled": true + }, + "aws4": { + "version": "1.6.0", + "bundled": true + }, + "babel-code-frame": { + "version": "6.26.0", + "bundled": true, + "requires": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "bundled": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + } + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true + }, + "base64url": { + "version": "2.0.0", + "bundled": true + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "bundled": true, + "optional": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "boom": { + "version": "4.3.1", + "bundled": true, + "requires": { + "hoek": "4.x.x" + } + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "browser-stdout": { + "version": "1.3.1", + "bundled": true + }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "bundled": true + }, + "builtin-modules": { + "version": "1.1.1", + "bundled": true + }, + "caseless": { + "version": "0.12.0", + "bundled": true + }, + "chai": { + "version": "4.1.2", + "bundled": true, + "requires": { + "assertion-error": "^1.0.1", + "check-error": "^1.0.1", + "deep-eql": "^3.0.0", + "get-func-name": "^2.0.0", + "pathval": "^1.0.0", + "type-detect": "^4.0.0" + } + }, + "chai-as-promised": { + "version": "7.1.1", + "bundled": true, + "requires": { + "check-error": "^1.0.2" + } + }, + "chalk": { + "version": "2.3.1", + "bundled": true, + "requires": { + "ansi-styles": "^3.2.0", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.2.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "bundled": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "supports-color": { + "version": "5.2.0", + "bundled": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "check-error": { + "version": "1.0.2", + "bundled": true + }, + "co": { + "version": "4.6.0", + "bundled": true + }, + "color-convert": { + "version": "1.9.1", + "bundled": true, + "requires": { + "color-name": "^1.1.1" + } + }, + "color-name": { + "version": "1.1.3", + "bundled": true + }, + "combined-stream": { + "version": "1.0.6", + "bundled": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.15.1", + "bundled": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true + }, + "cryptiles": { + "version": "3.1.2", + "bundled": true, + "requires": { + "boom": "5.x.x" + }, + "dependencies": { + "boom": { + "version": "5.2.0", + "bundled": true, + "requires": { + "hoek": "4.x.x" + } + } + } + }, + "dashdash": { + "version": "1.14.1", + "bundled": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "date-utils": { + "version": "1.2.21", + "bundled": true + }, + "debug": { + "version": "3.1.0", + "bundled": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-eql": { + "version": "3.0.1", + "bundled": true, + "requires": { + "type-detect": "^4.0.0" + } + }, + "delayed-stream": { + "version": "1.0.0", + "bundled": true + }, + "diff": { + "version": "3.4.0", + "bundled": true + }, + "dotenv": { + "version": "5.0.1", + "bundled": true + }, + "duplexer": { + "version": "0.1.1", + "bundled": true + }, + "ecc-jsbn": { + "version": "0.1.1", + "bundled": true, + "optional": true, + "requires": { + "jsbn": "~0.1.0" + } + }, + "ecdsa-sig-formatter": { + "version": "1.0.9", + "bundled": true, + "requires": { + "base64url": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "bundled": true + }, + "esprima": { + "version": "4.0.0", + "bundled": true + }, + "esutils": { + "version": "2.0.2", + "bundled": true + }, + "extend": { + "version": "3.0.1", + "bundled": true + }, + "extsprintf": { + "version": "1.3.0", + "bundled": true + }, + "fast-deep-equal": { + "version": "1.1.0", + "bundled": true + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "bundled": true + }, + "forever-agent": { + "version": "0.6.1", + "bundled": true + }, + "form-data": { + "version": "2.3.2", + "bundled": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "1.0.6", + "mime-types": "^2.1.12" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true + }, + "get-func-name": { + "version": "2.0.0", + "bundled": true + }, + "getpass": { + "version": "0.1.7", + "bundled": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.2", + "bundled": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "growl": { + "version": "1.10.3", + "bundled": true + }, + "har-schema": { + "version": "2.0.0", + "bundled": true + }, + "har-validator": { + "version": "5.0.3", + "bundled": true, + "requires": { + "ajv": "^5.1.0", + "har-schema": "^2.0.0" + } + }, + "has-ansi": { + "version": "2.0.0", + "bundled": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "bundled": true + }, + "hawk": { + "version": "6.0.2", + "bundled": true, + "requires": { + "boom": "4.x.x", + "cryptiles": "3.x.x", + "hoek": "4.x.x", + "sntp": "2.x.x" + } + }, + "he": { + "version": "1.1.1", + "bundled": true + }, + "hoek": { + "version": "4.2.1", + "bundled": true + }, + "http-signature": { + "version": "1.2.0", + "bundled": true, + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true + }, + "is-buffer": { + "version": "2.0.2", + "bundled": true + }, + "is-stream": { + "version": "1.1.0", + "bundled": true + }, + "is-typedarray": { + "version": "1.0.0", + "bundled": true + }, + "isstream": { + "version": "0.1.2", + "bundled": true + }, + "js-tokens": { + "version": "3.0.2", + "bundled": true + }, + "js-yaml": { + "version": "3.11.0", + "bundled": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsbn": { + "version": "0.1.1", + "bundled": true, + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "bundled": true + }, + "json-schema-traverse": { + "version": "0.3.1", + "bundled": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "bundled": true + }, + "jsprim": { + "version": "1.4.1", + "bundled": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "jwa": { + "version": "1.1.5", + "bundled": true, + "requires": { + "base64url": "2.0.0", + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.9", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.1.4", + "bundled": true, + "requires": { + "base64url": "^2.0.0", + "jwa": "^1.1.4", + "safe-buffer": "^5.0.1" + } + }, + "lodash": { + "version": "4.17.5", + "bundled": true + }, + "make-error": { + "version": "1.3.4", + "bundled": true + }, + "mime-db": { + "version": "1.33.0", + "bundled": true + }, + "mime-types": { + "version": "2.1.18", + "bundled": true, + "requires": { + "mime-db": "~1.33.0" + } + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "requires": { + "minimist": "0.0.8" + } + }, + "mocha": { + "version": "5.0.5", + "bundled": true, + "requires": { + "browser-stdout": "1.3.1", + "commander": "2.11.0", + "debug": "3.1.0", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.3", + "he": "1.1.1", + "mkdirp": "0.5.1", + "supports-color": "4.4.0" + }, + "dependencies": { + "commander": { + "version": "2.11.0", + "bundled": true + }, + "diff": { + "version": "3.5.0", + "bundled": true + }, + "has-flag": { + "version": "2.0.0", + "bundled": true + }, + "supports-color": { + "version": "4.4.0", + "bundled": true, + "requires": { + "has-flag": "^2.0.0" + } + } + } + }, + "moment": { + "version": "2.21.0", + "bundled": true + }, + "ms": { + "version": "2.0.0", + "bundled": true + }, + "ms-rest": { + "version": "2.3.3", + "bundled": true, + "requires": { + "@types/node": "^8.9.4", + "@types/request": "^2.47.0", + "@types/uuid": "^3.4.3", + "duplexer": "^0.1.1", + "is-buffer": "^1.1.6", + "is-stream": "^1.1.0", + "moment": "^2.21.0", + "request": "^2.83.0", + "through": "^2.3.8", + "tunnel": "0.0.5", + "uuid": "^3.2.1" + }, + "dependencies": { + "is-buffer": { + "version": "1.1.6", + "bundled": true + } + } + }, + "ms-rest-azure": { + "version": "2.5.5", + "bundled": true, + "requires": { + "@types/node": "^9.4.6", + "@types/uuid": "^3.4.3", + "adal-node": "^0.1.27", + "async": "2.6.0", + "moment": "^2.20.1", + "ms-rest": "^2.3.2", + "uuid": "^3.2.1" + }, + "dependencies": { + "@types/node": { + "version": "9.6.0", + "bundled": true + } + } + }, + "oauth-sign": { + "version": "0.8.2", + "bundled": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "requires": { + "wrappy": "1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true + }, + "path-parse": { + "version": "1.0.5", + "bundled": true + }, + "pathval": { + "version": "1.1.0", + "bundled": true + }, + "performance-now": { + "version": "2.1.0", + "bundled": true + }, + "punycode": { + "version": "1.4.1", + "bundled": true + }, + "qs": { + "version": "6.5.1", + "bundled": true + }, + "request": { + "version": "2.83.0", + "bundled": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.6.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.5", + "extend": "~3.0.1", + "forever-agent": "~0.6.1", + "form-data": "~2.3.1", + "har-validator": "~5.0.3", + "hawk": "~6.0.2", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.17", + "oauth-sign": "~0.8.2", + "performance-now": "^2.1.0", + "qs": "~6.5.1", + "safe-buffer": "^5.1.1", + "stringstream": "~0.0.5", + "tough-cookie": "~2.3.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.1.0" + } + }, + "resolve": { + "version": "1.6.0", + "bundled": true, + "requires": { + "path-parse": "^1.0.5" + } + }, + "rhea": { + "version": "0.2.12", + "bundled": true, + "requires": { + "debug": ">=0.8.0" + } + }, + "safe-buffer": { + "version": "5.1.1", + "bundled": true + }, + "semver": { + "version": "5.5.0", + "bundled": true + }, + "sntp": { + "version": "2.1.0", + "bundled": true, + "requires": { + "hoek": "4.x.x" + } + }, + "source-map": { + "version": "0.6.1", + "bundled": true + }, + "source-map-support": { + "version": "0.5.4", + "bundled": true, + "requires": { + "source-map": "^0.6.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "bundled": true + }, + "sshpk": { + "version": "1.13.1", + "bundled": true, + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "tweetnacl": "~0.14.0" + } + }, + "stringstream": { + "version": "0.0.5", + "bundled": true + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "bundled": true + }, + "through": { + "version": "2.3.8", + "bundled": true + }, + "tough-cookie": { + "version": "2.3.4", + "bundled": true, + "requires": { + "punycode": "^1.4.1" + } + }, + "ts-node": { + "version": "5.0.1", + "bundled": true, + "requires": { + "arrify": "^1.0.0", + "chalk": "^2.3.0", + "diff": "^3.1.0", + "make-error": "^1.1.1", + "minimist": "^1.2.0", + "mkdirp": "^0.5.1", + "source-map-support": "^0.5.3", + "yn": "^2.0.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true + } + } + }, + "tslib": { + "version": "1.9.0", + "bundled": true + }, + "tslint": { + "version": "5.9.1", + "bundled": true, + "requires": { + "babel-code-frame": "^6.22.0", + "builtin-modules": "^1.1.1", + "chalk": "^2.3.0", + "commander": "^2.12.1", + "diff": "^3.2.0", + "glob": "^7.1.1", + "js-yaml": "^3.7.0", + "minimatch": "^3.0.4", + "resolve": "^1.3.2", + "semver": "^5.3.0", + "tslib": "^1.8.0", + "tsutils": "^2.12.1" + } + }, + "tsutils": { + "version": "2.24.0", + "bundled": true, + "requires": { + "tslib": "^1.8.1" + } + }, + "tunnel": { + "version": "0.0.5", + "bundled": true + }, + "tunnel-agent": { + "version": "0.6.0", + "bundled": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "bundled": true, + "optional": true + }, + "type-detect": { + "version": "4.0.8", + "bundled": true + }, + "typescript": { + "version": "2.8.3", + "bundled": true + }, + "underscore": { + "version": "1.8.3", + "bundled": true + }, + "uuid": { + "version": "3.2.1", + "bundled": true + }, + "verror": { + "version": "1.10.0", + "bundled": true, + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true + }, + "xmldom": { + "version": "0.1.27", + "bundled": true + }, + "xpath.js": { + "version": "1.1.0", + "bundled": true + }, + "yn": { + "version": "2.0.0", + "bundled": true + } + } + }, + "azure-storage": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/azure-storage/-/azure-storage-2.8.2.tgz", + "integrity": "sha1-GctjKx5WMK6uVEUaZNrBYLFlyEg=", + "requires": { + "browserify-mime": "~1.2.9", + "extend": "~1.2.1", + "json-edm-parser": "0.1.2", + "md5.js": "1.3.4", + "readable-stream": "~2.0.0", + "request": "~2.83.0", + "underscore": "~1.8.3", + "uuid": "^3.0.0", + "validator": "~9.4.1", + "xml2js": "0.2.8", + "xmlbuilder": "0.4.3" + }, + "dependencies": { + "extend": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-1.2.1.tgz", + "integrity": "sha1-oPX9bPyDpf5J72mNYOyKYk3UV2w=" + } + } + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + } + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "optional": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "boom": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", + "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", + "requires": { + "hoek": "4.x.x" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "browserify-mime": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/browserify-mime/-/browserify-mime-1.2.9.tgz", + "integrity": "sha1-rrGvKN5sDXpqLOQK22j/GEIq8x8=" + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "chai": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz", + "integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=", + "dev": true, + "requires": { + "assertion-error": "^1.0.1", + "check-error": "^1.0.1", + "deep-eql": "^3.0.0", + "get-func-name": "^2.0.0", + "pathval": "^1.0.0", + "type-detect": "^4.0.0" + } + }, + "chai-as-promised": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz", + "integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==", + "dev": true, + "requires": { + "check-error": "^1.0.2" + } + }, + "chalk": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.1.tgz", + "integrity": "sha512-QUU4ofkDoMIVO7hcx1iPTISs88wsO8jA92RQIm4JAwZvFGGAV2hSAA1NX7oVj2Ej2Q6NDTcRDjPTFrMCRZoJ6g==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.2.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "supports-color": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.2.0.tgz", + "integrity": "sha512-F39vS48la4YvTZUPVeTqsjsFNrvcMwrV3RLZINsmHo+7djCvuUzSIeXOnZ5hmjef4bajL1dNccN+tg5XAliO5Q==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "color-convert": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", + "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", + "dev": true, + "requires": { + "color-name": "^1.1.1" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "combined-stream": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cryptiles": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", + "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", + "requires": { + "boom": "5.x.x" + }, + "dependencies": { + "boom": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", + "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", + "requires": { + "hoek": "4.x.x" + } + } + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "diff": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.4.0.tgz", + "integrity": "sha512-QpVuMTEoJMF7cKzi6bvWhRulU1fZqZnvyVQgNhPaxxuTYwyjn/j1v9falseQ/uXWwPnO56RBfwtg4h/EQXmucA==", + "dev": true + }, + "dotenv": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-5.0.1.tgz", + "integrity": "sha512-4As8uPrjfwb7VXC+WnLCbXK7y+Ueb2B3zgNCePYfhxS1PYeaO1YTeplffTEcbfLhvFNGLAz90VvJs9yomG7bow==", + "dev": true + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "optional": true, + "requires": { + "jsbn": "~0.1.0" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "esprima": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", + "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", + "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "1.0.6", + "mime-types": "^2.1.12" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "growl": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.3.tgz", + "integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==", + "dev": true + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", + "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", + "requires": { + "ajv": "^5.1.0", + "har-schema": "^2.0.0" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "hash-base": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", + "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "hawk": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", + "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", + "requires": { + "boom": "4.x.x", + "cryptiles": "3.x.x", + "hoek": "4.x.x", + "sntp": "2.x.x" + } + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, + "hoek": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", + "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==" + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "js-yaml": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.11.0.tgz", + "integrity": "sha512-saJstZWv7oNeOyBh3+Dx1qWzhW0+e6/8eDzo7p5rDFqxntSztloLtuKu+Ejhtq82jsilwOIZYsCz+lIjthg1Hw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "optional": true + }, + "json-edm-parser": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/json-edm-parser/-/json-edm-parser-0.1.2.tgz", + "integrity": "sha1-HmCw/vG8CvZ7wNFG393lSGzWFbQ=", + "requires": { + "jsonparse": "~1.2.0" + } + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsonparse": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.2.0.tgz", + "integrity": "sha1-XAxWhRBxYOcv50ib3eoLRMK8Z70=" + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "make-error": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.4.tgz", + "integrity": "sha512-0Dab5btKVPhibSalc9QGXb559ED7G7iLjFXBaj9Wq8O3vorueR5K5jaE3hkG6ZQINyhA/JgG6Qk4qdFQjsYV6g==", + "dev": true + }, + "md5.js": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", + "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" + }, + "mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "requires": { + "mime-db": "~1.33.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "mocha": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.0.5.tgz", + "integrity": "sha512-3MM3UjZ5p8EJrYpG7s+29HAI9G7sTzKEe4+w37Dg0QP7qL4XGsV+Q2xet2cE37AqdgN1OtYQB6Vl98YiPV3PgA==", + "dev": true, + "requires": { + "browser-stdout": "1.3.1", + "commander": "2.11.0", + "debug": "3.1.0", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.3", + "he": "1.1.1", + "mkdirp": "0.5.1", + "supports-color": "4.4.0" + }, + "dependencies": { + "commander": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", + "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==", + "dev": true + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, + "supports-color": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz", + "integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==", + "dev": true, + "requires": { + "has-flag": "^2.0.0" + } + } + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-parse": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", + "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", + "dev": true + }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "qs": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" + }, + "readable-stream": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "string_decoder": "~0.10.x", + "util-deprecate": "~1.0.1" + } + }, + "request": { + "version": "2.83.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz", + "integrity": "sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.6.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.5", + "extend": "~3.0.1", + "forever-agent": "~0.6.1", + "form-data": "~2.3.1", + "har-validator": "~5.0.3", + "hawk": "~6.0.2", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.17", + "oauth-sign": "~0.8.2", + "performance-now": "^2.1.0", + "qs": "~6.5.1", + "safe-buffer": "^5.1.1", + "stringstream": "~0.0.5", + "tough-cookie": "~2.3.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.1.0" + } + }, + "resolve": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.6.0.tgz", + "integrity": "sha512-mw7JQNu5ExIkcw4LPih0owX/TZXjD/ZUF/ZQ/pDnkw3ZKhDcZZw5klmBlj6gVMwjQ3Pz5Jgu7F3d0jcDVuEWdw==", + "dev": true, + "requires": { + "path-parse": "^1.0.5" + } + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + }, + "sax": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/sax/-/sax-0.5.8.tgz", + "integrity": "sha1-1HLbIo6zMcJQaw6MFVJK25OdEsE=" + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "dev": true + }, + "sntp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", + "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", + "requires": { + "hoek": "4.x.x" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-support": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.4.tgz", + "integrity": "sha512-PETSPG6BjY1AHs2t64vS2aqAgu6dMIMXJULWFBGbh2Gr8nVLbCFDo6i/RMMvviIQ2h1Z8+5gQhVKSn2je9nmdg==", + "dev": true, + "requires": { + "source-map": "^0.6.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "sshpk": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", + "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "tweetnacl": "~0.14.0" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, + "stringstream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "tough-cookie": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", + "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", + "requires": { + "punycode": "^1.4.1" + } + }, + "ts-node": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-5.0.1.tgz", + "integrity": "sha512-XK7QmDcNHVmZkVtkiwNDWiERRHPyU8nBqZB1+iv2UhOG0q3RQ9HsZ2CMqISlFbxjrYFGfG2mX7bW4dAyxBVzUw==", + "dev": true, + "requires": { + "arrify": "^1.0.0", + "chalk": "^2.3.0", + "diff": "^3.1.0", + "make-error": "^1.1.1", + "minimist": "^1.2.0", + "mkdirp": "^0.5.1", + "source-map-support": "^0.5.3", + "yn": "^2.0.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "tslib": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.0.tgz", + "integrity": "sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ==", + "dev": true + }, + "tslint": { + "version": "5.9.1", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.9.1.tgz", + "integrity": "sha1-ElX4ej/1frCw4fDmEKi0dIBGya4=", + "dev": true, + "requires": { + "babel-code-frame": "^6.22.0", + "builtin-modules": "^1.1.1", + "chalk": "^2.3.0", + "commander": "^2.12.1", + "diff": "^3.2.0", + "glob": "^7.1.1", + "js-yaml": "^3.7.0", + "minimatch": "^3.0.4", + "resolve": "^1.3.2", + "semver": "^5.3.0", + "tslib": "^1.8.0", + "tsutils": "^2.12.1" + } + }, + "tsutils": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.24.0.tgz", + "integrity": "sha512-rOIkvoe17acR3r96IPnqwa1+Z7zx9AroEtEKl20IeExXtoWptqG/zb806cYOvdbQGcxh1eOaZQNruOQ716Edig==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "optional": true + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "typescript": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.8.3.tgz", + "integrity": "sha512-K7g15Bb6Ra4lKf7Iq2l/I5/En+hLIHmxWZGq3D4DIRNFxMNV6j2SHSvDOqs2tGd4UvD/fJvrwopzQXjLrT7Itw==", + "dev": true + }, + "underscore": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", + "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "uuid": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", + "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==" + }, + "validator": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/validator/-/validator-9.4.1.tgz", + "integrity": "sha512-YV5KjzvRmSyJ1ee/Dm5UED0G+1L4GZnLN3w6/T+zZm8scVua4sOhYKWTUrKa0H/tMiJyO9QLHMPN+9mB/aMunA==" + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "xml2js": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.2.8.tgz", + "integrity": "sha1-m4FpCTFjH/CdGVdUn69U9PmAs8I=", + "requires": { + "sax": "0.5.x" + } + }, + "xmlbuilder": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-0.4.3.tgz", + "integrity": "sha1-xGFLp04K0ZbmCcknLNnh3bKKilg=" + }, + "yn": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", + "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=", + "dev": true + } + } +} diff --git a/processor/package.json b/processor/package.json new file mode 100644 index 0000000..bfb70c4 --- /dev/null +++ b/processor/package.json @@ -0,0 +1,46 @@ +{ + "name": "azure-event-processor-host", + "version": "0.1.0", + "description": "Azure Event Processor Host (Event Hubs) SDK for Node.js", + "author": "Microsoft Corporation", + "license": "MIT", + "main": "./dist/lib/index.js", + "types": "./typings/lib/index.d.ts", + "dependencies": { + "azure-event-hubs": "../client", + "azure-storage": "^2.8.2", + "uuid": "^3.2.1" + }, + "devDependencies": { + "@types/uuid": "^3.4.3", + "@types/debug": "^0.0.30", + "@types/node": "^8.0.37", + "@types/chai": "^4.1.2", + "@types/chai-as-promised": "^7.1.0", + "@types/mocha": "^5.0.0", + "chai": "^4.1.2", + "chai-as-promised": "^7.1.1", + "mocha": "^5.0.5", + "ts-node": "^5.0.1", + "tslint": "^5.9.1", + "typescript": "^2.8.3", + "dotenv": "^5.0.1", + "@types/dotenv": "^4.0.3" + }, + "scripts": { + "tslint": "tslint -p . -c tslint.json --exclude examples/**/*.ts --exclude tests/**/*.ts", + "tsc": "tsc", + "build": "npm run tslint && npm run tsc", + "test": "npm run build", + "unit": "mocha -r ts-node/register -t 50000 ./tests/**/*.spec.ts --abort-on-uncaught-exception", + "prepare": "npm run build" + }, + "homepage": "http://github.com/azure/azure-event-hubs-node", + "repository": { + "type": "git", + "url": "https://github.com/azure/azure-event-hubs-node.git" + }, + "bugs": { + "url": "http://github.com/Azure/azure-event-hubs-node/issues" + } +} diff --git a/tests/eph.spec.ts b/processor/tests/eph.spec.ts similarity index 96% rename from tests/eph.spec.ts rename to processor/tests/eph.spec.ts index 0df87ba..4131516 100644 --- a/tests/eph.spec.ts +++ b/processor/tests/eph.spec.ts @@ -8,11 +8,11 @@ chai.use(chaiAsPromised); import * as debugModule from "debug"; const should = chai.should(); const debug = debugModule("azure:event-hubs:eph-spec"); -import { EventHubClient, EventProcessorHost, OnEphOpen, EventData, EventPosition, delay } from "../lib"; +import { EventHubClient, EventData, EventPosition, delay } from "../../client/lib"; import * as dotenv from "dotenv"; -import { BlobLeaseManager } from "../lib/eph/blobLeaseManager"; -import { PartitionContext, OnEphMessage } from "../lib/eph"; -import { executePromisesSequentially } from "../lib/util/utils"; +import { BlobLeaseManager } from "../lib/blobLeaseManager"; +import { PartitionContext, OnEphMessage, EventProcessorHost, OnEphOpen } from "../lib"; +import { executePromisesSequentially } from "../../client/lib/util/utils"; dotenv.config(); describe("EPH", function () { diff --git a/tests/lease.spec.ts b/processor/tests/lease.spec.ts similarity index 98% rename from tests/lease.spec.ts rename to processor/tests/lease.spec.ts index 34041aa..fac7300 100644 --- a/tests/lease.spec.ts +++ b/processor/tests/lease.spec.ts @@ -8,8 +8,8 @@ chai.use(chaiAsPromised); import * as debugModule from "debug"; import * as uuid from "uuid/v4"; const debug = debugModule("azure:event-hubs:lease-spec"); -import { BlobLease } from "../lib/eph/blobLease"; -import { parseConnectionString, StorageConnectionStringModel } from "../lib/util/utils"; +import { BlobLease } from "../lib/blobLease"; +import { parseConnectionString, StorageConnectionStringModel } from "azure-event-hubs/lib/util/utils"; import * as dotenv from "dotenv"; dotenv.config(); diff --git a/tests/leaseManager.spec.ts b/processor/tests/leaseManager.spec.ts similarity index 95% rename from tests/leaseManager.spec.ts rename to processor/tests/leaseManager.spec.ts index 8242e35..c209376 100644 --- a/tests/leaseManager.spec.ts +++ b/processor/tests/leaseManager.spec.ts @@ -8,9 +8,9 @@ chai.use(chaiAsPromised); import * as debugModule from "debug"; import * as uuid from "uuid/v4"; const debug = debugModule("azure:event-hubs:lease-spec"); -import { BlobLease } from "../lib/eph/blobLease"; -import { BlobLeaseManager } from "../lib/eph/blobLeaseManager"; -import { parseConnectionString, StorageConnectionStringModel } from "../lib/util/utils"; +import { BlobLease } from "../lib/blobLease"; +import { BlobLeaseManager } from "../lib/blobLeaseManager"; +import { parseConnectionString, StorageConnectionStringModel } from "azure-event-hubs/lib/util/utils"; import * as dotenv from "dotenv"; dotenv.config(); diff --git a/tests/partitionContext.spec.ts b/processor/tests/partitionContext.spec.ts similarity index 92% rename from tests/partitionContext.spec.ts rename to processor/tests/partitionContext.spec.ts index 8d61a95..a40dac0 100644 --- a/tests/partitionContext.spec.ts +++ b/processor/tests/partitionContext.spec.ts @@ -9,10 +9,10 @@ chai.use(chaiAsPromised); import * as debugModule from "debug"; import * as uuid from "uuid/v4"; const debug = debugModule("azure:event-hubs:lease-spec"); -import { PartitionContext } from "../lib/eph/partitionContext" -import { BlobLease } from "../lib/eph/blobLease"; -import { BlobLeaseManager } from "../lib/eph/blobLeaseManager"; -import { parseConnectionString, StorageConnectionStringModel } from "../lib/util/utils"; +import { PartitionContext } from "../lib/partitionContext" +import { BlobLease } from "../lib/blobLease"; +import { BlobLeaseManager } from "../lib/blobLeaseManager"; +import { parseConnectionString, StorageConnectionStringModel } from "azure-event-hubs/lib/util/utils"; import * as dotenv from "dotenv"; dotenv.config(); diff --git a/tsconfig.json b/processor/tsconfig.json similarity index 95% rename from tsconfig.json rename to processor/tsconfig.json index afff4fc..269feaa 100644 --- a/tsconfig.json +++ b/processor/tsconfig.json @@ -31,7 +31,8 @@ "node_modules", "testhub", "typings/**", - "examples/**" + "examples/**", + "client" ], "include": [ "./lib/**/*.ts" diff --git a/tslint.json b/processor/tslint.json similarity index 98% rename from tslint.json rename to processor/tslint.json index ff53503..7e7469a 100644 --- a/tslint.json +++ b/processor/tslint.json @@ -107,7 +107,8 @@ "exclude": [ "examples", "tests", - "testhub/*.ts" + "testhub/*.ts", + "client" ] } } \ No newline at end of file