mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
c87d835533
Summary: Some WS-related code was touched in a recent PR to grist-core. This extends those changes to the rest of the codebase so that builds work again. Test Plan: N/A Reviewers: dsagal Reviewed By: dsagal Subscribers: dsagal Differential Revision: https://phab.getgrist.com/D4224
158 lines
4.4 KiB
TypeScript
158 lines
4.4 KiB
TypeScript
import WS from 'ws';
|
|
import { Socket as EIOSocket } from 'engine.io-client';
|
|
|
|
export interface GristClientSocketOptions {
|
|
headers?: Record<string, string>;
|
|
}
|
|
|
|
export class GristClientSocket {
|
|
// Exactly one of _wsSocket and _eioSocket will be set at any one time.
|
|
private _wsSocket: WS.WebSocket | WebSocket | undefined;
|
|
private _eioSocket: EIOSocket | undefined;
|
|
|
|
// Set to true if a WebSocket connection (in _wsSocket) was succesfully
|
|
// established. Errors from the underlying WebSocket are not forwarded to
|
|
// the client until that point, in case we end up downgrading to Engine.IO.
|
|
private _wsConnected: boolean = false;
|
|
|
|
private _messageHandler: null | ((data: string) => void);
|
|
private _openHandler: null | (() => void);
|
|
private _errorHandler: null | ((err: Error) => void);
|
|
private _closeHandler: null | (() => void);
|
|
|
|
constructor(private _url: string, private _options?: GristClientSocketOptions) {
|
|
this._createWSSocket();
|
|
}
|
|
|
|
public set onmessage(cb: null | ((data: string) => void)) {
|
|
this._messageHandler = cb;
|
|
}
|
|
|
|
public set onopen(cb: null | (() => void)) {
|
|
this._openHandler = cb;
|
|
}
|
|
|
|
public set onerror(cb: null | ((err: Error) => void)) {
|
|
this._errorHandler = cb;
|
|
}
|
|
|
|
public set onclose(cb: null | (() => void)) {
|
|
this._closeHandler = cb;
|
|
}
|
|
|
|
public close() {
|
|
if (this._wsSocket) {
|
|
this._wsSocket.close();
|
|
} else {
|
|
this._eioSocket!.close();
|
|
}
|
|
}
|
|
|
|
public send(data: string) {
|
|
if (this._wsSocket) {
|
|
this._wsSocket.send(data);
|
|
} else {
|
|
this._eioSocket!.send(data);
|
|
}
|
|
}
|
|
|
|
// pause(), resume(), and isOpen() are only used by tests and assume
|
|
// a WS.WebSocket transport.
|
|
public pause() {
|
|
(this._wsSocket as WS.WebSocket)?.pause();
|
|
}
|
|
|
|
public resume() {
|
|
(this._wsSocket as WS.WebSocket)?.resume();
|
|
}
|
|
|
|
public isOpen() {
|
|
return (this._wsSocket as WS.WebSocket)?.readyState === WS.OPEN;
|
|
}
|
|
|
|
private _createWSSocket() {
|
|
if (typeof WebSocket !== 'undefined') {
|
|
this._wsSocket = new WebSocket(this._url);
|
|
} else {
|
|
this._wsSocket = new WS(this._url, undefined, this._options);
|
|
}
|
|
this._wsSocket.onmessage = this._onWSMessage.bind(this);
|
|
this._wsSocket.onopen = this._onWSOpen.bind(this);
|
|
this._wsSocket.onerror = this._onWSError.bind(this);
|
|
this._wsSocket.onclose = this._onWSClose.bind(this);
|
|
}
|
|
|
|
private _destroyWSSocket() {
|
|
if (this._wsSocket) {
|
|
this._wsSocket.onmessage = null;
|
|
this._wsSocket.onopen = null;
|
|
this._wsSocket.onerror = null;
|
|
this._wsSocket.onclose = null;
|
|
this._wsSocket = undefined;
|
|
}
|
|
}
|
|
|
|
private _onWSMessage(event: WS.MessageEvent | MessageEvent<any>) {
|
|
// event.data is guaranteed to be a string here because we only send text frames.
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/message_event#event_properties
|
|
this._messageHandler?.(event.data);
|
|
}
|
|
|
|
private _onWSOpen() {
|
|
// The connection was established successfully. Any future events can now
|
|
// be forwarded to the client.
|
|
this._wsConnected = true;
|
|
this._openHandler?.();
|
|
}
|
|
|
|
private _onWSError(ev: Event) {
|
|
if (!this._wsConnected) {
|
|
// The WebSocket connection attempt failed. Switch to Engine.IO.
|
|
this._destroyWSSocket();
|
|
this._createEIOSocket();
|
|
return;
|
|
}
|
|
|
|
// WebSocket error events are deliberately void of information,
|
|
// see https://websockets.spec.whatwg.org/#eventdef-websocket-error,
|
|
// so we ignore the incoming event.
|
|
this._errorHandler?.(new Error("WebSocket error"));
|
|
}
|
|
|
|
private _onWSClose() {
|
|
this._closeHandler?.();
|
|
}
|
|
|
|
private _createEIOSocket() {
|
|
this._eioSocket = new EIOSocket(this._url, {
|
|
path: new URL(this._url).pathname,
|
|
addTrailingSlash: false,
|
|
transports: ['polling'],
|
|
upgrade: false,
|
|
extraHeaders: this._options?.headers,
|
|
withCredentials: true,
|
|
});
|
|
|
|
this._eioSocket.on('message', this._onEIOMessage.bind(this));
|
|
this._eioSocket.on('open', this._onEIOOpen.bind(this));
|
|
this._eioSocket.on('error', this._onEIOError.bind(this));
|
|
this._eioSocket.on('close', this._onEIOClose.bind(this));
|
|
}
|
|
|
|
private _onEIOMessage(data: string) {
|
|
this._messageHandler?.(data);
|
|
}
|
|
|
|
private _onEIOOpen() {
|
|
this._openHandler?.();
|
|
}
|
|
|
|
private _onEIOError(err: string | Error) {
|
|
this._errorHandler?.(typeof err === "string" ? new Error(err) : err);
|
|
}
|
|
|
|
private _onEIOClose() {
|
|
this._closeHandler?.();
|
|
}
|
|
}
|