Skip to content

Commit 3d6e4c5

Browse files
authored
feat(fetch): add selector (cherry pick #5306 for v6) (#5442)
* feat(fetch): add selector (#5306) * feat(fetch): add selector * chore: fix import in dtslint test * chore: update side-effect snapshots
1 parent 2bce0e3 commit 3d6e4c5

File tree

5 files changed

+149
-8
lines changed

5 files changed

+149
-8
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1+
import "tslib";
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { fromFetch } from 'rxjs/fetch';
2+
import { a as a$ } from '../../helpers';
3+
4+
it('should emit the fetch Response by default', () => {
5+
const a = fromFetch("a"); // $ExpectType Observable<Response>
6+
});
7+
8+
it('should support a selector that returns a Response promise', () => {
9+
const a = fromFetch("a", { selector: response => response.text() }); // $ExpectType Observable<string>
10+
});
11+
12+
it('should support a selector that returns an arbitrary type', () => {
13+
const a = fromFetch("a", { selector: response => a$ }); // $ExpectType Observable<A>
14+
});
15+
16+
it('should error for selectors that don\'t return an ObservableInput', () => {
17+
const a = fromFetch("a", { selector: response => 42 }); // $ExpectError
18+
});

spec-dtslint/tsconfig.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@
88
"noImplicitThis": true,
99
"paths": {
1010
"rxjs": ["../dist/typings"],
11-
"rxjs/operators": ["../dist/typings/operators"]
11+
"rxjs/ajax": ["../dist/typings/ajax"],
12+
"rxjs/fetch": ["../dist/typings/fetch"],
13+
"rxjs/operators": ["../dist/typings/operators"],
14+
"rxjs/testing": ["../dist/typings/testing"],
15+
"rxjs/webSocket": ["../dist/typings/webSocket"]
1216
},
1317
"skipLibCheck": true,
1418
"strictFunctionTypes": true,

spec/observables/dom/fetch-spec.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,4 +262,57 @@ describe('fromFetch', () => {
262262
}
263263
});
264264
});
265+
266+
it('should support a selector', done => {
267+
mockFetch.respondWith = {
268+
...OK_RESPONSE,
269+
text: () => Promise.resolve('bar')
270+
};
271+
const fetch$ = fromFetch('/foo', {
272+
selector: response => response.text()
273+
});
274+
expect(mockFetch.calls.length).to.equal(0);
275+
expect(MockAbortController.created).to.equal(0);
276+
277+
fetch$.subscribe({
278+
next: text => {
279+
expect(text).to.equal('bar');
280+
},
281+
error: done,
282+
complete: () => {
283+
// Wait until the complete and the subsequent unsubscribe are finished
284+
// before testing these expectations:
285+
setTimeout(() => {
286+
expect(MockAbortController.created).to.equal(1);
287+
expect(mockFetch.calls.length).to.equal(1);
288+
expect(mockFetch.calls[0].input).to.equal('/foo');
289+
expect(mockFetch.calls[0].init!.signal).not.to.be.undefined;
290+
expect(mockFetch.calls[0].init!.signal!.aborted).to.be.false;
291+
done();
292+
}, 0);
293+
}
294+
});
295+
});
296+
297+
it('should abort when unsubscribed and a selector is specified', () => {
298+
mockFetch.respondWith = {
299+
...OK_RESPONSE,
300+
text: () => Promise.resolve('bar')
301+
};
302+
const fetch$ = fromFetch('/foo', {
303+
selector: response => response.text()
304+
});
305+
expect(mockFetch.calls.length).to.equal(0);
306+
expect(MockAbortController.created).to.equal(0);
307+
const subscription = fetch$.subscribe();
308+
309+
expect(MockAbortController.created).to.equal(1);
310+
expect(mockFetch.calls.length).to.equal(1);
311+
expect(mockFetch.calls[0].input).to.equal('/foo');
312+
expect(mockFetch.calls[0].init!.signal).not.to.be.undefined;
313+
expect(mockFetch.calls[0].init!.signal!.aborted).to.be.false;
314+
315+
subscription.unsubscribe();
316+
expect(mockFetch.calls[0].init!.signal!.aborted).to.be.true;
317+
});
265318
});

src/internal/observable/dom/fetch.ts

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
import { Observable } from '../../Observable';
22
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>;
317

418
/**
519
* Uses [the Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) to
@@ -42,7 +56,36 @@ import { Subscription } from '../../Subscription';
4256
* data$.subscribe({
4357
* next: result => console.log(result),
4458
* complete: () => console.log('done')
45-
* })
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+
* });
4689
* ```
4790
*
4891
* @param input The resource you would like to fetch. Can be a url or a request object.
@@ -51,8 +94,14 @@ import { Subscription } from '../../Subscription';
5194
* @returns An Observable, that when subscribed to performs an HTTP request using the native `fetch`
5295
* function. The {@link Subscription} is tied to an `AbortController` for the the fetch.
5396
*/
54-
export function fromFetch(input: string | Request, init?: RequestInit): Observable<Response> {
55-
return new Observable<Response>(subscriber => {
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 => {
56105
const controller = new AbortController();
57106
const signal = controller.signal;
58107
let abortable = true;
@@ -91,9 +140,26 @@ export function fromFetch(input: string | Request, init?: RequestInit): Observab
91140
}
92141

93142
fetch(input, perSubscriberInit).then(response => {
94-
abortable = false;
95-
subscriber.next(response);
96-
subscriber.complete();
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+
}
97163
}).catch(err => {
98164
abortable = false;
99165
if (!unsubscribed) {

0 commit comments

Comments
 (0)