├── .babelrc ├── .gitattributes ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── dist ├── lightflow.js ├── lightflow.min.js └── lightflow.min.js.map ├── lib ├── 0.x │ └── index.js ├── index.js └── lts │ └── index.js ├── package.json ├── src └── lightflow.js └── test ├── index.js └── test.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env" : { 3 | "node" : { 4 | "plugins": ["add-module-exports"], 5 | "presets": [ 6 | ["modern-node", { "version": "6.5" }] 7 | ] 8 | }, 9 | "node_lts" : { 10 | "plugins": ["add-module-exports"], 11 | "presets": [ 12 | ["modern-node", { "version": "5.12" }] 13 | ] 14 | }, 15 | "node_old" : { 16 | "plugins": ["add-module-exports"], 17 | "presets": [ 18 | ["modern-node", { "version": "0.12" }] 19 | ] 20 | }, 21 | "browser" : { 22 | "plugins": ["transform-es2015-modules-umd"], 23 | "presets": [ 24 | ["es2015"] 25 | ] 26 | } 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v2.0.0 4 | ### API changes 5 | - remove `.with` 6 | - add `.race` 7 | - add *count* parameter to `.then` 8 | - add labels 9 | - add optional flags to Lightflow constructor 10 | - remove Classical API 11 | 12 | ### Bugfixes 13 | - add parameters check 14 | - add data object protection between steps and tasks 15 | - add protection to data object corruption after step is done 16 | 17 | ### Other changes 18 | - tests refactor 19 | 20 | ## v1.1.0 21 | - bugfixes 22 | 23 | ## v1.0.0 24 | - initial release -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Anton Sherstiuk (SAPer) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Lightflow 2 | =========== 3 | 4 | > A tiny Promise-inspired control flow library for browser and Node.js. 5 | 6 | [![npm package](https://img.shields.io/npm/v/lightflow.svg?style=flat-square)](https://www.npmjs.org/package/lightflow) 7 | 8 | ## Introduction 9 | Lightflow helps to run asynchronous code in synchronous way without the hassle. 10 | 11 | > **Important note**
12 | > Version 1 of Lightflow is not compatible with version 2+. Version 1 documentation, etc. has been moved to [this branch](https://github.com/saperio/lightflow/tree/1.x).
13 | > For notable changes, see [changelog](CHANGELOG.md). 14 | 15 | ### Usage 16 | - Create an *lightflow* instance `lightflow()`. 17 | - Describe your flow by adding a series of asynchronous functions - *steps* with `.then`, `.race`, `.error`, `.catch` and `.done`. 18 | - And then start, stop, restart, and even loop the flow as much as you needed, passing the new data on each run with `.start`, `.stop` and `.loop`. 19 | 20 | ### Quick example 21 | ```js 22 | import lightflow from 'lightflow'; 23 | 24 | lightflow() 25 | .then(({ next, error, data }) => { 26 | const { filename } = data; 27 | fs.readFile(filename, (err, content) => err ? error(err) : next({ raw : content, filename })); 28 | }) 29 | .then(({ next, error, data }) => { 30 | try { 31 | data.parsed = JSON.parse(data.raw); 32 | next(data); 33 | } 34 | catch (err) { 35 | error(err); 36 | } 37 | }) 38 | .done(data => { 39 | console.log(`This is content of ${data.filename}: ${data.parsed}`); 40 | }) 41 | .catch(err => { 42 | console.log(`Error: ${err}`); 43 | }) 44 | .start({ filename : 'file.json' }) 45 | ; 46 | ``` 47 | 48 | ### Differences from Promise 49 | - Simpler API. 50 | - When you run asynchronous functions with callbacks, you should not care about promisification. Simply use them in your flow. 51 | - Lightflow is not for one-time execution thing. Once you described it, you can start, stop and restart it many times. 52 | 53 | ## Installation 54 | ### Browser 55 | ```shell 56 | git clone https://github.com/saperio/lightflow.git 57 | ``` 58 | Use UMD module located in [dist](dist/) 59 | ```html 60 | 61 | ``` 62 | or just 63 | ```html 64 | 65 | ``` 66 | 67 | ### Node.js 68 | ```shell 69 | npm install lightflow --save 70 | ``` 71 | #### Node.js version >= 6.5 72 | ```js 73 | var lightflow = require('lightflow'); 74 | ``` 75 | 76 | #### Node.js version >= 4.x 77 | ```js 78 | var lightflow = require('lightflow/lib/lts'); 79 | ``` 80 | 81 | #### Node.js version >= 0.x 82 | ```js 83 | var lightflow = require('lightflow/lib/0.x'); 84 | ``` 85 | 86 | ## API 87 | 88 | * [`lightflow`](#lightflow-1) 89 | * [`.then`](#then) 90 | * [`.race`](#race) 91 | * [`.error`](#error) 92 | * [`.catch`](#catch) 93 | * [`.done`](#done) 94 | * [`.start`](#start) 95 | * [`.stop`](#stop) 96 | * [`.loop`](#loop) 97 | 98 | All api function are divided in two groups: functions for describe the flow and functions for control the flow. Functions in the first group accept one or more tasks (with optional contexts). All of them return `this` for handy chaining. 99 | 100 | ### lightflow 101 | ```js 102 | lightflow(params?: { 103 | datafencing?: boolean 104 | }) 105 | ``` 106 | Use `lightflow()` to create new flow instance. You can pass optional parameters object with some (just one for now) flags: 107 | * datafencing - (default - true) copy data object between steps and parallel tasks to prevent corrupting it in one task from another. 108 | 109 | ### .then 110 | ```js 111 | .then(task: string | TaskFn | Lightflow, context?: any, ...): this 112 | type taskFn = (param: taskFnParam) => void 113 | type taskFnParam = { 114 | error: (err?: any) => void; 115 | next: (data?: any, label?: string) => void; 116 | count: (c: number) => void; 117 | data: any; 118 | } 119 | ``` 120 | #### Overview 121 | `.then` adds one or more tasks (with optional contexts) to the flow. If first parameter is a string, then other parameters are ignored and this step used as label. All the tasks run in parallel, their output data objects are merged and passed to the next step. Each task can be function or another Lightflow instance. 122 | 123 | #### Task function parameters 124 | Task function will receive single parameter with this fields: 125 | - `next` - function to be called, when task is finished. Can take data for the next step. 126 | - `error` - function to be called, when error occurred. You can pass error object to it. 127 | - `count` - function, can be used to indicate how many times task assume to call `next` before flow marks this task as complete. If not called - flow will accept only one `next` call and ignore results from the others from within current task. 128 | - `data` - data object from previous step. 129 | 130 | #### Labels 131 | With the labels, you can mark steps in the flow, which you can jump to from one step, ignore the others. Labels are added to the flow in this way: `.then ('somelabel')`, so we created a label named *somelabel*. To jump to this label, you need to call the function `next` inside the task with two parameters: data object, as usual, and the label name - `next (data, 'somelabel');`. If you pass the nonexistent label, then there will be no jump, the next step will be executed. Using labels you can jump only forward, this is done in order not to create an infinite loop. If you need to loop your flow, use [`.loop`](#loop). 132 | 133 | 134 | #### Examples 135 | Simple, one step flow 136 | ```js 137 | lightflow() 138 | .then(({ next, error, data }) => { 139 | doAsync(data, (err, out) => { 140 | if (err) { 141 | error(e); 142 | } else { 143 | next(out); 144 | } 145 | }); 146 | }) 147 | .start(somedata) 148 | ; 149 | ``` 150 | 151 | Here example with two parallel tasks on one step: 152 | ```js 153 | lightflow() 154 | .then( 155 | ({ next, data }) => { 156 | fetchUrl(data.url, remote => next({ remote })); 157 | }, 158 | ({ next, error, data }) => { 159 | fs.readFile(data.filename, (err, local) => err ? error(err) : next({ local })); 160 | } 161 | ) 162 | .then(({ next, data }) => { 163 | const { remote, local } = data; 164 | // ... use remote and local 165 | next(); 166 | }) 167 | .start({ 168 | url: 'google.com', 169 | filename: 'config.json' 170 | }) 171 | ; 172 | ``` 173 | 174 | In the following example task gets a list of filenames, reads files in parallel and passes results into a list of strings. 175 | If you comment `count(data.length);` line, all files will be read but only content of the first one will be passed to the next step. 176 | ```js 177 | lightflow() 178 | .then(({ next, count, data }) => { 179 | let res = []; 180 | // data - array with filenames 181 | count(data.length); 182 | data.forEach(filename => { 183 | fs.readFile(filename, (err, content) => { 184 | res.push(content); 185 | next(res); 186 | }); 187 | }); 188 | }) 189 | .then(({ next, data }) => { 190 | // here data - is array of files contents 191 | next(); 192 | }) 193 | .start(['file1.json', 'file2.json']) 194 | ; 195 | ``` 196 | 197 | Labels example: 198 | ```js 199 | lightflow() 200 | .then(({ next, data }) => { 201 | next(data, 'jumphere') 202 | }) 203 | .then(({ next, data }) => { 204 | // never get here 205 | }) 206 | .then('jumphere') 207 | .then(({ next, data }) => { 208 | // and here we are 209 | next(data); 210 | }) 211 | .start() 212 | ; 213 | ``` 214 | 215 | Use one flow as task in another flow: 216 | ```js 217 | const parse = lightflow() 218 | .then(({ next, error, data }) => { 219 | doParse(data.raw, (err, parsed) => err ? error(err) : next({ parsed })); 220 | }) 221 | ; 222 | 223 | lightflow() 224 | .then(({ next, data }) => { 225 | fetchUrl(data.url, raw => next({ raw })); 226 | }) 227 | .then(parse) 228 | .then(({ next, data }) => { 229 | const { parsed } = data; 230 | // use parsed 231 | next(data); 232 | }) 233 | .start({ url: 'google.com' }) 234 | ; 235 | ``` 236 | 237 | ### .race 238 | ```js 239 | .race(task: string | TaskFn | Lightflow, context?: any, ...): this 240 | type taskFn = (param: taskFnParam) => void 241 | type taskFnParam = { 242 | error: (err?: any) => void; 243 | next: (data?: any, label?: string) => void; 244 | count: (c: number) => void; 245 | data: any; 246 | } 247 | ``` 248 | 249 | `.race` same as `.then`, except that the result only from the first completed task used for the next step. 250 | ```js 251 | lightflow() 252 | .race( 253 | // first race task 254 | ({ next, data }) => { 255 | setTimeout(() => { 256 | data.t1 = true; 257 | next(data); 258 | }, 50) 259 | }, 260 | 261 | // second race task 262 | ({ next, data }) => { 263 | setTimeout(() => { 264 | data.t2 = true; 265 | next(data); 266 | }, 100) 267 | } 268 | ) 269 | // wait a little longer 270 | .then(({ next, data }) => setTimeout(() => next(data), 100)) 271 | .then(({ next, data }) => { 272 | const { t1, t2 } = data; 273 | // here t1 === true and t2 === undefined 274 | next(); 275 | }) 276 | .start({}) 277 | ; 278 | ``` 279 | 280 | ### .error 281 | ```js 282 | .error(handler: ErrorFn, context?: any): this 283 | type ErrorFn = (param?: any) => any 284 | ``` 285 | 286 | Adds an error handler for the preceding step. Triggered when error occurs in the step it follows. Can be added many times. As a parameter gets the object passed to the `error` function. If handler returns something non-undefined, flow will continue and use this object as data for next step, otherwise flow will stop. 287 | 288 | In the next example error is handled only from `doAsync2`, not from `doAsync1`. 289 | ```js 290 | lightflow() 291 | .then(({ next, error }) => { 292 | doAsync1(err => err ? error(err) : next()); 293 | }) 294 | .then(({ next, error }) => { 295 | doAsync2(err => err ? error(err) : next()); 296 | }) 297 | .error(e => { 298 | console.log(`Error: ${e}`); 299 | }) 300 | .start() 301 | ; 302 | ``` 303 | 304 | ### .catch 305 | ```js 306 | .catch(handler: CatchFn, context?: any): this 307 | type CatchFn = (param?: any) => void 308 | ``` 309 | 310 | Adds an error handler to the flow. Catches errors from the **all** steps added before the `catch`. Can be added many times. As a parameter gets the object passed to the `error` function. 311 | 312 | In the following example error is handled from both `doAsync2` and `doAsync1`. 313 | ```js 314 | lightflow() 315 | .then(({ next, error }) => { 316 | doAsync1(err => err ? error(err) : next()); 317 | }) 318 | .then(({ next, error }) => { 319 | doAsync2(err => err ? error(err) : next()); 320 | }) 321 | .catch(e => { 322 | console.log(`Error: ${e}`); 323 | }) 324 | .start() 325 | ; 326 | ``` 327 | 328 | ### .done 329 | ```js 330 | .done(task: DoneFn, context?: any): this 331 | type DoneFn = (data: any) => void 332 | ``` 333 | 334 | Adds a final task to the flow. Regardless of where it's defined, called after **all** other steps, if errors don't occur. Can be added many times. Task function gets data from last step. 335 | 336 | ```js 337 | lightflow() 338 | .done(data => { 339 | console.log(data); 340 | }) 341 | .then(({ next }) => { 342 | doAsync(out => next(out)); 343 | }) 344 | .start() 345 | ; 346 | ``` 347 | 348 | ### .start 349 | ```js 350 | .start(data?: any): this 351 | ``` 352 | Starts the flow. Takes optional data object, pass it to the first step. 353 | 354 | 355 | ### .stop 356 | ```js 357 | .stop(handler?: StopFn, context?: any): this 358 | type StopFn = (data?: any) => void 359 | ``` 360 | Stops the flow processing. Can take optional `handler` parameter (and it's `context`). This optional handler is called when current step is finished and output data is received from it. 361 | 362 | In the following example the output from `doAsync1` will be printed to the console. 363 | ```js 364 | const flow = lightflow() 365 | .then(({ next }) => { 366 | doAsync1(out => next(out)); 367 | }) 368 | .then(({ next }) => { 369 | doAsync2(out => next(out)); 370 | }) 371 | .start() 372 | ; 373 | 374 | flow.stop(data => { 375 | console.log(data); 376 | }) 377 | ``` 378 | 379 | ### .loop 380 | ```js 381 | .loop(flag?: boolean): this 382 | ``` 383 | Sets loop flag for the flow. If set, after call `start` flow do not stop after all steps processed and starts from first step, until `stop` called. Call `loop(false)` and flow will stop after last step. 384 | 385 | This code prints increasing by one number every second: 386 | ```js 387 | lightflow() 388 | .then(({ next, data }) => { 389 | setTimeout(() => next(++data), 1000); 390 | }) 391 | .then(({ next, data }) => { 392 | console.log(data); 393 | next(data); 394 | }) 395 | .loop() 396 | .start(0) 397 | ; 398 | ``` 399 | 400 | ## Build and test 401 | ```shell 402 | npm install 403 | npm run build 404 | npm run test 405 | ``` 406 | 407 | ## License 408 | MIT -------------------------------------------------------------------------------- /dist/lightflow.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | if (typeof define === "function" && define.amd) { 3 | define(['exports'], factory); 4 | } else if (typeof exports !== "undefined") { 5 | factory(exports); 6 | } else { 7 | var mod = { 8 | exports: {} 9 | }; 10 | factory(mod.exports); 11 | global.lightflow = mod.exports; 12 | } 13 | })(this, function (exports) { 14 | 'use strict'; 15 | 16 | Object.defineProperty(exports, "__esModule", { 17 | value: true 18 | }); 19 | 20 | exports.default = function (params) { 21 | return new Lightflow(params || {}); 22 | }; 23 | 24 | function _classCallCheck(instance, Constructor) { 25 | if (!(instance instanceof Constructor)) { 26 | throw new TypeError("Cannot call a class as a function"); 27 | } 28 | } 29 | 30 | var _createClass = function () { 31 | function defineProperties(target, props) { 32 | for (var i = 0; i < props.length; i++) { 33 | var descriptor = props[i]; 34 | descriptor.enumerable = descriptor.enumerable || false; 35 | descriptor.configurable = true; 36 | if ("value" in descriptor) descriptor.writable = true; 37 | Object.defineProperty(target, descriptor.key, descriptor); 38 | } 39 | } 40 | 41 | return function (Constructor, protoProps, staticProps) { 42 | if (protoProps) defineProperties(Constructor.prototype, protoProps); 43 | if (staticProps) defineProperties(Constructor, staticProps); 44 | return Constructor; 45 | }; 46 | }(); 47 | 48 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { 49 | return typeof obj; 50 | } : function (obj) { 51 | return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; 52 | }; 53 | 54 | // extend target with src or clone src if no target 55 | var extend = function extend(target, src) { 56 | if (src === null || (typeof src === 'undefined' ? 'undefined' : _typeof(src)) !== 'object') { 57 | return src; 58 | } 59 | 60 | var needInit = (typeof target === 'undefined' ? 'undefined' : _typeof(target)) !== (typeof src === 'undefined' ? 'undefined' : _typeof(src)) || target instanceof Array !== src instanceof Array; 61 | 62 | if (src instanceof Array) { 63 | target = needInit ? [] : target; 64 | for (var i = 0; i < src.length; ++i) { 65 | target[i] = extend(target[i], src[i]); 66 | } 67 | return target; 68 | } 69 | 70 | if ((typeof src === 'undefined' ? 'undefined' : _typeof(src)) === 'object') { 71 | target = needInit ? {} : target; 72 | for (var attr in src) { 73 | if (src.hasOwnProperty(attr)) { 74 | target[attr] = extend(target[attr], src[attr]); 75 | } 76 | } 77 | return target; 78 | } 79 | 80 | return src; 81 | }; 82 | 83 | var process = function process(flow, data, label) { 84 | // check flow is done and call stopTask if needed 85 | if (!flow.active) { 86 | if (flow.stopTask) { 87 | var _flow$stopTask = flow.stopTask; 88 | var task = _flow$stopTask.task; 89 | var context = _flow$stopTask.context; 90 | 91 | task.call(context, data); 92 | flow.stopTask = undefined; 93 | } 94 | return; 95 | } 96 | 97 | var datafencing = flow.flags.datafencing; 98 | 99 | 100 | // check if all tasks of current step is done 101 | // and merge data from all parallel tasks in curStep.storage 102 | var nextData = data; 103 | if (flow.idx >= 0) { 104 | var curStep = flow.stepChain[flow.idx]; 105 | if (++curStep.currentCount < curStep.maxCount) { 106 | if (datafencing) { 107 | curStep.storage = extend(curStep.storage, data); 108 | } 109 | return; 110 | } 111 | 112 | if (curStep.storage) { 113 | nextData = extend(curStep.storage, data); 114 | curStep.storage = null; 115 | } 116 | } 117 | 118 | // check if we need skip to specific step 119 | if (typeof label === 'string') { 120 | for (var i = flow.idx + 1; i < flow.stepChain.length; ++i) { 121 | if (flow.stepChain[i].taskList[0].label === label) { 122 | flow.idx = i; 123 | break; 124 | } 125 | } 126 | } 127 | 128 | // process next step 129 | if (++flow.idx < flow.stepChain.length) { 130 | var nextStep = flow.stepChain[flow.idx]; 131 | 132 | ++flow.stepId; 133 | 134 | nextStep.stepId = flow.stepId; 135 | nextStep.currentCount = 0; 136 | nextStep.taskList.forEach(function (taskDesc) { 137 | taskDesc.currentCount = 0; 138 | taskDesc.maxCount = 1; 139 | taskDesc.processTaskFn(flow, taskDesc, datafencing ? extend(undefined, nextData) : nextData); 140 | }); 141 | } else { 142 | flow.stop(); 143 | flow.doneChain.forEach(function (item) { 144 | return item.task.call(item.context, nextData); 145 | }); 146 | 147 | if (flow.looped) { 148 | flow.start(nextData); 149 | } 150 | } 151 | }; 152 | 153 | var processTaskLabel = function processTaskLabel(flow, taskDesc, data) { 154 | process(flow, data); 155 | }; 156 | 157 | var processTaskFunction = function processTaskFunction(flow, taskDesc, data) { 158 | var task = taskDesc.task; 159 | var context = taskDesc.context; 160 | var stepId = flow.stepId; 161 | 162 | var next = function next(nextData, label) { 163 | // first check if flow still on this stepId 164 | // and then - if this task is done 165 | if (stepId === flow.stepId && ++taskDesc.currentCount >= taskDesc.maxCount) { 166 | process(flow, nextData, label); 167 | } 168 | }; 169 | var error = function error(err) { 170 | // check if flow still on this stepId 171 | if (stepId === flow.stepId) { 172 | processError(flow, err); 173 | } 174 | }; 175 | var count = function count(maxCount) { 176 | return taskDesc.maxCount = isNaN(parseInt(maxCount)) || maxCount < 1 ? 1 : maxCount; 177 | }; 178 | 179 | task.call(context, { next: next, error: error, count: count, data: data }); 180 | }; 181 | 182 | var processTaskFlow = function processTaskFlow(flow, taskDesc, data) { 183 | var task = taskDesc.task; 184 | 185 | 186 | task.loop(false).start(data); 187 | }; 188 | 189 | var processError = function processError(flow, err) { 190 | var continueData = flow.errorChain.filter(function (item) { 191 | return item.idx === flow.idx; 192 | }).reduce(function (prev, item) { 193 | var data = item.task.call(item.context, err); 194 | if (data !== undefined) { 195 | return data; 196 | } 197 | 198 | return prev; 199 | }, undefined); 200 | 201 | if (continueData !== undefined) { 202 | // update currentCount in current step 203 | // to proceed to the next step 204 | flow.stepChain[flow.idx].currentCount = flow.stepChain[flow.idx].maxCount; 205 | process(flow, continueData); 206 | } else { 207 | flow.stop(); 208 | flow.catchChain.filter(function (item) { 209 | return item.idx >= flow.idx; 210 | }).forEach(function (item) { 211 | return item.task.call(item.context, err); 212 | }); 213 | } 214 | }; 215 | 216 | var createTaskList = function createTaskList(flow, params) { 217 | var taskList = []; 218 | 219 | if (!params) { 220 | return taskList; 221 | } 222 | 223 | if (typeof params[0] === 'string') { 224 | taskList.push({ 225 | label: params[0], 226 | processTaskFn: processTaskLabel 227 | }); 228 | } else { 229 | params.forEach(function (param) { 230 | if (typeof param === 'function') { 231 | taskList.push({ 232 | task: param, 233 | processTaskFn: processTaskFunction 234 | }); 235 | } else if (param instanceof Lightflow) { 236 | (function () { 237 | var stepIdx = flow.stepChain.length; 238 | 239 | param.done(function (nextData) { 240 | if (flow.stepChain[stepIdx].stepId === flow.stepId) { 241 | process(flow, nextData); 242 | } 243 | }).catch(function (err) { 244 | if (flow.stepChain[stepIdx].stepId === flow.stepId) { 245 | processError(flow, err); 246 | } 247 | }); 248 | 249 | taskList.push({ 250 | task: param, 251 | processTaskFn: processTaskFlow 252 | }); 253 | })(); 254 | } else if (taskList.length) { 255 | taskList[taskList.length - 1].context = param; 256 | } 257 | }); 258 | } 259 | 260 | return taskList; 261 | }; 262 | 263 | var Lightflow = function () { 264 | function Lightflow(_ref) { 265 | var datafencing = _ref.datafencing; 266 | 267 | _classCallCheck(this, Lightflow); 268 | 269 | this.flags = { 270 | datafencing: datafencing === undefined || !!datafencing 271 | }; 272 | this.stepId = 0; 273 | this.idx = -1; 274 | this.active = false; 275 | this.looped = false; 276 | 277 | /* 278 | type Task = { 279 | currentCount?: number = 0; 280 | maxCount?: number = 1; 281 | task?: TaskFn | Lightflow; 282 | context?: any; 283 | label?: string; 284 | processTaskFn: Function; 285 | } 286 | type Step = { 287 | taskList: Task[]; 288 | stepId: number; 289 | currentCount?: number; 290 | maxCount: number; 291 | storage: any; 292 | } 293 | stepChain: Step[]; 294 | */ 295 | this.stepChain = []; 296 | 297 | this.doneChain = []; 298 | this.errorChain = []; 299 | this.catchChain = []; 300 | } 301 | 302 | /* 303 | type taskFnParam = { 304 | error: (err?: any) => void; 305 | next: (data?: any, label?: string) => void; 306 | count: (c: number) => void; 307 | data: any; 308 | } 309 | 310 | type taskFn = (param: taskFnParam) => void; 311 | 312 | then(task: string | TaskFn | Lightflow, context?: any, ...): this 313 | */ 314 | 315 | 316 | _createClass(Lightflow, [{ 317 | key: 'then', 318 | value: function then() { 319 | for (var _len = arguments.length, params = Array(_len), _key = 0; _key < _len; _key++) { 320 | params[_key] = arguments[_key]; 321 | } 322 | 323 | var taskList = createTaskList(this, params); 324 | var maxCount = taskList.length; 325 | 326 | if (maxCount) { 327 | this.stepChain.push({ taskList: taskList, maxCount: maxCount }); 328 | } 329 | 330 | return this; 331 | } 332 | }, { 333 | key: 'race', 334 | value: function race() { 335 | for (var _len2 = arguments.length, params = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { 336 | params[_key2] = arguments[_key2]; 337 | } 338 | 339 | var taskList = createTaskList(this, params); 340 | var maxCount = 1; 341 | 342 | if (taskList.length) { 343 | this.stepChain.push({ taskList: taskList, maxCount: maxCount }); 344 | } 345 | 346 | return this; 347 | } 348 | }, { 349 | key: 'error', 350 | value: function error(task, context) { 351 | var idx = this.stepChain.length - 1; 352 | 353 | this.errorChain.push({ task: task, context: context, idx: idx }); 354 | return this; 355 | } 356 | }, { 357 | key: 'catch', 358 | value: function _catch(task, context) { 359 | var idx = this.stepChain.length - 1; 360 | 361 | this.catchChain.push({ task: task, context: context, idx: idx }); 362 | return this; 363 | } 364 | }, { 365 | key: 'done', 366 | value: function done(task, context) { 367 | this.doneChain.push({ task: task, context: context }); 368 | return this; 369 | } 370 | }, { 371 | key: 'loop', 372 | value: function loop(flag) { 373 | this.looped = flag === undefined ? true : flag; 374 | return this; 375 | } 376 | }, { 377 | key: 'start', 378 | value: function start(data) { 379 | if (this.active) { 380 | return this; 381 | } 382 | 383 | this.active = true; 384 | this.idx = -1; 385 | 386 | process(this, data); 387 | return this; 388 | } 389 | }, { 390 | key: 'stop', 391 | value: function stop(task, context) { 392 | if (this.active) { 393 | this.active = false; 394 | if (task) { 395 | this.stopTask = { task: task, context: context }; 396 | } 397 | } 398 | return this; 399 | } 400 | }]); 401 | 402 | return Lightflow; 403 | }(); 404 | }); 405 | -------------------------------------------------------------------------------- /dist/lightflow.min.js: -------------------------------------------------------------------------------- 1 | !function(t,n){if("function"==typeof define&&define.amd)define(["exports"],n);else if("undefined"!=typeof exports)n(exports);else{var e={exports:{}};n(e.exports),t.lightflow=e.exports}}(this,function(t){"use strict";function n(t,n){if(!(t instanceof n))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(t){return new h(t||{})};var e=function(){function t(t,n){for(var e=0;e=0){var a=t.stepChain[t.idx];if(++a.currentCount=n.maxCount&&r(t,e,i)},u=function(n){a===t.stepId&&f(t,n)},c=function(t){return n.maxCount=isNaN(parseInt(t))||t<1?1:t};i.call(o,{next:s,error:u,count:c,data:e})},u=function(t,n,e){var i=n.task;i.loop(!1).start(e)},f=function(t,n){var e=t.errorChain.filter(function(n){return n.idx===t.idx}).reduce(function(t,e){var i=e.task.call(e.context,n);return void 0!==i?i:t},void 0);void 0!==e?(t.stepChain[t.idx].currentCount=t.stepChain[t.idx].maxCount,r(t,e)):(t.stop(),t.catchChain.filter(function(n){return n.idx>=t.idx}).forEach(function(t){return t.task.call(t.context,n)}))},c=function(t,n){var e=[];return n?("string"==typeof n[0]?e.push({label:n[0],processTaskFn:a}):n.forEach(function(n){"function"==typeof n?e.push({task:n,processTaskFn:s}):n instanceof h?!function(){var i=t.stepChain.length;n.done(function(n){t.stepChain[i].stepId===t.stepId&&r(t,n)}).catch(function(n){t.stepChain[i].stepId===t.stepId&&f(t,n)}),e.push({task:n,processTaskFn:u})}():e.length&&(e[e.length-1].context=n)}),e):e},h=function(){function t(e){var i=e.datafencing;n(this,t),this.flags={datafencing:void 0===i||!!i},this.stepId=0,this.idx=-1,this.active=!1,this.looped=!1,this.stepChain=[],this.doneChain=[],this.errorChain=[],this.catchChain=[]}return e(t,[{key:"then",value:function(){for(var t=arguments.length,n=Array(t),e=0;e= 0) { 67 | var curStep = flow.stepChain[flow.idx]; 68 | if (++curStep.currentCount < curStep.maxCount) { 69 | if (datafencing) { 70 | curStep.storage = extend(curStep.storage, data); 71 | } 72 | return; 73 | } 74 | 75 | if (curStep.storage) { 76 | nextData = extend(curStep.storage, data); 77 | curStep.storage = null; 78 | } 79 | } 80 | 81 | // check if we need skip to specific step 82 | if (typeof label === 'string') { 83 | for (var i = flow.idx + 1; i < flow.stepChain.length; ++i) { 84 | if (flow.stepChain[i].taskList[0].label === label) { 85 | flow.idx = i; 86 | break; 87 | } 88 | } 89 | } 90 | 91 | // process next step 92 | if (++flow.idx < flow.stepChain.length) { 93 | var nextStep = flow.stepChain[flow.idx]; 94 | 95 | ++flow.stepId; 96 | 97 | nextStep.stepId = flow.stepId; 98 | nextStep.currentCount = 0; 99 | nextStep.taskList.forEach(function (taskDesc) { 100 | taskDesc.currentCount = 0; 101 | taskDesc.maxCount = 1; 102 | taskDesc.processTaskFn(flow, taskDesc, datafencing ? extend(undefined, nextData) : nextData); 103 | }); 104 | } else { 105 | flow.stop(); 106 | flow.doneChain.forEach(function (item) { 107 | return item.task.call(item.context, nextData); 108 | }); 109 | 110 | if (flow.looped) { 111 | flow.start(nextData); 112 | } 113 | } 114 | }; 115 | 116 | var processTaskLabel = function processTaskLabel(flow, taskDesc, data) { 117 | process(flow, data); 118 | }; 119 | 120 | var processTaskFunction = function processTaskFunction(flow, taskDesc, data) { 121 | var task = taskDesc.task; 122 | var context = taskDesc.context; 123 | var stepId = flow.stepId; 124 | 125 | var next = function next(nextData, label) { 126 | // first check if flow still on this stepId 127 | // and then - if this task is done 128 | if (stepId === flow.stepId && ++taskDesc.currentCount >= taskDesc.maxCount) { 129 | process(flow, nextData, label); 130 | } 131 | }; 132 | var error = function error(err) { 133 | // check if flow still on this stepId 134 | if (stepId === flow.stepId) { 135 | processError(flow, err); 136 | } 137 | }; 138 | var count = function count(maxCount) { 139 | return taskDesc.maxCount = isNaN(parseInt(maxCount)) || maxCount < 1 ? 1 : maxCount; 140 | }; 141 | 142 | task.call(context, { next: next, error: error, count: count, data: data }); 143 | }; 144 | 145 | var processTaskFlow = function processTaskFlow(flow, taskDesc, data) { 146 | var task = taskDesc.task; 147 | 148 | 149 | task.loop(false).start(data); 150 | }; 151 | 152 | var processError = function processError(flow, err) { 153 | var continueData = flow.errorChain.filter(function (item) { 154 | return item.idx === flow.idx; 155 | }).reduce(function (prev, item) { 156 | var data = item.task.call(item.context, err); 157 | if (data !== undefined) { 158 | return data; 159 | } 160 | 161 | return prev; 162 | }, undefined); 163 | 164 | if (continueData !== undefined) { 165 | // update currentCount in current step 166 | // to proceed to the next step 167 | flow.stepChain[flow.idx].currentCount = flow.stepChain[flow.idx].maxCount; 168 | process(flow, continueData); 169 | } else { 170 | flow.stop(); 171 | flow.catchChain.filter(function (item) { 172 | return item.idx >= flow.idx; 173 | }).forEach(function (item) { 174 | return item.task.call(item.context, err); 175 | }); 176 | } 177 | }; 178 | 179 | var createTaskList = function createTaskList(flow, params) { 180 | var taskList = []; 181 | 182 | if (!params) { 183 | return taskList; 184 | } 185 | 186 | if (typeof params[0] === 'string') { 187 | taskList.push({ 188 | label: params[0], 189 | processTaskFn: processTaskLabel 190 | }); 191 | } else { 192 | params.forEach(function (param) { 193 | if (typeof param === 'function') { 194 | taskList.push({ 195 | task: param, 196 | processTaskFn: processTaskFunction 197 | }); 198 | } else if (param instanceof Lightflow) { 199 | (function () { 200 | var stepIdx = flow.stepChain.length; 201 | 202 | param.done(function (nextData) { 203 | if (flow.stepChain[stepIdx].stepId === flow.stepId) { 204 | process(flow, nextData); 205 | } 206 | })['catch'](function (err) { 207 | if (flow.stepChain[stepIdx].stepId === flow.stepId) { 208 | processError(flow, err); 209 | } 210 | }); 211 | 212 | taskList.push({ 213 | task: param, 214 | processTaskFn: processTaskFlow 215 | }); 216 | })(); 217 | } else if (taskList.length) { 218 | taskList[taskList.length - 1].context = param; 219 | } 220 | }); 221 | } 222 | 223 | return taskList; 224 | }; 225 | 226 | var Lightflow = function () { 227 | function Lightflow(_ref) { 228 | var datafencing = _ref.datafencing; 229 | 230 | _classCallCheck(this, Lightflow); 231 | 232 | this.flags = { 233 | datafencing: datafencing === undefined || !!datafencing 234 | }; 235 | this.stepId = 0; 236 | this.idx = -1; 237 | this.active = false; 238 | this.looped = false; 239 | 240 | /* 241 | type Task = { 242 | currentCount?: number = 0; 243 | maxCount?: number = 1; 244 | task?: TaskFn | Lightflow; 245 | context?: any; 246 | label?: string; 247 | processTaskFn: Function; 248 | } 249 | type Step = { 250 | taskList: Task[]; 251 | stepId: number; 252 | currentCount?: number; 253 | maxCount: number; 254 | storage: any; 255 | } 256 | stepChain: Step[]; 257 | */ 258 | this.stepChain = []; 259 | 260 | this.doneChain = []; 261 | this.errorChain = []; 262 | this.catchChain = []; 263 | } 264 | 265 | /* 266 | type taskFnParam = { 267 | error: (err?: any) => void; 268 | next: (data?: any, label?: string) => void; 269 | count: (c: number) => void; 270 | data: any; 271 | } 272 | 273 | type taskFn = (param: taskFnParam) => void; 274 | 275 | then(task: string | TaskFn | Lightflow, context?: any, ...): this 276 | */ 277 | 278 | 279 | _createClass(Lightflow, [{ 280 | key: 'then', 281 | value: function then() { 282 | for (var _len = arguments.length, params = Array(_len), _key = 0; _key < _len; _key++) { 283 | params[_key] = arguments[_key]; 284 | } 285 | 286 | var taskList = createTaskList(this, params); 287 | var maxCount = taskList.length; 288 | 289 | if (maxCount) { 290 | this.stepChain.push({ taskList: taskList, maxCount: maxCount }); 291 | } 292 | 293 | return this; 294 | } 295 | 296 | /* 297 | race (task: string | TaskFn | Lightflow, context?: any, ...): this 298 | */ 299 | 300 | }, { 301 | key: 'race', 302 | value: function race() { 303 | for (var _len2 = arguments.length, params = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { 304 | params[_key2] = arguments[_key2]; 305 | } 306 | 307 | var taskList = createTaskList(this, params); 308 | var maxCount = 1; 309 | 310 | if (taskList.length) { 311 | this.stepChain.push({ taskList: taskList, maxCount: maxCount }); 312 | } 313 | 314 | return this; 315 | } 316 | }, { 317 | key: 'error', 318 | value: function error(task, context) { 319 | var idx = this.stepChain.length - 1; 320 | 321 | this.errorChain.push({ task: task, context: context, idx: idx }); 322 | return this; 323 | } 324 | }, { 325 | key: 'catch', 326 | value: function _catch(task, context) { 327 | var idx = this.stepChain.length - 1; 328 | 329 | this.catchChain.push({ task: task, context: context, idx: idx }); 330 | return this; 331 | } 332 | }, { 333 | key: 'done', 334 | value: function done(task, context) { 335 | this.doneChain.push({ task: task, context: context }); 336 | return this; 337 | } 338 | }, { 339 | key: 'loop', 340 | value: function loop(flag) { 341 | this.looped = flag === undefined ? true : flag; 342 | return this; 343 | } 344 | }, { 345 | key: 'start', 346 | value: function start(data) { 347 | if (this.active) { 348 | return this; 349 | } 350 | 351 | this.active = true; 352 | this.idx = -1; 353 | 354 | process(this, data); 355 | return this; 356 | } 357 | }, { 358 | key: 'stop', 359 | value: function stop(task, context) { 360 | if (this.active) { 361 | this.active = false; 362 | if (task) { 363 | this.stopTask = { task: task, context: context }; 364 | } 365 | } 366 | return this; 367 | } 368 | }]); 369 | 370 | return Lightflow; 371 | }(); 372 | 373 | module.exports = exports['default']; 374 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | exports['default'] = function (params) { 8 | return new Lightflow(params || {}); 9 | }; 10 | 11 | // extend target with src or clone src if no target 12 | const extend = function (target, src) { 13 | if (src === null || typeof src !== 'object') { 14 | return src; 15 | } 16 | 17 | const needInit = typeof target !== typeof src || target instanceof Array !== src instanceof Array; 18 | 19 | if (src instanceof Array) { 20 | target = needInit ? [] : target; 21 | for (let i = 0; i < src.length; ++i) { 22 | target[i] = extend(target[i], src[i]); 23 | } 24 | return target; 25 | } 26 | 27 | if (typeof src === 'object') { 28 | target = needInit ? {} : target; 29 | for (let attr in src) { 30 | if (src.hasOwnProperty(attr)) { 31 | target[attr] = extend(target[attr], src[attr]); 32 | } 33 | } 34 | return target; 35 | } 36 | 37 | return src; 38 | }; 39 | 40 | const process = function (flow, data, label) { 41 | // check flow is done and call stopTask if needed 42 | if (!flow.active) { 43 | if (flow.stopTask) { 44 | let { task, context } = flow.stopTask; 45 | task.call(context, data); 46 | flow.stopTask = undefined; 47 | } 48 | return; 49 | } 50 | 51 | const { datafencing } = flow.flags; 52 | 53 | // check if all tasks of current step is done 54 | // and merge data from all parallel tasks in curStep.storage 55 | let nextData = data; 56 | if (flow.idx >= 0) { 57 | const curStep = flow.stepChain[flow.idx]; 58 | if (++curStep.currentCount < curStep.maxCount) { 59 | if (datafencing) { 60 | curStep.storage = extend(curStep.storage, data); 61 | } 62 | return; 63 | } 64 | 65 | if (curStep.storage) { 66 | nextData = extend(curStep.storage, data); 67 | curStep.storage = null; 68 | } 69 | } 70 | 71 | // check if we need skip to specific step 72 | if (typeof label === 'string') { 73 | for (let i = flow.idx + 1; i < flow.stepChain.length; ++i) { 74 | if (flow.stepChain[i].taskList[0].label === label) { 75 | flow.idx = i; 76 | break; 77 | } 78 | } 79 | } 80 | 81 | // process next step 82 | if (++flow.idx < flow.stepChain.length) { 83 | const nextStep = flow.stepChain[flow.idx]; 84 | 85 | ++flow.stepId; 86 | 87 | nextStep.stepId = flow.stepId; 88 | nextStep.currentCount = 0; 89 | nextStep.taskList.forEach(taskDesc => { 90 | taskDesc.currentCount = 0; 91 | taskDesc.maxCount = 1; 92 | taskDesc.processTaskFn(flow, taskDesc, datafencing ? extend(undefined, nextData) : nextData); 93 | }); 94 | } else { 95 | flow.stop(); 96 | flow.doneChain.forEach(item => item.task.call(item.context, nextData)); 97 | 98 | if (flow.looped) { 99 | flow.start(nextData); 100 | } 101 | } 102 | }; 103 | 104 | const processTaskLabel = function (flow, taskDesc, data) { 105 | process(flow, data); 106 | }; 107 | 108 | const processTaskFunction = function (flow, taskDesc, data) { 109 | const { task, context } = taskDesc; 110 | const { stepId } = flow; 111 | const next = (nextData, label) => { 112 | // first check if flow still on this stepId 113 | // and then - if this task is done 114 | if (stepId === flow.stepId && ++taskDesc.currentCount >= taskDesc.maxCount) { 115 | process(flow, nextData, label); 116 | } 117 | }; 118 | const error = err => { 119 | // check if flow still on this stepId 120 | if (stepId === flow.stepId) { 121 | processError(flow, err); 122 | } 123 | }; 124 | const count = maxCount => taskDesc.maxCount = isNaN(parseInt(maxCount)) || maxCount < 1 ? 1 : maxCount; 125 | 126 | task.call(context, { next, error, count, data }); 127 | }; 128 | 129 | const processTaskFlow = function (flow, taskDesc, data) { 130 | const { task } = taskDesc; 131 | 132 | task.loop(false).start(data); 133 | }; 134 | 135 | const processError = function (flow, err) { 136 | const continueData = flow.errorChain.filter(item => item.idx === flow.idx).reduce((prev, item) => { 137 | const data = item.task.call(item.context, err); 138 | if (data !== undefined) { 139 | return data; 140 | } 141 | 142 | return prev; 143 | }, undefined); 144 | 145 | if (continueData !== undefined) { 146 | // update currentCount in current step 147 | // to proceed to the next step 148 | flow.stepChain[flow.idx].currentCount = flow.stepChain[flow.idx].maxCount; 149 | process(flow, continueData); 150 | } else { 151 | flow.stop(); 152 | flow.catchChain.filter(item => item.idx >= flow.idx).forEach(item => item.task.call(item.context, err)); 153 | } 154 | }; 155 | 156 | const createTaskList = function (flow, params) { 157 | const taskList = []; 158 | 159 | if (!params) { 160 | return taskList; 161 | } 162 | 163 | if (typeof params[0] === 'string') { 164 | taskList.push({ 165 | label: params[0], 166 | processTaskFn: processTaskLabel 167 | }); 168 | } else { 169 | params.forEach(param => { 170 | if (typeof param === 'function') { 171 | taskList.push({ 172 | task: param, 173 | processTaskFn: processTaskFunction 174 | }); 175 | } else if (param instanceof Lightflow) { 176 | let stepIdx = flow.stepChain.length; 177 | 178 | param.done(nextData => { 179 | if (flow.stepChain[stepIdx].stepId === flow.stepId) { 180 | process(flow, nextData); 181 | } 182 | })['catch'](err => { 183 | if (flow.stepChain[stepIdx].stepId === flow.stepId) { 184 | processError(flow, err); 185 | } 186 | }); 187 | 188 | taskList.push({ 189 | task: param, 190 | processTaskFn: processTaskFlow 191 | }); 192 | } else if (taskList.length) { 193 | taskList[taskList.length - 1].context = param; 194 | } 195 | }); 196 | } 197 | 198 | return taskList; 199 | }; 200 | 201 | class Lightflow { 202 | constructor({ datafencing }) { 203 | this.flags = { 204 | datafencing: datafencing === undefined || !!datafencing 205 | }; 206 | this.stepId = 0; 207 | this.idx = -1; 208 | this.active = false; 209 | this.looped = false; 210 | 211 | /* 212 | type Task = { 213 | currentCount?: number = 0; 214 | maxCount?: number = 1; 215 | task?: TaskFn | Lightflow; 216 | context?: any; 217 | label?: string; 218 | processTaskFn: Function; 219 | } 220 | type Step = { 221 | taskList: Task[]; 222 | stepId: number; 223 | currentCount?: number; 224 | maxCount: number; 225 | storage: any; 226 | } 227 | stepChain: Step[]; 228 | */ 229 | this.stepChain = []; 230 | 231 | this.doneChain = []; 232 | this.errorChain = []; 233 | this.catchChain = []; 234 | } 235 | 236 | /* 237 | type taskFnParam = { 238 | error: (err?: any) => void; 239 | next: (data?: any, label?: string) => void; 240 | count: (c: number) => void; 241 | data: any; 242 | } 243 | 244 | type taskFn = (param: taskFnParam) => void; 245 | 246 | then(task: string | TaskFn | Lightflow, context?: any, ...): this 247 | */ 248 | then(...params) { 249 | const taskList = createTaskList(this, params); 250 | const maxCount = taskList.length; 251 | 252 | if (maxCount) { 253 | this.stepChain.push({ taskList, maxCount }); 254 | } 255 | 256 | return this; 257 | } 258 | 259 | /* 260 | race (task: string | TaskFn | Lightflow, context?: any, ...): this 261 | */ 262 | race(...params) { 263 | const taskList = createTaskList(this, params); 264 | const maxCount = 1; 265 | 266 | if (taskList.length) { 267 | this.stepChain.push({ taskList, maxCount }); 268 | } 269 | 270 | return this; 271 | } 272 | 273 | error(task, context) { 274 | const idx = this.stepChain.length - 1; 275 | 276 | this.errorChain.push({ task, context, idx }); 277 | return this; 278 | } 279 | 280 | catch(task, context) { 281 | const idx = this.stepChain.length - 1; 282 | 283 | this.catchChain.push({ task, context, idx }); 284 | return this; 285 | } 286 | 287 | done(task, context) { 288 | this.doneChain.push({ task, context }); 289 | return this; 290 | } 291 | 292 | loop(flag) { 293 | this.looped = flag === undefined ? true : flag; 294 | return this; 295 | } 296 | 297 | start(data) { 298 | if (this.active) { 299 | return this; 300 | } 301 | 302 | this.active = true; 303 | this.idx = -1; 304 | 305 | process(this, data); 306 | return this; 307 | } 308 | 309 | stop(task, context) { 310 | if (this.active) { 311 | this.active = false; 312 | if (task) { 313 | this.stopTask = { task, context }; 314 | } 315 | } 316 | return this; 317 | } 318 | } 319 | 320 | module.exports = exports['default']; 321 | -------------------------------------------------------------------------------- /lib/lts/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | exports['default'] = function (params) { 8 | return new Lightflow(params || {}); 9 | }; 10 | 11 | // extend target with src or clone src if no target 12 | const extend = function extend(target, src) { 13 | if (src === null || typeof src !== 'object') { 14 | return src; 15 | } 16 | 17 | const needInit = typeof target !== typeof src || target instanceof Array !== src instanceof Array; 18 | 19 | if (src instanceof Array) { 20 | target = needInit ? [] : target; 21 | for (let i = 0; i < src.length; ++i) { 22 | target[i] = extend(target[i], src[i]); 23 | } 24 | return target; 25 | } 26 | 27 | if (typeof src === 'object') { 28 | target = needInit ? {} : target; 29 | for (let attr in src) { 30 | if (src.hasOwnProperty(attr)) { 31 | target[attr] = extend(target[attr], src[attr]); 32 | } 33 | } 34 | return target; 35 | } 36 | 37 | return src; 38 | }; 39 | 40 | const process = function process(flow, data, label) { 41 | // check flow is done and call stopTask if needed 42 | if (!flow.active) { 43 | if (flow.stopTask) { 44 | var _flow$stopTask = flow.stopTask; 45 | let task = _flow$stopTask.task; 46 | let context = _flow$stopTask.context; 47 | 48 | task.call(context, data); 49 | flow.stopTask = undefined; 50 | } 51 | return; 52 | } 53 | 54 | const datafencing = flow.flags.datafencing; 55 | 56 | // check if all tasks of current step is done 57 | // and merge data from all parallel tasks in curStep.storage 58 | 59 | let nextData = data; 60 | if (flow.idx >= 0) { 61 | const curStep = flow.stepChain[flow.idx]; 62 | if (++curStep.currentCount < curStep.maxCount) { 63 | if (datafencing) { 64 | curStep.storage = extend(curStep.storage, data); 65 | } 66 | return; 67 | } 68 | 69 | if (curStep.storage) { 70 | nextData = extend(curStep.storage, data); 71 | curStep.storage = null; 72 | } 73 | } 74 | 75 | // check if we need skip to specific step 76 | if (typeof label === 'string') { 77 | for (let i = flow.idx + 1; i < flow.stepChain.length; ++i) { 78 | if (flow.stepChain[i].taskList[0].label === label) { 79 | flow.idx = i; 80 | break; 81 | } 82 | } 83 | } 84 | 85 | // process next step 86 | if (++flow.idx < flow.stepChain.length) { 87 | const nextStep = flow.stepChain[flow.idx]; 88 | 89 | ++flow.stepId; 90 | 91 | nextStep.stepId = flow.stepId; 92 | nextStep.currentCount = 0; 93 | nextStep.taskList.forEach(taskDesc => { 94 | taskDesc.currentCount = 0; 95 | taskDesc.maxCount = 1; 96 | taskDesc.processTaskFn(flow, taskDesc, datafencing ? extend(undefined, nextData) : nextData); 97 | }); 98 | } else { 99 | flow.stop(); 100 | flow.doneChain.forEach(item => item.task.call(item.context, nextData)); 101 | 102 | if (flow.looped) { 103 | flow.start(nextData); 104 | } 105 | } 106 | }; 107 | 108 | const processTaskLabel = function processTaskLabel(flow, taskDesc, data) { 109 | process(flow, data); 110 | }; 111 | 112 | const processTaskFunction = function processTaskFunction(flow, taskDesc, data) { 113 | const task = taskDesc.task; 114 | const context = taskDesc.context; 115 | const stepId = flow.stepId; 116 | 117 | const next = (nextData, label) => { 118 | // first check if flow still on this stepId 119 | // and then - if this task is done 120 | if (stepId === flow.stepId && ++taskDesc.currentCount >= taskDesc.maxCount) { 121 | process(flow, nextData, label); 122 | } 123 | }; 124 | const error = err => { 125 | // check if flow still on this stepId 126 | if (stepId === flow.stepId) { 127 | processError(flow, err); 128 | } 129 | }; 130 | const count = maxCount => taskDesc.maxCount = isNaN(parseInt(maxCount)) || maxCount < 1 ? 1 : maxCount; 131 | 132 | task.call(context, { next, error, count, data }); 133 | }; 134 | 135 | const processTaskFlow = function processTaskFlow(flow, taskDesc, data) { 136 | const task = taskDesc.task; 137 | 138 | 139 | task.loop(false).start(data); 140 | }; 141 | 142 | const processError = function processError(flow, err) { 143 | const continueData = flow.errorChain.filter(item => item.idx === flow.idx).reduce((prev, item) => { 144 | const data = item.task.call(item.context, err); 145 | if (data !== undefined) { 146 | return data; 147 | } 148 | 149 | return prev; 150 | }, undefined); 151 | 152 | if (continueData !== undefined) { 153 | // update currentCount in current step 154 | // to proceed to the next step 155 | flow.stepChain[flow.idx].currentCount = flow.stepChain[flow.idx].maxCount; 156 | process(flow, continueData); 157 | } else { 158 | flow.stop(); 159 | flow.catchChain.filter(item => item.idx >= flow.idx).forEach(item => item.task.call(item.context, err)); 160 | } 161 | }; 162 | 163 | const createTaskList = function createTaskList(flow, params) { 164 | const taskList = []; 165 | 166 | if (!params) { 167 | return taskList; 168 | } 169 | 170 | if (typeof params[0] === 'string') { 171 | taskList.push({ 172 | label: params[0], 173 | processTaskFn: processTaskLabel 174 | }); 175 | } else { 176 | params.forEach(param => { 177 | if (typeof param === 'function') { 178 | taskList.push({ 179 | task: param, 180 | processTaskFn: processTaskFunction 181 | }); 182 | } else if (param instanceof Lightflow) { 183 | let stepIdx = flow.stepChain.length; 184 | 185 | param.done(nextData => { 186 | if (flow.stepChain[stepIdx].stepId === flow.stepId) { 187 | process(flow, nextData); 188 | } 189 | })['catch'](err => { 190 | if (flow.stepChain[stepIdx].stepId === flow.stepId) { 191 | processError(flow, err); 192 | } 193 | }); 194 | 195 | taskList.push({ 196 | task: param, 197 | processTaskFn: processTaskFlow 198 | }); 199 | } else if (taskList.length) { 200 | taskList[taskList.length - 1].context = param; 201 | } 202 | }); 203 | } 204 | 205 | return taskList; 206 | }; 207 | 208 | class Lightflow { 209 | constructor(_ref) { 210 | let datafencing = _ref.datafencing; 211 | 212 | this.flags = { 213 | datafencing: datafencing === undefined || !!datafencing 214 | }; 215 | this.stepId = 0; 216 | this.idx = -1; 217 | this.active = false; 218 | this.looped = false; 219 | 220 | /* 221 | type Task = { 222 | currentCount?: number = 0; 223 | maxCount?: number = 1; 224 | task?: TaskFn | Lightflow; 225 | context?: any; 226 | label?: string; 227 | processTaskFn: Function; 228 | } 229 | type Step = { 230 | taskList: Task[]; 231 | stepId: number; 232 | currentCount?: number; 233 | maxCount: number; 234 | storage: any; 235 | } 236 | stepChain: Step[]; 237 | */ 238 | this.stepChain = []; 239 | 240 | this.doneChain = []; 241 | this.errorChain = []; 242 | this.catchChain = []; 243 | } 244 | 245 | /* 246 | type taskFnParam = { 247 | error: (err?: any) => void; 248 | next: (data?: any, label?: string) => void; 249 | count: (c: number) => void; 250 | data: any; 251 | } 252 | 253 | type taskFn = (param: taskFnParam) => void; 254 | 255 | then(task: string | TaskFn | Lightflow, context?: any, ...): this 256 | */ 257 | then() { 258 | for (var _len = arguments.length, params = Array(_len), _key = 0; _key < _len; _key++) { 259 | params[_key] = arguments[_key]; 260 | } 261 | 262 | const taskList = createTaskList(this, params); 263 | const maxCount = taskList.length; 264 | 265 | if (maxCount) { 266 | this.stepChain.push({ taskList, maxCount }); 267 | } 268 | 269 | return this; 270 | } 271 | 272 | /* 273 | race (task: string | TaskFn | Lightflow, context?: any, ...): this 274 | */ 275 | race() { 276 | for (var _len2 = arguments.length, params = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { 277 | params[_key2] = arguments[_key2]; 278 | } 279 | 280 | const taskList = createTaskList(this, params); 281 | const maxCount = 1; 282 | 283 | if (taskList.length) { 284 | this.stepChain.push({ taskList, maxCount }); 285 | } 286 | 287 | return this; 288 | } 289 | 290 | error(task, context) { 291 | const idx = this.stepChain.length - 1; 292 | 293 | this.errorChain.push({ task, context, idx }); 294 | return this; 295 | } 296 | 297 | catch(task, context) { 298 | const idx = this.stepChain.length - 1; 299 | 300 | this.catchChain.push({ task, context, idx }); 301 | return this; 302 | } 303 | 304 | done(task, context) { 305 | this.doneChain.push({ task, context }); 306 | return this; 307 | } 308 | 309 | loop(flag) { 310 | this.looped = flag === undefined ? true : flag; 311 | return this; 312 | } 313 | 314 | start(data) { 315 | if (this.active) { 316 | return this; 317 | } 318 | 319 | this.active = true; 320 | this.idx = -1; 321 | 322 | process(this, data); 323 | return this; 324 | } 325 | 326 | stop(task, context) { 327 | if (this.active) { 328 | this.active = false; 329 | if (task) { 330 | this.stopTask = { task, context }; 331 | } 332 | } 333 | return this; 334 | } 335 | } 336 | 337 | module.exports = exports['default']; 338 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lightflow", 3 | "version": "2.0.0", 4 | "description": "Tiny Promise-like control flow library for browser and Node.js", 5 | "main": "lib/index.js", 6 | "browser": "dist/lightflow.js", 7 | "jsnext:main": "src/lightflow.js", 8 | "scripts": { 9 | "test": "SET BABEL_ENV=browser&&ava --verbose --require babel-register test/index.js", 10 | "build": "npm run build-node && npm run build-browser", 11 | "build-browser": "SET BABEL_ENV=browser&&babel -o dist/lightflow.js src/lightflow.js && uglifyjs -c -m --source-map dist/lightflow.min.js.map -o dist/lightflow.min.js dist/lightflow.js", 12 | "build-node": "npm run build-node-current && npm run build-node-lts && npm run build-node-old", 13 | "build-node-current": "SET BABEL_ENV=node&&babel -o lib/index.js src/lightflow.js", 14 | "build-node-lts": "SET BABEL_ENV=node_lts&&babel -o lib/lts/index.js src/lightflow.js", 15 | "build-node-old": "SET BABEL_ENV=node_old&&babel -o lib/0.x/index.js src/lightflow.js" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/saperio/lightflow.git" 20 | }, 21 | "homepage": "https://github.com/saperio/lightflow", 22 | "author": "SAPer", 23 | "license": "MIT", 24 | "keywords": [ 25 | "es6", 26 | "control flow", 27 | "flow", 28 | "fast", 29 | "simple" 30 | ], 31 | "devDependencies": { 32 | "ava": "^0.16.0", 33 | "babel-cli": "^6.14.0", 34 | "babel-plugin-add-module-exports": "^0.2.1", 35 | "babel-plugin-transform-es2015-modules-umd": "^6.12.0", 36 | "babel-preset-es2015": "^6.14.0", 37 | "babel-preset-modern-node": "^3.2.0", 38 | "babel-register": "^6.16.3", 39 | "uglify-js": "^2.7.3" 40 | } 41 | } -------------------------------------------------------------------------------- /src/lightflow.js: -------------------------------------------------------------------------------- 1 | // extend target with src or clone src if no target 2 | const extend = function (target, src) { 3 | if (src === null || typeof src !== 'object') { 4 | return src; 5 | } 6 | 7 | const needInit = typeof target !== typeof src || target instanceof Array !== src instanceof Array; 8 | 9 | if (src instanceof Array) { 10 | target = needInit ? [] : target; 11 | for (let i = 0; i < src.length; ++i) { 12 | target[i] = extend(target[i], src[i]); 13 | } 14 | return target; 15 | } 16 | 17 | if (typeof src === 'object') { 18 | target = needInit ? {} : target; 19 | for (let attr in src) { 20 | if (src.hasOwnProperty(attr)) { 21 | target[attr] = extend(target[attr], src[attr]); 22 | } 23 | } 24 | return target; 25 | } 26 | 27 | return src; 28 | }; 29 | 30 | const process = function (flow, data, label) { 31 | // check flow is done and call stopTask if needed 32 | if (!flow.active) { 33 | if (flow.stopTask) { 34 | let { task, context } = flow.stopTask; 35 | task.call(context, data); 36 | flow.stopTask = undefined; 37 | } 38 | return; 39 | } 40 | 41 | const { datafencing } = flow.flags; 42 | 43 | // check if all tasks of current step is done 44 | // and merge data from all parallel tasks in curStep.storage 45 | let nextData = data; 46 | if (flow.idx >= 0) { 47 | const curStep = flow.stepChain[flow.idx]; 48 | if (++curStep.currentCount < curStep.maxCount) { 49 | if (datafencing) { 50 | curStep.storage = extend(curStep.storage, data); 51 | } 52 | return; 53 | } 54 | 55 | if (curStep.storage) { 56 | nextData = extend(curStep.storage, data); 57 | curStep.storage = null; 58 | } 59 | } 60 | 61 | // check if we need skip to specific step 62 | if (typeof label === 'string') { 63 | for (let i = flow.idx + 1; i < flow.stepChain.length; ++i) { 64 | if (flow.stepChain[i].taskList[0].label === label) { 65 | flow.idx = i; 66 | break; 67 | } 68 | } 69 | } 70 | 71 | // process next step 72 | if (++flow.idx < flow.stepChain.length) { 73 | const nextStep = flow.stepChain[flow.idx]; 74 | 75 | ++flow.stepId; 76 | 77 | nextStep.stepId = flow.stepId; 78 | nextStep.currentCount = 0; 79 | nextStep.taskList.forEach(taskDesc => { 80 | taskDesc.currentCount = 0; 81 | taskDesc.maxCount = 1; 82 | taskDesc.processTaskFn(flow, taskDesc, datafencing ? extend(undefined, nextData) : nextData); 83 | }); 84 | } else { 85 | flow.stop(); 86 | flow.doneChain.forEach(item => item.task.call(item.context, nextData)); 87 | 88 | if (flow.looped) { 89 | flow.start(nextData); 90 | } 91 | } 92 | }; 93 | 94 | const processTaskLabel = function (flow, taskDesc, data) { 95 | process(flow, data); 96 | }; 97 | 98 | const processTaskFunction = function (flow, taskDesc, data) { 99 | const { task, context } = taskDesc; 100 | const { stepId } = flow; 101 | const next = (nextData, label) => { 102 | // first check if flow still on this stepId 103 | // and then - if this task is done 104 | if (stepId === flow.stepId && ++taskDesc.currentCount >= taskDesc.maxCount) { 105 | process(flow, nextData, label); 106 | } 107 | }; 108 | const error = err => { 109 | // check if flow still on this stepId 110 | if (stepId === flow.stepId) { 111 | processError(flow, err); 112 | } 113 | }; 114 | const count = maxCount => taskDesc.maxCount = (isNaN(parseInt(maxCount)) || maxCount < 1) ? 1 : maxCount; 115 | 116 | task.call(context, { next, error, count, data }); 117 | }; 118 | 119 | const processTaskFlow = function (flow, taskDesc, data) { 120 | const { task } = taskDesc; 121 | 122 | task 123 | .loop(false) 124 | .start(data) 125 | ; 126 | }; 127 | 128 | const processError = function (flow, err) { 129 | const continueData = flow.errorChain 130 | .filter(item => item.idx === flow.idx) 131 | .reduce( 132 | (prev, item) => { 133 | const data = item.task.call(item.context, err); 134 | if (data !== undefined) { 135 | return data; 136 | } 137 | 138 | return prev; 139 | }, 140 | undefined 141 | ) 142 | ; 143 | 144 | if (continueData !== undefined) { 145 | // update currentCount in current step 146 | // to proceed to the next step 147 | flow.stepChain[flow.idx].currentCount = flow.stepChain[flow.idx].maxCount; 148 | process(flow, continueData); 149 | } else { 150 | flow.stop(); 151 | flow.catchChain 152 | .filter(item => item.idx >= flow.idx) 153 | .forEach(item => item.task.call(item.context, err)) 154 | ; 155 | } 156 | }; 157 | 158 | const createTaskList = function (flow, params) { 159 | const taskList = []; 160 | 161 | if (!params) { 162 | return taskList; 163 | } 164 | 165 | if (typeof params[0] === 'string') { 166 | taskList.push({ 167 | label : params[0], 168 | processTaskFn : processTaskLabel 169 | }); 170 | } else { 171 | params.forEach(param => { 172 | if (typeof param === 'function') { 173 | taskList.push({ 174 | task : param, 175 | processTaskFn : processTaskFunction 176 | }); 177 | } else if (param instanceof Lightflow) { 178 | let stepIdx = flow.stepChain.length; 179 | 180 | param 181 | .done(nextData => { 182 | if (flow.stepChain[stepIdx].stepId === flow.stepId) { 183 | process(flow, nextData); 184 | } 185 | }) 186 | .catch(err => { 187 | if (flow.stepChain[stepIdx].stepId === flow.stepId) { 188 | processError(flow, err); 189 | } 190 | }) 191 | ; 192 | 193 | taskList.push({ 194 | task : param, 195 | processTaskFn : processTaskFlow 196 | }); 197 | } else if (taskList.length) { 198 | taskList[taskList.length - 1].context = param; 199 | } 200 | }); 201 | } 202 | 203 | return taskList; 204 | }; 205 | 206 | class Lightflow { 207 | constructor ({ datafencing }) { 208 | this.flags = { 209 | datafencing: datafencing === undefined || !!datafencing 210 | }; 211 | this.stepId = 0; 212 | this.idx = -1; 213 | this.active = false; 214 | this.looped = false; 215 | 216 | /* 217 | type Task = { 218 | currentCount?: number = 0; 219 | maxCount?: number = 1; 220 | task?: TaskFn | Lightflow; 221 | context?: any; 222 | label?: string; 223 | processTaskFn: Function; 224 | } 225 | type Step = { 226 | taskList: Task[]; 227 | stepId: number; 228 | currentCount?: number; 229 | maxCount: number; 230 | storage: any; 231 | } 232 | stepChain: Step[]; 233 | */ 234 | this.stepChain = []; 235 | 236 | this.doneChain = []; 237 | this.errorChain = []; 238 | this.catchChain = []; 239 | } 240 | 241 | /* 242 | type taskFnParam = { 243 | error: (err?: any) => void; 244 | next: (data?: any, label?: string) => void; 245 | count: (c: number) => void; 246 | data: any; 247 | } 248 | 249 | type taskFn = (param: taskFnParam) => void; 250 | 251 | then(task: string | TaskFn | Lightflow, context?: any, ...): this 252 | */ 253 | then (...params) { 254 | const taskList = createTaskList(this, params); 255 | const maxCount = taskList.length; 256 | 257 | if (maxCount) { 258 | this.stepChain.push({ taskList, maxCount }); 259 | } 260 | 261 | return this; 262 | } 263 | 264 | /* 265 | race (task: string | TaskFn | Lightflow, context?: any, ...): this 266 | */ 267 | race (...params) { 268 | const taskList = createTaskList(this, params); 269 | const maxCount = 1; 270 | 271 | if (taskList.length) { 272 | this.stepChain.push({ taskList, maxCount }); 273 | } 274 | 275 | return this; 276 | } 277 | 278 | error (task, context) { 279 | const idx = this.stepChain.length - 1; 280 | 281 | this.errorChain.push({ task, context, idx }); 282 | return this; 283 | } 284 | 285 | catch (task, context) { 286 | const idx = this.stepChain.length - 1; 287 | 288 | this.catchChain.push({ task, context, idx }); 289 | return this; 290 | } 291 | 292 | done (task, context) { 293 | this.doneChain.push({ task, context }); 294 | return this; 295 | } 296 | 297 | loop (flag) { 298 | this.looped = flag === undefined ? true : flag; 299 | return this; 300 | } 301 | 302 | start (data) { 303 | if (this.active) { 304 | return this; 305 | } 306 | 307 | this.active = true; 308 | this.idx = -1; 309 | 310 | process(this, data); 311 | return this; 312 | } 313 | 314 | stop (task, context) { 315 | if (this.active) { 316 | this.active = false; 317 | if (task) { 318 | this.stopTask = { task, context }; 319 | } 320 | } 321 | return this; 322 | } 323 | } 324 | 325 | export default function (params) { 326 | return new Lightflow(params || {}); 327 | } -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import lightflow from '../src/lightflow'; 2 | import test from './test'; 3 | 4 | test('node', lightflow); -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | 3 | export default function (title, lightflow) { 4 | const simpleAsync = fn => setTimeout(fn, 1); 5 | const randomAsync = (range, fn) => setTimeout(fn, 1 + Math.random() * (range - 1)); 6 | const fixAsync = (timeout, fn) => setTimeout(fn, timeout); 7 | const label = `lightflow:${title}`; 8 | 9 | test(`${label}: object create`, t => { 10 | t.truthy(lightflow()); 11 | }); 12 | 13 | test(`${label}: object api check`, t => { 14 | const flow = lightflow(); 15 | const check = fn => t.is(typeof flow[fn], 'function', `check .${fn} func`); 16 | 17 | ['then', 'race', 'error', 'catch', 'done', 'loop', 'start', 'stop'].forEach(check); 18 | }); 19 | 20 | test.cb(`${label}: trivial flow`, t => { 21 | t.plan(1); 22 | 23 | const ret = 3; 24 | 25 | lightflow() 26 | .then(({ next }) => next(ret)) 27 | .done(data => { 28 | t.is(data, ret); 29 | t.end() 30 | }) 31 | .start() 32 | ; 33 | }); 34 | 35 | test.cb(`${label}: trivial flow with data traverce`, t => { 36 | t.plan(1); 37 | 38 | const ret = 3; 39 | 40 | lightflow() 41 | .then(({ next, data }) => next(data)) 42 | .done(data => { 43 | t.is(data, ret); 44 | t.end(); 45 | }) 46 | .start(ret) 47 | ; 48 | }); 49 | 50 | test.cb(`${label}: nested flows`, t => { 51 | t.plan(1); 52 | 53 | const ret = 3; 54 | 55 | lightflow() 56 | .then( 57 | lightflow() 58 | .then(({ next, data}) => simpleAsync(() => next(data))) 59 | ) 60 | .done(data => { 61 | t.is(data, ret); 62 | t.end(); 63 | }) 64 | .start(ret) 65 | ; 66 | }); 67 | 68 | test.cb(`${label}: parallel tasks`, t => { 69 | t.plan(1); 70 | 71 | const ret = 3; 72 | 73 | lightflow() 74 | .then( 75 | lightflow() 76 | .then(({ next, data}) => simpleAsync(() => next(data))) 77 | ) 78 | .done(data => { 79 | t.is(data, ret); 80 | t.end(); 81 | }) 82 | .start(ret) 83 | ; 84 | }); 85 | 86 | test.cb(`${label}: '.race' simple test`, t => { 87 | t.plan(3); 88 | 89 | lightflow() 90 | .race( 91 | // first race task 92 | ({ next, data }) => { 93 | randomAsync(50, () => { 94 | data.t1 = true; 95 | next(data); 96 | }) 97 | }, 98 | 99 | // second race task 100 | ({ next, data }) => { 101 | randomAsync(50, () => { 102 | data.t2 = true; 103 | next(data); 104 | }) 105 | } 106 | ) 107 | .then(({ next, data }) => fixAsync(60, () => next(data))) 108 | .then(({ next, data }) => { 109 | const { a, t1, t2 } = data; 110 | t.is(a, 1, 'check data pass through'); 111 | t.truthy(t1 || t2, 'never a one race functions done') 112 | t.truthy(!(t1 && t2), 'data from both race function come here!'); 113 | t.end(); 114 | 115 | next(); 116 | }) 117 | .start({ a: 1 }) 118 | ; 119 | }); 120 | 121 | test.cb(`${label}: '.then' with some parallel tasks`, t => { 122 | t.plan(2); 123 | 124 | lightflow() 125 | .then( 126 | ({ next, data }) => simpleAsync(() => next({ a: data + 1 })), 127 | ({ next, data }) => simpleAsync(() => next({ b: data + 2 })) 128 | ) 129 | .done(data => { 130 | t.is(data.a, 2); 131 | t.is(data.b, 3); 132 | t.end(); 133 | }) 134 | .start(1) 135 | ; 136 | }); 137 | 138 | test.cb(`${label}: check data fencing simple`, t => { 139 | t.plan(1); 140 | 141 | let data_1; 142 | let data_2_1; 143 | let data_2_2; 144 | 145 | lightflow() 146 | .then( 147 | ({ next, data }) => simpleAsync(() => { 148 | data_1 = data; 149 | next(data); 150 | }) 151 | ) 152 | .then( 153 | ({ next, data }) => simpleAsync(() => { 154 | data_2_1 = data; 155 | next(data); 156 | }), 157 | ({ next, data }) => simpleAsync(() => { 158 | data_2_2 = data; 159 | next(data); 160 | }) 161 | ) 162 | .done(data => { 163 | t.true(data !== data_1 && data !== data_2_1 && data !== data_2_2); 164 | t.end(); 165 | }) 166 | .start({ somedata: 1}) 167 | ; 168 | }); 169 | 170 | test.cb(`${label}: check data fencing`, t => { 171 | t.plan(3); 172 | 173 | lightflow() 174 | 175 | // first try to corrupt data object after step is ended 176 | .then(({ next, data}) => { 177 | data.step1 = 1; 178 | next(data); 179 | 180 | fixAsync(10, () => data.step1 = 100); 181 | }) 182 | .then(({ next, data }) => { 183 | fixAsync(100, () => { 184 | t.is(data.step1, 1, 'data corrupted with prev step'); 185 | next(data); 186 | }); 187 | }) 188 | 189 | // then try to corrupt data from parallel task on same step 190 | .then( 191 | ({ next, data }) => fixAsync(1, () => { 192 | data.step2 = 2; 193 | next(data); 194 | }), 195 | ({ next, data }) => fixAsync(100, () => { 196 | t.truthy(!data.step2, 'data corrupted from parallel task') 197 | next(data); 198 | }) 199 | ) 200 | 201 | // and the last one - try to corrupt data from concurent task 202 | .race( 203 | ({ next, data }) => fixAsync(1, () => { 204 | data.step3 = 3; 205 | next(data); 206 | }), 207 | ({ next, data }) => fixAsync(100, () => { 208 | t.truthy(!data.step3, 'data corrupted from concurent task') 209 | next(data); 210 | }) 211 | ) 212 | .then(({ next, data }) => fixAsync(150, () => { 213 | t.end(); 214 | next(data); 215 | })) 216 | .start({}) 217 | ; 218 | }); 219 | 220 | test.cb(`${label}: check data fencing disabling`, t => { 221 | t.plan(1); 222 | 223 | let data_1; 224 | let data_2_1; 225 | let data_2_2; 226 | 227 | lightflow({ datafencing: false }) 228 | .then( 229 | ({ next, data }) => simpleAsync(() => { 230 | data_1 = data; 231 | next(data); 232 | }) 233 | ) 234 | .then( 235 | ({ next, data }) => simpleAsync(() => { 236 | data_2_1 = data; 237 | next(data); 238 | }), 239 | ({ next, data }) => simpleAsync(() => { 240 | data_2_2 = data; 241 | next(data); 242 | }) 243 | ) 244 | .done(data => { 245 | t.true(data === data_1 && data === data_2_1 && data === data_2_2); 246 | t.end(); 247 | }) 248 | .start({ somedata: 1}) 249 | ; 250 | }); 251 | 252 | test.cb(`${label}: check flow with labels`, t => { 253 | t.plan(1); 254 | 255 | lightflow() 256 | .then('label') 257 | .then(({ next }) => simpleAsync(() => next())) 258 | .then('another-label') 259 | .then(({ next }) => simpleAsync(() => next())) 260 | .then('last-label') 261 | .done(() => { 262 | t.pass(); 263 | t.end(); 264 | }) 265 | .start() 266 | ; 267 | }); 268 | 269 | test.cb(`${label}: check single skip to label`, t => { 270 | t.plan(1); 271 | 272 | lightflow() 273 | .then(({ next }) => simpleAsync(() => next(null, 'label'))) 274 | .then(({ next }) => { 275 | t.fail('can\'t get here'); 276 | t.end(); 277 | next(); 278 | }) 279 | .then('label') 280 | .then(({ next }) => { 281 | t.pass(); 282 | t.end(); 283 | next(); 284 | }) 285 | .start() 286 | ; 287 | }); 288 | 289 | test.cb(`${label}: '.error' with continue`, t => { 290 | t.plan(2); 291 | 292 | lightflow() 293 | .then(({ next }) => simpleAsync(() => next())) 294 | .then(({ error }) => simpleAsync(() => error())) 295 | .error(() => 1) 296 | .catch(() => { 297 | t.fail(); 298 | t.end(); 299 | }) 300 | .then(({ next, data }) => { 301 | t.is(1, data); 302 | next(); 303 | }) 304 | .done(() => { 305 | t.pass(); 306 | t.end(); 307 | }) 308 | .start() 309 | ; 310 | }); 311 | 312 | 313 | test.cb(`${label}: '.catch' api`, t => { 314 | t.plan(2); 315 | 316 | const errorMsg = 'error'; 317 | 318 | lightflow() 319 | .then(({ error }) => simpleAsync(() => error(errorMsg))) 320 | .catch(e => { 321 | t.is(e, errorMsg); 322 | }) 323 | .then(({ next }) => simpleAsync(() => next())) 324 | .catch(e => { 325 | t.is(e, errorMsg); 326 | t.end(); 327 | }) 328 | .done(() => { 329 | t.failed(); 330 | }) 331 | .start() 332 | ; 333 | }); 334 | 335 | 336 | test.cb(`${label}: '.loop' api`, t => { 337 | t.plan(1); 338 | 339 | let counter = 0; 340 | 341 | const flow = lightflow() 342 | .then(({ next }) => simpleAsync(() => { 343 | ++counter; 344 | if (counter === 2) { 345 | flow.loop(false); 346 | t.pass(); 347 | t.end(); 348 | } 349 | 350 | next(); 351 | })) 352 | .loop() 353 | .start() 354 | ; 355 | }); 356 | 357 | 358 | test.cb(`${label}: '.loop' api with data pass`, t => { 359 | t.plan(2); 360 | 361 | let counter = 0; 362 | const flow = lightflow() 363 | .then(({ next, data }) => simpleAsync(() => { 364 | t.is(data, 0, `data must be 0, but it is ${data}`); 365 | if (++counter === 2) { 366 | flow.loop(false); 367 | t.end(); 368 | } 369 | 370 | next(data); 371 | })) 372 | .loop() 373 | .start(0) 374 | ; 375 | }); 376 | 377 | 378 | test.cb(`${label}: '.stop' api with callback`, t => { 379 | t.plan(1); 380 | 381 | const initial = 0; 382 | const final = 3; 383 | 384 | const flow = lightflow() 385 | .then(({ next, data }) => simpleAsync(() => next(++data))) 386 | .then(({ next, data }) => simpleAsync(() => next(++data))) 387 | .then(({ next, data }) => { 388 | simpleAsync(() => next(++data)); 389 | flow.stop(last => { 390 | t.is(last, final); 391 | t.end(); 392 | }); 393 | }) 394 | .then(({ next, data }) => simpleAsync(() => { 395 | t.fail('execute next task after stop called!'); 396 | t.end(); 397 | next(data); 398 | })) 399 | .start(initial) 400 | ; 401 | }); 402 | } --------------------------------------------------------------------------------