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

Commit fd39f97

Browse files
committedMar 29, 2016
feat: treat XHRs as macrotasks
Now, when an XHR is sent it is treated as a pending macrotask. ZoneSpecs, such as the AsyncTestZoneSpec, rely on this to ensure that XHRs are completed. The macrotask is cancelled if the XHR is aborted, and completed when the XHR reaches state DONE.
1 parent cc91561 commit fd39f97

File tree

4 files changed

+192
-24
lines changed

4 files changed

+192
-24
lines changed
 

‎lib/browser/browser.ts

+70-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {eventTargetPatch} from './event-target';
33
import {propertyPatch} from './define-property';
44
import {registerElementPatch} from './register-element';
55
import {propertyDescriptorPatch} from './property-descriptor';
6-
import {patchMethod, patchPrototype, patchClass} from "./utils";
6+
import {patchMethod, patchPrototype, patchClass, zoneSymbol} from "./utils";
77

88
const set = 'set';
99
const clear = 'clear';
@@ -15,7 +15,7 @@ patchTimer(_global, set, clear, 'Interval');
1515
patchTimer(_global, set, clear, 'Immediate');
1616
patchTimer(_global, 'request', 'cancelMacroTask', 'AnimationFrame');
1717
patchTimer(_global, 'mozRequest', 'mozCancel', 'AnimationFrame');
18-
patchTimer(_global, 'webkitRequest', 'webkitCancel', 'AnimationFrame')
18+
patchTimer(_global, 'webkitRequest', 'webkitCancel', 'AnimationFrame');
1919

2020
for (var i = 0; i < blockingMethods.length; i++) {
2121
var name = blockingMethods[i];
@@ -34,6 +34,74 @@ patchClass('FileReader');
3434
propertyPatch();
3535
registerElementPatch(_global);
3636

37+
// Treat XMLHTTPRequest as a macrotask.
38+
patchXHR(_global);
39+
40+
const XHR_TASK = zoneSymbol('xhrTask');
41+
42+
interface XHROptions extends TaskData {
43+
target: any,
44+
args: any[],
45+
aborted: boolean
46+
}
47+
48+
function patchXHR(window: any) {
49+
function findPendingTask(target: any) {
50+
var pendingTask: Task = target[XHR_TASK];
51+
return pendingTask;
52+
}
53+
54+
function scheduleTask(task: Task) {
55+
var data = <XHROptions>task.data;
56+
data.target.addEventListener('readystatechange', () => {
57+
if (data.target.readyState === XMLHttpRequest.DONE) {
58+
if (!data.aborted) {
59+
task.invoke();
60+
}
61+
}
62+
});
63+
var storedTask: Task = data.target[XHR_TASK];
64+
if (!storedTask) {
65+
data.target[XHR_TASK] = task;
66+
}
67+
setNative.apply(data.target, data.args);
68+
return task;
69+
}
70+
71+
function placeholderCallback() {
72+
}
73+
74+
function clearTask(task: Task) {
75+
var data = <XHROptions>task.data;
76+
// Note - ideally, we would call data.target.removeEventListener here, but it's too late
77+
// to prevent it from firing. So instead, we store info for the event listener.
78+
data.aborted = true;
79+
return clearNative.apply(data.target, data.args);
80+
}
81+
82+
var setNative = patchMethod(window.XMLHttpRequest.prototype, 'send', () => function(self: any, args: any[]) {
83+
var zone = Zone.current;
84+
85+
var options: XHROptions = {
86+
target: self,
87+
isPeriodic: false,
88+
delay: null,
89+
args: args,
90+
aborted: false
91+
};
92+
return zone.scheduleMacroTask('XMLHttpRequest.send', placeholderCallback, options, scheduleTask, clearTask);
93+
});
94+
95+
var clearNative = patchMethod(window.XMLHttpRequest.prototype, 'abort', (delegate: Function) => function(self: any, args: any[]) {
96+
var task: Task = findPendingTask(self);
97+
if (task && typeof task.type == 'string') {
98+
task.zone.cancelTask(task);
99+
} else {
100+
throw new Error('tried to abort an XHR which has not yet been sent');
101+
}
102+
});
103+
}
104+
37105
/// GEO_LOCATION
38106
if (_global['navigator'] && _global['navigator'].geolocation) {
39107
patchPrototype(_global['navigator'].geolocation, [

‎lib/zone-spec/async-test.ts

+6-22
Original file line numberDiff line numberDiff line change
@@ -16,28 +16,13 @@
1616
_finishCallbackIfDone() {
1717
if (!(this._pendingMicroTasks || this._pendingMacroTasks)) {
1818
// We do this because we would like to catch unhandled rejected promises.
19-
// To do this quickly when there are native promises, we must run using an unwrapped
20-
// promise implementation.
21-
var symbol = (<any>Zone).__symbol__;
22-
var NativePromise: typeof Promise = <any>window[symbol('Promise')];
23-
if (NativePromise) {
24-
NativePromise.resolve(true)[symbol('then')](() => {
25-
if (!this._alreadyErrored) {
26-
this.runZone.run(this._finishCallback);
19+
this.runZone.run(() => {
20+
setTimeout(() => {
21+
if (!this._alreadyErrored && !(this._pendingMicroTasks || this._pendingMacroTasks)) {
22+
this._finishCallback();
2723
}
28-
});
29-
} else {
30-
// For implementations which do not have nativePromise, use setTimeout(0). This is slower,
31-
// but it also works because Zones will handle errors when rejected promises have no
32-
// listeners after one macrotask.
33-
this.runZone.run(() => {
34-
setTimeout(() => {
35-
if (!this._alreadyErrored) {
36-
this._finishCallback();
37-
}
38-
}, 0);
39-
});
40-
}
24+
}, 0);
25+
});
4126
}
4227
}
4328

@@ -79,7 +64,6 @@
7964

8065
onHasTask(delegate: ZoneDelegate, current: Zone, target: Zone, hasTaskState: HasTaskState) {
8166
delegate.hasTask(target, hasTaskState);
82-
8367
if (hasTaskState.change == 'microTask') {
8468
this._pendingMicroTasks = hasTaskState.microTask;
8569
this._finishCallbackIfDone();

‎test/async-test.spec.ts

+51
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,33 @@ describe('AsyncTestZoneSpec', function() {
173173
});
174174
});
175175

176+
it('should wait for XHRs to complete', function(done) {
177+
var req;
178+
var finished = false;
179+
180+
var testZoneSpec = new AsyncTestZoneSpec(() => {
181+
expect(finished).toBe(true);
182+
done();
183+
}, (err) => {
184+
done.fail('async zone called failCallback unexpectedly');
185+
}, 'name');
186+
187+
var atz = Zone.current.fork(testZoneSpec);
188+
189+
atz.run(function() {
190+
req = new XMLHttpRequest();
191+
192+
req.onreadystatechange = () => {
193+
if (req.readyState === XMLHttpRequest.DONE) {
194+
finished = true;
195+
}
196+
};
197+
198+
req.open('get', '/', true);
199+
req.send();
200+
});
201+
});
202+
176203
it('should fail if setInterval is used', (done) => {
177204
var finished = false;
178205

@@ -227,6 +254,30 @@ describe('AsyncTestZoneSpec', function() {
227254
});
228255

229256
});
257+
258+
it('should fail if an xhr fails', function(done) {
259+
var req;
260+
261+
var testZoneSpec = new AsyncTestZoneSpec(() => {
262+
done.fail('expected failCallback to be called');
263+
}, (err) => {
264+
expect(err).toEqual('bad url failure');
265+
done();
266+
}, 'name');
267+
268+
var atz = Zone.current.fork(testZoneSpec);
269+
270+
atz.run(function() {
271+
req = new XMLHttpRequest();
272+
req.onload = () => {
273+
if (req.status != 200) {
274+
throw new Error('bad url failure');
275+
}
276+
}
277+
req.open('get', '/bad-url', true);
278+
req.send();
279+
});
280+
});
230281
});
231282

232283
export var __something__;

‎test/browser/XMLHttpRequest.spec.ts

+65
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,34 @@ import {ifEnvSupports} from '../util';
33
describe('XMLHttpRequest', function () {
44
var testZone = Zone.current.fork({name: 'test'});
55

6+
it('should intercept XHRs and treat them as MacroTasks', function(done) {
7+
var req: any;
8+
var testZoneWithWtf = Zone.current.fork(Zone['wtfZoneSpec']).fork({ name: 'TestZone' });
9+
testZoneWithWtf.run(() => {
10+
req = new XMLHttpRequest();
11+
req.onload = () => {
12+
// The last entry in the log should be the invocation for the current onload,
13+
// which will vary depending on browser environment. The prior entries
14+
// should be the invocation of the send macrotask.
15+
expect(wtfMock.log[wtfMock.log.length - 5]).toMatch(
16+
/\> Zone\:invokeTask.*addEventListener\:readystatechange/);
17+
expect(wtfMock.log[wtfMock.log.length - 4]).toEqual(
18+
'> Zone:invokeTask:XMLHttpRequest.send("<root>::WTF::TestZone")');
19+
expect(wtfMock.log[wtfMock.log.length - 3]).toEqual(
20+
'< Zone:invokeTask:XMLHttpRequest.send');
21+
expect(wtfMock.log[wtfMock.log.length - 2]).toMatch(
22+
/\< Zone\:invokeTask.*addEventListener\:readystatechange/);
23+
done();
24+
};
25+
26+
req.open('get', '/', true);
27+
req.send();
28+
29+
var lastScheduled = wtfMock.log[wtfMock.log.length - 1];
30+
expect(lastScheduled).toMatch('# Zone:schedule:macroTask:XMLHttpRequest.send');
31+
}, null, null, 'unit-test');
32+
});
33+
634
it('should work with onreadystatechange', function (done) {
735
var req;
836

@@ -42,6 +70,43 @@ describe('XMLHttpRequest', function () {
4270

4371
req.send();
4472
});
73+
74+
it('should allow canceling of an XMLHttpRequest', function(done) {
75+
var spy = jasmine.createSpy('spy');
76+
var req;
77+
var pending = false;
78+
79+
var trackingTestZone = Zone.current.fork({
80+
name: 'tracking test zone',
81+
onHasTask: (delegate: ZoneDelegate, current: Zone, target: Zone, hasTaskState: HasTaskState) => {
82+
if (hasTaskState.change == 'macroTask') {
83+
pending = hasTaskState.macroTask;
84+
}
85+
delegate.hasTask(target, hasTaskState);
86+
}
87+
});
88+
89+
trackingTestZone.run(function() {
90+
req = new XMLHttpRequest();
91+
req.onreadystatechange = function() {
92+
if (req.readyState === XMLHttpRequest.DONE) {
93+
if (req.status !== 0) {
94+
spy();
95+
}
96+
}
97+
};
98+
req.open('get', '/', true);
99+
100+
req.send();
101+
req.abort();
102+
});
103+
104+
setTimeout(function() {
105+
expect(spy).not.toHaveBeenCalled();
106+
expect(pending).toEqual(false);
107+
done();
108+
}, 0);
109+
});
45110
}));
46111

47112
it('should preserve other setters', function () {

0 commit comments

Comments
 (0)
This repository has been archived.