226 lines
6.5 KiB
TypeScript
226 lines
6.5 KiB
TypeScript
import { Transport } from "../transport"
|
|
import { encode } from "../contrib/parseqs"
|
|
import { yeast } from "../contrib/yeast"
|
|
import { pick } from "../util"
|
|
import {
|
|
defaultBinaryType,
|
|
nextTick,
|
|
usingBrowserWebSocket,
|
|
WebSocket
|
|
} from "./websocket-constructor"
|
|
// import debugModule from "debug" // debug()
|
|
import { encodePacket } from "../../engine.io-parser"
|
|
|
|
// const debug = debugModule("engine.io-client:websocket") // debug()
|
|
const debug = (...args: any) => console.debug('engine.io-client:websocket', ...args)
|
|
|
|
// detect ReactNative environment
|
|
const isReactNative =
|
|
typeof navigator !== "undefined" &&
|
|
typeof navigator.product === "string" &&
|
|
navigator.product.toLowerCase() === "reactnative"
|
|
|
|
export class WS extends Transport {
|
|
private ws: any
|
|
|
|
/**
|
|
* WebSocket transport constructor.
|
|
*
|
|
* @param {Object} opts - connection options
|
|
* @protected
|
|
*/
|
|
constructor(opts) {
|
|
super(opts)
|
|
|
|
this.supportsBinary = !opts.forceBase64
|
|
}
|
|
|
|
override get name() {
|
|
return "websocket"
|
|
}
|
|
|
|
override doOpen() {
|
|
if (!this.check()) {
|
|
// let probe timeout
|
|
return
|
|
}
|
|
|
|
const uri = this.uri()
|
|
const protocols = this.opts.protocols
|
|
|
|
// React Native only supports the 'headers' option, and will print a warning if anything else is passed
|
|
const opts = isReactNative
|
|
? {}
|
|
: pick(
|
|
this.opts,
|
|
"agent",
|
|
"perMessageDeflate",
|
|
"pfx",
|
|
"key",
|
|
"passphrase",
|
|
"cert",
|
|
"ca",
|
|
"ciphers",
|
|
"rejectUnauthorized",
|
|
"localAddress",
|
|
"protocolVersion",
|
|
"origin",
|
|
"maxPayload",
|
|
"family",
|
|
"checkServerIdentity"
|
|
)
|
|
|
|
if (this.opts.extraHeaders) {
|
|
opts.headers = this.opts.extraHeaders
|
|
}
|
|
|
|
try {
|
|
this.ws =
|
|
usingBrowserWebSocket && !isReactNative
|
|
? protocols
|
|
? new WebSocket(uri, protocols)
|
|
: new WebSocket(uri)
|
|
: new WebSocket(uri, protocols, opts)
|
|
} catch (err: any) {
|
|
return this.emitReserved("error", err)
|
|
}
|
|
|
|
this.ws.binaryType = this.socket.binaryType || defaultBinaryType
|
|
|
|
this.addEventListeners()
|
|
}
|
|
|
|
/**
|
|
* Adds event listeners to the socket
|
|
*
|
|
* @private
|
|
*/
|
|
private addEventListeners() {
|
|
this.ws.onopen = () => {
|
|
if (this.opts.autoUnref) {
|
|
this.ws._socket.unref()
|
|
}
|
|
this.onOpen()
|
|
}
|
|
this.ws.onclose = (closeEvent) =>
|
|
this.onClose({
|
|
description: "websocket connection closed",
|
|
context: closeEvent,
|
|
})
|
|
this.ws.onmessage = (ev) => this.onData(ev.data)
|
|
this.ws.onerror = (e) => this.onError("websocket error", e)
|
|
}
|
|
|
|
override write(packets) {
|
|
this.writable = false
|
|
|
|
// encodePacket efficient as it uses WS framing
|
|
// no need for encodePayload
|
|
for (let i = 0; i < packets.length; i++) {
|
|
const packet = packets[i]
|
|
const lastPacket = i === packets.length - 1
|
|
|
|
encodePacket(packet, this.supportsBinary, (data) => {
|
|
// always create a new object (GH-437)
|
|
const opts: { compress?: boolean } = {}
|
|
if (!usingBrowserWebSocket) {
|
|
if (packet.options) {
|
|
opts.compress = packet.options.compress
|
|
}
|
|
|
|
if (this.opts.perMessageDeflate) {
|
|
const len =
|
|
// @ts-ignore
|
|
"string" === typeof data ? Buffer.byteLength(data) : data.length
|
|
if (len < this.opts.perMessageDeflate.threshold) {
|
|
opts.compress = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sometimes the websocket has already been closed but the browser didn't
|
|
// have a chance of informing us about it yet, in that case send will
|
|
// throw an error
|
|
try {
|
|
if (usingBrowserWebSocket) {
|
|
// TypeError is thrown when passing the second argument on Safari
|
|
this.ws.send(data)
|
|
} else {
|
|
this.ws.send(data, opts)
|
|
}
|
|
} catch (e) {
|
|
debug("websocket closed before onclose event")
|
|
}
|
|
|
|
if (lastPacket) {
|
|
// fake drain
|
|
// defer to next tick to allow Socket to clear writeBuffer
|
|
nextTick(() => {
|
|
this.writable = true
|
|
this.emitReserved("drain")
|
|
}, this.setTimeoutFn)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
override doClose() {
|
|
if (typeof this.ws !== "undefined") {
|
|
this.ws.close()
|
|
this.ws = null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generates uri for connection.
|
|
*
|
|
* @private
|
|
*/
|
|
uri() {
|
|
let query: { b64?: number } = this.query || {}
|
|
const schema = this.opts.secure ? "wss" : "ws"
|
|
let port = ""
|
|
|
|
// avoid port if default for schema
|
|
if (
|
|
this.opts.port &&
|
|
(("wss" === schema && Number(this.opts.port) !== 443) ||
|
|
("ws" === schema && Number(this.opts.port) !== 80))
|
|
) {
|
|
port = ":" + this.opts.port
|
|
}
|
|
|
|
// append timestamp to URI
|
|
if (this.opts.timestampRequests) {
|
|
query[this.opts.timestampParam] = yeast()
|
|
}
|
|
|
|
// communicate binary support capabilities
|
|
if (!this.supportsBinary) {
|
|
query.b64 = 1
|
|
}
|
|
|
|
const encodedQuery = encode(query)
|
|
const ipv6 = this.opts.hostname.indexOf(":") !== -1
|
|
|
|
return (
|
|
schema +
|
|
"://" +
|
|
(ipv6 ? "[" + this.opts.hostname + "]" : this.opts.hostname) +
|
|
port +
|
|
this.opts.path +
|
|
(encodedQuery.length ? "?" + encodedQuery : "")
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Feature detection for WebSocket.
|
|
*
|
|
* @return {Boolean} whether this transport is available.
|
|
* @private
|
|
*/
|
|
private check() {
|
|
return !!WebSocket
|
|
}
|
|
}
|