UNPKG

actionhero

Version:

The reusable, scalable, and quick node.js API server for stateless and stateful applications

271 lines (239 loc) 7.87 kB
import { EventEmitter } from "events"; import { Connection } from "./connection"; import { ActionProcessor } from "./actionProcessor"; import { api } from "../index"; import { log } from "../modules/log"; 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.servers[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 (defined in locales, `actionhero.welcomeMessage`) */ 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 = {}; const defaultAttributes = this.defaultAttributes(); for (const key in defaultAttributes) { if (!this.attributes[key]) { this.attributes[key] = defaultAttributes[key]; } if (typeof this.attributes[key] === "function") { this.attributes[key] = this[key](); } } } /** * 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: Error, 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>; defaultAttributes() { return { type: null, canChat: true, logConnections: true, logExits: true, sendWelcomeMessage: true, verbs: [], }; } 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", ].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: any) { const details = { type: this.type, id: data.id, remotePort: data.remotePort, remoteIP: data.remoteAddress, rawConnection: data.rawConnection, messageId: data.messageId, canChat: null, fingerprint: null, }; if (this.attributes.canChat === true) { details.canChat = true; } if (data.fingerprint) { details.fingerprint = data.fingerprint; } 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: connection.localize("actionhero.welcomeMessage"), context: "api", }); } if (typeof this.attributes.sendWelcomeMessage === "number") { setTimeout(() => { try { connection.sendMessage({ welcome: connection.localize("actionhero.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?: string, data?: any) { log(`[server: ${this.type}] ${message}`, severity, data); } }