CP-Call is a protocol independent remote procedure call (RPC) library designed for JavaScript
-
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
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.
Bob: tcp server
import net from "node:net";
import { CpCall, createSocketCpc } from "cpcall";
async function onRpcConnected(cpc: CpCall) {
cpc.exposeObject(globalThis);
await cpc.call("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);
await cpc.call("console.log", "Alice Called Bob");
await cpc.endCall();
});Bob: tcp server
import { CpCall, createWebStreamCpc } from "cpcall";
async function onRpcConnected(cpc: CpCall) {
cpc.exposeObject(globalThis);
await cpc.call("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);
await cpc.call("console.log", "Alice called bob");
await cpc.endCall();
}
await connTcpCpc();Bob: websocket server. The server is implemented by Deno or Node
async function onRpcConnected(cpc: CpCall) {
cpc.exposeObject(globalThis);
await cpc.call("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/cpcall@0.6.x";
async function connectWsCpc() {
const ws = new WebSocket("ws://127.0.0.1:8887");
const cpc = await createWebSocketCpcOnOpen(ws);
cpc.exposeObject(globalThis);
await cpc.call("console.log", "Alice called Bob");
await cpc.endCall();
}
connectWsCpc();The following example shows invoking exposed methods with cpc.call().
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
const res = await cpc1.call("add", 1, 2); // res === 3
cpc1.exec("add", 1, 2); // No need to retrieve the return value
await cpc1.call("add", 1, 2);
await cpc1.call("getPromise", 100);
await cpc1.call("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 cpc1.call("multiType", ...args); // Supports many data types
await cpc1.call("obj.method0");
await cpc1.call("obj.lv1.lv2.method2"); // Deep callThe 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
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.
