UNPKG

barracuda-client-api

Version:

API Client to connect to Barracuda Enterprise Service Bus

1,714 lines (1,576 loc) 71.2 kB
import { IBarracudaClientFullDuplex, BarracudaConnectionStatus, BarracudaConnectionProps, IBarracudaBridgeSubscriptionResponse, IBarracudaBridgeMessage, IBarracudaBridgeConnectionStatus, BarracudaTopicInfoType, BarracudaBridgeSubscriptionStatus, IBarracudaBridgeRequestResponse, IBarracudaClient, BarracudaBridgeMessageType, BarracudaBuildInfo, BarracudaPublishingProps, BarracudaErrorHandler, BarracudaConnectionHandlers, BarracudaQuery, QuerySubscription, BarracudaQuerySort, BarracudaDefaultProps, ConnectionStateChangeHandler, SubscriptionUpdateHandler, BarracudaBridgeResponseCommandTypes, } from "./IBarracudaClient"; import { loglevels, shouldLogErrors, isDebug, logError, logInfo, isInfo, logDebug, isWarn, logWarn, isVerbose, logVerbose, } from "./logging/index"; import { makeConnectionQuery } from "./helpers/connectionProps"; import { BarracudaBrowserWebSocket } from "./BarracudaBrowserWebSocket"; import { IBarracudaWebSocket } from "./IBarracudaWebSocket"; import { BarracudaError } from "./error/index"; import { fetch, Headers } from "cross-fetch"; import BarracudaClientBuildDetails from "./BarracudaClientBuildDetail"; import URI from "urijs"; // tslint:disable-next-line:no-require-imports import WsWebSocket from "ws"; import { uuid4 } from "./react-native/uuid"; import { iBtoa } from "./react-native/Base64"; interface IBarracudaBridgePing extends IBarracudaBridgeMessage { tst: number; } interface IBarracudaBridgePong extends IBarracudaBridgeMessage { tst: number; } interface IBarracudaBridgeRequestBase extends IBarracudaBridgeMessage { requestId: string; } interface IBarracudaBridgeRequest extends IBarracudaBridgeRequestBase { command: BarracudaCommand; } interface IBarracudaBridgeCommandRequest extends IBarracudaBridgeRequestBase { database: string; command: any; } interface IBarracudaBridgeCommandRequestResponse extends IBarracudaBridgeRequestResponse { response: any; } interface IBarracudaBridgeSubscriptionStatus extends IBarracudaBridgeMessage { subscriptionId: string; status: Extract<"heartbeat" | "error", BarracudaBridgeSubscriptionStatus>; error?: string; } interface IBarracudaBridgeSubscriptionRequest extends IBarracudaBridgeRequestBase { filter?: string; pipeline?: any[]; orderBy?: unknown; projection?: string[] | undefined; command: | BarracudaBridgeReadQueryCommands | BarracudaBridgeQueryCommands | BarracudaBridgeAggregateCommands; database?: string; collection?: string; top?: number; subscriptionId?: string; heartbeatPeriod: number; batchSize?: number; batchPeriod?: number; } export enum BarracudaCommand { close = "close", } export enum BarracudaBridgeReadQueryCommands { aggregate = "aggregate", diffSubscribe = "diffSubscribe", snapshot = "snapshot", snapshotAndDiffSubscribe = "snapshotAndDiffSubscribe", snapshotAndSubscribe = "snapshotAndSubscribe", subscribe = "subscribe", } enum BarracudaBridgeQueryCommands { unsubscribe = "unsubscribe", } export enum BarracudaBridgeAggregateCommands { unsubscribe = "unsubscribe", } export interface IBarracudaSubscriptionStatus extends Omit<IBarracudaSubscription<unknown>, "onMessage" | "inboundQueue"> {} interface IBarracudaSubscription<T> { unsubscribed: boolean; pendingUnsubscribe: boolean; subscriptionId: string; requestBarracudaQuery?: BarracudaQuery<T>; requestCommandType?: BarracudaBridgeReadQueryCommands; responseMsg: IBarracudaBridgeRequestResponse | undefined; onMessage?: (msg: IBarracudaBridgeSubscriptionResponse<T>) => void; inboundQueue: IBarracudaBridgeSubscriptionResponse<T>[]; lastServerStatus: BarracudaBridgeSubscriptionStatus; confirmed: boolean; lastServerStatusDt: Date; heartbeatPeriod: number; messageCount: number; // requestMsg: IBarracudaBridgeSubscriptionRequest; lastError?: string; excludeFromRecovery?: boolean; } function createWebSocket(ep: string): IBarracudaWebSocket { if (typeof window === "undefined" || typeof WsWebSocket === "undefined") { return new WsWebSocket(ep) as IBarracudaWebSocket; } else { return new BarracudaBrowserWebSocket(ep); } } interface IBarracudaRequestState { request: IBarracudaBridgeRequestBase; responseReceived: boolean; responseReceivedDt: Date | undefined; responseProcessed: boolean; responseProcessedDt: Date | undefined; onResponse?: (response: IBarracudaBridgeRequestResponse) => void; } export const DefaultQueryTopValue = 50_000; interface Subscriptions { [subscriptionId: string]: IBarracudaSubscription<any>; } function toBoolean(v: boolean | string | undefined): boolean { switch (typeof v) { case "boolean": return v; case "string": switch (v.toLocaleLowerCase()) { case "true": case "1": case "on": case "yes": return true; default: return false; } default: return !!v; } } export class BarracudaClient implements IBarracudaClientFullDuplex { private skipReconnecting: boolean = false; private readonly _uid: string; private ws: IBarracudaWebSocket | undefined; private _requests: { [requestId: string]: IBarracudaRequestState; } = {}; private _onStale: SubscriptionUpdateHandler | undefined; private _staleTriggerTimer: number | undefined; private _staleNotice: { [subscriptionId: string]: Date } = {}; private _defaultRestPublishEndPoint = `https://barracuda-rest-dev.citadelgroup.com`; private _defaultInstance: string | undefined; private _connectedAckAtLeastOnce = false; private _reconnectCounter = 0; private _subscriptionHistory: Subscriptions[] = []; constructor(defaultProps?: BarracudaDefaultProps) { this._uid = uuid4(); this.setProps(defaultProps); } public static get Build(): BarracudaBuildInfo { return BarracudaClientBuildDetails; } private _appName: string | undefined; public get appName(): string | undefined { return this._appName; } private _appVersion: string | undefined; public get appVersion(): string | undefined { return this._appVersion; } private _lastConnectionProps: BarracudaConnectionProps = this as BarracudaConnectionProps; private _lastPong: Date | undefined; public get lastPong(): Date | undefined { return this._lastPong; } public get serverClientId(): any { return this.connectionAckMessage?.clientId; } private _serverDebug?: boolean; public get serverDebug(): boolean { return !!this._serverDebug; } public get restPublishEndPoint(): string { return this._defaultRestPublishEndPoint; } private _gaToken: string | undefined; public get gaToken(): string | undefined { return this._gaToken; } private _connectionCounter: number = 0; private _connectionAckMessage: IBarracudaBridgeConnectionStatus | undefined; public get serviceHost(): string | undefined { return this.connectionAckMessage?.host; } private _reconnect: boolean = false; public get reconnect(): boolean { return this._reconnect; } public set reconnect(value: boolean) { this._reconnect = value; } private _onConnectionError?: BarracudaErrorHandler | undefined; public get onConnectionError(): BarracudaErrorHandler | undefined { return this._onConnectionError; } public set onConnectionError(value: BarracudaErrorHandler | undefined) { this._onConnectionError = value; } private _onError?: BarracudaErrorHandler; public get onError(): BarracudaErrorHandler { return this._onError; } public set onError(value: BarracudaErrorHandler) { this._onError = value; } private _onConnectionStateChange?: ConnectionStateChangeHandler; public get onConnectionStateChange(): | ConnectionStateChangeHandler | undefined { return this._onConnectionStateChange; } public set onConnectionStateChange( value: ConnectionStateChangeHandler | undefined ) { this._onConnectionStateChange = value; } private _onSubscriptionUpdate: SubscriptionUpdateHandler | undefined; public get onSubscriptionUpdate(): SubscriptionUpdateHandler | undefined { return this._onSubscriptionUpdate; } public set onSubscriptionUpdate( value: SubscriptionUpdateHandler | undefined ) { this._onSubscriptionUpdate = value; } private _loglevel = loglevels.info; public get loglevel(): loglevels { return this._loglevel; } public set loglevel(value: loglevels) { this._loglevel = value; } public connectionState: BarracudaConnectionStatus = BarracudaConnectionStatus.unknown; public get uid(): string { return this._uid; } private _logUid: string | undefined; public get logUid(): string { return ( this._logUid ?? (this._logUid = this.uid.substr(this.uid.length - 5)) ); } public get connectionAckMessage(): | IBarracudaBridgeConnectionStatus | undefined { return this._connectionAckMessage; } public get lastConnectionProps(): BarracudaConnectionProps { return this._lastConnectionProps; } public get defaultConnectionProps(): BarracudaDefaultProps { return { ...BarracudaClient.getConnectionProps(this) }; } public async connect( overrideProps?: BarracudaConnectionProps ): Promise<IBarracudaClient> { const connectionProps = BarracudaClient.getConnectionProps( this, overrideProps ); if (isDebug(this.ll)) { logDebug(`${this.bcLog(this.lastConnectionProps)}`, { defaultConnectionProps: this.defaultConnectionProps, overrideProps, connectionProps, }); } if (!connectionProps.endpoint) { throw new BarracudaError( `${this.bcLog(connectionProps)} Can't connect to an empty endpoint.`, { connectionProps, } ); } switch (this.connectionState) { case BarracudaConnectionStatus.connected: case BarracudaConnectionStatus.connectedAck: if (isWarn(this.ll)) { logWarn( "Already connected. Cannot reconnect again without closing the connection first.", { activeSubscription: Object.values(this._subscriptions).length, activeRequests: Object.values(this._requests).filter( (r) => !r.responseProcessed ).length, } ); } return Promise.resolve(this); case BarracudaConnectionStatus.connecting: logWarn( "Actively connecting. Cannot connect again without closing the connection first.", { activeSubscription: Object.values(this._subscriptions).length, activeRequests: Object.values(this._requests).filter( (r) => !r.responseProcessed ).length, } ); return Promise.resolve(this); case BarracudaConnectionStatus.reconnecting: case BarracudaConnectionStatus.disconnected: case BarracudaConnectionStatus.connectionError: case BarracudaConnectionStatus.unknown: this.triggerOnConnectionStateChange( BarracudaConnectionStatus.connecting, connectionProps ); break; case BarracudaConnectionStatus.closing: logWarn("Closing, wait until closed.", { activeSubscription: !this._subscriptions ? 0 : Object.values(this._subscriptions).length, activeRequests: !this._requests ? 0 : Object.values(this._requests).filter((r) => !r?.responseProcessed) .length, }); return Promise.resolve(this); } return new Promise( async (connectionPendingResolve, rejectPendingConnection) => { try { this._lastConnectionProps = connectionProps; this.clearConnectionState(); const endPointUrlString = makeConnectionQuery( connectionProps, this.serverDebug ); if (isDebug(this.ll)) { logDebug( `${this.bcLog(connectionProps)} connecting Barracuda Client`, { connectionProps, endPointUrl: endPointUrlString, } ); } else { if (isInfo(this.ll)) { logInfo( `${this.bcLog(connectionProps)} connecting Barracuda Client` ); } } this.ws = createWebSocket(endPointUrlString); this.ws.once("message", (ack: string) => { if (isDebug(this.ll)) { logDebug(`${this.bcLog(connectionProps)} WS first MSG ACK`, { ack, connectionProps, }); } try { const ackMsg: any = JSON.parse(ack); this._connectionAckMessage = ackMsg; if (ackMsg.status === "Connected") { this._connectedAckAtLeastOnce = true; this._connectionCounter++; this.triggerOnConnectionStateChange( BarracudaConnectionStatus.connectedAck, connectionProps ); this.startSubscriptionListener(connectionProps); try { connectionPendingResolve(this); } catch (e) { if (isWarn(this.ll)) { logWarn( `${this.bcLog( connectionProps )} Unhandled exception in BCjs consumer. BCjs consumer should implement .catch and ensure it doesn't throw exceptions`, e ); } } } else { if (shouldLogErrors(this.ll)) { logError( `${this.bcLog( connectionProps )} ackMessage contained status other than 'Connected' ${ ackMsg.status }`, ackMsg ); } } } catch (error) { const err = new BarracudaError( "Error while parsing ACK message", { connectionProps, originalError: error as Error, } ); if (connectionProps?.onConnectionError) { this.callHandlerSafely( connectionProps, () => connectionProps?.onConnectionError?.call(this, err, this), { error: err }, "onConnectionError" ); } } }); this.ws.once("open", () => { if (isDebug(this.ll)) { logDebug( `${this.bcLog(connectionProps)} WS connected`, connectionProps ); } this._reconnectCounter = 0; this.triggerOnConnectionStateChange( BarracudaConnectionStatus.connected, connectionProps ); }); this.ws.once("close", (code, reason) => { this.triggerOnConnectionStateChange( BarracudaConnectionStatus.disconnected, connectionProps ); const closeData = { code, reason }; if (isInfo(this.ll)) { logInfo(`${this.bcLog(connectionProps)} closed ==>`, closeData); } if ( this._connectedAckAtLeastOnce && toBoolean(connectionProps?.reconnect) ) { const delay: number = 1000 * ++this._reconnectCounter + Math.random() * 1000; this.scheduleReconnect(connectionProps, delay); } else { if (rejectPendingConnection) { try { rejectPendingConnection( new BarracudaError("[closed]", { ...closeData, connectionProps, msg: this.connectionAckMessage, connectedAtLeastOnce: this._connectedAckAtLeastOnce, }) ); } catch (e) { // prevent bubbling up unhandled exceptions in reject clause. Consumer should implement .catch on promise if (isWarn(this.ll)) { logWarn( `Unhandled exception in BCjs consumer. BCjs consumer should implement .catch and ensure it doesn't throw/bubble exceptions`, e ); } } } } }); this.ws.on("error", (error) => { if (this.connectionState === BarracudaConnectionStatus.closing) { if (isWarn(this.ll)) { logWarn( `${this.bcLog( connectionProps )} ignoring processing errors while closing ==>`, { error } ); } return; } this.triggerOnConnectionStateChange( BarracudaConnectionStatus.connectionError, connectionProps ); if (shouldLogErrors(this.ll)) { logError(`${this.bcLog(connectionProps)} error ==>`, { connectionState: this.connectionState, error, }); } if (connectionProps?.onConnectionError) { this.callHandlerSafely( connectionProps, () => connectionProps?.onConnectionError?.call(this, error, this), { error, msg: this.connectionAckMessage, }, "onConnectionError" ); } }); } catch (error) { this.triggerOnConnectionStateChange( BarracudaConnectionStatus.connectionError, connectionProps ); try { if (rejectPendingConnection) { rejectPendingConnection(error); } } catch (e) { if (isWarn(this.ll)) { logWarn( `Unhandled exception in BCjs consumer. BCjs consumer should implement .catch and ensure it doesn't throw exceptions`, e ); } } } } ); } public _emulateServerDisconnect(): void { const message: string = `${this.bcLog( this.lastConnectionProps )} Emulating server disconnect`; const serverCloseMessage: IBarracudaBridgeRequest & { messsage: string } = { messageType: "IBarracudaBridgeRequest", requestId: uuid4(), command: BarracudaCommand.close, messsage: message, }; if (isInfo(this.ll)) { logInfo(message, serverCloseMessage); } this.ws?.send(JSON.stringify(serverCloseMessage)); } public close(): void { const props = this.lastConnectionProps; const reason: string = `[${this.logUid}] [sUid:${this.serverClientId}] connection gracefully closed by API consumer`; const code: number = 1000; switch (this.connectionState) { case BarracudaConnectionStatus.connected: case BarracudaConnectionStatus.connectedAck: case BarracudaConnectionStatus.connecting: case BarracudaConnectionStatus.connectionError: case BarracudaConnectionStatus.reconnecting: if (isInfo(this.ll)) { logInfo(`${this.bcLog(props)} closing connection`, { connectionState: this.connectionState, code, reason, }); } break; case BarracudaConnectionStatus.unknown: case BarracudaConnectionStatus.disconnected: case BarracudaConnectionStatus.closing: if (isWarn(this.ll)) { logWarn( `${this.bcLog(props)} Can not close this client, it is already [${ this.connectionState }]` ); } return; default: break; } this.triggerOnConnectionStateChange( BarracudaConnectionStatus.closing, props ); this.skipReconnecting = true; if (this._staleTriggerTimer) { clearTimeout(this._staleTriggerTimer); this._staleTimeOutMs = 0; } this.ws?.close(code, reason); } public get connectionCounter(): number { return this._connectionCounter; } public bcLog(props: BarracudaConnectionProps): string { if (this.serverClientId) { return `[${props.endpoint}] [${this.logUid}] [sUid:${this.serverClientId}]`; } else { return `[${props.endpoint}] [${this.logUid}]`; } } public disconnect(): Promise<{ code: number; reason: string }> { return new Promise<{ code: number; reason: string }>((resolve, reject) => { this.ws?.once("close", (code, reason) => { const closeConfirmation = { code, reason }; resolve(closeConfirmation); }); this.close(); }); } private _subscriptions: Subscriptions = {}; public get subscriptions(): IBarracudaSubscriptionStatus[] { return Object.values(this._subscriptions).map((s) => BarracudaClient.shareSubscriptionDetails(s) ); } public get previousSessionSubscriptions(): IBarracudaSubscriptionStatus[] { if (this._subscriptionHistory.length === 0) { return []; } const lastSubscriptions: Subscriptions = this._subscriptionHistory[this._subscriptionHistory.length - 1]; return Object.values(lastSubscriptions).map((s) => BarracudaClient.shareSubscriptionDetails(s) ); } public snapshot<T>( instance: string, topic: string, message: (m: IBarracudaBridgeSubscriptionResponse<T>) => void, filter?: string, top: number = DefaultQueryTopValue, project?: string[], sort?: BarracudaQuerySort, heartbeatPeriod: number = 0 ): Promise<string> { return this.subscribeQuery(BarracudaBridgeReadQueryCommands.snapshot, { instance, topic, filter, top, onMessage: (batch, subscriptionId) => { if (message) { message({ subscriptionId, messages: batch, } as IBarracudaBridgeSubscriptionResponse<T>); } }, project, sort, heartbeatPeriod, }).then((sub) => { return sub.subscriptionId; }); } public snapshotAndSubscribe<T>( instance: string, topic: string, message: (m: IBarracudaBridgeSubscriptionResponse<T>) => void, filter?: string, top: number = DefaultQueryTopValue, project?: string[], sort?: BarracudaQuerySort, heartbeatPeriod: number = 0 ): Promise<string> { return this.subscribeQuery( BarracudaBridgeReadQueryCommands.snapshotAndSubscribe, { instance, topic, filter, top, onMessage: (batch, subscriptionId) => { if (message) { message({ subscriptionId, messages: batch, } as IBarracudaBridgeSubscriptionResponse<T>); } }, project, sort, heartbeatPeriod, } ).then((sub) => { return sub.subscriptionId; }); } public snapshotAndDiffSubscribe<T>( instance: string, topic: string, message: (m: IBarracudaBridgeSubscriptionResponse<T>) => void, filter?: string, top: number = DefaultQueryTopValue, project?: string[], sort?: BarracudaQuerySort, heartbeatPeriod: number = 0 ): Promise<string> { return this.subscribeQuery( BarracudaBridgeReadQueryCommands.snapshotAndDiffSubscribe, { instance, topic, filter, top, onMessage: (batch, subscriptionId) => { if (message) { message({ subscriptionId, messages: batch, } as IBarracudaBridgeSubscriptionResponse<T>); } }, project, sort, heartbeatPeriod, } ).then((sub) => { return sub.subscriptionId; }); } public subscribeDiff<T>( instance: string, topic: string, message: (m: IBarracudaBridgeSubscriptionResponse<T>) => void, filter?: string, top: number = DefaultQueryTopValue, project?: string[], sort?: BarracudaQuerySort, heartbeatPeriod: number = 0 ): Promise<string> { return this.subscribeQuery(BarracudaBridgeReadQueryCommands.diffSubscribe, { instance, topic, filter, top, onMessage: (batch, subscriptionId) => { if (message) { message({ subscriptionId, messages: batch, } as IBarracudaBridgeSubscriptionResponse<T>); } }, project, sort, heartbeatPeriod, }).then((sub) => { return sub.subscriptionId; }); } public subscribe<T>( instance: string, topic: string, message: (m: IBarracudaBridgeSubscriptionResponse<T>) => void, filter?: string, top: number = DefaultQueryTopValue, project?: string[], sort?: BarracudaQuerySort, heartbeatPeriod: number = 0 ): Promise<string> { return this.subscribeQuery(BarracudaBridgeReadQueryCommands.subscribe, { instance, topic, filter, top, onMessage: (batch, subscriptionId) => { if (message) { message({ subscriptionId, messages: batch, } as IBarracudaBridgeSubscriptionResponse<T>); } }, project, sort, heartbeatPeriod, }).then((sub) => { return sub.subscriptionId; }); } public async unsubscribe(subscriptionId: string): Promise<void> { const props = this.lastConnectionProps; const subDetails = this._subscriptions[subscriptionId]; if (!subDetails) { throw new Error( `Unable to find internal details for [subId: ${subscriptionId}]. Please confirm subscriptionId veracity` ); } if ( subDetails?.unsubscribed || subDetails?.lastServerStatus === BarracudaBridgeSubscriptionStatus.unsubscribed ) { logWarn( `Subscription ${subscriptionId} is already unsubscribed`, BarracudaClient.shareSubscriptionDetails(subDetails) ); return; } if (isInfo(this.ll)) { logInfo(`Unsubscribing ${subscriptionId}`); } subDetails.onMessage = undefined; this._subscriptions[subscriptionId].pendingUnsubscribe = true; const unsubscribeMsg: IBarracudaBridgeSubscriptionRequest = { requestId: uuid4(), messageType: "IBarracudaBridgeSubscriptionRequest", command: BarracudaBridgeQueryCommands.unsubscribe, heartbeatPeriod: 0, subscriptionId, }; const response = await this.sendRequest(unsubscribeMsg); if (props?.onSubscriptionUpdate) { subDetails.lastServerStatus = BarracudaBridgeSubscriptionStatus.unsubscribeRequestAcknowledged; subDetails.lastServerStatusDt = new Date(); this.safeTriggerOnSubscriptionChanged( BarracudaClient.shareSubscriptionDetails(subDetails), props ); } if (response.success) { subDetails.unsubscribed = true; } } public async aggregate<T>( topic: any, pipeline: any[], instance?: any, maxTimeMS: number = 5 * 1000, batchSize: number = 100 ): Promise<any[] | undefined> { return new Promise(async (resolve, reject) => { let results: T[] = []; let subId: string; const onSubscriptionResponse: ( msg: IBarracudaBridgeSubscriptionResponse<any> ) => void = (subResponse: IBarracudaBridgeSubscriptionResponse<T>) => { subResponse.messages.forEach((m) => { switch (m.command) { case BarracudaBridgeResponseCommandTypes.sow: results.push(m.data); break; case BarracudaBridgeResponseCommandTypes.publish: results.push(m.data); break; case BarracudaBridgeResponseCommandTypes.groupEnd: resolve(results); break; } }); }; if (isDebug(this.ll)) { logDebug(`${this.bcLog(this.lastConnectionProps)} aggregate`, { topic, instance, pipeline, maxTimeMS, batchSize, }); } const query: Query<T> = { instance, topic, batchSize, pipeline, batchPeriod: maxTimeMS, onMessage: onSubscriptionResponse, }; try { await this.requestQueryCommand<T>( BarracudaBridgeReadQueryCommands.aggregate, query, undefined ); } catch (e) { reject(e); } }); } public async subscribeQuery<T>( commandType: BarracudaBridgeReadQueryCommands, queryDetails: BarracudaQuery<T> ): Promise<QuerySubscription> { let subId: string; const onSubscriptionResponse: ( msg: IBarracudaBridgeSubscriptionResponse<any> ) => void = (subResponse: IBarracudaBridgeSubscriptionResponse<T>) => { queryDetails.onMessage?.call(this, subResponse.messages, subId); }; if (isDebug(this.ll)) { logDebug( `${this.bcLog(this.lastConnectionProps)} subscribeQuery`, queryDetails ); } const query: Query<T> = { ...queryDetails, project: queryDetails.project, orderBy: queryDetails.sort, top: queryDetails?.top ?? DefaultQueryTopValue, onMessage: onSubscriptionResponse, }; subId = await this.requestQueryCommand<T>(commandType, query, queryDetails); return { subscriptionId: subId, unsubscribe: () => this.unsubscribe(subId), }; } public getTopicInfo( topic: string, infoType: BarracudaTopicInfoType, instance?: string ): Promise<{ result: any | any[] } | undefined> { if (isVerbose(this.ll)) { logVerbose("getTopicInfo", { topic, infoType, instance }, this.instance); } const database: string | undefined = instance ?? this.instance; if (!database) { throw new Error( "Instance is required. please provide either a default instance in the constructor, or pass an override instance in the parameter" ); } const requestBase: IBarracudaBridgeRequestBase = BarracudaClient.createRequest("IBarracudaBridgeCommandRequest"); let command: any; let getResult: (r: any) => { result: any | any[] }; switch (infoType) { case BarracudaTopicInfoType.barracuda_keys: command = { listIndexes: topic, }; getResult = (response: any) => { const indices = response?.cursor?.firstBatch as { key: string[]; name: string; }[]; return { result: indices?.find( (i) => "_" + BarracudaTopicInfoType.barracuda_keys === i?.name )?.key, }; }; break; case BarracudaTopicInfoType.documentsCount: command = { count: topic, }; getResult = (response: any) => ({ result: response?.n }); break; case BarracudaTopicInfoType.size: command = { dataSize: `${instance}.${topic}`, }; getResult = (response: any) => ({ result: response?.size }); break; default: throw new BarracudaError(`Unidentified infoType: ${infoType}`, { topic, instance, }); } const req: IBarracudaBridgeCommandRequest = { ...requestBase, database, command: { ...command, }, }; if (isInfo(this.ll)) { logInfo( `Requesting TopicInfo ${topic}`, isDebug(this.ll) ? req : undefined ); } return this.sendRequest<IBarracudaBridgeCommandRequestResponse>(req).then( (r: IBarracudaBridgeCommandRequestResponse) => { if (this.throwIfMissingField(r, "response")) { return; } return getResult(r.response); } ); } public setOnStaleSubscriptionAlert( onStale: SubscriptionUpdateHandler, seconds: number = 60 ): void { this._staleTimeOutMs = seconds * 1000; this._onStale = onStale; if (this._staleTimeOutMs && this._onStale) { if (!this._staleTriggerTimer) { clearTimeout(this._staleTriggerTimer); } setTimeout(() => this.detectStaleSubscriptions(), this.staleTimeOutMs); } } private _endpoint?: string; public get endpoint(): string | undefined { return this._endpoint; } public set endpoint(value: string | undefined) { this._endpoint = value; } public get instance(): string | undefined { return this._defaultInstance; } public set instance(value: string | undefined) { this._defaultInstance = value; } private _staleTimeOutMs: number | undefined; public get staleTimeOutMs(): number { return this._staleTimeOutMs ?? 0; } private get ll(): loglevels { return this._loglevel; } private static getConnectionProps( defaultProps?: BarracudaDefaultProps, overrideProps?: BarracudaDefaultProps ): BarracudaDefaultProps { return { endpoint: overrideProps?.endpoint ?? defaultProps?.endpoint, onConnectionError: overrideProps?.onConnectionError ?? defaultProps?.onConnectionError, onConnectionStateChange: overrideProps?.onConnectionStateChange ?? defaultProps?.onConnectionStateChange, onError: overrideProps?.onError ?? defaultProps?.onError, reconnect: overrideProps?.reconnect ?? defaultProps?.reconnect, gaToken: overrideProps?.gaToken ?? defaultProps?.gaToken, instance: overrideProps?.instance ?? defaultProps?.instance, onSubscriptionUpdate: overrideProps?.onSubscriptionUpdate ?? defaultProps?.onSubscriptionUpdate, loglevel: overrideProps?.loglevel ?? defaultProps?.loglevel, publishRestEndPoint: overrideProps?.publishRestEndPoint ?? defaultProps?.publishRestEndPoint, serverDebug: overrideProps?.serverDebug ?? defaultProps?.serverDebug, appName: overrideProps?.appName ?? defaultProps?.appName, appVersion: overrideProps?.appVersion ?? defaultProps?.appVersion, }; } private static createRequest( messageType: BarracudaBridgeMessageType ): IBarracudaBridgeRequestBase { return this.createBBMsgType<IBarracudaBridgeRequestBase>(messageType, { requestId: uuid4(), }) as IBarracudaBridgeRequestBase; } private static createBBMsgType<T extends IBarracudaBridgeMessage>( messageType: BarracudaBridgeMessageType, bMsg: Omit<T, keyof IBarracudaBridgeMessage> ): T { return { messageType, ...bMsg, } as T; } private static createRequestSubscription<T>( commandType: | BarracudaBridgeReadQueryCommands | BarracudaBridgeAggregateCommands, instance: string, topic: string, queryDetails: Query<T> ): IBarracudaBridgeSubscriptionRequest { let filter: string | undefined; if (typeof queryDetails.filter === "object") { filter = JSON.stringify(queryDetails.filter); } else { filter = queryDetails.filter; } // let pipeline: string | undefined; // if (Array.isArray(queryDetails.pipeline)) { // pipeline = JSON.stringify(queryDetails.pipeline); // } else { // pipeline = queryDetails.pipeline; // } return { ...BarracudaClient.createRequest("IBarracudaBridgeSubscriptionRequest"), command: commandType, database: instance, collection: topic, filter, pipeline: queryDetails.pipeline, projection: queryDetails.project, orderBy: queryDetails.orderBy, batchSize: queryDetails.batchSize, batchPeriod: queryDetails.batchPeriod, heartbeatPeriod: queryDetails.heartbeatPeriod ?? 0, top: queryDetails.top, }; } private static shareSubscriptionDetails<T>( subDetails: IBarracudaSubscription<T> ): IBarracudaSubscriptionStatus { const copy: IBarracudaSubscription<T> = { ...subDetails }; // @ts-ignore delete copy.inboundQueue; delete copy.onMessage; return copy as IBarracudaSubscriptionStatus; } private static hasError(msg: IBarracudaBridgeRequestResponse): boolean { return ( msg?.success === false || (!!msg?.error && msg?.error?.trim() !== "") ); } public async send( topic: string, msg: any, type: BarracudaPublisherCommandType, props?: BarracudaPublishingProps ): Promise<{ reply?: string; message?: string } | undefined> { return this.sendMessage(topic, msg, type, props); } public sendWithKey(topic: string, key: string, msg: any): Promise<void> { throw new Error( "Not implemented yet. Contact Francesco Laurita to enable sending messages with custom keys" ); } private safeTriggerOnSubscriptionChanged( subscriptionStatus: IBarracudaSubscriptionStatus, props: BarracudaConnectionProps ): void { if (props.onSubscriptionUpdate) { this.callHandlerSafely( props, () => props.onSubscriptionUpdate?.call(this, subscriptionStatus, this), { subscriptionStatus }, "onSubscriptionUpdate" ); } } private safeTriggerOnStale( subscriptionStatus: IBarracudaSubscriptionStatus ): void { this.callHandlerSafely( this.lastConnectionProps, () => this._onStale?.call(this, subscriptionStatus, this), { subscriptionStatus }, "onStale" ); this._staleNotice[subscriptionStatus.subscriptionId] = subscriptionStatus.lastServerStatusDt; } private async sendMessage( topic: string, msg: object | string, type: BarracudaPublisherCommandType, props?: BarracudaPublishingProps ): Promise< | { "x-ray": string; reply?: string; message?: string; url: string } | undefined > { let bMsg: IBarracudaPublisherMessage; const sendProps: BarracudaPublishingProps = { onError: props?.onError ?? this.onError, publishRestEndPoint: props?.publishRestEndPoint ?? this.restPublishEndPoint, gaToken: props?.gaToken ?? this?.gaToken, }; if (!topic) { const err = "Can not send a message without a topic"; const errObj = new BarracudaError(err, { msg }); this.handleSendError(errObj, sendProps); return undefined; } if (!msg) { const err = "Can not send an empty message"; const errObj = new BarracudaError(err, { msg }); this.handleSendError(errObj, sendProps); return undefined; } if (typeof msg === "string") { try { bMsg = JSON.parse(msg); } catch (e) { const errObj = new BarracudaError( "Can not send an un-parsable JSON message. JSON messages needs to be well formed.", { originalError: e as Error, msg, } ); this.handleSendError(errObj, sendProps); return undefined; } } else { bMsg = msg as any; } if (bMsg["_barracuda_meta"]) { bMsg["_barracuda_meta"] = { ...bMsg["_barracuda_meta"], bPublisherTst: Date.now(), } as IBarracudaPublisherMeta; } else { bMsg._barracuda_meta = { _payload_ver: 1, _ver: 4, bPublisherTst: Date.now(), bPublisherCommand: type, expiration: 0, sequenceId: 0, topic, bPublisherClient: { lang: BarracudaClientBuildDetails.name, version: BarracudaClientBuildDetails.version, }, bPublisherName: this.uid, }; } const requestHeaders: Headers = new Headers({ "Content-Type": "application/json", }); requestHeaders.append("gaToken", sendProps.gaToken as string); const init: RequestInit = { body: JSON.stringify(bMsg, null, "").trim(), method: "POST", headers: requestHeaders, }; const info: RequestInfo = `${sendProps?.publishRestEndPoint}/${type}/${topic}`; if (isInfo(this.ll)) { logInfo(`Sending Request to ${info}`, init); } let r: Response; try { r = await fetch(info, init); } catch (error) { const errObj = new BarracudaError( `Error while sending msg through REST to ${info}`, { httpRequest: { info, init }, originalError: error as Error, } ); this.handleSendError(errObj, sendProps); return undefined; } const responseHeader: Headers = r.headers; const xRay: string = JSON.stringify(responseHeader?.get("x-ray")); let responseBody: { reply?: string; message?: string; }; try { responseBody = await r.json(); } catch (error) { let body: string = ""; try { body = await r.text(); } catch (e) { logError(`Couldn't get text body from server response`, { httpRequest: { info, init, }, }); } const errObj = new BarracudaError( "Error while json parsing server response.", { httpRequest: { info, init, }, httpResponse: { "x-ray": xRay, body, headers: responseHeader, }, originalError: error as Error, } ); this.handleSendError(errObj, sendProps); throw errObj; } if (responseBody.reply && responseBody.reply.indexOf("enqueued to ") >= 0) { const piranhaResponse: { "x-ray": string; reply?: string; message?: string; url: string; } = { ...responseBody, "x-ray": xRay, url: sendProps?.publishRestEndPoint as string, }; if (isInfo(this.ll)) { logInfo("REST Publish Success ==>", piranhaResponse); } if (isDebug(this.ll)) { logDebug("REST Publish Success ==>", { ...piranhaResponse, headers: responseHeader, }); } return piranhaResponse; } else { const errObj = new BarracudaError( `Received failure response from server ==> ${responseBody}`, { httpRequest: { info, init, }, httpResponse: { "x-ray": xRay, body: responseBody, header: responseHeader, }, url: sendProps?.publishRestEndPoint as string, } ); this.handleSendError(errObj, sendProps); throw errObj; } } private handleSendError( err: Error, sendProps: BarracudaPublishingProps, logContext: { error: Error } = { error: err } ): void { if (shouldLogErrors(this.ll)) { logError(err); } if (sendProps?.onError) { this.callHandlerSafely( this.lastConnectionProps, () => sendProps?.onError?.call(this, err, this), logContext, "onError" ); } else { throw err; } } private setProps(props?: BarracudaDefaultProps): void { if (props?.loglevel) { this.loglevel = props.loglevel; } if (isDebug(this.ll)) { logDebug("BarracudaClient setting props", props); } if (props?.endpoint) { this._endpoint = props.endpoint; } if (props?.publishRestEndPoint) { this._defaultRestPublishEndPoint = props.publishRestEndPoint; } if (props?.serverDebug) { this._serverDebug = props.serverDebug; } if (props?.reconnect !== undefined) { this.reconnect = props.reconnect; } if (props?.onError) { this.onError = props.onError; } if (props?.onConnectionError) { this.onConnectionError = props.onConnectionError; } if (props?.onConnectionStateChange) { this.onConnectionStateChange = props.onConnectionStateChange; } if (props?.onSubscriptionUpdate) { this.onSubscriptionUpdate = props.onSubscriptionUpdate; } if (props?.instance) { this.instance = props?.instance; } if (props?.gaToken) { this._gaToken = props?.gaToken; } if (props?.appName) { this._appName = props.appName; } if (props?.appVersion) { this._appVersion = props.appVersion; } } private triggerOnConnectionStateChange( conState: BarracudaConnectionStatus, connectionProps: BarracudaConnectionProps ): void { this.connectionState = conState; const connectionStatusHandler = connectionProps?.onConnectionStateChange; switch (conState) { case BarracudaConnectionStatus.connecting: case BarracudaConnectionStatus.connected: case BarracudaConnectionStatus.reconnecting: case BarracudaConnectionStatus.closing: if (isDebug(this.ll)) { logDebug( `${this.bcLog(connectionProps)} connection state: ${conState}` ); } break; case BarracudaConnectionStatus.disconnected: case BarracudaConnectionStatus.connectedAck: if (isInfo(this.ll)) { logInfo( `${this.bcLog(connectionProps)} connection state: ${conState}` ); } break; case BarracudaConnectionStatus.connectionError: if (shouldLogErrors(this.ll)) { logError( `${this.bcLog(connectionProps)} connection state: ${conState}` ); } break; } if (connectionStatusHandler) { this.callHandlerSafely( connectionProps, () => connectionStatusHandler?.call(this, conState, this), { connectionState: conState }, "onConnectionStateChange" ); } } private scheduleReconnect( props?: BarracudaConnectionProps, delay: number = 1000 ): void { if (!this.skipReconnecting) { this.triggerOnConnectionStateChange( BarracudaConnectionStatus.reconnecting, this.lastConnectionProps ); if (isInfo(this.ll)) { const logArgs: | { lastConnectionProps: BarracudaConnectionProps; delay: number; newConnectionProps: BarracudaConnectionProps | undefined; } | undefined = isDebug(this.ll) ? { lastConnectionProps: this.lastConnectionProps, newConnectionProps: props, delay, } : undefined; logInfo(`Scheduling reconnection in ${delay?.toFixed(2)}ms`, logArgs); } setTimeout(() => this.connect(props), delay); } } private callHandlerSafely< T extends BarracudaConnectionHandlers, EventHandler extends keyof T >( logCProps: BarracudaConnectionProps, handler: () => void, logContext?: | Partial< | IBarracudaSubscriptionStatus | { statusDt: Date } | { msg?: string | IBarracudaBridgeMessage; error?: Error | BarracudaError; } | { subscriptionStatus?: IBarracudaSubscriptionStatus } | { connectionState?: BarracudaConnectionStatus } > | { fieldName: any }, handlerName?: "onStale" | EventHandler ): void { if (isDebug(this.ll)) { logDebug(`${this.bcLog(logCProps)} calling ${handlerName}`, { ...logContext, connectionProps: logCProps, }); } try { handler(); } catch (error) { const err = new BarracudaError( `Error while executing handler [${handlerName}]`, { originalError: error as Error, handlerName: handlerName as string, handlerContext: logContext, connectionProps: logCProps, } ); if (shouldLogErrors(this.ll)) { logError(err, { connectionProps: logCProps }); } } } private startSubscriptionListener(props: BarracudaConnectionProps): void { if (this.connectionState !== BarracudaConnectionStatus.connectedAck) { return; } this.ws?.on("message", (msg: string) => { if (this.connectionState !== BarracudaConnectionStatus.connectedAck) { return; } if (isVerbose(this.ll)) { logVerbose(`${this.bcLog(props)} onMessage ==> `, msg); } const receivedDt = new Date(); let bMsg: IBarracudaBridgeMessage; try { bMsg = JSON.parse(msg); } catch (error: any) { const bError = new BarracudaError( `${this.bcLog(props)} Error while parsing received JSON message`, { msg, originalError: error as Error, } ); if (shouldLogErrors(this.ll)) { logError(bError); } if (props?.onError) { this.callHandlerSafely( props, () => props?.onError?.call(this, bError, this), { error }, "onError" ); } return; } const bMsgType: IBarracudaBridgeMessage = bMsg; switch (bMsgType.messageType) { case "IBarracudaBridgeRequestResponse": case "IBarracudaBridgeCommandRequestResponse": this.handleRequestResponse( bMsg as IBarracudaBridgeRequestResponse, receivedDt, props ); break; case "IBarracudaBridgeConnectionStatus": // TODO break; case "IBarracudaBridgeSubscriptionStatus": this.handleSubscriptionStatus( bMsg as IBarracudaBridgeSubscriptionStatus, receivedDt, props ); break; case "IBarracudaBridgeSubscriptionResponse": this.handleSubscriptionResponse( bMsg as IBarracudaBridgeSubscriptionResponse<any>, receivedDt,