rxdb
Version:
A local-first realtime NoSQL Database for JavaScript applications - https://rxdb.info/
267 lines (244 loc) • 8.22 kB
text/typescript
import { newRxError } from '../../rx-error.ts';
/**
* A minimal websocket client that reconnects with an
* exponential backoff when the connection is lost.
* Messages that are sent while the client is not connected,
* are queued and flushed as soon as the connection is open again.
*
* This replaces the unmaintained 'reconnecting-websocket' npm module
* which was the only dependency of the websocket replication.
*/
export type ReconnectingWebSocketOptions = {
/**
* The WebSocket implementation to use.
* (optional, default: the global WebSocket)
*/
WebSocket?: any;
/**
* Time in milliseconds to wait before the first reconnect attempt.
* (optional, [default=1000])
*/
minReconnectionDelay?: number;
/**
* Maximum time in milliseconds to wait between two reconnect attempts.
* (optional, [default=10000])
*/
maxReconnectionDelay?: number;
/**
* Factor the reconnection delay grows with on each failed attempt.
* (optional, [default=1.3])
*/
reconnectionDelayGrowFactor?: number;
/**
* Time in milliseconds a connection attempt has to open the connection.
* When it takes longer, the attempt is aborted and retried.
* (optional, [default=4000])
*/
connectionTimeout?: number;
/**
* Time in milliseconds a connection has to stay open
* before the retry counter is reset.
* (optional, [default=5000])
*/
minUptime?: number;
/**
* How many times the client tries to reconnect before it gives up.
* (optional, [default=Infinity])
*/
maxRetries?: number;
};
const DEFAULT_OPTIONS = {
minReconnectionDelay: 1000,
maxReconnectionDelay: 10000,
reconnectionDelayGrowFactor: 1.3,
connectionTimeout: 4000,
minUptime: 5000,
maxRetries: Infinity
};
export class ReconnectingWebSocket {
public onopen: ((event: any) => void) | null = null;
public onclose: ((event: any) => void) | null = null;
public onerror: ((event: any) => void) | null = null;
public onmessage: ((event: any) => void) | null = null;
private socket: any = null;
/**
* Amount of connection attempts that were started
* since the last working connection.
* Is -1 before the first attempt so that the
* initial connect does not have to wait.
*/
private retryCount = -1;
private shouldReconnect = true;
private isConnecting = false;
private messageQueue: any[] = [];
private connectionTimeoutId: any;
private uptimeTimeoutId: any;
private reconnectTimeoutId: any;
constructor(
public readonly url: string,
public readonly protocols: string | string[] = [],
public readonly options: ReconnectingWebSocketOptions = {}
) {
this.connect();
}
get readyState(): number {
return this.socket ? this.socket.readyState : 0;
}
/**
* Sends the data to the server.
* When the client is not connected, the data is queued
* and sent as soon as the connection is open again.
*/
send(data: any) {
if (this.socket && this.socket.readyState === 1) {
this.socket.send(data);
} else {
this.messageQueue.push(data);
}
}
/**
* Closes the connection and stops reconnecting.
*/
close(code: number = 1000, reason?: string) {
this.shouldReconnect = false;
this.clearTimeouts();
const socket = this.socket;
if (!socket) {
return;
}
this.removeHandlers(socket);
this.socket = null;
try {
socket.close(code, reason);
} catch (err) {
// the socket was not open, ignore
}
}
private getOption<K extends keyof typeof DEFAULT_OPTIONS>(key: K): number {
const value = (this.options as any)[key];
return typeof value === 'number' ? value : DEFAULT_OPTIONS[key];
}
private getNextDelay(): number {
if (this.retryCount < 1) {
return 0;
}
const delay = this.getOption('minReconnectionDelay') *
Math.pow(this.getOption('reconnectionDelayGrowFactor'), this.retryCount - 1);
return Math.min(delay, this.getOption('maxReconnectionDelay'));
}
private connect() {
if (
this.isConnecting ||
!this.shouldReconnect ||
this.retryCount >= this.getOption('maxRetries')
) {
return;
}
this.isConnecting = true;
this.retryCount = this.retryCount + 1;
this.reconnectTimeoutId = setTimeout(() => {
if (!this.shouldReconnect) {
this.isConnecting = false;
return;
}
const WebSocketCtor = this.options.WebSocket
? this.options.WebSocket
: (typeof WebSocket !== 'undefined' ? WebSocket : undefined);
const socket = new WebSocketCtor(this.url, this.protocols);
this.socket = socket;
this.isConnecting = false;
socket.onopen = (event: any) => this.handleOpen(socket, event);
socket.onmessage = (event: any) => {
if (this.onmessage) {
this.onmessage(event);
}
};
socket.onerror = (event: any) => this.handleError(event);
socket.onclose = (event: any) => this.handleClose(event);
this.connectionTimeoutId = setTimeout(
() => this.handleError({
type: 'error',
message: 'TIMEOUT',
error: newRxError('RC_WEBSOCKET_TIMEOUT', {
url: this.url,
args: {
connectionTimeout: this.getOption('connectionTimeout')
}
}),
target: this
}),
this.getOption('connectionTimeout')
);
}, this.getNextDelay());
}
private handleOpen(socket: any, event: any) {
clearTimeout(this.connectionTimeoutId);
/**
* Only reset the retry counter when the connection
* stayed open long enough. Otherwise a server that accepts and
* instantly drops connections would cause a reconnect loop
* without any delay.
*/
this.uptimeTimeoutId = setTimeout(() => {
this.retryCount = 0;
}, this.getOption('minUptime'));
const queue = this.messageQueue;
this.messageQueue = [];
queue.forEach(message => socket.send(message));
if (this.onopen) {
this.onopen(event);
}
}
private handleError(event: any) {
/**
* An error always breaks the current connection,
* so the socket is thrown away and a new one is created.
*/
const socket = this.socket;
this.clearTimeouts();
if (socket) {
this.removeHandlers(socket);
this.socket = null;
try {
socket.close();
} catch (err) {
// the socket was not open, ignore
}
}
if (this.onclose) {
this.onclose({
type: 'close',
code: 1000,
reason: '',
wasClean: false,
target: this
});
}
if (this.onerror) {
this.onerror(event);
}
this.connect();
}
private handleClose(event: any) {
this.clearTimeouts();
if (this.socket) {
this.removeHandlers(this.socket);
this.socket = null;
}
if (this.onclose) {
this.onclose(event);
}
this.connect();
}
private removeHandlers(socket: any) {
socket.onopen = null;
socket.onclose = null;
socket.onerror = null;
socket.onmessage = null;
}
private clearTimeouts() {
clearTimeout(this.connectionTimeoutId);
clearTimeout(this.uptimeTimeoutId);
clearTimeout(this.reconnectTimeoutId);
}
}