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/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
+ }
@@ -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
+ }