Skip to content

Commit

Permalink
add retry limnit to websocketm patch type issues
Browse files Browse the repository at this point in the history
  • Loading branch information
heswell committed Oct 10, 2023
1 parent f6374c1 commit 3f77a61
Show file tree
Hide file tree
Showing 18 changed files with 237 additions and 42 deletions.
18 changes: 18 additions & 0 deletions vuu-ui/packages/vuu-data/src/connection-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ const pendingRequests = new Map();

type WorkerOptions = {
protocol: WebSocketProtocol;
retryLimitDisconnect?: number;
retryLimitStartup?: number;
url: string;
token?: string;
username: string | undefined;
Expand All @@ -89,6 +91,8 @@ type WorkerOptions = {
const getWorker = async ({
handleConnectionStatusChange,
protocol,
retryLimitDisconnect,
retryLimitStartup,
token = "",
username,
url,
Expand Down Expand Up @@ -120,6 +124,8 @@ const getWorker = async ({
window.clearTimeout(timer);
worker.postMessage({
protocol,
retryLimitDisconnect,
retryLimitStartup,
token,
type: "connect",
url,
Expand Down Expand Up @@ -274,6 +280,10 @@ export type ConnectOptions = {
authToken?: string;
username?: string;
protocol?: WebSocketProtocol;
/** Max number of reconnect attempts in the event of unsuccessful websocket connection at startup */
retryLimitStartup?: number;
/** Max number of reconnect attempts in the event of a disconnected websocket connection */
retryLimitDisconnect?: number;
};

class _ConnectionManager extends EventEmitter<ConnectionEvents> {
Expand All @@ -284,6 +294,8 @@ class _ConnectionManager extends EventEmitter<ConnectionEvents> {
authToken,
username,
protocol,
retryLimitDisconnect,
retryLimitStartup,
}: ConnectOptions): Promise<ServerAPI> {
// By passing handleMessageFromWorker here, we can get connection status
//messages while we wait for worker to resolve.
Expand All @@ -292,6 +304,8 @@ class _ConnectionManager extends EventEmitter<ConnectionEvents> {
url,
token: authToken,
username,
retryLimitDisconnect,
retryLimitStartup,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
handleConnectionStatusChange: handleMessageFromWorker,
Expand Down Expand Up @@ -321,13 +335,17 @@ export const connectToServer = async ({
protocol = undefined,
authToken,
username,
retryLimitDisconnect,
retryLimitStartup,
}: ConnectOptions) => {
try {
const serverAPI = await ConnectionManager.connect({
protocol,
url,
authToken,
username,
retryLimitDisconnect,
retryLimitStartup,
});
resolveServer(serverAPI);
} catch (err: unknown) {
Expand Down
2 changes: 2 additions & 0 deletions vuu-ui/packages/vuu-data/src/vuuUIMessageTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ export interface VuuUIMessageOutConnect {
token: string;
url: string;
username?: string;
retryLimitDisconnect?: number;
retryLimitStartup?: number;
}

export interface VuuUIMessageOutSubscribe extends ServerProxySubscribeMessage {
Expand Down
59 changes: 44 additions & 15 deletions vuu-ui/packages/vuu-data/src/websocket-connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,43 @@ const WS = "ws"; // to stop semGrep complaining
const isWebsocketUrl = (url: string) =>
url.startsWith(WS + "://") || url.startsWith(WS + "s://");

const connectionAttempts: {
[key: string]: { attemptsRemaining: number; status: ConnectionStatus };
} = {};
type ConnectionTracking = {
[key: string]: {
connect: {
allowed: number;
remaining: number;
};
reconnect: {
allowed: number;
remaining: number;
};
status: ConnectionStatus;
};
};

const connectionAttemptStatus: ConnectionTracking = {};

const setWebsocket = Symbol("setWebsocket");
const connectionCallback = Symbol("connectionCallback");

export async function connect(
connectionString: string,
protocol: WebSocketProtocol,
callback: ConnectionCallback
callback: ConnectionCallback,
retryLimitDisconnect = 10,
retryLimitStartup = 5
): Promise<Connection> {
connectionAttemptStatus[connectionString] = {
status: "connecting",
connect: {
allowed: retryLimitStartup,
remaining: retryLimitStartup,
},
reconnect: {
allowed: retryLimitDisconnect,
remaining: retryLimitDisconnect,
},
};
return makeConnection(connectionString, protocol, callback);
}

Expand All @@ -58,12 +83,14 @@ async function makeConnection(
callback: ConnectionCallback,
connection?: WebsocketConnection
): Promise<Connection> {
const connectionStatus =
connectionAttempts[url] ||
(connectionAttempts[url] = {
attemptsRemaining: 5,
status: "disconnected",
});
const {
status: currentStatus,
connect: connectStatus,
reconnect: reconnectStatus,
} = connectionAttemptStatus[url];

const trackedStatus =
currentStatus === "connecting" ? connectStatus : reconnectStatus;

try {
callback({ type: "connection-status", status: "connecting" });
Expand All @@ -89,18 +116,20 @@ async function makeConnection(
callback({ type: "connection-status", status });
websocketConnection.status = status;

// reset the retry attempts for subsequent disconnections
trackedStatus.remaining = trackedStatus.allowed;

return websocketConnection as Connection;
} catch (evt) {
console.log({ evt });
const retry = --connectionStatus.attemptsRemaining > 0;
} catch (err) {
const retry = --trackedStatus.remaining > 0;
callback({
type: "connection-status",
status: "disconnected",
reason: "failed to connect",
retry,
});
if (retry) {
return makeConnectionIn(url, protocol, callback, connection, 10000);
return makeConnectionIn(url, protocol, callback, connection, 2000);
} else {
throw Error("Failed to establish connection");
}
Expand Down Expand Up @@ -204,7 +233,7 @@ export class WebsocketConnection implements Connection<ClientToServerMessage> {
messagesLength: this.messagesCount,
});
this.messagesCount = 0;
}, 1000);
}, 2000);

ws.onerror = () => {
error(`⚡ connection error`);
Expand Down
12 changes: 9 additions & 3 deletions vuu-ui/packages/vuu-data/src/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ async function connectToServer(
protocol: WebSocketProtocol,
token: string,
username: string | undefined,
onConnectionStatusChange: (msg: ConnectionStatusMessage) => void
onConnectionStatusChange: (msg: ConnectionStatusMessage) => void,
retryLimitDisconnect?: number,
retryLimitStartup?: number
) {
const connection = await connectWebsocket(
url,
Expand All @@ -44,7 +46,9 @@ async function connectToServer(
} else {
server.handleMessageFromServer(msg);
}
}
},
retryLimitDisconnect,
retryLimitStartup
);

server = new ServerProxy(connection, (msg) => sendMessageToClient(msg));
Expand Down Expand Up @@ -72,7 +76,9 @@ const handleMessageFromClient = async ({
message.protocol,
message.token,
message.username,
postMessage
postMessage,
message.retryLimitDisconnect,
message.retryLimitStartup
);
postMessage({ type: "connected" });
break;
Expand Down
109 changes: 109 additions & 0 deletions vuu-ui/packages/vuu-data/test/websocket-connection.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import "./global-mocks";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import {
connect as connectWebsocket,
ConnectionMessage,
} from "../src/websocket-connection";

describe("websocket-connection", () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});

it("tries to connect by default a maximum of 5 times before throwing Exception", async () => {
const statusMessages: ConnectionMessage[] = [];
const callback = async (message: ConnectionMessage) => {
statusMessages.push(message);
await vi.advanceTimersByTimeAsync(2000);
};

try {
await connectWebsocket("tst/url", "", callback);
} catch (e) {
expect(e.message).toEqual("Failed to establish connection");
}

expect(statusMessages.length).toEqual(10);
expect(statusMessages).toEqual([
{ type: "connection-status", status: "connecting" },
{
type: "connection-status",
status: "disconnected",
reason: "failed to connect",
retry: true,
},
{ type: "connection-status", status: "connecting" },
{
type: "connection-status",
status: "disconnected",
reason: "failed to connect",
retry: true,
},
{ type: "connection-status", status: "connecting" },
{
type: "connection-status",
status: "disconnected",
reason: "failed to connect",
retry: true,
},
{ type: "connection-status", status: "connecting" },
{
type: "connection-status",
status: "disconnected",
reason: "failed to connect",
retry: true,
},
{ type: "connection-status", status: "connecting" },
{
type: "connection-status",
status: "disconnected",
reason: "failed to connect",
retry: false,
},
]);
});

it("fires connection-status messages when connecting/connected", async () => {
class MockWebSocket {
private openHandler: any;
private errorHandler: any;
constructor() {
setTimeout(() => {
this?.openHandler();
}, 0);
}
set onopen(callback) {
this.openHandler = callback;
}
set onerror(callback) {
this.errorHandler = callback;
}
}
vi.stubGlobal("WebSocket", MockWebSocket);

const statusMessages: ConnectionMessage[] = [];
const callback = async (message: ConnectionMessage) => {
statusMessages.push(message);
await vi.advanceTimersByTimeAsync(10);
};

try {
await connectWebsocket("tst/url", "", callback);
} catch (e) {
expect(e.message).toEqual("Failed to establish connection");
}

expect(statusMessages.length).toEqual(2);
expect(statusMessages).toEqual([
{ type: "connection-status", status: "connecting" },
{
type: "connection-status",
status: "connection-open-awaiting-session",
},
]);
});
});
2 changes: 1 addition & 1 deletion vuu-ui/packages/vuu-ui-controls/src/combo-box/ComboBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ export const ComboBox = forwardRef(function Combobox<
listHandlers={listHandlers}
onSelectionChange={onSelectionChange}
ref={listRef}
selected={collectionItemsToItem(selected)}
selected={collectionItemsToItem(selected as any)}
selectionStrategy={selectionStrategy}
/>
</DropdownBase>
Expand Down
4 changes: 4 additions & 0 deletions vuu-ui/packages/vuu-ui-controls/src/combo-box/useCombobox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,11 @@ export const useCombobox = <
if (selectedCollectionItem) {
if (Array.isArray(selectedCollectionItem)) {
// TODO multi select
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
} else if (selectedCollectionItem !== selected) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
setSelectedRef.current?.(selectedCollectionItem);
onSelectionChange?.(
evt,
Expand Down
Loading

0 comments on commit 3f77a61

Please sign in to comment.