socket.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501
  1. import { PacketType } from "socket.io-parser";
  2. import { on } from "./on.js";
  3. import { Emitter, } from "@socket.io/component-emitter";
  4. import debugModule from "debug"; // debug()
  5. const debug = debugModule("socket.io-client:socket"); // debug()
  6. /**
  7. * Internal events.
  8. * These events can't be emitted by the user.
  9. */
  10. const RESERVED_EVENTS = Object.freeze({
  11. connect: 1,
  12. connect_error: 1,
  13. disconnect: 1,
  14. disconnecting: 1,
  15. // EventEmitter reserved events: https://nodejs.org/api/events.html#events_event_newlistener
  16. newListener: 1,
  17. removeListener: 1,
  18. });
  19. export class Socket extends Emitter {
  20. /**
  21. * `Socket` constructor.
  22. *
  23. * @public
  24. */
  25. constructor(io, nsp, opts) {
  26. super();
  27. this.connected = false;
  28. this.disconnected = true;
  29. this.receiveBuffer = [];
  30. this.sendBuffer = [];
  31. this.ids = 0;
  32. this.acks = {};
  33. this.flags = {};
  34. this.io = io;
  35. this.nsp = nsp;
  36. if (opts && opts.auth) {
  37. this.auth = opts.auth;
  38. }
  39. if (this.io._autoConnect)
  40. this.open();
  41. }
  42. /**
  43. * Subscribe to open, close and packet events
  44. *
  45. * @private
  46. */
  47. subEvents() {
  48. if (this.subs)
  49. return;
  50. const io = this.io;
  51. this.subs = [
  52. on(io, "open", this.onopen.bind(this)),
  53. on(io, "packet", this.onpacket.bind(this)),
  54. on(io, "error", this.onerror.bind(this)),
  55. on(io, "close", this.onclose.bind(this)),
  56. ];
  57. }
  58. /**
  59. * Whether the Socket will try to reconnect when its Manager connects or reconnects
  60. */
  61. get active() {
  62. return !!this.subs;
  63. }
  64. /**
  65. * "Opens" the socket.
  66. *
  67. * @public
  68. */
  69. connect() {
  70. if (this.connected)
  71. return this;
  72. this.subEvents();
  73. if (!this.io["_reconnecting"])
  74. this.io.open(); // ensure open
  75. if ("open" === this.io._readyState)
  76. this.onopen();
  77. return this;
  78. }
  79. /**
  80. * Alias for connect()
  81. */
  82. open() {
  83. return this.connect();
  84. }
  85. /**
  86. * Sends a `message` event.
  87. *
  88. * @return self
  89. * @public
  90. */
  91. send(...args) {
  92. args.unshift("message");
  93. this.emit.apply(this, args);
  94. return this;
  95. }
  96. /**
  97. * Override `emit`.
  98. * If the event is in `events`, it's emitted normally.
  99. *
  100. * @return self
  101. * @public
  102. */
  103. emit(ev, ...args) {
  104. if (RESERVED_EVENTS.hasOwnProperty(ev)) {
  105. throw new Error('"' + ev + '" is a reserved event name');
  106. }
  107. args.unshift(ev);
  108. const packet = {
  109. type: PacketType.EVENT,
  110. data: args,
  111. };
  112. packet.options = {};
  113. packet.options.compress = this.flags.compress !== false;
  114. // event ack callback
  115. if ("function" === typeof args[args.length - 1]) {
  116. const id = this.ids++;
  117. debug("emitting packet with ack id %d", id);
  118. const ack = args.pop();
  119. this._registerAckCallback(id, ack);
  120. packet.id = id;
  121. }
  122. const isTransportWritable = this.io.engine &&
  123. this.io.engine.transport &&
  124. this.io.engine.transport.writable;
  125. const discardPacket = this.flags.volatile && (!isTransportWritable || !this.connected);
  126. if (discardPacket) {
  127. debug("discard packet as the transport is not currently writable");
  128. }
  129. else if (this.connected) {
  130. this.packet(packet);
  131. }
  132. else {
  133. this.sendBuffer.push(packet);
  134. }
  135. this.flags = {};
  136. return this;
  137. }
  138. /**
  139. * @private
  140. */
  141. _registerAckCallback(id, ack) {
  142. const timeout = this.flags.timeout;
  143. if (timeout === undefined) {
  144. this.acks[id] = ack;
  145. return;
  146. }
  147. // @ts-ignore
  148. const timer = this.io.setTimeoutFn(() => {
  149. delete this.acks[id];
  150. for (let i = 0; i < this.sendBuffer.length; i++) {
  151. if (this.sendBuffer[i].id === id) {
  152. debug("removing packet with ack id %d from the buffer", id);
  153. this.sendBuffer.splice(i, 1);
  154. }
  155. }
  156. debug("event with ack id %d has timed out after %d ms", id, timeout);
  157. ack.call(this, new Error("operation has timed out"));
  158. }, timeout);
  159. this.acks[id] = (...args) => {
  160. // @ts-ignore
  161. this.io.clearTimeoutFn(timer);
  162. ack.apply(this, [null, ...args]);
  163. };
  164. }
  165. /**
  166. * Sends a packet.
  167. *
  168. * @param packet
  169. * @private
  170. */
  171. packet(packet) {
  172. packet.nsp = this.nsp;
  173. this.io._packet(packet);
  174. }
  175. /**
  176. * Called upon engine `open`.
  177. *
  178. * @private
  179. */
  180. onopen() {
  181. debug("transport is open - connecting");
  182. if (typeof this.auth == "function") {
  183. this.auth((data) => {
  184. this.packet({ type: PacketType.CONNECT, data });
  185. });
  186. }
  187. else {
  188. this.packet({ type: PacketType.CONNECT, data: this.auth });
  189. }
  190. }
  191. /**
  192. * Called upon engine or manager `error`.
  193. *
  194. * @param err
  195. * @private
  196. */
  197. onerror(err) {
  198. if (!this.connected) {
  199. this.emitReserved("connect_error", err);
  200. }
  201. }
  202. /**
  203. * Called upon engine `close`.
  204. *
  205. * @param reason
  206. * @private
  207. */
  208. onclose(reason) {
  209. debug("close (%s)", reason);
  210. this.connected = false;
  211. this.disconnected = true;
  212. delete this.id;
  213. this.emitReserved("disconnect", reason);
  214. }
  215. /**
  216. * Called with socket packet.
  217. *
  218. * @param packet
  219. * @private
  220. */
  221. onpacket(packet) {
  222. const sameNamespace = packet.nsp === this.nsp;
  223. if (!sameNamespace)
  224. return;
  225. switch (packet.type) {
  226. case PacketType.CONNECT:
  227. if (packet.data && packet.data.sid) {
  228. const id = packet.data.sid;
  229. this.onconnect(id);
  230. }
  231. else {
  232. this.emitReserved("connect_error", new Error("It seems you are trying to reach a Socket.IO server in v2.x with a v3.x client, but they are not compatible (more information here: https://socket.io/docs/v3/migrating-from-2-x-to-3-0/)"));
  233. }
  234. break;
  235. case PacketType.EVENT:
  236. this.onevent(packet);
  237. break;
  238. case PacketType.BINARY_EVENT:
  239. this.onevent(packet);
  240. break;
  241. case PacketType.ACK:
  242. this.onack(packet);
  243. break;
  244. case PacketType.BINARY_ACK:
  245. this.onack(packet);
  246. break;
  247. case PacketType.DISCONNECT:
  248. this.ondisconnect();
  249. break;
  250. case PacketType.CONNECT_ERROR:
  251. this.destroy();
  252. const err = new Error(packet.data.message);
  253. // @ts-ignore
  254. err.data = packet.data.data;
  255. this.emitReserved("connect_error", err);
  256. break;
  257. }
  258. }
  259. /**
  260. * Called upon a server event.
  261. *
  262. * @param packet
  263. * @private
  264. */
  265. onevent(packet) {
  266. const args = packet.data || [];
  267. debug("emitting event %j", args);
  268. if (null != packet.id) {
  269. debug("attaching ack callback to event");
  270. args.push(this.ack(packet.id));
  271. }
  272. if (this.connected) {
  273. this.emitEvent(args);
  274. }
  275. else {
  276. this.receiveBuffer.push(Object.freeze(args));
  277. }
  278. }
  279. emitEvent(args) {
  280. if (this._anyListeners && this._anyListeners.length) {
  281. const listeners = this._anyListeners.slice();
  282. for (const listener of listeners) {
  283. listener.apply(this, args);
  284. }
  285. }
  286. super.emit.apply(this, args);
  287. }
  288. /**
  289. * Produces an ack callback to emit with an event.
  290. *
  291. * @private
  292. */
  293. ack(id) {
  294. const self = this;
  295. let sent = false;
  296. return function (...args) {
  297. // prevent double callbacks
  298. if (sent)
  299. return;
  300. sent = true;
  301. debug("sending ack %j", args);
  302. self.packet({
  303. type: PacketType.ACK,
  304. id: id,
  305. data: args,
  306. });
  307. };
  308. }
  309. /**
  310. * Called upon a server acknowlegement.
  311. *
  312. * @param packet
  313. * @private
  314. */
  315. onack(packet) {
  316. const ack = this.acks[packet.id];
  317. if ("function" === typeof ack) {
  318. debug("calling ack %s with %j", packet.id, packet.data);
  319. ack.apply(this, packet.data);
  320. delete this.acks[packet.id];
  321. }
  322. else {
  323. debug("bad ack %s", packet.id);
  324. }
  325. }
  326. /**
  327. * Called upon server connect.
  328. *
  329. * @private
  330. */
  331. onconnect(id) {
  332. debug("socket connected with id %s", id);
  333. this.id = id;
  334. this.connected = true;
  335. this.disconnected = false;
  336. this.emitBuffered();
  337. this.emitReserved("connect");
  338. }
  339. /**
  340. * Emit buffered events (received and emitted).
  341. *
  342. * @private
  343. */
  344. emitBuffered() {
  345. this.receiveBuffer.forEach((args) => this.emitEvent(args));
  346. this.receiveBuffer = [];
  347. this.sendBuffer.forEach((packet) => this.packet(packet));
  348. this.sendBuffer = [];
  349. }
  350. /**
  351. * Called upon server disconnect.
  352. *
  353. * @private
  354. */
  355. ondisconnect() {
  356. debug("server disconnect (%s)", this.nsp);
  357. this.destroy();
  358. this.onclose("io server disconnect");
  359. }
  360. /**
  361. * Called upon forced client/server side disconnections,
  362. * this method ensures the manager stops tracking us and
  363. * that reconnections don't get triggered for this.
  364. *
  365. * @private
  366. */
  367. destroy() {
  368. if (this.subs) {
  369. // clean subscriptions to avoid reconnections
  370. this.subs.forEach((subDestroy) => subDestroy());
  371. this.subs = undefined;
  372. }
  373. this.io["_destroy"](this);
  374. }
  375. /**
  376. * Disconnects the socket manually.
  377. *
  378. * @return self
  379. * @public
  380. */
  381. disconnect() {
  382. if (this.connected) {
  383. debug("performing disconnect (%s)", this.nsp);
  384. this.packet({ type: PacketType.DISCONNECT });
  385. }
  386. // remove socket from pool
  387. this.destroy();
  388. if (this.connected) {
  389. // fire events
  390. this.onclose("io client disconnect");
  391. }
  392. return this;
  393. }
  394. /**
  395. * Alias for disconnect()
  396. *
  397. * @return self
  398. * @public
  399. */
  400. close() {
  401. return this.disconnect();
  402. }
  403. /**
  404. * Sets the compress flag.
  405. *
  406. * @param compress - if `true`, compresses the sending data
  407. * @return self
  408. * @public
  409. */
  410. compress(compress) {
  411. this.flags.compress = compress;
  412. return this;
  413. }
  414. /**
  415. * Sets a modifier for a subsequent event emission that the event message will be dropped when this socket is not
  416. * ready to send messages.
  417. *
  418. * @returns self
  419. * @public
  420. */
  421. get volatile() {
  422. this.flags.volatile = true;
  423. return this;
  424. }
  425. /**
  426. * Sets a modifier for a subsequent event emission that the callback will be called with an error when the
  427. * given number of milliseconds have elapsed without an acknowledgement from the server:
  428. *
  429. * ```
  430. * socket.timeout(5000).emit("my-event", (err) => {
  431. * if (err) {
  432. * // the server did not acknowledge the event in the given delay
  433. * }
  434. * });
  435. * ```
  436. *
  437. * @returns self
  438. * @public
  439. */
  440. timeout(timeout) {
  441. this.flags.timeout = timeout;
  442. return this;
  443. }
  444. /**
  445. * Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the
  446. * callback.
  447. *
  448. * @param listener
  449. * @public
  450. */
  451. onAny(listener) {
  452. this._anyListeners = this._anyListeners || [];
  453. this._anyListeners.push(listener);
  454. return this;
  455. }
  456. /**
  457. * Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the
  458. * callback. The listener is added to the beginning of the listeners array.
  459. *
  460. * @param listener
  461. * @public
  462. */
  463. prependAny(listener) {
  464. this._anyListeners = this._anyListeners || [];
  465. this._anyListeners.unshift(listener);
  466. return this;
  467. }
  468. /**
  469. * Removes the listener that will be fired when any event is emitted.
  470. *
  471. * @param listener
  472. * @public
  473. */
  474. offAny(listener) {
  475. if (!this._anyListeners) {
  476. return this;
  477. }
  478. if (listener) {
  479. const listeners = this._anyListeners;
  480. for (let i = 0; i < listeners.length; i++) {
  481. if (listener === listeners[i]) {
  482. listeners.splice(i, 1);
  483. return this;
  484. }
  485. }
  486. }
  487. else {
  488. this._anyListeners = [];
  489. }
  490. return this;
  491. }
  492. /**
  493. * Returns an array of listeners that are listening for any event that is specified. This array can be manipulated,
  494. * e.g. to remove listeners.
  495. *
  496. * @public
  497. */
  498. listenersAny() {
  499. return this._anyListeners || [];
  500. }
  501. }