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 | }
|
---|