websocket.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. import { Transport } from "../transport.js";
  2. import { yeast } from "../contrib/yeast.js";
  3. import { pick } from "../util.js";
  4. import { nextTick, usingBrowserWebSocket, WebSocket, } from "./websocket-constructor.js";
  5. import debugModule from "debug"; // debug()
  6. import { encodePacket } from "engine.io-parser";
  7. const debug = debugModule("engine.io-client:websocket"); // debug()
  8. // detect ReactNative environment
  9. const isReactNative = typeof navigator !== "undefined" &&
  10. typeof navigator.product === "string" &&
  11. navigator.product.toLowerCase() === "reactnative";
  12. export class WS extends Transport {
  13. /**
  14. * WebSocket transport constructor.
  15. *
  16. * @param {Object} opts - connection options
  17. * @protected
  18. */
  19. constructor(opts) {
  20. super(opts);
  21. this.supportsBinary = !opts.forceBase64;
  22. }
  23. get name() {
  24. return "websocket";
  25. }
  26. doOpen() {
  27. if (!this.check()) {
  28. // let probe timeout
  29. return;
  30. }
  31. const uri = this.uri();
  32. const protocols = this.opts.protocols;
  33. // React Native only supports the 'headers' option, and will print a warning if anything else is passed
  34. const opts = isReactNative
  35. ? {}
  36. : pick(this.opts, "agent", "perMessageDeflate", "pfx", "key", "passphrase", "cert", "ca", "ciphers", "rejectUnauthorized", "localAddress", "protocolVersion", "origin", "maxPayload", "family", "checkServerIdentity");
  37. if (this.opts.extraHeaders) {
  38. opts.headers = this.opts.extraHeaders;
  39. }
  40. try {
  41. this.ws =
  42. usingBrowserWebSocket && !isReactNative
  43. ? protocols
  44. ? new WebSocket(uri, protocols)
  45. : new WebSocket(uri)
  46. : new WebSocket(uri, protocols, opts);
  47. }
  48. catch (err) {
  49. return this.emitReserved("error", err);
  50. }
  51. this.ws.binaryType = this.socket.binaryType;
  52. this.addEventListeners();
  53. }
  54. /**
  55. * Adds event listeners to the socket
  56. *
  57. * @private
  58. */
  59. addEventListeners() {
  60. this.ws.onopen = () => {
  61. if (this.opts.autoUnref) {
  62. this.ws._socket.unref();
  63. }
  64. this.onOpen();
  65. };
  66. this.ws.onclose = (closeEvent) => this.onClose({
  67. description: "websocket connection closed",
  68. context: closeEvent,
  69. });
  70. this.ws.onmessage = (ev) => this.onData(ev.data);
  71. this.ws.onerror = (e) => this.onError("websocket error", e);
  72. }
  73. write(packets) {
  74. this.writable = false;
  75. // encodePacket efficient as it uses WS framing
  76. // no need for encodePayload
  77. for (let i = 0; i < packets.length; i++) {
  78. const packet = packets[i];
  79. const lastPacket = i === packets.length - 1;
  80. encodePacket(packet, this.supportsBinary, (data) => {
  81. // always create a new object (GH-437)
  82. const opts = {};
  83. if (!usingBrowserWebSocket) {
  84. if (packet.options) {
  85. opts.compress = packet.options.compress;
  86. }
  87. if (this.opts.perMessageDeflate) {
  88. const len =
  89. // @ts-ignore
  90. "string" === typeof data ? Buffer.byteLength(data) : data.length;
  91. if (len < this.opts.perMessageDeflate.threshold) {
  92. opts.compress = false;
  93. }
  94. }
  95. }
  96. // Sometimes the websocket has already been closed but the browser didn't
  97. // have a chance of informing us about it yet, in that case send will
  98. // throw an error
  99. try {
  100. if (usingBrowserWebSocket) {
  101. // TypeError is thrown when passing the second argument on Safari
  102. this.ws.send(data);
  103. }
  104. else {
  105. this.ws.send(data, opts);
  106. }
  107. }
  108. catch (e) {
  109. debug("websocket closed before onclose event");
  110. }
  111. if (lastPacket) {
  112. // fake drain
  113. // defer to next tick to allow Socket to clear writeBuffer
  114. nextTick(() => {
  115. this.writable = true;
  116. this.emitReserved("drain");
  117. }, this.setTimeoutFn);
  118. }
  119. });
  120. }
  121. }
  122. doClose() {
  123. if (typeof this.ws !== "undefined") {
  124. this.ws.close();
  125. this.ws = null;
  126. }
  127. }
  128. /**
  129. * Generates uri for connection.
  130. *
  131. * @private
  132. */
  133. uri() {
  134. const schema = this.opts.secure ? "wss" : "ws";
  135. const query = this.query || {};
  136. // append timestamp to URI
  137. if (this.opts.timestampRequests) {
  138. query[this.opts.timestampParam] = yeast();
  139. }
  140. // communicate binary support capabilities
  141. if (!this.supportsBinary) {
  142. query.b64 = 1;
  143. }
  144. return this.createUri(schema, query);
  145. }
  146. /**
  147. * Feature detection for WebSocket.
  148. *
  149. * @return {Boolean} whether this transport is available.
  150. * @private
  151. */
  152. check() {
  153. return !!WebSocket;
  154. }
  155. }