This is a little project I've been kicking around for a while, and I'm finally in a place that I'd like to show it off for feedback/suggestions. Nerve is a RPC framework that produces a fully typed JavaScript client using nothing but a macro and normal Nim proc definitions. I'm not sure if this could be accomplished in any other language (besides a typed Lisp), and I think it shows off the power of Nim as a language. I'm still working on benchmarks, but I'm anticipating it'll be quick as the runtime is small, and dispatch is done via enum. I tried to document the readme well, and I'd love to get any feedback on the project.
You can install with nimble install nerve.
can the server rpc call the client?
Example makes it look like library is transport-agnostic. Seems like we could have both ends host their own APIs and one end could call another's APIs and we just pass messages between them.
websockets
Whatever you do please keep it transport-agnostic. I am already eyeing your library for my own project and it seems perfect :]
Example makes it look like library is transport-agnostic. Seems like we could have both ends host their own APIs and one end could call another's APIs and we just pass messages between them.
The server is transport agnostic. The dispatch function that it generates takes a JSON object, either parsed or in string form. That object can come from anywhere, and I'm open to including more helper functions to help with different transport layers.
The client is, at the moment, much more restricted. Right now, the client only generates HTTP requests and only works in the JavaScript target. A native client is something I was already planning on adding, and shouldn't be too difficult to support. I just need to decide on a clean mechanism for determining if a service should generate a server or client.
I hadn't thought about making the client transport agnostic, but I'm open adding it :) Off the top of my head, the easiest way I can think to support arbitrary transport layers while keeping the nice auto-generation of the client is to allow users to pass a transport function to the macro. So the macro definition could look something like:
rpc Hello, myTransportFunction:
where myTransportFunction takes a JSON object (still autogenerated by the client) and is responsible for sending it to the server.
Would this work for your purposes? If so, I'd be happy to write up the issue and start development! If there's any other tweaks that would make this more usable, please let me know.
I have nothing against callback, though i am bit unsure how it would work in case where we would like client to connect to multiple servers. Seems like this callback would depend on some kind of global state which probably assumes there is one client in application.
What if we could do something like:
var someConnection = connectToClient()
var helloClient = nerve.createClient(Hello, myTransportFunction, someConnection)
helloClient.helloWorld()
someConnection would be passed to myTransportFunction in this case.I have nothing against callback, though i am bit unsure how it would work in case where we would like client to connect to multiple servers. Seems like this callback would depend on some kind of global state which probably assumes there is one client in application.
I was originally thinking a higher order function that uses closures to close over whatever connection or other arguments. As long as the HOF has the right type signature, the function that produces the HOF could take whatever arguments. The other method I was kicking around was an extendable object type + method dispatch. However, not opposed to the method you suggest. Might workshop a couple things this weekend and see what feels best.
Sure thing, was thinking something like:
proc newTransportFunction(conn: Connection) =
result = proc (req: JsonNode): Future[JsonNode] =
conn.send(req)
let conn = connectToClient()
rpc Hello, newTransportFunction(conn):
# remote proc definitions
Hello.helloWorld()
Alternatively, was also thinking something like:
# From Nerve
type NerveTransport = object of RootObj
method send(transport: NerveTransport, req: JsonNode): Future[JsonNode] =
assert(false, "override")
# From user code
type MyTransport = object of NerveTransport
conn: Connection
method send(transport: MyTransport, req: JsonNode): Future[JsonNode] =
transport.conn.send(req)
let conn = connectToClient()
rpc Hello, MyTransport(conn):
# You get the idea
Just had an idea that I really like as a solution to this issue. I can modify the param signature of every remote proc to add a transport function with a default as the last argument. So a proc like proc greet(greeting, name: wstring): Future[wstring] gets modified to proc greet(greeting, name: wstring, driver: NerveDriver[wstring] = defaultDriver[wstring]): Future[wstring] and the client is able to select an appropriate transport function if desired. 1) No global state, transport function can be initialized with whatever connections right before the call. 2) Protocol agnostic, user just needs to come up with a function that returns type T where the proc return type is Future[T].
Does this meet your needs? This is probably the cleanest and most simple solution I've come up with.
With these changes, Nerve can now support different transport methods, as well as server injected references to other RPC services. I'm sure there are some corner cases I haven't covered, as well as some error messages that could be improved, so any feedback is welcome. The github repo has more information, and I've started documenting the individual modules as well.