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) {
action();
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() {
var boundAction = Function.prototype.apply.bind(action).bind(null, null, arguments);
return poll(boundAction, config);
}
}
function createPollingWrapper(action, config) {
return function callAction() {
const boundAction = Function.prototype.apply.bind(action).bind(null, null, arguments);
return poll(boundAction, config);
}
}
function createPollingWrapper(action, config) {
return function callAction() {
const boundAction = Function.prototype.apply.bind(action).bind(null, null, arguments);
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() {...},
}
Add this function to the index.js
file:
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;
}
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;
}
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) {
var actions = lore.actions;
var appConfig = lore.config.polling;
var modelConfigs = lore.loader.loadModels();
lore.polling = {};
_.mapKeys(flattenObject(actions), function(action, actionKey) {
var modelName = actionKey.split('.')[0];
var modelConfig = modelConfigs[modelName];
var config = _.defaults({}, modelConfig.polling, appConfig);
_.set(lore.polling, actionKey, createPollingWrapper(action, config));
});
}
...
...
load: (lore) => {
const actions = lore.actions;
const appConfig = lore.config.polling;
const modelConfigs = lore.loader.loadModels();
lore.polling = {};
_.mapKeys(flattenObject(actions), function(action, actionKey) {
const modelName = actionKey.split('.')[0];
const modelConfig = modelConfigs[modelName];
const config = _.defaults({}, modelConfig.polling, appConfig);
_.set(lore.polling, actionKey, createPollingWrapper(action, config));
});
}
...
...
load: (lore) => {
const actions = lore.actions;
const appConfig = lore.config.polling;
const modelConfigs = lore.loader.loadModels();
lore.polling = {};
_.mapKeys(flattenObject(actions), function(action, actionKey) {
const modelName = actionKey.split('.')[0];
const modelConfig = modelConfigs[modelName];
const config = _.defaults({}, modelConfig.polling, appConfig);
_.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';
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;
}
function poll(action, config) {
action();
setTimeout(function() {
poll(action, config);
}, config.interval);
}
function createPollingWrapper(action, config) {
return function callAction() {
var boundAction = Function.prototype.apply.bind(action).bind(null, null, arguments);
return poll(boundAction, config);
}
}
export default {
dependencies: ['bindActions'],
defaults: {
polling: {
interval: 3000
}
},
load: function(lore) {
var actions = lore.actions;
var appConfig = lore.config.polling;
var modelConfigs = lore.loader.loadModels();
lore.polling = {};
_.mapKeys(flattenObject(actions), function(action, actionKey) {
var modelName = actionKey.split('.')[0];
var modelConfig = modelConfigs[modelName];
var config = _.defaults({}, modelConfig.polling, appConfig);
_.set(lore.polling, actionKey, createPollingWrapper(action, config));
});
}
};
import _ from 'lodash';
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;
}
function poll(action, config) {
action();
setTimeout(function() {
poll(action, config);
}, config.interval);
}
function createPollingWrapper(action, config) {
return function callAction() {
const boundAction = Function.prototype.apply.bind(action).bind(null, null, arguments);
return poll(boundAction, config);
}
}
export default {
dependencies: ['bindActions'],
defaults: {
polling: {
interval: 3000
}
},
load: (lore) => {
const actions = lore.actions;
const appConfig = lore.config.polling;
const modelConfigs = lore.loader.loadModels();
lore.polling = {};
_.mapKeys(flattenObject(actions), function(action, actionKey) {
const modelName = actionKey.split('.')[0];
const modelConfig = modelConfigs[modelName];
const config = _.defaults({}, modelConfig.polling, appConfig);
_.set(lore.polling, actionKey, createPollingWrapper(action, config));
});
}
}
import _ from 'lodash';
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;
}
function poll(action, config) {
action();
setTimeout(function() {
poll(action, config);
}, config.interval);
}
function createPollingWrapper(action, config) {
return function callAction() {
const boundAction = Function.prototype.apply.bind(action).bind(null, null, arguments);
return poll(boundAction, config);
}
}
export default {
dependencies: ['bindActions'],
defaults: {
polling: {
interval: 3000
}
},
load: (lore) => {
const actions = lore.actions;
const appConfig = lore.config.polling;
const modelConfigs = lore.loader.loadModels();
lore.polling = {};
_.mapKeys(flattenObject(actions), function(action, actionKey) {
const modelName = actionKey.split('.')[0];
const modelConfig = modelConfigs[modelName];
const config = _.defaults({}, modelConfig.polling, appConfig);
_.set(lore.polling, actionKey, createPollingWrapper(action, config));
});
}
}
Next Steps
Next we're going to integrate the hook into our application.