RPC
A fully-typed, transport-agnostic, bi-directional RPC framework that also supports passing functions as parameters and returning functions as results.
In JavaScript, passing around first-class functions is a basic part of writing idiomatic code. Unfortunately, as soon as a process (or context) boundary is introduced between the caller and callee, this breaks down. JavaScript Functions are not Transferable objects. This library aims to help with situations where two processes or contexts need to invoke functions between themselves and may even want to pass around callback functions.
Example
Here is an example of one side of an RPC use-case. Imagine we are writing an in-browser editor (something like the Velcro Playground) where we have some expensive logic we want to delegate to a Worker. Here, let's imagine we want to ask the worker to acquire all typings files for a set of dependencies. For each dependency that is found, we provide a callback that should be falled with a { path, content } object.
Note: It is interesting to point out that even though this logic is crossing a process boundary, we are freely passing callback functions in the arguments. The local peer passes a callback and the remote peer receives a function while the library handles piping the two together.
workerClient.ts:
import { connect, Transport } from '@ggoodman/rpc';
import { WorkerApi } from './workerImpl';
const worker = new Worker('./workerImpl', { type: 'module' });
const workerPeer = connect<WorkerApi>(Transport.fromDomWorker(worker));
export function acquireTypes(
dependencies: Record<string, string>,
onDependency: (file: { path: string; content: string }) => void
) {
return workerPeer.invoke('acquireTypes', dependencies, onDependency);
}
workerImpl.ts:
import { connect, Transport } from '@ggoodman/rpc';
const workerApi = {
acquireTypes: async (
dependencies: Record<string, string>,
onDependency: (file: { path: string; content: string }) => void
) => {
for (const dependency in dependencies) {
onDependency({
path: `/node_modules/${dependency}/index.d.ts`,
content: `declare module "${dependency}" {}`,
});
}
},
};
Installation
npm install @ggoodman/rpc
Usage
Creating RPC instances uses a Builder Pattern.
expose(localApi): Builder<typeof localApi>
Exposes a local API where localApi is a map of function names to function implementations. Returns a Builder instance.
You can then obtain an API<RemoteApiType, typeof localApi> instance by connecting to a Transport:
const api = expose(localApi).connect<RemoteApiType>(transport);
connect<RemoteApiType>(transport): API<RemoteApiType>
Connects to a remote API over the given transport and returns an API instance.
API
Represents an instance of a connected RPC API.
invoke(methodName, ...args): Promise<ReturnType>
Invoke methodName on the other side of the transport with args ...args. This will always return a Promise.
dispose()
Frees up resources, disposes the transport and any installed Codecs.
Transport
Exposes factory functions for constructing Transports for various use-cases. Transport is also an interface describing the required API for compatible transports that can be used with this library.
export interface Transport {
dispose(): void;
onMessage(handler: (msg: unknown[]) => void): { dispose(): void };
sendMessage(msg: unknown[], transfer?: unknown[]): void;
}
fromNodeMessagePort(port): Transport
Construct a Transport from a Node-compatible MessagePort.
fromNodeDomPort(port): Transport
Construct a Transport from a browser-compatible MessagePort.
fromNodeDomWorker(worker): Transport
Construct a Transport from a browser-compatible Worker.