Skip to content

Commit db3527b

Browse files
authored
feat: Support custom options for custom fetch() functions (#36)
Closes Support extending the possible options that can be passed to the custom data-fetching function #22.
1 parent ae81d43 commit db3527b

File tree

3 files changed

+91
-36
lines changed

3 files changed

+91
-36
lines changed

README.md

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ This package:
1111
+ **Supports abortable HTTP requests; no boilerplate.**
1212
+ **Can auto-abort HTTP requests in favor of newer request versions, with optional delaying (debouncing).**
1313
+ Works in any runtime that implements `fetch()` (browsers, NodeJS, etc.).
14-
+ Is probably the tiniest fetch wrapper you'll ever need: **342 LOC** including typing (`npx cloc .\src --exclude-dir=tests`).
14+
+ Is probably the tiniest fetch wrapper you'll ever need: **364 LOC** including typing (`npx cloc .\src --exclude-dir=tests`).
1515

1616
## Does a Non-OK Status Code Warrant an Error?
1717

@@ -564,9 +564,9 @@ a simple class, the `fetch-api-progress` NPM package and a custom body processor
564564
[Live demo in the Svelte REPL](https://svelte.dev/playground/ddeedfb44ab74727ac40df320c552b92)
565565

566566
> [!NOTE]
567-
> If you wanted to, `fetch-api-progress` also supports upload progress. As of **v0.9.0** of this library, however, is
568-
> not too simple to integrate. Soon, the ability to pass custom options to the core `fetch()` function will be a
569-
> feature and will solve this integration scenario quite elegantly. Stay tuned.
567+
> If you wanted to, `fetch-api-progress` also supports upload progress. This is achieved by calling
568+
> `trackRequestProgress` to create a specialized `RequestInit` object. See [the next subsection](#custom-fetch-options)
569+
> for details.
570570
571571
```ts
572572
import { trackResponseProgress } from "fetch-api-progress";
@@ -631,7 +631,57 @@ When the button is clicked, the download is started. The custom processor simpl
631631
`DownloadProgress` class. Svelte's reactivity system takes care of the rest, effectively bringing the progress element
632632
to life as the download progresses.
633633

634+
### Custom Fetch Options
635+
636+
> Since **v0.10.0**
637+
638+
If your custom data-fetching function (your custom `fetch()`) has abilities that require extra custom options from the
639+
caller, you're in luck: You can and TypeScript and Intellisense will fully work for you.
640+
641+
To exemplify, let's do **upload progress** with the `fetch-api-progress` NPM package.
642+
643+
First, create your custom data-fetching function:
644+
645+
```typescript
646+
// uploader.ts
647+
import { DrFetch, type FetchFnUrl, type FetchFnInit } from "dr-fetch";
648+
import { trackRequestProgress, type FetchProgressEvent } from "fetch-api-progress";
649+
650+
export type UploaderInit = FetchFnInit & {
651+
onProgress?: (progress: FetchProgressEvent) => void;
652+
}
653+
654+
function uploadingFetch(url: FetchFnUrl, init?: UploaderInit) {
655+
const trackedRequest = trackRequestProgress(init, init.onProgress);
656+
return fetch(url, trackedRequest);
657+
}
658+
659+
export default new DrFetch(uploadingFetch); // This will be fully typed to support onProgress.
660+
```
661+
662+
This is it. From this point forward, you just use this uploader object and the uploads will report progress:
663+
664+
```typescript
665+
import uploader from './uploader.js';
666+
667+
const response = await uploader
668+
.for<200, undefined>()
669+
.post(
670+
'/upload/big/data',
671+
bigDataBody,
672+
{
673+
onProgress: (p) => console.log('Progress: %s', (p.lengthComputable && (p.loaded / p.total).toFixed(2)) || 0)
674+
}
675+
);
676+
```
677+
678+
And there you were, learning all sorts of weird syntax, writing callbacks left and right to achieve things like this
679+
with interceptors in `axios` and `ky`. :smile:
680+
634681
### I want fancier!
635682

636-
Ok, more features are incoming, but if you feel you definitely need more, remember that `DrFetch` is a class. You can
637-
always extend it as per JavaScript's own rules.
683+
If you feel you definitely need more, remember that `DrFetch` is a class. You can always extend it as per JavaScript's
684+
own rules.
685+
686+
Also, feel free to [open a new issue](https://github.com/WJSoftware/dr-fetch/issues) if you have an idea for a feature
687+
that cannot be easily achievable in "user land".

src/DrFetch.ts

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,18 @@ function textParser(response: Response) {
115115
* existing one using the parent's `clone` function. When cloning, pass a new data-fetching function (if required) so
116116
* the clone uses this one instead of the one of the parent fetcher.
117117
*/
118-
export class DrFetch<TStatusCode extends number = StatusCode, T = unknown, Abortable extends boolean = false> {
119-
#fetchFn: FetchFn;
118+
export class DrFetch<
119+
TStatusCode extends number = StatusCode,
120+
TFetchInit extends FetchFnInit = FetchFnInit,
121+
T = unknown,
122+
Abortable extends boolean = false
123+
> {
124+
#fetchFn: FetchFn<TFetchInit>;
120125
#customProcessors: [ProcessorPattern, (response: Response, stockParsers: { json: BodyParserFn<any>; text: BodyParserFn<string>; }) => Promise<any>][] = [];
121-
#fetchImpl: (url: FetchFnUrl, init?: FetchFnInit) => Promise<any>;
126+
#fetchImpl: (url: FetchFnUrl, init?: TFetchInit) => Promise<any>;
122127
#autoAbortMap: Map<AutoAbortKey, AbortController> | undefined;
123128

124-
async #abortableFetch(url: FetchFnUrl, init?: FetchFnInit) {
129+
async #abortableFetch(url: FetchFnUrl, init?: TFetchInit) {
125130
try {
126131
return await this.#simpleFetch(url, init);
127132
}
@@ -136,7 +141,7 @@ export class DrFetch<TStatusCode extends number = StatusCode, T = unknown, Abort
136141
}
137142
}
138143

139-
async #simpleFetch(url: FetchFnUrl, init?: FetchFnInit) {
144+
async #simpleFetch(url: FetchFnUrl, init?: TFetchInit) {
140145
const response = await this.#fetchFn(url, init);
141146
const body = await this.#readBody(response);
142147
return {
@@ -182,7 +187,7 @@ export class DrFetch<TStatusCode extends number = StatusCode, T = unknown, Abort
182187
* function to register a custom body processor.
183188
* @param fetchFn Optional data-fetching function to use instead of the stock `fetch` function.
184189
*/
185-
constructor(fetchFn?: FetchFn) {
190+
constructor(fetchFn?: FetchFn<TFetchInit>) {
186191
this.#fetchFn = fetchFn ?? fetch.bind(globalThis.window || global);
187192
this.#fetchImpl = this.#simpleFetch.bind(this);
188193
}
@@ -219,7 +224,7 @@ export class DrFetch<TStatusCode extends number = StatusCode, T = unknown, Abort
219224
if (opts.preserveAbortable && this.isAbortable) {
220225
newClone.abortable();
221226
}
222-
return newClone as DrFetch<TStatusCode, TInherit extends true ? T : unknown, CloneAbortable>;
227+
return newClone as DrFetch<TStatusCode, TFetchInit, TInherit extends true ? T : unknown, CloneAbortable>;
223228
}
224229

225230
/**
@@ -249,7 +254,7 @@ export class DrFetch<TStatusCode extends number = StatusCode, T = unknown, Abort
249254
* @returns This fetcher object with its response type modified to include the body specification provided.
250255
*/
251256
for<TStatus extends TStatusCode, TBody = {}>() {
252-
return this as DrFetch<TStatusCode, FetchResult<T, TStatus, TBody>, Abortable>;
257+
return this as DrFetch<TStatusCode, TFetchInit, FetchResult<T, TStatus, TBody>, Abortable>;
253258
}
254259

255260
#contentMatchesType(contentType: string, response: Response, ...types: ProcessorPattern[]) {
@@ -308,7 +313,7 @@ export class DrFetch<TStatusCode extends number = StatusCode, T = unknown, Abort
308313
abortable() {
309314
this.#fetchImpl = this.#abortableFetch.bind(this);
310315
this.#autoAbortMap ??= new Map<AutoAbortKey, AbortController>();
311-
return this as DrFetch<TStatusCode, T, true>;
316+
return this as DrFetch<TStatusCode, TFetchInit, T, true>;
312317
}
313318

314319
/**
@@ -318,7 +323,7 @@ export class DrFetch<TStatusCode extends number = StatusCode, T = unknown, Abort
318323
* @param init Options for the data-fetching function.
319324
* @returns A response object with the HTTP response's `ok`, `status`, `statusText` and `body` properties.
320325
*/
321-
async fetch(url: FetchFnUrl, init?: FetchFnInit): Promise<(Abortable extends true ? {
326+
async fetch(url: FetchFnUrl, init?: TFetchInit): Promise<(Abortable extends true ? {
322327
aborted: true;
323328
error: DOMException;
324329
} | T : T)> {
@@ -333,7 +338,7 @@ export class DrFetch<TStatusCode extends number = StatusCode, T = unknown, Abort
333338
this.#autoAbortMap?.get(autoAbort.key)?.abort();
334339
const ac = new AbortController();
335340
this.#autoAbortMap!.set(autoAbort.key, ac);
336-
init ??= {};
341+
init ??= {} as TFetchInit;
337342
init.signal = ac.signal;
338343
if (autoAbort.delay !== undefined) {
339344
const aborted = await new Promise<boolean>((rs) => {
@@ -371,17 +376,17 @@ export class DrFetch<TStatusCode extends number = StatusCode, T = unknown, Abort
371376
* @param url URL for the fetch function call.
372377
* @returns A response object with the HTTP response's `ok`, `status`, `statusText` and `body` properties.
373378
*/
374-
get(url: URL | string, init?: Omit<FetchFnInit, 'method' | 'body'>) {
375-
return this.fetch(url, { ...init, method: 'GET' });
379+
get(url: URL | string, init?: Omit<TFetchInit, 'method' | 'body'>) {
380+
return this.fetch(url, { ...init, method: 'GET' } as TFetchInit);
376381
}
377382

378383
/**
379384
* Shortcut method to emit a HEAD HTTP request.
380385
* @param url URL for the fetch function call.
381386
* @returns A response object with the HTTP response's `ok`, `status`, `statusText` and `body` properties.
382387
*/
383-
head(url: URL | string, init?: Omit<FetchFnInit, 'method' | 'body'>) {
384-
return this.fetch(url, { ...init, method: 'HEAD' });
388+
head(url: URL | string, init?: Omit<TFetchInit, 'method' | 'body'>) {
389+
return this.fetch(url, { ...init, method: 'HEAD' } as TFetchInit);
385390
}
386391

387392
/**
@@ -398,10 +403,10 @@ export class DrFetch<TStatusCode extends number = StatusCode, T = unknown, Abort
398403
* does in those cases.
399404
* @returns A response object with the HTTP response's `ok`, `status`, `statusText` and `body` properties.
400405
*/
401-
post(url: URL | string, body?: BodyInit | null | Record<string, any>, init?: Omit<FetchFnInit, 'method' | 'body'>) {
406+
post(url: URL | string, body?: BodyInit | null | Record<string, any>, init?: Omit<TFetchInit, 'method' | 'body'>) {
402407
const fullInit = this.#createInit(body, init);
403408
fullInit.method = 'POST';
404-
return this.fetch(url, fullInit);
409+
return this.fetch(url, fullInit as TFetchInit);
405410
}
406411

407412
/**
@@ -418,10 +423,10 @@ export class DrFetch<TStatusCode extends number = StatusCode, T = unknown, Abort
418423
* does in those cases.
419424
* @returns A response object with the HTTP response's `ok`, `status`, `statusText` and `body` properties.
420425
*/
421-
patch(url: URL | string, body?: BodyInit | null | Record<string, any>, init?: Omit<FetchFnInit, 'method' | 'body'>) {
426+
patch(url: URL | string, body?: BodyInit | null | Record<string, any>, init?: Omit<TFetchInit, 'method' | 'body'>) {
422427
const fullInit = this.#createInit(body, init);
423428
fullInit.method = 'PATCH';
424-
return this.fetch(url, fullInit);
429+
return this.fetch(url, fullInit as TFetchInit);
425430
}
426431

427432
/**
@@ -438,10 +443,10 @@ export class DrFetch<TStatusCode extends number = StatusCode, T = unknown, Abort
438443
* does in those cases.
439444
* @returns A response object with the HTTP response's `ok`, `status`, `statusText` and `body` properties.
440445
*/
441-
delete(url: URL | string, body?: BodyInit | null | Record<string, any>, init?: Omit<FetchFnInit, 'method' | 'body'>) {
446+
delete(url: URL | string, body?: BodyInit | null | Record<string, any>, init?: Omit<TFetchInit, 'method' | 'body'>) {
442447
const fullInit = this.#createInit(body, init);
443448
fullInit.method = 'DELETE';
444-
return this.fetch(url, fullInit);
449+
return this.fetch(url, fullInit as TFetchInit);
445450
}
446451

447452
/**
@@ -458,9 +463,9 @@ export class DrFetch<TStatusCode extends number = StatusCode, T = unknown, Abort
458463
* does in those cases.
459464
* @returns A response object with the HTTP response's `ok`, `status`, `statusText` and `body` properties.
460465
*/
461-
put(url: URL | string, body?: BodyInit | null | Record<string, any>, init?: Omit<FetchFnInit, 'method' | 'body'>) {
466+
put(url: URL | string, body?: BodyInit | null | Record<string, any>, init?: Omit<TFetchInit, 'method' | 'body'>) {
462467
const fullInit = this.#createInit(body, init);
463468
fullInit.method = 'PUT';
464-
return this.fetch(url, fullInit);
469+
return this.fetch(url, fullInit as TFetchInit);
465470
}
466471
}

src/types.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,10 @@ export type FetchResult<T, TStatus extends number, TBody = undefined> =
5151
T | CoreFetchResult<TStatus, TBody>
5252
) extends infer R ? R : never;
5353

54-
/**
55-
* Type of the stock fetch function.
56-
*/
57-
export type FetchFn = typeof fetch;
58-
5954
/**
6055
* Type of the fetch function's URL parameter.
6156
*/
62-
export type FetchFnUrl = Parameters<FetchFn>[0];
57+
export type FetchFnUrl = Parameters<typeof fetch>[0];
6358

6459
/**
6560
* Possible types of keys accepted by the `autoAbort` option.
@@ -69,7 +64,7 @@ export type AutoAbortKey = string | symbol | number;
6964
/**
7065
* Type of the fetch function's init parameter.
7166
*/
72-
export type FetchFnInit = Parameters<FetchFn>[1] & {
67+
export type FetchFnInit = Parameters<typeof fetch>[1] & {
7368
/**
7469
* Specifies the options for auto-aborting the HTTP request.
7570
*
@@ -89,6 +84,11 @@ export type FetchFnInit = Parameters<FetchFn>[1] & {
8984
};
9085
};
9186

87+
/**
88+
* Type of the stock fetch function.
89+
*/
90+
export type FetchFn<TInit extends FetchFnInit = FetchFnInit> = (url: FetchFnUrl, init?: TInit) => Promise<Response>;
91+
9292
/**
9393
* Fetcher cloning options.
9494
*/

0 commit comments

Comments
 (0)