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
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
# WebSocket Examples
|
|
2
|
+
|
|
3
|
+
Complete examples for common WebSocket use cases.
|
|
4
|
+
|
|
5
|
+
## Echo Server
|
|
6
|
+
|
|
7
|
+
Simple echo server that sends back received messages:
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
// route/websocket.js
|
|
11
|
+
Odac.Route.ws('/echo', Odac => {
|
|
12
|
+
Odac.ws.send({type: 'welcome', message: 'Connected!'})
|
|
13
|
+
|
|
14
|
+
Odac.ws.on('message', data => {
|
|
15
|
+
Odac.ws.send({type: 'echo', data})
|
|
16
|
+
})
|
|
17
|
+
})
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**Client:**
|
|
21
|
+
```javascript
|
|
22
|
+
const ws = Odac.ws('/echo')
|
|
23
|
+
ws.on('message', data => console.log(data))
|
|
24
|
+
ws.send({message: 'Hello!'})
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Authenticated Chat Room
|
|
28
|
+
|
|
29
|
+
### Using auth.ws() (Recommended)
|
|
30
|
+
|
|
31
|
+
```javascript
|
|
32
|
+
Odac.Route.auth.ws('/chat', async Odac => {
|
|
33
|
+
const user = await Odac.Auth.user()
|
|
34
|
+
|
|
35
|
+
Odac.ws.join('general')
|
|
36
|
+
Odac.ws.data.user = user
|
|
37
|
+
|
|
38
|
+
Odac.ws.to('general').send({
|
|
39
|
+
type: 'user_joined',
|
|
40
|
+
user: user.name
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
Odac.ws.on('message', data => {
|
|
44
|
+
Odac.ws.to('general').send({
|
|
45
|
+
type: 'message',
|
|
46
|
+
user: user.name,
|
|
47
|
+
text: data.text
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
Odac.ws.on('close', () => {
|
|
52
|
+
Odac.ws.to('general').send({
|
|
53
|
+
type: 'user_left',
|
|
54
|
+
user: user.name
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Manual Authentication Check
|
|
61
|
+
|
|
62
|
+
```javascript
|
|
63
|
+
Odac.Route.ws('/chat', async Odac => {
|
|
64
|
+
const user = await Odac.Auth.user()
|
|
65
|
+
|
|
66
|
+
if (!user) {
|
|
67
|
+
Odac.ws.close(4001, 'Unauthorized')
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
Odac.ws.join('general')
|
|
72
|
+
Odac.ws.data.user = user
|
|
73
|
+
|
|
74
|
+
Odac.ws.to('general').send({
|
|
75
|
+
type: 'user_joined',
|
|
76
|
+
user: user.name
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
Odac.ws.on('message', data => {
|
|
80
|
+
Odac.ws.to('general').send({
|
|
81
|
+
type: 'message',
|
|
82
|
+
user: user.name,
|
|
83
|
+
text: data.text
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
Odac.ws.on('close', () => {
|
|
88
|
+
Odac.ws.to('general').send({
|
|
89
|
+
type: 'user_left',
|
|
90
|
+
user: user.name
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Client:**
|
|
97
|
+
```javascript
|
|
98
|
+
const chat = Odac.ws('/chat')
|
|
99
|
+
|
|
100
|
+
chat.on('message', data => {
|
|
101
|
+
if (data.type === 'message') {
|
|
102
|
+
console.log(`${data.user}: ${data.text}`)
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
chat.send({text: 'Hello everyone!'})
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Room-Based Chat with URL Parameters
|
|
110
|
+
|
|
111
|
+
Dynamic rooms using URL parameters:
|
|
112
|
+
|
|
113
|
+
```javascript
|
|
114
|
+
Odac.Route.ws('/room/{roomId}', async Odac => {
|
|
115
|
+
const {roomId} = Odac.Request.data.url
|
|
116
|
+
const user = await Odac.Auth.user()
|
|
117
|
+
|
|
118
|
+
if (!user) {
|
|
119
|
+
Odac.ws.close(4001, 'Unauthorized')
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
Odac.ws.join(roomId)
|
|
124
|
+
Odac.ws.data.roomId = roomId
|
|
125
|
+
|
|
126
|
+
Odac.ws.send({
|
|
127
|
+
type: 'joined',
|
|
128
|
+
room: roomId
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
Odac.ws.on('message', data => {
|
|
132
|
+
Odac.ws.to(roomId).send({
|
|
133
|
+
type: 'message',
|
|
134
|
+
user: user.name,
|
|
135
|
+
text: data.text,
|
|
136
|
+
room: roomId
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**Client:**
|
|
143
|
+
```javascript
|
|
144
|
+
const room = Odac.ws('/room/gaming')
|
|
145
|
+
room.on('message', data => console.log(data))
|
|
146
|
+
room.send({text: 'Hi from gaming room!'})
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Real-Time Notifications
|
|
150
|
+
|
|
151
|
+
User-specific notification system:
|
|
152
|
+
|
|
153
|
+
```javascript
|
|
154
|
+
Odac.Route.ws('/notifications', async Odac => {
|
|
155
|
+
const user = await Odac.Auth.user()
|
|
156
|
+
|
|
157
|
+
if (!user) {
|
|
158
|
+
Odac.ws.close(4001, 'Unauthorized')
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
Odac.ws.join(`user-${user.id}`)
|
|
163
|
+
Odac.ws.data.userId = user.id
|
|
164
|
+
|
|
165
|
+
Odac.ws.send({
|
|
166
|
+
type: 'connected',
|
|
167
|
+
unreadCount: await getUnreadCount(user.id)
|
|
168
|
+
})
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
// Send notification from anywhere in your app
|
|
172
|
+
async function notifyUser(userId, notification) {
|
|
173
|
+
const wsServer = Odac.Route.wsServer
|
|
174
|
+
wsServer.toRoom(`user-${userId}`, {
|
|
175
|
+
type: 'notification',
|
|
176
|
+
...notification
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Client (with cross-tab sharing):**
|
|
182
|
+
```javascript
|
|
183
|
+
const notifications = Odac.ws('/notifications', {
|
|
184
|
+
shared: true,
|
|
185
|
+
autoReconnect: true
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
notifications.on('message', data => {
|
|
189
|
+
if (data.type === 'notification') {
|
|
190
|
+
showNotification(data.title, data.message)
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Broadcasting System
|
|
196
|
+
|
|
197
|
+
Broadcast messages to all connected clients:
|
|
198
|
+
|
|
199
|
+
```javascript
|
|
200
|
+
Odac.Route.ws('/broadcast', Odac => {
|
|
201
|
+
Odac.ws.on('message', data => {
|
|
202
|
+
if (data.type === 'broadcast') {
|
|
203
|
+
Odac.ws.broadcast({
|
|
204
|
+
type: 'announcement',
|
|
205
|
+
message: data.message,
|
|
206
|
+
timestamp: Date.now()
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
**Client:**
|
|
214
|
+
```javascript
|
|
215
|
+
const broadcast = Odac.ws('/broadcast')
|
|
216
|
+
|
|
217
|
+
broadcast.on('message', data => {
|
|
218
|
+
if (data.type === 'announcement') {
|
|
219
|
+
alert(data.message)
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
// Send to all clients
|
|
224
|
+
broadcast.send({
|
|
225
|
+
type: 'broadcast',
|
|
226
|
+
message: 'Server maintenance in 5 minutes'
|
|
227
|
+
})
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Live Dashboard
|
|
231
|
+
|
|
232
|
+
Real-time data updates for dashboards:
|
|
233
|
+
|
|
234
|
+
```javascript
|
|
235
|
+
Odac.Route.ws('/dashboard', async Odac => {
|
|
236
|
+
const user = await Odac.Auth.user()
|
|
237
|
+
|
|
238
|
+
if (!user || !user.isAdmin) {
|
|
239
|
+
Odac.ws.close(4001, 'Unauthorized')
|
|
240
|
+
return
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const sendStats = async () => {
|
|
244
|
+
const stats = await getSystemStats()
|
|
245
|
+
Odac.ws.send({type: 'stats', data: stats})
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
sendStats()
|
|
249
|
+
const interval = setInterval(sendStats, 5000)
|
|
250
|
+
|
|
251
|
+
Odac.ws.on('close', () => {
|
|
252
|
+
clearInterval(interval)
|
|
253
|
+
})
|
|
254
|
+
})
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
**Client:**
|
|
258
|
+
```javascript
|
|
259
|
+
const dashboard = Odac.ws('/dashboard')
|
|
260
|
+
|
|
261
|
+
dashboard.on('message', data => {
|
|
262
|
+
if (data.type === 'stats') {
|
|
263
|
+
updateDashboard(data.data)
|
|
264
|
+
}
|
|
265
|
+
})
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## WebSocket with Middleware
|
|
269
|
+
|
|
270
|
+
Use middleware for rate limiting, authentication, or custom logic:
|
|
271
|
+
|
|
272
|
+
```javascript
|
|
273
|
+
// middleware/rate-limit.js
|
|
274
|
+
const connections = new Map()
|
|
275
|
+
|
|
276
|
+
module.exports = async Odac => {
|
|
277
|
+
const ip = Odac.Request.ip
|
|
278
|
+
const now = Date.now()
|
|
279
|
+
|
|
280
|
+
if (connections.has(ip)) {
|
|
281
|
+
const lastConnection = connections.get(ip)
|
|
282
|
+
if (now - lastConnection < 1000) {
|
|
283
|
+
return false // Too many connections
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
connections.set(ip, now)
|
|
288
|
+
return true
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// route/websocket.js
|
|
292
|
+
Odac.Route.use('rate-limit').ws('/chat', Odac => {
|
|
293
|
+
Odac.ws.send({type: 'connected'})
|
|
294
|
+
})
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
**Multiple Middleware:**
|
|
298
|
+
|
|
299
|
+
```javascript
|
|
300
|
+
Odac.Route.use('auth', 'rate-limit', 'log-connection').ws('/secure', Odac => {
|
|
301
|
+
Odac.ws.send({type: 'authenticated'})
|
|
302
|
+
})
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## Multiplayer Game
|
|
306
|
+
|
|
307
|
+
Simple multiplayer game state synchronization:
|
|
308
|
+
|
|
309
|
+
```javascript
|
|
310
|
+
Odac.Route.ws('/game/{gameId}', async Odac => {
|
|
311
|
+
const {gameId} = Odac.Request.data.url
|
|
312
|
+
const user = await Odac.Auth.user()
|
|
313
|
+
|
|
314
|
+
if (!user) {
|
|
315
|
+
Odac.ws.close(4001, 'Unauthorized')
|
|
316
|
+
return
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
Odac.ws.join(`game-${gameId}`)
|
|
320
|
+
Odac.ws.data.gameId = gameId
|
|
321
|
+
Odac.ws.data.playerId = user.id
|
|
322
|
+
|
|
323
|
+
Odac.ws.to(`game-${gameId}`).send({
|
|
324
|
+
type: 'player_joined',
|
|
325
|
+
playerId: user.id,
|
|
326
|
+
name: user.name
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
Odac.ws.on('message', data => {
|
|
330
|
+
switch (data.type) {
|
|
331
|
+
case 'move':
|
|
332
|
+
Odac.ws.to(`game-${gameId}`).send({
|
|
333
|
+
type: 'player_moved',
|
|
334
|
+
playerId: user.id,
|
|
335
|
+
position: data.position
|
|
336
|
+
})
|
|
337
|
+
break
|
|
338
|
+
|
|
339
|
+
case 'action':
|
|
340
|
+
Odac.ws.to(`game-${gameId}`).send({
|
|
341
|
+
type: 'player_action',
|
|
342
|
+
playerId: user.id,
|
|
343
|
+
action: data.action
|
|
344
|
+
})
|
|
345
|
+
break
|
|
346
|
+
}
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
Odac.ws.on('close', () => {
|
|
350
|
+
Odac.ws.to(`game-${gameId}`).send({
|
|
351
|
+
type: 'player_left',
|
|
352
|
+
playerId: user.id
|
|
353
|
+
})
|
|
354
|
+
})
|
|
355
|
+
})
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
**Client:**
|
|
359
|
+
```javascript
|
|
360
|
+
const game = Odac.ws('/game/room-123')
|
|
361
|
+
|
|
362
|
+
game.on('message', data => {
|
|
363
|
+
switch (data.type) {
|
|
364
|
+
case 'player_joined':
|
|
365
|
+
addPlayer(data.playerId, data.name)
|
|
366
|
+
break
|
|
367
|
+
case 'player_moved':
|
|
368
|
+
updatePlayerPosition(data.playerId, data.position)
|
|
369
|
+
break
|
|
370
|
+
case 'player_action':
|
|
371
|
+
handlePlayerAction(data.playerId, data.action)
|
|
372
|
+
break
|
|
373
|
+
}
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
// Send player movement
|
|
377
|
+
game.send({
|
|
378
|
+
type: 'move',
|
|
379
|
+
position: {x: 100, y: 200}
|
|
380
|
+
})
|
|
381
|
+
```
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# WebSocket Quick Reference
|
|
2
|
+
|
|
3
|
+
Quick reference for Odac WebSocket API.
|
|
4
|
+
|
|
5
|
+
## Backend API
|
|
6
|
+
|
|
7
|
+
### Route Definition
|
|
8
|
+
```javascript
|
|
9
|
+
Odac.Route.ws('/path', Odac => {
|
|
10
|
+
// Handler - WebSocket client accessible via Odac.ws
|
|
11
|
+
})
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
### WebSocket Client Methods (Odac.ws)
|
|
15
|
+
|
|
16
|
+
| Method | Description |
|
|
17
|
+
|--------|-------------|
|
|
18
|
+
| `Odac.ws.send(data)` | Send JSON data to client |
|
|
19
|
+
| `Odac.ws.sendBinary(buffer)` | Send binary data |
|
|
20
|
+
| `Odac.ws.close(code, reason)` | Close connection |
|
|
21
|
+
| `Odac.ws.ping()` | Send ping frame |
|
|
22
|
+
| `Odac.ws.join(room)` | Join a room |
|
|
23
|
+
| `Odac.ws.leave(room)` | Leave a room |
|
|
24
|
+
| `Odac.ws.to(room).send(data)` | Send to room |
|
|
25
|
+
| `Odac.ws.broadcast(data)` | Send to all clients |
|
|
26
|
+
| `Odac.ws.on(event, handler)` | Add event listener |
|
|
27
|
+
| `Odac.ws.off(event, handler)` | Remove event listener |
|
|
28
|
+
|
|
29
|
+
### WebSocket Client Properties
|
|
30
|
+
|
|
31
|
+
| Property | Description |
|
|
32
|
+
|----------|-------------|
|
|
33
|
+
| `Odac.ws.id` | Unique client ID |
|
|
34
|
+
| `Odac.ws.rooms` | Array of joined rooms |
|
|
35
|
+
| `Odac.ws.data` | Custom data storage |
|
|
36
|
+
|
|
37
|
+
### Events
|
|
38
|
+
|
|
39
|
+
| Event | Description |
|
|
40
|
+
|-------|-------------|
|
|
41
|
+
| `message` | Incoming message |
|
|
42
|
+
| `close` | Connection closed |
|
|
43
|
+
| `error` | Error occurred |
|
|
44
|
+
| `pong` | Pong received |
|
|
45
|
+
|
|
46
|
+
### Server Methods
|
|
47
|
+
|
|
48
|
+
| Method | Description |
|
|
49
|
+
|--------|-------------|
|
|
50
|
+
| `Odac.Route.wsServer.clients` | Map of all clients |
|
|
51
|
+
| `Odac.Route.wsServer.clientCount` | Number of clients |
|
|
52
|
+
| `Odac.Route.wsServer.toRoom(room, data)` | Send to room |
|
|
53
|
+
| `Odac.Route.wsServer.broadcast(data)` | Broadcast to all |
|
|
54
|
+
|
|
55
|
+
## Frontend API
|
|
56
|
+
|
|
57
|
+
### Connection
|
|
58
|
+
```javascript
|
|
59
|
+
const ws = Odac.ws('/path', options)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Backend Options
|
|
63
|
+
|
|
64
|
+
| Option | Default | Description |
|
|
65
|
+
|--------|---------|-------------|
|
|
66
|
+
| `token` | `true` | Require CSRF token |
|
|
67
|
+
|
|
68
|
+
### Client Options
|
|
69
|
+
|
|
70
|
+
| Option | Default | Description |
|
|
71
|
+
|--------|---------|-------------|
|
|
72
|
+
| `autoReconnect` | `true` | Auto-reconnect on disconnect |
|
|
73
|
+
| `reconnectDelay` | `3000` | Delay between reconnects (ms) |
|
|
74
|
+
| `maxReconnectAttempts` | `10` | Max reconnect attempts |
|
|
75
|
+
| `shared` | `false` | Share across browser tabs |
|
|
76
|
+
| `token` | `true` | Send CSRF token |
|
|
77
|
+
|
|
78
|
+
### Client Methods
|
|
79
|
+
|
|
80
|
+
| Method | Description |
|
|
81
|
+
|--------|-------------|
|
|
82
|
+
| `ws.send(data)` | Send data to server |
|
|
83
|
+
| `ws.close()` | Close connection |
|
|
84
|
+
| `ws.on(event, handler)` | Add event listener |
|
|
85
|
+
| `ws.off(event, handler)` | Remove event listener |
|
|
86
|
+
|
|
87
|
+
### Client Properties
|
|
88
|
+
|
|
89
|
+
| Property | Description |
|
|
90
|
+
|----------|-------------|
|
|
91
|
+
| `ws.connected` | Connection status (boolean) |
|
|
92
|
+
| `ws.state` | WebSocket state |
|
|
93
|
+
|
|
94
|
+
### Events
|
|
95
|
+
|
|
96
|
+
| Event | Description |
|
|
97
|
+
|-------|-------------|
|
|
98
|
+
| `open` | Connection opened |
|
|
99
|
+
| `message` | Message received |
|
|
100
|
+
| `close` | Connection closed |
|
|
101
|
+
| `error` | Error occurred |
|
|
102
|
+
|
|
103
|
+
## Common Patterns
|
|
104
|
+
|
|
105
|
+
### Echo Server
|
|
106
|
+
```javascript
|
|
107
|
+
// With token (default)
|
|
108
|
+
Odac.Route.ws('/echo', Odac => {
|
|
109
|
+
Odac.ws.on('message', data => Odac.ws.send(data))
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
// Public (no token)
|
|
113
|
+
Odac.Route.ws('/public-echo', Odac => {
|
|
114
|
+
Odac.ws.on('message', data => Odac.ws.send(data))
|
|
115
|
+
}, {token: false})
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Authenticated Route
|
|
119
|
+
```javascript
|
|
120
|
+
// Using auth.ws() (recommended)
|
|
121
|
+
Odac.Route.auth.ws('/secure', async Odac => {
|
|
122
|
+
const user = await Odac.Auth.user()
|
|
123
|
+
// Handle connection
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
// Manual check
|
|
127
|
+
Odac.Route.ws('/secure', async Odac => {
|
|
128
|
+
if (!await Odac.Auth.check()) {
|
|
129
|
+
return Odac.ws.close(4001, 'Unauthorized')
|
|
130
|
+
}
|
|
131
|
+
// Handle connection
|
|
132
|
+
})
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### With Middleware
|
|
136
|
+
```javascript
|
|
137
|
+
Odac.Route.use('auth', 'rate-limit').ws('/chat', Odac => {
|
|
138
|
+
Odac.ws.send({type: 'welcome'})
|
|
139
|
+
})
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Room Broadcasting
|
|
143
|
+
```javascript
|
|
144
|
+
Odac.Route.ws('/chat', Odac => {
|
|
145
|
+
Odac.ws.join('room-1')
|
|
146
|
+
Odac.ws.on('message', data => {
|
|
147
|
+
Odac.ws.to('room-1').send(data)
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### URL Parameters
|
|
153
|
+
```javascript
|
|
154
|
+
Odac.Route.ws('/room/{id}', Odac => {
|
|
155
|
+
const {id} = Odac.Request.data.url
|
|
156
|
+
Odac.ws.join(id)
|
|
157
|
+
})
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Shared Connection (Client)
|
|
161
|
+
```javascript
|
|
162
|
+
const ws = Odac.ws('/chat', {shared: true})
|
|
163
|
+
// All tabs share one connection
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Status Codes
|
|
167
|
+
|
|
168
|
+
| Code | Description |
|
|
169
|
+
|------|-------------|
|
|
170
|
+
| `1000` | Normal closure |
|
|
171
|
+
| `1001` | Going away |
|
|
172
|
+
| `1002` | Protocol error |
|
|
173
|
+
| `1003` | Unsupported data |
|
|
174
|
+
| `1006` | Abnormal closure |
|
|
175
|
+
| `4000` | Middleware rejected |
|
|
176
|
+
| `4001` | Unauthorized |
|
|
177
|
+
| `4002` | Invalid/missing token |
|
|
178
|
+
| `4003` | Forbidden (middleware) |
|
|
179
|
+
|
|
180
|
+
## Best Practices
|
|
181
|
+
|
|
182
|
+
1. **Always handle authentication**
|
|
183
|
+
```javascript
|
|
184
|
+
if (!await Odac.Auth.check()) {
|
|
185
|
+
return Odac.ws.close(4001, 'Unauthorized')
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
2. **Use rooms for targeted messaging**
|
|
190
|
+
```javascript
|
|
191
|
+
Odac.ws.join(`user-${userId}`)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
3. **Clean up on close**
|
|
195
|
+
```javascript
|
|
196
|
+
Odac.ws.on('close', () => {
|
|
197
|
+
clearInterval(interval)
|
|
198
|
+
Odac.ws.leave('room')
|
|
199
|
+
})
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
4. **Store per-connection data**
|
|
203
|
+
```javascript
|
|
204
|
+
Odac.ws.data.userId = user.id
|
|
205
|
+
Odac.ws.data.joinedAt = Date.now()
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
5. **Use shared connections for notifications**
|
|
209
|
+
```javascript
|
|
210
|
+
const ws = Odac.ws('/notifications', {shared: true})
|
|
211
|
+
```
|