A routine library simplifies functional asynchronous javascript.
A JavaScript runtime supports most ES6 features and ES7 async/await, e.g Node.js >= v10.0.
npm install --save-prod lotress/common-node
See test for examples of APIs.
import {
identity,
None,
M,
allAwait,
raceAwait,
delay,
deadline,
retry,
pushMap,
sequence,
tco,
BinaryHeap,
genWrap,
genLog,
logInfo,
logError,
newMessageQueue,
newPool
} from 'common-node'
identity = x => x
None = _ => void 0
logInfo
and logError
are alias for console.log
and console.error
,
they are made by genLog
with logLevel = 2,
you can call genLog
with your logLevel,
less logLevel is more important.
- Example
const logInfo = genLog(1)(console.log.bind(console))
const logError = genLog(-1)(console.error.bind(console))
logInfo(identity('hello')) // hello
logError(None('wrong')) // undefined
M
takes a function, wrap it into a Monad which can be called as the original function,
with additional .then
and .catch
interface.
M: (input: *any -> output: any) -> ContinuationMonad
ContinuationMonad: input -> Promise(-> output)
ContinuationMonad.then: (onFulfilled: (input: output -> output: any)?, onRejected: (reason: any -> any)?) -> ContinuationMonad
at least one of two callbacks is required.
ContinuationMonad.catch: (onRejected: (reason: any -> any)) -> ContinuationMonad
- Example
let f = M(x => new Promise(resolve => setTimeout((_ => resolve(x)), 1000)))
.then(x => logInfo(x))
.then(_ => new Promise((_, reject) => setTimeout((_ => reject('wrong')), 1000)))
.catch(e => logError(e))
// do something
// you can manipulate f like a normal function
// or attach more callbacks to it
f('hello') // ContinuationMonad can be called multiple times, all callbacks are kept
f('world')
// waiting about 1s
// print 'hello'
// print 'world', order may change since they are asynchronous calls
// waiting about 1s
// log error 'wrong'
// log error 'wrong'
delay
and deadline
are functions return a Promise resolve/reject after given timeout.
delay: (time: number) -> (result?: any) -> Promise(-> result | time)
deadline: (time: number) -> (reason?: any) -> Promise(-> reason | Error(time))
allAwait
and raceAwait
are lazy modification of Promise.all and Promise.race.
allAwait: (funcs: Array[input: *any -> output: any]) -> (args: Array[input]) -> Promise(-> Array[output])
raceAwait: (funcs: Array[input: *any -> output: any]) -> (args: Array[input]) -> Promise(-> output)
args are passed to funcs with same index
- Example, see
Test('death race')
in test
let life = 1000
let f = time => {
let awaitFunc = allAwait([identity, delay(time), delay(time * 2)])
let g = M(awaitFunc)
.then(None) // no arguments should be passed to awaitFunc here
.then(awaitFunc)
.then(None)
.then(awaitFunc)
let h = raceAwait([g, deadline(life)])
logInfo(`race began with time interval ${time}ms`)
let start = Date.now()
return h().then(_ => {
logInfo(`after 600ms, race ends`)
}).catch(e => {
logError(`after 1000ms, life ends`)
})
}
f 100
f 200
/*
print 'race began with time interval 100ms'
print 'race began with time interval 200ms'
waiting about 600ms
print 'after 600ms, race ends'
waiting about 400ms
log error 'after 1000ms, life ends'
*/
retry
takes a function (synchronous or asynchronous) which possibly throws and a retry count,
returns a wrapped function with same input parameters,
call this wrapped function will repeatly try the original one until it didn't throw or retry count met,
return a Promise, if the original function didn't throw then resolve its return value,
else reject with the error it thrown.
retry: (input: *any -> output: any) -> (retryCount: number) -> (input: *any) -> Promise(-> output)
- Example, see
Test('retry')
in test
let f = (times = 3) => {
var count = 0
return _ => {
count += 1
if (count < times)
throw new Error(`${count} < ${times}`)
return count
}
}
retry(f())(2)()
.catch(e => logError(e.message)) // log error '2 < 3'
retry(f())(3)()
.then(logInfo) // print '3'
sequence
takes a function then a iterable object,
sequential apply and wait the function on elements of the iterable,
loop ends if the function returns null
or undefined
or reached the end of the iterable,
if memory
is true
then results will be returned in an Array, else returns undefined
.
sequence: (input: any -> output: any, memory = true) -> (iter: iterable) -> Promise(-> array[output])
- Example, see
Test('sequence')
in test
let number = function*() {
let n = 1
let k = 1
while (true) {
k = yield n + k
}
}
let f = x => x < 5 ? x : void 0
let g = x => delay(1000)()
.then(_ => f(x))
console.log(await sequence(g)(number())) // [2, 3, 4] after 4s
isGenerator
checks if the given object is a Generator.
Given g as input, it returns True if g is a Generator, g if g is falsy, otherwise false.
isGenerator: any -> boolean
tco
is a tail call optimizer utilizing generator function.
For a recursive function f without yield
and only recurse on tail,
replace its tail call's return
with yield
,
so it becomes a GeneratorFunction
, then wrap it by tco
,
the result function should work the same as the original recursive function without piling stack.
tco: (f: GeneratorFunction) -> ...any -> any
- Example, see
Test('sequence')
in test
const countR = n => n < 1 ? n : 1 + countR(n - 1)
try {
countR(1e7)
} catch (e) {
console.log(e.toString()) // RangeError: Maximum call stack size exceeded
}
const countG = function*(n, res = 0) {
if (n < 1)
yield res
else
yield countG(n - 1, res + 1)
}
let count = tco(countG)
console.log(count(1e7)) // 10000000
After comparing the performance of several priority queue implementations,
we implemented a BinaryHeap
using a typed key array.
The keys used for comparing stored in a TypedArray
, the type was assigned when constructing the heap.
The heap will keep the item with minimal key on the top.
The BinaryHeap
can be constructed by a given option object, the default option is
{Type: Float64Array, simple: false}
.
If simple is true
, the heap will only store keys, otherwise every item can includes a corresponding value.
let heap = BinaryHeap({Type: Uint8Array, simple: true})
heap.push(2).push(1).push(4)
console.log(heap.pop()) // 1
console.log(heap.peek()) // 2
let heap2 = BinaryHeap()
heap2.push(0.1, 'more').push(-1, 'less').push(0, 'zero')
console.log(heap2.pop()) // [-1, "less"]
console.log(heap2.pop()) // [0, "zero"]
console.log(heap2.pop()) // [0.1, "more"]
console.log(heap2.pop()) // undefined
genWrap
takes a Class then returns a factory function of this class.
let w = genWrap(Error)
// all arguments will be passed to the constructor if the first argument is not instance of the class
let e = w('my error')
let ee = w(e)
console.log(e === ee) // true
console.log(ee.message) // my error
newMessageArray
constructs a new message array
const items = []
var newItem, popItem, peek, getLength
;[newItem, popItem, peek, getLength] = newMessageArray(2, items)
for (let i = 4; i--; ) newItem()
let id = items[2].id
let p = peek(id)
console.log(getLength()) // 4
let item = popItem(id)
console.log(item.id === id) // true
console.log(p.id === id) // true
console.log(getLength()) // 3
item = newItem('hello')
console.log(item.id & 3) // 2
console.log(item.message) // hello
try {
newItem()
} catch (e) {
console.error(e.message) // Full
}
newPool
constructs a new pool, it has a optional timeout argument.
var ac, release, c = 0
const pool = [1, 2, 3], timeout = 20, times = 99, s = new Set()
[ac, release] = newPool(pool, timeout / 2)
const acquire = (g => async() => (await g()).value)((o => o.next.bind(o))(ac()))
const f = async() => {
if (c > times) return
c += 1
let v = await acquire()
console.log(s.has(v)) // false
s.add(v)
return new Promise(resolve => setTimeout(resolve, timeout))
.then(() => {
console.log(s.has(v)) // true
s.delete(v)
release(v)
return f()
})
}
const start = Date.now()
Promise.all([f(), f(), f()])
.then(start => console.log(Date.now() - start))
// should between [timeout * times / 3, timeout * times / 2]
.then => {
const g = async() => {
let v = await acquire()
if (v instanceof Error) {
console.error 'Acquire timed out' // should print once
return
}
console.log(v) // 3 2 1
await delay(timeout)()
return release(v)
}
Promise.all([g(), g(), g(), g()])
}
Routines for node.js environment are in common-node.js
.
import {getFullPath} from 'common-node/common-node'
getFullPath: (directory: string) -> (name: string) -> path: string
- Example
let path = getFullPath('./../common')('src/../common.js')
console.log(path) // ${cwd}\common\common.js under Windows
A framework for unit test in testFramework.js
.
import {Test} from 'common-node/testFramework'
For each test case, call Test with description and you test procedure.
Test: (description: string) -> ((report: ReportObject) -> Promise | undefined) -> Promise(-> boolean)
In test procedure, you can call a reportFn in the given ReportObject for an assertion.
There are two kind of assertion now,
assert
for a boolean value, pass if the value is truly;
assertSeq
for monotonous increase sequence number,
pass if seq number is larger than last called,
initial value for seq number is -Infinity.
A optional message can be given to log when assertion failed.
ReportObject: {assert?: boolean, seq?: number}
report.assert: (boolean | Promise(-> boolean), message = 'assert failed') -> Promise(-> boolean)
report.assertSeq: (number | Promise(-> number), message = 'wrong sequence') -> Promise(-> boolean)
- Example
Test('example test')(report =>
let start = Date.now()
let p = delay(1000)()
p.then(_ => {
return Promise.all([
report.assertSeq(2)
report.assert(Date.now() - start > 999)
])
})
report.assertSeq(1)
return p
)
npm install
npm run-script build