[6a3a178] | 1 | import { Observable } from '../../Observable';
|
---|
| 2 | import { Subscription } from '../../Subscription';
|
---|
| 3 | import { from } from '../../observable/from';
|
---|
| 4 | import { ObservableInput } from '../../types';
|
---|
| 5 |
|
---|
| 6 | export function fromFetch<T>(
|
---|
| 7 | input: string | Request,
|
---|
| 8 | init: RequestInit & {
|
---|
| 9 | selector: (response: Response) => ObservableInput<T>
|
---|
| 10 | }
|
---|
| 11 | ): Observable<T>;
|
---|
| 12 |
|
---|
| 13 | export function fromFetch(
|
---|
| 14 | input: string | Request,
|
---|
| 15 | init?: RequestInit
|
---|
| 16 | ): Observable<Response>;
|
---|
| 17 |
|
---|
| 18 | /**
|
---|
| 19 | * Uses [the Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) to
|
---|
| 20 | * make an HTTP request.
|
---|
| 21 | *
|
---|
| 22 | * **WARNING** Parts of the fetch API are still experimental. `AbortController` is
|
---|
| 23 | * required for this implementation to work and use cancellation appropriately.
|
---|
| 24 | *
|
---|
| 25 | * Will automatically set up an internal [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController)
|
---|
| 26 | * in order to teardown the internal `fetch` when the subscription tears down.
|
---|
| 27 | *
|
---|
| 28 | * If a `signal` is provided via the `init` argument, it will behave like it usually does with
|
---|
| 29 | * `fetch`. If the provided `signal` aborts, the error that `fetch` normally rejects with
|
---|
| 30 | * in that scenario will be emitted as an error from the observable.
|
---|
| 31 | *
|
---|
| 32 | * ### Basic Use
|
---|
| 33 | *
|
---|
| 34 | * ```ts
|
---|
| 35 | * import { of } from 'rxjs';
|
---|
| 36 | * import { fromFetch } from 'rxjs/fetch';
|
---|
| 37 | * import { switchMap, catchError } from 'rxjs/operators';
|
---|
| 38 | *
|
---|
| 39 | * const data$ = fromFetch('https://api.github.com/users?per_page=5').pipe(
|
---|
| 40 | * switchMap(response => {
|
---|
| 41 | * if (response.ok) {
|
---|
| 42 | * // OK return data
|
---|
| 43 | * return response.json();
|
---|
| 44 | * } else {
|
---|
| 45 | * // Server is returning a status requiring the client to try something else.
|
---|
| 46 | * return of({ error: true, message: `Error ${response.status}` });
|
---|
| 47 | * }
|
---|
| 48 | * }),
|
---|
| 49 | * catchError(err => {
|
---|
| 50 | * // Network or other error, handle appropriately
|
---|
| 51 | * console.error(err);
|
---|
| 52 | * return of({ error: true, message: err.message })
|
---|
| 53 | * })
|
---|
| 54 | * );
|
---|
| 55 | *
|
---|
| 56 | * data$.subscribe({
|
---|
| 57 | * next: result => console.log(result),
|
---|
| 58 | * complete: () => console.log('done')
|
---|
| 59 | * });
|
---|
| 60 | * ```
|
---|
| 61 | *
|
---|
| 62 | * ### Use with Chunked Transfer Encoding
|
---|
| 63 | *
|
---|
| 64 | * With HTTP responses that use [chunked transfer encoding](https://tools.ietf.org/html/rfc7230#section-3.3.1),
|
---|
| 65 | * the promise returned by `fetch` will resolve as soon as the response's headers are
|
---|
| 66 | * received.
|
---|
| 67 | *
|
---|
| 68 | * That means the `fromFetch` observable will emit a `Response` - and will
|
---|
| 69 | * then complete - before the body is received. When one of the methods on the
|
---|
| 70 | * `Response` - like `text()` or `json()` - is called, the returned promise will not
|
---|
| 71 | * resolve until the entire body has been received. Unsubscribing from any observable
|
---|
| 72 | * that uses the promise as an observable input will not abort the request.
|
---|
| 73 | *
|
---|
| 74 | * To facilitate aborting the retrieval of responses that use chunked transfer encoding,
|
---|
| 75 | * a `selector` can be specified via the `init` parameter:
|
---|
| 76 | *
|
---|
| 77 | * ```ts
|
---|
| 78 | * import { of } from 'rxjs';
|
---|
| 79 | * import { fromFetch } from 'rxjs/fetch';
|
---|
| 80 | *
|
---|
| 81 | * const data$ = fromFetch('https://api.github.com/users?per_page=5', {
|
---|
| 82 | * selector: response => response.json()
|
---|
| 83 | * });
|
---|
| 84 | *
|
---|
| 85 | * data$.subscribe({
|
---|
| 86 | * next: result => console.log(result),
|
---|
| 87 | * complete: () => console.log('done')
|
---|
| 88 | * });
|
---|
| 89 | * ```
|
---|
| 90 | *
|
---|
| 91 | * @param input The resource you would like to fetch. Can be a url or a request object.
|
---|
| 92 | * @param init A configuration object for the fetch.
|
---|
| 93 | * [See MDN for more details](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters)
|
---|
| 94 | * @returns An Observable, that when subscribed to performs an HTTP request using the native `fetch`
|
---|
| 95 | * function. The {@link Subscription} is tied to an `AbortController` for the the fetch.
|
---|
| 96 | */
|
---|
| 97 | export function fromFetch<T>(
|
---|
| 98 | input: string | Request,
|
---|
| 99 | initWithSelector: RequestInit & {
|
---|
| 100 | selector?: (response: Response) => ObservableInput<T>
|
---|
| 101 | } = {}
|
---|
| 102 | ): Observable<Response | T> {
|
---|
| 103 | const { selector, ...init } = initWithSelector;
|
---|
| 104 | return new Observable<Response | T>(subscriber => {
|
---|
| 105 | const controller = new AbortController();
|
---|
| 106 | const signal = controller.signal;
|
---|
| 107 | let abortable = true;
|
---|
| 108 | let unsubscribed = false;
|
---|
| 109 |
|
---|
| 110 | const subscription = new Subscription();
|
---|
| 111 | subscription.add(() => {
|
---|
| 112 | unsubscribed = true;
|
---|
| 113 | if (abortable) {
|
---|
| 114 | controller.abort();
|
---|
| 115 | }
|
---|
| 116 | });
|
---|
| 117 |
|
---|
| 118 | let perSubscriberInit: RequestInit;
|
---|
| 119 | if (init) {
|
---|
| 120 | // If a signal is provided, just have it teardown. It's a cancellation token, basically.
|
---|
| 121 | if (init.signal) {
|
---|
| 122 | if (init.signal.aborted) {
|
---|
| 123 | controller.abort();
|
---|
| 124 | } else {
|
---|
| 125 | const outerSignal = init.signal;
|
---|
| 126 | const outerSignalHandler = () => {
|
---|
| 127 | if (!signal.aborted) {
|
---|
| 128 | controller.abort();
|
---|
| 129 | }
|
---|
| 130 | };
|
---|
| 131 | outerSignal.addEventListener('abort', outerSignalHandler);
|
---|
| 132 | subscription.add(() => outerSignal.removeEventListener('abort', outerSignalHandler));
|
---|
| 133 | }
|
---|
| 134 | }
|
---|
| 135 | // init cannot be mutated or reassigned as it's closed over by the
|
---|
| 136 | // subscriber callback and is shared between subscribers.
|
---|
| 137 | perSubscriberInit = { ...init, signal };
|
---|
| 138 | } else {
|
---|
| 139 | perSubscriberInit = { signal };
|
---|
| 140 | }
|
---|
| 141 |
|
---|
| 142 | fetch(input, perSubscriberInit).then(response => {
|
---|
| 143 | if (selector) {
|
---|
| 144 | subscription.add(from(selector(response)).subscribe(
|
---|
| 145 | value => subscriber.next(value),
|
---|
| 146 | err => {
|
---|
| 147 | abortable = false;
|
---|
| 148 | if (!unsubscribed) {
|
---|
| 149 | // Only forward the error if it wasn't an abort.
|
---|
| 150 | subscriber.error(err);
|
---|
| 151 | }
|
---|
| 152 | },
|
---|
| 153 | () => {
|
---|
| 154 | abortable = false;
|
---|
| 155 | subscriber.complete();
|
---|
| 156 | }
|
---|
| 157 | ));
|
---|
| 158 | } else {
|
---|
| 159 | abortable = false;
|
---|
| 160 | subscriber.next(response);
|
---|
| 161 | subscriber.complete();
|
---|
| 162 | }
|
---|
| 163 | }).catch(err => {
|
---|
| 164 | abortable = false;
|
---|
| 165 | if (!unsubscribed) {
|
---|
| 166 | // Only forward the error if it wasn't an abort.
|
---|
| 167 | subscriber.error(err);
|
---|
| 168 | }
|
---|
| 169 | });
|
---|
| 170 |
|
---|
| 171 | return subscription;
|
---|
| 172 | });
|
---|
| 173 | }
|
---|