Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 1 commit into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions packages/@uppy/companion-client/src/Socket.js
Original file line number Diff line number Diff line change
@@ -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 changes: 176 additions & 0 deletions packages/@uppy/companion-client/src/Socket.test.js
Original file line number Diff line number Diff line change
@@ -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 changes: 3 additions & 0 deletions packages/@uppy/companion-client/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'