├── .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 | [![Build Status](https://api.travis-ci.org/chemdemo/node-stepify.png)](http://travis-ci.org/chemdemo/node-stepify) 3 | [![NPM version](https://badge.fury.io/js/stepify.png)](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 | --------------------------------------------------------------------------------