Last time I sketched out Raynor RPC. A bit of code got written and put on GitHub but nothing big. In this one I’m going to polish up that thing. The main focus is on getting the annotations right.
The big idea with these annotations is that they’re supposed to be a nicer way to describe a service and the mechanics of making an RPC. Consider last episode’s example service:
It exists at the source level. At compile time if you will. But we really want it to exist at runtime so it’s available as a base for all the other nice things we want to do - generated clients and servers and all the RPC machinery around it. So we would actually want it to be a data structure describing the service. Something like this:
But I don’t think it’s an understatement to say that this is ugly to both read and write. And it’s actually dangerously close to WSDL and other XML nightmares. Luckily TypeScript’s decorators allow enough introspection to do the translation from source-level constructs to the description. So we get the best of both worlds - nice syntax and An easy to use data structure in code.
So the initial game is to build the annotations and make them extract the proper data. For completeness here is the current set:
The descriptors themselves have the following types:
The actual current sources can be found here. There’s still some issues though, and I haven’t managed to capture types for parameters as generic types, just for the output.
|Note: one strategy here would be to allow a single parameter to RPC methods. This corresponds to best practices when designing services anyway, cause there’s a lot of issues around versioning when you allow multiple parameters and adding and removing them. A second strategy would be to allow at most a high number of them, like 8 and go with that.|
The big idea is that after we’ve run all this decorator code at module load time, the annotated class has a
__service field which contains the data structure from above. Notice that this bit is very much type unsafe. We don’t have the mechanisms to say that a class annotated with
@Service will have the field. So there’s a lot more instances of defensive programming down the line etc.
For example, the code for the
Param decorator looks like:
Notice that we can capture the type of the parameter here, but we can’t “store” it for later with the current type system. All of them look more or less the same and you can check them out on GitHub.
Note: in Raynor, we can’t actually ensure that the type
Param<T> which the
MarshallerConstructor uses is the same or compatible to the actual type of the parameter. The following will actually pass compilation:
That’s it for now. In part 3 we’ll focus on actually transforming the description so far into a client and server objects. A sneak preview - well do an in-memory and an Express based implementation at the same time. In this sort of situations I find it very useful to target two approaches at the same time, as possibilities for abstraction and defining the right interfaces crop up better. We won’t be tied down to the way a particular approach works. We worry a out context or any of the other big features just yet.