Explorar o código

Merge pull request #2677 from overleaf/jpa-socket-io-v2-shim

[socket.io] add support for v2 using a compatibility shim

GitOrigin-RevId: ef52a3d8e4fdf191f6a8c1a530dfce25f3a11466
Jakob Ackermann %!s(int64=6) %!d(string=hai) anos
pai
achega
a0359009f9

+ 3 - 3
services/web/frontend/js/ide/connection/ConnectionManager.js

@@ -10,7 +10,7 @@
 
 /* global io */
 
-define([], function() {
+define(['./SocketIoShim'], function(SocketIoShim) {
   let ConnectionManager
   const ONEHOUR = 1000 * 60 * 60
 
@@ -43,7 +43,7 @@ define([], function() {
           console.error(
             'Socket.io javascript not loaded. Please check that the real-time service is running and accessible.'
           )
-          this.ide.socket = { on() {} }
+          this.ide.socket = SocketIoShim.stub()
           this.$scope.$apply(() => {
             return (this.$scope.state.error =
               'Could not connect to websocket server :(')
@@ -116,7 +116,7 @@ define([], function() {
             pathname: '/socket.io'
           }
         }
-        this.ide.socket = io.connect(
+        this.ide.socket = SocketIoShim.connect(
           parsedURL.origin,
           {
             resource: parsedURL.pathname.slice(1),

+ 224 - 0
services/web/frontend/js/ide/connection/SocketIoShim.js

@@ -0,0 +1,224 @@
+/* global io */
+
+define([], function() {
+  class SocketShimBase {
+    static connect(url, options) {
+      return new SocketShimBase()
+    }
+    constructor(socket) {
+      this._socket = socket
+    }
+  }
+  const transparentMethods = [
+    'connect',
+    'disconnect',
+    'emit',
+    'on',
+    'removeListener'
+  ]
+  for (let method of transparentMethods) {
+    SocketShimBase.prototype[method] = function() {
+      this._socket[method].apply(this._socket, arguments)
+    }
+  }
+
+  class SocketShimNoop extends SocketShimBase {
+    static connect() {
+      return new SocketShimNoop()
+    }
+    constructor(socket) {
+      super(socket)
+      this.socket = {
+        get connected() {
+          return false
+        },
+        get sessionid() {
+          return undefined
+        },
+        get transport() {
+          return {}
+        },
+
+        connect() {},
+        disconnect(reason) {}
+      }
+    }
+    connect() {}
+    disconnect(reason) {}
+    emit() {}
+    on() {}
+    removeListener() {}
+  }
+
+  class SocketShimV0 extends SocketShimBase {
+    static connect(url, options) {
+      return new SocketShimV0(
+        io.connect(
+          url,
+          options
+        )
+      )
+    }
+    constructor(socket) {
+      super(socket)
+      this.socket = this._socket.socket
+    }
+  }
+
+  class SocketShimV2 extends SocketShimBase {
+    static connect(url, options) {
+      options.forceNew = options['force new connection']
+      // .resource has no leading slash, path wants to see one.
+      options.path = '/' + options.resource
+      options.reconnection = options.reconnect
+      options.timeout = options['connect timeout']
+      return new SocketShimV2(url, options)
+    }
+    static get EVENT_MAP() {
+      // Use the v2 event names transparently to the frontend.
+      const connectionFailureEvents = [
+        'connect_error',
+        'connect_timeout',
+        'error'
+      ]
+      return new Map([
+        ['connect_failed', connectionFailureEvents],
+        ['error', connectionFailureEvents]
+      ])
+    }
+    _on(event, handler) {
+      // Keep track of our event listeners.
+      // We move them to a new socket in ._replaceSocketWithNewInstance()
+      if (!this._events.has(event)) {
+        this._events.set(event, [handler])
+      } else {
+        this._events.get(event).push(handler)
+      }
+      this._socket.on(event, handler)
+    }
+    on(event, handler) {
+      if (SocketShimV2.EVENT_MAP.has(event)) {
+        for (const v2Event of SocketShimV2.EVENT_MAP.get(event)) {
+          this._on(v2Event, handler)
+        }
+      } else {
+        this._on(event, handler)
+      }
+    }
+    _removeListener(event, handler) {
+      // Keep track of our event listeners.
+      // We move them to a new socket in ._replaceSocketWithNewInstance()
+      if (this._events.has(event)) {
+        const listeners = this._events.get(event)
+        const pos = listeners.indexOf(handler)
+        if (pos !== -1) {
+          listeners.splice(pos, 1)
+        }
+      }
+      this._socket.removeListener(event, handler)
+    }
+    removeListener(event, handler) {
+      if (SocketShimV2.EVENT_MAP.has(event)) {
+        for (const v2Event of SocketShimV2.EVENT_MAP.get(event)) {
+          this._removeListener(v2Event, handler)
+        }
+      } else {
+        this._removeListener(event, handler)
+      }
+    }
+
+    static createNewSocket(url, options) {
+      // open a brand new connection for the default namespace '/'
+      // The old socket can still leak 'disconnect' events from the teardown
+      //  of the old transport. The leaking 'disconnect' events interfere with
+      //  the _new_ connection and cancel the new connect attempt.
+      // Also skip the caching in these locations:
+      // - `io.connect()` caches `io.Manager`s in `io.managers`
+      // - `io.Manager().socket()` caches `io.Socket`s in its `this.nsps`
+      return io.Manager(url, options).socket('/', options)
+    }
+
+    _replaceSocketWithNewInstance() {
+      const oldSocket = this._socket
+      const newSocket = SocketShimV2.createNewSocket(this._url, this._options)
+
+      // move our existing event handlers to the new socket
+      this._events.forEach((listeners, event) => {
+        for (const listener of listeners) {
+          oldSocket.removeListener(event, listener)
+          newSocket.on(event, listener)
+        }
+      })
+
+      if (oldSocket.connected) {
+        // We overwrite the reference to oldSocket soon.
+        // Make sure we are disconnected.
+        oldSocket.disconnect()
+      }
+      this._socket = newSocket
+    }
+
+    connect() {
+      // have the same logic behind socket.connect and socket.socket.connect
+      this._replaceSocketWithNewInstance()
+    }
+
+    constructor(url, options) {
+      super(SocketShimV2.createNewSocket(url, options))
+      this._url = url
+      this._options = options
+      this._events = new Map()
+
+      const self = this
+      function _getEngine() {
+        return (self._socket.io && self._socket.io.engine) || {}
+      }
+
+      this.socket = {
+        get connected() {
+          return self._socket.connected
+        },
+        get sessionid() {
+          if (self._socket.id) {
+            return self._socket.id
+          }
+          // socket.id is discarded upon disconnect
+          // the id is still available in the internal state
+          return _getEngine().id
+        },
+        get transport() {
+          return _getEngine().transport
+        },
+
+        connect() {
+          self._replaceSocketWithNewInstance()
+        },
+        disconnect(reason) {
+          return self._socket.disconnect(reason)
+        }
+      }
+    }
+  }
+
+  let current
+  if (typeof io === 'undefined' || !io) {
+    sl_console.log('[socket.io] Shim: socket.io is not loaded, returning noop')
+    current = SocketShimNoop
+  } else if (typeof io.version === 'string' && io.version.slice(0, 1) === '0') {
+    sl_console.log('[socket.io] Shim: detected v0')
+    current = SocketShimV0
+  } else {
+    // socket.io v2 does not have a global io.version attribute.
+    sl_console.log('[socket.io] Shim: detected v2')
+    current = SocketShimV2
+  }
+
+  return {
+    SocketShimNoop,
+    SocketShimV0,
+    SocketShimV2,
+    current,
+    connect: current.connect,
+    stub: () => new SocketShimNoop()
+  }
+})