Skip to content

vasyas/push-rpc

 
 

Repository files navigation

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:

Possible Applications

  • 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.

Getting Started

Installation

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.

Example code

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.

API

@push-rpc/core

@push-rpc/websocket

@push-rpc/http

@push-rpc/tcp

Protocol Details

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.
Each remote call results in either RESULT or ERROR message, otherwise timeout error thrown.

RESULT, 3 [3, ID, response]
[3, "dfd9742e-2d44-11ea-978f-2e728ce88125", {"email": "a@a.com"}]

Successful result of remote method call.
Message ID is the same as in corresponding CALL message.

ERROR, 4 [4, ID, code, description, details]
[4, "dfd9742e-2d44-11ea-978f-2e728ce88125", null, "Invalid Value", {"field": "id"}]

Indicate error during remote method call.
Message ID is the same as in corresponding CALL message.
Local proxy will raise Error with message equals to description or code and details fields copied to Error object.

SUBSCRIBE, 11 [11, ID, name, params]
[11, "dfd9742e-2d44-11ea-978f-2e728ce88125", "user", {"id": 246}]

Subscribe to remote topic with name and parameters.
After subscription , client will receive data updates.

Right after subscription remote will send current data.

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.
Called after subscribe or when data change.

GET, 14 [14, ID, name, params]
[14, "dfd9742e-2d44-11ea-978f-2e728ce88125", "user", {"id": 246}, {"email": "a@a.com"}]

Get topic data without subscription.
Will generate response RESULT message with the same ID.

FAQ

How to add path to WebSockets (routing)

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,
})

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

No packages published