@xarc/fastify-server
Version:
A configurable Fastify web server
376 lines • 15.3 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.fastify = void 0;
exports.electrodeServer = electrodeServer;
const tslib_1 = require("tslib");
/* eslint-disable no-magic-numbers, prefer-template, max-len, @typescript-eslint/no-var-requires */
const assert_1 = tslib_1.__importDefault(require("assert"));
const fastify_1 = tslib_1.__importDefault(require("fastify"));
const lodash_1 = tslib_1.__importDefault(require("lodash"));
const path_1 = tslib_1.__importDefault(require("path"));
const check_node_env_1 = require("./check-node-env");
const start_failed_1 = require("./start-failed");
const electrode_confippet_1 = require("electrode-confippet");
const AsyncEventEmitter = require("async-eventemitter");
const requireAt = require("require-at");
const xaa = require("xaa");
const util = require("util");
const { fastifyPluginDecorate } = require("./fastify-plugin-decorate");
const DEFAULT_KEEPALIVE_TIMEOUT = 60000;
async function emitEvent(context, event) {
const timeout = lodash_1.default.get(context, "config.electrode.eventTimeout", 10000);
if (!context.emitter._events[event]) {
return;
}
let promise = new Promise((resolve, reject) => {
context.emitter.emit(event, context, err => {
// @ts-expect-error ts-migrate(2794) FIXME: Expected 1 arguments, but got 0. Did you forget to... Remove this comment to see the full error message
return err ? reject(err) : resolve();
});
});
if (Number.isInteger(timeout) && timeout > 0) {
promise = xaa.runTimeout(promise, timeout);
}
try {
await promise;
}
catch (error) {
const massageError = err => {
err.timeout = timeout;
err.event = event;
if (err instanceof xaa.TimeoutError) {
err.message = `timeout waiting for event '${event}' handler`;
err.code = "XEVENT_TIMEOUT";
}
else {
err.message = `event '${event}' handler failed: ${err.message}`;
err.code = "XEVENT_FAILED";
}
return err;
};
throw massageError(error);
}
}
async function convertPluginsToArray(plugins) {
//
// The module could either be one in node_modules or a file in a path
// relative to CWD
// * module in node_modules: no leading "."
// * file in a directory, relative path with leading "." under CWD, resolve
// full path for require
//
const fullRequirePath = x => {
return x.startsWith(".") ? path_1.default.resolve(x) : x;
};
const topRequireFromPath = plugins.requireFromPath;
(0, assert_1.default)(!topRequireFromPath || lodash_1.default.isString(topRequireFromPath), `config.plugins.requireFromPath must be a string`);
const loadModule = async (p) => {
if (p.register) {
return fastifyPluginDecorate(p);
}
// if p.register is not defined then check p.module
// if no p.module or p.module !== false, then use field name (p.__name)
const getPluginModule = () => {
const requireFromPath = p.requireFromPath;
if (lodash_1.default.isString(p.module)) {
return { name: p.module, requireFromPath };
}
else if (lodash_1.default.isObject(p.module)) {
(0, assert_1.default)(p.module.name, `plugin ${p.__name} 'module' must have 'name' field`);
(0, assert_1.default)(!p.module.requireFromPath || lodash_1.default.isString(p.module.requireFromPath), `plugin ${p.__name} 'module.requireFromPath' must be a string`);
return Object.assign({ requireFromPath }, p.module);
}
else if (p.module !== false) {
return { name: p.__name, requireFromPath };
}
throw new Error(`plugin ${p.__name} disable 'module' but has no 'register' field`);
};
const doRequire = async () => {
const pluginMod = getPluginModule();
let name;
let mod = null;
const requireMod = () => {
//
// if has a name for the module to load, then try to load it.
//
let fromPath = pluginMod.requireFromPath || topRequireFromPath;
let xrequire;
if (fromPath) {
name = pluginMod.name;
// use require-at to load the module from path
xrequire = requireAt(fromPath);
p.requireFromPath = fromPath;
fromPath = ` from path: ${fromPath}`;
}
else {
// use require to load the module
xrequire = require;
name = fullRequirePath(pluginMod.name);
fromPath = p.requireFromPath = "";
}
try {
mod = xrequire(name);
}
catch (error) {
error.message = `Failed loading module ${pluginMod.name}${fromPath}: ${error.message}`;
throw error;
}
};
requireMod();
// order of fields to look for the hapi plugin from the module:
// 1. mod.fastifyPlugin
// 3. mod.default.fastifyPlugin (ES6)
// 2. mod.plugin
// 4. mod.default (ES6)
// 5. mod
const pluginField = ["fastifyPlugin", "default.fastifyPlugin", "plugin", "default"].find(x => lodash_1.default.get(mod, x));
// validate plugin
const pluginExport = (pluginField && lodash_1.default.get(mod, pluginField)) || mod;
p.register = pluginExport.register || pluginExport;
const msg = `for plugin '${p.__name}' from exported field '${pluginField}' of its module from '${name}'`;
const validatePlugin = pos => {
(0, assert_1.default)(pos.register, `register of plugin is falsy value: ${pos.register} - ${msg}`);
(0, assert_1.default)(typeof pos.register === "function", `register of plugin is not a function - ${msg}`);
};
validatePlugin(p);
return fastifyPluginDecorate(p);
};
return doRequire();
};
const num = x => {
return lodash_1.default.isString(x) ? parseInt(x, 10) : x;
};
const checkNaN = x => {
return isNaN(x) ? Infinity : x;
};
const priority = p => checkNaN(num(p.priority));
const isEnable = p => p.__name !== "requireFromPath" && p.enable !== false;
const transpose = (p, k) => Object.assign({ __name: k }, p);
//
// transpose each plugin, filter out disabled ones, and sort by priority
//
const pluginsArray = () => (0, lodash_1.default)(plugins).map(transpose).filter(isEnable).sortBy(priority).value();
// convert plugins object to array and check each one if it has a module to load.
const arr = pluginsArray();
return xaa.map(arr, loadModule);
}
async function startElectrodeServer(context) {
const server = context.server;
const config = context.config;
let started = false;
const registerPlugins = async (plugins) => {
const errorRegisterMessage = plg => {
if (plg.module) {
const fromPath = plg.requireFromPath ? ` from path: '${plg.requireFromPath}'` : "";
return `with module '${JSON.stringify(plg.module)}'${fromPath}`;
}
else {
return `with register function`;
}
};
const regFail = (err, plugin) => {
let err2 = err;
if (!err || !err.hasOwnProperty("message")) {
err2 = new Error(err);
}
else if (err.code === "ERR_AVVIO_PLUGIN_TIMEOUT") {
err2.message = `plugin '${plugin.__name}' with register function timeout \
- did you return a resolved promise?`;
}
err2.code = "XPLUGIN_FAILED";
err2.plugin = plugin;
err2.method = errorRegisterMessage(plugin);
return err2;
};
//
// - all register must be called before calling server.ready
// - but registration only executed after server.ready is called
// - So 1st call must setup all register calls and promises
// - 2nd call calls after(), but actual execution wait for server.ready
// - 3rd call wait for all registration to complete
//
const regPromises = plugins.map(async (plugin) => {
try {
const x = server.register(plugin.register, plugin.options);
return await util.promisify(x.after)();
}
catch (err) {
throw regFail(err, plugin);
}
});
await Promise.all(regPromises);
return emitEvent(context, "plugins-registered");
};
const handleFail = async (err) => {
if (started) {
await server.close();
}
return await (0, start_failed_1.startFailed)(err);
};
const startServer = async () => {
try {
// must call ready to kick off the plugin registration
await context.server.ready();
await context.registerPluginsPromise;
await server.listen({
port: config.connection.port,
host: config.connection.address
});
started = true;
await emitEvent(context, "server-started");
await emitEvent(context, "complete");
}
catch (err) {
await handleFail(err);
}
};
const setupServer = async () => {
context.server.decorate("start", startServer);
await emitEvent(context, "server-created");
const plugins = await convertPluginsToArray(config.plugins);
context.plugins = plugins;
await emitEvent(context, "plugins-sorted");
context.registerPluginsPromise = registerPlugins(context.plugins);
};
try {
await setupServer();
}
catch (err) {
return await handleFail(err);
}
if (!context.config.deferStart) {
await startServer();
}
return server;
}
/**
* A configuration base helper to create and start a HTTP server using fastify
*
* @param appConfig - configuration
* @param decors - extra decorators to add to the server
*
* @returns electrode fastify instance
*/
async function electrodeServer(appConfig = {}, decors) {
const check = () => {
(0, check_node_env_1.checkNodeEnv)();
if (lodash_1.default.isArray(decors)) {
decors = decors.filter(lodash_1.default.identity).map(x => (lodash_1.default.isFunction(x) ? x() : x));
}
else {
decors = [].concat(decors).filter(lodash_1.default.identity);
}
};
check();
const makeFastifyServerConfig = context => {
const fastifyServerConfig = {
app: {
electrodeServer: true
},
keepAliveTimeout: lodash_1.default.get(context.config, "keepAliveTimeout", lodash_1.default.get(context.config, "electrode.keepAliveTimeout", DEFAULT_KEEPALIVE_TIMEOUT))
};
electrode_confippet_1.util.merge(fastifyServerConfig, context.config.server);
lodash_1.default.assign(fastifyServerConfig, context.config.connection);
//
// This will allow Fastify to make config available through
// server.settings.app.config
//
fastifyServerConfig.app.config = context.config;
return fastifyServerConfig;
};
const start = async (context) => {
const settings = makeFastifyServerConfig(context);
const server = (context.server = (0, fastify_1.default)(settings));
const SYM_PATH = Symbol("request.path");
const SYM_INFO = Symbol("request.info");
// add request.path and request.info as compatibility with Hapi
server.decorateRequest("path", {
getter() {
return this[SYM_PATH] || (this[SYM_PATH] = this.raw.url.match("^[^?]*")[0]);
}
});
// request.info
server.decorateRequest("info", {
getter() {
return this[SYM_INFO] || (this[SYM_INFO] = { remoteAddress: this.ip });
}
});
const SERVER_SYM_INFO = Symbol("server.info");
// server.info micmic Hapi
/*
const sample = {
created: 1593114093676,
started: 1593114093685,
host: "m-c02yw1s0lvdt",
port: 3000,
protocol: "http",
id: "m-c02yw1s0lvdt:53774:kbv6zxws",
uri: "http://m-c02yw1s0lvdt:3000",
address: "0.0.0.0"
};
*/
server.decorate("info", {
getter() {
return (this[SERVER_SYM_INFO] ||
(this[SERVER_SYM_INFO] = {
get port() {
const address = server.server.address();
return address && address.port;
},
get address() {
const address = server.server.address();
return address && address.address;
}
}));
}
});
server.decorate("settings", settings);
// server.app
server.decorate("app", { config: context.config });
const SYM_APP = Symbol("request.app");
// request.app, should be different for each request
server.decorateRequest("app", {
getter() {
return this[SYM_APP] || (this[SYM_APP] = { config: context.config });
}
});
// Start server
return startElectrodeServer(context);
};
const applyDecorConfigs = context => {
// load internal defaults
const defaults = (0, electrode_confippet_1.loadConfig)({
dir: path_1.default.join(__dirname, "config"),
warnMissing: false,
failMissing: false,
context: {
deployment: process.env.NODE_ENV
},
cache: false
});
// apply decors
decors.forEach(d => defaults._$.use(d));
// apply appConfig
defaults._$.use(appConfig);
context.config = defaults;
delete defaults.listener;
return context;
};
const setListeners = context => {
context.emitter = new AsyncEventEmitter();
decors.forEach(d => {
return d.listener && d.listener(context.emitter);
});
if (appConfig.listener) {
appConfig.listener(context.emitter);
}
return context;
};
let ctx = setListeners({});
ctx = applyDecorConfigs(ctx);
await emitEvent(ctx, "config-composed");
return await start(ctx);
}
var fastify_2 = require("fastify");
Object.defineProperty(exports, "fastify", { enumerable: true, get: function () { return tslib_1.__importDefault(fastify_2).default; } });
//# sourceMappingURL=electrode-server.js.map
;