@anycable/core
Version:
AnyCable JavaScript client library core functionality
165 lines (136 loc) • 3.89 kB
JavaScript
import { createNanoEvents } from 'nanoevents'
export class WebSocketTransport {
constructor(url, opts = {}) {
this.url = url
let Impl = opts.websocketImplementation
if (Impl) {
this.Impl = Impl
} else if (typeof WebSocket !== 'undefined') {
this.Impl = WebSocket
} else {
throw new Error('No WebSocket support')
}
this.connected = false
this.emitter = createNanoEvents()
let { format, subprotocol, authStrategy } = opts
this.format = format || 'text'
this.connectionOptions = opts.websocketOptions
this.authStrategy = authStrategy || 'param'
this.authProtocol = ''
this.subprotocol = subprotocol
}
displayName() {
return 'WebSocket(' + this.url + ')'
}
open() {
let protocols = this.subprotocol
if (this.authStrategy === 'sub-protocol') {
protocols = [this.subprotocol, this.authProtocol]
}
if (this.connectionOptions) {
this.ws = new this.Impl(this.url, protocols, this.connectionOptions)
} else {
this.ws = new this.Impl(this.url, protocols)
}
this.ws.binaryType = 'arraybuffer'
this.initListeners()
return new Promise((resolve, reject) => {
let unbind = []
unbind.push(
this.once('open', () => {
unbind.forEach(clbk => clbk())
resolve()
})
)
unbind.push(
this.once('close', () => {
unbind.forEach(clbk => clbk())
reject(Error('WS connection closed'))
})
)
})
}
setURL(url) {
this.url = url
}
setParam(key, val) {
let url = new URL(this.url)
url.searchParams.set(key, val)
let newURL = `${url.protocol}//${url.host}${url.pathname}?${url.searchParams}`
this.setURL(newURL)
}
setToken(val, key = 'jid') {
if (this.authStrategy === 'param') {
this.setParam(key, val)
} else if (this.authStrategy === 'header') {
this.connectionOptions = this.connectionOptions || {}
this.connectionOptions.headers = this.connectionOptions.headers || {}
let authHeaderKey = `x-${key}`.toLowerCase()
// find existing auth header key (it could have a different case)
let existingKey = Object.keys(this.connectionOptions.headers).find(
k => k.toLowerCase() === authHeaderKey
)
authHeaderKey = existingKey || authHeaderKey
this.connectionOptions.headers[authHeaderKey] = val
} else if (this.authStrategy === 'sub-protocol') {
this.authProtocol = `anycable-token.${val}`
} else {
throw new Error('Unknown auth strategy: ' + this.authStrategy)
}
}
send(data) {
if (!this.ws || !this.connected) {
throw Error('WebSocket is not connected')
} else {
this.ws.send(data)
}
}
close() {
if (this.ws) {
this.onclose()
} else {
this.connected = false
}
}
on(event, callback) {
return this.emitter.on(event, callback)
}
once(event, callback) {
let unbind = this.emitter.on(event, (...args) => {
unbind()
callback(...args)
})
return unbind
}
initListeners() {
this.ws.onerror = event => {
// Only emit errors if the socket hasn't been closed
if (this.connected) {
this.emitter.emit('error', event.error || new Error('WS Error'))
}
}
this.ws.onclose = () => {
this.onclose()
}
this.ws.onmessage = event => {
let data = event.data
if (this.format === 'binary') {
data = new Uint8Array(data)
}
this.emitter.emit('data', data)
}
this.ws.onopen = () => {
this.connected = true
this.emitter.emit('open')
}
}
onclose() {
this.ws.onclose = undefined
this.ws.onmessage = undefined
this.ws.onopen = undefined
this.ws.close()
delete this.ws
this.connected = false
this.emitter.emit('close')
}
}