vibe-gx 4.1.0 → 4.1.2
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/README.md +66 -17
- package/package.json +1 -1
- package/utils/core/handler.js +9 -4
- package/utils/core/logger.js +185 -0
- package/utils/core/response.js +7 -0
- package/utils/core/server.js +64 -9
- package/utils/scaling/cache.js +42 -3
- package/vibe.d.ts +93 -6
- package/vibe.js +55 -8
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@ npm install vibe-gx
|
|
|
46
46
|
| :---------------------------- | :--------------------------------------------------------- |
|
|
47
47
|
| 🚀 **Code-Gen Serialization** | Schema-compiled JSON serializers via `new Function()` |
|
|
48
48
|
| 🎯 **Hybrid Router** | O(1) static + O(log n) Trie routing |
|
|
49
|
-
| 🔌 **Plugin System** |
|
|
49
|
+
| 🔌 **Plugin System** | Encapsulated `register()` with optional route prefixes |
|
|
50
50
|
| 🎨 **Decorators** | Extend app, request, and response |
|
|
51
51
|
| ⚡ **Cluster Mode** | Built-in multi-process scaling |
|
|
52
52
|
| 💾 **LRU Cache** | Built-in response caching with ETag |
|
|
@@ -133,7 +133,52 @@ app.post("/users", (req) => {
|
|
|
133
133
|
|
|
134
134
|
---
|
|
135
135
|
|
|
136
|
-
##
|
|
136
|
+
## 📝 Logging & Error Handling
|
|
137
|
+
|
|
138
|
+
Vibe ships with a structured JSON logger (Pino-compatible) and a powerful error interception system. Errors thrown, returned, or sent from any route are automatically caught and routed through a central error handler.
|
|
139
|
+
|
|
140
|
+
### JSON Structured Logging
|
|
141
|
+
|
|
142
|
+
Initialize the app with `logger: { lifecycle: true }` or add `prettyPrint: true` for development to get beautiful, human-readable terminal output.
|
|
143
|
+
|
|
144
|
+
```javascript
|
|
145
|
+
const app = vibe({
|
|
146
|
+
logger: {
|
|
147
|
+
lifecycle: true,
|
|
148
|
+
prettyPrint: process.env.NODE_ENV !== "production",
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// JSON native bindings
|
|
153
|
+
app.log.info({ database: "online" }, "System booting...");
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Contextual Sub-Loggers
|
|
157
|
+
|
|
158
|
+
Every incoming request dynamically extracts a fast UUID exposed securely on `req.id` natively piping through to `req.log`.
|
|
159
|
+
|
|
160
|
+
```javascript
|
|
161
|
+
app.get("/users/:id", (req) => {
|
|
162
|
+
req.log.warn("Database lookup constraint fired");
|
|
163
|
+
// Production Output -> {"level":40,"time":123,"reqId":"abcd-123", "msg":"..."}
|
|
164
|
+
|
|
165
|
+
return { success: true };
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Central Error Abstraction
|
|
170
|
+
|
|
171
|
+
To route an error into the central handler without halting execution via `throw`, simply return an `Error` object from your handler — Vibe intercepts it automatically:
|
|
172
|
+
|
|
173
|
+
```javascript
|
|
174
|
+
app.get("/test", (req, res) => {
|
|
175
|
+
return new Error("Something went wrong");
|
|
176
|
+
});
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## 🔌 Plugin System
|
|
137
182
|
|
|
138
183
|
Plugins provide encapsulated route groups with optional prefixes:
|
|
139
184
|
|
|
@@ -215,13 +260,13 @@ Extend app, request, or response with custom properties:
|
|
|
215
260
|
// App decorator - shared config
|
|
216
261
|
app.decorate("config", { env: "production", version: "1.0.0" });
|
|
217
262
|
|
|
218
|
-
// Access
|
|
219
|
-
app.get("/version", () => ({ version: app.
|
|
263
|
+
// Access directly on the app
|
|
264
|
+
app.get("/version", () => ({ version: app.config.version }));
|
|
220
265
|
|
|
221
|
-
//
|
|
266
|
+
// Same in plugins — decorators are spread directly
|
|
222
267
|
app.register(
|
|
223
268
|
async (api) => {
|
|
224
|
-
api.get("/env", () => ({ env: api.config.env }));
|
|
269
|
+
api.get("/env", () => ({ env: api.config.env }));
|
|
225
270
|
},
|
|
226
271
|
{ prefix: "/api" },
|
|
227
272
|
);
|
|
@@ -541,22 +586,26 @@ app.post(
|
|
|
541
586
|
|
|
542
587
|
### Application
|
|
543
588
|
|
|
544
|
-
| Method | Description
|
|
545
|
-
| :----------------------------------------------- |
|
|
546
|
-
| `
|
|
547
|
-
| `app.
|
|
548
|
-
| `app.
|
|
549
|
-
| `app.
|
|
550
|
-
| `app.
|
|
551
|
-
| `app.
|
|
552
|
-
| `app.
|
|
553
|
-
| `app.
|
|
554
|
-
| `app.
|
|
589
|
+
| Method | Description |
|
|
590
|
+
| :----------------------------------------------- | :--------------------- |
|
|
591
|
+
| `vibe({ logger?: LoggerConfig })` | Initialize app |
|
|
592
|
+
| `app.setErrorHandler(fn)` | Override error handler |
|
|
593
|
+
| `app.get/post/put/del/patch/head(path, handler)` | Register route |
|
|
594
|
+
| `app.listen(port, host?, callback?)` | Start server |
|
|
595
|
+
| `app.register(fn, { prefix })` | Register plugin |
|
|
596
|
+
| `app.plugin(fn)` | Global interceptor |
|
|
597
|
+
| `app.decorate(name, value)` | Add app property |
|
|
598
|
+
| `app.decorateRequest(name, value)` | Add to all requests |
|
|
599
|
+
| `app.decorateReply(name, value)` | Add to all responses |
|
|
600
|
+
| `app.setPublicFolder(path)` | Set static folder |
|
|
601
|
+
| `app.logRoutes()` | Log all routes |
|
|
555
602
|
|
|
556
603
|
### Request (`req`)
|
|
557
604
|
|
|
558
605
|
| Property | Description |
|
|
559
606
|
| :------------ | :------------------------- |
|
|
607
|
+
| `req.id` | Auto-generated UUID logic |
|
|
608
|
+
| `req.log` | Context-bound logger API |
|
|
560
609
|
| `req.params` | Route parameters (`:id`) |
|
|
561
610
|
| `req.query` | Query string (`?page=1`) |
|
|
562
611
|
| `req.body` | Parsed JSON/form body |
|
package/package.json
CHANGED
package/utils/core/handler.js
CHANGED
|
@@ -142,11 +142,16 @@ export function handleError(error, req, res) {
|
|
|
142
142
|
const isDev = process.env.NODE_ENV !== "production";
|
|
143
143
|
const message = error.message || "Unknown error";
|
|
144
144
|
|
|
145
|
-
// Log error
|
|
146
|
-
if (
|
|
147
|
-
|
|
145
|
+
// Log error using context-aware structured logger if available
|
|
146
|
+
if (req && req.log) {
|
|
147
|
+
req.log.error(error);
|
|
148
148
|
} else {
|
|
149
|
-
|
|
149
|
+
// Fallback: full stack in dev, message only in production
|
|
150
|
+
if (isDev) {
|
|
151
|
+
console.error("[VIBE ERROR]:", error);
|
|
152
|
+
} else {
|
|
153
|
+
console.error("[VIBE ERROR]:", message);
|
|
154
|
+
}
|
|
150
155
|
}
|
|
151
156
|
|
|
152
157
|
if (!res.headersSent) {
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import os from "os";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { color } from "../helpers/colors.js";
|
|
4
|
+
|
|
5
|
+
const LOG_LEVELS = {
|
|
6
|
+
trace: 10,
|
|
7
|
+
debug: 20,
|
|
8
|
+
info: 30,
|
|
9
|
+
warn: 40,
|
|
10
|
+
error: 50,
|
|
11
|
+
fatal: 60,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const LEVEL_NAMES = {
|
|
15
|
+
10: "TRACE",
|
|
16
|
+
20: "DEBUG",
|
|
17
|
+
30: "INFO",
|
|
18
|
+
40: "WARN",
|
|
19
|
+
50: "ERROR",
|
|
20
|
+
60: "FATAL",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* High-performance structured JSON logger (Fastify/Pino style).
|
|
25
|
+
*/
|
|
26
|
+
export class Logger {
|
|
27
|
+
constructor(options = {}) {
|
|
28
|
+
this.level = LOG_LEVELS[options.level || "info"] || 30;
|
|
29
|
+
this.colors = options.colors !== undefined ? options.colors : true;
|
|
30
|
+
this.prettyPrint =
|
|
31
|
+
options.prettyPrint !== undefined ? options.prettyPrint : this.colors;
|
|
32
|
+
this.lifecycle = options.lifecycle || false;
|
|
33
|
+
this.stream = options.stream || process.stdout;
|
|
34
|
+
this.dest = options.dest || "console"; // "console", "file", "both"
|
|
35
|
+
this.logFile = options.logFile;
|
|
36
|
+
this.bindings = options.bindings || {};
|
|
37
|
+
|
|
38
|
+
// Initialize file stream if needed
|
|
39
|
+
if (this.logFile && (this.dest === "file" || this.dest === "both")) {
|
|
40
|
+
this.fileStream = fs.createWriteStream(this.logFile, { flags: "a" });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!this.bindings.pid) this.bindings.pid = process.pid;
|
|
44
|
+
if (!this.bindings.hostname) this.bindings.hostname = os.hostname();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Creates a sub-logger with scoped bindings (e.g. reqId).
|
|
49
|
+
*/
|
|
50
|
+
child(bindings) {
|
|
51
|
+
return new Logger({
|
|
52
|
+
level: Object.keys(LOG_LEVELS).find(
|
|
53
|
+
(key) => LOG_LEVELS[key] === this.level,
|
|
54
|
+
),
|
|
55
|
+
colors: this.colors,
|
|
56
|
+
prettyPrint: this.prettyPrint,
|
|
57
|
+
lifecycle: this.lifecycle,
|
|
58
|
+
stream: this.stream,
|
|
59
|
+
dest: this.dest,
|
|
60
|
+
logFile: this.logFile,
|
|
61
|
+
bindings: { ...this.bindings, ...bindings },
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
trace(obj, msg, c) {
|
|
66
|
+
this._log(10, obj, msg, c);
|
|
67
|
+
}
|
|
68
|
+
debug(obj, msg, c) {
|
|
69
|
+
this._log(20, obj, msg, c);
|
|
70
|
+
}
|
|
71
|
+
info(obj, msg, c) {
|
|
72
|
+
this._log(30, obj, msg, c);
|
|
73
|
+
}
|
|
74
|
+
warn(obj, msg, c) {
|
|
75
|
+
this._log(40, obj, msg, c);
|
|
76
|
+
}
|
|
77
|
+
error(obj, msg, c) {
|
|
78
|
+
this._log(50, obj, msg, c);
|
|
79
|
+
}
|
|
80
|
+
fatal(obj, msg, c) {
|
|
81
|
+
this._log(60, obj, msg, c);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
_log(level, obj, msg, c) {
|
|
85
|
+
if (level < this.level) return;
|
|
86
|
+
|
|
87
|
+
const base = {
|
|
88
|
+
level,
|
|
89
|
+
time: Date.now(),
|
|
90
|
+
...this.bindings,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
let logData = {};
|
|
94
|
+
let customColor = undefined;
|
|
95
|
+
|
|
96
|
+
if (obj instanceof Error) {
|
|
97
|
+
logData.err = {
|
|
98
|
+
type: obj.name || "Error",
|
|
99
|
+
message: obj.message,
|
|
100
|
+
stack: obj.stack,
|
|
101
|
+
};
|
|
102
|
+
if (typeof msg === "string") logData.msg = msg;
|
|
103
|
+
else logData.msg = obj.message;
|
|
104
|
+
if (typeof c === "string") customColor = c;
|
|
105
|
+
} else if (typeof obj === "string") {
|
|
106
|
+
logData.msg = obj;
|
|
107
|
+
if (typeof msg === "string") customColor = msg;
|
|
108
|
+
} else if (typeof obj === "object" && obj !== null) {
|
|
109
|
+
logData = { ...obj };
|
|
110
|
+
if (typeof msg === "string") logData.msg = msg;
|
|
111
|
+
if (typeof c === "string") customColor = c;
|
|
112
|
+
} else {
|
|
113
|
+
logData.msg = String(obj);
|
|
114
|
+
if (typeof msg === "string") customColor = msg;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (customColor) {
|
|
118
|
+
logData.color = customColor;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const finalLog = { ...base, ...logData };
|
|
122
|
+
|
|
123
|
+
if (this.dest === "console" || this.dest === "both") {
|
|
124
|
+
if (this.prettyPrint) {
|
|
125
|
+
this._printPretty(finalLog);
|
|
126
|
+
} else {
|
|
127
|
+
this.stream.write(JSON.stringify(finalLog) + "\n");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if ((this.dest === "file" || this.dest === "both") && this.fileStream) {
|
|
132
|
+
this.fileStream.write(JSON.stringify(finalLog) + "\n");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
_printPretty(log) {
|
|
137
|
+
const time = new Date(log.time).toLocaleTimeString();
|
|
138
|
+
const lvlName = LEVEL_NAMES[log.level] || "INFO";
|
|
139
|
+
let prefixC = color.cyan;
|
|
140
|
+
if (log.level >= 50) prefixC = color.red;
|
|
141
|
+
else if (log.level === 40) prefixC = color.yellow;
|
|
142
|
+
else if (log.level <= 20) prefixC = color.dim;
|
|
143
|
+
|
|
144
|
+
const prefix = prefixC(`[VIBE ${lvlName} ${time}]`);
|
|
145
|
+
let context = "";
|
|
146
|
+
if (log.reqId) {
|
|
147
|
+
context = `\x1b[90m[${log.reqId}]\x1b[0m `;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
let content = log.msg || "";
|
|
151
|
+
if (log.color && color[log.color]) {
|
|
152
|
+
content = color[log.color](content);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (log.err && log.err.stack) {
|
|
156
|
+
content += "\n" + prefixC(log.err.stack);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Attempt to print remaining metadata if it's not standard
|
|
160
|
+
const skipKeys = [
|
|
161
|
+
"level",
|
|
162
|
+
"time",
|
|
163
|
+
"pid",
|
|
164
|
+
"hostname",
|
|
165
|
+
"reqId",
|
|
166
|
+
"msg",
|
|
167
|
+
"err",
|
|
168
|
+
"color",
|
|
169
|
+
];
|
|
170
|
+
let metaStr = "";
|
|
171
|
+
for (const key of Object.keys(log)) {
|
|
172
|
+
if (!skipKeys.includes(key)) {
|
|
173
|
+
metaStr += ` \x1b[90m${key}=${JSON.stringify(log[key])}\x1b[0m`;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
this.stream.write(`${prefix} ${context}${content}${metaStr}\n`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function createLogger(options = {}) {
|
|
182
|
+
return new Logger(options);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export default createLogger;
|
package/utils/core/response.js
CHANGED
|
@@ -35,6 +35,10 @@ const vibeResponseMethods = {
|
|
|
35
35
|
throw new Error("Response data is not a sendable data type");
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
if (data instanceof Error) {
|
|
39
|
+
return this._vibeOptions.errorHandler(data, this.req, this);
|
|
40
|
+
}
|
|
41
|
+
|
|
38
42
|
if (typeof data === "object" && data !== null) {
|
|
39
43
|
if (!this.headersSent) this.writeHead(this.statusCode || 200, JSON_CT);
|
|
40
44
|
this.end(JSON.stringify(data));
|
|
@@ -50,6 +54,9 @@ const vibeResponseMethods = {
|
|
|
50
54
|
* @param {Object} data
|
|
51
55
|
*/
|
|
52
56
|
json(data) {
|
|
57
|
+
if (data instanceof Error) {
|
|
58
|
+
return this._vibeOptions.errorHandler(data, this.req, this);
|
|
59
|
+
}
|
|
53
60
|
if (!this.headersSent) this.writeHead(this.statusCode || 200, JSON_CT);
|
|
54
61
|
this.end(JSON.stringify(data));
|
|
55
62
|
},
|
package/utils/core/server.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import http from "http";
|
|
2
|
+
import crypto from "crypto";
|
|
2
3
|
import { error, getNetworkIP, handleError, isSendAble } from "./handler.js";
|
|
3
4
|
import bodyParser from "./parser.js";
|
|
4
5
|
import { installResponseMethods, initResponse } from "./response.js";
|
|
5
|
-
import dns from "node:dns/promises";
|
|
6
6
|
import { parseQuery } from "../native.js";
|
|
7
7
|
|
|
8
8
|
// Pre-allocated headers (frozen for V8 optimization)
|
|
@@ -90,6 +90,33 @@ async function server(options, port, host, callback) {
|
|
|
90
90
|
|
|
91
91
|
// Main request handler - ULTRA OPTIMIZED
|
|
92
92
|
function reqListener(req, res) {
|
|
93
|
+
req.id = crypto.randomUUID();
|
|
94
|
+
req.log = options.logger.child({ reqId: req.id });
|
|
95
|
+
|
|
96
|
+
if (options.loggerConfig && options.loggerConfig.lifecycle) {
|
|
97
|
+
req.startTime = Date.now();
|
|
98
|
+
|
|
99
|
+
// Determine sender IP early for logging
|
|
100
|
+
const sender =
|
|
101
|
+
req.socket.remoteAddress || req.headers["x-forwarded-for"] || "unknown";
|
|
102
|
+
|
|
103
|
+
req.log.info(
|
|
104
|
+
{ type: "req", url: req.url, method: req.method, sender },
|
|
105
|
+
"Incoming request",
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
res.on("finish", () => {
|
|
109
|
+
req.log.info(
|
|
110
|
+
{
|
|
111
|
+
type: "res",
|
|
112
|
+
statusCode: res.statusCode,
|
|
113
|
+
responseTimeMs: Date.now() - req.startTime,
|
|
114
|
+
},
|
|
115
|
+
"Request completed",
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
93
120
|
// Fast pathname extraction
|
|
94
121
|
const url = req.url;
|
|
95
122
|
const qIdx = url.indexOf("?");
|
|
@@ -151,13 +178,19 @@ async function server(options, port, host, callback) {
|
|
|
151
178
|
if (result && typeof result.then === "function") {
|
|
152
179
|
result
|
|
153
180
|
.then((val) => {
|
|
181
|
+
if (val instanceof Error) {
|
|
182
|
+
return options.errorHandler(val, req, res);
|
|
183
|
+
}
|
|
154
184
|
if (val !== undefined && !res.writableEnded) {
|
|
155
185
|
res.writeHead(200, JSON_HEADERS);
|
|
156
186
|
res.end(serialize ? serialize(val) : JSON.stringify(val));
|
|
157
187
|
}
|
|
158
188
|
})
|
|
159
|
-
.catch((err) =>
|
|
189
|
+
.catch((err) => options.errorHandler(err, req, res));
|
|
160
190
|
} else if (typeof result === "object" && result !== null) {
|
|
191
|
+
if (result instanceof Error) {
|
|
192
|
+
return options.errorHandler(result, req, res);
|
|
193
|
+
}
|
|
161
194
|
res.writeHead(200, JSON_HEADERS);
|
|
162
195
|
res.end(serialize ? serialize(result) : JSON.stringify(result));
|
|
163
196
|
} else {
|
|
@@ -166,7 +199,7 @@ async function server(options, port, host, callback) {
|
|
|
166
199
|
}
|
|
167
200
|
}
|
|
168
201
|
} catch (err) {
|
|
169
|
-
|
|
202
|
+
options.errorHandler(err, req, res);
|
|
170
203
|
}
|
|
171
204
|
return;
|
|
172
205
|
}
|
|
@@ -226,6 +259,9 @@ async function server(options, port, host, callback) {
|
|
|
226
259
|
// Execute handler
|
|
227
260
|
if (typeof handler === "function") {
|
|
228
261
|
const result = await handler(req, res);
|
|
262
|
+
if (result instanceof Error) {
|
|
263
|
+
return options.errorHandler(result, req, res);
|
|
264
|
+
}
|
|
229
265
|
if (result !== undefined && !res.writableEnded) {
|
|
230
266
|
if (serialize) {
|
|
231
267
|
// Pre-compiled schema serializer — fastest path
|
|
@@ -247,7 +283,7 @@ async function server(options, port, host, callback) {
|
|
|
247
283
|
throw new Error("Invalid handler type");
|
|
248
284
|
}
|
|
249
285
|
} catch (err) {
|
|
250
|
-
|
|
286
|
+
options.errorHandler(err, req, res);
|
|
251
287
|
}
|
|
252
288
|
}
|
|
253
289
|
|
|
@@ -256,10 +292,7 @@ async function server(options, port, host, callback) {
|
|
|
256
292
|
|
|
257
293
|
const vibe_server = http.createServer(reqListener);
|
|
258
294
|
|
|
259
|
-
vibe_server.listen(port, mainHost,
|
|
260
|
-
try {
|
|
261
|
-
await dns.lookup("::", { all: true });
|
|
262
|
-
} catch {}
|
|
295
|
+
vibe_server.listen(port, mainHost, () => {
|
|
263
296
|
getNetworkIP(mainHost, port);
|
|
264
297
|
|
|
265
298
|
const strategy = useTrieMatching ? "Trie (O(log n))" : "Linear (O(n))";
|
|
@@ -271,7 +304,29 @@ async function server(options, port, host, callback) {
|
|
|
271
304
|
});
|
|
272
305
|
|
|
273
306
|
vibe_server.on("error", (err) => {
|
|
274
|
-
|
|
307
|
+
if (err.code === "EADDRINUSE") {
|
|
308
|
+
error(`Port ${port} is already in use! \n${err.message}`);
|
|
309
|
+
process.exit(1);
|
|
310
|
+
} else {
|
|
311
|
+
error(`Server error: \n${err.message}`);
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// Graceful shutdown support for node --watch, nodemon, and cluster mode
|
|
316
|
+
const shutdown = () => {
|
|
317
|
+
// vibe_server.close stops accepting new connections
|
|
318
|
+
// Existing keep-alive connections will still prevent instant exit,
|
|
319
|
+
// so we force an exit if it takes longer than 3 seconds.
|
|
320
|
+
vibe_server.close(() => {
|
|
321
|
+
process.exit(0);
|
|
322
|
+
});
|
|
323
|
+
setTimeout(() => process.exit(0), 3000).unref();
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
process.on("SIGTERM", shutdown);
|
|
327
|
+
process.on("SIGINT", shutdown);
|
|
328
|
+
process.on("message", (msg) => {
|
|
329
|
+
if (msg === "shutdown") shutdown();
|
|
275
330
|
});
|
|
276
331
|
}
|
|
277
332
|
|
package/utils/scaling/cache.js
CHANGED
|
@@ -145,7 +145,17 @@ export class LRUCache {
|
|
|
145
145
|
*/
|
|
146
146
|
export function cacheMiddleware(cache) {
|
|
147
147
|
return (req, res) => {
|
|
148
|
-
|
|
148
|
+
// Use the full original URL (includes query string) for the cache key.
|
|
149
|
+
// req.url is overwritten with just the pathname by the server internals,
|
|
150
|
+
// so we fall back to req._rawUrl which preserves the full URL.
|
|
151
|
+
// We also append serialized route params so that parameterised routes
|
|
152
|
+
// (e.g. /users/:id) with different param values get distinct cache entries.
|
|
153
|
+
const rawUrl = req._rawUrl || req.url;
|
|
154
|
+
const paramsStr =
|
|
155
|
+
req.params && Object.keys(req.params).length > 0
|
|
156
|
+
? JSON.stringify(req.params)
|
|
157
|
+
: "";
|
|
158
|
+
const key = LRUCache.key(req.method, rawUrl + paramsStr);
|
|
149
159
|
const entry = cache.get(key);
|
|
150
160
|
|
|
151
161
|
if (entry) {
|
|
@@ -164,16 +174,45 @@ export function cacheMiddleware(cache) {
|
|
|
164
174
|
return false; // Stop execution
|
|
165
175
|
}
|
|
166
176
|
|
|
167
|
-
// Store original json
|
|
177
|
+
// Store original json and end methods to intercept response
|
|
168
178
|
const originalJson = res.json.bind(res);
|
|
179
|
+
const originalEnd = res.end.bind(res);
|
|
180
|
+
|
|
181
|
+
// Intercept res.json (explicit json calls by handler)
|
|
169
182
|
res.json = (data) => {
|
|
170
|
-
// Cache the response
|
|
171
183
|
const newEntry = cache.set(key, data);
|
|
172
184
|
res.setHeader("ETag", newEntry.etag);
|
|
173
185
|
res.setHeader("X-Cache", "MISS");
|
|
174
186
|
originalJson(data);
|
|
175
187
|
};
|
|
176
188
|
|
|
189
|
+
// Intercept res.end (implicit return-value path in server.js uses
|
|
190
|
+
// res.writeHead + res.end directly, bypassing res.json).
|
|
191
|
+
// Note: res.getHeader() does NOT see headers set via res.writeHead(),
|
|
192
|
+
// so we can't check Content-Type that way. Instead, try JSON.parse directly.
|
|
193
|
+
res.end = (body) => {
|
|
194
|
+
if (body && !res._vibeCached) {
|
|
195
|
+
try {
|
|
196
|
+
const parsed = JSON.parse(body);
|
|
197
|
+
// Only cache plain objects/arrays — not error objects, not primitives
|
|
198
|
+
if (
|
|
199
|
+
typeof parsed === "object" &&
|
|
200
|
+
parsed !== null &&
|
|
201
|
+
!parsed.error // skip error responses
|
|
202
|
+
) {
|
|
203
|
+
res._vibeCached = true;
|
|
204
|
+
const newEntry = cache.set(key, parsed);
|
|
205
|
+
// setHeader is safe here — headers not yet flushed
|
|
206
|
+
res.setHeader("ETag", newEntry.etag);
|
|
207
|
+
res.setHeader("X-Cache", "MISS");
|
|
208
|
+
}
|
|
209
|
+
} catch {
|
|
210
|
+
// Not JSON — skip caching
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
originalEnd(body);
|
|
214
|
+
};
|
|
215
|
+
|
|
177
216
|
return true; // Continue to handler
|
|
178
217
|
};
|
|
179
218
|
}
|
package/vibe.d.ts
CHANGED
|
@@ -172,6 +172,57 @@ export interface RegisterOptions {
|
|
|
172
172
|
[key: string]: any;
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
// ==========================================
|
|
176
|
+
// Logging System
|
|
177
|
+
// ==========================================
|
|
178
|
+
|
|
179
|
+
export interface LoggerConfig {
|
|
180
|
+
/** If true, automatically logs request lifecycle hooks (Incoming Request, Request Completed) */
|
|
181
|
+
lifecycle?: boolean;
|
|
182
|
+
/** If true, formats JSON output into human-readable Vibe-styled terminal lines (like pino-pretty) */
|
|
183
|
+
prettyPrint?: boolean;
|
|
184
|
+
/** If true, applies ANSI color formatting to terminal logs. Default: true (unless overridden) */
|
|
185
|
+
colors?: boolean;
|
|
186
|
+
/** Destination for the logs. Accepts "console", "file", or "both". Default: "console" */
|
|
187
|
+
dest?: "console" | "file" | "both";
|
|
188
|
+
/** Absolute or relative path to the target log file. Active when dest is "file" or "both" */
|
|
189
|
+
logFile?: string;
|
|
190
|
+
/** Custom writable stream to output logs to (defaults to process.stdout) */
|
|
191
|
+
stream?: NodeJS.WritableStream;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Fastify/Pino-compatible structured logger API.
|
|
196
|
+
*
|
|
197
|
+
* All methods accept a message string OR a structured object (Pino-style).
|
|
198
|
+
* An optional color string can be passed as the last argument — in prettyPrint
|
|
199
|
+
* mode it will colorize the terminal output, in production mode it writes as
|
|
200
|
+
* a plain JSON `{ color: "..." }` key for log pipelines.
|
|
201
|
+
*
|
|
202
|
+
* @example
|
|
203
|
+
* req.log.info("Processing payment");
|
|
204
|
+
* req.log.info({ userId: 42, amount: 100 }, "Payment initiated");
|
|
205
|
+
* req.log.error(new Error("DB timeout"));
|
|
206
|
+
* req.log.warn("Slow query detected", "yellow"); // color override
|
|
207
|
+
*/
|
|
208
|
+
export interface LoggerAPI {
|
|
209
|
+
trace(obj: object | string | Error, msg?: string, color?: ColorName): void;
|
|
210
|
+
debug(obj: object | string | Error, msg?: string, color?: ColorName): void;
|
|
211
|
+
info(obj: object | string | Error, msg?: string, color?: ColorName): void;
|
|
212
|
+
warn(obj: object | string | Error, msg?: string, color?: ColorName): void;
|
|
213
|
+
error(obj: object | string | Error, msg?: string, color?: ColorName): void;
|
|
214
|
+
fatal(obj: object | string | Error, msg?: string, color?: ColorName): void;
|
|
215
|
+
/** Returns a child logger with merged bindings (e.g., { reqId }) */
|
|
216
|
+
child(bindings: Record<string, any>): LoggerAPI;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export interface VibeConfig {
|
|
220
|
+
/** Configuration for the native Vibe terminal logger */
|
|
221
|
+
logger?: LoggerConfig | boolean;
|
|
222
|
+
/** Enable automatic process restarting on crash. Spawns an internal cluster manager. Default: false */
|
|
223
|
+
autoRestart?: boolean;
|
|
224
|
+
}
|
|
225
|
+
|
|
175
226
|
// ==========================================
|
|
176
227
|
// Request & Response Extensions
|
|
177
228
|
// ==========================================
|
|
@@ -192,6 +243,10 @@ export interface VibeRequest extends IncomingMessage {
|
|
|
192
243
|
ip?: string;
|
|
193
244
|
/** Detailed client IP info */
|
|
194
245
|
fullIp?: string;
|
|
246
|
+
/** Automatically generated UUID for the request lifecycle */
|
|
247
|
+
id: string;
|
|
248
|
+
/** Context-bound logger automatically stamped with the req.id constraint */
|
|
249
|
+
log: LoggerAPI;
|
|
195
250
|
/** Custom properties added via decorateRequest */
|
|
196
251
|
[key: string]: any;
|
|
197
252
|
}
|
|
@@ -338,11 +393,14 @@ export interface RouterAPI {
|
|
|
338
393
|
head: RouteRegistrar;
|
|
339
394
|
|
|
340
395
|
/**
|
|
341
|
-
* Log helper
|
|
342
|
-
* @param value The message to log
|
|
343
|
-
* @param
|
|
396
|
+
* Log helper supporting native colors and Vibe-stylized log levels
|
|
397
|
+
* @param value The message or object to log
|
|
398
|
+
* @param typeOrColor Optional color name (e.g. 'green') or level ('info', 'warn', 'error', 'req')
|
|
344
399
|
*/
|
|
345
|
-
log: (
|
|
400
|
+
log: (
|
|
401
|
+
value: any,
|
|
402
|
+
typeOrColor?: ColorName | "info" | "error" | "warn" | "req",
|
|
403
|
+
) => void;
|
|
346
404
|
|
|
347
405
|
/** Register a global interceptor */
|
|
348
406
|
plugin: (interceptor: Interceptor) => void;
|
|
@@ -401,15 +459,44 @@ export interface VibeApp extends RouterAPI {
|
|
|
401
459
|
maybeFunc?: (router: RouterAPI) => void,
|
|
402
460
|
) => void;
|
|
403
461
|
|
|
404
|
-
/**
|
|
462
|
+
/**
|
|
463
|
+
* Access app decorators
|
|
464
|
+
* @type {Record<string, any>}
|
|
465
|
+
*/
|
|
405
466
|
readonly decorators: Record<string, any>;
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Override the default error handler (Fastify-style).
|
|
470
|
+
* Called for any unhandled `throw`, `return new Error()`, or `res.send(error)`.
|
|
471
|
+
* @example
|
|
472
|
+
* app.setErrorHandler((error, req, res) => {
|
|
473
|
+
* req.log.error(error);
|
|
474
|
+
* res.status(503).json({ success: false, message: error.message });
|
|
475
|
+
* });
|
|
476
|
+
*/
|
|
477
|
+
setErrorHandler(
|
|
478
|
+
fn: (error: Error, req: VibeRequest, res: VibeResponse) => void,
|
|
479
|
+
): void;
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Pino/Fastify-compatible structured logger instance.
|
|
483
|
+
* Use for application-level logging outside of routes.
|
|
484
|
+
* @example
|
|
485
|
+
* app.log.info({ db: "connected" }, "Server ready");
|
|
486
|
+
* app.log.info("Server ready", "green"); // with color in prettyPrint mode
|
|
487
|
+
*/
|
|
488
|
+
log: LoggerAPI;
|
|
489
|
+
|
|
490
|
+
/** Alias for `app.log` */
|
|
491
|
+
logger: LoggerAPI;
|
|
406
492
|
}
|
|
407
493
|
|
|
408
494
|
/**
|
|
409
495
|
* Initialize a new Vibe application.
|
|
496
|
+
* @param config Optional application configuration
|
|
410
497
|
* @returns Vibe application instance
|
|
411
498
|
*/
|
|
412
|
-
export default function vibe(): VibeApp;
|
|
499
|
+
export default function vibe(config?: VibeConfig): VibeApp;
|
|
413
500
|
|
|
414
501
|
// ==========================================
|
|
415
502
|
// LRU Cache
|
package/vibe.js
CHANGED
|
@@ -4,6 +4,9 @@ import { color } from "./utils/helpers/colors.js";
|
|
|
4
4
|
import { RouteTrie } from "./utils/core/trie.js";
|
|
5
5
|
import { PathToRegex } from "./utils/core/handler.js";
|
|
6
6
|
import { compileSerializer } from "./utils/core/compile-serializer.js";
|
|
7
|
+
import { createLogger, Logger } from "./utils/core/logger.js";
|
|
8
|
+
import { handleError } from "./utils/core/handler.js";
|
|
9
|
+
import { clusterize } from "./utils/scaling/cluster.js";
|
|
7
10
|
|
|
8
11
|
/**
|
|
9
12
|
* Helper to generate regex for a path
|
|
@@ -129,9 +132,12 @@ function pathToRegex(path) {
|
|
|
129
132
|
|
|
130
133
|
/**
|
|
131
134
|
* Initializes a Vibe application instance.
|
|
135
|
+
* @param {Object} [config={}]
|
|
136
|
+
* @param {Object|boolean} [config.logger] - Logger configuration
|
|
137
|
+
* @param {boolean} [config.autoRestart] - Restart server automatically on crash
|
|
132
138
|
* @returns {VibeApp}
|
|
133
139
|
*/
|
|
134
|
-
const vibe = () => {
|
|
140
|
+
const vibe = (config = {}) => {
|
|
135
141
|
// Route trie for O(log n) matching (used when routes > threshold)
|
|
136
142
|
const trie = new RouteTrie();
|
|
137
143
|
|
|
@@ -144,6 +150,14 @@ const vibe = () => {
|
|
|
144
150
|
// Static routes Map for O(1) lookup (routes without params)
|
|
145
151
|
const staticRoutes = new Map();
|
|
146
152
|
|
|
153
|
+
// Logger initialization
|
|
154
|
+
const loggerConfig =
|
|
155
|
+
config.logger !== false ? config.logger || {} : { level: "silent" };
|
|
156
|
+
const appLogger =
|
|
157
|
+
config.logger instanceof Logger
|
|
158
|
+
? config.logger
|
|
159
|
+
: createLogger(loggerConfig);
|
|
160
|
+
|
|
147
161
|
// Internal configuration
|
|
148
162
|
const options = {
|
|
149
163
|
trie,
|
|
@@ -156,8 +170,27 @@ const vibe = () => {
|
|
|
156
170
|
decorators: {},
|
|
157
171
|
requestDecorators: {},
|
|
158
172
|
replyDecorators: {},
|
|
173
|
+
logger: appLogger,
|
|
174
|
+
loggerConfig,
|
|
175
|
+
errorHandler: handleError,
|
|
159
176
|
};
|
|
160
177
|
|
|
178
|
+
// Add global uncaught exception handler to prevent silent deaths and log cleanly
|
|
179
|
+
process.on("uncaughtException", (err) => {
|
|
180
|
+
appLogger.fatal(err, "Uncaught Exception crashed the server");
|
|
181
|
+
// Only exit here if we're not exiting gracefully anyway,
|
|
182
|
+
// cluster manager will restart it.
|
|
183
|
+
setTimeout(() => process.exit(1), 100);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
process.on("unhandledRejection", (reason, promise) => {
|
|
187
|
+
appLogger.fatal(
|
|
188
|
+
{ err: reason },
|
|
189
|
+
"Unhandled Promise Rejection crashed the server",
|
|
190
|
+
);
|
|
191
|
+
setTimeout(() => process.exit(1), 100);
|
|
192
|
+
});
|
|
193
|
+
|
|
161
194
|
// Register default landing route
|
|
162
195
|
const defaultRoute = {
|
|
163
196
|
method: "GET",
|
|
@@ -362,7 +395,13 @@ const vibe = () => {
|
|
|
362
395
|
host = undefined;
|
|
363
396
|
}
|
|
364
397
|
|
|
365
|
-
server(options, Number(port), host, callback);
|
|
398
|
+
const startServer = () => server(options, Number(port), host, callback);
|
|
399
|
+
|
|
400
|
+
if (config.autoRestart) {
|
|
401
|
+
clusterize(startServer, { workers: 1, restart: true });
|
|
402
|
+
} else {
|
|
403
|
+
startServer();
|
|
404
|
+
}
|
|
366
405
|
}
|
|
367
406
|
|
|
368
407
|
/**
|
|
@@ -460,6 +499,8 @@ const vibe = () => {
|
|
|
460
499
|
throw new Error(`Decorator '${name}' already exists`);
|
|
461
500
|
}
|
|
462
501
|
options.decorators[name] = value;
|
|
502
|
+
// Also set directly on the app object for easy access (app.name)
|
|
503
|
+
if (app) app[name] = value;
|
|
463
504
|
}
|
|
464
505
|
|
|
465
506
|
/**
|
|
@@ -532,12 +573,13 @@ const vibe = () => {
|
|
|
532
573
|
}
|
|
533
574
|
|
|
534
575
|
/**
|
|
535
|
-
*
|
|
536
|
-
*
|
|
537
|
-
* @param {string} [colorValue="reset"]
|
|
576
|
+
* Log messages out using the Vibe stylized legacy logger.
|
|
577
|
+
* Native string logging bypasses the Pino JSON interface.
|
|
538
578
|
*/
|
|
539
|
-
const log = (message,
|
|
540
|
-
|
|
579
|
+
const log = (message, typeOrColor = "reset") => {
|
|
580
|
+
const c = color[typeOrColor] || color.reset;
|
|
581
|
+
process.stdout.write(c(message) + "\n");
|
|
582
|
+
};
|
|
541
583
|
|
|
542
584
|
// Build the app object with decorators
|
|
543
585
|
const app = {
|
|
@@ -549,8 +591,13 @@ const vibe = () => {
|
|
|
549
591
|
head,
|
|
550
592
|
listen,
|
|
551
593
|
logRoutes,
|
|
552
|
-
log,
|
|
594
|
+
log: appLogger, // Standard Fastify-like exposure (app.log.info())
|
|
595
|
+
logger: appLogger,
|
|
596
|
+
logLegacy: log,
|
|
553
597
|
setPublicFolder,
|
|
598
|
+
setErrorHandler: (fn) => {
|
|
599
|
+
options.errorHandler = fn;
|
|
600
|
+
},
|
|
554
601
|
include,
|
|
555
602
|
plugin,
|
|
556
603
|
register,
|