First commit.
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
import { PiPedalStateError } from './PiPedalError';
|
||||
import Utility from './Utility';
|
||||
|
||||
export type MessageHandler = (header: PiPedalMessageHeader, body: any | null) => void;
|
||||
export type ErrorHandler = (message: string, exception?: Error) => void;
|
||||
export type ReconnectHandler = () => void;
|
||||
export type ReconnectingHandler = (retry: number, maxRetries: number) => void;
|
||||
|
||||
const MAX_RETRIES = 6;
|
||||
const MAX_RETRY_TIME = 90*1000;
|
||||
|
||||
export type PiPedalMessageHeader = {
|
||||
replyTo?: number;
|
||||
reply?: number;
|
||||
message: string;
|
||||
}
|
||||
type ReplyHandler = (header: PiPedalMessageHeader, body: any | null) => void;
|
||||
|
||||
|
||||
|
||||
|
||||
class PiPedalSocket {
|
||||
onMessageReceived: MessageHandler;
|
||||
onError: ErrorHandler;
|
||||
onReconnecting: ReconnectingHandler;
|
||||
onReconnect: ReconnectHandler;
|
||||
|
||||
socket?: WebSocket;
|
||||
nextResponseCode: number = 0;
|
||||
url: string;
|
||||
retrying: boolean = false;
|
||||
retryCount: number = 0;
|
||||
retryDelay: number = 0;
|
||||
totalRetryDelay: number = 0;
|
||||
|
||||
constructor(
|
||||
url: string,
|
||||
onMessageReceived: MessageHandler,
|
||||
onError: ErrorHandler,
|
||||
onReconnecting: ReconnectingHandler,
|
||||
onReconnect: ReconnectHandler) {
|
||||
this.url = url;
|
||||
this.onMessageReceived = onMessageReceived;
|
||||
this.onError = onError;
|
||||
this.onReconnecting = onReconnecting;
|
||||
this.onReconnect = onReconnect;
|
||||
}
|
||||
|
||||
handleOpen(event: Event): any {
|
||||
|
||||
}
|
||||
|
||||
sendInternal_(json: string) {
|
||||
if (!this.retrying) {
|
||||
this.socket?.send(json);
|
||||
}
|
||||
}
|
||||
send(message: string, jsonObject?: any) {
|
||||
let msg: any;
|
||||
if (jsonObject === undefined) {
|
||||
msg = [{ message: message }];
|
||||
} else {
|
||||
msg = [{ message: message }, jsonObject];
|
||||
}
|
||||
let json = JSON.stringify(msg);
|
||||
this.sendInternal_(json);
|
||||
}
|
||||
reply(replyTo: number, message: string, jsonObject?: any) {
|
||||
if (replyTo !== -1) {
|
||||
let msg: any;
|
||||
if (jsonObject === undefined) {
|
||||
msg = [{ reply: replyTo, message: message }];
|
||||
} else {
|
||||
msg = [{ reply: replyTo, message: message }, jsonObject];
|
||||
}
|
||||
let json = JSON.stringify(msg);
|
||||
this.sendInternal_(json);
|
||||
}
|
||||
}
|
||||
|
||||
_nextResponseCode(): number {
|
||||
return ++this.nextResponseCode;
|
||||
}
|
||||
|
||||
_replyMap: Map<number, ReplyHandler> = new Map<number, ReplyHandler>();
|
||||
|
||||
_discardReplyReservations()
|
||||
{
|
||||
// it's ok. All pending reservations disappear into the GC.
|
||||
this._replyMap = new Map<number, ReplyHandler>();
|
||||
}
|
||||
|
||||
|
||||
_addReservation(replyCode: number, handler: ReplyHandler): void {
|
||||
this._replyMap.set(replyCode, handler);
|
||||
}
|
||||
|
||||
request<Type = any>(message_: string, requestArgs?: any): Promise<Type> {
|
||||
let responseCode = this._nextResponseCode();
|
||||
|
||||
return new Promise<Type>((resolve, reject) => {
|
||||
try {
|
||||
this._addReservation(responseCode, (header: PiPedalMessageHeader, jsonObject?: any) => {
|
||||
if (header.message === "error") {
|
||||
reject(jsonObject + "");
|
||||
} else {
|
||||
resolve(jsonObject as Type);
|
||||
}
|
||||
});
|
||||
let msg: any;
|
||||
if (requestArgs !== undefined) {
|
||||
msg = [{ message: message_, replyTo: responseCode }, requestArgs];
|
||||
} else {
|
||||
msg = [{ message: message_, replyTo: responseCode }];
|
||||
}
|
||||
let jsonMessage = JSON.stringify(msg);
|
||||
this.sendInternal_(jsonMessage);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
handleMessage(event: MessageEvent<string>): any {
|
||||
try {
|
||||
let message: any = JSON.parse(event.data);
|
||||
if (!Array.isArray(message)) {
|
||||
throw new PiPedalStateError("Invalid message received from server.");
|
||||
}
|
||||
let header = message[0] as PiPedalMessageHeader;
|
||||
let body = undefined;
|
||||
if (message.length === 2) {
|
||||
body = message[1];
|
||||
}
|
||||
if (header.reply !== undefined) {
|
||||
let handler = this._replyMap.get(header.reply);
|
||||
if (handler) {
|
||||
handler(header, body);
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
if (header.message === "error") {
|
||||
throw new PiPedalStateError("Server error: " + body);
|
||||
}
|
||||
this.onMessageReceived(header, body);
|
||||
}
|
||||
} catch (error) {
|
||||
if (this.onError) {
|
||||
this.onError("Invalid server response. " + error, error);
|
||||
} else {
|
||||
throw new PiPedalStateError("Invalid server response.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleError(_event: Event): any {
|
||||
if (this.onError) {
|
||||
this.onError("Server connection lost.");
|
||||
} else {
|
||||
throw new PiPedalStateError("Server connection lost.");
|
||||
}
|
||||
}
|
||||
handleClose(_event: any): any {
|
||||
if (this.retrying) {
|
||||
// treat this as a fatal error.
|
||||
if (this.onError) {
|
||||
this.onError("Server connection lost.");
|
||||
} else {
|
||||
throw new PiPedalStateError("Server connection closed.");
|
||||
}
|
||||
return;
|
||||
|
||||
}
|
||||
this._discardReplyReservations();
|
||||
this.retrying = true;
|
||||
this.retryCount = 0;
|
||||
this.socket = undefined;
|
||||
this.retryDelay = 10000;
|
||||
this.totalRetryDelay = 0;
|
||||
|
||||
this.reconnect();
|
||||
}
|
||||
|
||||
reconnect() {
|
||||
|
||||
if (this.socket)
|
||||
{
|
||||
this.close();
|
||||
}
|
||||
|
||||
this.onReconnecting(this.retryCount,MAX_RETRIES);
|
||||
++this.retryCount;
|
||||
|
||||
this.connectInternal_()
|
||||
.then((socket) => {
|
||||
this.socket = socket;
|
||||
this.retrying = false;
|
||||
this.onReconnect();
|
||||
})
|
||||
.catch(error => {
|
||||
if (this.totalRetryDelay === MAX_RETRY_TIME)
|
||||
{
|
||||
this.onError("Server connection lost.");
|
||||
return;
|
||||
} else {
|
||||
this.totalRetryDelay += this.retryDelay;
|
||||
Utility.delay(this.retryDelay).then(() => this.reconnect());
|
||||
this.retryDelay *= 2;
|
||||
if (this.retryDelay > 5000) this.retryDelay = 5000;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
close(): void {
|
||||
try {
|
||||
if (this.socket)
|
||||
{
|
||||
this.socket.onclose = null;
|
||||
this.socket.onerror = null;
|
||||
this.socket.onmessage = null;
|
||||
this.socket.onopen = null;
|
||||
this.socket?.close();
|
||||
}
|
||||
} catch (ignored)
|
||||
{
|
||||
|
||||
}
|
||||
this.socket = undefined;
|
||||
}
|
||||
connectInternal_(): Promise<WebSocket> {
|
||||
return new Promise<WebSocket>((resolve, reject) => {
|
||||
let ws = new WebSocket(this.url);
|
||||
let self = this;
|
||||
|
||||
ws.onmessage = this.handleMessage.bind(this);
|
||||
ws.onclose = (event: Event) => {
|
||||
ws.onclose = null;
|
||||
ws.onerror = null;
|
||||
reject("Can't connect to server.");
|
||||
};
|
||||
ws.onerror = (event: Event) => {
|
||||
ws.onclose = null;
|
||||
ws.onerror = null;
|
||||
reject("Can't connect to server.");
|
||||
};
|
||||
ws.onopen = (event: Event) => {
|
||||
ws.onerror = self.handleError.bind(self);
|
||||
ws.onclose = self.handleClose.bind(self);
|
||||
ws.onopen = null;
|
||||
resolve(ws);
|
||||
};
|
||||
});
|
||||
}
|
||||
connect(): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
|
||||
this.connectInternal_()
|
||||
.then((socket) => {
|
||||
this.socket = socket;
|
||||
resolve();
|
||||
})
|
||||
.catch((reason) => {
|
||||
reject(reason);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default PiPedalSocket;
|
||||
Reference in New Issue
Block a user