typed-rpc icon indicating copy to clipboard operation
typed-rpc copied to clipboard

FR: Support for nesting

Open benmerckx opened this issue 2 years ago • 1 comments
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 avatar Dec 20 '22 12:12 benmerckx

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

daniel-nagy avatar Jan 12 '24 15:01 daniel-nagy