UPDATE I had to pull out versions 0.1.0 and 0.1.1 from CPAN. See the followup post.

I don’t usually announce regular releases of my modules. But not this time. I start this new year with the new v0.1 branch of Cro::RPC::JSON. Version 0.1.1 is currently available on CPAN (will likely be replaced with fez as soon as it is ready). The release is a result of so extensive changes in the module that I had to bump its :api version to 2.

Here is what it’s all about.

Why Cro::RPC::JSON

Before I tell about the changes, let me briefly introduce the module itself.

The primary purpose of Cro::RPC::JSON is to provide a simple way to use the same API class for both server-side Raku code and for JSON-RPC calls. For example, if we have a class for accessing an inventory database of some kind then it should be easy to use the same class to serve our front-end JavaScript code:

class Inventory {
    ...
    method find-item(Str:D $name) is json-rpc { ... }
    ...
}

That’s all. The only limitation the module currently imposing on the methods is accepting simple arguments and returning JSONifiable values, as supported by JSON::Fast. Marshalling/unmarshalling of parameters/return values is considered, but I’m not certain yet as to how exactly to implement it.

Now, all we need to serve JSON-RPC calls is to add this kind of entry into Cro routes:

# If Inventory is not thread-safe this this line has to be placed inside the
# 'post' block.
my $inventory-actor = Inventory.new;
route {
    ...
    post -> 'api' {
        json-rpc $inventory-actor;
    }
}

And that’s all.

Cro::RPC::JSON also supports code objects as handlers of the RPC requests. But let me omit this part and send you directly to the documentation. Let’s get to the point instead!

WebSockets Support

A long-planned feature I never had enough time to get implemented. But when the life itself demands it in a form of an in-house project, where WebSockets are a natural fit, who am I to disobey the command?

The support comes in a form of implementing both JSON-RPC and asynchronous notifications support by treating socket as a bi-directional stream of JSON objects. JSON-RPC ones are recognized by either presence of jsonrpc key in an object; or by treating a JSON array as a JSON-RPC batch. Because the RPC traffic is prioritized over free-form notifications it means that the latter can only be represented by JSON objects (and without jsonrpc key in them!). No other limitations implied. I don’t consider the constraint as a problem because the primary purpose of non-RPC objects is to support push notifications. For any other kind of traffic it is always possible to open a new RPC-free socket.

The use of our actor/API class with WebSockets is as simple as:

route {
    ...
    get -> 'api' {
        json-rpc :web-socket, $inventory-actor;
    }
}

Note that there is no need to change our Inventory class aside of the already added is json-rpc trait. Same code will work for both HTTP/POST and WebSockets transports! It actually makes it possible to provide both interfaces on the server side by simply having two route entries – one for each case. Which one to use would depend on what kind of optimization developer is willing to achieve. I haven’t had time to benchmark, but common sense tells me that where WebSockets provide less latency and is great for single requests affecting your application response time on user actions; HTTP/POST would serve better on big parallelizable requests. For example, we can fill in a big table by requesting each individual line of it from a server in asynchronous manner over HTTP/POST, allowing the server to process all requests in parallel.

Modes Of Operation

The initial v0.0 branch of the module was strictly about HTTP/POST transport and thus only supported simple “invoke code - get result” mode. The only alternative provided to a user was the choice between using a class or a code object (see, for example, this route entry from Cro::RPC::JSON test suite.

Introduction of asynchronous WebSockets consequently introduced the demand for additional asynchronousity. If we’re about to support push notifications then we have to somehow react to server-side events too, right? Therefore the module now provides three different modes of operation: synchronous, asynchronous, and hybrid. Again, I’d better avoid citing the documentation here. Just single example for our Inventory class:

method subsribe(Str:D $notification) is json-rpc("rpc.on") { ... }
method unsubscribe(Str:D $notification) is json-rpc("rpc.off") { ... }
method on-inventory-update is json-rpc(:async) {
    supply {
        whenever $!database.updated -> $update-event {
            emit %(
                notification => 'ItemUpdate',
                params => %(
                    id => $update-event.item.id,
                    status => $update-event.status,
                ),
            ) if %!subsriptions<ItemUpdate>;
        }
    }
}

Now, all our client-side JavaScript code needs is to call rpc.on JSON-RPC method with “ItemUpdate” as the argument and start listening for incoming events. Any item update in the inventory will now be tracked on the client side automatically.

Roles And Classes

Don’t wanna focus on this, mostly technical fixes like support for parameterized roles, fixed inheritance and role consumption.

A note to myself: don’t you want to document all this?

Error Handling

This was perhaps the biggest nightmare. As it is always with asynchronous code. But, hopefully, I finally got it done right. For synchronous code and class method calls the module should now do all is needed to properly handle any exception leaked from the user code. For JSON-RPC method calls this would mean correct response produced with error key set.

For asynchronous code things are much harder to keep control of. Whereas HTTP/POST allows to do some guesses and provide some additional error processing by Cro::RPC::JSON, for WebSockets any unhandled exception in any async code means socket termination. Being rather new to this technology, I spent a lot of time trying to figure out how to properly respond to a request until realized that actually there is no solution to this problem. The reason is as simple as it only can be: if asynchronous user code thrown then the Supply it provided is actually terminated. Since the supply is an integral part of the whole JSON-RPC/WebSocket processing pipeline, its termination means the pipeline is disrupted.

LAST

It’s now time to dive into the muddy waters of TypeScript/JavaScript. I have already tested Cro::RPC::JSON with Vue framework and rpc-websockets module and it passed the basic tests of calling methods and processing push notifications. Will see where this all eventually takes me. After all, this is gonna be my first production project in Raku.

Comments