@@ -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