odac 0.9.0 → 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.
Files changed (208) hide show
  1. package/.github/workflows/auto-pr-description.yml +0 -2
  2. package/.github/workflows/codeql.yml +46 -0
  3. package/.github/workflows/release.yml +13 -6
  4. package/.github/workflows/test-coverage.yml +10 -9
  5. package/.releaserc.js +9 -6
  6. package/CHANGELOG.md +62 -150
  7. package/CODE_OF_CONDUCT.md +1 -1
  8. package/CONTRIBUTING.md +8 -8
  9. package/LICENSE +21 -661
  10. package/README.md +12 -12
  11. package/SECURITY.md +4 -4
  12. package/bin/odac.js +101 -0
  13. package/{framework/web/candy.js → client/odac.js} +310 -44
  14. package/docs/backend/01-overview/{01-whats-in-the-candy-box.md → 01-whats-in-the-odac-box.md} +4 -2
  15. package/docs/backend/01-overview/02-super-handy-helper-functions.md +29 -1
  16. package/docs/backend/01-overview/03-development-server.md +11 -11
  17. package/docs/backend/02-structure/01-typical-project-layout.md +4 -4
  18. package/docs/backend/03-config/00-configuration-overview.md +6 -6
  19. package/docs/backend/03-config/01-database-connection.md +1 -1
  20. package/docs/backend/03-config/02-static-route-mapping-optional.md +4 -4
  21. package/docs/backend/03-config/04-environment-variables.md +20 -20
  22. package/docs/backend/03-config/05-early-hints.md +4 -4
  23. package/docs/backend/04-routing/01-basic-page-routes.md +4 -4
  24. package/docs/backend/04-routing/02-controller-less-view-routes.md +5 -5
  25. package/docs/backend/04-routing/03-api-and-data-routes.md +3 -3
  26. package/docs/backend/04-routing/04-authentication-aware-routes.md +5 -5
  27. package/docs/backend/04-routing/05-advanced-routing.md +3 -3
  28. package/docs/backend/04-routing/06-error-pages.md +17 -17
  29. package/docs/backend/04-routing/07-cron-jobs.md +13 -13
  30. package/docs/backend/04-routing/08-middleware.md +214 -0
  31. package/docs/backend/04-routing/09-websocket-auth-middleware.md +292 -0
  32. package/docs/backend/04-routing/09-websocket-examples.md +381 -0
  33. package/docs/backend/04-routing/09-websocket-quick-reference.md +211 -0
  34. package/docs/backend/04-routing/09-websocket.md +298 -0
  35. package/docs/backend/05-controllers/01-how-to-build-a-controller.md +3 -3
  36. package/docs/backend/05-controllers/02-your-trusty-odac-assistant.md +41 -0
  37. package/docs/backend/05-controllers/03-controller-classes.md +19 -19
  38. package/docs/backend/05-forms/01-custom-forms.md +114 -114
  39. package/docs/backend/05-forms/02-automatic-database-insert.md +82 -82
  40. package/docs/backend/06-request-and-response/01-the-request-object-what-is-the-user-asking-for.md +26 -26
  41. package/docs/backend/06-request-and-response/02-sending-a-response-replying-to-the-user.md +10 -10
  42. package/docs/backend/07-views/01-the-view-directory.md +1 -1
  43. package/docs/backend/07-views/02-rendering-a-view.md +22 -22
  44. package/docs/backend/07-views/03-template-syntax.md +52 -52
  45. package/docs/backend/07-views/03-variables.md +84 -84
  46. package/docs/backend/07-views/04-request-data.md +57 -57
  47. package/docs/backend/07-views/05-conditionals.md +78 -78
  48. package/docs/backend/07-views/06-loops.md +114 -114
  49. package/docs/backend/07-views/07-translations.md +66 -66
  50. package/docs/backend/07-views/08-backend-javascript.md +103 -103
  51. package/docs/backend/07-views/09-comments.md +71 -71
  52. package/docs/backend/08-database/01-database-connection.md +8 -8
  53. package/docs/backend/08-database/02-using-mysql.md +49 -49
  54. package/docs/backend/09-validation/01-the-validator-service.md +38 -38
  55. package/docs/backend/10-authentication/01-user-logins-with-authjs.md +15 -15
  56. package/docs/backend/10-authentication/02-foiling-villains-with-csrf-protection.md +10 -10
  57. package/docs/backend/10-authentication/03-register.md +12 -12
  58. package/docs/backend/10-authentication/{04-candy-register-forms.md → 04-odac-register-forms.md} +141 -141
  59. package/docs/backend/10-authentication/05-session-management.md +10 -10
  60. package/docs/backend/10-authentication/{06-candy-login-forms.md → 06-odac-login-forms.md} +125 -125
  61. package/docs/backend/11-mail/01-the-mail-service.md +5 -5
  62. package/docs/backend/12-streaming/01-streaming-overview.md +96 -54
  63. package/docs/backend/13-utilities/{01-candy-var.md → 01-odac-var.md} +109 -109
  64. package/docs/frontend/01-overview/01-introduction.md +30 -30
  65. package/docs/frontend/02-ajax-navigation/01-quick-start.md +45 -45
  66. package/docs/frontend/02-ajax-navigation/02-configuration.md +14 -14
  67. package/docs/frontend/02-ajax-navigation/03-advanced-usage.md +36 -36
  68. package/docs/frontend/03-forms/01-form-handling.md +32 -32
  69. package/docs/frontend/04-api-requests/01-get-post.md +33 -33
  70. package/docs/frontend/05-streaming/01-client-streaming.md +15 -15
  71. package/docs/frontend/06-websocket/00-overview.md +76 -0
  72. package/docs/frontend/06-websocket/01-websocket-client.md +139 -0
  73. package/docs/frontend/06-websocket/02-shared-websocket.md +149 -0
  74. package/docs/index.json +49 -11
  75. package/eslint.config.mjs +6 -6
  76. package/{framework/index.js → index.js} +1 -1
  77. package/package.json +14 -39
  78. package/{framework/src → src}/Auth.js +59 -59
  79. package/{framework/src → src}/Config.js +3 -3
  80. package/{framework/src → src}/Lang.js +7 -7
  81. package/{framework/src → src}/Mail.js +5 -5
  82. package/{framework/src → src}/Mysql.js +42 -42
  83. package/src/Odac.js +112 -0
  84. package/{framework/src → src}/Request.js +38 -36
  85. package/{framework/src → src}/Route/Internal.js +116 -116
  86. package/src/Route/Middleware.js +75 -0
  87. package/src/Route.js +621 -0
  88. package/src/Server.js +22 -0
  89. package/{framework/src → src}/Stream.js +11 -3
  90. package/{framework/src → src}/Validator.js +21 -21
  91. package/{framework/src → src}/Var.js +5 -5
  92. package/{framework/src → src}/View/EarlyHints.js +1 -1
  93. package/{framework/src → src}/View/Form.js +69 -69
  94. package/{framework/src → src}/View.js +78 -81
  95. package/src/WebSocket.js +403 -0
  96. package/template/config.json +5 -0
  97. package/{web → template}/controller/page/about.js +6 -6
  98. package/{web → template}/controller/page/index.js +9 -9
  99. package/{web → template}/package.json +4 -5
  100. package/{web → template}/public/assets/css/style.css +4 -4
  101. package/{web → template}/public/assets/js/app.js +6 -6
  102. package/{web → template}/route/www.js +6 -6
  103. package/{web → template}/skeleton/main.html +1 -1
  104. package/{web → template}/view/content/about.html +5 -5
  105. package/{web → template}/view/content/home.html +12 -12
  106. package/template/view/footer/main.html +11 -0
  107. package/{web → template}/view/head/main.html +1 -1
  108. package/{web → template}/view/header/main.html +2 -2
  109. package/test/core/Candy.test.js +58 -58
  110. package/test/core/Commands.test.js +7 -7
  111. package/test/core/Config.test.js +82 -85
  112. package/test/core/Lang.test.js +2 -2
  113. package/test/core/Process.test.js +6 -6
  114. package/test/framework/Route.test.js +56 -37
  115. package/test/framework/View/EarlyHints.test.js +2 -2
  116. package/test/framework/WebSocket.test.js +100 -0
  117. package/test/framework/middleware.test.js +85 -0
  118. package/test/server/Api.test.js +31 -31
  119. package/test/server/DNS.test.js +11 -11
  120. package/test/server/Hub.test.js +497 -0
  121. package/test/server/Mail.account.test_.js +3 -3
  122. package/test/server/Mail.init.test_.js +10 -10
  123. package/test/server/Mail.test_.js +20 -20
  124. package/test/server/SSL.test_.js +54 -54
  125. package/test/server/Server.test.js +39 -39
  126. package/test/server/Service.test_.js +7 -7
  127. package/test/server/Subdomain.test.js +7 -7
  128. package/test/server/Web/Firewall.test.js +87 -87
  129. package/test/server/Web/Proxy.test.js +397 -0
  130. package/test/server/{Web.test_.js → Web.test.js} +137 -205
  131. package/test/server/__mocks__/fs.js +2 -2
  132. package/test/server/__mocks__/{globalCandy.js → globalOdac.js} +5 -5
  133. package/test/server/__mocks__/index.js +6 -6
  134. package/test/server/__mocks__/testFactories.js +1 -1
  135. package/test/server/__mocks__/testHelpers.js +7 -7
  136. package/.husky/pre-commit +0 -2
  137. package/.kiro/steering/code-style.md +0 -56
  138. package/.kiro/steering/product.md +0 -20
  139. package/.kiro/steering/structure.md +0 -77
  140. package/.kiro/steering/tech.md +0 -87
  141. package/AGENTS.md +0 -84
  142. package/bin/candy +0 -10
  143. package/bin/candypack +0 -10
  144. package/cli/index.js +0 -3
  145. package/cli/src/Cli.js +0 -348
  146. package/cli/src/Connector.js +0 -93
  147. package/cli/src/Monitor.js +0 -416
  148. package/core/Candy.js +0 -87
  149. package/core/Commands.js +0 -239
  150. package/core/Config.js +0 -1094
  151. package/core/Lang.js +0 -52
  152. package/core/Log.js +0 -43
  153. package/core/Process.js +0 -26
  154. package/docs/backend/05-controllers/02-your-trusty-candy-assistant.md +0 -20
  155. package/docs/server/01-installation/01-quick-install.md +0 -19
  156. package/docs/server/01-installation/02-manual-installation-via-npm.md +0 -9
  157. package/docs/server/02-get-started/01-core-concepts.md +0 -7
  158. package/docs/server/02-get-started/02-basic-commands.md +0 -57
  159. package/docs/server/02-get-started/03-cli-reference.md +0 -276
  160. package/docs/server/02-get-started/04-cli-quick-reference.md +0 -102
  161. package/docs/server/03-service/01-start-a-new-service.md +0 -57
  162. package/docs/server/03-service/02-delete-a-service.md +0 -48
  163. package/docs/server/04-web/01-create-a-website.md +0 -36
  164. package/docs/server/04-web/02-list-websites.md +0 -9
  165. package/docs/server/04-web/03-delete-a-website.md +0 -29
  166. package/docs/server/05-subdomain/01-create-a-subdomain.md +0 -32
  167. package/docs/server/05-subdomain/02-list-subdomains.md +0 -33
  168. package/docs/server/05-subdomain/03-delete-a-subdomain.md +0 -41
  169. package/docs/server/06-ssl/01-renew-an-ssl-certificate.md +0 -34
  170. package/docs/server/07-mail/01-create-a-mail-account.md +0 -23
  171. package/docs/server/07-mail/02-delete-a-mail-account.md +0 -20
  172. package/docs/server/07-mail/03-list-mail-accounts.md +0 -20
  173. package/docs/server/07-mail/04-change-account-password.md +0 -23
  174. package/framework/src/Candy.js +0 -81
  175. package/framework/src/Route.js +0 -455
  176. package/framework/src/Server.js +0 -15
  177. package/locale/de-DE.json +0 -80
  178. package/locale/en-US.json +0 -79
  179. package/locale/es-ES.json +0 -80
  180. package/locale/fr-FR.json +0 -80
  181. package/locale/pt-BR.json +0 -80
  182. package/locale/ru-RU.json +0 -80
  183. package/locale/tr-TR.json +0 -85
  184. package/locale/zh-CN.json +0 -80
  185. package/server/index.js +0 -5
  186. package/server/src/Api.js +0 -88
  187. package/server/src/DNS.js +0 -940
  188. package/server/src/Hub.js +0 -535
  189. package/server/src/Mail.js +0 -571
  190. package/server/src/SSL.js +0 -180
  191. package/server/src/Server.js +0 -27
  192. package/server/src/Service.js +0 -248
  193. package/server/src/Subdomain.js +0 -64
  194. package/server/src/Web/Firewall.js +0 -170
  195. package/server/src/Web/Proxy.js +0 -134
  196. package/server/src/Web.js +0 -451
  197. package/server/src/mail/imap.js +0 -1091
  198. package/server/src/mail/server.js +0 -32
  199. package/server/src/mail/smtp.js +0 -786
  200. package/test/server/Client.test.js +0 -338
  201. package/test/server/__mocks__/http-proxy.js +0 -105
  202. package/watchdog/index.js +0 -3
  203. package/watchdog/src/Watchdog.js +0 -156
  204. package/web/config.json +0 -5
  205. package/web/view/footer/main.html +0 -11
  206. /package/{framework/src → src}/Env.js +0 -0
  207. /package/{framework/src → src}/Route/Cron.js +0 -0
  208. /package/{framework/src → src}/Token.js +0 -0
@@ -1,4 +1,4 @@
1
- class candy {
1
+ class Odac {
2
2
  actions = {}
3
3
  #data = null
4
4
  fn = {}
@@ -52,7 +52,7 @@ class candy {
52
52
  }
53
53
 
54
54
  document.dispatchEvent(
55
- new CustomEvent('candy:ajaxSuccess', {
55
+ new CustomEvent('odac:ajaxSuccess', {
56
56
  detail: {response: responseData, status: xhr.statusText, xhr, requestUrl: url}
57
57
  })
58
58
  )
@@ -245,14 +245,14 @@ class candy {
245
245
  }
246
246
 
247
247
  client() {
248
- if (!document.cookie.includes('candy_client=')) return null
249
- return document.cookie.split('candy_client=')[1].split(';')[0]
248
+ if (!document.cookie.includes('odac_client=')) return null
249
+ return document.cookie.split('odac_client=')[1].split(';')[0]
250
250
  }
251
251
 
252
252
  data() {
253
253
  if (this.#data) return this.#data
254
- if (!document.cookie.includes('candy_data=')) return null
255
- return JSON.parse(unescape(document.cookie.split('candy_data=')[1].split(';')[0]))
254
+ if (!document.cookie.includes('odac_data=')) return null
255
+ return JSON.parse(unescape(document.cookie.split('odac_data=')[1].split(';')[0]))
256
256
  }
257
257
 
258
258
  form(obj, callback) {
@@ -284,11 +284,11 @@ class candy {
284
284
 
285
285
  const customMessage = input.getAttribute(`data-error-${errorType}`)
286
286
  if (customMessage) {
287
- let errorSpan = formElement.querySelector(`[candy-form-error="${input.name}"]`)
287
+ let errorSpan = formElement.querySelector(`[odac-form-error="${input.name}"]`)
288
288
 
289
289
  if (!errorSpan) {
290
290
  errorSpan = document.createElement('span')
291
- errorSpan.setAttribute('candy-form-error', input.name)
291
+ errorSpan.setAttribute('odac-form-error', input.name)
292
292
 
293
293
  if ((input.type === 'checkbox' || input.type === 'radio') && input.id) {
294
294
  const label = formElement.querySelector(`label[for="${input.id}"]`)
@@ -309,7 +309,7 @@ class candy {
309
309
 
310
310
  for (const input of inputs) {
311
311
  input.style.borderColor = ''
312
- const errorSpan = formElement.querySelector(`[candy-form-error="${input.name}"]`)
312
+ const errorSpan = formElement.querySelector(`[odac-form-error="${input.name}"]`)
313
313
  if (errorSpan) {
314
314
  errorSpan.style.display = 'none'
315
315
  errorSpan.textContent = ''
@@ -355,13 +355,13 @@ class candy {
355
355
 
356
356
  let actions = this.actions
357
357
  if (
358
- actions.candy &&
359
- actions.candy.form &&
360
- actions.candy.form.input &&
361
- actions.candy.form.input.class &&
362
- actions.candy.form.input.class.invalid
358
+ actions.odac &&
359
+ actions.odac.form &&
360
+ actions.odac.form.input &&
361
+ actions.odac.form.input.class &&
362
+ actions.odac.form.input.class.invalid
363
363
  ) {
364
- const invalidClass = actions.candy.form.input.class.invalid
364
+ const invalidClass = actions.odac.form.input.class.invalid
365
365
  formElement
366
366
  .querySelectorAll(`select.${invalidClass},input.${invalidClass},textarea.${invalidClass}`)
367
367
  .forEach(el => el.classList.remove(invalidClass))
@@ -369,10 +369,10 @@ class candy {
369
369
 
370
370
  if (obj.messages !== false) {
371
371
  if (obj.messages == undefined || obj.messages == true || obj.messages.includes('error')) {
372
- formElement.querySelectorAll('*[candy-form-error]').forEach(el => (el.style.display = 'none'))
372
+ formElement.querySelectorAll('*[odac-form-error]').forEach(el => (el.style.display = 'none'))
373
373
  }
374
374
  if (obj.messages == undefined || obj.messages == true || obj.messages.includes('success')) {
375
- formElement.querySelectorAll('*[candy-form-success]').forEach(el => (el.style.display = 'none'))
375
+ formElement.querySelectorAll('*[odac-form-success]').forEach(el => (el.style.display = 'none'))
376
376
  }
377
377
  }
378
378
 
@@ -414,17 +414,20 @@ class candy {
414
414
  if (!data.result) return false
415
415
  if (obj.messages == undefined || obj.messages) {
416
416
  if (data.result.success && (obj.messages == undefined || obj.messages.includes('success') || obj.messages == true)) {
417
- const successEl = formElement.querySelector('*[candy-form-success]')
417
+ const successEl = formElement.querySelector('*[odac-form-success]')
418
418
  if (successEl) {
419
419
  successEl.innerHTML = data.result.message
420
420
  this.#fadeIn(successEl)
421
421
  } else {
422
- formElement.insertAdjacentHTML('beforeend', `<span candy-form-success="${obj.form}">${data.result.message}</span>`)
422
+ const span = document.createElement('span')
423
+ span.setAttribute('odac-form-success', obj.form)
424
+ span.textContent = data.result.message
425
+ formElement.appendChild(span)
423
426
  }
424
427
  } else if (!data.result.success && data.errors) {
425
428
  Object.entries(data.errors).forEach(([name, message]) => {
426
429
  if (message) {
427
- let errorEl = formElement.querySelector(`[candy-form-error="${name}"]`)
430
+ let errorEl = formElement.querySelector(`[odac-form-error="${name}"]`)
428
431
  if (errorEl) {
429
432
  errorEl.textContent = message
430
433
  errorEl.style.cssText = 'display:block;color:#dc3545;font-size:0.875rem;margin-top:0.25rem'
@@ -432,7 +435,7 @@ class candy {
432
435
  const inputEl = formElement.querySelector(`*[name="${name}"]`)
433
436
  if (inputEl) {
434
437
  errorEl = document.createElement('span')
435
- errorEl.setAttribute('candy-form-error', name)
438
+ errorEl.setAttribute('odac-form-error', name)
436
439
  errorEl.textContent = message
437
440
  errorEl.style.cssText = 'display:block;color:#dc3545;font-size:0.875rem;margin-top:0.25rem'
438
441
 
@@ -446,9 +449,9 @@ class candy {
446
449
  } else {
447
450
  inputEl.parentNode.insertBefore(errorEl, inputEl.nextSibling)
448
451
  }
449
- } else if (name === '_candy_form') {
452
+ } else if (name === '_odac_form') {
450
453
  errorEl = document.createElement('div')
451
- errorEl.setAttribute('candy-form-error', name)
454
+ errorEl.setAttribute('odac-form-error', name)
452
455
  errorEl.textContent = message
453
456
  errorEl.style.cssText =
454
457
  'display:block;color:#dc3545;background-color:#f8d7da;border:1px solid #f5c2c7;border-radius:0.375rem;padding:0.75rem 1rem;margin-bottom:1rem;font-size:0.875rem'
@@ -465,7 +468,7 @@ class candy {
465
468
  'focus',
466
469
  function handler() {
467
470
  inputEl.style.borderColor = ''
468
- const errorEl = formElement.querySelector(`[candy-form-error="${name}"]`)
471
+ const errorEl = formElement.querySelector(`[odac-form-error="${name}"]`)
469
472
  if (errorEl) {
470
473
  errorEl.style.display = 'none'
471
474
  errorEl.textContent = ''
@@ -500,7 +503,7 @@ class candy {
500
503
  return xhr
501
504
  },
502
505
  error: () => {
503
- console.error('CandyJS:', 'Somethings went wrong...', '\nForm: ' + obj.form + '\nRequest: ' + formElement.getAttribute('action'))
506
+ console.error('Odac:', 'Somethings went wrong...', '\nForm: ' + obj.form + '\nRequest: ' + formElement.getAttribute('action'))
504
507
  },
505
508
  complete: () => {
506
509
  const submitButtons = formElement.querySelectorAll('button[type="submit"], input[type="submit"]')
@@ -541,12 +544,12 @@ class candy {
541
544
 
542
545
  token() {
543
546
  if (!this.#token.listener) {
544
- document.addEventListener('candy:ajaxSuccess', event => {
547
+ document.addEventListener('odac:ajaxSuccess', event => {
545
548
  const {detail} = event
546
549
  const {xhr, requestUrl} = detail
547
550
  if (requestUrl.includes('://')) return false
548
551
  try {
549
- const token = xhr.getResponseHeader('X-Candy-Token')
552
+ const token = xhr.getResponseHeader('X-Odac-Token')
550
553
  if (token) this.#token.hash.push(token)
551
554
  if (this.#token.hash.length > 2) this.#token.hash.shift()
552
555
  } catch (e) {
@@ -558,8 +561,8 @@ class candy {
558
561
  if (!this.#token.hash.length) {
559
562
  var req = new XMLHttpRequest()
560
563
  req.open('GET', '/', false)
561
- req.setRequestHeader('X-Candy', 'token')
562
- req.setRequestHeader('X-Candy-Client', this.client())
564
+ req.setRequestHeader('X-Odac', 'token')
565
+ req.setRequestHeader('X-Odac-Client', this.client())
563
566
  req.send(null)
564
567
  var req_data = JSON.parse(req.response)
565
568
  if (req_data.token) this.#token.hash.push(req_data.token)
@@ -570,7 +573,7 @@ class candy {
570
573
  this.#ajax({
571
574
  url: '/',
572
575
  type: 'GET',
573
- headers: {'X-Candy': 'token', 'X-Candy-Client': this.client()},
576
+ headers: {'X-Odac': 'token', 'X-Odac-Client': this.client()},
574
577
  success: data => {
575
578
  var result = JSON.parse(JSON.stringify(data))
576
579
  if (result.token) this.#token.hash.push(result.token)
@@ -622,7 +625,7 @@ class candy {
622
625
  window.history.pushState(null, document.title, finalUrl)
623
626
  }
624
627
 
625
- const newPage = ajaxXhr.getResponseHeader('X-Candy-Page')
628
+ const newPage = ajaxXhr.getResponseHeader('X-Odac-Page')
626
629
  if (newPage !== null) {
627
630
  this.#page = newPage
628
631
  document.documentElement.dataset.candyPage = newPage
@@ -668,9 +671,9 @@ class candy {
668
671
  url: url,
669
672
  type: 'GET',
670
673
  headers: {
671
- 'X-Candy': 'ajaxload',
672
- 'X-Candy-Load': Object.keys(this.#loader.elements).join(','),
673
- 'X-Candy-Skeleton': currentSkeleton || ''
674
+ 'X-Odac': 'ajaxload',
675
+ 'X-Odac-Load': Object.keys(this.#loader.elements).join(','),
676
+ 'X-Odac-Skeleton': currentSkeleton || ''
674
677
  },
675
678
  dataType: 'json',
676
679
  success: (data, status, xhr) => {
@@ -799,16 +802,279 @@ class candy {
799
802
  }
800
803
  }
801
804
  }
805
+
806
+ ws(path, options = {}) {
807
+ const {autoReconnect = true, reconnectDelay = 3000, maxReconnectAttempts = 10, shared = false, token = true} = options
808
+
809
+ if (shared && typeof SharedWorker !== 'undefined') {
810
+ return this.#createSharedWebSocket(path, {autoReconnect, reconnectDelay, maxReconnectAttempts, token})
811
+ }
812
+
813
+ let socket = null
814
+ let reconnectTimer = null
815
+ let reconnectAttempts = 0
816
+ let isClosed = false
817
+ const handlers = {}
818
+
819
+ const emit = (event, ...args) => {
820
+ if (handlers[event]) {
821
+ handlers[event].forEach(fn => fn(...args))
822
+ }
823
+ }
824
+
825
+ const connect = () => {
826
+ if (isClosed) return
827
+
828
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
829
+ const wsUrl = `${protocol}//${window.location.host}${path}`
830
+
831
+ const protocols = []
832
+ if (token) {
833
+ const csrfToken = this.token()
834
+ if (csrfToken) {
835
+ protocols.push(`odac-token-${csrfToken}`)
836
+ }
837
+ }
838
+
839
+ socket = protocols.length > 0 ? new WebSocket(wsUrl, protocols) : new WebSocket(wsUrl)
840
+
841
+ socket.onopen = () => {
842
+ reconnectAttempts = 0
843
+ emit('open')
844
+ }
845
+
846
+ socket.onmessage = e => {
847
+ try {
848
+ const data = JSON.parse(e.data)
849
+ emit('message', data)
850
+ } catch {
851
+ emit('message', e.data)
852
+ }
853
+ }
854
+
855
+ socket.onclose = e => {
856
+ emit('close', e)
857
+
858
+ if (autoReconnect && !isClosed && reconnectAttempts < maxReconnectAttempts) {
859
+ reconnectAttempts++
860
+ reconnectTimer = setTimeout(connect, reconnectDelay)
861
+ }
862
+ }
863
+
864
+ socket.onerror = e => {
865
+ emit('error', e)
866
+ }
867
+ }
868
+
869
+ connect()
870
+
871
+ return {
872
+ on: (event, handler) => {
873
+ if (!handlers[event]) handlers[event] = []
874
+ handlers[event].push(handler)
875
+ return this
876
+ },
877
+ off: (event, handler) => {
878
+ if (!handlers[event]) return
879
+ if (handler) {
880
+ handlers[event] = handlers[event].filter(h => h !== handler)
881
+ } else {
882
+ delete handlers[event]
883
+ }
884
+ return this
885
+ },
886
+ send: data => {
887
+ if (socket && socket.readyState === WebSocket.OPEN) {
888
+ socket.send(typeof data === 'object' ? JSON.stringify(data) : data)
889
+ }
890
+ return this
891
+ },
892
+ close: () => {
893
+ isClosed = true
894
+ if (reconnectTimer) clearTimeout(reconnectTimer)
895
+ if (socket) socket.close()
896
+ },
897
+ get state() {
898
+ return socket ? socket.readyState : WebSocket.CLOSED
899
+ },
900
+ get connected() {
901
+ return socket && socket.readyState === WebSocket.OPEN
902
+ }
903
+ }
904
+ }
905
+
906
+ #createSharedWebSocket(path, options) {
907
+ const workerUrl = this.#createWorkerBlob()
908
+ const worker = new SharedWorker(workerUrl, `odac-ws-${path}`)
909
+ const handlers = {}
910
+ let isConnected = false
911
+
912
+ const emit = (event, ...args) => {
913
+ if (handlers[event]) {
914
+ handlers[event].forEach(fn => fn(...args))
915
+ }
916
+ }
917
+
918
+ worker.port.onmessage = e => {
919
+ const {type, data} = e.data
920
+
921
+ switch (type) {
922
+ case 'open':
923
+ isConnected = true
924
+ emit('open')
925
+ break
926
+ case 'message':
927
+ emit('message', data)
928
+ break
929
+ case 'close':
930
+ isConnected = false
931
+ emit('close', data)
932
+ break
933
+ case 'error':
934
+ emit('error', data)
935
+ break
936
+ }
937
+ }
938
+
939
+ worker.port.start()
940
+
941
+ const token = options.token ? this.token() : null
942
+
943
+ worker.port.postMessage({
944
+ type: 'connect',
945
+ path,
946
+ host: window.location.host,
947
+ protocol: window.location.protocol === 'https:' ? 'wss:' : 'ws:',
948
+ token,
949
+ options
950
+ })
951
+
952
+ return {
953
+ on: (event, handler) => {
954
+ if (!handlers[event]) handlers[event] = []
955
+ handlers[event].push(handler)
956
+ return this
957
+ },
958
+ off: (event, handler) => {
959
+ if (!handlers[event]) return
960
+ if (handler) {
961
+ handlers[event] = handlers[event].filter(h => h !== handler)
962
+ } else {
963
+ delete handlers[event]
964
+ }
965
+ return this
966
+ },
967
+ send: data => {
968
+ worker.port.postMessage({
969
+ type: 'send',
970
+ data: typeof data === 'object' ? JSON.stringify(data) : data
971
+ })
972
+ return this
973
+ },
974
+ close: () => {
975
+ worker.port.postMessage({type: 'close'})
976
+ worker.port.close()
977
+ },
978
+ get connected() {
979
+ return isConnected
980
+ }
981
+ }
982
+ }
983
+
984
+ #createWorkerBlob() {
985
+ const workerCode = `
986
+ let socket = null
987
+ let reconnectTimer = null
988
+ let reconnectAttempts = 0
989
+ let options = {}
990
+ let protocols = []
991
+ const ports = new Set()
992
+
993
+ function broadcast(type, data) {
994
+ ports.forEach(port => {
995
+ port.postMessage({type, data})
996
+ })
997
+ }
998
+
999
+ function connect(wsUrl, protocols) {
1000
+ if (socket && socket.readyState !== WebSocket.CLOSED) return
1001
+
1002
+ socket = protocols && protocols.length > 0 ? new WebSocket(wsUrl, protocols) : new WebSocket(wsUrl)
1003
+
1004
+ socket.onopen = () => {
1005
+ reconnectAttempts = 0
1006
+ broadcast('open')
1007
+ }
1008
+
1009
+ socket.onmessage = e => {
1010
+ try {
1011
+ const data = JSON.parse(e.data)
1012
+ broadcast('message', data)
1013
+ } catch {
1014
+ broadcast('message', e.data)
1015
+ }
1016
+ }
1017
+
1018
+ socket.onclose = e => {
1019
+ broadcast('close', e)
1020
+
1021
+ if (options.autoReconnect && reconnectAttempts < options.maxReconnectAttempts) {
1022
+ reconnectAttempts++
1023
+ reconnectTimer = setTimeout(() => connect(wsUrl, protocols), options.reconnectDelay)
1024
+ }
1025
+ }
1026
+
1027
+ socket.onerror = e => {
1028
+ broadcast('error', e)
1029
+ }
1030
+ }
1031
+
1032
+ self.onconnect = e => {
1033
+ const port = e.ports[0]
1034
+ ports.add(port)
1035
+
1036
+ port.onmessage = event => {
1037
+ const {type, path, host, protocol, token, options: opts, data} = event.data
1038
+
1039
+ switch (type) {
1040
+ case 'connect':
1041
+ options = opts
1042
+ const wsUrl = protocol + '//' + host + path
1043
+ protocols = token ? ['odac-token-' + token] : []
1044
+ connect(wsUrl, protocols)
1045
+ break
1046
+ case 'send':
1047
+ if (socket && socket.readyState === WebSocket.OPEN) {
1048
+ socket.send(data)
1049
+ }
1050
+ break
1051
+ case 'close':
1052
+ ports.delete(port)
1053
+ if (ports.size === 0 && socket) {
1054
+ socket.close()
1055
+ socket = null
1056
+ }
1057
+ break
1058
+ }
1059
+ }
1060
+
1061
+ port.start()
1062
+ }
1063
+ `
1064
+
1065
+ const blob = new Blob([workerCode], {type: 'application/javascript'})
1066
+ return URL.createObjectURL(blob)
1067
+ }
802
1068
  }
803
1069
 
804
- window.Candy = new candy()
1070
+ window.Odac = new Odac()
805
1071
 
806
- // Auto-initialize navigation from data-candy-navigate attribute
1072
+ // Auto-initialize navigation from data-odac-navigate attribute
807
1073
  ;(function initAutoNavigate() {
808
1074
  const init = () => {
809
- const contentEl = document.querySelector('[data-candy-navigate="content"]')
1075
+ const contentEl = document.querySelector('[data-odac-navigate="content"]')
810
1076
  if (contentEl) {
811
- window.Candy.loader('a[href^="/"]:not([data-navigate="false"]):not(.no-navigate)', {content: '[data-candy-navigate="content"]'}, null)
1077
+ window.Odac.loader('a[href^="/"]:not([data-navigate="false"]):not(.no-navigate)', {content: '[data-odac-navigate="content"]'}, null)
812
1078
  }
813
1079
  }
814
1080
 
@@ -823,16 +1089,16 @@ document.addEventListener('DOMContentLoaded', () => {
823
1089
  const formTypes = ['register', 'login']
824
1090
 
825
1091
  formTypes.forEach(type => {
826
- const forms = document.querySelectorAll(`form.candy-${type}-form[data-candy-${type}]`)
1092
+ const forms = document.querySelectorAll(`form.odac-${type}-form[data-odac-${type}]`)
827
1093
  forms.forEach(form => {
828
- const token = form.getAttribute(`data-candy-${type}`)
829
- window.Candy.form({form: `form[data-candy-${type}="${token}"]`})
1094
+ const token = form.getAttribute(`data-odac-${type}`)
1095
+ window.Odac.form({form: `form[data-odac-${type}="${token}"]`})
830
1096
  })
831
1097
  })
832
1098
 
833
- const customForms = document.querySelectorAll('form.candy-custom-form[data-candy-form]')
1099
+ const customForms = document.querySelectorAll('form.odac-custom-form[data-odac-form]')
834
1100
  customForms.forEach(form => {
835
- const token = form.getAttribute('data-candy-form')
836
- window.Candy.form({form: `form[data-candy-form="${token}"]`})
1101
+ const token = form.getAttribute('data-odac-form')
1102
+ window.Odac.form({form: `form[data-odac-form="${token}"]`})
837
1103
  })
838
1104
  })
@@ -1,9 +1,11 @@
1
- ## 🧰 What's in the `Candy` box?
1
+ ## 🧰 What's in the `Odac` box?
2
2
 
3
- Your `Candy` assistant comes with a bunch of handy services:
3
+ Your `Odac` assistant comes with a bunch of handy services:
4
4
 
5
5
  * `Request`: All the details about the user's incoming request.
6
6
  * `View`: A tool to render your beautiful HTML pages.
7
7
  * `Auth`: Your friendly security guard for managing the current user's login.
8
8
  * `Token`: Helps protect the user's forms from nasty CSRF attacks.
9
9
  * `Lang`: A helper for translating your app into the user's language.
10
+ * `setInterval()` / `setTimeout()`: Memory-safe timers with automatic cleanup.
11
+ * `stream()`: Create streaming responses (Server-Sent Events).
@@ -1,9 +1,37 @@
1
1
  ## ✨ Super-Handy Helper Functions
2
2
 
3
- On top of that, `Candy` has some quick-and-easy helper functions:
3
+ On top of that, `Odac` has some quick-and-easy helper functions:
4
4
 
5
5
  * `return(data)`: Quickly send a response back to the user and you're done.
6
6
  * `direct(url)`: Need to send the user to another page? This is your tool.
7
7
  * `cookie(key, value)`: Leave a little cookie in the user's browser.
8
8
  * `env(key, defaultValue)`: Access environment variables with an optional default value.
9
9
  * `validator()`: A powerful tool to check the user's submitted data.
10
+ * `setInterval(callback, delay)`: Schedule repeating tasks with automatic cleanup.
11
+ * `setTimeout(callback, delay)`: Schedule one-time tasks with automatic cleanup.
12
+ * `clearInterval(id)`: Manually clear an interval.
13
+ * `clearTimeout(id)`: Manually clear a timeout.
14
+
15
+ ### Memory-Safe Timers
16
+
17
+ Odac provides memory-safe timer functions that automatically clean up when the request ends:
18
+
19
+ ```javascript
20
+ module.exports = async (Odac) => {
21
+ // ✅ Automatically cleaned up when request ends
22
+ Odac.setInterval(() => {
23
+ console.log('This will stop when the request ends')
24
+ }, 1000)
25
+
26
+ // ❌ NOT automatically cleaned up - causes memory leaks
27
+ setInterval(() => {
28
+ console.log('This keeps running forever!')
29
+ }, 1000)
30
+ }
31
+ ```
32
+
33
+ **Why use Odac timers?**
34
+ - Prevents memory leaks
35
+ - No orphaned intervals after request ends
36
+ - Especially important for streaming/SSE endpoints
37
+ - Automatic cleanup on connection close
@@ -1,6 +1,6 @@
1
1
  ## 🚀 Development Server
2
2
 
3
- CandyPack provides a built-in development server that allows you to test your website locally without running the full CandyPack server infrastructure.
3
+ Odac provides a built-in development server that allows you to test your website locally without running the full Odac server infrastructure.
4
4
 
5
5
  ### Quick Start
6
6
 
@@ -10,8 +10,8 @@ Navigate to your website directory and run one of these commands:
10
10
  # Using npm script
11
11
  npm start
12
12
 
13
- # Using CandyPack directly
14
- candypack framework run
13
+ # Using Odac directly
14
+ odac framework run
15
15
  ```
16
16
 
17
17
  Both commands will start a local development server on port `1071` by default, and your website will be accessible at `http://127.0.0.1:1071`.
@@ -24,8 +24,8 @@ You can specify a custom port by adding it as an argument:
24
24
  # Using npm script with custom port
25
25
  npm start 8080
26
26
 
27
- # Using CandyPack directly with custom port
28
- candypack framework run 8080
27
+ # Using Odac directly with custom port
28
+ odac framework run 8080
29
29
  ```
30
30
 
31
31
  This will start the server on your specified port (e.g., `http://127.0.0.1:8080`).
@@ -41,13 +41,13 @@ The development server (`npm start`) is designed for:
41
41
 
42
42
  **Important**: The development server does NOT provide DNS, SSL, or other production services.
43
43
 
44
- For production deployment with full CandyPack server features, create your website using:
44
+ For production deployment with full Odac server features, create your website using:
45
45
 
46
46
  ```bash
47
- candy web create
47
+ odac web create
48
48
  ```
49
49
 
50
- This registers your website with the CandyPack server and provides:
50
+ This registers your website with the Odac server and provides:
51
51
 
52
52
  - **Automatic SSL** certificate management
53
53
  - **DNS handling** for your domain
@@ -57,12 +57,12 @@ This registers your website with the CandyPack server and provides:
57
57
 
58
58
  ### Package.json Scripts
59
59
 
60
- When you create a new website, CandyPack automatically generates a `package.json` with these useful scripts:
60
+ When you create a new website, Odac automatically generates a `package.json` with these useful scripts:
61
61
 
62
62
  ```json
63
63
  {
64
64
  "scripts": {
65
- "start": "candy framework run",
65
+ "start": "odac framework run",
66
66
  "test": "echo \"Error: no test specified\" && exit 1"
67
67
  }
68
68
  }
@@ -76,4 +76,4 @@ When you create a new website, CandyPack automatically generates a `package.json
76
76
  - The development server automatically detects changes in your code
77
77
  - Use `Ctrl+C` to stop the development server
78
78
  - The server will show helpful error messages in the console
79
- - All CandyPack framework features are available in development mode
79
+ - All Odac framework features are available in development mode
@@ -4,7 +4,7 @@ Let's take a look at a typical project layout:
4
4
 
5
5
  - `project_root/`
6
6
  - `config.json`: This is where you'll keep your app's secrets and settings, like database passwords or API keys.
7
- - `index.js`: The starting pistol for your web application! This file kicks off the CandyPack framework.
7
+ - `index.js`: The starting pistol for your web application! This file kicks off the Odac framework.
8
8
  - `package.json`: Contains project metadata and npm scripts for development. Automatically generated when creating a new website.
9
9
  - `public/`: All files placed in this folder are directly accessible from the outside. This is the perfect place for your images, stylesheets, and client-side JavaScript.
10
10
  - `route/`: This folder holds all your route definitions. The filename of the route file corresponds to the subdomain it serves.
@@ -27,13 +27,13 @@ npm start
27
27
  npm start 8080
28
28
  ```
29
29
 
30
- You can also use CandyPack commands directly:
30
+ You can also use Odac commands directly:
31
31
 
32
32
  ```bash
33
33
  # Development server (local testing only)
34
- candypack framework run [port]
34
+ odac framework run [port]
35
35
  ```
36
36
 
37
- **Note**: For production websites with DNS and SSL, use `candy web create` to register with CandyPack server.
37
+ **Note**: For production websites with DNS and SSL, use `odac web create` to register with Odac server.
38
38
 
39
39
  Following this structure helps keep your code's responsibilities separate. Your routing logic lives in one place, your app logic in another, and your presentation files in a third. It's a recipe for success!
@@ -1,6 +1,6 @@
1
1
  ## ⚙️ Configuration Overview
2
2
 
3
- CandyPack uses a simple and flexible configuration system based on `config.json` and optional `.env` files. You can choose the approach that best fits your needs.
3
+ Odac uses a simple and flexible configuration system based on `config.json` and optional `.env` files. You can choose the approach that best fits your needs.
4
4
 
5
5
  ### Configuration Files
6
6
 
@@ -87,12 +87,12 @@ Combine both methods - use direct values for non-sensitive data and environment
87
87
  You can access configuration values in three ways:
88
88
 
89
89
  ```javascript
90
- // 1. From Candy.Config (recommended for structured config)
91
- const dbHost = Candy.Config.mysql.host
90
+ // 1. From Odac.Config (recommended for structured config)
91
+ const dbHost = Odac.Config.mysql.host
92
92
 
93
- // 2. Using Candy.env() helper
94
- const apiKey = Candy.env('API_KEY')
95
- const debug = Candy.env('DEBUG', 'false')
93
+ // 2. Using Odac.env() helper
94
+ const apiKey = Odac.env('API_KEY')
95
+ const debug = Odac.env('DEBUG', 'false')
96
96
 
97
97
  // 3. Direct process.env access
98
98
  const nodeEnv = process.env.NODE_ENV