odac 1.1.0 → 1.2.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.
Files changed (113) hide show
  1. package/.agent/rules/coding.md +27 -0
  2. package/.agent/rules/memory.md +33 -0
  3. package/.agent/rules/project.md +30 -0
  4. package/.agent/rules/workflow.md +16 -0
  5. package/.github/workflows/release.yml +42 -1
  6. package/.github/workflows/test-coverage.yml +6 -5
  7. package/.github/workflows/test-publish.yml +36 -0
  8. package/.husky/pre-commit +10 -0
  9. package/.husky/pre-push +13 -0
  10. package/.releaserc.js +3 -3
  11. package/CHANGELOG.md +67 -0
  12. package/README.md +16 -0
  13. package/bin/odac.js +182 -40
  14. package/client/odac.js +10 -4
  15. package/docs/backend/01-overview/03-development-server.md +38 -45
  16. package/docs/backend/02-structure/01-typical-project-layout.md +59 -26
  17. package/docs/backend/03-config/00-configuration-overview.md +6 -6
  18. package/docs/backend/03-config/01-database-connection.md +2 -2
  19. package/docs/backend/03-config/02-static-route-mapping-optional.md +1 -1
  20. package/docs/backend/03-config/03-request-timeout.md +1 -1
  21. package/docs/backend/03-config/04-environment-variables.md +4 -4
  22. package/docs/backend/03-config/05-early-hints.md +2 -2
  23. package/docs/backend/04-routing/03-api-and-data-routes.md +18 -0
  24. package/docs/backend/04-routing/07-cron-jobs.md +17 -1
  25. package/docs/backend/05-controllers/01-how-to-build-a-controller.md +48 -3
  26. package/docs/backend/05-controllers/03-controller-classes.md +40 -20
  27. package/docs/backend/06-request-and-response/01-the-request-object-what-is-the-user-asking-for.md +17 -0
  28. package/docs/backend/07-views/10-styling-and-tailwind.md +93 -0
  29. package/docs/backend/08-database/01-getting-started.md +2 -2
  30. package/docs/backend/10-authentication/03-register.md +1 -1
  31. package/docs/backend/10-authentication/04-odac-register-forms.md +2 -2
  32. package/docs/backend/10-authentication/05-session-management.md +15 -1
  33. package/docs/backend/10-authentication/06-odac-login-forms.md +2 -2
  34. package/docs/backend/10-authentication/07-magic-links.md +1 -1
  35. package/docs/index.json +5 -1
  36. package/jest.config.js +1 -1
  37. package/package.json +9 -5
  38. package/src/Auth.js +58 -23
  39. package/src/Config.js +7 -7
  40. package/src/Env.js +3 -1
  41. package/src/Ipc.js +7 -0
  42. package/src/Lang.js +9 -2
  43. package/src/Odac.js +44 -35
  44. package/src/Request.js +1 -1
  45. package/src/Route/Cron.js +58 -17
  46. package/src/Route/Internal.js +1 -1
  47. package/src/Route.js +282 -99
  48. package/src/Server.js +40 -3
  49. package/src/Storage.js +4 -0
  50. package/src/Token.js +6 -4
  51. package/src/Validator.js +1 -1
  52. package/src/Var.js +22 -6
  53. package/src/View/EarlyHints.js +43 -33
  54. package/src/View/Form.js +17 -11
  55. package/src/View.js +62 -6
  56. package/template/package.json +3 -1
  57. package/template/view/content/home.html +3 -3
  58. package/template/view/head/main.html +2 -2
  59. package/test/Client.test.js +168 -0
  60. package/test/Config.test.js +112 -0
  61. package/test/Lang.test.js +92 -0
  62. package/test/Odac.test.js +86 -0
  63. package/test/{framework/middleware.test.js → Route/Middleware.test.js} +2 -2
  64. package/test/{framework/Route.test.js → Route.test.js} +1 -1
  65. package/test/{framework/View → View}/EarlyHints.test.js +1 -1
  66. package/test/{framework/WebSocket.test.js → WebSocket.test.js} +2 -2
  67. package/test/scripts/check-coverage.js +4 -4
  68. package/test/cli/Cli.test.js +0 -36
  69. package/test/core/Commands.test.js +0 -538
  70. package/test/core/Config.test.js +0 -1432
  71. package/test/core/Lang.test.js +0 -250
  72. package/test/core/Odac.test.js +0 -234
  73. package/test/core/Process.test.js +0 -156
  74. package/test/server/Api.test.js +0 -647
  75. package/test/server/DNS.test.js +0 -2050
  76. package/test/server/DNS.test.js.bak +0 -2084
  77. package/test/server/Hub.test.js +0 -497
  78. package/test/server/Log.test.js +0 -73
  79. package/test/server/Mail.account.test_.js +0 -460
  80. package/test/server/Mail.init.test_.js +0 -411
  81. package/test/server/Mail.test_.js +0 -1340
  82. package/test/server/SSL.test_.js +0 -1491
  83. package/test/server/Server.test.js +0 -765
  84. package/test/server/Service.test_.js +0 -1127
  85. package/test/server/Subdomain.test.js +0 -440
  86. package/test/server/Web/Firewall.test.js +0 -175
  87. package/test/server/Web/Proxy.test.js +0 -397
  88. package/test/server/Web.test.js +0 -1494
  89. package/test/server/__mocks__/acme-client.js +0 -17
  90. package/test/server/__mocks__/bcrypt.js +0 -50
  91. package/test/server/__mocks__/child_process.js +0 -389
  92. package/test/server/__mocks__/crypto.js +0 -432
  93. package/test/server/__mocks__/fs.js +0 -450
  94. package/test/server/__mocks__/globalOdac.js +0 -227
  95. package/test/server/__mocks__/http.js +0 -575
  96. package/test/server/__mocks__/https.js +0 -272
  97. package/test/server/__mocks__/index.js +0 -249
  98. package/test/server/__mocks__/mail/server.js +0 -100
  99. package/test/server/__mocks__/mail/smtp.js +0 -31
  100. package/test/server/__mocks__/mailparser.js +0 -81
  101. package/test/server/__mocks__/net.js +0 -369
  102. package/test/server/__mocks__/node-forge.js +0 -328
  103. package/test/server/__mocks__/os.js +0 -320
  104. package/test/server/__mocks__/path.js +0 -291
  105. package/test/server/__mocks__/selfsigned.js +0 -8
  106. package/test/server/__mocks__/server/src/mail/server.js +0 -100
  107. package/test/server/__mocks__/server/src/mail/smtp.js +0 -31
  108. package/test/server/__mocks__/smtp-server.js +0 -106
  109. package/test/server/__mocks__/sqlite3.js +0 -394
  110. package/test/server/__mocks__/testFactories.js +0 -299
  111. package/test/server/__mocks__/testHelpers.js +0 -363
  112. package/test/server/__mocks__/tls.js +0 -229
  113. /package/template/{config.json → odac.json} +0 -0
package/src/Route/Cron.js CHANGED
@@ -60,7 +60,7 @@ class Cron {
60
60
  if (job.lastRun && Math.floor(unix / 86400) % condition.value !== 0) shouldRun = false
61
61
  break
62
62
  case 'everyWeekDay':
63
- if (job.lastRun && weekDay % condition.value !== 0) shouldRun = false
63
+ if (condition.value !== weekDay) shouldRun = false
64
64
  break
65
65
  case 'everyMonth':
66
66
  if (job.lastRun && (year * 12 + month) % condition.value !== 0) shouldRun = false
@@ -81,7 +81,9 @@ class Cron {
81
81
  if (job.function || fs.existsSync(job.path)) {
82
82
  if (!job.function) job.function = require(job.path)
83
83
  if (job.function && typeof job.function === 'function') {
84
- job.function()
84
+ const _odac = global.Odac.instance(null, 'cron')
85
+ job.function(_odac)
86
+ if (_odac.cleanup) _odac.cleanup()
85
87
  }
86
88
  }
87
89
  } catch (error) {
@@ -109,22 +111,61 @@ class Cron {
109
111
  updated: new Date()
110
112
  })
111
113
  let id = this.#jobs.length - 1
112
- return {
113
- minute: value => this.#jobs[id].condition.push({type: 'minute', value: value}),
114
- hour: value => this.#jobs[id].condition.push({type: 'hour', value: value}),
115
- day: value => this.#jobs[id].condition.push({type: 'day', value: value}),
116
- weekDay: value => this.#jobs[id].condition.push({type: 'weekDay', value: value}),
117
- month: value => this.#jobs[id].condition.push({type: 'month', value: value}),
118
- year: value => this.#jobs[id].condition.push({type: 'year', value: value}),
119
- yearDay: value => this.#jobs[id].condition.push({type: 'yearDay', value: value}),
120
- everyMinute: value => this.#jobs[id].condition.push({type: 'everyMinute', value: value}),
121
- everyHour: value => this.#jobs[id].condition.push({type: 'everyHour', value: value}),
122
- everyDay: value => this.#jobs[id].condition.push({type: 'everyDay', value: value}),
123
- everyWeekDay: value => this.#jobs[id].condition.push({type: 'everyWeekDay', value: value}),
124
- everyMonth: value => this.#jobs[id].condition.push({type: 'everyMonth', value: value}),
125
- everyYear: value => this.#jobs[id].condition.push({type: 'everyYear', value: value}),
126
- everyYearDay: value => this.#jobs[id].condition.push({type: 'everyYearDay', value: value})
114
+ const addCondition = (type, value) => {
115
+ this.#jobs[id].condition.push({type, value})
116
+ return chain
117
+ }
118
+
119
+ const chain = {
120
+ minute: value => addCondition('minute', value),
121
+ hour: value => addCondition('hour', value),
122
+ day: value => addCondition('day', value),
123
+ at: time => {
124
+ if (!/^\d{1,2}:\d{1,2}$/.test(time)) throw new Error('Invalid time format for .at(). Use HH:MM')
125
+ const [h, m] = time.split(':')
126
+ addCondition('hour', parseInt(h))
127
+ addCondition('minute', parseInt(m))
128
+ return chain
129
+ },
130
+ raw: pattern => {
131
+ const parts = pattern.split(' ').filter(p => p.trim() !== '')
132
+ if (parts.length !== 5) throw new Error('Invalid cron expression. Expected 5 fields (min hour day month weekDay)')
133
+
134
+ const [min, hour, day, month, weekDay] = parts
135
+ const parse = (val, type, everyType) => {
136
+ if (val === '*') return
137
+ if (val.startsWith('*/') && everyType) {
138
+ addCondition(everyType, parseInt(val.split('/')[1]))
139
+ return
140
+ }
141
+ if (!isNaN(val)) {
142
+ addCondition(type, parseInt(val))
143
+ return
144
+ }
145
+ throw new Error(`Unsupported cron value '${val}' for ${type}`)
146
+ }
147
+
148
+ parse(min, 'minute', 'everyMinute')
149
+ parse(hour, 'hour', 'everyHour')
150
+ parse(day, 'day', 'everyDay')
151
+ parse(month, 'month', 'everyMonth')
152
+ parse(weekDay, 'weekDay', null)
153
+
154
+ return chain
155
+ },
156
+ weekDay: value => addCondition('weekDay', value),
157
+ month: value => addCondition('month', value),
158
+ year: value => addCondition('year', value),
159
+ yearDay: value => addCondition('yearDay', value),
160
+ everyMinute: value => addCondition('everyMinute', value),
161
+ everyHour: value => addCondition('everyHour', value),
162
+ everyDay: value => addCondition('everyDay', value),
163
+ everyWeekDay: value => addCondition('everyWeekDay', value),
164
+ everyMonth: value => addCondition('everyMonth', value),
165
+ everyYear: value => addCondition('everyYear', value),
166
+ everyYearDay: value => addCondition('everyYearDay', value)
127
167
  }
168
+ return chain
128
169
  }
129
170
  }
130
171
 
@@ -528,7 +528,7 @@ class Internal {
528
528
  if (error.message === 'Database connection failed') {
529
529
  return Odac.return({
530
530
  result: {success: false},
531
- errors: {_odac_form: 'Database not configured. Please check your config.json'}
531
+ errors: {_odac_form: 'Database not configured. Please check your odac.json'}
532
532
  })
533
533
  }
534
534
 
package/src/Route.js CHANGED
@@ -1,4 +1,5 @@
1
1
  const fs = require('fs')
2
+ const fsPromises = fs.promises
2
3
 
3
4
  const Cron = require('./Route/Cron.js')
4
5
  const Internal = require('./Route/Internal.js')
@@ -68,7 +69,10 @@ class Route {
68
69
  routes = {}
69
70
  middlewares = {}
70
71
  _pendingMiddlewares = []
72
+ _pendingRouteLoads = []
71
73
  #wsServer = new WebSocketServer()
74
+ #configCache = {}
75
+ #publicCache = {}
72
76
  auth = {
73
77
  page: (path, authFile, file) => this.authPage(path, authFile, file),
74
78
  post: (path, authFile, file) => this.authPost(path, authFile, file),
@@ -115,6 +119,21 @@ class Route {
115
119
  const middlewareResult = await this.#runMiddlewares(Odac, controller.middlewares)
116
120
  if (middlewareResult !== undefined) return middlewareResult
117
121
 
122
+ if (controller.action) {
123
+ const ControllerClass = controller.cache
124
+ try {
125
+ const instance = new ControllerClass(Odac)
126
+ if (typeof instance[controller.action] === 'function') {
127
+ return instance[controller.action](Odac)
128
+ }
129
+ } catch {
130
+ if (typeof ControllerClass[controller.action] === 'function') {
131
+ return ControllerClass[controller.action](Odac)
132
+ }
133
+ }
134
+ return Odac.Request.abort(500)
135
+ }
136
+
118
137
  if (typeof controller.cache === 'function') {
119
138
  return controller.cache(Odac)
120
139
  }
@@ -131,6 +150,7 @@ class Route {
131
150
  if (['post', 'put', 'patch', 'delete'].includes(Odac.Request.method)) {
132
151
  const formToken = await Odac.request('_odac_form_token')
133
152
  if (formToken) {
153
+ Odac.Request.setSession()
134
154
  await Internal.processForm(Odac)
135
155
  }
136
156
  }
@@ -157,20 +177,40 @@ class Route {
157
177
  Odac.Request.isAjaxLoad = true
158
178
  Odac.Request.clientSkeleton = Odac.Request.header('X-Odac-Skeleton')
159
179
  }
160
- if (Odac.Config && Odac.Config.route && Odac.Config.route[url]) {
161
- if (fs.existsSync(Odac.Config.route[url])) {
162
- let stat = fs.lstatSync(Odac.Config.route[url])
163
- if (stat.isFile()) {
164
- let type = 'text/html'
165
- if (Odac.Config.route[url].includes('.')) {
166
- let arr = Odac.Config.route[url].split('.')
167
- type = mime[arr[arr.length - 1]]
180
+ if (Odac.Config?.route?.[url]) {
181
+ // PROD CACHE HIT
182
+ if (!Odac.Config.debug && this.#configCache[url]) {
183
+ const cached = this.#configCache[url]
184
+ Odac.Request.header('Content-Type', cached.type)
185
+ Odac.Request.header('Cache-Control', 'public, max-age=31536000')
186
+ Odac.Request.header('Content-Length', cached.size)
187
+ return cached.content
188
+ }
189
+
190
+ const filePath = Odac.Config.route[url]
191
+ try {
192
+ const content = await fsPromises.readFile(filePath)
193
+ let type = 'text/html'
194
+ if (filePath.includes('.')) {
195
+ let arr = filePath.split('.')
196
+ type = mime[arr[arr.length - 1]]
197
+ }
198
+
199
+ // PROD CACHE SET
200
+ if (!Odac.Config.debug) {
201
+ this.#configCache[url] = {
202
+ content,
203
+ type,
204
+ size: content.length
168
205
  }
169
- Odac.Request.header('Content-Type', type)
170
- Odac.Request.header('Cache-Control', 'public, max-age=31536000')
171
- Odac.Request.header('Content-Length', stat.size)
172
- return fs.readFileSync(Odac.Config.route[url])
173
206
  }
207
+
208
+ Odac.Request.header('Content-Type', type)
209
+ Odac.Request.header('Cache-Control', 'public, max-age=31536000')
210
+ Odac.Request.header('Content-Length', content.length)
211
+ return content
212
+ } catch {
213
+ // File not found or error, continue routing
174
214
  }
175
215
  }
176
216
  for (let method of ['#' + Odac.Request.method, Odac.Request.method]) {
@@ -207,18 +247,42 @@ class Route {
207
247
  if (typeof page === 'string') Odac.Request.page = page
208
248
  return await this.#executeController(Odac, pageController)
209
249
  }
210
- if (url && !url.includes('/../') && fs.existsSync(`${__dir}/public${url}`)) {
211
- let stat = fs.lstatSync(`${__dir}/public${url}`)
212
- if (stat.isFile()) {
213
- let type = 'text/html'
214
- if (url.includes('.')) {
215
- let arr = url.split('.')
216
- type = mime[arr[arr.length - 1]]
217
- }
218
- Odac.Request.header('Content-Type', type)
250
+ if (url && !url.includes('/../')) {
251
+ const publicPath = `${__dir}/public${url}`
252
+
253
+ // PROD CACHE HIT (Metadata)
254
+ if (!Odac.Config.debug && this.#publicCache[publicPath]) {
255
+ const cached = this.#publicCache[publicPath]
256
+ Odac.Request.header('Content-Type', cached.type)
219
257
  Odac.Request.header('Cache-Control', 'public, max-age=31536000')
220
- Odac.Request.header('Content-Length', stat.size)
221
- return fs.createReadStream(`${__dir}/public${url}`)
258
+ Odac.Request.header('Content-Length', cached.size)
259
+ return fs.createReadStream(publicPath)
260
+ }
261
+
262
+ try {
263
+ const stat = await fsPromises.stat(publicPath)
264
+ if (stat.isFile()) {
265
+ let type = 'text/html'
266
+ if (url.includes('.')) {
267
+ let arr = url.split('.')
268
+ type = mime[arr[arr.length - 1]]
269
+ }
270
+
271
+ // PROD CACHE SET (Metadata Only)
272
+ if (!Odac.Config.debug) {
273
+ this.#publicCache[publicPath] = {
274
+ type,
275
+ size: stat.size
276
+ }
277
+ }
278
+
279
+ Odac.Request.header('Content-Type', type)
280
+ Odac.Request.header('Cache-Control', 'public, max-age=31536000')
281
+ Odac.Request.header('Content-Length', stat.size)
282
+ return fs.createReadStream(publicPath)
283
+ }
284
+ } catch {
285
+ // File not found in public
222
286
  }
223
287
  }
224
288
 
@@ -257,15 +321,21 @@ class Route {
257
321
  return false
258
322
  }
259
323
 
260
- #loadMiddlewares() {
324
+ async #loadMiddlewares() {
261
325
  const middlewareDir = `${__dir}/middleware/`
262
- if (!fs.existsSync(middlewareDir)) return
326
+ try {
327
+ await fsPromises.access(middlewareDir)
328
+ } catch {
329
+ return
330
+ }
263
331
 
264
- for (const file of fs.readdirSync(middlewareDir)) {
332
+ const files = await fsPromises.readdir(middlewareDir)
333
+ for (const file of files) {
265
334
  if (!file.endsWith('.js')) continue
266
335
  const name = file.replace('.js', '')
267
336
  const path = `${middlewareDir}${file}`
268
- const mtime = fs.statSync(path).mtimeMs
337
+ const stat = await fsPromises.stat(path)
338
+ const mtime = stat.mtimeMs
269
339
 
270
340
  if (this.middlewares[name] && this.middlewares[name].mtime >= mtime - 1000) continue
271
341
 
@@ -278,70 +348,123 @@ class Route {
278
348
  }
279
349
  }
280
350
 
281
- #init() {
351
+ async #init() {
282
352
  if (this.loading) return
283
353
  this.loading = true
284
- this.#loadMiddlewares()
354
+ await this.#loadMiddlewares()
285
355
  const classDir = `${__dir}/class/`
286
- if (fs.existsSync(classDir)) {
287
- for (const file of fs.readdirSync(classDir)) {
356
+ try {
357
+ await fsPromises.access(classDir)
358
+ const files = await fsPromises.readdir(classDir)
359
+ for (const file of files) {
288
360
  if (!file.endsWith('.js')) continue
289
361
  let name = file.replace('.js', '')
290
362
  if (!Odac.Route.class) Odac.Route.class = {}
363
+ const filePath = `${__dir}/class/${file}`
364
+
365
+ let shouldLoad = true
366
+ let stat = null
367
+
291
368
  if (Odac.Route.class[name]) {
292
- const fileStat = fs.statSync(Odac.Route.class[name].path)
293
- if (Odac.Route.class[name].mtime >= fileStat.mtimeMs || Date.now() < fileStat.mtimeMs + 1000) continue
294
- delete require.cache[require.resolve(Odac.Route.class[name].path)]
369
+ stat = await fsPromises.stat(Odac.Route.class[name].path)
370
+ if (Odac.Route.class[name].mtime >= stat.mtimeMs || Date.now() < stat.mtimeMs + 1000) {
371
+ shouldLoad = false
372
+ } else {
373
+ delete require.cache[require.resolve(Odac.Route.class[name].path)]
374
+ }
375
+ } else {
376
+ stat = await fsPromises.stat(filePath)
295
377
  }
296
- Odac.Route.class[name] = {
297
- path: `${__dir}/class/${file}`,
298
- mtime: fs.statSync(`${__dir}/class/${file}`).mtimeMs,
299
- module: require(`${__dir}/class/${file}`)
378
+
379
+ if (shouldLoad) {
380
+ Odac.Route.class[name] = {
381
+ path: filePath,
382
+ mtime: stat.mtimeMs,
383
+ module: require(filePath)
384
+ }
300
385
  }
301
386
  }
387
+ } catch {
388
+ // Class dir might not exist
302
389
  }
303
- let dir = fs.readdirSync(`${__dir}/route/`)
304
- for (const file of dir) {
305
- if (!file.endsWith('.js')) continue
306
- let mtime = fs.statSync(`${__dir}/route/${file}`).mtimeMs
307
- Odac.Route.buff = file.replace('.js', '')
308
- if (!routes2[Odac.Route.buff] || routes2[Odac.Route.buff] < mtime - 1000) {
309
- delete require.cache[require.resolve(`${__dir}/route/${file}`)]
310
- routes2[Odac.Route.buff] = mtime
311
- const routeModule = require(`${__dir}/route/${file}`)
312
- if (typeof routeModule === 'function') {
313
- routeModule(Odac)
390
+
391
+ try {
392
+ const dir = await fsPromises.readdir(`${__dir}/route/`)
393
+ for (const file of dir) {
394
+ if (!file.endsWith('.js')) continue
395
+ const filePath = `${__dir}/route/${file}`
396
+ const stat = await fsPromises.stat(filePath)
397
+ let mtime = stat.mtimeMs
398
+ Odac.Route.buff = file.replace('.js', '')
399
+
400
+ if (!routes2[Odac.Route.buff] || routes2[Odac.Route.buff] < mtime - 1000) {
401
+ delete require.cache[require.resolve(filePath)]
402
+ routes2[Odac.Route.buff] = mtime
403
+ const routeModule = require(filePath)
404
+ if (typeof routeModule === 'function') {
405
+ // routeModule calls .set(), which pushes promises to _pendingRouteLoads
406
+ routeModule(Odac)
407
+ }
314
408
  }
315
- }
316
- for (const type of ['page', '#page', 'post', '#post', 'get', '#get', 'error']) {
317
- if (!this.routes[Odac.Route.buff]) continue
318
- if (!this.routes[Odac.Route.buff][type]) continue
319
- for (const route in this.routes[Odac.Route.buff][type]) {
320
- if (routes2[Odac.Route.buff] > this.routes[Odac.Route.buff][type][route].loaded) {
321
- delete require.cache[require.resolve(this.routes[Odac.Route.buff][type][route].path)]
322
- delete this.routes[Odac.Route.buff][type][route]
323
- } else if (this.routes[Odac.Route.buff][type][route]) {
324
- if (typeof this.routes[Odac.Route.buff][type][route].type === 'function') continue
325
- if (this.routes[Odac.Route.buff][type][route].mtime < fs.statSync(this.routes[Odac.Route.buff][type][route].path).mtimeMs) {
326
- delete require.cache[require.resolve(this.routes[Odac.Route.buff][type][route].path)]
327
- this.routes[Odac.Route.buff][type][route].cache = require(this.routes[Odac.Route.buff][type][route].path)
328
- this.routes[Odac.Route.buff][type][route].mtime = fs.statSync(this.routes[Odac.Route.buff][type][route].path).mtimeMs
409
+
410
+ // Wait for all route sets to complete for this file
411
+ await Promise.all(this._pendingRouteLoads)
412
+ this._pendingRouteLoads = []
413
+
414
+ // Clean up deleted routes logic
415
+ for (const type of ['page', '#page', 'post', '#post', 'get', '#get', 'error']) {
416
+ if (!this.routes[Odac.Route.buff]) continue
417
+ if (!this.routes[Odac.Route.buff][type]) continue
418
+ for (const route in this.routes[Odac.Route.buff][type]) {
419
+ const routeObj = this.routes[Odac.Route.buff][type][route]
420
+ if (!routeObj) continue
421
+
422
+ if (routes2[Odac.Route.buff] > routeObj.loaded) {
423
+ if (routeObj.path) {
424
+ try {
425
+ delete require.cache[require.resolve(routeObj.path)]
426
+ } catch {
427
+ // Silently fail
428
+ }
429
+ }
430
+ delete this.routes[Odac.Route.buff][type][route]
431
+ } else if (routeObj) {
432
+ if (typeof routeObj.type === 'function') continue
433
+ // Check if controller file modified
434
+ try {
435
+ const cStat = await fsPromises.stat(routeObj.path)
436
+ if (routeObj.mtime < cStat.mtimeMs) {
437
+ delete require.cache[require.resolve(routeObj.path)]
438
+ routeObj.cache = require(routeObj.path)
439
+ routeObj.mtime = cStat.mtimeMs
440
+ }
441
+ } catch {
442
+ // Controller file might have been deleted?
443
+ }
329
444
  }
330
445
  }
331
446
  }
447
+ delete Odac.Route.buff
332
448
  }
333
- delete Odac.Route.buff
449
+ } catch (e) {
450
+ // route dir issue?
451
+ console.error(e)
334
452
  }
453
+
335
454
  Cron.init()
336
455
  this.loading = false
337
456
  }
338
457
 
339
- init() {
340
- this.#init()
458
+ async init() {
459
+ await this.#init()
341
460
  this.#registerInternalRoutes()
342
- setInterval(() => {
343
- this.#init()
344
- }, 5000)
461
+
462
+ // Hot Reload only in Debug Mode
463
+ if (Odac.Config.debug) {
464
+ setInterval(async () => {
465
+ await this.#init()
466
+ }, 5000)
467
+ }
345
468
  }
346
469
 
347
470
  #registerInternalRoutes() {
@@ -456,6 +579,8 @@ class Route {
456
579
  }
457
580
 
458
581
  set(type, url, file, options = {}) {
582
+ const capturedMiddlewares = this._pendingMiddlewares.length > 0 ? [...this._pendingMiddlewares] : undefined
583
+
459
584
  if (Array.isArray(type)) {
460
585
  type = type.map(t => t.toLowerCase())
461
586
  for (const t of type) {
@@ -473,40 +598,82 @@ class Route {
473
598
  const isFunction = typeof file === 'function'
474
599
  let path = `${__dir}/route/${Odac.Route.buff}.js`
475
600
 
601
+ let action = null
602
+
476
603
  if (!isFunction && file) {
477
- path = `${__dir}/controller/${type.replace('#', '')}/${file}.js`
478
- if (typeof file === 'string' && file.includes('.')) {
479
- let arr = file.split('.')
480
- path = `${__dir}/controller/${arr[0]}/${type.replace('#', '')}/${arr.slice(1).join('.')}.js`
604
+ if (typeof file === 'string' && file.includes('@')) {
605
+ let arr = file.split('@')
606
+ file = arr[0]
607
+ action = arr[1]
608
+ path = `${__dir}/controller/${file.replace(/\./g, '/')}.js`
609
+ } else {
610
+ path = `${__dir}/controller/${type.replace('#', '')}/${file}.js`
611
+ if (typeof file === 'string' && file.includes('.')) {
612
+ let arr = file.split('.')
613
+ path = `${__dir}/controller/${arr[0]}/${type.replace('#', '')}/${arr.slice(1).join('.')}.js`
614
+ }
481
615
  }
482
616
  }
483
617
 
484
618
  if (!this.routes[Odac.Route.buff]) this.routes[Odac.Route.buff] = {}
485
619
  if (!this.routes[Odac.Route.buff][type]) this.routes[Odac.Route.buff][type] = {}
486
620
 
487
- if (this.routes[Odac.Route.buff][type][url]) {
488
- this.routes[Odac.Route.buff][type][url].loaded = routes2[Odac.Route.buff]
489
- if (!isFunction && this.routes[Odac.Route.buff][type][url].mtime < fs.statSync(path).mtimeMs) {
490
- delete this.routes[Odac.Route.buff][type][url]
491
- delete require.cache[require.resolve(path)]
492
- } else return this
493
- }
621
+ const task = async () => {
622
+ if (this.routes[Odac.Route.buff][type][url]) {
623
+ this.routes[Odac.Route.buff][type][url].loaded = routes2[Odac.Route.buff]
624
+ if (!isFunction) {
625
+ try {
626
+ const stat = await fsPromises.stat(path)
627
+ if (this.routes[Odac.Route.buff][type][url].mtime < stat.mtimeMs) {
628
+ delete this.routes[Odac.Route.buff][type][url]
629
+ delete require.cache[require.resolve(path)]
630
+ } else {
631
+ return
632
+ }
633
+ } catch {
634
+ // File error, proceed to reload or re-set
635
+ }
636
+ } else {
637
+ return
638
+ }
639
+ }
494
640
 
495
- if (isFunction || fs.existsSync(path)) {
496
- if (!this.routes[Odac.Route.buff][type][url]) this.routes[Odac.Route.buff][type][url] = {}
497
- this.routes[Odac.Route.buff][type][url].cache = isFunction ? file : require(path)
498
- this.routes[Odac.Route.buff][type][url].type = isFunction ? 'function' : 'controller'
499
- this.routes[Odac.Route.buff][type][url].file = file
500
- this.routes[Odac.Route.buff][type][url].mtime = isFunction ? Date.now() : fs.statSync(path).mtimeMs
501
- this.routes[Odac.Route.buff][type][url].path = path
502
- this.routes[Odac.Route.buff][type][url].loaded = routes2[Odac.Route.buff]
503
- this.routes[Odac.Route.buff][type][url].token = options.token ?? true
504
-
505
- this.routes[Odac.Route.buff][type][url].middlewares = this._pendingMiddlewares.length > 0 ? [...this._pendingMiddlewares] : undefined
506
- } else if (file && typeof file === 'string') {
507
- console.error(`\x1b[31m[Odac]\x1b[0m Controller not found: \x1b[33m${path}\x1b[0m`)
641
+ if (isFunction) {
642
+ if (!this.routes[Odac.Route.buff][type][url]) this.routes[Odac.Route.buff][type][url] = {}
643
+ this.routes[Odac.Route.buff][type][url].cache = file
644
+ this.routes[Odac.Route.buff][type][url].type = 'function'
645
+ this.routes[Odac.Route.buff][type][url].file = file
646
+ this.routes[Odac.Route.buff][type][url].mtime = Date.now()
647
+ this.routes[Odac.Route.buff][type][url].path = path
648
+ this.routes[Odac.Route.buff][type][url].loaded = routes2[Odac.Route.buff]
649
+ this.routes[Odac.Route.buff][type][url].token = options.token ?? true
650
+ this.routes[Odac.Route.buff][type][url].action = action
651
+
652
+ this.routes[Odac.Route.buff][type][url].middlewares = capturedMiddlewares
653
+ } else {
654
+ try {
655
+ const stat = await fsPromises.stat(path)
656
+ if (!this.routes[Odac.Route.buff][type][url]) this.routes[Odac.Route.buff][type][url] = {}
657
+ this.routes[Odac.Route.buff][type][url].cache = require(path)
658
+ this.routes[Odac.Route.buff][type][url].type = 'controller'
659
+ this.routes[Odac.Route.buff][type][url].file = file
660
+ this.routes[Odac.Route.buff][type][url].mtime = stat.mtimeMs
661
+ this.routes[Odac.Route.buff][type][url].path = path
662
+ this.routes[Odac.Route.buff][type][url].loaded = routes2[Odac.Route.buff]
663
+ this.routes[Odac.Route.buff][type][url].token = options.token ?? true
664
+ this.routes[Odac.Route.buff][type][url].action = action
665
+
666
+ this.routes[Odac.Route.buff][type][url].middlewares = capturedMiddlewares
667
+ } catch {
668
+ if (file && typeof file === 'string') {
669
+ console.error(`\x1b[31m[Odac]\x1b[0m Controller not found: \x1b[33m${path}\x1b[0m`)
670
+ }
671
+ }
672
+ }
508
673
  }
509
674
 
675
+ this._pendingRouteLoads.push(task())
676
+
510
677
  return this
511
678
  }
512
679
 
@@ -566,15 +733,31 @@ class Route {
566
733
  return this
567
734
  }
568
735
 
569
- authPost(path, authFile, file) {
570
- if (authFile) this.set('#post', path, authFile)
571
- if (file) this.post(path, file)
736
+ authPost(path, authFile, file, options) {
737
+ let opts = options
738
+ let publicFile = file
739
+
740
+ if (publicFile && typeof publicFile === 'object' && !opts) {
741
+ opts = publicFile
742
+ publicFile = undefined
743
+ }
744
+
745
+ if (authFile) this.set('#post', path, authFile, opts)
746
+ if (publicFile) this.post(path, publicFile, opts)
572
747
  return this
573
748
  }
574
749
 
575
- authGet(path, authFile, file) {
576
- if (authFile) this.set('#get', path, authFile)
577
- if (file) this.get(path, file)
750
+ authGet(path, authFile, file, options) {
751
+ let opts = options
752
+ let publicFile = file
753
+
754
+ if (publicFile && typeof publicFile === 'object' && !opts) {
755
+ opts = publicFile
756
+ publicFile = undefined
757
+ }
758
+
759
+ if (authFile) this.set('#get', path, authFile, opts)
760
+ if (publicFile) this.get(path, publicFile, opts)
578
761
  return this
579
762
  }
580
763
 
package/src/Server.js CHANGED
@@ -7,13 +7,17 @@ module.exports = {
7
7
  init: function () {
8
8
  let args = process.argv.slice(2)
9
9
  if (args[0] == 'framework' && args[1] == 'run') args = args.slice(2)
10
- let port = parseInt(args[0] ?? '1071')
10
+ let port = parseInt(args[0])
11
+ if (isNaN(port)) port = parseInt(process.env.PORT || '1071')
11
12
 
12
13
  if (cluster.isPrimary) {
13
- const numCPUs = os.cpus().length
14
+ const numCPUs = Odac.Config.debug ? 1 : os.cpus().length
14
15
  let isShuttingDown = false
15
16
 
16
- console.log(`Odac Server running on \x1b]8;;http://127.0.0.1:${port}\x1b\\\x1b[4mhttp://127.0.0.1:${port}\x1b[0m\x1b]8;;\x1b\\.`)
17
+ const mode = Odac.Config.debug ? '\x1b[33mDevelopment\x1b[0m' : '\x1b[32mProduction\x1b[0m'
18
+ console.log(
19
+ `Odac Server running on \x1b]8;;http://127.0.0.1:${port}\x1b\\\x1b[4mhttp://127.0.0.1:${port}\x1b[0m\x1b]8;;\x1b\\ in ${mode} mode.`
20
+ )
17
21
 
18
22
  // Start session garbage collector (runs every hour, expires after 7 days)
19
23
  Odac.Storage.startSessionGC()
@@ -68,6 +72,39 @@ module.exports = {
68
72
  return Odac.Route.request(req, res)
69
73
  })
70
74
 
75
+ /**
76
+ * ENTERPRISE PERFORMANCE CONFIGURATION
77
+ * ------------------------------------
78
+ * 1. Keep-Alive: Set higher than the upstream Load Balancer/Proxy (usually 60s).
79
+ * This prevents the "502 Bad Gateway" race condition where Node closes
80
+ * an idle connection while the proxy attempts to reuse it.
81
+ */
82
+ server.keepAliveTimeout = 65000 // 65 seconds
83
+ server.headersTimeout = 66000 // 66 seconds (Must be > keepAliveTimeout)
84
+
85
+ /**
86
+ * 2. Low Latency: Disable Nagle's Algorithm.
87
+ * We want to send data immediately, even if the packet is small.
88
+ * Critical for sub-millisecond API responses.
89
+ */
90
+ server.on('connection', socket => {
91
+ socket.setNoDelay(true)
92
+ })
93
+
94
+ /**
95
+ * 3. Connection Rotation: Force reset after 10k requests.
96
+ * - Helps with Load Balancing (clients are forced to reconnect and potentially pick a new pod/worker).
97
+ * - Mitigates long-term memory leaks in the TLS/Socket layer.
98
+ */
99
+ server.maxRequestsPerSocket = 10000
100
+
101
+ /**
102
+ * 4. Hard Timeout: Kill connection if request processing (headers + body) takes too long.
103
+ * - Defaults to 0 (unlimited) or 5min in older Node.
104
+ * - 30s is more than enough for an API; fail fast if the client is stuck.
105
+ */
106
+ server.requestTimeout = 30000 // 30 seconds
107
+
71
108
  server.on('upgrade', (req, socket, head) => {
72
109
  const id = nodeCrypto.randomBytes(16).toString('hex')
73
110
  const param = Odac.instance(id, req, null)