├── .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 | [](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 | }
--------------------------------------------------------------------------------