diff --git a/xrpc/client.ts b/xrpc/client.ts index 94c9573..e6fd154 100644 --- a/xrpc/client.ts +++ b/xrpc/client.ts @@ -56,6 +56,38 @@ export class Client { this.headers.clear(); } + xrpc( + input: M, + ): Promise; + xrpc( + input: M, + options: O & XrpcCallCompatibleOptions, + ): Promise; + async xrpc( + input: M, + options: XrpcCallOptions = {} as XrpcCallOptions, + ): Promise { + return await this.performXrpc(input, options); + } + + xrpcSafe( + input: M, + ): Promise; + xrpcSafe( + input: M, + options: O & XrpcCallCompatibleOptions, + ): Promise; + async xrpcSafe( + input: M, + options: XrpcCallOptions = {} as XrpcCallOptions, + ): Promise { + try { + return await this.performXrpc(input, options); + } catch (err) { + return XRPCError.from(err); + } + } + call( input: M, ): Promise; @@ -66,6 +98,13 @@ export class Client { async call( input: M, options: XrpcCallOptions = {} as XrpcCallOptions, + ): Promise { + return await this.xrpc(input, options); + } + + private async performXrpc( + input: M, + options: XrpcCallOptions, ): Promise { const method = getXrpcMethod(input); const params = this.getValidatedParams(method, options); diff --git a/xrpc/mod.ts b/xrpc/mod.ts index b2f1c30..80dfea4 100644 --- a/xrpc/mod.ts +++ b/xrpc/mod.ts @@ -36,7 +36,7 @@ * pingLexicon, * ]) * - * const res1 = await client.call('io.example.ping', { + * const res1 = await client.xrpc('io.example.ping', { * message: 'hello world', * }) * res1.body // => {message: 'hello world'} diff --git a/xrpc/tests/client_test.ts b/xrpc/tests/client_test.ts index f6bfba7..916577e 100644 --- a/xrpc/tests/client_test.ts +++ b/xrpc/tests/client_test.ts @@ -27,6 +27,72 @@ Deno.test("calls query with lex method and params", async () => { assertEquals(result.data, { value: "ok" }); }); +Deno.test("calls query with xrpc", async () => { + const method = l.query( + "io.example.query", + l.params({ limit: l.optional(l.integer()) }), + l.jsonPayload({ value: l.string() }), + ); + + const client = new Client((url, init) => { + assertEquals(url, "/xrpc/io.example.query?limit=9"); + assertEquals(init.method, "get"); + return Promise.resolve(Response.json({ value: "ok" })); + }); + + const result = await client.xrpc(method, { + params: { limit: 9 }, + }); + + assertEquals(result.data, { value: "ok" }); +}); + +Deno.test("narrows xrpcSafe success results on success flag", async () => { + const method = l.query( + "io.example.query", + l.params({ limit: l.optional(l.integer()) }), + l.jsonPayload({ value: l.string() }), + ); + + const client = new Client((url, init) => { + assertEquals(url, "/xrpc/io.example.query?limit=8"); + assertEquals(init.method, "get"); + return Promise.resolve(Response.json({ value: "ok" })); + }); + + const result = await client.xrpcSafe(method, { + params: { limit: 8 }, + }); + + assertEquals(result.success, true); + + if (result.success) { + assertEquals(result.data, { value: "ok" }); + } else { + throw new Error(result.error); + } +}); + +Deno.test("keeps call as a compatibility alias for xrpc", async () => { + const method = l.query( + "io.example.query", + l.params({ limit: l.optional(l.integer()) }), + l.jsonPayload({ value: l.string() }), + ); + + const client = new Client((url, init) => { + assertEquals(url, "/xrpc/io.example.query?limit=4"); + assertEquals(init.method, "get"); + return Promise.resolve(Response.json({ value: "ok" })); + }); + + const result = await client.call(method, { + params: { limit: 4 }, + }); + + assertEquals(result.data, { value: "ok" }); +}); + Deno.test("serializes params using schema transforms", async () => { const method = l.query( "io.example.query", @@ -201,6 +267,37 @@ Deno.test("validates request and response when enabled", async () => { ); }); +Deno.test("returns xrpc errors from xrpcSafe", async () => { + const method = l.query( + "io.example.query", + l.params({ limit: l.optional(l.integer()) }), + l.jsonPayload({ value: l.string() }), + ); + + const client = new Client(() => + Promise.resolve( + Response.json( + { error: "BadRequest", message: "nope" }, + { status: 400 }, + ), + ) + ); + + const result = await client.xrpcSafe(method, { + params: { limit: 1 }, + }); + + assertEquals(result.success, false); + + if (!result.success) { + assertEquals(result.success, false); + assertEquals(result.error, "BadRequest"); + assertEquals(result.message, "nope"); + } else { + throw new Error(JSON.stringify(result.data)); + } +}); + Deno.test("accepts formatted strings in json request bodies", async () => { const method = l.procedure( "io.example.proc", diff --git a/xrpc/types.ts b/xrpc/types.ts index 0743f7c..baf83ea 100644 --- a/xrpc/types.ts +++ b/xrpc/types.ts @@ -480,7 +480,7 @@ export function httpResponseCodeToString(status: number): string { * Response type of a successful XRPC request. */ export class XRPCResponse { - success = true; + readonly success: true = true; constructor( public data: any, @@ -492,7 +492,7 @@ export class XRPCResponse { * Response type of a failed XRPC request with details of the error. */ export class XRPCError extends Error { - success = false; + readonly success: false = false; public status: ResponseType;