forked from ReactiveX/rxjs
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for Zone.js in a way which should have minimal impact on performance. Ensures that all callbacks are invoked in the same `Zone` as the `Zone` which was current at the time of the callback registration with the `Rx`. - Add `getZone()` method which returs always `null` if no `Zone` is present or returrns the current zone. - All places where the `Zone` needs to be intered are quarded to only go through zone if the Zone is different from the current `Zone` for performance reasons. You can read up on Zones in [The Zone Primer](https://docs.google.com/document/d/1F5Ug0jcrm031vhSMJEOgp1l-Is-Vf0UCNDY-LsQtAIY). Before this change when Rx runs in the Zone environment it will propagate Zones according to VM Tasks. In the case of Rx this is not correct behavior because Rx does its own scheduling and the result is that all callbacks end up running in the same zone as Task zone. This behavior is mostly correct most of the time, but there are cases which result in wrong behavior which this change corrects. This change is needed to enable proper implementation of angular/angular#13248.
- Loading branch information
Showing
9 changed files
with
239 additions
and
16 deletions.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
import {expect} from 'chai'; | ||
import 'zone.js'; | ||
import {Subscriber, Observable} from '../dist/cjs/Rx'; | ||
|
||
/** | ||
* The point of these tests, is to ensure that all callbacks execute in the Zone which was active | ||
* when the callback was passed into the Rx. | ||
* | ||
* The implications are: | ||
* - Observable callback passed into `Observable` executes in the same Zone as when the | ||
* `new Observable` was invoked. | ||
* - The subscription callbacks passed into `subscribe` execute in the same Zone as when the | ||
* `subscribe` method was invoked. | ||
* - The operator callbacks passe into `map`, etc..., execute in the same Zone as when the | ||
* `operator` (`lift`) method was invoked. | ||
*/ | ||
describe('Zone interaction', () => { | ||
it('should run methods in the zone of declaration', () => { | ||
const log: string[] = []; | ||
const constructorZone: Zone = Zone.current.fork({ name: 'Constructor Zone'}); | ||
const subscriptionZone: Zone = Zone.current.fork({ name: 'Subscription Zone'}); | ||
let subscriber: Subscriber<string> = null; | ||
const observable = constructorZone.run(() => new Observable<string>((_subscriber) => { | ||
subscriber = _subscriber; | ||
log.push('setup'); | ||
expect(Zone.current.name).to.eq(constructorZone.name); | ||
return () => { | ||
expect(Zone.current.name).to.eq(constructorZone.name); | ||
log.push('cleanup'); | ||
}; | ||
})) as Observable<string>; | ||
subscriptionZone.run(() => observable.subscribe( | ||
() => { | ||
expect(Zone.current.name).to.eq(subscriptionZone.name); | ||
log.push('next'); | ||
}, | ||
() => null, | ||
() => { | ||
expect(Zone.current.name).to.eq(subscriptionZone.name); | ||
log.push('complete'); | ||
} | ||
)); | ||
subscriber.next('MyValue'); | ||
subscriber.complete(); | ||
|
||
expect(log).to.deep.equal(['setup', 'next', 'complete', 'cleanup']); | ||
log.length = 0; | ||
|
||
subscriptionZone.run(() => observable.subscribe( | ||
() => null, | ||
() => { | ||
expect(Zone.current.name).to.eq(subscriptionZone.name); | ||
log.push('error'); | ||
}, | ||
() => null | ||
)); | ||
subscriber.next('MyValue'); | ||
subscriber.error('MyError'); | ||
|
||
expect(log).to.deep.equal(['setup', 'error', 'cleanup']); | ||
}); | ||
|
||
it('should run methods in the zone of declaration when nexting synchronously', () => { | ||
const log: string[] = []; | ||
const rootZone: Zone = Zone.current; | ||
const constructorZone: Zone = Zone.current.fork({ name: 'Constructor Zone'}); | ||
const subscriptionZone: Zone = Zone.current.fork({ name: 'Subscription Zone'}); | ||
const observable = constructorZone.run(() => new Observable<string>((subscriber) => { | ||
// Execute the `next`/`complete` in different zone, and assert that correct zone | ||
// is restored. | ||
rootZone.run(() => { | ||
subscriber.next('MyValue'); | ||
subscriber.complete(); | ||
}); | ||
return () => { | ||
expect(Zone.current.name).to.eq(constructorZone.name); | ||
log.push('cleanup'); | ||
}; | ||
})) as Observable<string>; | ||
|
||
subscriptionZone.run(() => observable.subscribe( | ||
() => { | ||
expect(Zone.current.name).to.eq(subscriptionZone.name); | ||
log.push('next'); | ||
}, | ||
() => null, | ||
() => { | ||
expect(Zone.current.name).to.eq(subscriptionZone.name); | ||
log.push('complete'); | ||
} | ||
)); | ||
|
||
expect(log).to.deep.equal(['next', 'complete', 'cleanup']); | ||
}); | ||
|
||
it('should run operators in the zone of declaration', () => { | ||
const log: string[] = []; | ||
const rootZone: Zone = Zone.current; | ||
const constructorZone: Zone = Zone.current.fork({ name: 'Constructor Zone'}); | ||
const operatorZone: Zone = Zone.current.fork({ name: 'Operator Zone'}); | ||
const subscriptionZone: Zone = Zone.current.fork({ name: 'Subscription Zone'}); | ||
let observable = constructorZone.run(() => new Observable<string>((subscriber) => { | ||
// Execute the `next`/`complete` in different zone, and assert that correct zone | ||
// is restored. | ||
rootZone.run(() => { | ||
subscriber.next('MyValue'); | ||
subscriber.complete(); | ||
}); | ||
return () => { | ||
expect(Zone.current.name).to.eq(constructorZone.name); | ||
log.push('cleanup'); | ||
}; | ||
})) as Observable<string>; | ||
|
||
observable = operatorZone.run(() => observable.map((value) => { | ||
expect(Zone.current.name).to.eq(operatorZone.name); | ||
log.push('map: ' + value); | ||
return value; | ||
})) as Observable<string>; | ||
|
||
subscriptionZone.run(() => observable.subscribe( | ||
() => { | ||
expect(Zone.current.name).to.eq(subscriptionZone.name); | ||
log.push('next'); | ||
}, | ||
(e) => { | ||
expect(Zone.current.name).to.eq(subscriptionZone.name); | ||
log.push('error: ' + e); | ||
}, | ||
() => { | ||
expect(Zone.current.name).to.eq(subscriptionZone.name); | ||
log.push('complete'); | ||
} | ||
)); | ||
|
||
expect(log).to.deep.equal(['map: MyValue', 'next', 'complete', 'cleanup']); | ||
}); | ||
|
||
}); |
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
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,9 @@ | ||
/** | ||
* This function returns the current `Zone` if `Zone` is loaded or `null` if `Zone` is not loaded. | ||
* | ||
* It is expected that the VM will inline the `() => null` case when no `Zone` is present resulting | ||
* in no performance impact. | ||
*/ | ||
export const getZone: () => Zone = typeof Zone !== 'undefined' && Zone.current | ||
? () => Zone.current | ||
: () => null; |
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 |
---|---|---|
@@ -1,17 +1,26 @@ | ||
import { errorObject } from './errorObject'; | ||
import { getZone } from './getZone'; | ||
|
||
let tryCatchTarget: Function; | ||
let tryCatchZone: Zone; | ||
|
||
function tryCatcher(this: any): any { | ||
try { | ||
return tryCatchTarget.apply(this, arguments); | ||
return tryCatchZone && tryCatchZone != getZone() | ||
? tryCatchZone.run(tryCatchTarget, this, arguments as any) | ||
: tryCatchTarget.apply(this, arguments); | ||
} catch (e) { | ||
errorObject.e = e; | ||
return errorObject; | ||
} finally { | ||
// Cleanup to prevent unnecessarily holding onto memory. | ||
tryCatchZone = null; | ||
tryCatchTarget = null; | ||
} | ||
} | ||
|
||
export function tryCatch<T extends Function>(fn: T): T { | ||
export function tryCatch<T extends Function>(fn: T, zone?: Zone): T { | ||
tryCatchTarget = fn; | ||
tryCatchZone = zone; | ||
return <any>tryCatcher; | ||
}; |
Oops, something went wrong.