
prpcow
An MIT WebSocket RPC library in TypeScript that I put into production inside a commercial taxi dispatch platform
prpcow is a small open-source library of mine: plain RPC over WebSocket, written in TypeScript and published on npm as prpcow under the MIT license. You call a function that lives on the other side of the socket and await it like a local one. It grew out of a real problem in one of my own products, and later it carried the live data of a taxi and dispatch platform.
The problem showed up in 2021. We were finishing a production-stable version of Cabinet24, every live table in its browser app opened its own WebSocket. At first glance that isn't really an issue, Cabinet24 was one product and could carry it. It starts becoming one when a second site opens its own sockets as well, and once you count how many sockets the next, bigger project would hold open if we kept the same pace, you pretty quickly understand there's no way of going on like that. The number just grows with every table.
So I went through the libraries I knew. GraphQL was the obvious first candidate, it was really popular in 2021 and 2022. Federation back then had no support for subscriptions, and the subscription was the part that decided it for me. A subscription couldn't send a delta. You got either the full data or a single item. Sometimes you need exactly the delta, and with a lot of tables and a lot of other information open at once, it's the only thing that keeps frontend memory use sane.
What I wanted came down to 4 things. It had to be easy to add on any device, in Node and in the browser. It had to be easy to maintain, because once you start piling functionality on, even GraphQL or plain HTTP starts to crack. It had to support rapid development, since we needed APIs fast. And it had to be easy to integrate into any part of the application, even the deepest one. I read through the other options too, gRPC and JSON-RPC among them, and nothing really fit all 4. So I started writing prpcow.
Underneath it's one WebSocket. ws is a peer dependency, and the WebSocket implementation itself can be swapped, which gives us ability to run it inside react-native as well. Once the connection is open, the server and the client are symmetric: the client can expose functions for the server to call, the same way the server exposes them to the client, and every call and every subscription rides that one session. A screen full of live tables holds a single connection open.
There's no schema and no code generation. Adding an API means adding a function, and the endpoint, the schema type, the channel and the code that keeps all three in agreement just never get written. On a growing product that's the difference between a change in one place and a change in several. The behaviour is put together from resolvers you can replace, for functions, models, ids, compression, the session store and a universal RPC layer, so you integrate as shallow or as deep as the application needs. The price is that nothing generates a typed client across the wire. I took that trade on purpose, because I wanted the speed and no generation step in the way.
Transport security comes from wss, and authentication is the host application's job. It runs on the server and in the browser, with CommonJS and ES module builds and type declarations.
The delta I was missing in GraphQL comes as a pattern the transport makes easy. A subscription is just a callback you hand the other side once. The producer holds it by reference, and when something changes, it calls that callback with only the changed slice. The payload carries no schema, so nothing forces the whole object back on every change. A model update is the one case that travels whole.
flowchart LR
C[Client] <-->|one persistent connection| S[Server]
C --> H[Callback handed over once]
H --> P[Server holds it by reference]
P -->|calls it with only what changed| D[Delta arrives at the client]
D --> M[Frontend memory stays small across many tables]prpcow went into production on Express Soft, the commercial taxi and dispatch platform I took on right after Cabinet24. The driver's phone, the passenger's phone and the dispatcher's browser all talked to the server over it. That's 3 applications: the dispatcher and fleet-administrator application, the client application and the driver application, each one of them holding a single persistent connection. On the server it ran in the gateway service, and in the browser it ran in the dispatcher application. The dispatcher side carried live data about vehicles (position, speed, heading), orders (status, driver assignment, scheduling) and driver resources (network state, availability, shifts). Every client got one channel, calls were asynchronous and never blocked the connection, and several data streams went multiplexed over that one connection. The live model deltas went through @node-elion/syncron, another public package of mine.
The driver application worked on 3G and even 2G links. The saving there is the connection itself: you pay to open it once, and after that every call and every push goes over a socket that's already up, in place of a fresh handshake for every request. Compression sits on the same swappable seam, for an application that wants a real compressor on top. On that platform it was really one task less for the whole team.