diff --git a/json_rpc/client.nim b/json_rpc/client.nim index c3d85bb..135cce7 100644 --- a/json_rpc/client.nim +++ b/json_rpc/client.nim @@ -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 # ------------------------------------------------------------------------------ diff --git a/json_rpc/clients/httpclient.nim b/json_rpc/clients/httpclient.nim index 02ca22b..5a8c555 100644 --- a/json_rpc/clients/httpclient.nim +++ b/json_rpc/clients/httpclient.nim @@ -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: diff --git a/json_rpc/clients/socketclient.nim b/json_rpc/clients/socketclient.nim index f765062..29207b7 100644 --- a/json_rpc/clients/socketclient.nim +++ b/json_rpc/clients/socketclient.nim @@ -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: diff --git a/json_rpc/clients/websocketclientimpl.nim b/json_rpc/clients/websocketclientimpl.nim index 16d0073..d53af38 100644 --- a/json_rpc/clients/websocketclientimpl.nim +++ b/json_rpc/clients/websocketclientimpl.nim @@ -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 diff --git a/json_rpc/private/client_handler_wrapper.nim b/json_rpc/private/client_handler_wrapper.nim index bca1202..95c7be1 100644 --- a/json_rpc/private/client_handler_wrapper.nim +++ b/json_rpc/private/client_handler_wrapper.nim @@ -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 diff --git a/json_rpc/private/server_handler_wrapper.nim b/json_rpc/private/server_handler_wrapper.nim index 9d88f75..fe20c59 100644 --- a/json_rpc/private/server_handler_wrapper.nim +++ b/json_rpc/private/server_handler_wrapper.nim @@ -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)) diff --git a/json_rpc/router.nim b/json_rpc/router.nim index a1c2cdd..53ae8c7 100644 --- a/json_rpc/router.nim +++ b/json_rpc/router.nim @@ -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.} diff --git a/tests/all.nim b/tests/all.nim index 696a09a..42474cb 100644 --- a/tests/all.nim +++ b/tests/all.nim @@ -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 diff --git a/tests/test_batch_call.nim b/tests/test_batch_call.nim new file mode 100644 index 0000000..0b14889 --- /dev/null +++ b/tests/test_batch_call.nim @@ -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()