kaelum 1.4.8 → 1.6.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/core/plugin.js ADDED
@@ -0,0 +1,82 @@
1
+ // core/plugin.js
2
+ // Kaelum plugin registration system.
3
+ // Plugins are functions that receive (app, options) and can add routes,
4
+ // middleware, and config to the Kaelum app instance.
5
+
6
+ /**
7
+ * Resolve the display name and dedup key for a plugin function.
8
+ * Checks fn.pluginName first, then fn.name, falls back to null (anonymous).
9
+ * @param {Function} fn
10
+ * @returns {string|null}
11
+ */
12
+ function resolvePluginName(fn) {
13
+ if (typeof fn.pluginName === "string" && fn.pluginName) {
14
+ return fn.pluginName;
15
+ }
16
+ if (typeof fn.name === "string" && fn.name) {
17
+ return fn.name;
18
+ }
19
+ return null;
20
+ }
21
+
22
+ /**
23
+ * Register a plugin on the Kaelum app.
24
+ *
25
+ * @param {Object} app - Kaelum/Express app instance
26
+ * @param {Function} fn - Plugin function with signature (app, options) => void
27
+ * @param {Object} [options={}] - Options passed to the plugin
28
+ * @returns {Object} app - for chaining
29
+ * @throws {Error} if fn is not a function
30
+ * @throws {Error} if a named plugin with the same name is already registered
31
+ */
32
+ function registerPlugin(app, fn, options = {}) {
33
+ if (typeof fn !== "function") {
34
+ throw new Error(
35
+ `Kaelum plugin: expected a function, got ${typeof fn}`
36
+ );
37
+ }
38
+
39
+ // ensure registry exists
40
+ if (!app.locals) app.locals = {};
41
+ if (!Array.isArray(app.locals._kaelum_plugins)) {
42
+ app.locals._kaelum_plugins = [];
43
+ }
44
+
45
+ const name = resolvePluginName(fn);
46
+
47
+ // duplicate guard for named plugins
48
+ if (name) {
49
+ const already = app.locals._kaelum_plugins.find((p) => p.name === name);
50
+ if (already) {
51
+ throw new Error(
52
+ `Kaelum plugin: "${name}" is already registered. ` +
53
+ `Each named plugin can only be registered once.`
54
+ );
55
+ }
56
+ }
57
+
58
+ // execute the plugin
59
+ fn(app, options);
60
+
61
+ // record in registry
62
+ app.locals._kaelum_plugins.push({
63
+ name: name || `anonymous_${app.locals._kaelum_plugins.length}`,
64
+ options,
65
+ });
66
+
67
+ return app;
68
+ }
69
+
70
+ /**
71
+ * Return the list of registered plugin names.
72
+ * @param {Object} app
73
+ * @returns {string[]}
74
+ */
75
+ function getPlugins(app) {
76
+ if (!app.locals || !Array.isArray(app.locals._kaelum_plugins)) {
77
+ return [];
78
+ }
79
+ return app.locals._kaelum_plugins.map((p) => p.name);
80
+ }
81
+
82
+ module.exports = { registerPlugin, getPlugins };
package/core/setConfig.js CHANGED
@@ -257,6 +257,26 @@ function setConfig(app, options = {}) {
257
257
  }
258
258
  }
259
259
 
260
+ // --- Graceful Shutdown ---
261
+ if (options.hasOwnProperty("gracefulShutdown")) {
262
+ const server = app.locals && app.locals._kaelum_server;
263
+ if (server && server.listening) {
264
+ const {
265
+ enableGracefulShutdown,
266
+ removeSignalHandlers,
267
+ } = require("./shutdown");
268
+ if (options.gracefulShutdown === false) {
269
+ removeSignalHandlers(app);
270
+ } else {
271
+ const shutdownOpts =
272
+ typeof options.gracefulShutdown === "object"
273
+ ? options.gracefulShutdown
274
+ : {};
275
+ enableGracefulShutdown(app, shutdownOpts);
276
+ }
277
+ }
278
+ }
279
+
260
280
  // Return the full merged config for convenience
261
281
  return app.locals.kaelumConfig;
262
282
  }
@@ -0,0 +1,167 @@
1
+ // core/shutdown.js
2
+ // Kaelum graceful shutdown module.
3
+ // Handles process signal interception, connection draining, and cleanup hook execution.
4
+
5
+ const DEFAULT_TIMEOUT = 10000;
6
+ const DEFAULT_SIGNALS = ["SIGTERM", "SIGINT"];
7
+
8
+ /**
9
+ * Register a cleanup function to run during shutdown.
10
+ */
11
+ function onShutdown(app, fn) {
12
+ if (!app) throw new Error("onShutdown requires an app instance");
13
+ if (typeof fn !== "function") {
14
+ throw new Error("onShutdown: expected a function, got " + typeof fn);
15
+ }
16
+
17
+ if (!Array.isArray(app.locals._kaelum_shutdown_hooks)) {
18
+ app.locals._kaelum_shutdown_hooks = [];
19
+ }
20
+
21
+ app.locals._kaelum_shutdown_hooks.push(fn);
22
+ return app;
23
+ }
24
+
25
+ /**
26
+ * Run all registered cleanup hooks in order.
27
+ * Errors in individual hooks are caught and logged but do not abort the sequence.
28
+ */
29
+ async function runCleanupHooks(app) {
30
+ const hooks = (app.locals && app.locals._kaelum_shutdown_hooks) || [];
31
+ for (const fn of hooks) {
32
+ try {
33
+ await Promise.resolve(fn());
34
+ } catch (err) {
35
+ console.error(
36
+ "Kaelum shutdown: cleanup hook error:",
37
+ err && err.message ? err.message : err
38
+ );
39
+ }
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Gracefully close the server and run cleanup hooks.
45
+ *
46
+ * If callback is provided: calls cb(err) and returns app.
47
+ * If no callback: returns a Promise<void>.
48
+ */
49
+ function close(app, cb) {
50
+ if (!app) throw new Error("close requires an app instance");
51
+
52
+ // Prevent double-shutdown
53
+ if (app.locals._kaelum_shutdown_in_progress) {
54
+ if (typeof cb === "function") {
55
+ process.nextTick(() => cb());
56
+ return app;
57
+ }
58
+ return Promise.resolve();
59
+ }
60
+
61
+ app.locals._kaelum_shutdown_in_progress = true;
62
+
63
+ const timeout = app.locals._kaelum_shutdown_timeout || DEFAULT_TIMEOUT;
64
+ const server = app.locals._kaelum_server;
65
+
66
+ // Remove signal handlers to prevent re-entry
67
+ removeSignalHandlers(app);
68
+
69
+ const doShutdown = async () => {
70
+ try {
71
+ // Phase 1: close the HTTP server (drain existing connections)
72
+ if (server && typeof server.close === "function") {
73
+ await new Promise((resolve) => {
74
+ const timer = setTimeout(() => {
75
+ console.error(
76
+ "Kaelum shutdown: server close timed out after " + timeout + "ms"
77
+ );
78
+ resolve();
79
+ }, timeout);
80
+ if (timer.unref) timer.unref();
81
+
82
+ server.close((err) => {
83
+ clearTimeout(timer);
84
+ if (err) {
85
+ console.error(
86
+ "Kaelum shutdown: server close error:",
87
+ err.message || err
88
+ );
89
+ }
90
+ resolve();
91
+ });
92
+ });
93
+ }
94
+
95
+ // Phase 2: run cleanup hooks (always runs, even after timeout)
96
+ await runCleanupHooks(app);
97
+ } finally {
98
+ app.locals._kaelum_shutdown_in_progress = false;
99
+ }
100
+ };
101
+
102
+ if (typeof cb === "function") {
103
+ doShutdown()
104
+ .then(() => cb(null))
105
+ .catch((err) => cb(err));
106
+ return app;
107
+ }
108
+
109
+ return doShutdown();
110
+ }
111
+
112
+ /**
113
+ * Register process signal handlers for automatic graceful shutdown.
114
+ * Called internally from start.js after server creation.
115
+ */
116
+ function enableGracefulShutdown(app, options) {
117
+ if (!app) return;
118
+
119
+ const cfg = typeof options === "object" && options !== null ? options : {};
120
+ const timeout = cfg.timeout || DEFAULT_TIMEOUT;
121
+ const signals = Array.isArray(cfg.signals) ? cfg.signals : DEFAULT_SIGNALS;
122
+
123
+ app.locals._kaelum_shutdown_timeout = timeout;
124
+
125
+ // Remove existing handlers before registering new ones
126
+ removeSignalHandlers(app);
127
+
128
+ const handlers = {};
129
+
130
+ for (const signal of signals) {
131
+ const handler = () => {
132
+ console.log(
133
+ "\nKaelum: received " + signal + ", starting graceful shutdown..."
134
+ );
135
+ close(app)
136
+ .then(() => process.exit(0))
137
+ .catch((err) => {
138
+ console.error("Kaelum shutdown error:", err.message || err);
139
+ process.exit(1);
140
+ });
141
+ };
142
+ handlers[signal] = handler;
143
+ process.on(signal, handler);
144
+ }
145
+
146
+ app.locals._kaelum_shutdown_handlers = handlers;
147
+ }
148
+
149
+ /**
150
+ * Remove signal handlers previously installed by enableGracefulShutdown.
151
+ */
152
+ function removeSignalHandlers(app) {
153
+ const handlers = app.locals && app.locals._kaelum_shutdown_handlers;
154
+ if (handlers && typeof handlers === "object") {
155
+ for (const signal of Object.keys(handlers)) {
156
+ process.removeListener(signal, handlers[signal]);
157
+ }
158
+ app.locals._kaelum_shutdown_handlers = null;
159
+ }
160
+ }
161
+
162
+ module.exports = {
163
+ onShutdown,
164
+ close,
165
+ enableGracefulShutdown,
166
+ removeSignalHandlers,
167
+ };
package/core/start.js CHANGED
@@ -5,6 +5,9 @@
5
5
  // - If a server is already running, returns the existing server (no double-listen)
6
6
  // - Stores the server instance in app.locals._kaelum_server
7
7
  // - Attaches basic error logging for startup errors
8
+ // - Enables graceful shutdown by default (unless config disables it)
9
+
10
+ const { enableGracefulShutdown } = require("./shutdown");
8
11
 
9
12
  /**
10
13
  * Normalize and validate a port-like value.
@@ -109,6 +112,16 @@ function start(app, port, cb) {
109
112
  // persist reference so we can check or close later
110
113
  app.locals._kaelum_server = server;
111
114
 
115
+ // enable graceful shutdown by default unless explicitly disabled
116
+ const shutdownCfg = cfg.gracefulShutdown;
117
+ if (shutdownCfg !== false) {
118
+ const shutdownOpts =
119
+ typeof shutdownCfg === "object" && shutdownCfg !== null
120
+ ? shutdownCfg
121
+ : {};
122
+ enableGracefulShutdown(app, shutdownOpts);
123
+ }
124
+
112
125
  return server;
113
126
  }
114
127
 
package/createApp.js CHANGED
@@ -19,6 +19,8 @@ const { errorHandler } = require("./core/errorHandler");
19
19
  const registerHealth = require("./core/healthCheck");
20
20
  const redirect = require("./core/redirect");
21
21
  const { removeMiddlewareByFn } = require("./core/utils");
22
+ const { registerPlugin, getPlugins } = require("./core/plugin");
23
+ const { onShutdown, close } = require("./core/shutdown");
22
24
 
23
25
  function createApp() {
24
26
  const app = express();
@@ -29,6 +31,9 @@ function createApp() {
29
31
  // ensure locals object and initial persisted config
30
32
  app.locals = app.locals || {};
31
33
  app.locals.kaelumConfig = app.locals.kaelumConfig || {};
34
+ app.locals._kaelum_plugins = [];
35
+ app.locals._kaelum_shutdown_hooks = [];
36
+ app.locals._kaelum_shutdown_in_progress = false;
32
37
  // persist baseline config so app.get("kaelum:config") is always available
33
38
  app.set("kaelum:config", app.locals.kaelumConfig);
34
39
 
@@ -169,6 +174,28 @@ function createApp() {
169
174
  // alias for convenience
170
175
  app.errorHandler = app.useErrorHandler;
171
176
 
177
+ // ---------------------------
178
+ // Plugin system
179
+ // ---------------------------
180
+ app.plugin = function (fn, options) {
181
+ return registerPlugin(app, fn, options);
182
+ };
183
+
184
+ app.getPlugins = function () {
185
+ return getPlugins(app);
186
+ };
187
+
188
+ // ---------------------------
189
+ // Graceful shutdown
190
+ // ---------------------------
191
+ app.onShutdown = function (fn) {
192
+ return onShutdown(app, fn);
193
+ };
194
+
195
+ app.close = function (cb) {
196
+ return close(app, cb);
197
+ };
198
+
172
199
  return app;
173
200
  }
174
201
 
package/index.d.ts CHANGED
@@ -10,6 +10,7 @@ interface KaelumConfig {
10
10
  port?: number;
11
11
  views?: { engine?: string; path?: string };
12
12
  logger?: boolean | false;
13
+ gracefulShutdown?: boolean | GracefulShutdownConfig;
13
14
  }
14
15
 
15
16
  interface HealthOptions {
@@ -32,6 +33,13 @@ interface ErrorHandlerOptions {
32
33
  onError?: (err: Error, req: any, res: any) => void;
33
34
  }
34
35
 
36
+ interface GracefulShutdownConfig {
37
+ /** Timeout in milliseconds before forcing shutdown (default: 10000) */
38
+ timeout?: number;
39
+ /** Process signals to handle (default: ["SIGTERM", "SIGINT"]) */
40
+ signals?: string[];
41
+ }
42
+
35
43
  interface RedirectEntry {
36
44
  path: string;
37
45
  to: string;
@@ -53,6 +61,9 @@ interface RouteHandlers {
53
61
  [subpath: string]: any;
54
62
  }
55
63
 
64
+ /** Plugin function signature */
65
+ type KaelumPlugin = (app: KaelumApp, options?: Record<string, any>) => void;
66
+
56
67
  interface KaelumApp extends Express {
57
68
  /** Configure Kaelum features (cors, helmet, static, logs, etc.) */
58
69
  setConfig(options: KaelumConfig): KaelumConfig;
@@ -93,6 +104,19 @@ interface KaelumApp extends Express {
93
104
 
94
105
  /** Remove static file serving */
95
106
  removeStatic(): KaelumConfig;
107
+
108
+ /** Register a plugin */
109
+ plugin(fn: KaelumPlugin, options?: Record<string, any>): KaelumApp;
110
+
111
+ /** List registered plugin names */
112
+ getPlugins(): string[];
113
+
114
+ /** Gracefully close the server and run cleanup hooks */
115
+ close(): Promise<void>;
116
+ close(cb: (err?: Error | null) => void): KaelumApp;
117
+
118
+ /** Register a cleanup function to run during graceful shutdown */
119
+ onShutdown(fn: () => void | Promise<void>): KaelumApp;
96
120
  }
97
121
 
98
122
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kaelum",
3
- "version": "1.4.8",
3
+ "version": "1.6.0",
4
4
  "description": "A minimalist Node.js framework for building web pages and APIs with simplicity and speed.",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -38,7 +38,7 @@
38
38
  "license": "MIT",
39
39
  "repository": {
40
40
  "type": "git",
41
- "url": "https://github.com/kaelumjs/kaelum.git"
41
+ "url": "git+https://github.com/kaelumjs/kaelum.git"
42
42
  },
43
43
  "bugs": {
44
44
  "url": "https://github.com/kaelumjs/kaelum/issues"