@wandelbots/nova-js
Version:
Official JS client for the Wandelbots API
164 lines (141 loc) • 4.26 kB
text/typescript
import ReconnectingWebSocket, { type ErrorEvent } from "reconnecting-websocket"
import type * as v1 from "./v1/mock/MockNovaInstance"
import type * as v2 from "./v2/mock/MockNovaInstance"
export class AutoReconnectingWebsocket extends ReconnectingWebSocket {
receivedFirstMessage?: MessageEvent
targetUrl: string
disposed = false
constructor(
targetUrl: string,
readonly opts: {
mock?: v1.MockNovaInstance | v2.MockNovaInstance
onDispose?: () => void
} = {},
) {
console.log("Opening websocket to", targetUrl)
super(() => this.targetUrl || targetUrl, undefined, {
startClosed: true,
})
// Reconnecting websocket doesn't set this properly with startClosed
Object.defineProperty(this, "url", {
get() {
return this.targetUrl
},
})
this.targetUrl = targetUrl
this.addEventListener("open", () => {
console.log(`Websocket to ${this.url} opened`)
})
this.addEventListener("message", (ev) => {
if (!this.receivedFirstMessage) {
this.receivedFirstMessage = ev
}
})
this.addEventListener("close", () => {
console.log(`Websocket to ${this.url} closed`)
})
const origReconnect = this.reconnect
this.reconnect = () => {
if (this.opts.mock) {
this.opts.mock.handleWebsocketConnection(this)
} else {
origReconnect.apply(this)
}
}
this.reconnect()
}
changeUrl(targetUrl: string) {
this.receivedFirstMessage = undefined
this.targetUrl = targetUrl
this.reconnect()
}
sendJson(data: unknown) {
if (this.opts.mock) {
this.opts.mock.handleWebsocketMessage(this, JSON.stringify(data))
} else {
this.send(JSON.stringify(data))
}
}
/**
* Permanently close this websocket and indicate that
* this object should not be used again.
**/
dispose() {
this.close()
this.disposed = true
if (this.opts.onDispose) {
this.opts.onDispose()
}
}
/**
* Returns a promise that resolves once the websocket
* is in the OPEN state. */
async opened() {
return new Promise<void>((resolve, reject) => {
if (this.readyState === WebSocket.OPEN) {
resolve()
} else {
this.addEventListener("open", () => resolve())
this.addEventListener("error", reject)
}
})
}
/**
* Returns a promise that resolves once the websocket
* is in the CLOSED state. */
async closed() {
return new Promise<void>((resolve, reject) => {
if (this.readyState === WebSocket.CLOSED) {
resolve()
} else {
this.addEventListener("close", () => resolve())
this.addEventListener("error", reject)
}
})
}
/**
* Returns a promise that resolves when the first message
* is received from the websocket. Resolves immediately if
* the first message has already been received.
*/
async firstMessage() {
if (this.receivedFirstMessage) {
return this.receivedFirstMessage
}
return new Promise<MessageEvent>((resolve, reject) => {
const onMessage = (ev: MessageEvent) => {
this.receivedFirstMessage = ev
this.removeEventListener("message", onMessage)
this.removeEventListener("error", onError)
resolve(ev)
}
const onError = (ev: ErrorEvent) => {
this.removeEventListener("message", onMessage)
this.removeEventListener("error", onError)
reject(ev)
}
this.addEventListener("message", onMessage)
this.addEventListener("error", onError)
})
}
/**
* Returns a promise that resolves when the next message
* is received from the websocket.
*/
async nextMessage() {
return new Promise<MessageEvent>((resolve, reject) => {
const onMessage = (ev: MessageEvent) => {
this.removeEventListener("message", onMessage)
this.removeEventListener("error", onError)
resolve(ev)
}
const onError = (ev: ErrorEvent) => {
this.removeEventListener("message", onMessage)
this.removeEventListener("error", onError)
reject(ev)
}
this.addEventListener("message", onMessage)
this.addEventListener("error", onError)
})
}
}