odac 1.4.14 → 1.4.16

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 (41) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/bin/odac.js +1 -0
  3. package/client/odac.js +57 -1
  4. package/docs/ai/skills/backend/forms.md +54 -3
  5. package/docs/ai/skills/backend/validation.md +30 -2
  6. package/docs/ai/skills/frontend/core.md +5 -5
  7. package/docs/backend/05-controllers/02-your-trusty-odac-assistant.md +1 -1
  8. package/docs/backend/05-forms/01-custom-forms.md +96 -0
  9. package/docs/backend/09-validation/01-the-validator-service.md +40 -0
  10. package/docs/frontend/01-overview/01-introduction.md +8 -8
  11. package/docs/frontend/02-ajax-navigation/01-quick-start.md +14 -14
  12. package/docs/frontend/02-ajax-navigation/02-configuration.md +4 -4
  13. package/docs/frontend/02-ajax-navigation/03-advanced-usage.md +16 -16
  14. package/docs/frontend/03-forms/01-form-handling.md +84 -31
  15. package/docs/frontend/04-api-requests/01-get-post.md +25 -25
  16. package/docs/frontend/05-streaming/01-client-streaming.md +14 -14
  17. package/docs/frontend/06-websocket/00-overview.md +1 -1
  18. package/index.js +11 -1
  19. package/package.json +2 -1
  20. package/src/Auth.js +252 -24
  21. package/src/Config.js +5 -1
  22. package/src/Odac.js +3 -0
  23. package/src/Request.js +235 -34
  24. package/src/Route/Internal.js +32 -3
  25. package/src/Validator.js +143 -2
  26. package/src/View/Form.js +56 -0
  27. package/src/View/Image.js +15 -6
  28. package/src/View.js +2 -2
  29. package/test/Auth/check.test.js +219 -18
  30. package/test/Auth/verifyMagicLink.test.js +8 -1
  31. package/test/Odac/cache.test.js +5 -2
  32. package/test/Odac/image.test.js +8 -5
  33. package/test/Odac/instance.test.js +5 -2
  34. package/test/Request/cache.test.js +1 -0
  35. package/test/Request/multipart.test.js +164 -0
  36. package/test/Validator/file.test.js +172 -0
  37. package/test/View/Form/generateFieldHtml.test.js +56 -0
  38. package/test/View/Image/serve.test.js +9 -4
  39. package/test/View/Image/url.test.js +10 -4
  40. package/test/View/addNavigateAttribute.test.js +10 -1
  41. package/test/View/print.test.js +10 -1
@@ -6,31 +6,31 @@ Advanced techniques and patterns for AJAX navigation in Odac.
6
6
 
7
7
  ## Programmatic Navigation
8
8
 
9
- ### Using odac.load()
9
+ ### Using Odac.load()
10
10
 
11
11
  Navigate programmatically from your code:
12
12
 
13
13
  ```javascript
14
14
  // Basic usage
15
- odac.load('/about')
15
+ Odac.load('/about')
16
16
 
17
17
  // With callback
18
- odac.load('/about', function(page, variables) {
18
+ Odac.load('/about', function(page, variables) {
19
19
  console.log('Loaded:', page)
20
20
  console.log('Data:', variables)
21
21
  })
22
22
 
23
23
  // Without updating history
24
- odac.load('/about', callback, false)
24
+ Odac.load('/about', callback, false)
25
25
  ```
26
26
 
27
27
  ### Use Cases
28
28
 
29
29
  **Redirect after form submission:**
30
30
  ```javascript
31
- odac.form('#my-form', function(data) {
31
+ Odac.form('#my-form', function(data) {
32
32
  if (data.result.success) {
33
- odac.load('/success')
33
+ Odac.load('/success')
34
34
  }
35
35
  })
36
36
  ```
@@ -38,16 +38,16 @@ odac.form('#my-form', function(data) {
38
38
  **Conditional navigation:**
39
39
  ```javascript
40
40
  if (user.isLoggedIn) {
41
- odac.load('/dashboard')
41
+ Odac.load('/dashboard')
42
42
  } else {
43
- odac.load('/login')
43
+ Odac.load('/login')
44
44
  }
45
45
  ```
46
46
 
47
47
  **Timed navigation:**
48
48
  ```javascript
49
49
  setTimeout(() => {
50
- odac.load('/next-page')
50
+ Odac.load('/next-page')
51
51
  }, 3000)
52
52
  ```
53
53
 
@@ -76,8 +76,8 @@ Define all parts in your controller:
76
76
 
77
77
  ```javascript
78
78
  module.exports = function(Odac) {
79
- odac.View.skeleton('main')
80
- odac.View.set({
79
+ Odac.View.skeleton('main')
80
+ Odac.View.set({
81
81
  header: 'main',
82
82
  content: 'dashboard',
83
83
  sidebar: 'dashboard',
@@ -333,10 +333,10 @@ Odac.action({
333
333
  Gracefully handle navigation errors:
334
334
 
335
335
  ```javascript
336
- // Override odac.load to add error handling
337
- const originalLoad = odac.load.bind(Odac)
336
+ // Override Odac.load to add error handling
337
+ const originalLoad = Odac.load.bind(Odac)
338
338
 
339
- odac.load = function(url, callback, push) {
339
+ Odac.load = function(url, callback, push) {
340
340
  try {
341
341
  originalLoad(url, function(page, variables) {
342
342
  if (callback) callback(page, variables)
@@ -359,7 +359,7 @@ function loadWithRetry(url, maxRetries = 3) {
359
359
 
360
360
  function attempt() {
361
361
  attempts++
362
- odac.load(url,
362
+ Odac.load(url,
363
363
  (page, vars) => {
364
364
  console.log('Success after', attempts, 'attempts')
365
365
  },
@@ -491,7 +491,7 @@ describe('Navigation', () => {
491
491
  })
492
492
 
493
493
  // Simulate navigation
494
- odac.load('/about')
494
+ Odac.load('/about')
495
495
 
496
496
  // Assert
497
497
  expect(document.querySelector('.nav-link.active').href)
@@ -14,7 +14,7 @@ Learn how to handle forms with automatic AJAX submission, CSRF protection, and v
14
14
  ```
15
15
 
16
16
  ```javascript
17
- odac.form('#contact-form', function(data) {
17
+ Odac.form('#contact-form', function(data) {
18
18
  if (data.result.success) {
19
19
  alert('Form submitted successfully!')
20
20
  }
@@ -35,7 +35,7 @@ That's it! odac.js handles:
35
35
  ### Basic Usage
36
36
 
37
37
  ```javascript
38
- odac.form('#my-form', function(data) {
38
+ Odac.form('#my-form', function(data) {
39
39
  console.log('Response:', data)
40
40
  })
41
41
  ```
@@ -43,7 +43,7 @@ odac.form('#my-form', function(data) {
43
43
  ### With Options
44
44
 
45
45
  ```javascript
46
- odac.form({
46
+ Odac.form({
47
47
  form: '#my-form',
48
48
  messages: true, // Show error/success messages
49
49
  loading: function(percent) {
@@ -59,10 +59,37 @@ odac.form({
59
59
  ### Redirect After Submit
60
60
 
61
61
  ```javascript
62
- odac.form('#my-form', '/success-page')
62
+ Odac.form('#my-form', '/success-page')
63
63
  // Redirects to /success-page on success
64
64
  ```
65
65
 
66
+ ## Using with `<odac:form>` Components
67
+
68
+ `Odac.form()` works with both plain `<form>` elements and server-rendered
69
+ [`<odac:form>` components](../../backend/05-forms/01-custom-forms.md).
70
+
71
+ An `<odac:form>` already registers itself automatically, so you only call
72
+ `Odac.form()` when you want to add your own callback. Give the component an `id`
73
+ and bind to it:
74
+
75
+ ```html
76
+ <odac:form action="Contact.submit" id="contact-form">
77
+ <!-- fields -->
78
+ <odac:submit text="Send"/>
79
+ </odac:form>
80
+ ```
81
+
82
+ ```javascript
83
+ Odac.form('#contact-form', function(data) {
84
+ if (data.result.success) {
85
+ closeModal()
86
+ }
87
+ })
88
+ ```
89
+
90
+ Calling `Odac.form()` on an already-registered `<odac:form>` **re-binds** the
91
+ same form instead of adding a second handler, so it still submits only once.
92
+
66
93
  ## Error Handling
67
94
 
68
95
  ### Automatic Error Display
@@ -85,7 +112,7 @@ If you want to control where errors appear, add error elements manually:
85
112
  ### Custom Error Display
86
113
 
87
114
  ```javascript
88
- odac.form('#my-form', function(data) {
115
+ Odac.form('#my-form', function(data) {
89
116
  if (!data.result.success) {
90
117
  // Custom error handling
91
118
  Object.entries(data.errors).forEach(([field, message]) => {
@@ -137,7 +164,7 @@ If you want to control where success messages appear, add a success element manu
137
164
  ### Custom Success Message
138
165
 
139
166
  ```javascript
140
- odac.form('#my-form', function(data) {
167
+ Odac.form('#my-form', function(data) {
141
168
  if (data.result.success) {
142
169
  document.querySelector('#custom-message').innerHTML =
143
170
  'Thank you! Your form has been submitted.'
@@ -147,49 +174,66 @@ odac.form('#my-form', function(data) {
147
174
 
148
175
  ## File Uploads
149
176
 
150
- ### Basic File Upload
177
+ ### Basic File Upload with `<odac:form>`
178
+
179
+ File upload in Odac forms is seamless — just add a `type="file"` input and validation rules:
151
180
 
152
181
  ```html
153
- <form id="upload-form" action="/api/upload" method="POST">
154
- <input name="file" type="file" required>
155
- <button type="submit">Upload</button>
156
- </form>
182
+ <odac:form action="Profile.saveAvatar" id="avatar-form">
183
+ <odac:input type="file" name="avatar" label="Profile Picture">
184
+ <odac:validate rule="required|maxsize:2MB|mimetype:image/png,image/jpeg" message="PNG/JPEG, max 2MB"/>
185
+ </odac:input>
186
+ <odac:submit text="Upload"/>
187
+ </odac:form>
157
188
  ```
158
189
 
159
190
  ```javascript
160
- odac.form('#upload-form', function(data) {
191
+ Odac.form('#avatar-form', function(data) {
161
192
  if (data.result.success) {
162
- console.log('File uploaded:', data.result.filename)
193
+ alert('Avatar uploaded!')
163
194
  }
164
195
  })
165
196
  ```
166
197
 
167
- ### Upload Progress
198
+ **Features:**
199
+ - ✅ Client-side validation (size, MIME type, extension)
200
+ - ✅ Server-side validation with magic-byte sniffing for images
201
+ - ✅ Automatic multipart/form-data handling
202
+ - ✅ File size progress tracking
203
+
204
+ ### Upload Progress Tracking
168
205
 
169
206
  ```javascript
170
- odac.form({
171
- form: '#upload-form',
207
+ Odac.form({
208
+ form: '#avatar-form',
172
209
  loading: function(percent) {
173
210
  document.querySelector('#progress').style.width = percent + '%'
174
- document.querySelector('#progress-text').textContent = percent + '%'
175
211
  }
176
212
  }, function(data) {
177
- console.log('Upload complete!')
213
+ if (data.result.success) {
214
+ console.log('Upload complete!')
215
+ }
178
216
  })
179
217
  ```
180
218
 
181
- ### Multiple Files
219
+ ### Multiple File Upload
220
+
221
+ For multiple file selection, add the `multiple` attribute and use `maxfiles` validation:
182
222
 
183
223
  ```html
184
- <input name="files[]" type="file" multiple>
224
+ <odac:input type="file" name="documents" label="Documents" multiple>
225
+ <odac:validate rule="maxfiles:5|ext:pdf,docx" message="Max 5 PDFs/Word docs"/>
226
+ </odac:input>
185
227
  ```
186
228
 
229
+ **Note:** File inputs are never repopulated on validation error (for security). Users must re-select files after an error.
230
+
187
231
  ## Advanced Features
188
232
 
189
233
  ### Disable Messages
190
234
 
191
235
  ```javascript
192
- odac.form({
236
+ Odac.form({
193
237
  form: '#my-form',
194
238
  messages: false // Don't show automatic messages
195
239
  }, function(data) {
@@ -201,7 +245,7 @@ odac.form({
201
245
 
202
246
  ```javascript
203
247
  // Only show error messages, suppress success messages
204
- odac.form({
248
+ Odac.form({
205
249
  form: '#my-form',
206
250
  messages: ['error']
207
251
  }, function(data) {
@@ -211,7 +255,7 @@ odac.form({
211
255
  })
212
256
 
213
257
  // Only show success messages, suppress error messages
214
- odac.form({
258
+ Odac.form({
215
259
  form: '#my-form',
216
260
  messages: ['success']
217
261
  }, function(data) {
@@ -224,7 +268,7 @@ odac.form({
224
268
  ### Form Reset
225
269
 
226
270
  ```javascript
227
- odac.form('#my-form', function(data) {
271
+ Odac.form('#my-form', function(data) {
228
272
  if (data.result.success) {
229
273
  // Reset the form
230
274
  document.querySelector('#my-form').reset()
@@ -242,7 +286,7 @@ document.querySelector('#my-form').addEventListener('submit', function(e) {
242
286
  }
243
287
  })
244
288
 
245
- odac.form('#my-form', function(data) {
289
+ Odac.form('#my-form', function(data) {
246
290
  console.log('Submitted!')
247
291
  })
248
292
  ```
@@ -254,8 +298,8 @@ odac.form('#my-form', function(data) {
254
298
  ```javascript
255
299
  // controller/post/contact.js
256
300
  module.exports = async function(Odac) {
257
- const email = await odac.Request.request('email')
258
- const message = await odac.Request.request('message')
301
+ const email = await Odac.Request.request('email')
302
+ const message = await Odac.Request.request('message')
259
303
 
260
304
  // Validation
261
305
  const errors = {}
@@ -285,7 +329,7 @@ module.exports = async function(Odac) {
285
329
 
286
330
  ```javascript
287
331
  // route/www.js
288
- odac.Route.post('/api/contact', 'contact')
332
+ Odac.Route.post('/api/contact', 'contact')
289
333
  ```
290
334
 
291
335
  ## Validation
@@ -306,7 +350,7 @@ Always validate on the server:
306
350
 
307
351
  ```javascript
308
352
  module.exports = async function(Odac) {
309
- const email = await odac.Request.request('email')
353
+ const email = await Odac.Request.request('email')
310
354
 
311
355
  const errors = {}
312
356
 
@@ -349,7 +393,7 @@ module.exports = async function(Odac) {
349
393
  **Note:** Error and success elements are auto-generated. Add them manually only if you need custom positioning or styling.
350
394
 
351
395
  ```javascript
352
- odac.form('#contact-form', function(data) {
396
+ Odac.form('#contact-form', function(data) {
353
397
  if (data.result.success) {
354
398
  document.querySelector('#contact-form').reset()
355
399
  }
@@ -367,7 +411,7 @@ odac.form('#contact-form', function(data) {
367
411
  ```
368
412
 
369
413
  ```javascript
370
- odac.form('#login-form', function(data) {
414
+ Odac.form('#login-form', function(data) {
371
415
  if (data.result.success) {
372
416
  // Redirect to dashboard
373
417
  window.location.href = '/dashboard'
@@ -420,6 +464,15 @@ odac.form('#login-form', function(data) {
420
464
  - Verify `messages` option is not set to `false`
421
465
  - If using custom error elements, ensure `odac-form-error` attributes match field names exactly
422
466
 
467
+ ### Form Submits Twice
468
+
469
+ - Make sure you don't register the **same form under two different selectors**
470
+ (e.g. `Odac.form('#my-form')` and `Odac.form('form#my-form')`) — use one
471
+ consistent selector.
472
+ - Binding `Odac.form()` to an `<odac:form>` is safe and will **not** double up;
473
+ it re-binds the existing handler. If you still see two submits, check that you
474
+ bind **after** the form is in the DOM, not before it renders.
475
+
423
476
  ### CSRF Token Errors
424
477
 
425
478
  - CSRF tokens are handled automatically
@@ -7,7 +7,7 @@ Learn how to make GET and POST requests to your API endpoints using odac.js.
7
7
  ### Basic GET Request
8
8
 
9
9
  ```javascript
10
- odac.get('/api/data', function(response) {
10
+ Odac.get('/api/data', function(response) {
11
11
  console.log('Data:', response)
12
12
  })
13
13
  ```
@@ -16,7 +16,7 @@ odac.get('/api/data', function(response) {
16
16
 
17
17
  ```javascript
18
18
  const userId = 123
19
- odac.get(`/api/users/${userId}`, function(user) {
19
+ Odac.get(`/api/users/${userId}`, function(user) {
20
20
  console.log('User:', user.name)
21
21
  console.log('Email:', user.email)
22
22
  })
@@ -25,7 +25,7 @@ odac.get(`/api/users/${userId}`, function(user) {
25
25
  ### Error Handling
26
26
 
27
27
  ```javascript
28
- odac.get('/api/data', function(response) {
28
+ Odac.get('/api/data', function(response) {
29
29
  if (response.error) {
30
30
  console.error('Error:', response.error)
31
31
  return
@@ -43,7 +43,7 @@ odac.get('/api/data', function(response) {
43
43
  The recommended way to make POST requests is using `Odac.form()`:
44
44
 
45
45
  ```javascript
46
- odac.form('#my-form', function(data) {
46
+ Odac.form('#my-form', function(data) {
47
47
  if (data.result.success) {
48
48
  console.log('Success!')
49
49
  }
@@ -58,12 +58,12 @@ For custom POST requests without forms, use the internal AJAX method:
58
58
 
59
59
  ```javascript
60
60
  // Note: This is an advanced pattern
61
- // For most cases, use odac.form() instead
61
+ // For most cases, use Odac.form() instead
62
62
 
63
63
  const formData = new FormData()
64
64
  formData.append('name', 'John')
65
65
  formData.append('email', 'john@example.com')
66
- formData.append('_token', odac.token())
66
+ formData.append('_token', Odac.token())
67
67
 
68
68
  fetch('/api/submit', {
69
69
  method: 'POST',
@@ -83,12 +83,12 @@ odac.js automatically handles CSRF tokens:
83
83
 
84
84
  ```javascript
85
85
  // Token is automatically included
86
- odac.get('/api/data', function(response) {
86
+ Odac.get('/api/data', function(response) {
87
87
  // ...
88
88
  })
89
89
 
90
90
  // Token is automatically included in forms
91
- odac.form('#my-form', function(data) {
91
+ Odac.form('#my-form', function(data) {
92
92
  // ...
93
93
  })
94
94
  ```
@@ -98,7 +98,7 @@ odac.form('#my-form', function(data) {
98
98
  If you need the token manually:
99
99
 
100
100
  ```javascript
101
- const token = odac.token()
101
+ const token = Odac.token()
102
102
  console.log('Current token:', token)
103
103
  ```
104
104
 
@@ -108,7 +108,7 @@ console.log('Current token:', token)
108
108
 
109
109
  ```javascript
110
110
  // Get user profile
111
- odac.get('/api/profile', function(profile) {
111
+ Odac.get('/api/profile', function(profile) {
112
112
  document.querySelector('#username').textContent = profile.name
113
113
  document.querySelector('#email').textContent = profile.email
114
114
  })
@@ -118,7 +118,7 @@ odac.get('/api/profile', function(profile) {
118
118
 
119
119
  ```javascript
120
120
  // Get products
121
- odac.get('/api/products', function(products) {
121
+ Odac.get('/api/products', function(products) {
122
122
  const container = document.querySelector('#products')
123
123
 
124
124
  products.forEach(product => {
@@ -143,7 +143,7 @@ searchInput.addEventListener('input', function() {
143
143
 
144
144
  if (query.length < 3) return
145
145
 
146
- odac.get(`/api/search?q=${encodeURIComponent(query)}`, function(results) {
146
+ Odac.get(`/api/search?q=${encodeURIComponent(query)}`, function(results) {
147
147
  displayResults(results)
148
148
  })
149
149
  })
@@ -172,7 +172,7 @@ document.querySelector('#autocomplete').addEventListener('input', function() {
172
172
  debounceTimer = setTimeout(() => {
173
173
  if (value.length < 2) return
174
174
 
175
- odac.get(`/api/autocomplete?q=${value}`, function(suggestions) {
175
+ Odac.get(`/api/autocomplete?q=${value}`, function(suggestions) {
176
176
  showSuggestions(suggestions)
177
177
  })
178
178
  }, 300)
@@ -200,7 +200,7 @@ function loadMore() {
200
200
  loading = true
201
201
  page++
202
202
 
203
- odac.get(`/api/posts?page=${page}`, function(posts) {
203
+ Odac.get(`/api/posts?page=${page}`, function(posts) {
204
204
  posts.forEach(post => {
205
205
  appendPost(post)
206
206
  })
@@ -214,7 +214,7 @@ function loadMore() {
214
214
  ```javascript
215
215
  // Poll for updates every 30 seconds
216
216
  setInterval(function() {
217
- odac.get('/api/notifications', function(notifications) {
217
+ Odac.get('/api/notifications', function(notifications) {
218
218
  updateNotificationBadge(notifications.count)
219
219
  })
220
220
  }, 30000)
@@ -239,7 +239,7 @@ module.exports = function(Odac) {
239
239
 
240
240
  ```javascript
241
241
  // route/www.js
242
- odac.Route.get('/api/users', 'users')
242
+ Odac.Route.get('/api/users', 'users')
243
243
  ```
244
244
 
245
245
  ### GET with Parameters
@@ -247,13 +247,13 @@ odac.Route.get('/api/users', 'users')
247
247
  ```javascript
248
248
  // controller/get/user.js
249
249
  module.exports = async function(Odac) {
250
- const userId = odac.Request.data.url.id
250
+ const userId = Odac.Request.data.url.id
251
251
 
252
252
  // Fetch user from database
253
253
  const user = await getUserById(userId)
254
254
 
255
255
  if (!user) {
256
- odac.Request.status(404)
256
+ Odac.Request.status(404)
257
257
  return {error: 'User not found'}
258
258
  }
259
259
 
@@ -263,7 +263,7 @@ module.exports = async function(Odac) {
263
263
 
264
264
  ```javascript
265
265
  // route/www.js
266
- odac.Route.get('/api/users/{id}', 'user')
266
+ Odac.Route.get('/api/users/{id}', 'user')
267
267
  ```
268
268
 
269
269
  ### POST Endpoint
@@ -271,8 +271,8 @@ odac.Route.get('/api/users/{id}', 'user')
271
271
  ```javascript
272
272
  // controller/post/create.js
273
273
  module.exports = async function(Odac) {
274
- const name = await odac.Request.request('name')
275
- const email = await odac.Request.request('email')
274
+ const name = await Odac.Request.request('name')
275
+ const email = await Odac.Request.request('email')
276
276
 
277
277
  // Validation
278
278
  if (!name || !email) {
@@ -300,7 +300,7 @@ module.exports = async function(Odac) {
300
300
 
301
301
  ```javascript
302
302
  // route/www.js
303
- odac.Route.post('/api/users/create', 'create')
303
+ Odac.Route.post('/api/users/create', 'create')
304
304
  ```
305
305
 
306
306
  ## Response Format
@@ -357,7 +357,7 @@ function getCached(url, callback) {
357
357
  return
358
358
  }
359
359
 
360
- odac.get(url, function(data) {
360
+ Odac.get(url, function(data) {
361
361
  cache[url] = data
362
362
  callback(data)
363
363
  })
@@ -386,7 +386,7 @@ function processQueue() {
386
386
  processing = true
387
387
  const {url, callback} = requestQueue.shift()
388
388
 
389
- odac.get(url, function(data) {
389
+ Odac.get(url, function(data) {
390
390
  callback(data)
391
391
  processing = false
392
392
  processQueue()
@@ -403,7 +403,7 @@ function getWithRetry(url, callback, maxRetries = 3) {
403
403
  function attempt() {
404
404
  attempts++
405
405
 
406
- odac.get(url, function(data) {
406
+ Odac.get(url, function(data) {
407
407
  if (data.error && attempts < maxRetries) {
408
408
  setTimeout(attempt, 1000 * attempts)
409
409
  } else {
@@ -6,7 +6,7 @@ Odac provides a simple client-side API for consuming Server-Sent Events (SSE) st
6
6
 
7
7
  ```javascript
8
8
  // Simple callback
9
- odac.listen('/events', (data) => {
9
+ Odac.listen('/events', (data) => {
10
10
  console.log('Received:', data)
11
11
  })
12
12
  ```
@@ -14,7 +14,7 @@ odac.listen('/events', (data) => {
14
14
  ### With Options
15
15
 
16
16
  ```javascript
17
- const stream = odac.listen('/events',
17
+ const stream = Odac.listen('/events',
18
18
  (data) => {
19
19
  console.log('Message:', data)
20
20
  },
@@ -34,7 +34,7 @@ stream.close()
34
34
 
35
35
  ```javascript
36
36
  // Server sends: data: {"message": "Hello"}\n\n
37
- odac.listen('/events', (data) => {
37
+ Odac.listen('/events', (data) => {
38
38
  console.log(data.message) // "Hello"
39
39
  })
40
40
  ```
@@ -47,8 +47,8 @@ odac.listen('/events', (data) => {
47
47
 
48
48
  ```javascript
49
49
  // Server
50
- odac.Route.get('/dashboard/stats', async (Odac) => {
51
- odac.stream((send) => {
50
+ Odac.Route.get('/dashboard/stats', async (Odac) => {
51
+ Odac.stream((send) => {
52
52
  const interval = setInterval(async () => {
53
53
  const stats = await getServerStats()
54
54
  send(stats)
@@ -62,7 +62,7 @@ odac.Route.get('/dashboard/stats', async (Odac) => {
62
62
  })
63
63
 
64
64
  // Client
65
- odac.listen('/dashboard/stats', (stats) => {
65
+ Odac.listen('/dashboard/stats', (stats) => {
66
66
  document.getElementById('cpu').textContent = stats.cpu + '%'
67
67
  document.getElementById('memory').textContent = stats.memory + 'MB'
68
68
  })
@@ -72,16 +72,16 @@ odac.listen('/dashboard/stats', (stats) => {
72
72
 
73
73
  ```javascript
74
74
  // Server
75
- odac.Route.get('/notifications', async (Odac) => {
76
- const userId = await odac.request('userId')
75
+ Odac.Route.get('/notifications', async (Odac) => {
76
+ const userId = await Odac.request('userId')
77
77
 
78
- odac.stream((send) => {
78
+ Odac.stream((send) => {
79
79
  global.notificationStreams[userId] = { send }
80
80
  })
81
81
  })
82
82
 
83
83
  // Client
84
- odac.listen('/notifications', (notification) => {
84
+ Odac.listen('/notifications', (notification) => {
85
85
  showToast(notification.message)
86
86
  })
87
87
  ```
@@ -90,8 +90,8 @@ odac.listen('/notifications', (notification) => {
90
90
 
91
91
  ```javascript
92
92
  // Server
93
- odac.Route.get('/build/logs', async (Odac) => {
94
- odac.stream(async function* () {
93
+ Odac.Route.get('/build/logs', async (Odac) => {
94
+ Odac.stream(async function* () {
95
95
  for await (const log of getBuildLogs()) {
96
96
  yield { timestamp: Date.now(), message: log }
97
97
  }
@@ -100,7 +100,7 @@ odac.Route.get('/build/logs', async (Odac) => {
100
100
 
101
101
  // Client
102
102
  const logs = []
103
- odac.listen('/build/logs', (log) => {
103
+ Odac.listen('/build/logs', (log) => {
104
104
  logs.push(log)
105
105
  updateLogsUI(logs)
106
106
  })
@@ -108,7 +108,7 @@ odac.listen('/build/logs', (log) => {
108
108
 
109
109
  ## API Reference
110
110
 
111
- ### odac.listen(url, onMessage, options)
111
+ ### Odac.listen(url, onMessage, options)
112
112
 
113
113
  **Parameters:**
114
114
  - `url` (string): Stream endpoint URL
@@ -6,7 +6,7 @@ Odac provides built-in WebSocket support with automatic reconnection and cross-t
6
6
 
7
7
  **Backend (route/main.js):**
8
8
  ```javascript
9
- odac.Route.ws('/chat', (ws, Odac) => {
9
+ Odac.Route.ws('/chat', (ws, Odac) => {
10
10
  ws.on('message', data => {
11
11
  ws.broadcast(data)
12
12
  })
package/index.js CHANGED
@@ -1,4 +1,14 @@
1
1
  'use strict'
2
2
 
3
3
  global.__dir = process.cwd()
4
- require('./src/Odac.js').init()
4
+
5
+ // Surface startup failures loudly. init() is async; without a .catch() any
6
+ // rejection (migration, DB connection, config parse) becomes an UnhandledPromiseRejection
7
+ // that the structured logger never sees — making a broken boot look like a clean start.
8
+ require('./src/Odac.js')
9
+ .init()
10
+ .catch(err => {
11
+ console.error('\x1b[31m[ODAC Fatal]\x1b[0m Startup failed:', err.message)
12
+ if (err.cause) console.error('\x1b[31m[ODAC Fatal]\x1b[0m Cause:', err.cause.message || err.cause)
13
+ process.exit(1)
14
+ })