nitro5 1.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/.install.js +158 -0
- package/README.md +258 -0
- package/binding.gyp +25 -0
- package/index.js +54 -0
- package/native/nitro5.cc +742 -0
- package/nitro5.config.js +8 -0
- package/package.json +62 -0
- package/public/app.tsx +10 -0
- package/public/index.html +10 -0
- package/public/main.tsx +5 -0
- package/src/app.js +1162 -0
- package/src/cache.js +53 -0
- package/src/dashboard.js +215 -0
- package/src/deps-cache.js +37 -0
- package/src/disk-cache.js +37 -0
- package/src/file-worker.js +19 -0
- package/src/hmr.js +38 -0
- package/src/logger.js +51 -0
- package/src/mime.js +32 -0
- package/src/msg.js +16 -0
- package/src/native.js +75 -0
- package/src/router.js +85 -0
- package/src/stat.js +25 -0
- package/src/static.js +201 -0
- package/src/stats.js +25 -0
- package/src/supervisor.js +249 -0
- package/src/thread-pool.js +107 -0
- package/src/tsc.js +329 -0
- package/src/vite.js +15 -0
- package/src/watcher.js +63 -0
package/src/cache.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import ms from "ms";
|
|
2
|
+
|
|
3
|
+
export class MemoryCache {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.store = new Map();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
set(key, value, ttl) {
|
|
9
|
+
const ttlMs = typeof ttl === "string" ? ms(ttl) : ttl;
|
|
10
|
+
const expiresAt = Date.now() + ttlMs;
|
|
11
|
+
|
|
12
|
+
this.store.set(key, {
|
|
13
|
+
value,
|
|
14
|
+
expiresAt
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get(key) {
|
|
19
|
+
const item = this.store.get(key);
|
|
20
|
+
if (!item) return null;
|
|
21
|
+
|
|
22
|
+
if (Date.now() > item.expiresAt) {
|
|
23
|
+
this.store.delete(key);
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return item.value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
delete(key) {
|
|
31
|
+
this.store.delete(key);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
clear() {
|
|
35
|
+
this.store.clear();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function matchesCacheRule(filePath, patterns = []) {
|
|
40
|
+
const lower = filePath.toLowerCase();
|
|
41
|
+
|
|
42
|
+
return patterns.some((pattern) => {
|
|
43
|
+
const p = pattern.toLowerCase();
|
|
44
|
+
|
|
45
|
+
if (p === "*.css") return lower.endsWith(".css");
|
|
46
|
+
if (p === "*.js") return lower.endsWith(".js");
|
|
47
|
+
if (p === "*.svg") return lower.endsWith(".svg");
|
|
48
|
+
if (p === "*.webp") return lower.endsWith(".webp");
|
|
49
|
+
if (p === "*.html") return lower.endsWith(".html");
|
|
50
|
+
|
|
51
|
+
return false;
|
|
52
|
+
});
|
|
53
|
+
}
|
package/src/dashboard.js
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { getStats } from "./stats.js";
|
|
2
|
+
import { getMetrics } from "./native.js";
|
|
3
|
+
|
|
4
|
+
/* =========================
|
|
5
|
+
EVENT BUS (Redis-like)
|
|
6
|
+
========================= */
|
|
7
|
+
class EventBus {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.channels = new Map();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
subscribe(event, fn) {
|
|
13
|
+
if (!this.channels.has(event)) {
|
|
14
|
+
this.channels.set(event, new Set());
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
this.channels.get(event).add(fn);
|
|
18
|
+
|
|
19
|
+
return () => this.channels.get(event)?.delete(fn);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
publish(event, data) {
|
|
23
|
+
const subs = this.channels.get(event);
|
|
24
|
+
if (!subs) return;
|
|
25
|
+
|
|
26
|
+
for (const fn of subs) {
|
|
27
|
+
try {
|
|
28
|
+
fn(data);
|
|
29
|
+
} catch (e) {
|
|
30
|
+
console.error("EventBus error:", e);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const bus = new EventBus();
|
|
37
|
+
|
|
38
|
+
/* =========================
|
|
39
|
+
SSE CLIENT STORE
|
|
40
|
+
========================= */
|
|
41
|
+
const clients = new Set();
|
|
42
|
+
const PING_INTERVAL_MS = 15000;
|
|
43
|
+
|
|
44
|
+
/* =========================
|
|
45
|
+
SAFE WRITE
|
|
46
|
+
========================= */
|
|
47
|
+
function safeWrite(res, data) {
|
|
48
|
+
if (!res || res.ended) return false;
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
if (typeof data === "string" || Buffer.isBuffer(data)) {
|
|
52
|
+
res.write(data);
|
|
53
|
+
} else {
|
|
54
|
+
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* =========================
|
|
63
|
+
REMOVE CLIENT
|
|
64
|
+
========================= */
|
|
65
|
+
function removeClient(res) {
|
|
66
|
+
clients.delete(res);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* =========================
|
|
70
|
+
EVENT BROADCAST
|
|
71
|
+
========================= */
|
|
72
|
+
export function sendEvent(data) {
|
|
73
|
+
bus.publish(data.type || "message", data);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* =========================
|
|
77
|
+
ADAPTIVE INTERVAL ENGINE
|
|
78
|
+
========================= */
|
|
79
|
+
function getLoadScore() {
|
|
80
|
+
const cpu = process.cpuUsage();
|
|
81
|
+
const mem = process.memoryUsage().heapUsed;
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
clients: clients.size,
|
|
85
|
+
cpu: cpu.user + cpu.system,
|
|
86
|
+
mem,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function calculateInterval() {
|
|
91
|
+
const load = getLoadScore();
|
|
92
|
+
|
|
93
|
+
let ms = 1000;
|
|
94
|
+
|
|
95
|
+
if (load.clients > 50) ms = 3000;
|
|
96
|
+
else if (load.clients > 20) ms = 2000;
|
|
97
|
+
else if (load.clients > 5) ms = 1000;
|
|
98
|
+
|
|
99
|
+
if (load.mem > 200 * 1024 * 1024) ms += 2000;
|
|
100
|
+
|
|
101
|
+
return ms;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* =========================
|
|
105
|
+
SSE HANDLER
|
|
106
|
+
========================= */
|
|
107
|
+
export function handleSSE(req, res) {
|
|
108
|
+
if (!req || !res) return;
|
|
109
|
+
|
|
110
|
+
const socket = req.socket;
|
|
111
|
+
|
|
112
|
+
// header support fallback
|
|
113
|
+
if (typeof res.stream === "function") {
|
|
114
|
+
res.stream(200, {
|
|
115
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
116
|
+
"Cache-Control": "no-cache, no-transform",
|
|
117
|
+
Connection: "keep-alive",
|
|
118
|
+
"X-Accel-Buffering": "no",
|
|
119
|
+
});
|
|
120
|
+
} else {
|
|
121
|
+
res.writeHead(200, {
|
|
122
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
123
|
+
"Cache-Control": "no-cache, no-transform",
|
|
124
|
+
Connection: "keep-alive",
|
|
125
|
+
"X-Accel-Buffering": "no",
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
clients.add(res);
|
|
130
|
+
|
|
131
|
+
/* INIT MESSAGE */
|
|
132
|
+
safeWrite(res, "retry: 1000\n\n");
|
|
133
|
+
|
|
134
|
+
safeWrite(res, {
|
|
135
|
+
type: "stats",
|
|
136
|
+
stats: getStats(),
|
|
137
|
+
metrics: getMetrics(),
|
|
138
|
+
ts: Date.now(),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
/* =========================
|
|
142
|
+
SUBSCRIBE CLIENT TO BUS
|
|
143
|
+
========================= */
|
|
144
|
+
const unsubscribe = bus.subscribe("stats", (data) => {
|
|
145
|
+
safeWrite(res, data);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
/* =========================
|
|
149
|
+
PING KEEP ALIVE
|
|
150
|
+
========================= */
|
|
151
|
+
const pingTimer = setInterval(() => {
|
|
152
|
+
if (res.ended) {
|
|
153
|
+
cleanup();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
res.write(": ping\n\n");
|
|
159
|
+
} catch {
|
|
160
|
+
cleanup();
|
|
161
|
+
}
|
|
162
|
+
}, PING_INTERVAL_MS);
|
|
163
|
+
|
|
164
|
+
pingTimer.unref?.();
|
|
165
|
+
|
|
166
|
+
/* =========================
|
|
167
|
+
CLEANUP
|
|
168
|
+
========================= */
|
|
169
|
+
const cleanup = () => {
|
|
170
|
+
clearInterval(pingTimer);
|
|
171
|
+
unsubscribe?.();
|
|
172
|
+
removeClient(res);
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
res.end?.();
|
|
176
|
+
} catch {}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/* SAFE SOCKET HOOK */
|
|
180
|
+
if (socket?.once) {
|
|
181
|
+
socket.once("close", cleanup);
|
|
182
|
+
socket.once("end", cleanup);
|
|
183
|
+
socket.once("error", cleanup);
|
|
184
|
+
} else {
|
|
185
|
+
req?.on?.("close", cleanup);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/* =========================
|
|
190
|
+
ADAPTIVE BROADCAST LOOP
|
|
191
|
+
========================= */
|
|
192
|
+
let lastPayload = null;
|
|
193
|
+
let intervalMs = 1000;
|
|
194
|
+
|
|
195
|
+
setInterval(() => {
|
|
196
|
+
intervalMs = calculateInterval();
|
|
197
|
+
}, 5000);
|
|
198
|
+
|
|
199
|
+
setInterval(() => {
|
|
200
|
+
if (clients.size === 0) return;
|
|
201
|
+
|
|
202
|
+
const payload = {
|
|
203
|
+
type: "stats",
|
|
204
|
+
stats: getStats(),
|
|
205
|
+
metrics: getMetrics(),
|
|
206
|
+
ts: Date.now(),
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const serialized = JSON.stringify(payload);
|
|
210
|
+
|
|
211
|
+
if (serialized === lastPayload) return;
|
|
212
|
+
lastPayload = serialized;
|
|
213
|
+
|
|
214
|
+
sendEvent(payload);
|
|
215
|
+
}, intervalMs).unref?.();
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const depGraph = new Map(); // file -> [deps]
|
|
2
|
+
const reverseGraph = new Map(); // dep -> [files]
|
|
3
|
+
|
|
4
|
+
const cache = new Map(); // file -> code
|
|
5
|
+
|
|
6
|
+
export function setCache(file, code) {
|
|
7
|
+
cache.set(file, code);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function getCache(file) {
|
|
11
|
+
return cache.get(file);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function hasCache(file) {
|
|
15
|
+
return cache.has(file);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function setDeps(file, deps = []) {
|
|
19
|
+
depGraph.set(file, deps);
|
|
20
|
+
|
|
21
|
+
for (const dep of deps) {
|
|
22
|
+
if (!reverseGraph.has(dep)) reverseGraph.set(dep, []);
|
|
23
|
+
reverseGraph.get(dep).push(file);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function invalidate(file) {
|
|
28
|
+
cache.delete(file);
|
|
29
|
+
|
|
30
|
+
const dependents = reverseGraph.get(file) || [];
|
|
31
|
+
|
|
32
|
+
for (const dep of dependents) {
|
|
33
|
+
cache.delete(dep);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
cache.delete(file);
|
|
37
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const CACHE_DIR = "./.nitro-cache";
|
|
5
|
+
|
|
6
|
+
if (!fs.existsSync(CACHE_DIR)) {
|
|
7
|
+
fs.mkdirSync(CACHE_DIR);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function safeName(file) {
|
|
11
|
+
return file.replace(/[\/\\:]/g, "_");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getDiskPath(file) {
|
|
15
|
+
return path.join(CACHE_DIR, safeName(file) + ".json");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function readDiskCache(file) {
|
|
19
|
+
const p = getDiskPath(file);
|
|
20
|
+
if (!fs.existsSync(p)) return null;
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(fs.readFileSync(p, "utf8"));
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function writeDiskCache(file, data) {
|
|
30
|
+
const p = getDiskPath(file);
|
|
31
|
+
fs.writeFileSync(p, JSON.stringify(data));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function clearDiskCache(file) {
|
|
35
|
+
const p = getDiskPath(file);
|
|
36
|
+
if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
37
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { parentPort } from "node:worker_threads";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
parentPort.on("message", async (message) => {
|
|
5
|
+
try {
|
|
6
|
+
const buffer = await readFile(message.filePath);
|
|
7
|
+
parentPort.postMessage({
|
|
8
|
+
id: message.id,
|
|
9
|
+
ok: true,
|
|
10
|
+
base64: buffer.toString("base64")
|
|
11
|
+
});
|
|
12
|
+
} catch (error) {
|
|
13
|
+
parentPort.postMessage({
|
|
14
|
+
id: message.id,
|
|
15
|
+
ok: false,
|
|
16
|
+
error: error?.message || "Failed to read file"
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
});
|
package/src/hmr.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { WebSocketServer } from "ws";
|
|
2
|
+
|
|
3
|
+
export function createHMRServer({ port = 3001 } = {}) {
|
|
4
|
+
const clients = new Set();
|
|
5
|
+
const wss = new WebSocketServer({ port });
|
|
6
|
+
|
|
7
|
+
wss.on("connection", (ws) => {
|
|
8
|
+
clients.add(ws);
|
|
9
|
+
|
|
10
|
+
ws.on("close", () => {
|
|
11
|
+
clients.delete(ws);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
function broadcast(message) {
|
|
16
|
+
const data = JSON.stringify(message);
|
|
17
|
+
|
|
18
|
+
for (const client of clients) {
|
|
19
|
+
if (client.readyState === 1) {
|
|
20
|
+
client.send(data);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function close() {
|
|
26
|
+
for (const client of clients) {
|
|
27
|
+
client.close();
|
|
28
|
+
}
|
|
29
|
+
wss.close();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
port,
|
|
34
|
+
wss,
|
|
35
|
+
broadcast,
|
|
36
|
+
close
|
|
37
|
+
};
|
|
38
|
+
}
|
package/src/logger.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { sendEvent } from "./dashboard.js";
|
|
4
|
+
|
|
5
|
+
export function createLogger(config) {
|
|
6
|
+
const logDir = config.logging?.dir || "logs";
|
|
7
|
+
|
|
8
|
+
if (!fs.existsSync(logDir)) {
|
|
9
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const accessLog = path.join(logDir, "access.log");
|
|
13
|
+
const errorLog = path.join(logDir, "error.log");
|
|
14
|
+
|
|
15
|
+
function write(file, message) {
|
|
16
|
+
fs.appendFile(file, message + "\n", () => {});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function formatAccess(req, res, timeMs) {
|
|
20
|
+
return `${new Date().toISOString()} | ${req.method} ${req.pathname} | ${res.statusCode} | ${timeMs}ms | ${req.headers["host"] || "-"}`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
access(req, res, timeMs) {
|
|
25
|
+
const line = formatAccess(req, res, timeMs);
|
|
26
|
+
console.log(line);
|
|
27
|
+
write(accessLog, line);
|
|
28
|
+
|
|
29
|
+
sendEvent({
|
|
30
|
+
type: "access",
|
|
31
|
+
method: req.method,
|
|
32
|
+
path: req.pathname,
|
|
33
|
+
status: res.statusCode,
|
|
34
|
+
time: timeMs,
|
|
35
|
+
ts: Date.now()
|
|
36
|
+
});
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
error(err, req) {
|
|
40
|
+
const line = `${new Date().toISOString()} | ERROR | ${req?.method || "-"} ${req?.pathname || "-"} | ${err.stack || err}`;
|
|
41
|
+
console.error(line);
|
|
42
|
+
write(errorLog, line);
|
|
43
|
+
|
|
44
|
+
sendEvent({
|
|
45
|
+
type: "error",
|
|
46
|
+
message: err.stack || String(err),
|
|
47
|
+
ts: Date.now()
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
package/src/mime.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
export function getMimeType(filePath) {
|
|
4
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
5
|
+
|
|
6
|
+
switch (ext) {
|
|
7
|
+
case ".html":
|
|
8
|
+
return "text/html; charset=utf-8";
|
|
9
|
+
case ".css":
|
|
10
|
+
return "text/css; charset=utf-8";
|
|
11
|
+
case ".js":
|
|
12
|
+
case ".mjs":
|
|
13
|
+
return "application/javascript; charset=utf-8";
|
|
14
|
+
case ".json":
|
|
15
|
+
return "application/json; charset=utf-8";
|
|
16
|
+
case ".svg":
|
|
17
|
+
return "image/svg+xml";
|
|
18
|
+
case ".png":
|
|
19
|
+
return "image/png";
|
|
20
|
+
case ".jpg":
|
|
21
|
+
case ".jpeg":
|
|
22
|
+
return "image/jpeg";
|
|
23
|
+
case ".webp":
|
|
24
|
+
return "image/webp";
|
|
25
|
+
case ".ico":
|
|
26
|
+
return "image/x-icon";
|
|
27
|
+
case ".txt":
|
|
28
|
+
return "text/plain; charset=utf-8";
|
|
29
|
+
default:
|
|
30
|
+
return "application/octet-stream";
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/msg.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = path.dirname(__filename);
|
|
7
|
+
|
|
8
|
+
const pkgPath = path.join(__dirname, "../package.json");
|
|
9
|
+
|
|
10
|
+
const data= JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
11
|
+
const msg = {
|
|
12
|
+
info: `Nitro 5 Web Server v${data.version}, Running on Node.js ${process.version}.\n\n`,
|
|
13
|
+
noConfigFound: `[NITRO 5 ERROR]: No config file are found in ${process.cwd()}.\n\nHint: you are no nitro5.config.js file, create the file and see https://nitro5.opendnf.cloud/config-file#example`
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default msg;
|
package/src/native.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
|
|
3
|
+
const require = createRequire(import.meta.url);
|
|
4
|
+
|
|
5
|
+
let addon = null;
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
addon = require("../build/Release/nitro5.node");
|
|
9
|
+
} catch {
|
|
10
|
+
addon = null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function jsFallbackParse(raw) {
|
|
14
|
+
const headerEnd = raw.indexOf("\r\n\r\n");
|
|
15
|
+
const head = headerEnd >= 0 ? raw.slice(0, headerEnd) : raw;
|
|
16
|
+
const body = headerEnd >= 0 ? raw.slice(headerEnd + 4) : "";
|
|
17
|
+
|
|
18
|
+
const lines = head.split("\r\n");
|
|
19
|
+
const [requestLine = "GET / HTTP/1.1"] = lines;
|
|
20
|
+
const [method = "GET", fullPath = "/", httpVersion = "HTTP/1.1"] = requestLine.split(" ");
|
|
21
|
+
|
|
22
|
+
const [pathname = "/", query = ""] = fullPath.split("?");
|
|
23
|
+
const headers = {};
|
|
24
|
+
|
|
25
|
+
for (const line of lines.slice(1)) {
|
|
26
|
+
const idx = line.indexOf(":");
|
|
27
|
+
if (idx === -1) continue;
|
|
28
|
+
|
|
29
|
+
const key = line.slice(0, idx).trim().toLowerCase();
|
|
30
|
+
const value = line.slice(idx + 1).trim();
|
|
31
|
+
headers[key] = value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
method,
|
|
36
|
+
fullPath,
|
|
37
|
+
pathname,
|
|
38
|
+
query,
|
|
39
|
+
httpVersion,
|
|
40
|
+
headers,
|
|
41
|
+
body,
|
|
42
|
+
raw
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function parseHttpRequest(raw) {
|
|
47
|
+
if (addon && typeof addon.parseHttpRequest === "function") {
|
|
48
|
+
return addon.parseHttpRequest(raw);
|
|
49
|
+
}
|
|
50
|
+
return jsFallbackParse(raw);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function getMetrics() {
|
|
54
|
+
try {
|
|
55
|
+
if (!nativeReady || !addon?.getMetrics) {
|
|
56
|
+
return {
|
|
57
|
+
memoryKB: process.memoryUsage().rss / 1024,
|
|
58
|
+
cpuUser: 0,
|
|
59
|
+
cpuSystem: 0,
|
|
60
|
+
fallback: true
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return addon.getMetrics();
|
|
65
|
+
} catch (err) {
|
|
66
|
+
return {
|
|
67
|
+
memoryKB: process.memoryUsage().rss / 1024,
|
|
68
|
+
cpuUser: 0,
|
|
69
|
+
cpuSystem: 0,
|
|
70
|
+
error: true
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const nativeReady = Boolean(addon);
|
package/src/router.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export class Router {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.routes = {
|
|
4
|
+
GET: new Map(),
|
|
5
|
+
POST: new Map(),
|
|
6
|
+
PUT: new Map(),
|
|
7
|
+
PATCH: new Map(),
|
|
8
|
+
DELETE: new Map()
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
this.middlewares = [];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// ---------------------------
|
|
15
|
+
// utils
|
|
16
|
+
// ---------------------------
|
|
17
|
+
normalize(path) {
|
|
18
|
+
if (!path) return "/";
|
|
19
|
+
|
|
20
|
+
let p = String(path);
|
|
21
|
+
|
|
22
|
+
// buang query + hash
|
|
23
|
+
p = p.split("?")[0].split("#")[0];
|
|
24
|
+
|
|
25
|
+
// normalize slash
|
|
26
|
+
p = p.replace(/\/+/g, "/");
|
|
27
|
+
|
|
28
|
+
// hapus trailing slash kecuali root
|
|
29
|
+
if (p.length > 1) p = p.replace(/\/+$/, "");
|
|
30
|
+
|
|
31
|
+
if (!p.startsWith("/")) p = "/" + p;
|
|
32
|
+
|
|
33
|
+
return p || "/";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ---------------------------
|
|
37
|
+
// middleware
|
|
38
|
+
// ---------------------------
|
|
39
|
+
use(fn) {
|
|
40
|
+
this.middlewares.push(fn);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ---------------------------
|
|
44
|
+
// register routes
|
|
45
|
+
// ---------------------------
|
|
46
|
+
get(path, handler) {
|
|
47
|
+
this.routes.GET.set(this.normalize(path), handler);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
post(path, handler) {
|
|
51
|
+
this.routes.POST.set(this.normalize(path), handler);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
put(path, handler) {
|
|
55
|
+
this.routes.PUT.set(this.normalize(path), handler);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
patch(path, handler) {
|
|
59
|
+
this.routes.PATCH.set(this.normalize(path), handler);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
delete(path, handler) {
|
|
63
|
+
this.routes.DELETE.set(this.normalize(path), handler);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------------------------
|
|
67
|
+
// resolve route
|
|
68
|
+
// ---------------------------
|
|
69
|
+
resolve(method, pathname) {
|
|
70
|
+
const m = String(method || "GET").toUpperCase();
|
|
71
|
+
const p = this.normalize(pathname);
|
|
72
|
+
|
|
73
|
+
return this.routes[m]?.get(p) ?? null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---------------------------
|
|
77
|
+
// debug helper (optional)
|
|
78
|
+
// ---------------------------
|
|
79
|
+
debug() {
|
|
80
|
+
console.log("=== ROUTES ===");
|
|
81
|
+
for (const method of Object.keys(this.routes)) {
|
|
82
|
+
console.log(method, [...this.routes[method].keys()]);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
package/src/stat.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const stats = {
|
|
2
|
+
totalRequests: 0,
|
|
3
|
+
totalErrors: 0,
|
|
4
|
+
activeConnections: 0
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export function incRequest() {
|
|
8
|
+
stats.totalRequests++;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function incError() {
|
|
12
|
+
stats.totalErrors++;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function incConn() {
|
|
16
|
+
stats.activeConnections++;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function decConn() {
|
|
20
|
+
stats.activeConnections--;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getStats() {
|
|
24
|
+
return stats;
|
|
25
|
+
}
|