@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.
@@ -63,7 +63,7 @@ class FileAnalyzer {
63
63
 
64
64
  this.#validateGraph(graph, entries, importedBy);
65
65
 
66
- return { entries, layouts, meta: graph };
66
+ return { entries, layouts, meta: graph, importedBy };
67
67
  }
68
68
 
69
69
  #findTsxFiles(dir, result = []) {
@@ -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 changedFiles = [];
321
+ const entryLayoutSet = new Set(allFiles);
322
+ const changedSources = new Set();
322
323
 
323
- for (const file of allFiles) {
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
- changedFiles.push(file);
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
  }
@@ -283,7 +283,12 @@ class Server {
283
283
  console.log("\x1b[32m✓ Server stopped gracefully\x1b[0m");
284
284
 
285
285
  try {
286
- (await SessionManager.getInstance()).stopGC();
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))
@@ -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
- this.#store = this.#config.driver === "file" ? new File() : new Memory();
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.26",
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"
@@ -1,6 +1,7 @@
1
1
  const SESSION_LIFETIME = 1000 * 60 * 60 * 2;
2
2
 
3
3
  export default {
4
+ // Driver: none | file | memory | redis
4
5
  driver: "file",
5
6
  lifetime: SESSION_LIFETIME,
6
7
  cookieName: "session",