vibe-gx 3.1.0 → 4.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 +33 -39
- package/package.json +1 -1
- package/utils/core/compile-serializer.js +186 -93
- package/utils/core/response.js +16 -19
- package/utils/core/server.js +68 -56
- package/vibe.d.ts +72 -0
- package/vibe.js +38 -1
package/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
<div align="center">
|
|
2
|
-
<img src="
|
|
2
|
+
<img src="https://github.com/thesixers/vibe/blob/808b45722a0b3ca0d266215bbe4cc074f62283e5/assets/vlogo.png?raw=true" alt="Vibe Logo" width="180" />
|
|
3
3
|
<h1>Vibe</h1>
|
|
4
4
|
<p>
|
|
5
5
|
<b>The fastest Node.js web framework with the simplest syntax.</b>
|
|
6
6
|
</p>
|
|
7
7
|
<p>
|
|
8
|
-
<img src="https://img.shields.io/badge/performance-
|
|
9
|
-
<img src="https://img.shields.io/badge/vs_Express-
|
|
10
|
-
<img src="https://img.shields.io/badge/vs_Fastify-
|
|
8
|
+
<img src="https://img.shields.io/badge/performance-11,472_RPS-brightgreen" alt="Performance" />
|
|
9
|
+
<img src="https://img.shields.io/badge/vs_Express-4.7x_faster-blue" alt="vs Express" />
|
|
10
|
+
<img src="https://img.shields.io/badge/vs_Fastify-faster-orange" alt="vs Fastify" />
|
|
11
11
|
<img src="https://img.shields.io/badge/license-MIT-green" alt="License" />
|
|
12
12
|
</p>
|
|
13
13
|
</div>
|
|
@@ -20,29 +20,21 @@
|
|
|
20
20
|
npm install vibe-gx
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
For maximum performance, build the optional C++ native module:
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
npm run build:native
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
> If the build fails, Vibe automatically falls back to pure JavaScript with zero configuration.
|
|
23
|
+
> Pure JavaScript — no native dependencies, no build steps, just install and go.
|
|
32
24
|
|
|
33
25
|
---
|
|
34
26
|
|
|
35
27
|
## 🏆 Why Vibe?
|
|
36
28
|
|
|
37
|
-
| Metric
|
|
38
|
-
|
|
|
39
|
-
| **JSON Performance**
|
|
40
|
-
| **Install Size**
|
|
41
|
-
| **Lines for Hello World**
|
|
42
|
-
| **Dependencies**
|
|
43
|
-
| **Built-in Clustering**
|
|
44
|
-
| **Built-in Caching**
|
|
45
|
-
| **
|
|
29
|
+
| Metric | Vibe | Express | Fastify |
|
|
30
|
+
| :------------------------- | :------------: | :-------: | :--------: |
|
|
31
|
+
| **JSON Performance** | **11,472 RPS** | 2,421 RPS | 11,334 RPS |
|
|
32
|
+
| **Install Size** | **~280 KB** | ~5 MB | ~4 MB |
|
|
33
|
+
| **Lines for Hello World** | 3 | 5 | 6 |
|
|
34
|
+
| **Dependencies** | 1 | 30+ | 15+ |
|
|
35
|
+
| **Built-in Clustering** | ✅ | ❌ | ❌ |
|
|
36
|
+
| **Built-in Caching** | ✅ | ❌ | ❌ |
|
|
37
|
+
| **Code-Gen Serialization** | ✅ | ❌ | ✅ |
|
|
46
38
|
|
|
47
39
|
> **Vibe is faster than Fastify, simpler than Express, and 14-18x smaller than both.**
|
|
48
40
|
|
|
@@ -50,19 +42,19 @@ npm run build:native
|
|
|
50
42
|
|
|
51
43
|
## ⚡ Features
|
|
52
44
|
|
|
53
|
-
| Feature
|
|
54
|
-
|
|
|
55
|
-
| 🚀 **
|
|
56
|
-
| 🎯 **Hybrid Router**
|
|
57
|
-
| 🔌 **Plugin System**
|
|
58
|
-
| 🎨 **Decorators**
|
|
59
|
-
| ⚡ **Cluster Mode**
|
|
60
|
-
| 💾 **LRU Cache**
|
|
61
|
-
| 🔗 **Connection Pool**
|
|
62
|
-
| 📂 **File Uploads**
|
|
63
|
-
| 🌊 **Streaming**
|
|
64
|
-
| 🔒 **Security**
|
|
65
|
-
| 🔄 **Express Adapter**
|
|
45
|
+
| Feature | Description |
|
|
46
|
+
| :---------------------------- | :--------------------------------------------------------- |
|
|
47
|
+
| 🚀 **Code-Gen Serialization** | Schema-compiled JSON serializers via `new Function()` |
|
|
48
|
+
| 🎯 **Hybrid Router** | O(1) static + O(log n) Trie routing |
|
|
49
|
+
| 🔌 **Plugin System** | Fastify-style `register()` with encapsulation |
|
|
50
|
+
| 🎨 **Decorators** | Extend app, request, and response |
|
|
51
|
+
| ⚡ **Cluster Mode** | Built-in multi-process scaling |
|
|
52
|
+
| 💾 **LRU Cache** | Built-in response caching with ETag |
|
|
53
|
+
| 🔗 **Connection Pool** | Generic pool for databases |
|
|
54
|
+
| 📂 **File Uploads** | Multipart uploads with size/type validation |
|
|
55
|
+
| 🌊 **Streaming** | Large file uploads without buffering |
|
|
56
|
+
| 🔒 **Security** | Path traversal protection, body limits, error sanitization |
|
|
57
|
+
| 🔄 **Express Adapter** | Use any Express middleware with `adapt()` |
|
|
66
58
|
|
|
67
59
|
---
|
|
68
60
|
|
|
@@ -520,6 +512,7 @@ app.get(
|
|
|
520
512
|
**Benefits:**
|
|
521
513
|
|
|
522
514
|
- ✅ 2-3x faster JSON serialization
|
|
515
|
+
- ✅ Zero-loop code generation via `new Function()`
|
|
523
516
|
- ✅ No `Object.keys()` enumeration
|
|
524
517
|
- ✅ Zero runtime type checking
|
|
525
518
|
- ✅ Completely optional (routes work without schemas)
|
|
@@ -633,14 +626,15 @@ Run benchmarks yourself:
|
|
|
633
626
|
npm run benchmark
|
|
634
627
|
```
|
|
635
628
|
|
|
636
|
-
Tested
|
|
629
|
+
Tested under overload (20,000 requests × 200 concurrent):
|
|
637
630
|
|
|
638
631
|
```
|
|
639
632
|
Framework | JSON RPS | vs Express | vs Fastify
|
|
640
633
|
-------------|-------------|------------|------------
|
|
641
|
-
Vibe |
|
|
642
|
-
Fastify | 11,
|
|
643
|
-
|
|
634
|
+
Vibe | 11,472 | 4.7x ✅ | 1.01x ✅
|
|
635
|
+
Fastify | 11,334 | 4.7x | baseline
|
|
636
|
+
Hono | 7,351 | 3.0x | 0.6x
|
|
637
|
+
Express | 2,421 | baseline | 0.2x
|
|
644
638
|
```
|
|
645
639
|
|
|
646
640
|
---
|
package/package.json
CHANGED
|
@@ -1,16 +1,68 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Schema-based JSON serializer compiler.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Uses `new Function()` code generation to produce zero-loop,
|
|
5
|
+
* straight-line serializer functions from JSON schemas.
|
|
6
|
+
* Each generated function directly accesses known properties
|
|
7
|
+
* by name — no loops, no dynamic dispatch, no Object.keys().
|
|
8
|
+
*
|
|
9
|
+
* This is the same technique used by Fastify's fast-json-stringify.
|
|
8
10
|
*
|
|
9
11
|
* @module compile-serializer
|
|
10
12
|
*/
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
|
-
*
|
|
15
|
+
* Escapes a string for JSON output.
|
|
16
|
+
* Handles: " \ \b \f \n \r \t and control chars.
|
|
17
|
+
* This function is passed into generated serializers.
|
|
18
|
+
*/
|
|
19
|
+
function escapeString(str) {
|
|
20
|
+
if (str.length === 0) return '""';
|
|
21
|
+
|
|
22
|
+
let result = '"';
|
|
23
|
+
let last = 0;
|
|
24
|
+
|
|
25
|
+
for (let i = 0; i < str.length; i++) {
|
|
26
|
+
const code = str.charCodeAt(i);
|
|
27
|
+
if (code < 32 || code === 34 || code === 92) {
|
|
28
|
+
if (i > last) result += str.slice(last, i);
|
|
29
|
+
switch (code) {
|
|
30
|
+
case 34:
|
|
31
|
+
result += '\\"';
|
|
32
|
+
break;
|
|
33
|
+
case 92:
|
|
34
|
+
result += "\\\\";
|
|
35
|
+
break;
|
|
36
|
+
case 8:
|
|
37
|
+
result += "\\b";
|
|
38
|
+
break;
|
|
39
|
+
case 12:
|
|
40
|
+
result += "\\f";
|
|
41
|
+
break;
|
|
42
|
+
case 10:
|
|
43
|
+
result += "\\n";
|
|
44
|
+
break;
|
|
45
|
+
case 13:
|
|
46
|
+
result += "\\r";
|
|
47
|
+
break;
|
|
48
|
+
case 9:
|
|
49
|
+
result += "\\t";
|
|
50
|
+
break;
|
|
51
|
+
default:
|
|
52
|
+
result += "\\u" + code.toString(16).padStart(4, "0");
|
|
53
|
+
}
|
|
54
|
+
last = i + 1;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (last === 0) return '"' + str + '"';
|
|
59
|
+
if (last < str.length) result += str.slice(last);
|
|
60
|
+
return result + '"';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Compiles a JSON schema into a specialized serializer function
|
|
65
|
+
* using code generation for maximum V8 optimization.
|
|
14
66
|
*
|
|
15
67
|
* @param {Object} schema - JSON schema (subset)
|
|
16
68
|
* @returns {(data: any) => string} Compiled serializer
|
|
@@ -20,47 +72,144 @@ export function compileSerializer(schema) {
|
|
|
20
72
|
return JSON.stringify;
|
|
21
73
|
}
|
|
22
74
|
|
|
23
|
-
|
|
24
|
-
return fn;
|
|
75
|
+
return compileType(schema);
|
|
25
76
|
}
|
|
26
77
|
|
|
27
78
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
79
|
+
* Generates a code-gen serializer for an object schema.
|
|
80
|
+
* Produces a single straight-line function with no loops.
|
|
30
81
|
*/
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
"\b": "\\b",
|
|
35
|
-
"\f": "\\f",
|
|
36
|
-
"\n": "\\n",
|
|
37
|
-
"\r": "\\r",
|
|
38
|
-
"\t": "\\t",
|
|
39
|
-
};
|
|
82
|
+
function compileObject(schema) {
|
|
83
|
+
const props = schema.properties;
|
|
84
|
+
if (!props) return JSON.stringify;
|
|
40
85
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
let last = 0;
|
|
86
|
+
const keys = Object.keys(props);
|
|
87
|
+
if (keys.length === 0) return () => "{}";
|
|
44
88
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
89
|
+
// Build the function body as a single expression
|
|
90
|
+
const parts = [];
|
|
91
|
+
|
|
92
|
+
for (let i = 0; i < keys.length; i++) {
|
|
93
|
+
const key = keys[i];
|
|
94
|
+
const prop = props[key];
|
|
95
|
+
const comma = i > 0 ? "," : "";
|
|
96
|
+
const jsonKey = `${comma}"${key}":`;
|
|
97
|
+
|
|
98
|
+
// Generate inline serialization code per property type
|
|
99
|
+
parts.push({ key, jsonKey, type: prop.type || "unknown", schema: prop });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Check if all string — ultra-fast codegen path
|
|
103
|
+
const allSimple = parts.every(
|
|
104
|
+
(p) =>
|
|
105
|
+
p.type === "string" ||
|
|
106
|
+
p.type === "number" ||
|
|
107
|
+
p.type === "integer" ||
|
|
108
|
+
p.type === "boolean",
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
if (allSimple && keys.length <= 16) {
|
|
112
|
+
// Generate a straight-line function via new Function()
|
|
113
|
+
// This avoids all loops and dynamic dispatch
|
|
114
|
+
let body = 'if(o===null||o===undefined)return"null";\n';
|
|
115
|
+
body += "return '{' + ";
|
|
116
|
+
|
|
117
|
+
const segments = [];
|
|
118
|
+
for (let i = 0; i < parts.length; i++) {
|
|
119
|
+
const p = parts[i];
|
|
120
|
+
const accessor = `o[${JSON.stringify(p.key)}]`;
|
|
121
|
+
let valExpr;
|
|
122
|
+
|
|
123
|
+
switch (p.type) {
|
|
124
|
+
case "string":
|
|
125
|
+
valExpr = `(${accessor}===null||${accessor}===undefined?"null":e(""+${accessor}))`;
|
|
126
|
+
break;
|
|
127
|
+
case "number":
|
|
128
|
+
case "integer":
|
|
129
|
+
valExpr = `(${accessor}!=${accessor}||${accessor}===1/0||${accessor}===-1/0?"null":""+${accessor})`;
|
|
130
|
+
break;
|
|
131
|
+
case "boolean":
|
|
132
|
+
valExpr = `(${accessor}?"true":"false")`;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
segments.push(`'${p.jsonKey}'+${valExpr}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
body += segments.join("+") + "+'}';";
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
return new Function("e", "o", body).bind(null, escapeString);
|
|
143
|
+
} catch {
|
|
144
|
+
// Fallback if code-gen fails
|
|
145
|
+
return buildFallbackSerializer(parts);
|
|
53
146
|
}
|
|
54
147
|
}
|
|
55
148
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
149
|
+
// Complex types (nested objects, arrays) — use pre-compiled sub-serializers
|
|
150
|
+
return buildComplexSerializer(parts);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Builds a serializer for complex schemas with nested objects/arrays.
|
|
155
|
+
* Uses pre-compiled child serializers (no code-gen).
|
|
156
|
+
*/
|
|
157
|
+
function buildComplexSerializer(parts) {
|
|
158
|
+
// Pre-compile child serializers
|
|
159
|
+
const entries = parts.map((p) => ({
|
|
160
|
+
key: p.key,
|
|
161
|
+
jsonKey: p.jsonKey,
|
|
162
|
+
serializer: compileType(p.schema),
|
|
163
|
+
}));
|
|
164
|
+
|
|
165
|
+
return function serializeComplex(obj) {
|
|
166
|
+
if (obj === null || obj === undefined) return "null";
|
|
167
|
+
let result = "{";
|
|
168
|
+
for (let i = 0; i < entries.length; i++) {
|
|
169
|
+
const e = entries[i];
|
|
170
|
+
const v = obj[e.key];
|
|
171
|
+
result += e.jsonKey;
|
|
172
|
+
if (v === null || v === undefined) {
|
|
173
|
+
result += "null";
|
|
174
|
+
} else {
|
|
175
|
+
result += e.serializer(v);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return result + "}";
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Fallback serializer when code-gen fails.
|
|
184
|
+
*/
|
|
185
|
+
function buildFallbackSerializer(parts) {
|
|
186
|
+
const entries = parts.map((p) => ({
|
|
187
|
+
key: p.key,
|
|
188
|
+
jsonKey: p.jsonKey,
|
|
189
|
+
type: p.type,
|
|
190
|
+
}));
|
|
191
|
+
|
|
192
|
+
return function serializeFallback(obj) {
|
|
193
|
+
if (obj === null || obj === undefined) return "null";
|
|
194
|
+
let result = "{";
|
|
195
|
+
for (let i = 0; i < entries.length; i++) {
|
|
196
|
+
const e = entries[i];
|
|
197
|
+
const v = obj[e.key];
|
|
198
|
+
result += e.jsonKey;
|
|
199
|
+
if (v === null || v === undefined) {
|
|
200
|
+
result += "null";
|
|
201
|
+
} else if (e.type === "string") {
|
|
202
|
+
result += escapeString("" + v);
|
|
203
|
+
} else {
|
|
204
|
+
result += "" + v;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return result + "}";
|
|
208
|
+
};
|
|
59
209
|
}
|
|
60
210
|
|
|
61
211
|
/**
|
|
62
212
|
* Compiles a type-specific serializer from schema.
|
|
63
|
-
* Returns a function (value) => string.
|
|
64
213
|
*/
|
|
65
214
|
function compileType(schema) {
|
|
66
215
|
switch (schema.type) {
|
|
@@ -83,7 +232,7 @@ function compileType(schema) {
|
|
|
83
232
|
}
|
|
84
233
|
|
|
85
234
|
function serializeNumber(v) {
|
|
86
|
-
if (v !== v || v === Infinity || v === -Infinity) return "null";
|
|
235
|
+
if (v !== v || v === Infinity || v === -Infinity) return "null";
|
|
87
236
|
return "" + v;
|
|
88
237
|
}
|
|
89
238
|
|
|
@@ -91,63 +240,6 @@ function serializeBoolean(v) {
|
|
|
91
240
|
return v ? "true" : "false";
|
|
92
241
|
}
|
|
93
242
|
|
|
94
|
-
/**
|
|
95
|
-
* Compiles an object serializer from schema.properties.
|
|
96
|
-
*
|
|
97
|
-
* Generates a function that directly accesses known properties
|
|
98
|
-
* by name, avoiding Object.keys() enumeration entirely.
|
|
99
|
-
*/
|
|
100
|
-
function compileObject(schema) {
|
|
101
|
-
const props = schema.properties;
|
|
102
|
-
if (!props) return JSON.stringify;
|
|
103
|
-
|
|
104
|
-
const keys = Object.keys(props);
|
|
105
|
-
if (keys.length === 0) return () => "{}";
|
|
106
|
-
|
|
107
|
-
// Pre-compute property serializers and JSON key prefixes
|
|
108
|
-
const entries = keys.map((key, i) => {
|
|
109
|
-
const serializer = compileType(props[key]);
|
|
110
|
-
// Pre-stringify the key + colon (and comma for non-first)
|
|
111
|
-
const prefix = (i > 0 ? "," : "") + '"' + key + '":';
|
|
112
|
-
return { key, serializer, prefix };
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
// Check if all properties are simple strings — ultra-fast path
|
|
116
|
-
const allStrings = keys.every((k) => props[k].type === "string");
|
|
117
|
-
|
|
118
|
-
if (allStrings && keys.length <= 8) {
|
|
119
|
-
// Ultra-fast path for small all-string objects (most common API response)
|
|
120
|
-
return function serializeStringObject(obj) {
|
|
121
|
-
if (obj === null || obj === undefined) return "null";
|
|
122
|
-
let result = "{";
|
|
123
|
-
for (let i = 0; i < entries.length; i++) {
|
|
124
|
-
const e = entries[i];
|
|
125
|
-
const v = obj[e.key];
|
|
126
|
-
result += e.prefix;
|
|
127
|
-
result += v === undefined || v === null ? "null" : escapeString("" + v);
|
|
128
|
-
}
|
|
129
|
-
return result + "}";
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// General path for mixed-type objects
|
|
134
|
-
return function serializeObject(obj) {
|
|
135
|
-
if (obj === null || obj === undefined) return "null";
|
|
136
|
-
let result = "{";
|
|
137
|
-
for (let i = 0; i < entries.length; i++) {
|
|
138
|
-
const e = entries[i];
|
|
139
|
-
const v = obj[e.key];
|
|
140
|
-
result += e.prefix;
|
|
141
|
-
if (v === undefined || v === null) {
|
|
142
|
-
result += "null";
|
|
143
|
-
} else {
|
|
144
|
-
result += e.serializer(v);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
return result + "}";
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
|
|
151
243
|
/**
|
|
152
244
|
* Compiles an array serializer from schema.items.
|
|
153
245
|
*/
|
|
@@ -158,10 +250,11 @@ function compileArray(schema) {
|
|
|
158
250
|
|
|
159
251
|
return function serializeArray(arr) {
|
|
160
252
|
if (!Array.isArray(arr)) return "null";
|
|
161
|
-
|
|
253
|
+
const len = arr.length;
|
|
254
|
+
if (len === 0) return "[]";
|
|
162
255
|
|
|
163
256
|
let result = "[" + itemSerializer(arr[0]);
|
|
164
|
-
for (let i = 1; i <
|
|
257
|
+
for (let i = 1; i < len; i++) {
|
|
165
258
|
result += "," + itemSerializer(arr[i]);
|
|
166
259
|
}
|
|
167
260
|
return result + "]";
|
package/utils/core/response.js
CHANGED
|
@@ -2,6 +2,11 @@ import fs from "fs";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { mimeTypes } from "../helpers/mime.js";
|
|
4
4
|
|
|
5
|
+
// Pre-allocated headers (avoid per-request object creation)
|
|
6
|
+
const JSON_CT = { "content-type": "application/json" };
|
|
7
|
+
const TEXT_CT = { "content-type": "text/plain" };
|
|
8
|
+
const HTML_CT = { "Content-Type": "text/html" };
|
|
9
|
+
|
|
5
10
|
// Pre-allocated response templates (stringified once at startup)
|
|
6
11
|
const RESPONSES = {
|
|
7
12
|
notFound: JSON.stringify({ success: false, message: "Resource not found" }),
|
|
@@ -31,12 +36,12 @@ const vibeResponseMethods = {
|
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
if (typeof data === "object" && data !== null) {
|
|
34
|
-
this.
|
|
39
|
+
if (!this.headersSent) this.writeHead(this.statusCode || 200, JSON_CT);
|
|
35
40
|
this.end(JSON.stringify(data));
|
|
36
41
|
return;
|
|
37
42
|
}
|
|
38
43
|
|
|
39
|
-
this.
|
|
44
|
+
if (!this.headersSent) this.writeHead(this.statusCode || 200, TEXT_CT);
|
|
40
45
|
this.end(String(data));
|
|
41
46
|
},
|
|
42
47
|
|
|
@@ -45,7 +50,7 @@ const vibeResponseMethods = {
|
|
|
45
50
|
* @param {Object} data
|
|
46
51
|
*/
|
|
47
52
|
json(data) {
|
|
48
|
-
this.
|
|
53
|
+
if (!this.headersSent) this.writeHead(this.statusCode || 200, JSON_CT);
|
|
49
54
|
this.end(JSON.stringify(data));
|
|
50
55
|
},
|
|
51
56
|
|
|
@@ -146,8 +151,7 @@ const vibeResponseMethods = {
|
|
|
146
151
|
* @param {string} message
|
|
147
152
|
*/
|
|
148
153
|
success(data = null, message = "Success") {
|
|
149
|
-
this.
|
|
150
|
-
this.setHeader("Content-Type", "application/json");
|
|
154
|
+
this.writeHead(200, JSON_CT);
|
|
151
155
|
this.end(JSON.stringify({ success: true, message, data }));
|
|
152
156
|
},
|
|
153
157
|
|
|
@@ -157,8 +161,7 @@ const vibeResponseMethods = {
|
|
|
157
161
|
* @param {string} message
|
|
158
162
|
*/
|
|
159
163
|
created(data = null, message = "Resource created") {
|
|
160
|
-
this.
|
|
161
|
-
this.setHeader("Content-Type", "application/json");
|
|
164
|
+
this.writeHead(201, JSON_CT);
|
|
162
165
|
this.end(JSON.stringify({ success: true, message, data }));
|
|
163
166
|
},
|
|
164
167
|
|
|
@@ -168,8 +171,7 @@ const vibeResponseMethods = {
|
|
|
168
171
|
* @param {any} errors
|
|
169
172
|
*/
|
|
170
173
|
badRequest(message = "Bad Request", errors = null) {
|
|
171
|
-
this.
|
|
172
|
-
this.setHeader("Content-Type", "application/json");
|
|
174
|
+
this.writeHead(400, JSON_CT);
|
|
173
175
|
this.end(
|
|
174
176
|
errors
|
|
175
177
|
? JSON.stringify({ success: false, message, errors })
|
|
@@ -182,8 +184,7 @@ const vibeResponseMethods = {
|
|
|
182
184
|
* @param {string} message
|
|
183
185
|
*/
|
|
184
186
|
unauthorized(message) {
|
|
185
|
-
this.
|
|
186
|
-
this.setHeader("Content-Type", "application/json");
|
|
187
|
+
this.writeHead(401, JSON_CT);
|
|
187
188
|
this.end(
|
|
188
189
|
message
|
|
189
190
|
? JSON.stringify({ success: false, message })
|
|
@@ -196,8 +197,7 @@ const vibeResponseMethods = {
|
|
|
196
197
|
* @param {string} message
|
|
197
198
|
*/
|
|
198
199
|
forbidden(message) {
|
|
199
|
-
this.
|
|
200
|
-
this.setHeader("Content-Type", "application/json");
|
|
200
|
+
this.writeHead(403, JSON_CT);
|
|
201
201
|
this.end(
|
|
202
202
|
message
|
|
203
203
|
? JSON.stringify({ success: false, message })
|
|
@@ -210,8 +210,7 @@ const vibeResponseMethods = {
|
|
|
210
210
|
* @param {string} message
|
|
211
211
|
*/
|
|
212
212
|
notFound(message) {
|
|
213
|
-
this.
|
|
214
|
-
this.setHeader("Content-Type", "application/json");
|
|
213
|
+
this.writeHead(404, JSON_CT);
|
|
215
214
|
this.end(
|
|
216
215
|
message
|
|
217
216
|
? JSON.stringify({ success: false, message })
|
|
@@ -224,8 +223,7 @@ const vibeResponseMethods = {
|
|
|
224
223
|
* @param {string} message
|
|
225
224
|
*/
|
|
226
225
|
conflict(message) {
|
|
227
|
-
this.
|
|
228
|
-
this.setHeader("Content-Type", "application/json");
|
|
226
|
+
this.writeHead(409, JSON_CT);
|
|
229
227
|
this.end(
|
|
230
228
|
message
|
|
231
229
|
? JSON.stringify({ success: false, message })
|
|
@@ -239,8 +237,7 @@ const vibeResponseMethods = {
|
|
|
239
237
|
*/
|
|
240
238
|
serverError(error) {
|
|
241
239
|
console.error(error);
|
|
242
|
-
this.
|
|
243
|
-
this.setHeader("Content-Type", "application/json");
|
|
240
|
+
this.writeHead(500, JSON_CT);
|
|
244
241
|
this.end(RESPONSES.serverError);
|
|
245
242
|
},
|
|
246
243
|
|
package/utils/core/server.js
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import http from "http";
|
|
2
|
-
import {
|
|
3
|
-
error,
|
|
4
|
-
getNetworkIP,
|
|
5
|
-
handleError,
|
|
6
|
-
isSendAble,
|
|
7
|
-
matchPath,
|
|
8
|
-
} from "./handler.js";
|
|
2
|
+
import { error, getNetworkIP, handleError, isSendAble } from "./handler.js";
|
|
9
3
|
import bodyParser from "./parser.js";
|
|
10
4
|
import { installResponseMethods, initResponse } from "./response.js";
|
|
11
5
|
import dns from "node:dns/promises";
|
|
12
6
|
import { parseQuery } from "../native.js";
|
|
13
7
|
|
|
8
|
+
// Pre-allocated headers (frozen for V8 optimization)
|
|
9
|
+
const JSON_HEADERS = { "content-type": "application/json" };
|
|
10
|
+
const TEXT_HEADERS = { "content-type": "text/plain" };
|
|
11
|
+
|
|
14
12
|
// Pre-allocated 404 response
|
|
15
13
|
const NOT_FOUND_BODY = "Not Found";
|
|
16
14
|
|
|
15
|
+
// Shared frozen empty params (avoids per-request object allocation)
|
|
16
|
+
const EMPTY_PARAMS = Object.freeze(Object.create(null));
|
|
17
|
+
|
|
17
18
|
/**
|
|
18
19
|
* Creates and starts the Vibe HTTP server.
|
|
19
20
|
* HEAVILY OPTIMIZED for performance
|
|
@@ -22,6 +23,20 @@ async function server(options, port, host, callback) {
|
|
|
22
23
|
// Install response methods on prototype ONCE (zero per-request cost)
|
|
23
24
|
installResponseMethods(http.ServerResponse);
|
|
24
25
|
|
|
26
|
+
// Install lazy query getter on IncomingMessage prototype ONCE
|
|
27
|
+
if (!http.IncomingMessage.prototype._vibeQueryInstalled) {
|
|
28
|
+
Object.defineProperty(http.IncomingMessage.prototype, "query", {
|
|
29
|
+
get() {
|
|
30
|
+
if (this._parsedQuery !== undefined) return this._parsedQuery;
|
|
31
|
+
this._parsedQuery =
|
|
32
|
+
this._qIdx < 0 ? {} : parseQuery(this._rawUrl.slice(this._qIdx + 1));
|
|
33
|
+
return this._parsedQuery;
|
|
34
|
+
},
|
|
35
|
+
configurable: true,
|
|
36
|
+
});
|
|
37
|
+
http.IncomingMessage.prototype._vibeQueryInstalled = true;
|
|
38
|
+
}
|
|
39
|
+
|
|
25
40
|
// Pre-compute everything we can
|
|
26
41
|
const useTrieMatching = options.routeCount > options.trieThreshold;
|
|
27
42
|
const staticRoutes = options.staticRoutes || new Map();
|
|
@@ -73,29 +88,22 @@ async function server(options, port, host, callback) {
|
|
|
73
88
|
return null;
|
|
74
89
|
}
|
|
75
90
|
|
|
76
|
-
// Main request handler - OPTIMIZED
|
|
91
|
+
// Main request handler - ULTRA OPTIMIZED
|
|
77
92
|
function reqListener(req, res) {
|
|
78
93
|
// Fast pathname extraction
|
|
79
94
|
const url = req.url;
|
|
80
95
|
const qIdx = url.indexOf("?");
|
|
81
96
|
const pathname = qIdx < 0 ? url : url.slice(0, qIdx);
|
|
82
97
|
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (_query === undefined) {
|
|
88
|
-
_query = qIdx < 0 ? {} : parseQuery(url.slice(qIdx + 1));
|
|
89
|
-
}
|
|
90
|
-
return _query;
|
|
91
|
-
},
|
|
92
|
-
configurable: true,
|
|
93
|
-
});
|
|
98
|
+
// Store raw values for lazy query parsing (prototype getter handles the rest)
|
|
99
|
+
req._qIdx = qIdx;
|
|
100
|
+
req._rawUrl = url;
|
|
101
|
+
req._parsedQuery = undefined;
|
|
94
102
|
|
|
95
103
|
req.url = pathname;
|
|
96
104
|
|
|
97
105
|
// Stamp response with options ref (ONLY per-request cost for response methods)
|
|
98
|
-
|
|
106
|
+
res._vibeOptions = options;
|
|
99
107
|
|
|
100
108
|
// Apply decorators (only if exist)
|
|
101
109
|
if (requestDecoratorEntries) {
|
|
@@ -117,41 +125,44 @@ async function server(options, port, host, callback) {
|
|
|
117
125
|
const staticMatch = staticRoutes.get(routeKey);
|
|
118
126
|
|
|
119
127
|
if (staticMatch && !staticMatch.intercept) {
|
|
120
|
-
|
|
121
|
-
|
|
128
|
+
req.params = EMPTY_PARAMS;
|
|
129
|
+
|
|
130
|
+
// Pre-built response (string/object/number/boolean registered at route time)
|
|
131
|
+
if (staticMatch._handlerType === 2) {
|
|
132
|
+
const pb = staticMatch._prebuilt;
|
|
133
|
+
// Check if it looks like JSON (starts with { or [)
|
|
134
|
+
const c = pb.charCodeAt(0);
|
|
135
|
+
if (c === 123 || c === 91) {
|
|
136
|
+
res.writeHead(200, JSON_HEADERS);
|
|
137
|
+
} else {
|
|
138
|
+
res.writeHead(200, TEXT_HEADERS);
|
|
139
|
+
}
|
|
140
|
+
res.end(pb);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Function handler
|
|
145
|
+
const handler = staticMatch.handler;
|
|
146
|
+
const serialize = staticMatch.serialize;
|
|
122
147
|
|
|
123
148
|
try {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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));
|
|
149
|
+
const result = handler(req, res);
|
|
150
|
+
if (result !== undefined && !res.writableEnded) {
|
|
151
|
+
if (result && typeof result.then === "function") {
|
|
152
|
+
result
|
|
153
|
+
.then((val) => {
|
|
154
|
+
if (val !== undefined && !res.writableEnded) {
|
|
155
|
+
res.writeHead(200, JSON_HEADERS);
|
|
156
|
+
res.end(serialize ? serialize(val) : JSON.stringify(val));
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
.catch((err) => handleError(err, req, res));
|
|
160
|
+
} else if (typeof result === "object" && result !== null) {
|
|
161
|
+
res.writeHead(200, JSON_HEADERS);
|
|
162
|
+
res.end(serialize ? serialize(result) : JSON.stringify(result));
|
|
153
163
|
} else {
|
|
154
|
-
res.
|
|
164
|
+
res.writeHead(200, TEXT_HEADERS);
|
|
165
|
+
res.end(String(result));
|
|
155
166
|
}
|
|
156
167
|
}
|
|
157
168
|
} catch (err) {
|
|
@@ -190,7 +201,7 @@ async function server(options, port, host, callback) {
|
|
|
190
201
|
}
|
|
191
202
|
|
|
192
203
|
if (!match) {
|
|
193
|
-
res.writeHead(404,
|
|
204
|
+
res.writeHead(404, TEXT_HEADERS);
|
|
194
205
|
res.end(NOT_FOUND_BODY);
|
|
195
206
|
return;
|
|
196
207
|
}
|
|
@@ -218,15 +229,16 @@ async function server(options, port, host, callback) {
|
|
|
218
229
|
if (result !== undefined && !res.writableEnded) {
|
|
219
230
|
if (serialize) {
|
|
220
231
|
// Pre-compiled schema serializer — fastest path
|
|
221
|
-
res.
|
|
232
|
+
res.writeHead(200, JSON_HEADERS);
|
|
222
233
|
res.end(serialize(result));
|
|
223
234
|
} else {
|
|
224
|
-
res.
|
|
235
|
+
res.writeHead(200, JSON_HEADERS);
|
|
236
|
+
res.end(JSON.stringify(result));
|
|
225
237
|
}
|
|
226
238
|
}
|
|
227
239
|
} else if (isSendAble(handler)) {
|
|
228
240
|
if (serialize && typeof handler === "object" && handler !== null) {
|
|
229
|
-
res.
|
|
241
|
+
res.writeHead(200, JSON_HEADERS);
|
|
230
242
|
res.end(serialize(handler));
|
|
231
243
|
} else {
|
|
232
244
|
res.send(handler);
|
package/vibe.d.ts
CHANGED
|
@@ -51,6 +51,62 @@ export interface MediaOptions {
|
|
|
51
51
|
streaming?: boolean;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* JSON Schema property definition for schema-based serialization.
|
|
56
|
+
*/
|
|
57
|
+
export interface JsonSchemaProperty {
|
|
58
|
+
/** The type of the property */
|
|
59
|
+
type:
|
|
60
|
+
| "string"
|
|
61
|
+
| "number"
|
|
62
|
+
| "integer"
|
|
63
|
+
| "boolean"
|
|
64
|
+
| "object"
|
|
65
|
+
| "array"
|
|
66
|
+
| "null";
|
|
67
|
+
/** Nested properties (for type: "object") */
|
|
68
|
+
properties?: Record<string, JsonSchemaProperty>;
|
|
69
|
+
/** Item schema (for type: "array") */
|
|
70
|
+
items?: JsonSchemaProperty;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* JSON Schema definition for response serialization.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* {
|
|
78
|
+
* type: "object",
|
|
79
|
+
* properties: {
|
|
80
|
+
* id: { type: "number" },
|
|
81
|
+
* name: { type: "string" },
|
|
82
|
+
* active: { type: "boolean" }
|
|
83
|
+
* }
|
|
84
|
+
* }
|
|
85
|
+
*/
|
|
86
|
+
export interface JsonSchema extends JsonSchemaProperty {
|
|
87
|
+
type: "object" | "array";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Schema options for a route.
|
|
92
|
+
* Used for pre-compiled response serialization (2-3x faster than JSON.stringify).
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* {
|
|
96
|
+
* response: {
|
|
97
|
+
* type: "object",
|
|
98
|
+
* properties: {
|
|
99
|
+
* id: { type: "number" },
|
|
100
|
+
* name: { type: "string" }
|
|
101
|
+
* }
|
|
102
|
+
* }
|
|
103
|
+
* }
|
|
104
|
+
*/
|
|
105
|
+
export interface SchemaOptions {
|
|
106
|
+
/** Response schema for pre-compiled JSON serialization */
|
|
107
|
+
response?: JsonSchema;
|
|
108
|
+
}
|
|
109
|
+
|
|
54
110
|
/**
|
|
55
111
|
* Options for registering a route.
|
|
56
112
|
*
|
|
@@ -88,6 +144,22 @@ export interface RouteOptions {
|
|
|
88
144
|
* Files will be available in req.files array.
|
|
89
145
|
*/
|
|
90
146
|
media?: MediaOptions;
|
|
147
|
+
/**
|
|
148
|
+
* JSON Schema for pre-compiled response serialization.
|
|
149
|
+
* Generates a zero-overhead serializer at route registration time.
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* schema: {
|
|
153
|
+
* response: {
|
|
154
|
+
* type: "object",
|
|
155
|
+
* properties: {
|
|
156
|
+
* id: { type: "number" },
|
|
157
|
+
* name: { type: "string" }
|
|
158
|
+
* }
|
|
159
|
+
* }
|
|
160
|
+
* }
|
|
161
|
+
*/
|
|
162
|
+
schema?: SchemaOptions;
|
|
91
163
|
}
|
|
92
164
|
|
|
93
165
|
/**
|
package/vibe.js
CHANGED
|
@@ -85,6 +85,7 @@ function pathToRegex(path) {
|
|
|
85
85
|
* @typedef {Object} RouteOptions
|
|
86
86
|
* @property {Interceptor | Interceptor[]} [intercept]
|
|
87
87
|
* @property {MediaOptions} [media]
|
|
88
|
+
* @property {{ response?: Object }} [schema] Schema for pre-compiled serialization
|
|
88
89
|
*/
|
|
89
90
|
|
|
90
91
|
/**
|
|
@@ -92,9 +93,14 @@ function pathToRegex(path) {
|
|
|
92
93
|
* @typedef {Object} VibeRoute
|
|
93
94
|
* @property {string} method
|
|
94
95
|
* @property {string} path
|
|
96
|
+
* @property {RegExp | null} pathRegex
|
|
95
97
|
* @property {Handler | string | number | object} handler
|
|
96
98
|
* @property {Interceptor | Interceptor[] | null} intercept
|
|
97
|
-
* @property {
|
|
99
|
+
* @property {((data: any) => string) | null} serialize
|
|
100
|
+
* @property {MediaOptions | null} media
|
|
101
|
+
* @property {boolean} [isStatic]
|
|
102
|
+
* @property {number} [_handlerType]
|
|
103
|
+
* @property {string | null} [_prebuilt]
|
|
98
104
|
*/
|
|
99
105
|
|
|
100
106
|
/**
|
|
@@ -198,6 +204,9 @@ const vibe = () => {
|
|
|
198
204
|
intercept: null,
|
|
199
205
|
serialize: null,
|
|
200
206
|
media: null, // Only set when explicitly configured
|
|
207
|
+
// Pre-computed handler metadata (avoids typeof checks on hot path)
|
|
208
|
+
_handlerType: 0, // 0=unknown, 1=function, 2=prebuilt-string
|
|
209
|
+
_prebuilt: null, // Pre-stringified response for static handlers
|
|
201
210
|
};
|
|
202
211
|
|
|
203
212
|
// Handle overriding root route
|
|
@@ -227,6 +236,7 @@ const vibe = () => {
|
|
|
227
236
|
}
|
|
228
237
|
route.pathRegex = /^\/$/;
|
|
229
238
|
route.isStatic = true;
|
|
239
|
+
finalizeRoute(route);
|
|
230
240
|
trie.insert(method, "/", route);
|
|
231
241
|
staticRoutes.set(method + "/", route);
|
|
232
242
|
// Update existing root route in routes array
|
|
@@ -270,6 +280,9 @@ const vibe = () => {
|
|
|
270
280
|
const isStatic = !fullPath.includes(":") && !fullPath.includes("*");
|
|
271
281
|
route.isStatic = isStatic;
|
|
272
282
|
|
|
283
|
+
// Pre-compute handler type for fast-path dispatch
|
|
284
|
+
finalizeRoute(route);
|
|
285
|
+
|
|
273
286
|
// Add to static routes Map for O(1) lookup
|
|
274
287
|
if (isStatic) {
|
|
275
288
|
staticRoutes.set(method + fullPath, route);
|
|
@@ -281,6 +294,28 @@ const vibe = () => {
|
|
|
281
294
|
options.routeCount++;
|
|
282
295
|
}
|
|
283
296
|
|
|
297
|
+
/**
|
|
298
|
+
* Pre-computes handler type and pre-stringifies static values.
|
|
299
|
+
* This moves work from request-time to registration-time.
|
|
300
|
+
*/
|
|
301
|
+
function finalizeRoute(route) {
|
|
302
|
+
const h = route.handler;
|
|
303
|
+
if (typeof h === "function") {
|
|
304
|
+
route._handlerType = 1; // function
|
|
305
|
+
} else if (typeof h === "string") {
|
|
306
|
+
route._handlerType = 2; // pre-built string
|
|
307
|
+
route._prebuilt = h;
|
|
308
|
+
} else if (typeof h === "object" && h !== null) {
|
|
309
|
+
route._handlerType = 2; // pre-built JSON
|
|
310
|
+
route._prebuilt = route.serialize
|
|
311
|
+
? route.serialize(h)
|
|
312
|
+
: JSON.stringify(h);
|
|
313
|
+
} else if (typeof h === "number" || typeof h === "boolean") {
|
|
314
|
+
route._handlerType = 2; // pre-built primitive
|
|
315
|
+
route._prebuilt = String(h);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
284
319
|
/**
|
|
285
320
|
* Wraps interceptors with adapt() for consistent behavior
|
|
286
321
|
* @param {Interceptor | Interceptor[]} intercept
|
|
@@ -492,6 +527,8 @@ const vibe = () => {
|
|
|
492
527
|
media: { public: true, dest: null },
|
|
493
528
|
};
|
|
494
529
|
trie.insert("GET", routePath, route);
|
|
530
|
+
routes.push(route);
|
|
531
|
+
options.routeCount++;
|
|
495
532
|
}
|
|
496
533
|
|
|
497
534
|
/**
|