vibe-gx 4.0.0 → 4.1.1
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 +67 -18
- package/package.json +1 -1
- package/utils/core/handler.js +9 -4
- package/utils/core/logger.js +166 -0
- package/utils/core/response.js +7 -0
- package/utils/core/server.js +56 -9
- package/utils/scaling/cache.js +42 -3
- package/vibe.d.ts +115 -6
- package/vibe.js +31 -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
|
);
|
|
@@ -428,7 +473,7 @@ process.on("SIGTERM", () => dbPool.close());
|
|
|
428
473
|
Use any Express middleware with the adapter:
|
|
429
474
|
|
|
430
475
|
```javascript
|
|
431
|
-
import { adapt } from "vibe-gx
|
|
476
|
+
import vibe, { adapt } from "vibe-gx";
|
|
432
477
|
import cors from "cors";
|
|
433
478
|
import helmet from "helmet";
|
|
434
479
|
import compression from "compression";
|
|
@@ -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,166 @@
|
|
|
1
|
+
import os from "os";
|
|
2
|
+
import { color } from "../helpers/colors.js";
|
|
3
|
+
|
|
4
|
+
const LOG_LEVELS = {
|
|
5
|
+
trace: 10,
|
|
6
|
+
debug: 20,
|
|
7
|
+
info: 30,
|
|
8
|
+
warn: 40,
|
|
9
|
+
error: 50,
|
|
10
|
+
fatal: 60,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const LEVEL_NAMES = {
|
|
14
|
+
10: "TRACE",
|
|
15
|
+
20: "DEBUG",
|
|
16
|
+
30: "INFO",
|
|
17
|
+
40: "WARN",
|
|
18
|
+
50: "ERROR",
|
|
19
|
+
60: "FATAL",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* High-performance structured JSON logger (Fastify/Pino style).
|
|
24
|
+
*/
|
|
25
|
+
export class Logger {
|
|
26
|
+
constructor(options = {}) {
|
|
27
|
+
this.level = LOG_LEVELS[options.level || "info"] || 30;
|
|
28
|
+
this.prettyPrint = options.prettyPrint || false;
|
|
29
|
+
this.lifecycle = options.lifecycle || false;
|
|
30
|
+
this.stream = options.stream || process.stdout;
|
|
31
|
+
this.bindings = options.bindings || {};
|
|
32
|
+
|
|
33
|
+
if (!this.bindings.pid) this.bindings.pid = process.pid;
|
|
34
|
+
if (!this.bindings.hostname) this.bindings.hostname = os.hostname();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Creates a sub-logger with scoped bindings (e.g. reqId).
|
|
39
|
+
*/
|
|
40
|
+
child(bindings) {
|
|
41
|
+
return new Logger({
|
|
42
|
+
level: Object.keys(LOG_LEVELS).find(
|
|
43
|
+
(key) => LOG_LEVELS[key] === this.level,
|
|
44
|
+
),
|
|
45
|
+
prettyPrint: this.prettyPrint,
|
|
46
|
+
lifecycle: this.lifecycle,
|
|
47
|
+
stream: this.stream,
|
|
48
|
+
bindings: { ...this.bindings, ...bindings },
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
trace(obj, msg, c) {
|
|
53
|
+
this._log(10, obj, msg, c);
|
|
54
|
+
}
|
|
55
|
+
debug(obj, msg, c) {
|
|
56
|
+
this._log(20, obj, msg, c);
|
|
57
|
+
}
|
|
58
|
+
info(obj, msg, c) {
|
|
59
|
+
this._log(30, obj, msg, c);
|
|
60
|
+
}
|
|
61
|
+
warn(obj, msg, c) {
|
|
62
|
+
this._log(40, obj, msg, c);
|
|
63
|
+
}
|
|
64
|
+
error(obj, msg, c) {
|
|
65
|
+
this._log(50, obj, msg, c);
|
|
66
|
+
}
|
|
67
|
+
fatal(obj, msg, c) {
|
|
68
|
+
this._log(60, obj, msg, c);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
_log(level, obj, msg, c) {
|
|
72
|
+
if (level < this.level) return;
|
|
73
|
+
|
|
74
|
+
const base = {
|
|
75
|
+
level,
|
|
76
|
+
time: Date.now(),
|
|
77
|
+
...this.bindings,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
let logData = {};
|
|
81
|
+
let customColor = undefined;
|
|
82
|
+
|
|
83
|
+
if (obj instanceof Error) {
|
|
84
|
+
logData.err = {
|
|
85
|
+
type: obj.name || "Error",
|
|
86
|
+
message: obj.message,
|
|
87
|
+
stack: obj.stack,
|
|
88
|
+
};
|
|
89
|
+
if (typeof msg === "string") logData.msg = msg;
|
|
90
|
+
else logData.msg = obj.message;
|
|
91
|
+
if (typeof c === "string") customColor = c;
|
|
92
|
+
} else if (typeof obj === "string") {
|
|
93
|
+
logData.msg = obj;
|
|
94
|
+
if (typeof msg === "string") customColor = msg;
|
|
95
|
+
} else if (typeof obj === "object" && obj !== null) {
|
|
96
|
+
logData = { ...obj };
|
|
97
|
+
if (typeof msg === "string") logData.msg = msg;
|
|
98
|
+
if (typeof c === "string") customColor = c;
|
|
99
|
+
} else {
|
|
100
|
+
logData.msg = String(obj);
|
|
101
|
+
if (typeof msg === "string") customColor = msg;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (customColor) {
|
|
105
|
+
logData.color = customColor;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const finalLog = { ...base, ...logData };
|
|
109
|
+
|
|
110
|
+
if (this.prettyPrint) {
|
|
111
|
+
this._printPretty(finalLog);
|
|
112
|
+
} else {
|
|
113
|
+
this.stream.write(JSON.stringify(finalLog) + "\n");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
_printPretty(log) {
|
|
118
|
+
const time = new Date(log.time).toLocaleTimeString();
|
|
119
|
+
const lvlName = LEVEL_NAMES[log.level] || "INFO";
|
|
120
|
+
let prefixC = color.cyan;
|
|
121
|
+
if (log.level >= 50) prefixC = color.red;
|
|
122
|
+
else if (log.level === 40) prefixC = color.yellow;
|
|
123
|
+
else if (log.level <= 20) prefixC = color.dim;
|
|
124
|
+
|
|
125
|
+
const prefix = prefixC(`[VIBE ${lvlName} ${time}]`);
|
|
126
|
+
let context = "";
|
|
127
|
+
if (log.reqId) {
|
|
128
|
+
context = `\x1b[90m[${log.reqId}]\x1b[0m `;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let content = log.msg || "";
|
|
132
|
+
if (log.color && color[log.color]) {
|
|
133
|
+
content = color[log.color](content);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (log.err && log.err.stack) {
|
|
137
|
+
content += "\n" + prefixC(log.err.stack);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Attempt to print remaining metadata if it's not standard
|
|
141
|
+
const skipKeys = [
|
|
142
|
+
"level",
|
|
143
|
+
"time",
|
|
144
|
+
"pid",
|
|
145
|
+
"hostname",
|
|
146
|
+
"reqId",
|
|
147
|
+
"msg",
|
|
148
|
+
"err",
|
|
149
|
+
"color",
|
|
150
|
+
];
|
|
151
|
+
let metaStr = "";
|
|
152
|
+
for (const key of Object.keys(log)) {
|
|
153
|
+
if (!skipKeys.includes(key)) {
|
|
154
|
+
metaStr += ` \x1b[90m${key}=${JSON.stringify(log[key])}\x1b[0m`;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
this.stream.write(`${prefix} ${context}${content}${metaStr}\n`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function createLogger(options = {}) {
|
|
163
|
+
return new Logger(options);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
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,25 @@ 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
|
+
req.log.info({ type: "req" }, "Incoming request");
|
|
99
|
+
|
|
100
|
+
res.on("finish", () => {
|
|
101
|
+
req.log.info(
|
|
102
|
+
{
|
|
103
|
+
type: "res",
|
|
104
|
+
statusCode: res.statusCode,
|
|
105
|
+
responseTimeMs: Date.now() - req.startTime,
|
|
106
|
+
},
|
|
107
|
+
"Request completed",
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
93
112
|
// Fast pathname extraction
|
|
94
113
|
const url = req.url;
|
|
95
114
|
const qIdx = url.indexOf("?");
|
|
@@ -151,13 +170,19 @@ async function server(options, port, host, callback) {
|
|
|
151
170
|
if (result && typeof result.then === "function") {
|
|
152
171
|
result
|
|
153
172
|
.then((val) => {
|
|
173
|
+
if (val instanceof Error) {
|
|
174
|
+
return options.errorHandler(val, req, res);
|
|
175
|
+
}
|
|
154
176
|
if (val !== undefined && !res.writableEnded) {
|
|
155
177
|
res.writeHead(200, JSON_HEADERS);
|
|
156
178
|
res.end(serialize ? serialize(val) : JSON.stringify(val));
|
|
157
179
|
}
|
|
158
180
|
})
|
|
159
|
-
.catch((err) =>
|
|
181
|
+
.catch((err) => options.errorHandler(err, req, res));
|
|
160
182
|
} else if (typeof result === "object" && result !== null) {
|
|
183
|
+
if (result instanceof Error) {
|
|
184
|
+
return options.errorHandler(result, req, res);
|
|
185
|
+
}
|
|
161
186
|
res.writeHead(200, JSON_HEADERS);
|
|
162
187
|
res.end(serialize ? serialize(result) : JSON.stringify(result));
|
|
163
188
|
} else {
|
|
@@ -166,7 +191,7 @@ async function server(options, port, host, callback) {
|
|
|
166
191
|
}
|
|
167
192
|
}
|
|
168
193
|
} catch (err) {
|
|
169
|
-
|
|
194
|
+
options.errorHandler(err, req, res);
|
|
170
195
|
}
|
|
171
196
|
return;
|
|
172
197
|
}
|
|
@@ -226,6 +251,9 @@ async function server(options, port, host, callback) {
|
|
|
226
251
|
// Execute handler
|
|
227
252
|
if (typeof handler === "function") {
|
|
228
253
|
const result = await handler(req, res);
|
|
254
|
+
if (result instanceof Error) {
|
|
255
|
+
return options.errorHandler(result, req, res);
|
|
256
|
+
}
|
|
229
257
|
if (result !== undefined && !res.writableEnded) {
|
|
230
258
|
if (serialize) {
|
|
231
259
|
// Pre-compiled schema serializer — fastest path
|
|
@@ -247,7 +275,7 @@ async function server(options, port, host, callback) {
|
|
|
247
275
|
throw new Error("Invalid handler type");
|
|
248
276
|
}
|
|
249
277
|
} catch (err) {
|
|
250
|
-
|
|
278
|
+
options.errorHandler(err, req, res);
|
|
251
279
|
}
|
|
252
280
|
}
|
|
253
281
|
|
|
@@ -256,10 +284,7 @@ async function server(options, port, host, callback) {
|
|
|
256
284
|
|
|
257
285
|
const vibe_server = http.createServer(reqListener);
|
|
258
286
|
|
|
259
|
-
vibe_server.listen(port, mainHost,
|
|
260
|
-
try {
|
|
261
|
-
await dns.lookup("::", { all: true });
|
|
262
|
-
} catch {}
|
|
287
|
+
vibe_server.listen(port, mainHost, () => {
|
|
263
288
|
getNetworkIP(mainHost, port);
|
|
264
289
|
|
|
265
290
|
const strategy = useTrieMatching ? "Trie (O(log n))" : "Linear (O(n))";
|
|
@@ -271,7 +296,29 @@ async function server(options, port, host, callback) {
|
|
|
271
296
|
});
|
|
272
297
|
|
|
273
298
|
vibe_server.on("error", (err) => {
|
|
274
|
-
|
|
299
|
+
if (err.code === "EADDRINUSE") {
|
|
300
|
+
error(`Port ${port} is already in use! \n${err.message}`);
|
|
301
|
+
process.exit(1);
|
|
302
|
+
} else {
|
|
303
|
+
error(`Server error: \n${err.message}`);
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Graceful shutdown support for node --watch, nodemon, and cluster mode
|
|
308
|
+
const shutdown = () => {
|
|
309
|
+
// vibe_server.close stops accepting new connections
|
|
310
|
+
// Existing keep-alive connections will still prevent instant exit,
|
|
311
|
+
// so we force an exit if it takes longer than 3 seconds.
|
|
312
|
+
vibe_server.close(() => {
|
|
313
|
+
process.exit(0);
|
|
314
|
+
});
|
|
315
|
+
setTimeout(() => process.exit(0), 3000).unref();
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
process.on("SIGTERM", shutdown);
|
|
319
|
+
process.on("SIGINT", shutdown);
|
|
320
|
+
process.on("message", (msg) => {
|
|
321
|
+
if (msg === "shutdown") shutdown();
|
|
275
322
|
});
|
|
276
323
|
}
|
|
277
324
|
|
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,49 @@ 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
|
+
/** Custom writable stream to output logs to (defaults to process.stdout) */
|
|
185
|
+
stream?: NodeJS.WritableStream;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Fastify/Pino-compatible structured logger API.
|
|
190
|
+
*
|
|
191
|
+
* All methods accept a message string OR a structured object (Pino-style).
|
|
192
|
+
* An optional color string can be passed as the last argument — in prettyPrint
|
|
193
|
+
* mode it will colorize the terminal output, in production mode it writes as
|
|
194
|
+
* a plain JSON `{ color: "..." }` key for log pipelines.
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* req.log.info("Processing payment");
|
|
198
|
+
* req.log.info({ userId: 42, amount: 100 }, "Payment initiated");
|
|
199
|
+
* req.log.error(new Error("DB timeout"));
|
|
200
|
+
* req.log.warn("Slow query detected", "yellow"); // color override
|
|
201
|
+
*/
|
|
202
|
+
export interface LoggerAPI {
|
|
203
|
+
trace(obj: object | string | Error, msg?: string, color?: ColorName): void;
|
|
204
|
+
debug(obj: object | string | Error, msg?: string, color?: ColorName): void;
|
|
205
|
+
info(obj: object | string | Error, msg?: string, color?: ColorName): void;
|
|
206
|
+
warn(obj: object | string | Error, msg?: string, color?: ColorName): void;
|
|
207
|
+
error(obj: object | string | Error, msg?: string, color?: ColorName): void;
|
|
208
|
+
fatal(obj: object | string | Error, msg?: string, color?: ColorName): void;
|
|
209
|
+
/** Returns a child logger with merged bindings (e.g., { reqId }) */
|
|
210
|
+
child(bindings: Record<string, any>): LoggerAPI;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export interface VibeConfig {
|
|
214
|
+
/** Configuration for the native Vibe terminal logger */
|
|
215
|
+
logger?: LoggerConfig | boolean;
|
|
216
|
+
}
|
|
217
|
+
|
|
175
218
|
// ==========================================
|
|
176
219
|
// Request & Response Extensions
|
|
177
220
|
// ==========================================
|
|
@@ -192,6 +235,10 @@ export interface VibeRequest extends IncomingMessage {
|
|
|
192
235
|
ip?: string;
|
|
193
236
|
/** Detailed client IP info */
|
|
194
237
|
fullIp?: string;
|
|
238
|
+
/** Automatically generated UUID for the request lifecycle */
|
|
239
|
+
id: string;
|
|
240
|
+
/** Context-bound logger automatically stamped with the req.id constraint */
|
|
241
|
+
log: LoggerAPI;
|
|
195
242
|
/** Custom properties added via decorateRequest */
|
|
196
243
|
[key: string]: any;
|
|
197
244
|
}
|
|
@@ -338,11 +385,14 @@ export interface RouterAPI {
|
|
|
338
385
|
head: RouteRegistrar;
|
|
339
386
|
|
|
340
387
|
/**
|
|
341
|
-
* Log helper
|
|
342
|
-
* @param value The message to log
|
|
343
|
-
* @param
|
|
388
|
+
* Log helper supporting native colors and Vibe-stylized log levels
|
|
389
|
+
* @param value The message or object to log
|
|
390
|
+
* @param typeOrColor Optional color name (e.g. 'green') or level ('info', 'warn', 'error', 'req')
|
|
344
391
|
*/
|
|
345
|
-
log: (
|
|
392
|
+
log: (
|
|
393
|
+
value: any,
|
|
394
|
+
typeOrColor?: ColorName | "info" | "error" | "warn" | "req",
|
|
395
|
+
) => void;
|
|
346
396
|
|
|
347
397
|
/** Register a global interceptor */
|
|
348
398
|
plugin: (interceptor: Interceptor) => void;
|
|
@@ -401,15 +451,44 @@ export interface VibeApp extends RouterAPI {
|
|
|
401
451
|
maybeFunc?: (router: RouterAPI) => void,
|
|
402
452
|
) => void;
|
|
403
453
|
|
|
404
|
-
/**
|
|
454
|
+
/**
|
|
455
|
+
* Access app decorators
|
|
456
|
+
* @type {Record<string, any>}
|
|
457
|
+
*/
|
|
405
458
|
readonly decorators: Record<string, any>;
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Override the default error handler (Fastify-style).
|
|
462
|
+
* Called for any unhandled `throw`, `return new Error()`, or `res.send(error)`.
|
|
463
|
+
* @example
|
|
464
|
+
* app.setErrorHandler((error, req, res) => {
|
|
465
|
+
* req.log.error(error);
|
|
466
|
+
* res.status(503).json({ success: false, message: error.message });
|
|
467
|
+
* });
|
|
468
|
+
*/
|
|
469
|
+
setErrorHandler(
|
|
470
|
+
fn: (error: Error, req: VibeRequest, res: VibeResponse) => void,
|
|
471
|
+
): void;
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Pino/Fastify-compatible structured logger instance.
|
|
475
|
+
* Use for application-level logging outside of routes.
|
|
476
|
+
* @example
|
|
477
|
+
* app.log.info({ db: "connected" }, "Server ready");
|
|
478
|
+
* app.log.info("Server ready", "green"); // with color in prettyPrint mode
|
|
479
|
+
*/
|
|
480
|
+
log: LoggerAPI;
|
|
481
|
+
|
|
482
|
+
/** Alias for `app.log` */
|
|
483
|
+
logger: LoggerAPI;
|
|
406
484
|
}
|
|
407
485
|
|
|
408
486
|
/**
|
|
409
487
|
* Initialize a new Vibe application.
|
|
488
|
+
* @param config Optional application configuration
|
|
410
489
|
* @returns Vibe application instance
|
|
411
490
|
*/
|
|
412
|
-
export default function vibe(): VibeApp;
|
|
491
|
+
export default function vibe(config?: VibeConfig): VibeApp;
|
|
413
492
|
|
|
414
493
|
// ==========================================
|
|
415
494
|
// LRU Cache
|
|
@@ -560,3 +639,33 @@ export function parseJsonStream(
|
|
|
560
639
|
onEnd?: () => void,
|
|
561
640
|
onError?: (err: Error) => void,
|
|
562
641
|
): void;
|
|
642
|
+
|
|
643
|
+
// ==========================================
|
|
644
|
+
// Express Middleware Adapter
|
|
645
|
+
// ==========================================
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Adapt an Express-style middleware to work as a Vibe interceptor.
|
|
649
|
+
* @param mw - Express middleware function (req, res, next)
|
|
650
|
+
* @returns Vibe-compatible interceptor
|
|
651
|
+
*
|
|
652
|
+
* @example
|
|
653
|
+
* import { adapt } from "vibe-gx";
|
|
654
|
+
* import cors from "cors";
|
|
655
|
+
* import cookieParser from "cookie-parser";
|
|
656
|
+
*
|
|
657
|
+
* app.plugin(adapt(cors()));
|
|
658
|
+
* app.plugin(adapt(cookieParser()));
|
|
659
|
+
*/
|
|
660
|
+
export function adapt(
|
|
661
|
+
mw: (req: any, res: any, next: (err?: any) => void) => void,
|
|
662
|
+
): Interceptor;
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Adapt multiple Express middlewares at once.
|
|
666
|
+
* @param middlewares - Express middleware functions
|
|
667
|
+
* @returns Array of Vibe-compatible interceptors
|
|
668
|
+
*/
|
|
669
|
+
export function adaptAll(
|
|
670
|
+
...middlewares: Array<(req: any, res: any, next: (err?: any) => void) => void>
|
|
671
|
+
): Interceptor[];
|
package/vibe.js
CHANGED
|
@@ -4,6 +4,8 @@ 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";
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* Helper to generate regex for a path
|
|
@@ -129,9 +131,11 @@ function pathToRegex(path) {
|
|
|
129
131
|
|
|
130
132
|
/**
|
|
131
133
|
* Initializes a Vibe application instance.
|
|
134
|
+
* @param {Object} [config={}]
|
|
135
|
+
* @param {Object|boolean} [config.logger] - Logger configuration
|
|
132
136
|
* @returns {VibeApp}
|
|
133
137
|
*/
|
|
134
|
-
const vibe = () => {
|
|
138
|
+
const vibe = (config = {}) => {
|
|
135
139
|
// Route trie for O(log n) matching (used when routes > threshold)
|
|
136
140
|
const trie = new RouteTrie();
|
|
137
141
|
|
|
@@ -144,6 +148,14 @@ const vibe = () => {
|
|
|
144
148
|
// Static routes Map for O(1) lookup (routes without params)
|
|
145
149
|
const staticRoutes = new Map();
|
|
146
150
|
|
|
151
|
+
// Logger initialization
|
|
152
|
+
const loggerConfig =
|
|
153
|
+
config.logger !== false ? config.logger || {} : { level: "silent" };
|
|
154
|
+
const appLogger =
|
|
155
|
+
config.logger instanceof Logger
|
|
156
|
+
? config.logger
|
|
157
|
+
: createLogger(loggerConfig);
|
|
158
|
+
|
|
147
159
|
// Internal configuration
|
|
148
160
|
const options = {
|
|
149
161
|
trie,
|
|
@@ -156,6 +168,9 @@ const vibe = () => {
|
|
|
156
168
|
decorators: {},
|
|
157
169
|
requestDecorators: {},
|
|
158
170
|
replyDecorators: {},
|
|
171
|
+
logger: appLogger,
|
|
172
|
+
loggerConfig,
|
|
173
|
+
errorHandler: handleError,
|
|
159
174
|
};
|
|
160
175
|
|
|
161
176
|
// Register default landing route
|
|
@@ -460,6 +475,8 @@ const vibe = () => {
|
|
|
460
475
|
throw new Error(`Decorator '${name}' already exists`);
|
|
461
476
|
}
|
|
462
477
|
options.decorators[name] = value;
|
|
478
|
+
// Also set directly on the app object for easy access (app.name)
|
|
479
|
+
if (app) app[name] = value;
|
|
463
480
|
}
|
|
464
481
|
|
|
465
482
|
/**
|
|
@@ -532,12 +549,13 @@ const vibe = () => {
|
|
|
532
549
|
}
|
|
533
550
|
|
|
534
551
|
/**
|
|
535
|
-
*
|
|
536
|
-
*
|
|
537
|
-
* @param {string} [colorValue="reset"]
|
|
552
|
+
* Log messages out using the Vibe stylized legacy logger.
|
|
553
|
+
* Native string logging bypasses the Pino JSON interface.
|
|
538
554
|
*/
|
|
539
|
-
const log = (message,
|
|
540
|
-
|
|
555
|
+
const log = (message, typeOrColor = "reset") => {
|
|
556
|
+
const c = color[typeOrColor] || color.reset;
|
|
557
|
+
process.stdout.write(c(message) + "\n");
|
|
558
|
+
};
|
|
541
559
|
|
|
542
560
|
// Build the app object with decorators
|
|
543
561
|
const app = {
|
|
@@ -549,8 +567,13 @@ const vibe = () => {
|
|
|
549
567
|
head,
|
|
550
568
|
listen,
|
|
551
569
|
logRoutes,
|
|
552
|
-
log,
|
|
570
|
+
log: appLogger, // Standard Fastify-like exposure (app.log.info())
|
|
571
|
+
logger: appLogger,
|
|
572
|
+
logLegacy: log,
|
|
553
573
|
setPublicFolder,
|
|
574
|
+
setErrorHandler: (fn) => {
|
|
575
|
+
options.errorHandler = fn;
|
|
576
|
+
},
|
|
554
577
|
include,
|
|
555
578
|
plugin,
|
|
556
579
|
register,
|
|
@@ -570,7 +593,7 @@ const vibe = () => {
|
|
|
570
593
|
};
|
|
571
594
|
|
|
572
595
|
export default vibe;
|
|
573
|
-
export { color };
|
|
596
|
+
export { color, adapt };
|
|
574
597
|
|
|
575
598
|
// Scalability utilities
|
|
576
599
|
export {
|