@gtkx/utils 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +373 -0
- package/dist/class.d.ts +16 -0
- package/dist/class.d.ts.map +1 -0
- package/dist/class.js +2 -0
- package/dist/class.js.map +1 -0
- package/dist/collection.d.ts +58 -0
- package/dist/collection.d.ts.map +1 -0
- package/dist/collection.js +98 -0
- package/dist/collection.js.map +1 -0
- package/dist/error.d.ts +13 -0
- package/dist/error.d.ts.map +1 -0
- package/dist/error.js +13 -0
- package/dist/error.js.map +1 -0
- package/dist/graceful-shutdown.d.ts +70 -0
- package/dist/graceful-shutdown.d.ts.map +1 -0
- package/dist/graceful-shutdown.js +94 -0
- package/dist/graceful-shutdown.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/source.d.ts +34 -0
- package/dist/source.d.ts.map +1 -0
- package/dist/source.js +99 -0
- package/dist/source.js.map +1 -0
- package/dist/string.d.ts +59 -0
- package/dist/string.d.ts.map +1 -0
- package/dist/string.js +73 -0
- package/dist/string.js.map +1 -0
- package/package.json +35 -0
- package/src/class.ts +17 -0
- package/src/collection.ts +103 -0
- package/src/error.ts +12 -0
- package/src/graceful-shutdown.ts +141 -0
- package/src/index.ts +10 -0
- package/src/source.ts +103 -0
- package/src/string.ts +74 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-process graceful shutdown primitive for long-running Node processes.
|
|
3
|
+
*
|
|
4
|
+
* The helper installs `SIGINT`/`SIGTERM`/`SIGHUP` handlers, routes the first
|
|
5
|
+
* delivered signal through a user-supplied close callback, and escalates either
|
|
6
|
+
* on a second `SIGINT` or after a configurable timeout. On completion it calls
|
|
7
|
+
* `process.exit` with the canonical exit code for the signal.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const HANDLED_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"] as const satisfies readonly NodeJS.Signals[];
|
|
11
|
+
const DEFAULT_FORCE_KILL_TIMEOUT_MS = 5000;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Maps the POSIX signal that ended a process into the exit code shells use to
|
|
15
|
+
* report it. `SIGINT` yields `130` (Ctrl-C), every other tracked signal yields
|
|
16
|
+
* `143` (`SIGTERM`). A `null` signal — i.e. a clean exit — yields `0`.
|
|
17
|
+
*
|
|
18
|
+
* @param signal - The signal name, or `null` for a clean exit.
|
|
19
|
+
* @returns The exit code shells expect for `signal`.
|
|
20
|
+
*/
|
|
21
|
+
export const exitCodeForSignal = (signal: NodeJS.Signals | null): number => {
|
|
22
|
+
if (!signal) return 0;
|
|
23
|
+
return signal === "SIGINT" ? 130 : 143;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Caller-supplied behaviour for {@link installGracefulShutdown}.
|
|
28
|
+
*/
|
|
29
|
+
export type GracefulShutdownOptions = {
|
|
30
|
+
/**
|
|
31
|
+
* Invoked once on the first delivered signal. Its returned promise is
|
|
32
|
+
* awaited before {@link process.exit} fires; rejection is logged but does
|
|
33
|
+
* not block the exit.
|
|
34
|
+
*/
|
|
35
|
+
onSignal: (signal: NodeJS.Signals) => void | Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Invoked when escalation is required: on a second `SIGINT`, or when
|
|
38
|
+
* {@link GracefulShutdownOptions.forceKillAfterMs} elapses before the
|
|
39
|
+
* primary close finishes.
|
|
40
|
+
*/
|
|
41
|
+
onForce?: () => void;
|
|
42
|
+
/**
|
|
43
|
+
* Milliseconds to wait for the primary close before invoking `onForce`
|
|
44
|
+
* and exiting. Defaults to {@link DEFAULT_FORCE_KILL_TIMEOUT_MS}. Set to
|
|
45
|
+
* `0` to disable timeout-based escalation.
|
|
46
|
+
*/
|
|
47
|
+
forceKillAfterMs?: number;
|
|
48
|
+
/**
|
|
49
|
+
* Overrides the exit code computed from the triggering signal. Use this
|
|
50
|
+
* when the caller wants to propagate a child's own exit code instead.
|
|
51
|
+
*/
|
|
52
|
+
exitCode?: (signal: NodeJS.Signals) => number;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Handle returned by {@link installGracefulShutdown}, used to detach the
|
|
57
|
+
* helper's signal handlers (for example in tests).
|
|
58
|
+
*/
|
|
59
|
+
export type GracefulShutdownHandle = {
|
|
60
|
+
/**
|
|
61
|
+
* Removes the installed signal handlers and cancels any pending
|
|
62
|
+
* escalation timer. Idempotent.
|
|
63
|
+
*/
|
|
64
|
+
uninstall: () => void;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Installs the graceful-shutdown primitive on the current process.
|
|
69
|
+
*
|
|
70
|
+
* On the first matching signal: invokes `onSignal`, optionally schedules
|
|
71
|
+
* `onForce` after `forceKillAfterMs`, awaits the close, then exits. A second
|
|
72
|
+
* `SIGINT` (the canonical "force-kill" gesture) invokes `onForce`
|
|
73
|
+
* immediately. The exit code defaults to {@link exitCodeForSignal} but can
|
|
74
|
+
* be overridden by `exitCode`.
|
|
75
|
+
*
|
|
76
|
+
* @param options - Shutdown behaviour.
|
|
77
|
+
* @returns A handle that detaches the installed signal handlers.
|
|
78
|
+
*/
|
|
79
|
+
export const installGracefulShutdown = (options: GracefulShutdownOptions): GracefulShutdownHandle => {
|
|
80
|
+
const forceKillMs = options.forceKillAfterMs ?? DEFAULT_FORCE_KILL_TIMEOUT_MS;
|
|
81
|
+
|
|
82
|
+
let firstSignal: NodeJS.Signals | null = null;
|
|
83
|
+
let exited = false;
|
|
84
|
+
let forceTimer: NodeJS.Timeout | null = null;
|
|
85
|
+
|
|
86
|
+
const clearTimer = (): void => {
|
|
87
|
+
if (forceTimer) {
|
|
88
|
+
clearTimeout(forceTimer);
|
|
89
|
+
forceTimer = null;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const finish = (signal: NodeJS.Signals): void => {
|
|
94
|
+
if (exited) return;
|
|
95
|
+
exited = true;
|
|
96
|
+
clearTimer();
|
|
97
|
+
const code = options.exitCode ? options.exitCode(signal) : exitCodeForSignal(signal);
|
|
98
|
+
process.exit(code);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const handle = (signal: NodeJS.Signals): void => {
|
|
102
|
+
if (firstSignal === null) {
|
|
103
|
+
firstSignal = signal;
|
|
104
|
+
if (options.onForce && forceKillMs > 0) {
|
|
105
|
+
forceTimer = setTimeout(() => {
|
|
106
|
+
options.onForce?.();
|
|
107
|
+
finish(signal);
|
|
108
|
+
}, forceKillMs);
|
|
109
|
+
forceTimer.unref?.();
|
|
110
|
+
}
|
|
111
|
+
Promise.resolve()
|
|
112
|
+
.then(() => options.onSignal(signal))
|
|
113
|
+
.catch((error: unknown) => {
|
|
114
|
+
console.error("Graceful shutdown error:", error);
|
|
115
|
+
})
|
|
116
|
+
.finally(() => finish(signal));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (signal === "SIGINT" && firstSignal === "SIGINT") {
|
|
120
|
+
options.onForce?.();
|
|
121
|
+
finish(signal);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const handlers = new Map<NodeJS.Signals, () => void>();
|
|
126
|
+
for (const sig of HANDLED_SIGNALS) {
|
|
127
|
+
const listener = (): void => handle(sig);
|
|
128
|
+
handlers.set(sig, listener);
|
|
129
|
+
process.on(sig, listener);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
uninstall: () => {
|
|
134
|
+
for (const [sig, listener] of handlers) {
|
|
135
|
+
process.removeListener(sig, listener);
|
|
136
|
+
}
|
|
137
|
+
handlers.clear();
|
|
138
|
+
clearTimer();
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type { AnyClass } from "./class.js";
|
|
2
|
+
export { isShallowArrayEqual, isShallowEqual, omit, reverseNumericEnum } from "./collection.js";
|
|
3
|
+
export { errorMessage } from "./error.js";
|
|
4
|
+
export {
|
|
5
|
+
exitCodeForSignal,
|
|
6
|
+
type GracefulShutdownHandle,
|
|
7
|
+
installGracefulShutdown,
|
|
8
|
+
} from "./graceful-shutdown.js";
|
|
9
|
+
export { quote, toIdentifier } from "./source.js";
|
|
10
|
+
export { toCamelCase, toKebabCase, toPascalCase, toUpperFirst } from "./string.js";
|
package/src/source.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for shaping values into safe JavaScript/TypeScript source fragments:
|
|
3
|
+
* a reserved-word-safe identifier and a source-safe string literal. Both are
|
|
4
|
+
* pure and runtime-agnostic, intended for code generators that emit TypeScript.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Reserved words and global identifiers a generated identifier must not collide with. */
|
|
8
|
+
const RESERVED: ReadonlySet<string> = new Set([
|
|
9
|
+
"arguments",
|
|
10
|
+
"await",
|
|
11
|
+
"break",
|
|
12
|
+
"case",
|
|
13
|
+
"catch",
|
|
14
|
+
"class",
|
|
15
|
+
"const",
|
|
16
|
+
"continue",
|
|
17
|
+
"debugger",
|
|
18
|
+
"default",
|
|
19
|
+
"delete",
|
|
20
|
+
"do",
|
|
21
|
+
"else",
|
|
22
|
+
"enum",
|
|
23
|
+
"eval",
|
|
24
|
+
"export",
|
|
25
|
+
"extends",
|
|
26
|
+
"false",
|
|
27
|
+
"finally",
|
|
28
|
+
"for",
|
|
29
|
+
"function",
|
|
30
|
+
"if",
|
|
31
|
+
"import",
|
|
32
|
+
"in",
|
|
33
|
+
"instanceof",
|
|
34
|
+
"interface",
|
|
35
|
+
"let",
|
|
36
|
+
"new",
|
|
37
|
+
"null",
|
|
38
|
+
"package",
|
|
39
|
+
"private",
|
|
40
|
+
"protected",
|
|
41
|
+
"public",
|
|
42
|
+
"return",
|
|
43
|
+
"static",
|
|
44
|
+
"super",
|
|
45
|
+
"switch",
|
|
46
|
+
"this",
|
|
47
|
+
"throw",
|
|
48
|
+
"true",
|
|
49
|
+
"try",
|
|
50
|
+
"typeof",
|
|
51
|
+
"var",
|
|
52
|
+
"void",
|
|
53
|
+
"while",
|
|
54
|
+
"with",
|
|
55
|
+
"yield",
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Rewrites a candidate name into a JavaScript identifier safe to use at
|
|
60
|
+
* variable, parameter, or property position.
|
|
61
|
+
*
|
|
62
|
+
* The input is expected to already use valid identifier characters (for
|
|
63
|
+
* example the output of a case-conversion helper); the only transformation
|
|
64
|
+
* applied is appending an underscore when `name` collides with a reserved
|
|
65
|
+
* word or global identifier, so `toIdentifier("class")` is `"class_"` and
|
|
66
|
+
* `toIdentifier("iconName")` is `"iconName"`.
|
|
67
|
+
*
|
|
68
|
+
* @param name - The candidate identifier.
|
|
69
|
+
* @returns A reserved-word-safe identifier.
|
|
70
|
+
*/
|
|
71
|
+
export const toIdentifier = (name: string): string => (RESERVED.has(name) ? `${name}_` : name);
|
|
72
|
+
|
|
73
|
+
const UNSAFE_SOURCE_CHARS = /[<>\u2028\u2029]/g;
|
|
74
|
+
|
|
75
|
+
const escapeSourceChar = (char: string): string => {
|
|
76
|
+
switch (char) {
|
|
77
|
+
case "<":
|
|
78
|
+
return "\\u003C";
|
|
79
|
+
case ">":
|
|
80
|
+
return "\\u003E";
|
|
81
|
+
case "\u2028":
|
|
82
|
+
return "\\u2028";
|
|
83
|
+
case "\u2029":
|
|
84
|
+
return "\\u2029";
|
|
85
|
+
default:
|
|
86
|
+
return char;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Quotes a string for safe embedding as a literal in generated TypeScript
|
|
92
|
+
* source.
|
|
93
|
+
*
|
|
94
|
+
* Builds a double-quoted literal with `JSON.stringify`, then escapes the
|
|
95
|
+
* characters that are valid inside a JSON string yet unsafe inside JavaScript
|
|
96
|
+
* source: the angle brackets that could otherwise break out of an enclosing
|
|
97
|
+
* `</script>` and the U+2028/U+2029 line separators that prematurely terminate
|
|
98
|
+
* a string literal. The escaped form parses back to the original value.
|
|
99
|
+
*
|
|
100
|
+
* @param value - The string to embed.
|
|
101
|
+
* @returns A source-safe double-quoted string literal.
|
|
102
|
+
*/
|
|
103
|
+
export const quote = (value: string): string => JSON.stringify(value).replace(UNSAFE_SOURCE_CHARS, escapeSourceChar);
|
package/src/string.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, runtime-agnostic string-case helpers.
|
|
3
|
+
*
|
|
4
|
+
* The conversions translate between snake_case, kebab-case, camelCase, and
|
|
5
|
+
* PascalCase. They split only on underscores and hyphens and preserve the case
|
|
6
|
+
* of each segment, so they are not a substitute for a full Unicode-aware case
|
|
7
|
+
* transform.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Uppercases the first character of `value`, leaving the remaining characters
|
|
12
|
+
* untouched.
|
|
13
|
+
*
|
|
14
|
+
* The tail is preserved verbatim rather than lowercased, so
|
|
15
|
+
* `toUpperFirst("fooBar")` is `"FooBar"` and `toUpperFirst("URL")` is `"URL"`. An
|
|
16
|
+
* empty string returns an empty string.
|
|
17
|
+
*
|
|
18
|
+
* @param value - The string to transform.
|
|
19
|
+
* @returns `value` with its first character uppercased.
|
|
20
|
+
*/
|
|
21
|
+
export const toUpperFirst = (value: string): string => value.charAt(0).toUpperCase() + value.slice(1);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Converts a snake_case or kebab-case string to camelCase.
|
|
25
|
+
*
|
|
26
|
+
* The input is split on underscores and hyphens, dropping empty segments from
|
|
27
|
+
* leading, trailing, or repeated separators. The first segment is kept
|
|
28
|
+
* verbatim and every later segment is {@link toUpperFirst}-cased before joining,
|
|
29
|
+
* so `toCamelCase("icon_name")` is `"iconName"` and `toCamelCase("Box")` is
|
|
30
|
+
* `"Box"`. A string with no separators is returned unchanged.
|
|
31
|
+
*
|
|
32
|
+
* @param input - The snake_case or kebab-case identifier.
|
|
33
|
+
* @returns The camelCase form of `input`.
|
|
34
|
+
*/
|
|
35
|
+
export const toCamelCase = (input: string): string => {
|
|
36
|
+
const parts = input.split(/[_-]/g).filter((part) => part.length > 0);
|
|
37
|
+
if (parts.length === 0) return input;
|
|
38
|
+
const [first, ...rest] = parts;
|
|
39
|
+
const head = first ?? "";
|
|
40
|
+
return head + rest.map(toUpperFirst).join("");
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Converts a snake_case, kebab-case, or already-PascalCase string to
|
|
45
|
+
* PascalCase.
|
|
46
|
+
*
|
|
47
|
+
* The input is split on underscores and hyphens, dropping empty segments, and
|
|
48
|
+
* every remaining segment is {@link toUpperFirst}-cased before joining, so
|
|
49
|
+
* `toPascalCase("icon_name")` is `"IconName"` and `toPascalCase("Box")` is
|
|
50
|
+
* `"Box"`. An empty string is returned unchanged.
|
|
51
|
+
*
|
|
52
|
+
* @param input - The identifier to transform.
|
|
53
|
+
* @returns The PascalCase form of `input`.
|
|
54
|
+
*/
|
|
55
|
+
export const toPascalCase = (input: string): string => {
|
|
56
|
+
if (input.length === 0) return input;
|
|
57
|
+
const parts = input.split(/[_-]/g).filter((part) => part.length > 0);
|
|
58
|
+
if (parts.length === 0) return input;
|
|
59
|
+
return parts.map(toUpperFirst).join("");
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Converts a camelCase or PascalCase string to kebab-case.
|
|
64
|
+
*
|
|
65
|
+
* Each uppercase character is lowercased; every uppercase character other than
|
|
66
|
+
* the first is additionally prefixed with a hyphen, so `toKebabCase("iconName")`
|
|
67
|
+
* is `"icon-name"` and `toKebabCase("Title")` is `"title"`. The leading
|
|
68
|
+
* character is never prefixed with a hyphen.
|
|
69
|
+
*
|
|
70
|
+
* @param input - The camelCase or PascalCase identifier.
|
|
71
|
+
* @returns The kebab-case form of `input`.
|
|
72
|
+
*/
|
|
73
|
+
export const toKebabCase = (input: string): string =>
|
|
74
|
+
input.replaceAll(/[A-Z]/g, (char, index: number) => (index === 0 ? char.toLowerCase() : `-${char.toLowerCase()}`));
|