Files
ms/packages/websocket/src/engine.io-client/transports/websocket.ts
2023-02-09 13:49:48 +08:00

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
}
}