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

Commit 100b82b

Browse files
alxhubmhevery
authored andcommitted
feat(node): patch outgoing http requests to capture the zone (#430)
* feat(node): patch outgoing http requests to capture the zone * feat(node): patch all EventEmitters
1 parent de318ce commit 100b82b

File tree

6 files changed

+216
-72
lines changed

6 files changed

+216
-72
lines changed

lib/common/utils.ts

Lines changed: 103 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,11 @@ export function patchOnProperties(obj: any, properties: string[]) {
108108
};
109109

110110
const EVENT_TASKS = zoneSymbol('eventTasks');
111+
112+
// For EventTarget
111113
const ADD_EVENT_LISTENER = 'addEventListener';
112114
const REMOVE_EVENT_LISTENER = 'removeEventListener';
113-
const SYMBOL_ADD_EVENT_LISTENER = zoneSymbol(ADD_EVENT_LISTENER);
114-
const SYMBOL_REMOVE_EVENT_LISTENER = zoneSymbol(REMOVE_EVENT_LISTENER);
115+
115116

116117
interface ListenerTaskMeta extends TaskData {
117118
useCapturing: boolean;
@@ -151,83 +152,113 @@ function attachRegisteredEvent(target: any, eventTask: Task): void {
151152
eventTasks.push(eventTask);
152153
}
153154

154-
function scheduleEventListener(eventTask: Task): any {
155-
const meta = <ListenerTaskMeta>eventTask.data;
156-
attachRegisteredEvent(meta.target, eventTask);
157-
return meta.target[SYMBOL_ADD_EVENT_LISTENER](meta.eventName, eventTask.invoke,
158-
meta.useCapturing);
159-
}
155+
export function makeZoneAwareAddListener(addFnName: string, removeFnName: string, useCapturingParam: boolean = true, allowDuplicates: boolean = false) {
156+
const addFnSymbol = zoneSymbol(addFnName);
157+
const removeFnSymbol = zoneSymbol(removeFnName);
158+
const defaultUseCapturing = useCapturingParam ? false : undefined;
160159

161-
function cancelEventListener(eventTask: Task): void {
162-
const meta = <ListenerTaskMeta>eventTask.data;
163-
findExistingRegisteredTask(meta.target, eventTask.invoke, meta.eventName,
164-
meta.useCapturing, true);
165-
meta.target[SYMBOL_REMOVE_EVENT_LISTENER](meta.eventName, eventTask.invoke,
166-
meta.useCapturing);
167-
}
168-
169-
function zoneAwareAddEventListener(self: any, args: any[]) {
170-
const eventName: string = args[0];
171-
const handler: EventListenerOrEventListenerObject = args[1];
172-
const useCapturing: boolean = args[2] || false;
173-
// - Inside a Web Worker, `this` is undefined, the context is `global`
174-
// - When `addEventListener` is called on the global context in strict mode, `this` is undefined
175-
// see https://github.com/angular/zone.js/issues/190
176-
const target = self || _global;
177-
let delegate: EventListener = null;
178-
if (typeof handler == 'function') {
179-
delegate = <EventListener>handler;
180-
} else if (handler && (<EventListenerObject>handler).handleEvent) {
181-
delegate = (event) => (<EventListenerObject>handler).handleEvent(event);
182-
}
183-
var validZoneHandler = false;
184-
try {
185-
// In cross site contexts (such as WebDriver frameworks like Selenium),
186-
// accessing the handler object here will cause an exception to be thrown which
187-
// will fail tests prematurely.
188-
validZoneHandler = handler && handler.toString() === "[object FunctionWrapper]";
189-
} catch(e) {
190-
// Returning nothing here is fine, because objects in a cross-site context are unusable
191-
return;
160+
function scheduleEventListener(eventTask: Task): any {
161+
const meta = <ListenerTaskMeta>eventTask.data;
162+
attachRegisteredEvent(meta.target, eventTask);
163+
return meta.target[addFnSymbol](meta.eventName, eventTask.invoke,
164+
meta.useCapturing);
192165
}
193-
// Ignore special listeners of IE11 & Edge dev tools, see https://github.com/angular/zone.js/issues/150
194-
if (!delegate || validZoneHandler) {
195-
return target[SYMBOL_ADD_EVENT_LISTENER](eventName, handler, useCapturing);
196-
}
197-
const eventTask: Task
198-
= findExistingRegisteredTask(target, handler, eventName, useCapturing, false);
199-
if (eventTask) {
200-
// we already registered, so this will have noop.
201-
return target[SYMBOL_ADD_EVENT_LISTENER](eventName, eventTask.invoke, useCapturing);
166+
167+
function cancelEventListener(eventTask: Task): void {
168+
const meta = <ListenerTaskMeta>eventTask.data;
169+
findExistingRegisteredTask(meta.target, eventTask.invoke, meta.eventName,
170+
meta.useCapturing, true);
171+
meta.target[removeFnSymbol](meta.eventName, eventTask.invoke,
172+
meta.useCapturing);
202173
}
203-
const zone: Zone = Zone.current;
204-
const source = target.constructor['name'] + '.addEventListener:' + eventName;
205-
const data: ListenerTaskMeta = {
206-
target: target,
207-
eventName: eventName,
208-
name: eventName,
209-
useCapturing: useCapturing,
210-
handler: handler
174+
175+
return function zoneAwareAddListener(self: any, args: any[]) {
176+
const eventName: string = args[0];
177+
const handler: EventListenerOrEventListenerObject = args[1];
178+
const useCapturing: boolean = args[2] || defaultUseCapturing;
179+
// - Inside a Web Worker, `this` is undefined, the context is `global`
180+
// - When `addEventListener` is called on the global context in strict mode, `this` is undefined
181+
// see https://github.com/angular/zone.js/issues/190
182+
const target = self || _global;
183+
let delegate: EventListener = null;
184+
if (typeof handler == 'function') {
185+
delegate = <EventListener>handler;
186+
} else if (handler && (<EventListenerObject>handler).handleEvent) {
187+
delegate = (event) => (<EventListenerObject>handler).handleEvent(event);
188+
}
189+
var validZoneHandler = false;
190+
try {
191+
// In cross site contexts (such as WebDriver frameworks like Selenium),
192+
// accessing the handler object here will cause an exception to be thrown which
193+
// will fail tests prematurely.
194+
validZoneHandler = handler && handler.toString() === "[object FunctionWrapper]";
195+
} catch(e) {
196+
// Returning nothing here is fine, because objects in a cross-site context are unusable
197+
return;
198+
}
199+
// Ignore special listeners of IE11 & Edge dev tools, see https://github.com/angular/zone.js/issues/150
200+
if (!delegate || validZoneHandler) {
201+
return target[addFnSymbol](eventName, handler, useCapturing);
202+
}
203+
204+
if (!allowDuplicates) {
205+
const eventTask: Task
206+
= findExistingRegisteredTask(target, handler, eventName, useCapturing, false);
207+
if (eventTask) {
208+
// we already registered, so this will have noop.
209+
return target[addFnSymbol](eventName, eventTask.invoke, useCapturing);
210+
}
211+
}
212+
213+
const zone: Zone = Zone.current;
214+
const source = target.constructor['name'] + '.' + addFnName + ':' + eventName;
215+
const data: ListenerTaskMeta = {
216+
target: target,
217+
eventName: eventName,
218+
name: eventName,
219+
useCapturing: useCapturing,
220+
handler: handler
221+
};
222+
zone.scheduleEventTask(source, delegate, data, scheduleEventListener, cancelEventListener);
211223
};
212-
zone.scheduleEventTask(source, delegate, data, scheduleEventListener, cancelEventListener);
213224
}
214225

215-
function zoneAwareRemoveEventListener(self: any, args: any[]) {
216-
const eventName: string = args[0];
217-
const handler: EventListenerOrEventListenerObject = args[1];
218-
const useCapturing: boolean = args[2] || false;
219-
// - Inside a Web Worker, `this` is undefined, the context is `global`
220-
// - When `addEventListener` is called on the global context in strict mode, `this` is undefined
221-
// see https://github.com/angular/zone.js/issues/190
222-
const target = self || _global;
223-
const eventTask = findExistingRegisteredTask(target, handler, eventName, useCapturing, true);
224-
if (eventTask) {
225-
eventTask.zone.cancelTask(eventTask);
226-
} else {
227-
target[SYMBOL_REMOVE_EVENT_LISTENER](eventName, handler, useCapturing);
226+
export function makeZoneAwareRemoveListener(fnName: string, useCapturingParam: boolean = true) {
227+
const symbol = zoneSymbol(fnName);
228+
const defaultUseCapturing = useCapturingParam ? false : undefined;
229+
230+
return function zoneAwareRemoveListener(self: any, args: any[]) {
231+
const eventName: string = args[0];
232+
const handler: EventListenerOrEventListenerObject = args[1];
233+
const useCapturing: boolean = args[2] || defaultUseCapturing;
234+
// - Inside a Web Worker, `this` is undefined, the context is `global`
235+
// - When `addEventListener` is called on the global context in strict mode, `this` is undefined
236+
// see https://github.com/angular/zone.js/issues/190
237+
const target = self || _global;
238+
const eventTask = findExistingRegisteredTask(target, handler, eventName, useCapturing, true);
239+
if (eventTask) {
240+
eventTask.zone.cancelTask(eventTask);
241+
} else {
242+
target[symbol](eventName, handler, useCapturing);
243+
}
244+
};
245+
}
246+
247+
export function makeZoneAwareListeners(fnName: string) {
248+
const symbol = zoneSymbol(fnName);
249+
250+
return function zoneAwareEventListeners(self: any, args: any[]) {
251+
const eventName: string = args[0];
252+
const target = self || _global;
253+
return target[EVENT_TASKS]
254+
.filter(task => task.data.eventName === eventName)
255+
.map(task => task.data.handler);
228256
}
229257
}
230258

259+
const zoneAwareAddEventListener = makeZoneAwareAddListener(ADD_EVENT_LISTENER, REMOVE_EVENT_LISTENER);
260+
const zoneAwareRemoveEventListener = makeZoneAwareRemoveListener(REMOVE_EVENT_LISTENER);
261+
231262
export function patchEventTargetMethods(obj: any): boolean {
232263
if (obj && obj.addEventListener) {
233264
patchMethod(obj, ADD_EVENT_LISTENER, () => zoneAwareAddEventListener);
@@ -236,7 +267,8 @@ export function patchEventTargetMethods(obj: any): boolean {
236267
} else {
237268
return false;
238269
}
239-
};
270+
}
271+
240272

241273
const originalInstanceKey = zoneSymbol('originalInstance');
242274

lib/node/events.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import {makeZoneAwareAddListener, makeZoneAwareListeners, makeZoneAwareRemoveListener, patchMethod} from '../common/utils';
2+
3+
4+
// For EventEmitter
5+
const EE_ADD_LISTENER = 'addListener';
6+
const EE_PREPEND_LISTENER = 'prependListener';
7+
const EE_REMOVE_LISTENER = 'removeListener';
8+
const EE_LISTENERS = 'listeners';
9+
const EE_ON = 'on';
10+
11+
12+
const zoneAwareAddListener = makeZoneAwareAddListener(EE_ADD_LISTENER, EE_REMOVE_LISTENER, false, true);
13+
const zoneAwarePrependListener = makeZoneAwareAddListener(EE_PREPEND_LISTENER, EE_REMOVE_LISTENER, false, true);
14+
const zoneAwareRemoveListener = makeZoneAwareRemoveListener(EE_REMOVE_LISTENER, false);
15+
const zoneAwareListeners = makeZoneAwareListeners(EE_LISTENERS);
16+
17+
export function patchEventEmitterMethods(obj: any): boolean {
18+
if (obj && obj.addListener) {
19+
patchMethod(obj, EE_ADD_LISTENER, () => zoneAwareAddListener);
20+
patchMethod(obj, EE_PREPEND_LISTENER, () => zoneAwarePrependListener);
21+
patchMethod(obj, EE_REMOVE_LISTENER, () => zoneAwareRemoveListener);
22+
patchMethod(obj, EE_LISTENERS, () => zoneAwareListeners);
23+
obj[EE_ON] = obj[EE_ADD_LISTENER];
24+
return true;
25+
} else {
26+
return false;
27+
}
28+
}
29+
30+
// EventEmitter
31+
let events;
32+
try {
33+
events = require('events');
34+
} catch (err) {}
35+
36+
if (events && events.EventEmitter) {
37+
patchEventEmitterMethods(events.EventEmitter.prototype);
38+
}

lib/node/node.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import '../zone';
22
import {patchTimer} from '../common/timers';
33

4+
import './events';
5+
46
const set = 'set';
57
const clear = 'clear';
68
const _global = typeof window === 'object' && window || typeof self === 'object' && self || global;
@@ -52,3 +54,21 @@ if (crypto) {
5254
}
5355
}.bind(crypto);
5456
}
57+
58+
// HTTP Client
59+
let httpClient;
60+
try {
61+
httpClient = require('_http_client');
62+
} catch (err) {}
63+
64+
if (httpClient && httpClient.ClientRequest) {
65+
let ClientRequest = httpClient.ClientRequest.bind(httpClient);
66+
httpClient.ClientRequest = function(options: any, callback?: Function) {
67+
if (!callback) {
68+
return new ClientRequest(options);
69+
} else {
70+
let zone = Zone.current;
71+
return new ClientRequest(options, zone.wrap(callback, 'http.ClientRequest'));
72+
}
73+
}
74+
}

test/node/events.spec.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import {EventEmitter} from 'events';
2+
3+
describe('nodejs EventEmitter', () => {
4+
let zone, zoneA, zoneB, emitter, expectZoneACount;
5+
beforeEach(() => {
6+
zone = Zone.current;
7+
zoneA = zone.fork({ name: 'A' });
8+
zoneB = zone.fork({ name: 'B' });
9+
10+
emitter = new EventEmitter();
11+
expectZoneACount = 0;
12+
});
13+
14+
function expectZoneA(value) {
15+
expectZoneACount++;
16+
expect(Zone.current).toBe(zoneA);
17+
expect(value).toBe('test value');
18+
}
19+
20+
function shouldNotRun() {
21+
fail('this listener should not run');
22+
}
23+
24+
it('should register listeners in the current zone', () => {
25+
zoneA.run(() => {
26+
emitter.on('test', expectZoneA);
27+
emitter.addListener('test', expectZoneA);
28+
});
29+
zoneB.run(() => emitter.emit('test', 'test value'));
30+
expect(expectZoneACount).toBe(2);
31+
});
32+
it('should remove listeners properly', () => {
33+
zoneA.run(() => {
34+
emitter.on('test', shouldNotRun);
35+
emitter.on('test2', shouldNotRun);
36+
emitter.removeListener('test', shouldNotRun);
37+
});
38+
zoneB.run(() => {
39+
emitter.removeListener('test2', shouldNotRun);
40+
emitter.emit('test', 'test value');
41+
emitter.emit('test2', 'test value');
42+
});
43+
});
44+
it('should return all listeners for an event', () => {
45+
zoneA.run(() => {
46+
emitter.on('test', expectZoneA);
47+
});
48+
zoneB.run(() => {
49+
emitter.on('test', shouldNotRun);
50+
});
51+
expect(emitter.listeners('test')).toEqual([expectZoneA, shouldNotRun]);
52+
});
53+
});

test/node_entry_point.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,4 @@ import './test-env-setup';
1717

1818
// List all tests here:
1919
import './common_tests';
20-
20+
import './node_tests';

test/node_tests.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import './node/events.spec';

0 commit comments

Comments
 (0)