@aeternity/aepp-sdk
Version:
SDK for the æternity blockchain
224 lines (219 loc) • 7.38 kB
JavaScript
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