actionhero
Version:
The reusable, scalable, and quick node.js API server for stateless and stateful applications
252 lines (221 loc) • 7.52 kB
text/typescript
import { EventEmitter } from "events";
import { api, config } from "../index";
import { log, ActionheroLogLevel } from "../modules/log";
import { ActionProcessor } from "./actionProcessor";
import { Connection } from "./connection";
interface ServerConfig {
[key: string]: any;
}
/**
* Create a new Actionhero Server. The required properties of an server. These can be defined statically (this.name) or as methods which return a value.
*/
export abstract class Server extends EventEmitter {
/**The name & type of the server. */
type: string;
/**What connection verbs can connections of this type use? */
verbs?: Array<string>;
/**Shorthand for `api.config[this.type]` */
config?: ServerConfig;
options?: {
[key: string]: any;
};
/** attributes of the server */
attributes: {
[key: string]: any;
};
/**Can connections of this server use the chat system? */
canChat: boolean;
/**Should we log every new connection? */
logConnections: boolean;
/**Should we log when a connection disconnects/exits? */
logExits: boolean;
/**Should every new connection of this server type receive the welcome message */
sendWelcomeMessage: boolean;
/**Methods described by the server to apply to each connection (like connection.setHeader for web connections) */
connectionCustomMethods: {
[key: string]: Function;
};
/**A place to store the actually server object you create */
server?: any;
constructor() {
super();
this.options = {};
this.attributes = {};
this.config = {}; // will be applied by the initializer
this.connectionCustomMethods = {};
this.canChat = this.canChat ?? true;
this.logExits = this.logExits ?? true;
this.sendWelcomeMessage = this.sendWelcomeMessage ?? true;
this.logConnections = this.logConnections ?? true;
this.verbs = this.verbs ?? [];
}
/**
* Event called when a formal new connection is created for this server type. This is a response to calling Actionhero.Server#buildConnection
*
* @event Actionhero.Server#connection
*/
/**
* Event called when a an action is complete for a connection created by this server. You may want to send a response to the client as a response to this event.
*
* @event Actionhero.Server#actionComplete
* @property {object} data - The same data from the Action. Includes the connection, response, etc.
*/
/**
* Method run as part of the `initialize` lifecycle of your server. Usually configures the server.
*/
abstract initialize(): Promise<void>;
/**
* Method run as part of the `start` lifecycle of your server. Usually boots the server (listens on port, etc).
*/
abstract start(): Promise<void>;
/**
* Method run as part of the `stop` lifecycle of your server. Usually configures the server (disconnects from port, etc).
*/
abstract stop(): Promise<void>;
/**
* Must be defined explaining how to send a message to an individual connection.
*/
abstract sendMessage(
connection: Connection,
message: string | object | Array<any>,
messageId?: string,
): Promise<void>;
/**
* Must be defined explaining how to send a file to an individual connection. Might be a noop for some connection types.
*/
abstract sendFile(
connection: Connection,
error: NodeJS.ErrnoException,
fileStream: any,
mime: string,
length: number,
lastModified: Date,
): Promise<void>;
/**An optional message to send to clients when they disconnect */
async goodbye?(connection: Connection): Promise<void>;
validate() {
if (!this.type) {
throw new Error("type is required for this server");
}
(
[
"start",
"stop",
"sendFile", // connection, error, fileStream, mime, length, lastModified
"sendMessage", // connection, message
"goodbye",
] as const
).forEach((method) => {
if (!this[method] || typeof this[method] !== "function") {
throw new Error(
`${method} is a required method for the server \`${this.type}\``,
);
}
});
}
/**
* * Build a the Actionhero.Connection from the raw parts provided by the server.
* ```js
*this.buildConnection({
* rawConnection: {
* req: req,
* res: res,
* params: {},
* method: method,
* cookies: cookies,
* responseHeaders: responseHeaders,
* responseHttpCode: responseHttpCode,
* parsedURL: parsedURL
* },
* id: fingerprint + '-' + uuid.v4(),
* fingerprint: fingerprint,
* remoteAddress: remoteIP,
* remotePort: remotePort
*})
* ```
*/
async buildConnection(data: { [key: string]: any }) {
const details = {
type: this.type,
id: data.id,
remotePort: data.remotePort,
remoteIP: data.remoteAddress,
rawConnection: data.rawConnection,
messageId: data.messageId,
canChat: this.attributes.canChat ?? null,
fingerprint: data.fingerprint ?? null,
};
const connection = await Connection.createAsync(details);
connection.sendMessage = async (message) => {
this.sendMessage(connection, message);
};
connection.sendFile = async (path) => {
connection.params.file = path;
this.processFile(connection);
};
this.emit("connection", connection);
if (this.attributes.logConnections === true) {
this.log("new connection", "info", { to: connection.remoteIP });
}
if (this.attributes.sendWelcomeMessage === true) {
connection.sendMessage({
welcome: config.general.welcomeMessage,
context: "api",
});
}
if (typeof this.attributes.sendWelcomeMessage === "number") {
setTimeout(() => {
try {
connection.sendMessage({
welcome: config.general.welcomeMessage,
context: "api",
});
} catch (e) {
this.log(e, "error");
}
}, this.attributes.sendWelcomeMessage);
}
}
/**
* When a connection has called an Action command, and all properties are set. Connection should have `params.action` set at least.
* on(event: 'actionComplete', cb: (data: object) => void): this;
*/
async processAction(connection: Connection) {
const actionProcessor = new ActionProcessor(connection);
const data = await actionProcessor.processAction();
this.emit("actionComplete", data);
}
/**
* When a connection has called an File command, and all properties are set. Connection should have `params.file` set at least. Will eventually call Actionhero.Server#sendFile.
*/
async processFile(connection: Connection) {
const results = await api.staticFile.get(connection);
this.sendFile(
results.connection,
results.error,
results.fileStream,
results.mime,
results.length,
results.lastModified,
);
}
/**
* Enumerate the connections for this server type on this server.
*/
connections(): Array<Connection> {
const connections = [];
for (const i in api.connections.connections) {
const connection = api.connections.connections[i];
if (connection.type === this.type) {
connections.push(connection);
}
}
return connections;
}
/**
* Log a message from this server type. A wrapper around log() with a server prefix.
*/
log(message: string, severity?: ActionheroLogLevel, data?: any) {
log(`[server: ${this.type}] ${message}`, severity, data);
}
}