Files
Tom Moor 0139b91b5d chore: Replace lodash with es-toolkit (#12281)
* chore: Replace lodash with es-toolkit

Migrate all direct lodash imports to es-toolkit/compat for a smaller,
faster, lodash-compatible utility library. Transitive lodash usage from
other packages remains unchanged.

* fix: Restore isPlainObject semantics in CanCan policy

The lodash migration aliased `isObject` to `lodash/isPlainObject` and
the codemod incorrectly mapped the local name to es-toolkit's `isObject`,
which also returns true for arrays and functions. This caused condition
objects in policy definitions to be skipped, breaking authorization
checks across the codebase.

* fix: Restore unicode-aware length counting in validators

es-toolkit/compat's size() returns string.length, while lodash's _.size()
counts unicode code points. Switch to [...value].length to preserve the
previous behavior so multi-byte characters like emoji count as one.
2026-05-06 21:03:47 -04:00

108 lines
2.8 KiB
TypeScript

import { groupBy } from "es-toolkit/compat";
import Logger from "@server/logging/Logger";
import { sleep } from "@shared/utils/timers";
export enum ShutdownOrder {
first = 0,
normal = 1,
last = 2,
}
type Handler = {
key: string;
order: ShutdownOrder;
callback: () => Promise<unknown>;
};
export default class ShutdownHelper {
/**
* The amount of time to wait for connections to close before forcefully
* closing them. This allows for regular HTTP requests to complete but
* prevents long running requests from blocking shutdown.
*/
public static readonly connectionGraceTimeout = 5 * 1000;
/**
* The maximum amount of time to wait for ongoing work to finish before
* force quitting the process. In the event of a force quit, the process
* will exit with a non-zero exit code.
*/
public static readonly forceQuitTimeout = 60 * 1000;
/** Whether the server is currently shutting down */
private static isShuttingDown = false;
/** List of shutdown handlers to execute */
private static handlers: Handler[] = [];
/**
* Add a shutdown handler to be executed when the process is exiting
*
* @param key The key of the handler
* @param callback The callback to execute
*/
public static add(
key: string,
order: ShutdownOrder,
callback: () => Promise<unknown>
) {
this.handlers.push({ key, order, callback });
}
/**
* Remove a shutdown handler, if it exists
*
* @param key The key of the handler to remove
*/
public static remove(key: string) {
this.handlers = this.handlers.filter((handler) => handler.key !== key);
}
/**
* Exit the process after all shutdown handlers have completed
*
* @param code The exit code to use
*/
public static async execute(code = 0) {
if (this.isShuttingDown) {
return;
}
this.isShuttingDown = true;
// Start the shutdown timer
void sleep(this.forceQuitTimeout).then(() => {
Logger.info("lifecycle", "Force quitting");
process.exit(1);
});
// Group handlers by order
const shutdownGroups = groupBy(this.handlers, "order");
const orderedKeys = Object.keys(shutdownGroups).sort();
// Execute handlers in order
for (const key of orderedKeys) {
Logger.debug("lifecycle", `Running shutdown group ${key}`);
const handlers = shutdownGroups[key];
await Promise.allSettled(
handlers.map(async (handler) => {
Logger.debug("lifecycle", `Running shutdown handler ${handler.key}`);
await handler.callback().catch((error) => {
Logger.error(
`Error inside shutdown handler ${handler.key}`,
error,
{
key: handler.key,
}
);
});
})
);
}
Logger.info("lifecycle", "Gracefully quitting");
process.exit(code);
}
}