-
-
Save mindplay-dk/2f9790779d310472e52392c4b75e5690 to your computer and use it in GitHub Desktop.
Fetchy
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
type Fetch = typeof fetch; | |
type SimpleURLSearchParams = Iterable<[string, string]>; | |
type URLSearchParamsInit = ConstructorParameters<typeof URLSearchParams>[0]; | |
type SimpleRequestInit = RequestInit & { | |
params?: SimpleURLSearchParams; | |
} | |
type EnhancedRequestInit = RequestInit & { | |
params?: URLSearchParamsInit | |
} | |
/** | |
* This type is a subtype of `typeof Fetch`, with argument overloads removed. | |
* | |
* Note that `fetch` is assignable to `Fetchy`. | |
* | |
* While the `fetch` overloads are convenient for developers, they are very | |
* inconvenient if you're trying to compose functions, because it forces | |
* every function to deal with every possible permutation of arguments. | |
* | |
* Besides convenience, the `fetch` overloads are also problematic, because | |
* the `URL` and `Request` constructors (as well as `fetch` proper) eagerly | |
* resolves relative URLs - if you attempt to compose functions with calls | |
* to the `URL` or `Request` constructors anywhere in those functions, the | |
* relative URL is lost, which would mean (for example) that `withBaseURL` | |
* would only work if it's the first function that gets composed. | |
* | |
* Long story short, composition using the original `typeof fetch` is both | |
* impractical and problematic. | |
*/ | |
type SimpleFetch = (url: string, init?: SimpleRequestInit) => Promise<Response>; | |
type EnhancedFetch = (input: RequestInfo | URL, init?: EnhancedRequestInit) => Promise<Response>; | |
/** | |
* You can optionally use this function to convert a `Fetchy` back into a | |
* `fetch`-compatible function, if required for dependency injection. | |
* | |
* I don't really recommend using this, unless you have to, as this will | |
* bring back all the overloads and possible confusion with the original | |
* `fetch` API - for example, if you apply `withBaseURL`, and then call | |
* the resulting function with `new URL("/data")` or `new Request("/data")`, | |
* this might not do what you expect, as the `Request` or `URL` constructors | |
* will eagerly resolve the URL relative to `window.location`. | |
* | |
* On the other hand, maybe that's just what you expected? I don't think it | |
* makes for very readable code, and I would prefer to work with a `Fetchy` | |
* directly, forcing all `fetch` calls to use the same arguments, forcing | |
* all URLs into a `string` form. It's just less confusing. | |
*/ | |
function enhance(fetch: SimpleFetch): EnhancedFetch { | |
return (input, init) => { | |
if (typeof input === "string") { | |
return fetch(input, init); | |
} | |
if (input instanceof Request) { | |
return fetch(input.url, extractRequest(new Request(input, init))); | |
} | |
return fetch(input.toString(), init); | |
}; | |
} | |
function extractRequest({ body, cache, credentials, headers, integrity, keepalive, method, mode, redirect, referrer, referrerPolicy, signal }: Request): SimpleRequestInit { | |
return { | |
body, | |
cache, | |
credentials, | |
headers, | |
integrity, | |
keepalive, | |
method, | |
mode, | |
redirect, | |
referrer, | |
referrerPolicy, | |
signal, | |
} | |
} | |
function applyParams(url: string, params: URLSearchParamsInit = []): string { | |
const $url = new URL(url, "thismessage:/"); // 😭 https://github.com/whatwg/url/issues/531#issuecomment-682158237 | |
const query = new URLSearchParams([...$url.searchParams, ...new URLSearchParams(params)]).toString(); | |
return url.replace(/\?.*|$/, query ? "?" + query : ""); | |
} | |
/** | |
* This interfaces defines a simple `Middleware` format, allowing for | |
* simple composition of fetch-like functions. | |
*/ | |
type Middleware = (fetch: SimpleFetch) => SimpleFetch; | |
/** | |
* This function enhances `fetch` and applies a list of `Middleware` to it. | |
*/ | |
function compose(fetch: Fetch, middleware: Middleware[]): EnhancedFetch { | |
return enhance(middleware.reduce<SimpleFetch>((_fetch, apply) => apply(_fetch), fetch)); | |
} | |
// Example middleware: | |
function withBaseURL(baseURL: string): Middleware { | |
return (fetch) => (url, init) => fetch(new URL(url, baseURL).toString(), init); | |
} | |
function withMethod(method: string): Middleware { | |
return (fetch) => (url, init) => fetch(url, { ...init, method }); | |
} | |
function withHeaders(headers: HeadersInit): Middleware { | |
const $headers = [...new Headers(headers)]; | |
return (fetch) => (url, init) => | |
fetch(url, { | |
...init, | |
headers: [...$headers, ...new Headers(init?.headers)], | |
}); | |
} | |
function withParams(params: URLSearchParamsInit): Middleware { | |
const $params = [...new URLSearchParams(params)]; | |
return (fetch) => (url, init) => { | |
console.log({ $params, init }); | |
return fetch(url, { | |
...init, | |
params: [...$params, ...init?.params || []] | |
}); | |
} | |
} | |
// Example usage: | |
const fetchy = compose(fetch, [ | |
withMethod("GET"), | |
withBaseURL("https://mocki.io/v1/"), | |
withHeaders({ "X-Hello": "Hello World" }), | |
withParams({ oh: "hai" }) // TODO WTF? | |
]); | |
debugger; | |
fetchy("d4867d8b-b5d5-4a48-a4ab-79131b5809b8", { headers: { "X-LOL": "whatevs" }, params: { lol: "wat" } }) | |
.then((r) => r.json()) | |
.then((d) => console.log(d)); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment