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
63 changes: 63 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,28 @@ const router = new Router(openrpcDocument, methodHandlerMapping);
const router = new Router(openrpcDocument, { mockMode: true });
```

###### router plugins (`x-implementedBy` + client context)

```typescript
import { Router, plugins } from "@open-rpc/server-js";

const router = new Router(openrpcDocument, methodHandlerMapping, {
plugins: [plugins.implementedByPlugin()],
});
```

You can also pass `routerOptions` through `Server`:

```typescript
const server = new Server({
openrpcDocument,
methodMapping,
routerOptions: {
plugins: [plugins.implementedByPlugin()],
},
});
```

##### Creating Transports

###### IPC
Expand Down Expand Up @@ -190,6 +212,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
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
"test": "npm run build && npm run test:unit",
"test:unit": "jest --coverage",
"build": "tsc",
"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",
"watch:build": "tsc --watch",
"watch:test": "jest --watch",
"lint": "eslint . --ext .ts",
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) {

Check warning on line 35 in src/examples/bidirectional/client-outbound.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
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) {

Check warning on line 64 in src/examples/bidirectional/client-outbound.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
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) {

Check warning on line 46 in src/examples/bidirectional/client.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
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) {

Check warning on line 66 in src/examples/bidirectional/client.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
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
Loading