@techiev2/vajra 1.5.0 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README +6 -0
- package/README.md +3 -0
- package/app.db +0 -0
- package/index.js +7 -6
- package/libs/data/dialects/mongo.js +3 -0
- package/libs/data/dialects/sqlite.js +44 -0
- package/libs/data/index.js +5 -0
- package/libs/data/sql.js +21 -0
- package/package.json +1 -1
- package/_ +0 -0
- package/_README.md +0 -226
- package/_index.js +0 -110
- package/benchmark.sh +0 -58
package/README
CHANGED
|
@@ -22,6 +22,12 @@ Like the Vajra, this server delivers maximum power in minimal form.
|
|
|
22
22
|
|
|
23
23
|
## Changelog
|
|
24
24
|
|
|
25
|
+
### 1.5.1 (2026-01-04)
|
|
26
|
+
- Adds a pre-allocated buffer based body reader for improvement.
|
|
27
|
+
|
|
28
|
+
### 1.5.0 (2026-01-03)
|
|
29
|
+
- Adds support for res.sendFile.
|
|
30
|
+
|
|
25
31
|
### 1.4.3 (2026-01-02)
|
|
26
32
|
- Adds guardrails for unsafe operations with template paths.
|
|
27
33
|
|
package/README.md
CHANGED
package/app.db
ADDED
|
Binary file
|
package/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createServer } from 'node:
|
|
1
|
+
import { createServer } from 'node:https'
|
|
2
2
|
import { readFile, access } from 'node:fs/promises'
|
|
3
3
|
import { createReadStream, existsSync } from 'node:fs'
|
|
4
4
|
import { resolve } from 'path'
|
|
@@ -62,13 +62,14 @@ export default class Vajra {
|
|
|
62
62
|
if (req.method === 'GET' || req.method === 'HEAD') { return runMiddlwares() }
|
|
63
63
|
async function runMiddlwares() { let idx = 0; const next = async () => { if (idx >= Vajra.#middlewares.length) { return setImmediate(handleRoute) } const fn = Vajra.#middlewares[idx]; idx++; try { await fn(req, res, next); } catch (err) { return default_500({ url: req.url, method: req.method }, res, err); } }; await next(); }
|
|
64
64
|
setImmediate(() => {
|
|
65
|
-
req.body = {}; req.rawData = ''; req.formData = {}; let
|
|
66
|
-
req.on('data', (chunk) => {
|
|
65
|
+
req.body = {}; req.rawData = ''; req.formData = {}; let totalSize = 0; let chunks = []
|
|
66
|
+
req.on('data', (chunk) => { if (totalSize + chunk.length > Vajra.#MAX_FILE_SIZE) { return default_413(res) }; chunks.push(chunk); totalSize += chunk.length; })
|
|
67
67
|
const formDataMatcher = /Content-Disposition: form-data; name=['"](?<name>[^"']+)['"]\s+(?<value>.*?)$/smi;
|
|
68
68
|
let boundaryMatch = (req.headers['Content-Type'] || '').match(/boundary=(.*)/); const boundary = boundaryMatch ? '--' + boundaryMatch[1] : null; const fileDataMatcher = /^Content-Disposition:.*?name=["'](?<field>[^"']+)["'].*?filename=["'](?<fileName>[^"']+)["'].*?Content-Type:\s*(?<contentType>[^\r\n]*)\r?\n\r?\n(?<content>[\s\S]*)$/ims
|
|
69
69
|
req.on('end', async () => {
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
const buffer = Buffer.allocUnsafe(totalSize); let offset = 0;
|
|
71
|
+
for (const chunk of chunks) { chunk.copy(buffer, offset); offset += chunk.length; }
|
|
72
|
+
req.rawData = buffer.toString(); req.files = []; if (boundary) { req.rawData.split(boundary).filter(Boolean).map((line) => {
|
|
72
73
|
let key, value; if (line.includes('filename')) { req.files.push(fileDataMatcher.exec(line)?.groups || {}); return }
|
|
73
74
|
[key, value] = Object.values(line.match(formDataMatcher)?.groups || {}); (key && value) && Object.assign(req.formData, { [key]: value }); return
|
|
74
75
|
})
|
|
@@ -104,4 +105,4 @@ export default class Vajra {
|
|
|
104
105
|
}
|
|
105
106
|
const defaults = Object.freeze(Object.assign({}, { use, setProperty, start, log }, Object.fromEntries('get__post__put__patch__delete__head__options'.split('__').map((method) => [method, (...args) => register(method, ...args)])))); return Object.assign({}, { start }, defaults)
|
|
106
107
|
}
|
|
107
|
-
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite'
|
|
2
|
+
|
|
3
|
+
export default class Sqlite {
|
|
4
|
+
#tableName
|
|
5
|
+
#db
|
|
6
|
+
#schema
|
|
7
|
+
constructor(tableName, dbPath) {
|
|
8
|
+
if (typeof dbPath !== 'string') { throw new Exception("Db path for sqlite needs to be a string.") }
|
|
9
|
+
this.#tableName = tableName; this.#db = new DatabaseSync(dbPath)
|
|
10
|
+
}
|
|
11
|
+
get schema() {
|
|
12
|
+
if (this.#schema) return this.#schema
|
|
13
|
+
this.#schema = this._schema; return this._schema
|
|
14
|
+
}
|
|
15
|
+
get _schema() {
|
|
16
|
+
let pk_
|
|
17
|
+
const _fields = []
|
|
18
|
+
this.#db.prepare(`SELECT * FROM pragma_table_info('${this.#tableName}');`).all().forEach(({ name, type, notnull, dflt_value: defaultValue, pk }) => {
|
|
19
|
+
const isPk = !!pk; const nullable = !notnull
|
|
20
|
+
if (isPk) { pk_ = name }
|
|
21
|
+
_fields.push([name, { name, isPk, nullable, defaultValue }])
|
|
22
|
+
})
|
|
23
|
+
const __schema = Object.fromEntries(_fields)
|
|
24
|
+
Object.assign(__schema, { pk: pk_ })
|
|
25
|
+
return __schema
|
|
26
|
+
}
|
|
27
|
+
async find(options, rest) {
|
|
28
|
+
let query, fields
|
|
29
|
+
if (typeof options === 'object' && options.query) {
|
|
30
|
+
({ query = {}, projection: fields = [] } = options)
|
|
31
|
+
} else { query = options; fields = rest }
|
|
32
|
+
if (!Object.keys(query).length) { throw new Error('Empty raw queries are unsafe and not allowed.') }
|
|
33
|
+
const queryFields = Object.keys(query)
|
|
34
|
+
const queryValues = Object.values(query)
|
|
35
|
+
const validFields = queryFields.filter((field) => !!this.schema[field])
|
|
36
|
+
if (!validFields.length) { throw new Error("No valid fields in query.") }
|
|
37
|
+
const validProjection = fields.filter(field => !!this.schema[field])
|
|
38
|
+
let projection = (validProjection.length ? validProjection : Object.keys(this.schema))
|
|
39
|
+
if (this.schema.pk && !projection.includes(this.schema.pk)) {
|
|
40
|
+
projection = [this.schema.pk, ...projection]
|
|
41
|
+
}
|
|
42
|
+
return this.#db.prepare(`select ${projection.join(',')} from ${this.#tableName} where ${validFields.map((field) => `${field} = ?`).join(' and ')}`).all(...queryValues)
|
|
43
|
+
}
|
|
44
|
+
}
|
package/libs/data/sql.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const skipProps = new Set(["constructor", "schema", "_schema"])
|
|
2
|
+
|
|
3
|
+
export default class Table {
|
|
4
|
+
#provider
|
|
5
|
+
#setup(dialect) {
|
|
6
|
+
Object.getOwnPropertyNames(dialect.prototype)
|
|
7
|
+
.forEach((k) => { if (!skipProps.has(k)) { this[k] = this.#provider[k].bind(this.#provider) } })
|
|
8
|
+
}
|
|
9
|
+
get name() { return `${this.constructor.name.toLowerCase()}s` }
|
|
10
|
+
constructor(dialect, dbConfig = {}) {
|
|
11
|
+
switch (dialect?.name) {
|
|
12
|
+
case 'Sqlite':
|
|
13
|
+
this.#provider = new dialect(this.name, dbConfig);
|
|
14
|
+
this.#setup(dialect)
|
|
15
|
+
break;
|
|
16
|
+
default:
|
|
17
|
+
console.error(`Unknown/invalid dialect`)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@techiev2/vajra",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.2",
|
|
4
4
|
"description": "Blazing-fast, zero-dependency Node.js server with routing, middleware, multipart uploads, and templating. 111 lines · ~95k req/s · ~52 MB idle.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"http-server",
|
package/_
DELETED
|
File without changes
|
package/_README.md
DELETED
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
# Vajra ⚡
|
|
2
|
-
|
|
3
|
-
 <!-- or use one of the above hosted URLs if you like -->
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
**Ultra-minimal, zero-dependency Node.js HTTP server**
|
|
7
|
-
Routing · Middleware · Multipart parsing · HTML templating
|
|
8
|
-
All in **111 lines** of pure JavaScript
|
|
9
|
-
|
|
10
|
-
## Name Origin
|
|
11
|
-
|
|
12
|
-
Vajra draws from the Rigvedic thunderbolt weapon of Indra — crafted from the bones of Sage Dadhichi, symbolizing unbreakable strength through selfless sacrifice.
|
|
13
|
-
|
|
14
|
-
Like the Vajra, this server delivers maximum power in minimal form.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
[](https://www.npmjs.com/package/@techiev2/vajra)
|
|
18
|
-
[](https://www.npmjs.com/package/@techiev2/vajra)
|
|
19
|
-
[](https://nodejs.org)
|
|
20
|
-
[](LICENSE)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
## Changelog
|
|
24
|
-
|
|
25
|
-
### 1.4.3 (2026-01-02)
|
|
26
|
-
- Adds guardrails for unsafe operations with template paths.
|
|
27
|
-
|
|
28
|
-
### 1.4.2 (2026-01-02)
|
|
29
|
-
- Fixes bug in parsing params that dropped file extensions.
|
|
30
|
-
|
|
31
|
-
### 1.4.1 (2026-01-01)
|
|
32
|
-
- Added support to handle drift in system time after signing
|
|
33
|
-
|
|
34
|
-
### 1.4.0 (2025-12-31)
|
|
35
|
-
- Added full HS256 JWT support (`@techiev2/vajra/libs/auth/jwt.js`)
|
|
36
|
-
- Ultra-minimal, zero-dependency implementation
|
|
37
|
-
- Key and header caching for maximum performance
|
|
38
|
-
- Robust base64url handling
|
|
39
|
-
- Numeric exp validation and expiration checks
|
|
40
|
-
|
|
41
|
-
### 1.3.0 (2025-12-30)
|
|
42
|
-
- Performance improvements to routing in bare routes
|
|
43
|
-
|
|
44
|
-
### 1.2.0 (2025-12-30)
|
|
45
|
-
- Adds cookie support
|
|
46
|
-
|
|
47
|
-
### 1.0.0 (2025-12-25)
|
|
48
|
-
- Initial release
|
|
49
|
-
|
|
50
|
-
## Features
|
|
51
|
-
|
|
52
|
-
- Zero external dependencies
|
|
53
|
-
- Built-in routing with named parameters (`:id`)
|
|
54
|
-
- Asynchronous batched logging for performance
|
|
55
|
-
- Global middleware support with `next()` chaining
|
|
56
|
-
- JSON, urlencoded, and **multipart/form-data** body parsing
|
|
57
|
-
- Fast HTML templating with loops, nested objects, and simple array headers
|
|
58
|
-
- Helper methods: `res.json()`, `res.html()`, `res.status()`, `res.writeMessage()`
|
|
59
|
-
- Payload size limiting with 413 responses
|
|
60
|
-
- Sensible defaults for 404/405/500
|
|
61
|
-
|
|
62
|
-
## Performance (Apple M4, Node 20+)
|
|
63
|
-
|
|
64
|
-
| Test Case | Vajra | Express + Multer | Notes |
|
|
65
|
-
|------------------------------------------------|----------------|------------------|---------------------------|
|
|
66
|
-
| 1MB Multipart Upload (wrk -t16 -c600) | **~94–98k req/s** | ~72k req/s | +30% faster |
|
|
67
|
-
| Idle RSS | ~52–53 MB | ~44 MB | Zero deps vs extra packages |
|
|
68
|
-
| Peak RSS under load | ~228 MB | ~209 MB | Full buffering trade-off |
|
|
69
|
-
| Code size (source) | **111 lines** | ~2k+ lines | Hand-crafted minimalism |
|
|
70
|
-
|
|
71
|
-
## Performance Benchmarks (wrk)
|
|
72
|
-
|
|
73
|
-

|
|
74
|
-
|
|
75
|
-
## Installation
|
|
76
|
-
|
|
77
|
-
```bash
|
|
78
|
-
npm install vajra
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
## Quick Start
|
|
83
|
-
```JavaScript
|
|
84
|
-
import Vajra from '../index.js';
|
|
85
|
-
import { encode } from 'node:querystring';
|
|
86
|
-
|
|
87
|
-
async function getUsers(query = {}) {
|
|
88
|
-
return (await fetch(`https://jsonplaceholder.typicode.com/users?${encode(query)}`)).json()
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const { get, post, use, start, setProperty, log } = Vajra.create();
|
|
92
|
-
|
|
93
|
-
setProperty({ viewsRoot: `${import.meta.url}/views` })
|
|
94
|
-
// Or as a key-value pair
|
|
95
|
-
// setProperty('viewsRoot', `${import.meta.url}/views`)
|
|
96
|
-
|
|
97
|
-
use((req, res, next) => {
|
|
98
|
-
// Vajra provides an async batched logger to provide a balance between 100% log coverage and performance.
|
|
99
|
-
// If you prefer blocking immediate logs, you can switch to console.log
|
|
100
|
-
// or any other library of your choice.
|
|
101
|
-
log(`${req.method} ${req.url}`);
|
|
102
|
-
next();
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
get('/', (req, res) => {
|
|
106
|
-
res.writeMessage('Hello from Vajra ⚡');
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
post('/upload', (req, res) => {
|
|
110
|
-
res.json({ received: true, filesCount: req.files.length, files: req.files, body: req.body });
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
start({ port: 4002 }, () => {
|
|
114
|
-
console.log('Ready at http://localhost:4002');
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
get('/api/users', async ({ query }, res) => {
|
|
118
|
-
const users = await getUsers(query)
|
|
119
|
-
return res.json({ users })
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
get('/web/users', async ({ query }, res) => {
|
|
123
|
-
const users = await getUsers(query)
|
|
124
|
-
const headers = Object.keys(users[0])
|
|
125
|
-
return res.html(`users.html`, { users, headers })
|
|
126
|
-
})
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
## HTML Templating
|
|
130
|
-
```JavaScript
|
|
131
|
-
import Vajra from '../index.js';
|
|
132
|
-
const { get, post, use, start, setProperty } = Vajra.create();
|
|
133
|
-
|
|
134
|
-
get('/users', (req, res) => {
|
|
135
|
-
const data = {
|
|
136
|
-
users: [
|
|
137
|
-
{ id: 1, name: 'Alice' },
|
|
138
|
-
{ id: 2, name: 'Bob' }
|
|
139
|
-
],
|
|
140
|
-
headers: ['ID', 'Name']
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
// If no view root is set, .html() expects the absolute path.
|
|
144
|
-
res.html('views/users.html', data);
|
|
145
|
-
});
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
#### views/users.html
|
|
149
|
-
```html
|
|
150
|
-
<table>
|
|
151
|
-
<thead>
|
|
152
|
-
{{# headers }}
|
|
153
|
-
<th>{{ header@ }}</th>
|
|
154
|
-
{{/ headers }}
|
|
155
|
-
</thead>
|
|
156
|
-
<tbody>
|
|
157
|
-
{{# users }}
|
|
158
|
-
<tr>
|
|
159
|
-
<td>{{ id }}</td>
|
|
160
|
-
<td>{{ name }}</td>
|
|
161
|
-
</tr>
|
|
162
|
-
{{/ users }}
|
|
163
|
-
</tbody>
|
|
164
|
-
</table>
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
Supports:
|
|
168
|
-
|
|
169
|
-
- Loops ({{# array }} ... {{/ array }})
|
|
170
|
-
- Dot notation ({{ user.name }})
|
|
171
|
-
- Special header shorthand ({{ header@ }} for simple arrays)
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
## Configuration
|
|
175
|
-
```JavaScript
|
|
176
|
-
const app = vajra.create({
|
|
177
|
-
maxFileSize: 10 // in MB (default: 2)
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
// Set view root path
|
|
181
|
-
app.setProperty('viewRoot', './views');
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
## API
|
|
186
|
-
|
|
187
|
-
- `get/post/put/patch/delete/head/options(path, handler)`
|
|
188
|
-
- `use(middleware)`
|
|
189
|
-
- `start({ port, host }, callback?)`
|
|
190
|
-
- `setProperty(key, value)` or `setProperty({ key: value })`
|
|
191
|
-
- `log(message)`
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
#### Response helpers:
|
|
195
|
-
|
|
196
|
-
`res.status(code)`
|
|
197
|
-
`res.json(data)`
|
|
198
|
-
`res.writeMessage(text)`
|
|
199
|
-
`res.html(pathOrString, data)`
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
## Philosophy
|
|
203
|
-
|
|
204
|
-
Vajra is built on the principle that minimalism can maximise outcomes.
|
|
205
|
-
|
|
206
|
-
Everything you need for real internal tools, admin panels, APIs, and prototypes — without the bloat.
|
|
207
|
-
|
|
208
|
-
No dependencies.
|
|
209
|
-
No build step.
|
|
210
|
-
Just copy `index.js` and go.
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
## Benchmarks & Memory
|
|
214
|
-
Run under extreme multipart load (wrk -t16 -c600 -d30s 1MB payloads):
|
|
215
|
-
|
|
216
|
-
Throughput: ~95k req/s
|
|
217
|
-
Idle RSS: ~52 MB
|
|
218
|
-
Peak under load: ~228 MB (drops back on idle)
|
|
219
|
-
|
|
220
|
-
## License
|
|
221
|
-
MIT
|
|
222
|
-
|
|
223
|
-
## Credits
|
|
224
|
-
Hand-crafted by [[Sriram Velamur](https://linkedin.com/in/techiev2)/[@techiev2](https://x.com/techiev2)]
|
|
225
|
-
|
|
226
|
-
Inspired by the desire for a truly tiny, powerful, and dependency-free Node server.
|
package/_index.js
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import { createServer } from 'node:http'
|
|
2
|
-
import { readFile, access } from 'node:fs/promises'
|
|
3
|
-
import { resolve, isAbsolute } from 'path'
|
|
4
|
-
|
|
5
|
-
const BLOCK_MATCHER=/{{\s*#\s*(?<grpStart>\w+)\s*}}\s*(?<block>.*?)\s*{{\s*\/\s*(?<grpEnd>\w+)\s*}}/gmsi; const INNER_BLOCK_MATCHER = /{\s*(.*?)\s*}/gmsi
|
|
6
|
-
const LOOP_MATCHER=/({\s*\w+@\s*})/gmis; const ESCAPE = { regex: /[&<>"']/g, map: { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }}
|
|
7
|
-
|
|
8
|
-
function default_404({ url, method, isPossibleJSON }, res) {
|
|
9
|
-
const message = `Route ${url} not found for method ${method}.`
|
|
10
|
-
res.status(404); return isPossibleJSON ? res.json({ message }) : res.writeMessage(message)
|
|
11
|
-
}
|
|
12
|
-
function default_405({ url, method, isPossibleJSON }, res) {
|
|
13
|
-
const message = `Method ${method} not allowed by route ${url}.`
|
|
14
|
-
res.status(405); return isPossibleJSON ? res.json({ message }) : res.writeMessage(message)
|
|
15
|
-
}
|
|
16
|
-
function default_500({ url, method }, res, error) {
|
|
17
|
-
process.env.DEBUG && console.log({ error })
|
|
18
|
-
res.status(500).writeMessage(process.env.DEBUG ? `${error.stack}` : `Server error.\nRoute: ${url}\nMethod: ${method}\nTimestamp: ${new Date().getTime()}\n`)
|
|
19
|
-
}
|
|
20
|
-
function default_413(res) {
|
|
21
|
-
res.status(413).writeMessage('Payload Too Large')
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const MAX_MB = 2; const MAX_FILE_SIZE = MAX_MB * 1024 * 1024
|
|
25
|
-
|
|
26
|
-
export default class Vajra {
|
|
27
|
-
static #app; static #routes = {}; static #middlewares = []; static #straightRoutes = {}; static #MAX_FILE_SIZE; static #onCreate; static #props = {}
|
|
28
|
-
static create({ maxFileSize } = { maxFileSize: 2 }) {
|
|
29
|
-
Vajra.#app = createServer()
|
|
30
|
-
const _queue = []; const LOG_QUEUE_SIZE = 100; const logOut = () => { if (_queue.length) { process.stdout.write(`${_queue.join('').trim()}\n`) }; _queue.length = 0 };
|
|
31
|
-
const flushAndShutDown = () => { logOut(); Vajra.#app.close(() => { process.exit(0); }); }; 'SIGINT_SIGTERM_SIGABRT'.split('_').map((evt) => process.on(evt, flushAndShutDown));
|
|
32
|
-
process.on('exit', logOut); function log(message) { _queue.push(`${message}\n`); if (_queue.length >= LOG_QUEUE_SIZE) { logOut(); _queue.length = 0; } }
|
|
33
|
-
Vajra.#MAX_FILE_SIZE = !+MAX_FILE_SIZE ? +maxFileSize * 1024 * 1024 : MAX_FILE_SIZE
|
|
34
|
-
Vajra.#app.on('request', async (req, res) => {
|
|
35
|
-
if (+(req.headers['Content-Length'] || req.headers['content-length']) > Vajra.#MAX_FILE_SIZE) { return default_413(res) }
|
|
36
|
-
res.sent = false; res.status = (/**@type{code} Number*/ code) => { if (!+code || +code < 100 || +code > 599) { throw new Error(`Invalid status code ${code}`) }; res.statusCode = code; res.statusSet = true; return res }
|
|
37
|
-
res.json = data => {
|
|
38
|
-
if (res.sent) return res; if (!res.statusSet) res.statusCode = 200
|
|
39
|
-
const response = JSON.stringify(data); res.sent = true; res.setHeader('Content-Type', 'application/json'); res.setHeader('Content-Length', Buffer.from(response).byteLength); res.write(response); res.end(); return res
|
|
40
|
-
}
|
|
41
|
-
res.writeMessage = (message = '') => {
|
|
42
|
-
if (res.sent) { return res }; if (!message) { res.status(500); message = 'Server error'; }
|
|
43
|
-
res.sent = true; res.setHeader('Content-Type', 'text/plain'); res.setHeader('Content-Length', Buffer.from(message).byteLength); res.write(message); res.end()
|
|
44
|
-
return res
|
|
45
|
-
}
|
|
46
|
-
function sanitize(value) { if (value == null) return ''; return String(value).replace(ESCAPE.regex, m => ESCAPE.map[m]); }
|
|
47
|
-
res.html = async (templatePath, data = {}) => {
|
|
48
|
-
templatePath = resolve(templatePath); const appRoot = resolve(process.cwd()); const root = this.#props.viewRoot ? resolve(this.#props.viewRoot) : appRoot;
|
|
49
|
-
const _templatePath = resolve(`${root}/${templatePath}`)
|
|
50
|
-
if (templatePath.includes('..') || !_templatePath.startsWith(appRoot) || !root.startsWith(appRoot)) { return default_500(req, res, new Error("Invalid template path")) }
|
|
51
|
-
if (res.sent) { return res }; let content; templatePath = `${root}/${_templatePath}`
|
|
52
|
-
try { await access(templatePath); content = (await readFile(templatePath)).toString() } catch (_) { content = templatePath }
|
|
53
|
-
content.matchAll(BLOCK_MATCHER).forEach((match) => { if (!match?.groups || (match.groups.grpStart !== match.groups.grpEnd)) { return }; const data_ = (data[match.groups.grpStart] || []); if (match.groups.block.indexOf('@') !== -1) { content = content.replace(match[2], data_.map((key) => match.groups.block.replace(LOOP_MATCHER, sanitize(key))).join('')); return }
|
|
54
|
-
if (!data_.length) { content = content.replace(match.groups.block, '') } else { data_.forEach((dataItem) => { match.groups.block.matchAll(INNER_BLOCK_MATCHER).forEach((_match) => { const parts = _match[1].split('.').slice(1); let value = dataItem; parts.forEach((part) => (value = value ? value[part] : value)); content = content.replace(_match[0], sanitize(value)) }) }) }
|
|
55
|
-
}); content = content.replace(/{{\s*# .*?\s*}}/gmsi, '').replace(/{{\s*\/.*?}}/gmsi, '')
|
|
56
|
-
if (!res.statusSet) res.statusCode = 200; res.sent = true; res.setHeader('Content-Type', 'text/html'); res.setHeader('Content-Length', Buffer.from(content).byteLength); res.write(content); res.end(); return res
|
|
57
|
-
}
|
|
58
|
-
req._headers = { ...req.headers }; req.headers = Object.fromEntries(req.rawHeaders.map((e, i) => i % 2 ? false : [e, req.rawHeaders[i + 1]]).filter(Boolean)); req.isPossibleJSON = req._headers['content-type'] === 'application/json'; req.params = {}
|
|
59
|
-
res.cookie = (k, v, options) => {
|
|
60
|
-
let { expires, path, maxAge, domain, secure, httpOnly, sameSite } = typeof options === 'object' ? options : typeof v === 'object' ? v : {}; const cookieOpts = [];!isNaN(+maxAge) && cookieOpts.push(`Max-Age=${Math.floor(maxAge)}`); !isNaN(+expires) && cookieOpts.push(`Expires=${new Date(expires).toUTCString()}`); expires instanceof Date && cookieOpts.push(`Expires=${expires.toUTCString()}`)
|
|
61
|
-
path && cookieOpts.push(`Path=${path}`); domain && cookieOpts.push(`Domain=${domain}`); !!secure && cookieOpts.push(`Secure`); !!httpOnly && cookieOpts.push(`HttpOnly`); sameSite = sameSite && (sameSite === true ? 'Strict' : typeof sameSite === 'string' ? sameSite.charAt(0).toUpperCase() + sameSite.slice(1).toLowerCase() : ''); !['Strict', 'Lax', 'None'].includes(sameSite) ? sameSite = 'Strict' : sameSite = sameSite; cookieOpts.push(`SameSite=${sameSite}`)
|
|
62
|
-
res.setHeader('Set-Cookie', (typeof k === 'object') ? Object.entries(k).map(([k_, v_]) => `${k_}=${encodeURIComponent(v_)}${cookieOpts.length ? `; ${cookieOpts.join('; ')}` : ''}`) : [...(res.getHeader('Set-Cookie') || []).filter(Boolean), `${k}=${encodeURIComponent(v)}${cookieOpts.length ? `; ${cookieOpts.join('; ')}` : ''}`])
|
|
63
|
-
}
|
|
64
|
-
let url = `http://${req.headers.host || req.headers.host}/${req.url}`; req.query = Object.fromEntries(new URL(url).searchParams)
|
|
65
|
-
if (req.method === 'GET' || req.method === 'HEAD') { return runMiddlwares() }
|
|
66
|
-
async function runMiddlwares() { let idx = 0; const next = async () => { if (idx >= Vajra.#middlewares.length) { return setImmediate(handleRoute) } const fn = Vajra.#middlewares[idx]; idx++; try { await fn(req, res, next); } catch (err) { return default_500({ url: req.url, method: req.method }, res, err); } }; await next(); }
|
|
67
|
-
setImmediate(() => {
|
|
68
|
-
req.body = {}; req.rawData = ''; req.formData = {}; let dataSize = 0
|
|
69
|
-
req.on('data', (chunk) => { dataSize += chunk.length; if (dataSize > Vajra.#MAX_FILE_SIZE) { return default_413(res) }; req.rawData+=chunk })
|
|
70
|
-
const formDataMatcher = /Content-Disposition: form-data; name=['"](?<name>[^"']+)['"]\s+(?<value>.*?)$/smi;
|
|
71
|
-
let boundaryMatch = (req.headers['Content-Type'] || '').match(/boundary=(.*)/); const boundary = boundaryMatch ? '--' + boundaryMatch[1] : null; const fileDataMatcher = /^Content-Disposition:.*?name=["'](?<field>[^"']+)["'].*?filename=["'](?<fileName>[^"']+)["'].*?Content-Type:\s*(?<contentType>[^\r\n]*)\r?\n\r?\n(?<content>[\s\S]*)$/ims
|
|
72
|
-
req.on('end', async () => {
|
|
73
|
-
req.files = []; if (boundary) { req.rawData.split(boundary).filter(Boolean).map((line) => {
|
|
74
|
-
let key, value; if (line.includes('filename')) { req.files.push(fileDataMatcher.exec(line)?.groups || {}); return }
|
|
75
|
-
[key, value] = Object.values(line.match(formDataMatcher)?.groups || {}); (key && value) && Object.assign(req.formData, { [key]: value }); return
|
|
76
|
-
})
|
|
77
|
-
}
|
|
78
|
-
if (Object.keys(req.formData).length) { req.body = req.formData } else {
|
|
79
|
-
try { req.body = JSON.parse(req.rawData); req.isPossibleJSON = true } catch (_) { req.body = Object.fromEntries(req.rawData.split('&').map((pair) => pair.split('='))) }
|
|
80
|
-
}; setImmediate(runMiddlwares)
|
|
81
|
-
})
|
|
82
|
-
req.cookies = Object.fromEntries((req.headers.Cookie || req.headers.cookie || '').split(/;\s*/).map((k) => k.split('=')).map(([k, v]) => [k.trim(), decodeURIComponent(v).trim()]))
|
|
83
|
-
})
|
|
84
|
-
async function handleRoute() {
|
|
85
|
-
let _url = req.url.split('?')[0]; if (_url.endsWith('/')) _url = _url.split('/').slice(0, -1).join('/')
|
|
86
|
-
let match_; const directHandler = (Vajra.#straightRoutes[_url] || Vajra.#straightRoutes[`${_url}/`] || {})[req.method.toLowerCase()]
|
|
87
|
-
if (directHandler) { try { await directHandler(req, res); if (!res.sent && !res.writableEnded) res.end() } catch (error) { return default_500(req, res, error) }; return }
|
|
88
|
-
Object.entries(Vajra.#routes).map(([route, handler]) => {
|
|
89
|
-
if (match_) { return }; const match = new RegExp(route).exec(req.url); if (!!match && handler[req.method.toLowerCase()]) { match_ = handler; Object.assign(req.params, match.groups); return }
|
|
90
|
-
})
|
|
91
|
-
if (!match_) { return default_404(req, res) }
|
|
92
|
-
if (!match_[req.method.toLowerCase()]) { return default_405(req, res) }
|
|
93
|
-
try { await match_[req.method.toLowerCase()](req, res); if (!res.sent && !res.writableEnded) res.end() } catch (error) { return default_500(req, res, error) }
|
|
94
|
-
}
|
|
95
|
-
})
|
|
96
|
-
function setProperty(k, v) { Object.assign(Vajra.#props, typeof k == 'object' ? k: { k: v }); return defaults }
|
|
97
|
-
function start({ port, host = '127.0.0.1' }, cb) { Vajra.#app.listen(port, () => { console.log(`App listening at http://${host}:${port}`); if (typeof cb === 'function') { cb() } }); return defaults }
|
|
98
|
-
function register(method, path, handler) {
|
|
99
|
-
const paramMatcher = /.*?(?<param>\:[a-zA-Z]{1,})/g; let pathMatcherStr = path
|
|
100
|
-
path.matchAll(paramMatcher).forEach(match => pathMatcherStr = pathMatcherStr.replace(match.groups.param, `{0,1}(?<${match.groups.param.slice(1)}>[\\w|\.]+)`))
|
|
101
|
-
if (path !== '/' && pathMatcherStr.endsWith('/')) { pathMatcherStr = pathMatcherStr.replace(/(\/)$/, '/?') }
|
|
102
|
-
if (!paramMatcher.exec(path)?.groups) { Vajra.#straightRoutes[pathMatcherStr] = Object.assign(Vajra.#straightRoutes[pathMatcherStr] || {}, { [method]: handler }); return }
|
|
103
|
-
Vajra.#routes[pathMatcherStr] = Object.assign(Vajra.#routes[pathMatcherStr] || {}, {[method]: handler}); return defaults
|
|
104
|
-
}
|
|
105
|
-
function use(fn) {
|
|
106
|
-
if (typeof fn !== "function") { throw new Error(`${fn} is not a function. Can't use as middleware`) }; Vajra.#middlewares.push(fn); return defaults
|
|
107
|
-
}
|
|
108
|
-
const defaults = Object.freeze(Object.assign({}, { use, setProperty, start, log }, Object.fromEntries('get__post__put__patch__delete__head__options'.split('__').map((method) => [method, (...args) => register(method, ...args)])))); return Object.assign({}, { start }, defaults)
|
|
109
|
-
}
|
|
110
|
-
}
|
package/benchmark.sh
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
set -euo pipefail
|
|
3
|
-
declare -A portMap=(
|
|
4
|
-
["elysia"]=4000
|
|
5
|
-
["express"]=4001
|
|
6
|
-
["fastify"]=4002
|
|
7
|
-
["hono"]=4003
|
|
8
|
-
["vajra"]=4004
|
|
9
|
-
)
|
|
10
|
-
declare -A runnerMap=(
|
|
11
|
-
["elysia"]="index.js"
|
|
12
|
-
["elysia_node"]="index.js"
|
|
13
|
-
["express"]="index.js"
|
|
14
|
-
["fastify"]="index.js"
|
|
15
|
-
["hono"]="index.js"
|
|
16
|
-
["vajra"]="examples/api.js"
|
|
17
|
-
)
|
|
18
|
-
frameworks=$(ls -d */ | sed 's|/$||' | grep -v auth | grep vajra)
|
|
19
|
-
|
|
20
|
-
LOOPS=5
|
|
21
|
-
|
|
22
|
-
runWrk() {
|
|
23
|
-
port=$1
|
|
24
|
-
sync && echo 3 > tee /proc/sys/vm/drop_caches
|
|
25
|
-
wrk -t16 -c600 -d10s \
|
|
26
|
-
-H "Content-Type: multipart/form-data; boundary=BOUNDARY123456" \
|
|
27
|
-
"http://localhost:${port}/upload" < ../test_upload_2mb.txt
|
|
28
|
-
}
|
|
29
|
-
main() {
|
|
30
|
-
echo "" > report.txt
|
|
31
|
-
for framework in ${frameworks[@]}; do
|
|
32
|
-
cd $framework
|
|
33
|
-
mkdir -p benchmarks
|
|
34
|
-
echo "" > benchmarks/report.txt
|
|
35
|
-
port=${portMap[$framework]}
|
|
36
|
-
runner=${runnerMap[$framework]}
|
|
37
|
-
for i in $(seq 1 $LOOPS); do
|
|
38
|
-
bun $runner &
|
|
39
|
-
sleep 2
|
|
40
|
-
runningPID=$(lsof | grep -P ":$port" | head -n1 | awk '{print $2}')
|
|
41
|
-
echo "$framework running at $runningPID"
|
|
42
|
-
sleep 2
|
|
43
|
-
runWrk $port >> benchmarks/report.txt
|
|
44
|
-
sleep 2
|
|
45
|
-
echo "Killing $framework running at $runningPID"
|
|
46
|
-
kill -9 $runningPID
|
|
47
|
-
done
|
|
48
|
-
avg_rps=$(grep "Requests/sec:" benchmarks/report.txt | awk -F' ' '{print $2}' | awk '{x+=$1; n++} END { print x/n}')
|
|
49
|
-
avg_tps=$(grep "Transfer/sec:" benchmarks/report.txt | awk -F' ' '{print $2}' | awk '{x+=$1; n++} END { print x/n}')
|
|
50
|
-
echo "${framework}
|
|
51
|
-
Average RPS: ${avg_rps}/s
|
|
52
|
-
Average TPS: ${avg_tps}MB/s
|
|
53
|
-
|
|
54
|
-
" >> "../report.txt"
|
|
55
|
-
cd ..
|
|
56
|
-
done
|
|
57
|
-
}
|
|
58
|
-
main
|