Well after some research I think it works kinda like this:
Suppose we are implementing zone.js. We implement the module like this:
function Zone(name, parent) {
this.name = name; this.parent = parent; this.localStorage = {},
this.fork = function (name) { return new Zone(name, this) },
this.run = function (f, fthis, args) {
var formerZone = currentZone
currentZone = this
f.apply(fthis, args)
currentZone = formerZone
}
}
//On initialization
var currentZone = new Zone("root", null)
module.exports.currentZone = function(){return currentZone}
And we use it like this:
const zone = require('./z.js')
//root zone
console.log(zone.currentZone())
//We create new zones that carry a reference to their parent
var zone2 = zone.currentZone().fork("newZone")
//We can run any function in a newly forked zone
zone2.run(() => {
zone.currentZone().localStorage.ctx = "custom context/zone related data"
console.log(zone.currentZone())
//Zone will not change back until we return from here so any nested call
//will still be in this zone
})
//Back to parent zone (root)
console.log(zone.currentZone())
Now we have an overview of what happens. But what if there are async calls? In that scenario JS VM may switch between zones before our .run() returns. Our module must track and trap any action that might change currentZone. One case is to patch Promises for example. Each time a Promise is created we place a trap code to catch the resolution/rejection of that promise. Our trap code switches currentZone to that Promise's zone whenever it is resolved/rejected. Zone.js patches various modules to track the zones.
This writing may not be accurate of course and may contain technical errors as there are some aspects of the matter which are not crystal clear in my head.