@nitronjs/framework 0.2.26 → 0.2.27
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/lib/Build/FileAnalyzer.js +1 -1
- package/lib/Build/Manager.js +40 -4
- package/lib/Http/Server.js +6 -1
- package/lib/Session/Manager.js +42 -1
- package/lib/Session/Redis.js +117 -0
- package/package.json +2 -1
- package/skeleton/config/session.js +1 -0
package/lib/Build/Manager.js
CHANGED
|
@@ -309,7 +309,7 @@ class Builder {
|
|
|
309
309
|
return { entries: [], layouts: [], meta: new Map(), srcDir, namespace, changedFiles: [] };
|
|
310
310
|
}
|
|
311
311
|
|
|
312
|
-
const { entries, layouts, meta } = this.#analyzer.discoverEntries(srcDir);
|
|
312
|
+
const { entries, layouts, meta, importedBy } = this.#analyzer.discoverEntries(srcDir);
|
|
313
313
|
|
|
314
314
|
if (!entries.length && !layouts.length) {
|
|
315
315
|
return { entries: [], layouts: [], meta: new Map(), srcDir, namespace, changedFiles: [] };
|
|
@@ -318,19 +318,36 @@ class Builder {
|
|
|
318
318
|
this.#addToManifest(entries, layouts, meta, srcDir, namespace);
|
|
319
319
|
|
|
320
320
|
const allFiles = [...entries, ...layouts];
|
|
321
|
-
const
|
|
321
|
+
const entryLayoutSet = new Set(allFiles);
|
|
322
|
+
const changedSources = new Set();
|
|
322
323
|
|
|
323
|
-
for (const file of
|
|
324
|
+
for (const file of meta.keys()) {
|
|
324
325
|
const content = await fs.promises.readFile(file, "utf8");
|
|
325
326
|
const hash = crypto.createHash("md5").update(content).digest("hex");
|
|
326
327
|
const cachedHash = this.#cache.viewHashes.get(file);
|
|
327
328
|
|
|
328
329
|
if (cachedHash !== hash) {
|
|
329
330
|
this.#cache.viewHashes.set(file, hash);
|
|
330
|
-
|
|
331
|
+
changedSources.add(file);
|
|
331
332
|
}
|
|
332
333
|
}
|
|
333
334
|
|
|
335
|
+
const filesToBuild = new Set();
|
|
336
|
+
|
|
337
|
+
for (const file of changedSources) {
|
|
338
|
+
if (entryLayoutSet.has(file)) {
|
|
339
|
+
filesToBuild.add(file);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
for (const dep of this.#getTransitiveDependents(file, importedBy)) {
|
|
343
|
+
if (entryLayoutSet.has(dep)) {
|
|
344
|
+
filesToBuild.add(dep);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const changedFiles = [...filesToBuild];
|
|
350
|
+
|
|
334
351
|
if (changedFiles.length) {
|
|
335
352
|
this.#cache.viewsChanged = true;
|
|
336
353
|
await this.#runEsbuild(changedFiles, outDir, { meta, outbase: srcDir });
|
|
@@ -444,6 +461,25 @@ class Builder {
|
|
|
444
461
|
this.#stats.islands = hydrationFiles.length;
|
|
445
462
|
}
|
|
446
463
|
|
|
464
|
+
#getTransitiveDependents(file, importedBy) {
|
|
465
|
+
const result = new Set();
|
|
466
|
+
const queue = [file];
|
|
467
|
+
|
|
468
|
+
while (queue.length > 0) {
|
|
469
|
+
const current = queue.shift();
|
|
470
|
+
const dependents = importedBy.get(current) || [];
|
|
471
|
+
|
|
472
|
+
for (const dep of dependents) {
|
|
473
|
+
if (!result.has(dep)) {
|
|
474
|
+
result.add(dep);
|
|
475
|
+
queue.push(dep);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return result;
|
|
481
|
+
}
|
|
482
|
+
|
|
447
483
|
async #buildCss() {
|
|
448
484
|
this.#stats.css = await this.#cssBuilder.build(this.#cache.viewsChanged);
|
|
449
485
|
}
|
package/lib/Http/Server.js
CHANGED
|
@@ -283,7 +283,12 @@ class Server {
|
|
|
283
283
|
console.log("\x1b[32m✓ Server stopped gracefully\x1b[0m");
|
|
284
284
|
|
|
285
285
|
try {
|
|
286
|
-
|
|
286
|
+
const sessionManager = await SessionManager.getInstance();
|
|
287
|
+
|
|
288
|
+
await Promise.race([
|
|
289
|
+
sessionManager.close(),
|
|
290
|
+
new Promise(resolve => setTimeout(resolve, 1000))
|
|
291
|
+
]);
|
|
287
292
|
await Promise.race([
|
|
288
293
|
DB.close(),
|
|
289
294
|
new Promise(resolve => setTimeout(resolve, 1000))
|
package/lib/Session/Manager.js
CHANGED
|
@@ -2,6 +2,7 @@ import crypto from "crypto";
|
|
|
2
2
|
import Config from "../Core/Config.js";
|
|
3
3
|
import Memory from "./Memory.js";
|
|
4
4
|
import File from "./File.js";
|
|
5
|
+
import RedisStore from "./Redis.js";
|
|
5
6
|
import Session from "./Session.js";
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -44,7 +45,26 @@ class SessionManager {
|
|
|
44
45
|
this.#config = Config.all("session");
|
|
45
46
|
this.#config.cookie.signed = true;
|
|
46
47
|
|
|
47
|
-
|
|
48
|
+
switch (this.#config.driver) {
|
|
49
|
+
case "file":
|
|
50
|
+
this.#store = new File();
|
|
51
|
+
break;
|
|
52
|
+
|
|
53
|
+
case "memory":
|
|
54
|
+
this.#store = new Memory();
|
|
55
|
+
break;
|
|
56
|
+
|
|
57
|
+
case "redis":
|
|
58
|
+
this.#store = new RedisStore(this.#config.lifetime);
|
|
59
|
+
break;
|
|
60
|
+
|
|
61
|
+
case "none":
|
|
62
|
+
this.#store = null;
|
|
63
|
+
return;
|
|
64
|
+
|
|
65
|
+
default:
|
|
66
|
+
throw new Error(`[Session] Unknown driver: "${this.#config.driver}"`);
|
|
67
|
+
}
|
|
48
68
|
|
|
49
69
|
if (this.#store.ready) {
|
|
50
70
|
await this.#store.ready;
|
|
@@ -182,6 +202,17 @@ class SessionManager {
|
|
|
182
202
|
}
|
|
183
203
|
}
|
|
184
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Closes the session store connection.
|
|
207
|
+
*/
|
|
208
|
+
async close() {
|
|
209
|
+
this.stopGC();
|
|
210
|
+
|
|
211
|
+
if (this.#store && typeof this.#store.close === "function") {
|
|
212
|
+
await this.#store.close();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
185
216
|
/**
|
|
186
217
|
* Sets up session middleware for Fastify.
|
|
187
218
|
* @param {import("fastify").FastifyInstance} server
|
|
@@ -189,9 +220,19 @@ class SessionManager {
|
|
|
189
220
|
static async setup(server) {
|
|
190
221
|
const manager = await SessionManager.getInstance();
|
|
191
222
|
|
|
223
|
+
if (!manager.#store) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
192
227
|
server.decorateRequest("session", null);
|
|
193
228
|
|
|
194
229
|
server.addHook("preHandler", async (request, response) => {
|
|
230
|
+
const lastSegment = request.url.split("/").pop().split("?")[0];
|
|
231
|
+
|
|
232
|
+
if (lastSegment.includes(".")) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
195
236
|
request.session = await manager.load(request, response);
|
|
196
237
|
});
|
|
197
238
|
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { createClient } from "redis";
|
|
2
|
+
import Log from "../Logging/Log.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Redis-based session storage.
|
|
6
|
+
* Stores sessions as JSON strings with automatic TTL expiry.
|
|
7
|
+
*/
|
|
8
|
+
class RedisStore {
|
|
9
|
+
#client;
|
|
10
|
+
#prefix;
|
|
11
|
+
#ttl;
|
|
12
|
+
ready;
|
|
13
|
+
|
|
14
|
+
constructor(lifetime) {
|
|
15
|
+
this.#prefix = (process.env.APP_NAME || "app") + ":session:";
|
|
16
|
+
this.#ttl = Math.ceil(lifetime / 1000);
|
|
17
|
+
this.ready = this.#connect();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Gets session data by ID.
|
|
22
|
+
* @param {string} id
|
|
23
|
+
* @returns {Promise<Object|null>}
|
|
24
|
+
*/
|
|
25
|
+
async get(id) {
|
|
26
|
+
const data = await this.#client.get(this.#prefix + id);
|
|
27
|
+
|
|
28
|
+
if (!data) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(data);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
await this.delete(id);
|
|
37
|
+
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Stores session data with automatic TTL.
|
|
44
|
+
* @param {string} id
|
|
45
|
+
* @param {Object} value
|
|
46
|
+
*/
|
|
47
|
+
async set(id, value) {
|
|
48
|
+
await this.#client.set(this.#prefix + id, JSON.stringify(value), {
|
|
49
|
+
EX: this.#ttl
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Deletes a session.
|
|
55
|
+
* @param {string} id
|
|
56
|
+
*/
|
|
57
|
+
async delete(id) {
|
|
58
|
+
await this.#client.del(this.#prefix + id);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Garbage collection — no-op for Redis.
|
|
63
|
+
* Redis handles expiry automatically via TTL.
|
|
64
|
+
* @returns {Promise<number>}
|
|
65
|
+
*/
|
|
66
|
+
async gc() {
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Closes the Redis connection gracefully.
|
|
72
|
+
*/
|
|
73
|
+
async close() {
|
|
74
|
+
if (this.#client) {
|
|
75
|
+
await this.#client.quit();
|
|
76
|
+
this.#client = null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** @private */
|
|
81
|
+
async #connect() {
|
|
82
|
+
const host = process.env.REDIS_HOST || "127.0.0.1";
|
|
83
|
+
const port = Number(process.env.REDIS_PORT) || 6379;
|
|
84
|
+
const password = process.env.REDIS_PASSWORD || undefined;
|
|
85
|
+
|
|
86
|
+
this.#client = createClient({
|
|
87
|
+
socket: {
|
|
88
|
+
host,
|
|
89
|
+
port,
|
|
90
|
+
reconnectStrategy: (retries) => {
|
|
91
|
+
if (retries > 10) {
|
|
92
|
+
console.error("\x1b[31m✕ [Session Redis] Connection lost after 10 retries. Shutting down.\x1b[0m");
|
|
93
|
+
Log.fatal("Redis connection lost permanently", { retries });
|
|
94
|
+
process.emit("SIGTERM");
|
|
95
|
+
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return Math.min(retries * 100, 5000);
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
password
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
this.#client.on("error", (err) => {
|
|
106
|
+
console.error(`\x1b[31m✕ [Session Redis] ${err.message}\x1b[0m`);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
this.#client.on("ready", () => {
|
|
110
|
+
console.log("\x1b[32m✓ [Session Redis] Connected\x1b[0m");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await this.#client.connect();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export default RedisStore;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nitronjs/framework",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.27",
|
|
4
4
|
"description": "NitronJS is a modern and extensible Node.js MVC framework built on Fastify. It focuses on clean architecture, modular structure, and developer productivity, offering built-in routing, middleware, configuration management, CLI tooling, and native React integration for scalable full-stack applications.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"njs": "./cli/njs.js"
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"react": "^19.2.3",
|
|
37
37
|
"react-dom": "^19.2.3",
|
|
38
38
|
"react-refresh": "^0.18.0",
|
|
39
|
+
"redis": "^5.6.0",
|
|
39
40
|
"socket.io": "^4.8.1",
|
|
40
41
|
"tailwindcss": "^4.1.18",
|
|
41
42
|
"typescript": "^5.9.3"
|