node package manager

can-zone

can-zone-logo

can-zone

Build Status npm version

A library that tracks asynchronous activity and lets you know when it has completed. Useful when you need to call a function and wait for all async behavior to complete, such as when performing server-side rendering.

Install

npm install can-zone --save

Usage

var Zone = require("can-zone");
 
new Zone().run(function(){
 
    setTimeout(function(){
        
    }, 29);
 
    setTimeout(function(){
        
    }, 13);
 
    var xhr = new XMLHttpRequest();
    xhr.open("GET", "http://chat.donejs.com/api/messages");
    xhr.onload = function(){
        
    };
    xhr.send();
 
}).then(function(){
    // All done! 
});

Note: See the can-zone/register docs about ensuring can-zone is registered properly.

Tasks

JavaScript uses various task queues (and a microtask queue) to run JavaScript in the event loop. See this article and this StackOverflow answer to learn more.

For can-zone to work we have to override various task-creating functionality, this is the list of what we currently implement:

Macrotasks

  • setTimeout
  • XMLHttpRequest

Microtasks

  • requestAnimationFrame
  • Promise
  • process.nextTick

API

can-zone function

new Zone()

Creates a new Zone with no additional overrides. Can then call zone.run to call a function within the Zone.

var Zone = require("can-zone");
 
var zone = new Zone();
 
zone.run(function(){
 
    return "hello world";
 
}).then(function(data){
    data.result // -> "hello world" 
});

new Zone(zoneSpec)

Create a new Zone using the provided ZoneSpec to configure the Zone. The following examples configures a Zone that will time out after 5 seconds.

var Zone = require("can-zone");
 
var timeoutSpec = function(){
    var timeoutId;
 
    return {
        created: function(){
            timeoutId = setTimeout(function(){
                Zone.error(new Error("This took too long!"));
            }, 5000);
        },
        ended: function(){
            clearTimeout(timeoutId);
        }
    };
};
 
var zone = new Zone(timeoutSpec);
  1. zoneSpec {ZoneSpec|makeZoneSpec(data)}: A ZoneSpec object or a function that returns a ZoneSpec object.

These two are equivalent:

new Zone({
    created: function(){
        
    }
});
 
new Zone(function(){
    return {
        created: function(){
 
        }
    };
});

The latter form is useful so that you have a closure specific to that Zone.

Zone.waitFor(fn)

Zone.waitFor is a function that creates a callback that can be used with any async functionality. Calling Zone.waitFor registers a wait with the currently running request and returns a function that, when called, will decrement the wait count.

This is useful if there is async functionality other than what we implement. You might be using a library that has C++ bindings and doesn't go through the normal JavaScript async APIs.

var Zone = require("can-zone");
var fs = require("fs");
 
fs.readFile("data.json", "utf8", Zone.waitFor(function(){
    // We waited on this! 
}));
  1. fn {function}:

Zone.current

Represents the currently running zone. If the code using Zone.current is not running within a zone the value will be undefined.

var Zone = require("can-zone");
 
var myZone = new Zone();
 
myZone.run(function(){
 
    Zone.current === myZone;
 
});

Zone.ignore(fn)

Creates a function that, when called, will not track any calls. This might be needed if you are calling code that does unusual things, like using setTimeout recursively indefinitely.

var Zone = require("can-zone");
 
new Zone().run(function(){
    function recursive(){
        setTimeout(function(){
            recursive();
        }, 20000);
    }
 
    var fn = Zone.ignore(recursive);
 
    // This call will not be waited on. 
    fn();
});
  1. fn {function}: A function that contains calls to asynchronous functions that are needing to be ignored.
  • returns {function}: A function in which calls to addWait and removeWait will be ignored, preventing the Zone's promise from remaining unresolved while asynchronous activity continues within.

Zone.error(err)

Allows you to add an error to the currently running zone.

var Zone = require("can-zone");
 
new Zone().run(function(){
 
    setTimeout(function(){
        Zone.error(new Error("oh no"));
    }, 100);
 
}).then(null, function(error){
    error; // -> {message: "oh no"} 
});
  1. err {Error}:

zone.run(fn)

Runs a function within a Zone. Calling run will set the Zone's internal Promise which will only resolve once all asynchronous calls within fn are complete.

  1. fn {function}: Any function which needs to run within the Zone. The function will be executed immediately.
  • returns {Promise<data>}: Returns a promise that will resolve with the Zone's data object.

    var zone = new Zone();
     
    zone.run(function(){
     
        setTimeout(function(){
            zone.data.foo = "bar";
        });
     
    }).then(function(data){
        data.foo // -> "bar" 
    });

zone.data

You might want to get data back from can-zone, for example if you are using the library to track asynchronous rendering requests. Each zone contains a data object which can be used to store artibitrary values.

var Zone = require("can-zone");
 
var xhr = new XMLHttpRequest();
xhr.open("GET", "http://example.com");
xhr.onload = function(){
    // Save this data for later 
    Zone.current.data.xhr = xhr.responseText;
};
xhr.send();

zone.addWait()

Adds a wait to the Zone. Adding a wait will delay the Zone's Promise from resolving (the promise created by calling zone.run) by incrementing its internal counter.

Usually a corresponding removeWait will be called to decrement the counter.

new Zone().run(function(){
 
    var zone = Zone.current;
 
    zone.addWait(); // counter at 1 
    zone.removeWait(); // counter at 0, Promise resolves 
 
}).then(function(){
 
});

zone.removeWait()

Decrements the Zone's internal counter that is used to decide when its run Promise will resolve.

Usually used in conjuction with addWait. Most of the time you'll want to use waitFor, but in some cases where a callback is not enough to know waiting is complete, using addWait/removeWait gives you finer grained control.

var zone = new Zone();
 
var obj = new SomeObject();
 
// This is only done when the event.status is 3 
obj.onprogress = function(ev){
    if(ev.status === 3) {
        zone.removeWait();
    }
};
 
zone.addWait();

ZoneSpec {Object}

A ZoneSpec is the way you tap into the lifecycle hooks of a Zone. The hooks are described below.

Using these hooks you can do things like create timers and override global variables that will change the shape of code that runs within the Zone.

Object
  • created {function}:

    Called when the zone is first created, after all ZoneSpecs have been parsed. this is useful if you need to do setup behavior that covers the entire zone lifecycle.

    new Zone({
        created: function(){
            // Called as soon as `new Zone` is called 
        }
    });
  • beforeRun {function}:

    Called immediately before the Zone.prototype.run function is called.

    var zone = new Zone({
        beforeRun: function(){
            // Setup that needs to happen immediately before running 
            // the zone function 
        }
    });
     
    zone.run(function() { ... });
  • beforeTask {function}:

    Called before each Task is called. Use this to override any globals you want to exist during the execution of the task:

    new Zone({
        beforeTask: function(){
            window.setTimeout = mySpecialSetTimeout;
        }
    });
  • ended {function}:

    Called when the Zone has ended and is about to exit (it's Promise will resolve).

  • hooks {Array<string>}:

    hooks allows you to specify custom hooks that your plugin calls. This is mostly to communicate between plugins that inherit each other.

    var barZone = {
        created: function(){
            this.execHook("beforeBar");
        },
     
        hooks: ["beforeBar"]
    };
     
    var fooZone = {
        beforeBar: function(){
            // Called! 
        },
        plugins: [barZone]
    };
     
    new Zone({
        plugins: [fooZone]
    });
     
    zone.run(function() { ... });
  • plugins {Array<ZoneSpec|makeZoneSpec(data)>}:

    Allows specifying nested ZoneSpecs that the current depends on. This allows creating rich plugins that depend on other plugins (ZoneSpecs). You can imagine having a bunch of tiny plugins that do one thing and then composing them together into one meta-plugin that is more end-user friendly.

    Similar to the Zone constructor you can either specify ZoneSpec objects or functions that return ZoneSpec objects. The former gives you a closure specific to the Zone, which is often needed for variables. These two forms are equivalent:

    var specOne = {
        created: function(){
     
        }
    };
     
    var specTwo = function(){
        return {
            created: function(){
     
            }
        }
    };
     
    var zone = new Zone({
        plugins: [ specOne, specTwo ]
    });

makeZoneSpec {function([data](#zonedata))}

A function that returns a ZoneSpec object. This can be used any place where a ZoneSpec is accepted.

function(data)
  1. data {data}: The Zone's data object, useful when you want to append data to the Zone.

This examples wraps document.createElement to keep count of how many elements are created, and appends the count to data when the Zone ends.

var mySpec = function(data){
    var realCreateElement,
        count = 0;
 
    return {
        beforeTask: function(){
            realCreateElement = document.createElement;
            document.createElement = function(){
                count++;
                return realCreateElement.apply(this, arguments);
            };
        },
        afterTask: function(){
            document.createElement = realCreateElement;
        },
        ended: function(){
            data.elementsCreated = count;
        }
    };
};
 
var zone = new Zone(mySpec);
 
zone.run(function(){
    // Do stuff here 
})
.then(function(data){
    data.elementsCreated; // -> 5 
});

can-zone/register {function}

In order to do it's magic, can-zone has to register handlers for all of the common JavaScript async operations. If you have code (or a dependency with this code) that does:

var st = setTimeout;

And this module loads before can-zone, any time st is used we won't be able to track that within the Zone.

To work around this, can-zone/register is used as a script that you run before any other modules.

In Node

require("can-zone/register");

At the top of your entry-point script.

In the Browser

You can either add a script tag above all others:

<script src="node_modules/can-zone/register.js"></script>

Or, if you're using a module loader / bundler, configure it so that can-zone/register is placed above all others in the bundle.

function()
  • returns {undefined}:

can-zone/timeout function(ms)

timeout(ms)

Creates a ZoneSpec that you can use as a plugin for your Zone in order to timeout after a certain length of time (as ms).

If the Zone times out it's run promise will be rejected with a TimeoutError, a special error that also includes the number of milliseconds waited before timing out.

var Zone = require("can-zone");
var timeout = require("can-zone/timeout");
 
var zone = new Zone({
    plugins: [ timeout(5000) ]
});
 
zone.run(function(){
    setTimeout(function(){
 
    }, 10000); // waiting over 5 sec 
})
.catch(function(err){
    // Called because we exceeded the timeout. 
});
  1. ms {Number}: The number of milliseconds to wait before timing out the Zone.
  • returns {ZoneSpec}: A ZoneSpec that can be passed as a plugin.
TimeoutError {Error}

A special type of Error that also includes the number of milliseconds that were waited before timing out. The error object is included with the timeout module:

var timeout = require("can-zone/timeout");
 
var TimeoutError = timeout.TimeoutError;
// Maybe use this to check `instanceof`. 

####### Error

  • timeout {Number}: Specifies the timeout that was exceeded.

can-zone/debug function

debug(ms)

Creates a new ZoneSpec that can be provided to your Zone, timing out in ms (milliseconds).

var Zone = require("can-zone");
var debug = require("can-zone/debug");
 
var zone = new Zone({
    plugins: [debug(5000)]
})
.catch(function(err){
    var info = zone.data.debugInfo;
});

See the DebugInfo type for a list of properties

  1. ms {Number}: The timeout, in milliseconds, before the Zone will be rejected and debug information attached to the zone's data object.
debug(timeoutZone)

Like the previous signature, but directly pass it a timeout ZoneSpec object that you create yourself.

var debug = require("can-zone/debug");
var timeout = require("can-zone/timeout");
 
var timeoutZone = timeout(5000);
var debugZone = debug(timeoutZone):
 
...
  1. timeoutZone {can-zone/timeout}: A ZoneSpec created using the timeout plugin.
DebugInfo {Array\<Object\>}

An array of objects containing information useful for debugging. Gives you the name of the task that failed to complete and a stack trace of where the error occured. Each object has a shape of:

{
    "task": "setTimeout",
    "stack": Error ...."
}

####### Array<Object>

  • task {String}: An identifier of the task that failed to complete. This can be any of the asynchronous tasks supported by can-zone like setTimeout or Promise.

  • stack {String}: A stack trace taken as a snapshot when the task was called. This allows you t see the source of the call to help debug why the task never completed.

License

MIT