UNPKG

@aeternity/aepp-sdk

Version:

SDK for the æternity blockchain

224 lines (219 loc) 7.38 kB
import { default as websocket } from 'websocket'; import JsonBig from '../utils/json-big.js'; import { pascalToSnake } from '../utils/string.js'; import { ChannelCallError, ChannelPingTimedOutError, UnexpectedTsError, UnknownChannelStateError, ChannelIncomingMessageError, ChannelError } from '../utils/errors.js'; import { buildContractId } from '../tx/builder/helpers.js'; import { ensureError } from '../utils/other.js'; const { w3cwebsocket: W3CWebSocket } = websocket; // TODO: SignTx shouldn't return number or null /** * @see {@link https://github.com/aeternity/protocol/blob/6734de2e4c7cce7e5e626caa8305fb535785131d/node/api/channels_api_usage.md#channel-establishing-parameters} */ // Send ping message every 10 seconds const PING_TIMEOUT_MS = 10000; // Close connection if pong message is not received within 15 seconds const PONG_TIMEOUT_MS = 15000; export function emit(channel, ...args) { const [eventName, ...rest] = args; channel._eventEmitter.emit(eventName, ...rest); } function enterState(channel, nextState) { if (nextState == null) { throw new UnknownChannelStateError(); } channel._debug('enter state', nextState.handler.name); channel._fsm = nextState; if (nextState?.handler?.enter != null) { nextState.handler.enter(channel); } // eslint-disable-next-line @typescript-eslint/no-use-before-define void dequeueAction(channel); } // TODO: rewrite to enum export function changeStatus(channel, newStatus, debug) { channel._debug(newStatus, `(prev. ${channel._status})`, debug !== null && debug !== void 0 ? debug : ''); if (newStatus === channel._status) return; channel._status = newStatus; emit(channel, 'statusChanged', newStatus); } export function changeState(channel, newState) { channel._state = newState; emit(channel, 'stateChanged', newState); } function send(channel, message) { channel._debug('send message', message.method, message.params); channel._websocket.send(JsonBig.stringify({ jsonrpc: '2.0', ...message })); } export function notify(channel, method, params = {}) { send(channel, { method, params }); } async function dequeueAction(channel) { if (channel._isActionQueueLocked) return; const queue = channel._actionQueue; if (queue.length === 0) return; const index = queue.findIndex(action => action.guard(channel, channel._fsm)); if (index === -1) return; channel._actionQueue = queue.filter((_, i) => index !== i); channel._isActionQueueLocked = true; const nextState = await queue[index].action(channel, channel._fsm); channel._isActionQueueLocked = false; enterState(channel, nextState); } export async function enqueueAction(channel, guard, action) { const promise = new Promise((resolve, reject) => { channel._actionQueue.push({ guard, action() { const res = action(); return { ...res, state: { ...res.state, resolve, reject } }; } }); }); void dequeueAction(channel); return promise; } async function handleMessage(channel, message) { const { handler, state: st } = channel._fsm; const nextState = await Promise.resolve(handler(channel, message, st)); enterState(channel, nextState); // TODO: emit message and handler name (?) to move this code to Contract constructor if (message?.params?.data?.updates?.[0]?.op === 'OffChainNewContract' && // if name is channelOpen, the contract was created by other participant nextState?.handler.name === 'channelOpen') { const round = channel.round(); if (round == null) throw new UnexpectedTsError('Round is null'); const owner = message?.params?.data?.updates?.[0]?.owner; emit(channel, 'newContract', buildContractId(owner, round + 1)); } } async function dequeueMessage(channel) { if (channel._isMessageQueueLocked) return; channel._isMessageQueueLocked = true; while (channel._messageQueue.length > 0) { const message = channel._messageQueue.shift(); if (message == null) throw new UnexpectedTsError(); try { await handleMessage(channel, message); } catch (error) { ensureError(error); emit(channel, 'error', new ChannelIncomingMessageError(error, message)); } } channel._isMessageQueueLocked = false; } export function disconnect(channel) { channel._websocket.close(); clearTimeout(channel._pingTimeoutId); } function ping(channel) { clearTimeout(channel._pingTimeoutId); channel._pingTimeoutId = setTimeout(() => { notify(channel, 'channels.system', { action: 'ping' }); channel._pingTimeoutId = setTimeout(() => { disconnect(channel); emit(channel, 'error', new ChannelPingTimedOutError()); }, PONG_TIMEOUT_MS); }, PING_TIMEOUT_MS); } function onMessage(channel, data) { const message = JsonBig.parse(data); channel._debug('received message', message.method, message.params); if (message.id != null) { const callback = channel._rpcCallbacks.get(message.id); if (callback == null) { emit(channel, 'error', new ChannelError(`Can't find callback by id: ${message.id}`)); return; } try { callback(message); } finally { channel._rpcCallbacks.delete(message.id); } return; } if (message.method === 'channels.message') { emit(channel, 'message', message.params.data.message); return; } if (message.method === 'channels.system.pong') { if (message.params.channel_id === channel._channelId || channel._channelId == null) { ping(channel); } return; } channel._messageQueue.push(message); void dequeueMessage(channel); } export async function call(channel, method, params) { return new Promise((resolve, reject) => { const id = channel._nextRpcMessageId; channel._nextRpcMessageId += 1; channel._rpcCallbacks.set(id, message => { if (message.error != null) { var _message$error$data$; const details = (_message$error$data$ = message.error.data[0].message) !== null && _message$error$data$ !== void 0 ? _message$error$data$ : ''; reject(new ChannelCallError(message.error.message + details)); } else resolve(message.result); }); send(channel, { method, id, params }); }); } export async function initialize(channel, connectionHandler, openHandler, { url, ...channelOptions }) { channel._options = { url, ...channelOptions }; const wsUrl = new URL(url); Object.entries(channelOptions).filter(([key]) => !['sign', 'debug'].includes(key)).forEach(([key, value]) => wsUrl.searchParams.set(pascalToSnake(key), value.toString())); wsUrl.searchParams.set('protocol', 'json-rpc'); changeStatus(channel, 'connecting'); channel._websocket = new W3CWebSocket(wsUrl.toString()); await new Promise((resolve, reject) => { Object.assign(channel._websocket, { onerror: reject, onopen: async event => { resolve(); changeStatus(channel, 'connected', event); enterState(channel, { handler: connectionHandler }); ping(channel); }, onclose: event => { changeStatus(channel, 'disconnected', event); clearTimeout(channel._pingTimeoutId); }, onmessage: ({ data }) => onMessage(channel, data) }); }); } //# sourceMappingURL=internal.js.map