* socket: Fork @iov/socket * socket: Remove nonces * socket: Update package.json * socket: Update README * socket: Remove tslint * socket: Fix lint warnings * scripts: Fork socketserver from IOV Core * root: Update NOTICE for socket * tendermint-rpc: Replace @iov/socket dependency with @cosmjs/socket * root: Update CI config for tendermint/socket * scripts: Add wait to tendermint start script * socket: Add coverage
204 lines
6.0 KiB
TypeScript
204 lines
6.0 KiB
TypeScript
import WebSocket from "isomorphic-ws";
|
|
|
|
function environmentIsNodeJs(): boolean {
|
|
return (
|
|
typeof process !== "undefined" &&
|
|
typeof process.versions !== "undefined" &&
|
|
typeof process.versions.node !== "undefined"
|
|
);
|
|
}
|
|
|
|
export interface SocketWrapperCloseEvent {
|
|
readonly wasClean: boolean;
|
|
readonly code: number;
|
|
}
|
|
|
|
export interface SocketWrapperErrorEvent {
|
|
// fields available in browsers
|
|
readonly isTrusted?: boolean;
|
|
|
|
// fields available in node
|
|
readonly type?: string;
|
|
readonly message?: string;
|
|
}
|
|
|
|
export interface SocketWrapperMessageEvent {
|
|
readonly data: string;
|
|
readonly type: string;
|
|
}
|
|
|
|
/**
|
|
* A thin wrapper around isomorphic-ws' WebSocket class that adds
|
|
* - constant message/error/open/close handlers
|
|
* - explict connection via a connect() method
|
|
* - type support for events
|
|
* - handling of corner cases in the open and close behaviour
|
|
*/
|
|
export class SocketWrapper {
|
|
public readonly connected: Promise<void>;
|
|
|
|
private connectedResolver: (() => void) | undefined;
|
|
private connectedRejecter: ((reason: any) => void) | undefined;
|
|
private socket: WebSocket | undefined;
|
|
private timeoutId: NodeJS.Timeout | undefined;
|
|
private closed = false;
|
|
private readonly url: string;
|
|
private readonly messageHandler: (event: SocketWrapperMessageEvent) => void;
|
|
private readonly errorHandler: (event: SocketWrapperErrorEvent) => void;
|
|
private readonly openHandler?: () => void;
|
|
private readonly closeHandler?: (event: SocketWrapperCloseEvent) => void;
|
|
private readonly timeout: number;
|
|
|
|
public constructor(
|
|
url: string,
|
|
messageHandler: (event: SocketWrapperMessageEvent) => void,
|
|
errorHandler: (event: SocketWrapperErrorEvent) => void,
|
|
openHandler?: () => void,
|
|
closeHandler?: (event: SocketWrapperCloseEvent) => void,
|
|
timeout = 10_000,
|
|
) {
|
|
this.connected = new Promise((resolve, reject) => {
|
|
this.connectedResolver = resolve;
|
|
this.connectedRejecter = reject;
|
|
});
|
|
|
|
this.url = url;
|
|
this.messageHandler = messageHandler;
|
|
this.errorHandler = errorHandler;
|
|
this.openHandler = openHandler;
|
|
this.closeHandler = closeHandler;
|
|
this.timeout = timeout;
|
|
}
|
|
|
|
/**
|
|
* returns a promise that resolves when connection is open
|
|
*/
|
|
public connect(): void {
|
|
const socket = new WebSocket(this.url);
|
|
|
|
socket.onerror = (error) => {
|
|
this.clearTimeout();
|
|
if (this.errorHandler) {
|
|
this.errorHandler(error);
|
|
}
|
|
};
|
|
socket.onmessage = (messageEvent) => {
|
|
this.messageHandler({
|
|
type: messageEvent.type,
|
|
data: messageEvent.data as string,
|
|
});
|
|
};
|
|
socket.onopen = (_) => {
|
|
this.clearTimeout();
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
this.connectedResolver!();
|
|
|
|
if (this.openHandler) {
|
|
this.openHandler();
|
|
}
|
|
};
|
|
socket.onclose = (closeEvent) => {
|
|
this.closed = true;
|
|
if (this.closeHandler) {
|
|
this.closeHandler(closeEvent);
|
|
}
|
|
};
|
|
|
|
const started = Date.now();
|
|
this.timeoutId = setTimeout(() => {
|
|
socket.onmessage = () => 0;
|
|
socket.onerror = () => 0;
|
|
socket.onopen = () => 0;
|
|
socket.onclose = () => 0;
|
|
socket.close();
|
|
this.socket = undefined;
|
|
|
|
const elapsed = Math.floor(Date.now() - started);
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
this.connectedRejecter!(`Connection attempt timed out after ${elapsed} ms`);
|
|
}, this.timeout);
|
|
|
|
this.socket = socket;
|
|
}
|
|
|
|
/**
|
|
* Closes an established connection and aborts other connection states
|
|
*/
|
|
public disconnect(): void {
|
|
if (!this.socket) {
|
|
throw new Error("Socket undefined. This must be called after connecting.");
|
|
}
|
|
|
|
this.clearTimeout();
|
|
|
|
switch (this.socket.readyState) {
|
|
case WebSocket.OPEN:
|
|
this.socket.close(1000 /* Normal Closure */);
|
|
break;
|
|
case WebSocket.CLOSED:
|
|
// nothing to be done
|
|
break;
|
|
case WebSocket.CONNECTING:
|
|
// imitate missing abort API
|
|
this.socket.onopen = () => 0;
|
|
this.socket.onclose = () => 0;
|
|
this.socket.onerror = () => 0;
|
|
this.socket.onmessage = () => 0;
|
|
this.socket = undefined;
|
|
if (this.closeHandler) {
|
|
this.closeHandler({ wasClean: false, code: 4001 });
|
|
}
|
|
break;
|
|
case WebSocket.CLOSING:
|
|
// already closing. Let it proceed
|
|
break;
|
|
default:
|
|
throw new Error(`Unknown readyState: ${this.socket.readyState}`);
|
|
}
|
|
}
|
|
|
|
public async send(data: string): Promise<void> {
|
|
return new Promise((resolve, reject) => {
|
|
if (!this.socket) {
|
|
throw new Error("Socket undefined. This must be called after connecting.");
|
|
}
|
|
|
|
if (this.closed) {
|
|
throw new Error("Socket was closed, so no data can be sent anymore.");
|
|
}
|
|
|
|
// this exception should be thrown by send() automatically according to
|
|
// https://developer.mozilla.org/de/docs/Web/API/WebSocket#send() but it does not work in browsers
|
|
if (this.socket.readyState !== WebSocket.OPEN) {
|
|
throw new Error("Websocket is not open");
|
|
}
|
|
|
|
if (environmentIsNodeJs()) {
|
|
this.socket.send(data, (err) => (err ? reject(err) : resolve()));
|
|
} else {
|
|
// Browser websocket send method does not accept a callback
|
|
this.socket.send(data);
|
|
resolve();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Clears the timeout function, such that no timeout error will be raised anymore. This should be
|
|
* called when the connection is established, a connection error occurred or the socket is disconnected.
|
|
*
|
|
* This method must not be called before `connect()`.
|
|
* This method is idempotent.
|
|
*/
|
|
private clearTimeout(): void {
|
|
if (!this.timeoutId) {
|
|
throw new Error(
|
|
"Timeout ID not set. This should not happen and usually means connect() was not called.",
|
|
);
|
|
}
|
|
|
|
// Note: do not unset this.timeoutId to allow multiple calls to this function
|
|
clearTimeout(this.timeoutId);
|
|
}
|
|
}
|