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
package/src/WebSocket.js
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
const nodeCrypto = require('crypto')
|
|
2
|
+
|
|
3
|
+
const WS_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
|
|
4
|
+
const MAX_PAYLOAD_LENGTH = 10 * 1024 * 1024
|
|
5
|
+
const OPCODE = {
|
|
6
|
+
CONTINUATION: 0x0,
|
|
7
|
+
TEXT: 0x1,
|
|
8
|
+
BINARY: 0x2,
|
|
9
|
+
CLOSE: 0x8,
|
|
10
|
+
PING: 0x9,
|
|
11
|
+
PONG: 0xa
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class WebSocketClient {
|
|
15
|
+
#socket
|
|
16
|
+
#handlers = {}
|
|
17
|
+
#closed = false
|
|
18
|
+
#server
|
|
19
|
+
#id
|
|
20
|
+
#rooms = new Set()
|
|
21
|
+
data = {}
|
|
22
|
+
|
|
23
|
+
constructor(socket, server, id) {
|
|
24
|
+
this.#socket = socket
|
|
25
|
+
this.#server = server
|
|
26
|
+
this.#id = id
|
|
27
|
+
this.#setupListeners()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get id() {
|
|
31
|
+
return this.#id
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get rooms() {
|
|
35
|
+
return Array.from(this.#rooms)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
#setupListeners() {
|
|
39
|
+
let buffer = Buffer.alloc(0)
|
|
40
|
+
|
|
41
|
+
this.#socket.on('data', chunk => {
|
|
42
|
+
buffer = Buffer.concat([buffer, chunk])
|
|
43
|
+
|
|
44
|
+
while (buffer.length >= 2) {
|
|
45
|
+
const frame = this.#parseFrame(buffer)
|
|
46
|
+
if (!frame) break
|
|
47
|
+
|
|
48
|
+
buffer = buffer.slice(frame.totalLength)
|
|
49
|
+
this.#handleFrame(frame)
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
this.#socket.on('close', () => this.#handleClose())
|
|
54
|
+
this.#socket.on('error', err => this.#emit('error', err))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
#parseFrame(buffer) {
|
|
58
|
+
if (buffer.length < 2) return null
|
|
59
|
+
|
|
60
|
+
const firstByte = buffer[0]
|
|
61
|
+
const secondByte = buffer[1]
|
|
62
|
+
|
|
63
|
+
const fin = (firstByte & 0x80) !== 0
|
|
64
|
+
const opcode = firstByte & 0x0f
|
|
65
|
+
const masked = (secondByte & 0x80) !== 0
|
|
66
|
+
|
|
67
|
+
if (!masked) {
|
|
68
|
+
this.close(1002, 'Protocol error: client-to-server frames must be masked.')
|
|
69
|
+
return null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let payloadLength = secondByte & 0x7f
|
|
73
|
+
|
|
74
|
+
let offset = 2
|
|
75
|
+
|
|
76
|
+
if (payloadLength === 126) {
|
|
77
|
+
if (buffer.length < 4) return null
|
|
78
|
+
payloadLength = buffer.readUInt16BE(2)
|
|
79
|
+
offset = 4
|
|
80
|
+
} else if (payloadLength === 127) {
|
|
81
|
+
if (buffer.length < 10) return null
|
|
82
|
+
const payloadLengthBigInt = buffer.readBigUInt64BE(2)
|
|
83
|
+
if (payloadLengthBigInt > Number.MAX_SAFE_INTEGER) {
|
|
84
|
+
this.close(1009, 'Payload too large')
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
payloadLength = Number(payloadLengthBigInt)
|
|
88
|
+
offset = 10
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (payloadLength > MAX_PAYLOAD_LENGTH) {
|
|
92
|
+
this.close(1009, 'Payload too large')
|
|
93
|
+
return null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let maskKey = null
|
|
97
|
+
if (masked) {
|
|
98
|
+
if (buffer.length < offset + 4) return null
|
|
99
|
+
maskKey = buffer.slice(offset, offset + 4)
|
|
100
|
+
offset += 4
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (buffer.length < offset + payloadLength) return null
|
|
104
|
+
|
|
105
|
+
let payload = buffer.slice(offset, offset + payloadLength)
|
|
106
|
+
|
|
107
|
+
if (masked && maskKey) {
|
|
108
|
+
payload = Buffer.from(payload)
|
|
109
|
+
for (let i = 0; i < payload.length; i++) {
|
|
110
|
+
payload[i] ^= maskKey[i % 4]
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
fin,
|
|
116
|
+
opcode,
|
|
117
|
+
payload,
|
|
118
|
+
totalLength: offset + payloadLength
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
#handleFrame(frame) {
|
|
123
|
+
switch (frame.opcode) {
|
|
124
|
+
case OPCODE.TEXT:
|
|
125
|
+
this.#handleMessage(frame.payload.toString('utf8'))
|
|
126
|
+
break
|
|
127
|
+
case OPCODE.BINARY:
|
|
128
|
+
this.#handleMessage(frame.payload)
|
|
129
|
+
break
|
|
130
|
+
case OPCODE.PING:
|
|
131
|
+
this.#sendFrame(OPCODE.PONG, frame.payload)
|
|
132
|
+
break
|
|
133
|
+
case OPCODE.PONG:
|
|
134
|
+
this.#emit('pong')
|
|
135
|
+
break
|
|
136
|
+
case OPCODE.CLOSE:
|
|
137
|
+
this.close()
|
|
138
|
+
break
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
#handleMessage(data) {
|
|
143
|
+
try {
|
|
144
|
+
const parsed = JSON.parse(data)
|
|
145
|
+
this.#emit('message', parsed)
|
|
146
|
+
} catch {
|
|
147
|
+
this.#emit('message', data)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
#handleClose() {
|
|
152
|
+
if (this.#closed) return
|
|
153
|
+
this.#closed = true
|
|
154
|
+
|
|
155
|
+
for (const room of this.#rooms) {
|
|
156
|
+
this.#server.leaveRoom(this.#id, room)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
this.#emit('close')
|
|
160
|
+
this.#server.removeClient(this.#id)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
#sendFrame(opcode, data) {
|
|
164
|
+
if (this.#closed) return
|
|
165
|
+
|
|
166
|
+
const payload = Buffer.isBuffer(data) ? data : Buffer.from(data)
|
|
167
|
+
const length = payload.length
|
|
168
|
+
|
|
169
|
+
let header
|
|
170
|
+
if (length < 126) {
|
|
171
|
+
header = Buffer.alloc(2)
|
|
172
|
+
header[0] = 0x80 | opcode
|
|
173
|
+
header[1] = length
|
|
174
|
+
} else if (length < 65536) {
|
|
175
|
+
header = Buffer.alloc(4)
|
|
176
|
+
header[0] = 0x80 | opcode
|
|
177
|
+
header[1] = 126
|
|
178
|
+
header.writeUInt16BE(length, 2)
|
|
179
|
+
} else {
|
|
180
|
+
header = Buffer.alloc(10)
|
|
181
|
+
header[0] = 0x80 | opcode
|
|
182
|
+
header[1] = 127
|
|
183
|
+
header.writeBigUInt64BE(BigInt(length), 2)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
this.#socket.write(Buffer.concat([header, payload]))
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
#emit(event, ...args) {
|
|
190
|
+
if (this.#handlers[event]) {
|
|
191
|
+
for (const handler of this.#handlers[event]) {
|
|
192
|
+
handler(...args)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
on(event, handler) {
|
|
198
|
+
if (!this.#handlers[event]) this.#handlers[event] = []
|
|
199
|
+
this.#handlers[event].push(handler)
|
|
200
|
+
return this
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
off(event, handler) {
|
|
204
|
+
if (!this.#handlers[event]) return this
|
|
205
|
+
if (handler) {
|
|
206
|
+
this.#handlers[event] = this.#handlers[event].filter(h => h !== handler)
|
|
207
|
+
} else {
|
|
208
|
+
delete this.#handlers[event]
|
|
209
|
+
}
|
|
210
|
+
return this
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
send(data) {
|
|
214
|
+
if (this.#closed) return this
|
|
215
|
+
const payload = typeof data === 'object' ? JSON.stringify(data) : String(data)
|
|
216
|
+
this.#sendFrame(OPCODE.TEXT, payload)
|
|
217
|
+
return this
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
sendBinary(data) {
|
|
221
|
+
if (this.#closed) return this
|
|
222
|
+
this.#sendFrame(OPCODE.BINARY, data)
|
|
223
|
+
return this
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
ping() {
|
|
227
|
+
this.#sendFrame(OPCODE.PING, Buffer.alloc(0))
|
|
228
|
+
return this
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
close(code = 1000, reason = '') {
|
|
232
|
+
if (this.#closed) return
|
|
233
|
+
this.#closed = true
|
|
234
|
+
|
|
235
|
+
const reasonBuffer = Buffer.from(reason)
|
|
236
|
+
const payload = Buffer.alloc(2 + reasonBuffer.length)
|
|
237
|
+
payload.writeUInt16BE(code, 0)
|
|
238
|
+
reasonBuffer.copy(payload, 2)
|
|
239
|
+
|
|
240
|
+
this.#sendFrame(OPCODE.CLOSE, payload)
|
|
241
|
+
this.#socket.end()
|
|
242
|
+
|
|
243
|
+
for (const room of this.#rooms) {
|
|
244
|
+
this.#server.leaveRoom(this.#id, room)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
this.#emit('close')
|
|
248
|
+
this.#server.removeClient(this.#id)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
join(room) {
|
|
252
|
+
this.#rooms.add(room)
|
|
253
|
+
this.#server.joinRoom(this.#id, room)
|
|
254
|
+
return this
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
leave(room) {
|
|
258
|
+
this.#rooms.delete(room)
|
|
259
|
+
this.#server.leaveRoom(this.#id, room)
|
|
260
|
+
return this
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
to(room) {
|
|
264
|
+
return {
|
|
265
|
+
send: data => this.#server.toRoom(room, data),
|
|
266
|
+
sendBinary: data => this.#server.toRoomBinary(room, data)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
broadcast(data, includesSelf = false) {
|
|
271
|
+
this.#server.broadcast(data, includesSelf ? null : this.#id)
|
|
272
|
+
return this
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
class WebSocketServer {
|
|
277
|
+
#clients = new Map()
|
|
278
|
+
#rooms = new Map()
|
|
279
|
+
#routes = new Map()
|
|
280
|
+
|
|
281
|
+
route(path, handler) {
|
|
282
|
+
this.#routes.set(path, handler)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
getRoute(path) {
|
|
286
|
+
if (this.#routes.has(path)) return this.#routes.get(path)
|
|
287
|
+
|
|
288
|
+
for (const [pattern, handler] of this.#routes) {
|
|
289
|
+
if (!pattern.includes('{')) continue
|
|
290
|
+
const regex = new RegExp('^' + pattern.replace(/\{[^}]+\}/g, '([^/]+)') + '$')
|
|
291
|
+
const match = path.match(regex)
|
|
292
|
+
if (match) {
|
|
293
|
+
const params = {}
|
|
294
|
+
const paramNames = pattern.match(/\{([^}]+)\}/g) || []
|
|
295
|
+
paramNames.forEach((name, i) => {
|
|
296
|
+
params[name.slice(1, -1)] = match[i + 1]
|
|
297
|
+
})
|
|
298
|
+
return {handler, params}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return null
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
handleUpgrade(req, socket, head, Odac) {
|
|
305
|
+
const path = req.url.split('?')[0]
|
|
306
|
+
const routeInfo = this.getRoute(path)
|
|
307
|
+
|
|
308
|
+
if (!routeInfo) {
|
|
309
|
+
socket.write('HTTP/1.1 404 Not Found\r\n\r\n')
|
|
310
|
+
socket.destroy()
|
|
311
|
+
return
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const handler = typeof routeInfo === 'function' ? routeInfo : routeInfo.handler
|
|
315
|
+
const params = routeInfo.params || {}
|
|
316
|
+
|
|
317
|
+
const key = req.headers['sec-websocket-key']
|
|
318
|
+
if (!key) {
|
|
319
|
+
socket.write('HTTP/1.1 400 Bad Request\r\n\r\n')
|
|
320
|
+
socket.destroy()
|
|
321
|
+
return
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const acceptKey = nodeCrypto
|
|
325
|
+
.createHash('sha1')
|
|
326
|
+
.update(key + WS_GUID)
|
|
327
|
+
.digest('base64')
|
|
328
|
+
|
|
329
|
+
const responseHeaders = [
|
|
330
|
+
'HTTP/1.1 101 Switching Protocols',
|
|
331
|
+
'Upgrade: websocket',
|
|
332
|
+
'Connection: Upgrade',
|
|
333
|
+
`Sec-WebSocket-Accept: ${acceptKey}`,
|
|
334
|
+
'',
|
|
335
|
+
''
|
|
336
|
+
]
|
|
337
|
+
|
|
338
|
+
socket.write(responseHeaders.join('\r\n'))
|
|
339
|
+
|
|
340
|
+
const clientId = nodeCrypto.randomUUID()
|
|
341
|
+
const client = new WebSocketClient(socket, this, clientId)
|
|
342
|
+
this.#clients.set(clientId, client)
|
|
343
|
+
|
|
344
|
+
if (params) {
|
|
345
|
+
for (const [k, v] of Object.entries(params)) {
|
|
346
|
+
Odac.Request.data.url[k] = v
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (Odac.Request && req.headers) {
|
|
351
|
+
Odac.Request._wsHeaders = req.headers
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
handler(client, Odac)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
removeClient(id) {
|
|
358
|
+
this.#clients.delete(id)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
joinRoom(clientId, room) {
|
|
362
|
+
if (!this.#rooms.has(room)) this.#rooms.set(room, new Set())
|
|
363
|
+
this.#rooms.get(room).add(clientId)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
leaveRoom(clientId, room) {
|
|
367
|
+
if (!this.#rooms.has(room)) return
|
|
368
|
+
this.#rooms.get(room).delete(clientId)
|
|
369
|
+
if (this.#rooms.get(room).size === 0) this.#rooms.delete(room)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
toRoom(room, data) {
|
|
373
|
+
if (!this.#rooms.has(room)) return
|
|
374
|
+
for (const clientId of this.#rooms.get(room)) {
|
|
375
|
+
const client = this.#clients.get(clientId)
|
|
376
|
+
if (client) client.send(data)
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
toRoomBinary(room, data) {
|
|
381
|
+
if (!this.#rooms.has(room)) return
|
|
382
|
+
for (const clientId of this.#rooms.get(room)) {
|
|
383
|
+
const client = this.#clients.get(clientId)
|
|
384
|
+
if (client) client.sendBinary(data)
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
broadcast(data, excludeId = null) {
|
|
389
|
+
for (const [id, client] of this.#clients) {
|
|
390
|
+
if (id !== excludeId) client.send(data)
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
get clients() {
|
|
395
|
+
return this.#clients
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
get clientCount() {
|
|
399
|
+
return this.#clients.size
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
module.exports = {WebSocketServer, WebSocketClient}
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* About Page Controller
|
|
3
3
|
*
|
|
4
|
-
* This controller renders the about page using
|
|
5
|
-
* Provides information about
|
|
4
|
+
* This controller renders the about page using Odac's skeleton-based view system.
|
|
5
|
+
* Provides information about Odac and its key components.
|
|
6
6
|
*
|
|
7
7
|
* For AJAX requests, only content is returned. For full page loads, skeleton + content.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
module.exports = function (
|
|
10
|
+
module.exports = function (Odac) {
|
|
11
11
|
// Set variables for AJAX responses
|
|
12
|
-
|
|
12
|
+
Odac.set(
|
|
13
13
|
{
|
|
14
|
-
pageTitle: 'About
|
|
14
|
+
pageTitle: 'About Odac',
|
|
15
15
|
version: '1.0.0'
|
|
16
16
|
},
|
|
17
17
|
true
|
|
18
18
|
)
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
Odac.View.set({
|
|
21
21
|
skeleton: 'main',
|
|
22
22
|
head: 'main',
|
|
23
23
|
header: 'main',
|
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Home Page Controller
|
|
3
3
|
*
|
|
4
|
-
* This controller renders the home page using
|
|
4
|
+
* This controller renders the home page using Odac's skeleton-based view system.
|
|
5
5
|
* The skeleton provides the layout (header, nav, footer) and the view provides the content.
|
|
6
6
|
*
|
|
7
|
-
* For AJAX requests (
|
|
7
|
+
* For AJAX requests (odac-link navigation), only the content is returned.
|
|
8
8
|
* For full page loads, skeleton + content is returned.
|
|
9
9
|
*
|
|
10
10
|
* This page demonstrates:
|
|
11
11
|
* - Modern, responsive design
|
|
12
|
-
* -
|
|
13
|
-
* -
|
|
14
|
-
* - Dynamic page loading with
|
|
12
|
+
* - odac.js AJAX form handling
|
|
13
|
+
* - odac.js GET requests
|
|
14
|
+
* - Dynamic page loading with odac-link
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
module.exports = function (
|
|
17
|
+
module.exports = function (Odac) {
|
|
18
18
|
// Set variables that will be available in AJAX responses
|
|
19
|
-
|
|
19
|
+
Odac.set(
|
|
20
20
|
{
|
|
21
|
-
welcomeMessage: 'Welcome to
|
|
21
|
+
welcomeMessage: 'Welcome to Odac!',
|
|
22
22
|
timestamp: Date.now()
|
|
23
23
|
},
|
|
24
24
|
true
|
|
25
25
|
) // true = include in AJAX responses
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
Odac.View.set({
|
|
28
28
|
skeleton: 'main',
|
|
29
29
|
head: 'main',
|
|
30
30
|
header: 'main',
|
|
@@ -3,16 +3,15 @@
|
|
|
3
3
|
"version": "1.0.0",
|
|
4
4
|
"description": "Website for {{domain_original}}",
|
|
5
5
|
"scripts": {
|
|
6
|
-
"
|
|
7
|
-
"test": "echo \"Error: no test specified\" && exit 1"
|
|
6
|
+
"dev": "odac dev"
|
|
8
7
|
},
|
|
9
8
|
"dependencies": {
|
|
10
|
-
"
|
|
9
|
+
"odac": "*"
|
|
11
10
|
},
|
|
12
11
|
"keywords": [
|
|
13
|
-
"
|
|
12
|
+
"odac",
|
|
14
13
|
"website"
|
|
15
14
|
],
|
|
16
15
|
"author": "",
|
|
17
16
|
"license": "ISC"
|
|
18
|
-
}
|
|
17
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* ============================================
|
|
2
|
-
|
|
2
|
+
Odac Template Stylesheet
|
|
3
3
|
Modern, responsive design with smooth transitions
|
|
4
4
|
============================================ */
|
|
5
5
|
|
|
@@ -536,7 +536,7 @@ textarea.error {
|
|
|
536
536
|
display: block;
|
|
537
537
|
}
|
|
538
538
|
|
|
539
|
-
[
|
|
539
|
+
[odac-form-success] {
|
|
540
540
|
background-color: var(--accent);
|
|
541
541
|
color: var(--white);
|
|
542
542
|
padding: var(--spacing-sm);
|
|
@@ -545,7 +545,7 @@ textarea.error {
|
|
|
545
545
|
display: none;
|
|
546
546
|
}
|
|
547
547
|
|
|
548
|
-
[
|
|
548
|
+
[odac-form-success]:not(:empty) {
|
|
549
549
|
display: block;
|
|
550
550
|
}
|
|
551
551
|
|
|
@@ -1333,7 +1333,7 @@ footer a:hover {
|
|
|
1333
1333
|
}
|
|
1334
1334
|
|
|
1335
1335
|
/* Smooth page transitions */
|
|
1336
|
-
[
|
|
1336
|
+
[odac-page] {
|
|
1337
1337
|
animation: fadeIn 0.4s ease-out;
|
|
1338
1338
|
}
|
|
1339
1339
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Odac Template - Client-Side Application
|
|
3
3
|
*
|
|
4
|
-
* This file demonstrates
|
|
5
|
-
* - AJAX page loading with
|
|
4
|
+
* This file demonstrates odac.js features including:
|
|
5
|
+
* - AJAX page loading with odac.loader() for smooth navigation
|
|
6
6
|
* - History API integration
|
|
7
7
|
* - Event delegation
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
odac.action({
|
|
11
11
|
/**
|
|
12
12
|
* AJAX Navigation
|
|
13
13
|
* Enables smooth page transitions without full page reloads
|
|
@@ -22,7 +22,7 @@ Candy.action({
|
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Custom functions
|
|
25
|
-
* These become available as
|
|
25
|
+
* These become available as odac.fn.functionName()
|
|
26
26
|
*/
|
|
27
27
|
function: {
|
|
28
28
|
/**
|
|
@@ -56,7 +56,7 @@ Candy.action({
|
|
|
56
56
|
*/
|
|
57
57
|
load: function () {
|
|
58
58
|
// Set initial active navigation state
|
|
59
|
-
|
|
59
|
+
odac.fn.updateActiveNav(window.location.pathname)
|
|
60
60
|
},
|
|
61
61
|
|
|
62
62
|
/**
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
// ============================================
|
|
2
2
|
// Page Routes
|
|
3
3
|
// ============================================
|
|
4
|
-
// Page routes render HTML views and support AJAX loading via
|
|
4
|
+
// Page routes render HTML views and support AJAX loading via odac.js
|
|
5
5
|
// Controllers are located in controller/page/ directory
|
|
6
6
|
|
|
7
7
|
// Home page - displays welcome message, features, and interactive demos
|
|
8
|
-
|
|
8
|
+
Odac.Route.page('/', 'index')
|
|
9
9
|
|
|
10
|
-
// About page - provides information about
|
|
11
|
-
|
|
10
|
+
// About page - provides information about Odac
|
|
11
|
+
Odac.Route.page('/about', 'about')
|
|
12
12
|
|
|
13
13
|
// ============================================
|
|
14
14
|
// API Routes
|
|
15
15
|
// ============================================
|
|
16
16
|
// Add your API routes here
|
|
17
17
|
// Example:
|
|
18
|
-
//
|
|
19
|
-
//
|
|
18
|
+
// Odac.Route.post('/api/contact', 'contact')
|
|
19
|
+
// Odac.Route.get('/api/data', 'data')
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
<div class="container">
|
|
2
2
|
<section class="page-hero">
|
|
3
3
|
<h1>🍭 About This Template</h1>
|
|
4
|
-
<p class="lead">This is a starter template for your
|
|
4
|
+
<p class="lead">This is a starter template for your Odac website. Customize it to build your own application.</p>
|
|
5
5
|
</section>
|
|
6
6
|
|
|
7
7
|
<section class="content-section">
|
|
8
8
|
<h2>What You Can Build</h2>
|
|
9
9
|
<p>
|
|
10
|
-
With
|
|
10
|
+
With Odac, you can build any type of web application - from simple websites to complex web apps.
|
|
11
11
|
This template provides a solid foundation with modern features and best practices already configured.
|
|
12
12
|
</p>
|
|
13
13
|
</section>
|
|
@@ -51,13 +51,13 @@
|
|
|
51
51
|
<section class="content-section">
|
|
52
52
|
<h2>Learn More</h2>
|
|
53
53
|
<div class="links-grid">
|
|
54
|
-
<a href="https://docs.
|
|
54
|
+
<a href="https://docs.odac.run" class="link-card" target="_blank" data-navigate="false">
|
|
55
55
|
<h3>📚 Documentation</h3>
|
|
56
56
|
<p>Complete guides, API reference, and tutorials</p>
|
|
57
57
|
</a>
|
|
58
58
|
|
|
59
|
-
<a href="https://
|
|
60
|
-
<h3>🌐
|
|
59
|
+
<a href="https://odac.run" class="link-card" target="_blank" data-navigate="false">
|
|
60
|
+
<h3>🌐 odac.run</h3>
|
|
61
61
|
<p>Official website with examples and community</p>
|
|
62
62
|
</a>
|
|
63
63
|
</div>
|