subtask.js
A JavaScript class and design pattern to make async tasks clear and simple.
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 = // multiply is a task creator to do sync jobs// multiply(1, 2) is a created task instance { return ;} // plus is a task creator to do async jobs// plus(3, 4) is a created task instance { return ;};
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.
; ; ;
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 { return ;}); ;
Pipe the tasks
- Use the result of previous task as input of next task creator
var { return ;}); ;
Transform then pipe
- Use .transform() to change the task result or pick wanted value
- Use .pick('path.to.value') to pick wanted value
// take result * 2 of task1 , send into task2 as input // take result of task2 , send into task3; // when .execute() we get the title of first story// Same with task2(456).transform(function (R) {return R.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 apivar { return ;}; // renderProduct is a task creator for getProduct + .pipe(renderTask)var renderProduct = subtask;
- use subtask.before() to do extra logic before you create the task
// An example to apply cache logic on task creatorvar cachedGetProduct = subtask;
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 = ; errorTask; // Use task.quiet() or task.throwError = false to stop all exception.anotherErrorTask;
Good Practices
- Return
undefined
means error in a task. - Use
this.error(yourException)
to throw delayed exception for specific error information
{ return ;};
- Check input and output in your task creator.
- Create an empty task when input error.
{ // input validation if return ; // .... all others.... return ;};
- 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;
Modulize the page
We can make the page standalone, then we can mount the page to anywhere.
// The pagevar { res;}); // Mount itapp;
Modules in the page
We always do this for a page, right?
var { var header = body = + footer = ; res;});
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 { var header = body = + footer = ; res;}); var { var id = reqparamsid || defaultId page = ...});
- 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 { ;});
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 { async;
Take care of Response
Maybe the getStoryModule
wanna set cookie? So we should send req
to all modules...
var { async;
Use one Object
Stop appending input parameters, we use one object to handle all requirements.
var { var CX = req: req res: res ; async;
How about Title?
Hmmm...In most case, the title is story title. Do it....
var { async;
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;
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;
subtask is created for this coding style.