|
import { transports } from "./transports/index.js"; |
|
import { installTimerFunctions, byteLength } from "./util.js"; |
|
import { decode } from "./contrib/parseqs.js"; |
|
import { parse } from "./contrib/parseuri.js"; |
|
import { Emitter } from "@socket.io/component-emitter"; |
|
import { protocol } from "engine.io-parser"; |
|
import { defaultBinaryType } from "./transports/websocket-constructor.js"; |
|
export class Socket extends Emitter { |
|
|
|
|
|
|
|
|
|
|
|
|
|
constructor(uri, opts = {}) { |
|
super(); |
|
this.binaryType = defaultBinaryType; |
|
this.writeBuffer = []; |
|
if (uri && "object" === typeof uri) { |
|
opts = uri; |
|
uri = null; |
|
} |
|
if (uri) { |
|
uri = parse(uri); |
|
opts.hostname = uri.host; |
|
opts.secure = uri.protocol === "https" || uri.protocol === "wss"; |
|
opts.port = uri.port; |
|
if (uri.query) |
|
opts.query = uri.query; |
|
} |
|
else if (opts.host) { |
|
opts.hostname = parse(opts.host).host; |
|
} |
|
installTimerFunctions(this, opts); |
|
this.secure = |
|
null != opts.secure |
|
? opts.secure |
|
: typeof location !== "undefined" && "https:" === location.protocol; |
|
if (opts.hostname && !opts.port) { |
|
|
|
opts.port = this.secure ? "443" : "80"; |
|
} |
|
this.hostname = |
|
opts.hostname || |
|
(typeof location !== "undefined" ? location.hostname : "localhost"); |
|
this.port = |
|
opts.port || |
|
(typeof location !== "undefined" && location.port |
|
? location.port |
|
: this.secure |
|
? "443" |
|
: "80"); |
|
this.transports = opts.transports || [ |
|
"polling", |
|
"websocket", |
|
"webtransport", |
|
]; |
|
this.writeBuffer = []; |
|
this.prevBufferLen = 0; |
|
this.opts = Object.assign({ |
|
path: "/engine.io", |
|
agent: false, |
|
withCredentials: false, |
|
upgrade: true, |
|
timestampParam: "t", |
|
rememberUpgrade: false, |
|
addTrailingSlash: true, |
|
rejectUnauthorized: true, |
|
perMessageDeflate: { |
|
threshold: 1024, |
|
}, |
|
transportOptions: {}, |
|
closeOnBeforeunload: false, |
|
}, opts); |
|
this.opts.path = |
|
this.opts.path.replace(/\/$/, "") + |
|
(this.opts.addTrailingSlash ? "/" : ""); |
|
if (typeof this.opts.query === "string") { |
|
this.opts.query = decode(this.opts.query); |
|
} |
|
|
|
this.id = null; |
|
this.upgrades = null; |
|
this.pingInterval = null; |
|
this.pingTimeout = null; |
|
|
|
this.pingTimeoutTimer = null; |
|
if (typeof addEventListener === "function") { |
|
if (this.opts.closeOnBeforeunload) { |
|
|
|
|
|
|
|
this.beforeunloadEventListener = () => { |
|
if (this.transport) { |
|
|
|
this.transport.removeAllListeners(); |
|
this.transport.close(); |
|
} |
|
}; |
|
addEventListener("beforeunload", this.beforeunloadEventListener, false); |
|
} |
|
if (this.hostname !== "localhost") { |
|
this.offlineEventListener = () => { |
|
this.onClose("transport close", { |
|
description: "network connection lost", |
|
}); |
|
}; |
|
addEventListener("offline", this.offlineEventListener, false); |
|
} |
|
} |
|
this.open(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
createTransport(name) { |
|
const query = Object.assign({}, this.opts.query); |
|
|
|
query.EIO = protocol; |
|
|
|
query.transport = name; |
|
|
|
if (this.id) |
|
query.sid = this.id; |
|
const opts = Object.assign({}, this.opts, { |
|
query, |
|
socket: this, |
|
hostname: this.hostname, |
|
secure: this.secure, |
|
port: this.port, |
|
}, this.opts.transportOptions[name]); |
|
return new transports[name](opts); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
open() { |
|
let transport; |
|
if (this.opts.rememberUpgrade && |
|
Socket.priorWebsocketSuccess && |
|
this.transports.indexOf("websocket") !== -1) { |
|
transport = "websocket"; |
|
} |
|
else if (0 === this.transports.length) { |
|
|
|
this.setTimeoutFn(() => { |
|
this.emitReserved("error", "No transports available"); |
|
}, 0); |
|
return; |
|
} |
|
else { |
|
transport = this.transports[0]; |
|
} |
|
this.readyState = "opening"; |
|
|
|
try { |
|
transport = this.createTransport(transport); |
|
} |
|
catch (e) { |
|
this.transports.shift(); |
|
this.open(); |
|
return; |
|
} |
|
transport.open(); |
|
this.setTransport(transport); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
setTransport(transport) { |
|
if (this.transport) { |
|
this.transport.removeAllListeners(); |
|
} |
|
|
|
this.transport = transport; |
|
|
|
transport |
|
.on("drain", this.onDrain.bind(this)) |
|
.on("packet", this.onPacket.bind(this)) |
|
.on("error", this.onError.bind(this)) |
|
.on("close", (reason) => this.onClose("transport close", reason)); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
probe(name) { |
|
let transport = this.createTransport(name); |
|
let failed = false; |
|
Socket.priorWebsocketSuccess = false; |
|
const onTransportOpen = () => { |
|
if (failed) |
|
return; |
|
transport.send([{ type: "ping", data: "probe" }]); |
|
transport.once("packet", (msg) => { |
|
if (failed) |
|
return; |
|
if ("pong" === msg.type && "probe" === msg.data) { |
|
this.upgrading = true; |
|
this.emitReserved("upgrading", transport); |
|
if (!transport) |
|
return; |
|
Socket.priorWebsocketSuccess = "websocket" === transport.name; |
|
this.transport.pause(() => { |
|
if (failed) |
|
return; |
|
if ("closed" === this.readyState) |
|
return; |
|
cleanup(); |
|
this.setTransport(transport); |
|
transport.send([{ type: "upgrade" }]); |
|
this.emitReserved("upgrade", transport); |
|
transport = null; |
|
this.upgrading = false; |
|
this.flush(); |
|
}); |
|
} |
|
else { |
|
const err = new Error("probe error"); |
|
|
|
err.transport = transport.name; |
|
this.emitReserved("upgradeError", err); |
|
} |
|
}); |
|
}; |
|
function freezeTransport() { |
|
if (failed) |
|
return; |
|
|
|
failed = true; |
|
cleanup(); |
|
transport.close(); |
|
transport = null; |
|
} |
|
|
|
const onerror = (err) => { |
|
const error = new Error("probe error: " + err); |
|
|
|
error.transport = transport.name; |
|
freezeTransport(); |
|
this.emitReserved("upgradeError", error); |
|
}; |
|
function onTransportClose() { |
|
onerror("transport closed"); |
|
} |
|
|
|
function onclose() { |
|
onerror("socket closed"); |
|
} |
|
|
|
function onupgrade(to) { |
|
if (transport && to.name !== transport.name) { |
|
freezeTransport(); |
|
} |
|
} |
|
|
|
const cleanup = () => { |
|
transport.removeListener("open", onTransportOpen); |
|
transport.removeListener("error", onerror); |
|
transport.removeListener("close", onTransportClose); |
|
this.off("close", onclose); |
|
this.off("upgrading", onupgrade); |
|
}; |
|
transport.once("open", onTransportOpen); |
|
transport.once("error", onerror); |
|
transport.once("close", onTransportClose); |
|
this.once("close", onclose); |
|
this.once("upgrading", onupgrade); |
|
if (this.upgrades.indexOf("webtransport") !== -1 && |
|
name !== "webtransport") { |
|
|
|
this.setTimeoutFn(() => { |
|
if (!failed) { |
|
transport.open(); |
|
} |
|
}, 200); |
|
} |
|
else { |
|
transport.open(); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
onOpen() { |
|
this.readyState = "open"; |
|
Socket.priorWebsocketSuccess = "websocket" === this.transport.name; |
|
this.emitReserved("open"); |
|
this.flush(); |
|
|
|
|
|
if ("open" === this.readyState && this.opts.upgrade) { |
|
let i = 0; |
|
const l = this.upgrades.length; |
|
for (; i < l; i++) { |
|
this.probe(this.upgrades[i]); |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
onPacket(packet) { |
|
if ("opening" === this.readyState || |
|
"open" === this.readyState || |
|
"closing" === this.readyState) { |
|
this.emitReserved("packet", packet); |
|
|
|
this.emitReserved("heartbeat"); |
|
this.resetPingTimeout(); |
|
switch (packet.type) { |
|
case "open": |
|
this.onHandshake(JSON.parse(packet.data)); |
|
break; |
|
case "ping": |
|
this.sendPacket("pong"); |
|
this.emitReserved("ping"); |
|
this.emitReserved("pong"); |
|
break; |
|
case "error": |
|
const err = new Error("server error"); |
|
|
|
err.code = packet.data; |
|
this.onError(err); |
|
break; |
|
case "message": |
|
this.emitReserved("data", packet.data); |
|
this.emitReserved("message", packet.data); |
|
break; |
|
} |
|
} |
|
else { |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
onHandshake(data) { |
|
this.emitReserved("handshake", data); |
|
this.id = data.sid; |
|
this.transport.query.sid = data.sid; |
|
this.upgrades = this.filterUpgrades(data.upgrades); |
|
this.pingInterval = data.pingInterval; |
|
this.pingTimeout = data.pingTimeout; |
|
this.maxPayload = data.maxPayload; |
|
this.onOpen(); |
|
|
|
if ("closed" === this.readyState) |
|
return; |
|
this.resetPingTimeout(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
resetPingTimeout() { |
|
this.clearTimeoutFn(this.pingTimeoutTimer); |
|
this.pingTimeoutTimer = this.setTimeoutFn(() => { |
|
this.onClose("ping timeout"); |
|
}, this.pingInterval + this.pingTimeout); |
|
if (this.opts.autoUnref) { |
|
this.pingTimeoutTimer.unref(); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
onDrain() { |
|
this.writeBuffer.splice(0, this.prevBufferLen); |
|
|
|
|
|
|
|
this.prevBufferLen = 0; |
|
if (0 === this.writeBuffer.length) { |
|
this.emitReserved("drain"); |
|
} |
|
else { |
|
this.flush(); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
flush() { |
|
if ("closed" !== this.readyState && |
|
this.transport.writable && |
|
!this.upgrading && |
|
this.writeBuffer.length) { |
|
const packets = this.getWritablePackets(); |
|
this.transport.send(packets); |
|
|
|
|
|
this.prevBufferLen = packets.length; |
|
this.emitReserved("flush"); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
getWritablePackets() { |
|
const shouldCheckPayloadSize = this.maxPayload && |
|
this.transport.name === "polling" && |
|
this.writeBuffer.length > 1; |
|
if (!shouldCheckPayloadSize) { |
|
return this.writeBuffer; |
|
} |
|
let payloadSize = 1; |
|
for (let i = 0; i < this.writeBuffer.length; i++) { |
|
const data = this.writeBuffer[i].data; |
|
if (data) { |
|
payloadSize += byteLength(data); |
|
} |
|
if (i > 0 && payloadSize > this.maxPayload) { |
|
return this.writeBuffer.slice(0, i); |
|
} |
|
payloadSize += 2; |
|
} |
|
return this.writeBuffer; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
write(msg, options, fn) { |
|
this.sendPacket("message", msg, options, fn); |
|
return this; |
|
} |
|
send(msg, options, fn) { |
|
this.sendPacket("message", msg, options, fn); |
|
return this; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sendPacket(type, data, options, fn) { |
|
if ("function" === typeof data) { |
|
fn = data; |
|
data = undefined; |
|
} |
|
if ("function" === typeof options) { |
|
fn = options; |
|
options = null; |
|
} |
|
if ("closing" === this.readyState || "closed" === this.readyState) { |
|
return; |
|
} |
|
options = options || {}; |
|
options.compress = false !== options.compress; |
|
const packet = { |
|
type: type, |
|
data: data, |
|
options: options, |
|
}; |
|
this.emitReserved("packetCreate", packet); |
|
this.writeBuffer.push(packet); |
|
if (fn) |
|
this.once("flush", fn); |
|
this.flush(); |
|
} |
|
|
|
|
|
|
|
close() { |
|
const close = () => { |
|
this.onClose("forced close"); |
|
this.transport.close(); |
|
}; |
|
const cleanupAndClose = () => { |
|
this.off("upgrade", cleanupAndClose); |
|
this.off("upgradeError", cleanupAndClose); |
|
close(); |
|
}; |
|
const waitForUpgrade = () => { |
|
|
|
this.once("upgrade", cleanupAndClose); |
|
this.once("upgradeError", cleanupAndClose); |
|
}; |
|
if ("opening" === this.readyState || "open" === this.readyState) { |
|
this.readyState = "closing"; |
|
if (this.writeBuffer.length) { |
|
this.once("drain", () => { |
|
if (this.upgrading) { |
|
waitForUpgrade(); |
|
} |
|
else { |
|
close(); |
|
} |
|
}); |
|
} |
|
else if (this.upgrading) { |
|
waitForUpgrade(); |
|
} |
|
else { |
|
close(); |
|
} |
|
} |
|
return this; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
onError(err) { |
|
Socket.priorWebsocketSuccess = false; |
|
this.emitReserved("error", err); |
|
this.onClose("transport error", err); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
onClose(reason, description) { |
|
if ("opening" === this.readyState || |
|
"open" === this.readyState || |
|
"closing" === this.readyState) { |
|
|
|
this.clearTimeoutFn(this.pingTimeoutTimer); |
|
|
|
this.transport.removeAllListeners("close"); |
|
|
|
this.transport.close(); |
|
|
|
this.transport.removeAllListeners(); |
|
if (typeof removeEventListener === "function") { |
|
removeEventListener("beforeunload", this.beforeunloadEventListener, false); |
|
removeEventListener("offline", this.offlineEventListener, false); |
|
} |
|
|
|
this.readyState = "closed"; |
|
|
|
this.id = null; |
|
|
|
this.emitReserved("close", reason, description); |
|
|
|
|
|
this.writeBuffer = []; |
|
this.prevBufferLen = 0; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
filterUpgrades(upgrades) { |
|
const filteredUpgrades = []; |
|
let i = 0; |
|
const j = upgrades.length; |
|
for (; i < j; i++) { |
|
if (~this.transports.indexOf(upgrades[i])) |
|
filteredUpgrades.push(upgrades[i]); |
|
} |
|
return filteredUpgrades; |
|
} |
|
} |
|
Socket.protocol = protocol; |
|
|