Suite of AT Protocol TypeScript libraries built on web standards
1import type { Gettable } from "./types.ts";
2import { combineHeaders } from "./util.ts";
3
4export type FetchHandler = (
5 this: void,
6 /**
7 * The URL (pathname + query parameters) to make the request to, without the
8 * origin. The origin (protocol, hostname, and port) must be added by this
9 * {@link FetchHandler}, typically based on authentication or other factors.
10 */
11 url: string,
12 init: RequestInit,
13) => Promise<Response>;
14
15export type FetchHandlerOptions = BuildFetchHandlerOptions | string | URL;
16
17export type BuildFetchHandlerOptions = {
18 /**
19 * The service URL to make requests to. This can be a string, URL, or a
20 * function that returns a string or URL. This is useful for dynamic URLs,
21 * such as a service URL that changes based on authentication.
22 */
23 service: Gettable<string | URL>;
24
25 /**
26 * Headers to be added to every request. If a function is provided, it will be
27 * called on each request to get the headers. This is useful for dynamic
28 * headers, such as authentication tokens that may expire.
29 */
30 headers?: {
31 [_ in string]?: Gettable<null | string>;
32 };
33
34 /**
35 * Bring your own fetch implementation. Typically useful for testing, logging,
36 * mocking, or adding retries, session management, signatures, proof of
37 * possession (DPoP), SSRF protection, etc. Defaults to the global `fetch`
38 * function.
39 */
40 fetch?: typeof globalThis.fetch;
41};
42
43export interface FetchHandlerObject {
44 fetchHandler: (
45 this: FetchHandlerObject,
46 /**
47 * The URL (pathname + query parameters) to make the request to, without the
48 * origin. The origin (protocol, hostname, and port) must be added by this
49 * {@link FetchHandler}, typically based on authentication or other factors.
50 */
51 url: string,
52 init: RequestInit,
53 ) => Promise<Response>;
54}
55
56export function buildFetchHandler(
57 options: FetchHandler | FetchHandlerObject | FetchHandlerOptions,
58): FetchHandler {
59 // Already a fetch handler (allowed for convenience)
60 if (typeof options === "function") return options;
61 if (typeof options === "object" && "fetchHandler" in options) {
62 return options.fetchHandler.bind(options);
63 }
64
65 const {
66 service,
67 headers: defaultHeaders = undefined,
68 fetch = globalThis.fetch,
69 } = typeof options === "string" || options instanceof URL
70 ? { service: options }
71 : options;
72
73 if (typeof fetch !== "function") {
74 throw new TypeError(
75 "XrpcDispatcher requires fetch() to be available in your environment.",
76 );
77 }
78
79 const defaultHeadersEntries = defaultHeaders != null
80 ? Object.entries(defaultHeaders)
81 : undefined;
82
83 return function (url, init) {
84 const base = typeof service === "function" ? service() : service;
85 const fullUrl = new URL(url, base);
86
87 const headers = combineHeaders(init.headers, defaultHeadersEntries);
88
89 return fetch(fullUrl, { ...init, headers });
90 };
91}