import { Data, Effect, Schema as S } from "effect"; import type * as Rpc from "./Rpc"; export class RpcReservedMethodNameError extends Data.TaggedError("RpcReservedMethodNameError")<{ readonly definition: string; readonly method: string; }> {} export class RpcArgumentCountError extends Data.TaggedError("RpcArgumentCountError")<{ readonly definition: string; readonly method: string; readonly expected: number; readonly actual: number; }> {} export class RpcArgumentDecodeError extends Data.TaggedError("RpcArgumentDecodeError")<{ readonly definition: string; readonly method: string; readonly index: number; readonly cause: unknown; }> {} export class RpcArgumentEncodeError extends Data.TaggedError("RpcArgumentEncodeError")<{ readonly definition: string; readonly method: string; readonly index: number; readonly cause: unknown; }> {} export class RpcSuccessDecodeError extends Data.TaggedError("RpcSuccessDecodeError")<{ readonly definition: string; readonly method: string; readonly cause: unknown; }> {} export class RpcSuccessEncodeError extends Data.TaggedError("RpcSuccessEncodeError")<{ readonly definition: string; readonly method: string; readonly cause: unknown; }> {} export const reservedMethodNames = new Set([ "constructor", "fetch", "connect", "alarm", "webSocketMessage", "webSocketClose", "webSocketError", "then", "dup", "dispose", "serialize", "deserialize", ]); export type ReservedMethodName = | "constructor" | "fetch" | "connect" | "alarm" | "webSocketMessage" | "webSocketClose" | "webSocketError" | "then" | "dup" | "dispose" | "serialize" | "deserialize"; export type ServiceFreeSchema = S.Codec; export interface Method< Args extends ReadonlyArray = ReadonlyArray, Success extends ServiceFreeSchema = ServiceFreeSchema, > { readonly args: Args; readonly success: Success; } export namespace Method { export type Any = Method, ServiceFreeSchema>; type ArgsFromSchemas> = Args extends readonly [] ? [] : Args extends readonly [ infer Head extends ServiceFreeSchema, ...infer Tail extends ReadonlyArray, ] ? [S.Schema.Type, ...ArgsFromSchemas] : Array>; type EncodedArgsFromSchemas> = Args extends readonly [] ? [] : Args extends readonly [ infer Head extends ServiceFreeSchema, ...infer Tail extends ReadonlyArray, ] ? [S.Codec.Encoded, ...EncodedArgsFromSchemas] : Array>; export type Args = ArgsFromSchemas; export type EncodedArgs = EncodedArgsFromSchemas; export type Success = S.Schema.Type; export type EncodedSuccess = S.Codec.Encoded; } export type Methods = Record; export type NoReservedMethods< MethodsShape extends Methods, Reserved extends string = ReservedMethodName, > = Extract extends never ? MethodsShape : never; export interface Definition { readonly id: Id; readonly methods: MethodsShape; } export namespace Definition { export type Any = Definition; export type ServerApi = { readonly [Key in keyof Self["methods"]]: ( ...args: Method.Args ) => Promise>; }; export type Api = Rpc.Provider< ServerApi, Reserved >; export type MethodNames = Extract; } export class ReservedMethodNameError extends Error { readonly method: string; readonly target: string; constructor(target: string, method: string) { super(`${target} RPC method "${method}" is reserved by Cloudflare Workers RPC`); this.name = "ReservedMethodNameError"; this.target = target; this.method = method; } } export const assertNoReservedMethods = ( target: string, methods: Record, reserved: ReadonlySet, ) => { for (const method of Object.keys(methods)) { if (reserved.has(method)) { throw new ReservedMethodNameError(target, method); } } }; export function method(definition: { readonly success: Success; }): Method; export function method< const Args extends ReadonlyArray, Success extends ServiceFreeSchema, >(definition: { readonly args: Args; readonly success: Success }): Method; export function method(definition: { readonly args?: ReadonlyArray; readonly success: ServiceFreeSchema; }) { return { args: definition.args ?? [], success: definition.success, }; } export const assertNoReservedMethodNames = ( definition: Definition.Any, ): Effect.Effect => Effect.forEach(Object.keys(definition.methods), (method) => reservedMethodNames.has(method) ? Effect.fail(new RpcReservedMethodNameError({ definition: definition.id, method })) : Effect.void, ).pipe(Effect.asVoid); export const decodeArgs = < const Self extends Definition.Any, MethodName extends Definition.MethodNames, >( definition: Self, methodName: MethodName, args: ReadonlyArray, ): Effect.Effect< Method.Args, RpcArgumentCountError | RpcArgumentDecodeError > => Effect.gen(function* () { const methodDefinition = definition.methods[methodName]; if (args.length !== methodDefinition.args.length) { return yield* Effect.fail( new RpcArgumentCountError({ definition: definition.id, method: methodName, expected: methodDefinition.args.length, actual: args.length, }), ); } const decoded: Array = []; for (let index = 0; index < methodDefinition.args.length; index++) { const schema = methodDefinition.args[index]; decoded.push( yield* (S.decodeUnknownEffect(schema)(args[index]) as Effect.Effect).pipe( Effect.mapError( (cause) => new RpcArgumentDecodeError({ definition: definition.id, method: methodName, index, cause, }), ), ), ); } return decoded as Method.Args; }); export const encodeArgs = < const Self extends Definition.Any, MethodName extends Definition.MethodNames, >( definition: Self, methodName: MethodName, args: Method.Args, ): Effect.Effect< Method.EncodedArgs, RpcArgumentCountError | RpcArgumentEncodeError > => Effect.gen(function* () { const methodDefinition = definition.methods[methodName]; if (args.length !== methodDefinition.args.length) { return yield* Effect.fail( new RpcArgumentCountError({ definition: definition.id, method: methodName, expected: methodDefinition.args.length, actual: args.length, }), ); } const encoded: Array = []; for (let index = 0; index < methodDefinition.args.length; index++) { const schema = methodDefinition.args[index]; encoded.push( yield* (S.encodeEffect(schema)(args[index]) as Effect.Effect).pipe( Effect.mapError( (cause) => new RpcArgumentEncodeError({ definition: definition.id, method: methodName, index, cause, }), ), ), ); } return encoded as Method.EncodedArgs; }); export const encodeSuccess = < const Self extends Definition.Any, MethodName extends Definition.MethodNames, >( definition: Self, methodName: MethodName, value: Method.Success, ): Effect.Effect, RpcSuccessEncodeError> => { const methodDefinition = definition.methods[methodName]; return ( S.encodeEffect(methodDefinition.success)(value) as Effect.Effect< Method.EncodedSuccess, unknown > ).pipe( Effect.mapError( (cause) => new RpcSuccessEncodeError({ definition: definition.id, method: methodName, cause, }), ), ); }; export const decodeSuccess = < const Self extends Definition.Any, MethodName extends Definition.MethodNames, >( definition: Self, methodName: MethodName, value: unknown, ): Effect.Effect, RpcSuccessDecodeError> => { const methodDefinition = definition.methods[methodName]; return ( S.decodeUnknownEffect(methodDefinition.success)(value) as Effect.Effect< Method.Success, unknown > ).pipe( Effect.mapError( (cause) => new RpcSuccessDecodeError({ definition: definition.id, method: methodName, cause, }), ), ); }; export const make = ( id: Id, methods: MethodsShape, ): Definition => { const definition = { id, methods } as Definition; const reserved = Object.keys(definition.methods).find((methodName) => reservedMethodNames.has(methodName), ); if (reserved !== undefined) { throw new RpcReservedMethodNameError({ definition: id, method: reserved }); } return definition; };