trikl
v0.2.0
Published
The antidote for "callback hell"
Downloads
5
Maintainers
Readme
Trikl
Creating callback heaven. Trikl provides a simplified promise chain api, allowing you to callback and callforth with reckless abandon.
Installation
Install using npm:
npm install trikl
Usage
const trikl = require('trikl')
The Basics
let trickle = trikl()
.drop(drip => drip())
.drop(drip => drip())
This illustrates the basic trickle flow. Let's break it down:
trikl()
– creates a new chain (a "trickle").trickle.drop()
– adds a "drop" to the trickle. A drop is just a function whose execution is delayed until a previous drop tells it to go ahead. The first drop in the chain is run automatically by default.drip
– the first argument that Trikl passes to every drop. Calling this will pass execution on to the next drop in the chain.
Simple enough. Let's bite off a bit more:
let trickle = trikl(drip => drip(1))
.drop((drip, val) => drip(val + 1))
.drop((drip, val) => drip(val + 1))
trickle.then(result => {
console.log(result) // <- logs "3"
})
Not too bad. Let's dig into this one:
trikl(drop)
– you can optionally pass drops right to thetrikl()
factory.drip(args)
– anything you pass todrip()
will be passed along as arguments to the next drop in the chain. Callingdrip()
in the last drop in the chain resolves the underlying promise.
Wait, what promise? – Every trickle creates a promise chain internally. The first promise in the chain can be accessed via
trickle.promise
and resolved and rejected withtrickle.resolve()
andtrickle.reject()
respectively.
trickle.then()
– callsthen()
on the underlying promise, but returns the trickle for chaining. The usage is therefore exactly the same as Promise.prototype.then().
Yep, you can add links to the underlying promise chain via trickle.then()
and trickle.catch()
respectively. Since Trikl exposes then
and catch
methods, most code that expects a promise can receive a trickle instead.
And that does it for the basics. You can now use Trikl like a pro (basically)! But that isn't everything. Read through the Method API and the more in-depth examples for real pro status.
API
Unless otherwise noted, all methods return their trickle for chaining.
trikl()
The default factory for creating trickles. Accepts zero or more drops as arguments.
let trickle = trikl()
let trickle = trikl(drop1, drop2)
trickles
These properties and methods are available on trickles.
trickle.promise
A reference to the underlying promise – the first promise in the underlying promise chain.
trickle.lastPromise
A reference to the last promise in the underlying promise chain.
trickle.then()
Attach onFulfilled handlers to the last promise in the underlying promise chain. Can, of course, add onRejected handlers too, but prefer trickle.catch()
for that. Adds a new promise to the underlying promise chain and points the trickle.lastPromise
property to it. Returns the trickle for chaining.
let trickle = trikl()
// This adds a second promise to the underlying promise chain.
.then(result => result)
// This adds a third promise to the underlying promise chain.
// It will not be resolved until the second promise in the chain resolves it.
.then(result => result)
// Note:
trickle.then(() => {})
// is equivalent to
trickle.lastPromise = trickle.lastPromise.then(() => {})
trickle.catch()
Attach onRejected handlers to the last promise in the underlying promise chain. Adds a new promise to the underlying promise chain and points the trickle.lastPromise
property to it. Returns the trickle for chaining.
trickle.catch(() => {})
// is equivalent to
trickle.lastPromise = trickle.lastPromise.catch(() => {})
trickle.resolve()
Resolve the underlying promise (the first one in the underlying promise chain). This starts the chain unraveling. Calling trickle.drip()
in the last drop in the trickle will call this automatically (unless it's called with an instance of Error
, in which case, trickle.reject()
will be called).
trikl()
.then(result => result + 1)
.then(result => {
console.log(result) // logs "2"
})
.resolve(1)
trickle.reject()
Reject the underlying promise (the first one in the underlying promise chain). This starts the chain unraveling. Calling trickle.drip()
with an instance of Error
in the last drop in the trickle will call this automatically. Throwing anything in any drops will also make Trikl call this automatically.
trikl()
.then(result => console.log('fulfilled'))
.catch(result => console.log('rejected'))
.reject() // logs "rejected"
Note:
trickle.resolve()
andtrickle.reject()
do not stop the trickle! If there are more drops, they'll keep on dripping. For this reason, you'll typically usetrickle.drip.stop()
instead of calling these directly.
trickle.drop()
Add drops modularly to a trickle. Signature is the same as the trikl()
factory itself; zero or more drops may be specified.
let trickle = trikl()
.drop(drip => drip(), drip => drip()) // add two drops
.drop(drip => {
// Oh! We discovered that we have to do another task. Add it to the list.
trickle.drop(drip => drip())
drip()
})
Note: If we had called
drip()
in that last drop before adding the new task, it would have resolved the underlying promise, as Trikl would have detected that it was the last drop in the chain.
trickle.drip()
Drip to the next drop in the trickle. Any arguments will be passed to the target drop. By default, all drops will receive a reference to this function as their first argument. Typically, Trikl will call this automatically for the first drop in every trickle (to get the ball rolling). Then each drop will typically contain exactly one call to trickle.drip()
to pass execution on to the next drop in line once it's finished doing its thing. Calling this after all drops are done (usually from within the last drop) is equivalent to calling trickle.drip.stop()
.
let trickle = trikl()
// `trickle.drip()` is the first argument of drops by default.
.drop(drip => drip('one', 'two'))
// The last drop passed `val1` and `val2` to this guy.
.drop((drip, val1, val2) => drip(val1 + val2))
// That last call to `trickle.drip()` resolved the underlying promise.
.then(result => {
console.log(result) // logs "onetwo"
})
trickle.drip.skip()
Skip one or more drops. Returns a modified version of trickle.drip()
that, when called (if called), removes the specified number of drops from the remaining drops in the chain. If this results in the last drop being removed, or there are no drops to remove, then this is equivalent to calling trickle.drip.stop()
. One drop will be skipped if this is called with no arguments, a number less than 1, or anything that isNaN
.
trikl(drip => drip())
.drop(drip => {
drip.skip(2)('better')
})
.drop(drip => drip()) // will be skipped
.drop(drip => drip('meh')) // will be skipped
.drop((drip, val) => drip(`I got a ${val} value!`))
.then(result => {
console.log(result) // <- logs "I got a better value!"
})
trickle.drip.stop()
Stop the trickle and resolve or reject the underlying promise. Accepts one argument. If that argument is an instance of Error
(ReferenceError
, TypeError
, etc), the underlying promise is rejected with the argument as the reason. Otherwise, it's resolved with the argument as the result.
trikl(drip => drip())
.drop(drip => {
drip.stop("actually, we're done early")
})
.drop(drip => drip("finally done")) // never runs
.then(result => {
console.log(result) // <- logs "actually, we're done early"
})
trickle.dropArgs
A reference to the list of arguments passed to the last-called drop. Can be used for an alternative workflow.
let trickle = trikl(drip => drip(1))
.drop(drip => {
let [val] = trickle.dropArgs
drip(val, val + 1)
})
.drop(drip => {
let [originalVal, newVal] = trickle.dropArgs
drip(newVal + 1)
})
trickle.drops
A reference to this trickle's array of drops. Exposed for fine-grained control. Use with caution! Typically you'll use this as a read-only value (e.g. to see how many tasks are remaining in the trickle). Prefer trickle.drip.skip()
and trickle.drip.stop()
when those suit your needs. But sometimes you need to add/remove a drop to/from the middle or end of the list:
let trickle = trikl(drip => {
// Oh! We discovered that we no longer need the last task. Kill it.
trickle.drops.pop()
drip('a')
}).drop((drip, val) => {
drip(val + 'b')
}).drop((drip, val) => {
drip(val + 'c')
})
trickle.then(result => {
console.log(result) // <- logs "ab"
})
Flags
These flags will create a whole new trickle factory that will create specific types of trickles. They are completely inter-compatible and can be mixed and combined in any order.
trikl.pure
Creates a factory for creating pure trickles. The drops of pure trickles will not receive trickle.drip()
as an argument. Instead, the drops must maintain a reference to their trickle and call trickle.drip()
directly. This makes the drop function signatures simpler and potentially easier to read.
const trikl = require('trikl').pure
let trickle = trikl(() => trickle.drip(2))
.drop(val => trickle.drip(val * 2))
.drop(val => trickle.drip(val * 3))
.then(result => {
console.log(result) // <- logs "12"
return result
})
trikl.halt
Creates a factory for creating halted trickles. A halted trickle will not start dripping automatically. Instead, it will wait until you call trickle.drip()
manually. Useful for packaging operations together for later use, e.g. for build processes:
const trikl = require('trikl').halt
const APP_DIR = 'app'
function getBuildProcess() {
return trikl(findFiles, runBabel, uglify, writeFiles)
}
getBuildProcess().drip(APP_DIR)
Note that with this flag, arguments can be passed to the first drop. Can also be used to force a trickle to begin dripping immediately. This shaves off some time, since trickles normally wait until the end of the current turn of the event loop (via calling setTimeout
) to start dripping. This flag is required to make synchronous trickles.
trikl.hard
Creates a factory for creating hard trickles. A hard trickle will pass a modified version of trickle.drip()
to its drops. This modified version can be called only once. Useful for enforcing a strict drop-by-drop flow. Note that this flag will have no effect if used with the pure
flag, as trickle.drip()
won't be passed to drops anyway.
let trickle = trikl.hard(drip => drip())
.drop(drip => {
drip(1)
drip(2) // will have no effect
})
.drop(drip => {
setTimeout(drip.bond(3))
})
.then(result => {
console.log(result) // logs "3"
})
Note that we could have still bypassed hard mode in that first drop by doing either of the following:
// 1) we can still access the un-modified `drip()` method on the trickle itself:
drip(1)
trickle.drip(2)
// 2) `drip()` returns the trickle for chaining, so this one is really just an alias of the first:
drip(1).drip(2)
Examples
Example One – Using drip()
asynchronously.
trikl(drip => {
setTimeout(drip)
}).drop(drip => {
let val = 'some-val'
setTimeout(drip.bind(null, val))
}).drop((drip, val) => {
console.log(val) // <- logs "some-val"
})
Mainly just two things to note here:
You'll usually pass the
drip
function reference directly to async functions (e.g.setTimeout
,fs.readFile
).You can partially apply the
drip
function usingdrip.bind(null, val1, val2...)
. Those bound values will be passed to the next drop normally.
Example Two – A convention: Return the underlying promise from functions that define trickles. This just ensures that code that might expect a real promise receives one.
function compile(code) {
return trikl(drip => {
transpile(code, drip)
}).drop((drip, transpiledCode) => {
compile(transpiledCode, drip) // this last drop will resolve the promise
}).promise // <- return the underlying promise
}
// Anything that calls `compile()` can just use the promise normally.
compile(theCode)
.then(compiledCode => {})
.catch(error => {})
Example Three – A much more in-depth example with various methods of error handling. This example reads some JSON from a file, parses it asynchronously, makes a modification, stringifies it asynchronously, and saves it:
const fs = require('fs')
const parse = require('json-parse-async')
const stringify = require('async-json')
const trikl = require('trikl')
function modifyJsonFile(file) {
return trikl(drip => {
// An example of callback control (note: the next drop handles errors):
fs.readFile(file, 'utf8', drip)
}).drop((drip, err, contents) => {
// Yes, we can throw stuff! Trikl will catch it and reject the underlying promise for us.
if (err) throw err // or drip.stop(err)
// An example of promise control (note: errors are handled right here):
parse(contents)
.then(drip).catch(drip.stop) // just a good habit; whenever a drop handles promises, catch any errors and pass them to`drip.stop()`
}).drop((drip, json) => {
json.modification = 'modified!'
stringify(json, drip)
}).drop((drip, err, str) => {
// Instead of throwing the error, we can also call `drip.stop()` with it and Trikl will reject the promise for us.
if (err) drip.stop(err)
// We don't need another drop to handle errors for the last operation;
// Trikl will detect errors and reject/resolve the underlying promise accordingly.
fs.writeFile(file, str, drip)
}).promise
}
Note: If you want to reject the underlying promise with something that isn't an instance of Error, calling
drip.stop()
won't work. You have to throw it or calltrickle.reject()
manually.
Example Four – A trikl()
factory with multiple flags.
let trickle = trikl.halt.pure(val => trickle.drip(val + 'b'))
.drop(val => trickle.drip(val + 'c'))
.drip('a') // start the halted trickle
.then(result => {
console.log(result) // <- logs "abc"
})
Advantages
So you were probably impressed by this. But in case you weren't, or you just didn't notice some of these, I figured I'd spell out the benefits of using Trikl over a normal promise chain here:
Drops are asynchronous by default
Whereas in a promise chain, the links are assumed to be synchronous, unless told otherwise, in Trikl it's the other way around. This makes Trikl ideal for performing many dependent asynchronous operations, while promise chains are more useful as middleware – a series of functions that take a value, modify it, and pass it on to the next function.
This saves a level of nesting. To get a promise chain to flow like Trikl does by default, you have to create and return a new promise from every link in the chain:
let promise = new Promise(resolve => {
resolve(1)
}).then(result => {
return new Promise(resolve => {
setTimeout(resolve.bind(null, result + 1))
})
}).then(result => {
console.log(result) // <- logs "2"
return result
})
This is equivalent to the following trickle:
let trickle = trikl(drip => {
drip(1)
}).drop((drip, result) => {
setTimeout(drip.bind(null, result + 1))
}).drop((drip, result) => {
console.log(result) // <- logs "2"
drip(result)
})
On the other hand, making a drop synchronous is simple; just call drip()
synchronously. Making drops asynchronous by default is definitely the way to go, offering no disadvantages and several advantages over the synchronous-by-default approach.
Less confusing
I have seen many people confused by the difference between the function passed to the Promise constructor and the function passed to promise.then()
. Trikl doesn't have any difference here. A drop is a drop.
It provides another asynchronous layer
Trikl is not meant to replace the Promise api. You should use Trikl and promises hand-in-hand. I found while working with promise chains that it can be confusing which links are middleware and which are endware – which links modify the value and pass it on and which take the finished value and use it. Trikl can be useful here. Use Trikl to perform the business logic and then use the underlying promise to perform the end operations. Since the last drop in a trickle resolves the underlying promise, it gives you a clear "business logic done" point:
trikl(doSomeBusinessLogic)
.drop(doSomeMoreBusinessLogic)
.then(doStuffWithResult)
Multiple values can trickle down
Once you start using Trikl, you'll probably start to find this pretty annoying about promise chains: You can only resolve a promise with a single value. This means that in order to pass multiple points of data on to the next link, you have to wrap them all in an object or an array, or save them in a containing scope. While with ES6, this isn't all that annoying, it's still simpler with Trikl:
// With Promises:
let promise = new Promise(resolve => {
resolve(['one', 'two'])
}).then(([one, two]) => {
console.log(one, two)
return [one, two]
})
// With Trikl:
let trickle = trikl(drip => {
drip('one', 'two')
}).drop((drip, one, two) => {
console.log(one, two)
drip([one, two])
})
Note that since the last drop in the trickle resolves the underlying promise, it unfortunately does have to pass just one value to drip()
.
More control
With promise chains, you must assume the code will run asynchronously and basically immediately. Using the trikl.halt
flag, you have perfect control over when and how your trickle executes. You can run it synchronously:
// Yeah, don't do this at home. These impure functions will give you nightmares:
let val = 0
trikl.halt(() => val++)
.drop(() => val++)
.drip()
.drip()
console.log(val) // <- logs "2"
Or put a trickle together and use it later:
let buildProcess = trikl.halt(findFiles, convertJsx, uglify, bundle, compileSass)
// somewhere later:
buildProcess.drip()
Smarter workflow
I doubt I'm the only one who's had to do this many times:
let resolve, reject
let promise = new Promise((_resolve, _reject) => {
// I don't want to resolve or reject anything in here!
// Maybe because I don't know whether to resolve or reject yet.
// Maybe because I don't want to nest more async calls inside this already-nested callback.
resolve = _resolve
reject = _reject
})
promise.then(...)
.then(...)
.catch(...)
resolve('I am resolving you down here! Not inside the lame promise resolver function.')
Trikl does this for you. The resolve()
and reject()
functions come packaged with every trickle.
let trickle = trikl()
trickle.then(...)
.then(...)
.catch(...)
trickle.resolve('Whoa, my life just got easy.')
Bugs, Pull Requests, Feedback, Comments, Requests, Whatever
Bugs can be reported at the github issues page. All suggestions and feedback are most welcome. Also feel free to fork and pull request – just make sure the tests pass (npm test
) and try to keep the tests at ~100% coverage. Happy coding!
License
The MIT License