Skip to content

Instantly share code, notes, and snippets.

@fluffywaffles
Last active May 25, 2016 06:31
Show Gist options
  • Save fluffywaffles/112a3f665c9df6bd6bb043f583925dab to your computer and use it in GitHub Desktop.
Save fluffywaffles/112a3f665c9df6bd6bb043f583925dab to your computer and use it in GitHub Desktop.

Botloader Spec

An example

// 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)
  })
}

But how?

There are essentially 2 components to the botloader:

  1. The actual loader API
  2. 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:

Underpinnings (getter and cache)

getter

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

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.

cache.fetch(what, then)

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.

cache.fetchAll(keyArray, then)

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.

quick tip

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.

botloader

botloader API

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment