vibe-gx 2.0.0 → 3.1.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.
@@ -1,14 +1,8 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
3
  import { mimeTypes } from "../helpers/mime.js";
4
- import { stringify as nativeStringify, isNativeEnabled } from "../native.js";
5
4
 
6
- // Pre-computed headers for performance
7
- const JSON_HEADERS = { "Content-Type": "application/json" };
8
- const TEXT_HEADERS = { "Content-Type": "text/plain" };
9
- const HTML_HEADERS = { "Content-Type": "text/html" };
10
-
11
- // Pre-allocated response templates
5
+ // Pre-allocated response templates (stringified once at startup)
12
6
  const RESPONSES = {
13
7
  notFound: JSON.stringify({ success: false, message: "Resource not found" }),
14
8
  unauthorized: JSON.stringify({ success: false, message: "Unauthorized" }),
@@ -22,141 +16,114 @@ const RESPONSES = {
22
16
  };
23
17
 
24
18
  /**
25
- * Fast JSON stringify - uses native C++ when available
26
- * @param {any} data
27
- * @returns {string}
19
+ * Vibe response methods mixin.
20
+ * These are added to ServerResponse.prototype ONCE at server startup,
21
+ * avoiding per-request closure allocations.
28
22
  */
29
- function fastStringify(data) {
30
- // Use native C++ stringify if available
31
- if (isNativeEnabled()) {
32
- return nativeStringify(data);
33
- }
34
-
35
- // JavaScript fallback
36
- if (data == null) return "null";
37
-
38
- const type = typeof data;
39
- if (type === "string") return JSON.stringify(data);
40
- if (type === "number" || type === "boolean") return String(data);
41
-
42
- return JSON.stringify(data);
43
- }
44
-
45
- /**
46
- * Extends the native ServerResponse with helper methods.
47
- * Optimized for performance with pre-computed headers and fast JSON.
48
- *
49
- * @param {import("http").ServerResponse} res
50
- * @param {Object} options
51
- * @param {string} options.publicFolder
52
- */
53
- export default function responseMethods(res, options) {
54
- // Cache headers reference for faster access
55
- const setHeader = res.setHeader.bind(res);
56
- const endResponse = res.end.bind(res);
57
-
23
+ const vibeResponseMethods = {
58
24
  /**
59
- * Sends a response. Optimized fast path for JSON.
25
+ * Sends a response. Fast path for JSON objects.
60
26
  * @param {any} data
61
27
  */
62
- res.send = (data) => {
28
+ send(data) {
63
29
  if (data === undefined) {
64
30
  throw new Error("Response data is not a sendable data type");
65
31
  }
66
32
 
67
33
  if (typeof data === "object" && data !== null) {
68
- setHeader("Content-Type", "application/json");
69
- endResponse(fastStringify(data));
34
+ this.setHeader("Content-Type", "application/json");
35
+ this.end(JSON.stringify(data));
70
36
  return;
71
37
  }
72
38
 
73
- setHeader("Content-Type", "text/plain");
74
- endResponse(String(data));
75
- };
39
+ this.setHeader("Content-Type", "text/plain");
40
+ this.end(String(data));
41
+ },
76
42
 
77
43
  /**
78
- * Sends a JSON response. Fast path.
44
+ * Sends a JSON response.
79
45
  * @param {Object} data
80
46
  */
81
- res.json = (data) => {
82
- setHeader("Content-Type", "application/json");
83
- endResponse(fastStringify(data));
84
- };
47
+ json(data) {
48
+ this.setHeader("Content-Type", "application/json");
49
+ this.end(JSON.stringify(data));
50
+ },
85
51
 
86
52
  /**
87
- * Sets HTTP status code.
53
+ * Sets HTTP status code. Chainable.
88
54
  * @param {number} code
89
- * @returns {import("http").ServerResponse}
55
+ * @returns {this}
90
56
  */
91
- res.status = (code) => {
92
- res.statusCode = code;
93
- return res;
94
- };
57
+ status(code) {
58
+ this.statusCode = code;
59
+ return this;
60
+ },
95
61
 
96
62
  /**
97
63
  * Safely send an HTML file from the public folder.
98
64
  * @param {string} filename
99
65
  */
100
- res.sendHtml = (filename) => {
101
- if (!options.publicFolder) throw new Error("No Public folder set");
66
+ sendHtml(filename) {
67
+ const publicFolder = this._vibeOptions.publicFolder;
68
+ if (!publicFolder) throw new Error("No Public folder set");
102
69
 
103
- const resolvedPath = path.resolve(options.publicFolder, filename);
70
+ const resolvedPath = path.resolve(publicFolder, filename);
104
71
 
105
- if (!resolvedPath.startsWith(path.resolve(options.publicFolder))) {
106
- res.statusCode = 403;
107
- return endResponse("Forbidden");
72
+ if (!resolvedPath.startsWith(path.resolve(publicFolder))) {
73
+ this.statusCode = 403;
74
+ return this.end("Forbidden");
108
75
  }
109
76
 
110
77
  if (!fs.existsSync(resolvedPath)) {
111
- res.statusCode = 404;
112
- return endResponse("Not Found");
78
+ this.statusCode = 404;
79
+ return this.end("Not Found");
113
80
  }
114
81
 
115
- res.writeHead(200, HTML_HEADERS);
116
- fs.createReadStream(resolvedPath).pipe(res);
117
- };
82
+ this.writeHead(200, { "Content-Type": "text/html" });
83
+ fs.createReadStream(resolvedPath).pipe(this);
84
+ },
118
85
 
119
86
  /**
120
87
  * Safely send any static file from the public folder.
121
88
  * @param {string} filePath
122
89
  */
123
- res.sendFile = (filePath) => {
124
- if (!options.publicFolder) throw new Error("No Public folder set");
90
+ sendFile(filePath) {
91
+ const publicFolder = this._vibeOptions.publicFolder;
92
+ if (!publicFolder) throw new Error("No Public folder set");
125
93
 
126
- const resolvedPath = path.resolve(options.publicFolder, filePath);
94
+ const resolvedPath = path.resolve(publicFolder, filePath);
127
95
 
128
- if (!resolvedPath.startsWith(path.resolve(options.publicFolder))) {
129
- res.statusCode = 403;
130
- return endResponse("Forbidden");
96
+ if (!resolvedPath.startsWith(path.resolve(publicFolder))) {
97
+ this.statusCode = 403;
98
+ return this.end("Forbidden");
131
99
  }
132
100
 
133
101
  if (!fs.existsSync(resolvedPath)) {
134
- res.statusCode = 404;
135
- return endResponse("Not Found");
102
+ this.statusCode = 404;
103
+ return this.end("Not Found");
136
104
  }
137
105
 
138
106
  const ext = path.extname(resolvedPath);
139
- res.writeHead(200, {
107
+ this.writeHead(200, {
140
108
  "Content-Type": mimeTypes[ext] || "application/octet-stream",
141
109
  });
142
110
 
143
- fs.createReadStream(resolvedPath).pipe(res);
144
- };
111
+ fs.createReadStream(resolvedPath).pipe(this);
112
+ },
145
113
 
146
114
  /**
147
115
  * Send any file from an absolute path (not restricted to public folder).
148
- * Use with caution - ensure you validate the path yourself.
149
116
  * @param {string} absolutePath - Full path to the file
150
117
  * @param {object} [opts] - Options
151
- * @param {boolean} [opts.download=false] - Force download with Content-Disposition
118
+ * @param {boolean} [opts.download=false] - Force download
152
119
  * @param {string} [opts.filename] - Custom filename for download
153
120
  */
154
- res.sendAbsoluteFile = (absolutePath, opts = {}) => {
121
+ sendAbsoluteFile(absolutePath, opts = {}) {
155
122
  const resolvedPath = path.resolve(absolutePath);
156
123
 
157
124
  if (!fs.existsSync(resolvedPath)) {
158
- res.statusCode = 404;
159
- return endResponse("Not Found");
125
+ this.statusCode = 404;
126
+ return this.end("Not Found");
160
127
  }
161
128
 
162
129
  const ext = path.extname(resolvedPath);
@@ -169,118 +136,153 @@ export default function responseMethods(res, options) {
169
136
  headers["Content-Disposition"] = `attachment; filename="${filename}"`;
170
137
  }
171
138
 
172
- res.writeHead(200, headers);
173
- fs.createReadStream(resolvedPath).pipe(res);
174
- };
139
+ this.writeHead(200, headers);
140
+ fs.createReadStream(resolvedPath).pipe(this);
141
+ },
175
142
 
176
143
  /**
177
144
  * Sends a 200 OK success response.
178
145
  * @param {any} data
179
146
  * @param {string} message
180
147
  */
181
- res.success = (data = null, message = "Success") => {
182
- res.statusCode = 200;
183
- setHeader("Content-Type", "application/json");
184
- endResponse(fastStringify({ success: true, message, data }));
185
- };
148
+ success(data = null, message = "Success") {
149
+ this.statusCode = 200;
150
+ this.setHeader("Content-Type", "application/json");
151
+ this.end(JSON.stringify({ success: true, message, data }));
152
+ },
186
153
 
187
154
  /**
188
155
  * Sends a 201 Created response.
189
156
  * @param {any} data
190
157
  * @param {string} message
191
158
  */
192
- res.created = (data = null, message = "Resource created") => {
193
- res.statusCode = 201;
194
- setHeader("Content-Type", "application/json");
195
- endResponse(fastStringify({ success: true, message, data }));
196
- };
159
+ created(data = null, message = "Resource created") {
160
+ this.statusCode = 201;
161
+ this.setHeader("Content-Type", "application/json");
162
+ this.end(JSON.stringify({ success: true, message, data }));
163
+ },
197
164
 
198
165
  /**
199
166
  * Sends a 400 Bad Request response.
200
167
  * @param {string} message
201
168
  * @param {any} errors
202
169
  */
203
- res.badRequest = (message = "Bad Request", errors = null) => {
204
- res.statusCode = 400;
205
- setHeader("Content-Type", "application/json");
206
- endResponse(
170
+ badRequest(message = "Bad Request", errors = null) {
171
+ this.statusCode = 400;
172
+ this.setHeader("Content-Type", "application/json");
173
+ this.end(
207
174
  errors
208
- ? fastStringify({ success: false, message, errors })
175
+ ? JSON.stringify({ success: false, message, errors })
209
176
  : RESPONSES.badRequest,
210
177
  );
211
- };
178
+ },
212
179
 
213
180
  /**
214
181
  * Sends a 401 Unauthorized response.
215
182
  * @param {string} message
216
183
  */
217
- res.unauthorized = (message) => {
218
- res.statusCode = 401;
219
- setHeader("Content-Type", "application/json");
220
- endResponse(
184
+ unauthorized(message) {
185
+ this.statusCode = 401;
186
+ this.setHeader("Content-Type", "application/json");
187
+ this.end(
221
188
  message
222
- ? fastStringify({ success: false, message })
189
+ ? JSON.stringify({ success: false, message })
223
190
  : RESPONSES.unauthorized,
224
191
  );
225
- };
192
+ },
226
193
 
227
194
  /**
228
195
  * Sends a 403 Forbidden response.
229
196
  * @param {string} message
230
197
  */
231
- res.forbidden = (message) => {
232
- res.statusCode = 403;
233
- setHeader("Content-Type", "application/json");
234
- endResponse(
198
+ forbidden(message) {
199
+ this.statusCode = 403;
200
+ this.setHeader("Content-Type", "application/json");
201
+ this.end(
235
202
  message
236
- ? fastStringify({ success: false, message })
203
+ ? JSON.stringify({ success: false, message })
237
204
  : RESPONSES.forbidden,
238
205
  );
239
- };
206
+ },
240
207
 
241
208
  /**
242
209
  * Sends a 404 Not Found response.
243
210
  * @param {string} message
244
211
  */
245
- res.notFound = (message) => {
246
- res.statusCode = 404;
247
- setHeader("Content-Type", "application/json");
248
- endResponse(
249
- message ? fastStringify({ success: false, message }) : RESPONSES.notFound,
212
+ notFound(message) {
213
+ this.statusCode = 404;
214
+ this.setHeader("Content-Type", "application/json");
215
+ this.end(
216
+ message
217
+ ? JSON.stringify({ success: false, message })
218
+ : RESPONSES.notFound,
250
219
  );
251
- };
220
+ },
252
221
 
253
222
  /**
254
223
  * Sends a 409 Conflict response.
255
224
  * @param {string} message
256
225
  */
257
- res.conflict = (message) => {
258
- res.statusCode = 409;
259
- setHeader("Content-Type", "application/json");
260
- endResponse(
261
- message ? fastStringify({ success: false, message }) : RESPONSES.conflict,
226
+ conflict(message) {
227
+ this.statusCode = 409;
228
+ this.setHeader("Content-Type", "application/json");
229
+ this.end(
230
+ message
231
+ ? JSON.stringify({ success: false, message })
232
+ : RESPONSES.conflict,
262
233
  );
263
- };
234
+ },
264
235
 
265
236
  /**
266
237
  * Sends a 500 Internal Server Error response.
267
238
  * @param {Error} error
268
239
  */
269
- res.serverError = (error) => {
240
+ serverError(error) {
270
241
  console.error(error);
271
- res.statusCode = 500;
272
- setHeader("Content-Type", "application/json");
273
- endResponse(RESPONSES.serverError);
274
- };
242
+ this.statusCode = 500;
243
+ this.setHeader("Content-Type", "application/json");
244
+ this.end(RESPONSES.serverError);
245
+ },
275
246
 
276
247
  /**
277
248
  * Redirects the client to another URL.
278
249
  * @param {string} url
279
250
  * @param {number} [status=302]
280
251
  */
281
- res.redirect = (url, status = 302) => {
282
- res.statusCode = status;
283
- setHeader("Location", url);
284
- endResponse();
285
- };
252
+ redirect(url, status = 302) {
253
+ this.statusCode = status;
254
+ this.setHeader("Location", url);
255
+ this.end();
256
+ },
257
+ };
258
+
259
+ /**
260
+ * Installs Vibe response methods onto the ServerResponse prototype.
261
+ * Called ONCE at server startup — zero per-request cost.
262
+ *
263
+ * @param {typeof import("http").ServerResponse} ResponseProto
264
+ */
265
+ export function installResponseMethods(ResponseProto) {
266
+ const proto = ResponseProto.prototype;
267
+ for (const [name, fn] of Object.entries(vibeResponseMethods)) {
268
+ if (!(name in proto)) {
269
+ proto[name] = fn;
270
+ }
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Stamps a single res object with the options ref.
276
+ * This is the ONLY per-request work we need to do.
277
+ *
278
+ * @param {import("http").ServerResponse} res
279
+ * @param {Object} options
280
+ */
281
+ export function initResponse(res, options) {
282
+ res._vibeOptions = options;
283
+ }
284
+
285
+ // Keep default export for backwards compatibility
286
+ export default function responseMethods(res, options) {
287
+ initResponse(res, options);
286
288
  }
@@ -7,9 +7,9 @@ import {
7
7
  matchPath,
8
8
  } from "./handler.js";
9
9
  import bodyParser from "./parser.js";
10
- import responseMethods from "./response.js";
10
+ import { installResponseMethods, initResponse } from "./response.js";
11
11
  import dns from "node:dns/promises";
12
- import { parseQuery as nativeParseQuery, isNativeEnabled } from "../native.js";
12
+ import { parseQuery } from "../native.js";
13
13
 
14
14
  // Pre-allocated 404 response
15
15
  const NOT_FOUND_BODY = "Not Found";
@@ -19,6 +19,9 @@ const NOT_FOUND_BODY = "Not Found";
19
19
  * HEAVILY OPTIMIZED for performance
20
20
  */
21
21
  async function server(options, port, host, callback) {
22
+ // Install response methods on prototype ONCE (zero per-request cost)
23
+ installResponseMethods(http.ServerResponse);
24
+
22
25
  // Pre-compute everything we can
23
26
  const useTrieMatching = options.routeCount > options.trieThreshold;
24
27
  const staticRoutes = options.staticRoutes || new Map();
@@ -77,24 +80,22 @@ async function server(options, port, host, callback) {
77
80
  const qIdx = url.indexOf("?");
78
81
  const pathname = qIdx < 0 ? url : url.slice(0, qIdx);
79
82
 
80
- // Lazy query (getter) - uses native C++ when available
83
+ // Lazy query via Object.defineProperty (replaces deprecated __defineGetter__)
81
84
  let _query;
82
- req.__defineGetter__("query", function () {
83
- if (_query === undefined) {
84
- if (qIdx < 0) {
85
- _query = {};
86
- } else {
87
- // Use native parser if available
88
- _query = nativeParseQuery(url.slice(qIdx + 1));
85
+ Object.defineProperty(req, "query", {
86
+ get() {
87
+ if (_query === undefined) {
88
+ _query = qIdx < 0 ? {} : parseQuery(url.slice(qIdx + 1));
89
89
  }
90
- }
91
- return _query;
90
+ return _query;
91
+ },
92
+ configurable: true,
92
93
  });
93
94
 
94
95
  req.url = pathname;
95
96
 
96
- // Extend response
97
- responseMethods(res, options);
97
+ // Stamp response with options ref (ONLY per-request cost for response methods)
98
+ initResponse(res, options);
98
99
 
99
100
  // Apply decorators (only if exist)
100
101
  if (requestDecoratorEntries) {
@@ -110,7 +111,57 @@ async function server(options, port, host, callback) {
110
111
  }
111
112
  }
112
113
 
113
- // Main async handler
114
+ // SYNC FAST PATH — avoid async/await for simple GET routes
115
+ if (!hasInterceptors && req.method === "GET") {
116
+ const routeKey = "GET" + pathname;
117
+ const staticMatch = staticRoutes.get(routeKey);
118
+
119
+ if (staticMatch && !staticMatch.intercept) {
120
+ const { handler, serialize } = staticMatch;
121
+ req.params = {};
122
+
123
+ try {
124
+ if (typeof handler === "function") {
125
+ const result = handler(req, res);
126
+ // Check if handler returned a Promise (async handler)
127
+ if (result !== undefined && !res.writableEnded) {
128
+ if (result && typeof result.then === "function") {
129
+ // Async handler — fall through to async path
130
+ result
131
+ .then((val) => {
132
+ if (val !== undefined && !res.writableEnded) {
133
+ if (serialize) {
134
+ res.setHeader("Content-Type", "application/json");
135
+ res.end(serialize(val));
136
+ } else {
137
+ res.json(val);
138
+ }
139
+ }
140
+ })
141
+ .catch((err) => handleError(err, req, res));
142
+ } else if (serialize) {
143
+ res.setHeader("Content-Type", "application/json");
144
+ res.end(serialize(result));
145
+ } else {
146
+ res.json(result);
147
+ }
148
+ }
149
+ } else if (isSendAble(handler)) {
150
+ if (serialize && typeof handler === "object" && handler !== null) {
151
+ res.setHeader("Content-Type", "application/json");
152
+ res.end(serialize(handler));
153
+ } else {
154
+ res.send(handler);
155
+ }
156
+ }
157
+ } catch (err) {
158
+ handleError(err, req, res);
159
+ }
160
+ return;
161
+ }
162
+ }
163
+
164
+ // Fallback to full async handler for complex routes
114
165
  handleRequest(req, res, pathname);
115
166
  }
116
167
 
@@ -145,7 +196,7 @@ async function server(options, port, host, callback) {
145
196
  }
146
197
 
147
198
  const { route, params } = match;
148
- const { handler, intercept, media } = route;
199
+ const { handler, intercept, media, serialize } = route;
149
200
 
150
201
  try {
151
202
  // Body parsing (only for non-GET with body)
@@ -165,10 +216,21 @@ async function server(options, port, host, callback) {
165
216
  if (typeof handler === "function") {
166
217
  const result = await handler(req, res);
167
218
  if (result !== undefined && !res.writableEnded) {
168
- res.json(result);
219
+ if (serialize) {
220
+ // Pre-compiled schema serializer — fastest path
221
+ res.setHeader("Content-Type", "application/json");
222
+ res.end(serialize(result));
223
+ } else {
224
+ res.json(result);
225
+ }
169
226
  }
170
227
  } else if (isSendAble(handler)) {
171
- res.send(handler);
228
+ if (serialize && typeof handler === "object" && handler !== null) {
229
+ res.setHeader("Content-Type", "application/json");
230
+ res.end(serialize(handler));
231
+ } else {
232
+ res.send(handler);
233
+ }
172
234
  } else {
173
235
  throw new Error("Invalid handler type");
174
236
  }