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.
- package/.agent/rules/coding.md +27 -0
- package/.agent/rules/memory.md +33 -0
- package/.agent/rules/project.md +30 -0
- package/.agent/rules/workflow.md +16 -0
- package/.github/workflows/release.yml +42 -1
- package/.github/workflows/test-coverage.yml +6 -5
- package/.github/workflows/test-publish.yml +36 -0
- package/.husky/pre-commit +10 -0
- package/.husky/pre-push +13 -0
- package/.releaserc.js +3 -3
- package/CHANGELOG.md +67 -0
- package/README.md +16 -0
- package/bin/odac.js +182 -40
- package/client/odac.js +10 -4
- package/docs/backend/01-overview/03-development-server.md +38 -45
- package/docs/backend/02-structure/01-typical-project-layout.md +59 -26
- package/docs/backend/03-config/00-configuration-overview.md +6 -6
- package/docs/backend/03-config/01-database-connection.md +2 -2
- package/docs/backend/03-config/02-static-route-mapping-optional.md +1 -1
- package/docs/backend/03-config/03-request-timeout.md +1 -1
- package/docs/backend/03-config/04-environment-variables.md +4 -4
- package/docs/backend/03-config/05-early-hints.md +2 -2
- package/docs/backend/04-routing/03-api-and-data-routes.md +18 -0
- package/docs/backend/04-routing/07-cron-jobs.md +17 -1
- package/docs/backend/05-controllers/01-how-to-build-a-controller.md +48 -3
- package/docs/backend/05-controllers/03-controller-classes.md +40 -20
- package/docs/backend/06-request-and-response/01-the-request-object-what-is-the-user-asking-for.md +17 -0
- package/docs/backend/07-views/10-styling-and-tailwind.md +93 -0
- package/docs/backend/08-database/01-getting-started.md +2 -2
- package/docs/backend/10-authentication/03-register.md +1 -1
- package/docs/backend/10-authentication/04-odac-register-forms.md +2 -2
- package/docs/backend/10-authentication/05-session-management.md +15 -1
- package/docs/backend/10-authentication/06-odac-login-forms.md +2 -2
- package/docs/backend/10-authentication/07-magic-links.md +1 -1
- package/docs/index.json +5 -1
- package/jest.config.js +1 -1
- package/package.json +9 -5
- package/src/Auth.js +58 -23
- package/src/Config.js +7 -7
- package/src/Env.js +3 -1
- package/src/Ipc.js +7 -0
- package/src/Lang.js +9 -2
- package/src/Odac.js +44 -35
- package/src/Request.js +1 -1
- package/src/Route/Cron.js +58 -17
- package/src/Route/Internal.js +1 -1
- package/src/Route.js +282 -99
- package/src/Server.js +40 -3
- package/src/Storage.js +4 -0
- package/src/Token.js +6 -4
- package/src/Validator.js +1 -1
- package/src/Var.js +22 -6
- package/src/View/EarlyHints.js +43 -33
- package/src/View/Form.js +17 -11
- package/src/View.js +62 -6
- package/template/package.json +3 -1
- package/template/view/content/home.html +3 -3
- package/template/view/head/main.html +2 -2
- package/test/Client.test.js +168 -0
- package/test/Config.test.js +112 -0
- package/test/Lang.test.js +92 -0
- package/test/Odac.test.js +86 -0
- package/test/{framework/middleware.test.js → Route/Middleware.test.js} +2 -2
- package/test/{framework/Route.test.js → Route.test.js} +1 -1
- package/test/{framework/View → View}/EarlyHints.test.js +1 -1
- package/test/{framework/WebSocket.test.js → WebSocket.test.js} +2 -2
- package/test/scripts/check-coverage.js +4 -4
- package/test/cli/Cli.test.js +0 -36
- package/test/core/Commands.test.js +0 -538
- package/test/core/Config.test.js +0 -1432
- package/test/core/Lang.test.js +0 -250
- package/test/core/Odac.test.js +0 -234
- package/test/core/Process.test.js +0 -156
- package/test/server/Api.test.js +0 -647
- package/test/server/DNS.test.js +0 -2050
- package/test/server/DNS.test.js.bak +0 -2084
- package/test/server/Hub.test.js +0 -497
- package/test/server/Log.test.js +0 -73
- package/test/server/Mail.account.test_.js +0 -460
- package/test/server/Mail.init.test_.js +0 -411
- package/test/server/Mail.test_.js +0 -1340
- package/test/server/SSL.test_.js +0 -1491
- package/test/server/Server.test.js +0 -765
- package/test/server/Service.test_.js +0 -1127
- package/test/server/Subdomain.test.js +0 -440
- package/test/server/Web/Firewall.test.js +0 -175
- package/test/server/Web/Proxy.test.js +0 -397
- package/test/server/Web.test.js +0 -1494
- package/test/server/__mocks__/acme-client.js +0 -17
- package/test/server/__mocks__/bcrypt.js +0 -50
- package/test/server/__mocks__/child_process.js +0 -389
- package/test/server/__mocks__/crypto.js +0 -432
- package/test/server/__mocks__/fs.js +0 -450
- package/test/server/__mocks__/globalOdac.js +0 -227
- package/test/server/__mocks__/http.js +0 -575
- package/test/server/__mocks__/https.js +0 -272
- package/test/server/__mocks__/index.js +0 -249
- package/test/server/__mocks__/mail/server.js +0 -100
- package/test/server/__mocks__/mail/smtp.js +0 -31
- package/test/server/__mocks__/mailparser.js +0 -81
- package/test/server/__mocks__/net.js +0 -369
- package/test/server/__mocks__/node-forge.js +0 -328
- package/test/server/__mocks__/os.js +0 -320
- package/test/server/__mocks__/path.js +0 -291
- package/test/server/__mocks__/selfsigned.js +0 -8
- package/test/server/__mocks__/server/src/mail/server.js +0 -100
- package/test/server/__mocks__/server/src/mail/smtp.js +0 -31
- package/test/server/__mocks__/smtp-server.js +0 -106
- package/test/server/__mocks__/sqlite3.js +0 -394
- package/test/server/__mocks__/testFactories.js +0 -299
- package/test/server/__mocks__/testHelpers.js +0 -363
- package/test/server/__mocks__/tls.js +0 -229
- /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 (
|
|
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
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
package/src/Route/Internal.js
CHANGED
|
@@ -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
|
|
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
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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('/../')
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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',
|
|
221
|
-
return fs.createReadStream(
|
|
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
|
-
|
|
326
|
+
try {
|
|
327
|
+
await fsPromises.access(middlewareDir)
|
|
328
|
+
} catch {
|
|
329
|
+
return
|
|
330
|
+
}
|
|
263
331
|
|
|
264
|
-
|
|
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
|
|
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
|
-
|
|
287
|
-
|
|
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
|
-
|
|
293
|
-
if (Odac.Route.class[name].mtime >=
|
|
294
|
-
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
-
|
|
488
|
-
this.routes[Odac.Route.buff][type][url]
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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
|
-
|
|
571
|
-
|
|
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
|
-
|
|
577
|
-
|
|
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]
|
|
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
|
-
|
|
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)
|