// app.js
var botloader = require('botloader')
botloader.with({
slack: new Slack(<Bot Access Key>),
octopus: require('./botconfig')
}).prefetch(
'teamUrl'
).load(
aBotController,
require('./controllers/anotherBotController')
)
// here's the first controller
function aBotController () {
console.log(this.a) // "a"
}
// here's the second controller
// in ./controllers/anotherBotController.js
module.exports = function anotherBotController () {
console.log(this.prefetched.teamUrl) // the Slack teamUrl
this.cache('somethingThatMightOrMightNotBeCached', function (value) {
// gets 'value' of 'somethingThatMightOrMightNotBeCached' from the cache if possible, otherwise fetches it using a pre-registered "getter" from wherever it lives (e.g., from a firebase ref, or from the slack API)
// ... (do something with that value)
})
}
There are essentially 2 components to the botloader:
- The actual loader API
- A better
node-cache
wrapper
This better node-cache
wrapper is used to prefetch data. It uses an abstraction I (unceremoniously) named a "getter".
getter
underpins cache
, which itself underpins the prefetch
functionality of the botloader itself.
Let's spec out about those components:
getter
asynchronously resolves a value for a given key. A getfn
for a key can perform asynchronous operations or resolve immediately. How it indicates that it has resolved
is up to the implementation. If you use Promises, you literally call this.resolve()
. That said, there are a couple ways you could do this.
####getter.register(name, getFn)
Registers a "getter" for a resource identified by "name". For instance, getter.register('teamUrl', getTeamUrlFromSlackAPI)
registers the function getTeamUrlFromSlackAPI
as a getter for teamUrl
.
The getter can be asked to fetch multiple registered keys in a row. For instance, it could be used like:
getter.multiple(2)
.onAllComplete(functionToRunWhenAllValuesHaveBeenGotten)
.get('teamUrl', cacheResult)
.get('users', cacheResult)
// Once TeamUrl and Users callbacks have both been called, the onAllCompete callback will be called. Values for 'teamUrl' and 'users' should be passed into the onAllComplete as a single array argument. (eg `['epicatnu', ['fluffywaffles', ..., 'jkang']]`)
In this way, you can make sure you have gotten all the values you need before running your final callback. For instance, if you need both the teamUrl and users list for a Slack app before you can... I dunno. Order them all cookies or something. Then you can make the getter.onAllComplete
handler be orderCookies
.
How the getter
for a key is accessed is up to you. One possible method is presented in the example above.
Cache is a node-cache
wrapper. It first tries to get a value from the cache and, that failing, calls the getter for that value instead. If no getter can be found, an error is thrown.
First, cache.fetch
tries to get the key what
from the in-memory cache. If it doesn't find anything, it looks for a getter for what
. If it finds no getter, it throws an error. This error could be something like "CacheError: No getter exists for {what} and no cached value could be found."
If a cached value was found, the then
callback is called with the cached value.
If a cached value is not found but a getter is found, the then
callback is called with the result of running the getter. The then
callback is not responsible for error handling if, for instance, the HTTP request happening in the background times out. This should be handled by cache.fetch
. If there's no error, then cache.fetch
should first cache the value
for what
, and then call the then
callback with the value gotten.
Much like cache.fetch(what, then)
, but it simultaneously fires off getters for every key in keyArray
and only calls the then
callback once every getter has finished. The callback should be called with the resulting values in an array as its argument.
There has to be only one instance of the node-cache
underpinning the cache
and getter
. One option is to put both the getter
and the cache
into the same JS file. (This is not ideal.) Alternatively, you can create a node-cache
instance in cache
and pass it to getter
. For instance, you could write getter such that when you require it you have to pass in the node-cache
instance. (eg var getter = require('./getter')(cacheInstance)
.) Or, you could have a method like setCache
and do getter.setCache(octopusCache)
before using any other methods on getter
.
The botloader API itself is very simple. You just need to write the following methods:
botloader.with(contextObj)
specifies the context that loader controllers will be called with. (In essence, contextObj
will become this
in those functions.)
botloader.args(arg1, arg2, ...)
specifies the arguments that should be passed into the controllers.
botloader.prefetch(key1, key2, ...)
specifies the cache keys to prefetch (by using their getters if they are not already cached) and added to ctx.prefetched
(which will become this.prefetched
inside of the loader controllers).
botloader.load(controller1, controller2, ...)
specifies the controllers to require and call with contextObj
as this
, with the values for key1, key2, ...
added to this.prefetched
(eg this.prefetched.key1 === value1
, etc.), and with arg1, arg2, ...
as their arguments.
The API is simple, but it isn't immediately obvious how to implement.
You'll need to use function.apply
to set function context and call with arguments.