Zone.js does a pretty good job of this. It's what Angular uses internally to track state across asynchronous calls but it does exist as a separate API. It works by patching a whole load of browser APIs (such as setTimeout, event subscription, etc.)
Here's an example piece of HTML showing it in use. It runs up a couple of Zones (thread contexts, basically) and does asynchronous work in each zone. You can see the current zone magically being passed around during the flow of asynchronous work.
<html>
<script src="https://unpkg.com/zone.js@0.11.5/bundles/zone.umd.js"></script>
<script>
const rootZone = Zone.current;
// Create two zones (thread contexts, basically)
const zone1 = rootZone.fork({
name: 'zone1',
properties: { pizzaInfo: { topping: 'pepperoni' } },
});
const zone2 = rootZone.fork({
name: 'zone2',
properties: { pizzaInfo: { topping: 'pineapple' } },
});
// A function which will trigger some asynchronous work
// Doesn't know anything about zones, but magically the
// work will run in whatever zone launched it.
function someWork() {
setTimeout(someAsyncWork, 1000);
}
// Some asynchronous work
function someAsyncWork() {
const initialZone = Zone.current;
const pizzaInfo = initialZone.get('pizzaInfo');
console.log(`someAsyncWork: in zone ${initialZone.name}`);
console.log(`someAsyncWork: best pizza topping for this zone is ${pizzaInfo.topping}`);
// Have some async fun with promises and timers
delay(500).then(yetMoreWork);
}
function yetMoreWork() {
const zone = Zone.current;
console.log(`yetMoreWork: in zone ${zone.name}`);
}
function delay(millis) {
return new Promise((resolve) => setTimeout(resolve, millis));
}
zone1.run(someWork);
zone2.run(someWork);
</script>
</html>
(Here's a JSFiddle for that code: https://jsfiddle.net/rbkxd512/)
There's a quite a lot for Zone.js to patch in a browser, so it's broken into modules which you can select from depending on whether you care about multimedia APIs, network APIs, etc.