View on GitHub

Good Job

A simple way to write complex asynchronous code.

Download this project as a .zip file Download this project as a tar.gz file

Good Job is a Node.js module that lets you glue interdependent asynchronous calls (Tasks) into complex Jobs. These Jobs can then be executed at once, converted to functions, extended and combined.

Good Job goes the extra mile by providing functions and options for error handling, Task logging and Job timeouts. Good Job makes it easier to find bugs, recover from unexpected situations and notice code that needs refactoring.

Although not tested, Good Job should run on any JavaScript platform that provides a synchronous require(module_name) function.

Quick Example

/// It makes sense to use a very short name for good-job variable.
/// "X" is used as a convention in this documentation.
var X =         require("good-job");

X.run({
    file_name:  "/etc/hosts",

    contents:   X.call("fs#readFile", X.get("file_name"), "base64")
                .onError(console.error),

    output:     X.callSync(console.log, X.get("contents")),
});

Download

Install using Node Package Manager (npm):

$ npm install good-job

Download from GitHub:

$ git clone git://github.com/emilis/good-job.git

Or get the zipped version here.

Documentation

All of the functions exported by good-job (except options()) create a new Job, Task or Request and call their own methods. You can then use method chaining to extend them:

/// Create a new Task, specify its callback and error handler:
X.wait("check_permission")
    .call(fs.readFile, X.get("file_name"))
    .onError(console.error, "Failed to read the file.");

/// Create a new Request that will transform its result:
X.get("file_contents")
    .apply(function(contents){
        return contents.toLowerCase();
    });

Exports and methods:

good-job exports Job Task Request
job, extend Job.extend
tasks Job.addTasks
set Job.set
createFunction Job.compile
run Job.run
wait Task.wait
call Task.call
callSync Task.callSync
subTasks Task.subTasks
get Request.get
getAll Request.getAll
options Job.clone
Job.setCallback
Job.setErrback
Job.options
Job.log
Job.logDone
Task.onError
Task.exitOnError
Task.compile
Request.map
Request.apply
Request.getValue
Request.toTask
Request.getDependencies

good-job

options(obj)

Utility function: wraps a given object as an Options object so that it won't be mixed up with a task list by a Job.

Arguments

Job

A Job manages asynchronous tasks: tracks their dependencies, executes them when possible, logs their errors. A Job is a dictionary with task names for keys and tasks (or their results) for values.

Job.extend(...)
extend(...)

Adds more tasks for the Job, sets callback or error handler and options. Returns the Job.

Arguments

Any number of the following:

Example

X.extend(
    X.options({
        id:     "MyJob",
        log:    true,
    }),
    {
        task1:  ...,
        task2:  ...,
    },
    function(err, result) {
        /// This will be called when the job finishes successfully.
        /// This would also be called if the job had no error handler.
    },
    function(err, result) {
        /// This will be called when a task emits an error.
    });

Job.addTasks(task_list)
tasks(task_list)

Adds more tasks for the Job. Returns the Job.

Arguments

Example

myJob.addTasks({
    file_name:  "/etc/hosts",

    contents:   X.call(fs.readFile, X.get("file_name"), "ascii"),

    print:      X.call(console.log, X.get("contents")),
});

Job.set(name, value)
set(name, value)

Add task or set value for the Job. Returns the Job.

Arguments

Example

var myJob = X.job();

myJob.set("file_name",  "/etc/hosts");
myJob.set("contents",   X.call("fs#readFile", X.get("file_name"), "ascii"));
myJob.set("print",      X.call(console.log, X.get("contents"))); 

myJob.run();

Job.compile(arg_names, ...)
createFunction(arg_names, ...)

Returns an asynchronous function that runs the Job with the given arguments used as finished task results.

Arguments

Example

Create a function that joins contents of two files:

/// You could use "job.compile(["f1_name","f2_name])" on an existing job.
var cat2files = X.createFunction(
    ["f1_name", "f2_name"],
    {
        f1_contents:    X.call(fs.readFile, X.get("f1_name"), "utf8"),
        f2_contents:    X.call(fs.readFile, X.get("f2_name"), "utf8"),
        cat:            X.get(["f1_contents","f2_contents"])
                        .apply(function(c1, c2){
                            return [c1, c2].join("\n");
                        }),
    });

/// We can now use the function. Note that it is asynchronous:
cat2files("/etc/hosts", "/etc/passwd", console.log);

Job.run(...)
run(...)

Executes the Job.

Arguments

Job.clone()

Returns a clone of the Job object. Needed when executing the Job more than once, because Jobs save their execution state. See also Job.compile.

Job.setCallback(cb)

Sets a function to call when the Job finishes. If the Job has an error handler, this function will only be called on success. Returns the Job.

Note that if a task uses Task.exitOnError, this Job callback won't be called.

Arguments

Job.setErrback(cb)

Sets a function to call when the Job finishes with error - a Task returns an error or a timeout occurs. Returns the Job.

Note that if a task uses Task.exitOnError, this Job error handler won't be called.

Job.options(obj)

Overwrites the current Job options. Returns the Job.

Supported Options

Example

X.run(
    X.options({
        id:         module.id,
        log:        true,
        timeout:    2000, // 2 seconds
    }),
    {
        t1: X.callSync(console.log, new Date()),

        t2: X.call(function(cb){
                /// This task will cause the Job to timeout:
                setTimeout(cb, 3000, null, "Long task result");
            }),

        t3: X.callSync(console.log, X.get("t2")), // This task will not run
    });

Job.log(msg_type, ...)

Prints a log message for the Job.

Arguments

Job.logDone(name, err, result)

Prints a log message for a Task or Job that is done.

Arguments

Task

A Task is a wrapper around a function that exposes its dependencies for the Job. It handles error results, catches exceptions.

Task.wait(...)
wait(...)

Explicitely adds dependencies for the Task. Returns the Task.

Arguments

Example

X.wait("t1", "t2", "t3")...

Task.call(fn, ...)
call(fn, ...)

Wraps an asynchronous function. Returns the Task.

Arguments

Example

/// Note that this only creates the Task and does not execute it:
X.call("fs#readFile", "/etc/hosts", "utf8");

Task.callSync(fn, ...)
callSync(fn, ...)

Wraps a synchronous function. Returns the Task.

Arguments

Example

/// Note that this only creates the Task and does not execute it:
X.callSync("fs#readFileSync", "/etc/hosts", "utf8");

Task.subTasks(result_map, tasks)
subTasks(result_map, tasks)

Adds a Job as a Task. Returns the Task.

Arguments

Example

X.run({
    file_name:  "/etc/hosts",
    file:       X.subTasks({"file_name":"name"},
                {
                    contents:   X.call("fs#readFile", X.get("name"), "utf8"),
                    stat:       X.call("fs#stat", X.get("name")),
                }),
    print:      X.callSync(console.log, X.get("file.name"), X.get("file.stat.ctime")),
});

Task.onError(fn, ...)

Sets an error handler for the Task. This error handler is executed when the Task returns an error or throws an exception. Returns the Task.

Job callback/error handler will be called after this function is called.

Arguments

Example

X.run({
    t1: X.call("fs#readFile", "does-not-exist", "ascii")
        .onError(console.error, "The file did not exist!"),
    },
    function(err, result) {
        console.log("Job result", err, result);
    });

Task.exitOnError(fn, ...)

Sets an error handler for the Task. This error handler is executed when the Task returns an error or throws an exception. Returns the Task.

Job callback/error handler will not be called if this function is called.

Arguments

Example

X.run({
    t1: X.call("fs#readFile", "does-not-exist", "ascii")
        .exitOnError(console.error, "The file did not exist!"),
    },
    function(err, result) {
        /// This will not be called if t1 fails.
        console.log("Job result", err, result);
    });

Task.compile()

Creates and returns a function from the Task. The function accepts callback as its first argument and an object holding requested dependencies as its second argument.

Example

var getFileContents = X.call("fs#readFile", X.get("file_name"), "utf8").compile();

getFileContents(console.log, {file_name:"/etc/hosts"});

Request

A Request specifies a Task dependency on a result from some other Task and provides ways to transform this result before using. Requests can also be used as Tasks in a Job.

Request.get(query)
get(query)

Specifies what value to extract from Job results (when it will be needed). Returns the Request.

Arguments

Example

X.get("env.SHELL").getValue(process);

Request.getAll()
getAll()

Used as a placeholder for all Job results. Returns the Request.

Example

X.getAll().getValue({a:42});

Request.map(fn)

Sets a map function to use when getting value from a result. Returns the Request.

Arguments

Example

/// This should print: [1, 4, 9 ] [1, 2, 3 ]
X.run({
    input:  [1,4,9],
    roots:  X.get("input").map(Math.sqrt),
    print:  X.callSync(console.log, X.get("input"), X.get("roots")),
});

Request.apply(fn)

Sets a filter function to use when getting value from results. Returns the Request.

Arguments

Example

/// These should print the commands to list your home directory:

X.get("HOME")
    .apply(function(home){
        return "ls " + home;
    })
    .getValue(process.env);

X.get(["SHELL","HOME"])
    .apply(function(shell, home){
        return shell+' -c "ls '+home+'"';
    })
    .getValue(process.env);

X.getAll()
    .apply(function(env){
        return "ls " + env.HOME;
    })
    .getValue(process.env);

Request.getValue(results)

Resolves this Request for the given Job results and returns the requested value.

Arguments

Example

X.get("a").getValue({a:42});

Request.toTask()

Converts the Request to a Task that just returns the requested value when possible. Returns the Task.

Example

/// See also Task.compile().
var returnAProperty = X.get("a").toTask().compile();
returnAProperty(console.log, {a:42});

Request.getDependencies()

Returns an Array of names of Tasks that this Request needs to finish before getting the value.

Example

X.get("a.b.c").getDependencies();

Other

Function URIs

Function URIs are Strings representing a function that can be loaded from some external module.

URIs consist of a path and a fragment separated by "#". "path#fragment" is used as require("path")["fragment"] when compiling a Task.

Task.call, Task.callSync, Task.onError, Task.exitOnError accept URIs in place of functions.

Example

/// These are equivalent:

var fs =    require("fs");
X.call(fs.readFile, "/etc/hosts", "utf8");

X.call("fs#readFile", "/etc/hosts", "utf8");

About Good Job

Copyright and License

Copyright 2012 UAB PriceOn http://www.priceon.lt/.

This is free software, and you are welcome to redistribute it under certain conditions; see LICENSE.txt for details.

Author

Emilis Dambauskas emilis@priceon.lt, http://emilis.github.com/.