@anephenix/sarus
Advanced tools
Comparing version 0.5.0 to 0.6.0
// File Dependencies | ||
import Sarus from "../../src/index"; | ||
import { WS } from "jest-websocket-mock"; | ||
import { calculateRetryDelayFactor } from "../../src/index"; | ||
import type { ExponentialBackoffParams } from "../../src/index"; | ||
@@ -64,1 +66,87 @@ const url = "ws://localhost:1234"; | ||
}); | ||
describe("Exponential backoff delay", () => { | ||
describe("with rate 2, backoffLimit 8000 ms", () => { | ||
// The initial delay shall be 1 s | ||
const initialDelay = 1000; | ||
const exponentialBackoff: ExponentialBackoffParams = { | ||
backoffRate: 2, | ||
// We put the ceiling at exactly 8000 ms | ||
backoffLimit: 8000, | ||
}; | ||
const attempts: [number, number][] = [ | ||
[1000, 0], | ||
[2000, 1], | ||
[4000, 2], | ||
[8000, 3], | ||
[8000, 4], | ||
]; | ||
it("will never be more than 8000 ms with rate set to 2", () => { | ||
attempts.forEach(([delay, failedAttempts]) => { | ||
expect( | ||
calculateRetryDelayFactor( | ||
exponentialBackoff, | ||
initialDelay, | ||
failedAttempts, | ||
), | ||
).toBe(delay); | ||
}); | ||
}); | ||
it("should delay reconnection attempts exponentially", async () => { | ||
// Somehow we need to convincen typescript here that "WebSocket" is | ||
// totally valid. Could be because it doesn't assume WebSocket is part of | ||
// global / the index key is missing | ||
const webSocketSpy = jest.spyOn(global, "WebSocket" as any); | ||
webSocketSpy.mockImplementation(() => {}); | ||
const setTimeoutSpy = jest.spyOn(global, "setTimeout"); | ||
const sarus = new Sarus({ url, exponentialBackoff }); | ||
expect(sarus.state).toStrictEqual({ | ||
kind: "connecting", | ||
failedConnectionAttempts: 0, | ||
}); | ||
let instance: WebSocket; | ||
// Get the first WebSocket instance, and ... | ||
[instance] = webSocketSpy.mock.instances; | ||
if (!instance.onopen) { | ||
throw new Error(); | ||
} | ||
// tell the sarus instance that it is open, and ... | ||
instance.onopen(new Event("open")); | ||
if (!instance.onclose) { | ||
throw new Error(); | ||
} | ||
// close it immediately | ||
instance.onclose(new CloseEvent("close")); | ||
expect(sarus.state).toStrictEqual({ | ||
kind: "closed", | ||
}); | ||
let cb: Sarus["connect"]; | ||
// We iteratively call sarus.connect() and let it fail, seeing | ||
// if it reaches 8000 as a delay and stays there | ||
attempts.forEach(([delay, failedAttempts]) => { | ||
const call = | ||
setTimeoutSpy.mock.calls[setTimeoutSpy.mock.calls.length - 1]; | ||
if (!call) { | ||
throw new Error(); | ||
} | ||
// Make sure that setTimeout was called with the correct delay | ||
expect(call[1]).toBe(delay); | ||
cb = call[0]; | ||
cb(); | ||
// Get the most recent WebSocket instance | ||
instance = | ||
webSocketSpy.mock.instances[webSocketSpy.mock.instances.length - 1]; | ||
if (!instance.onclose) { | ||
throw new Error(); | ||
} | ||
instance.onclose(new CloseEvent("close")); | ||
expect(sarus.state).toStrictEqual({ | ||
kind: "connecting", | ||
failedConnectionAttempts: failedAttempts + 1, | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); |
@@ -18,7 +18,12 @@ // File Dependencies | ||
const sarus: Sarus = new Sarus(sarusConfig); | ||
expect(sarus.state).toBe("connecting"); | ||
// Since Sarus jumps into connecting directly, 1 connection attempt is made | ||
// right in the beginning, but none have failed | ||
expect(sarus.state).toStrictEqual({ | ||
kind: "connecting", | ||
failedConnectionAttempts: 0, | ||
}); | ||
// We wait until we are connected, and see a "connected" state | ||
await server.connected; | ||
expect(sarus.state).toBe("connected"); | ||
expect(sarus.state.kind).toBe("connected"); | ||
@@ -28,14 +33,21 @@ // When the connection drops, the state will be "closed" | ||
await server.closed; | ||
expect(sarus.state).toBe("closed"); | ||
expect(sarus.state).toStrictEqual({ | ||
kind: "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"); | ||
// In the beginning, no connection attempts have been made, since in the | ||
// case of a closed connection, we wait a bit until we try to connect again. | ||
expect(sarus.state).toStrictEqual({ | ||
kind: "connecting", | ||
failedConnectionAttempts: 0, | ||
}); | ||
// We restart the server and let the Sarus instance reconnect: | ||
server = new WS(url); | ||
// When we connect in our mock server, we are "connected" again | ||
await server.connected; | ||
expect(sarus.state).toBe("connected"); | ||
expect(sarus.state.kind).toBe("connected"); | ||
@@ -51,9 +63,9 @@ // Cleanup | ||
const sarus: Sarus = new Sarus(sarusConfig); | ||
expect(sarus.state).toBe("connecting"); | ||
expect(sarus.state.kind).toBe("connecting"); | ||
await server.connected; | ||
expect(sarus.state).toBe("connected"); | ||
expect(sarus.state.kind).toBe("connected"); | ||
// The user can disconnect and the state will be "disconnected" | ||
sarus.disconnect(); | ||
expect(sarus.state).toBe("disconnected"); | ||
expect(sarus.state.kind).toBe("disconnected"); | ||
await server.closed; | ||
@@ -64,9 +76,9 @@ | ||
sarus.connect(); | ||
expect(sarus.state).toBe("connecting"); | ||
expect(sarus.state.kind).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"); | ||
expect(sarus.state.kind).toBe("connected"); | ||
server.close(); | ||
}); | ||
}); |
# CHANGELOG | ||
### 0.6.0 - Saturday 17th August, 2024 | ||
- Fixed a performance regression relating to the auditEventListeners function (PR#461) | ||
- Minimize production dependencies by removing current package.json dependencies (PR#424) | ||
= Added support for Exponential backoff on reconnection attempts (PR#403) | ||
- Updated dependencies | ||
### 0.5.0 - Saturday 27th April, 2024 | ||
@@ -4,0 +11,0 @@ |
import { PartialEventListenersInterface, EventListenersInterface } from "./lib/validators"; | ||
export interface ExponentialBackoffParams { | ||
backoffRate: number; | ||
backoffLimit: number; | ||
} | ||
export declare function calculateRetryDelayFactor(params: ExponentialBackoffParams, initialDelay: number, failedConnectionAttempts: number): number; | ||
export interface SarusClassParams { | ||
@@ -10,2 +15,3 @@ url: string; | ||
retryConnectionDelay?: boolean | number; | ||
exponentialBackoff?: ExponentialBackoffParams; | ||
storageType?: string; | ||
@@ -24,3 +30,4 @@ storageKey?: string; | ||
* @param {number} param0.retryProcessTimePeriod - An optional number for how long the time period between retrying to send a messgae to a WebSocket server should be | ||
* @param {number} param0.retryConnectionDelay - A parameter for the amount of time to delay a reconnection attempt by, in miliseconds. | ||
* @param {boolean|number} param0.retryConnectionDelay - An optional parameter for whether to delay WebSocket reconnection attempts by a time period. If true, the delay is 1000ms, otherwise it is the number passed. The default value when this parameter is undefined will be interpreted as 1000ms. | ||
* @param {ExponentialBackoffParams} param0.exponentialBackoff - An optional containing configuration for exponential backoff. If this parameter is undefined, exponential backoff is disabled. The minimum delay is determined by retryConnectionDelay. If retryConnectionDelay is set is false, this setting will not be in effect. | ||
* @param {string} param0.storageType - An optional string specifying the type of storage to use for persisting messages in the message queue | ||
@@ -38,2 +45,3 @@ * @param {string} param0.storageKey - An optional string specifying the key used to store the messages data against in sessionStorage/localStorage | ||
retryConnectionDelay: number; | ||
exponentialBackoff?: ExponentialBackoffParams; | ||
storageType: string; | ||
@@ -43,3 +51,12 @@ storageKey: string; | ||
ws: WebSocket | undefined; | ||
state: "connecting" | "connected" | "disconnected" | "closed"; | ||
state: { | ||
kind: "connecting"; | ||
failedConnectionAttempts: number; | ||
} | { | ||
kind: "connected"; | ||
} | { | ||
kind: "disconnected"; | ||
} | { | ||
kind: "closed"; | ||
}; | ||
constructor(props: SarusClassParams); | ||
@@ -83,3 +100,8 @@ /** | ||
*/ | ||
auditEventListeners(eventListeners: PartialEventListenersInterface): EventListenersInterface; | ||
auditEventListeners(eventListeners: PartialEventListenersInterface | undefined): { | ||
open: Function[]; | ||
message: Function[]; | ||
error: Function[]; | ||
close: Function[]; | ||
}; | ||
/** | ||
@@ -90,3 +112,4 @@ * Connects the WebSocket client, and attaches event listeners | ||
/** | ||
* Reconnects the WebSocket client based on the retryConnectionDelay setting. | ||
* Reconnects the WebSocket client based on the retryConnectionDelay and | ||
* ExponentialBackoffParam setting. | ||
*/ | ||
@@ -93,0 +116,0 @@ reconnect(): void; |
"use strict"; | ||
var __assign = (this && this.__assign) || function () { | ||
__assign = Object.assign || function(t) { | ||
for (var s, i = 1, n = arguments.length; i < n; i++) { | ||
s = arguments[i]; | ||
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) | ||
t[p] = s[p]; | ||
} | ||
return t; | ||
}; | ||
return __assign.apply(this, arguments); | ||
}; | ||
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { | ||
@@ -23,2 +12,3 @@ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.calculateRetryDelayFactor = calculateRetryDelayFactor; | ||
// File Dependencies | ||
@@ -80,2 +70,16 @@ var constants_1 = require("./lib/constants"); | ||
}; | ||
/* | ||
* Calculate the exponential backoff delay for a given number of connection | ||
* attempts. | ||
* @param {ExponentialBackoffParams} params - configuration parameters for | ||
* exponential backoff. | ||
* @param {number} initialDelay - the initial delay before any backoff is | ||
* applied | ||
* @param {number} failedConnectionAttempts - the number of connection attempts | ||
* that have previously failed | ||
* @returns {void} - set does not return | ||
*/ | ||
function calculateRetryDelayFactor(params, initialDelay, failedConnectionAttempts) { | ||
return Math.min(initialDelay * Math.pow(params.backoffRate, failedConnectionAttempts), params.backoffLimit); | ||
} | ||
/** | ||
@@ -91,3 +95,4 @@ * The Sarus client class | ||
* @param {number} param0.retryProcessTimePeriod - An optional number for how long the time period between retrying to send a messgae to a WebSocket server should be | ||
* @param {number} param0.retryConnectionDelay - A parameter for the amount of time to delay a reconnection attempt by, in miliseconds. | ||
* @param {boolean|number} param0.retryConnectionDelay - An optional parameter for whether to delay WebSocket reconnection attempts by a time period. If true, the delay is 1000ms, otherwise it is the number passed. The default value when this parameter is undefined will be interpreted as 1000ms. | ||
* @param {ExponentialBackoffParams} param0.exponentialBackoff - An optional containing configuration for exponential backoff. If this parameter is undefined, exponential backoff is disabled. The minimum delay is determined by retryConnectionDelay. If retryConnectionDelay is set is false, this setting will not be in effect. | ||
* @param {string} param0.storageType - An optional string specifying the type of storage to use for persisting messages in the message queue | ||
@@ -119,9 +124,24 @@ * @param {string} param0.storageKey - An optional string specifying the key used to store the messages data against in sessionStorage/localStorage | ||
* | ||
* this.reconnect() is called internally when automatic reconnection is | ||
* enabled, but can also be called by the user | ||
* When disconnected by the WebSocket itself (i.e., this.ws.onclose), | ||
* this.reconnect() is called automatically if reconnection is enabled. | ||
* this.reconnect() can also be called by the user, for example if | ||
* this.disconnect() was purposefully called and reconnection is desired. | ||
* | ||
* The current state is specified by the 'kind' property of state | ||
* Each state can have additional data contained in properties other than | ||
* 'kind'. Those properties might be unique to one state, or contained in | ||
* several states. To access a property, it might be necessary to narrow down | ||
* the 'kind' of state. | ||
* | ||
* The initial state is connecting, as a Sarus client tries to connect right | ||
* after the constructor wraps up. | ||
*/ | ||
this.state = "connecting"; | ||
this.state = { | ||
kind: "connecting", | ||
failedConnectionAttempts: 0, | ||
}; | ||
// Extract the properties that are passed to the class | ||
var url = props.url, binaryType = props.binaryType, protocols = props.protocols, _b = props.eventListeners, eventListeners = _b === void 0 ? constants_1.DEFAULT_EVENT_LISTENERS_OBJECT : _b, reconnectAutomatically = props.reconnectAutomatically, retryProcessTimePeriod = props.retryProcessTimePeriod, // TODO - write a test case to check this | ||
retryConnectionDelay = props.retryConnectionDelay, _c = props.storageType, storageType = _c === void 0 ? "memory" : _c, _d = props.storageKey, storageKey = _d === void 0 ? "sarus" : _d; | ||
var url = props.url, binaryType = props.binaryType, protocols = props.protocols, eventListeners = props.eventListeners, // = DEFAULT_EVENT_LISTENERS_OBJECT, | ||
reconnectAutomatically = props.reconnectAutomatically, retryProcessTimePeriod = props.retryProcessTimePeriod, // TODO - write a test case to check this | ||
retryConnectionDelay = props.retryConnectionDelay, exponentialBackoff = props.exponentialBackoff, _b = props.storageType, storageType = _b === void 0 ? "memory" : _b, _c = props.storageKey, storageKey = _c === void 0 ? "sarus" : _c; | ||
this.eventListeners = this.auditEventListeners(eventListeners); | ||
@@ -161,2 +181,8 @@ // Sets the WebSocket server url for the client to connect to. | ||
/* | ||
When a exponential backoff parameter object is provided, reconnection | ||
attemptions will be increasingly delayed by an exponential factor. | ||
This feature is disabled by default. | ||
*/ | ||
this.exponentialBackoff = exponentialBackoff; | ||
/* | ||
Sets the storage type for the messages in the message queue. By default | ||
@@ -268,10 +294,8 @@ it is an in-memory option, but can also be set as 'session' for | ||
Sarus.prototype.auditEventListeners = function (eventListeners) { | ||
var defaultEventListeners = { | ||
open: [], | ||
message: [], | ||
error: [], | ||
close: [], | ||
return { | ||
open: (eventListeners === null || eventListeners === void 0 ? void 0 : eventListeners.open) || [], | ||
message: (eventListeners === null || eventListeners === void 0 ? void 0 : eventListeners.message) || [], | ||
error: (eventListeners === null || eventListeners === void 0 ? void 0 : eventListeners.error) || [], | ||
close: (eventListeners === null || eventListeners === void 0 ? void 0 : eventListeners.close) || [], | ||
}; | ||
var mergedEventListeners = __assign(__assign({}, defaultEventListeners), eventListeners); // Type assertion added here | ||
return mergedEventListeners; | ||
}; | ||
@@ -282,3 +306,6 @@ /** | ||
Sarus.prototype.connect = function () { | ||
this.state = "connecting"; | ||
// If we aren't already connecting, we are now | ||
if (this.state.kind !== "connecting") { | ||
this.state = { kind: "connecting", failedConnectionAttempts: 0 }; | ||
} | ||
this.ws = new WebSocket(this.url, this.protocols); | ||
@@ -291,8 +318,20 @@ this.setBinaryType(); | ||
/** | ||
* Reconnects the WebSocket client based on the retryConnectionDelay setting. | ||
* Reconnects the WebSocket client based on the retryConnectionDelay and | ||
* ExponentialBackoffParam setting. | ||
*/ | ||
Sarus.prototype.reconnect = function () { | ||
var self = this; | ||
var retryConnectionDelay = self.retryConnectionDelay; | ||
setTimeout(self.connect, retryConnectionDelay); | ||
var retryConnectionDelay = self.retryConnectionDelay, exponentialBackoff = self.exponentialBackoff; | ||
// If we are already in a "connecting" state, we need to refer to the | ||
// current amount of connection attemps to correctly calculate the | ||
// exponential delay -- if exponential backoff is enabled. | ||
var failedConnectionAttempts = self.state.kind === "connecting" | ||
? self.state.failedConnectionAttempts | ||
: 0; | ||
// If no exponential backoff is enabled, retryConnectionDelay will | ||
// be scaled by a factor of 1 and it will stay the original value. | ||
var delay = exponentialBackoff | ||
? calculateRetryDelayFactor(exponentialBackoff, retryConnectionDelay, failedConnectionAttempts) | ||
: retryConnectionDelay; | ||
setTimeout(self.connect, delay); | ||
}; | ||
@@ -306,3 +345,3 @@ /** | ||
Sarus.prototype.disconnect = function (overrideDisableReconnect) { | ||
this.state = "disconnected"; | ||
this.state = { kind: "disconnected" }; | ||
var self = this; | ||
@@ -426,6 +465,22 @@ // We do this to prevent automatic reconnections; | ||
if (eventName === "open") { | ||
self.state = "connected"; | ||
self.state = { kind: "connected" }; | ||
} | ||
else if (eventName === "close" && self.reconnectAutomatically) { | ||
self.state = "closed"; | ||
var state = self.state; | ||
// If we have previously been "connecting", we carry over the amount | ||
// of failed connection attempts and add 1, since the current | ||
// connection attempt failed. We stay "connecting" instead of | ||
// "closed", since we've never been fully "connected" in the first | ||
// place. | ||
if (state.kind === "connecting") { | ||
self.state = { | ||
kind: "connecting", | ||
failedConnectionAttempts: state.failedConnectionAttempts + 1, | ||
}; | ||
} | ||
else { | ||
// If we were in a different state, we assume that our connection | ||
// freshly closed and have not made any failed connection attempts. | ||
self.state = { kind: "closed" }; | ||
} | ||
self.removeEventListeners(); | ||
@@ -432,0 +487,0 @@ self.reconnect(); |
{ | ||
"name": "@anephenix/sarus", | ||
"version": "0.5.0", | ||
"version": "0.6.0", | ||
"description": "A WebSocket JavaScript library", | ||
@@ -28,3 +28,2 @@ "main": "dist/index.js", | ||
"@types/window-or-global": "^1.0.6", | ||
"coveralls": "^3.1.1", | ||
"dom-storage": "^2.1.0", | ||
@@ -38,3 +37,2 @@ "ip-regex": "^5.0.0", | ||
"mock-socket": "^9.2.1", | ||
"npm-upgrade": "^3.1.0", | ||
"prettier": "^3.0.3", | ||
@@ -47,3 +45,2 @@ "ts-jest": "^29.1.0", | ||
"watch": "tsc --project tsconfig.json --watch", | ||
"cover": "jest --coverage --coverageReporters=text-lcov | coveralls", | ||
"test": "jest --coverage", | ||
@@ -50,0 +47,0 @@ "prettier": "prettier src __tests__ --write", |
@@ -5,3 +5,3 @@ # Sarus | ||
[![npm version](https://badge.fury.io/js/%40anephenix%2Fsarus.svg)](https://badge.fury.io/js/%40anephenix%2Fsarus) ![example workflow](https://github.com/anephenix/sarus/actions/workflows/node.js.yml/badge.svg) [![Maintainability](https://api.codeclimate.com/v1/badges/0671cfc9630a97854b30/maintainability)](https://codeclimate.com/github/anephenix/sarus/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/0671cfc9630a97854b30/test_coverage)](https://codeclimate.com/github/anephenix/sarus/test_coverage) | ||
[![npm version](https://badge.fury.io/js/%40anephenix%2Fsarus.svg)](https://badge.fury.io/js/%40anephenix%2Fsarus) ![example workflow](https://github.com/anephenix/sarus/actions/workflows/node.js.yml/badge.svg) [![Maintainability](https://api.codeclimate.com/v1/badges/0671cfc9630a97854b30/maintainability)](https://codeclimate.com/github/anephenix/sarus/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/0671cfc9630a97854b30/test_coverage)](https://codeclimate.com/github/anephenix/sarus/test_coverage) [![Socket Badge](https://socket.dev/api/badge/npm/package/@anephenix/sarus)](https://socket.dev/npm/package/@anephenix/sarus) | ||
@@ -356,2 +356,64 @@ ### Features | ||
### Exponential backoff | ||
Configure exponential backoff like so: | ||
```typescript | ||
import Sarus from '@anephenix/sarus'; | ||
const sarus = new Sarus({ | ||
url: 'wss://ws.anephenix.com', | ||
exponentialBackoff: { | ||
// Exponential factor, here 2 will result in | ||
// 1 s, 2 s, 4 s, and so on increasing delays | ||
backoffRate: 2, | ||
// Never wait more than 2000 seconds | ||
backoffLimit: 2000, | ||
}, | ||
}); | ||
``` | ||
When a connection attempt repeatedly fails, decreasing the delay | ||
exponentially between each subsequent reconnection attempt is called | ||
[Exponential backoff](https://en.wikipedia.org/wiki/Exponential_backoff). The | ||
idea is that if a connection attempt failed after 1 second, and 2 seconds, then it is | ||
not necessary to check it on the 3rd second, since the probability of a | ||
reconnection succeeding on the third attempt is most likely not going up. | ||
Therefore, increasing the delay between each attempt factors in the assumption | ||
that a connection is not more likely to succeed by repeatedly probing in regular | ||
intervals. | ||
This decreases both the load on the client, as well as on the server. For | ||
a client, fewer websocket connection attempts decrease the load on the client | ||
and on the network connection. For the server, should websocket requests fail | ||
within, then the load for handling repeatedly failing requests will fall | ||
as well. Furthermore, the burden on the network will also be decreased. Should | ||
for example a server refuse to accept websocket connections for one client, | ||
then there is the possibility that other clients will also not be able to connect. | ||
Sarus implements _truncated exponential backoff_, meaning that the maximum | ||
reconnection delay is capped by another factor `backoffLimit` and will never | ||
exceed it. The exponential backoff rate itself is determined by `backoffRate`. | ||
If `backoffRate` is 2, then the delays will be 1 s, 2 s, 4 s, and so on. | ||
The algorithm for reconnection looks like this in pseudocode: | ||
```javascript | ||
// Configurable | ||
const backoffRate = 2; | ||
// The maximum delay will be 400s | ||
const backoffLimit = 400; | ||
let notConnected = false; | ||
let connectionAttempts = 1; | ||
while (notConnected) { | ||
const delay = Math.min( | ||
Math.pow(connectionAttempts, backoffRate), | ||
backoffLimit, | ||
); | ||
await delay(delay); | ||
notConnected = tryToConnect(); | ||
connectionAttempts += 1; | ||
} | ||
``` | ||
### Advanced options | ||
@@ -358,0 +420,0 @@ |
140
src/index.ts
@@ -76,2 +76,29 @@ // File Dependencies | ||
export interface ExponentialBackoffParams { | ||
backoffRate: number; | ||
backoffLimit: number; | ||
} | ||
/* | ||
* Calculate the exponential backoff delay for a given number of connection | ||
* attempts. | ||
* @param {ExponentialBackoffParams} params - configuration parameters for | ||
* exponential backoff. | ||
* @param {number} initialDelay - the initial delay before any backoff is | ||
* applied | ||
* @param {number} failedConnectionAttempts - the number of connection attempts | ||
* that have previously failed | ||
* @returns {void} - set does not return | ||
*/ | ||
export function calculateRetryDelayFactor( | ||
params: ExponentialBackoffParams, | ||
initialDelay: number, | ||
failedConnectionAttempts: number, | ||
): number { | ||
return Math.min( | ||
initialDelay * Math.pow(params.backoffRate, failedConnectionAttempts), | ||
params.backoffLimit, | ||
); | ||
} | ||
export interface SarusClassParams { | ||
@@ -85,2 +112,3 @@ url: string; | ||
retryConnectionDelay?: boolean | number; | ||
exponentialBackoff?: ExponentialBackoffParams; | ||
storageType?: string; | ||
@@ -100,3 +128,4 @@ storageKey?: string; | ||
* @param {number} param0.retryProcessTimePeriod - An optional number for how long the time period between retrying to send a messgae to a WebSocket server should be | ||
* @param {number} param0.retryConnectionDelay - A parameter for the amount of time to delay a reconnection attempt by, in miliseconds. | ||
* @param {boolean|number} param0.retryConnectionDelay - An optional parameter for whether to delay WebSocket reconnection attempts by a time period. If true, the delay is 1000ms, otherwise it is the number passed. The default value when this parameter is undefined will be interpreted as 1000ms. | ||
* @param {ExponentialBackoffParams} param0.exponentialBackoff - An optional containing configuration for exponential backoff. If this parameter is undefined, exponential backoff is disabled. The minimum delay is determined by retryConnectionDelay. If retryConnectionDelay is set is false, this setting will not be in effect. | ||
* @param {string} param0.storageType - An optional string specifying the type of storage to use for persisting messages in the message queue | ||
@@ -115,2 +144,3 @@ * @param {string} param0.storageKey - An optional string specifying the key used to store the messages data against in sessionStorage/localStorage | ||
retryConnectionDelay: number; | ||
exponentialBackoff?: ExponentialBackoffParams; | ||
storageType: string; | ||
@@ -141,6 +171,24 @@ storageKey: string; | ||
* | ||
* this.reconnect() is called internally when automatic reconnection is | ||
* enabled, but can also be called by the user | ||
* When disconnected by the WebSocket itself (i.e., this.ws.onclose), | ||
* this.reconnect() is called automatically if reconnection is enabled. | ||
* this.reconnect() can also be called by the user, for example if | ||
* this.disconnect() was purposefully called and reconnection is desired. | ||
* | ||
* The current state is specified by the 'kind' property of state | ||
* Each state can have additional data contained in properties other than | ||
* 'kind'. Those properties might be unique to one state, or contained in | ||
* several states. To access a property, it might be necessary to narrow down | ||
* the 'kind' of state. | ||
* | ||
* The initial state is connecting, as a Sarus client tries to connect right | ||
* after the constructor wraps up. | ||
*/ | ||
state: "connecting" | "connected" | "disconnected" | "closed" = "connecting"; | ||
state: | ||
| { kind: "connecting"; failedConnectionAttempts: number } | ||
| { kind: "connected" } | ||
| { kind: "disconnected" } | ||
| { kind: "closed" } = { | ||
kind: "connecting", | ||
failedConnectionAttempts: 0, | ||
}; | ||
@@ -153,6 +201,7 @@ constructor(props: SarusClassParams) { | ||
protocols, | ||
eventListeners = DEFAULT_EVENT_LISTENERS_OBJECT, | ||
eventListeners, // = DEFAULT_EVENT_LISTENERS_OBJECT, | ||
reconnectAutomatically, | ||
retryProcessTimePeriod, // TODO - write a test case to check this | ||
retryConnectionDelay, | ||
exponentialBackoff, | ||
storageType = "memory", | ||
@@ -203,2 +252,9 @@ storageKey = "sarus", | ||
/* | ||
When a exponential backoff parameter object is provided, reconnection | ||
attemptions will be increasingly delayed by an exponential factor. | ||
This feature is disabled by default. | ||
*/ | ||
this.exponentialBackoff = exponentialBackoff; | ||
/* | ||
Sets the storage type for the messages in the message queue. By default | ||
@@ -314,16 +370,11 @@ it is an in-memory option, but can also be set as 'session' for | ||
*/ | ||
auditEventListeners(eventListeners: PartialEventListenersInterface) { | ||
const defaultEventListeners: EventListenersInterface = { | ||
open: [], | ||
message: [], | ||
error: [], | ||
close: [], | ||
auditEventListeners( | ||
eventListeners: PartialEventListenersInterface | undefined, | ||
) { | ||
return { | ||
open: eventListeners?.open || [], | ||
message: eventListeners?.message || [], | ||
error: eventListeners?.error || [], | ||
close: eventListeners?.close || [], | ||
}; | ||
const mergedEventListeners: EventListenersInterface = { | ||
...defaultEventListeners, | ||
...eventListeners, | ||
} as EventListenersInterface; // Type assertion added here | ||
return mergedEventListeners; | ||
} | ||
@@ -335,3 +386,6 @@ | ||
connect() { | ||
this.state = "connecting"; | ||
// If we aren't already connecting, we are now | ||
if (this.state.kind !== "connecting") { | ||
this.state = { kind: "connecting", failedConnectionAttempts: 0 }; | ||
} | ||
this.ws = new WebSocket(this.url, this.protocols); | ||
@@ -344,8 +398,27 @@ this.setBinaryType(); | ||
/** | ||
* Reconnects the WebSocket client based on the retryConnectionDelay setting. | ||
* Reconnects the WebSocket client based on the retryConnectionDelay and | ||
* ExponentialBackoffParam setting. | ||
*/ | ||
reconnect() { | ||
const self = this; | ||
const { retryConnectionDelay } = self; | ||
setTimeout(self.connect, retryConnectionDelay); | ||
const { retryConnectionDelay, exponentialBackoff } = self; | ||
// If we are already in a "connecting" state, we need to refer to the | ||
// current amount of connection attemps to correctly calculate the | ||
// exponential delay -- if exponential backoff is enabled. | ||
const failedConnectionAttempts = | ||
self.state.kind === "connecting" | ||
? self.state.failedConnectionAttempts | ||
: 0; | ||
// If no exponential backoff is enabled, retryConnectionDelay will | ||
// be scaled by a factor of 1 and it will stay the original value. | ||
const delay = exponentialBackoff | ||
? calculateRetryDelayFactor( | ||
exponentialBackoff, | ||
retryConnectionDelay, | ||
failedConnectionAttempts, | ||
) | ||
: retryConnectionDelay; | ||
setTimeout(self.connect, delay); | ||
} | ||
@@ -360,3 +433,3 @@ | ||
disconnect(overrideDisableReconnect?: boolean) { | ||
this.state = "disconnected"; | ||
this.state = { kind: "disconnected" }; | ||
const self = this; | ||
@@ -491,8 +564,23 @@ // We do this to prevent automatic reconnections; | ||
WS_EVENT_NAMES.forEach((eventName) => { | ||
self.ws[`on${eventName}`] = (e: Function) => { | ||
self.ws[`on${eventName}`] = (e: Event | CloseEvent | MessageEvent) => { | ||
self.eventListeners[eventName].forEach((f: Function) => f(e)); | ||
if (eventName === "open") { | ||
self.state = "connected"; | ||
self.state = { kind: "connected" }; | ||
} else if (eventName === "close" && self.reconnectAutomatically) { | ||
self.state = "closed"; | ||
const { state } = self; | ||
// If we have previously been "connecting", we carry over the amount | ||
// of failed connection attempts and add 1, since the current | ||
// connection attempt failed. We stay "connecting" instead of | ||
// "closed", since we've never been fully "connected" in the first | ||
// place. | ||
if (state.kind === "connecting") { | ||
self.state = { | ||
kind: "connecting", | ||
failedConnectionAttempts: state.failedConnectionAttempts + 1, | ||
}; | ||
} else { | ||
// If we were in a different state, we assume that our connection | ||
// freshly closed and have not made any failed connection attempts. | ||
self.state = { kind: "closed" }; | ||
} | ||
self.removeEventListeners(); | ||
@@ -499,0 +587,0 @@ self.reconnect(); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
126770
16
2163
465