UNPKG

actionhero

Version:

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

252 lines (221 loc) 7.52 kB
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); } }