mirror of
https://github.com/logos-storage/nim-json-rpc.git
synced 2026-01-02 13:43:11 +00:00
Implement RPC batch call both in servers and clients
This commit is contained in:
parent
85d6a67fbc
commit
0b8cec3aa8
@ -24,15 +24,30 @@ export
|
||||
tables,
|
||||
jsonmarshal,
|
||||
RequestParamsTx,
|
||||
RequestBatchTx,
|
||||
ResponseBatchRx,
|
||||
results
|
||||
|
||||
type
|
||||
RpcBatchItem* = object
|
||||
meth*: string
|
||||
params*: RequestParamsTx
|
||||
|
||||
RpcBatchCallRef* = ref object of RootRef
|
||||
client*: RpcClient
|
||||
batch*: seq[RpcBatchItem]
|
||||
|
||||
RpcBatchResponse* = object
|
||||
error*: Opt[string]
|
||||
result*: JsonString
|
||||
|
||||
RpcClient* = ref object of RootRef
|
||||
awaiting*: Table[RequestId, Future[JsonString]]
|
||||
lastId: int
|
||||
onDisconnect*: proc() {.gcsafe, raises: [].}
|
||||
onProcessMessage*: proc(client: RpcClient, line: string):
|
||||
Result[bool, string] {.gcsafe, raises: [].}
|
||||
batchFut*: Future[ResponseBatchRx]
|
||||
|
||||
GetJsonRpcRequestHeaders* = proc(): seq[(string, string)] {.gcsafe, raises: [].}
|
||||
|
||||
@ -42,10 +57,65 @@ type
|
||||
# Public helpers
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
func validateResponse(resIndex: int, res: ResponseRx): Result[void, string] =
|
||||
if res.jsonrpc.isNone:
|
||||
return err("missing or invalid `jsonrpc` in response " & $resIndex)
|
||||
|
||||
if res.id.isNone:
|
||||
if res.error.isSome:
|
||||
let error = JrpcSys.encode(res.error.get)
|
||||
return err(error)
|
||||
else:
|
||||
return err("missing or invalid response id in response " & $resIndex)
|
||||
|
||||
if res.error.isSome:
|
||||
let error = JrpcSys.encode(res.error.get)
|
||||
return err(error)
|
||||
|
||||
# Up to this point, the result should contains something
|
||||
if res.result.string.len == 0:
|
||||
return err("missing or invalid response result in response " & $resIndex)
|
||||
|
||||
ok()
|
||||
|
||||
proc processResponse(resIndex: int,
|
||||
map: var Table[RequestId, int],
|
||||
responses: var seq[RpcBatchResponse],
|
||||
response: ResponseRx): Result[void, string] =
|
||||
let r = validateResponse(resIndex, response)
|
||||
if r.isErr:
|
||||
if response.id.isSome:
|
||||
let id = response.id.get
|
||||
var index: int
|
||||
if not map.pop(id, index):
|
||||
return err("cannot find message id: " & $id & " in response " & $resIndex)
|
||||
responses[index] = RpcBatchResponse(
|
||||
error: Opt.some(r.error)
|
||||
)
|
||||
else:
|
||||
return err(r.error)
|
||||
else:
|
||||
let id = response.id.get
|
||||
var index: int
|
||||
if not map.pop(id, index):
|
||||
return err("cannot find message id: " & $id & " in response " & $resIndex)
|
||||
responses[index] = RpcBatchResponse(
|
||||
result: response.result
|
||||
)
|
||||
|
||||
ok()
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Public helpers
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
func requestTxEncode*(name: string, params: RequestParamsTx, id: RequestId): string =
|
||||
let req = requestTx(name, params, id)
|
||||
JrpcSys.encode(req)
|
||||
|
||||
func requestBatchEncode*(calls: RequestBatchTx): string =
|
||||
JrpcSys.encode(calls)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Public functions
|
||||
# ------------------------------------------------------------------------------
|
||||
@ -68,6 +138,11 @@ method call*(client: RpcClient, name: string,
|
||||
method close*(client: RpcClient): Future[void] {.base, gcsafe, async.} =
|
||||
doAssert(false, "`RpcClient.close` not implemented")
|
||||
|
||||
method callBatch*(client: RpcClient,
|
||||
calls: RequestBatchTx): Future[ResponseBatchRx]
|
||||
{.base, gcsafe, async.} =
|
||||
doAssert(false, "`RpcClient.callBatch` not implemented")
|
||||
|
||||
proc processMessage*(client: RpcClient, line: string): Result[void, string] =
|
||||
if client.onProcessMessage.isNil.not:
|
||||
let fallBack = client.onProcessMessage(client, line).valueOr:
|
||||
@ -78,8 +153,14 @@ proc processMessage*(client: RpcClient, line: string): Result[void, string] =
|
||||
# Note: this doesn't use any transport code so doesn't need to be
|
||||
# differentiated.
|
||||
try:
|
||||
let response = JrpcSys.decode(line, ResponseRx)
|
||||
let batch = JrpcSys.decode(line, ResponseBatchRx)
|
||||
if batch.kind == rbkMany:
|
||||
if client.batchFut.isNil or client.batchFut.finished():
|
||||
client.batchFut = newFuture[ResponseBatchRx]()
|
||||
client.batchFut.complete(batch)
|
||||
return ok()
|
||||
|
||||
let response = batch.single
|
||||
if response.jsonrpc.isNone:
|
||||
return err("missing or invalid `jsonrpc`")
|
||||
|
||||
@ -114,6 +195,41 @@ proc processMessage*(client: RpcClient, line: string): Result[void, string] =
|
||||
except CatchableError as exc:
|
||||
return err(exc.msg)
|
||||
|
||||
proc prepareBatch*(client: RpcClient): RpcBatchCallRef =
|
||||
RpcBatchCallRef(client: client)
|
||||
|
||||
proc send*(batch: RpcBatchCallRef):
|
||||
Future[Result[seq[RpcBatchResponse], string]] {.
|
||||
async: (raises: []).} =
|
||||
var
|
||||
calls = RequestBatchTx(
|
||||
kind: rbkMany,
|
||||
many: newSeqOfCap[RequestTx](batch.batch.len),
|
||||
)
|
||||
responses = newSeq[RpcBatchResponse](batch.batch.len)
|
||||
map = initTable[RequestId, int]()
|
||||
|
||||
for item in batch.batch:
|
||||
let id = batch.client.getNextId()
|
||||
map[id] = calls.many.len
|
||||
calls.many.add requestTx(item.meth, item.params, id)
|
||||
|
||||
try:
|
||||
let res = await batch.client.callBatch(calls)
|
||||
if res.kind == rbkSingle:
|
||||
let r = processResponse(0, map, responses, res.single)
|
||||
if r.isErr:
|
||||
return err(r.error)
|
||||
else:
|
||||
for i, z in res.many:
|
||||
let r = processResponse(i, map, responses, z)
|
||||
if r.isErr:
|
||||
return err(r.error)
|
||||
except CatchableError as exc:
|
||||
return err(exc.msg)
|
||||
|
||||
return ok(responses)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Signature processing
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
# json-rpc
|
||||
# Copyright (c) 2019-2023 Status Research & Development GmbH
|
||||
# Copyright (c) 2019-2024 Status Research & Development GmbH
|
||||
# Licensed under either of
|
||||
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
|
||||
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
|
||||
@ -38,6 +38,10 @@ const
|
||||
|
||||
{.push gcsafe, raises: [].}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Private helpers
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
proc new(
|
||||
T: type RpcHttpClient, maxBodySize = MaxHttpRequestSize, secure = false,
|
||||
getHeaders: GetJsonRpcRequestHeaders = nil, flags: HttpClientFlags = {}): T =
|
||||
@ -53,15 +57,24 @@ proc new(
|
||||
getHeaders: getHeaders
|
||||
)
|
||||
|
||||
proc newRpcHttpClient*(
|
||||
maxBodySize = MaxHttpRequestSize, secure = false,
|
||||
getHeaders: GetJsonRpcRequestHeaders = nil,
|
||||
flags: HttpClientFlags = {}): RpcHttpClient =
|
||||
RpcHttpClient.new(maxBodySize, secure, getHeaders, flags)
|
||||
template closeRefs(req, res: untyped) =
|
||||
# We can't trust try/finally in async/await in all nim versions, so we
|
||||
# do it manually instead
|
||||
if req != nil:
|
||||
try:
|
||||
await req.closeWait()
|
||||
except CatchableError as exc: # shouldn't happen
|
||||
debug "Error closing JSON-RPC HTTP resuest/response", err = exc.msg
|
||||
discard exc
|
||||
|
||||
method call*(client: RpcHttpClient, name: string,
|
||||
params: RequestParamsTx): Future[JsonString]
|
||||
{.async, gcsafe.} =
|
||||
if res != nil:
|
||||
try:
|
||||
await res.closeWait()
|
||||
except CatchableError as exc: # shouldn't happen
|
||||
debug "Error closing JSON-RPC HTTP resuest/response", err = exc.msg
|
||||
discard exc
|
||||
|
||||
proc callImpl(client: RpcHttpClient, reqBody: string): Future[string] {.async.} =
|
||||
doAssert client.httpSession != nil
|
||||
if client.httpAddress.isErr:
|
||||
raise newException(RpcAddressUnresolvableError, client.httpAddress.error)
|
||||
@ -73,33 +86,9 @@ method call*(client: RpcHttpClient, name: string,
|
||||
@[]
|
||||
headers.add(("Content-Type", "application/json"))
|
||||
|
||||
let
|
||||
id = client.getNextId()
|
||||
reqBody = requestTxEncode(name, params, id)
|
||||
|
||||
var req: HttpClientRequestRef
|
||||
var res: HttpClientResponseRef
|
||||
|
||||
template used(x: typed) =
|
||||
# silence unused warning
|
||||
discard
|
||||
|
||||
template closeRefs() =
|
||||
# We can't trust try/finally in async/await in all nim versions, so we
|
||||
# do it manually instead
|
||||
if req != nil:
|
||||
try:
|
||||
await req.closeWait()
|
||||
except CatchableError as exc: # shouldn't happen
|
||||
used(exc)
|
||||
debug "Error closing JSON-RPC HTTP resuest/response", err = exc.msg
|
||||
if res != nil:
|
||||
try:
|
||||
await res.closeWait()
|
||||
except CatchableError as exc: # shouldn't happen
|
||||
used(exc)
|
||||
debug "Error closing JSON-RPC HTTP resuest/response", err = exc.msg
|
||||
|
||||
debug "Sending message to RPC server",
|
||||
address = client.httpAddress, msg_len = len(reqBody), name
|
||||
trace "Message", msg = reqBody
|
||||
@ -113,17 +102,17 @@ method call*(client: RpcHttpClient, name: string,
|
||||
await req.send()
|
||||
except CancelledError as e:
|
||||
debug "Cancelled POST Request with JSON-RPC", e = e.msg
|
||||
closeRefs()
|
||||
closeRefs(req, res)
|
||||
raise e
|
||||
except CatchableError as e:
|
||||
debug "Failed to send POST Request with JSON-RPC", e = e.msg
|
||||
closeRefs()
|
||||
closeRefs(req, res)
|
||||
raise (ref RpcPostError)(msg: "Failed to send POST Request with JSON-RPC: " & e.msg, parent: e)
|
||||
|
||||
if res.status < 200 or res.status >= 300: # res.status is not 2xx (success)
|
||||
debug "Unsuccessful POST Request with JSON-RPC",
|
||||
status = res.status, reason = res.reason
|
||||
closeRefs()
|
||||
closeRefs(req, res)
|
||||
raise (ref ErrorResponse)(status: res.status, msg: res.reason)
|
||||
|
||||
let resBytes =
|
||||
@ -131,15 +120,34 @@ method call*(client: RpcHttpClient, name: string,
|
||||
await res.getBodyBytes(client.maxBodySize)
|
||||
except CancelledError as e:
|
||||
debug "Cancelled POST Response for JSON-RPC", e = e.msg
|
||||
closeRefs()
|
||||
closeRefs(req, res)
|
||||
raise e
|
||||
except CatchableError as e:
|
||||
debug "Failed to read POST Response for JSON-RPC", e = e.msg
|
||||
closeRefs()
|
||||
closeRefs(req, res)
|
||||
raise (ref FailedHttpResponse)(msg: "Failed to read POST Response for JSON-RPC: " & e.msg, parent: e)
|
||||
|
||||
let resText = string.fromBytes(resBytes)
|
||||
trace "Response", text = resText
|
||||
result = string.fromBytes(resBytes)
|
||||
trace "Response", text = result
|
||||
closeRefs(req, res)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Public functions
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
proc newRpcHttpClient*(
|
||||
maxBodySize = MaxHttpRequestSize, secure = false,
|
||||
getHeaders: GetJsonRpcRequestHeaders = nil,
|
||||
flags: HttpClientFlags = {}): RpcHttpClient =
|
||||
RpcHttpClient.new(maxBodySize, secure, getHeaders, flags)
|
||||
|
||||
method call*(client: RpcHttpClient, name: string,
|
||||
params: RequestParamsTx): Future[JsonString]
|
||||
{.async, gcsafe.} =
|
||||
let
|
||||
id = client.getNextId()
|
||||
reqBody = requestTxEncode(name, params, id)
|
||||
resText = await client.callImpl(reqBody)
|
||||
|
||||
# completed by processMessage - the flow is quite weird here to accomodate
|
||||
# socket and ws clients, but could use a more thorough refactoring
|
||||
@ -155,13 +163,10 @@ method call*(client: RpcHttpClient, name: string,
|
||||
let exc = newException(JsonRpcError, msgRes.error)
|
||||
newFut.fail(exc)
|
||||
client.awaiting.del(id)
|
||||
closeRefs()
|
||||
raise exc
|
||||
|
||||
client.awaiting.del(id)
|
||||
|
||||
closeRefs()
|
||||
|
||||
# processMessage should have completed this future - if it didn't, `read` will
|
||||
# raise, which is reasonable
|
||||
if newFut.finished:
|
||||
@ -171,6 +176,34 @@ method call*(client: RpcHttpClient, name: string,
|
||||
debug "Invalid POST Response for JSON-RPC"
|
||||
raise newException(InvalidResponse, "Invalid response")
|
||||
|
||||
method callBatch*(client: RpcHttpClient,
|
||||
calls: RequestBatchTx): Future[ResponseBatchRx]
|
||||
{.gcsafe, async.} =
|
||||
let
|
||||
reqBody = requestBatchEncode(calls)
|
||||
resText = await client.callImpl(reqBody)
|
||||
|
||||
if client.batchFut.isNil or client.batchFut.finished():
|
||||
client.batchFut = newFuture[ResponseBatchRx]()
|
||||
|
||||
# Might error for all kinds of reasons
|
||||
let msgRes = client.processMessage(resText)
|
||||
if msgRes.isErr:
|
||||
# Need to clean up in case the answer was invalid
|
||||
debug "Failed to process POST Response for JSON-RPC", msg = msgRes.error
|
||||
let exc = newException(JsonRpcError, msgRes.error)
|
||||
client.batchFut.fail(exc)
|
||||
raise exc
|
||||
|
||||
# processMessage should have completed this future - if it didn't, `read` will
|
||||
# raise, which is reasonable
|
||||
if client.batchFut.finished:
|
||||
return client.batchFut.read()
|
||||
else:
|
||||
# TODO: Provide more clarity regarding the failure here
|
||||
debug "Invalid POST Response for JSON-RPC"
|
||||
raise newException(InvalidResponse, "Invalid response")
|
||||
|
||||
proc connect*(client: RpcHttpClient, url: string) {.async.} =
|
||||
client.httpAddress = client.httpSession.getAddress(url)
|
||||
if client.httpAddress.isErr:
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
# json-rpc
|
||||
# Copyright (c) 2019-2023 Status Research & Development GmbH
|
||||
# Copyright (c) 2019-2024 Status Research & Development GmbH
|
||||
# Licensed under either of
|
||||
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
|
||||
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
|
||||
@ -35,26 +35,45 @@ proc newRpcSocketClient*: RpcSocketClient =
|
||||
## Creates a new client instance.
|
||||
RpcSocketClient.new()
|
||||
|
||||
method call*(self: RpcSocketClient, name: string,
|
||||
method call*(client: RpcSocketClient, name: string,
|
||||
params: RequestParamsTx): Future[JsonString] {.async, gcsafe.} =
|
||||
## Remotely calls the specified RPC method.
|
||||
let id = self.getNextId()
|
||||
var value = requestTxEncode(name, params, id) & "\r\n"
|
||||
if self.transport.isNil:
|
||||
let id = client.getNextId()
|
||||
var jsonBytes = requestTxEncode(name, params, id) & "\r\n"
|
||||
if client.transport.isNil:
|
||||
raise newException(JsonRpcError,
|
||||
"Transport is not initialised (missing a call to connect?)")
|
||||
|
||||
# completed by processMessage.
|
||||
var newFut = newFuture[JsonString]()
|
||||
# add to awaiting responses
|
||||
self.awaiting[id] = newFut
|
||||
client.awaiting[id] = newFut
|
||||
|
||||
let res = await self.transport.write(value)
|
||||
let res = await client.transport.write(jsonBytes)
|
||||
# TODO: Add actions when not full packet was send, e.g. disconnect peer.
|
||||
doAssert(res == len(value))
|
||||
doAssert(res == jsonBytes.len)
|
||||
|
||||
return await newFut
|
||||
|
||||
method callBatch*(client: RpcSocketClient,
|
||||
calls: RequestBatchTx): Future[ResponseBatchRx]
|
||||
{.gcsafe, async.} =
|
||||
if client.transport.isNil:
|
||||
raise newException(JsonRpcError,
|
||||
"Transport is not initialised (missing a call to connect?)")
|
||||
|
||||
if client.batchFut.isNil or client.batchFut.finished():
|
||||
client.batchFut = newFuture[ResponseBatchRx]()
|
||||
|
||||
let
|
||||
jsonBytes = requestBatchEncode(calls) & "\r\n"
|
||||
res = await client.transport.write(jsonBytes)
|
||||
|
||||
# TODO: Add actions when not full packet was send, e.g. disconnect peer.
|
||||
doAssert(res == jsonBytes.len)
|
||||
|
||||
return await client.batchFut
|
||||
|
||||
proc processData(client: RpcSocketClient) {.async.} =
|
||||
while true:
|
||||
while true:
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
# json-rpc
|
||||
# Copyright (c) 2019-2023 Status Research & Development GmbH
|
||||
# Copyright (c) 2019-2024 Status Research & Development GmbH
|
||||
# Licensed under either of
|
||||
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
|
||||
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
|
||||
@ -38,23 +38,39 @@ proc newRpcWebSocketClient*(
|
||||
## Creates a new client instance.
|
||||
RpcWebSocketClient.new(getHeaders)
|
||||
|
||||
method call*(self: RpcWebSocketClient, name: string,
|
||||
method call*(client: RpcWebSocketClient, name: string,
|
||||
params: RequestParamsTx): Future[JsonString] {.async, gcsafe.} =
|
||||
## Remotely calls the specified RPC method.
|
||||
let id = self.getNextId()
|
||||
var value = requestTxEncode(name, params, id) & "\r\n"
|
||||
if self.transport.isNil:
|
||||
if client.transport.isNil:
|
||||
raise newException(JsonRpcError,
|
||||
"Transport is not initialised (missing a call to connect?)")
|
||||
"Transport is not initialised (missing a call to connect?)")
|
||||
|
||||
let id = client.getNextId()
|
||||
var value = requestTxEncode(name, params, id) & "\r\n"
|
||||
|
||||
# completed by processMessage.
|
||||
var newFut = newFuture[JsonString]()
|
||||
# add to awaiting responses
|
||||
self.awaiting[id] = newFut
|
||||
client.awaiting[id] = newFut
|
||||
|
||||
await self.transport.send(value)
|
||||
await client.transport.send(value)
|
||||
return await newFut
|
||||
|
||||
method callBatch*(client: RpcWebSocketClient,
|
||||
calls: RequestBatchTx): Future[ResponseBatchRx]
|
||||
{.gcsafe, async.} =
|
||||
if client.transport.isNil:
|
||||
raise newException(JsonRpcError,
|
||||
"Transport is not initialised (missing a call to connect?)")
|
||||
|
||||
if client.batchFut.isNil or client.batchFut.finished():
|
||||
client.batchFut = newFuture[ResponseBatchRx]()
|
||||
|
||||
let jsonBytes = requestBatchEncode(calls) & "\r\n"
|
||||
await client.transport.send(jsonBytes)
|
||||
|
||||
return await client.batchFut
|
||||
|
||||
proc processData(client: RpcWebSocketClient) {.async.} =
|
||||
var error: ref CatchableError
|
||||
|
||||
|
||||
@ -32,6 +32,17 @@ proc createRpcProc(procName, parameters, callBody: NimNode): NimNode =
|
||||
# export this proc
|
||||
result[0] = nnkPostfix.newTree(ident"*", newIdentNode($procName))
|
||||
|
||||
proc createBatchCallProc(procName, parameters, callBody: NimNode): NimNode =
|
||||
# parameters come as a tree
|
||||
var paramList = newSeq[NimNode]()
|
||||
for p in parameters: paramList.add(p)
|
||||
|
||||
# build proc
|
||||
result = newProc(procName, paramList, callBody)
|
||||
|
||||
# export this proc
|
||||
result[0] = nnkPostfix.newTree(ident"*", newIdentNode($procName))
|
||||
|
||||
proc setupConversion(reqParams, params: NimNode): NimNode =
|
||||
# populate json params
|
||||
# even rpcs with no parameters have an empty json array node sent
|
||||
@ -47,7 +58,7 @@ proc setupConversion(reqParams, params: NimNode): NimNode =
|
||||
|
||||
proc createRpcFromSig*(clientType, rpcDecl: NimNode, alias = NimNode(nil)): NimNode =
|
||||
## This procedure will generate something like this:
|
||||
## - Currently it always send posisitional parameters to the server
|
||||
## - Currently it always send positional parameters to the server
|
||||
##
|
||||
## proc rpcApi(client: RpcClient; paramA: TypeA; paramB: TypeB): Future[RetType] =
|
||||
## {.gcsafe.}:
|
||||
@ -66,11 +77,11 @@ proc createRpcFromSig*(clientType, rpcDecl: NimNode, alias = NimNode(nil)): NimN
|
||||
procName = if alias.isNil: rpcDecl.name else: alias
|
||||
pathStr = $rpcDecl.name
|
||||
returnType = params[0]
|
||||
reqParams = genSym(nskVar, "reqParams")
|
||||
reqParams = ident "reqParams"
|
||||
setup = setupConversion(reqParams, params)
|
||||
clientIdent = ident"client"
|
||||
# temporary variable to hold `Response` from rpc call
|
||||
rpcResult = genSym(nskLet, "res")
|
||||
rpcResult = ident "res"
|
||||
# proc return variable
|
||||
procRes = ident"result"
|
||||
doDecode = quote do:
|
||||
@ -79,6 +90,9 @@ proc createRpcFromSig*(clientType, rpcDecl: NimNode, alias = NimNode(nil)): NimN
|
||||
if returnType.noWrap: quote do:
|
||||
`procRes` = `rpcResult`
|
||||
else: doDecode
|
||||
|
||||
batchParams = params.copy
|
||||
batchIdent = ident "batch"
|
||||
|
||||
# insert rpc client as first parameter
|
||||
params.insert(1, nnkIdentDefs.newTree(
|
||||
@ -99,8 +113,29 @@ proc createRpcFromSig*(clientType, rpcDecl: NimNode, alias = NimNode(nil)): NimN
|
||||
let `rpcResult` = await `clientIdent`.call(`pathStr`, `reqParams`)
|
||||
`maybeWrap`
|
||||
|
||||
|
||||
# insert RpcBatchCallRef as first parameter
|
||||
batchParams.insert(1, nnkIdentDefs.newTree(
|
||||
batchIdent,
|
||||
ident "RpcBatchCallRef",
|
||||
newEmptyNode()
|
||||
))
|
||||
|
||||
# remove return type
|
||||
batchParams[0] = newEmptyNode()
|
||||
|
||||
let batchCallBody = quote do:
|
||||
`setup`
|
||||
`batchIdent`.batch.add RpcBatchItem(
|
||||
meth: `pathStr`,
|
||||
params: `reqParams`
|
||||
)
|
||||
|
||||
# create rpc proc
|
||||
result = createRpcProc(procName, params, callBody)
|
||||
result = newStmtList()
|
||||
result.add createRpcProc(procName, params, callBody)
|
||||
result.add createBatchCallProc(procName, batchParams, batchCallBody)
|
||||
|
||||
when defined(nimDumpRpcs):
|
||||
echo pathStr, ":\n", result.repr
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
# json-rpc
|
||||
# Copyright (c) 2019-2023 Status Research & Development GmbH
|
||||
# Copyright (c) 2019-2024 Status Research & Development GmbH
|
||||
# Licensed under either of
|
||||
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
|
||||
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
# json-rpc
|
||||
# Copyright (c) 2019-2023 Status Research & Development GmbH
|
||||
# Copyright (c) 2019-2024 Status Research & Development GmbH
|
||||
# Licensed under either of
|
||||
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
|
||||
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
|
||||
@ -155,21 +155,32 @@ proc route*(router: RpcRouter, data: string):
|
||||
|
||||
let request =
|
||||
try:
|
||||
JrpcSys.decode(data, RequestRx)
|
||||
JrpcSys.decode(data, RequestBatchRx)
|
||||
except CatchableError as err:
|
||||
return wrapError(JSON_PARSE_ERROR, err.msg)
|
||||
except Exception as err:
|
||||
# TODO https://github.com/status-im/nimbus-eth2/issues/2430
|
||||
return wrapError(JSON_PARSE_ERROR, err.msg)
|
||||
|
||||
let reply =
|
||||
try:
|
||||
let response = await router.route(request)
|
||||
JrpcSys.encode(response)
|
||||
let reply = try:
|
||||
if request.kind == rbkSingle:
|
||||
let response = await router.route(request.single)
|
||||
JrpcSys.encode(response)
|
||||
elif request.many.len == 0:
|
||||
wrapError(INVALID_REQUEST, "no request object in request array")
|
||||
else:
|
||||
var resFut: seq[Future[ResponseTx]]
|
||||
for req in request.many:
|
||||
resFut.add router.route(req)
|
||||
await noCancel(allFutures(resFut))
|
||||
var response = ResponseBatchTx(kind: rbkMany)
|
||||
for fut in resFut:
|
||||
response.many.add fut.read()
|
||||
JrpcSys.encode(response)
|
||||
except CatchableError as err:
|
||||
return wrapError(JSON_ENCODE_ERROR, err.msg)
|
||||
wrapError(JSON_ENCODE_ERROR, err.msg)
|
||||
except Exception as err:
|
||||
return wrapError(JSON_ENCODE_ERROR, err.msg)
|
||||
wrapError(JSON_ENCODE_ERROR, err.msg)
|
||||
|
||||
when defined(nimHasWarnBareExcept):
|
||||
{.pop warning[BareExcept]:on.}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
# json-rpc
|
||||
# Copyright (c) 2019-2023 Status Research & Development GmbH
|
||||
# Copyright (c) 2019-2024 Status Research & Development GmbH
|
||||
# Licensed under either of
|
||||
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
|
||||
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
|
||||
@ -20,4 +20,5 @@ import
|
||||
test_jrpc_sys,
|
||||
test_router_rpc,
|
||||
test_callsigs,
|
||||
test_client_hook
|
||||
test_client_hook,
|
||||
test_batch_call
|
||||
|
||||
145
tests/test_batch_call.nim
Normal file
145
tests/test_batch_call.nim
Normal file
@ -0,0 +1,145 @@
|
||||
# json-rpc
|
||||
# Copyright (c) 2024 Status Research & Development GmbH
|
||||
# Licensed under either of
|
||||
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
|
||||
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
|
||||
# at your option.
|
||||
# This file may not be copied, modified, or distributed except according to
|
||||
# those terms.
|
||||
|
||||
import
|
||||
unittest2,
|
||||
../json_rpc/rpcclient,
|
||||
../json_rpc/rpcserver
|
||||
|
||||
createRpcSigsFromNim(RpcClient):
|
||||
proc get_banana(id: int): bool
|
||||
proc get_apple(id: string): string
|
||||
proc get_except(): string
|
||||
|
||||
proc setupServer(server: RpcServer) =
|
||||
server.rpc("get_banana") do(id: int) -> bool:
|
||||
return id == 13
|
||||
|
||||
server.rpc("get_apple") do(id: string) -> string:
|
||||
return "apple: " & id
|
||||
|
||||
server.rpc("get_except") do() -> string:
|
||||
raise newException(ValueError, "get_except error")
|
||||
|
||||
suite "Socket batch call":
|
||||
var srv = newRpcSocketServer(["127.0.0.1:0"])
|
||||
var client = newRpcSocketClient()
|
||||
|
||||
srv.setupServer()
|
||||
srv.start()
|
||||
waitFor client.connect(srv.localAddress()[0])
|
||||
|
||||
test "batch call basic":
|
||||
let batch = client.prepareBatch()
|
||||
|
||||
batch.get_banana(11)
|
||||
batch.get_apple("green")
|
||||
batch.get_except()
|
||||
|
||||
let res = waitFor batch.send()
|
||||
check res.isOk
|
||||
if res.isErr:
|
||||
debugEcho res.error
|
||||
break
|
||||
|
||||
let r = res.get
|
||||
check r[0].error.isNone
|
||||
check r[0].result.string == "false"
|
||||
|
||||
check r[1].error.isNone
|
||||
check r[1].result.string == "\"apple: green\""
|
||||
|
||||
check r[2].error.isSome
|
||||
check r[2].error.get == """{"code":-32000,"message":"get_except raised an exception","data":"get_except error"}"""
|
||||
check r[2].result.string.len == 0
|
||||
|
||||
test "rpc call after batch call":
|
||||
let res = waitFor client.get_banana(13)
|
||||
check res == true
|
||||
|
||||
srv.stop()
|
||||
waitFor srv.closeWait()
|
||||
|
||||
suite "HTTP batch call":
|
||||
var srv = newRpcHttpServer(["127.0.0.1:0"])
|
||||
var client = newRpcHttpClient()
|
||||
|
||||
srv.setupServer()
|
||||
srv.start()
|
||||
waitFor client.connect("http://" & $srv.localAddress()[0])
|
||||
|
||||
test "batch call basic":
|
||||
let batch = client.prepareBatch()
|
||||
|
||||
batch.get_banana(11)
|
||||
batch.get_apple("green")
|
||||
batch.get_except()
|
||||
|
||||
let res = waitFor batch.send()
|
||||
check res.isOk
|
||||
if res.isErr:
|
||||
debugEcho res.error
|
||||
break
|
||||
|
||||
let r = res.get
|
||||
check r[0].error.isNone
|
||||
check r[0].result.string == "false"
|
||||
|
||||
check r[1].error.isNone
|
||||
check r[1].result.string == "\"apple: green\""
|
||||
|
||||
check r[2].error.isSome
|
||||
check r[2].error.get == """{"code":-32000,"message":"get_except raised an exception","data":"get_except error"}"""
|
||||
check r[2].result.string.len == 0
|
||||
|
||||
test "rpc call after batch call":
|
||||
let res = waitFor client.get_banana(13)
|
||||
check res == true
|
||||
|
||||
waitFor srv.stop()
|
||||
waitFor srv.closeWait()
|
||||
|
||||
suite "Websocket batch call":
|
||||
var srv = newRpcWebSocketServer("127.0.0.1", Port(0))
|
||||
var client = newRpcWebSocketClient()
|
||||
|
||||
srv.setupServer()
|
||||
srv.start()
|
||||
waitFor client.connect("ws://" & $srv.localAddress())
|
||||
|
||||
test "batch call basic":
|
||||
let batch = client.prepareBatch()
|
||||
|
||||
batch.get_banana(11)
|
||||
batch.get_apple("green")
|
||||
batch.get_except()
|
||||
|
||||
let res = waitFor batch.send()
|
||||
check res.isOk
|
||||
if res.isErr:
|
||||
debugEcho res.error
|
||||
break
|
||||
|
||||
let r = res.get
|
||||
check r[0].error.isNone
|
||||
check r[0].result.string == "false"
|
||||
|
||||
check r[1].error.isNone
|
||||
check r[1].result.string == "\"apple: green\""
|
||||
|
||||
check r[2].error.isSome
|
||||
check r[2].error.get == """{"code":-32000,"message":"get_except raised an exception","data":"get_except error"}"""
|
||||
check r[2].result.string.len == 0
|
||||
|
||||
test "rpc call after batch call":
|
||||
let res = waitFor client.get_banana(13)
|
||||
check res == true
|
||||
|
||||
srv.stop()
|
||||
waitFor srv.closeWait()
|
||||
Loading…
x
Reference in New Issue
Block a user