websocket.js 5.2 KB

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