UNPKG

@aeternity/aepp-sdk

Version:
218 lines (195 loc) 6.52 kB
/* * ISC License (ISC) * Copyright (c) 2018 aeternity developers * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. */ import { w3cwebsocket as W3CWebSocket } from 'websocket' import { EventEmitter } from 'events' import * as R from 'ramda' import { pascalToSnake } from '../utils/string' import { awaitingConnection } from './handlers' const options = new WeakMap() const status = new WeakMap() const state = new WeakMap() const fsm = new WeakMap() const websockets = new WeakMap() const eventEmitters = new WeakMap() const messageQueue = new WeakMap() const messageQueueLocked = new WeakMap() const actionQueue = new WeakMap() const actionQueueLocked = new WeakMap() const sequence = new WeakMap() const channelId = new WeakMap() const rpcCallbacks = new WeakMap() function channelURL (url, params) { const paramString = R.join('&', R.values(R.mapObjIndexed((value, key) => `${pascalToSnake(key)}=${encodeURIComponent(value)}`, params))) return `${url}?${paramString}` } function emit (channel, ...args) { eventEmitters.get(channel).emit(...args) } function enterState (channel, nextState) { if (!nextState) { throw new Error('State Channels FSM entered unknown state') } fsm.set(channel, nextState) if (nextState.handler.enter) { nextState.handler.enter(channel) } dequeueAction(channel) } function changeStatus (channel, newStatus) { const prevStatus = status.get(channel) if (newStatus !== prevStatus) { status.set(channel, newStatus) emit(channel, 'statusChanged', newStatus) } } function changeState (channel, newState) { state.set(channel, newState) emit(channel, 'stateChanged', newState) } function send (channel, message) { websockets.get(channel).send(JSON.stringify(message, undefined, 2)) } function enqueueAction (channel, guard, action) { actionQueue.set(channel, [ ...actionQueue.get(channel) || [], { guard, action } ]) dequeueAction(channel) } async function dequeueAction (channel) { const locked = actionQueueLocked.get(channel) const queue = actionQueue.get(channel) || [] if (locked || !queue.length) { return } const state = fsm.get(channel) const index = queue.findIndex(item => item.guard(channel, state)) if (index === -1) { return } actionQueue.set(channel, queue.filter((_, i) => index !== i)) actionQueueLocked.set(channel, true) const nextState = await Promise.resolve(queue[index].action(channel, state)) actionQueueLocked.set(channel, false) enterState(channel, nextState) } async function handleMessage (channel, message) { const { handler, state } = fsm.get(channel) enterState(channel, await Promise.resolve(handler(channel, message, state))) } async function dequeueMessage (channel) { const queue = messageQueue.get(channel) if (messageQueueLocked.get(channel) || !queue.length) { return } const [message, ...remaining] = queue messageQueue.set(channel, remaining || []) messageQueueLocked.set(channel, true) await handleMessage(channel, message) messageQueueLocked.set(channel, false) dequeueMessage(channel) } function onMessage (channel, data) { const message = JSON.parse(data) if (message.id) { const callback = rpcCallbacks.get(channel).get(message.id) try { callback(message) } finally { rpcCallbacks.get(channel).delete(message.id) } } else if (message.method === 'channels.message') { emit(channel, 'message', message.params.data.message) } else { messageQueue.set(channel, [...(messageQueue.get(channel) || []), message]) dequeueMessage(channel) } } function wrapCallErrorMessage (message) { const [{ message: details } = {}] = message.error.data || [] if (details) { return Error(`${message.error.message}: ${details}`) } return Error(message.error.message) } function call (channel, method, params) { return new Promise((resolve, reject) => { const id = sequence.set(channel, sequence.get(channel) + 1).get(channel) rpcCallbacks.get(channel).set(id, (message) => { if (message.result) return resolve(message.result) if (message.error) return reject(wrapCallErrorMessage(message)) }) send(channel, { jsonrpc: '2.0', method, id, params }) }) } function disconnect (channel) { const ws = websockets.get(channel) if (ws.readyState === ws.OPEN) { ws._connection.close() } } function WebSocket (url, callbacks) { function fireOnce (target, key, always) { target[key] = (...args) => { always(...args) target[key] = callbacks[key] if (typeof target === 'function') { target(...args) } } } return new Promise((resolve, reject) => { const ws = new W3CWebSocket(url) // eslint-disable-next-line no-return-assign Object.entries(callbacks).forEach(([key, callback]) => ws[key] = callback) fireOnce(ws, 'onopen', () => resolve(ws)) fireOnce(ws, 'onerror', (err) => reject(err)) }) } async function initialize (channel, channelOptions) { const optionsKeys = ['sign', 'url'] const params = R.pickBy(key => !optionsKeys.includes(key), channelOptions) const { url } = channelOptions const wsUrl = channelURL(url, { ...params, protocol: 'json-rpc' }) options.set(channel, channelOptions) fsm.set(channel, { handler: awaitingConnection }) eventEmitters.set(channel, new EventEmitter()) sequence.set(channel, 0) rpcCallbacks.set(channel, new Map()) const ws = await WebSocket(wsUrl, { onopen: () => changeStatus(channel, 'connected'), onclose: () => changeStatus(channel, 'disconnected'), onmessage: ({ data }) => onMessage(channel, data) }) websockets.set(channel, ws) } export { initialize, options, status, state, eventEmitters, emit, changeStatus, changeState, send, enqueueAction, channelId, call, disconnect }