123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590 |
- "use strict";
- var __importDefault = (this && this.__importDefault) || function (mod) {
- return (mod && mod.__esModule) ? mod : { "default": mod };
- };
- Object.defineProperty(exports, "__esModule", { value: true });
- exports.Socket = void 0;
- const index_js_1 = require("./transports/index.js");
- const util_js_1 = require("./util.js");
- const parseqs_1 = __importDefault(require("parseqs"));
- const parseuri_1 = __importDefault(require("parseuri"));
- const debug_1 = __importDefault(require("debug")); // debug()
- const component_emitter_1 = require("@socket.io/component-emitter");
- const engine_io_parser_1 = require("engine.io-parser");
- const debug = (0, debug_1.default)("engine.io-client:socket"); // debug()
- class Socket extends component_emitter_1.Emitter {
- /**
- * Socket constructor.
- *
- * @param {String|Object} uri or options
- * @param {Object} opts - options
- * @api public
- */
- constructor(uri, opts = {}) {
- super();
- if (uri && "object" === typeof uri) {
- opts = uri;
- uri = null;
- }
- if (uri) {
- uri = (0, parseuri_1.default)(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 = (0, parseuri_1.default)(opts.host).host;
- }
- (0, util_js_1.installTimerFunctions)(this, opts);
- this.secure =
- null != opts.secure
- ? opts.secure
- : typeof location !== "undefined" && "https:" === location.protocol;
- if (opts.hostname && !opts.port) {
- // if no port is specified manually, use the protocol default
- 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"];
- this.readyState = "";
- this.writeBuffer = [];
- this.prevBufferLen = 0;
- this.opts = Object.assign({
- path: "/engine.io",
- agent: false,
- withCredentials: false,
- upgrade: true,
- timestampParam: "t",
- rememberUpgrade: false,
- rejectUnauthorized: true,
- perMessageDeflate: {
- threshold: 1024
- },
- transportOptions: {},
- closeOnBeforeunload: true
- }, opts);
- this.opts.path = this.opts.path.replace(/\/$/, "") + "/";
- if (typeof this.opts.query === "string") {
- this.opts.query = parseqs_1.default.decode(this.opts.query);
- }
- // set on handshake
- this.id = null;
- this.upgrades = null;
- this.pingInterval = null;
- this.pingTimeout = null;
- // set on heartbeat
- this.pingTimeoutTimer = null;
- if (typeof addEventListener === "function") {
- if (this.opts.closeOnBeforeunload) {
- // Firefox closes the connection when the "beforeunload" event is emitted but not Chrome. This event listener
- // ensures every browser behaves the same (no "disconnect" event at the Socket.IO level when the page is
- // closed/reloaded)
- addEventListener("beforeunload", () => {
- if (this.transport) {
- // silently close the transport
- this.transport.removeAllListeners();
- this.transport.close();
- }
- }, false);
- }
- if (this.hostname !== "localhost") {
- this.offlineEventListener = () => {
- this.onClose("transport close");
- };
- addEventListener("offline", this.offlineEventListener, false);
- }
- }
- this.open();
- }
- /**
- * Creates transport of the given type.
- *
- * @param {String} transport name
- * @return {Transport}
- * @api private
- */
- createTransport(name) {
- debug('creating transport "%s"', name);
- const query = clone(this.opts.query);
- // append engine.io protocol identifier
- query.EIO = engine_io_parser_1.protocol;
- // transport name
- query.transport = name;
- // session id if we already have one
- if (this.id)
- query.sid = this.id;
- const opts = Object.assign({}, this.opts.transportOptions[name], this.opts, {
- query,
- socket: this,
- hostname: this.hostname,
- secure: this.secure,
- port: this.port
- });
- debug("options: %j", opts);
- return new index_js_1.transports[name](opts);
- }
- /**
- * Initializes transport to use and starts probe.
- *
- * @api private
- */
- open() {
- let transport;
- if (this.opts.rememberUpgrade &&
- Socket.priorWebsocketSuccess &&
- this.transports.indexOf("websocket") !== -1) {
- transport = "websocket";
- }
- else if (0 === this.transports.length) {
- // Emit error on next tick so it can be listened to
- this.setTimeoutFn(() => {
- this.emitReserved("error", "No transports available");
- }, 0);
- return;
- }
- else {
- transport = this.transports[0];
- }
- this.readyState = "opening";
- // Retry with the next transport if the transport is disabled (jsonp: false)
- try {
- transport = this.createTransport(transport);
- }
- catch (e) {
- debug("error while creating transport: %s", e);
- this.transports.shift();
- this.open();
- return;
- }
- transport.open();
- this.setTransport(transport);
- }
- /**
- * Sets the current transport. Disables the existing one (if any).
- *
- * @api private
- */
- setTransport(transport) {
- debug("setting transport %s", transport.name);
- if (this.transport) {
- debug("clearing existing transport %s", this.transport.name);
- this.transport.removeAllListeners();
- }
- // set up transport
- this.transport = transport;
- // set up transport listeners
- transport
- .on("drain", this.onDrain.bind(this))
- .on("packet", this.onPacket.bind(this))
- .on("error", this.onError.bind(this))
- .on("close", () => {
- this.onClose("transport close");
- });
- }
- /**
- * Probes a transport.
- *
- * @param {String} transport name
- * @api private
- */
- probe(name) {
- debug('probing transport "%s"', name);
- let transport = this.createTransport(name);
- let failed = false;
- Socket.priorWebsocketSuccess = false;
- const onTransportOpen = () => {
- if (failed)
- return;
- debug('probe transport "%s" opened', name);
- transport.send([{ type: "ping", data: "probe" }]);
- transport.once("packet", msg => {
- if (failed)
- return;
- if ("pong" === msg.type && "probe" === msg.data) {
- debug('probe transport "%s" pong', name);
- this.upgrading = true;
- this.emitReserved("upgrading", transport);
- if (!transport)
- return;
- Socket.priorWebsocketSuccess = "websocket" === transport.name;
- debug('pausing current transport "%s"', this.transport.name);
- this.transport.pause(() => {
- if (failed)
- return;
- if ("closed" === this.readyState)
- return;
- debug("changing transport and sending upgrade packet");
- cleanup();
- this.setTransport(transport);
- transport.send([{ type: "upgrade" }]);
- this.emitReserved("upgrade", transport);
- transport = null;
- this.upgrading = false;
- this.flush();
- });
- }
- else {
- debug('probe transport "%s" failed', name);
- const err = new Error("probe error");
- // @ts-ignore
- err.transport = transport.name;
- this.emitReserved("upgradeError", err);
- }
- });
- };
- function freezeTransport() {
- if (failed)
- return;
- // Any callback called by transport should be ignored since now
- failed = true;
- cleanup();
- transport.close();
- transport = null;
- }
- // Handle any error that happens while probing
- const onerror = err => {
- const error = new Error("probe error: " + err);
- // @ts-ignore
- error.transport = transport.name;
- freezeTransport();
- debug('probe transport "%s" failed because of error: %s', name, err);
- this.emitReserved("upgradeError", error);
- };
- function onTransportClose() {
- onerror("transport closed");
- }
- // When the socket is closed while we're probing
- function onclose() {
- onerror("socket closed");
- }
- // When the socket is upgraded while we're probing
- function onupgrade(to) {
- if (transport && to.name !== transport.name) {
- debug('"%s" works - aborting "%s"', to.name, transport.name);
- freezeTransport();
- }
- }
- // Remove all listeners on the transport and on self
- 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);
- transport.open();
- }
- /**
- * Called when connection is deemed open.
- *
- * @api private
- */
- onOpen() {
- debug("socket open");
- this.readyState = "open";
- Socket.priorWebsocketSuccess = "websocket" === this.transport.name;
- this.emitReserved("open");
- this.flush();
- // we check for `readyState` in case an `open`
- // listener already closed the socket
- if ("open" === this.readyState &&
- this.opts.upgrade &&
- this.transport.pause) {
- debug("starting upgrade probes");
- let i = 0;
- const l = this.upgrades.length;
- for (; i < l; i++) {
- this.probe(this.upgrades[i]);
- }
- }
- }
- /**
- * Handles a packet.
- *
- * @api private
- */
- onPacket(packet) {
- if ("opening" === this.readyState ||
- "open" === this.readyState ||
- "closing" === this.readyState) {
- debug('socket receive: type "%s", data "%s"', packet.type, packet.data);
- this.emitReserved("packet", packet);
- // Socket is live - any packet counts
- this.emitReserved("heartbeat");
- switch (packet.type) {
- case "open":
- this.onHandshake(JSON.parse(packet.data));
- break;
- case "ping":
- this.resetPingTimeout();
- this.sendPacket("pong");
- this.emitReserved("ping");
- this.emitReserved("pong");
- break;
- case "error":
- const err = new Error("server error");
- // @ts-ignore
- err.code = packet.data;
- this.onError(err);
- break;
- case "message":
- this.emitReserved("data", packet.data);
- this.emitReserved("message", packet.data);
- break;
- }
- }
- else {
- debug('packet received with socket readyState "%s"', this.readyState);
- }
- }
- /**
- * Called upon handshake completion.
- *
- * @param {Object} data - handshake obj
- * @api private
- */
- 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.onOpen();
- // In case open handler closes socket
- if ("closed" === this.readyState)
- return;
- this.resetPingTimeout();
- }
- /**
- * Sets and resets ping timeout timer based on server pings.
- *
- * @api private
- */
- resetPingTimeout() {
- this.clearTimeoutFn(this.pingTimeoutTimer);
- this.pingTimeoutTimer = this.setTimeoutFn(() => {
- this.onClose("ping timeout");
- }, this.pingInterval + this.pingTimeout);
- if (this.opts.autoUnref) {
- this.pingTimeoutTimer.unref();
- }
- }
- /**
- * Called on `drain` event
- *
- * @api private
- */
- onDrain() {
- this.writeBuffer.splice(0, this.prevBufferLen);
- // setting prevBufferLen = 0 is very important
- // for example, when upgrading, upgrade packet is sent over,
- // and a nonzero prevBufferLen could cause problems on `drain`
- this.prevBufferLen = 0;
- if (0 === this.writeBuffer.length) {
- this.emitReserved("drain");
- }
- else {
- this.flush();
- }
- }
- /**
- * Flush write buffers.
- *
- * @api private
- */
- flush() {
- if ("closed" !== this.readyState &&
- this.transport.writable &&
- !this.upgrading &&
- this.writeBuffer.length) {
- debug("flushing %d packets in socket", this.writeBuffer.length);
- this.transport.send(this.writeBuffer);
- // keep track of current length of writeBuffer
- // splice writeBuffer and callbackBuffer on `drain`
- this.prevBufferLen = this.writeBuffer.length;
- this.emitReserved("flush");
- }
- }
- /**
- * Sends a message.
- *
- * @param {String} message.
- * @param {Function} callback function.
- * @param {Object} options.
- * @return {Socket} for chaining.
- * @api public
- */
- write(msg, options, fn) {
- this.sendPacket("message", msg, options, fn);
- return this;
- }
- send(msg, options, fn) {
- this.sendPacket("message", msg, options, fn);
- return this;
- }
- /**
- * Sends a packet.
- *
- * @param {String} packet type.
- * @param {String} data.
- * @param {Object} options.
- * @param {Function} callback function.
- * @api private
- */
- 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();
- }
- /**
- * Closes the connection.
- *
- * @api public
- */
- close() {
- const close = () => {
- this.onClose("forced close");
- debug("socket closing - telling transport to close");
- this.transport.close();
- };
- const cleanupAndClose = () => {
- this.off("upgrade", cleanupAndClose);
- this.off("upgradeError", cleanupAndClose);
- close();
- };
- const waitForUpgrade = () => {
- // wait for upgrade to finish since we can't send packets while pausing a transport
- 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;
- }
- /**
- * Called upon transport error
- *
- * @api private
- */
- onError(err) {
- debug("socket error %j", err);
- Socket.priorWebsocketSuccess = false;
- this.emitReserved("error", err);
- this.onClose("transport error", err);
- }
- /**
- * Called upon transport close.
- *
- * @api private
- */
- onClose(reason, desc) {
- if ("opening" === this.readyState ||
- "open" === this.readyState ||
- "closing" === this.readyState) {
- debug('socket close with reason: "%s"', reason);
- // clear timers
- this.clearTimeoutFn(this.pingTimeoutTimer);
- // stop event from firing again for transport
- this.transport.removeAllListeners("close");
- // ensure transport won't stay open
- this.transport.close();
- // ignore further transport communication
- this.transport.removeAllListeners();
- if (typeof removeEventListener === "function") {
- removeEventListener("offline", this.offlineEventListener, false);
- }
- // set ready state
- this.readyState = "closed";
- // clear session id
- this.id = null;
- // emit close event
- this.emitReserved("close", reason, desc);
- // clean buffers after, so users can still
- // grab the buffers on `close` event
- this.writeBuffer = [];
- this.prevBufferLen = 0;
- }
- }
- /**
- * Filters upgrades, returning only those matching client transports.
- *
- * @param {Array} server upgrades
- * @api private
- *
- */
- 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;
- }
- }
- exports.Socket = Socket;
- Socket.protocol = engine_io_parser_1.protocol;
- function clone(obj) {
- const o = {};
- for (let i in obj) {
- if (obj.hasOwnProperty(i)) {
- o[i] = obj[i];
- }
- }
- return o;
- }
|