tina4-nodejs 3.11.17 → 3.11.18
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.
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background tasks — periodic callbacks that run alongside the HTTP server.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors Python's `tina4_python.core.server.background(fn, interval=1.0)`.
|
|
5
|
+
* Use this instead of `setInterval` directly, so timers integrate with the
|
|
6
|
+
* server lifecycle and clear cleanly on graceful shutdown (SIGTERM/SIGINT)
|
|
7
|
+
* or when `stopAllBackgroundTasks()` is called.
|
|
8
|
+
*
|
|
9
|
+
* import { background } from "@tina4/core";
|
|
10
|
+
*
|
|
11
|
+
* background(() => processQueue(), 2); // every 2 seconds
|
|
12
|
+
* background(async () => await healthCheck(), 30); // async also fine
|
|
13
|
+
*
|
|
14
|
+
* Errors thrown from a callback are caught and logged so a single failing
|
|
15
|
+
* task cannot bring down the rest of the timer wheel.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { Log } from "./logger.js";
|
|
19
|
+
|
|
20
|
+
/** A registered background task — kept so `stopAllBackgroundTasks()` can clear them. */
|
|
21
|
+
interface BackgroundTask {
|
|
22
|
+
callback: () => unknown | Promise<unknown>;
|
|
23
|
+
intervalSeconds: number;
|
|
24
|
+
timer: NodeJS.Timeout;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const _tasks: BackgroundTask[] = [];
|
|
28
|
+
let _signalsBound = false;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Register signal handlers exactly once so SIGTERM/SIGINT during a long-running
|
|
32
|
+
* process clears all background timers before the runtime exits. The handler
|
|
33
|
+
* is additive — it does not call `process.exit()` or interfere with other
|
|
34
|
+
* shutdown logic registered by the CLI or user code.
|
|
35
|
+
*/
|
|
36
|
+
function _bindSignalsOnce(): void {
|
|
37
|
+
if (_signalsBound) return;
|
|
38
|
+
_signalsBound = true;
|
|
39
|
+
const cleanup = () => stopAllBackgroundTasks();
|
|
40
|
+
process.on("SIGTERM", cleanup);
|
|
41
|
+
process.on("SIGINT", cleanup);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Register a callback to run periodically alongside the HTTP server.
|
|
46
|
+
*
|
|
47
|
+
* @param callback Function to call (sync or async, no arguments).
|
|
48
|
+
* @param intervalSeconds Seconds between invocations (default: 1).
|
|
49
|
+
* @returns A handle whose `stop()` clears just this one task.
|
|
50
|
+
*/
|
|
51
|
+
export function background(
|
|
52
|
+
callback: () => unknown | Promise<unknown>,
|
|
53
|
+
intervalSeconds = 1,
|
|
54
|
+
): { stop: () => void } {
|
|
55
|
+
if (typeof callback !== "function") {
|
|
56
|
+
throw new TypeError("background(callback, interval): callback must be a function");
|
|
57
|
+
}
|
|
58
|
+
if (typeof intervalSeconds !== "number" || !isFinite(intervalSeconds) || intervalSeconds <= 0) {
|
|
59
|
+
throw new RangeError(
|
|
60
|
+
`background(callback, interval): interval must be a positive number (got ${intervalSeconds})`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
_bindSignalsOnce();
|
|
65
|
+
|
|
66
|
+
const ms = Math.max(1, Math.round(intervalSeconds * 1000));
|
|
67
|
+
const timer = setInterval(() => {
|
|
68
|
+
try {
|
|
69
|
+
const result = callback();
|
|
70
|
+
if (result && typeof (result as Promise<unknown>).then === "function") {
|
|
71
|
+
(result as Promise<unknown>).catch((err) => {
|
|
72
|
+
Log.error?.(`background task error: ${err instanceof Error ? err.message : String(err)}`);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
} catch (err) {
|
|
76
|
+
Log.error?.(`background task error: ${err instanceof Error ? err.message : String(err)}`);
|
|
77
|
+
}
|
|
78
|
+
}, ms);
|
|
79
|
+
|
|
80
|
+
// Don't keep the event loop alive solely for background tasks — this matches
|
|
81
|
+
// Python's behaviour, where background tasks live in the server's loop and
|
|
82
|
+
// exit with it rather than blocking shutdown.
|
|
83
|
+
if (typeof timer.unref === "function") {
|
|
84
|
+
timer.unref();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const task: BackgroundTask = { callback, intervalSeconds, timer };
|
|
88
|
+
_tasks.push(task);
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
stop: () => {
|
|
92
|
+
clearInterval(task.timer);
|
|
93
|
+
const idx = _tasks.indexOf(task);
|
|
94
|
+
if (idx !== -1) _tasks.splice(idx, 1);
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Clear every registered background task. Called automatically on SIGTERM/SIGINT;
|
|
101
|
+
* also called from the server's `close()` so a manual server shutdown stops
|
|
102
|
+
* the timer wheel along with HTTP listeners.
|
|
103
|
+
*/
|
|
104
|
+
export function stopAllBackgroundTasks(): void {
|
|
105
|
+
while (_tasks.length > 0) {
|
|
106
|
+
const task = _tasks.pop()!;
|
|
107
|
+
clearInterval(task.timer);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Number of currently-registered background tasks (test helper). */
|
|
112
|
+
export function backgroundTaskCount(): number {
|
|
113
|
+
return _tasks.length;
|
|
114
|
+
}
|
|
@@ -13,6 +13,7 @@ export type {
|
|
|
13
13
|
} from "./types.js";
|
|
14
14
|
|
|
15
15
|
export { startServer, resolvePortAndHost, handle, start, stop } from "./server.js";
|
|
16
|
+
export { background, stopAllBackgroundTasks, backgroundTaskCount } from "./background.js";
|
|
16
17
|
export { Router, RouteGroup, RouteRef, defaultRouter, runRouteMiddlewares } from "./router.js";
|
|
17
18
|
export { get, post, put, patch, del, any, websocket, del as delete } from "./router.js";
|
|
18
19
|
export type { RouteInfo } from "./router.js";
|
|
@@ -20,6 +20,7 @@ import { rateLimiter } from "./rateLimiter.js";
|
|
|
20
20
|
import { Log } from "./logger.js";
|
|
21
21
|
import { DevAdmin, RequestInspector } from "./devAdmin.js";
|
|
22
22
|
import { I18n } from "./i18n.js";
|
|
23
|
+
import { stopAllBackgroundTasks } from "./background.js";
|
|
23
24
|
|
|
24
25
|
const __filename = fileURLToPath(import.meta.url);
|
|
25
26
|
const __dirname = dirname(__filename);
|
|
@@ -549,6 +550,7 @@ ${reset}
|
|
|
549
550
|
// Return a handle that kills all workers
|
|
550
551
|
return {
|
|
551
552
|
close: () => {
|
|
553
|
+
stopAllBackgroundTasks();
|
|
552
554
|
for (const id in cluster.workers) {
|
|
553
555
|
cluster.workers[id]?.kill();
|
|
554
556
|
}
|
|
@@ -1075,6 +1077,8 @@ ${reset}
|
|
|
1075
1077
|
}
|
|
1076
1078
|
resolvePromise({
|
|
1077
1079
|
close: () => {
|
|
1080
|
+
// Clear any registered background timers so graceful shutdown actually exits.
|
|
1081
|
+
stopAllBackgroundTasks();
|
|
1078
1082
|
if (aiServer) aiServer.close();
|
|
1079
1083
|
server.close();
|
|
1080
1084
|
// Close database if ORM was initialized
|