Skip to content

Commit 9e1fe1a

Browse files
jmorrellclaude
andcommitted
Add bidirectional RPC tests
Tests where both sides have typed services and both sides make calls: - Both sides call each other's methods sequentially - Both sides call each other concurrently - Server calls back to client during request handling - Errors propagate correctly in both directions - Close rejects pending calls on both sides Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9f1106d commit 9e1fe1a

File tree

1 file changed

+223
-0
lines changed

1 file changed

+223
-0
lines changed

src/__tests__/session.test.ts

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1319,3 +1319,226 @@ describe("session error resilience", () => {
13191319
sessionB.close();
13201320
});
13211321
});
1322+
1323+
describe("Bidirectional RPC", () => {
1324+
type MathService = {
1325+
add(a: number, b: number): number;
1326+
multiply(a: number, b: number): number;
1327+
};
1328+
1329+
type GreetingService = {
1330+
greet(name: string): string;
1331+
getLocale(): string;
1332+
};
1333+
1334+
it("both sides call each other's methods", async () => {
1335+
const [transportA, transportB] = createLinkedTransports();
1336+
1337+
const mathService: MathService = {
1338+
add: (a, b) => a + b,
1339+
multiply: (a, b) => a * b,
1340+
};
1341+
1342+
const greetingService: GreetingService = {
1343+
greet: (name) => `Hello, ${name}!`,
1344+
getLocale: () => "en-US",
1345+
};
1346+
1347+
const sessionA = new RpcSession<GreetingService, MathService>(
1348+
transportA,
1349+
mathService,
1350+
{ role: "initiator" },
1351+
);
1352+
const sessionB = new RpcSession<MathService, GreetingService>(
1353+
transportB,
1354+
greetingService,
1355+
{ role: "acceptor" },
1356+
);
1357+
1358+
// A calls B's greeting service
1359+
expect(await sessionA.remote.greet("world")).toBe("Hello, world!");
1360+
expect(await sessionA.remote.getLocale()).toBe("en-US");
1361+
1362+
// B calls A's math service
1363+
expect(await sessionB.remote.add(3, 4)).toBe(7);
1364+
expect(await sessionB.remote.multiply(5, 6)).toBe(30);
1365+
1366+
sessionA.close();
1367+
});
1368+
1369+
it("both sides call each other concurrently", async () => {
1370+
const [transportA, transportB] = createLinkedTransports();
1371+
1372+
const mathService: MathService = {
1373+
add: (a, b) => a + b,
1374+
multiply: (a, b) => a * b,
1375+
};
1376+
1377+
const greetingService: GreetingService = {
1378+
greet: (name) => `Hello, ${name}!`,
1379+
getLocale: () => "en-US",
1380+
};
1381+
1382+
const sessionA = new RpcSession<GreetingService, MathService>(
1383+
transportA,
1384+
mathService,
1385+
{ role: "initiator" },
1386+
);
1387+
const sessionB = new RpcSession<MathService, GreetingService>(
1388+
transportB,
1389+
greetingService,
1390+
{ role: "acceptor" },
1391+
);
1392+
1393+
// Both sides fire calls at the same time
1394+
const [greeting, locale, sum, product] = await Promise.all([
1395+
sessionA.remote.greet("world"),
1396+
sessionA.remote.getLocale(),
1397+
sessionB.remote.add(10, 20),
1398+
sessionB.remote.multiply(3, 7),
1399+
]);
1400+
1401+
expect(greeting).toBe("Hello, world!");
1402+
expect(locale).toBe("en-US");
1403+
expect(sum).toBe(30);
1404+
expect(product).toBe(21);
1405+
1406+
sessionA.close();
1407+
});
1408+
1409+
it("server method calls back to client during handling", async () => {
1410+
const [transportA, transportB] = createLinkedTransports();
1411+
1412+
type ClientService = {
1413+
getMultiplier(): number;
1414+
};
1415+
1416+
type ServerService = {
1417+
computeWithClientMultiplier(a: number, b: number): Promise<number>;
1418+
};
1419+
1420+
const clientService: ClientService = {
1421+
getMultiplier: () => 10,
1422+
};
1423+
1424+
const sessionA = new RpcSession<ServerService, ClientService>(
1425+
transportA,
1426+
clientService,
1427+
{ role: "initiator" },
1428+
);
1429+
1430+
// Server service calls back to the client to get the multiplier
1431+
const serverService: ServerService = {
1432+
computeWithClientMultiplier: async (a, b) => {
1433+
const multiplier = await sessionB.remote.getMultiplier();
1434+
return (a + b) * multiplier;
1435+
},
1436+
};
1437+
1438+
const sessionB = new RpcSession<ClientService, ServerService>(
1439+
transportB,
1440+
serverService,
1441+
{ role: "acceptor" },
1442+
);
1443+
1444+
const result = await sessionA.remote.computeWithClientMultiplier(3, 4);
1445+
expect(result).toBe(70); // (3 + 4) * 10
1446+
1447+
sessionA.close();
1448+
});
1449+
1450+
it("errors propagate correctly in both directions", async () => {
1451+
const [transportA, transportB] = createLinkedTransports();
1452+
1453+
type ServiceA = {
1454+
failA(): never;
1455+
};
1456+
1457+
type ServiceB = {
1458+
failB(): never;
1459+
};
1460+
1461+
const serviceA: ServiceA = {
1462+
failA() {
1463+
const err = new Error("Error from A");
1464+
(err as any).code = -32001;
1465+
throw err;
1466+
},
1467+
};
1468+
1469+
const serviceB: ServiceB = {
1470+
failB() {
1471+
const err = new Error("Error from B");
1472+
(err as any).code = -32002;
1473+
throw err;
1474+
},
1475+
};
1476+
1477+
const sessionA = new RpcSession<ServiceB, ServiceA>(
1478+
transportA,
1479+
serviceA,
1480+
{ role: "initiator" },
1481+
);
1482+
const sessionB = new RpcSession<ServiceA, ServiceB>(
1483+
transportB,
1484+
serviceB,
1485+
{ role: "acceptor" },
1486+
);
1487+
1488+
// A calls B, gets B's error
1489+
await expect(sessionA.remote.failB()).rejects.toThrow(RpcError);
1490+
try {
1491+
await sessionA.remote.failB();
1492+
} catch (err) {
1493+
expect((err as RpcError).code).toBe(-32002);
1494+
expect((err as RpcError).message).toBe("Error from B");
1495+
}
1496+
1497+
// B calls A, gets A's error
1498+
await expect(sessionB.remote.failA()).rejects.toThrow(RpcError);
1499+
try {
1500+
await sessionB.remote.failA();
1501+
} catch (err) {
1502+
expect((err as RpcError).code).toBe(-32001);
1503+
expect((err as RpcError).message).toBe("Error from A");
1504+
}
1505+
1506+
sessionA.close();
1507+
});
1508+
1509+
it("close rejects pending calls on both sides", async () => {
1510+
const [transportA, transportB] = createLinkedTransports();
1511+
1512+
type SlowService = {
1513+
slow(): Promise<string>;
1514+
};
1515+
1516+
// Services that never resolve
1517+
const neverResolveA: SlowService = {
1518+
slow: () => new Promise(() => {}),
1519+
};
1520+
const neverResolveB: SlowService = {
1521+
slow: () => new Promise(() => {}),
1522+
};
1523+
1524+
const sessionA = new RpcSession<SlowService, SlowService>(
1525+
transportA,
1526+
neverResolveA,
1527+
{ role: "initiator" },
1528+
);
1529+
const sessionB = new RpcSession<SlowService, SlowService>(
1530+
transportB,
1531+
neverResolveB,
1532+
{ role: "acceptor" },
1533+
);
1534+
1535+
// Both sides have pending outgoing calls
1536+
const pA = sessionA.remote.slow();
1537+
const pB = sessionB.remote.slow();
1538+
1539+
sessionA.close();
1540+
1541+
await expect(pA).rejects.toThrow();
1542+
await expect(pB).rejects.toThrow();
1543+
});
1544+
});

0 commit comments

Comments
 (0)