├── .gitignore
├── .npmignore
├── .travis.yml
├── README.md
├── examples
├── chain.js
├── common_handle.js
├── done.js
├── error.js
├── fulfill.js
├── mix.js
├── parallel.js
├── result.js
├── run_mode.js
├── simple.js
└── step.js
├── index.js
├── lib
├── Step.js
├── Stepify.js
├── Task.js
└── util.js
├── package.json
└── test
├── step.js
└── task.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .git
2 | .DS_Store
3 | /node_modules
4 | npm-debug.log
5 | /test/test.js
6 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | examples/
2 | test/test.js
3 | lib/Step_bak.js
4 | lib/Stepify_bak.js
5 | Makefile
6 | .travis.yml
7 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "0.8"
4 | - "0.10"
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # stepify
2 | [](http://travis-ci.org/chemdemo/node-stepify)
3 | [](https://npmjs.org/package/stepify)
4 |
5 | stepify是一个简单易扩展的Node.js流程控制引擎,采用方法链(methods chain)的方式定制异步任务,使得Node.js工作流易于理解和维护。
6 |
7 | 目标是将复杂的任务进行拆分成多步完成,使得每一步的执行过程更加透明,化繁为简。
8 |
9 | **详细介绍请阅读[这篇文章](https://github.com/chemdemo/chemdemo.github.io/blob/master/blogs/stepify.md)**。
10 |
11 | ## stepify特点
12 |
13 | - 最基本的API的就3个:`step()`,`done()`,`run()`,简单容易理解。
14 |
15 | - 精细的粒度划分(同时支持单/多任务),执行顺序可定制化。
16 |
17 | - 每一个异步操作都经过特殊的封装,内部只需要关心这个异步的执行过程。
18 |
19 | - 链式(chain)调用,代码逻辑看起来比较清晰。
20 |
21 | - 灵活的回调函数定制和参数传递。
22 |
23 | - 统一处理单个异步操作的异常,也可根据需要单独处理某个任务的异常。
24 |
25 | ## 最简单的用法
26 |
27 | 简单实现基于oauth2授权获取用户基本资料的例子:
28 |
29 | ``` javascript
30 | // Authorizing based on oauth2 workflow
31 | Stepify()
32 | .step('getCode', function(appId, rUri) {
33 | var root = this;
34 | request.get('[authorize_uri]', function(err, res, body) {
35 | root.done(err, JSON.parse(body).code);
36 | });
37 | }, [appId], [redirectUri])
38 | .step('getToken', function(code) {
39 | var root = this;
40 | request.post('[token_uri]', function(err, res, body) {
41 | root.done(err, JSON.parse(body).access_token);
42 | });
43 | })
44 | .step('getInfo', function(token) {
45 | request.get('[info_uri]?token=' + token, function(err, res, body) {
46 | // got user info, pass it to client via http response
47 | });
48 | })
49 | .run();
50 | ```
51 |
52 | 多个step共用一个handle、静态参数、动态参数传递的例子:
53 |
54 | ``` javascript
55 | Stepify()
56 | .step('read', __filename)
57 | .step(function(buf) {
58 | // buf is the buffer content of __filename
59 | var root = this;
60 | var writed = 'test.js';
61 |
62 | // do more stuff with buf
63 | // this demo just replace all spaces simply
64 | buf = buf.toString().replace(/\s+/g, '');
65 | fs.writeFile(writed, buf, function(err) {
66 | // writed is the name of target file,
67 | // it will be passed into next step as the first argument
68 | root.done(err, writed);
69 | });
70 | })
71 | .step('read')
72 | // `read` here is a common handle stored in workflow
73 | .read(function(p, encoding) {
74 | fs.readFile(p, encoding || null, this.done.bind(this));
75 | })
76 | .run();
77 | ```
78 |
79 | 这里多了一个`read()`方法,但read方法并不是stepify内置的方法。实际上,您可以任意“扩展”stepify链!它的奥妙在于`step()`方法的参数,详细请看[step调用说明](#step)。
80 |
81 | 可以看到,一个复杂的异步操作,通过stepify定制,每一步都是那么清晰可读!
82 |
83 | ## 安装
84 |
85 | ``` javascript
86 | $ npm install stepify
87 | ```
88 |
89 | ## 运行测试
90 |
91 | ``` javascript
92 | $ npm install
93 | $ mocha
94 | ```
95 |
96 | ## 灵活使用
97 |
98 | ``` javascript
99 | var Stepify = require('stepify');
100 | var workflow1 = Stepify().step(fn).step(fn)...run();
101 | // or
102 | var workflow2 = new Stepify().step(fn).step(fn)...run();
103 | // or
104 | var workflow3 = Stepify().step(fn).step(fn);
105 | // do some stuff ...
106 | workflow3.run();
107 | // or
108 | var workflow4 = Stepify().task('foo').step(fn).step(fn).task('bar').step(fn).step(fn);
109 | // do some stuff ...
110 | workflow4.run(['foo', 'bar']);
111 | var workflow5 = Stepify().step(fn).step(fn);
112 | workflow5.debug = true;
113 | workflow5.error = function(err) {};
114 | workflow5.result = function(result) {};
115 | ...
116 | workflow5.run();
117 | // more ...
118 | ```
119 |
120 | 注:文档几乎所有的例子都是采用链式调用,但是拆开执行也是没有问题的。
121 |
122 | ## 原理
123 |
124 | 概念:
125 |
126 | - task:完成一件复杂的事情,可以把它拆分成一系列任务,这些个任务有可能它的执行需要依赖上一个任务的完成结果,它执行的同时也有可能可以和其他一些任务并行,串行并行相结合,这其实跟真实世界是很吻合的。
127 |
128 | - step:每一个task里边可再细分,可以理解成“一步一步完成一个任务(Finish a task step by step)”,正所谓“一步一个脚印”是也。
129 |
130 | stepify内部实际上有两个主要的类,一个是Stepify,一个是Step。
131 |
132 | `Stepify()`的调用会返回一个Stepify实例,在这里称之为workflow,用于调度所有task的执行。
133 |
134 | `step()`的调用会创建一个Step实例,用于完成具体的异步操作(当然也可以是同步操作,不过意义不大),step之间使用简单的api([done](#done)方法和[next](#next)方法)传递。
135 |
136 | ## API 文档
137 |
138 | ### Stepify类:
139 |
140 | 调用Stepify即可创建一个workflow。
141 |
142 | - [debug](#debug)
143 |
144 | - [task](#task)
145 |
146 | - [step](#step)
147 |
148 | - [pend](#pend)
149 |
150 | - *[stepName](#stepname)*
151 |
152 | - [error](#error)
153 |
154 | - [result](#result)
155 |
156 | - [run](#run)
157 |
158 | ### Step类:
159 |
160 | Step类只在Stepify实例调用step方法时创建,不必显式调用。
161 |
162 | - [done](#done)
163 |
164 | - [wrap](#wrap)
165 |
166 | - [fulfill](#fulfill)
167 |
168 | - [vars](#vars)
169 |
170 | - [parallel](#parallel)
171 |
172 | - [jump](#jump)
173 |
174 | - [next](#next)
175 |
176 | - [end](#end)
177 |
178 | ---
179 |
180 | #### debug()
181 |
182 | 描述:开启debug模式,打印一些log,方便开发。
183 |
184 | 调用:debug(flag)
185 |
186 | 参数:
187 |
188 | - {Boolean} flag 默认是false
189 |
190 | 例子:
191 |
192 | ``` javascript
193 | var work = Stepify().debug(true);
194 | // or
195 | var work = Stepify();
196 | work.debug = true;
197 | ```
198 |
199 | #### task()
200 |
201 | 描述:显式创建一个task,task()的调用是可选的。在新定制一个task时,如果没有显式调用task(),则这个task的第一个step()内部会先生成一个task,后续的step都是挂在这个task上面,每一个task内部会维持自己的step队列。多个task使用[pend](#pend)方法分割。
202 |
203 | 调用:task([taskName])
204 |
205 | 参数:
206 |
207 | - {String} taskName 可选参数,默认是`_UNAMED_TASK_[index]`。为这个task分配一个名字,如果有多个task实例并且执行顺序需要(使用run()方法)自定义,则设置下taskName方便一点。
208 |
209 | 例子:
210 |
211 | ``` javascript
212 | var myWork1 = Stepify().task('foo').step('step1').step('step2').run();
213 | // equal to
214 | var myWork1 = Stepify().step('step1').step('step2').run();
215 | // multiply tasks
216 | var myWork2 = Stepify()
217 | .task('foo')
218 | .step(fn)
219 | .step(fn)
220 | .task('bar')
221 | .step(fn)
222 | .step(fn)
223 | .task('baz')
224 | .step(fn)
225 | .step(fn)
226 | .run();
227 | ```
228 |
229 | #### step()
230 |
231 | 描述:定义当前task的一个异步操作,每一次step()调用都会实例化一个Step推入task的step队列。**这个方法是整个lib的核心所在。**
232 |
233 | 调用:step(stepName, stepHandle, *args)
234 |
235 | 参数:
236 |
237 | - {String} stepName 可选参数,但在不传stepHandle时是必传参数。为这个step分配一个名称。当stepHandle没有传入时,会在Stepify原型上扩展一个以stepName命名的方法,而它具体的实现则在调用stepName方法时决定,这个方法详情请看[*stepName说明*](#stepname)。
238 |
239 | - {Function} stepHandle 可选参数,但在stepName不传时是必传参数。在里边具体定义一个异步操作的过程。stepHandle的执行分两步,先查找这个step所属的task上有没有stepHandle,找不到则查找Stepify实例上有没有stepHandle,再没有就抛异常。
240 |
241 | - {Mix} *args 可选参数,表示这个step的已知参数(即静态参数),在stepHandle执行的时候会把静态参与动态参数(通过[done](#done)或者[next](#next)传入)合并作为stepHandle的最终参数。
242 |
243 | 例子:
244 |
245 | - 参数传递
246 |
247 | ``` javascript
248 | Stepify()
249 | .step(function() {
250 | var root = this;
251 | setTimeout(function() {
252 | // 这里done的第二个参数(100)即成为下一个stepHandle的动态参数
253 | root.done(null, 100);
254 | }, 100);
255 | })
256 | .step(function(start, n) {
257 | // start === 50
258 | // n === 100
259 | var root = this;
260 | setTimeout(function() {
261 | root.done();
262 | }, start + n);
263 | }, 50)
264 | .run();
265 | ```
266 |
267 | - 扩展原型链
268 |
269 | ``` javascript
270 | Stepify()
271 | .step('sleep')
272 | // more step ...
273 | .step('sleep', 50)
274 | .sleep(function(start, n) {
275 | var args = [].slice.call(arguments, 0);
276 | var root = this;
277 |
278 | n = args.length ? args.reduce(function(mem, arg) {return mem + arg;}) : 100;
279 | setTimeout(function() {
280 | root.done(null, n);
281 | }, n);
282 | })
283 | .run();
284 | ```
285 |
286 | #### pend()
287 |
288 | 描述:结束一个task的定义,会影响扩展到Stepify原型链上的stepName方法的执行。
289 |
290 | 调用:pend()
291 |
292 | 参数: 无参数。
293 |
294 | 例子:见*[stepName](#stepname)*部分
295 |
296 | #### *stepName()*
297 |
298 | 描述:这是一个虚拟方法,它是通过动态扩展Stepify类原型链实现的,具体调用的名称由step方法的`stepName`参数决定。扩展原型链的stepName的必要条件是step方法传了stepName(stepName需要是一个可以通过`.`访问属性的js变量)但是stepHandle没有传,且stepName在原型链上没定义过,当workflow执行结束之后会删除已经扩展到原型链上的所有方法。当调用实例上的stepName方法时,会检测此时有没有在定义的task(使用pend方法结束一个task的定义),如果有则把传入的handle挂到到这个task的handles池里,没有则挂到Stepify的handles池。
299 |
300 | 调用:stepName(stepHandle)
301 |
302 | 参数:
303 |
304 | - {Function} stepHandle 必传参数,定义stepName对应的stepHandle,可以在多个task之间共享。
305 |
306 | 例子:
307 |
308 | - pend()的影响
309 |
310 | ``` javascript
311 | Stepify()
312 | .step('mkdir', './foo')
313 | // 这样定义,在执行到sleep时会抛异常,
314 | // 因为这个task上面没定义过sleep的具体操作
315 | .step('sleep', 100)
316 | .pend()
317 | .step('sleep', 200)
318 | .step('mkdir', './bar')
319 | .sleep(function(n) {
320 | var root = this;
321 | setTimeout(function() {
322 | root.done();
323 | }, n);
324 | })
325 | // 这个pend的调用,使得mkdir方法传入的handle挂在了Stepify handles池中,
326 | // 所以第一个task调用mkdir方法不会抛异常
327 | .pend()
328 | .mkdir(function(p) {
329 | fs.mkdir(p, this.done.bind(this));
330 | })
331 | .run();
332 | ```
333 |
334 | - stepHandle的查找
335 |
336 | ``` javascript
337 | Stepify()
338 | .step('mkdir', './foo')
339 | // 定义当前task上的mkdirHandle,这里其实直接.step('mkdir', fn)更清晰
340 | .mkdir(function(p) {
341 | fs.mkdir(p, 0755, this.done.bind(this));
342 | })
343 | .step('sleep', 100)
344 | .pend()
345 | // 这个task上没定义mkdirHandle,会往Stepify类的handles池去找
346 | .step('mkdir', './bar')
347 | .step('sleep', 200)
348 | .pend()
349 | .sleep(function(n) {
350 | var root = this;
351 | setTimeout(function() {
352 | root.done();
353 | }, n);
354 | })
355 | .mkdir(function(p) {
356 | fs.mkdir(p, this.done.bind(this));
357 | })
358 | .run();
359 | ```
360 |
361 | #### error()
362 |
363 | 描述:定制task的异常处理函数。
364 |
365 | 调用:error(errorHandle)
366 |
367 | 参数:
368 |
369 | - {Function} errorHandle 必传参数 **默认会直接抛出异常并中断当前task的执行**。每一个task都可以定制自己的errorHandle,亦可为所有task定制errorHandle。每个step执行如果出错会直接进入这个errorHandle,后面是否继续执行取决于errorHandle内部定义。errorHandle第一个参数便是具体异常信息。
370 |
371 | 注意:errorHandle的执行环境是发生异常所在的那个step,也就是说Step类定义的所有方法在errorHandle内部均可用,您可以在异常时决定是否继续执行下一步,或者使用`this.taskName`和`this.name`分别访问所属task的名称和step的名称,进而得到更详细的异常信息。
372 |
373 | 例子:
374 |
375 | ``` javascript
376 | Stepify()
377 | .step(fn)
378 | .step(fn)
379 | // 这个task的异常会走到这里
380 | .error(function(err) {
381 | console.error('Error occurs when running task %s\'s %s step!', this.taskName, this.name);
382 | if(err.message.match(/can_ignore/)) {
383 | // 继续执行下一步
384 | this.next();
385 | } else {
386 | throw err;
387 | }
388 | })
389 | .pend()
390 | .step(fn)
391 | .step(fn)
392 | .pend()
393 | // 所有没显式定义errorHandle的所有task异常都会走到这里
394 | .error(function(err) {
395 | console.error(err.stack);
396 | res.send(500, 'Server error!');
397 | })
398 | .run();
399 | ```
400 |
401 | #### result()
402 |
403 | 描述:所有task执行完之后,输出结果。在Stepify内部,会保存一份结果数组,通过step的[fulfill方法](#fulfill)可以将结果push到这个数组里,result执行的时候将这个数组传入finishHandle。
404 |
405 | 调用:result(finishHandle)
406 |
407 | 参数:
408 |
409 | - {Function} finishHandle,result本身是可选调用的,如果调用了result,则finishHandle是必传参数。
410 |
411 | 例子:
412 |
413 | ``` javascript
414 | Stepify()
415 | .step(function() {
416 | var root = this;
417 | setTimeout(function() {
418 | root.fulfill(100);
419 | root.done(null);
420 | }, 100);
421 | })
422 | .step(function() {
423 | var root = this;
424 | fs.readFile(__filename, function(err, buf) {
425 | if(err) return root.done(err);
426 | root.fulfill(buf.toString());
427 | root.done();
428 | });
429 | })
430 | .result(function(r) {
431 | console.log(r); // [100, fs.readFileSync(__filename).toString()]
432 | })
433 | .run();
434 | ```
435 |
436 | #### run()
437 |
438 | 描述:开始执行所定制的整个workflow。这里比较灵活,执行顺序可自行定制,甚至可以定义一个workflow,分多种模式执行。
439 |
440 | 调用:run(*args)
441 |
442 | 参数:
443 |
444 | - {Mix} 可选参数,类型可以是字符串(taskName)、数字(task定义的顺序,从0开始)、数组(指定哪些tasks可以并行),也可以混合起来使用(数组不支持嵌套)。默认是按照定义的顺序串行执行所有tasks。
445 |
446 | 例子:
447 |
448 | ``` javascript
449 | function createTask() {
450 | return Stepify()
451 | .task('task1')
452 | .step(function() {
453 | c1++;
454 | fs.readdir(__dirname, this.wrap());
455 | })
456 | .step('sleep')
457 | .step('exec', 'cat', __filename)
458 | .task('task2')
459 | .step('sleep')
460 | .step(function() {
461 | c2++;
462 | var root = this;
463 | setTimeout(function() {
464 | root.done(null);
465 | }, 1500);
466 | })
467 | .step('exec', 'ls', '-l')
468 | .task('task3')
469 | .step('readFile', __filename)
470 | .step('timer', function() {
471 | c3++;
472 | var root = this;
473 | setTimeout(function() {
474 | root.done();
475 | }, 1000);
476 | })
477 | .step('sleep')
478 | .readFile(function(p) {
479 | fs.readFile(p, this.done.bind(this));
480 | })
481 | .task('task4')
482 | .step('sleep')
483 | .step(function(p) {
484 | c4++;
485 | fs.readFile(p, this.wrap());
486 | }, __filename)
487 | .pend()
488 | .sleep(function() {
489 | console.log('Task %s sleep.', this.taskName);
490 | var root = this;
491 | setTimeout(function() {
492 | root.done(null);
493 | }, 2000);
494 | })
495 | .exec(function(cmd, args) {
496 | cmd = [].slice.call(arguments, 0);
497 | var root = this;
498 | exec(cmd.join(' '), this.wrap());
499 | });
500 | };
501 |
502 | var modes = {
503 | 'Default(serial)': [], // 10621 ms.
504 | 'Customized-serial': ['task1', 'task3', 'task4', 'task2'], // 10624 ms.
505 | 'Serial-mix-parallel-1': ['task1', ['task3', 'task4'], 'task2'], // 8622 ms.
506 | 'Serial-mix-parallel-2': [['task1', 'task3', 'task4'], 'task2'], // 6570 ms.
507 | 'Serial-mix-parallel-3': [['task1', 'task3'], ['task4', 'task2']], // 6576 ms.
508 | 'All-parallel': [['task1', 'task3', 'task4', 'task2']], // 3552 ms.
509 | 'Part-of': ['task2', 'task4'] // 5526 ms.
510 | };
511 |
512 | var test = function() {
513 | var t = Date.now();
514 | var task;
515 |
516 | Object.keys(modes).forEach(function(mode) {
517 | task = createTask();
518 |
519 | task.result = function() {
520 | console.log(mode + ' mode finished and took %d ms.', Date.now() - t);
521 | };
522 |
523 | task.run.apply(task, modes[mode]);
524 | });
525 |
526 | setTimeout(function() {
527 | log(c1, c2, c3 ,c4); // [6, 7, 6, 7]
528 | }, 15000);
529 | };
530 |
531 | test();
532 | ```
533 |
534 | ---------
535 |
536 | #### done()
537 |
538 | 描述:标识完成了一个异步操作(step)。
539 |
540 | 调用:done([err, callback, *args])
541 |
542 | 参数:
543 |
544 | - {String|Error|null} err 错误描述或Error对象实例。参数遵循Node.js的回调约定,可以不传参数,如果需要传递参数,则第一个参数必须是error对象。
545 |
546 | - {Function} callback 可选参数 自定义回调函数,默认是next,即执行下一步。
547 |
548 | - {Mix} *args 这个参数是传递给callback的参数,也就是作为下一步的动态参数。一般来说是将这一步的执行结果传递给下一步。
549 |
550 | 例子:
551 |
552 | ``` javascript
553 | Stepify()
554 | .step(function() {
555 | var root = this;
556 | setTimeout(function() {
557 | root.done();
558 | }, 200);
559 | })
560 | .step(function() {
561 | var root = this;
562 | exec('curl "https://github.com/"', function(err, res) {
563 | // end this task in error occured
564 | if(err) root.end();
565 | else root.done(null, res);
566 | });
567 | })
568 | .step(function(res) {
569 | var root = this;
570 | setTimeout(function() {
571 | // do some stuff with res ...
572 | console.log(res);
573 | root.done();
574 | }, 100);
575 | })
576 | .run();
577 | ```
578 |
579 | #### wrap()
580 |
581 | 描述:其实就是`this.done.bind(this)`的简写,包装done函数保证它的执行环境是当前step。比如原生的`fs.readFile()`的callback的执行环境被设置为null[fs.js#L91](https://github.com/joyent/node/blob/master/lib/fs.js#L91)。
582 |
583 | 调用:wrap()
584 |
585 | 参数:无
586 |
587 | 例子:
588 |
589 | ``` javascript
590 | Stepify()
591 | .step(function() {
592 | fs.readdir(__dirname, this.done.bind(this));
593 | })
594 | .step(function() {
595 | fs.readFile(__filename, this.wrap());
596 | })
597 | .run();
598 | ```
599 |
600 | #### fulfill()
601 |
602 | 描述:把step执行的结果推入结果队列,最终传入finishHandle。最终结果数组的元素顺序在传入给finishHandle时不做任何修改。
603 |
604 | 调用:fulfill(*args)
605 |
606 | 参数:
607 |
608 | - {Mix} 可选参数 可以是一个或者多个参数,会一一push到结果队列。
609 |
610 | 例子:
611 |
612 | ``` javascript
613 | // Assuming retrieving user info
614 | Stepify()
615 | .step(function() {
616 | var root = this;
617 | db.getBasic(function(err, basic) {
618 | root.fulfill(basic || null);
619 | root.done(err, basic.id);
620 | });
621 | })
622 | .step(function(id) {
623 | var root = this;
624 | db.getDetail(id, function(err, detail) {
625 | root.fulfill(detail || null);
626 | root.done(err);
627 | });
628 | })
629 | .error(function(err) {
630 | console.error(err);
631 | res.send(500, 'Get user info error.');
632 | })
633 | .result(function(r) {
634 | res.render('user', {basic: r[0], detail: r[1]});
635 | })
636 | .run();
637 | ```
638 |
639 | #### vars()
640 |
641 | 描述:暂存临时变量,在整个workflow的运行期可用。如果不想在workflow之外使用`var`申明别的变量,可以考虑用vars()。
642 |
643 | 调用:vars(key[, value])
644 |
645 | 参数:
646 |
647 | - {String} key 变量名。访问临时变量。
648 |
649 | - {Mix} value 变量值。如果只传入key则是访问变量,如果传入两个值则是写入变量并返回这个value。
650 |
651 | 例子:
652 |
653 | ``` javascript
654 | Stepify()
655 | .step(function() {
656 | this.vars('foo', 'bar');
657 | // todo
658 | })
659 | .pend()
660 | .step(function() {
661 | // todo
662 | console.log(this.vars('foo')); // bar
663 | })
664 | .run();
665 | ```
666 |
667 | #### parallel()
668 |
669 | 描述:简单的并发支持。*这里还可以考虑引用其他模块(如:[async](https://github.com/caolan/async))完成并行任务。*
670 |
671 | 调用:parallel(arr[, iterator, *args, callback])
672 |
673 | 参数:
674 |
675 | - {Array} arr 必传参数。需要并行执行的一个数组,对于数组元素只有一个要求,就是如果有函数则所有元素都必须是一个函数。
676 |
677 | - {Function} iterator 如果arr参数是一个函数数组,这个参数是不用传的,否则是必传参数,它迭代运行arr的每一个元素。iterator的第一个参数是arr中的某一个元素,第二个参数是回调函数(`callback`),当异步执行完之后需要调用`callback(err, data)`。
678 |
679 | - {Mix} \*args 传递给iterator的参数,在迭代器执行的时候,arr数组的每一个元素作为iterator的第一个参数,\*args则作为剩下的传入。
680 |
681 | - {Function} callback 可选参数(约定当最后一个参数是函数时认为它是回调函数) 默认是next。这个并行任务的执行结果会作为一个数组按arr中定义的顺序传入callback,如果执行遇到错误,则直接交给errHandle处理。
682 |
683 | 例子:
684 |
685 | 传入一个非函数数组(parallel(arr, iterator[, *arg, callback]))
686 |
687 | ``` javascript
688 | Stepify()
689 | .step(function() {
690 | fs.readdir(path.resolve('./test'), this.wrap());
691 | })
692 | .step(function(list) {
693 | list = list.filter(function(p) {return path.extname(p).match('js');});
694 | list.forEach(function(file, i) {list[i] = path.resolve('./test', file);});
695 | // 注释部分就相当于默认的this.next
696 | this.parallel(list, fs.readFile, {encoding: 'utf8'}/*, function(bufArr) {this.next(bufArr);}*/);
697 | })
698 | .step(function(bufArr) {
699 | // fs.writeFile('./combiled.js', Buffer.concat(bufArr), this.done.bind(this));
700 | // or
701 | this.parallel(bufArr, fs.writeFile.bind(this, './combiled.js'));
702 | })
703 | .run();
704 | ```
705 |
706 | 传入函数数组(parallel(fnArr[]))
707 |
708 | ``` javascript
709 | Stepify()
710 | .step(function() {
711 | this.parallel([
712 | function(callback) {
713 | fs.readFile(__filename, callback);
714 | },
715 | function(callback) {
716 | setTimeout(function() {
717 | callback(null, 'some string...');
718 | }, 500);
719 | }
720 | ]);
721 | })
722 | .step(function(list) {
723 | console.log(list); // [fs.readFileSync(__filename), 'some string...']
724 | // todo...
725 | })
726 | .run();
727 | ```
728 |
729 | 下面是一个应用到某项目里的例子:
730 |
731 | ``` javascript
732 | ...
733 | .step('fetch-images', function() {
734 | var root = this;
735 | var localQ = [];
736 | var remoteQ = [];
737 |
738 | // do dome stuff to get localQ and remoteQ
739 |
740 | this.parallel([
741 | function(callback) {
742 | root.parallel(localQ, function(frameData, cb) {
743 | // ...
744 | db.getContentType(frameData.fileName, function(type) {
745 | var imgPath = frameData.fileName + '.' + type;
746 | // ...
747 | db.fetchByFileName(imgPath).pipe(fs.createWriteStream(targetUrl));
748 | cb(null);
749 | });
750 | }, function(r) {callback(null, r);});
751 | },
752 | function(callback) {
753 | root.parallel(remoteQ, function(frameData, cb) {
754 | var prop = frames[frameData['frame']].children[frameData['elem']]['property'];
755 | // ...
756 | request({url: prop.src}, function(err, res, body) {
757 | // ...
758 | cb(null);
759 | }).pipe(fs.createWriteStream(targetUrl));
760 | }, function(r) {callback(null, r);});
761 | },
762 | ]);
763 | })
764 | ...
765 | ```
766 |
767 | #### jump()
768 |
769 | 描述:在step之间跳转。**这样会打乱step的执行顺序,谨慎使用jump,以免导致死循环**。
770 |
771 | 调用:jump(index|stepName[, args])
772 |
773 | 参数:
774 |
775 | - {Number} index 要跳转的step索引。在step创建的时候会自建一个索引属性,使用`this._index`可以访问它。
776 |
777 | - {String} stepName step创建时传入的名称。
778 |
779 | 例子:
780 |
781 | ``` javascript
782 | Stepify()
783 | .step('a', fn)
784 | .step('b', fn)
785 | .step(function() {
786 | if(!this.vars('flag')) {
787 | this.jump('a');
788 | this.vars('flag', 1)
789 | } else {
790 | this.next();
791 | }
792 |
793 | // do some stuff ...
794 | })
795 | .step('c', fn)
796 | .run();
797 | ```
798 |
799 | #### next()
800 |
801 | 描述:显式调用下一个step,并将数据传给下一step(即下一个step的动态参数)。其实等同于done(null, *args)。
802 |
803 | 调用:next([*args])
804 |
805 | 参数:
806 |
807 | - {Mix} *args 可选参数 类型不限,数量不限。
808 |
809 | 例子:
810 |
811 | ``` javascript
812 | Stepify()
813 | .step(function() {
814 | // do some stuff ...
815 | this.next('foo', 'bar');
816 | })
817 | .step(function(a, b, c) {
818 | a.should.equal('test');
819 | b.should.equal('foo');
820 | c.should.equal('bar');
821 | }, 'test')
822 | .run();
823 | ```
824 |
825 | #### end()
826 |
827 | 描述:终止当前task的执行。如果遇到异常并传递给end,则直接交给errorHandle,和done一样。不传或者传null则跳出所在task执行下一个task,没有则走到result,没有定义result则退出进程。
828 |
829 | 调用:end(err)
830 |
831 | 参数:
832 |
833 | - {Error|null} err 可选参数, 默认null。
834 |
835 | 例子:
836 |
837 | ``` javascript
838 | Stepify()
839 | .step(fn)
840 | .step(function() {
841 | if(Math.random() > 0.5) {
842 | this.end();
843 | } else {
844 | // todo ...
845 | }
846 | })
847 | .step(fn)
848 | .run();
849 | ```
850 |
851 | ---
852 |
853 | 最后,欢迎fork或者[提交bug](https://github.com/chemdemo/node-stepify/issues)。
854 |
855 | ## License
856 |
857 | MIT [http://rem.mit-license.org](http://rem.mit-license.org)
858 |
--------------------------------------------------------------------------------
/examples/chain.js:
--------------------------------------------------------------------------------
1 | var Stepify = require('../index');
2 | var fs = require('fs');
3 |
4 | // Stepify()
5 | // .step('mkdir', './foo')
6 | // // 这样定义,在执行到sleep时会抛异常,
7 | // // 因为这个task上面没定义过sleep的具体操作
8 | // .step('sleep', 100)
9 | // .pend()
10 | // .step('sleep', 200)
11 | // .step('mkdir', './bar')
12 | // .sleep(function(n) {
13 | // var root = this;
14 | // setTimeout(function() {
15 | // root.done();
16 | // }, n);
17 | // })
18 | // // 这个pend的调用,使得mkdir方法传入的handle挂在了Stepify handles队列中,
19 | // // 所以第一个task调用mkdir方法不会抛异常
20 | // .pend()
21 | // .mkdir(function(p) {
22 | // fs.mkdir(p, this.done.bind(this));
23 | // })
24 | // .run();
25 |
26 | Stepify()
27 | .step('mkdir', './foo')
28 | // 定义当前task上的mkdirHandle,这里其实直接.step('mkdir', fn)更清晰
29 | .mkdir(function(p) {
30 | fs.mkdir(p, 0755, this.done.bind(this));
31 | })
32 | .step('sleep', 100)
33 | .pend()
34 | // 这个task上没定义mkdirHandle,会往Stepify类的handles池去找
35 | .step('mkdir', './bar')
36 | .step('sleep', 200)
37 | .pend()
38 | .sleep(function(n) {
39 | var root = this;
40 | setTimeout(function() {
41 | root.done();
42 | }, n);
43 | })
44 | .mkdir(function(p) {
45 | fs.mkdir(p, this.done.bind(this));
46 | })
47 | .run();
48 |
--------------------------------------------------------------------------------
/examples/common_handle.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs');
2 | var Stepify = require('../index');
3 |
4 | // 单任务
5 | Stepify()
6 | .step('read', __filename)
7 | .step(function(buf) {
8 | // buf就是当前文档的内容
9 | var root = this;
10 | var writed = 'test.js';
11 |
12 | // 对buffer做更多的操作
13 | // 这里简单把所有空格去掉
14 | buf = buf.toString().replace(/\s+/g, '');
15 | fs.writeFile(writed, buf, function(err) {
16 | // writed就是写入的文件名,它将作为第一个动态参数传入下一步的函数体,即下一步rread
17 | root.done(err, writed);
18 | });
19 | })
20 | .step('read')
21 | // 这里read是一个公共的方法,读取文件内容,可传入不同的path参数
22 | .read(function(p, encoding) {
23 | fs.readFile(p, encoding || null, this.done.bind(this));
24 | })
25 | .run();
26 |
--------------------------------------------------------------------------------
/examples/done.js:
--------------------------------------------------------------------------------
1 | var Stepify = require('../index');
2 | var exec = require('child_process');
3 |
4 | Stepify()
5 | .step(function() {
6 | var root = this;
7 | setTimeout(function() {
8 | root.done();
9 | }, 200);
10 | })
11 | .step(function() {
12 | var root = this;
13 | exec('curl "https://github.com/"', function(err, res) {
14 | // end this task in error occured
15 | if(err) root.end();
16 | else root.done(null, res);
17 | });
18 | })
19 | .step(function(res) {
20 | var root = this;
21 | setTimeout(function() {
22 | // do some stuff with res ...
23 | console.log(res);
24 | root.done();
25 | }, 100);
26 | })
27 | .run();
28 |
--------------------------------------------------------------------------------
/examples/error.js:
--------------------------------------------------------------------------------
1 | var Stepify = require('../index');
2 |
3 | Stepify()
4 | .step(fn)
5 | .step(fn)
6 | // 这个task的异常会走到这里
7 | .error(handle)
8 | .pend()
9 | .step(fn)
10 | .step(fn)
11 | .pend()
12 | // 所有没显式定义errorHandle的所有task异常都会走到这里
13 | .error(handle)
14 | .run();
15 |
--------------------------------------------------------------------------------
/examples/fulfill.js:
--------------------------------------------------------------------------------
1 | var Stepify = require('../index');
2 |
3 | // Assuming retrieving user info
4 | Stepify()
5 | .step(function() {
6 | var root = this;
7 | db.getBasic(function(err, basic) {
8 | root.fulfill(basic || null);
9 | root.done(err, basic.id);
10 | });
11 | })
12 | .step(function(id) {
13 | var root = this;
14 | db.getDetail(id, function(err, detail) {
15 | root.fulfill(detail || null);
16 | root.done(err);
17 | });
18 | })
19 | .error(function(err) {
20 | console.error(err);
21 | res.send(500, 'Get user info error.');
22 | })
23 | .result(function(r) {
24 | res.render('user', {basic: r[0], detail: r[1]});
25 | })
26 | .run();
27 |
--------------------------------------------------------------------------------
/examples/mix.js:
--------------------------------------------------------------------------------
1 | var Stepify = require('../index');
2 |
3 | var Stepify = Task()
4 | // .debug(true)
5 | .task('foo')
6 | .step(function() {
7 | var root = this;
8 | // setTimeout(function() {
9 | // root.done(null, 'first');
10 | // }, 1000);
11 | require('fs').readdir(__dirname, this.done.bind(this));
12 | }, 10)
13 | .step('bar', 1, 2)
14 | .bar(function(x, y) {
15 | console.log([].slice.call(arguments, 0)) // [1, 2, 'first']
16 | var root = this;
17 | setTimeout(function() {
18 | // console.log('index:', root._index);
19 | root.vars('num', 100);
20 | root.done(null, 'second');
21 | }, 500);
22 | })
23 | .step('x', function() {
24 | console.log([].slice.call(arguments, 0)) // ['second']
25 | var root = this;
26 | setTimeout(function() {
27 | console.log(root.vars('num')); // 100
28 | // return root.done('err1~~~~');
29 | return root.done(null, 'haha');
30 | root.done(Math.random() > 0.5 ? null: 'Error!!!');
31 | }, 1000);
32 | })
33 | .step('baz', function() {
34 | console.log([].slice.call(arguments, 0)); // ['good', 'haha']
35 | var root = this;
36 | setTimeout(function() {
37 | root.done(null, 'baz');
38 | }, 2000);
39 | }, 'good')
40 | .step('common') // ['baz']
41 | .error(function(err) {
42 | console.log('has error: ', err);
43 | })
44 | // .pend()
45 | .task('foo2')
46 | .step('bar2', 'params for bar2 step..')
47 | .bar2(function() {
48 | console.log([].slice.call(arguments, 0)) // ['params for bar2 step..']
49 | var root = this;
50 | setTimeout(function() {
51 | root.fulfill(500);
52 | // console.log(root.stepName, Date.now());
53 | root.done(null);
54 | }, 1000);
55 | })
56 | .step('common') // []
57 | // .step('err_test', function() {
58 | // console.log([].slice.call(arguments, 0));
59 | // var root = this;
60 | // setTimeout(function() {
61 | // root.done('err_test!!!');
62 | // }, 800);
63 | // })
64 | .pend()
65 | .common(function() {
66 | console.log([].slice.call(arguments, 0))
67 | var root = this;
68 | setTimeout(function() {
69 | // console.log(root.stepName, Date.now());
70 | root.done(null);
71 | }, 1000);
72 | })
73 | .error(function(err) {
74 | console.log('Error in common error method:', err);
75 | })
76 | .result(function(r) {
77 | console.log(r); // 500
78 | })
79 | .run();
80 |
81 | // myTask.result = function() {
82 | // console.log('good~~!');
83 | // };
84 |
85 | // myTask.error = function(err) {
86 | // console.log('has error: ', err);
87 | // };
88 |
89 | // myTask.run();
90 |
--------------------------------------------------------------------------------
/examples/parallel.js:
--------------------------------------------------------------------------------
1 | var Stepify = require('../index');
2 |
3 | var fs = require('fs');
4 | var path = require('path');
5 |
6 | // 非函数数组
7 | // Stepify()
8 | // .step(function() {
9 | // fs.readdir(path.resolve('./test'), this.wrap());
10 | // })
11 | // .step(function(list) {
12 | // list = list.filter(function(p) {return path.extname(p).match('js');});
13 | // list.forEach(function(file, i) {list[i] = path.resolve('./test', file);});
14 | // // 注释部分就相当于默认的this.next
15 | // this.parallel(list, fs.readFile, {encoding: 'utf8'}/*, function(bufArr) {this.next(bufArr);}*/);
16 | // })
17 | // .step(function(bufArr) {
18 | // // fs.writeFile('./combiled.js', Buffer.concat(bufArr), this.done.bind(this));
19 | // // or
20 | // this.parallel(bufArr, fs.writeFile.bind(this, './combiled.js'));
21 | // })
22 | // .run();
23 |
24 | // 函数数组
25 | Stepify()
26 | .step(function() {
27 | this.parallel([
28 | function(callback) {
29 | fs.readFile(__filename, callback);
30 | },
31 | function(callback) {
32 | setTimeout(function() {
33 | callback(null, 'some string...');
34 | }, 500);
35 | }
36 | ]);
37 | })
38 | .step(function(list) {
39 | console.log(list); // [fs.readFileSync(__filename), 'some string...']
40 | // todo...
41 | })
42 | .run();
43 |
--------------------------------------------------------------------------------
/examples/result.js:
--------------------------------------------------------------------------------
1 | var Stepify = require('../index');
2 | var fs = require('fs');
3 |
4 | Stepify()
5 | .step(function() {
6 | var root = this;
7 | setTimeout(function() {
8 | root.fulfill(100);
9 | root.done(null);
10 | }, 100);
11 | })
12 | .step(function() {
13 | var root = this;
14 | fs.readFile(__filename, function(err, buf) {
15 | if(err) return root.done(err);
16 | root.fulfill(buf.toString());
17 | root.done();
18 | });
19 | })
20 | .result(function(r) {
21 | console.log(r); // [100, fs.readFileSync(__filename).toString()]
22 | })
23 | .run();
24 |
--------------------------------------------------------------------------------
/examples/run_mode.js:
--------------------------------------------------------------------------------
1 | var Stepify = require('../index');
2 |
3 | var fs = require('fs');
4 | var path = require('path');
5 | var exec = require('child_process').exec;
6 |
7 | var c1 = 0, c2 = 0, c3 = 0, c4 = 0;
8 |
9 | function createTask() {
10 | return Stepify()
11 | .task('task1')
12 | .step(function() {c1++;
13 | var root = this;
14 | fs.readdir(__dirname, function(err) {
15 | if(err) throw err;
16 | root.done(null);
17 | });
18 | })
19 | .step('sleep')
20 | .step('exec', 'cat', __filename)
21 | .task('task2')
22 | .step('sleep')
23 | .step(function() {
24 | c2++;
25 | var root = this;
26 | setTimeout(function() {
27 | root.done(null);
28 | }, 1500);
29 | })
30 | .step('exec', 'ls', '-l')
31 | .task('task3')
32 | .step('readFile', __filename)
33 | .step('timer', function() {
34 | c3++;
35 | var root = this;
36 | setTimeout(function() {
37 | console.log('task3-timer')
38 | root.done();
39 | }, 1000);
40 | })
41 | .step('sleep')
42 | .readFile(function(p) {
43 | var root = this;
44 | fs.readFile(p, function(err) {
45 | if(err) throw err;
46 | console.log('task3-readFile')
47 | root.done(null);
48 | });
49 | })
50 | .task('task4')
51 | .step('sleep')
52 | .step(function(p) {
53 | c4++;
54 | var root = this;
55 | fs.readFile(p, function(err) {
56 | if(err) throw err;
57 | console.log('task4-readFile')
58 | root.done(null);
59 | });
60 | }, __filename)
61 | .pend()
62 | .sleep(function() {
63 | console.log('Task %s sleep.', this.taskName);
64 | var root = this;
65 | setTimeout(function() {
66 | root.done(null);
67 | }, 2000);
68 | })
69 | .exec(function(cmd, args) {
70 | cmd = [].slice.call(arguments, 0);
71 | var root = this;
72 | exec(cmd.join(' '), function(err) {
73 | if(err) throw err;
74 | root.done(null);
75 | });
76 | });
77 | };
78 |
79 | var modes = {
80 | 'Default(serial)': [], // 10621 ms.
81 | 'Customized-serial': ['task1', 'task3', 'task4', 'task2'], // 10624 ms.
82 | 'Serial-mix-parallel-1': ['task1', ['task3', 'task4'], 'task2'], // 8622 ms.
83 | 'Serial-mix-parallel-2': [['task1', 'task3', 'task4'], 'task2'], // 6570 ms.
84 | 'Serial-mix-parallel-3': [['task1', 'task3'], ['task4', 'task2']], // 6576 ms.
85 | 'All-parallel': [['task1', 'task3', 'task4', 'task2']], // 3552 ms.
86 | 'Part-of': ['task2', 'task4'] // 5526 ms.
87 | };
88 |
89 | var test = module.exports = function() {
90 | var t = Date.now();
91 | var task;
92 | var log = console.log;
93 |
94 | console.log = function() {};
95 |
96 | Object.keys(modes).forEach(function(mode) {
97 | task = createTask();
98 |
99 | task.result = function() {
100 | log(mode + ' mode finished and took %d ms.', Date.now() - t);
101 | };
102 |
103 | task.run.apply(task, modes[mode]);
104 | });
105 |
106 | setTimeout(function() {
107 | log(c1, c2, c3 ,c4); // [6, 7, 6, 7]
108 | }, 15000);
109 | };
110 |
111 | test();
112 |
--------------------------------------------------------------------------------
/examples/simple.js:
--------------------------------------------------------------------------------
1 | var request = require('request');
2 | var Stepify = require('../index');
3 |
4 | // Authorizing based on oauth2 workflow
5 | Stepify()
6 | .step('getCode', function(appId, rUri) {
7 | var root = this;
8 | request.get('[authorize_uri]', function(err, res, body) {
9 | root.done(err, JSON.parse(body).code);
10 | });
11 | }, [appId], [redirectUri])
12 | .step('getToken', function(code) {
13 | var root = this;
14 | request.post('[token_uri]', function(err, res, body) {
15 | root.done(err, JSON.parse(body).access_token);
16 | });
17 | })
18 | .step('getInfo', function(token) {
19 | request.get('[info_uri]?token=' + token, function(err, res, body) {
20 | // got user info, pass it to client via http response
21 | });
22 | })
23 | .run();
24 |
25 | // muitiply tasks case
26 | Stepify()
27 | .step(function() {
28 | var root = this;
29 | setTimeout(function() {
30 | // do some stuff ...
31 | root.done(null, 'a');
32 | }, 100);
33 | })
34 | .step(function() {
35 | var root = this;
36 | setTimeout(function() {
37 | // do some stuff ...
38 | root.done();
39 | }, 100);
40 | })
41 | .pend()
42 | .step(function() {
43 | var root = this;
44 | setTimeout(function() {
45 | // do some stuff ...
46 | root.done();
47 | }, 200);
48 | })
49 | .step(function() {
50 | var root = this;
51 | setTimeout(function() {
52 | // do some stuff ...
53 | root.done();
54 | }, 200);
55 | })
56 | .run();
57 |
--------------------------------------------------------------------------------
/examples/step.js:
--------------------------------------------------------------------------------
1 | var Stepify = require('../index');
2 |
3 | // arguments accessing
4 | // Stepify()
5 | // .step(function() {
6 | // var root = this;
7 | // setTimeout(function() {
8 | // // 这里n+100即成为下一个stepHandle的动态参数
9 | // root.done(null, 100);
10 | // }, 100);
11 | // })
12 | // .step(function(start, n) {
13 | // // start === 50
14 | // // n === 100
15 | // var root = this;
16 | // setTimeout(function() {
17 | // root.done();
18 | // }, start + n);
19 | // }, 50)
20 | // .run();
21 |
22 | // extend prototype chain
23 | Stepify()
24 | .step('sleep')
25 | // more step ...
26 | .step('sleep', 50)
27 | .sleep(function(start, n) {
28 | var args = [].slice.call(arguments, 0);
29 | var root = this;
30 |
31 | n = args.length ? args.reduce(function(mem, arg) {return mem + arg;}) : 100;
32 | setTimeout(function() {
33 | root.done(null, n);
34 | }, n);
35 | })
36 | .run();
37 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @description Define the stepify library.
3 | * @author dmyang
4 | **/
5 |
6 | module.exports = require('./lib/Stepify');
7 |
--------------------------------------------------------------------------------
/lib/Step.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * node-stepify - Step.js
3 | * Copyright(c) 2013 dmyang
4 | * MIT Licensed
5 | */
6 |
7 | 'use strict';
8 |
9 | var util = require('./util');
10 |
11 | // Define the `Step` Class.
12 | // A step is just do only an asynchronous task.
13 | // usage:
14 | // new Step(task, 'foo', function function() {}[, args])
15 | // new Step(task, 'foo', [, args])
16 | // new Step(task, function() {}[, args])
17 | var Step = module.exports = function(task, stepName, stepHandle, knownArgs) {
18 | // Declare which task this step belongs to.
19 | this._task = task;
20 |
21 | // The name of task this step belongs to.
22 | this.taskName = task._name;
23 |
24 | // Every step has an uinque stepName which can not be rewrite.
25 | this.name = stepName;
26 |
27 | // Step handle define what should done after this step.
28 | this._stepHandle = stepHandle;
29 |
30 | // The known arguments bafore this step declared.
31 | this._knownArgs = knownArgs;
32 |
33 | return this;
34 | };
35 |
36 | var _proto = Step.prototype;
37 |
38 | // To finish current step manually.
39 | // The first parame `err` is required and is the same as asynchronous callback in Node.JS
40 | // the second param `callback` is optional and default is `this.next`,
41 | // The rest parame(s) are(is) optional, and they(it) will be passed to the next step.
42 | // usage:
43 | // step.done(err[, function() {this.jump(2);}, args])
44 | _proto.done = function(err) {
45 | var args = util.slice(arguments, 0);
46 | var callback;
47 |
48 | err = args.shift();
49 |
50 | if(undefined === err) err = null;
51 |
52 | callback = typeof args[0] === 'function' ? args.shift() : this.next;
53 |
54 | if(err) this.end(err);
55 | else callback.apply(this, args);
56 | };
57 |
58 | // Wrap a context for asynchronous callback,
59 | // just work as a shortcut of `this.done.bind(this)`.
60 | // It is usefull when working with some asynchronous APIs such as `fs.readdir`,
61 | // because nodejs has limit it's callback param to run in the global context
62 | // see: https://github.com/joyent/node/blob/master/lib/fs.js#L91
63 | _proto.wrap = function() {
64 | var root = this;
65 | return function() {
66 | root.done.apply(root, arguments);
67 | };
68 | };
69 |
70 | // Output this task's finally result, which will access to the global finish handle.
71 | // store this step's result is optional,
72 | // just call `next` or `done` can access current result to next step,
73 | // if this result is not expected for finally result.
74 | // maybe `promises` or `result` better?
75 | _proto.fulfill = function(result) {
76 | var args = util.slice(arguments, 0);
77 | var task = this._task;
78 | var fn = task._result;
79 |
80 | args.forEach(fn.bind(this._task));
81 | };
82 |
83 | // Set(or get) temporary variables which visible in this task's runtime.
84 | _proto.vars = function(key, value) {
85 | var len = arguments.length;
86 |
87 | if(len === 1) {return this._task._variables[key];}
88 | if(len === 2) {return this._task._variables[key] = value;}
89 | return null;
90 | };
91 |
92 | // Simple parallel support.
93 | // usage:
94 | // this.parallel(['a.js', 'b.js'], fs.readFile[, *args, callback]);
95 | // this.parallel([readFile1, readFile1][, callback]);
96 | // the callback(default is this.next) has only one:
97 | // a results array which has the same order as arr
98 | _proto.parallel = function(arr) {
99 | var root = this;
100 | var completed = 0;
101 | var isFunction = util.isFunction;
102 | var each = util._.each;
103 | var result = [];
104 | var callback;
105 | var args = util.slice(arguments, 1);
106 | var done = function(n, err, r) {
107 | if(err) {
108 | this.end(err);
109 | } else {
110 | // make sure the result array has the same index as arr
111 | result[n] = r;
112 | if(++completed >= arr.length) {
113 | callback.call(this, result);
114 | }
115 | }
116 | };
117 |
118 | // each element should be a function in this case
119 | if(isFunction(arr[0])) {
120 | callback = isFunction(args[0]) ? args[0] : this.next;
121 |
122 | each(arr, function(fn, i) {
123 | if(!isFunction(fn)) throw new Error('Every element should be a function \
124 | as the first one does.');
125 | fn(done.bind(root, i));
126 | });
127 | } else {
128 | var iterator = args.shift();
129 | // use the last param as callback(default is this.next)
130 | callback = isFunction(args[args.length - 1]) ? args.pop() : this.next;
131 |
132 | each(arr, function(arg, i) {
133 | var a = [arg];
134 | a = a.concat(args);
135 | a.push(done.bind(root, i));
136 | iterator.apply(null, a);
137 | });
138 | }
139 | };
140 |
141 | // The default callback handle is this.next,
142 | // use .jump() one can execute any other step manually.
143 | // jump accepts at last one param, the first one `step` is
144 | // required to declare which step will be jump to.
145 | // Be careful using this method, really hope you never use it as
146 | // it will disrupt the normal order of execution!
147 | // usage:
148 | // jump(3) || jump(-2) || jump('foo')
149 | _proto.jump = function(step) {
150 | if(undefined === step) throw new Error('You must access the step you wish to jump to.');
151 |
152 | var root = this;
153 | var task = this._task;
154 | var currIndex = task._currIndex;
155 | var currStep = task._getStep(currIndex);
156 | var targetStep = function() {
157 | var type = typeof step;
158 |
159 | if('string' === type) return task._getStep(step);
160 |
161 | // step index started from 0
162 | if('number' === type) return task._getStep(step < 0 ? currIndex + step : step);
163 |
164 | return null;
165 | }();
166 | var targetIndex;
167 |
168 | if(!targetStep) throw new Error('The target step which will jump to was not exists.');
169 |
170 | targetIndex = targetStep._index;
171 |
172 | if(this._debug && currStep) console.log('The step: %s has done.', currStep.name);
173 |
174 | if(targetIndex !== currIndex) {
175 | task._run.apply(task, [targetIndex].concat(util.slice(arguments, 1)));
176 | } else {
177 | throw new Error('The step ' + currStep.name + ' is executing now!');
178 | }
179 | };
180 |
181 | // Finish current step and access the result to next step,
182 | // the next step will be execute automatically,
183 | // if the has no next step, then the current task will be identified finished.
184 | _proto.next = function() {
185 | var task = this._task;
186 | var currIndex = task._currIndex;
187 | var args = util.slice(arguments, 0);
188 |
189 | if(currIndex + 1 < task._steps.length) {
190 | this.jump.apply(this, [currIndex + 1].concat(args));
191 | } else {
192 | this.end();
193 | }
194 | };
195 |
196 | // To break off current task manually and run next task automatically.
197 | // If the has no next task it will run the `finish` handle if accessed.
198 | // maybe `interrupt` or `stop` better?
199 | _proto.end = function(err) {
200 | if(this._debug) console.log('Task: %s has ended in the step: %s with error: ' + (err ? err.stack : null) + '.',
201 | this.taskName, this.name);
202 |
203 | this._task.emit('done', err);
204 | };
205 |
--------------------------------------------------------------------------------
/lib/Stepify.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * node-stepify - Stepify.js
3 | * Copyright(c) 2013 dmyang
4 | * MIT Licensed
5 | */
6 |
7 | 'use strict';
8 |
9 | var nutil = require('util');
10 | var util = require('./util');
11 |
12 | var Task = require('./Task');
13 | var UNAMED_TASK = '_UNAMED_TASK_';
14 | var UNAMED_STEP = '_UNAMED_STEP_';
15 |
16 | /*
17 | * @description Define the `Stepify` Class aims to manage numbers of `Task` instances.
18 | * @public
19 | * @param [Function] result optional
20 | * @return this
21 | * @usage:
22 | * var myTask = new Stepify([handle]);
23 | * var myTask = Stepify([handle]);
24 | */
25 | var Stepify = module.exports = function(result) {
26 | // Both `new Stepify()` and `Stepify()` are supported.
27 | if(!(this instanceof Stepify)) return new Stepify(result);
28 |
29 | var args = util.slice(arguments, 0);
30 |
31 | result = args[0] && 'function' === typeof args[0] ? args[0] : null;
32 |
33 | this._debug = false;
34 | this._taskSequences = [];
35 | this._currTask = null;
36 | this._handles = {};
37 | this._results = [];
38 |
39 | // Library insert keys, the stepName(the first string parame of `step` method) should not be one of them.
40 | this._insetNames = ['debug', 'task', 'step', 'pend', 'error', 'timeout', 'result', 'run'];
41 |
42 | // The methods extend by user when defining workflow using step(stepName)
43 | this._definedNames = [];
44 |
45 | // Optional, will called when all tasked tasks done.
46 | if(util.isFunction(result)) this._finishHandle = result;
47 |
48 | return this;
49 | };
50 | var _proto = Stepify.prototype;
51 |
52 | /*
53 | * @description Switch `this._debug` between true or false dynamically.
54 | * @public
55 | * @param debug {Mix} canbe Boolean or Function.
56 | * @return this
57 | * @usage:
58 | * Stepify()...debug([true, false])
59 | * Stepify()...debug(function() {return true;}[, args])
60 | */
61 | Object.defineProperty(_proto, 'debug', {
62 | get: function() {
63 | return function(debug) {
64 | if(typeof debug === 'function') this._debug = debug.apply(this, util.slice(arguments, 1));
65 | else this._debug = debug || false;
66 |
67 | return this;
68 | };
69 | },
70 | set: function(debug) {
71 | this._debug = debug || false;
72 | }
73 | });
74 |
75 | /*
76 | * @description Register a task.
77 | * @public
78 | * @param taskName {String} optional
79 | * @return this
80 | * @usage:
81 | * Stepify().task('foo')
82 | */
83 | Object.defineProperty(_proto, 'task', {
84 | // No need to rewrite this method
85 | writable: false,
86 | value: function(taskName) {
87 | if(this._currTask) this.pend();
88 |
89 | var root = this;
90 | var index = this._taskSequences.length;
91 | var task;
92 |
93 | taskName = taskName && typeof taskName === 'string' ? taskName : UNAMED_TASK + index;
94 | task = new Task(taskName, this);
95 |
96 | task._index = index;
97 | task._debug = this._debug;
98 |
99 | if(this._debug) {console.log('Task: %s added.', taskName);}
100 |
101 | this._currTask = task;
102 |
103 | return this;
104 | }
105 | });
106 |
107 | /*
108 | * @description Add a asynchronous method to current task.
109 | * @public
110 | * @param stepName {String} the name of this step
111 | * @param stepHandle {Function} optional step handle method
112 | * @param args {Mix} optional the data access to stepHandle
113 | * @return this
114 | * @usage:
115 | * Stepify().task('foo').step('bar', handle[, *args]).step(handle)
116 | * Stepify().task('foo').step('bar').bar(handle[, *args])
117 | */
118 | Object.defineProperty(_proto, 'step', {
119 | writable: false,
120 | value: function(stepName, stepHandle) {
121 | // `task` method will be called automatically before `step` if not called manually.
122 | if(!this._currTask) this.task();
123 | // if(!this._currTask) throw new Error('The task for this step has not declared, \
124 | // just call .task() before .step()');
125 | if(!arguments.length) throw new Error('Step handle should be accessed.');
126 |
127 | var currTask = this._currTask;
128 | var args = util.slice(arguments, 0);
129 | var stepName = args.shift();
130 | var _name;
131 | var stepHandle;
132 | var step;
133 | var find = util._.find;
134 | var isFunction = util.isFunction;
135 |
136 | if(isFunction(stepName)) {
137 | stepHandle = stepName;
138 | _name = stepName = UNAMED_STEP + currTask._steps.length;
139 | } else {
140 | if(find(this._insetNames, function(name) {return name === stepName})) {
141 | throw new Error('The name `' + stepName + '` was preset within the construtor, try another one?');
142 | }
143 |
144 | stepHandle = isFunction(args[0]) ? args.shift() : null;
145 | }
146 |
147 | step = currTask._step(stepName, stepHandle, args);
148 |
149 | if(!stepHandle
150 | && stepName !== _name
151 | && !find(this._definedNames, function(n) {return n === stepName;})
152 | // can not be rewrite
153 | // && !isFunction(currTask._handles[stepName])
154 | // && !isFunction(this._handles[stepName])
155 | ) {
156 | // Modify the prototype chain dynamically,
157 | // add a method as `step._stepHandle` which has the same name as `step.name`
158 | // usage:
159 | // Stepify().task('foo').step('bar').bar(handle[, *args])
160 | // var task = Stepify().task('foo').step('bar'); task.bar = handle;
161 | var _stepHandle = function(handle) {
162 | if(typeof handle !== 'function') throw new Error('Step handle should be a function.');
163 |
164 | if(this._currTask) {
165 | this._currTask._handles[stepName] = handle;
166 | } else {
167 | this._handles[stepName] = handle;
168 | }
169 |
170 | return this;
171 | };
172 |
173 | Object.defineProperty(_proto, stepName, {
174 | // if not set configurable,
175 | // _proto[stepName] can not be released after workflow finish
176 | // see run() method
177 | configurable: true,
178 | get: function() {
179 | return _stepHandle.bind(this);
180 | },
181 | set: _stepHandle.bind(this)
182 | });
183 |
184 | this._definedNames.push(stepName);
185 | }
186 |
187 | if(isFunction(stepHandle)) {
188 | step._task._handles[stepName] = stepHandle;
189 | }
190 |
191 | _name = null;
192 |
193 | return this;
194 | }
195 | });
196 |
197 | /*
198 | * @description Finish a task workflow declare and prepare to declare another one.
199 | * If a new task workflow has started (task() been called), pend() will be call firstly automatically.
200 | * @public
201 | * @return this
202 | * @usage:
203 | * Stepify().task('foo').step('bar').pend().task('biz')
204 | */
205 | Object.defineProperty(_proto, 'pend', {
206 | writable: false,
207 | value: function() {
208 | var task = this._currTask;
209 |
210 | if(task) {
211 | this._taskSequences.push(task);
212 | this._currTask = task = null;
213 | }
214 |
215 | return this;
216 | }
217 | });
218 |
219 | /*
220 | * @description Define a method which will call when all tasks done.
221 | * @public
222 | * @return null
223 | * @usage:
224 | * Stepify().task('foo').step('foo').result(handle)
225 | * var task = Stepify().task('foo').step('foo'); task.result = handle;
226 | */
227 | Object.defineProperty(_proto, 'result', {
228 | get: function() {
229 | return function(handle) {
230 | if(!util.isFunction(handle)) throw new Error('The param `handle` should be a function.');
231 | this._finishHandle = handle;
232 |
233 | return this;
234 | };
235 | },
236 | set: function(handle) {
237 | if(!util.isFunction(handle)) throw new Error('The param `handle` should be a function.');
238 | this._finishHandle = handle;
239 |
240 | return this;
241 | }
242 | });
243 |
244 | /*
245 | * @description Define error handle for one task.
246 | * @public
247 | * @return this
248 | * @usage:
249 | * Stepify().task('foo').step('foo').error(errHandle);
250 | * var myTask = Stepify().task('foo').step('foo'); myTask.error = errHandle;
251 | */
252 | Object.defineProperty(_proto, 'error', {
253 | get: function() {
254 | return function(handle) {
255 | if('function' !== typeof handle) throw new Error('The param `handle` should be a function.');
256 |
257 | // rewrite _errorHandle
258 | if(this._currTask) this._currTask._errorHandle = handle;
259 | else this._errorHandle = handle;
260 |
261 | return this;
262 | };
263 | },
264 | set: function(handle) {
265 | if('function' !== typeof handle) throw new Error('The param `handle` should be a function.');
266 |
267 | // rewrite _errorHandle
268 | if(this._currTask) this._currTask._errorHandle = handle;
269 | else this._errorHandle = handle;
270 |
271 | return this;
272 | }
273 | });
274 |
275 | // Define the default error handle for Stepify instance.
276 | _proto._errorHandle = function(err) {
277 | if(!(err instanceof Error)) throw new Error(err.toString());
278 | else throw err;
279 | };
280 |
281 | /*
282 | * This method will make the task sequences running by the order customed.
283 | * The type of params can be String or Array.
284 | * The tasks in array param will be executed parallel.
285 | * usage:
286 | * run()
287 | * run('task1', 'task3', 'task4', 'task2')
288 | * run('task1', ['task3', 'task4'], 'task2')
289 | * run(['task1', 'task3', 'task4'], 'task2')
290 | * run(['task1', 'task3', 'task4', 'task2'])
291 | */
292 | Object.defineProperty(_proto, 'run', {
293 | writable: false,
294 | value: function() {
295 | if(this._currTask) this.pend();
296 |
297 | var root = this;
298 | var tasks = root._taskSequences;
299 | var args = util.slice(arguments, 0);
300 | var isString = util.isString;
301 | var isArray = util.isArray;
302 | var isNumber = util.isNumber;
303 | var isUndefined = util.isUndefined;
304 | var isFunction = util.isFunction;
305 | var find = function(key) {
306 | var type = typeof(key);
307 |
308 | return isString(key) ?
309 | util._.find(tasks, function(task) {return task._name === key;}) :
310 | isNumber(key) ? tasks[key] : null;
311 | };
312 | var each = util._.each;
313 | var index = 0;
314 | var arr = args.length ? args : util._.range(tasks.length);
315 | var finishHandle = isFunction(this._finishHandle) ? util.once(this._finishHandle) : null;
316 | var errHandleWrap = function(err) {
317 | // bind context to current step
318 | var step = this._getStep(this._currIndex);
319 | var handle = isFunction(this._errorHandle) ? this._errorHandle : root._errorHandle;
320 |
321 | if(!step) throw new Error('Step was not found in task ' + task._name + '.');
322 |
323 | handle.call(step, err);
324 | };
325 | var handle = function(err) {
326 | if(err) {
327 | this.emit('error', err);
328 | } else {
329 | var next = arr[++index];
330 | var task;
331 | var step;
332 |
333 | if(isUndefined(next)) {
334 | if(this._debug) console.timeEnd('Tasks finished and cast');
335 | finishHandle && finishHandle(root._results);
336 |
337 | // Release all the extended properties on Stepify.prototype
338 | each(root._definedNames, function(prop) {delete _proto[prop];});
339 | } else if(isArray(next)) {
340 | execEach(index);
341 | } else {
342 | task = find(next);
343 | step = task._getStep(task._currIndex);
344 | if(!task) throw new Error('Task has not registered.');
345 | task.on('done', task._doneHandle = handle.bind(task));
346 | task.on('error', errHandleWrap.bind(task));
347 | if(root._debug) console.log('Start to run task: %s.', task._name);
348 | task._run();
349 | }
350 | }
351 | };
352 | var execEach = function(n) {
353 | n = n || 0;
354 |
355 | var taskArr = isArray(arr[n]) ? arr[n] : [arr[n]];
356 | var count = 0;
357 | var task;
358 |
359 | each(taskArr, function(key) {
360 | task = find(key);
361 | if(!task) throw new Error('Task has not registered.');
362 | task._doneHandle = function(err) {
363 | if(err || ++count >= taskArr.length) {
364 | handle.apply(task, arguments);
365 | }
366 | };
367 | task.on('done', task._doneHandle.bind(task));
368 | task.on('error', errHandleWrap.bind(task));
369 | if(root._debug) console.log('Start to run task: %s.', task._name);
370 | task._run();
371 | });
372 | };
373 |
374 | // for consuming statistics
375 | if(this._debug) console.time('Tasks finished and cast');
376 |
377 | // Tasks will be executed by the order they declared if not customed order accessed.
378 | execEach();
379 | }
380 | });
381 |
--------------------------------------------------------------------------------
/lib/Task.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * node-stepify - Task.js
3 | * Copyright(c) 2013 dmyang
4 | * MIT Licensed
5 | */
6 |
7 | 'use strict';
8 |
9 | var EventEmitter = require('events').EventEmitter;
10 | var util = require('./util');
11 |
12 | var Step = require('./Step');
13 |
14 | // Define the `Task` Class.
15 | // class Task
16 | // usage:
17 | // new Task().err(fn).finish(fn)
18 | // new Task()...run(succ, err)
19 | var Task = module.exports = function(taskName, taskMgr) {
20 | this._name = taskName;
21 | this._taskMgr = taskMgr;
22 |
23 | // [new Step(), new Step()]
24 | this._steps = [];
25 |
26 | this._currIndex = 0;
27 |
28 | // steps handles map
29 | this._handles = {};
30 |
31 | // temporary variables visible in task's runtime.
32 | this._variables = {};
33 | };
34 |
35 | var _proto = Task.prototype;
36 |
37 | // Extend all properties from EventEmitter constructor
38 | _proto.__proto__ = EventEmitter.prototype;
39 |
40 | // take out step instance by stepName
41 | // return all step instances if stepName has not accessed.
42 | _proto._getStep = function(key) {
43 | if(key !== undefined) {
44 | return typeof key === 'string' ?
45 | util._.find(this._steps, function(step) {return step.name === key;}) :
46 | this._steps[key];
47 | } else {
48 | return this._steps;
49 | }
50 | };
51 |
52 | // get step handle by accessing step name.
53 | _proto._getHandle = function(stepName) {
54 | var fn = this._handles[stepName] || this._taskMgr._handles[stepName];
55 |
56 | if(!util.isFunction(fn)) throw new Error('Handle of step `' + stepName + '` has not defined.');
57 |
58 | return fn;
59 | };
60 |
61 | // push current step's result into the final results array.
62 | _proto._result = function(result) {
63 | return this._taskMgr._results.push(result);
64 | };
65 |
66 | // Assign an asynchronous step for this task.
67 | _proto._step = function(stepName, handle, args) {
68 | var step = new Step(this, stepName, handle, args);
69 |
70 | step._debug = this._debug;
71 | step._index = this._steps.length;
72 | this._steps.push(step);
73 |
74 | return step;
75 | };
76 |
77 | // Start to execute this task step by step.
78 | _proto._run = function() {
79 | var root = this;
80 | var args = util.slice(arguments, 0);
81 | var currStep;
82 | var handle;
83 | var isFunction = util.isFunction;
84 | var nextTick = util.nextTick;
85 |
86 | this._currIndex = args.shift() || 0;
87 |
88 | currStep = this._getStep(this._currIndex);
89 | handle = this._getHandle(currStep.name);
90 |
91 | if(isFunction(handle)) {
92 | nextTick(function() {
93 | if(root._debug) console.log('Start to run step: %s.', currStep.name);
94 | handle.apply(currStep, currStep._knownArgs.concat(args));
95 | });
96 | } else {
97 | this.emit('error', 'Step handle was not function.');
98 | }
99 | };
100 |
--------------------------------------------------------------------------------
/lib/util.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // The module to be exported.
4 | var util = module.exports = {};
5 |
6 | // util.hooker = require('hooker');
7 | // util.async = require('async');
8 | // util._ = require('lodash/dist/lodash.underscore');
9 | var _ = util._ = require('lodash');
10 |
11 | var isFunction = util.isFunction = _.isFunction;
12 | var isArray = util.isArray = _.isArray;
13 | var isString = util.isString = _.isString;
14 | var isNumber = util.isNumber = _.isNumber;
15 | var isUndefined = util.isUndefined = _.isUndefined;
16 |
17 | // https://github.com/gruntjs/grunt/blob/master/lib/grunt/util.js#L38
18 | // Return a function that normalizes the given function either returning a
19 | // value or accepting a "done" callback that accepts a single value.
20 | util.callbackify = function(fn) {
21 | return function callbackable() {
22 | var result = fn.apply(this, arguments);
23 | var length = arguments.length;
24 | if (length === fn.length) { return; }
25 | var done = arguments[length - 1];
26 | if (typeof done === 'function') { done(result); }
27 | };
28 | };
29 |
30 | util.slice = function(args, index) {
31 | return Array.prototype.slice.call(args, index);
32 | };
33 |
34 | // Ensure that a function only be executed once.
35 | util.once = function(fn, context) {
36 | var called = false;
37 |
38 | return function() {
39 | if(called) throw new Error('Callback was already called.');
40 | called = true;
41 | fn.apply(context || null, arguments);
42 | };
43 | };
44 |
45 | // node v0.8 has no setImmediate method
46 | util.nextTick = typeof setImmediate === 'function' ? setImmediate : process.nextTick;
47 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "stepify",
3 | "description": "Executing nodejs asynchronous tasks by steps chain",
4 | "version": "0.1.5",
5 | "engine": {
6 | "node": ">= 0.8.0"
7 | },
8 | "scripts": {
9 | "test": "mocha -R spec"
10 | },
11 | "main": "./index",
12 | "repository": {
13 | "type": "git",
14 | "url": "https://github.com/chemdemo/node-stepify.git"
15 | },
16 | "dependencies": {
17 | "lodash": "~2.3.0"
18 | },
19 | "devDependencies": {
20 | "mocha": "~1.16.2",
21 | "should": "~2.1.1"
22 | },
23 | "keywords": [
24 | "node-stepify", "flow-control", "stepify", "step", "async", "asynchronous", "chain"
25 | ],
26 | "author": "dmyang ",
27 | "maintainers": [
28 | {
29 | "name": "dmyang",
30 | "email": "yangdemo@gmail.com",
31 | "web": "http://www.dmfeel.com"
32 | }
33 | ],
34 | "license": "MIT"
35 | }
36 |
--------------------------------------------------------------------------------
/test/step.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert');
2 | var should = require('should');
3 | var domain = require('domain');
4 |
5 | var Stepify = require('../index');
6 |
7 | // for test...
8 | var fs = require('fs');
9 | var path = require('path');
10 | var exec = require('child_process').exec;
11 |
12 | describe('Step', function() {
13 | describe('#done()', function() {
14 | it('should execute without error when nothing or `null` been accessing in', function(done) {
15 | Stepify()
16 | .step(function() {
17 | var root = this;
18 | setTimeout(function() {
19 | root.fulfill(1);
20 | root.done();
21 | }, 300);
22 | })
23 | .step(function() {
24 | var root = this;
25 | fs.readFile(__filename, function(err) {
26 | root.done(null);
27 | });
28 | })
29 | .result(function(r) {
30 | r.should.eql([1]);
31 | done();
32 | })
33 | .run();
34 | });
35 |
36 | it('should access error to errorHandle', function(done) {
37 | Stepify()
38 | .step(function() {
39 | var root = this;
40 | fs.readdir('./not_exists.js', function(err) {
41 | if(err) err = 'Error mock: file was not found.';
42 | root.done(err, 1);
43 | });
44 | })
45 | .step(function() {
46 | var root = this;
47 | setTimeout(function() {
48 | root.done();
49 | }, 300);
50 | })
51 | .error(function(err) {
52 | err.should.equal('Error mock: file was not found.');
53 | done();
54 | })
55 | .run();
56 | });
57 |
58 | it('should execute customed callback after async task done', function(done) {
59 | var c = 0;
60 | Stepify()
61 | .step(function() {
62 | var root = this;
63 | setTimeout(function() {
64 | root.done(null, function() {
65 | c++;
66 | root.next(c);
67 | });
68 | }, 300);
69 | })
70 | .step(function(n) {
71 | n.should.equal(1);
72 | var root = this;
73 | setTimeout(function() {
74 | root.done(null, function(x) {
75 | root.next(x);
76 | }, n + 10);
77 | }, 200);
78 | })
79 | .step(function(n) {
80 | n.should.equal(11);
81 | var root = this;
82 | setTimeout(function() {
83 | root.done();
84 | }, 100);
85 | })
86 | .result(function() {
87 | done();
88 | })
89 | .run();
90 | });
91 |
92 | it('should access extra params to callback', function(done) {
93 | Stepify()
94 | .step(function(n) {
95 | n.should.equal(300);
96 | var root = this;
97 | setTimeout(function() {
98 | root.done(null, n);
99 | }, n);
100 | }, 300)
101 | .step(function(n) {
102 | n.should.equal(300);
103 | var root = this;
104 | var n2 = n - 100;
105 | setTimeout(function() {
106 | root.done(null, function(x) {
107 | root.fulfill(x);
108 | root.next();
109 | }, n2);
110 | }, n2);
111 | })
112 | .result(function(n) {
113 | n.should.eql([200]);
114 | done();
115 | })
116 | .run();
117 | });
118 | });
119 |
120 | describe('#wrap()', function() {
121 | it('should run as a shortcut of this.done.bind(this) inner stepHandle', function(done) {
122 | Stepify()
123 | .step(function() {
124 | fs.readdir(__dirname, this.wrap());
125 | })
126 | .step(function(list) {
127 | list.sort().should.eql(fs.readdirSync(__dirname).sort());
128 | fs.readFile(__filename, this.wrap());
129 | })
130 | .step(function(fileStr) {
131 | fileStr.toString().should.equal(fs.readFileSync(__filename).toString());
132 | this.done();
133 | })
134 | .result(function() {
135 | done();
136 | })
137 | .run();
138 | });
139 | });
140 |
141 | describe('#fulfill()', function() {
142 | it('should push step\'s result to finish handle ', function(done) {
143 | Stepify()
144 | .task('timer')
145 | .step(function(n) {
146 | var root = this;
147 | setTimeout(function() {
148 | root.fulfill(n);
149 | root.done(null, n*2);
150 | }, n);
151 | }, 200)
152 | .step(function(n) {
153 | var root = this;
154 | setTimeout(function() {
155 | root.fulfill(n, 'for test');
156 | root.done();
157 | }, n);
158 | })
159 | .task('fs')
160 | .step(function() {
161 | var root = this;
162 | fs.readFile(__filename, function(err, fileStr) {
163 | if(err) root.done(err);
164 | root.fulfill({__filename: fileStr.toString()});
165 | root.done();
166 | });
167 | })
168 | .step(function() {
169 | var root = this;
170 | fs.readdir(__dirname, function(err, list) {
171 | if(err) root.done(err);
172 | root.fulfill(list.sort());
173 | root.done();
174 | });
175 | })
176 | .result(function(r) {
177 | r.should.eql([
178 | 200,
179 | 400,
180 | 'for test',
181 | {
182 | __filename: fs.readFileSync(__filename).toString()
183 | },
184 | fs.readdirSync(__dirname).sort()
185 | ]);
186 | done();
187 | })
188 | .run();
189 | });
190 | });
191 |
192 | describe('#vars()', function() {
193 | it('should store variables for task runtime', function(done) {
194 | Stepify()
195 | .task('foo')
196 | .step(function() {
197 | var root = this;
198 | setTimeout(function() {
199 | root.vars('key', 'value');
200 | root.done();
201 | }, 200);
202 | })
203 | .step(function() {
204 | this.vars('key').should.equal('value');
205 | should.strictEqual(undefined, this.vars('not_exists'));
206 | this.done();
207 | })
208 | .pend()
209 | .step(function() {
210 | // variables stored via `vars()` method can only avaiable to this task
211 | should.strictEqual(undefined, this.vars('key'));
212 | this.done();
213 | })
214 | .result(function() {
215 | done();
216 | })
217 | .run();
218 | });
219 | });
220 |
221 | describe('#parallel()', function() {
222 | var index = path.resolve(__dirname, '../index.js');
223 | var files = [index, __filename];
224 | var exed = [];
225 |
226 | it('should support parallel(arr, iterator[, callback]) mode', function(done) {
227 | Stepify()
228 | .step('a', function() {
229 | exed.push(this.name);
230 | this.parallel(files, fs.readFile, {encoding: 'utf8'});
231 | })
232 | .step('b', function(list) {
233 | exed.push(this.name);
234 |
235 | list.should.have.length(2);
236 | list[0].toString().should.equal(fs.readFileSync(index).toString());
237 | list[1].toString().should.equal(fs.readFileSync(__filename).toString());
238 |
239 | this.parallel(files, fs.readFile, {encoding: 'utf8'}, this.next);
240 | })
241 | .step('c', function(list) {
242 | list.should.have.length(2);
243 | list[0].toString().should.equal(fs.readFileSync(index).toString());
244 | list[1].toString().should.equal(fs.readFileSync(__filename).toString());
245 |
246 | this.parallel(files, fs.readFile, function(results) {
247 | exed.push(this.name);
248 | results.should.be.an.Array;
249 | this.next(results);
250 | });
251 | })
252 | .step('d', function(list) {
253 | list.should.have.length(2);
254 | list[0].toString().should.equal(fs.readFileSync(index).toString());
255 | list[1].toString().should.equal(fs.readFileSync(__filename).toString());
256 |
257 | var root = this;
258 |
259 | setTimeout(function() {
260 | exed.push(root.name);
261 | root.done();
262 | }, 300);
263 | })
264 | .result(function() {
265 | exed.should.eql(['a', 'b', 'c', 'd']);
266 | done();
267 | })
268 | .run();
269 | });
270 |
271 | it('should support parallel(fnArr[, callback]) mode', function(done) {
272 | Stepify()
273 | .step('a', function() {
274 | this.parallel([
275 | function(callback) {
276 | fs.readFile(__filename, callback);
277 | },
278 | function(callback) {
279 | setTimeout(function() {
280 | callback(null, 'timer return');
281 | }, 500);
282 | }
283 | ]);
284 | })
285 | .step('b', function(r) {
286 | r.should.be.an.Array;
287 | r.should.have.length(2);
288 | r[0].toString().should.equal(fs.readFileSync(__filename).toString());
289 | r[1].should.equal('timer return');
290 |
291 | this.parallel([
292 | function(callback) {
293 | fs.readFile(index, callback);
294 | },
295 | function(callback) {
296 | setTimeout(function() {
297 | callback(null, 'timer2 return');
298 | }, 500);
299 | }
300 | ], function(results) {
301 | this.next(results);
302 | });
303 | })
304 | .step('c', function(r) {
305 | r.should.be.an.Array;
306 | r.should.have.length(2);
307 | r[0].toString().should.equal(fs.readFileSync(index).toString());
308 | r[1].should.equal('timer2 return');
309 |
310 | done();
311 | })
312 | .run();
313 | });
314 |
315 | it('should access exceptions into errorHandle when using parallel(arr, iterator[, callback]) mode', function(done) {
316 | // mocha can not caught errors when working with node v0.8.x
317 | if(process.version.match(/v0.8/)) return done();
318 |
319 | var d = domain.create();
320 | var c = 0;
321 |
322 | d.on('error', function(err) {
323 | c.should.equal(1);
324 | err.message.should.equal('non_existent.js was not found');
325 | done();
326 | });
327 |
328 | d.enter();
329 |
330 | Stepify()
331 | .step(function() {
332 | c++;
333 | var mock = files.splice(0);
334 | mock.splice(1, 0, 'non_existent.js');
335 | this.parallel(mock, fs.readFile, 'utf8');
336 | })
337 | .step(function() {
338 | // should not step into this step
339 | c++;
340 | })
341 | .error(function(err) {
342 | // rewrite err for testing...
343 | if(err) err = 'non_existent.js was not found';
344 | throw new Error(err);
345 | })
346 | .run();
347 |
348 | d.exit();
349 | });
350 |
351 | it('should access exceptions into errorHandle when using parallel(arr[, callback]) mode', function(done) {
352 | // mocha can not caught errors when working with node v0.8.x
353 | if(process.version.match(/v0.8/)) return done();
354 |
355 | var d = domain.create();
356 | var c = 0;
357 |
358 | d.on('error', function(err) {
359 | c.should.equal(1);
360 | err.message.should.equal('non_existent.js was not found');
361 | done();
362 | });
363 |
364 | d.enter();
365 |
366 | Stepify()
367 | .step(function() {
368 | this.parallel([
369 | function(callback) {
370 | setTimeout(function() {
371 | c++;
372 | // do some more stuff ...
373 | callback(null, 1);
374 | }, 10);
375 | },
376 | function(callback) {
377 | c++;
378 | fs.readFile('non_existent.js', callback);
379 | },
380 | function(callback) {
381 | setTimeout(function() {
382 | c++;
383 | callback(null, 1);
384 | }, 20);
385 | }
386 | ]);
387 | })
388 | .step(function() {
389 | // should not run into this step
390 | c++;
391 | })
392 | .error(function(err) {
393 | // rewrite err for testing...
394 | if(err) err = 'non_existent.js was not found';
395 | throw new Error(err);
396 | })
397 | .run();
398 |
399 | d.exit();
400 | });
401 | });
402 |
403 | // Be careful using this method
404 | describe('#jump()', function() {
405 | it('should support jump(stepName) mode', function(done) {
406 | var steps = [];
407 | Stepify()
408 | .step('a', function() {
409 | steps.push(this.name);
410 | this.done();
411 | })
412 | .step('b', function() {
413 | steps.push(this.name);
414 | this.done();
415 | })
416 | .step(function() {
417 | if(!this.vars('flag')) {
418 | this.jump('a');
419 | this.vars('flag', 1)
420 | } else {
421 | this.next();
422 | }
423 | })
424 | .step('c', function() {
425 | steps.push(this.name);
426 | this.done();
427 | })
428 | .result(function() {
429 | steps.should.eql(['a', 'b', 'a', 'b', 'c']);
430 | done();
431 | })
432 | .run();
433 | });
434 |
435 | it('should support jump(index) mode', function(done) {
436 | var steps = [];
437 | Stepify()
438 | .step('a', function() {
439 | steps.push(this.name);
440 | this.done();
441 | })
442 | .step('b', function() {
443 | steps.push(this.name);
444 | this.done();
445 | })
446 | .step(function() {
447 | if(!this.vars('flag')) {
448 | this.jump(1);
449 | this.vars('flag', 1)
450 | } else {
451 | this.next();
452 | }
453 | })
454 | .step('c', function() {
455 | steps.push(this.name);
456 | this.done();
457 | })
458 | .result(function() {
459 | steps.should.eql(['a', 'b', 'b', 'c']);
460 | done();
461 | })
462 | .run();
463 | });
464 |
465 | it('should support jump(step) mode', function(done) {
466 | var steps = [];
467 | Stepify()
468 | .step('a', function() {
469 | steps.push(this.name);
470 | this.done();
471 | })
472 | .step('b', function() {
473 | steps.push(this.name);
474 | this.done();
475 | })
476 | .step(function() {
477 | if(!this.vars('flag')) {
478 | this.jump(-2);
479 | this.vars('flag', 1)
480 | } else {
481 | this.next();
482 | }
483 | })
484 | .step('c', function() {
485 | steps.push(this.name);
486 | this.done();
487 | })
488 | .result(function() {
489 | steps.should.eql(['a', 'b', 'a', 'b', 'c']);
490 | done();
491 | })
492 | .run();
493 | });
494 | });
495 |
496 | describe('#next()', function() {
497 | it('should pass variables into next step handle', function(done) {
498 | var steps = [];
499 | Stepify()
500 | .step('a', function() {
501 | var root = this;
502 | setTimeout(function() {
503 | steps.push(root.name);
504 | root.next();
505 | }, 500);
506 | })
507 | .step('b', function() {
508 | var root = this;
509 | setTimeout(function() {
510 | steps.push(root.name);
511 | root.next('params will be passed to the next step');
512 | }, 500);
513 | })
514 | .step('c', function(param) {
515 | steps.push(this.name);
516 | param.should.equal('params will be passed to the next step');
517 | this.done();
518 | })
519 | .result(function() {
520 | steps.should.eql(['a', 'b', 'c']);
521 | done();
522 | })
523 | .run();
524 | });
525 | });
526 |
527 | describe('#end()', function() {
528 | it('should stop executing the rest tasks(or steps) when end([null]) called', function(done) {
529 | var c = 0;
530 | var execd = [];
531 | Stepify()
532 | .step(function() {
533 | var root = this;
534 | setTimeout(function() {
535 | c++;
536 | execd.push(root.name);
537 | root.done();
538 | }, 300);
539 | })
540 | .step(function() {
541 | var root = this;
542 | setTimeout(function() {
543 | c++;
544 | execd.push(root.name);
545 | // return root.end(null);
546 | root.done(null, function() {
547 | root.end();
548 | });
549 | }, 200);
550 | })
551 | .step(function() {
552 | var root = this;
553 | setTimeout(function() {
554 | c++;
555 | execd.push(root.name);
556 | root.done(null);
557 | }, 300);
558 | })
559 | .pend()
560 | .step('foo', function() {
561 | var root = this;
562 | setTimeout(function() {
563 | c++;
564 | execd.push(root.name);
565 | root.done(null);
566 | }, 300);
567 | })
568 | .result(function() {
569 | c.should.equal(3);
570 | execd.should.eql(['_UNAMED_STEP_0', '_UNAMED_STEP_1', 'foo']);
571 | done();
572 | })
573 | .run();
574 | });
575 |
576 | it('should access error to errorHandle when end(error) called', function(done) {
577 | var c = 0;
578 | var flag = 0;
579 | Stepify()
580 | .task('foo')
581 | .step(function() {
582 | var root = this;
583 | setTimeout(function() {
584 | c++;
585 | root.done();
586 | }, 500);
587 | })
588 | .step(function() {
589 | var root = this;
590 | setTimeout(function() {
591 | c++;
592 | root.end(new Error('There sth error.'));
593 | }, 200);
594 | })
595 | .task('bar')
596 | .step(function() {
597 | var root = this;
598 | setTimeout(function() {
599 | c++;
600 | root.done();
601 | }, 300);
602 | })
603 | .pend()
604 | .error(function(err) {
605 | flag++;
606 | err.message.should.equal('There sth error.');
607 | // continue executing when error accuring
608 | this.next();
609 | })
610 | .result(function() {
611 | c.should.equal(3);
612 | flag.should.equal(1);
613 | done();
614 | })
615 | .run();
616 | });
617 | });
618 | });
619 |
--------------------------------------------------------------------------------
/test/task.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert');
2 | var should = require('should');
3 |
4 | var Stepify = require('../index');
5 |
6 | // for test...
7 | var fs = require('fs');
8 | var path = require('path');
9 | var exec = require('child_process').exec;
10 | var domain = require('domain');
11 |
12 | describe('Stepify', function() {
13 | describe('#Stepify()', function() {
14 | it('should get an instanceOf Stepify with new', function() {
15 | var myTask = new Stepify();
16 | myTask.should.be.an.instanceOf(Stepify);
17 | });
18 |
19 | it('should get an instanceOf Stepify without new', function() {
20 | var myTask = Stepify();
21 | myTask.should.be.an.instanceOf(Stepify);
22 | });
23 | });
24 |
25 | describe('#debug()', function() {
26 | var myTask = Stepify();
27 |
28 | it('should be set to "false" by default', function() {
29 | myTask._debug.should.equal(false);
30 | });
31 |
32 | it('should be reset to "true"', function() {
33 | myTask.debug(true);
34 | myTask._debug.should.equal(true);
35 | });
36 | });
37 |
38 | describe('#task()', function() {
39 | it('shoule executed without error', function(done) {
40 | Stepify()
41 | .task('foo')
42 | .step('setTimeout', function() {
43 | var root = this;
44 | setTimeout(function() {
45 | root.done(null, 'foo ok');
46 | }, 300);
47 | })
48 | .step('readFile', function(str) {
49 | fs.readFile(__filename, this.done.bind(this));
50 | })
51 | .result(function() {
52 | done(null);
53 | })
54 | .run();
55 | });
56 |
57 | it('shoule executed without error even if the task() method has not explicitly called', function(done) {
58 | Stepify()
59 | .step('setTimeout', function() {
60 | this.taskName.should.equal('_UNAMED_TASK_0');
61 | var root = this;
62 | setTimeout(function() {
63 | root.done(null, 'foo ok');
64 | }, 300);
65 | })
66 | .step('readFile', function(str) {
67 | fs.readFile(__filename, this.done.bind(this));
68 | })
69 | .result(function() {
70 | done(null);
71 | })
72 | .run();
73 | });
74 |
75 | it('should work well when multiply tasks were added', function(done) {
76 | Stepify()
77 | .task('task1')
78 | .step(function() {
79 | fs.readFile(__filename, this.done.bind(this));
80 | })
81 | .step('timer')
82 | .timer(function() {
83 | var root = this;
84 | setTimeout(function() {
85 | root.done(null);
86 | }, 100);
87 | })
88 | .task('task2')
89 | .step('foo', function() {
90 | var root = this;
91 | setTimeout(function() {
92 | root.done(null);
93 | }, 100);
94 | })
95 | .step(function() {
96 | fs.readFile(__filename, this.done.bind(this));
97 | })
98 | .result(function() {
99 | done(null);
100 | })
101 | .run();
102 | });
103 | });
104 |
105 | describe('#step()', function() {
106 | it('should throw error if nothing has been accessed to step()', function() {
107 | (function() {
108 | Stepify()
109 | .step()
110 | .run();
111 | }).should.throw('Step handle should be accessed.');
112 | });
113 |
114 | it('should throw error if the accessed `stepName` was preset within the construtor', function() {
115 | var inserts = ['debug', 'task', 'step', 'pend', 'error', 'result', 'run'];
116 | var name = inserts[Math.floor(Math.random()*inserts.length)];
117 | (function() {
118 | Stepify()
119 | .step(name)
120 | .run();
121 | }).should.throw('The name `' + name + '` was preset within the construtor, try another one?');
122 | });
123 |
124 | it('should execute without error even if `stepName` param has not accessed into step() method', function(done) {
125 | Stepify()
126 | .step(function() {
127 | this.name.should.equal('_UNAMED_STEP_0');
128 | var root = this;
129 | setTimeout(function() {
130 | root.done(null);
131 | }, 100);
132 | })
133 | .step('foo', function() {
134 | this.name.should.equal('foo');
135 | var root = this;
136 | setTimeout(function() {
137 | root.done(null);
138 | }, 200);
139 | })
140 | .step(function() {
141 | this.name.should.equal('_UNAMED_STEP_2');
142 | var root = this;
143 | setTimeout(function() {
144 | root.done(null);
145 | }, 120);
146 | })
147 | .result(function() {
148 | done(null);
149 | })
150 | .run();
151 | });
152 |
153 | it('should execute without error even if stepHandle defined after `step(stepName)` called', function(done) {
154 | Stepify()
155 | .step('foo', 123)
156 | .foo(function(n) {
157 | var root = this;
158 | setTimeout(function() {
159 | root.done(null, n);
160 | }, 200);
161 | })
162 | .step(function(n) {
163 | n.should.equal(123);
164 | this.done(null);
165 | })
166 | .result(function() {
167 | done(null);
168 | })
169 | .run();
170 | });
171 |
172 | it('should support multiply steps to be added', function(done) {
173 | var steps = [];
174 | Stepify()
175 | .step(function() {
176 | var root = this;
177 | setTimeout(function() {
178 | steps.push('step1');
179 | root.done(null);
180 | }, 200);
181 | })
182 | .step(function() {
183 | var root = this;
184 | setTimeout(function() {
185 | steps.push('step2');
186 | root.done(null);
187 | }, 100);
188 | })
189 | .step('step3', function() {
190 | var root = this;
191 | setTimeout(function() {
192 | steps.push('step3');
193 | root.done(null);
194 | }, 100);
195 | })
196 | .step('step4')
197 | .step4(function() {
198 | var root = this;
199 | setTimeout(function() {
200 | steps.push('step4');
201 | root.done(null);
202 | }, 100);
203 | })
204 | .result(function() {
205 | steps.should.have.length(4);
206 | steps.should.eql(['step1', 'step2', 'step3', 'step4']);
207 | done(null);
208 | })
209 | .run();
210 | });
211 |
212 | it('should support multiply tasks and multiply steps to be added', function(done) {
213 | var n = 0;
214 | Stepify()
215 | .task('task1')
216 | .step('task1_step1', function() {
217 | n++;
218 | var root = this;
219 | setTimeout(function() {
220 | root.done(null, n);
221 | }, 200);
222 | })
223 | .step('task1_step2', function() {
224 | n++;
225 | var root = this;
226 | setTimeout(function() {
227 | root.done(null, n);
228 | }, 200);
229 | })
230 | .task()
231 | .step('task2_step1', function() {
232 | n++;
233 | fs.readdir(__dirname, this.done.bind(this));
234 | })
235 | .step('task2_step2', function() {
236 | n++;
237 | fs.stat(__dirname, this.done.bind(this));
238 | })
239 | .result(function() {
240 | n.should.equal(4);
241 | done(null);
242 | })
243 | .run();
244 | });
245 |
246 | it('should support common stepHandle which defined after task pended when multiply tasks added', function(done) {
247 | var testStr = fs.readFileSync(__filename).toString();
248 | var indexStr = fs.readFileSync(path.resolve(__dirname, '../index.js')).toString();
249 |
250 | var fileStr = [];
251 | var statCount = 0;
252 | var timerArr = [];
253 |
254 | Stepify()
255 | .task('t1')
256 | .step('readFile', __filename)
257 | .step('stat')
258 | .step('timer')
259 | .task('t2')
260 | .step('readFile', path.resolve(__dirname, '../index.js'))
261 | .step('stat')
262 | .step('timer')
263 | .timer(function() {
264 | var root = this;
265 | setTimeout(function() {
266 | timerArr.push(100);
267 | root.done(null);
268 | }, 100);
269 | })
270 | .pend()
271 | .readFile(function(p) {
272 | var root = this;
273 | fs.readFile(p, function(err, str) {
274 | if(err) throw err;
275 | fileStr.push(str.toString());
276 | root.done(null);
277 | });
278 | })
279 | .stat(function() {
280 | var root = this;
281 | fs.stat(__dirname, function(err, stat) {
282 | if(err) throw err;
283 | statCount++;
284 | root.done(null);
285 | });
286 | })
287 | .timer(function() {
288 | var root = this;
289 | setTimeout(function() {
290 | timerArr.push(200);
291 | root.done(null);
292 | }, 200);
293 | })
294 | .result(function() {
295 | fileStr.should.have.length(2);
296 | fileStr[0].should.equal(testStr);
297 | fileStr[1].should.equal(indexStr);
298 | statCount.should.equal(2);
299 | timerArr.should.eql([200, 100]);
300 | done(null);
301 | })
302 | .run();
303 | });
304 | });
305 |
306 | describe('#pend()', function() {
307 | it('should work well even if not called before `run()` method called', function(done) {
308 | Stepify()
309 | .step(function() {
310 | var root = this;
311 | setTimeout(function() {
312 | root.done(null);
313 | }, 100);
314 | })
315 | .step(function() {
316 | var root = this;
317 | setTimeout(function() {
318 | root.done(null);
319 | }, 200);
320 | })
321 | .result(function() {
322 | done(null);
323 | })
324 | .run();
325 | });
326 |
327 | it('should be a multiply workflow after using `pend()` to split steps', function(done) {
328 | var taskNames = [];
329 | Stepify()
330 | .step(function() {
331 | var name = this.taskName;
332 | var root = this;
333 | setTimeout(function() {
334 | if(taskNames.indexOf(name) === -1) {
335 | taskNames.push(name);
336 | }
337 | root.done(null);
338 | }, 200);
339 | })
340 | .step(function() {
341 | var name = this.taskName;
342 | var root = this;
343 | fs.readFile(__filename, function(err) {
344 | if(err) throw err;
345 | if(taskNames.indexOf(name) === -1) {
346 | taskNames.push(name);
347 | }
348 | root.done(null);
349 | });
350 | })
351 | .pend()
352 | .step(function() {
353 | var name = this.taskName;
354 | var root = this;
355 | fs.stat(__dirname, function(err) {
356 | if(err) throw err;
357 | if(taskNames.indexOf(name) === -1) {
358 | taskNames.push(name);
359 | }
360 | root.done(null);
361 | });
362 | })
363 | .result(function() {
364 | taskNames.should.have.length(2);
365 | taskNames[0].should.not.equal(taskNames[1]);
366 | done();
367 | })
368 | .run();
369 | });
370 | });
371 |
372 | describe('#error()', function() {
373 | var c1 = 0, c2 = 0, c3 = 0;
374 | var d = domain.create();
375 |
376 | describe('use default errorHandle case', function() {
377 | // mocha can not caught errors when working with node v0.8.x
378 | if(process.version.match(/v0.8/)) return;
379 |
380 | it('should simplily throw error if error method has not defined for task', function(done) {
381 | var errHandle = function(err) {
382 | err.message.should.equal('There sth error!');
383 | done();
384 | d.removeListener('error', errHandle);
385 | d.exit();
386 | };
387 |
388 | d.on('error', errHandle);
389 |
390 | d.run(function() {
391 | Stepify()
392 | .step(function() {
393 | var root = this;
394 | setTimeout(function() {
395 | c1++;
396 | root.done(null);
397 | }, 200);
398 | })
399 | .step(function() {
400 | var root = this;
401 | setTimeout(function() {
402 | c1++;
403 | root.done('There sth error!');
404 | }, 100);
405 | })
406 | .step(function() {
407 | var root = this;
408 | setTimeout(function() {
409 | c1++;
410 | root.done(null);
411 | }, 300);
412 | })
413 | .run();
414 | });
415 | });
416 | });
417 |
418 | describe('use customed errorHandle case', function() {
419 | it('should access error to `error()` method witch defined manually', function(done) {
420 | Stepify()
421 | .step(function() {
422 | var root = this;
423 | setTimeout(function() {
424 | c2++;
425 | root.done(null);
426 | }, 200);
427 | })
428 | .step(function() {
429 | var root = this;
430 | setTimeout(function() {
431 | c2++;
432 | root.done('There sth error...');
433 | }, 100);
434 | })
435 | .step(function() {
436 | var root = this;
437 | setTimeout(function() {
438 | c2++;
439 | root.done(null);
440 | }, 300);
441 | })
442 | .error(function(err) {
443 | err.should.equal('There sth error...');
444 | c2.should.equal(2);
445 | done();
446 | })
447 | .run();
448 | });
449 | });
450 |
451 | describe('use customed errorHandle and multiply tasks case', function() {
452 | // mocha can not caught errors when working with node v0.8.x
453 | if(process.version.match(/v0.8/)) return;
454 |
455 | it('should stop executing immediate error occured', function(done) {
456 | var errHandle = function(err) {
457 | err.message.should.equal('The file not_exist.js was not found.');
458 | done();
459 | d.removeListener('error', errHandle);
460 | d.exit();
461 | };
462 |
463 | d.on('error', errHandle);
464 |
465 | d.run(function() {
466 | Stepify()
467 | .task('foo')
468 | .step(function() {
469 | var root = this;
470 | setTimeout(function() {
471 | c3++;
472 | root.done(null);
473 | }, 300);
474 | })
475 | .step(function() {
476 | var root = this;
477 | fs.readFile(path.join(__dirname, 'not_exist.js'), function(err) {
478 | c3++;
479 | if(err) err = 'The file not_exist.js was not found.';
480 | root.done(err);
481 | });
482 | })
483 | .error(function(err) {
484 | throw new Error('The file not_exist.js was not found.');
485 | })
486 | .pend()
487 | .task('bar')
488 | .step(function() {
489 | var root = this;
490 | setTimeout(function() {
491 | c3++;
492 | root.done(null);
493 | }, 300);
494 | })
495 | .step(function() {
496 | var root = this;
497 | setTimeout(function() {
498 | c3++;
499 | root.done('should not executed ever.');
500 | }, 100);
501 | })
502 | .run();
503 | });
504 | });
505 | });
506 | });
507 |
508 | describe('#result()', function() {
509 | it('should execute after all tasks finish without error', function(done) {
510 | var flag = 0;
511 |
512 | Stepify()
513 | .task('foo')
514 | .step(function() {
515 | var root = this;
516 | setTimeout(function() {
517 | root.fulfill(100);
518 | root.done(null);
519 | }, 100);
520 | })
521 | .step(function() {
522 | var root = this;
523 | fs.readFile(__filename, function(err, str) {
524 | if(err) return root.done(err);
525 | str = str.toString();
526 | root.fulfill(str);
527 | root.done();
528 | });
529 | })
530 | .result(function(result) {
531 | result.should.eql([100, fs.readFileSync(__filename).toString()]);
532 | flag = 1;
533 | done();
534 | })
535 | .run();
536 | });
537 |
538 | it('finishHandle can be accessed as the first param of `Stepify()`', function(done) {
539 | var flag = 0;
540 | var finishHandle = function(result) {
541 | result.should.eql([100, fs.readFileSync(__filename).toString()]);
542 | flag = 1;
543 | done(null);
544 | };
545 |
546 | Stepify(finishHandle)
547 | .task('foo')
548 | .step(function() {
549 | var root = this;
550 | setTimeout(function() {
551 | root.fulfill(100);
552 | root.done(null);
553 | }, 100);
554 | })
555 | .step(function() {
556 | var root = this;
557 | fs.readFile(__filename, function(err, str) {
558 | if(err) return root.done(err);
559 | str = str.toString();
560 | root.fulfill(str);
561 | root.done(null);
562 | });
563 | })
564 | .run();
565 | });
566 | });
567 |
568 | describe('#run()', function() {
569 | var a = [];
570 |
571 | describe('executing tasks by the order the tasks was defined', function() {
572 | it('should execute without error', function(done) {
573 | Stepify()
574 | .step('timer', 200)
575 | .step(function() {
576 | var root = this;
577 | setTimeout(function() {
578 | root.done(null);
579 | }, 100);
580 | })
581 | .step(function() {
582 | fs.readFile(__filename, this.done.bind(this));
583 | })
584 | .task()
585 | .step('readFile', function() {
586 | fs.readFile(__filename, this.done.bind(this));
587 | })
588 | .step('timer', 300)
589 | .pend()
590 | .timer(function(timeout) {
591 | a.push(timeout);
592 | var root = this;
593 | setTimeout(function() {
594 | root.done(null);
595 | }, timeout);
596 | })
597 | .result(function() {
598 | done();
599 | })
600 | .run();
601 | });
602 | });
603 |
604 | after(function() {
605 | a.should.have.length(2);
606 | a[0].should.equal(200);
607 | a[1].should.equal(300);
608 | });
609 |
610 | describe('executing tasks by the order customized', function() {
611 | it('should execute without error when ordering with task name', function(done) {
612 | Stepify()
613 | .task('task1')
614 | .step(function() {
615 | var root = this;
616 | fs.readdir(__dirname, function(err) {
617 | if(err) throw err;
618 | root.fulfill(root.taskName + '.step1');
619 | root.done(null);
620 | });
621 | })
622 | .step('sleep')
623 | .step('exec', 'cat', __filename)
624 | .task('task2')
625 | .step('sleep')
626 | .step(function() {
627 | var root = this;
628 | setTimeout(function() {
629 | root.fulfill(root.taskName + '.step2');
630 | root.done(null);
631 | }, 300);
632 | })
633 | .step('exec', 'ls', '-l')
634 | .task('task3')
635 | .step('readFile', __filename)
636 | .step('timer', function() {
637 | var root = this;
638 | setTimeout(function() {
639 | root.fulfill(root.taskName + '.step2');
640 | root.done(null);
641 | }, 300);
642 | })
643 | .step('sleep')
644 | .readFile(function(p) {
645 | var root = this;
646 | fs.readFile(p, function(err) {
647 | if(err) throw err;
648 | root.fulfill('readFile.' + p);
649 | root.done(null);
650 | });
651 | })
652 | .pend()
653 | .sleep(function() {
654 | var root = this;
655 | setTimeout(function() {
656 | root.fulfill(root.taskName + '.sleep');
657 | root.done(null);
658 | }, 200);
659 | })
660 | .exec(function(cmd, args) {
661 | cmd = [].slice.call(arguments, 0);
662 | var root = this;
663 | exec(cmd.join(' '), function(err) {
664 | if(err) throw err;
665 | root.fulfill('exec.' + cmd.join('.'));
666 | root.done(null);
667 | });
668 | })
669 | .result(function(r) {
670 | r.should.eql([
671 | 'task1.step1', 'task1.sleep', 'exec.cat.' + __filename,
672 | 'readFile.' + __filename, 'task3.step2', 'task3.sleep',
673 | 'task2.sleep', 'task2.step2', 'exec.ls.-l'
674 | ]);
675 | done(null);
676 | })
677 | .run('task1', 'task3', 'task2');
678 | });
679 |
680 | it('should execute without error when ordering with task index', function(done) {
681 | Stepify()
682 | .task()
683 | .step(function() {
684 | var root = this;
685 | fs.readdir(__dirname, function(err) {
686 | if(err) throw err;
687 | root.fulfill(root.taskName + '.step1');
688 | root.done(null);
689 | });
690 | })
691 | .step('sleep')
692 | .step('exec', 'cat', __filename)
693 | .task()
694 | .step('sleep')
695 | .step(function() {
696 | var root = this;
697 | setTimeout(function() {
698 | root.fulfill(root.taskName + '.step2');
699 | root.done(null);
700 | }, 300);
701 | })
702 | .step('exec', 'ls', '-l')
703 | .task()
704 | .step('readFile', __filename)
705 | .step('timer', function() {
706 | var root = this;
707 | setTimeout(function() {
708 | root.fulfill(root.taskName + '.step2');
709 | root.done(null);
710 | }, 300);
711 | })
712 | .step('sleep')
713 | .readFile(function(p) {
714 | var root = this;
715 | fs.readFile(p, function(err) {
716 | if(err) throw err;
717 | root.fulfill('readFile.' + p);
718 | root.done(null);
719 | });
720 | })
721 | .pend()
722 | .sleep(function() {
723 | var root = this;
724 | setTimeout(function() {
725 | root.fulfill(root.taskName + '.sleep');
726 | root.done(null);
727 | }, 200);
728 | })
729 | .exec(function(cmd, args) {
730 | cmd = [].slice.call(arguments, 0);
731 | var root = this;
732 | exec(cmd.join(' '), function(err) {
733 | if(err) throw err;
734 | root.fulfill('exec.' + cmd.join('.'));
735 | root.done(null);
736 | });
737 | })
738 | .result(function(r) {
739 | r.should.eql([
740 | '_UNAMED_TASK_0.step1', '_UNAMED_TASK_0.sleep', 'exec.cat.' + __filename,
741 | 'readFile.' + __filename, '_UNAMED_TASK_2.step2', '_UNAMED_TASK_2.sleep',
742 | '_UNAMED_TASK_1.sleep', '_UNAMED_TASK_1.step2', 'exec.ls.-l'
743 | ]);
744 | done(null);
745 | })
746 | .run(0, 2, 1);
747 | });
748 |
749 | it('should execute without error when synchronous and asynchronous tasks mixed', function(done) {
750 | Stepify()
751 | .task('task1')
752 | .step(function() {
753 | var root = this;
754 | fs.readdir(__dirname, function(err) {
755 | if(err) throw err;
756 | root.done(null);
757 | });
758 | })
759 | .step('sleep')
760 | .step('exec', 'cat', __filename)
761 | .task('task2')
762 | .step('sleep')
763 | .step(function() {
764 | var root = this;
765 | setTimeout(function() {
766 | root.done(null);
767 | }, 300);
768 | })
769 | .step('exec', 'ls', '-l')
770 | .task('task3')
771 | .step('readFile', __filename)
772 | .step('timer', function() {
773 | var root = this;
774 | setTimeout(function() {
775 | root.done(null);
776 | }, 300);
777 | })
778 | .step('sleep')
779 | .readFile(function(p) {
780 | var root = this;
781 | fs.readFile(p, function(err) {
782 | if(err) throw err;
783 | root.done(null);
784 | });
785 | })
786 | .task('task4')
787 | .step('sleep')
788 | .step(function(p) {
789 | var root = this;
790 | fs.readFile(p, function(err) {
791 | if(err) throw err;
792 | root.done(null);
793 | });
794 | }, __filename)
795 | .pend()
796 | .sleep(function() {
797 | var root = this;
798 | setTimeout(function() {
799 | root.done(null);
800 | }, 200);
801 | })
802 | .exec(function(cmd, args) {
803 | cmd = [].slice.call(arguments, 0);
804 | var root = this;
805 | exec(cmd.join(' '), function(err) {
806 | if(err) throw err;
807 | root.done(null);
808 | });
809 | })
810 | .result(function(r) {
811 | done(null);
812 | })
813 | .run('task1', ['task4', 'task3'], 'task2');
814 | });
815 | });
816 | });
817 | });
818 |
--------------------------------------------------------------------------------