Skip to content
This repository has been archived by the owner on Feb 26, 2024. It is now read-only.

feat(fetch): schedule macroTask when fetch #1075

Merged
merged 1 commit into from
Jun 29, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions file-size-limit.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
{
"path": "dist/zone.min.js",
"checkTarget": true,
"limit": 40144
"limit": 41500
}
]
}
}
46 changes: 29 additions & 17 deletions lib/browser/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,16 @@ Zone.__load_patch('XHR', (global: any, Zone: ZoneType) => {
});

const XMLHTTPREQUEST_SOURCE = 'XMLHttpRequest.send';
const sendNative =
const fetchTaskAborting = zoneSymbol('fetchTaskAborting');
const fetchTaskScheduling = zoneSymbol('fetchTaskScheduling');
const sendNative: Function|null =
patchMethod(XMLHttpRequestPrototype, 'send', () => function(self: any, args: any[]) {
if ((Zone.current as any)[fetchTaskScheduling] === true) {
// a fetch is scheduling, so we are using xhr to polyfill fetch
// and because we already schedule macroTask for fetch, we should
// not schedule a macroTask for xhr again
return sendNative!.apply(self, args);
}
if (self[XHR_SYNC]) {
// if the XHR is sync there is no task to schedule, just execute the code.
return sendNative!.apply(self, args);
Expand All @@ -212,22 +220,26 @@ Zone.__load_patch('XHR', (global: any, Zone: ZoneType) => {
}
});

const abortNative = patchMethod(XMLHttpRequestPrototype, 'abort', () => function(self: any) {
const task: Task = findPendingTask(self);
if (task && typeof task.type == 'string') {
// If the XHR has already completed, do nothing.
// If the XHR has already been aborted, do nothing.
// Fix #569, call abort multiple times before done will cause
// macroTask task count be negative number
if (task.cancelFn == null || (task.data && (<XHROptions>task.data).aborted)) {
return;
}
task.zone.cancelTask(task);
}
// Otherwise, we are trying to abort an XHR which has not yet been sent, so there is no
// task
// to cancel. Do nothing.
});
const abortNative =
patchMethod(XMLHttpRequestPrototype, 'abort', () => function(self: any, args: any[]) {
const task: Task = findPendingTask(self);
if (task && typeof task.type == 'string') {
// If the XHR has already completed, do nothing.
// If the XHR has already been aborted, do nothing.
// Fix #569, call abort multiple times before done will cause
// macroTask task count be negative number
if (task.cancelFn == null || (task.data && (<XHROptions>task.data).aborted)) {
return;
}
task.zone.cancelTask(task);
} else if ((Zone.current as any)[fetchTaskAborting] === true) {
// the abort is called from fetch polyfill, we need to call native abort of XHR.
return abortNative!.apply(self, args);
}
// Otherwise, we are trying to abort an XHR which has not yet been sent, so there is no
// task
// to cancel. Do nothing.
});
}
});

Expand Down
1 change: 1 addition & 0 deletions lib/browser/rollup-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@

import '../zone';
import '../common/promise';
import '../common/fetch';
import '../common/to-string';
import './browser';
100 changes: 100 additions & 0 deletions lib/common/fetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

Zone.__load_patch('fetch', (global: any, Zone: ZoneType, api: _ZonePrivate) => {
const fetch = global['fetch'];
const ZoneAwarePromise = global.Promise;
const symbolThenPatched = api.symbol('thenPatched');
const fetchTaskScheduling = api.symbol('fetchTaskScheduling');
const fetchTaskAborting = api.symbol('fetchTaskAborting');
if (typeof fetch !== 'function') {
return;
}
const OriginalAbortController = global['AbortController'];
const supportAbort = typeof OriginalAbortController === 'function';
let abortNative: Function|null = null;
if (supportAbort) {
global['AbortController'] = function() {
const abortController = new OriginalAbortController();
const signal = abortController.signal;
signal.abortController = abortController;
return abortController;
};
abortNative = api.patchMethod(
OriginalAbortController.prototype, 'abort',
(delegate: Function) => (self: any, args: any) => {
if (self.task) {
return self.task.zone.cancelTask(self.task);
}
return delegate.apply(self, args);
});
}
const placeholder = function() {};
global['fetch'] = function() {
const args = Array.prototype.slice.call(arguments);
const options = args.length > 1 ? args[1] : null;
const signal = options && options.signal;
return new Promise((res, rej) => {
const task = Zone.current.scheduleMacroTask(
'fetch', placeholder, args,
() => {
let fetchPromise;
let zone = Zone.current;
try {
(zone as any)[fetchTaskScheduling] = true;
fetchPromise = fetch.apply(this, args);
} catch (error) {
rej(error);
return;
} finally {
(zone as any)[fetchTaskScheduling] = false;
}

if (!(fetchPromise instanceof ZoneAwarePromise)) {
let ctor = fetchPromise.constructor;
if (!ctor[symbolThenPatched]) {
api.patchThen(ctor);
}
}
fetchPromise.then(
(resource: any) => {
if (task.state !== 'notScheduled') {
task.invoke();
}
res(resource);
},
(error: any) => {
if (task.state !== 'notScheduled') {
task.invoke();
}
rej(error);
});
},
() => {
if (!supportAbort) {
rej('No AbortController supported, can not cancel fetch');
return;
}
if (signal && signal.abortController && !signal.aborted &&
typeof signal.abortController.abort === 'function' && abortNative) {
try {
(Zone.current as any)[fetchTaskAborting] = true;
abortNative.call(signal.abortController);
} finally {
(Zone.current as any)[fetchTaskAborting] = false;
}
} else {
rej('cancel fetch need a AbortController.signal');
}
});
if (signal && signal.abortController) {
signal.abortController.task = task;
}
});
};
});
6 changes: 4 additions & 2 deletions lib/common/promise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,8 @@ Zone.__load_patch('ZoneAwarePromise', (global: any, Zone: ZoneType, api: _ZonePr
(Ctor as any)[symbolThenPatched] = true;
}

api.patchThen = patchThen;

function zoneify(fn: Function) {
return function() {
let resultPromise = fn.apply(this, arguments);
Expand All @@ -475,10 +477,10 @@ Zone.__load_patch('ZoneAwarePromise', (global: any, Zone: ZoneType, api: _ZonePr
if (NativePromise) {
patchThen(NativePromise);

let fetch = global['fetch'];
/*let fetch = global['fetch'];
if (typeof fetch == 'function') {
global['fetch'] = zoneify(fetch);
}
}*/
}

// This is not part of public API, but it is useful for tests, so we expose it.
Expand Down
4 changes: 3 additions & 1 deletion lib/zone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,7 @@ interface _ZonePrivate {
showUncaughtError: () => boolean;
patchEventTarget: (global: any, apis: any[], options?: any) => boolean[];
patchOnProperties: (obj: any, properties: string[]|null) => void;
patchThen: (ctro: Function) => void;
setNativePromise: (nativePromise: any) => void;
patchMethod:
(target: any, name: string,
Expand Down Expand Up @@ -1326,7 +1327,8 @@ const Zone: ZoneType = (function(global: any) {
patchEventTarget: () => [],
patchOnProperties: noop,
patchMethod: () => noop,
bindArguments: () => (null as any),
bindArguments: () => [],
patchThen: () => noop,
setNativePromise: (NativePromise: any) => {
// sometimes NativePromise.resolve static function
// is not ready yet, (such as core-js/es6.promise)
Expand Down
1 change: 1 addition & 0 deletions test/browser-zone-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ if (typeof window !== 'undefined') {
}
import '../lib/common/to-string';
import '../lib/browser/browser';
import '../lib/common/fetch';
import '../lib/browser/webapis-user-media';
import '../lib/browser/webapis-media-query';
import '../lib/testing/zone-testing';
Expand Down
72 changes: 0 additions & 72 deletions test/common/Promise.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -549,76 +549,4 @@ describe(
testPromiseSubClass();
});
});

describe('fetch', ifEnvSupports('fetch', function() {
it('should work for text response', function(done) {
testZone.run(function() {
global['fetch']('/base/test/assets/sample.json').then(function(response: any) {
const fetchZone = Zone.current;
expect(fetchZone).toBe(testZone);

response.text().then(function(text: string) {
expect(Zone.current).toBe(fetchZone);
expect(text.trim()).toEqual('{"hello": "world"}');
done();
});
});
});
});

it('should work for json response', function(done) {
testZone.run(function() {
global['fetch']('/base/test/assets/sample.json').then(function(response: any) {
const fetchZone = Zone.current;
expect(fetchZone).toBe(testZone);

response.json().then(function(obj: any) {
expect(Zone.current).toBe(fetchZone);
expect(obj.hello).toEqual('world');
done();
});
});
});
});

it('should work for blob response', function(done) {
testZone.run(function() {
global['fetch']('/base/test/assets/sample.json').then(function(response: any) {
const fetchZone = Zone.current;
expect(fetchZone).toBe(testZone);

// Android 4.3- doesn't support response.blob()
if (response.blob) {
response.blob().then(function(blob: any) {
expect(Zone.current).toBe(fetchZone);
expect(blob instanceof Blob).toEqual(true);
done();
});
} else {
done();
}
});
});
});

it('should work for arrayBuffer response', function(done) {
testZone.run(function() {
global['fetch']('/base/test/assets/sample.json').then(function(response: any) {
const fetchZone = Zone.current;
expect(fetchZone).toBe(testZone);

// Android 4.3- doesn't support response.arrayBuffer()
if (response.arrayBuffer) {
response.arrayBuffer().then(function(blob: any) {
expect(Zone.current).toBe(fetchZone);
expect(blob instanceof ArrayBuffer).toEqual(true);
done();
});
} else {
done();
}
});
});
});
}));
}));
Loading