-
Notifications
You must be signed in to change notification settings - Fork 3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Adds `fromFetch` implementation that uses native `fetch` - Adds tests and basic documentation - DOES NOT polyfill fetch
- Loading branch information
Showing
10 changed files
with
272 additions
and
1 deletion.
There are no files selected for viewing
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
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,195 @@ | ||
import { fromFetch } from 'rxjs/fetch'; | ||
import { expect } from 'chai'; | ||
import { root } from '../../../src/internal/util/root'; | ||
|
||
const OK_RESPONSE = { | ||
ok: true, | ||
} as Response; | ||
|
||
function mockFetchImpl(input: string | Request, init?: RequestInit): Promise<Response> { | ||
(mockFetchImpl as MockFetch).calls.push({ input, init }); | ||
return new Promise<any>((resolve, reject) => { | ||
if (init.signal) { | ||
init.signal.addEventListener('abort', () => { | ||
console.log('triggered'); | ||
reject(new MockDOMException()); | ||
}); | ||
} | ||
return Promise.resolve(null).then(() => { | ||
console.log('resolved'); | ||
resolve((mockFetchImpl as any).respondWith); | ||
}); | ||
}); | ||
} | ||
(mockFetchImpl as MockFetch).reset = function (this: any) { | ||
this.calls = [] as any[]; | ||
this.respondWith = OK_RESPONSE; | ||
}; | ||
(mockFetchImpl as MockFetch).reset(); | ||
|
||
const mockFetch: MockFetch = mockFetchImpl as MockFetch; | ||
|
||
class MockDOMException {} | ||
|
||
class MockAbortController { | ||
readonly signal = new MockAbortSignal(); | ||
|
||
abort() { | ||
this.signal._signal(); | ||
} | ||
|
||
constructor() { | ||
MockAbortController.created++; | ||
} | ||
|
||
static created = 0; | ||
|
||
static reset() { | ||
MockAbortController.created = 0; | ||
} | ||
} | ||
|
||
class MockAbortSignal { | ||
private _listeners: Function[] = []; | ||
|
||
aborted = false; | ||
|
||
addEventListener(name: 'abort', handler: Function) { | ||
this._listeners.push(handler); | ||
} | ||
|
||
removeEventListener(name: 'abort', handler: Function) { | ||
const index = this._listeners.indexOf(handler); | ||
if (index >= 0) { | ||
this._listeners.splice(index, 1); | ||
} | ||
} | ||
|
||
_signal() { | ||
this.aborted = true; | ||
while (this._listeners.length > 0) { | ||
this._listeners.shift()(); | ||
} | ||
} | ||
} | ||
|
||
interface MockFetch { | ||
(input: string | Request, init?: RequestInit): Promise<Response>; | ||
calls: { input: string | Request, init: RequestInit | undefined }[]; | ||
reset(): void; | ||
respondWith: Response; | ||
} | ||
|
||
describe('fromFetch', () => { | ||
let _fetch: typeof fetch; | ||
let _AbortController: AbortController; | ||
|
||
beforeEach(() => { | ||
mockFetch.reset(); | ||
if (root.fetch) { | ||
_fetch = root.fetch; | ||
} | ||
root.fetch = mockFetch; | ||
|
||
MockAbortController.reset(); | ||
if (root.AbortController) { | ||
_AbortController = root.AbortController; | ||
} | ||
root.AbortController = MockAbortController; | ||
}); | ||
|
||
afterEach(() => { | ||
root.fetch = _fetch; | ||
root.AbortController = _AbortController; | ||
}); | ||
|
||
it('should exist', () => { | ||
expect(fromFetch).to.be.a('function'); | ||
}); | ||
|
||
it('should fetch', done => { | ||
const fetch$ = fromFetch('/foo'); | ||
expect(mockFetch.calls.length).to.equal(0); | ||
expect(MockAbortController.created).to.equal(0); | ||
|
||
fetch$.subscribe({ | ||
next: response => { | ||
expect(response).to.equal(OK_RESPONSE); | ||
}, | ||
error: done, | ||
complete: done, | ||
}); | ||
|
||
expect(MockAbortController.created).to.equal(1); | ||
expect(mockFetch.calls.length).to.equal(1); | ||
expect(mockFetch.calls[0].input).to.equal('/foo'); | ||
expect(mockFetch.calls[0].init.signal).not.to.be.undefined; | ||
expect(mockFetch.calls[0].init.signal.aborted).to.be.false; | ||
}); | ||
|
||
it('should handle Response that is not `ok`', done => { | ||
mockFetch.respondWith = { | ||
ok: false, | ||
status: 400, | ||
body: 'Bad stuff here' | ||
} as any as Response; | ||
|
||
const fetch$ = fromFetch('/foo'); | ||
expect(mockFetch.calls.length).to.equal(0); | ||
expect(MockAbortController.created).to.equal(0); | ||
|
||
fetch$.subscribe({ | ||
next: () => done(new Error('should not be called')), | ||
error: err => { | ||
expect(err.message).to.equal('RxJS Fetch HTTP Error\n\nStatus:\n400\n\nBody:\nBad stuff here\n'); | ||
done(); | ||
}, | ||
}); | ||
|
||
expect(MockAbortController.created).to.equal(1); | ||
expect(mockFetch.calls.length).to.equal(1); | ||
expect(mockFetch.calls[0].input).to.equal('/foo'); | ||
expect(mockFetch.calls[0].init.signal).not.to.be.undefined; | ||
expect(mockFetch.calls[0].init.signal.aborted).to.be.false; | ||
}); | ||
|
||
it('should abort when unsubscribed', () => { | ||
const fetch$ = fromFetch('/foo'); | ||
expect(mockFetch.calls.length).to.equal(0); | ||
expect(MockAbortController.created).to.equal(0); | ||
const subscription = fetch$.subscribe(); | ||
|
||
expect(MockAbortController.created).to.equal(1); | ||
expect(mockFetch.calls.length).to.equal(1); | ||
expect(mockFetch.calls[0].input).to.equal('/foo'); | ||
expect(mockFetch.calls[0].init.signal).not.to.be.undefined; | ||
expect(mockFetch.calls[0].init.signal.aborted).to.be.false; | ||
|
||
subscription.unsubscribe(); | ||
expect(mockFetch.calls[0].init.signal.aborted).to.be.true; | ||
}); | ||
|
||
it('should allow passing of init object', () => { | ||
const myInit = {}; | ||
const fetch$ = fromFetch('/foo', myInit); | ||
fetch$.subscribe(); | ||
expect(mockFetch.calls[0].init).to.equal(myInit); | ||
expect(mockFetch.calls[0].init.signal).not.to.be.undefined; | ||
}); | ||
|
||
it('should treat passed signals as a cancellation token which triggers an error', done => { | ||
const controller = new MockAbortController(); | ||
const signal = controller.signal as any; | ||
const fetch$ = fromFetch('/foo', { signal }); | ||
const subscription = fetch$.subscribe({ | ||
error: err => { | ||
expect(err).to.be.instanceof(MockDOMException); | ||
done(); | ||
} | ||
}); | ||
controller.abort(); | ||
expect(mockFetch.calls[0].init.signal.aborted).to.be.true; | ||
// The subscription will not be closed until the error fires when the promise resolves. | ||
expect(subscription.closed).to.be.false; | ||
}); | ||
}); |
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { fromFetch } from '../internal/observable/dom/fetch'; |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{ | ||
"name": "rxjs/fetch", | ||
"typings": "./index.d.ts", | ||
"main": "./index.js", | ||
"module": "../_esm5/fetch/index.js", | ||
"es2015": "../_esm2015/fetch/index.js", | ||
"sideEffects": false | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import { Observable } from '../../Observable'; | ||
|
||
/** | ||
* Uses [the Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) to | ||
* make an HTTP request. | ||
* | ||
* Will automatically set up an internal [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) | ||
* in order to teardown the internal `fetch` when the subscription tears down. | ||
* | ||
* If a `signal` is provided via the `init` argument, it will behave like it usually does with | ||
* `fetch`. If the provided `signal` aborts, the error that `fetch` normally rejects with | ||
* in that scenario will be emitted as an error from the observable. | ||
* | ||
* @param input The resource you would like to fetch. Can be a url or a request object. | ||
* @param init A configuration object for the fetch. | ||
* [See MDN for more details](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters) | ||
* @returns An Observable, that when subscribed to performs an HTTP request using the native `fetch` | ||
* function. The {@link Subscription} is tied to an `AbortController` for the the fetch. | ||
*/ | ||
export function fromFetch(input: string | Request, init?: RequestInit): Observable<Response> { | ||
return new Observable<Response>(subscriber => { | ||
const controller = new AbortController(); | ||
const signal = controller.signal; | ||
let outerSignalHandler: () => void; | ||
let unsubscribed = false; | ||
|
||
if (init) { | ||
// If we a signal is provided, just have it teardown. It's a cancellation token, basically. | ||
if (init.signal) { | ||
outerSignalHandler = () => { | ||
if (!signal.aborted) { | ||
controller.abort(); | ||
} | ||
}; | ||
init.signal.addEventListener('abort', outerSignalHandler); | ||
} | ||
init.signal = signal; | ||
} else { | ||
init = { signal }; | ||
} | ||
|
||
fetch(input, init).then(response => { | ||
if (response.ok) { | ||
subscriber.next(response); | ||
subscriber.complete(); | ||
} else { | ||
subscriber.error(new Error(`RxJS Fetch HTTP Error\n\nStatus:\n${response.status}\n\nBody:\n${response.body}\n`)); | ||
} | ||
}).catch(err => { | ||
if (!unsubscribed) { | ||
// Only forward the error if it wasn't an abort. | ||
subscriber.error(err); | ||
} | ||
}); | ||
|
||
return () => { | ||
unsubscribed = true; | ||
controller.abort(); | ||
}; | ||
}); | ||
} |
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
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
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