// https://gist.github.com/nfantone/1eaa803772025df69d07f4dbf5df7e58

"use strict";

/**
 * @callback BeforeShutdownListener
 * @param {string} [signalOrEvent] The exit signal or event name received on the process.
 */

/**
 * System signals the app will listen to initiate shutdown.
 * @const {string[]}
 */
const SHUTDOWN_SIGNALS = ["SIGINT", "SIGTERM"];

/**
 * Time in milliseconds to wait before forcing shutdown.
 * @const {number}
 */
const SHUTDOWN_TIMEOUT = 15000;

/**
 * A queue of listener callbacks to execute before shutting
 * down the process.
 * @type {BeforeShutdownListener[]}
 */
const shutdownListeners: ((signalOrEvent: string) => void)[] = [];

/**
 * Listen for signals and execute given `fn` function once.
 * @param  {string[]} signals System signals to listen to.
 * @param  {function(string)} fn Function to execute on shutdown.
 */
const processOnce = (
	signals: string[],
	fn: (signalOrEvent: string) => void,
) => {
	for (const sig of signals) {
		process.once(sig, fn);
	}
};

/**
 * Sets a forced shutdown mechanism that will exit the process after `timeout` milliseconds.
 * @param {number} timeout Time to wait before forcing shutdown (milliseconds)
 */
const forceExitAfter = (timeout: number) => () => {
	setTimeout(() => {
		// Force shutdown after timeout
		console.warn(
			`Could not close resources gracefully after ${timeout}ms: forcing shutdown`,
		);
		return process.exit(1);
	}, timeout).unref();
};

/**
 * Main process shutdown handler. Will invoke every previously registered async shutdown listener
 * in the queue and exit with a code of `0`. Any `Promise` rejections from any listener will
 * be logged out as a warning, but won't prevent other callbacks from executing.
 * @param {string} signalOrEvent The exit signal or event name received on the process.
 */
async function shutdownHandler(signalOrEvent: string) {
	if (process.env.NODE_ENV === "test") return process.exit(0);

	console.warn(`Shutting down: received [${signalOrEvent}] signal`);

	for (const listener of shutdownListeners) {
		try {
			await listener(signalOrEvent);
		} catch (err) {
			if (err instanceof Error) {
				console.warn(
					`A shutdown handler failed before completing with: ${
						err.message || err
					}`,
				);
			}
		}
	}

	return process.exit(0);
}

/**
 * Registers a new shutdown listener to be invoked before exiting
 * the main process. Listener handlers are guaranteed to be called in the order
 * they were registered.
 * @param {BeforeShutdownListener} listener The shutdown listener to register.
 * @returns {BeforeShutdownListener} Echoes back the supplied `listener`.
 */
export function beforeShutdown(listener: () => void) {
	shutdownListeners.push(listener);
	return listener;
}

// Register shutdown callback that kills the process after `SHUTDOWN_TIMEOUT` milliseconds
// This prevents custom shutdown handlers from hanging the process indefinitely
processOnce(SHUTDOWN_SIGNALS, forceExitAfter(SHUTDOWN_TIMEOUT));

// Register process shutdown callback
// Will listen to incoming signal events and execute all registered handlers in the stack
processOnce(SHUTDOWN_SIGNALS, shutdownHandler);
