Skip to content

Commit

Permalink
websocket connection updates (#358)
Browse files Browse the repository at this point in the history
* make sure to close the connection

* make sure to close the connection

* save

* fix client code

* cleanup

* add some tests
  • Loading branch information
rileylnapier authored Jan 3, 2023
1 parent 2a3a2ef commit 983c00b
Show file tree
Hide file tree
Showing 16 changed files with 356 additions and 156 deletions.
1 change: 0 additions & 1 deletion babel.config.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,4 @@ module.exports = (root) => ({
"@babel/preset-env",
"@babel/preset-react",
],
ignore: ["src/__tests__", "src/__mocks__"],
});
12 changes: 7 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"test:gql": "lerna run test --scope @trycourier/client-graphql --stream",
"test:api": "lerna run test --scope @trycourier/client-api --stream",
"test:hooks": "lerna run test --scope @trycourier/react-hooks --stream",
"test:provider": "lerna run test --scope @trycourier/react-provider --stream",
"test": "lerna run test --stream --parallel",
"type-check": "lerna run type-check --parallel --stream",
"type-coverage:detail": "type-coverage --strict --detail",
Expand All @@ -50,7 +51,8 @@
"@babel/preset-react": "^7.12.10",
"@babel/preset-typescript": "^7.12.7",
"@testing-library/jest-dom": "^5.11.9",
"@testing-library/react": "^11.2.3",
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "^8.0.1",
"@types/jest": "^26.0.20",
"@types/react": "^17.0.0",
"@typescript-eslint/eslint-plugin": "^4.15.2",
Expand All @@ -64,26 +66,26 @@
"babel-plugin-styled-components": "^1.12.0",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-inline-environment-variables": "^0.4.3",
"eslint": "^7.18.0",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-jest": "^22.11.0",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-react-hooks": "^4.0.2",
"eslint-plugin-react": "^7.20.0",
"eslint": "^7.18.0",
"eslint-plugin-react-hooks": "^4.0.2",
"fetch-mock": "^9.11.0",
"husky": "^6.0.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^26.6.3",
"jest-styled-components": "^7.0.3",
"jest-websocket-mock": "^2.3.0",
"jest": "^26.6.3",
"lerna": "^5.4.0",
"msw": "^0.43.0",
"nx": "^14.4.2",
"pretty-quick": "^3.1.0",
"react-dom": "^17.0.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"rimraf": "^3.0.2",
"ts-jest": "^26.4.4",
"ts-loader": "^8.0.17",
Expand Down
5 changes: 5 additions & 0 deletions packages/react-provider/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const jestConfigBase = require("../../jest.config.base");
const babelConfig = require("./babel.config.js");

module.exports = jestConfigBase(babelConfig);
4 changes: 3 additions & 1 deletion packages/react-provider/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"main": "dist/index.js",
"types": "typings/index.d.ts",
"scripts": {
"babel": "babel src -d dist --extensions \".ts,.tsx\" --ignore \"src/**/__tests__/**\"",
"test": "jest -c jest.config.js --runInBand",
"babel": "babel src -d dist --extensions \".ts,.tsx\" --ignore \"src/**/__tests__/**\" --ignore \"src/**/__mocks__/**\"",
"build:watch": "yarn babel --watch",
"build": "rimraf dist && yarn babel",
"clean": "rimraf dist",
Expand All @@ -17,6 +18,7 @@
"dependencies": {
"@trycourier/client-graphql": "^1.57.0",
"buffer": "^6.0.3",
"jwt-decode": "^3.1.2",
"react-use": "^17.2.1",
"reconnecting-websocket": "^4.4.0",
"rimraf": "^3.0.2",
Expand Down
13 changes: 13 additions & 0 deletions packages/react-provider/src/__mocks__/ws.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const mockConnect = jest.fn();
export const mockSend = jest.fn();
export const mockRenewSession = jest.fn();

const mock = jest.fn().mockImplementation(() => {
return {
connect: mockConnect,
send: mockSend,
renewSession: mockRenewSession,
};
});

export const WS = mock;
100 changes: 100 additions & 0 deletions packages/react-provider/src/hooks/__tests__/use-transport.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { renderHook } from "@testing-library/react-hooks"; // will attempt to auto-detect
import { Transport } from "../../transports/base";
import useTransport from "../use-transport";

import { WS } from "../../ws";

const ws = new WS({});

const mockConnect = ws.connect as jest.Mock;
const mockRenewSession = ws.renewSession as jest.Mock;

jest.mock("../../ws");
describe("useTransport", () => {
afterEach(() => {
jest.clearAllMocks();
});

test("will throw an error if missing auth and clientkey", () => {
expect.assertions(1);
try {
const { result } = renderHook(() =>
useTransport({
clientSourceId: "abc123",
})
);

if (result.error) {
throw result.error;
}
} catch (ex) {
expect(String(ex)).toBe("Error: Missing ClientKey or Authorization");
}
});

test("will return the same transport passed in", () => {
const transport = new Transport();
const { result } = renderHook(() =>
useTransport({
transport,
})
);

expect(result.current).toEqual(transport);
});

test("will create a new transport if one is not provided", () => {
const { result } = renderHook(() =>
useTransport({
clientSourceId: "mockClientSourceId",
authorization: "mockAuth",
})
);

expect(result.current).toBeTruthy();
expect(mockConnect.mock.calls.length).toEqual(1);
});

test("will call renewSession if a new token is provided", () => {
let authorization =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6InVzZXJfaWQ6NzBmNmE0ZjQtMjkwNy00NTE4LWI4ZjMtYjljZmFiMjI0NzY0IGluYm94OnJlYWQ6bWVzc2FnZXMiLCJ0ZW5hbnRfc2NvcGUiOiJwdWJsaXNoZWQvcHJvZHVjdGlvbiIsInRlbmFudF9pZCI6Ijc2ODI1MWNmLTNlYjgtNDI2YS05MmViLWZhYTBlNzY3ODc2OCIsImlhdCI6MTY3MjI1NzY1OSwianRpIjoiYmJlMDMyMmMtZWY4Mi00M2FkLWI3NGMtOGZlYWNiNTczYTY0In0.Xs_yd8IhdNORK8LyleS10FDLQbb4sXkCtGHPq7tUGa4";

const { result, rerender } = renderHook(() =>
useTransport({
clientSourceId: "mockClientSourceId",
authorization,
})
);

expect(result.current).toBeTruthy();
expect(mockConnect.mock.calls.length).toEqual(1);

authorization =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6InVzZXJfaWQ6NzBmNmE0ZjQtMjkwNy00NTE4LWI4ZjMtYjljZmFiMjI0NzY0IGluYm94OnJlYWQ6bWVzc2FnZXMiLCJ0ZW5hbnRfc2NvcGUiOiJwdWJsaXNoZWQvcHJvZHVjdGlvbiIsInRlbmFudF9pZCI6Ijc2ODI1MWNmLTNlYjgtNDI2YS05MmViLWZhYTBlNzY3ODc2OCIsImlhdCI6MTY3Mjc4MDE5MSwianRpIjoiMzU1NmU1OTYtNjljZi00NjdiLTg1YjMtNDk5ZjZmYzk2YjVhIn0.peUty0F94bhulmD4HS-7H7N3-HI31mIvU8jLFBEpUgM";

rerender();
expect(mockRenewSession.mock.calls.length).toEqual(1);
});

test("will NOT call renewSession if a new token is provided but the scope changes", () => {
let authorization =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6InVzZXJfaWQ6NzBmNmE0ZjQtMjkwNy00NTE4LWI4ZjMtYjljZmFiMjI0NzY0IGluYm94OnJlYWQ6bWVzc2FnZXMiLCJ0ZW5hbnRfc2NvcGUiOiJwdWJsaXNoZWQvcHJvZHVjdGlvbiIsInRlbmFudF9pZCI6Ijc2ODI1MWNmLTNlYjgtNDI2YS05MmViLWZhYTBlNzY3ODc2OCIsImlhdCI6MTY3MjI1NzY1OSwianRpIjoiYmJlMDMyMmMtZWY4Mi00M2FkLWI3NGMtOGZlYWNiNTczYTY0In0.Xs_yd8IhdNORK8LyleS10FDLQbb4sXkCtGHPq7tUGa4";

const { result, rerender } = renderHook(() =>
useTransport({
clientSourceId: "mockClientSourceId",
authorization,
})
);

expect(result.current).toBeTruthy();
expect(mockConnect.mock.calls.length).toEqual(1);

authorization =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6InVzZXJfaWQ6YmxhaCBpbmJveDpyZWFkOm1lc3NhZ2VzIiwidGVuYW50X3Njb3BlIjoicHVibGlzaGVkL3Byb2R1Y3Rpb24iLCJ0ZW5hbnRfaWQiOiI3NjgyNTFjZi0zZWI4LTQyNmEtOTJlYi1mYWEwZTc2Nzg3NjgiLCJpYXQiOjE2NzI3ODIwNzYsImp0aSI6ImJjZjRiN2QzLWMyNDktNDQzNC04ZTQ0LWFjMTYxY2U0NTRiZCJ9.J7k0OQ1qfFR5MpdoP13mCusQWejpx7VB6Z6A194RxU8";

rerender();
expect(mockRenewSession.mock.calls.length).toEqual(0);
expect(mockConnect.mock.calls.length).toEqual(2);
});
});
24 changes: 17 additions & 7 deletions packages/react-provider/src/hooks/use-client-source-id.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import useHash from "./use-hash";
import * as uuid from "uuid";
import { useMemo } from "react";

// this hash or clientsoureid is used to help create a managealbe sized property to store information in localstorage
// its possible we will just have the "authorization" as an identifier and that is a huge string
import jwtDecode from "jwt-decode";

const useClientSourceId = ({
clientKey,
Expand All @@ -12,9 +9,22 @@ const useClientSourceId = ({
id,
localStorage,
}): string => {
const clientSourceKey = useHash(
authorization ? authorization : `${clientKey}/${userId}`
);
const clientSourceKey = useMemo(() => {
if (!authorization) {
return `${clientKey}/${userId}`;
}

const decoded = jwtDecode(authorization) as {
tenantId: string;
scope: string;
};
const scopeUserId = decoded?.scope
?.split(" ")
?.find((s) => s.includes("user_id"))
?.replace("user_id", "");

return `${decoded?.tenantId}/${scopeUserId}`;
}, [authorization, clientKey, userId]);

return useMemo(() => {
if (!localStorage) {
Expand Down
24 changes: 0 additions & 24 deletions packages/react-provider/src/hooks/use-hash.ts

This file was deleted.

77 changes: 65 additions & 12 deletions packages/react-provider/src/hooks/use-transport.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,82 @@
import { useMemo } from "react";
import { Transport, CourierTransport } from "~/transports";
import { useMemo, useRef } from "react";
import { CourierTransport, Transport } from "~/transports";
import jwtDecode from "jwt-decode";
import { ITransportOptions } from "~/transports/courier/types";
interface DecodedAuth {
scope: string;
tenantId: string;
}

const useCourierTransport = ({
const useTransport = ({
authorization,
clientSourceId,
clientKey,
transport,
userSignature,
wsOptions,
}): Transport => {
}: {
authorization?: string;
clientSourceId?: string;
clientKey?: string;
transport?: CourierTransport | Transport;
userSignature?: string;
wsOptions?: ITransportOptions["wsOptions"];
}): CourierTransport | Transport => {
const transportRef =
useRef<{
authorization: string;
transport: CourierTransport;
}>();

return useMemo(() => {
if (transport) {
return transport;
}

if ((clientKey || authorization) && !transport) {
return new CourierTransport({
if (
authorization &&
transportRef?.current?.authorization &&
transportRef?.current.transport
) {
const oldDecodedAuth = jwtDecode(
transportRef?.current?.authorization
) as DecodedAuth;
const newDecodedAuth = jwtDecode(authorization) as DecodedAuth;

if (
oldDecodedAuth.scope === newDecodedAuth.scope &&
oldDecodedAuth.tenantId === newDecodedAuth.tenantId
) {
transportRef.current.transport.renewSession(authorization);
return transportRef.current.transport;
}
}

if (!clientKey && !authorization) {
throw new Error("Missing ClientKey or Authorization");
}

if (!clientSourceId) {
throw new Error("Missing ClientSourceId");
}

const newTransport = new CourierTransport({
authorization,
clientSourceId,
clientKey,
userSignature,
wsOptions,
});

// keep track of the transport so we don't reconnect when we don't have to
if (authorization) {
transportRef.current = {
authorization,
clientSourceId,
clientKey,
userSignature,
wsOptions,
});
transport: newTransport,
};
}
return newTransport;
}, [authorization, clientKey, transport, userSignature, wsOptions]);
};

export default useCourierTransport;
export default useTransport;
5 changes: 5 additions & 0 deletions packages/react-provider/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,12 @@ export const CourierProvider: React.FunctionComponent<ICourierProviderProps> =
courierTransport.intercept(onMessage);
}

const intervalId = setInterval(() => {
//courierTransport.keepAlive();
}, 300000); // 5 minutes

return () => {
clearInterval(intervalId);
courierTransport.unsubscribe(userId);
courierTransport.closeConnection();
};
Expand Down
7 changes: 7 additions & 0 deletions packages/react-provider/src/transports/courier/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export class CourierTransport extends Transport {
options: options.wsOptions,
userSignature: options.userSignature,
});

this.ws.connect();
}

Expand All @@ -41,6 +42,12 @@ export class CourierTransport extends Transport {
this.ws.connect();
}

keepAlive(): void {
this.ws.send({
action: "keepAlive",
});
}

send(message: ICourierMessage): void {
this.ws.send({
...message,
Expand Down
4 changes: 2 additions & 2 deletions packages/react-provider/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Transport } from "./transports";
import { CourierTransport, Transport } from "./transports";
import { IActionBlock, Interceptor, ITextBlock } from "./transports/types";
import { ErrorEvent } from "reconnecting-websocket";

Expand Down Expand Up @@ -73,7 +73,7 @@ export interface ICourierProviderProps {
middleware?: any;
localStorage?: Storage;
onMessage?: Interceptor;
transport?: Transport;
transport?: CourierTransport | Transport;
userId?: string;
userSignature?: string;
wsOptions?: WSOptions;
Expand Down
Loading

0 comments on commit 983c00b

Please sign in to comment.