From 738b105beba18a561cfadc10d504888245107ec7 Mon Sep 17 00:00:00 2001 From: Justus Perlwitz Date: Tue, 2 Jan 2024 09:46:47 +0900 Subject: [PATCH 1/5] Track current connection state internally Add a new class property `state` that stores the current state of a Sarus object. This allows us to cleanly determine whether a Sarus object has ever connected, whether a connection is failing, whether a user has disconnected a Sarus object, and so on. From 5f908d1c2d75d3d55f44981e16035e3271ce9e3e Mon Sep 17 00:00:00 2001 From: Justus Perlwitz Date: Tue, 2 Jan 2024 10:01:01 +0900 Subject: [PATCH 2/5] Add state property This doesn't do anything for now, it's always "created" --- src/index.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/index.ts b/src/index.ts index aa9fe00..c057389 100644 --- a/src/index.ts +++ b/src/index.ts @@ -115,6 +115,26 @@ export default class Sarus { // Internally set messageStore: any; ws: WebSocket | undefined; + /* + * Track the current state of the Sarus object. See the diagram below. + * connect() this.ws.onopen + * ┌───────┐ │ ┌──────────┐ │ ┌─────────┐ + * │created│─┴─►│connecting│──────┴───►│connected│ ◄───────────────────┐ + * └───────┘ └──────────┘ └─────────┘ │ + * disconnect() │ ▲ │ │ + * ┌────────────────────────────────┘ │ │ this.ws.onclose │ + * ▼ │ ▼ │ + * ┌────────────┐ connect() │ ┌──────┐ │ + * │disconnected│──────────────────────────┘ │closed│ ─────────────────┘ + * └────────────┘ └──────┘ this.reconnect() + * + * connect(), disconnect() are generally called by the user + * + * this.reconnect() is called internally when automatic reconnection is + * enabled, but can also be called by the user + */ + state: "created" | "connecting" | "connected" | "disconnected" | "closed" = + "created"; constructor(props: SarusClassParams) { // Extract the properties that are passed to the class From 3be37fc2f5d52bc6f9051ee929c235ca66c1dd21 Mon Sep 17 00:00:00 2001 From: Justus Perlwitz Date: Tue, 2 Jan 2024 10:06:06 +0900 Subject: [PATCH 3/5] Set Sarus state property in connect()/etc. methods --- src/index.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index c057389..53c4db9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -313,6 +313,7 @@ export default class Sarus { * Connects the WebSocket client, and attaches event listeners */ connect() { + this.state = "connecting"; this.ws = new WebSocket(this.url, this.protocols); this.setBinaryType(); this.attachEventListeners(); @@ -346,6 +347,7 @@ export default class Sarus { * @param {boolean} overrideDisableReconnect */ disconnect(overrideDisableReconnect?: boolean) { + this.state = "disconnected"; const self = this; // We do this to prevent automatic reconnections; if (!overrideDisableReconnect) { @@ -477,7 +479,10 @@ export default class Sarus { WS_EVENT_NAMES.forEach((eventName) => { self.ws[`on${eventName}`] = (e: Function) => { self.eventListeners[eventName].forEach((f: Function) => f(e)); - if (eventName === "close" && self.reconnectAutomatically) { + if (eventName === "open") { + self.state = "connected"; + } else if (eventName === "close" && self.reconnectAutomatically) { + self.state = "closed"; self.removeEventListeners(); self.reconnect(); } From 63690e71d1979f5c5a96429250ddb8db58230fae Mon Sep 17 00:00:00 2001 From: Justus Perlwitz Date: Tue, 2 Jan 2024 10:11:55 +0900 Subject: [PATCH 4/5] Remove "created" state Sarus will try to connect automatically in the constructor, so it is not necessary to distinguish between "created" and "connecting". --- src/index.ts | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/index.ts b/src/index.ts index 53c4db9..b8ee74a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -117,24 +117,27 @@ export default class Sarus { ws: WebSocket | undefined; /* * Track the current state of the Sarus object. See the diagram below. - * connect() this.ws.onopen - * ┌───────┐ │ ┌──────────┐ │ ┌─────────┐ - * │created│─┴─►│connecting│──────┴───►│connected│ ◄───────────────────┐ - * └───────┘ └──────────┘ └─────────┘ │ - * disconnect() │ ▲ │ │ - * ┌────────────────────────────────┘ │ │ this.ws.onclose │ - * ▼ │ ▼ │ - * ┌────────────┐ connect() │ ┌──────┐ │ - * │disconnected│──────────────────────────┘ │closed│ ─────────────────┘ - * └────────────┘ └──────┘ this.reconnect() + * + * reconnect() ┌──────┐ + * ┌───────────────────────────────│closed│ + * │ └──────┘ + * │ ▲ + * ▼ │ this.ws.onclose + * ┌──────────┐ this.ws.onopen ┌───┴─────┐ + * │connecting├───────────────────────►│connected│ + * └──────────┘ └───┬─────┘ + * ▲ │ disconnect() + * │ ▼ + * │ reconnect() ┌────────────┐ + * └─────────────────────────────┤disconnected│ + * └────────────┘ * * connect(), disconnect() are generally called by the user * * this.reconnect() is called internally when automatic reconnection is * enabled, but can also be called by the user */ - state: "created" | "connecting" | "connected" | "disconnected" | "closed" = - "created"; + state: "connecting" | "connected" | "disconnected" | "closed" = "connecting"; constructor(props: SarusClassParams) { // Extract the properties that are passed to the class From 9578a8bb25a75f41f253261c96ee192ed7fcba54 Mon Sep 17 00:00:00 2001 From: Justus Perlwitz Date: Tue, 2 Jan 2024 20:58:18 +0900 Subject: [PATCH 5/5] Add state transition cycle test cases --- __tests__/index/state.test.ts | 68 +++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 __tests__/index/state.test.ts diff --git a/__tests__/index/state.test.ts b/__tests__/index/state.test.ts new file mode 100644 index 0000000..470868c --- /dev/null +++ b/__tests__/index/state.test.ts @@ -0,0 +1,68 @@ +// File Dependencies +import Sarus from "../../src/index"; +import { WS } from "jest-websocket-mock"; +import { delay } from "../helpers/delay"; + +const url: string = "ws://localhost:1234"; +const sarusConfig = { + url, + retryConnectionDelay: 1, +}; + +describe("state machine", () => { + it("cycles through a closed connection correctly", async () => { + let server: WS = new WS(url); + + // In the beginning, the state is "connecting" + const sarus: Sarus = new Sarus(sarusConfig); + expect(sarus.state).toBe("connecting"); + + // We wait until we are connected, and see a "connected" state + await server.connected; + expect(sarus.state).toBe("connected"); + + // When the connection drops, the state will be "closed" + server.close(); + await server.closed; + expect(sarus.state).toBe("closed"); + + // Restart server + server = new WS(url); + + // We wait a while, and the status is "connecting" again + await delay(1); + expect(sarus.state).toBe("connecting"); + + // When we connect in our mock server, we are "connected" again + await server.connected; + expect(sarus.state).toBe("connected"); + + // Cleanup + server.close(); + }); + + it("cycles through disconnect() correctly", async () => { + let server: WS = new WS(url); + + // Same initial state transition as above + const sarus: Sarus = new Sarus(sarusConfig); + expect(sarus.state).toBe("connecting"); + await server.connected; + expect(sarus.state).toBe("connected"); + + // The user can disconnect and the state will be "disconnected" + sarus.disconnect(); + expect(sarus.state).toBe("disconnected"); + await server.closed; + + // The user can now reconnect, and the state will be "connecting", and then + // "connected" again + sarus.connect(); + expect(sarus.state).toBe("connecting"); + await server.connected; + // XXX for some reason the test will fail without waiting 10 ms here + await delay(10); + expect(sarus.state).toBe("connected"); + server.close(); + }); +});