UNPKG

froge

Version:

Jump-start your NodeJS/Bun/... services with dependency & lifecycle management and handy helper methods

289 lines 10.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.envs = void 0; exports.default = froge; const env_1 = __importDefault(require("./env")); exports.envs = env_1.default; const plug_1 = require("./plug"); const AsyncFunction = async function () { }.constructor; const defaultConfig = { parallelStartGroups: true, parallelStopGroups: true, forceExitAfterShutdown: false, verbose: true, }; ; class FrogeServer { map = new Map(); groups = new Set(); config = { ...defaultConfig }; plugins = new Map; currentLevel = -1; symbol = Symbol(); configure(config) { this.config = { ...this.config, ...config }; return this; } services = new Proxy({}, { get: (_, prop) => { const service = this.map.get(prop); if (typeof service === 'undefined') { throw new Error(`Can't access service "${prop}", make sure it exists and started`); } if (service.plug) { return service.plug; } if (!service.up) { throw new Error(`Can't access service "${prop}" before it was started`); } return service.value; }, }); up(services, group) { if (group && this.groups.has(group)) { throw new Error(`Group with key ${group} already exists, trying to add new group with the same name (${Object.keys(services).join(', ')})`); } const level = ++this.currentLevel; for (const key in services) { const existing = this.map.get(key); let maybePlug; if (existing) { const errorMsg = `Trying to override existing service ${key} from group ${existing.group ?? 'undefined'}.` + '\nOnly plugs can be overridden. Function defining a plug must not be async and must only use ctx.plug() method.' + '\nExample: ctx => ctx.plug<MyService>()'; if (existing.init instanceof AsyncFunction) { throw new Error(errorMsg + `\nInit function for ${key} is async`); } const plugContextMock = new Proxy({}, { get(_, prop) { if (prop === 'plug') { return plug_1.plug; } throw new Error(errorMsg + `\nInit function for ${key} tried accessing ctx.${prop}`); } }); try { maybePlug = existing.init(plugContextMock); } catch (e) { throw new Error(errorMsg + `\nInit function for ${key} raised an error: ${e}`); } if (!maybePlug?.isFrogePlug) { throw new Error(errorMsg + `\nInit function for ${key} didn't return a Froge plug`); } // Ok, we are satisfied, it's definitely a plug. Deleted to ensure correct startup order. this.map.delete(key); } this.map.set(key, { level, group, up: false, init: services[key], plug: maybePlug, serverSymbol: this.symbol }); } group && this.groups.add(group); return this; } down(destroyers) { for (const key in destroyers) { const service = this.map.get(key); if (typeof service === 'undefined') { throw new Error(`Trying to add destroyer to unknown service ${key}`); } service.destroy = destroyers[key]; } return this; } use(other, /** Change plugin config to match main instance */ pushConfig = true) { const container = { factory: other instanceof FrogeServer ? () => other : other, pushConfig, }; const after = this.currentLevel; if (this.plugins.has(after)) { this.plugins.get(after)?.push(container); } else { this.plugins.set(after, [container]); } return this; } async startPlugins(level) { const groupPlugins = this.plugins.get(level); if (!groupPlugins) { return; } for (const plugin of groupPlugins) { if (plugin.server) { continue; } plugin.server = await plugin.factory({ services: this.services, envs: env_1.default, }); if (plugin.pushConfig) { plugin.server.configure(this.config); } for (const key of plugin.server.map.keys()) { if (this.map.has(key)) { throw new Error(`Plugin service ${key} is conflicting with existing service ${key}`); } } await plugin.server.start(); for (const [key, container] of plugin.server.map) { this.map.set(key, container); } } } async stopPlugins(level) { const groupPlugins = this.plugins.get(level); if (!groupPlugins) { return; } for (const plugin of groupPlugins.toReversed()) { if (!plugin.server) { continue; } await plugin.server.stop(); for (const key of plugin.server.map.keys()) { this.map.delete(key); } plugin.server = undefined; } } async startService(key, container) { if (container.up) { this.config.verbose && console.log(`[${key}] Already initialized`); return; } this.config.verbose && console.log(`[${key}] Initializing...`); const value = container.init({ services: this.services, envs: env_1.default, log: (...items) => this.config.verbose && console.log(`[${key}]`, ...items), plug: () => (0, plug_1.plug)(key), }); if (value instanceof Promise) { container.value = await value; } else { container.value = value; } if (container.plug) { container.plug.__startedService = container.value; } container.up = true; if (container.value?.isFrogePlug) { console.warn(`[${key}] Got a plug instead of the service`); } else { this.config.verbose && console.log(`[${key}] Ready`); } } async stopService(key, container) { if (typeof container.destroy === 'undefined' || typeof container.value === 'undefined') { return; } this.config.verbose && console.log(`[${key}] Destroying...`); await container.destroy(container.value); container.value = undefined; if (container.plug) { container.plug.__startedService = undefined; } container.up = false; this.config.verbose && console.log(`[${key}] Destroyed`); } async startInternal(target) { const targetLevel = target && this.map.get(target)?.level; const startGroups = Map.groupBy(this.map.entries().filter(([key, info]) => key === target || typeof targetLevel === 'undefined' || info.level < targetLevel), ([, info]) => info.level); await this.startPlugins(-1); for (const [level, group] of startGroups.entries()) { if (this.config.parallelStartGroups) { await Promise.all(group.map(entry => this.startService(...entry))); } else { for (const entry of group) { await this.startService(...entry); } } if (level !== targetLevel) { await this.startPlugins(level); } } } async only(key) { const info = this.map.get(key); if (!info) { throw new Error(`Service ${key} doesn't exist or is from a plugin`); } if (!info.up) { this.config.verbose && console.log(`Starting only service '${String(key)}' and dependencies...`); await this.startInternal(key); } return this.services[key]; } async start() { this.config.verbose && console.log('Starting...'); await this.startInternal(); return this; } async stop(reasonText) { this.config.verbose && console.log(`Stopping (${reasonText ?? 'unspecified reason'})...`); const stopGroups = Map.groupBy(Array.from(this.map.entries()) // only stop my services .filter(([, c]) => c.serverSymbol === this.symbol) // in reverse order .reverse(), ([, info]) => info.level); for (const [level, group] of stopGroups.entries()) { await this.stopPlugins(level); if (this.config.parallelStopGroups) { await Promise.all(group.map(entry => this.stopService(...entry))); } else { for (const entry of group) { await this.stopService(...entry); } } } await this.stopPlugins(-1); } async launch() { if (!this.config.gracefulShutdownTimeoutMs) { console.info('gracefulShutdownTimeoutMs config option not set, fallback to 60 sec'); this.config.gracefulShutdownTimeoutMs = 60000; } try { await this.start(); process.once('SIGINT', () => this.shutdown('SIGINT')); process.once('SIGTERM', () => this.shutdown('SIGTERM')); } catch (e) { console.error('Failed to start: ', e); await this.shutdown('failed start cleanup'); } return this; } async shutdown(reasonText) { const timeoutInfo = this.config.gracefulShutdownTimeoutMs ? `timeout: ${this.config.gracefulShutdownTimeoutMs}ms` : 'no timeout'; if (this.config.gracefulShutdownTimeoutMs) { setTimeout(() => { console.error(`Reached shutdown timeout ${this.config.gracefulShutdownTimeoutMs}ms, killing...`); process.exit(1); }, this.config.gracefulShutdownTimeoutMs).unref(); } try { await this.stop((reasonText ?? 'shutdown') + ', ' + timeoutInfo); if (this.config.forceExitAfterShutdown) { process.exit(0); } } catch (e) { console.error('Shutdown incomplete, killing... Reason:', e); process.exit(1); } } } function froge() { return new FrogeServer(); } //# sourceMappingURL=index.js.map