@ -0,0 +1,11 @@ | |||||
{ | |||||
"name": "@autoplex/websocket-client", | |||||
"version": "0.0.0", | |||||
"main": "dist/lib/index.js", | |||||
"types": "dist/typings/index.d.ts", | |||||
"license": "MIT", | |||||
"scripts": { | |||||
"build": "yarn run clean && tsc", | |||||
"clean": "rimraf ./dist" | |||||
} | |||||
} |
@ -0,0 +1,286 @@ | |||||
import { IPendingRequest, IWebSocketRequest, IWebSocketResponse } from "./schema"; | |||||
export abstract class WebSocketClient | |||||
{ | |||||
/** | |||||
* The URL to connect to | |||||
*/ | |||||
protected readonly WEBSOCKET_URL: string; | |||||
/** | |||||
* Indicate if the websocket is ready | |||||
*/ | |||||
protected isReady: boolean = false; | |||||
/** | |||||
* Indicate if the websocket client is currently running | |||||
*/ | |||||
protected running: boolean = false; | |||||
/** | |||||
* The websocket instance | |||||
*/ | |||||
protected socket: WebSocket|null = null; | |||||
/** | |||||
* Store the timestamp of the most recent connection attempt | |||||
*/ | |||||
#lastConnectAttempt: number = 0; | |||||
/** | |||||
* Maintain a mapping of pending requests | |||||
*/ | |||||
#pendingRequests: { [requestId: number]: IPendingRequest } = {}; | |||||
/** | |||||
* Store the next request ID | |||||
*/ | |||||
#requestId: number = 0; | |||||
/** | |||||
* The auth state change event handler | |||||
*/ | |||||
protected onAuthStateChanged: (() => void)|null = null; | |||||
public constructor(url: string) { | |||||
this.WEBSOCKET_URL = url; | |||||
this.setup(); | |||||
this.run(); | |||||
} | |||||
// Overridable --------------------------------------------------------------------------------- | |||||
/** | |||||
* Setup the socket | |||||
*/ | |||||
protected abstract setup(): void; | |||||
/** | |||||
* Get the app's current auth JWT | |||||
*/ | |||||
protected abstract jwt(): string|null; | |||||
/** | |||||
* Forget the JWT and log out of the app | |||||
*/ | |||||
protected abstract forgetJwt(): void; | |||||
// Websocket Service Interface ----------------------------------------------------------------- | |||||
/** | |||||
* Connect to the server | |||||
*/ | |||||
public async run() { | |||||
if (this.running) { | |||||
return; | |||||
} | |||||
this.running = true; | |||||
while (this.running) { | |||||
this.close(); | |||||
await this.waitForAuthReady(); | |||||
try { | |||||
await this.connect(); | |||||
await this.authenticate(); | |||||
await this.exec(); | |||||
} catch(e) { | |||||
continue; | |||||
} | |||||
} | |||||
this.close(); | |||||
} | |||||
/** | |||||
* Close the connection if it exists | |||||
*/ | |||||
public async close() { | |||||
this.isReady = false; | |||||
for (let requestId of Object.keys(this.#pendingRequests)) { | |||||
this.#pendingRequests[<any>requestId].reject(new Error("closed")); | |||||
delete this.#pendingRequests[<any>requestId]; | |||||
} | |||||
if (this.socket === null) { | |||||
return; | |||||
} | |||||
this.socket.onclose = null; | |||||
this.socket.onerror = null; | |||||
this.socket.onmessage = null; | |||||
this.socket.close(); | |||||
} | |||||
// Connection Handling ------------------------------------------------------------------------- | |||||
/** | |||||
* If authentication info isn't in the store, wait for it | |||||
*/ | |||||
protected async waitForAuthReady() { | |||||
while (!this.jwt()) { | |||||
console.log("Waiting for auth state change..."); | |||||
await new Promise<void>(resolve => this.onAuthStateChanged = resolve); | |||||
} | |||||
} | |||||
/** | |||||
* Connect to the server | |||||
*/ | |||||
protected connect() { | |||||
return new Promise<void>((resolve, reject) => { | |||||
let timeout = setTimeout(() => { | |||||
console.log("Connecting..."); | |||||
this.#lastConnectAttempt = Date.now(); | |||||
this.socket = new WebSocket(this.WEBSOCKET_URL); | |||||
this.socket.onopen = () => resolve(); | |||||
this.socket.onerror = reject; | |||||
this.socket.onclose = reject; | |||||
}, this.#lastConnectAttempt - Date.now() + 5000); | |||||
this.onAuthStateChanged = () => { | |||||
clearTimeout(timeout); | |||||
reject(); | |||||
}; | |||||
}); | |||||
} | |||||
/** | |||||
* Authenticate the connection | |||||
*/ | |||||
protected authenticate() { | |||||
return new Promise<void>((resolve, reject) => { | |||||
console.log("Authenticating..."); | |||||
let timeout = setTimeout(reject, 5000); | |||||
this.onAuthStateChanged = this.socket!.onerror = this.socket!.onclose = () => { | |||||
clearTimeout(timeout); | |||||
reject(); | |||||
} | |||||
this.socket!.onmessage = (event) => { | |||||
clearTimeout(timeout); | |||||
if (event.data !== "true") { | |||||
this.forgetJwt(); | |||||
reject(); | |||||
return; | |||||
} | |||||
resolve(); | |||||
}; | |||||
this.socket!.send(this.jwt() ?? ""); | |||||
}); | |||||
} | |||||
/** | |||||
* Connection established. Wait for close | |||||
*/ | |||||
protected exec() { | |||||
return new Promise<void>((resolve, reject) => { | |||||
console.log("Connected"); | |||||
this.isReady = true; | |||||
this.onAuthStateChanged = () => reject(); | |||||
this.socket!.onclose = () => resolve(); | |||||
this.socket!.onerror = () => reject(); | |||||
this.socket!.onmessage = event => this.onMessage(event.data); | |||||
}); | |||||
} | |||||
/** | |||||
* Get the next request Id | |||||
*/ | |||||
#nextRequestId() { | |||||
this.#requestId = (this.#requestId + 1) % Number.MAX_SAFE_INTEGER; | |||||
return this.#requestId; | |||||
} | |||||
/** | |||||
* Send a payload to the websocket server | |||||
*/ | |||||
#send(payload: any) { | |||||
this.socket!.send(JSON.stringify(payload)); | |||||
} | |||||
// Event Handling ------------------------------------------------------------------------------ | |||||
/** | |||||
* Invoked when a message is received by the socket | |||||
*/ | |||||
protected onMessage(data: string) { | |||||
let parsed: IWebSocketRequest | IWebSocketResponse; | |||||
try { | |||||
parsed = JSON.parse(data); | |||||
} catch(e) { | |||||
console.warn("Failed to parse JSON response"); | |||||
return; | |||||
} | |||||
if (parsed.type === "request") { | |||||
this.handleRequest(<IWebSocketRequest><any>parsed); // WAT!?!? | |||||
} else { | |||||
this.handleResponse(<IWebSocketResponse><any>parsed); // WHY?!?! | |||||
} | |||||
} | |||||
/** | |||||
* Handle an incoming request from the server | |||||
*/ | |||||
protected handleRequest(request: IWebSocketRequest) { | |||||
} | |||||
/** | |||||
* Handle an incoming response from the server | |||||
*/ | |||||
protected handleResponse(response: IWebSocketResponse) { | |||||
if (this.#pendingRequests[response.requestId] === undefined) { | |||||
return; | |||||
} | |||||
let pendingRequest = this.#pendingRequests[response.requestId]; | |||||
pendingRequest.resolve(response.payload); | |||||
} | |||||
// Public Interface ---------------------------------------------------------------------------- | |||||
/** | |||||
* Notify the socket that the authentication state of the app has changed | |||||
*/ | |||||
public authStateChanged() { | |||||
if (this.onAuthStateChanged === null) { | |||||
return; | |||||
} | |||||
this.onAuthStateChanged(); | |||||
} | |||||
/** | |||||
* Send a payload to the server | |||||
*/ | |||||
public send(method: string, payload?: any) { | |||||
this.#send({ | |||||
requestId: this.#nextRequestId(), | |||||
method, | |||||
payload | |||||
}); | |||||
} | |||||
/** | |||||
* Request info from the server | |||||
*/ | |||||
public request<T>(method: string, payload?: any) { | |||||
const requestId = this.#nextRequestId(); | |||||
return new Promise<T>((resolve, reject) => { | |||||
let timeout = setTimeout(() => { | |||||
reject(new Error("timeout")); | |||||
delete this.#pendingRequests[requestId]; | |||||
}, 5000); | |||||
this.#pendingRequests[requestId] = { | |||||
resolve: (data: T) => { | |||||
clearTimeout(timeout); | |||||
delete this.#pendingRequests[requestId]; | |||||
resolve(data); | |||||
}, | |||||
reject: (error: any) => { | |||||
clearTimeout(timeout); | |||||
delete this.#pendingRequests[requestId]; | |||||
reject(error); | |||||
} | |||||
}; | |||||
this.#send({ | |||||
requestId, | |||||
method, | |||||
payload | |||||
}); | |||||
}); | |||||
} | |||||
} |
@ -0,0 +1,2 @@ | |||||
export * from "./schema"; | |||||
export * from "./WebSocketClient"; |
@ -0,0 +1,18 @@ | |||||
export interface IWebSocketRequest { | |||||
type: "request", | |||||
requestId: number, | |||||
method: string, | |||||
payload: any | |||||
} | |||||
export interface IWebSocketResponse { | |||||
type : "response", | |||||
requestId: number, | |||||
payload : any | |||||
} | |||||
export interface IPendingRequest { | |||||
resolve(payload?: any): void, | |||||
reject(error: Error|string): void | |||||
} |
@ -0,0 +1,9 @@ | |||||
{ | |||||
"extends": "../../tsconfig.package.json", | |||||
"compilerOptions": { | |||||
"target": "esnext", | |||||
"module": "esnext", | |||||
"outDir": "./dist/lib", | |||||
"declarationDir": "./dist/typings" | |||||
} | |||||
} |
@ -0,0 +1,122 @@ | |||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. | |||||
# yarn lockfile v1 | |||||
"@types/jsonwebtoken@^8.5.1": | |||||
version "8.5.1" | |||||
resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#56958cb2d80f6d74352bd2e501a018e2506a8a84" | |||||
integrity sha512-rNAPdomlIUX0i0cg2+I+Q1wOUr531zHBQ+cV/28PJ39bSPKjahatZZ2LMuhiguETkCgLVzfruw/ZvNMNkKoSzw== | |||||
dependencies: | |||||
"@types/node" "*" | |||||
"@types/node@*": | |||||
version "15.12.2" | |||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-15.12.2.tgz#1f2b42c4be7156ff4a6f914b2fb03d05fa84e38d" | |||||
integrity sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww== | |||||
"@types/ws@^7.4.5": | |||||
version "7.4.5" | |||||
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.5.tgz#8ff0f7efcd8fea19f51f9dd66cb8b498d172a752" | |||||
integrity sha512-8mbDgtc8xpxDDem5Gwj76stBDJX35KQ3YBoayxlqUQcL5BZUthiqP/VQ4PQnLHqM4PmlbyO74t98eJpURO+gPA== | |||||
dependencies: | |||||
"@types/node" "*" | |||||
buffer-equal-constant-time@1.0.1: | |||||
version "1.0.1" | |||||
resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" | |||||
integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= | |||||
ecdsa-sig-formatter@1.0.11: | |||||
version "1.0.11" | |||||
resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" | |||||
integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== | |||||
dependencies: | |||||
safe-buffer "^5.0.1" | |||||
jsonwebtoken@^8.5.1: | |||||
version "8.5.1" | |||||
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" | |||||
integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== | |||||
dependencies: | |||||
jws "^3.2.2" | |||||
lodash.includes "^4.3.0" | |||||
lodash.isboolean "^3.0.3" | |||||
lodash.isinteger "^4.0.4" | |||||
lodash.isnumber "^3.0.3" | |||||
lodash.isplainobject "^4.0.6" | |||||
lodash.isstring "^4.0.1" | |||||
lodash.once "^4.0.0" | |||||
ms "^2.1.1" | |||||
semver "^5.6.0" | |||||
jwa@^1.4.1: | |||||
version "1.4.1" | |||||
resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" | |||||
integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== | |||||
dependencies: | |||||
buffer-equal-constant-time "1.0.1" | |||||
ecdsa-sig-formatter "1.0.11" | |||||
safe-buffer "^5.0.1" | |||||
jws@^3.2.2: | |||||
version "3.2.2" | |||||
resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" | |||||
integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== | |||||
dependencies: | |||||
jwa "^1.4.1" | |||||
safe-buffer "^5.0.1" | |||||
lodash.includes@^4.3.0: | |||||
version "4.3.0" | |||||
resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" | |||||
integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8= | |||||
lodash.isboolean@^3.0.3: | |||||
version "3.0.3" | |||||
resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" | |||||
integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY= | |||||
lodash.isinteger@^4.0.4: | |||||
version "4.0.4" | |||||
resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" | |||||
integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M= | |||||
lodash.isnumber@^3.0.3: | |||||
version "3.0.3" | |||||
resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" | |||||
integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w= | |||||
lodash.isplainobject@^4.0.6: | |||||
version "4.0.6" | |||||
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" | |||||
integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= | |||||
lodash.isstring@^4.0.1: | |||||
version "4.0.1" | |||||
resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" | |||||
integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= | |||||
lodash.once@^4.0.0: | |||||
version "4.1.1" | |||||
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" | |||||
integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= | |||||
ms@^2.1.1: | |||||
version "2.1.3" | |||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" | |||||
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== | |||||
safe-buffer@^5.0.1: | |||||
version "5.2.1" | |||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" | |||||
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== | |||||
semver@^5.6.0: | |||||
version "5.7.1" | |||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" | |||||
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== | |||||
ws@^7.5.0: | |||||
version "7.5.0" | |||||
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.0.tgz#0033bafea031fb9df041b2026fc72a571ca44691" | |||||
integrity sha512-6ezXvzOZupqKj4jUqbQ9tXuJNo+BR2gU8fFRk3XCP3e0G6WT414u5ELe6Y0vtp7kmSJ3F7YWObSNr1ESsgi4vw== |
@ -0,0 +1,20 @@ | |||||
{ | |||||
"name": "@autoplex/websocket-server", | |||||
"version": "0.0.0", | |||||
"main": "dist/lib/index.js", | |||||
"types": "dist/typings/index.d.ts", | |||||
"license": "MIT", | |||||
"scripts": { | |||||
"build": "yarn run clean && tsc", | |||||
"clean": "rimraf ./dist" | |||||
}, | |||||
"devDependencies": { | |||||
"@autoplex/microservice": "^0.0.0", | |||||
"@types/jsonwebtoken": "^8.5.1", | |||||
"@types/ws": "^7.4.5" | |||||
}, | |||||
"dependencies": { | |||||
"jsonwebtoken": "^8.5.1", | |||||
"ws": "^7.5.0" | |||||
} | |||||
} |
@ -0,0 +1,168 @@ | |||||
import { InternalService, Microservice } from "@autoplex/microservice"; | |||||
import { IncomingMessage } from "http"; | |||||
import WebSocket, { Server } from "ws"; | |||||
import jsonwebtoken from "jsonwebtoken"; | |||||
import { IWebSocketRequest, IWebSocketResponse } from "./schema"; | |||||
export abstract class WebSocketServerService<M extends Microservice> extends InternalService<M> | |||||
{ | |||||
/** | |||||
* The list of active client connections | |||||
*/ | |||||
protected connections: WebSocket[] = []; | |||||
/** | |||||
* The websocket server instance | |||||
*/ | |||||
protected server!: Server; | |||||
/** | |||||
* The list of registered methods | |||||
*/ | |||||
#methods: { [method: string]: (payload?: any) => Promise<any>|any } = {}; | |||||
// Overridable --------------------------------------------------------------------------------- | |||||
/** | |||||
* The application key for the application | |||||
*/ | |||||
protected abstract get appKey(): string; | |||||
/** | |||||
* Install methods into the websocket service | |||||
*/ | |||||
protected installMethods() { | |||||
// no-op | |||||
} | |||||
// Service Methods ----------------------------------------------------------------------------- | |||||
/** | |||||
* Boot the service | |||||
*/ | |||||
public override async boot() { | |||||
this.installMethods(); | |||||
this.server = new Server({ port: 3250 }); | |||||
this.server.on("connection", this.acceptConnection.bind(this)); | |||||
} | |||||
/** | |||||
* Shutdown the service | |||||
*/ | |||||
public override async shutdown() { | |||||
this.server.close(); | |||||
for (let socket of this.connections) { | |||||
socket.close(); | |||||
} | |||||
} | |||||
/** | |||||
* Accept the pending websocket connection | |||||
*/ | |||||
protected acceptConnection(socket: WebSocket, request: IncomingMessage) { | |||||
let timeout = setTimeout(() => socket.close.bind(socket), 5000); | |||||
socket.once("message", async (data) => { | |||||
clearTimeout(timeout); | |||||
let cookies = this.parseCookies(request); | |||||
let token = data + '.' + (cookies["jwt_signature"] ?? ""); | |||||
try { | |||||
await this.authenticate(token); | |||||
socket.send("true"); | |||||
socket.on("close", () => this.onClose(socket)); | |||||
socket.on("error", (error) => this.onError(socket, error)); | |||||
socket.on("message", (data) => this.onMessage(socket, <string>data)); | |||||
this.connections.push(socket); | |||||
} catch(e) { | |||||
socket.send("false"); | |||||
socket.close(); | |||||
} | |||||
}); | |||||
} | |||||
/** | |||||
* Verify the provided JWT | |||||
*/ | |||||
protected async authenticate(token: string) { | |||||
await new Promise((resolve, reject) => { | |||||
jsonwebtoken.verify(token, this.appKey, (err, decoded) => { | |||||
if (err) { | |||||
reject(err); | |||||
return; | |||||
} | |||||
resolve(decoded); | |||||
}); | |||||
}); | |||||
} | |||||
/** | |||||
* Parse the received cookies from the request | |||||
* https://stackoverflow.com/a/3409200/16243951 | |||||
*/ | |||||
protected parseCookies(request: IncomingMessage) { | |||||
let cookies: { [cookie: string]: string } = {}; | |||||
for (let cookie of request.headers.cookie?.split(';') ?? []) { | |||||
let parts = cookie.split('='); | |||||
cookies[parts.shift()!.trim()] = decodeURI(parts.join('=')); | |||||
} | |||||
return cookies; | |||||
} | |||||
/** | |||||
* Install a method into the server service | |||||
*/ | |||||
protected installMethod(name: string, method: (payload?: any) => Promise<any>|any) { | |||||
if (this.#methods[name] !== undefined) { | |||||
throw new Error("Attempted to install method with duplicate name in websocket service"); | |||||
} | |||||
this.#methods[name] = method; | |||||
} | |||||
// Event Handling ------------------------------------------------------------------------------ | |||||
/** | |||||
* Handle an incoming request | |||||
*/ | |||||
protected async handleRequest(socket: WebSocket, request: IWebSocketRequest) { | |||||
if (this.#methods[request.method] === undefined) { | |||||
console.warn(`Requested unknown method: '${request.method}' with payload:`, request.payload); | |||||
return; | |||||
} | |||||
let result = await this.#methods[request.method](request.payload); | |||||
socket.send(JSON.stringify(<IWebSocketResponse>{ | |||||
type: "response", | |||||
requestId: request.requestId, | |||||
payload: JSON.stringify(result) | |||||
})); | |||||
} | |||||
/** | |||||
* Invoked when a connection closes | |||||
*/ | |||||
protected onClose(socket: WebSocket) { | |||||
let index = this.connections.indexOf(socket); | |||||
this.connections.splice(index, 1); | |||||
} | |||||
/** | |||||
* Invoked when a connection encounters an error | |||||
*/ | |||||
protected onError(socket: WebSocket, error: Error) { | |||||
this.onClose(socket); | |||||
} | |||||
/** | |||||
* Invoked when a message is received from a connection | |||||
*/ | |||||
protected onMessage(socket: WebSocket, data: string) { | |||||
let parsed: IWebSocketRequest | IWebSocketResponse; | |||||
try { | |||||
parsed = JSON.parse(data); | |||||
} catch(e) { | |||||
console.warn("Failed to parse JSON response"); | |||||
return; | |||||
} | |||||
if (parsed.type !== "request") { | |||||
this.handleRequest(socket, <IWebSocketRequest><any>parsed); // WAT!?!? | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,2 @@ | |||||
export * from "./schema"; | |||||
export * from "./WebSocketServerService"; |
@ -0,0 +1,17 @@ | |||||
export interface IWebSocketRequest { | |||||
type: "request", | |||||
requestId: number, | |||||
method: string, | |||||
payload: any | |||||
} | |||||
export interface IWebSocketResponse { | |||||
type : "response", | |||||
requestId: number, | |||||
payload : any | |||||
} | |||||
export interface IPendingRequest { | |||||
resolve(payload?: any): void, | |||||
reject(error: Error|string): void | |||||
} |
@ -0,0 +1,7 @@ | |||||
{ | |||||
"extends": "../../tsconfig.package.json", | |||||
"compilerOptions": { | |||||
"outDir": "./dist/lib", | |||||
"declarationDir": "./dist/typings" | |||||
} | |||||
} |
@ -0,0 +1,122 @@ | |||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. | |||||
# yarn lockfile v1 | |||||
"@types/jsonwebtoken@^8.5.1": | |||||
version "8.5.1" | |||||
resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#56958cb2d80f6d74352bd2e501a018e2506a8a84" | |||||
integrity sha512-rNAPdomlIUX0i0cg2+I+Q1wOUr531zHBQ+cV/28PJ39bSPKjahatZZ2LMuhiguETkCgLVzfruw/ZvNMNkKoSzw== | |||||
dependencies: | |||||
"@types/node" "*" | |||||
"@types/node@*": | |||||
version "15.12.2" | |||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-15.12.2.tgz#1f2b42c4be7156ff4a6f914b2fb03d05fa84e38d" | |||||
integrity sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww== | |||||
"@types/ws@^7.4.5": | |||||
version "7.4.5" | |||||
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.5.tgz#8ff0f7efcd8fea19f51f9dd66cb8b498d172a752" | |||||
integrity sha512-8mbDgtc8xpxDDem5Gwj76stBDJX35KQ3YBoayxlqUQcL5BZUthiqP/VQ4PQnLHqM4PmlbyO74t98eJpURO+gPA== | |||||
dependencies: | |||||
"@types/node" "*" | |||||
buffer-equal-constant-time@1.0.1: | |||||
version "1.0.1" | |||||
resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" | |||||
integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= | |||||
ecdsa-sig-formatter@1.0.11: | |||||
version "1.0.11" | |||||
resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" | |||||
integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== | |||||
dependencies: | |||||
safe-buffer "^5.0.1" | |||||
jsonwebtoken@^8.5.1: | |||||
version "8.5.1" | |||||
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" | |||||
integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== | |||||
dependencies: | |||||
jws "^3.2.2" | |||||
lodash.includes "^4.3.0" | |||||
lodash.isboolean "^3.0.3" | |||||
lodash.isinteger "^4.0.4" | |||||
lodash.isnumber "^3.0.3" | |||||
lodash.isplainobject "^4.0.6" | |||||
lodash.isstring "^4.0.1" | |||||
lodash.once "^4.0.0" | |||||
ms "^2.1.1" | |||||
semver "^5.6.0" | |||||
jwa@^1.4.1: | |||||
version "1.4.1" | |||||
resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" | |||||
integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== | |||||
dependencies: | |||||
buffer-equal-constant-time "1.0.1" | |||||
ecdsa-sig-formatter "1.0.11" | |||||
safe-buffer "^5.0.1" | |||||
jws@^3.2.2: | |||||
version "3.2.2" | |||||
resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" | |||||
integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== | |||||
dependencies: | |||||
jwa "^1.4.1" | |||||
safe-buffer "^5.0.1" | |||||
lodash.includes@^4.3.0: | |||||
version "4.3.0" | |||||
resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" | |||||
integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8= | |||||
lodash.isboolean@^3.0.3: | |||||
version "3.0.3" | |||||
resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" | |||||
integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY= | |||||
lodash.isinteger@^4.0.4: | |||||
version "4.0.4" | |||||
resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" | |||||
integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M= | |||||
lodash.isnumber@^3.0.3: | |||||
version "3.0.3" | |||||
resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" | |||||
integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w= | |||||
lodash.isplainobject@^4.0.6: | |||||
version "4.0.6" | |||||
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" | |||||
integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= | |||||
lodash.isstring@^4.0.1: | |||||
version "4.0.1" | |||||
resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" | |||||
integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= | |||||
lodash.once@^4.0.0: | |||||
version "4.1.1" | |||||
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" | |||||
integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= | |||||
ms@^2.1.1: | |||||
version "2.1.3" | |||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" | |||||
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== | |||||
safe-buffer@^5.0.1: | |||||
version "5.2.1" | |||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" | |||||
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== | |||||
semver@^5.6.0: | |||||
version "5.7.1" | |||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" | |||||
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== | |||||
ws@^7.5.0: | |||||
version "7.5.0" | |||||
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.0.tgz#0033bafea031fb9df041b2026fc72a571ca44691" | |||||
integrity sha512-6ezXvzOZupqKj4jUqbQ9tXuJNo+BR2gU8fFRk3XCP3e0G6WT414u5ELe6Y0vtp7kmSJ3F7YWObSNr1ESsgi4vw== |