Hook Tutorial

WARNING! v0.13 was just released, and the tutorial is currently undergoing final testing. It's recommended that you DO NOT follow along until this message is removed (please check back tomorrow).

A tutorial for learning to create your own hooks

Step 5: Add Implementation

In this step we're going to implement the core functionality of our hook.

You can view the finished code for this step by checking out the step-5 branch.

Define the Interface

For this tutorial, we want to be able to continually fetch the array of tweets every X seconds. To do that, we're going to configure this hook so that it has an interface that looks like this:

lore.polling.tweet.find(query);

This interface will mean "invoke the tweet.find action with the provided query and continually invoke that action every X seconds".

Add Polling Function

Let's start by adding a function called poll that will repeatedly call an action. Add this function to the top of your index.js file:

function poll(action, config) {
  // invoke the action
  action();

  // wait the specified interval, then invoke the action again
  setTimeout(function() {
    poll(action, config);
  }, config.interval);
}

This function will take an action and a config object, and will invoke that action every X milliseconds (determined by the interval value in the config object).

Add Polling Wrapper Function

You may notice that we don't provide any arguments to the action we invoke in the poll function, and that's intentional.

This hook is designed to repeatedly call any action, but it doesn't know what the interface for any of those actions looks like. But luckily, through the magic of JavaScript, we also don't need to. The action we invoke above is actually a wrapper around the real action, where the arguments are already bound to it.

To illustrate, add this function to your index.js file as well:

function createPollingWrapper(action, config) {
  return function callAction() {
    // Create a version of the action that is bound to the arguments provided by the
    // user. This makes sure the hook will work with any arbitrary function - it simply
    // invokes that action with the provided arguments on the requested interval
    var boundAction = Function.prototype.apply.bind(action).bind(null, null, arguments);

    // Begin polling the action
    return poll(boundAction, config);
  }
}
function createPollingWrapper(action, config) {
  return function callAction() {
    // Create a version of the action that is bound to the arguments provided by the
    // user. This makes sure the hook will work with any arbitrary function - it simply
    // invokes that action with the provided arguments on the requested interval
    const boundAction = Function.prototype.apply.bind(action).bind(null, null, arguments);

    // Begin polling the action
    return poll(boundAction, config);
  }
}
function createPollingWrapper(action, config) {
  return function callAction() {
    // Create a version of the action that is bound to the arguments provided by the
    // user. This makes sure the hook will work with any arbitrary function - it simply
    // invokes that action with the provided arguments on the requested interval
    const boundAction = Function.prototype.apply.bind(action).bind(null, null, arguments);

    // Begin polling the action
    return poll(boundAction, config);
  }
}

This function might look strange, but it's pretty nifty. Let's say our application wants to poll for tweets by the user with the userId of 1. That call (given our interface defined above) would look like this:

lore.polling.tweet.find({
  userId: 1
})

This function essentially creates a function (the boundAction) that looks like this:

function boundAction() {
  return lore.actions.tweet.find({
    userId: 1
  })
}

It's that boundAction function that gets passed to (and invoked) by poll, and which already contains whatever arguments were originally provided by the user.

Add Function to flatten the Actions Object

The last helper function we're going to create will help us convert the actions object into an object that mirrors the structure, but where each function is a pollable wrapper over the action (what will ultimately be exposed by our hook).

The actions object (lore.actions) for this application looks like this:

lore.actions = {
  currentUser: function() {...},
  tweet: {
    create: function() {...},
    destroy: function() {...},
    find: function() {...},
    get: function() {...},
    update: function() {...}
  },
  user: {
    create: function() {...},
    destroy: function() {...},
    find: function() {...},
    get: function() {...},
    update: function() {...}
  }
}

To help us iterate through it, we're going to write a function that will flatten that object into a structure that looks like this:

lore.actions = {
  'currentUser': function() {...},
  'tweet.create': function() {...},
  'tweet.destroy': function() {...},
  'tweet.find': function() {...},
  'tweet.get': function() {...},
  'tweet.update': function() {...},
  'user.create': function() {...},
  // ... etc.
}

Add this function to the index.js file:

/**
* Flatten javascript objects into a single-depth object
* https://gist.github.com/penguinboy/762197
*/
function flattenObject(ob) {
  var toReturn = {};

  for (var i in ob) {
    if (!ob.hasOwnProperty(i)) continue;

    if ((typeof ob[i]) == 'object') {
      var flatObject = flattenObject(ob[i]);
      for (var x in flatObject) {
        if (!flatObject.hasOwnProperty(x)) continue;

        toReturn[i + '.' + x] = flatObject[x];
      }
    } else {
      toReturn[i] = ob[i];
    }
  }
  return toReturn;
}
/**
* Flatten javascript objects into a single-depth object
* https://gist.github.com/penguinboy/762197
*/
function flattenObject(ob) {
  const toReturn = {};

  for (let i in ob) {
    if (!ob.hasOwnProperty(i)) continue;

    if ((typeof ob[i]) == 'object') {
      const flatObject = flattenObject(ob[i]);
      for (let x in flatObject) {
        if (!flatObject.hasOwnProperty(x)) continue;

        toReturn[i + '.' + x] = flatObject[x];
      }
    } else {
      toReturn[i] = ob[i];
    }
  }
  return toReturn;
}
/**
* Flatten javascript objects into a single-depth object
* https://gist.github.com/penguinboy/762197
*/
function flattenObject(ob) {
  const toReturn = {};

  for (let i in ob) {
    if (!ob.hasOwnProperty(i)) continue;

    if ((typeof ob[i]) == 'object') {
      const flatObject = flattenObject(ob[i]);
      for (let x in flatObject) {
        if (!flatObject.hasOwnProperty(x)) continue;

        toReturn[i + '.' + x] = flatObject[x];
      }
    } else {
      toReturn[i] = ob[i];
    }
  }
  return toReturn;
}

Add Implementation

With those functions in place, we're ready to finish our hook. Update the load method to look like this:

...
  load: function(lore) {
    // 1. Get the actions so we can make them pollable
    var actions = lore.actions;

    // 2. Get the application level config (defaults + config/polling.js)
    var appConfig = lore.config.polling;

    // 3. Get the model specific configs
    var modelConfigs = lore.loader.loadModels();

    // 4. Create a polling object that will mirror the structure of the actions object
    lore.polling = {};

    // 5. Iterate over each action and create a pollable version attached to the polling object
    _.mapKeys(flattenObject(actions), function(action, actionKey) {
      // 6. Get the model specific config
      var modelName = actionKey.split('.')[0];
      var modelConfig = modelConfigs[modelName];

      // 7. Combine values from both configs, giving priority to values in the model config
      var config = _.defaults({}, modelConfig.polling, appConfig);

      // 8. Generate the pollable version of the action
      _.set(lore.polling, actionKey, createPollingWrapper(action, config));
    });
  }
...
...
  load: (lore) => {
    // 1. Get the actions so we can make them pollable
    const actions = lore.actions;

    // 2. Get the application level config (defaults + config/polling.js)
    const appConfig = lore.config.polling;

    // 3. Get the model specific configs
    const modelConfigs = lore.loader.loadModels();

    // 4. Create a polling object that will mirror the structure of the actions object
    lore.polling = {};

    // 5. Iterate over each action and create a pollable version attached to the polling object
    _.mapKeys(flattenObject(actions), function(action, actionKey) {
      // 6. Get the model specific config
      const modelName = actionKey.split('.')[0];
      const modelConfig = modelConfigs[modelName];

      // 7. Combine values from both configs, giving priority to values in the model config
      const config = _.defaults({}, modelConfig.polling, appConfig);

      // 8. Generate the pollable version of the action
      _.set(lore.polling, actionKey, createPollingWrapper(action, config));
    });
  }
...
...
  load: (lore) => {
    // 1. Get the actions so we can make them pollable
    const actions = lore.actions;

    // 2. Get the application level config (defaults + config/polling.js)
    const appConfig = lore.config.polling;

    // 3. Get the model specific configs
    const modelConfigs = lore.loader.loadModels();

    // 4. Create a polling object that will mirror the structure of the actions object
    lore.polling = {};

    // 5. Iterate over each action and create a pollable version attached to the polling object
    _.mapKeys(flattenObject(actions), function(action, actionKey) {
      // 6. Get the model specific config
      const modelName = actionKey.split('.')[0];
      const modelConfig = modelConfigs[modelName];

      // 7. Combine values from both configs, giving priority to values in the model config
      const config = _.defaults({}, modelConfig.polling, appConfig);

      // 8. Generate the pollable version of the action
      _.set(lore.polling, actionKey, createPollingWrapper(action, config));
    });
  }
...

There's a few things happening here again, so let's break down each line to discuss what this code means.

4. Expose Hook Functionality

Some (though not all) hooks are intended to expose functionality for the user to leverage. The typical way of doing that is by modifying the lore object and attaching the functionality we want to expose. Since the inteface for this hook is going to access through calls like lore.polling.tweet.find() we're going to extend lore with a polling object we'll fill in shortly.

5. Iterate over the Actions

This line flattens the actions object (as described above), and then iterates through it, returning the action and the actionKey (such as tweet.find).

6. Extract Model Config

To respect the behavior of cascading overrides, we need to get the config file corresponding to the action we're mapping. We do this by splitting the actionKey (tweet.find) and grabbing the first token (tweet). When we get the config for the tweet model.

7. Generate Combined Config

This line creates the final config, starting with values defined in the polling section of the model config (if it exists) and then adding any values from config/polling.js that aren't defined (it's the same effect as using the model config to override values in the application level config).

8. Populate Polling Object

This line populates our polling object by creating an entry for the action name and assigning a value that is our pollable function. For example, given an actionKey of tweet.find, this line will nest the wrapped action at lore.polling.tweet.find.

Check In

With these changes in place, our hook is finished, and your index.js file should look like this:

import _ from 'lodash';

/**
 * Flatten javascript objects into a single-depth object
 * https://gist.github.com/penguinboy/762197
 */
function flattenObject(ob) {
  var toReturn = {};

  for (var i in ob) {
    if (!ob.hasOwnProperty(i)) continue;

    if ((typeof ob[i]) == 'object') {
      var flatObject = flattenObject(ob[i]);
      for (var x in flatObject) {
        if (!flatObject.hasOwnProperty(x)) continue;

        toReturn[i + '.' + x] = flatObject[x];
      }
    } else {
      toReturn[i] = ob[i];
    }
  }
  return toReturn;
}

/**
 * Call the action (with the bound arguments) every [interval] milliseconds
 */
function poll(action, config) {
  // invoke the action
  action();

  // wait the specified interval, then invoke the action again
  setTimeout(function() {
    poll(action, config);
  }, config.interval);
}

function createPollingWrapper(action, config) {
  return function callAction() {
    // Create a version of the action that is bound to the arguments provided by the
    // user. This makes sure the hook will work with any arbitrary function - it simply
    // invokes that action with the provided arguments on the requested interval
    var boundAction = Function.prototype.apply.bind(action).bind(null, null, arguments);

    // Begin polling the action
    return poll(boundAction, config);
  }
}

export default {

  dependencies: ['bindActions'],

  defaults: {
    polling: {
      interval: 3000
    }
  },

  load: function(lore) {
    // 1. Get the actions so we can make them pollable
    var actions = lore.actions;

    // 2. Get the application level config (defaults + config/polling.js)
    var appConfig = lore.config.polling;

    // 3. Get the model specific configs
    var modelConfigs = lore.loader.loadModels();

    // 4. Create a polling object that will mirror the structure of the actions object
    lore.polling = {};

    // 5. Iterate over each action and create a pollable version attached to the polling object
    _.mapKeys(flattenObject(actions), function(action, actionKey) {
      // 6. Get the model specific config
      var modelName = actionKey.split('.')[0];
      var modelConfig = modelConfigs[modelName];

      // 7. Combine values from both configs, giving priority to values in the model config
      var config = _.defaults({}, modelConfig.polling, appConfig);

      // 8. Generate the pollable version of the action
      _.set(lore.polling, actionKey, createPollingWrapper(action, config));
    });
  }

};
import _ from 'lodash';

/**
 * Flatten javascript objects into a single-depth object
 * https://gist.github.com/penguinboy/762197
 */
function flattenObject(ob) {
  const toReturn = {};

  for (let i in ob) {
    if (!ob.hasOwnProperty(i)) continue;

    if ((typeof ob[i]) == 'object') {
      const flatObject = flattenObject(ob[i]);
      for (let x in flatObject) {
        if (!flatObject.hasOwnProperty(x)) continue;

        toReturn[i + '.' + x] = flatObject[x];
      }
    } else {
      toReturn[i] = ob[i];
    }
  }
  return toReturn;
}

/**
 * Call the action (with the bound arguments) every [interval] milliseconds
 */
function poll(action, config) {
  // invoke the action
  action();

  // wait the specified interval, then invoke the action again
  setTimeout(function() {
    poll(action, config);
  }, config.interval);
}

function createPollingWrapper(action, config) {
  return function callAction() {
    // Create a version of the action that is bound to the arguments provided by the
    // user. This makes sure the hook will work with any arbitrary function - it simply
    // invokes that action with the provided arguments on the requested interval
    const boundAction = Function.prototype.apply.bind(action).bind(null, null, arguments);

    // Begin polling the action
    return poll(boundAction, config);
  }
}

export default {

  dependencies: ['bindActions'],

  defaults: {
    polling: {
      interval: 3000
    }
  },

  load: (lore) => {
    // 1. Get the actions so we can make them pollable
    const actions = lore.actions;

    // 2. Get the application level config (defaults + config/polling.js)
    const appConfig = lore.config.polling;

    // 3. Get the model specific configs
    const modelConfigs = lore.loader.loadModels();

    // 4. Create a polling object that will mirror the structure of the actions object
    lore.polling = {};

    // 5. Iterate over each action and create a pollable version attached to the polling object
    _.mapKeys(flattenObject(actions), function(action, actionKey) {
      // 6. Get the model specific config
      const modelName = actionKey.split('.')[0];
      const modelConfig = modelConfigs[modelName];

      // 7. Combine values from both configs, giving priority to values in the model config
      const config = _.defaults({}, modelConfig.polling, appConfig);

      // 8. Generate the pollable version of the action
      _.set(lore.polling, actionKey, createPollingWrapper(action, config));
    });
  }

}
import _ from 'lodash';

/**
 * Flatten javascript objects into a single-depth object
 * https://gist.github.com/penguinboy/762197
 */
function flattenObject(ob) {
  const toReturn = {};

  for (let i in ob) {
    if (!ob.hasOwnProperty(i)) continue;

    if ((typeof ob[i]) == 'object') {
      const flatObject = flattenObject(ob[i]);
      for (let x in flatObject) {
        if (!flatObject.hasOwnProperty(x)) continue;

        toReturn[i + '.' + x] = flatObject[x];
      }
    } else {
      toReturn[i] = ob[i];
    }
  }
  return toReturn;
}

/**
 * Call the action (with the bound arguments) every [interval] milliseconds
 */
function poll(action, config) {
  // invoke the action
  action();

  // wait the specified interval, then invoke the action again
  setTimeout(function() {
    poll(action, config);
  }, config.interval);
}

function createPollingWrapper(action, config) {
  return function callAction() {
    // Create a version of the action that is bound to the arguments provided by the
    // user. This makes sure the hook will work with any arbitrary function - it simply
    // invokes that action with the provided arguments on the requested interval
    const boundAction = Function.prototype.apply.bind(action).bind(null, null, arguments);

    // Begin polling the action
    return poll(boundAction, config);
  }
}

export default {

  dependencies: ['bindActions'],

  defaults: {
    polling: {
      interval: 3000
    }
  },

  load: (lore) => {
    // 1. Get the actions so we can make them pollable
    const actions = lore.actions;

    // 2. Get the application level config (defaults + config/polling.js)
    const appConfig = lore.config.polling;

    // 3. Get the model specific configs
    const modelConfigs = lore.loader.loadModels();

    // 4. Create a polling object that will mirror the structure of the actions object
    lore.polling = {};

    // 5. Iterate over each action and create a pollable version attached to the polling object
    _.mapKeys(flattenObject(actions), function(action, actionKey) {
      // 6. Get the model specific config
      const modelName = actionKey.split('.')[0];
      const modelConfig = modelConfigs[modelName];

      // 7. Combine values from both configs, giving priority to values in the model config
      const config = _.defaults({}, modelConfig.polling, appConfig);

      // 8. Generate the pollable version of the action
      _.set(lore.polling, actionKey, createPollingWrapper(action, config));
    });
  }

}

Next Steps

Next we're going to integrate the hook into our application.