Skip to content

asnowc/cpcall

Repository files navigation

ESM package NPM version JSR version Node version Install size Build status

CP-Call is a protocol independent remote procedure call (RPC) library designed for JavaScript

中文 | API | Examples

Features

  • A remote call manipulates the remote proxy object with almost the same syntax as a native JavaScript call

  • Services can be defined using ECMA Script decorators. See the use of decorators

  • Protocol independent, can be based on TCP, IPC, WebSocket, etc

  • Type safety

  • Two-way remote call

  • By default, JBOD binary encoding is used for data transfer. Compared to JSON, it has the following advantages:

    • More data types Examples include bigint, Set, Map, RegExp, Error, UInt8Array, etc. (see supported data types), which means that when calling remote methods, you can pass these arguments directly without conversion
    • Smaller data size. For common use cases, the encoded size is about 70% of JSON, No need to define data structures, ideal for dynamically typed languages
  • No need to define data structures, ideal for dynamically typed languages

Usage

Expose globalThis

In the following example, Alice serves as the client and Bob serves as the server. They exposed global objects to each other and called each other's console.log() method before ending the call.

Node

Bob: tcp server

import net from "node:net";
import { CpCall, createSocketCpc } from "cpcall";

async function onRpcConnected(cpc: CpCall) {
  cpc.exposeObject(globalThis);
  const remoteAlice = cpc.genCaller();
  await remoteAlice.console.log("Bob called Alice");
  await cpc.endCall();
}

const server = new net.Server((socket) => {
  const cpc = createSocketCpc(socket);
  onRpcConnected(cpc);
});
server.listen(8888);

Alice: tcp client

import { connect } from "node:net";
import { createSocketCpc } from "cpcall";

const socket = connect(8888);
socket.on("connect", async () => {
  const cpc = createSocketCpc(socket);
  const remote = cpc.genCaller<typeof globalThis>();

  await remote.console.log("Alice Called Bob");

  await cpc.endCall();
});

Deno

Bob: tcp server

import { CpCall, createWebStreamCpc } from "cpcall";

async function onRpcConnected(cpc: CpCall) {
  cpc.exposeObject(globalThis);
  const remoteAlice = cpc.genCaller();
  await remoteAlice.console.log("Bob called Alice");
  await cpc.endCall();
}

const server = Deno.listen({ port: 8888 });
for await (const conn of server) {
  const cpc = createWebStreamCpc(conn);
  onRpcConnected(cpc);
}

Alice: TCP client

import { createWebStreamCpc } from "cpcall";

async function connTcpCpc() {
  const conn = await Deno.connect({ port: 8888 });
  const cpc = createWebStreamCpc(conn);
  cpc.exposeObject(globalThis);

  const remoteBob = cpc.genCaller<typeof globalThis>();
  await remoteBob.console.log("Alice called bob");

  await cpc.endCall();
}

await connTcpCpc();

Browser

Bob: websocket server. The server is implemented by Deno or Node

async function onRpcConnected(cpc: CpCall) {
  cpc.exposeObject(globalThis);
  const remoteAlice = cpc.genCaller();
  await remoteAlice.console.log("Bob called Alice");
  await cpc.endCall();
}

Deno http server

import { CpCall, createWebSocketCpcOnOpen } from "cpcall";
Deno.serve({ port: 8887 }, function (req, res): Response {
  const upgrade = req.headers.get("upgrade") || "";
  if (upgrade.toLowerCase() != "websocket") {
    return new Response("hi");
  }
  const { response, socket } = Deno.upgradeWebSocket(req);
  createWebSocketCpcOnOpen(socket).then(onRpcConnected, console.error);
  return response;
});

Node http server

import { WebSocketServer } from "npm:ws";
import http from "node:http";
import { CpCall, createWebSocketCpcOnOpen } from "cpcall";

const server = new http.Server();
const wsServer = new WebSocketServer({ server });
wsServer.on("connection", async (ws) => {
  createWebSocketCpcOnOpen(ws).then(onRpcConnected);
});
server.listen(8887);

Alice: websocket client

import { createWebSocketCpcOnOpen } from "https://esm.sh/[email protected]";

async function connectWsCpc() {
  const ws = new WebSocket("ws://127.0.0.1:8887");
  const cpc = await createWebSocketCpcOnOpen(ws);
  cpc.exposeObject(globalThis);
  const remote = cpc.genCaller<typeof globalThis>();
  await remote.console.log("Alice called Bob");

  await cpc.endCall();
}

connectWsCpc();

Expose object

The following example shows the invocation of the proxy object

server.ts

class Service {
  add(a: number, b: number) {
    return a + b;
  }
  multiType(...args: any[]) {
    return args.length;
  }
  async getPromise(time: number): Promise<number> {
    return new Promise<number>((resolve) => setTimeout(() => resolve(time), time));
  }
  throwError() {
    throw new Error("throw an error");
  }
  obj = {
    method0() {
      return 0;
    },
    lv1: {
      lv2: {
        method2() {
          return 2;
        },
      },
    },
  };
}

cpc.exposeObject(new Service());

export type { Service };

client.ts

import type { Service } from "./server.ts";

const res = await cpc1.call("add", 1, 2); // res === 3
cpc1.exec("add", 1, 2); // No need to retrieve the return value

const service = cpc1.genCaller<Service>(); // Use proxy objects to obtain complete type prompts

await service.add(1, 2);
await service.getPromise(100);
await service.throwError().catch((e) => {
  console.log(e); // Error: throw an error
});

const args = [
  {
    number: 1,
    bigint: 2n,
    string: "text",
    regExp: /\d+/,
    boolean: true,
    symbol: Symbol("cpcall"),
  },
  undefined,
  null,
  new Uint8Array(10),
  new Error("ha ha"),
  new Set([1, 2, 3]),
  new Map([["str", 1]]),
];
await service.multiType(...args); // Supports many data types

await service.obj.method0();
await service.obj.lv1.lv2.method2(); // Deep call

More Examples

The use of decorators
Custom datagram serialization (documentation to be supplemented)
Implementing a CpCall over http via the CpCall class (documentation to be supplemented)
More Examples

Concepts

Remote Procedure Call (RPC)

Differences with tRpcgRpcsocket.io

One of the most direct differences between tRpc and gRpc is that tRpc and gRpc are both modeled as a client initiating requests (calls) and the server responding, which means they can only initiate requests in one direction. However, cpcall can initiate calls in both directions.

Name Protocol based Direction of call
tRpc http One-way call
gRpc http2 One-way call
cpcall Two-way streaming (protocol independent)) Two-way call

Socket.io is a WebSocket-based library that enables bidirectional real-time communication between two ends. It provides behaviors such as unicast and multicast. The publish-subscribe pattern is mainly used when working with it.

While cpCall is an end-to-end bidirectional calling RPC library. Essentially, cpCall and socket.io are not libraries of the same type, but under the WebSocket protocol, they can both achieve similar behaviors.

Others

CPCALL Data frame protocol

About

Two-way Remote Procedure Call (RPC) Based on Data Frame

Resources

License

Stars

Watchers

Forks