Skip to content

Commit e895a8b

Browse files
committed
feat(actions): include a serial action
Similar to the repeat action be ensure each task is both run and completes before moving on to the next. With a lot of async tasks the implementation of repeat can't ensure the order of each task. While it may ensure each task is invoked in order, it does not ensure each completes.
1 parent c50a2a2 commit e895a8b

8 files changed

Lines changed: 228 additions & 3 deletions

File tree

README.md

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
* [`map(collection, iterator, label)`](#mapcollection-iterator-label)
1515
* [`sort(collection, comparator, label)`](#sortcollection-comparator-label)
1616
* [`repeat(times, name, opts, label)`](#repeattimes-name-opts-label)
17+
* [`serial(times, name, opts, label)`](#serialtimes-name-opts-label)
1718
* [`sleep(opts)`](#sleepopts)
1819
* [`reset()`](#reset)
1920
* [`execute()`](#execute)
@@ -247,8 +248,10 @@ const state = await chain
247248

248249
## `repeat(times, name, opts, label)`
249250

250-
Executes each task added sequentially and collects the results into a single object.
251-
This is a chain action resolved by [execute](#execute).
251+
Executes each task added and collects the results into a single object.
252+
This is a chain action resolved by [execute](#execute). Internally this uses `Promise.all`
253+
and does not necessarily ensure that actions execute in the order they are specified.
254+
If the order of execution is important, you should use [serial](#serial)
252255

253256
**Parameters**
254257

@@ -284,6 +287,61 @@ chain
284287
> {result: ['hi', 'hi', 'hi', 'hi', 'hi']}
285288
```
286289

290+
## `serial(times, name, opts, label)`
291+
292+
Executes each task added sequentially and collects the results into a single array.
293+
This is a chain action resolved by [execute](#execute).
294+
295+
**Parameters**
296+
297+
* **times** [Number][]: The number of times to execute the given action name
298+
* **name** [String][]: The name of the action to execute. This **must** already exist
299+
on the chain as a valid action.
300+
* **opts** [Object][]: Any options that the named action requires. Currently, this
301+
only supports actions with an `(opts)` signature. Custom signatures are not yet
302+
supported.
303+
* **label** [String][]: Optional label in which to store the result.
304+
305+
**Returns:** `this<SetupChain>`
306+
307+
308+
```javascript
309+
const {promisify} = require('util')
310+
const SetupChain = require('@logdna/setup-chain')
311+
312+
function randInt(min, max) {
313+
return Math.random() * (max - min) + min;
314+
}
315+
316+
class MyChain extends SetupChain {
317+
constructor(state) {
318+
super(state, {
319+
delay: ({value}) => {
320+
return new Promise((resolve, reject) => {
321+
const timeout = randInt(1, 1000)
322+
setTimeout(() => {
323+
resolve(`${value} (${timeout}ms)`)
324+
}, timeout)
325+
})
326+
}
327+
})
328+
329+
$incr() {
330+
return ++this.count
331+
}
332+
}
333+
334+
}
335+
336+
const chain = new MyChain()
337+
chain
338+
.serial(5, 'delay', {value: '!incr'}, 'result')
339+
.execute()
340+
.then(console.log)
341+
342+
> {result: ['1 (10ms)', '2 (1ms)', '3 (1000ms)', '4 (22ms)', '5 (38ms)']}
343+
```
344+
287345
## `sleep(opts)`
288346

289347
When used in a chain, it will wait for a specified number of milliseconds before returning. Since this is a chain action, the sleep will appear to happen sequentially

lib/actions/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ module.exports = {
44
map: require('./map.js')
55
, repeat: require('./repeat.js')
66
, set: require('./set.js')
7+
, serial: require('./serial.js')
78
, sleep: require('./sleep.js')
89
, sort: require('./sort.js')
910
}

lib/actions/serial.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
'use strict'
2+
3+
/**
4+
* @module lib/actions/serial
5+
* @author Eric Satterwhite
6+
**/
7+
8+
const assert = require('assert')
9+
const {typeOf, object} = require('@logdna/stdlib')
10+
11+
const NAME = 'SetupChain.serial'
12+
const TIMES_ERR = `
13+
${NAME} requires 'times' to be an integer and greater than or equal to 0
14+
`.trim()
15+
16+
const ACTION_ERR = `${NAME} 'action' invalid`
17+
18+
module.exports = async function serial(times, action, opts) {
19+
assert.equal(typeOf(times), 'number', TIMES_ERR)
20+
assert.ok(times >= 0, TIMES_ERR)
21+
assert.ok(Number.isInteger(times), TIMES_ERR)
22+
assert.ok(Number.isFinite(times), TIMES_ERR)
23+
assert.ok(
24+
object.has(this.actions, action)
25+
, `${ACTION_ERR}. '${action}' does not exist as a chain action.`
26+
)
27+
28+
const results = new Array(times)
29+
const fn = this.actions[action]
30+
31+
for (let i = 0; i < times; i++) {
32+
results[i] = await fn.call(this, opts)
33+
}
34+
35+
return results
36+
}
37+
38+
/**
39+
* Chain action that similar to {@link module:lib/actions/repeat|repeat} but always executes
40+
* in a sequential order.
41+
* @async
42+
* @function module:lib/actions/serial
43+
* @param {Object} input
44+
* @param {Number} input.times The number of times to run the action
45+
* @param {String} input.action The name of the chain action to execute
46+
* @param {Object} [input.opts] Options to pass to the action
47+
* @example
48+
* await new SetupChain().serial(10, 'delay', {value: '!template("{{!increment}}")'}).execute()
49+
**/
50+

lib/chain.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ module.exports = class SetupChain {
7272
return this
7373
}
7474

75+
serial(times, action, opts, label) {
76+
this.tasks.push(['serial', label, times, action, opts])
77+
return this
78+
}
79+
7580
sort(collection, comparator, label) {
7681
this.tasks.push(['sort', label, collection, comparator])
7782
return this

test/fixtures/actions/delay.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
'use strict'
2+
3+
function randInt(min, max) {
4+
return Math.random() * (max - min) + min
5+
}
6+
7+
module.exports = function delay(opts) {
8+
return new Promise((resolve) => {
9+
const delay = randInt(1, 500)
10+
setTimeout(() => {
11+
resolve(this.lookup(opts.value))
12+
}, delay)
13+
})
14+
}

test/fixtures/actions/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ module.exports = {
44
error: require('./error.js')
55
, greet: require('./greet.js')
66
, name: require('./name.js')
7+
, delay: require('./delay.js')
78
}

test/integration/actions/serial.js

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
'use strict'
2+
3+
const {test, threw} = require('tap')
4+
const Chain = require('../../../lib/chain.js')
5+
const actions = require('../../fixtures/actions/index.js')
6+
7+
class ActionsChain extends Chain {
8+
constructor(state) {
9+
super(state, actions)
10+
this.count = 0
11+
}
12+
$incr() {
13+
return ++this.count
14+
}
15+
}
16+
17+
test('SetupChain.serial() as a builtin action', async (t) => {
18+
const chain = new ActionsChain()
19+
20+
t.test('chain.serial is a function', async (t) => {
21+
t.type(chain.serial, 'function', 'serial is a function')
22+
})
23+
24+
t.test('Success; Call serial for an existing action', async (t) => {
25+
const state = await chain
26+
.serial(5, 'delay', {value: '!template("Bodhi {{!incr}}")'}, 'all_names')
27+
.execute()
28+
29+
const expected = ['Bodhi 1', 'Bodhi 2', 'Bodhi 3', 'Bodhi 4', 'Bodhi 5']
30+
t.same(state.all_names, expected, 'Expected result in state')
31+
t.same(chain.state.all_names, expected, 'Expected result in chain.state')
32+
})
33+
34+
t.test('Success; Call serial using a lookup', async (t) => {
35+
const state = await chain
36+
.serial(2, 'greet', {names: '#all_names', greeting: 'Hi'}, 'all_greetings')
37+
.execute()
38+
39+
const greetings = [
40+
'Hi Bodhi 1'
41+
, 'Hi Bodhi 2'
42+
, 'Hi Bodhi 3'
43+
, 'Hi Bodhi 4'
44+
, 'Hi Bodhi 5'
45+
]
46+
const expected = [greetings, greetings]
47+
t.same(state.all_greetings, expected, 'Expected result in state')
48+
t.same(chain.state.all_greetings, expected, 'Result in chain.state')
49+
})
50+
51+
t.test('Error: times is not a number', async (t) => {
52+
const msg = new RegExp(
53+
'requires \'times\' to be an integer and greater than or equal to 0'
54+
)
55+
56+
t.rejects(chain.serial().execute(), {
57+
err: msg
58+
}, 'No parameters')
59+
60+
t.rejects(chain.serial(null).execute(), {
61+
err: msg
62+
}, 'times is null')
63+
64+
t.rejects(chain.serial({}).execute(), {
65+
err: msg
66+
}, 'times is an object')
67+
68+
t.rejects(chain.serial('ten').execute(), {
69+
err: msg
70+
}, 'times is a string')
71+
72+
t.rejects(chain.serial(-1).execute(), {
73+
err: msg
74+
}, 'times is a a negetive number')
75+
76+
t.rejects(chain.serial(1.1).execute(), {
77+
err: msg
78+
}, 'times is a decimal')
79+
80+
t.rejects(chain.serial(Number.POSITIVE_INFINITY).execute(), {
81+
err: msg
82+
}, 'times is a infinate')
83+
84+
t.rejects(chain.serial(NaN).execute(), {
85+
err: msg
86+
}, 'times is NaN')
87+
})
88+
89+
t.test('Error: action is not a function', async (t) => {
90+
const msg = new RegExp('\'NOPE\' does not exist as a chain action')
91+
t.rejects(chain.serial(10, 'NOPE').execute(), {
92+
err: msg
93+
}, 'Unknown action')
94+
})
95+
}).catch(threw)

test/integration/chain.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,11 +210,12 @@ test('Setup chain', async (t) => {
210210
t.test('Default SetupChain (no actions, state); Only built-ins', async (t) => {
211211
const chain = new Chain(null, null)
212212
t.same(chain.state, {}, 'Empty state')
213-
t.equal(Object.keys(chain.actions).length, 5, 'Built-in action count')
213+
t.equal(Object.keys(chain.actions).length, 6, 'Built-in action count')
214214
t.match(chain.actions, {
215215
map: Function
216216
, repeat: Function
217217
, sleep: Function
218+
, serial: Function
218219
, sort: Function
219220
, set: Function
220221
}, 'Built-in action names')

0 commit comments

Comments
 (0)