@azure/event-hubs
Version:
Azure Event Hubs SDK for JS.
336 lines • 15.9 kB
JavaScript
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import { EventProcessor } from "./eventProcessor.js";
import { createConnectionContext } from "./connectionContext.js";
import { BalancedLoadBalancingStrategy } from "./loadBalancerStrategies/balancedStrategy.js";
import { Constants } from "@azure/core-amqp";
import { GreedyLoadBalancingStrategy } from "./loadBalancerStrategies/greedyStrategy.js";
import { InMemoryCheckpointStore } from "./inMemoryCheckpointStore.js";
import { PartitionGate } from "./impl/partitionGate.js";
import { UnbalancedLoadBalancingStrategy } from "./loadBalancerStrategies/unbalancedStrategy.js";
import { isCredential } from "./util/typeGuards.js";
import { logger } from "./logger.js";
import { validateEventPositions } from "./eventPosition.js";
import { getRandomName } from "./util/utils.js";
const defaultConsumerClientOptions = {
// to support our current "process single event only" workflow we'll also purposefully
// only request a single event at a time.
maxBatchSize: 1,
maxWaitTimeInSeconds: 60,
};
/**
* The `EventHubConsumerClient` class is used to consume events from an Event Hub.
*
* There are multiple ways to create an `EventHubConsumerClient`
* - Use the connection string from the SAS policy created for your Event Hub instance.
* - Use the connection string from the SAS policy created for your Event Hub namespace,
* and the name of the Event Hub instance
* - Use the full namespace like `<yournamespace>.servicebus.windows.net`, and a credentials object.
*
* Optionally, you can also pass:
* - An options bag to configure the retry policy or proxy settings.
* - A checkpoint store that is used by the client to read checkpoints to determine the position from where it should
* resume receiving events when your application gets restarted. The checkpoint store is also used by the client
* to load balance multiple instances of your application.
*/
export class EventHubConsumerClient {
_consumerGroup;
/**
* Describes the amqp connection context for the client.
*/
_context;
/**
* The options passed by the user when creating the EventHubClient instance.
*/
_clientOptions;
_partitionGate = new PartitionGate();
/**
* The Subscriptions that were spawned by calling `subscribe()`.
* Subscriptions that have been stopped by the user will not
* be present in this set.
*/
_subscriptions = new Set();
/**
* The name of the default consumer group in the Event Hubs service.
*/
static defaultConsumerGroupName = Constants.defaultConsumerGroup;
_checkpointStore;
_userChoseCheckpointStore;
/**
* Options for configuring load balancing.
*/
_loadBalancingOptions;
/**
* @readonly
* The name of the Event Hub instance for which this client is created.
*/
get eventHubName() {
return this._context.config.entityPath;
}
/**
* @readonly
* The fully qualified namespace of the Event Hub instance for which this client is created.
* This is likely to be similar to <yournamespace>.servicebus.windows.net.
*/
get fullyQualifiedNamespace() {
return this._context.config.host;
}
/**
* The name used to identify this EventHubConsumerClient.
* If not specified or empty, a random unique one will be generated.
*/
identifier;
constructor(_consumerGroup, connectionStringOrFullyQualifiedNamespace2, checkpointStoreOrEventHubNameOrOptions3, checkpointStoreOrCredentialOrOptions4, checkpointStoreOrOptions5, options6) {
this._consumerGroup = _consumerGroup;
if (isCredential(checkpointStoreOrCredentialOrOptions4)) {
// #3 or 3.1
logger.info("Creating EventHubConsumerClient with TokenCredential.");
if (isCheckpointStore(checkpointStoreOrOptions5)) {
// 3.1
this._checkpointStore = checkpointStoreOrOptions5;
this._userChoseCheckpointStore = true;
this._clientOptions = options6 || {};
}
else {
this._checkpointStore = new InMemoryCheckpointStore();
this._userChoseCheckpointStore = false;
this._clientOptions = checkpointStoreOrOptions5 || {};
}
this._context = createConnectionContext(connectionStringOrFullyQualifiedNamespace2, checkpointStoreOrEventHubNameOrOptions3, checkpointStoreOrCredentialOrOptions4, this._clientOptions);
}
else if (typeof checkpointStoreOrEventHubNameOrOptions3 === "string") {
// #2 or 2.1
logger.info("Creating EventHubConsumerClient with connection string and event hub name.");
if (isCheckpointStore(checkpointStoreOrCredentialOrOptions4)) {
// 2.1
this._checkpointStore = checkpointStoreOrCredentialOrOptions4;
this._userChoseCheckpointStore = true;
this._clientOptions = checkpointStoreOrOptions5 || {};
}
else {
// 2
this._checkpointStore = new InMemoryCheckpointStore();
this._userChoseCheckpointStore = false;
this._clientOptions = checkpointStoreOrCredentialOrOptions4 || {};
}
this._context = createConnectionContext(connectionStringOrFullyQualifiedNamespace2, checkpointStoreOrEventHubNameOrOptions3, this._clientOptions);
}
else {
// #1 or 1.1
logger.info("Creating EventHubConsumerClient with connection string.");
if (isCheckpointStore(checkpointStoreOrEventHubNameOrOptions3)) {
// 1.1
this._checkpointStore = checkpointStoreOrEventHubNameOrOptions3;
this._userChoseCheckpointStore = true;
this._clientOptions =
checkpointStoreOrCredentialOrOptions4 || {};
}
else {
// 1
this._checkpointStore = new InMemoryCheckpointStore();
this._userChoseCheckpointStore = false;
this._clientOptions =
checkpointStoreOrEventHubNameOrOptions3 || {};
}
this._context = createConnectionContext(connectionStringOrFullyQualifiedNamespace2, this._clientOptions);
}
this.identifier = this._clientOptions.identifier ?? getRandomName();
this._loadBalancingOptions = {
// default options
strategy: "balanced",
updateIntervalInMs: 10000,
partitionOwnershipExpirationIntervalInMs: 60000,
// options supplied by user
...this._clientOptions?.loadBalancingOptions,
};
}
/**
* Closes the AMQP connection to the Event Hub instance,
* returning a promise that will be resolved when disconnection is completed.
* @returns Promise<void>
* @throws Error if the underlying connection encounters an error while closing.
*/
async close() {
// Stop all the actively running subscriptions.
const activeSubscriptions = Array.from(this._subscriptions);
await Promise.all(activeSubscriptions.map((subscription) => {
return subscription.close();
}));
// Close the connection via the connection context.
return this._context.close();
}
/**
* Provides the id for each partition associated with the Event Hub.
* @param options - The set of options to apply to the operation call.
* @returns A promise that resolves with an Array of strings representing the id for
* each partition associated with the Event Hub.
* @throws Error if the underlying connection has been closed, create a new EventHubConsumerClient.
* @throws AbortError if the operation is cancelled via the abortSignal.
*/
async getPartitionIds(options = {}) {
const eventHubProperties = await this._context.managementSession.getEventHubProperties({
...options,
retryOptions: this._clientOptions.retryOptions,
});
return eventHubProperties.partitionIds;
}
/**
* Provides information about the state of the specified partition.
* @param partitionId - The id of the partition for which information is required.
* @param options - The set of options to apply to the operation call.
* @returns A promise that resolves with information about the state of the partition .
* @throws Error if the underlying connection has been closed, create a new EventHubConsumerClient.
* @throws AbortError if the operation is cancelled via the abortSignal.
*/
getPartitionProperties(partitionId, options = {}) {
return this._context.managementSession.getPartitionProperties(partitionId, {
...options,
retryOptions: this._clientOptions.retryOptions,
});
}
/**
* Provides the Event Hub runtime information.
* @param options - The set of options to apply to the operation call.
* @returns A promise that resolves with information about the Event Hub instance.
* @throws Error if the underlying connection has been closed, create a new EventHubConsumerClient.
* @throws AbortError if the operation is cancelled via the abortSignal.
*/
getEventHubProperties(options = {}) {
return this._context.managementSession.getEventHubProperties({
...options,
retryOptions: this._clientOptions.retryOptions,
});
}
subscribe(handlersOrPartitionId1, optionsOrHandlers2, possibleOptions3) {
let eventProcessor;
let targetedPartitionId;
if (isSubscriptionEventHandlers(handlersOrPartitionId1)) {
// #1: subscribe overload - read from all partitions
const options = optionsOrHandlers2;
if (options && options.startPosition) {
validateEventPositions(options.startPosition);
}
({ targetedPartitionId, eventProcessor } = this.createEventProcessorForAllPartitions(handlersOrPartitionId1, options));
}
else if (isSubscriptionEventHandlers(optionsOrHandlers2)) {
// #2: subscribe overload (read from specific partition IDs), don't coordinate
const options = possibleOptions3;
if (options && options.startPosition) {
validateEventPositions(options.startPosition);
}
({ targetedPartitionId, eventProcessor } = this.createEventProcessorForSinglePartition(
// cast to string as downstream code expects partitionId to be string, but JS users could have given us anything.
// we don't validate the user input and instead rely on service throwing errors if any
String(handlersOrPartitionId1), optionsOrHandlers2, possibleOptions3));
}
else {
throw new TypeError("Unhandled subscribe() overload");
}
eventProcessor.start();
const subscription = {
get isRunning() {
return eventProcessor.isRunning();
},
close: () => {
this._partitionGate.remove(targetedPartitionId);
this._subscriptions.delete(subscription);
return eventProcessor.stop();
},
};
this._subscriptions.add(subscription);
return subscription;
}
/**
* Gets the LoadBalancing strategy that should be used based on what the user provided.
*/
_getLoadBalancingStrategy() {
if (!this._userChoseCheckpointStore) {
// The default behavior when a checkpointstore isn't provided
// is to always grab all the partitions.
return new UnbalancedLoadBalancingStrategy();
}
const partitionOwnershipExpirationIntervalInMs = this._loadBalancingOptions.partitionOwnershipExpirationIntervalInMs;
if (this._loadBalancingOptions?.strategy === "greedy") {
return new GreedyLoadBalancingStrategy(partitionOwnershipExpirationIntervalInMs);
}
// The default behavior when a checkpointstore is provided is
// to grab one partition at a time.
return new BalancedLoadBalancingStrategy(partitionOwnershipExpirationIntervalInMs);
}
createEventProcessorForAllPartitions(subscriptionEventHandlers, options) {
this._partitionGate.add("all");
if (this._userChoseCheckpointStore) {
logger.verbose("EventHubConsumerClient subscribing to all partitions, using a checkpoint store.");
}
else {
logger.verbose("EventHubConsumerClient subscribing to all partitions, no checkpoint store.");
}
const loadBalancingStrategy = this._getLoadBalancingStrategy();
const eventProcessor = this._createEventProcessor(this._context, subscriptionEventHandlers, this._checkpointStore, {
...defaultConsumerClientOptions,
...options,
ownerLevel: getOwnerLevel(options, this._userChoseCheckpointStore),
// make it so all the event processors process work with the same overarching owner ID
// this allows the EventHubConsumer to unify all the work for any processors that it spawns
ownerId: this.identifier,
retryOptions: this._clientOptions.retryOptions,
loadBalancingStrategy,
loopIntervalInMs: this._loadBalancingOptions.updateIntervalInMs,
});
return { targetedPartitionId: "all", eventProcessor };
}
createEventProcessorForSinglePartition(partitionId, eventHandlers, options) {
this._partitionGate.add(partitionId);
const subscribeOptions = options;
if (this._userChoseCheckpointStore) {
logger.verbose(`EventHubConsumerClient subscribing to specific partition (${partitionId}), using a checkpoint store.`);
}
else {
logger.verbose(`EventHubConsumerClient subscribing to specific partition (${partitionId}), no checkpoint store.`);
}
const eventProcessor = this._createEventProcessor(this._context, eventHandlers, this._checkpointStore, {
...defaultConsumerClientOptions,
...options,
processingTarget: partitionId,
ownerLevel: getOwnerLevel(subscribeOptions, this._userChoseCheckpointStore),
retryOptions: this._clientOptions.retryOptions,
loadBalancingStrategy: new UnbalancedLoadBalancingStrategy(),
loopIntervalInMs: this._loadBalancingOptions.updateIntervalInMs ?? 10000,
});
return { targetedPartitionId: partitionId, eventProcessor };
}
_createEventProcessor(connectionContext, subscriptionEventHandlers, checkpointStore, options) {
return new EventProcessor(this._consumerGroup, connectionContext, subscriptionEventHandlers, checkpointStore, options);
}
}
/**
* @internal
*/
export function isCheckpointStore(possible) {
if (!possible) {
return false;
}
const checkpointStore = possible;
return (typeof checkpointStore.claimOwnership === "function" &&
typeof checkpointStore.listCheckpoints === "function" &&
typeof checkpointStore.listOwnership === "function" &&
typeof checkpointStore.updateCheckpoint === "function");
}
/**
* @internal
*/
function isSubscriptionEventHandlers(possible) {
return typeof possible.processEvents === "function";
}
function getOwnerLevel(options, userChoseCheckpointStore) {
if (options && options.ownerLevel) {
return options.ownerLevel;
}
if (userChoseCheckpointStore) {
return 0;
}
else {
return undefined;
}
}
//# sourceMappingURL=eventHubConsumerClient.js.map