UNPKG

edge-core-js

Version:

Edge account & wallet management library

427 lines (272 loc) 8.11 kB
function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }import { asMaybe, asNumber, asObject, asOptional, asString, asUnknown, asValue, uncleaner } from 'cleaners' /** * A codec object can make calls to the remote system, * and can process incoming messages from the remote system. */ /** * Accepts cleaners for the two sides of a protocol, * and returns a codec factory. */ export function makeRpcProtocol ( opts ) { const { serverMethods, clientMethods, asCall = asJsonRpcCall, asReturn = asJsonRpcReturn, allowMultipleReturns = false } = opts return { makeServerCodec(opts) { return makeCodec( serverMethods, clientMethods, asCall, asReturn, allowMultipleReturns, opts ) }, makeClientCodec(opts) { return makeCodec( clientMethods, serverMethods, asCall, asReturn, allowMultipleReturns, opts ) } } } function makeCodec( localCleaners, remoteCleaners, asCall, asReturn, allowMultipleReturns, opts ) { const { handleError, handleSend, localMethods } = opts const wasCall = uncleaner(asCall) const wasReturn = uncleaner(asReturn) const sendError = async ( code, message, id = null ) => { const payload = wasReturn({ jsonrpc: '2.0', result: undefined, error: { code, message, data: undefined }, id }) return await handleSend(JSON.stringify(payload)) } // Remote state: let nextRemoteId = 0 const remoteCalls = new Map() const remoteSubscriptions = new Map() // Create proxy functions for each remote method: const remoteMethods = {} for (const methodName of Object.keys(remoteCleaners)) { const { asParams, asResult } = remoteCleaners[methodName] const wasParams = uncleaner(asParams) if (asResult == null) { // It's a notification, so send the message with no result handling: remoteMethods[methodName] = (params) => { const payload = wasCall({ jsonrpc: '2.0', method: methodName, params: wasParams(params) }) handleSend(JSON.stringify(payload)).catch(handleError) } } else { // It's a method call, so sign up to receive a result: remoteMethods[methodName] = (params) => { const id = nextRemoteId++ const out = new Promise((resolve, reject) => { remoteCalls.set(id, { asResult, resolve, reject }) }) // This is a subscription request: if ( allowMultipleReturns && localCleaners[methodName] != null && localCleaners[methodName].asResult == null ) { // Keep track of the remote method name to send notifications remoteSubscriptions.set(id, methodName) } const request = wasCall({ jsonrpc: '2.0', method: methodName, params: wasParams(params), id }) handleSend(JSON.stringify(request)).catch(handleError) return out } } } function handleMessage(message) { let json try { json = JSON.parse(message) } catch (error) { sendError(-32700, `Parse error: ${errorMessage(error)}`).catch( handleError ) return } // TODO: We need to add support for batch calls. const call = asMaybe(asCall)(json) const response = asMaybe(asReturn)(json) if (call != null) { const cleaners = localCleaners[call.method] if (cleaners == null) { sendError(-32601, `Method not found: ${call.method}`).catch(handleError) return } if (cleaners.asResult != null && call.id == null) { sendError(-32600, `Invalid JSON-RPC request: missing id`).catch( handleError ) return } if (cleaners.asResult == null && call.id != null) { sendError( -32600, `Invalid JSON-RPC request: notification has an id` ).catch(handleError) return } let cleanParams try { cleanParams = cleaners.asParams(call.params) } catch (error) { sendError(-32602, `Invalid params: ${errorMessage(error)}`).catch( handleError ) return } const out = localMethods[call.method](cleanParams) if (cleaners.asResult != null && _optionalChain([out, 'optionalAccess', _ => _.then]) != null) { const wasResult = uncleaner(cleaners.asResult) out.then( (result) => { const response = wasReturn({ jsonrpc: '2.0', result: wasResult(result), error: undefined, id: _nullishCoalesce(call.id, () => ( null)) }) handleSend(JSON.stringify(response)).catch(handleError) }, (error) => { sendError(1, errorMessage(error), call.id).catch(handleError) } ) } } else if (response != null) { const { error, id, result } = response if (typeof id !== 'number') { // It's not a call we made... sendError(-32603, `Cannot find id ${String(id)}`, id).catch(handleError) return } const pendingCall = remoteCalls.get(id) // Handle subscription calls masquerading as returns: if (pendingCall == null) { const methodName = remoteSubscriptions.get(id) if (methodName == null) { sendError(-32603, `Cannot find id ${String(id)}`, id).catch( handleError ) return } let cleanParams try { cleanParams = localCleaners[methodName].asParams(result) } catch (error) { sendError(-32602, `Invalid params: ${errorMessage(error)}`).catch( handleError ) return } localMethods[methodName](cleanParams) return } remoteCalls.delete(id) if (error != null) { pendingCall.reject(new Error(error.message)) } else { const { asResult } = pendingCall let cleanResult try { cleanResult = asResult(result) } catch (error) { pendingCall.reject(error) } pendingCall.resolve(cleanResult) } } else { sendError(-32600, `Invalid JSON-RPC request / response`).catch( handleError ) } } function handleClose() { for (const call of remoteCalls.values()) { call.reject(new Error('JSON-RPC connection closed')) } } return { handleClose, handleMessage, remoteMethods: remoteMethods } } function errorMessage(error) { return error instanceof Error ? error.message : String(error) } const asRpcId = (raw) => { if (raw === null) return raw if (typeof raw === 'string') return raw if (typeof raw === 'number' && Number.isInteger(raw)) return raw throw new TypeError('Expected a string or an integer') } export const asJsonRpcCall = asObject({ jsonrpc: asValue('2.0'), method: asString, params: asUnknown, id: asOptional(asRpcId) }) export const asJsonRpcReturn = asObject({ jsonrpc: asValue('2.0'), result: asUnknown, error: asOptional( asObject({ code: asNumber, message: asString, data: asUnknown }) ), id: asRpcId })