diff --git a/xrpc/client.ts b/xrpc/client.ts index e6fd154..07e27be 100644 --- a/xrpc/client.ts +++ b/xrpc/client.ts @@ -25,6 +25,40 @@ import { } from "./util.ts"; import type { DidString } from "@atp/lex"; +export function xrpc( + agentOpts: Agent | AgentOptions, + input: M, +): Promise; +export function xrpc( + agentOpts: Agent | AgentOptions, + input: M, + options: O & XrpcCallCompatibleOptions, +): Promise; +export async function xrpc( + agentOpts: Agent | AgentOptions, + input: M, + options: XrpcCallOptions = {} as XrpcCallOptions, +): Promise { + return await new Client(agentOpts).xrpc(input, options); +} + +export function xrpcSafe( + agentOpts: Agent | AgentOptions, + input: M, +): Promise; +export function xrpcSafe( + agentOpts: Agent | AgentOptions, + input: M, + options: O & XrpcCallCompatibleOptions, +): Promise; +export async function xrpcSafe( + agentOpts: Agent | AgentOptions, + input: M, + options: XrpcCallOptions = {} as XrpcCallOptions, +): Promise { + return await new Client(agentOpts).xrpcSafe(input, options); +} + export class Client { readonly agent: Agent; readonly fetchHandler: FetchHandler; @@ -326,6 +360,10 @@ function getXrpcMethod(input: XrpcMethodLike): XrpcMethod { return input.main; } + if ("Main" in input && isXrpcMethod(input.Main)) { + return input.Main; + } + throw new TypeError("Expected an XRPC method or a namespace with main"); } diff --git a/xrpc/tests/client_test.ts b/xrpc/tests/client_test.ts index 916577e..2952149 100644 --- a/xrpc/tests/client_test.ts +++ b/xrpc/tests/client_test.ts @@ -1,6 +1,6 @@ import { l } from "@atp/lex"; import { assertEquals, assertRejects } from "@std/assert"; -import { Client } from "../mod.ts"; +import { Client, xrpc, xrpcSafe } from "../mod.ts"; import type { XrpcCallCompatibleOptions } from "../types.ts"; import { XRPCError, XRPCInvalidResponseError } from "../types.ts"; @@ -47,6 +47,28 @@ Deno.test("calls query with xrpc", async () => { assertEquals(result.data, { value: "ok" }); }); +Deno.test("calls top-level xrpc", async () => { + const method = l.query( + "io.example.query", + l.params({ limit: l.optional(l.integer()) }), + l.jsonPayload({ value: l.string() }), + ); + + const result = await xrpc( + (url, init) => { + assertEquals(url, "/xrpc/io.example.query?limit=6"); + assertEquals(init.method, "get"); + return Promise.resolve(Response.json({ value: "ok" })); + }, + method, + { + params: { limit: 6 }, + }, + ); + + assertEquals(result.data, { value: "ok" }); +}); + Deno.test("narrows xrpcSafe success results on success flag", async () => { const method = l.query( "io.example.query", @@ -73,6 +95,37 @@ Deno.test("narrows xrpcSafe success results on success flag", async () => { } }); +Deno.test("calls top-level xrpcSafe", async () => { + const method = l.query( + "io.example.query", + l.params({ limit: l.optional(l.integer()) }), + l.jsonPayload({ value: l.string() }), + ); + + const result = await xrpcSafe( + () => + Promise.resolve( + Response.json( + { error: "BadRequest", message: "nope" }, + { status: 400 }, + ), + ), + method, + { + params: { limit: 2 }, + }, + ); + + assertEquals(result.success, false); + + if (!result.success) { + assertEquals(result.error, "BadRequest"); + assertEquals(result.message, "nope"); + } else { + throw new Error(JSON.stringify(result.data)); + } +}); + Deno.test("keeps call as a compatibility alias for xrpc", async () => { const method = l.query( "io.example.query",