vibe-gx 2.0.0 → 3.0.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/README.md +66 -42
- package/package.json +7 -18
- package/utils/core/compile-serializer.js +171 -0
- package/utils/core/parser.js +16 -2
- package/utils/core/response.js +139 -137
- package/utils/core/server.js +80 -18
- package/utils/native.js +6 -80
- package/vibe.js +27 -8
- package/binding.gyp +0 -36
- package/native/json_stringify.cc +0 -241
- package/native/json_stringify.h +0 -32
- package/native/url_parser.cc +0 -178
- package/native/url_parser.h +0 -34
- package/native/vibe_native.cc +0 -46
package/utils/core/response.js
CHANGED
|
@@ -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-
|
|
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
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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
|
-
|
|
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.
|
|
25
|
+
* Sends a response. Fast path for JSON objects.
|
|
60
26
|
* @param {any} data
|
|
61
27
|
*/
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
75
|
-
}
|
|
39
|
+
this.setHeader("Content-Type", "text/plain");
|
|
40
|
+
this.end(String(data));
|
|
41
|
+
},
|
|
76
42
|
|
|
77
43
|
/**
|
|
78
|
-
* Sends a JSON response.
|
|
44
|
+
* Sends a JSON response.
|
|
79
45
|
* @param {Object} data
|
|
80
46
|
*/
|
|
81
|
-
|
|
82
|
-
setHeader("Content-Type", "application/json");
|
|
83
|
-
|
|
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 {
|
|
55
|
+
* @returns {this}
|
|
90
56
|
*/
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
return
|
|
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
|
-
|
|
101
|
-
|
|
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(
|
|
70
|
+
const resolvedPath = path.resolve(publicFolder, filename);
|
|
104
71
|
|
|
105
|
-
if (!resolvedPath.startsWith(path.resolve(
|
|
106
|
-
|
|
107
|
-
return
|
|
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
|
-
|
|
112
|
-
return
|
|
78
|
+
this.statusCode = 404;
|
|
79
|
+
return this.end("Not Found");
|
|
113
80
|
}
|
|
114
81
|
|
|
115
|
-
|
|
116
|
-
fs.createReadStream(resolvedPath).pipe(
|
|
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
|
-
|
|
124
|
-
|
|
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(
|
|
94
|
+
const resolvedPath = path.resolve(publicFolder, filePath);
|
|
127
95
|
|
|
128
|
-
if (!resolvedPath.startsWith(path.resolve(
|
|
129
|
-
|
|
130
|
-
return
|
|
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
|
-
|
|
135
|
-
return
|
|
102
|
+
this.statusCode = 404;
|
|
103
|
+
return this.end("Not Found");
|
|
136
104
|
}
|
|
137
105
|
|
|
138
106
|
const ext = path.extname(resolvedPath);
|
|
139
|
-
|
|
107
|
+
this.writeHead(200, {
|
|
140
108
|
"Content-Type": mimeTypes[ext] || "application/octet-stream",
|
|
141
109
|
});
|
|
142
110
|
|
|
143
|
-
fs.createReadStream(resolvedPath).pipe(
|
|
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
|
|
118
|
+
* @param {boolean} [opts.download=false] - Force download
|
|
152
119
|
* @param {string} [opts.filename] - Custom filename for download
|
|
153
120
|
*/
|
|
154
|
-
|
|
121
|
+
sendAbsoluteFile(absolutePath, opts = {}) {
|
|
155
122
|
const resolvedPath = path.resolve(absolutePath);
|
|
156
123
|
|
|
157
124
|
if (!fs.existsSync(resolvedPath)) {
|
|
158
|
-
|
|
159
|
-
return
|
|
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
|
-
|
|
173
|
-
fs.createReadStream(resolvedPath).pipe(
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
setHeader("Content-Type", "application/json");
|
|
184
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
setHeader("Content-Type", "application/json");
|
|
195
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
setHeader("Content-Type", "application/json");
|
|
206
|
-
|
|
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
|
-
?
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
setHeader("Content-Type", "application/json");
|
|
220
|
-
|
|
184
|
+
unauthorized(message) {
|
|
185
|
+
this.statusCode = 401;
|
|
186
|
+
this.setHeader("Content-Type", "application/json");
|
|
187
|
+
this.end(
|
|
221
188
|
message
|
|
222
|
-
?
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
setHeader("Content-Type", "application/json");
|
|
234
|
-
|
|
198
|
+
forbidden(message) {
|
|
199
|
+
this.statusCode = 403;
|
|
200
|
+
this.setHeader("Content-Type", "application/json");
|
|
201
|
+
this.end(
|
|
235
202
|
message
|
|
236
|
-
?
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
setHeader("Content-Type", "application/json");
|
|
248
|
-
|
|
249
|
-
message
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
setHeader("Content-Type", "application/json");
|
|
260
|
-
|
|
261
|
-
message
|
|
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
|
-
|
|
240
|
+
serverError(error) {
|
|
270
241
|
console.error(error);
|
|
271
|
-
|
|
272
|
-
setHeader("Content-Type", "application/json");
|
|
273
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
setHeader("Location", url);
|
|
284
|
-
|
|
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
|
}
|
package/utils/core/server.js
CHANGED
|
@@ -7,9 +7,9 @@ import {
|
|
|
7
7
|
matchPath,
|
|
8
8
|
} from "./handler.js";
|
|
9
9
|
import bodyParser from "./parser.js";
|
|
10
|
-
import
|
|
10
|
+
import { installResponseMethods, initResponse } from "./response.js";
|
|
11
11
|
import dns from "node:dns/promises";
|
|
12
|
-
import { parseQuery
|
|
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
|
|
83
|
+
// Lazy query via Object.defineProperty (replaces deprecated __defineGetter__)
|
|
81
84
|
let _query;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (
|
|
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
|
-
|
|
90
|
+
return _query;
|
|
91
|
+
},
|
|
92
|
+
configurable: true,
|
|
92
93
|
});
|
|
93
94
|
|
|
94
95
|
req.url = pathname;
|
|
95
96
|
|
|
96
|
-
//
|
|
97
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|