@firebolt-js/sdk
Version:
The Firebolt JS SDK
291 lines (256 loc) • 8.36 kB
JavaScript
/*
* Copyright 2021 Comcast Cable Communications Management, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
import mock from './MockTransport.mjs'
import Queue from './queue.mjs'
import Settings, { initSettings } from '../Settings/index.mjs'
import LegacyTransport from './LegacyTransport.mjs'
import WebsocketTransport from './WebsocketTransport.mjs'
import Results from '../Results/index.mjs'
const LEGACY_TRANSPORT_SERVICE_NAME = 'com.comcast.BridgeObject_1'
let moduleInstance = null
const isEventSuccess = (x) =>
x && typeof x.event === 'string' && typeof x.listening === 'boolean'
const win = typeof window !== 'undefined' ? window : {}
export default class Transport {
constructor() {
this._promises = []
this._transport = null
this._id = 1
this._eventEmitters = []
this._eventIds = []
this._queue = new Queue()
this._deprecated = {}
this.isMock = false
}
static addEventEmitter(emitter) {
Transport.get()._eventEmitters.push(emitter)
}
static registerDeprecatedMethod(module, method, alternative) {
Transport.get()._deprecated[
module.toLowerCase() + '.' + method.toLowerCase()
] = {
alternative: alternative || '',
}
}
_endpoint() {
if (win.__firebolt && win.__firebolt.endpoint) {
return win.__firebolt.endpoint
}
return null
}
constructTransportLayer() {
let transport
const endpoint = this._endpoint()
if (
endpoint &&
(endpoint.startsWith('ws://') || endpoint.startsWith('wss://'))
) {
transport = new WebsocketTransport(endpoint)
transport.receive(this.receiveHandler.bind(this))
} else if (
typeof win.ServiceManager !== 'undefined' &&
win.ServiceManager &&
win.ServiceManager.version
) {
// Wire up the queue
transport = this._queue
// get the default bridge service, and flush the queue
win.ServiceManager.getServiceForJavaScript(
LEGACY_TRANSPORT_SERVICE_NAME,
(service) => {
if (LegacyTransport.isLegacy(service)) {
transport = new LegacyTransport(service)
} else {
transport = service
}
this.setTransportLayer(transport)
},
)
} else {
this.isMock = true
transport = mock
transport.receive(this.receiveHandler.bind(this))
}
return transport
}
setTransportLayer(tl) {
this._transport = tl
this._queue.flush(tl)
}
static send(module, method, params, transforms) {
/** Transport singleton across all SDKs to keep single id map */
return Transport.get()._send(module, method, params, transforms)
}
static listen(module, method, params, transforms) {
return Transport.get()._sendAndGetId(module, method, params, transforms)
}
_send(module, method, params, transforms) {
if (Array.isArray(module) && !method && !params) {
return this._batch(module)
} else {
return this._sendAndGetId(module, method, params, transforms).promise
}
}
_sendAndGetId(module, method, params, transforms) {
const { promise, json, id } = this._processRequest(
module,
method,
params,
transforms,
)
const msg = JSON.stringify(json)
if (Settings.getLogLevel() === 'DEBUG') {
console.debug('Sending message to transport: ' + msg)
}
this._transport.send(msg)
return { id, promise }
}
_batch(requests) {
const results = []
const json = []
requests.forEach(({ module, method, params, transforms }) => {
const result = this._processRequest(module, method, params, transforms)
results.push({
promise: result.promise,
id: result.id,
})
json.push(result.json)
})
const msg = JSON.stringify(json)
if (Settings.getLogLevel() === 'DEBUG') {
console.debug('Sending message to transport: ' + msg)
}
this._transport.send(msg)
return results
}
_processRequest(module, method, params, transforms) {
const p = this._addPromiseToQueue(module, method, params, transforms)
const json = this._createRequestJSON(module, method, params)
const result = {
promise: p,
json: json,
id: this._id,
}
this._id++
return result
}
_createRequestJSON(module, method, params) {
return {
jsonrpc: '2.0',
method: module.toLowerCase() + '.' + method,
params: params,
id: this._id,
}
}
_addPromiseToQueue(module, method, params, transforms) {
return new Promise((resolve, reject) => {
this._promises[this._id] = {}
this._promises[this._id].promise = this
this._promises[this._id].resolve = resolve
this._promises[this._id].reject = reject
this._promises[this._id].transforms = transforms
const deprecated =
this._deprecated[module.toLowerCase() + '.' + method.toLowerCase()]
if (deprecated) {
console.warn(
`WARNING: ${module}.${method}() is deprecated. ` +
deprecated.alternative,
)
}
// store the ID of the first listen for each event
// TODO: what about wild cards?
if (method.match(/^on[A-Z]/)) {
if (params.listen) {
this._eventIds.push(this._id)
} else {
this._eventIds = this._eventIds.filter((id) => id !== this._id)
}
}
})
}
/**
* If we have a global transport, use that. Otherwise, use the module-scoped transport instance.
* @returns {Transport}
*/
static get() {
/** Set up singleton and initialize it */
win.__firebolt = win.__firebolt || {}
if (win.__firebolt.transport == null && moduleInstance == null) {
const transport = new Transport()
transport.init()
if (transport.isMock) {
/** We should use the mock transport built with the SDK, not a global */
moduleInstance = transport
} else {
win.__firebolt = win.__firebolt || {}
win.__firebolt.transport = transport
}
win.__firebolt.setTransportLayer =
transport.setTransportLayer.bind(transport)
}
return win.__firebolt.transport ? win.__firebolt.transport : moduleInstance
}
receiveHandler(message) {
if (Settings.getLogLevel() === 'DEBUG') {
console.debug('Received message from transport: ' + message)
}
const json = JSON.parse(message)
const p = this._promises[json.id]
if (p) {
if (json.error) p.reject(json.error)
else {
// Do any module-specific transforms on the result
let result = json.result
if (p.transforms) {
if (Array.isArray(json.result)) {
result = result.map((x) => Results.transform(x, p.transforms))
} else {
result = Results.transform(result, p.transforms)
}
}
p.resolve(result)
}
delete this._promises[json.id]
}
// event responses need to be emitted, even after the listen call is resolved
if (this._eventIds.includes(json.id) && !isEventSuccess(json.result)) {
this._eventEmitters.forEach((emit) => {
emit(json.id, json.result)
})
}
}
init() {
initSettings({}, { log: true })
this._queue.receive(this.receiveHandler.bind(this))
if (win.__firebolt) {
if (win.__firebolt.mockTransportLayer === true) {
this.isMock = true
this.setTransportLayer(mock)
} else if (win.__firebolt.getTransportLayer) {
this.setTransportLayer(win.__firebolt.getTransportLayer())
}
}
if (this._transport == null) {
this._transport = this.constructTransportLayer()
}
}
}
win.__firebolt = win.__firebolt || {}
win.__firebolt.setTransportLayer = (transport) => {
Transport.get().setTransportLayer(transport)
}