Suite of AT Protocol TypeScript libraries built on web standards
20
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: client name and call type inference

+642 -69
+13 -13
lex-gen/codegen/client.ts
··· 70 70 gen(project, "/index.ts", (file) => { 71 71 const importExtension = options?.importSuffix ?? 72 72 (options?.useJsExtension ? ".js" : ".ts"); 73 - //= import { XrpcClient, type FetchHandler, type FetchHandlerOptions } from '@atp/xrpc' 73 + //= import { Client, type FetchHandler, type FetchHandlerOptions } from '@atp/xrpc' 74 74 file.addImportDeclaration({ 75 75 moduleSpecifier: "@atp/xrpc", 76 76 namedImports: [ 77 - { name: "XrpcClient" }, 77 + { name: "Client" }, 78 78 { name: "FetchHandler", isTypeOnly: true }, 79 79 { name: "FetchHandlerOptions", isTypeOnly: true }, 80 80 ], ··· 187 187 const clientCls = file.addClass({ 188 188 name: "AtpBaseClient", 189 189 isExported: true, 190 - extends: "XrpcClient", 190 + extends: "Client", 191 191 }); 192 192 193 193 for (const ns of nsidTree) { ··· 215 215 }); 216 216 217 217 //= /** @deprecated use `this` instead */ 218 - //= get xrpc(): XrpcClient { 218 + //= get xrpc(): Client { 219 219 //= return this 220 220 //= } 221 221 clientCls 222 222 .addGetAccessor({ 223 223 name: "xrpc", 224 - returnType: "XrpcClient", 224 + returnType: "Client", 225 225 statements: ["return this"], 226 226 }) 227 227 .addJsDoc("@deprecated use `this` instead"); ··· 238 238 name: ns.className, 239 239 isExported: true, 240 240 }); 241 - //= _client: XrpcClient 241 + //= _client: Client 242 242 cls.addProperty({ 243 243 name: "_client", 244 - type: "XrpcClient", 244 + type: "Client", 245 245 }); 246 246 247 247 for (const userType of ns.userTypes) { ··· 267 267 genNamespaceCls(file, child); 268 268 } 269 269 270 - //= constructor(public client: XrpcClient) { 270 + //= constructor(public client: Client) { 271 271 //= this._client = client 272 272 //= {child namespace prop declarations} 273 273 //= {record prop declarations} ··· 276 276 parameters: [ 277 277 { 278 278 name: "client", 279 - type: "XrpcClient", 279 + type: "Client", 280 280 }, 281 281 ], 282 282 statements: [ ··· 353 353 name: `${toTitleCase(nsid)}Record`, 354 354 isExported: true, 355 355 }); 356 - //= _client: XrpcClient 356 + //= _client: Client 357 357 cls.addProperty({ 358 358 name: "_client", 359 - type: "XrpcClient", 359 + type: "Client", 360 360 }); 361 361 362 - //= constructor(client: XrpcClient) { 362 + //= constructor(client: Client) { 363 363 //= this._client = client 364 364 //= } 365 365 const cons = cls.addConstructor(); 366 366 cons.addParameter({ 367 367 name: "client", 368 - type: "XrpcClient", 368 + type: "Client", 369 369 }); 370 370 cons.setBodyText(`this._client = client`); 371 371
+6 -4
xrpc-server/tests/_xrpc-client.ts
··· 3 3 import { 4 4 type Agent, 5 5 type AgentOptions, 6 + Client as ModernClient, 6 7 ResponseType, 7 8 type XrpcCallOptions, 8 - XrpcClient as ModernXrpcClient, 9 9 XRPCError, 10 10 XRPCInvalidResponseError, 11 11 type XRPCResponse, ··· 25 25 26 26 export { ResponseType, XRPCError, XRPCInvalidResponseError }; 27 27 28 - export class XrpcClient { 29 - readonly #client: ModernXrpcClient; 28 + export class Client { 29 + readonly #client: ModernClient; 30 30 readonly #methods: Map<string, Method>; 31 31 32 32 constructor(agentOpts: Agent | AgentOptions, lexicons: LexiconDoc[] = []) { 33 - this.#client = new ModernXrpcClient(agentOpts); 33 + this.#client = new ModernClient(agentOpts); 34 34 this.#methods = buildMethodMap(lexicons); 35 35 } 36 36 ··· 93 93 ); 94 94 } 95 95 } 96 + 97 + export { Client as XrpcClient }; 96 98 97 99 function buildMethodMap(lexicons: LexiconDoc[]): Map<string, Method> { 98 100 const methods = new Map<string, Method>();
+3 -3
xrpc-server/tests/auth_test.ts
··· 1 1 import { MINUTE } from "@atp/common"; 2 2 import { Secp256k1Keypair } from "@atp/crypto"; 3 3 import type { LexiconDoc } from "@atp/lexicon"; 4 - import { XrpcClient, XRPCError } from "./_xrpc-client.ts"; 4 + import { Client, XRPCError } from "./_xrpc-client.ts"; 5 5 import * as xrpcServer from "../mod.ts"; 6 6 7 7 import { ··· 50 50 51 51 let server: ReturnType<typeof xrpcServer.createServer>; 52 52 let s: Deno.HttpServer; 53 - let client: XrpcClient; 53 + let client: Client; 54 54 55 55 type AuthTestResponse = { 56 56 username: string | undefined; ··· 82 82 83 83 s = await createServer(server); 84 84 const port = (s as Deno.HttpServer & { port: number }).port; 85 - client = new XrpcClient(`http://localhost:${port}`, LEXICONS); 85 + client = new Client(`http://localhost:${port}`, LEXICONS); 86 86 }); 87 87 88 88 Deno.test.afterAll(async () => {
+2 -2
xrpc-server/tests/bodies_test.ts
··· 1 1 import { cidForCbor } from "@atp/common"; 2 2 import { randomBytes } from "@atp/crypto"; 3 3 import type { LexiconDoc } from "@atp/lexicon"; 4 - import { ResponseType, XrpcClient, XRPCError } from "./_xrpc-client.ts"; 4 + import { Client, ResponseType, XRPCError } from "./_xrpc-client.ts"; 5 5 import * as xrpcServer from "../mod.ts"; 6 6 import { closeServer, createServer } from "./_util.ts"; 7 7 import { ··· 186 186 const s = await createServer(server); 187 187 const port = (s as Deno.HttpServer & { port: number }).port; 188 188 const url = `http://localhost:${port}`; 189 - const client = new XrpcClient(url, LEXICONS); 189 + const client = new Client(url, LEXICONS); 190 190 191 191 // Tests 192 192 await t.step("validates input and output bodies", async () => {
+7 -11
xrpc-server/tests/errors_test.ts
··· 1 1 import type { LexiconDoc } from "@atp/lexicon"; 2 - import { 3 - XrpcClient, 4 - XRPCError, 5 - XRPCInvalidResponseError, 6 - } from "./_xrpc-client.ts"; 2 + import { Client, XRPCError, XRPCInvalidResponseError } from "./_xrpc-client.ts"; 7 3 import * as xrpcServer from "../mod.ts"; 8 4 import { closeServer, createServer } from "./_util.ts"; 9 5 import { ··· 141 137 142 138 let upstreamServer: ReturnType<typeof xrpcServer.createServer>; 143 139 let upstreamS: Deno.HttpServer; 144 - let upstreamClient: XrpcClient; 140 + let upstreamClient: Client; 145 141 let server: ReturnType<typeof xrpcServer.createServer>; 146 142 let s: Deno.HttpServer; 147 - let client: XrpcClient; 148 - let badClient: XrpcClient; 143 + let client: Client; 144 + let badClient: Client; 149 145 150 146 Deno.test.beforeAll(async () => { 151 147 // Setup upstream server ··· 157 153 }); 158 154 upstreamS = await createServer(upstreamServer); 159 155 const upstreamPort = (upstreamS as Deno.HttpServer & { port: number }).port; 160 - upstreamClient = new XrpcClient( 156 + upstreamClient = new Client( 161 157 `http://localhost:${upstreamPort}`, 162 158 UPSTREAM_LEXICONS, 163 159 ); ··· 199 195 return undefined; 200 196 }); 201 197 202 - client = new XrpcClient(`http://localhost:${port}`, LEXICONS); 203 - badClient = new XrpcClient( 198 + client = new Client(`http://localhost:${port}`, LEXICONS); 199 + badClient = new Client( 204 200 `http://localhost:${port}`, 205 201 MISMATCHED_LEXICONS, 206 202 );
+3 -3
xrpc-server/tests/ipld_test.ts
··· 1 1 import { CID } from "multiformats/cid"; 2 2 import type { LexiconDoc } from "@atp/lexicon"; 3 - import { XrpcClient } from "./_xrpc-client.ts"; 3 + import { Client } from "./_xrpc-client.ts"; 4 4 import * as xrpcServer from "../mod.ts"; 5 5 import { closeServer, createServer } from "./_util.ts"; 6 6 import { assertEquals, assertExists } from "@std/assert"; ··· 47 47 48 48 let server: ReturnType<typeof xrpcServer.createServer>; 49 49 let s: Deno.HttpServer; 50 - let client: XrpcClient; 50 + let client: Client; 51 51 52 52 Deno.test.beforeAll(async () => { 53 53 server = xrpcServer.createServer(LEXICONS); ··· 75 75 ); 76 76 77 77 const port = (s as Deno.HttpServer & { port: number }).port; 78 - client = new XrpcClient(`http://localhost:${port}`, LEXICONS); 78 + client = new Client(`http://localhost:${port}`, LEXICONS); 79 79 }); 80 80 81 81 Deno.test.afterAll(async () => {
+3 -3
xrpc-server/tests/parameters_test.ts
··· 1 1 import type { LexiconDoc } from "@atp/lexicon"; 2 - import { XrpcClient } from "./_xrpc-client.ts"; 2 + import { Client } from "./_xrpc-client.ts"; 3 3 import * as xrpcServer from "../mod.ts"; 4 4 import { closeServer, createServer } from "./_util.ts"; 5 5 import { assertEquals, assertRejects } from "@std/assert"; ··· 32 32 33 33 let server: ReturnType<typeof xrpcServer.createServer>; 34 34 let s: Deno.HttpServer; 35 - let client: XrpcClient; 35 + let client: Client; 36 36 37 37 Deno.test.beforeAll(async () => { 38 38 server = xrpcServer.createServer(LEXICONS); ··· 46 46 47 47 s = await createServer(server); 48 48 const port = (s as Deno.HttpServer & { port: number }).port; 49 - client = new XrpcClient(`http://localhost:${port}`, LEXICONS); 49 + client = new Client(`http://localhost:${port}`, LEXICONS); 50 50 }); 51 51 52 52 Deno.test.afterAll(async () => {
+3 -3
xrpc-server/tests/procedures_test.ts
··· 1 1 import type { LexiconDoc } from "@atp/lexicon"; 2 - import { XrpcClient } from "./_xrpc-client.ts"; 2 + import { Client } from "./_xrpc-client.ts"; 3 3 import * as xrpcServer from "../mod.ts"; 4 4 import { closeServer, createServer } from "./_util.ts"; 5 5 import { assertEquals } from "@std/assert"; ··· 82 82 83 83 let server: ReturnType<typeof xrpcServer.createServer>; 84 84 let s: Deno.HttpServer; 85 - let client: XrpcClient; 85 + let client: Client; 86 86 87 87 Deno.test.beforeAll(async () => { 88 88 server = xrpcServer.createServer(LEXICONS); ··· 120 120 121 121 s = await createServer(server); 122 122 const port = (s as Deno.HttpServer & { port: number }).port; 123 - client = new XrpcClient(`http://localhost:${port}`, LEXICONS); 123 + client = new Client(`http://localhost:${port}`, LEXICONS); 124 124 }); 125 125 126 126 Deno.test.afterAll(async () => {
+2 -2
xrpc-server/tests/queries_test.ts
··· 1 1 import type { LexiconDoc } from "@atp/lexicon"; 2 - import { XrpcClient } from "./_xrpc-client.ts"; 2 + import { Client } from "./_xrpc-client.ts"; 3 3 import * as xrpcServer from "../mod.ts"; 4 4 import { closeServer, createServer } from "./_util.ts"; 5 5 import { assertEquals, assertExists } from "@std/assert"; ··· 96 96 97 97 const s = await createServer(server); 98 98 const port = (s as Deno.HttpServer & { port: number }).port; 99 - const client = new XrpcClient(`http://localhost:${port}`, LEXICONS); 99 + const client = new Client(`http://localhost:${port}`, LEXICONS); 100 100 101 101 return { server: s, client }; 102 102 }
+2 -2
xrpc-server/tests/rate-limiter_test.ts
··· 1 1 import { MINUTE } from "@atp/common"; 2 2 import type { LexiconDoc } from "@atp/lexicon"; 3 - import { XrpcClient } from "./_xrpc-client.ts"; 3 + import { Client } from "./_xrpc-client.ts"; 4 4 import * as xrpcServer from "../mod.ts"; 5 5 import { closeServer, createServer } from "./_util.ts"; 6 6 import { assertRejects } from "@std/assert"; ··· 239 239 240 240 const s = await createServer(server); 241 241 const port = (s as Deno.HttpServer & { port: number }).port; 242 - const client = new XrpcClient(`http://localhost:${port}`, LEXICONS); 242 + const client = new Client(`http://localhost:${port}`, LEXICONS); 243 243 244 244 return { server: s, client }; 245 245 }
+2 -2
xrpc-server/tests/responses_test.ts
··· 1 1 import { byteIterableToStream } from "@atp/common"; 2 2 import type { LexiconDoc } from "@atp/lexicon"; 3 - import { XrpcClient } from "./_xrpc-client.ts"; 3 + import { Client } from "./_xrpc-client.ts"; 4 4 import * as xrpcServer from "../mod.ts"; 5 5 import { closeServer, createServer } from "./_util.ts"; 6 6 import { assertEquals, assertInstanceOf } from "@std/assert"; ··· 48 48 49 49 const s = await createServer(server); 50 50 const port = (s as Deno.HttpServer & { port: number }).port; 51 - const client = new XrpcClient(`http://localhost:${port}`, LEXICONS); 51 + const client = new Client(`http://localhost:${port}`, LEXICONS); 52 52 53 53 return { server: s, client }; 54 54 }
+33 -6
xrpc/client.ts
··· 1 - import { Procedure, type Query } from "@atp/lex"; 1 + import { Procedure, Query } from "@atp/lex"; 2 2 import { 3 3 type Agent, 4 4 type AgentOptions, ··· 9 9 type Gettable, 10 10 httpResponseCodeToEnum, 11 11 ResponseType, 12 + type XrpcCallCompatibleOptions, 12 13 type XrpcCallOptions, 13 14 XRPCError, 14 15 XRPCInvalidResponseError, 16 + type XrpcMethod, 17 + type XrpcMethodLike, 15 18 XRPCResponse, 16 19 } from "./types.ts"; 17 20 import { ··· 22 25 } from "./util.ts"; 23 26 import type { DidString } from "@atp/lex"; 24 27 25 - type XrpcMethod = Query | Procedure; 26 - 27 - export class XrpcClient { 28 + export class Client { 28 29 readonly agent: Agent; 29 30 readonly fetchHandler: FetchHandler; 30 31 readonly headers: Map<string, Gettable<null | string>> = new Map< ··· 55 56 this.headers.clear(); 56 57 } 57 58 58 - async call<const M extends XrpcMethod>( 59 - method: M, 59 + call<const M extends XrpcMethodLike>( 60 + input: M, 61 + ): Promise<XRPCResponse>; 62 + call<const M extends XrpcMethodLike, const O>( 63 + input: M, 64 + options: O & XrpcCallCompatibleOptions<M, O>, 65 + ): Promise<XRPCResponse>; 66 + async call<const M extends XrpcMethodLike>( 67 + input: M, 60 68 options: XrpcCallOptions<M> = {} as XrpcCallOptions<M>, 61 69 ): Promise<XRPCResponse> { 70 + const method = getXrpcMethod(input); 62 71 const params = this.getValidatedParams(method, options); 63 72 const reqUrl = this.constructMethodCallUrl(method, params); 64 73 const reqHeaders = this.constructMethodCallHeaders(method, options); ··· 265 274 ); 266 275 } 267 276 } 277 + } 278 + 279 + export { Client as XrpcClient }; 280 + 281 + function getXrpcMethod(input: XrpcMethodLike): XrpcMethod { 282 + if (isXrpcMethod(input)) { 283 + return input; 284 + } 285 + 286 + if ("main" in input && isXrpcMethod(input.main)) { 287 + return input.main; 288 + } 289 + 290 + throw new TypeError("Expected an XRPC method or a namespace with main"); 291 + } 292 + 293 + function isXrpcMethod(value: unknown): value is XrpcMethod { 294 + return value instanceof Query || value instanceof Procedure; 268 295 } 269 296 270 297 function resolveProcedurePayload(
+3 -3
xrpc/mod.ts
··· 7 7 * @example Fetching an XRPC endpoint 8 8 * ```typescript 9 9 * import { LexiconDoc } from '@atp/lexicon' 10 - * import { XrpcClient } from '@atp/xrpc' 10 + * import { Client } from '@atp/xrpc' 11 11 * 12 12 * const pingLexicon = { 13 13 * lexicon: 1, ··· 32 32 * }, 33 33 * } satisfies LexiconDoc 34 34 * 35 - * const xrpc = new XrpcClient('https://ping.example.com', [ 35 + * const client = new Client('https://ping.example.com', [ 36 36 * pingLexicon, 37 37 * ]) 38 38 * 39 - * const res1 = await xrpc.call('io.example.ping', { 39 + * const res1 = await client.call('io.example.ping', { 40 40 * message: 'hello world', 41 41 * }) 42 42 * res1.body // => {message: 'hello world'}
+205 -9
xrpc/tests/client_test.ts
··· 1 1 import { l } from "@atp/lex"; 2 2 import { assertEquals, assertRejects } from "@std/assert"; 3 - import { XrpcClient } from "../mod.ts"; 3 + import { Client } from "../mod.ts"; 4 + import type { XrpcCallCompatibleOptions } from "../types.ts"; 4 5 import { XRPCError, XRPCInvalidResponseError } from "../types.ts"; 6 + 7 + type Expect<T extends true> = T; 8 + type IsNever<T> = [T] extends [never] ? true : false; 5 9 6 10 Deno.test("calls query with lex method and params", async () => { 7 11 const method = l.query( ··· 10 14 l.jsonPayload({ value: l.string() }), 11 15 ); 12 16 13 - const client = new XrpcClient((url, init) => { 17 + const client = new Client((url, init) => { 14 18 assertEquals(url, "/xrpc/io.example.query?limit=7"); 15 19 assertEquals(init.method, "get"); 16 20 return Promise.resolve(Response.json({ value: "ok" })); ··· 32 36 l.jsonPayload({ value: l.string() }), 33 37 ); 34 38 35 - const client = new XrpcClient((url) => { 39 + const client = new Client((url) => { 36 40 assertEquals( 37 41 url, 38 42 "/xrpc/io.example.query?since=2024-01-02T03%3A04%3A05.000Z", ··· 49 53 assertEquals(result.data, { value: "ok" }); 50 54 }); 51 55 56 + Deno.test("accepts plain strings for formatted query params", async () => { 57 + const method = l.query( 58 + "io.example.getRecord", 59 + l.params({ 60 + repo: l.string({ format: "at-identifier" }), 61 + collection: l.string({ format: "nsid" }), 62 + rkey: l.string({ format: "record-key" }), 63 + uri: l.optional(l.string({ format: "uri" })), 64 + }), 65 + l.payload(), 66 + ); 67 + 68 + const client = new Client((url, init) => { 69 + assertEquals( 70 + url, 71 + "/xrpc/io.example.getRecord?repo=did%3Aplc%3A6hbqm2oftpotwuw7gvvrui3i&collection=app.bsky.feed.post&rkey=3mjlhmszzo22h&uri=https%3A%2F%2Fexample.com%2Fpost%2F1", 72 + ); 73 + assertEquals(init.method, "get"); 74 + return Promise.resolve(new Response(null)); 75 + }); 76 + 77 + await client.call(method, { 78 + params: { 79 + repo: "did:plc:6hbqm2oftpotwuw7gvvrui3i", 80 + collection: "app.bsky.feed.post", 81 + rkey: "3mjlhmszzo22h", 82 + uri: "https://example.com/post/1", 83 + }, 84 + }); 85 + }); 86 + 87 + Deno.test("only matching string literals satisfy formatted params", () => { 88 + const method = l.query( 89 + "io.example.getRecord", 90 + l.params({ 91 + repo: l.string({ format: "at-identifier" }), 92 + collection: l.string({ format: "nsid" }), 93 + rkey: l.string({ format: "record-key" }), 94 + }), 95 + l.payload(), 96 + ); 97 + 98 + type Valid = XrpcCallCompatibleOptions<typeof method, { 99 + params: { 100 + repo: "did:plc:6hbqm2oftpotwuw7gvvrui3i"; 101 + collection: "app.bsky.feed.post"; 102 + rkey: "3mjlhmszzo22h"; 103 + }; 104 + }>; 105 + type InvalidRepo = XrpcCallCompatibleOptions<typeof method, { 106 + params: { 107 + repo: "not-a-valid-at-identifier"; 108 + collection: "app.bsky.feed.post"; 109 + rkey: "3mjlhmszzo22h"; 110 + }; 111 + }>; 112 + type GenericRepo = XrpcCallCompatibleOptions<typeof method, { 113 + params: { 114 + repo: string; 115 + collection: "app.bsky.feed.post"; 116 + rkey: "3mjlhmszzo22h"; 117 + }; 118 + }>; 119 + 120 + type ValidParams = NonNullable<Valid["params"]>; 121 + type InvalidRepoParams = NonNullable<InvalidRepo["params"]>; 122 + type GenericRepoParams = NonNullable<GenericRepo["params"]>; 123 + 124 + type _validRepo = Expect< 125 + IsNever<ValidParams["repo"]> extends false ? true : false 126 + >; 127 + type _invalidRepo = Expect<IsNever<InvalidRepoParams["repo"]>>; 128 + type _genericRepo = Expect<IsNever<GenericRepoParams["repo"]>>; 129 + }); 130 + 131 + Deno.test("calls query with namespace main export", async () => { 132 + const main = l.query( 133 + "io.example.query", 134 + l.params({ limit: l.optional(l.integer()) }), 135 + l.jsonPayload({ value: l.string() }), 136 + ); 137 + const namespace = { main } as const; 138 + 139 + const client = new Client((url, init) => { 140 + assertEquals(url, "/xrpc/io.example.query?limit=3"); 141 + assertEquals(init.method, "get"); 142 + return Promise.resolve(Response.json({ value: "ok" })); 143 + }); 144 + 145 + const result = await client.call(namespace, { 146 + params: { limit: 3 }, 147 + }); 148 + 149 + assertEquals(result.data, { value: "ok" }); 150 + }); 151 + 152 + Deno.test("calls query with namespace Main export", async () => { 153 + const Main = l.query( 154 + "io.example.query", 155 + l.params({ limit: l.optional(l.integer()) }), 156 + l.jsonPayload({ value: l.string() }), 157 + ); 158 + const namespace = { Main } as const; 159 + 160 + const client = new Client((url, init) => { 161 + assertEquals(url, "/xrpc/io.example.query?limit=5"); 162 + assertEquals(init.method, "get"); 163 + return Promise.resolve(Response.json({ value: "ok" })); 164 + }); 165 + 166 + const result = await client.call(namespace, { 167 + params: { limit: 5 }, 168 + }); 169 + 170 + assertEquals(result.data, { value: "ok" }); 171 + }); 172 + 52 173 Deno.test("validates request and response when enabled", async () => { 53 174 const method = l.procedure( 54 175 "io.example.proc", ··· 57 178 l.jsonPayload({ id: l.string() }), 58 179 ); 59 180 60 - const client = new XrpcClient(() => 61 - Promise.resolve(Response.json({ id: 123 })) 62 - ); 181 + const client = new Client(() => Promise.resolve(Response.json({ id: 123 }))); 63 182 64 183 await assertRejects( 65 184 async () => { ··· 82 201 ); 83 202 }); 84 203 204 + Deno.test("accepts formatted strings in json request bodies", async () => { 205 + const method = l.procedure( 206 + "io.example.proc", 207 + l.params(), 208 + l.jsonPayload({ 209 + repo: l.string({ format: "at-identifier" }), 210 + rkey: l.string({ format: "record-key" }), 211 + createdAt: l.string({ format: "datetime" }), 212 + }), 213 + l.payload(), 214 + ); 215 + 216 + const client = new Client((_url, init) => { 217 + assertEquals(init.method, "post"); 218 + assertEquals( 219 + new Headers(init.headers).get("content-type"), 220 + "application/json", 221 + ); 222 + return Promise.resolve(new Response(null)); 223 + }); 224 + 225 + await client.call(method, { 226 + body: { 227 + repo: "did:plc:6hbqm2oftpotwuw7gvvrui3i", 228 + rkey: "3mjlhmszzo22h", 229 + createdAt: "2024-01-02T03:04:05.000Z", 230 + }, 231 + }); 232 + }); 233 + 234 + Deno.test("only matching string literals satisfy formatted json bodies", () => { 235 + const method = l.procedure( 236 + "io.example.proc", 237 + l.params(), 238 + l.jsonPayload({ 239 + createdAt: l.string({ format: "datetime" }), 240 + uri: l.string({ format: "at-uri" }), 241 + cid: l.string({ format: "cid" }), 242 + }), 243 + l.payload(), 244 + ); 245 + 246 + type Valid = XrpcCallCompatibleOptions<typeof method, { 247 + body: { 248 + createdAt: "2024-01-02T03:04:05.000Z"; 249 + uri: 250 + "at://did:plc:6hbqm2oftpotwuw7gvvrui3i/app.bsky.feed.post/3mjlhmszzo22h"; 251 + cid: "bafyreihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku"; 252 + }; 253 + }>; 254 + type InvalidDatetime = XrpcCallCompatibleOptions<typeof method, { 255 + body: { 256 + createdAt: "2024-01-02"; 257 + uri: 258 + "at://did:plc:6hbqm2oftpotwuw7gvvrui3i/app.bsky.feed.post/3mjlhmszzo22h"; 259 + cid: "bafyreihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku"; 260 + }; 261 + }>; 262 + type InvalidUri = XrpcCallCompatibleOptions<typeof method, { 263 + body: { 264 + createdAt: "2024-01-02T03:04:05.000Z"; 265 + uri: "https://example.com/post/1"; 266 + cid: "bafyreihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku"; 267 + }; 268 + }>; 269 + 270 + type ValidBody = NonNullable<Valid["body"]>; 271 + type InvalidDatetimeBody = NonNullable<InvalidDatetime["body"]>; 272 + type InvalidUriBody = NonNullable<InvalidUri["body"]>; 273 + 274 + type _validBody = Expect< 275 + IsNever<ValidBody["createdAt"]> extends false ? true : false 276 + >; 277 + type _invalidDatetime = Expect<IsNever<InvalidDatetimeBody["createdAt"]>>; 278 + type _invalidUri = Expect<IsNever<InvalidUriBody["uri"]>>; 279 + }); 280 + 85 281 Deno.test("uses method encoding defaults for wildcard payloads", async () => { 86 282 const method = l.procedure( 87 283 "io.example.upload", ··· 90 286 l.jsonPayload({ ok: l.boolean() }), 91 287 ); 92 288 93 - const client = new XrpcClient((_url, init) => { 289 + const client = new Client((_url, init) => { 94 290 const headers = new Headers(init.headers); 95 291 assertEquals(headers.get("content-type"), "image/png"); 96 292 assertEquals(init.method, "post"); ··· 113 309 l.payload(), 114 310 ); 115 311 116 - const client = new XrpcClient((_url, init) => { 312 + const client = new Client((_url, init) => { 117 313 const headers = new Headers(init.headers); 118 314 assertEquals(headers.get("content-type"), "text/csv"); 119 315 return Promise.resolve(new Response(null)); ··· 133 329 ); 134 330 135 331 const seen: string[] = []; 136 - const client = new XrpcClient((_url, init) => { 332 + const client = new Client((_url, init) => { 137 333 seen.push(new Headers(init.headers).get("content-type") ?? ""); 138 334 return Promise.resolve(new Response(null)); 139 335 });
+355 -3
xrpc/types.ts
··· 1 1 import { z } from "zod"; 2 2 import type { 3 + AtIdentifierString, 4 + AtUriString, 5 + CidString, 6 + DatetimeString, 7 + DidString, 8 + HandleString, 3 9 InferMethodInputBody, 4 10 InferMethodParams, 5 11 Procedure, 6 12 Query, 13 + RecordKeyString, 14 + TidString, 7 15 } from "@atp/lex"; 8 16 9 17 export type QueryParams = Record<string, unknown>; 10 18 export type HeadersMap = Record<string, string | undefined>; 19 + export type XrpcMethod = Query | Procedure; 20 + export type XrpcMethodNamespace<M extends XrpcMethod = XrpcMethod> = 21 + | { readonly main: M } 22 + | { readonly Main: M }; 23 + export type XrpcMethodLike<M extends XrpcMethod = XrpcMethod> = 24 + | M 25 + | XrpcMethodNamespace<M>; 26 + 27 + type InferXrpcMethod<M extends XrpcMethodLike> = M extends XrpcMethod ? M 28 + : M extends { readonly main: infer Inner } ? Inner extends XrpcMethod ? Inner 29 + : never 30 + : M extends { readonly Main: infer Inner } ? Inner extends XrpcMethod ? Inner 31 + : never 32 + : never; 33 + 34 + type Digit = 35 + | "0" 36 + | "1" 37 + | "2" 38 + | "3" 39 + | "4" 40 + | "5" 41 + | "6" 42 + | "7" 43 + | "8" 44 + | "9"; 45 + type LowerAlpha = 46 + | "a" 47 + | "b" 48 + | "c" 49 + | "d" 50 + | "e" 51 + | "f" 52 + | "g" 53 + | "h" 54 + | "i" 55 + | "j" 56 + | "k" 57 + | "l" 58 + | "m" 59 + | "n" 60 + | "o" 61 + | "p" 62 + | "q" 63 + | "r" 64 + | "s" 65 + | "t" 66 + | "u" 67 + | "v" 68 + | "w" 69 + | "x" 70 + | "y" 71 + | "z"; 72 + type UpperAlpha = Uppercase<LowerAlpha>; 73 + type Alpha = LowerAlpha | UpperAlpha; 74 + type DidChar = Alpha | Digit | "." | "_" | ":" | "%" | "-"; 75 + type DidEndChar = Alpha | Digit | "." | "_" | "-"; 76 + type HandleChar = Alpha | Digit | "-"; 77 + type RecordKeyChar = Alpha | Digit | "_" | "~" | "." | ":" | "-"; 78 + type TidInitialChar = 79 + | "2" 80 + | "3" 81 + | "4" 82 + | "5" 83 + | "6" 84 + | "7" 85 + | "a" 86 + | "b" 87 + | "c" 88 + | "d" 89 + | "e" 90 + | "f" 91 + | "g" 92 + | "h" 93 + | "i" 94 + | "j"; 95 + type TidChar = 96 + | "2" 97 + | "3" 98 + | "4" 99 + | "5" 100 + | "6" 101 + | "7" 102 + | LowerAlpha; 103 + type Base32Char = LowerAlpha | "2" | "3" | "4" | "5" | "6" | "7"; 104 + type Base58Char = 105 + | "1" 106 + | "2" 107 + | "3" 108 + | "4" 109 + | "5" 110 + | "6" 111 + | "7" 112 + | "8" 113 + | "9" 114 + | "A" 115 + | "B" 116 + | "C" 117 + | "D" 118 + | "E" 119 + | "F" 120 + | "G" 121 + | "H" 122 + | "J" 123 + | "K" 124 + | "L" 125 + | "M" 126 + | "N" 127 + | "P" 128 + | "Q" 129 + | "R" 130 + | "S" 131 + | "T" 132 + | "U" 133 + | "V" 134 + | "W" 135 + | "X" 136 + | "Y" 137 + | "Z" 138 + | "a" 139 + | "b" 140 + | "c" 141 + | "d" 142 + | "e" 143 + | "f" 144 + | "g" 145 + | "h" 146 + | "i" 147 + | "j" 148 + | "k" 149 + | "m" 150 + | "n" 151 + | "o" 152 + | "p" 153 + | "q" 154 + | "r" 155 + | "s" 156 + | "t" 157 + | "u" 158 + | "v" 159 + | "w" 160 + | "x" 161 + | "y" 162 + | "z"; 163 + 164 + type IsLiteralString<S extends string> = string extends S ? false : true; 165 + 166 + type IsChars<S extends string, Allowed extends string> = 167 + IsLiteralString<S> extends false ? false 168 + : S extends "" ? true 169 + : S extends `${infer First}${infer Rest}` 170 + ? First extends Allowed ? IsChars<Rest, Allowed> 171 + : false 172 + : false; 173 + 174 + type IsDidMethod<S extends string> = IsLiteralString<S> extends false ? false 175 + : S extends `${infer First}${infer Rest}` 176 + ? First extends LowerAlpha 177 + ? Rest extends "" ? true : IsChars<Rest, LowerAlpha> 178 + : false 179 + : false; 180 + 181 + type IsDidIdentifier<S extends string> = IsLiteralString<S> extends false 182 + ? false 183 + : S extends `${infer First}${infer Rest}` 184 + ? Rest extends "" ? First extends DidEndChar ? true : false 185 + : First extends DidChar ? IsDidIdentifier<Rest> 186 + : false 187 + : false; 188 + 189 + type IsDidLiteral<S extends string> = S extends 190 + `did:${infer Method}:${infer Id}` 191 + ? IsDidMethod<Method> extends true ? IsDidIdentifier<Id> 192 + : false 193 + : false; 194 + 195 + type IsHandleLabel<S extends string> = IsLiteralString<S> extends false ? false 196 + : S extends `${infer First}${infer Rest}` 197 + ? First extends Alpha | Digit 198 + ? Rest extends "" ? true : IsHandleLabelTail<Rest> 199 + : false 200 + : false; 201 + 202 + type IsHandleLabelTail<S extends string> = S extends 203 + `${infer First}${infer Rest}` 204 + ? Rest extends "" ? First extends Alpha | Digit ? true : false 205 + : First extends HandleChar ? IsHandleLabelTail<Rest> 206 + : false 207 + : false; 208 + 209 + type IsFinalHandleLabel<S extends string> = IsLiteralString<S> extends false 210 + ? false 211 + : S extends `${infer First}${infer Rest}` 212 + ? First extends Alpha ? Rest extends "" ? true : IsHandleLabelTail<Rest> 213 + : false 214 + : false; 215 + 216 + type IsHandleParts<S extends string> = S extends `${infer Label}.${infer Rest}` 217 + ? IsHandleLabel<Label> extends true ? IsHandleParts<Rest> : false 218 + : IsFinalHandleLabel<S>; 219 + 220 + type IsHandleLiteral<S extends string> = S extends `${string}.${string}` 221 + ? IsHandleParts<S> 222 + : false; 223 + 224 + type IsAtIdentifierLiteral<S extends string> = IsDidLiteral<S> extends true 225 + ? true 226 + : IsHandleLiteral<S>; 227 + 228 + type IsRecordKeyLiteral<S extends string> = IsLiteralString<S> extends false 229 + ? false 230 + : S extends "." | ".." ? false 231 + : S extends `${infer _First}${infer _Rest}` ? IsChars<S, RecordKeyChar> 232 + : false; 233 + 234 + type IsExactChars< 235 + S extends string, 236 + Allowed extends string, 237 + Length extends number, 238 + Count extends unknown[] = [], 239 + > = Count["length"] extends Length ? (S extends "" ? true : false) 240 + : S extends `${infer First}${infer Rest}` 241 + ? First extends Allowed 242 + ? IsExactChars<Rest, Allowed, Length, [...Count, unknown]> 243 + : false 244 + : false; 245 + 246 + type IsTidLiteral<S extends string> = S extends `${infer First}${infer Rest}` 247 + ? First extends TidInitialChar ? IsExactChars<Rest, TidChar, 12> : false 248 + : false; 249 + 250 + type IsDatetimeDate<S extends string> = S extends 251 + `${infer Year}-${infer Month}-${infer Day}` 252 + ? IsExactChars<Year, Digit, 4> extends true 253 + ? IsExactChars<Month, Digit, 2> extends true ? IsExactChars<Day, Digit, 2> 254 + : false 255 + : false 256 + : false; 257 + 258 + type IsDatetimeTime<S extends string> = S extends 259 + `${infer Hour}:${infer Minute}:${infer Second}` 260 + ? IsExactChars<Hour, Digit, 2> extends true 261 + ? IsExactChars<Minute, Digit, 2> extends true 262 + ? IsExactChars<Second, Digit, 2> 263 + : false 264 + : false 265 + : false; 266 + 267 + type IsDatetimeOffset<S extends string> = S extends 268 + `${infer Hour}:${infer Minute}` 269 + ? IsExactChars<Hour, Digit, 2> extends true ? IsExactChars<Minute, Digit, 2> 270 + : false 271 + : false; 272 + 273 + type IsDatetimeTail<S extends string> = S extends 274 + `${infer Time}.${infer Fraction}Z` 275 + ? IsDatetimeTime<Time> extends true ? IsChars<Fraction, Digit> : false 276 + : S extends `${infer Time}Z` ? IsDatetimeTime<Time> 277 + : S extends `${infer Time}+${infer Offset}` 278 + ? IsDatetimeTime<Time> extends true ? IsDatetimeOffset<Offset> : false 279 + : S extends `${infer Time}-${infer Offset}` 280 + ? IsDatetimeTime<Time> extends true ? IsDatetimeOffset<Offset> : false 281 + : S extends `${infer Time}.${infer Fraction}+${infer Offset}` 282 + ? IsDatetimeTime<Time> extends true 283 + ? IsChars<Fraction, Digit> extends true ? IsDatetimeOffset<Offset> : false 284 + : false 285 + : S extends `${infer Time}.${infer Fraction}-${infer Offset}` 286 + ? IsDatetimeTime<Time> extends true 287 + ? IsChars<Fraction, Digit> extends true ? IsDatetimeOffset<Offset> : false 288 + : false 289 + : false; 290 + 291 + type IsDatetimeLiteral<S extends string> = S extends 292 + `${infer Date}T${infer Tail}` 293 + ? IsDatetimeDate<Date> extends true ? IsDatetimeTail<Tail> : false 294 + : false; 295 + 296 + type AtUriHost<S extends string> = S extends `at://${infer Host}/${string}` 297 + ? Host 298 + : S extends `at://${infer Host}?${string}` ? Host 299 + : S extends `at://${infer Host}#${string}` ? Host 300 + : S extends `at://${infer Host}` ? Host 301 + : never; 302 + 303 + type IsAtUriLiteral<S extends string> = [AtUriHost<S>] extends [never] ? false 304 + : AtUriHost<S> extends infer Host extends string 305 + ? Host extends "" ? false : IsAtIdentifierLiteral<Host> 306 + : false; 307 + 308 + type IsCidLiteral<S extends string> = S extends `b${infer Rest}` 309 + ? IsChars<Rest, Base32Char> 310 + : S extends `z${infer Rest}` ? IsChars<Rest, Base58Char> 311 + : S extends `Qm${infer Rest}` ? IsChars<Rest, Base58Char> 312 + : false; 313 + 314 + type ValidateStringLiteral<Actual, Expected> = Actual extends string 315 + ? IsLiteralString<Actual> extends false ? never 316 + : [Expected] extends [DidString] ? IsDidLiteral<Actual> extends true ? Actual 317 + : never 318 + : [Expected] extends [HandleString] 319 + ? IsHandleLiteral<Actual> extends true ? Actual : never 320 + : [Expected] extends [AtIdentifierString] 321 + ? IsAtIdentifierLiteral<Actual> extends true ? Actual : never 322 + : [Expected] extends [RecordKeyString] 323 + ? IsRecordKeyLiteral<Actual> extends true ? Actual : never 324 + : [Expected] extends [TidString] ? IsTidLiteral<Actual> extends true ? Actual 325 + : never 326 + : [Expected] extends [DatetimeString] 327 + ? IsDatetimeLiteral<Actual> extends true ? Actual : never 328 + : [Expected] extends [AtUriString] 329 + ? IsAtUriLiteral<Actual> extends true ? Actual : never 330 + : [Expected] extends [CidString] ? IsCidLiteral<Actual> extends true ? Actual 331 + : never 332 + : never 333 + : never; 334 + 335 + type ValidateCallValue<Actual, Expected> = [Actual] extends [Expected] ? Actual 336 + : undefined extends Expected 337 + ? ValidateCallValue<Actual, Exclude<Expected, undefined>> 338 + : Expected extends string ? ValidateStringLiteral<Actual, Expected> 339 + : Expected extends readonly (infer Value)[] 340 + ? Actual extends readonly unknown[] 341 + ? { [K in keyof Actual]: ValidateCallValue<Actual[K], Value> } 342 + : never 343 + : Expected extends object 344 + ? Actual extends object ? ValidateCallObject<Actual, Expected> 345 + : never 346 + : never; 347 + 348 + type ValidateCallObject<Actual, Expected> = 349 + & { 350 + [K in keyof Expected]: K extends keyof Actual 351 + ? ValidateCallValue<Actual[K], Expected[K]> 352 + : Expected[K]; 353 + } 354 + & { 355 + [K in Exclude<keyof Actual, keyof Expected>]: never; 356 + }; 11 357 12 358 export type { 13 359 /** ··· 35 381 | string; 36 382 37 383 export interface XrpcCallOptions< 38 - M extends Query | Procedure = Query | Procedure, 384 + M extends XrpcMethodLike = XrpcMethod, 39 385 > extends CallOptions { 40 - params?: InferMethodParams<M>; 41 - body?: M extends Procedure ? InferMethodInputBody<M, BinaryBodyInit> 386 + params?: InferMethodParams<InferXrpcMethod<M>>; 387 + body?: InferXrpcMethod<M> extends Procedure 388 + ? InferMethodInputBody<InferXrpcMethod<M>, BinaryBodyInit> 42 389 : undefined; 43 390 validateRequest?: boolean; 44 391 validateResponse?: boolean; 45 392 } 393 + 394 + export type XrpcCallCompatibleOptions< 395 + M extends XrpcMethodLike = XrpcMethod, 396 + O = XrpcCallOptions<M>, 397 + > = ValidateCallValue<O, XrpcCallOptions<M>>; 46 398 47 399 export const errorResponseBody: z.ZodObject<{ 48 400 error: z.ZodOptional<z.ZodString>;