npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

subtask

v0.0.7-4

Published

A JavaScript class and design pattern to make async tasks clear and simple.

Downloads

31

Readme

subtask.js

A JavaScript class and design pattern to make async tasks clear and simple.

npm version Dependency Status Build Status Test Coverage Code Climate License

Features

  • Execute all child tasks in parallel.
  • Execute tasks sequentially, pipe previous output into next task.
  • Exception safe, auto delay exceptions after tasks done.
  • Cache the result naturally.

Why not...

  • Why not async.series()? Because we need to handle task output as another task input, it make callback functions access variables in another scope and mess up everything.
  • Why not async.parallel()? Because we like to put results with semantic naming under an object.
  • Why not extend async.*? Because we like all tasks can be defined and be executed in the same way... we need something like promise to ensure the interface is normalized.
  • Why not promise? Because we want to handle all success + failed cases in same place, promise.then() takes 2 callbacks.
  • Why not extends promise? Because the requirement is different, and we do not want to confuse developers.

How to Use

Define task

  • A task is created by task creator function; the function will take your input parameters then return the created task instance.
  • After the task is created, the final result should be always same because the input parameters were already put into the task.
  • Therefore, the logic inside a task will be executed only once and the result is kept by subtask.
var task = require('subtask'),

// multiply is a task creator to do sync jobs
// multiply(1, 2) is a created task instance
multiply = function (a, b) {
    return task(a * b);
},

// plus is a task creator to do async jobs
// plus(3, 4) is a created task instance
plus = function (a, b) {
    return task(function (cb) {
        mathApi.plus(a, b, function (value) {
            cb(value);
        });
    });
};

Execute task

  • When you run .execute() the first time, subtask will run the inner logic inside the task.
  • When you run .execute() many times, subtask will return the result of first execution.
  • If the task is async, all .execute() will wait for first result. Subtask will ensure the inner logic is be executed only once.
multiply(3, 5).execute(function (R) {
    console.log('3 * 5 = ' + R);
});

plus(4, 6).execute(function (R) {
    console.log('4 + 6 = ' + R);
});

plus(3, 5).execute(function (R) {
    console.log('3 * 5 = ' + R);
}).execute(function (R) {
    console.log('3 * 5 still = ' + R + ', mathApi.plus only be executed once');
});

Parallel subtasks

  • Use hash to define subtasks.
  • task.execute() will trigger all subtasks.execute() in parallel.
  • After all subtasks .execute() done , callback of task.execute() will be triggered.
  • Results of all subtasks .execute() will be collected into the hash.
var mathLogic = function (a, b) {
    return task({
        multiply: multiply(a, b),
        plus: puls(a, b),
        minus: minus(a, b)
    });
});

mathLogic(9, 8).execute(function (R) {
    // R will be {multiply: 72, plus: 17, minus: 1}
});

Pipe the tasks

  • Use the result of previous task as input of next task creator
var taskQueue = function (input) {
    return firstTask(input).pipe(secondTask).pipe(thirdTask);
});

taskQueue(123).execute(function (D) {
    // get result1 from firstTask(123).execute()
    // then get result2 from secondTask(result1).execute()
    // then get D from thirdTask(result2).execute()
});

Transform then pipe

  • Use .transform() to change the task result or pick wanted value
  • Use .pick('path.to.value') to pick wanted value
task1(123)
.transform(function (R) {
    return R * 2;
})
.pipe(task2)   // take result * 2 of task1 , send into task2 as input
.pipe(task3)   // take result of task2 , send into task3
.execute(function (D) {
    // now D is result of task3
});

// when .execute() we get the title of first story
// Same with task2(456).transform(function (R) {return R.story[0].title});
task2(456).pick('story.0.title');

Modify Task Creator

  • use subtask.after() to get a new task creator which updates the created task
// getProduct is a task creator to call product api
var getProduct = function (id) {
   return subtask(function (cb) {
       if (!id) { // input validation
           return cb();
       }
       request(apiUrl + id, function (err, res, body) {
           cb(body);
       });
   });
};

// renderProduct is a task creator for getProduct + .pipe(renderTask)
var renderProduct = subtask.after(getProduct, function (task) {
   return task.pipe(renderTask);
});
  • use subtask.before() to do extra logic before you create the task
// An example to apply cache logic on task creator
var cachedGetProduct = subtask.before(getProduct, function (task, args) {
    var T = cache.get(args[0]);

    // not in cache...create and store.
    if (!T) {
        T = task.apply(this, args);
        cache.set(id, T);
    }

    return T;
});

Error handling

  • Error in an async task will be auto delayed.
  • Error in a .execute() callback will be delayed.
  • Error in a .transform() callback will be delayed.
  • All delayed error will be throw later, only once.
  • If you pipe/transform/parallel execute tasks, all delayed error will be tracked by final/parent task.
  • To silently ignore these error, use .quiet()
var errorTask = subtask({
   good: 'OK!',
   correct: subtask('Yes'),
   badCallback: subtask().transform(function (D) {return D.a.b})
                                    // TypeError: Cannot read property 'a' of undefined
});

errorTask.execute(function (R) {
   // you will get {good: 'OK!', correct: 'Yes', badCallback: undefined} here
   // after this function, the delayed exception will be throw once
}).execute(function (R) {
   R.a.b.c = 10; // Error in .execute() callback will be delayed
}).execute(function (R) {
   // This callback function still works!
   // the previous exceptions will be throw later.
});

// Use task.quiet() or task.throwError = false to stop all exception.
anotherErrorTask.quiet().execute(function (R) {
   // still safe, and now exception will not be throw
   // access stored exception from this.errors
});

Good Practices

  • Return undefined means error in a task.
  • Use this.error(yourException) to throw delayed exception for specific error information
myTaskCreator = function () {
    return subtask(function (cb) {
        var thisTask = this;
        doSomeAsyncApiCall(function (err, D) {
            // error handling
            if (err) {
                thisTask.error(err);
                return cb();
            }
            // .... all others....
            cb(result);
        });
    });
};
  • Check input and output in your task creator.
  • Create an empty task when input error.
myTaskCreator = function (a) {
    // input validation
    if (isNotValid(a)) {
        return subtask();
    }

    // .... all others....
    return subtask(....);
};
  • Do not .quite() in your subtask modules.
  • Use .quite() as late as you can.

The Long Story

Serve a page

With Express, we do this:

app.get('/', function (req, res) {
   res.send('The page content...');
});

Modulize the page

We can make the page standalone, then we can mount the page to anywhere.

// The page
var somePage = function (req, res) {
    res.send('The page content...');
});


// Mount it
app.get('/some/where', somePage);

Modules in the page

We always do this for a page, right?

var somePage = function (req, res) {
    var header = getHeaderModule(),
        body = getStoryModule() + getRelatedStoryModule(),
        footer = getFooterModule();

    res.send(TemplateEngine(header, body, footer));
});

We should provide input for modules

But, how do we get the data? By the query parameters? We decide to make modules handle itself.

var somePage = function (req, res) {
    var header = getHeaderModule(req),
        body = getStoryModule(req) + getRelatedStoryModule(req),
        footer = getFooterModule(req);

    res.send(TemplateEngine(header, body, footer));
});

var someModule = function (req) {
    var id = req.params.id || defaultId,
        page = getPage(req),
        ....
});
  • ISSUE 1: many small pieces of code do similar tasks for input.
  • ISSUE 2: the real life of a page is async.

Everything should be Async

Yes, it's our real life.

var somePage = function (req, res) {
    getHeaderModule(req, function(header) {
        getStoryModule(req, function(body) {
            getFooterModule(req, function(footer) {
               res.send(TemplateEngine(header, body, footer));
            });
        });
    });
});

We can use promise to prevent callback hell.

Parallel is better

For performance, maybe we can get modules in parallel? For this we should change the interfaces of modules a bit, make then return a function.

var somePage = function (req, res) {
    async.parallel([
        getHeaderModule(req),
        getStoryModule(req),
        getFooterModule(req)
    ], function (R) {
        res.send(TemplateEngine(R[0], R[1], R[2]));
    });

Take care of Response

Maybe the getStoryModule wanna set cookie? So we should send req to all modules...

var somePage = function (req, res) {
    async.parallel([
        getHeaderModule(req, res),
        getStoryModule(req, res),
        getFooterModule(req, res)
    ], function (R) {
        res.send(TemplateEngine(R[0], R[1], R[2]));
    });

Use one Object

Stop appending input parameters, we use one object to handle all requirements.

var somePage = function (req, res) {
    var CX = {
        req: req,
        res: res
    };

    async.parallel([
        getHeaderModule(CX),
        getStoryModule(CX),
        getFooterModule(CX)
    ], function (R) {
        res.send(TemplateEngine(R[0], R[1], R[2]));
    });

How about Title?

Hmmm...In most case, the title is story title. Do it....

var somePage = function (CX) {
    async.parallel([
        getStoryTitle(CX),
        getHeaderModule(CX),
        getStoryModule(CX),
        getFooterModule(CX)
    ], function (R) {
        res.send(TemplateEngine(R[0], R[1], R[2], R[3]));
    });

Stop! R[0] to R[n] are bad! And, why we get the story two times? (one for the page title, another one for the story module) . Maybe we should reuse the fetched data and store it.

Namespace

We can store data in the context CX, and define good namespace rule. And we make a framework to handle modules and page. Now the code seems better:

framework.defindPage('somePage', function (CX) {
    CX.getData('storyTitle').then(function () {
        CX.getModule(CX, ['header', 'story', 'footer']).then(function () {
            CX.render('someTemplate', {
                title: CX.data.story.title,
                header: CX.module.header,
                body: CX.module.story,
                footer: CX.module.footer
            });
        });
    });
});

Namespace rule is hard to maintain:

  • CX.data.stories: a list of stories. Good for all pages.
  • CX.data.user.name: user name. Good for all pages.
  • CX.module.story: story module...What happened when I put 2 story modules in 1 page?!!!

We do not believe all developers in the team remember all naming rules.

Make it local

Stop using namespace, it is a 'global variable' solution under CX. We should use local variable.

framework.definePage('somePage', function (CX) {
    CX.executeJobs({
        title: CX.getData('storyTitle'),
        header: CX.getModule('header'),
        body: CX.getModule('story'),
        footer: CX.getModule('footer')
    }, function (data) {
       CX.render('someTemplate', data);
    });
});

subtask is created for this coding style.