Browse Source

@uppy/companion-client: revert breaking change (#4801)

Revert breaking change

This reverts parts of commit 17826da517f5a18d67f5e9f6cbe7c35e230818f5.
Antoine du Hamel 1 năm trước cách đây
mục cha
commit
a67f0ffdf2

+ 87 - 0
packages/@uppy/companion-client/src/Socket.js

@@ -0,0 +1,87 @@
+import ee from 'namespace-emitter'
+
+export default class UppySocket {
+  #queued = []
+
+  #emitter = ee()
+
+  #isOpen = false
+
+  #socket
+
+  constructor (opts) {
+    this.opts = opts
+
+    if (!opts || opts.autoOpen !== false) {
+      this.open()
+    }
+  }
+
+  get isOpen () { return this.#isOpen }
+
+  [Symbol.for('uppy test: getSocket')] () { return this.#socket }
+
+  [Symbol.for('uppy test: getQueued')] () { return this.#queued }
+
+  open () {
+    if (this.#socket != null) return
+
+    this.#socket = new WebSocket(this.opts.target)
+
+    this.#socket.onopen = () => {
+      this.#isOpen = true
+
+      while (this.#queued.length > 0 && this.#isOpen) {
+        const first = this.#queued.shift()
+        this.send(first.action, first.payload)
+      }
+    }
+
+    this.#socket.onclose = () => {
+      this.#isOpen = false
+      this.#socket = null
+    }
+
+    this.#socket.onmessage = this.#handleMessage
+  }
+
+  close () {
+    this.#socket?.close()
+  }
+
+  send (action, payload) {
+    // attach uuid
+
+    if (!this.#isOpen) {
+      this.#queued.push({ action, payload })
+      return
+    }
+
+    this.#socket.send(JSON.stringify({
+      action,
+      payload,
+    }))
+  }
+
+  on (action, handler) {
+    this.#emitter.on(action, handler)
+  }
+
+  emit (action, payload) {
+    this.#emitter.emit(action, payload)
+  }
+
+  once (action, handler) {
+    this.#emitter.once(action, handler)
+  }
+
+  #handleMessage = (e) => {
+    try {
+      const message = JSON.parse(e.data)
+      this.emit(message.action, message.payload)
+    } catch (err) {
+      // TODO: use a more robust error handler.
+      console.log(err) // eslint-disable-line no-console
+    }
+  }
+}

+ 176 - 0
packages/@uppy/companion-client/src/Socket.test.js

@@ -0,0 +1,176 @@
+import { afterEach, beforeEach, vi, describe, it, expect } from 'vitest'
+import UppySocket from './Socket.js'
+
+describe('Socket', () => {
+  let webSocketConstructorSpy
+  let webSocketCloseSpy
+  let webSocketSendSpy
+
+  beforeEach(() => {
+    webSocketConstructorSpy = vi.fn()
+    webSocketCloseSpy = vi.fn()
+    webSocketSendSpy = vi.fn()
+
+    globalThis.WebSocket = class WebSocket {
+      constructor (target) {
+        webSocketConstructorSpy(target)
+      }
+
+      // eslint-disable-next-line class-methods-use-this
+      close (args) {
+        webSocketCloseSpy(args)
+      }
+
+      // eslint-disable-next-line class-methods-use-this
+      send (json) {
+        webSocketSendSpy(json)
+      }
+
+      triggerOpen () {
+        this.onopen()
+      }
+
+      triggerClose () {
+        this.onclose()
+      }
+    }
+  })
+  afterEach(() => {
+    globalThis.WebSocket = undefined
+  })
+
+  it('should expose a class', () => {
+    expect(UppySocket.name).toEqual('UppySocket')
+    expect(
+      new UppySocket({
+        target: 'foo',
+      }) instanceof UppySocket,
+    )
+  })
+
+  it('should setup a new WebSocket', () => {
+    new UppySocket({ target: 'foo' }) // eslint-disable-line no-new
+    expect(webSocketConstructorSpy.mock.calls[0][0]).toEqual('foo')
+  })
+
+  it('should send a message via the websocket if the connection is open', () => {
+    const uppySocket = new UppySocket({ target: 'foo' })
+    const webSocketInstance = uppySocket[Symbol.for('uppy test: getSocket')]()
+    webSocketInstance.triggerOpen()
+
+    uppySocket.send('bar', 'boo')
+    expect(webSocketSendSpy.mock.calls.length).toEqual(1)
+    expect(webSocketSendSpy.mock.calls[0]).toEqual([
+      JSON.stringify({ action: 'bar', payload: 'boo' }),
+    ])
+  })
+
+  it('should queue the message for the websocket if the connection is not open', () => {
+    const uppySocket = new UppySocket({ target: 'foo' })
+
+    uppySocket.send('bar', 'boo')
+    expect(uppySocket[Symbol.for('uppy test: getQueued')]()).toEqual([{ action: 'bar', payload: 'boo' }])
+    expect(webSocketSendSpy.mock.calls.length).toEqual(0)
+  })
+
+  it('should queue any messages for the websocket if the connection is not open, then send them when the connection is open', () => {
+    const uppySocket = new UppySocket({ target: 'foo' })
+    const webSocketInstance = uppySocket[Symbol.for('uppy test: getSocket')]()
+
+    uppySocket.send('bar', 'boo')
+    uppySocket.send('moo', 'baa')
+    expect(uppySocket[Symbol.for('uppy test: getQueued')]()).toEqual([
+      { action: 'bar', payload: 'boo' },
+      { action: 'moo', payload: 'baa' },
+    ])
+    expect(webSocketSendSpy.mock.calls.length).toEqual(0)
+
+    webSocketInstance.triggerOpen()
+
+    expect(uppySocket[Symbol.for('uppy test: getQueued')]()).toEqual([])
+    expect(webSocketSendSpy.mock.calls.length).toEqual(2)
+    expect(webSocketSendSpy.mock.calls[0]).toEqual([
+      JSON.stringify({ action: 'bar', payload: 'boo' }),
+    ])
+    expect(webSocketSendSpy.mock.calls[1]).toEqual([
+      JSON.stringify({ action: 'moo', payload: 'baa' }),
+    ])
+  })
+
+  it('should start queuing any messages when the websocket connection is closed', () => {
+    const uppySocket = new UppySocket({ target: 'foo' })
+    const webSocketInstance = uppySocket[Symbol.for('uppy test: getSocket')]()
+    webSocketInstance.triggerOpen()
+    uppySocket.send('bar', 'boo')
+    expect(uppySocket[Symbol.for('uppy test: getQueued')]()).toEqual([])
+
+    webSocketInstance.triggerClose()
+    uppySocket.send('bar', 'boo')
+    expect(uppySocket[Symbol.for('uppy test: getQueued')]()).toEqual([{ action: 'bar', payload: 'boo' }])
+  })
+
+  it('should close the websocket when it is force closed', () => {
+    const uppySocket = new UppySocket({ target: 'foo' })
+    const webSocketInstance = uppySocket[Symbol.for('uppy test: getSocket')]()
+    webSocketInstance.triggerOpen()
+
+    uppySocket.close()
+    expect(webSocketCloseSpy.mock.calls.length).toEqual(1)
+  })
+
+  it('should be able to subscribe to messages received on the websocket', () => {
+    const uppySocket = new UppySocket({ target: 'foo' })
+    const webSocketInstance = uppySocket[Symbol.for('uppy test: getSocket')]()
+
+    const emitterListenerMock = vi.fn()
+    uppySocket.on('hi', emitterListenerMock)
+
+    webSocketInstance.triggerOpen()
+    webSocketInstance.onmessage({
+      data: JSON.stringify({ action: 'hi', payload: 'ho' }),
+    })
+    expect(emitterListenerMock.mock.calls).toEqual([
+      ['ho', undefined, undefined, undefined, undefined, undefined],
+    ])
+  })
+
+  it('should be able to emit messages and subscribe to them', () => {
+    const uppySocket = new UppySocket({ target: 'foo' })
+
+    const emitterListenerMock = vi.fn()
+    uppySocket.on('hi', emitterListenerMock)
+
+    uppySocket.emit('hi', 'ho')
+    uppySocket.emit('hi', 'ho')
+    uppySocket.emit('hi', 'off to work we go')
+
+    expect(emitterListenerMock.mock.calls).toEqual([
+      ['ho', undefined, undefined, undefined, undefined, undefined],
+      ['ho', undefined, undefined, undefined, undefined, undefined],
+      [
+        'off to work we go',
+        undefined,
+        undefined,
+        undefined,
+        undefined,
+        undefined,
+      ],
+    ])
+  })
+
+  it('should be able to subscribe to the first event for a particular action', () => {
+    const uppySocket = new UppySocket({ target: 'foo' })
+
+    const emitterListenerMock = vi.fn()
+    uppySocket.once('hi', emitterListenerMock)
+
+    uppySocket.emit('hi', 'ho')
+    uppySocket.emit('hi', 'ho')
+    uppySocket.emit('hi', 'off to work we go')
+
+    expect(emitterListenerMock.mock.calls.length).toEqual(1)
+    expect(emitterListenerMock.mock.calls).toEqual([
+      ['ho', undefined, undefined, undefined, undefined, undefined],
+    ])
+  })
+})

+ 3 - 0
packages/@uppy/companion-client/src/index.js

@@ -7,3 +7,6 @@
 export { default as RequestClient } from './RequestClient.js'
 export { default as Provider } from './Provider.js'
 export { default as SearchProvider } from './SearchProvider.js'
+
+// TODO: remove in the next major
+export { default as Socket } from './Socket.js'