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.
- package/.github/workflows/auto-pr-description.yml +0 -2
- package/.github/workflows/codeql.yml +46 -0
- package/.github/workflows/release.yml +13 -6
- package/.github/workflows/test-coverage.yml +10 -9
- package/.releaserc.js +9 -6
- package/CHANGELOG.md +62 -150
- package/CODE_OF_CONDUCT.md +1 -1
- package/CONTRIBUTING.md +8 -8
- package/LICENSE +21 -661
- package/README.md +12 -12
- package/SECURITY.md +4 -4
- package/bin/odac.js +101 -0
- package/{framework/web/candy.js → client/odac.js} +310 -44
- package/docs/backend/01-overview/{01-whats-in-the-candy-box.md → 01-whats-in-the-odac-box.md} +4 -2
- package/docs/backend/01-overview/02-super-handy-helper-functions.md +29 -1
- package/docs/backend/01-overview/03-development-server.md +11 -11
- package/docs/backend/02-structure/01-typical-project-layout.md +4 -4
- package/docs/backend/03-config/00-configuration-overview.md +6 -6
- package/docs/backend/03-config/01-database-connection.md +1 -1
- package/docs/backend/03-config/02-static-route-mapping-optional.md +4 -4
- package/docs/backend/03-config/04-environment-variables.md +20 -20
- package/docs/backend/03-config/05-early-hints.md +4 -4
- package/docs/backend/04-routing/01-basic-page-routes.md +4 -4
- package/docs/backend/04-routing/02-controller-less-view-routes.md +5 -5
- package/docs/backend/04-routing/03-api-and-data-routes.md +3 -3
- package/docs/backend/04-routing/04-authentication-aware-routes.md +5 -5
- package/docs/backend/04-routing/05-advanced-routing.md +3 -3
- package/docs/backend/04-routing/06-error-pages.md +17 -17
- package/docs/backend/04-routing/07-cron-jobs.md +13 -13
- package/docs/backend/04-routing/08-middleware.md +214 -0
- package/docs/backend/04-routing/09-websocket-auth-middleware.md +292 -0
- package/docs/backend/04-routing/09-websocket-examples.md +381 -0
- package/docs/backend/04-routing/09-websocket-quick-reference.md +211 -0
- package/docs/backend/04-routing/09-websocket.md +298 -0
- package/docs/backend/05-controllers/01-how-to-build-a-controller.md +3 -3
- package/docs/backend/05-controllers/02-your-trusty-odac-assistant.md +41 -0
- package/docs/backend/05-controllers/03-controller-classes.md +19 -19
- package/docs/backend/05-forms/01-custom-forms.md +114 -114
- package/docs/backend/05-forms/02-automatic-database-insert.md +82 -82
- package/docs/backend/06-request-and-response/01-the-request-object-what-is-the-user-asking-for.md +26 -26
- package/docs/backend/06-request-and-response/02-sending-a-response-replying-to-the-user.md +10 -10
- package/docs/backend/07-views/01-the-view-directory.md +1 -1
- package/docs/backend/07-views/02-rendering-a-view.md +22 -22
- package/docs/backend/07-views/03-template-syntax.md +52 -52
- package/docs/backend/07-views/03-variables.md +84 -84
- package/docs/backend/07-views/04-request-data.md +57 -57
- package/docs/backend/07-views/05-conditionals.md +78 -78
- package/docs/backend/07-views/06-loops.md +114 -114
- package/docs/backend/07-views/07-translations.md +66 -66
- package/docs/backend/07-views/08-backend-javascript.md +103 -103
- package/docs/backend/07-views/09-comments.md +71 -71
- package/docs/backend/08-database/01-database-connection.md +8 -8
- package/docs/backend/08-database/02-using-mysql.md +49 -49
- package/docs/backend/09-validation/01-the-validator-service.md +38 -38
- package/docs/backend/10-authentication/01-user-logins-with-authjs.md +15 -15
- package/docs/backend/10-authentication/02-foiling-villains-with-csrf-protection.md +10 -10
- package/docs/backend/10-authentication/03-register.md +12 -12
- package/docs/backend/10-authentication/{04-candy-register-forms.md → 04-odac-register-forms.md} +141 -141
- package/docs/backend/10-authentication/05-session-management.md +10 -10
- package/docs/backend/10-authentication/{06-candy-login-forms.md → 06-odac-login-forms.md} +125 -125
- package/docs/backend/11-mail/01-the-mail-service.md +5 -5
- package/docs/backend/12-streaming/01-streaming-overview.md +96 -54
- package/docs/backend/13-utilities/{01-candy-var.md → 01-odac-var.md} +109 -109
- package/docs/frontend/01-overview/01-introduction.md +30 -30
- package/docs/frontend/02-ajax-navigation/01-quick-start.md +45 -45
- package/docs/frontend/02-ajax-navigation/02-configuration.md +14 -14
- package/docs/frontend/02-ajax-navigation/03-advanced-usage.md +36 -36
- package/docs/frontend/03-forms/01-form-handling.md +32 -32
- package/docs/frontend/04-api-requests/01-get-post.md +33 -33
- package/docs/frontend/05-streaming/01-client-streaming.md +15 -15
- package/docs/frontend/06-websocket/00-overview.md +76 -0
- package/docs/frontend/06-websocket/01-websocket-client.md +139 -0
- package/docs/frontend/06-websocket/02-shared-websocket.md +149 -0
- package/docs/index.json +49 -11
- package/eslint.config.mjs +6 -6
- package/{framework/index.js → index.js} +1 -1
- package/package.json +14 -39
- package/{framework/src → src}/Auth.js +59 -59
- package/{framework/src → src}/Config.js +3 -3
- package/{framework/src → src}/Lang.js +7 -7
- package/{framework/src → src}/Mail.js +5 -5
- package/{framework/src → src}/Mysql.js +42 -42
- package/src/Odac.js +112 -0
- package/{framework/src → src}/Request.js +38 -36
- package/{framework/src → src}/Route/Internal.js +116 -116
- package/src/Route/Middleware.js +75 -0
- package/src/Route.js +621 -0
- package/src/Server.js +22 -0
- package/{framework/src → src}/Stream.js +11 -3
- package/{framework/src → src}/Validator.js +21 -21
- package/{framework/src → src}/Var.js +5 -5
- package/{framework/src → src}/View/EarlyHints.js +1 -1
- package/{framework/src → src}/View/Form.js +69 -69
- package/{framework/src → src}/View.js +78 -81
- package/src/WebSocket.js +403 -0
- package/template/config.json +5 -0
- package/{web → template}/controller/page/about.js +6 -6
- package/{web → template}/controller/page/index.js +9 -9
- package/{web → template}/package.json +4 -5
- package/{web → template}/public/assets/css/style.css +4 -4
- package/{web → template}/public/assets/js/app.js +6 -6
- package/{web → template}/route/www.js +6 -6
- package/{web → template}/skeleton/main.html +1 -1
- package/{web → template}/view/content/about.html +5 -5
- package/{web → template}/view/content/home.html +12 -12
- package/template/view/footer/main.html +11 -0
- package/{web → template}/view/head/main.html +1 -1
- package/{web → template}/view/header/main.html +2 -2
- package/test/core/Candy.test.js +58 -58
- package/test/core/Commands.test.js +7 -7
- package/test/core/Config.test.js +82 -85
- package/test/core/Lang.test.js +2 -2
- package/test/core/Process.test.js +6 -6
- package/test/framework/Route.test.js +56 -37
- package/test/framework/View/EarlyHints.test.js +2 -2
- package/test/framework/WebSocket.test.js +100 -0
- package/test/framework/middleware.test.js +85 -0
- package/test/server/Api.test.js +31 -31
- package/test/server/DNS.test.js +11 -11
- package/test/server/Hub.test.js +497 -0
- package/test/server/Mail.account.test_.js +3 -3
- package/test/server/Mail.init.test_.js +10 -10
- package/test/server/Mail.test_.js +20 -20
- package/test/server/SSL.test_.js +54 -54
- package/test/server/Server.test.js +39 -39
- package/test/server/Service.test_.js +7 -7
- package/test/server/Subdomain.test.js +7 -7
- package/test/server/Web/Firewall.test.js +87 -87
- package/test/server/Web/Proxy.test.js +397 -0
- package/test/server/{Web.test_.js → Web.test.js} +137 -205
- package/test/server/__mocks__/fs.js +2 -2
- package/test/server/__mocks__/{globalCandy.js → globalOdac.js} +5 -5
- package/test/server/__mocks__/index.js +6 -6
- package/test/server/__mocks__/testFactories.js +1 -1
- package/test/server/__mocks__/testHelpers.js +7 -7
- package/.husky/pre-commit +0 -2
- package/.kiro/steering/code-style.md +0 -56
- package/.kiro/steering/product.md +0 -20
- package/.kiro/steering/structure.md +0 -77
- package/.kiro/steering/tech.md +0 -87
- package/AGENTS.md +0 -84
- package/bin/candy +0 -10
- package/bin/candypack +0 -10
- package/cli/index.js +0 -3
- package/cli/src/Cli.js +0 -348
- package/cli/src/Connector.js +0 -93
- package/cli/src/Monitor.js +0 -416
- package/core/Candy.js +0 -87
- package/core/Commands.js +0 -239
- package/core/Config.js +0 -1094
- package/core/Lang.js +0 -52
- package/core/Log.js +0 -43
- package/core/Process.js +0 -26
- package/docs/backend/05-controllers/02-your-trusty-candy-assistant.md +0 -20
- package/docs/server/01-installation/01-quick-install.md +0 -19
- package/docs/server/01-installation/02-manual-installation-via-npm.md +0 -9
- package/docs/server/02-get-started/01-core-concepts.md +0 -7
- package/docs/server/02-get-started/02-basic-commands.md +0 -57
- package/docs/server/02-get-started/03-cli-reference.md +0 -276
- package/docs/server/02-get-started/04-cli-quick-reference.md +0 -102
- package/docs/server/03-service/01-start-a-new-service.md +0 -57
- package/docs/server/03-service/02-delete-a-service.md +0 -48
- package/docs/server/04-web/01-create-a-website.md +0 -36
- package/docs/server/04-web/02-list-websites.md +0 -9
- package/docs/server/04-web/03-delete-a-website.md +0 -29
- package/docs/server/05-subdomain/01-create-a-subdomain.md +0 -32
- package/docs/server/05-subdomain/02-list-subdomains.md +0 -33
- package/docs/server/05-subdomain/03-delete-a-subdomain.md +0 -41
- package/docs/server/06-ssl/01-renew-an-ssl-certificate.md +0 -34
- package/docs/server/07-mail/01-create-a-mail-account.md +0 -23
- package/docs/server/07-mail/02-delete-a-mail-account.md +0 -20
- package/docs/server/07-mail/03-list-mail-accounts.md +0 -20
- package/docs/server/07-mail/04-change-account-password.md +0 -23
- package/framework/src/Candy.js +0 -81
- package/framework/src/Route.js +0 -455
- package/framework/src/Server.js +0 -15
- package/locale/de-DE.json +0 -80
- package/locale/en-US.json +0 -79
- package/locale/es-ES.json +0 -80
- package/locale/fr-FR.json +0 -80
- package/locale/pt-BR.json +0 -80
- package/locale/ru-RU.json +0 -80
- package/locale/tr-TR.json +0 -85
- package/locale/zh-CN.json +0 -80
- package/server/index.js +0 -5
- package/server/src/Api.js +0 -88
- package/server/src/DNS.js +0 -940
- package/server/src/Hub.js +0 -535
- package/server/src/Mail.js +0 -571
- package/server/src/SSL.js +0 -180
- package/server/src/Server.js +0 -27
- package/server/src/Service.js +0 -248
- package/server/src/Subdomain.js +0 -64
- package/server/src/Web/Firewall.js +0 -170
- package/server/src/Web/Proxy.js +0 -134
- package/server/src/Web.js +0 -451
- package/server/src/mail/imap.js +0 -1091
- package/server/src/mail/server.js +0 -32
- package/server/src/mail/smtp.js +0 -786
- package/test/server/Client.test.js +0 -338
- package/test/server/__mocks__/http-proxy.js +0 -105
- package/watchdog/index.js +0 -3
- package/watchdog/src/Watchdog.js +0 -156
- package/web/config.json +0 -5
- package/web/view/footer/main.html +0 -11
- /package/{framework/src → src}/Env.js +0 -0
- /package/{framework/src → src}/Route/Cron.js +0 -0
- /package/{framework/src → src}/Token.js +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
# API Requests with
|
|
1
|
+
# API Requests with odac.js
|
|
2
2
|
|
|
3
|
-
Learn how to make GET and POST requests to your API endpoints using
|
|
3
|
+
Learn how to make GET and POST requests to your API endpoints using odac.js.
|
|
4
4
|
|
|
5
5
|
## GET Requests
|
|
6
6
|
|
|
7
7
|
### Basic GET Request
|
|
8
8
|
|
|
9
9
|
```javascript
|
|
10
|
-
|
|
10
|
+
odac.get('/api/data', function(response) {
|
|
11
11
|
console.log('Data:', response)
|
|
12
12
|
})
|
|
13
13
|
```
|
|
@@ -16,7 +16,7 @@ Candy.get('/api/data', function(response) {
|
|
|
16
16
|
|
|
17
17
|
```javascript
|
|
18
18
|
const userId = 123
|
|
19
|
-
|
|
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 @@ Candy.get(`/api/users/${userId}`, function(user) {
|
|
|
25
25
|
### Error Handling
|
|
26
26
|
|
|
27
27
|
```javascript
|
|
28
|
-
|
|
28
|
+
odac.get('/api/data', function(response) {
|
|
29
29
|
if (response.error) {
|
|
30
30
|
console.error('Error:', response.error)
|
|
31
31
|
return
|
|
@@ -40,10 +40,10 @@ Candy.get('/api/data', function(response) {
|
|
|
40
40
|
|
|
41
41
|
### Using Forms
|
|
42
42
|
|
|
43
|
-
The recommended way to make POST requests is using `
|
|
43
|
+
The recommended way to make POST requests is using `Odac.form()`:
|
|
44
44
|
|
|
45
45
|
```javascript
|
|
46
|
-
|
|
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
|
|
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',
|
|
66
|
+
formData.append('_token', odac.token())
|
|
67
67
|
|
|
68
68
|
fetch('/api/submit', {
|
|
69
69
|
method: 'POST',
|
|
@@ -79,16 +79,16 @@ fetch('/api/submit', {
|
|
|
79
79
|
|
|
80
80
|
### Automatic Token Management
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
odac.js automatically handles CSRF tokens:
|
|
83
83
|
|
|
84
84
|
```javascript
|
|
85
85
|
// Token is automatically included
|
|
86
|
-
|
|
86
|
+
odac.get('/api/data', function(response) {
|
|
87
87
|
// ...
|
|
88
88
|
})
|
|
89
89
|
|
|
90
90
|
// Token is automatically included in forms
|
|
91
|
-
|
|
91
|
+
odac.form('#my-form', function(data) {
|
|
92
92
|
// ...
|
|
93
93
|
})
|
|
94
94
|
```
|
|
@@ -98,7 +98,7 @@ Candy.form('#my-form', function(data) {
|
|
|
98
98
|
If you need the token manually:
|
|
99
99
|
|
|
100
100
|
```javascript
|
|
101
|
-
const 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
|
-
|
|
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 @@ Candy.get('/api/profile', function(profile) {
|
|
|
118
118
|
|
|
119
119
|
```javascript
|
|
120
120
|
// Get products
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
217
|
+
odac.get('/api/notifications', function(notifications) {
|
|
218
218
|
updateNotificationBadge(notifications.count)
|
|
219
219
|
})
|
|
220
220
|
}, 30000)
|
|
@@ -226,7 +226,7 @@ setInterval(function() {
|
|
|
226
226
|
|
|
227
227
|
```javascript
|
|
228
228
|
// controller/get/users.js
|
|
229
|
-
module.exports = function(
|
|
229
|
+
module.exports = function(Odac) {
|
|
230
230
|
// Get all users
|
|
231
231
|
const users = [
|
|
232
232
|
{id: 1, name: 'John', email: 'john@example.com'},
|
|
@@ -239,21 +239,21 @@ module.exports = function(Candy) {
|
|
|
239
239
|
|
|
240
240
|
```javascript
|
|
241
241
|
// route/www.js
|
|
242
|
-
|
|
242
|
+
odac.Route.get('/api/users', 'users')
|
|
243
243
|
```
|
|
244
244
|
|
|
245
245
|
### GET with Parameters
|
|
246
246
|
|
|
247
247
|
```javascript
|
|
248
248
|
// controller/get/user.js
|
|
249
|
-
module.exports = async function(
|
|
250
|
-
const userId =
|
|
249
|
+
module.exports = async function(Odac) {
|
|
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
|
-
|
|
256
|
+
odac.Request.status(404)
|
|
257
257
|
return {error: 'User not found'}
|
|
258
258
|
}
|
|
259
259
|
|
|
@@ -263,16 +263,16 @@ module.exports = async function(Candy) {
|
|
|
263
263
|
|
|
264
264
|
```javascript
|
|
265
265
|
// route/www.js
|
|
266
|
-
|
|
266
|
+
odac.Route.get('/api/users/{id}', 'user')
|
|
267
267
|
```
|
|
268
268
|
|
|
269
269
|
### POST Endpoint
|
|
270
270
|
|
|
271
271
|
```javascript
|
|
272
272
|
// controller/post/create.js
|
|
273
|
-
module.exports = async function(
|
|
274
|
-
const name = await
|
|
275
|
-
const email = await
|
|
273
|
+
module.exports = async function(Odac) {
|
|
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(Candy) {
|
|
|
300
300
|
|
|
301
301
|
```javascript
|
|
302
302
|
// route/www.js
|
|
303
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
406
|
+
odac.get(url, function(data) {
|
|
407
407
|
if (data.error && attempts < maxRetries) {
|
|
408
408
|
setTimeout(attempt, 1000 * attempts)
|
|
409
409
|
} else {
|
|
@@ -440,4 +440,4 @@ function getWithRetry(url, callback, maxRetries = 3) {
|
|
|
440
440
|
|
|
441
441
|
- Learn about [Form Handling](../03-forms/01-form-handling.md)
|
|
442
442
|
- Explore [AJAX Navigation](../02-ajax-navigation/01-quick-start.md)
|
|
443
|
-
- Check [
|
|
443
|
+
- Check [odac.js Overview](../01-overview/01-introduction.md)
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# Client-Side Streaming
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Odac provides a simple client-side API for consuming Server-Sent Events (SSE) streams.
|
|
4
4
|
|
|
5
5
|
### Basic Usage
|
|
6
6
|
|
|
7
7
|
```javascript
|
|
8
8
|
// Simple callback
|
|
9
|
-
|
|
9
|
+
odac.listen('/events', (data) => {
|
|
10
10
|
console.log('Received:', data)
|
|
11
11
|
})
|
|
12
12
|
```
|
|
@@ -14,7 +14,7 @@ Candy.listen('/events', (data) => {
|
|
|
14
14
|
### With Options
|
|
15
15
|
|
|
16
16
|
```javascript
|
|
17
|
-
const stream =
|
|
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
|
-
|
|
37
|
+
odac.listen('/events', (data) => {
|
|
38
38
|
console.log(data.message) // "Hello"
|
|
39
39
|
})
|
|
40
40
|
```
|
|
@@ -47,8 +47,8 @@ Candy.listen('/events', (data) => {
|
|
|
47
47
|
|
|
48
48
|
```javascript
|
|
49
49
|
// Server
|
|
50
|
-
|
|
51
|
-
|
|
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 @@ Candy.Route.get('/dashboard/stats', async (Candy) => {
|
|
|
62
62
|
})
|
|
63
63
|
|
|
64
64
|
// Client
|
|
65
|
-
|
|
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 @@ Candy.listen('/dashboard/stats', (stats) => {
|
|
|
72
72
|
|
|
73
73
|
```javascript
|
|
74
74
|
// Server
|
|
75
|
-
|
|
76
|
-
const userId = await
|
|
75
|
+
odac.Route.get('/notifications', async (Odac) => {
|
|
76
|
+
const userId = await odac.request('userId')
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
odac.stream((send) => {
|
|
79
79
|
global.notificationStreams[userId] = { send }
|
|
80
80
|
})
|
|
81
81
|
})
|
|
82
82
|
|
|
83
83
|
// Client
|
|
84
|
-
|
|
84
|
+
odac.listen('/notifications', (notification) => {
|
|
85
85
|
showToast(notification.message)
|
|
86
86
|
})
|
|
87
87
|
```
|
|
@@ -90,8 +90,8 @@ Candy.listen('/notifications', (notification) => {
|
|
|
90
90
|
|
|
91
91
|
```javascript
|
|
92
92
|
// Server
|
|
93
|
-
|
|
94
|
-
|
|
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 @@ Candy.Route.get('/build/logs', async (Candy) => {
|
|
|
100
100
|
|
|
101
101
|
// Client
|
|
102
102
|
const logs = []
|
|
103
|
-
|
|
103
|
+
odac.listen('/build/logs', (log) => {
|
|
104
104
|
logs.push(log)
|
|
105
105
|
updateLogsUI(logs)
|
|
106
106
|
})
|
|
@@ -108,7 +108,7 @@ Candy.listen('/build/logs', (log) => {
|
|
|
108
108
|
|
|
109
109
|
## API Reference
|
|
110
110
|
|
|
111
|
-
###
|
|
111
|
+
### odac.listen(url, onMessage, options)
|
|
112
112
|
|
|
113
113
|
**Parameters:**
|
|
114
114
|
- `url` (string): Stream endpoint URL
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# WebSocket Overview
|
|
2
|
+
|
|
3
|
+
Odac provides built-in WebSocket support with automatic reconnection and cross-tab sharing capabilities.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
**Backend (route/main.js):**
|
|
8
|
+
```javascript
|
|
9
|
+
odac.Route.ws('/chat', (ws, Odac) => {
|
|
10
|
+
ws.on('message', data => {
|
|
11
|
+
ws.broadcast(data)
|
|
12
|
+
})
|
|
13
|
+
})
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
**Frontend:**
|
|
17
|
+
```javascript
|
|
18
|
+
const ws = Odac.ws('/chat')
|
|
19
|
+
ws.on('message', data => console.log(data))
|
|
20
|
+
ws.send({message: 'Hello!'})
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Key Features
|
|
24
|
+
|
|
25
|
+
### 🔄 Automatic Reconnection
|
|
26
|
+
Automatically reconnects on connection loss with configurable retry logic.
|
|
27
|
+
|
|
28
|
+
### 🔗 Cross-Tab Sharing
|
|
29
|
+
Share a single WebSocket connection across multiple browser tabs using SharedWorker.
|
|
30
|
+
|
|
31
|
+
### 🏠 Room Support
|
|
32
|
+
Group clients into rooms for targeted broadcasting.
|
|
33
|
+
|
|
34
|
+
### 🔐 Authentication
|
|
35
|
+
Full access to Odac context for authentication and authorization.
|
|
36
|
+
|
|
37
|
+
### 📦 JSON Auto-Parsing
|
|
38
|
+
Automatically parses JSON messages on both client and server.
|
|
39
|
+
|
|
40
|
+
### 🎯 URL Parameters
|
|
41
|
+
Support for dynamic route parameters like `/room/{id}`.
|
|
42
|
+
|
|
43
|
+
## Architecture
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
Browser Tab 1 ─┐
|
|
47
|
+
Browser Tab 2 ─┼─> SharedWorker ─> WebSocket ─> Odac Server ─> Your Handler
|
|
48
|
+
Browser Tab 3 ─┘
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
With `shared: true`, all tabs share one connection through a SharedWorker.
|
|
52
|
+
|
|
53
|
+
## Use Cases
|
|
54
|
+
|
|
55
|
+
- **Real-time chat applications**
|
|
56
|
+
- **Live notifications**
|
|
57
|
+
- **Collaborative editing**
|
|
58
|
+
- **Live dashboards**
|
|
59
|
+
- **Multiplayer games**
|
|
60
|
+
- **Stock/crypto price updates**
|
|
61
|
+
- **IoT device monitoring**
|
|
62
|
+
|
|
63
|
+
## Browser Support
|
|
64
|
+
|
|
65
|
+
| Feature | Chrome | Firefox | Safari | Edge |
|
|
66
|
+
|---------|--------|---------|--------|------|
|
|
67
|
+
| WebSocket | ✅ | ✅ | ✅ | ✅ |
|
|
68
|
+
| Shared WebSocket | ✅ | ✅ | ❌ | ✅ |
|
|
69
|
+
|
|
70
|
+
*Shared WebSocket automatically falls back to regular WebSocket in unsupported browsers.*
|
|
71
|
+
|
|
72
|
+
## Next Steps
|
|
73
|
+
|
|
74
|
+
- [WebSocket Client](01-websocket-client.md) - Basic client usage
|
|
75
|
+
- [Shared WebSocket](02-shared-websocket.md) - Cross-tab communication
|
|
76
|
+
- [Backend WebSocket](../../backend/04-routing/09-websocket.md) - Server-side implementation
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# WebSocket Client
|
|
2
|
+
|
|
3
|
+
odac.js provides a simple WebSocket client with automatic reconnection and cross-tab sharing support.
|
|
4
|
+
|
|
5
|
+
## Basic Usage
|
|
6
|
+
|
|
7
|
+
```javascript
|
|
8
|
+
const ws = Odac.ws('/chat')
|
|
9
|
+
|
|
10
|
+
ws.on('open', () => {
|
|
11
|
+
console.log('Connected!')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
ws.on('message', data => {
|
|
15
|
+
console.log('Received:', data)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
ws.send({type: 'hello', message: 'Hi there!'})
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Configuration Options
|
|
22
|
+
|
|
23
|
+
```javascript
|
|
24
|
+
const ws = Odac.ws('/chat', {
|
|
25
|
+
autoReconnect: true, // Auto-reconnect on disconnect (default: true)
|
|
26
|
+
reconnectDelay: 3000, // Delay between reconnect attempts (default: 3000ms)
|
|
27
|
+
maxReconnectAttempts: 10, // Max reconnect attempts (default: 10)
|
|
28
|
+
shared: false, // Share connection across browser tabs (default: false)
|
|
29
|
+
token: true // Send CSRF token (default: true)
|
|
30
|
+
})
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## CSRF Token Protection
|
|
34
|
+
|
|
35
|
+
By default, odac.js automatically sends a CSRF token during the WebSocket handshake (similar to AJAX requests). The token is sent via the `Sec-WebSocket-Protocol` header.
|
|
36
|
+
|
|
37
|
+
**Disable token (for public WebSockets):**
|
|
38
|
+
```javascript
|
|
39
|
+
const ws = Odac.ws('/public', {token: false})
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**How it works:**
|
|
43
|
+
1. Client calls `Odac.token()` to get current CSRF token
|
|
44
|
+
2. Token is sent as `odac-token-{token}` in WebSocket protocol header
|
|
45
|
+
3. Server validates token before accepting connection
|
|
46
|
+
4. If invalid, connection closes with code `4002`
|
|
47
|
+
|
|
48
|
+
## Shared WebSocket (Cross-Tab)
|
|
49
|
+
|
|
50
|
+
Enable `shared: true` to share a single WebSocket connection across all browser tabs:
|
|
51
|
+
|
|
52
|
+
```javascript
|
|
53
|
+
const ws = Odac.ws('/chat', {shared: true})
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Benefits:**
|
|
57
|
+
- Single connection shared across all tabs
|
|
58
|
+
- Reduced server load
|
|
59
|
+
- Synchronized state across tabs
|
|
60
|
+
- Automatic cleanup when all tabs close
|
|
61
|
+
|
|
62
|
+
**Browser Support:**
|
|
63
|
+
- Uses SharedWorker API (supported in Chrome, Edge, Firefox)
|
|
64
|
+
- Falls back to regular WebSocket if SharedWorker is unavailable
|
|
65
|
+
|
|
66
|
+
**Example:**
|
|
67
|
+
```javascript
|
|
68
|
+
// Tab 1
|
|
69
|
+
const ws = Odac.ws('/notifications', {shared: true})
|
|
70
|
+
ws.on('message', data => {
|
|
71
|
+
console.log('Notification:', data)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
// Tab 2 (same connection)
|
|
75
|
+
const ws2 = Odac.ws('/notifications', {shared: true})
|
|
76
|
+
ws2.on('message', data => {
|
|
77
|
+
console.log('Same notification:', data)
|
|
78
|
+
})
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Event Handlers
|
|
82
|
+
|
|
83
|
+
```javascript
|
|
84
|
+
ws.on('open', () => {}) // Connection established
|
|
85
|
+
ws.on('message', data => {}) // Message received (auto-parsed JSON)
|
|
86
|
+
ws.on('close', event => {}) // Connection closed
|
|
87
|
+
ws.on('error', event => {}) // Error occurred
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Sending Messages
|
|
91
|
+
|
|
92
|
+
```javascript
|
|
93
|
+
// Objects are automatically JSON-stringified
|
|
94
|
+
ws.send({type: 'chat', message: 'Hello!'})
|
|
95
|
+
|
|
96
|
+
// Strings sent as-is
|
|
97
|
+
ws.send('Plain text')
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Connection State
|
|
101
|
+
|
|
102
|
+
```javascript
|
|
103
|
+
ws.connected // true if connected
|
|
104
|
+
ws.state // WebSocket.OPEN, CLOSED, etc.
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Closing Connection
|
|
108
|
+
|
|
109
|
+
```javascript
|
|
110
|
+
ws.close()
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Removing Event Handlers
|
|
114
|
+
|
|
115
|
+
```javascript
|
|
116
|
+
const handler = data => console.log(data)
|
|
117
|
+
ws.on('message', handler)
|
|
118
|
+
ws.off('message', handler) // Remove specific handler
|
|
119
|
+
ws.off('message') // Remove all message handlers
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Example: Chat Application
|
|
123
|
+
|
|
124
|
+
```javascript
|
|
125
|
+
const ws = Odac.ws('/chat')
|
|
126
|
+
const messages = document.getElementById('messages')
|
|
127
|
+
const input = document.getElementById('input')
|
|
128
|
+
|
|
129
|
+
ws.on('message', data => {
|
|
130
|
+
if (data.type === 'chat') {
|
|
131
|
+
messages.innerHTML += `<p>${data.user}: ${data.text}</p>`
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
document.getElementById('send').onclick = () => {
|
|
136
|
+
ws.send({type: 'chat', text: input.value})
|
|
137
|
+
input.value = ''
|
|
138
|
+
}
|
|
139
|
+
```
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# Shared WebSocket (Cross-Tab Communication)
|
|
2
|
+
|
|
3
|
+
Shared WebSocket allows multiple browser tabs to share a single WebSocket connection using the SharedWorker API.
|
|
4
|
+
|
|
5
|
+
## Why Use Shared WebSocket?
|
|
6
|
+
|
|
7
|
+
**Without Shared WebSocket:**
|
|
8
|
+
- Each tab creates its own connection
|
|
9
|
+
- 5 tabs = 5 WebSocket connections
|
|
10
|
+
- Higher server load
|
|
11
|
+
- Inconsistent state across tabs
|
|
12
|
+
|
|
13
|
+
**With Shared WebSocket:**
|
|
14
|
+
- All tabs share one connection
|
|
15
|
+
- 5 tabs = 1 WebSocket connection
|
|
16
|
+
- Lower server load
|
|
17
|
+
- Synchronized state across tabs
|
|
18
|
+
|
|
19
|
+
## Basic Usage
|
|
20
|
+
|
|
21
|
+
```javascript
|
|
22
|
+
const ws = Odac.ws('/chat', {shared: true})
|
|
23
|
+
|
|
24
|
+
ws.on('message', data => {
|
|
25
|
+
console.log('Message received in this tab:', data)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
ws.send({message: 'Hello from this tab'})
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Real-World Example: Notification System
|
|
32
|
+
|
|
33
|
+
```javascript
|
|
34
|
+
// All tabs share the same notification connection
|
|
35
|
+
const notifications = Odac.ws('/notifications', {
|
|
36
|
+
shared: true,
|
|
37
|
+
autoReconnect: true
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
notifications.on('open', () => {
|
|
41
|
+
console.log('Notification system connected')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
notifications.on('message', data => {
|
|
45
|
+
if (data.type === 'notification') {
|
|
46
|
+
showNotification(data.title, data.message)
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
function showNotification(title, message) {
|
|
51
|
+
if (Notification.permission === 'granted') {
|
|
52
|
+
new Notification(title, {body: message})
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Chat Application Example
|
|
58
|
+
|
|
59
|
+
```javascript
|
|
60
|
+
// Shared chat connection across all tabs
|
|
61
|
+
const chat = Odac.ws('/chat/room/general', {shared: true})
|
|
62
|
+
|
|
63
|
+
chat.on('message', data => {
|
|
64
|
+
if (data.type === 'message') {
|
|
65
|
+
addMessageToUI(data.user, data.text)
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
document.getElementById('send').onclick = () => {
|
|
70
|
+
const text = document.getElementById('input').value
|
|
71
|
+
chat.send({type: 'message', text})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// All tabs will receive the same messages
|
|
75
|
+
// Only one connection to the server
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Browser Compatibility
|
|
79
|
+
|
|
80
|
+
Shared WebSocket uses the SharedWorker API:
|
|
81
|
+
|
|
82
|
+
- ✅ Chrome/Edge: Full support
|
|
83
|
+
- ✅ Firefox: Full support
|
|
84
|
+
- ❌ Safari: Not supported (falls back to regular WebSocket)
|
|
85
|
+
|
|
86
|
+
**Automatic Fallback:**
|
|
87
|
+
```javascript
|
|
88
|
+
// If SharedWorker is not available, automatically falls back to regular WebSocket
|
|
89
|
+
const ws = Odac.ws('/chat', {shared: true})
|
|
90
|
+
// Works in all browsers, shared only where supported
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Lifecycle Management
|
|
94
|
+
|
|
95
|
+
The shared connection automatically manages its lifecycle:
|
|
96
|
+
|
|
97
|
+
```javascript
|
|
98
|
+
// Tab 1 opens
|
|
99
|
+
const ws1 = Odac.ws('/chat', {shared: true})
|
|
100
|
+
// Connection established
|
|
101
|
+
|
|
102
|
+
// Tab 2 opens
|
|
103
|
+
const ws2 = Odac.ws('/chat', {shared: true})
|
|
104
|
+
// Reuses existing connection
|
|
105
|
+
|
|
106
|
+
// Tab 1 closes
|
|
107
|
+
// Connection stays alive (Tab 2 still using it)
|
|
108
|
+
|
|
109
|
+
// Tab 2 closes
|
|
110
|
+
// Connection automatically closes (no tabs using it)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## When to Use Shared WebSocket
|
|
114
|
+
|
|
115
|
+
**Good Use Cases:**
|
|
116
|
+
- Notification systems
|
|
117
|
+
- Real-time updates (stock prices, sports scores)
|
|
118
|
+
- Chat applications
|
|
119
|
+
- Collaborative editing
|
|
120
|
+
- Live dashboards
|
|
121
|
+
|
|
122
|
+
**Not Recommended For:**
|
|
123
|
+
- User-specific authenticated connections
|
|
124
|
+
- File uploads/downloads
|
|
125
|
+
- Connections requiring per-tab state
|
|
126
|
+
|
|
127
|
+
## Performance Comparison
|
|
128
|
+
|
|
129
|
+
**Regular WebSocket (5 tabs):**
|
|
130
|
+
- Server connections: 5
|
|
131
|
+
- Memory usage: ~5MB
|
|
132
|
+
- Network overhead: 5x
|
|
133
|
+
|
|
134
|
+
**Shared WebSocket (5 tabs):**
|
|
135
|
+
- Server connections: 1
|
|
136
|
+
- Memory usage: ~1MB
|
|
137
|
+
- Network overhead: 1x
|
|
138
|
+
|
|
139
|
+
## Debugging
|
|
140
|
+
|
|
141
|
+
Check if SharedWorker is being used:
|
|
142
|
+
|
|
143
|
+
```javascript
|
|
144
|
+
const ws = Odac.ws('/chat', {shared: true})
|
|
145
|
+
|
|
146
|
+
// In Chrome DevTools:
|
|
147
|
+
// chrome://inspect/#workers
|
|
148
|
+
// You'll see "odac-ws-/chat" if shared
|
|
149
|
+
```
|