Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,27 @@ const router = new Router(openrpcDocument, methodHandlerMapping);
const router = new Router(openrpcDocument, { mockMode: true });
```

###### `x-implemented-by` (bidirectional calls)

`Router` natively understands the OpenRPC `x-implemented-by` extension:

- Methods default to `["server"]` when the extension is omitted.
- Methods tagged with `["client"]` (or both) are excluded from inbound server routing.
- `router.getMethodsImplementedBy("client")` returns the client-handled method list.
- When a `client` proxy is provided via the transport context (WebSocket does this automatically), it is appended as the final handler argument so server logic can call client methods directly.

```typescript
const router = new Router(openrpcDocument, {
addition: async (a: number, b: number, client?: { notify: (value: number) => Promise<void> }) => {
if (client) {
await client.notify(a + b);
}
return a + b;
},
notify: async () => undefined,
});
```

##### Creating Transports

###### IPC
Expand Down Expand Up @@ -190,6 +211,47 @@ const wsFromHttpsTransport = new WebSocketServerTransport(webSocketFromHttpsOpti
const wsTransport = new WebSocketServerTransport(webSocketOptions); // Accepts http transport as well.
```

###### Bidirectional `x-implementedBy` example

This repository includes a minimal server/client pair that demonstrates:
- server-only methods (`"x-implementedBy": ["server"]`)
- client-only methods (`"x-implementedBy": ["client"]`)
- methods implemented by both (`"x-implementedBy": ["server", "client"]`)

Run in separate terminals:

```bash
npm run example:bidirectional:server
```

```bash
npm run example:bidirectional:client
```

Example sources:
- `src/examples/bidirectional/openrpc.ts`
- `src/examples/bidirectional/server.ts`
- `src/examples/bidirectional/client.ts`

###### `outboundHandler` example

This repository also includes a minimal `outboundHandler` example where the server
proactively calls connected client methods on an interval.

Run in separate terminals:

```bash
npm run example:outbound:server
```

```bash
npm run example:outbound:client
```

Example sources:
- `src/examples/bidirectional/server-outbound.ts`
- `src/examples/bidirectional/client-outbound.ts`

###### Add components as you go
```
const server = new Server();
Expand Down
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@
"watch:build": "tsc --watch",
"watch:test": "jest --watch",
"lint": "eslint . --ext .ts",
"lint:fix": "eslint . --ext .ts --fix"
"lint:fix": "eslint . --ext .ts --fix",
"example:bidirectional:server": "npm run build && node build/examples/bidirectional/server.js",
"example:bidirectional:client": "npm run build && node build/examples/bidirectional/client.js",
"example:outbound:server": "npm run build && node build/examples/bidirectional/server-outbound.js",
"example:outbound:client": "npm run build && node build/examples/bidirectional/client-outbound.js"
},
"author": "",
"license": "Apache-2.0",
Expand Down
108 changes: 108 additions & 0 deletions src/examples/bidirectional/client-outbound.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import WebSocket from "ws";

const URL = "ws://localhost:9851";

interface PendingRequest {
resolve: (value: unknown) => void;
reject: (err: Error) => void;
}

const ws = new WebSocket(URL);
const pending = new Map<string, PendingRequest>();
let nextRequestId = 0;

function sendRequest(method: string, params: unknown[]): Promise<unknown> {
return new Promise((resolve, reject) => {
const id = `client-${nextRequestId++}`;
pending.set(id, { resolve, reject });
ws.send(JSON.stringify({
id,
jsonrpc: "2.0",
method,
params,
}));
});
}

function sendResult(id: string, result: unknown) {
ws.send(JSON.stringify({
id,
jsonrpc: "2.0",
result,
}));
}

function handleIncomingRequest(payload: any) {
if (!payload.id) {
return;
}

if (payload.method === "clientHello") {
const name = payload.params?.[0];
sendResult(payload.id, `Hello ${name} (from outbound client).`);
return;
}

if (payload.method === "bounce") {
const text = payload.params?.[0];
const result = `[outbound client bounce] ${text}`;
console.log("received outbound call:", result);
sendResult(payload.id, result);
return;
}

ws.send(JSON.stringify({
id: payload.id,
jsonrpc: "2.0",
error: {
code: -32601,
message: `Unknown method "${payload.method}"`,
},
}));
}

function handleIncomingResponse(payload: any) {
if (!payload.id) {
return;
}
const pendingRequest = pending.get(payload.id);
if (!pendingRequest) {
return;
}

pending.delete(payload.id);
if (payload.error) {
pendingRequest.reject(new Error(payload.error.message || "Unknown JSON-RPC error"));
return;
}
pendingRequest.resolve(payload.result);
}

ws.on("message", (raw) => {
const payload = JSON.parse(raw.toString());
if (payload.method) {
handleIncomingRequest(payload);
return;
}
handleIncomingResponse(payload);
});

ws.on("open", async () => {
try {
const response = await sendRequest("serverCallsClient", ["Bob"]);
console.log("serverCallsClient:", response);
console.log("waiting 6 seconds for outboundHandler calls...");
setTimeout(() => ws.close(), 6000);
} catch (err) {
console.error("Client request failed:", err);
ws.close();
}
});

ws.on("error", (err) => {
console.error("WebSocket error:", err);
});

ws.on("close", () => {
process.exit(0);
});
117 changes: 117 additions & 0 deletions src/examples/bidirectional/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import WebSocket from "ws";

const URL = "ws://localhost:9850";

interface PendingRequest {
resolve: (value: unknown) => void;
reject: (err: Error) => void;
}

const ws = new WebSocket(URL);
const pending = new Map<string, PendingRequest>();
let nextRequestId = 0;

function sendRequest(method: string, params: unknown[]): Promise<unknown> {
return new Promise((resolve, reject) => {
const id = `client-${nextRequestId++}`;
pending.set(id, { resolve, reject });
ws.send(JSON.stringify({
id,
jsonrpc: "2.0",
method,
params,
}));
});
}

function sendResult(id: string, result: unknown) {
ws.send(JSON.stringify({
id,
jsonrpc: "2.0",
result,
}));
}

function sendError(id: string, message: string) {
ws.send(JSON.stringify({
id,
jsonrpc: "2.0",
error: {
code: -32601,
message,
},
}));
}

function handleIncomingRequest(payload: any) {
if (!payload.id) {
return;
}

if (payload.method === "clientHello") {
const name = payload.params?.[0];
sendResult(payload.id, `Hello ${name} (from client).`);
return;
}

if (payload.method === "bounce") {
const text = payload.params?.[0];
sendResult(payload.id, `[client bounce] ${text}`);
return;
}

sendError(payload.id, `Unknown method "${payload.method}"`);
}

function handleIncomingResponse(payload: any) {
if (!payload.id) {
return;
}

const pendingRequest = pending.get(payload.id);
if (!pendingRequest) {
return;
}

pending.delete(payload.id);
if (payload.error) {
pendingRequest.reject(new Error(payload.error.message || "Unknown JSON-RPC error"));
return;
}

pendingRequest.resolve(payload.result);
}

ws.on("message", (raw) => {
const payload = JSON.parse(raw.toString());
if (payload.method) {
handleIncomingRequest(payload);
return;
}
handleIncomingResponse(payload);
});

ws.on("open", async () => {
try {
const hello = await sendRequest("serverHello", ["Alice"]);
console.log("serverHello:", hello);

const serverUsedClient = await sendRequest("serverCallsClient", ["Alice"]);
console.log("serverCallsClient:", serverUsedClient);

const bounce = await sendRequest("bounce", ["Hello from client"]);
console.log("bounce:", bounce);
} catch (err) {
console.error("Client call failed:", err);
} finally {
ws.close();
}
});

ws.on("error", (err) => {
console.error("WebSocket error:", err);
});

ws.on("close", () => {
process.exit(0);
});
41 changes: 41 additions & 0 deletions src/examples/bidirectional/openrpc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { OpenrpcDocument as OpenRPC } from "@open-rpc/meta-schema";

const bidirectionalOpenRPCDocument: OpenRPC = {
openrpc: "1.2.6",
info: {
title: "Bidirectional Example",
version: "1.0.0",
},
methods: [
{
name: "serverHello",
summary: "Implemented by the server only.",
params: [{ name: "name", schema: { type: "string" } }],
result: { name: "message", schema: { type: "string" } },
"x-implemented-by": ["server"],
},
{
name: "clientHello",
summary: "Implemented by the client only.",
params: [{ name: "name", schema: { type: "string" } }],
result: { name: "message", schema: { type: "string" } },
"x-implemented-by": ["client"],
},
{
name: "bounce",
summary: "Implemented by both client and server.",
params: [{ name: "text", schema: { type: "string" } }],
result: { name: "message", schema: { type: "string" } },
"x-implemented-by": ["server", "client"],
},
{
name: "serverCallsClient",
summary: "Server method that calls client methods via injected client proxy.",
params: [{ name: "name", schema: { type: "string" } }],
result: { name: "message", schema: { type: "string" } },
"x-implemented-by": ["server"],
},
],
};

export default bidirectionalOpenRPCDocument;
Loading