barracuda-client-api
Version:
API Client to connect to Barracuda Enterprise Service Bus
1,714 lines (1,576 loc) • 71.2 kB
text/typescript
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,