Skip to content

Commit

Permalink
fix(app-websockets): reconnect on window focus [skip ci] (#4142)
Browse files Browse the repository at this point in the history
  • Loading branch information
brunozoric authored May 17, 2024
1 parent 9e0cd9d commit 49e5151
Show file tree
Hide file tree
Showing 9 changed files with 190 additions and 145 deletions.
64 changes: 50 additions & 14 deletions packages/app-websockets/src/WebsocketsContextProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { useTenancy } from "@webiny/app-tenancy";
import { useI18N } from "@webiny/app-i18n";
import { getToken } from "./utils/getToken";
import { getUrl } from "./utils/getUrl";
import { IncomingGenericData, IWebsocketsContext, IWebsocketsContextSendCallable } from "~/types";
import {
IncomingGenericData,
IWebsocketsContext,
IWebsocketsContextSendCallable,
WebsocketsCloseCode
} from "~/types";
import {
createWebsocketsAction,
createWebsocketsActions,
Expand All @@ -12,6 +16,7 @@ import {
createWebsocketsSubscriptionManager
} from "./domain";
import { IGenericData, IWebsocketsManager } from "./domain/types";
import { getUrl } from "./utils/getUrl";

export interface IWebsocketsContextProviderProps {
loader?: React.ReactElement;
Expand Down Expand Up @@ -41,21 +46,52 @@ export const WebsocketsContextProvider = (props: IWebsocketsContextProviderProps

let currentIteration = 0;
manager.onClose(event => {
if (currentIteration > 5 || event.code !== 1001) {
if (currentIteration > 5 || event.code !== WebsocketsCloseCode.GOING_AWAY) {
return;
}
currentIteration++;
setTimeout(() => {
if (!socketsRef.current) {
return;
} else if (socketsRef.current.isClosed()) {
console.log("Running auto-reconnect.");

socketsRef.current.connect();
}
console.log("Running auto-reconnect.");
socketsRef.current.connect();
}, 1000);
});

return manager;
}, []);
/**
* We need this useEffect to close the websocket connection and remove window focus event in case component is unmounted.
* This will, probably, happen only during the development phase.
*
* If we did not disconnect on component unmount, we would have a memory leak - multiple connections would be opened.
*/
useEffect(() => {
/**
* We want to add a window event listener which will check if the connection is closed, and if its - it will connect again.
*/
const fn = () => {
if (!socketsRef.current) {
return;
} else if (socketsRef.current.isClosed()) {
console.log("Running auto-reconnect on focus.");
socketsRef.current.connect();
}
};
window.addEventListener("focus", fn);

return () => {
window.removeEventListener("focus", fn);
// if (!socketsRef.current) {
// return;
// }

// socketsRef.current.close(WebsocketsCloseCode.NORMAL, "Component unmounted.");
};
}, []);

useEffect(() => {
(async () => {
Expand All @@ -65,9 +101,12 @@ export const WebsocketsContextProvider = (props: IWebsocketsContextProviderProps
} else if (current.tenant === tenant && current.locale === locale) {
return;
} else if (socketsRef.current) {
socketsRef.current.close();
await socketsRef.current.close(
WebsocketsCloseCode.NORMAL,
"Changing tenant/locale."
);
}
const url = getUrl({ tenant, locale, token });
const url = getUrl();

if (!url) {
console.error("Not possible to connect to the websocket without a valid URL.", {
Expand All @@ -82,10 +121,13 @@ export const WebsocketsContextProvider = (props: IWebsocketsContextProviderProps
createWebsocketsConnection({
subscriptionManager,
url,
tenant,
locale,
getToken,
protocol: ["webiny-ws-v1"]
})
);
socketsRef.current.connect();
await socketsRef.current.connect();

setCurrent({
tenant,
Expand Down Expand Up @@ -142,12 +184,6 @@ export const WebsocketsContextProvider = (props: IWebsocketsContextProviderProps
return props.loader || null;
}

// TODO remove when finished with development
(window as any).webinySockets = socketsRef.current;
(window as any).send = send;
(window as any).createAction = createAction;
(window as any).onMessage = onMessage;

const value: IWebsocketsContext = {
send,
createAction,
Expand Down
53 changes: 0 additions & 53 deletions packages/app-websockets/src/domain/BlackHoleWebsocketsManager.ts

This file was deleted.

158 changes: 116 additions & 42 deletions packages/app-websockets/src/domain/WebsocketsConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,88 +9,162 @@ import {
WebsocketsReadyState
} from "./types";

interface ICreateUrlResult {
token: string;
url: string;
}

const defaultFactory: IWebsocketsConnectionFactory = (url, protocol) => {
return new WebSocket(url, protocol);
};

interface IConnection {
ws: WebSocket | null;
}

/**
* We need to attach the websockets cache to window object, or it will be reset on every hot reload.
*/
declare global {
interface Window {
WebinyWebsocketsConnectionCache: IConnection;
}
}

if (!window.WebinyWebsocketsConnectionCache) {
window.WebinyWebsocketsConnectionCache = {
ws: null
};
}

const connectionCache = window.WebinyWebsocketsConnectionCache;

export interface IWebsocketsConnectionParams {
url: string;
tenant: string;
locale: string;
getToken(): Promise<string | null>;
subscriptionManager: IWebsocketsSubscriptionManager;
protocol?: IWebsocketsConnectProtocol;
factory?: IWebsocketsConnectionFactory;
}

export class WebsocketsConnection implements IWebsocketsConnection {
private connection: WebSocket | null = null;
private url: string;
private protocol: IWebsocketsConnectProtocol;
private readonly url: string;
private readonly getToken: () => Promise<string | null>;
private tenant: string;
private locale: string;
private readonly protocol: IWebsocketsConnectProtocol;
public readonly subscriptionManager: IWebsocketsSubscriptionManager;
private readonly factory: IWebsocketsConnectionFactory;

public constructor(params: IWebsocketsConnectionParams) {
this.url = params.url;
this.tenant = params.tenant;
this.locale = params.locale;
this.getToken = params.getToken;
this.protocol = params.protocol;
this.subscriptionManager = params.subscriptionManager;
this.factory = params.factory || defaultFactory;
}

public init(): void {
this.connect(this.url, this.protocol);
public setTenant(tenant: string): void {
this.tenant = tenant;
}

public connect(url: string, protocol?: IWebsocketsConnectProtocol): void {
if (this.connection && this.connection.readyState !== WebsocketsReadyState.CLOSED) {
public setLocale(locale: string): void {
this.locale = locale;
}

public async connect(): Promise<void> {
await this.getConnection();
}

public async close(code: WebsocketsCloseCode, reason: string): Promise<boolean> {
if (
!connectionCache.ws ||
connectionCache.ws.readyState === WebsocketsReadyState.CLOSED ||
connectionCache.ws.readyState === WebsocketsReadyState.CLOSING
) {
connectionCache.ws = undefined as unknown as null;

return true;
}
connectionCache.ws.close(code, reason);

connectionCache.ws = undefined as unknown as null;
return true;
}

public async send<T extends IGenericData = IGenericData>(data: T): Promise<void> {
const connection = await this.getConnection();
if (connection.readyState !== WebsocketsReadyState.OPEN) {
console.info("Websocket connection is not open, cannot send any data.", data);
return;
}
this.url = url;
this.protocol = protocol || this.protocol;
this.connection = this.factory(this.url, this.protocol);
connection.send(JSON.stringify(data));
}

this.connection.addEventListener("open", event => {
public isConnected(): boolean {
return connectionCache.ws?.readyState === WebsocketsReadyState.OPEN;
}

public isClosed(): boolean {
return connectionCache.ws?.readyState === WebsocketsReadyState.CLOSED;
}

private async createUrl(): Promise<ICreateUrlResult | null> {
const token = await this.getToken();
if (!token) {
console.error(`Missing token to connect to websockets.`);
return null;
}
return {
token,
url: `${this.url}?token=${token}&tenant=${this.tenant}&locale=${this.locale}`
};
}

private async getConnection(): Promise<WebSocket> {
if (connectionCache.ws?.readyState === WebsocketsReadyState.OPEN) {
return connectionCache.ws;
} else if (connectionCache.ws?.readyState === WebsocketsReadyState.CONNECTING) {
return connectionCache.ws;
}

const result = await this.createUrl();
if (!result) {
throw new Error(`Missing URL for WebSocket to connect to.`);
}
const { url } = result;

connectionCache.ws = this.factory(url, this.protocol);

const start = new Date().getTime();

console.log(`Websockets connecting to ${this.url}...`);

connectionCache.ws.addEventListener("open", event => {
const end = new Date().getTime();
console.log(`...connected in ${end - start}ms.`);
return this.subscriptionManager.triggerOnOpen(event);
});
this.connection.addEventListener("close", event => {
connectionCache.ws.addEventListener("close", event => {
return this.subscriptionManager.triggerOnClose(event);
});
this.connection.addEventListener("error", event => {
connectionCache.ws.addEventListener("error", event => {
console.info(`Error in the Websocket connection.`, event);
return this.subscriptionManager.triggerOnError(event);
});
this.connection.addEventListener(

connectionCache.ws.addEventListener(
"message",
(event: IWebsocketsManagerMessageEvent<string>) => {
return this.subscriptionManager.triggerOnMessage(event);
}
);
}

public close(code?: WebsocketsCloseCode, reason?: string): boolean {
if (!this.connection || this.connection.readyState === WebsocketsReadyState.CLOSED) {
this.connection = null;
return true;
} else if (this.connection.readyState !== WebsocketsReadyState.OPEN) {
return false;
}
this.connection.close(code, reason);
this.connection = null;
return true;
}

public reconnect(url?: string, protocol?: IWebsocketsConnectProtocol): void {
if (!this.close(WebsocketsCloseCode.RECONNECT, "Trying to reconnect.")) {
console.error("Failed to close the connection before reconnecting.");
return;
}

this.connect(url || this.url, protocol || this.protocol);
}

public send<T extends IGenericData = IGenericData>(data: T): void {
if (!this.connection || this.connection.readyState !== WebsocketsReadyState.OPEN) {
console.info("Websocket connection is not open, cannot send any data.", data);
return;
}
this.connection.send(JSON.stringify(data));
return connectionCache.ws;
}
}

Expand Down
Loading

0 comments on commit 49e5151

Please sign in to comment.