Actions

General

/////////////////////
// A Simple Action //
/////////////////////

exports.action = {
  name: 'randomNumber',
  description: 'I am an API method which will generate a random number',
  outputExample: {
    randomNumber: 0.123
  },

  run: function(api, data, next){
    data.response.randomNumber = Math.random();
    next();
  }
};
  

The core of ActionHero is the Action framework, and actions are the basic units of work. All connection types from all servers can use actions. This means that you only need to write an action once, and both HTTP clients and websocket clients can consume it.

The goal of an action is to read data.params (which are the arguments a connection provides), do work, and set the data.response (and error when needed) values to build the response to the client.

You can create you own actions by placing them in a ./actions/ folder at the root of your application. You can use the generator with actionhero generate action --name=myAction

Here's an example of a simple action which will return a random number to the client:

You can also define more than one action per file if you would like, to share common methods and components (like input parsers):

/////////////////////////////////////////
// A Combound Action with Shared Inputs//
/////////////////////////////////////////

var commonInputs = {
  email: {
    required: true,
    validator: function(param){
      var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
      if( re.test(email) ){
        return true;
      }else{
        return new Error('that is not a valid email address');
      }
    },
  },
  password: {
    required: true,
    validator: function(param){
      if(param.length < 4){
        return new Error('password should be at least 3 letters long');
      }else{
        return true;
      }
    },
    formatter: function(param){
      return String(param);
    },
  }
};

// the actions

exports.userAdd = {
  name: 'userAdd',
  description: 'I add a user',
  inputs: commonInputs,
  run: function(api, data, next){
    // your code here
    next(error);
  }
};

exports.userDelete = {
  name: 'userDelete',
  description: 'I delete a user',
  inputs: commonInputs,
  run: function(api, data, next){
    // your code here
    next(error);
  }
}
  

Versions

ActionHero supports multiple versions of the same action. This will allow you to support actions/routes of the same name with upgraded functionality.

  • actions optionally have the action.version attribute.
  • a reserved param, apiVersion is used to directly specify the version of an action a client may request.
  • if a client doesn't specify an apiVersion, they will be directed to the highest numerical version of that action.

You can optionally create routes to handle your API versioning:

exports.routes = {
  all: [
    // creates routes like `/api/myAction/1/` and `/api/myAction/2/`
    // will also default `/api/myAction` to the latest version
    { path: "/myAction/:apiVersion", action: "myAction" },

    // creates routes like `/api/1/myAction/` and `/api/2/myAction/`
    // will also default `/api/myAction` to the latest version
    { path: "/:apiVersion/myAction", action: "myAction" },
  ]
};
  

As a note, if a client accessing ActionHero via routes does not provide an apiVersion and it is explicitly defined in the route, the highest number will not be assigned automatically, and will be seen as a routing error.

Options

exports.action = {
  // the action's name (the `exports` key doesn't matter)
  name: "randomNumber",
  // the description
  description: "I am an API method which will generate a random number",
  // a hash of all the inputs this action will accept
  // any inputs provided to the action not in this hash will be stripped
  inputs: {
    multiplier: {
      required: false,
      validator: function(param, connection, actionTemplate){ if(param < 0){
        return 'must be > 0' }else{ return true; }
      },
      formatter: function(param, connection, actionTemplate){
        return parseInt(param);
      },
      default:   function(param, connection, actionTemplate){
        return 1;
      },
    }
  },
  // any middlewares to apply before/after this action
  // global middleware will be applied automatically
  middleware: [],
  // an example response
  outputExample: { randomNumber: 123 },
  // you can choose to block certain servers from using this action
  blockedConnectionTypes: ["webSocket"],
  // how should this action be logged?
  logLevel: "warning",
  // (HTTP only) if the route for this action includes an extension (like .jpg), should the response MIME be adjusted to match?
  matchExtensionMimeType: true,
  // should this action appear within `api.documentation.documentation`
  toDocument: true,

  run: function(api, data, next){
    var error = null;

    data.response.randomNumber = Math.random() * data.params.multiplier;
    next(error);
  }
}

The complete set of options an action can have are:

Inputs

action.inputs = {
  // a simple input
  // defaults assume required = false
  minimalInput: {}
  // a complex input
  multiplier: {
    required: true,
    validator: function(param, connection, actionTemplate){
      if(param < 0){ return 'must be > 0'; }else{ return true; }
    },
    formatter: function(param, connection, actionTemplate){
      return parseInt(param);
    },
    default:   function(param, connection, actionTemplate){
      return 1;
    },
  },
  // a schema input
  schemaInput: {
    required: true,
    default: {},
    schema: {
      nestedInput: {
        required: true,
        default: 1,
        validator: function(param, connection, actionTemplate){
          if(param < 0){ return 'must be > 0'; }else{ return true; }
        },
        formatter: function(param, connection, actionTemplate){
          return parseInt(param);
        },
      },
      otherInput: {},
    }
  }
};
  

The properties of an input are:

  • required (boolean)
    • Default: false
  • formatter = function(param, connection, actionTemplate)
    • will return the new value of the param
    • Default: The parameter is not reformatted
  • default = function(param, connection, actionTemplate)
    • will return the default value of the param
    • you can also have a static assignment for default father than a function, ie: default: 123
    • Default: Parameter has no default value
  • validator = function(param, connection, actionTemplate)
    • should return true if validation passed
    • should return an error message if validation fails which will be returned to the client
    • Default: Parameter is always valid
  • schema (object)
    • optional nested inputs definition
    • accept object similar to regular input
    • nested input also have properties: required, formatter, default and validator

You can define api.config.general.missingParamChecks = [null, '', undefined] to choose explicitly how you want un-set params to be handled in your actions. For example, if you want to allow explicit null values in a JSON payload but not undefined, you can now opt-in to that behavior. This is what action.inputs.x.required = true will check against.

Since all properties of an input are optional, the smallest possible definition of an input is: name : {}. However, you should usually specify that an input is required (or not), ie: name: {required: false}.

The methods default, formatter, and validator have the api object set as this within their scopes. This means that you can define common formatters within middleware and reference them in each action.

The methods are applied in this order:

  • default()
  • formatter()
  • validator()
  • required()

Here's an example...

moneyInCents: {
  required:  true,
  default:   function(p){ return 0; },
  formatter: function(p){ return parseFloat(p); },
  validator: function(p){
    if(isNaN(parseFloat(p)){ return new Error('not a number'); }
    if(p < 0){ return new Error('money cannot be negative'); }
    else{ return true; }
  },
}
  

...and the results would be:

  • If moneyInCents = 4 => (4 => 4 => 400 => ok)
  • If moneyInCents = "4" => ("4" => 4 => 400 => ok)
  • If moneyInCents = "-4" => ("-4" => -4 => -400 => Error(‘money cannot be negative'))
  • If moneyInCents = "" => 0 (default value)
  • If moneyInCents = null => 0 (default value)
  • If moneyInCents = "hello" => Error(‘not a number')

Formatters and Validators can also be named method names. For example, you might have an action like:

exports.cacheTest = {
  name: 'cacheTest',
  description: 'I will test the internal cache functions of the API',
  outputExample: {},

  inputs: {
    key: {
      required: true,
      formatter: [
         function(s){ return String(s); },
         'api.formatter.uniqueKeyName' // <----------- HERE
      ]
    },
    value: {
      required: true,
      formatter: function(s){ return String(s); },
      validator: function(s){
        if(s.length < 3){ return '`value` should be at least 3 letters long'; }
        else{ return true; }
      }
    },
  },

  run: function(api, data, next){
    // ...
  }

};
  

You can define api.formatter.uniqueKeyName elsewhere in your project, like this initializer:

module.exports = {
  initialize: function(api, next){
    api.formatter = {
      uniqueKeyName: function(key){
        return key + '-' + this.connection.id;
      }
    };

    next();
  },
};
  

Example schema input:

  exports.addUser = {
    name: 'api/addUser',
    description: 'I add user',
    
    firstName: { required: true },
    lastName: { required: false },
    username: { required: true },
    address: {
      required: false,
      schema: {
        country: {
          required: true,
          default: 'USA'
        },
        state: { required: false },
        city: {
          required: true,
          formatter: (val) => `City:${val}`,
          validator: (val) => val.length > 10,
        }
      }
    }
    run: () => {},
  }
  

The Data Object

data = {
  connection: connection,
  action: 'randomNumber',
  toProcess: true,
  toRender: true,
  messageCount: 123,
  params: { action: 'randomNumber', apiVersion: 1 },
  actionStartTime: 123,
  response: {},
}
  

The data object passed into your action captures the state of the connection at the time the action was started. Middleware preProcessors have already fired, and input formatting and validation has occurred. Here are the properties of the data object:

The goal of most actions is to do work and then modify the value of data.response, which will eventually be sent down to the client.

You can also modify properties of the connection by accessing data.connection, IE changing the response header for a HTTP request.

If you don't want your action to respond to the client, or you have already sent data to the client (perhaps you already rendered a file to them or sent an error HTTP header), you can set data.toRender = false;

Using Middleware in Actions

You can create middlware which would apply to the connection both before and after an action. Middleware can be either global (applied to all actions) or local, specified in each action via action.middleware = []. Supply the names of any middleware you want to use.

You can learn more about middleware here.

Notes

  • Actions are asynchronous, and require in the API object, the data object, and the callback function. Completing an action is as simple as calling next(error). If you have an error, be sure that it is a new Error() object, and not a string.
  • The metadata outputExample is used in reflexive and self-documenting actions in the API, available via the documentation verb (and /api/ showDocumenation action).
  • You can limit how many actions a persistent client (websocket, tcp, etc) can have pending at once with api.config.general.simultaneousActions
  • actions.inputs are used for both documentation and for building the whitelist of allowed parameters the API will accept. Client params not included in these whitelists will be ignored for security. If you wish to disable the whitelisting you can use the flag at api.config.general.disableParamScrubbing. Note that Middleware preProcessors will always have access to all params pre-scrubbing.
  • matchExtensionMimeType is currently only used by the web server, and it indicates that if this action is successfully called by a client with connection.extension set, the headers of the response should be changed to match that file type. This is useful when creating actions that download files.
  • ActionHero strives to keep the data.connection object uniform among various client types, and more importantly, present data.params in a homogeneous way. You can inspect data.connection.type to learn more about the connection. The gory details of the connection (which vary on its type) are stored in data.connection.rawConnection which will contain the websocket, tcp connection, etc. For web clients, data.connection.rawConnection = {req: req, res: res} for example.

You can learn more about handling HTTP verbs and file uploads here and TCP Clients and Web-Socket Clients

Tasks

General

Tasks are background jobs meant to be run separately from a client's request. They can be started by an action or by the server itself. With ActionHero, there is no need to run a separate daemon to process these jobs. ActionHero uses the node-resque package to store and process tasks in a way compatible with the resque ecosystem.

There are 3 types of tasks ActionHero can process: normal, delayed, and periodic.

  • normal tasks are enqueued and processed one-by-one by the task TaskProcessors
  • delayed tasks are enqueued in a special ‘delayed' queue to only be processed at some time in the future (defined either by a timestamp in ms or milliseconds-from-now)
  • periodic tasks are like delayed tasks, but they run on a set frequency (e.g. every 5 minutes).
    • Periodic tasks can take no input parameters.

Enqueuing a Task

// Enqueue the task now, and process it ASAP
// api.tasks.enqueue(nameOfTask, args, queue, callback)
api.tasks.enqueue("sendWelcomeEmail", {to: [email protected]'}, 'default', function(error, toRun){
  // enqueued!
});

// Enqueue the task now, and process it once `timestamp` has arrived
// api.tasks.enqueueAt(timestamp, nameOfTask, args, queue, callback)
api.tasks.enqueueAt(1234556, "sendWelcomeEmail", {to: [email protected]'}, 'default', function(error, toRun){
  // enqueued!
});

// Enqueue the task now, and process it once `delay` (ms) has passed
// api.tasks.enqueueIn(delay, nameOfTask, args, queue, callback)
api.tasks.enqueueIn(10000, "sendWelcomeEmail", {to: [email protected]'}, 'default', function(error, toRun){
  // enqueued!
});
  

Here are examples of the 3 ways to programmatically enqueue a task.

"sendWelcomeEmail" should be a task defined in the project, and {to: [email protected]'} are arguments to that task. This task will be processed by TaskProcessors assigned to the ‘default queue'.

You can also enqueue tasks to be run at some time in the future (timestamp is in ms):

enqueueAt asks for a timestamp (in ms) to run at, and enqueueIn asks for the number of ms from now to run.

The final type of task, periodic tasks, are defined with a task.frequency of greater than 0, and are loaded in by ActionHero when it boots. You cannot modify these tasks once the server is running.

Processing Tasks

// From /config/tasks.js:

exports.default = {
  tasks: function(api){
    return {
      // Should this node run a scheduler to promote delayed tasks?
      scheduler: false,
      // what queues should the TaskProcessors work?
      queues: ['*'],
      // Logging levels of task workers
      workerLogging : {
        failure   : 'error', // task failure
        success   : 'info',  // task success
        start     : 'info',
        end       : 'info',
        cleaning_worker : 'info',
        poll      : 'debug',
        job       : 'debug',
        pause     : 'debug',
        internalError : 'error',
        multiWorkerAction : 'debug'
      },
      // Logging levels of the task scheduler
      schedulerLogging : {
        start     : 'info',
        end       : 'info',
        poll      : 'debug',
        enqueue   : 'debug',
        reEnqueue : 'debug',
        working_timestamp : 'debug',
        transferred_job   : 'debug'
      },
      // how long to sleep between jobs / scheduler checks
      timeout: 5000,
      // at minimum, how many parallel taskProcessors should this node spawn?
      // (have number > 0 to enable, and < 1 to disable)
      minTaskProcessors: 0,
      // at maximum, how many parallel taskProcessors should this node spawn?
      maxTaskProcessors: 0,
      // how often should we check the event loop to spawn more TaskProcessors?
      checkTimeout: 500,
      // how many ms would constitue an event loop delay to halt TaskProcessors spawning?
      maxEventLoopDelay: 5,
      // When we kill off a taskProcessor, should we disonnect that local redis connection?
      toDisconnectProcessors: true,
      // Customize Resque primitives, replace null with required replacement.
      resque_overrides: {
        queue: null,
        multiWorker: null,
        scheduler: null
      }
    }
  }
}
  

To work these tasks, you need to run ActionHero with at least one taskProcessor. TaskProcessors run in-line with the rest of your server and process jobs. This is controlled by settings in /config/tasks.js.

If you are enqueuing delayed or periodic tasks, you also need to enable the scheduler. This is a part of ActionHero that will periodically check the delayed queues for jobs that are ready to work now, and move them to the normal queues when the time comes.

Because node and ActionHero are asynchronous, we can process more than one job at a time. However, if the jobs we are processing are CPU-intensive, we want to limit how many we are working on at one time. To do this, we tell ActionHero to run somewhere between minTaskProcessors and maxTaskProcessors and check every so often if the server could be working more or less jobs at a time. Depending on the response characteristics you want for your server, you can modify these values.

In production, it is best to set up some ActionHero servers that only handle requests from clients (that is, servers with no TaskProcessors) and others that handle no requests, and only process jobs (that is, no servers, many TaskProcessors).

As you noticed above, when you enqueue a task, you tell it which queue to be enqueued within. This is so you can separate load or priority. For example, you might have a high priority queue which does jobs like "sendPushMessage" and a low priority queue which does a task like "cleanupCache". You tell the taskProcessors which jobs to work, and in which priority. For the example above, you would ensure that all high jobs happen before all low jobs by setting: api.config.tasks.queues = ['high', 'low']. You could also configure more nodes to work on the high queue than the low queue, thus further ensuring that high priority jobs are processed faster and sooner than low priority jobs.

Creating a Task

// define a single task in a file

var task = {
  name:          "sendWelcomeEmail",
  description:   "I will send a new user a welcome email",
  queue:         "default",
  plugins:       [],
  pluginOptions: [],
  frequency:     0,
  run: function(api, params, next){
    api.sendEmail(params.email, function(error){
      next(error); //task will fail if sendEmail does
    })
  }
};

exports.task = task;

// define multiple tasks (so you can share methods)

exports.sayHello = {
  name:          'sayHello',
  description:   'I say hello',
  queue:         "default",
  plugins:       [],
  pluginOptions: [],
  frequency:     1000,
  run: function(api, params, next){
    api.log("hello")
    next();
  }
};

exports.sayGoodbye = {
  name:          'sayGoodbye',
  description:   'I say goodbye',
  queue:         "default",
  plugins:       [],
  pluginOptions: [],
  frequency:     2000,
  run: function(api, params, next){
    api.log("goodbye")
    next();
  }
};
  
# The output of running the last 2 tasks would be:

2013-11-28 15:21:56 - debug: resque scheduler working timestamp 1385680913
2013-11-28 15:21:56 - debug: resque scheduler enquing job 1385680913 class=sayHello, queue=default,
2013-11-28 15:21:56 - debug: resque scheduler working timestamp 1385680914
2013-11-28 15:21:56 - debug: resque scheduler enquing job 1385680914 class=sayGoodbye, queue=default,
2013-11-28 15:21:56 - debug: resque worker #1 working job default class=sayHello, queue=default,
2013-11-28 15:21:56 - info: hello
2013-11-28 15:21:56 - debug: re-enqueued reccurent job sayHello
2013-11-28 15:21:56 - debug: resque worker #1 working job default class=sayGoodbye, queue=default,
2013-11-28 15:21:56 - info: goodbye
2013-11-28 15:21:56 - debug: re-enqueued reccurent job sayGoodbye
  

You can create you own tasks by placing them in a ./tasks/ directory at the root of your application. You can use the generator actionhero generate task --name=myTask. Like actions, all tasks have some required metadata:

  • task.name: The unique name of your task
  • task.description: a description
  • task.queue: the default queue to run this task within (can be overwritten when enqueued)
  • task.frequency: In milliseconds, how often should I run?. A frequency of >0 denotes this task as periodic and ActionHero will automatically enqueued when the server boots. Only one instance of a periodic task will be enqueued within the cluster at a time, regardless of how many ActionHero nodes are connected.
  • task.plugins: You can use resque plugins in your task from the node-resque project. Plugins modify how your tasks are enqueued. For example, if you use the queue-lock plugin, only one instance of any job (with similar arguments) can be enqueued at a time. You can learn more about plugins from the node-resque project.
  • task.pluginOptions: a hash of options for the plugins

task.run contains the actual work that the task does. It takes the following arguments:

  • api: The ActionHero api object
  • params: An array of parameters that the task was enqueued with. This is whatever was passed as the second argument to api.tasks.enqueue
  • next: A callback to call when the task is done. This callback is of the type function(error, result).
    • Passing an error object will cause the job to be marked as a failure.
    • The result is currently not captured anywhere.

Queue Inspection

ActionHero provides some methods to help inspect the state of your queue. You can use these methods to check if your jobs are processing in a timely manner, if there are errors in task processing, etc.

api.tasks.scheduledAt(queue, taskName, args, next)

  • next(error, timestamps)
  • finds all matching instances of queue + taskName + args from the delayed queues
  • timestamps will be an array of the delayed timestamps

api.tasks.del(queue, taskName, args, count, next)

  • next(error, count)
  • removes all matching instances of queue + taskName + args from the normal queues
  • count is how many instances of this task were removed

api.tasks.delDelayed(queue, taskName, args, next)

  • next(error, timestamps)
  • removes all matching instances of queue + taskName + args from the delayed queues
  • timestamps will be an array of the delayed timestamps which the task was removed from

api.tasks.delQueue(queue, next)

  • next(error)
  • removes all jobs in a resque queue

api.tasks.enqueueRecurrentJob(taskName, next)

  • next()
  • will enqueue are recurring job
  • might not actually enqueue the job if it is already enqueued due to resque plugins

api.tasks.stopRecurrentJob(taskName, next)

  • next(error, removedCount)
  • will remove all instances of taskName from the delayed queues and normal queues
  • removedCount will inform you of how many instances of this job were removed

api.tasks.timestamps(next)

  • next(error, timestamps)
  • will return an array of all timesamps which have at least one job scheduled to be run
  • for use with api.tasks.delayedAt

api.tasks.queued(q, start, stop, next)

  • next(error, jobs)
  • will return an array of all pending jobs in a resque queue (paginated via start/stop)

api.tasks.stats(next)

  • next(error, stats)
  • will return an array of all stats from your resque cluster

api.tasks.locks(next)

  • next(error, locks)
  • will return an array of all locks from your resque cluster (both queue and worker)

api.tasks.delLock(lockName, next)

  • next(error, count)
  • will return the count of locks deleted (if any)

api.tasks.delayedAt(timestamp, next)

  • next(error, jobs)
  • will return the list of jobs enqueued to run after this timestamp

api.tasks.allDelayed(next)

  • next(error, jobs)
  • will return the list of all jobs enqueued by the timestamp they are enqueued to run at

api.tasks.workers(next)

  • next(error, workers)
  • list all taskProcessors

api.tasks.workingOn(workerName, queues, next)

  • next(error, status)
  • list what a specific taskProcessors (defined by the name of the server + queues) is working on (or sleeping)

api.tasks.allWorkingOn(next)

  • next(error, workers)
  • list what all taskProcessors are working on (or sleeping)

api.tasks.details(next)

  • next(error, details)
  • details is a hash of all the queues in the system and how long they are
  • this method also returns metadata about the taskProcessors and what they are currently working on

api.tasks.failedCount(next)

  • next(error, failedCount)
  • failedCount is how many resque jobs are in the failed queue.

api.tasks.failed(start, stop, next)

api.tasks.removeFailed(failedJob, next)

  • next(error, removedCount)
  • the input failedJob is an expanded node object representing the failed job, retrieved via api.tasks.failed

api.tasks.retryAndRemoveFailed(failedJob, next)

  • next(error, failedJob)
  • the error failedJob is an expanded node object representing the failed job, retrieved via api.tasks.failed

Job Schedules

// file: initializers/node_schedule.js

var schedule = require('node-schedule');

module.exports = {
  initialize: function(api, next){
    api.scheduledJobs = [];
    next();
  },

  start: function(api, next){

    // do this job every 10 seconds, cron style
    var job = schedule.scheduleJob('0,10,20,30,40,50 * * * * *', function(){
      // we want to ensure that only one instance of this job is scheduled in our environment at once,
      // no matter how many schedulers we have running

      if(api.resque.scheduler && api.resque.scheduler.master){
        api.tasks.enqueue('sayHello', {time: new Date().toString()}, 'default', function(error){
          if(error){ api.log(error, 'error'); }
        });
      }
    });

    api.scheduledJobs.push(job);

    next();
  },

  stop: function(api, next){
    api.scheduledJobs.forEach(function(job){
      job.canel();
    });

    next();
  }
};
  

You may want to schedule jobs every minute/hour/day, like a distributed CRON job. There are a number of excellent node packages to help you with this, like node-schedule and node-cron. ActionHero exposes node-resque's scheduler to you so you can use the scheduler package of your choice.

Assuming you are running ActionHero across multiple machines, you will need to ensure that only one of your processes is actually scheduling the jobs. To help you with this, you can inspect which of the scheduler processes is correctly acting as master, and flag only the master scheduler process to run the schedule. An initializer for this would look like:

Be sure to have the scheduler enabled on at least one of your ActionHero servers!

Failed Job Management

var removeStuckWorkersOlderThan = 10000; // 10000ms
api.log('removing stuck workers solder than ' + removeStuckWorkersOlderThan + 'ms', 'info');
api.tasks.cleanOldWorkers(removeStuckWorkersOlderThan, function(error, result){
  if(error){
    api.log(error, 'error');
  }
  if(Object.keys(result).length > 0){
    api.log('removed stuck workers with errors: ', 'info', result);
  }
  callback();
});
  

Sometimes a worker crashes is a severe way, and it doesn't get the time/chance to notify redis that it is leaving the pool (this happens all the time on PAAS providers like Heroku). When this happens, you will not only need to extract the job from the now-zombie worker's "working on" status, but also remove the stuck worker. To aid you in these edge cases, api.tasks.cleanOldWorkers(age, callback) is available.

Because there are no ‘heartbeats' in resque, it is impossible for the application to know if a worker has been working on a long job or it is dead. You are required to provide an "age" for how long a worker has been "working", and all those older than that age will be removed, and the job they are working on moved to the error queue (where you can then use api.tasks.retryAndRemoveFailed) to re-enqueue the job.

You can handle this with an own initializer and the following logic =>

Extending Resque

In cases where you would like to extend or modify the underlying behaviour or capabilities of Resque you can specify replacements for the Queues, Scheduler, or Multi Worker implementations in the Tasks configuration.

// From /config/tasks.js:
var myQueue = require('../util/myQueue.js');

exports.default = {
  tasks: function(api){
    return {
      ...
      // Customize Resque primitives, replace null with required replacement.
      resque_overrides: {
        queue: myQueue,  //<-- Explicitly pass replacement Queue implementation
        multiWorker: null,
        scheduler: null
      }
    }
  }
}
  
//From util/myQueue.js:
var NR = require('node-resque');
var pluginRunner = require('../node_modules/node-resque/lib/pluginRunner.js');

let myQueue = NR.queue;

myQueue.prototype.enqueueFront = function(q, func, args, callback){
  var self = this;
  if(arguments.length === 3 && typeof args === 'function'){
   callback = args;
   args = [];
  }else if(arguments.length < 3){
   args = [];
  }

  args = arrayify(args);
  var job = self.jobs[func];
  pluginRunner.runPlugins(self, 'before_enqueue', func, q, job, args, function(err, toRun){
   if(toRun === false){
     if(typeof callback === 'function'){ callback(err, toRun); }
   }else{
     self.connection.redis.sadd(self.connection.key('queues'), q, function(){
	   self.connection.redis.lpush(self.connection.key('queue', q), self.encode(q, func, args), function(){
	     pluginRunner.runPlugins(self, 'after_enqueue', func, q, job, args, function(){
		   if(typeof callback === 'function'){ callback(err, toRun); }
	     });
	   });
     });
   }
  });
};

module.exports = myQueue;
  

The above example will give you access to api.resque.queue.enqueueFront(), which you could use directly or wrap by extending the api.tasks object.

Notes

Note that the frequency, enqueueIn and enqueueAt times are when a task is allowed to run, not when it will run. TaskProcessors will work tasks in a first-in-first-out manner. TaskProcessors also sleep when there is no work to do, and will take some time (default 5 seconds) to wake up and check for more work to do.

Remember that each ActionHero server uses one thread and one event loop, so that if you have computationally intensive task (like computing Fibonacci numbers), this will block tasks, actions, and clients from working. However, if your tasks are meant to communicate with external services (reading from a database, sending an email, etc), then these are perfect candidates to be run simultaneously as the single thread can work on other things while waiting for these operations to complete.

Tasks are stored in redis. Be sure to enable non-fake redis if you want your tasks to persist and be shared across more than one ActionHero server.

If you are running a single ActionHero server, all tasks will be run locally. As you add more servers, the work will be split evenly across all nodes. It is very likely that your job will be run on different nodes each time.

Middleware

Middleware

There are 4 types of middleware in ActionHero:

  • Action
  • Connection
  • Chat
  • Task
> Client **Connects**
#     connection middleware, `create` hook
> Client requests an **action**
#     action middleware, `preProcessor` hook
#     action middleware, `postProcessor` hook
> Client **joins a room**
#     chat middleware, `join` hook
> Client **says a message** in a room
#     chat middleware, `say` hook
#     chat middleware, `onSayReceive` hook
> Client requests a **disconnect** (quit)
#     chat middleware, `leave` hook
#     connection middleware, `destroy` hook
> Client executes a **task**
#     task middleware, `preProcessor` hook
#     task middleware, `postProcessor` hook
  

Each type of middleware is distinct from the others, and operates on distinct parts of a client's lifecycle. For a logical example, please inspect the following connection lifecycle:

Action Request Flow

Action Middleware

var middleware = {
  name: 'userId checker',
  global: false,
  priority: 1000,
  preProcessor: function(data, next){
    if(!data.params.userId){
      next(new Error('All actions require a userId') );
    }else{
      next();
    }
  },
  postProcessor: function(data, next){
    if(data.thing.stuff == false){
      data.toRender = false;
    }
    next(error);
  }
}

api.actions.addMiddleware(middleware);
  

ActionHero provides hooks for you to execute custom code both before and after the execution of all or some actions. This is a great place to write authentication logic or custom loggers.

Action middleware requires a name and at least one of preProcessor or postProcessor. Middleware can be global, or you can choose to apply each middleware to an action specifically via action.middleware = [] in the action's definition. You supply a list of middleware names, like action.middleware = ['userId checker'] in the example above.

Each processor is passed data and the callback next. Just like within actions, you can modify the data object to add to data.response to create a response to the client. If you pass error to the callback next, that error will be returned to the client. If a preProcessor has an error, the action will never be called.

The priority of a middleware orders it with all other middleware which might fire for an action. Lower numbers happen first. If you do not provide a priority, the default from api.config.general.defaultProcessorPriority will be used

The Data Object

data contains:

data = {
  connection: {},
  action: 'randomNumber',
  toProcess: true,
  toRender: true,
  messageCount: 1,
  params: { action: 'randomNumber', apiVersion: 1 },
  missingParams: [],
  validatorErrors: [],
  actionStartTime: 1429531553417,
  actionTemplate: {}, // the actual object action definition
  working: true,
  response: {},
  duration: null,
  actionStatus: null,
}
  

Connection Middleware

var connectionMiddleware = {
  name: 'connection middleware',
  priority: 1000,
  create: function(connection){
    // do stuff
  },
  destroy: function(connection){
    // do stuff
  }
};

api.connections.addMiddleware(connectionMiddleware);
  

Like the action middleware above, you can also create middleware to react to the creation or destruction of all connections. Unlike action middleware, connection middleware is non-blocking and connection logic will continue as normal regardless of what you do in this type of middleware.

Keep in mind that some connections persist (webSocket, socket) and some only exist for the duration of a single request (web). You will likely want to inspect connection.type in this middleware. Again, if you do not provide a priority, the default from api.config.general.defaultProcessorPriority will be used.

Any modification made to the connection at this stage may happen either before or after an action, and may or may not persist to the connection depending on how the server is implemented.

Chat Middleware

var chatMiddleware = {
  name: 'chat middleware',
  priority: 1000,
  join: function(connection, room, callback){
    // announce all connections entering a room
    api.chatRoom.broadcast({}, room, 'I have joined the room: ' + connection.id, callback);
  },
  leave: function(connection, room, callback){
    // announce all connections leaving a room
    api.chatRoom.broadcast({}, room, 'I have left the room: ' + connection.id, callback);
  },
  /**
   * Will be executed once per client connection before delivering the message.
   */
  say: function(connection, room, messagePayload, callback){
    // do stuff
    api.log(messagePayload);
    callback(null, messagePayload);
  },
  /**
   * Will be executed only once, when the message is sent to the server.
   */
  onSayReceive: function(connection, room, messagePayload, callback){
    // do stuff
    api.log(messagePayload);
    callback(null, messagePayload);
  }
};

api.chatRoom.addMiddleware(chatMiddleware);
  

The last type of middleware is used to act when a connection joins, leaves, or communicates within a chat room. We have 4 types of middleware for each step: say, onSayReceive, join, and leave.

Priority is optional in all cases, but can be used to order your middleware. If an error is returned in any of these methods, it will be returned to the user, and the action/verb/message will not be sent.

More detail and nuance on chat middleware can be found in the chat section

Chat Midleware Notes

  • In the example above, I want to announce the member joining the room, but he has not yet been added to the room, as the callback chain is still firing. If the connection itself were to make the broadcast, it would fail because the connection is not in the room. Instead, an empty {} connection is used to proxy the message coming from the ‘system'
  • Only the sayCallbacks have a second return value on the callback, messagePayload. This allows you to modify the message being sent to your clients.
  • messagePayload will be modified and and passed on to all addSayCallback middlewares inline, so you can append and modify it as you go
  • If you have a number of callbacks (say, onSayReceive, join or leave), the priority maters, and you can block subsequent methods from firing by returning an error to the callback.
  • sayCallbacks are executed once per client connection. This makes it suitable for customizing the message based on the individual client.
  • onSayReceiveCallbacks are executed only once, when the message is sent to the server.
// in this example no one will be able to join any room, and the `say` callback will never be invoked.

api.chatRoom.addMiddleware({
  name: 'blocking chat middleware',
  join: function(connection, room, callback){
    callback(new Error('blocked from joining the room'));
  }),
  say: function(connection, room, messagePayload, callback){
    api.chatRoom.broadcast({}, room, 'I have entered the room: ' + connection.id, function(e){
      callback();
    });
  },
});
  

If a say is blocked/errored, the message will simply not be delivered to the client. If a join or leave is blocked/errored, the verb or method used to invoke the call will be returned that error.

Task Request Flow

Task Middleware

Task middleware is implemented as a thin wrapper around Node Resque plugins and currently exposes the before_perform, after_perform, before_enqueue, and after_enqueue functions of Resque plugins through preProcessor, postProcessor, preEnqueue, and postEnqueue methods. Each middleware requires a name and at least one function. In addition, a middleware can be global, in which case it also requires a priority.

In the preProcessor, you can access the original task params through this.args[0]. In the postProcessor, you can access the task result at this.worker.result. In the preEnqueue and postEnqueue you can access the task params through this.args[0]. If you wish to prevent a task from being enqueued using the preEnqueue middleware you must explicitly set the toRun value to false in the callback. Because the task middleware is executed by Resque this is an instance of a Resque Worker and contains a number of other elements which may be useful in a middleware.

Task Middleware Example

The following example is a simplistic implementation of a task execution timer middleware.

'use strict';

module.exports = {
  loadPriority:  1000,
  initialize: function(api, next){
    api.taskTimer = {
      middleware: {
        name: 'timer',
        global: true,
        priority: 90,
        preProcessor: function(next){
          var worker = this.worker;
          worker.start = process.hrtime();
          next();
        },
        postProcessor: function(next){
          var worker = this.worker;
          var elapsed = process.hrtime(worker.start);
          var seconds = elapsed[0];
          var millis = elapsed[1] / 1000000;
          api.log('Task ' + worker.job.class + ' finished in ' + seconds + ' s and ' + millis + ' ms.', 'info');
          next();
        },
        preEnqueue: function(next){
          var params = this.args[0];
          //Validate params
          next(null, true); //callback is in form cb(error, toRun)
        },
        postEnqueue: function(next){
          api.log("Task successfully enqueued!");
          next();
        }
      }
    };

    api.tasks.addMiddleware(api.taskTimer.middleware);
  }
};
  

Initializers

Overview

Initializers are the main way you expand your ActionHero server. This is where you connect to databases, modify the global api object with new classes and helper methods, and set up your middleware.

Initializers run in 3 phases coinciding with the lifecycles of the application: init, start, and stop. All init steps happen before all start steps. Initializers can define both methods and priorities which will happen at each phase of the server's lifecycle.

System initializers (like setting up redis and the cache) have priority levels in the 100 to 1000 level range. Application initializers should run with a priority level of over 1000 to use methods created by the system. You can of course set priority levels lower than 1000 in your application (perhaps you connect to a database). The priority level split is purely convention.

In general, initialize() methods should create prototypes and new objects, and start() should boot things or connect to external resources.

Format

// initializers/stuffInit.js

module.exports = {
  loadPriority:  1000,
  startPriority: 1000,
  stopPriority:  1000,
  initialize: function(api, next){
    api.myObject = {};

    next();
  },
  start: function(api, next){
    // connect to server
    next();
  },
  stop: function(api, next){
    // disconnect from server
    next();
  }
}
  

To use a custom initializer, create a initializers directory in your project. Export an object with at least one of start, stop or initialize and specify your priorities.

You can generate a file of this type with actionhero generate initializer --name=stuffInit

Errors

You can pass an error to the callback of any step in the initializer. Doing so will cause ActionHero to log the error and stop the server. For example, you might throw an error if you cannot connect to an external service at boot, like a database.

CLI

Allow actionhero developers to create new files in ./bin which can be run via the CLI. These commands will have access to a the ActionHero api and CLI arguments object within a run method.

You can create namespaces for commands by using folders. For example, a file in ./bin/redis/keys would be run via ./node_modules/.bin/actionhero redis keys --prefix actionhero

General

///////////////////
// A CLI Command //
///////////////////

module.exports = {
  name: 'redis keys',
  description: 'I list all the keys in redis',
  example: 'actionhero keys --prefix actionhero',

  inputs: {
    prefix: {
      requried: true,
      default: 'actionhero',
      note: 'the redis prefix for searching keys'
    }
  },

  run: function (api, data, next) {
    api.redis.clients.client.keys(data.params.prefix, (error, keys) => {
      if (error) { throw error }

      api.log(`Found ${keys.length} keys:`)
      keys.forEach((k) => { api.log(k) })

      return next(null, true)
    })
  }
}
  

ActionHero CLI commands have:

  • name
  • description
  • example

Inputs for CLI commands have

  • required (true/false)
  • default (string only)
  • note

These are sourced by `actionhero help`, and the example above would return:

* redis keys
  description: I list all the keys in redis
  example: actionhero keys --prefix actionhero
  inputs:
    [prefix] (optional)
      note: the redis prefix for searching keys
      default: actionhero
  

Action Cluster

AKA: Running ActionHero in a Cluster

Overview

ActionHero can be run either as a solitary server or as part of a cluster. The goal of these cluster helpers is to allow you to create a group of servers which will share state and each be able to handle requests and run tasks. You can add or remove nodes from the cluster without fear of data loss or task duplication. You can also run many instances of ActionHero on the same server using node.js' cluster methods (ActionHero start cluster), which you can learn more about here.

Cluster instances are named sequentially, starting with actionhero-worker-1, and can be retrieved from 'api.id'. Logs and PID's, as well as other instance-specific information follow this pattern as well.

Cache

Using a redis backend, ActionHero nodes share memory objects (using the api.cache methods) and have a common queue for tasks. This means that all peers will have access to all data stored in the cache. The task system also becomes a common queue which all peers will work on draining. There should be no changes required to deploy your application in a cluster.

Keep in mind that many clients/server can access a cached value simultaneously, so build your actions carefully not to have conflicting state. You can learn more about the cache methods here. You can also review recommendations about Production Redis configurations.

RPC

// This will ask all nodes connected to the cluster if they have connection #`abc123` and if they do, run `connection.set('auth', true) on it`
api.connections.apply('abc123', 'set', ['auth', true], function(error){
  // do stuff
});
  

In version 9.0.0, ActionHero introduced Remote Procedure Calls, or RPC for short. You can call an RPC method to be executed on all nodes in your cluster or just a node which holds a specific connection. You can call RPC methods with the api.redis.doCluster method. If you provide the optional callback, you will get the first response back (or a timeout error). RPC calls are invoked with api.redis.doCluster(method, args, connectionId, callback).

For example, if you wanted all nodes to log a message, you would do: api.redis.doCluster('api.log', ["hello from " + api.id]);

If you wanted the node which holds connection abc123 to change their authorized status (perhaps because your room authentication relies on this), you would do:

The RPC system is used heavily by Chat.

Two options have been added to the config/redis.js config file to support this: api.config.general.channel ( Which channel to use on redis pub/sub for RPC communication ) and api.config.general.rpcTimeout ( How long to wait for an RPC call before considering it a failure )

WARNING

RPC calls are authenticated against api.config.serverToken and communication happens over redis pub/sub. BE CAREFUL, as you can call any method within the API namespace on an ActionHero server, including shutdown() and read any data on that node.

Connections

Some special RPC tools have been added so that you can interact with connections across multiple nodes. Specifically the chat sub-system needs to be able to boot and move connections into rooms, regardless of which node they are connected to.

ActionHero has exposed api.connections.apply which can be used to retrieve data about and modify a connection on any node.

api.connections.apply(connectionId, method, args, callback)

  • connectionId is required
  • if method and args can be ignored if you just want to retrieve information about a connection, IE: api.connections.apply(connectionId, callback)
  • callback is of the form function(error, connectionDetails)

Generic Pub/Sub

// To subscribe to messages, add a callback for your `messageType`, IE:
api.redis.subscriptionHandlers['myMessageType'] = function(message){
  // do stuff
}

// send a message
var payload = {
  messageType: 'myMessageType',
  serverId: api.id,
  serverToken: api.config.general.serverToken,
  message: 'hello!',
}
api.redis.publish(payload)
  

ActionHero also uses redis to allow for pub/sub communication between nodes.

You can broadcast and receive messages from other peers in the cluster:

api.redis.publish(payload)

  • payload must contain:
    • messageType : ‘{the name of your payload type}',
    • serverId : api.id,
    • serverToken : api.config.general.serverToken,

Cache

Overview

ActionHero ships with the functions needed for a distributed key-value cache. You can cache strings, numbers, arrays and objects (anything that responds to JSON.stringify).

The cache's redis server is defined by api.config.redis. It is possible to use fakeredis.

Cache Methods

api.cache.save

  • Invoke: api.cache.save(key, value, expireTimeMS, next)
    • expireTimeMS can be null if you never want the object to expire
  • Callback: next(error, newObject)
    • error will be null unless the object can't be saved (perhaps out of ram or a bad object type).
    • overwriting an existing object will return newObject = true

api.cache.save is used to both create new entries or update existing cache entries. If you don't define an expireTimeMS, null will be assumed, and using null will cause this cached item to never expire. Expired cache objects will be periodically swept away (but not necessarily exactly when they expire)

api.cache.load

  • Invoke: api.cache.load(key, next) or api.cache.load(key, options, next)
    • options can be {expireTimeMS: 1234} where the act of reading the key will reset the key's expire time
    • If the requested key is not found (or is expired), all values returned will be null.
  • Callback: next(error, value, expireTimestamp, createdAt, readAt)
    • value will be the object which was saved and null if the object cannot be found or is expired
    • expireTimestamp (ms) is when the object is set to expire in system time
    • createdAt (ms) is when the object was created
    • readAt (ms) is the timestamp at which the object was last read with api.cache.load. Useful for telling if another worker has consumed the object recently

api.cache.destroy

  • Invoke: api.cache.destroy(key)
  • Callback: next(error, destroyed)
    • will be false if the object cannot be found, and true if destroyed

List methods

api.cache implements a distributed shared list. 3 simple functions are provided to interact with this list, push, pop, and listLength. These lists are stored in Redis, and cannot be locked. That said, a push and pop operation will guarantee that one-and-only-one copy of your data is returned to whichever application acted first (when popping) or an error will be returned (when pushing).

api.cache.push

  • Invoke: api.cache.push(key, data, next)
    • data must be serializable via JSON.stringify
  • Callback: next(error)

api.cache.pop

  • Invoke: api.cache.pop(key, next)
  • Callback: next(error, data)
    • data will be returned in the object form it was saved (array, object, string)

api.cache.listLength

  • Invoke: api.cache.listLength(key, next)
  • Callback: next(error, length)
    • length will be an integer.
      • if the list does not exist, 0 will be returned

Lock Methods

You may optionally implement locking methods along with your cache objects. This will allow one ActionHero server to obtain a lock on an object and prevent modification of it by another member of the cluster. For example you may want to first api.cache.lock a key, and then save it to prevent other nodes from modifying the object.

api.cache.lock

  • Invoke: api.cache.lock(key, expireTimeMS, next)
    • expireTimeMS is optional, and will be expireTimeMS = api.cache.lockDuration = api.config.general.lockDuration
  • Callback: next(error, lockOk)
    • error will be null unless there was something wrong with the connection (perhaps a redis error)
    • lockOk will be true or false depending on if the lock was obtained.

api.cache.unlock

  • Invoke: api.cache.unlock(key, next)
  • Callback: next(error, lockOk)
    • error will be null unless there was something wrong with the connection (perhaps a redis error)
    • lockOk will be true or false depending on if the lock was removed.

api.cache.checkLock

  • Invoke: api.cache.checkLock(key,retry, next)
    • retry is either null or an integer (ms) that we should keep retrying until the lock is free to be re-obtained
  • Callback: next(error, lockOk)
    • error will be null unless there was something wrong with the connection (perhaps a redis error)
    • lockOk will be true or false depending on if the lock is currently obtainable.

api.cache.locks

  • Invoke: api.cache.locks(next)
  • Callback: next(error, locks)
    • locks is an array of all currently active locks

You can see an example of using the cache within an action in actions/cacheTest.js

Redis

The timestamps regarding api.cache.load are to help clients understand if they are working with data which has been modified by another peer (when running in a cluster).

Keep in mind that many clients/servers can access a cached value simultaneously, so build your actions carefully not to have conflicting state. You can learn more about the cache methods here. You can also review recommendations about Production Redis configurations.

Chat

Overview

ActionHero ships with a chat framework which may be used by all persistent connections (socket and websocket). There are methods to create and manage chat rooms and control the users in those rooms. Chat does not have to be for peer-to-peer communication, and is a metaphor used for many things, including game state in MMOs.

Clients themselves interact with rooms via verbs. Verbs are short-form commands that will attempt to modify the connection's state, either joining or leaving a room. Clients can be in many rooms at once.

Relevant chat verbs are:

  • roomAdd
  • roomLeave
  • roomView
  • say

The special verb for persistent connections say makes use of api.chatRoom.broadcast to tell a message to all other users in the room, IE: say myRoom Hello World from a socket client or client.say("myRoom", 'Hello World") for a websocket.

Chat on multiple actionHero nodes relies on redis for both chat (pub/sub) and a key store defined by api.config.redis. Note that if you elect to use fakeredis, you will be using an in-memory redis server rather than a real redis process, which does not work to share data across nodes. The redis store and the key store don't need to be the same instance of redis, but they do need to be the same for all ActionHero servers you are running in parallel. This is how ActionHero scales the chat features.

There is no limit to the number of rooms which can be created, but keep in mind that each room stores information in redis, and there load created for each connection.

Methods

These methods are to be used within your server (perhaps an action or initializer). They are not exposed directly to clients, but they can be within an action.

api.chatRoom.broadcast(connection, room, message, callback)

  • tell a message to all members in a room.
  • connection can either be a real connection (A message coming from a client), or a mockConnection. A mockConnection at the very least has the form {room: "someOtherRoom"}. mockConnections without an id will be assigned the id of 0
  • The context of messages sent with api.chatRoom.broadcast always be user to differentiate these responses from a response to a request

api.chatRoom.list(callback)

  • callback will return (error, [rooms])

api.chatRoom.add(room, callback)

  • callback will return 1 if you created the room, 0 if it already existed

api.chatRoom.destroy(room, callback)

  • callback is empty

api.chatRoom.exists(room, callback)

  • callback returns (error, found); found is a boolean

api.chatRoom.roomStatus(room, callback)

  • callback returns (error, details); details is a hash containing room information
  • details of the form:
{
  room: "myRoom",
  membersCount: 2,
  members: {
    aaa: {id: "aaa", joinedAt: 123456789 },
    bbb: {id: "bbb", joinedAt: 123456789 },
  }
}
  

api.chatRoom.addMember(connectionId, room, callback)

  • callback is of the form (error, wasAdded)
  • you can add connections from this or any other server in the cluster

api.chatRoom.removeMember(connectionId, room, callback)

  • callback is of the form (error, wasRemoved)
  • you can remove connections from this or any other server in the cluster

api.chatRoom.generateMemberDetails( connection )

  • defines what is stored from the connection object in the member data
  • default is id: connection.id
  • other data that is stored by default is host: api.id and joinedAt: new Date().getTime()
  • override the entire method to store custom data that is on the connection

api.chatRoom.sanitizeMemberDetails( memberData )

  • Defines what is pulled out of the member data when returning roomStatus
  • Defaults to joinedAt : memberData.joinedAt
  • After method call, always filled with id, based on the connection.id used to store the data
  • Override the entire method to use custom data as defined in api.chatRoom.generateMemberDetails

api.chatRoom.generateMessagePayload( message )

  • Defiens how messages from clients are sanitized
  • Override the entire method to use custom data as defined in api.chatRoom.generateMessagePayload

Middleware

There are 4 types of middleware you can install for the chat system: say, onSayReceive, join, and leave. You can learn more about chat middleware in the middleware section of this document

Chatting to specific clients

Every connection object also has a connection.sendMessage(message) method which you can call directly from the server.

Client Use

The details of communicating within a chat room are up to each individual server (see websocket or socket), but the same principals apply:

  • Client will join a room (client.roomAdd(room)).
  • Once in the room, clients can send messages (which are strings) to everyone else in the room via say, ie: client.say('room', Hello World')
  • Once a client is in a room, they will revive messages from other members of the room as events. For example, catching say events from the websocket client looks like client.on('say', function(message){ console.log(message); }). You can inspect message.room if you are in more than one room.
    • The payload of a message will contain the room, sender, and the message body: {message: "Hello World", room: "SecretRoom", from: "7d419af9-accf-40ac-8d78-9281591dd59e", context: "user", sentAt: 1399437579346}

If you want to create an authenticated room, there are 2 steps:

  • First, create an action which modifies some property eitehr on the connection object it self, or stores permissions to a database.
  • Then, create a joinCallback-style middleware which cheks these values.

File Server

Overview

> curl localhost:8080/simple.html -v

*   Trying ::1...
* connect to ::1 port 8080 failed: Connection refused
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /simple.html HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Last-Modified: Fri Jun 12 2015 02:51:29 GMT-0700 (PDT)
< Cache-Control: max-age=60, must-revalidate, public
< Expires: Sun, 15 Nov 2015 02:07:46 GMT
< Content-Type: text/html
< Access-Control-Allow-Headers: Content-Type
< Access-Control-Allow-Methods: HEAD, GET, POST, PUT, PATCH, DELETE, OPTIONS, TRACE
< Access-Control-Allow-Origin: *
< X-Powered-By: actionhero API
< Set-Cookie: sessionID=d4453f54ff066a2ef078e5c80f18dc78a81f44ff;path=/;expires=Sun, 15 Nov 2015 03:06:46 GMT;
< Content-Length: 101
< Date: Sun, 15 Nov 2015 02:06:46 GMT
< Connection: keep-alive
<
* Connection #0 to host localhost left intact

<h1>ActionHero</h1>\nI am a flat file being served to you via the API from ./public/simple.html<br />
  

ActionHero comes with a file server which clients can make use of to request files on the ActionHero server. ActionHero is not meant to be a ‘rendering' server (like express or rails), but can serve static files.

If a directory is requested rather than a file, ActionHero will look for the file in that directory defined by api.config.commonWeb.directoryFileType (which defaults to index.html). Failing to find this file, an error will be returned defined in api.config.general.flatFileIndexPageNotFoundMessage

You can use the api.staticFile.get(connection, next) in your actions (where next(connection, error, fileStream, mime, length)). Note that fileStream is a stream which can be pipe'd to a client. You can use this in actions if you wish,

On .nix operating system's symlinks for both files and folders will be followed.

Web Clients

  • Cache-Control and Expires or respectively ETag headers (depending on configuration) will be sent with it's caching or revalidation time defined by api.config.servers.web.flatFileCacheDuration
  • Content-Types for files will attempt to be determined using the mime package
  • web clients may request connection.params.file directly within an action which makes use of api.sendFile, or if they are under the api.config.servers.web.urlPathForFiles route, the file will be looked up as if the route matches the directory structure under flatFileDirectory.
  • if your action wants to send content down to a client directly, you will do so like this server.sendFile(connection, null, stream, 'text/html', length);

Non-web Clients

  • the param file should be used to request a path
  • file data is sent raw, and is likely to contain binary content and line breaks. Parse your responses accordingly!

Sending files from Actions

// success case
data.connection.sendFile('/path/to/file.mp3');
data.toRender = false;
next();

// failure case
data.connection.rawConnection.responseHttpCode = 404;
data.connection.sendFile('404.html');
data.toRender = false;
next();
  

You can send files from within actions using connection.sendFile(). Here's an example:

Note that you can optionally modify responseCodes (for HTTP clients only). Be sure to set toRender = false in the callback, as you have already sent data to the client, and probably don't want to do so again on a file request. If you try to sendFile on a path that doesn't exist (within your public directory), the 404 header will be handled automatically for you.

Customizing the File Server

// in an initializer, override api.staticFile.path

api.staticFile.path = function(connection){
  if(connection.action == 'sendFile'){
    return '/tmp/uploads';
  }else{
    return api.config.general.paths.public[0];
  }
}
  

By default, we want ActionHero's file server to be very locked-down, and only serve files from directories defined in api.config.general.paths.public. This is the safest default for beginners. However, you can customize things by changing the behavior of api.staticFile.path().

This would serve files from /public for all requests except the sendFile action, which will serve files from /tmp

Logging

Winston

ActionHero uses the Winston logger. This allows for better, more customizable logging.

Defaults

config.logger = {
  transports: [
    function(api){
      return new (winston.transports.Console)({
        colorize: true,
        level: "debug",
      });
    },
    function(api){
      return new (winston.transports.File)({
        filename: './log/' + api.pids.title + '.log',
        level: "info",
        timestamp: true,
      });
    }
  ]
};
  

In your config/logger.js, you can customize which transports you would like the logger to use. If none are provided, a default logger which only will print to stdout will be used. See winston's documentation for all the logger types, but know that they include console, file, s3, riak, and more.

You can set a transport directly, IE new (winston.transports.Console)() or in a function which will be passed the api object like the examples above. The benefit of using the function invocation is you will have access to other methods and configuration options (like the title of the process).

Levels

api.log('hello'); // will use the default, 'info' level
api.log('debug message', 'debug') // will not show up unless you have configured your logger in this NODE_ENV to be debug
api.log('OH NO', 'emerg') // will show up in all logger levels
api.log('the params were', 'info', data.params) // you can log objects too
  

Note that you can set a level which indicates which level (and those above it) you wish to log per transport. The log levels are:

  • 0=debug
  • 1=info
  • 2=notice
  • 3=warning
  • 4=error
  • 5=crit
  • 6=alert
  • 7=emerg

You can customize these via api.config.logger.levels and api.config.logger.colors. See Winston's documenation for more information

For example, if you set the logger's level to "notice", you would also see "crit" messages, but not "debug" messages.

To invoke the logger from your code, use: api.log(message, severity, metadata)

Methods

api.logger.log and api.logger[severity] also exist which allow you to call and modify the Winston instance directly.

api.log will pass your message to all transports.

api.log(message, severity, metadata)

// the most basic use.  Will assume 'info' as the severity
api.log('hello');

// custom severity
api.log('OH NO!', 'warning');

// custom severity with a metadata object
api.log('OH NO, something went wrong', 'warning', { error: new Error('things are busted') });
  

Plugins

Overview

As of ActionHero version v8.0.0, you can create and include plugins for you ActionHero project. Plugins are collections of tasks, actions, servers, and initializers that are collected as a module. You can install plugins via NPM or keep them in a local path.

Plugins are loaded after all local ActionHero project files, but initializers follow the same priority scheme as all other initializers.

Including Plugins

api.config.general.paths.plugin (loaded from /config/api.js) is an array which contains the search path for your plugins. This will default to ./node_modules, but you can add a local path to your project. Once you have the plugin search paths set up, you use npm run actionhero link -- --name nameOfPlugin (or ./node_modules/.bin/actionhero link --name nameOfPlugin, which is equivalent) to create links in your top-level project to the plugin. This will also copy over any config files from the plugin into your project so you can modify them. The act of "linking" simply creates a myPlugin.link file in each component of your top-level project (actions, tasks, etc) which tells ActionHero to load up files at boot from that plugin.

When you want to overwrite the config files when the plugin is linked, add the parameter overwriteConfig to the link call (e.g. npm run actionhero link --name nameOfPlugin --overwriteConfig)

You can delete the linked files with the "unlink" method using npm run actionhero unlink --name nameOfPlugin. Remember that you have to delete the config files of unlinked plugins manually! If your plugin was installed via NPM, also be sure to remove it from your package.json or uninstall it with npm uninstall --save

Creating Plugins

/
| - actions
| - tasks
| - servers
| - initializers
| - scripts
| - config
|
| - package.json
  

To create a plugin, create a project with the following structure:

This structure will allow elements to be loaded into ActionHero (we search /actions for actions, /tasks for tasks, etc)

When developing your plugin locally, you can load it into an existing ActionHero project to test it out.

First, add the path your plugin is in to api.config.general.paths.plugin. If your ActionHero app is in /var/ah/actionhero and your plugin is in /var/ah/my_plugin, add /var/ah to api.config.general.paths.plugin

Please use the npm naming convention ah-(name)-plugin when uploading your plugin to npm

Plugin methods

When creating plugins, you may find yourself wanting to do things which could normally be accomplished easily with a "top level" ActionHero project, but might be difficult from within the node_modules folder. Here are some helpers:

Routes:

  • api.routes.registerRoute(method, path, action, apiVersion, matchTrailingPathParts)
    • Add a route to the system.

Example Plugin

You can view a sample plugin here

Published Plugins

You can view a list of plugins maintained by @l0oky via Awesome

Servers

Overview

var initialize = function(api, options, next){

  //////////
  // INIT //
  //////////

  var type = "test"
  var attributes = {
    canChat: true,
    logConnections: true,
    logExits: true,
    sendWelcomeMessage: true,
    verbs: [],
  }

  var server = new api.GenericServer(type, options, attributes);

  //////////////////////
  // REQUIRED METHODS //
  //////////////////////

  server.start = function(next){}

  server.stop = function(next){}

  server.sendMessage = function(connection, message, messageCount){}

  server.sendFile = function(connection, error, fileStream, mime, length){};

  server.goodbye = function(connection, reason){};

  ////////////
  // EVENTS //
  ////////////

  server.on("connection", function(connection){});

  server.on('actionComplete', function(data){
    completeResponse(data);
  });

  /////////////
  // HELPERS //
  /////////////

  next(server);
}

/////////////////////////////////////////////////////////////////////
// exports
exports.initialize = initialize;
  

In ActionHero v6.0.0 and later, we have introduced a modular server system which allows you to create your own servers. Servers should be thought of as any type of listener to clients, streams or your file system. In ActionHero, the goal of each server is to ingest a specific type of connection and transform each client into a generic connection object which can be operated on by the rest of ActionHero. To help with this, all servers extend api.GenericServer and fill in the required methods.

To get started, you can use the generateServer action (name is required). This will generate a template server which looks like the above.

Like initializers, the start() and stop() methods will be called when the server is to boot up in ActionHero's lifecycle, but before any clients are permitted into the system. Here is where you should actually initialize your server (IE: https.createServer.listen, etc).

Designing Servers

server.buildConnection({
  rawConnection: {
    req: req,
    res: res,
    method: method,
    cookies: cookies,
    responseHeaders: responseHeaders,
    responseHttpCode: responseHttpCode,
    parsedURL: parsedURL
  },
  id: randomNumber(),
  remoteAddress: remoteIP,
  remotePort: req.connection.remotePort}
); // will emit "connection"

// Note that connections will have a `rawConnection` property.  This is where you should store the actual object(s) returned by your server so that you can use them to communicate back with the client.  Again, an example from the `web` server:

server.sendMessage = function(connection, message){
   cleanHeaders(connection);
   var headers = connection.rawConnection.responseHeaders;
   var responseHttpCode = parseInt(connection.rawConnection.responseHttpCode);
   var stringResponse = String(message)
   connection.rawConnection.res.writeHead(responseHttpCode, headers);
   connection.rawConnection.res.end(stringResponse);
   server.destroyConnection(connection);
 }
  

Your job, as a server designer, is to coerce every client's connection into a connection object. This is done with the sever.buildConnection helper. Here is an example from the web server:

Options and Attributes

A server defines attributes which define it's behavior. Variables like canChat are defined here. options are passed in, and come from api.config.servers[serverName]. These can be new variables (like https?) or they can also overwrite the set attributes.

The required attributes are provided in a generated server.

Verbs

allowedVerbs: [
      "quit",
      "exit",
      "paramAdd",
      "paramDelete",
      "paramView",
      "paramsView",
      "paramsDelete",
      "roomChange",
      "roomView",
      "listenToRoom",
      "silenceRoom",
      "detailsView",
      "say"
    ]
  

When an incoming message is detected, it is the server's job to build connection.params. In the web server, this is accomplished by reading GET, POST, and form data. For websocket clients, that information is expected to be emitted as part of the action's request. For other clients, like socket, ActionHero provides helpers for long-lasting clients to operate on themselves. These are called connection verbs.

Clients use verbs to add params to themselves, update the chat room they are in, and more. The list of verbs currently supported is listed above.

Your server should be smart enough to tell when a client is trying to run an action, request a file, or use a verb. One of the attributes of each server is allowedVerbs, which defines what verbs a client is allowed to preform. A simplified example of how the socket server does this:

var parseRequest = function(connection, line){
   var words = line.split(" ");
   var verb = words.shift();
   if(verb == "file"){
     if (words.length > 0){
       connection.params.file = words[0];
     }
     server.processFile(connection);
   }else{
     connection.verbs(verb, words, function(error, data){
       if(error == null){
         var message = {status: "OK", context: "response", data: data}
         server.sendMessage(connection, message);
       }else if(error === "verb not found or not allowed"){
         connection.error = null;
         connection.response = {};
         server.processAction(connection);
       }else{
         var message = {status: error, context: "response", data: data}
         server.sendMessage(connection, message);
       }
     });
   }
 }
  

Chat

The attribute "canChat" defines if clients of this server can chat. If clients can chat, they should be allowed to use vebs like "roomChange" and "say". They will also be sent messages in their room (and rooms they are listening too) automatically.

Sending Responses

All servers need to implement the server.sendMessage = function(connection, message, messageCount) method so ActionHero knows how to talk to each client. This is likely to make use of connection.rawConnection. If you are writing a server for a persistent connection, it is likely you will need to respond with messageCount so that the client knows which request your response is about (as they are not always going to get the responses in order).

Sending Files

Servers can optionally implement the server.sendFile = function(connection, error, fileStream, mime, length) method. This method is responsible for any connection-specific file transport (headers, chinking, encoding, etc). Note that fileStream is a stream which should be piped to the client.

Localization

Overview

Starting in ActionHero v13.0.0, you can now use the i18n module to customize all aspects of ActionHero.

Locale files

  • When running ActionHero with api.config.i18n.updateFiles = true, you will see ActionHero generate a ‘locales' folder at the top level of your project which will contain translations of all strings in your project with are passed though the new localization system. This includes all uses of api.i18n.localize, connection.localize and api.log.
    • be sure to use sprintf-style string interpolation for variables!
  • From here, it is an easy matter to change the strings, per locale, to how you would like them presented back in your application. The next time you restart the server, the values you've updated in your locale strings file will be used.
  • disable api.config.i18n.updateFiles if you do not want this behavior.

Determining connection locales

Since every ActionHero implementation is unique, we cannot ship with a "guess" about how to determine a given connection's locale. Perhaps you have an HTTP server and you can trust your client's accept-language headers. Or perhaps you run your API under a number of different host names and you can presume locale based on them. Whatever the case, you need to create a synchronous method in an initializer which will be called when each connection connects to return its locale.

For example, I may have an initializer in my project like this:

module.exports = {
  initialize: function(api, next){
    api.customLocalization = {
      lookup: function(connection){
        var locale = 'en';
        if(connection.type === 'web'){
          if(connection.rawConnection.req.headers.host === 'usa.site.com'){ locale = 'en-US'; }
          if(connection.rawConnection.req.headers.host === 'uk.site.com'){  locale = 'en-GB'; }
          if(connection.rawConnection.req.headers.host === 'es.site.com'){  locale = 'es-ES'; }
          if(connection.rawConnection.req.headers.host === 'mx.site.com'){  locale = 'es-MX'; }
        }

        return locale;
      }
    }


    next();
  }
}
  

To tell the i18n to use this method with a new connection, set api.config.i18n.determineConnectionLocale = 'api.customLocalization.lookup'

Connection Methods

  • connection.localize(string) or connection.localize([string-with-interpolation, value])
    • Allows you to interpolate a string based on the connection's current locale. For example, say in an action you wanted to respond with data.response.message = connection.localize('the count was 8', {count: 4}); In your locale files, you would define the count was 8 in every language you cared about, and not need to modify the action itself at all.

Localizing the Logger

  • Just like connections, the ActionHero logger itself now has localization interpolation built in. Note how in the new settings you set which locale you want the server's logs to be expressed in. Using that, you can now use sprintf-style string interpolation as the first argument of api.log()
    • You might want to log the message api.log(['The time is ', {date: new Date()}], 'alert').
    • Changing the translation of the string The time is in your locale files would apply to the logger as well!
    • You can of course continue to log plain strings as we have been with the logger as well.

Localizing other strings

  • To localize strings that are not used in methods mentioned above you can use api.i18n.localize(string, options).
    • Allows you to interpolate a string.
    • Just as the other localize methods above, the input string will be in your locale files for you to change it anytime you want.
    • The second options optional argument (default value is api.i18n) allows you to configure i18n. Note that you will use this argument only in very few special cases, It is recommended to edit the global api.config.i18n settings to suit your localization needs.

Config

Overview

There are 2 ways to manage actionHero configuration: config files and overrides. In both cases, ActionHero starts by reading the config in ./config/. Here is a documented example.

The normal way to deal with configuration changes is to use the files in /config/ and to have special options for each environment. First we load in all settings from the default config block, and then we replace those with anything defined in the relevant environment section. ActionHero uses the standard node environment variable NODE_ENV to determine environment, and defaults to ‘development' when one isn't found. This pattern is very similar the Rails and Sails frameworks. A good way to visualize this is to note that, by default, the web server will return metadata in response JSON, but we change that in the production NODE_ENV and disable it.

exports.default = {
  general: function(api){
    return {
      //...
      developmentMode: true
      //...
    }
  }
}

exports.production = {
  general: function(api){
    return {
      developmentMode: false
    }
  }
}
  

The other way to modify the config is to pass a "changes" hash to the server directly at boot. You can do things like: actionhero.start({configChanges: configChanges}, callback).

The priority order of configs is:

  1. options passed in to boot with actionhero.start({configChanges: configChanges}, callback)
  2. environment-specific values in /config
  3. default values in /config
  4. default values of undefined settings from a plugin
  5. default values of undefined settings from ActionHero's core

When building config files of your own, note that an exports.default is always required, and any environment overrides are optional. What is exported is a hash which eventually resolves a synchronous function which accepts the api variable.

Config Changes

A configChanges example:

var actionhero = require("actionhero").actionhero;

var params = {};
params.configChanges = {
  general: {
    developmentMode: true
  }
}

// start the server!
actionhero.start(params, function(error, api){
  api.log("Boot Successful!");
});
  

Boot Options

When launching ActionHero you can specify which config directory to use with --config=/path/to/dir or the environment variable ACTIONHERO_CONFIG, otherwise /config/ will be used from your working directory.

The priority of arguments is:

  1. Use the project ‘config' folder, if it exists.
  2. actionhero --config=PATH1 --config=PATH2 --config=PATH3,PATH4
  3. ACTIONHERO_CONFIG=PATH1,PATH2 npm start

Note that if --config or ACTIONHERO_CONFIG are used, they overwrite the use of the default /config folder. If you wish to use both, you need to re-specify "config", e.g. --config=config,local-config. Also, note that specifying multiple --config options on the command line does exactly the same thing as using one parameter with comma separators, however the environment variable method only supports the comma-delimited syntax.

Utilities

ActionHero ships with a few utility methods exposed for your convince:

api.utils.hashMerge(a, b)

  • create a new hash which looks like b merged into a
  • {a:1, b:2} merged with {b:3, c:4} looks like {a: 1, b:3, c:4}

api.utils.isPlainObject(object)

  • determines if object is a plain js ‘Object' or something more complex, like a stream

api.utils.arrayUniqueify(arr)

  • removes duplicate entries from an array

api.utils.objClone(obj)

  • creates a new object with the same keys and values of the original object

api.utils.getExternalIPAddress()

  • attempts to determine this server's external IP address out of all plausible addressees this host is listening on

api.utils.parseCookies(req)

  • a helper to parse the request object's headers and returns a hash of the client's cookies

api.utils.parseIPv6URI(address)

  • will return {host: host, port: port} for an IPv6 address

The API Object

  var api = {
    // STATE VARIABLES //
    running: true,
    initialized: true,
    shuttingDown: false,

    // METADATA //
    bootTime: 1421016104943,
    env: 'development',
    id: '10.0.1.5',
    actionheroVersion: '10.0.0',
    projectRoot: '/app/actionhero',
    _startingParams: { configChanges: { general: [] } },

    // DEVELOPER MODE //
    watchedFiles: [],
    watchFileAndAct: [Function],
    unWatchAllFiles: [Function],
    loadConfigDirectory: [Function],

    // SERVER COMMAND AND CONTROL //
    commands:{
      initialize: [Function],
      start:      [Function],
      stop:       [Function],
      restart:    [Function]
    },

    // COMAND AND CONTROL //
    _self:{
      initializers: {},
      startingParams: { configChanges: [] },

       // arrays containing init/stop/start methods
       configInitializers: [],
       loadInitializers:   [],
       startInitializers:  [],
       stopInitializers:   []
     },

    // INITIALZER DEFAULTS //
    initializerDefaults:{
      load:  1000,
      start: 1000,
      stop:  1000
    },

    // UTILS //
    utils:{
      hashMerge:              [Function],
      isPlainObject:          [Function],
      arrayUniqueify:         [Function],
      objClone:               [Function],
      collapseObjectToArray:  [Function],
      getExternalIPAddress:   [Function],
      parseCookies:           [Function],
      parseIPv6URI:           [Function]
    },

    // CONFIG //
    config:
     { general:
        { apiVersion: '0.0.1',
          serverName: 'actionhero API',
          serverToken: 'change-me',
          welcomeMessage: 'Hello! Welcome to the actionhero api',
          cachePrefix: 'actionhero:cache:',
          lockPrefix: 'actionhero:lock:',
          lockDuration: 10000,
          developmentMode: false,
          simultaneousActions: 5,
          disableParamScrubbing: false,
          filteredParams: [],
          missingParamChecks: [Object],
          directoryFileType: 'index.html',
          defaultMiddlewarePriority: 100,
          paths: [Object],
          startingChatRooms: [Object],
          plugins: [] },
       errors:
        { _toExpand: false,
          missingParams: [Function],
          unknownAction: [Function],
          unsupportedServerType: [Function],
          serverShuttingDown: [Function],
          tooManyPendingActions: [Function],
          fileNotFound: [Function],
          fileNotProvided: [Function],
          fileReadError: [Function],
          verbNotFound: [Function],
          verbNotAllowed: [Function],
          connectionRoomAndMessage: [Function],
          connectionNotInRoom: [Function],
          connectionAlreadyInRoom: [Function],
          connectionRoomHasBeenDeleted: [Function],
          connectionRoomNotExist: [Function],
          connectionRoomExists: [Function],
          connectionRoomRequired: [Function] },
       logger: { transports: [Object] },
       redis:
        { channel: 'actionhero',
          rpcTimeout: 5000,
          pkg: 'fakeredis' },
       routes: {},
       servers:
        { socket: [Object],
          web: [Object],
          websocket: [Object] },
       stats: { writeFrequency: 1000, keys: [Object] },
       tasks:
        { scheduler: false,
          queues: [],
          timeout: 5000,
          minTaskProcessors: 0,
          maxTaskProcessors: 0,
          checkTimeout: 500,
          maxEventLoopDelay: 5,
          toDisconnectProcessors: true,
          redis: [Object] } },

    // PIDS //
    pids:
     { pid: 26168,
       path: '/app/actionhero/pids',
       sanitizeId: [Function],
       title: 'actionhero-10.0.1.5',
       writePidFile: [Function],
       clearPidFile: [Function] },

    // LOGGER //
    logger: {},
    log: [Function],

    // EXCEPTION HANDLERS //
    exceptionHandlers:{
      reporters: [ [Function] ],
      report: [Function],
      loader: [Function],
      action: [Function],
      task: [Function]
    },

    // REDIS //
    redis:{
      clusterCallbaks: {},
      clusterCallbakTimeouts: {},
      subscriptionHandlers:{
        do: [Function],
        doResponse: [Function],
        chat: [Function]
      },
      status:{
        client: true,
        subscriber: true,
        subscribed: true,
        calledback: true
      },
      initialize: [Function],
      subscribe: [Function],
      publish: [Function],
      doCluster: [Function],
      respondCluster: [Function],
      client: { },
      subscriber: { },

    // CACHE //
    cache:{
      redisPrefix: 'actionhero:cache:',
      lockPrefix: 'actionhero:lock:',
      lockDuration: 10000,
      lockName: '10.0.1.5',
      lockRetry: 100,
      keys: [Function],
      locks: [Function],
      size: [Function],
      clear: [Function],
      dumpWrite: [Function],
      dumpRead: [Function],
      saveDumpedElement: [Function],
      load: [Function],
      destroy: [Function],
      save: [Function],
      lock: [Function],
      unlock: [Function],
      checkLock: [Function]
    },

    // STATS //
    stats:{
      // timer: null,
      // pendingIncrements: {},
      increment: [Function],
      // writeIncrements: [Function],
      get: [Function],
      getAll: [Function]
    },

    // CONNECTIONS //
    connections:{
      createCallbacks: {},
      destroyCallbacks: {},
      allowedVerbs:[
        'quit',
        'exit',
        'documentation',
        'paramAdd',
        'paramDelete',
        'paramView',
        'paramsView',
        'paramsDelete',
        'roomAdd',
        'roomLeave',
        'roomView',
        'detailsView',
        'say'
        ],
       connections: {},
       // apply: [Function],
       // applyCatch: [Function],
       addCreateCallback: [Function],
       addDestroyCallback: [Function]
    },
    connection: [Function], // prototype

    // ACTIONS //
    actions:{
      actions: {},
      preProcessors: {},
      postProcessors: {},
      addPreProcessor: [Function],
      addPostProcessor: [Function],
      // validateAction: [Function],
      // loadFile: [Function]
    },
    ActionProcessor: [Function], // prototype

    // PARAMS //
    params:{
      globalSafeParams: [
        'file',
        'apiVersion',
        'callback',
        'action'
      ],
      // buildPostVariables: [Function],
      postVariables: []
     },

    // SERVERS //
    GenericServer: {}, // prototype
    servers: {
      servers: []
    },

    // ROUTES //
    routes: {
      routes: {
        get: [Object],
        post: [Object],
        put: [Object],
        patch: [Object],
        delete: [Object]
      },
       // verbs: [],
       // processRoute: [Function],
       // matchURL: [Function],
       // loadRoutes: [Function],
     },

    // STATIC FILES //
    staticFile:{
      // path: [Function],
      // get: [Function],
      sendFile: [Function],
      // sendFileNotFound: [Function],
      // checkExistence: [Function],
      // logRequest: [Function]
    },

    // CHAT //
    chatRoom:{
      keys: {
        rooms: 'actionhero:chatRoom:rooms',
        members: 'actionhero:chatRoom:members:'
      },
      messageChannel: '/actionhero/chat/chat',
      joinCallbacks: {},
      leaveCallbacks: {},
      sayCallbacks: {},
      addJoinCallback: [Function],
      addLeaveCallback: [Function],
      addSayCallback: [Function],
      broadcast: [Function],
      generateMessagePayload: [Function],
      incomingMessage: [Function],
      add: [Function],
      destroy: [Function],
      exists: [Function],
      sanitizeMemberDetails: [Function],
      roomStatus: [Function],
      generateMemberDetails: [Function],
      addMember: [Function],
      removeMember: [Function],
      // handleCallbacks: [Function],
    },

    // RESQUE //
    resque: {
      queue: {},
      multiWorker: {},
      scheduler: null,
      // connectionDetails: {},
      // startQueue: [Function],
      // startScheduler: [Function],
      // stopScheduler: [Function],
      // startMultiWorker: [Function],
      // stopMultiWorker: [Function],
    },

    // TASKS //
    tasks:{
      tasks: {},
      jobs:  {},
      // loadFile: [Function],
      // jobWrapper: [Function],
      // validateTask: [Function],
      enqueue: [Function],
      enqueueAt: [Function],
      enqueueIn: [Function],
      del: [Function],
      delDelayed: [Function],
      scheduledAt: [Function],
      timestamps: [Function],
      delayedAt: [Function],
      allDelayed: [Function],
      // enqueueRecurrentJob: [Function],
      // enqueueAllRecurrentJobs: [Function],
      stopRecurrentJob: [Function],
      details: [Function] },

    // DOCUMENTATION //
    documentation: {}

  };
  

By now you will have noticed that most sections of ActionHero are initialized with access to the api object. The api object is the top-level container/namespace for all of ActionHero's data and methods. We use the api object to avoid polluting any global namespaces. The api object is available to all parts of ActionHero to share data and state. Feel free to modify or add too the api object as you see fit, but be mindful of the data it already contains.

Collections that you are recommended to leave unmodified are un-expanded [Object]s and/or commented out.