In Actionhero 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 remote connections, streams, or event your server.
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 Actionhero.Server
and fill in the required methods.
To get started, you can use the actionhero generate-server --name=myServer
. This will generate a template server which looks like the below.
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).
import { Server } from "actionhero";
module.exports = class MyServer extends Server {
constructor() {
super();
this.type = "my-server";
this.attributes = {
canChat: false,
logConnections: true,
logExits: true,
sendWelcomeMessage: false,
verbs: [],
};
// this.config will be set to equal config[this.type]
}
async initialize() {
this.on("connection", (connection) => {});
this.on("actionComplete", (data) => {});
}
async start() {
// this.buildConnection (data)
// this.processAction (connection)
// this.processFile (connection)
}
async stop() {}
async sendMessage(connection, message, messageId) {}
async sendFile(connection, error, fileStream, mime, length, lastModified) {}
async goodbye(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:
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 = (connection, message) => {
cleanHeaders(connection);
const headers = connection.rawConnection.responseHeaders;
const responseHttpCode = parseInt(connection.rawConnection.responseHttpCode);
const stringResponse = String(message);
connection.rawConnection.res.writeHead(responseHttpCode, headers);
connection.rawConnection.res.end(stringResponse);
server.destroyConnection(connection);
};
A server defines attributes
which define it's behavior. Variables like canChat
are defined here. options
are passed in, and come from config[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.
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:
async parseRequest (connection, line) {
let words = line.split(' ')
let verb = words.shift()
if (verb === 'file') {
if (words.length > 0) { connection.params.file = words[0] }
return this.processFile(connection)
}
if (this.attributes.verbs.indexOf(verb) >= 0) {
try {
let data = await connection.verbs(verb, words)
return this.sendMessage(connection, {status: 'OK', context: 'response', data: data})
} catch (error) {
return this.sendMessage(connection, {error: error, context: 'response'})
}
}
try {
let requestHash = JSON.parse(line)
if (requestHash.params !== undefined) {
connection.params = {}
for (let v in requestHash.params) {
connection.params[v] = requestHash.params[v]
}
}
if (requestHash.action) {
connection.params.action = requestHash.action
}
} catch (e) {
connection.params.action = verb
}
connection.error = null
connection.response = {}
return this.processAction(connection)
}
The attribute
"canChat" defines if clients of this server can chat. If clients can chat, they should be allowed to use verbs like "roomChange" and "say". They will also be sent messages in their room (and rooms they are listening too) automatically.
All servers need to implement the server.sendMessage = function(connection, message, messageId)
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 messageId
so that the client knows which request your response is about (as they are not always going to get the responses in order).
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 pipe
'd to the client.
//Initializer
module.exports = {
startPriority: 1000,
start: function (api, next) {
let webServer = api.servers.servers.web
webServer.connectionCustomMethods = webServer.connectionCustomMethods || {}
webServer.connectionCustomMethods.requestHeaders = function (connection) {
return connection.rawConnection.req.headers
}
}
}
//Action
module.exports = {
name: 'logHeaders',
description 'Log Web Request Headers',
run: function (api, data, next) {
let headers = data.connection.requestHeaders()
api.log('Headers:', 'debug', headers)
next()
}
}
The connection
object passed to a server can be customized on a per server basis through the use of the server.connectionCustomMethods
hash. The hash can be populated with functions whose signature must match function (connection, ...)
. Once populated, these functions are curried to always pass connection
as the first argument and applied to the data.connection
object passed to Actions, and can be accessed via data.connection.functionName(...)
within the action or middleware.
In this way, you can create custom methods on your connections.
The Actionhero server is open source, under the Apache-2 license
Actionhero runs on Linux, OS X, and Windows
You always have access to the Actionhero team via Slack and Github
We provide support for corporate & nonprofit customers starting at a flat rate of $200/hr. Our services include:
We have packages appropriate for all company sizes. Contact us to learn more.
We provide support for corporate & nonprofit customers starting at a flat rate of $200/hr. Our services include:
We have packages appropriate for all company sizes. Contact us to learn more.
For larger customers in need of a support contract, we offer an enterprise plan including everything in the Premium plan plus: