edge-core-js
Version:
Edge account & wallet management library
427 lines (272 loc) • 8.11 kB
JavaScript
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
})