A framework for organizing bidirectional client-server communication based on JSON, including server-initiated data push.
Client establishes connection to server and then client and server exchange JSON packets.
Push-RPC allows you to:
- Bi-directionally invoke remote methods on server and client
- Subscribe client for server-side events and vice versa
- Auto-reconnect with subscription refresh
- Create type-safe contracts of remote APIs using TypeScript code shared between client and server
- Create local proxies for remote objects, including support for transferring Date object
- Supported client envs: Node.JS (with
isomorphic-fetch
), browser, React Native. - Client-initiated ping/pong for WS transport (missing feature in Browser websocket)
Supports multiple pluggable transports:
- WebSocket (recommended)
- Plain TCP
- HTTP REST-like, includes generating OpenAPI schema.
- Data-driven apps with or without server-initiated updates
- OCPP-J clients and servers
- IoT devices connecting to servers
- General client/server apps using WebSockets for communications
Not a good place for Push-RPC:
- Communications between microservices. MSs usually require a lot more things besides transport, including discovery, HA and so on. And it would be better to use some Service Mesh framework like Istio or NATS+TypedSubjects.
yarn add @push-rpc/core @push-rpc/websocket
For the server, you will also need
yarn add ws
You can use standard browser WebSockets on the client, or use ws
npm package.
Basic server & client with contract defined in shared code. You can find more examples at https://github.com/vasyas/push-rpc/tree/master/packages/examples
shared.ts:
import {Topic} from "@push-rpc/core"
export interface Services {
todo: TodoService
}
export interface TodoService {
addTodo({text}, ctx?): Promise<void>
todos: Topic<Todo[]>
}
export interface Todo {
id: string
text: string
status: "open" | "closed"
}
client.ts:
import {createRpcClient} from "@push-rpc/core"
import {createWebsocket} from "@push-rpc/websocket"
import {Services} from "./shared"
;(async () => {
const services: Services = (
await createRpcClient(1, () => createWebsocket("ws://localhost:5555"))
).remote
console.log("Client connected")
services.todo.todos.subscribe(todos => {
console.log("Got todo items", todos)
})
await services.todo.addTodo({text: "Buy groceries"})
})()
server.ts:
import {createRpcServer, LocalTopicImpl} from "@push-rpc/core"
import {createWebsocketServer} from "@push-rpc/websocket"
import {Services, Todo, TodoService} from "./shared"
let storage: Todo[] = []
class TodoServiceImpl implements TodoService {
async addTodo({text}) {
storage.push({
id: "" + Math.random(),
text,
status: "open",
})
console.log("New todo item added")
this.todos.trigger()
}
todos = new LocalTopicImpl(async () => storage)
}
const services: Services = {
todo: new TodoServiceImpl(),
}
createRpcServer(services, createWebsocketServer({port: 5555}))
console.log("RPC Server started at ws://localhost:5555")
Run server.ts
and then client.ts
.
Server will send empty todo list on client connecting and then will send updated list on adding new item.
You can use this information to implement Push-RPC protocol in different languages.
Each message is encoded as JSON array. Each message contain message type, message ID, and multiple payload fields. For example, CALL message:
[2, "dfd9742e-2d44-11ea-978f-2e728ce88125", "getRemoteData", {}]
Message | Details |
---|---|
CALL, 2 |
[2, ID, remoteMethodName, params] [2, "dfd9742e-2d44-11ea-978f-2e728ce88125", "getUser", {"id": 5}] Call remote method with params. |
RESULT, 3 |
[3, ID, response] [3, "dfd9742e-2d44-11ea-978f-2e728ce88125", {"email": "a@a.com"}] Successful result of remote method call. |
ERROR, 4 |
[4, ID, code, description, details] [4, "dfd9742e-2d44-11ea-978f-2e728ce88125", null, "Invalid Value", {"field": "id"}] Indicate error during remote method call. |
SUBSCRIBE, 11 |
[11, ID, name, params] [11, "dfd9742e-2d44-11ea-978f-2e728ce88125", "user", {"id": 246}] Subscribe to remote topic with name and parameters. |
UNSUBSCRIBE, 12 |
[12, ID, name, params] [12, "dfd9742e-2d44-11ea-978f-2e728ce88125", "user", {"id": 246}] Unsubscribe remote topic with parameters. |
DATA, 13 |
[13, ID, name, params, data] [13, "dfd9742e-2d44-11ea-978f-2e728ce88125", "user", {"id": 246}, {"email": "a@a.com"}] Send topic data to subscriber. |
GET, 14 |
[14, ID, name, params] [14, "dfd9742e-2d44-11ea-978f-2e728ce88125", "user", {"id": 246}, {"email": "a@a.com"}] Get topic data without subscription. |
Often there're multiple WebSocket servers using same port but different paths. You can use following code to route connections between those servers.
import * as WebSocket from "ws"
import * as http from "http"
function websocketRouter(httpServer, routes) {
httpServer.on("upgrade", (request, socket, head) => {
const pathname = url.parse(request.url).pathname
const serverKey = Object.keys(routes).find(key => pathname.indexOf(key) == 0)
if (!serverKey) {
socket.destroy()
} else {
const server = routes[serverKey]
server.handleUpgrade(request, socket, head, ws => {
server.emit("connection", ws, request)
})
}
})
}
...
const server = http.createServer(requestListener).listen(5555)
const ocppServer: WebSocket.Server = await createOcppServer()
const clientServer: WebSocket.Server = await createClientServer()
const adminServer: WebSocket.Server = await createAdminServer()
websocketRouter(httpServer, {
["/ocpp:"]: ocppServer,
["/client"]: clientServer,
["/admin"]: adminServer,
})