// // Copyright 2021 DXOS.org // import { type EncodingOptions, type ServiceDescriptor, type ServiceHandler, type ServiceProvider, } from '@dxos/codec-protobuf'; import { invariant } from '@dxos/invariant'; import { RpcPeer, type RpcPeerOptions } from './rpc'; /** * Map of service definitions. */ // TODO(burdon): Rename ServiceMap. export type ServiceBundle = { [Key in keyof Services]: ServiceDescriptor }; export type ServiceHandlers = { [ServiceName in keyof Services]: ServiceProvider }; export type ServiceTypesOf> = Bundle extends ServiceBundle ? Services : never; /** * Groups multiple services together to be served by a single RPC peer. */ export const createServiceBundle = (services: ServiceBundle): ServiceBundle => services; /** * Type-safe RPC peer. */ export class ProtoRpcPeer { constructor( public readonly rpc: Service, private readonly _peer: RpcPeer, ) {} async open(): Promise { await this._peer.open(); } async close(): Promise { await this._peer.close(); } async abort(): Promise { await this._peer.abort(); } } export interface ProtoRpcPeerOptions extends Omit { /** * Services that are expected to be implemented by the counter-space. */ // TODO(burdon): Rename proxy. requested?: ServiceBundle; /** * Services exposed to the counter-space. */ // TODO(burdon): Rename service. exposed?: ServiceBundle; /** * Handlers for the exposed services */ handlers?: ServiceHandlers; /** * Encoding options passed to the underlying proto codec. */ encodingOptions?: EncodingOptions; } /** * Create type-safe RPC peer from a service bundle. * Can both handle and issue requests. */ // TODO(burdon): Currently assumes that the proto service name is unique. // Support multiple instances services definitions (e.g., halo/space invitations). export const createProtoRpcPeer = ({ requested, exposed, handlers, encodingOptions, ...rest }: ProtoRpcPeerOptions): ProtoRpcPeer => { // Create map of RPCs. const exposedRpcs: Record> = {}; if (exposed) { invariant(handlers); for (const serviceName of Object.keys(exposed) as (keyof Server)[]) { // Get full service name with the package name without '.' at the beginning. const serviceFqn = exposed[serviceName].serviceProto.fullName.slice(1); const serviceProvider = handlers[serviceName]; exposedRpcs[serviceFqn] = exposed[serviceName].createServer(serviceProvider, encodingOptions); } } // Create peer. const peer = new RpcPeer({ ...rest, callHandler: (method, request, options) => { const [serviceName, methodName] = parseMethodName(method); if (!exposedRpcs[serviceName]) { throw new Error(`Service not supported: ${serviceName}`); } return exposedRpcs[serviceName].call(methodName, request, options); }, streamHandler: (method, request, options) => { const [serviceName, methodName] = parseMethodName(method); if (!exposedRpcs[serviceName]) { throw new Error(`Service not supported: ${serviceName}`); } return exposedRpcs[serviceName].callStream(methodName, request, options); }, }); const requestedRpcs: Client = {} as Client; if (requested) { for (const serviceName of Object.keys(requested) as (keyof Client)[]) { // Get full service name with the package name without '.' at the beginning. const serviceFqn = requested[serviceName].serviceProto.fullName.slice(1); requestedRpcs[serviceName] = requested[serviceName].createClient( { call: (method, req, options) => peer.call(`${serviceFqn}.${method}`, req, options), callStream: (method, req, options) => peer.callStream(`${serviceFqn}.${method}`, req, options), }, encodingOptions, ); } } return new ProtoRpcPeer(requestedRpcs, peer); }; export const parseMethodName = (method: string): [serviceName: string, methodName: string] => { const separator = method.lastIndexOf('.'); const serviceName = method.slice(0, separator); const methodName = method.slice(separator + 1); if (serviceName.length === 0 || methodName.length === 0) { throw new Error(`Invalid method: ${method}`); } return [serviceName, methodName]; }; // // TODO(burdon): Remove deprecated (only bot factory). // /** * Create a type-safe RPC client. * @deprecated Use createProtoRpcPeer instead. */ export const createRpcClient = ( serviceDef: ServiceDescriptor, options: Omit, ): ProtoRpcPeer => { const peer = new RpcPeer({ ...options, callHandler: () => { throw new Error('Requests to client are not supported.'); }, }); const client = serviceDef.createClient({ call: peer.call.bind(peer), callStream: peer.callStream.bind(peer), }); return new ProtoRpcPeer(client, peer); }; /** * @deprecated */ export interface RpcServerOptions extends Omit { service: ServiceDescriptor; handlers: S; } /** * Create a type-safe RPC server. * @deprecated Use createProtoRpcPeer instead. */ export const createRpcServer = ({ service, handlers, ...rest }: RpcServerOptions): RpcPeer => { const server = service.createServer(handlers); return new RpcPeer({ ...rest, callHandler: server.call.bind(server), streamHandler: server.callStream.bind(server), }); }; /** * Create type-safe RPC client from a service bundle. * @deprecated Use createProtoRpcPeer instead. */ export const createBundledRpcClient = ( descriptors: ServiceBundle, options: Omit, ): ProtoRpcPeer => { return createProtoRpcPeer({ requested: descriptors, ...options, }); }; /** * @deprecated */ export interface RpcBundledServerOptions extends Omit { services: ServiceBundle; handlers: S; } /** * Create type-safe RPC server from a service bundle. * @deprecated Use createProtoRpcPeer instead. */ // TODO(burdon): Support late-binding via providers. export const createBundledRpcServer = ({ services, handlers, ...rest }: RpcBundledServerOptions): RpcPeer => { const rpc: Record> = {}; for (const serviceName of Object.keys(services) as (keyof S)[]) { // Get full service name with the package name without '.' at the beginning. const serviceFqn = services[serviceName].serviceProto.fullName.slice(1); rpc[serviceFqn] = services[serviceName].createServer(handlers[serviceName] as any); } return new RpcPeer({ ...rest, callHandler: (method, request) => { const [serviceName, methodName] = parseMethodName(method); if (!rpc[serviceName]) { throw new Error(`Service not supported: ${serviceName}`); } return rpc[serviceName].call(methodName, request); }, streamHandler: (method, request) => { const [serviceName, methodName] = parseMethodName(method); if (!rpc[serviceName]) { throw new Error(`Service not supported: ${serviceName}`); } return rpc[serviceName].callStream(methodName, request); }, }); };