typed-rpc
typed-rpc copied to clipboard
FR: Support for nesting
trafficstars
An example use-case:
export const library = {
music: {
listArtists() {
return [ /* ... */ ]
}
},
books: {
listAuthors() {
return [ /* ... */ ]
}
}
}
export type LibraryService = typeof library
const client = rpcClient<LibraryService>('/api')
console.log(
await client.books.listAuthors()
)
I think this could be achieved by:
- wrapping the request resolver function that is returned in the client getter in a proxy as well
- add the dot path to the method that is called... not sure if valid jsonrpc?
- recursively apply Promisify to the typescript types that are not functions
- parse the dot path and follow properties to the method server side
@benmerckx I am the author of Transporter. Transporter was designed to use object composition. Your example use-case would work as is with Transporter.
Transporter is a little more low level and general purpose than most other TypeScript RPC libraries out there. Currently I don't provide a specific HTTP server or client. Here is what your example would look like using Transporter.
First on the server. In this case I'm using Bun.serve as my server.
import * as Message from "@daniel-nagy/transporter/Message";
import * as Observable from "@daniel-nagy/transporter/Observable";
import * as Session from "@daniel-nagy/transporter/Session";
import * as Subprotocol from "@daniel-nagy/transporter/Subprotocol";
import * as SuperJson from "@daniel-nagy/transporter/SuperJson";
export const library = {
music: {
listArtists() {
return [ /* ... */ ]
}
},
books: {
listAuthors() {
return [ /* ... */ ]
}
}
}
export type LibraryService = typeof library
const protocol = Subprotocol.init({
connectionMode: Subprotocol.ConnectionMode.ConnectionLess,
operationMode: Subprotocol.OperationMode.Unicast,
protocol: Subprotocol.Protocol<SuperJson.t>(),
transmissionMode: Subprotocol.TransmissionMode.HalfDuplex
});
Bun.serve({
async fetch(req) {
using session = Session.server({ protocol, provide: library });
const reply = Observable.firstValueFrom(session.output);
const message = SuperJson.fromJson(await req.json())
session.input.next(message as Message.t<SuperJson.t>);
return Response.json(SuperJson.toJson(await reply));
},
port: 3000
});
Then on the client using fetch.
import * as Message from "@daniel-nagy/transporter/Message";
import * as Observable from "@daniel-nagy/transporter/Observable";
import * as Session from "@daniel-nagy/transporter/Session";
import * as Subprotocol from "@daniel-nagy/transporter/Subprotocol";
import * as SuperJson from "@daniel-nagy/transporter/SuperJson";
import type { LibraryService } from '../server';
const client = createClient();
console.log(
await client.books.listAuthors()
)
function createClient() {
const protocol = Subprotocol.init({
connectionMode: Subprotocol.ConnectionMode.Connectionless,
operationMode: Subprotocol.OperationMode.Unicast,
protocol: Subprotocol.Protocol<SuperJson.t>(),
transmissionMode: Subprotocol.TransmissionMode.HalfDuplex,
});
const session = Session.client({
protocol,
resource: Session.Resource<LibraryService>(),
});
const toRequest = (message: string) =>
new Request("http://localhost:3000", {
body: message,
headers: {
"Content-Type": "application/json"
},
method: "POST",
});
session.output
.pipe(
Observable.map(SuperJson.toJson),
Observable.map(JSON.stringify),
Observable.map(toRequest),
Observable.flatMap(fetch),
Observable.flatMap(response => response.json()),
Observable.map(SuperJson.fromJson),
Observable.filter(Message.isMessage)
)
.subscribe(session.input);
return session.createProxy();
}
Using Transporter you would typically expose your API from a single endpoint and use module composition instead of a router.