SocketIoShim.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. /* global io */
  2. import { debugConsole } from '@/utils/debugging'
  3. import EventEmitter from '@/utils/EventEmitter'
  4. class SocketShimBase {
  5. static connect(url, options) {
  6. return new SocketShimBase()
  7. }
  8. constructor(socket) {
  9. this._socket = socket
  10. }
  11. forceDisconnectWithoutEvent() {}
  12. }
  13. const transparentMethods = [
  14. 'connect',
  15. 'disconnect',
  16. 'emit',
  17. 'on',
  18. 'removeListener',
  19. ]
  20. for (const method of transparentMethods) {
  21. SocketShimBase.prototype[method] = function () {
  22. this._socket[method].apply(this._socket, arguments)
  23. }
  24. }
  25. class SocketShimNoop extends SocketShimBase {
  26. static connect() {
  27. return new SocketShimNoop()
  28. }
  29. constructor(socket) {
  30. super(socket)
  31. this.socket = {
  32. get connected() {
  33. return false
  34. },
  35. get sessionid() {
  36. return undefined
  37. },
  38. get transport() {
  39. return {}
  40. },
  41. connect() {},
  42. disconnect(reason) {},
  43. }
  44. }
  45. connect() {}
  46. disconnect(reason) {}
  47. emit() {}
  48. on() {}
  49. removeListener() {}
  50. }
  51. class SocketShimV0 extends SocketShimBase {
  52. static connect(url, options) {
  53. return new SocketShimV0(io.connect(url, options))
  54. }
  55. constructor(socket) {
  56. super(socket)
  57. this.socket = this._socket.socket
  58. const self = this
  59. Object.defineProperty(this.socket, 'transport', {
  60. get() {
  61. return self._transport
  62. },
  63. set(v) {
  64. self.forceDisconnectWithoutEvent()
  65. self._transport = v
  66. },
  67. })
  68. }
  69. forceDisconnectWithoutEvent() {
  70. clearTimeout(this.socket.heartbeatTimeoutTimer)
  71. if (this._transport) this.forceCloseTransport(this._transport)
  72. }
  73. forceCloseTransport(transport) {
  74. transport.clearTimeouts()
  75. if (transport instanceof io.Transport.websocket) {
  76. // retry closing
  77. transport.websocket.onopen = transport.websocket.onmessage = () =>
  78. transport.websocket.close()
  79. // mute close/error handler
  80. transport.websocket.onclose = transport.websocket.onerror = () => {}
  81. // disconnect
  82. try {
  83. transport.websocket.close()
  84. } catch {}
  85. } else if (transport instanceof io.Transport['xhr-polling']) {
  86. // mute data/close handler and block new polling GET requests
  87. transport.onData = transport.onClose = transport.get = () => {}
  88. // abort pending long-polling/POST request
  89. for (const xhr of [transport.xhr, transport.sendXHR]) {
  90. if (!xhr) continue // not pending
  91. // mute xhr callbacks
  92. xhr.onreadystatechange = xhr.onload = xhr.onerror = () => {}
  93. try {
  94. xhr.abort()
  95. } catch {}
  96. }
  97. transport.xhr = transport.sendXHR = null
  98. // Mark long-polling client as disconnected to avoid "ghost" connected client.
  99. fetch(transport.prepareUrl() + '/?disconnect=1', {
  100. // Let the request continue after navigating away from or reloading the page.
  101. keepalive: true,
  102. })
  103. // Avoid leaving a dangling response on the wire.
  104. .then(res => res.text())
  105. .catch(() => {})
  106. } else {
  107. try {
  108. transport.close()
  109. } catch {}
  110. debugConsole.warn('unexpected socket.io transport', transport)
  111. }
  112. }
  113. }
  114. class SocketShimV2 extends SocketShimBase {
  115. static connect(url, options) {
  116. options.forceNew = options['force new connection']
  117. // .resource has no leading slash, path wants to see one.
  118. options.path = '/' + options.resource
  119. options.reconnection = options.reconnect
  120. options.timeout = options['connect timeout']
  121. return new SocketShimV2(url, options)
  122. }
  123. static get EVENT_MAP() {
  124. // Use the v2 event names transparently to the frontend.
  125. const connectionFailureEvents = [
  126. 'connect_error',
  127. 'connect_timeout',
  128. 'error',
  129. ]
  130. return new Map([
  131. ['connect_failed', connectionFailureEvents],
  132. ['error', connectionFailureEvents],
  133. ])
  134. }
  135. _on(event, handler) {
  136. // Keep track of our event listeners.
  137. // We move them to a new socket in ._replaceSocketWithNewInstance()
  138. if (!this._events.has(event)) {
  139. this._events.set(event, [handler])
  140. } else {
  141. this._events.get(event).push(handler)
  142. }
  143. this._socket.on(event, handler)
  144. }
  145. on(event, handler) {
  146. if (SocketShimV2.EVENT_MAP.has(event)) {
  147. for (const v2Event of SocketShimV2.EVENT_MAP.get(event)) {
  148. this._on(v2Event, handler)
  149. }
  150. } else {
  151. this._on(event, handler)
  152. }
  153. }
  154. _removeListener(event, handler) {
  155. // Keep track of our event listeners.
  156. // We move them to a new socket in ._replaceSocketWithNewInstance()
  157. if (this._events.has(event)) {
  158. const listeners = this._events.get(event)
  159. const pos = listeners.indexOf(handler)
  160. if (pos !== -1) {
  161. listeners.splice(pos, 1)
  162. }
  163. }
  164. this._socket.removeListener(event, handler)
  165. }
  166. removeListener(event, handler) {
  167. if (SocketShimV2.EVENT_MAP.has(event)) {
  168. for (const v2Event of SocketShimV2.EVENT_MAP.get(event)) {
  169. this._removeListener(v2Event, handler)
  170. }
  171. } else {
  172. this._removeListener(event, handler)
  173. }
  174. }
  175. static createNewSocket(url, options) {
  176. // open a brand new connection for the default namespace '/'
  177. // The old socket can still leak 'disconnect' events from the teardown
  178. // of the old transport. The leaking 'disconnect' events interfere with
  179. // the _new_ connection and cancel the new connect attempt.
  180. // Also skip the caching in these locations:
  181. // - `io.connect()` caches `io.Manager`s in `io.managers`
  182. // - `io.Manager().socket()` caches `io.Socket`s in its `this.nsps`
  183. return io.Manager(url, options).socket('/', options)
  184. }
  185. _replaceSocketWithNewInstance() {
  186. const oldSocket = this._socket
  187. const newSocket = SocketShimV2.createNewSocket(this._url, this._options)
  188. // move our existing event handlers to the new socket
  189. this._events.forEach((listeners, event) => {
  190. for (const listener of listeners) {
  191. oldSocket.removeListener(event, listener)
  192. newSocket.on(event, listener)
  193. }
  194. })
  195. if (oldSocket.connected) {
  196. // We overwrite the reference to oldSocket soon.
  197. // Make sure we are disconnected.
  198. oldSocket.disconnect()
  199. }
  200. this._socket = newSocket
  201. }
  202. connect() {
  203. // have the same logic behind socket.connect and socket.socket.connect
  204. this._replaceSocketWithNewInstance()
  205. }
  206. constructor(url, options) {
  207. super(SocketShimV2.createNewSocket(url, options))
  208. this._url = url
  209. this._options = options
  210. this._events = new Map()
  211. const self = this
  212. function _getEngine() {
  213. return (self._socket.io && self._socket.io.engine) || {}
  214. }
  215. this.socket = {
  216. get connected() {
  217. return self._socket.connected
  218. },
  219. get sessionid() {
  220. if (self._socket.id) {
  221. return self._socket.id
  222. }
  223. // socket.id is discarded upon disconnect
  224. // the id is still available in the internal state
  225. return _getEngine().id
  226. },
  227. get transport() {
  228. return _getEngine().transport
  229. },
  230. connect() {
  231. self._replaceSocketWithNewInstance()
  232. },
  233. disconnect(reason) {
  234. return self._socket.disconnect(reason)
  235. },
  236. }
  237. }
  238. }
  239. let current
  240. if (typeof io === 'undefined' || !io) {
  241. debugConsole.log('[socket.io] Shim: socket.io is not loaded, returning noop')
  242. current = SocketShimNoop
  243. } else if (typeof io.version === 'string' && io.version.slice(0, 1) === '0') {
  244. debugConsole.log('[socket.io] Shim: detected v0')
  245. current = SocketShimV0
  246. } else {
  247. // socket.io v2 does not have a global io.version attribute.
  248. debugConsole.log('[socket.io] Shim: detected v2')
  249. current = SocketShimV2
  250. }
  251. export class SocketIOMock extends SocketShimBase {
  252. constructor() {
  253. super(new EventEmitter())
  254. this.socket = {
  255. get connected() {
  256. return false
  257. },
  258. get sessionid() {
  259. return undefined
  260. },
  261. get transport() {
  262. return {}
  263. },
  264. get transports() {
  265. return []
  266. },
  267. connect() {},
  268. disconnect(reason) {},
  269. }
  270. }
  271. addListener(event, listener) {
  272. this._socket.on(event, listener)
  273. }
  274. removeListener(event, listener) {
  275. this._socket.off(event, listener)
  276. }
  277. disconnect() {
  278. this.emitToClient('disconnect')
  279. }
  280. emitToClient(...args) {
  281. // Round-trip through JSON.parse/stringify to simulate (de-)serializing on network layer.
  282. this.emit(...JSON.parse(JSON.stringify(args)))
  283. }
  284. countEventListeners(event) {
  285. return this._socket.events[event].length
  286. }
  287. }
  288. export default {
  289. SocketShimNoop,
  290. SocketShimV0,
  291. SocketShimV2,
  292. current,
  293. connect: current.connect,
  294. stub: () => new SocketShimNoop(),
  295. }