@jaricardodev/ews-javascript-api
Version:
EWS Managed api in JavaScript
501 lines (428 loc) • 17.3 kB
text/typescript
import { ArgumentException, ArgumentOutOfRangeException } from "../Exceptions/ArgumentException";
import { ArrayHelper } from "../ExtensionMethods";
import { Dictionary, DictionaryWithStringKey } from "../AltDictionary";
import { EwsUtilities } from "../Core/EwsUtilities";
import { Exception } from "../Exceptions/Exception";
import { ExchangeService } from "../Core/ExchangeService";
import { ExchangeVersion } from "../Enumerations/ExchangeVersion";
import { GetStreamingEventsRequest } from "../Core/Requests/GetStreamingEventsRequest";
import { GetStreamingEventsResponse } from "../Core/Responses/GetStreamingEventsResponse";
import { HangingRequestDisconnectEventArgs } from "../Core/Requests/HangingRequestDisconnectEventArgs";
import { NotificationEventArgs } from "./NotificationEventArgs";
import { ServiceError } from "../Enumerations/ServiceError";
import { ServiceLocalException } from "../Exceptions/ServiceLocalException";
import { ServiceResponseException } from "../Exceptions/ServiceResponseException";
import { ServiceResult } from "../Enumerations/ServiceResult";
import { StreamingSubscription } from "./StreamingSubscription";
import { Strings } from "../Strings";
import { SubscriptionErrorEventArgs } from "./SubscriptionErrorEventArgs";
/**
* Represents a connection to an ongoing stream of events.
*
* @sealed
*/
export class StreamingSubscriptionConnection {
/**
* Mapping of streaming id to subscriptions currently on the connection.
*/
private subscriptions: Dictionary<string, StreamingSubscription> = new DictionaryWithStringKey<StreamingSubscription>();
/**
* connection lifetime, in minutes
*/
private connectionTimeout: number = 0;
/**
* ExchangeService instance used to make the EWS call.
*/
private session: ExchangeService = null;
/**
* Value indicating whether the class is disposed.
*/
private isDisposed: boolean = false;
/**
* Currently used instance of a GetStreamingEventsRequest connected to EWS.
*/
private currentHangingRequest: GetStreamingEventsRequest = null;
/**
* Lock object
*/
private lockObject: { isLocked: boolean } = { isLocked: false };
/**
* Occurs when notifications are received from the server.
*/
OnNotificationEvent: NotificationEventDelegate[] = [];
/**
* Occurs when a subscription encounters an error.
*/
OnSubscriptionError: SubscriptionErrorDelegate[] = [];
/**
* Occurs when a streaming subscription connection is disconnected from the server.
*/
OnDisconnect: SubscriptionErrorDelegate[] = [];
/**
* Occurs when a streaming subscription connection gets headers from the server.
*/
OnResponseHeader: ResponseHeaderDelegate[] = [];
/**
* Getting the current subscriptions in this connection.
*/
get CurrentSubscriptions(): StreamingSubscription[] {
var list: StreamingSubscription[] = [];
//while (this.lockObject.isLocked) { }; //todo: implement blocking and non blocking multi threading when available.
//this.lockObject.isLocked = true;
//try {
ArrayHelper.AddRange(list, this.subscriptions.Values);
// } finally {
// this.lockObject.isLocked = false;
// }
return list;
}
/**
* Gets a value indicating whether this connection is opened
*/
get IsOpen(): boolean {
this.ThrowIfDisposed();
if (this.currentHangingRequest == null) {
return false
}
else {
return this.currentHangingRequest.IsConnected;
}
}
/**
* Initializes a new instance of the **StreamingSubscriptionConnection** class.
*
* @param {ExchangeService} service The ExchangeService instance this connection uses to connect to the server.
* @param {number} lifetime The maximum time, in minutes, the connection will remain open. Lifetime must be between 1 and 30.
*/
constructor(service: ExchangeService, lifetime: number);
/**
* Initializes a new instance of the **StreamingSubscriptionConnection** class.
*
* @param {ExchangeService} service The ExchangeService instance this connection uses to connect to the server.
* @param {StreamingSubscription[]} subscriptions The streaming subscriptions this connection is receiving events for.
* @param {number} lifetime The maximum time, in minutes, the connection will remain open. Lifetime must be between 1 and 30.
*/
constructor(service: ExchangeService, subscriptions: StreamingSubscription[], lifetime: number);
constructor(service: ExchangeService, lifetimeOrSubscriptions: number | StreamingSubscription[], lifetime?: number) {
EwsUtilities.ValidateParam(service, "service");
EwsUtilities.ValidateClassVersion(
service,
ExchangeVersion.Exchange2010_SP1,
"StreamingSubscriptionConnection");
let argsLength = arguments.length;
let lifeTime: number = argsLength === 2 ? <number>lifetimeOrSubscriptions : lifetime;
if (lifeTime < 1 || lifeTime > 30) {
throw new ArgumentOutOfRangeException("lifetime");
}
this.session = service;
this.connectionTimeout = lifeTime;
if (argsLength === 3) {
let subscriptions: StreamingSubscription[] = <StreamingSubscription[]>lifetimeOrSubscriptions;
EwsUtilities.ValidateParamCollection(subscriptions, "subscriptions");
subscriptions.forEach((subscription: StreamingSubscription) => {
this.subscriptions.Add(subscription.Id, subscription);
})
}
}
/**
* Adds a subscription to this connection.
*
* @param {StreamingSubscription} subscription The subscription to add.
*/
AddSubscription(subscription: StreamingSubscription): void {
this.ThrowIfDisposed();
EwsUtilities.ValidateParam(subscription, "subscription");
this.ValidateConnectionState(false, Strings.CannotAddSubscriptionToLiveConnection);
//while (this.lockObject.isLocked) { }; //todo: implement blocking and non blocking multi threading when available.
//this.lockObject.isLocked = true;
//try {
if (!this.subscriptions.containsKey(subscription.Id)) {
this.subscriptions.Add(subscription.Id, subscription);
}
// } finally {
// this.lockObject.isLocked = false;
// }
}
/**
* Closes this connection so it stops receiving events from the server.
* This terminates a long-standing call to EWS.
*
* @exception {InvalidOperationException} Thrown when Close is called while not connected.
*/
Close(): void {
//todo: implement blocking and non blocking multi threading when available.
//lock(this.lockObject) {
this.ThrowIfDisposed();
this.ValidateConnectionState(true, Strings.CannotCallDisconnectWithNoLiveConnection);
// Further down in the stack, this will result in a call to our OnRequestDisconnect event handler,
// doing the necessary cleanup.
this.currentHangingRequest.Disconnect();
//}
}
/**
* Frees resources associated with this StreamingSubscriptionConnection.
*/
Dispose(): void;
/**
* @private Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
*
* @param {boolean} suppressFinalizer Value indicating whether to suppress the garbage collector's finalizer..
*/
Dispose(suppressFinalizer: boolean): void;
Dispose(suppressFinalizer: boolean = false): void {
if (suppressFinalizer) {
//GC.SuppressFinalize(this);
}
//todo: implement blocking and non blocking multi threading when available.
//lock(this.lockObject) {
if (!this.isDisposed) {
if (this.currentHangingRequest != null) {
this.currentHangingRequest = null;
}
this.subscriptions = null;
this.session = null;
this.isDisposed = true;
}
//}
}
/**
* Handles the service response object.
*
* @param {any} response The response.
*/
private HandleServiceResponseObject(response: any): void {
let gseResponse: GetStreamingEventsResponse = response as GetStreamingEventsResponse;
if (!(gseResponse instanceof GetStreamingEventsResponse)) {
throw new ArgumentException();
}
else {
if (gseResponse.Result == ServiceResult.Success || gseResponse.Result == ServiceResult.Warning) {
if (gseResponse.Results.Notifications.length > 0) {
// We got notifications; dole them out.
this.IssueNotificationEvents(gseResponse);
}
else {
//// This was just a heartbeat, nothing to do here.
}
}
else if (gseResponse.Result == ServiceResult.Error) {
if (gseResponse.ErrorSubscriptionIds == null ||
gseResponse.ErrorSubscriptionIds.length == 0) {
// General error
this.IssueGeneralFailure(gseResponse);
}
else {
// subscription-specific errors
this.IssueSubscriptionFailures(gseResponse);
}
}
}
}
/**
* Internal helper method called when the request disconnects.
*
* @param {Exception} ex The exception that caused the disconnection. May be null.
*/
private InternalOnDisconnect(ex: Exception): void {
this.currentHangingRequest = null;
if (this.OnDisconnect && ArrayHelper.isArray(this.OnDisconnect)) {
try {
this.OnDisconnect.forEach((onDisconnect) => { onDisconnect(this, new SubscriptionErrorEventArgs(null, ex)) });
}
catch (e) { }
}
}
/**
* Internal helper method called when the request receives headers.
*
* @param {any} headers The headerf from server.
*/
private InternalOnResponseHeader(headers: any): void {
if (this.OnResponseHeader && ArrayHelper.isArray(this.OnResponseHeader)) {
try {
this.OnResponseHeader.forEach((onHeader) => { onHeader(headers) });
}
catch (e) { }
}
}
/**
* Issues the general failure.
*
* @param {GetStreamingEventsResponse} gseResponse The GetStreamingEvents response.
*/
private IssueGeneralFailure(gseResponse: GetStreamingEventsResponse): void {
let eventArgs: SubscriptionErrorEventArgs = new SubscriptionErrorEventArgs(
null,
new ServiceResponseException(gseResponse));
if (this.OnSubscriptionError && ArrayHelper.isArray(this.OnSubscriptionError)) {
try {
this.OnSubscriptionError.forEach((OnSubscriptionError) => { OnSubscriptionError(this, eventArgs) });
}
catch (e) { }
}
}
/**
* Issues the notification events.
*
* @param {GetStreamingEventsResponse} gseResponse The GetStreamingEvents response.
*/
private IssueNotificationEvents(gseResponse: GetStreamingEventsResponse): void {
for (let events of gseResponse.Results.Notifications) {
let subscription: StreamingSubscription = null;
//todo: implement blocking and non blocking multi threading when available.
//lock(this.lockObject) {
// Client can do any good or bad things in the below event handler
if (this.subscriptions != null && this.subscriptions.containsKey(events.SubscriptionId)) {
subscription = this.subscriptions.get(events.SubscriptionId);
}
//}
if (subscription != null) {
let eventArgs: NotificationEventArgs = new NotificationEventArgs(
subscription,
events.Events);
if (this.OnNotificationEvent && ArrayHelper.isArray(this.OnNotificationEvent)) {
try {
this.OnNotificationEvent.forEach((OnNotificationEvent) => { OnNotificationEvent(this, eventArgs) });
}
catch (e) { }
}
}
}
}
/**
* Issues the subscription failures.
*
* @param {GetStreamingEventsResponse} gseResponse The GetStreamingEvents response.
*/
private IssueSubscriptionFailures(gseResponse: GetStreamingEventsResponse): void {
let exception: ServiceResponseException = new ServiceResponseException(gseResponse);
for (let id of gseResponse.ErrorSubscriptionIds) {
let subscription: StreamingSubscription = null;
//todo: implement blocking and non blocking multi threading when available.
//lock(this.lockObject) {
// Client can do any good or bad things in the below event handler
if (this.subscriptions != null && this.subscriptions.containsKey(id)) {
subscription = this.subscriptions.get(id);
}
//}
if (subscription != null) {
let eventArgs: SubscriptionErrorEventArgs = new SubscriptionErrorEventArgs(
subscription,
exception);
if (this.OnSubscriptionError && ArrayHelper.isArray(this.OnSubscriptionError)) {
try {
this.OnSubscriptionError.forEach((OnSubscriptionError) => { OnSubscriptionError(this, eventArgs) });
}
catch (e) { }
}
}
if (gseResponse.ErrorCode != ServiceError.ErrorMissedNotificationEvents) {
// Client can do any good or bad things in the above event handler
//todo: implement blocking and non blocking multi threading when available.
//lock(this.lockObject) {
if (this.subscriptions != null && this.subscriptions.containsKey(id)) {
// We are no longer servicing the subscription.
this.subscriptions.remove(id);
}
//}
}
}
}
/**
* Called when the request is disconnected.
*
* @param {any} sender The sender.
* @param {HangingRequestDisconnectEventArgs} args The instance containing the event data.
*/
private OnRequestDisconnect(sender: any, args: HangingRequestDisconnectEventArgs): void {
this.InternalOnDisconnect(args.Exception);
}
/**
* Opens this connection so it starts receiving events from the server.
* This results in a long-standing call to EWS.
*
* @exception {InvalidOperationException} Thrown when Open is called while connected.
*/
Open(): Promise<void> {
//todo: implement blocking and non blocking multi threading when available.
//lock(this.lockObject) {
this.ThrowIfDisposed();
this.ValidateConnectionState(false, Strings.CannotCallConnectDuringLiveConnection);
if (this.subscriptions.Count == 0) {
throw new ServiceLocalException(Strings.NoSubscriptionsOnConnection);
}
this.currentHangingRequest = new GetStreamingEventsRequest(
this.session,
this.HandleServiceResponseObject.bind(this),
this.subscriptions.Keys,
this.connectionTimeout);
this.currentHangingRequest.OnDisconnect.push(this.OnRequestDisconnect.bind(this)); //todo: fix if needed multiple instance new HangingServiceRequestBase.HangingRequestDisconnectHandler(this.OnRequestDisconnect)
this.currentHangingRequest.OnResponseHeader = this.InternalOnResponseHeader.bind(this); //todo: fix if needed multiple instance new HangingServiceRequestBase.HangingRequestDisconnectHandler(this.OnRequestDisconnect)
return this.currentHangingRequest.InternalExecute();
//}
}
/**
* Removes the specified streaming subscription from the connection.
*
* @param {StreamingSubscription} subscription The subscription to remove.
*/
RemoveSubscription(subscription: StreamingSubscription): void {
this.ThrowIfDisposed();
EwsUtilities.ValidateParam(subscription, "subscription");
this.ValidateConnectionState(false, Strings.CannotRemoveSubscriptionFromLiveConnection);
//while (this.lockObject.isLocked) { }; //todo: implement blocking and non blocking multi threading when available.
//this.lockObject.isLocked = true;
//try {
this.subscriptions.remove(subscription.Id);
// } finally {
// this.lockObject.isLocked = false;
// }
}
/**
* Throws if disposed.
*/
ThrowIfDisposed(): void {
if (this.isDisposed) {
throw new Error("ObjectDisposedException - StreamingSubscriptionConnection"); //ObjectDisposedException
}
}
/**
* Validates the state of the connection.
*
* @param {boolean} isConnectedExpected Value indicating whether we expect to be currently connected.
* @param {string} errorMessage The error message.
*/
ValidateConnectionState(isConnectedExpected: boolean, errorMessage: string): void {
if ((isConnectedExpected && !this.IsOpen) ||
(!isConnectedExpected && this.IsOpen)) {
throw new ServiceLocalException(errorMessage);
}
}
}
/**
* Represents a delegate that is invoked when notifications are received from the server
*
* @param {any} sender The StreamingSubscriptionConnection instance that received the events.
* @param {NotificationEventArgs} args The event data.
*/
export interface NotificationEventDelegate {
(sender: any, args: NotificationEventArgs): void;
}
/**
* Represents a delegate that is invoked when an error occurs within a streaming subscription connection.
*
* @param {any} sender The StreamingSubscriptionConnection instance within which the error occurred.
* @param {SubscriptionErrorEventArgs} args The event data.
*/
export interface SubscriptionErrorDelegate {
(sender: any, args: SubscriptionErrorEventArgs): void;
}
/**
* Represents a delegate that is invoked when an error occurs within a streaming subscription connection.
*
* @param {any} sender The StreamingSubscriptionConnection instance within which the error occurred.
* @param {SubscriptionErrorEventArgs} args The event data.
*/
export interface ResponseHeaderDelegate {
(header: any): void;
}