@anycable/core
Version:
AnyCable JavaScript client library core functionality
205 lines (171 loc) • 5.17 kB
JavaScript
import { Cable } from '../cable/index.js'
import { ActionCableProtocol } from '../action_cable/index.js'
import { ActionCableExtendedProtocol } from '../action_cable_ext/index.js'
import { JSONEncoder } from '../encoder/index.js'
import { NoopLogger } from '../logger/index.js'
import { WebSocketTransport } from '../websocket/index.js'
import { Monitor, backoffWithJitter } from '../monitor/index.js'
import { FallbackTransport } from '../transport/index.js'
import { ActionCableConsumer } from '../action_cable_compat/index.js'
export const DEFAULT_OPTIONS = {
protocol: 'actioncable-v1-json',
pingInterval: 3000,
maxReconnectAttempts: Infinity,
maxMissingPings: 2,
logLevel: 'warn',
lazy: true
}
export function createCable(url, opts) {
if (typeof url === 'object' && typeof opts === 'undefined') {
opts = url
url = undefined
}
opts = opts || {}
if (!url && !opts.transport) throw Error('URL or transport must be specified')
opts = Object.assign({}, DEFAULT_OPTIONS, opts)
let {
protocol,
websocketImplementation,
websocketFormat,
websocketOptions,
websocketAuthStrategy,
fallbacks,
logLevel,
logger,
transport,
encoder,
lazy,
monitor,
pingInterval,
reconnectStrategy,
maxMissingPings,
maxReconnectAttempts,
subprotocol,
tokenRefresher,
historyTimestamp,
protocolOptions,
concurrentSubscribes,
performFailures,
transportConfigurator,
auth
} = opts
logger = logger || new NoopLogger(logLevel)
if (typeof protocol === 'string') {
subprotocol = subprotocol || protocol
// split protocol into two parts by last '-' symbol
// e.g. actioncable-v1-json -> actioncable-v1, json
let protocolName = protocol.substring(0, protocol.lastIndexOf('-'))
let protocolEncoderName = protocol.substring(protocol.lastIndexOf('-') + 1)
protocolOptions = protocolOptions || {}
if (protocolName === 'actioncable-v1') {
protocol = new ActionCableProtocol({ logger, ...protocolOptions })
} else if (protocolName === 'actioncable-v1-ext') {
protocol = new ActionCableExtendedProtocol({
logger,
historyTimestamp,
...protocolOptions
})
} else {
throw Error(`Protocol is not supported yet: ${protocol}`)
}
if (protocolEncoderName === 'json') {
encoder = encoder || new JSONEncoder()
websocketFormat = websocketFormat || 'text'
} else if (protocolEncoderName === 'msgpack') {
websocketFormat = 'binary'
if (!encoder) {
throw Error(
'Msgpack encoder must be specified explicitly. Use `@anycable/msgpack-encoder` package or build your own'
)
}
} else if (protocolEncoderName === 'protobuf') {
websocketFormat = websocketFormat || 'binary'
if (!encoder) {
throw Error(
'Protobuf encoder must be specified explicitly. Use `@anycable/protobuf-encoder` package or build your own'
)
}
} else {
throw Error(`Protocol is not supported yet: ${protocol}`)
}
}
if (!protocol) throw Error('Protocol must be specified')
transport =
transport ||
new WebSocketTransport(url, {
websocketImplementation,
websocketOptions,
subprotocol,
authStrategy: websocketAuthStrategy,
format: websocketFormat
})
if (fallbacks) {
transport = new FallbackTransport([transport, ...fallbacks], { logger })
}
if (auth && auth.token) {
transport.setToken(auth.token, auth.param || 'jid')
}
reconnectStrategy = reconnectStrategy || backoffWithJitter(pingInterval)
if (monitor !== false) {
monitor =
monitor ||
new Monitor({
pingInterval,
reconnectStrategy,
maxMissingPings,
maxReconnectAttempts,
logger
})
}
let hubOptions = { concurrentSubscribes }
let cable = new Cable({
protocol,
transport,
encoder,
logger,
lazy,
hubOptions,
performFailures,
transportConfigurator
})
if (monitor) {
monitor.watch(cable)
cable.monitor = monitor
}
if (tokenRefresher) {
watchForExpiredToken(cable, async () => {
try {
await tokenRefresher(transport)
} catch (err) {
logger.error('Failed to refresh authentication token: ' + err)
return false
}
// Initiate cable connection.
// No need to wait for it to complete, it could
// fail due to network errors (which is not our responsibility)
cable.connect().catch(() => {})
return true
})
}
return cable
}
function watchForExpiredToken(cable, callback) {
let attempted = false
cable.on('connect', () => (attempted = false))
cable.on('close', async ev => {
if (!ev) return
// If we closed by server two times in a row
if (attempted) {
cable.logger.warn('Token auto-refresh is disabled', ev)
return
}
if (ev.reason === 'token_expired') {
attempted = true
await callback()
}
})
}
export function createConsumer(url, opts) {
let cable = createCable(url, opts)
return new ActionCableConsumer(cable)
}