api-ape 3.0.1 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/README.md +58 -570
  2. package/client/README.md +73 -14
  3. package/client/auth/crypto/aead.js +214 -0
  4. package/client/auth/crypto/constants.js +32 -0
  5. package/client/auth/crypto/encoding.js +104 -0
  6. package/client/auth/crypto/files.md +27 -0
  7. package/client/auth/crypto/kdf.js +217 -0
  8. package/client/auth/crypto-utils.js +118 -0
  9. package/client/auth/files.md +52 -0
  10. package/client/auth/key-recovery.js +288 -0
  11. package/client/auth/recovery/constants.js +37 -0
  12. package/client/auth/recovery/files.md +23 -0
  13. package/client/auth/recovery/key-derivation.js +61 -0
  14. package/client/auth/recovery/sss-browser.js +189 -0
  15. package/client/auth/share-storage.js +205 -0
  16. package/client/auth/storage/constants.js +18 -0
  17. package/client/auth/storage/db.js +132 -0
  18. package/client/auth/storage/files.md +27 -0
  19. package/client/auth/storage/keys.js +173 -0
  20. package/client/auth/storage/shares.js +200 -0
  21. package/client/browser.js +190 -23
  22. package/client/connectSocket.js +418 -988
  23. package/client/connection/README.md +23 -0
  24. package/client/connection/fileDownload.js +256 -0
  25. package/client/connection/fileHandling.js +450 -0
  26. package/client/connection/fileUtils.js +346 -0
  27. package/client/connection/files.md +71 -0
  28. package/client/connection/messageHandler.js +105 -0
  29. package/client/connection/network.js +350 -0
  30. package/client/connection/proxy.js +233 -0
  31. package/client/connection/sender.js +333 -0
  32. package/client/connection/state.js +321 -0
  33. package/client/connection/subscriptions.js +151 -0
  34. package/client/files.md +53 -0
  35. package/client/index.js +298 -142
  36. package/client/transports/README.md +50 -0
  37. package/client/transports/files.md +41 -0
  38. package/client/transports/streamParser.js +195 -0
  39. package/client/transports/streaming.js +555 -202
  40. package/dist/ape.js +6 -1
  41. package/dist/ape.js.map +4 -4
  42. package/index.d.ts +38 -16
  43. package/package.json +32 -7
  44. package/server/README.md +287 -53
  45. package/server/adapters/README.md +28 -19
  46. package/server/adapters/files.md +68 -0
  47. package/server/adapters/firebase.js +543 -160
  48. package/server/adapters/index.js +362 -112
  49. package/server/adapters/mongo.js +530 -140
  50. package/server/adapters/postgres.js +534 -155
  51. package/server/adapters/redis.js +508 -143
  52. package/server/adapters/supabase.js +555 -186
  53. package/server/client/README.md +43 -0
  54. package/server/client/connection.js +586 -0
  55. package/server/client/files.md +40 -0
  56. package/server/client/index.js +342 -0
  57. package/server/files.md +54 -0
  58. package/server/index.js +332 -27
  59. package/server/lib/README.md +26 -0
  60. package/server/lib/broadcast/clients.js +219 -0
  61. package/server/lib/broadcast/files.md +58 -0
  62. package/server/lib/broadcast/index.js +57 -0
  63. package/server/lib/broadcast/publishProxy.js +110 -0
  64. package/server/lib/broadcast/pubsub.js +137 -0
  65. package/server/lib/broadcast/sendProxy.js +103 -0
  66. package/server/lib/bun.js +315 -99
  67. package/server/lib/fileTransfer/README.md +63 -0
  68. package/server/lib/fileTransfer/files.md +30 -0
  69. package/server/lib/fileTransfer/streaming.js +435 -0
  70. package/server/lib/fileTransfer.js +710 -326
  71. package/server/lib/files.md +111 -0
  72. package/server/lib/httpUtils.js +283 -0
  73. package/server/lib/loader.js +208 -7
  74. package/server/lib/longPolling/README.md +63 -0
  75. package/server/lib/longPolling/files.md +44 -0
  76. package/server/lib/longPolling/getHandler.js +365 -0
  77. package/server/lib/longPolling/postHandler.js +327 -0
  78. package/server/lib/longPolling.js +174 -221
  79. package/server/lib/main.js +369 -532
  80. package/server/lib/runtimes/README.md +42 -0
  81. package/server/lib/runtimes/bun.js +586 -0
  82. package/server/lib/runtimes/files.md +56 -0
  83. package/server/lib/runtimes/node.js +511 -0
  84. package/server/lib/wiring.js +539 -98
  85. package/server/lib/ws/README.md +35 -0
  86. package/server/lib/ws/adapters/README.md +54 -0
  87. package/server/lib/ws/adapters/bun.js +538 -170
  88. package/server/lib/ws/adapters/deno.js +623 -149
  89. package/server/lib/ws/adapters/files.md +42 -0
  90. package/server/lib/ws/files.md +74 -0
  91. package/server/lib/ws/frames.js +532 -154
  92. package/server/lib/ws/index.js +207 -10
  93. package/server/lib/ws/server.js +385 -92
  94. package/server/lib/ws/socket.js +549 -181
  95. package/server/lib/wsProvider.js +363 -89
  96. package/server/plugins/binary.js +282 -0
  97. package/server/security/README.md +92 -0
  98. package/server/security/auth/README.md +319 -0
  99. package/server/security/auth/adapters/files.md +95 -0
  100. package/server/security/auth/adapters/ldap/constants.js +37 -0
  101. package/server/security/auth/adapters/ldap/files.md +19 -0
  102. package/server/security/auth/adapters/ldap/helpers.js +111 -0
  103. package/server/security/auth/adapters/ldap.js +353 -0
  104. package/server/security/auth/adapters/oauth2/constants.js +41 -0
  105. package/server/security/auth/adapters/oauth2/files.md +19 -0
  106. package/server/security/auth/adapters/oauth2/helpers.js +123 -0
  107. package/server/security/auth/adapters/oauth2.js +273 -0
  108. package/server/security/auth/adapters/opaque-handlers.js +314 -0
  109. package/server/security/auth/adapters/opaque.js +205 -0
  110. package/server/security/auth/adapters/saml/constants.js +52 -0
  111. package/server/security/auth/adapters/saml/files.md +19 -0
  112. package/server/security/auth/adapters/saml/helpers.js +74 -0
  113. package/server/security/auth/adapters/saml.js +173 -0
  114. package/server/security/auth/adapters/totp.js +703 -0
  115. package/server/security/auth/adapters/webauthn.js +625 -0
  116. package/server/security/auth/files.md +61 -0
  117. package/server/security/auth/framework/constants.js +27 -0
  118. package/server/security/auth/framework/files.md +23 -0
  119. package/server/security/auth/framework/handlers.js +272 -0
  120. package/server/security/auth/framework/socket-auth.js +177 -0
  121. package/server/security/auth/handlers/auth-messages.js +143 -0
  122. package/server/security/auth/handlers/files.md +28 -0
  123. package/server/security/auth/index.js +290 -0
  124. package/server/security/auth/mfa/crypto/aead.js +148 -0
  125. package/server/security/auth/mfa/crypto/constants.js +35 -0
  126. package/server/security/auth/mfa/crypto/files.md +27 -0
  127. package/server/security/auth/mfa/crypto/kdf.js +120 -0
  128. package/server/security/auth/mfa/crypto/utils.js +68 -0
  129. package/server/security/auth/mfa/crypto-utils.js +80 -0
  130. package/server/security/auth/mfa/files.md +77 -0
  131. package/server/security/auth/mfa/ledger/constants.js +75 -0
  132. package/server/security/auth/mfa/ledger/errors.js +73 -0
  133. package/server/security/auth/mfa/ledger/files.md +23 -0
  134. package/server/security/auth/mfa/ledger/share-record.js +32 -0
  135. package/server/security/auth/mfa/ledger.js +255 -0
  136. package/server/security/auth/mfa/recovery/constants.js +67 -0
  137. package/server/security/auth/mfa/recovery/files.md +19 -0
  138. package/server/security/auth/mfa/recovery/handlers.js +216 -0
  139. package/server/security/auth/mfa/recovery.js +191 -0
  140. package/server/security/auth/mfa/sss/constants.js +21 -0
  141. package/server/security/auth/mfa/sss/files.md +23 -0
  142. package/server/security/auth/mfa/sss/gf256.js +103 -0
  143. package/server/security/auth/mfa/sss/serialization.js +82 -0
  144. package/server/security/auth/mfa/sss.js +161 -0
  145. package/server/security/auth/mfa/two-of-three/constants.js +58 -0
  146. package/server/security/auth/mfa/two-of-three/files.md +23 -0
  147. package/server/security/auth/mfa/two-of-three/handlers.js +241 -0
  148. package/server/security/auth/mfa/two-of-three/helpers.js +71 -0
  149. package/server/security/auth/mfa/two-of-three.js +136 -0
  150. package/server/security/auth/nonce-manager.js +89 -0
  151. package/server/security/auth/state-machine-mfa.js +269 -0
  152. package/server/security/auth/state-machine.js +257 -0
  153. package/server/security/extractRootDomain.js +144 -16
  154. package/server/security/files.md +51 -0
  155. package/server/security/origin.js +197 -15
  156. package/server/security/reply.js +274 -16
  157. package/server/socket/README.md +119 -0
  158. package/server/socket/authMiddleware.js +299 -0
  159. package/server/socket/files.md +86 -0
  160. package/server/socket/open.js +154 -8
  161. package/server/socket/pluginHooks.js +334 -0
  162. package/server/socket/receive.js +184 -225
  163. package/server/socket/receiveContext.js +117 -0
  164. package/server/socket/send.js +416 -78
  165. package/server/socket/tagUtils.js +402 -0
  166. package/server/utils/README.md +19 -0
  167. package/server/utils/deepRequire.js +255 -30
  168. package/server/utils/files.md +57 -0
  169. package/server/utils/genId.js +182 -20
  170. package/server/utils/parseUserAgent.js +313 -251
  171. package/server/utils/userAgent/README.md +65 -0
  172. package/server/utils/userAgent/files.md +46 -0
  173. package/server/utils/userAgent/patterns.js +545 -0
  174. package/utils/README.md +21 -0
  175. package/utils/files.md +66 -0
  176. package/utils/jss/README.md +21 -0
  177. package/utils/jss/decode.js +471 -0
  178. package/utils/jss/encode.js +312 -0
  179. package/utils/jss/files.md +68 -0
  180. package/utils/jss/plugins.js +210 -0
  181. package/utils/jss.js +219 -273
  182. package/utils/messageHash.js +238 -35
  183. package/dist/api-ape.min.js +0 -2
  184. package/dist/api-ape.min.js.map +0 -7
  185. package/server/client.js +0 -308
  186. package/server/lib/broadcast.js +0 -146
@@ -1,186 +1,660 @@
1
1
  /**
2
- * Deno Native WebSocket Adapter
3
- * Wraps Deno's native WebSocket to be compatible with the ws library API
4
- *
5
- * Deno uses a different pattern:
6
- * - Deno.serve() with Deno.upgradeWebSocket(req)
7
- * - Returns { socket, response } where socket is a standard WebSocket
8
- *
9
- * This adapter provides a ws-compatible WebSocketServer that works
10
- * with the existing api-ape code.
2
+ * @fileoverview Deno Native WebSocket Adapter
3
+ *
4
+ * This module provides adapter classes that wrap Deno's native WebSocket
5
+ * implementation to be compatible with the `ws` library API. This enables
6
+ * api-ape to work seamlessly with Deno's runtime.
7
+ *
8
+ * ## Why an Adapter?
9
+ *
10
+ * Deno has a unique WebSocket API compared to Node.js:
11
+ * - Uses `Deno.upgradeWebSocket(req)` which returns `{ socket, response }`
12
+ * - WebSocket uses `onmessage`, `onclose`, `onerror` properties instead of EventEmitter
13
+ * - The upgrade process returns a Response that must be returned from the handler
14
+ *
15
+ * This adapter bridges these differences by:
16
+ * - Wrapping Deno's WebSocket in an EventEmitter-based interface
17
+ * - Providing `ws`-compatible methods (`send`, `close`, `readyState`)
18
+ * - Handling the upgrade response flow that Deno requires
19
+ *
20
+ * ## Architecture
21
+ *
22
+ * ```
23
+ * Deno.serve() ──> DenoWebSocketServer ──> DenoWebSocket
24
+ * │ │ │
25
+ * │ Request │ handleUpgrade() │ EventEmitter
26
+ * │ Response │ clients Set │ send(), close()
27
+ * │ │ │ readyState
28
+ * ```
29
+ *
30
+ * ## Usage with Deno
31
+ *
32
+ * ```javascript
33
+ * const { DenoWebSocketServer } = require('./ws/adapters/deno')
34
+ *
35
+ * const wss = new DenoWebSocketServer({ noServer: true })
36
+ *
37
+ * wss.on('connection', (ws, req) => {
38
+ * ws.on('message', (data) => console.log('Received:', data))
39
+ * ws.send('Hello from Deno!')
40
+ * })
41
+ *
42
+ * Deno.serve((req) => {
43
+ * if (new URL(req.url).pathname === '/ws') {
44
+ * const result = wss.handleUpgrade(req)
45
+ * return result?.response || new Response('Upgrade failed', { status: 500 })
46
+ * }
47
+ * return new Response('Hello')
48
+ * })
49
+ * ```
50
+ *
51
+ * @module server/lib/ws/adapters/deno
52
+ * @see {@link module:server/lib/ws} - Polyfill WebSocket implementation
53
+ * @see {@link module:server/lib/ws/adapters/bun} - Similar adapter for Bun runtime
54
+ *
55
+ * @example
56
+ * // Basic Deno server with WebSocket
57
+ * const { DenoWebSocketServer } = require('./ws/adapters/deno')
58
+ *
59
+ * const wss = new DenoWebSocketServer({ noServer: true })
60
+ *
61
+ * wss.on('connection', (ws) => {
62
+ * console.log('Client connected')
63
+ *
64
+ * ws.on('message', (data) => {
65
+ * console.log('Message:', data.toString())
66
+ * ws.send('Echo: ' + data.toString())
67
+ * })
68
+ *
69
+ * ws.on('close', () => {
70
+ * console.log('Client disconnected')
71
+ * })
72
+ * })
73
+ *
74
+ * @example
75
+ * // Broadcasting to all connected clients
76
+ * function broadcast(message) {
77
+ * for (const client of wss.clients) {
78
+ * if (client.readyState === client.OPEN) {
79
+ * client.send(message)
80
+ * }
81
+ * }
82
+ * }
11
83
  */
12
84
 
13
- const { EventEmitter } = require('events')
85
+ const { EventEmitter } = require("events");
14
86
 
15
- // WebSocket ready states (matching ws library)
87
+ /**
88
+ * WebSocket ready state constants matching the W3C WebSocket API.
89
+ *
90
+ * These match the standard WebSocket readyState values and are
91
+ * compatible with both the browser WebSocket API and the ws library.
92
+ *
93
+ * @readonly
94
+ * @enum {number}
95
+ * @private
96
+ */
16
97
  const READY_STATES = {
17
- CONNECTING: 0,
18
- OPEN: 1,
19
- CLOSING: 2,
20
- CLOSED: 3
21
- }
98
+ /** Connection is being established (0) */
99
+ CONNECTING: 0,
100
+ /** Connection is open and ready for communication (1) */
101
+ OPEN: 1,
102
+ /** Connection is in the process of closing (2) */
103
+ CLOSING: 2,
104
+ /** Connection has been closed (3) */
105
+ CLOSED: 3,
106
+ };
22
107
 
23
108
  /**
24
- * Wrapper around Deno's native WebSocket to provide ws-compatible API
25
- * Deno's WebSocket uses onmessage/onclose properties instead of EventEmitter
109
+ * Wrapper around Deno's native WebSocket to provide ws-compatible API.
110
+ *
111
+ * Deno's WebSocket uses property-based event handlers (`onmessage`, `onclose`,
112
+ * `onerror`) instead of Node.js EventEmitter pattern. This class bridges
113
+ * that difference by:
114
+ * - Extending EventEmitter for event-based API
115
+ * - Wiring Deno's property handlers to emit events
116
+ * - Converting message data to Buffer for consistency
117
+ *
118
+ * ## Events
119
+ *
120
+ * | Event | Arguments | Description |
121
+ * |-----------|---------------------|--------------------------------|
122
+ * | `message` | `(data: Buffer)` | Received a message |
123
+ * | `close` | `(code?, reason?)` | Connection was closed |
124
+ * | `error` | `(event)` | An error occurred |
125
+ *
126
+ * ## Data Handling
127
+ *
128
+ * All incoming messages are converted to Buffer for consistency with
129
+ * the ws library. String messages are encoded as UTF-8 Buffers.
130
+ *
131
+ * When sending, Buffers are converted to ArrayBuffer (which Deno's
132
+ * WebSocket expects), while strings are passed through directly.
133
+ *
134
+ * @class DenoWebSocket
135
+ * @extends EventEmitter
136
+ *
137
+ * @param {WebSocket} denoSocket - Deno's native WebSocket instance
138
+ *
139
+ * @example
140
+ * // Creating a wrapper (done by DenoWebSocketServer)
141
+ * const { socket } = Deno.upgradeWebSocket(req)
142
+ * const ws = new DenoWebSocket(socket)
143
+ *
144
+ * ws.on('message', (data) => {
145
+ * console.log('Received:', data.toString())
146
+ * })
147
+ *
148
+ * ws.send('Hello!')
149
+ *
150
+ * @example
151
+ * // Check ready state before sending
152
+ * if (ws.readyState === ws.OPEN) {
153
+ * ws.send(JSON.stringify({ type: 'ping' }))
154
+ * }
155
+ *
156
+ * @example
157
+ * // Handle connection close
158
+ * ws.on('close', (code, reason) => {
159
+ * console.log(`Connection closed: ${code} - ${reason}`)
160
+ * })
26
161
  */
27
162
  class DenoWebSocket extends EventEmitter {
28
- constructor(denoSocket) {
29
- super()
30
- this._socket = denoSocket
31
- this._readyState = READY_STATES.OPEN
32
-
33
- // Define constants on instance (matching ws library API)
34
- this.CONNECTING = READY_STATES.CONNECTING
35
- this.OPEN = READY_STATES.OPEN
36
- this.CLOSING = READY_STATES.CLOSING
37
- this.CLOSED = READY_STATES.CLOSED
38
-
39
- // Wire up Deno's event properties to our EventEmitter
40
- this._setupDenoEvents()
41
- }
163
+ /**
164
+ * Create a new DenoWebSocket wrapper.
165
+ *
166
+ * Sets up the wrapper around Deno's native WebSocket and wires
167
+ * up the event handlers to emit EventEmitter events.
168
+ *
169
+ * @param {WebSocket} denoSocket - Deno's native WebSocket instance
170
+ * (from Deno.upgradeWebSocket())
171
+ */
172
+ constructor(denoSocket) {
173
+ super();
42
174
 
43
- get readyState() {
44
- return this._readyState
45
- }
175
+ /**
176
+ * The underlying Deno WebSocket instance.
177
+ * @type {WebSocket}
178
+ * @private
179
+ */
180
+ this._socket = denoSocket;
46
181
 
47
182
  /**
48
- * Setup Deno WebSocket event handlers
49
- * @internal
183
+ * Current connection state.
184
+ * @type {number}
185
+ * @private
50
186
  */
51
- _setupDenoEvents() {
52
- this._socket.onmessage = (event) => {
53
- // Convert to Buffer for consistency with ws library
54
- const data = event.data
55
- const buffer = typeof data === 'string'
56
- ? Buffer.from(data)
57
- : Buffer.from(data)
58
- this.emit('message', buffer)
59
- }
60
-
61
- this._socket.onclose = (event) => {
62
- this._readyState = READY_STATES.CLOSED
63
- this.emit('close', event.code, event.reason)
64
- }
65
-
66
- this._socket.onerror = (event) => {
67
- this.emit('error', event)
68
- }
69
- }
187
+ this._readyState = READY_STATES.OPEN;
188
+
189
+ // Expose ready states as instance properties for convenience
190
+ // (matching ws library API)
70
191
 
71
192
  /**
72
- * Send data to the client
73
- * @param {string|Buffer|ArrayBuffer} data - Data to send
193
+ * CONNECTING ready state constant (0).
194
+ * @type {number}
195
+ * @readonly
74
196
  */
75
- send(data) {
76
- if (this._readyState !== READY_STATES.OPEN) {
77
- throw new Error('WebSocket is not open')
78
- }
79
- // Deno's WebSocket.send() accepts string, ArrayBuffer, or Blob
80
- if (Buffer.isBuffer(data)) {
81
- this._socket.send(data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength))
82
- } else {
83
- this._socket.send(data)
84
- }
85
- }
197
+ this.CONNECTING = READY_STATES.CONNECTING;
198
+
199
+ /**
200
+ * OPEN ready state constant (1).
201
+ * @type {number}
202
+ * @readonly
203
+ */
204
+ this.OPEN = READY_STATES.OPEN;
205
+
206
+ /**
207
+ * CLOSING ready state constant (2).
208
+ * @type {number}
209
+ * @readonly
210
+ */
211
+ this.CLOSING = READY_STATES.CLOSING;
212
+
213
+ /**
214
+ * CLOSED ready state constant (3).
215
+ * @type {number}
216
+ * @readonly
217
+ */
218
+ this.CLOSED = READY_STATES.CLOSED;
219
+
220
+ // Wire up Deno's event properties to our EventEmitter
221
+ this._setupDenoEvents();
222
+ }
223
+
224
+ /**
225
+ * Get the current ready state of the WebSocket connection.
226
+ *
227
+ * @type {number}
228
+ * @readonly
229
+ *
230
+ * @example
231
+ * switch (ws.readyState) {
232
+ * case ws.CONNECTING:
233
+ * console.log('Connecting...')
234
+ * break
235
+ * case ws.OPEN:
236
+ * console.log('Ready to send')
237
+ * ws.send('Hello!')
238
+ * break
239
+ * case ws.CLOSING:
240
+ * console.log('Closing...')
241
+ * break
242
+ * case ws.CLOSED:
243
+ * console.log('Closed')
244
+ * break
245
+ * }
246
+ */
247
+ get readyState() {
248
+ return this._readyState;
249
+ }
250
+
251
+ /**
252
+ * Setup Deno WebSocket event handlers.
253
+ *
254
+ * Wires Deno's property-based event handlers (`onmessage`, `onclose`,
255
+ * `onerror`) to emit EventEmitter events for ws library compatibility.
256
+ *
257
+ * Message data is converted to Buffer for consistency with the ws
258
+ * library, which always provides Buffer instances for message data.
259
+ *
260
+ * @private
261
+ */
262
+ _setupDenoEvents() {
263
+ /**
264
+ * Handle incoming messages from Deno's WebSocket.
265
+ * Converts all data to Buffer for consistency with ws library.
266
+ */
267
+ this._socket.onmessage = (event) => {
268
+ // Convert to Buffer for consistency with ws library
269
+ const data = event.data;
270
+ const buffer =
271
+ typeof data === "string" ? Buffer.from(data) : Buffer.from(data);
272
+ this.emit("message", buffer);
273
+ };
274
+
275
+ /**
276
+ * Handle connection close from Deno's WebSocket.
277
+ * Updates ready state and emits close event with code and reason.
278
+ */
279
+ this._socket.onclose = (event) => {
280
+ this._readyState = READY_STATES.CLOSED;
281
+ this.emit("close", event.code, event.reason);
282
+ };
86
283
 
87
284
  /**
88
- * Close the WebSocket connection
89
- * @param {number} code - Status code
90
- * @param {string} reason - Close reason
285
+ * Handle errors from Deno's WebSocket.
286
+ * Emits the error event with the event object.
91
287
  */
92
- close(code = 1000, reason = '') {
93
- if (this._readyState === READY_STATES.CLOSING ||
94
- this._readyState === READY_STATES.CLOSED) {
95
- return
96
- }
97
- this._readyState = READY_STATES.CLOSING
98
- this._socket.close(code, reason)
288
+ this._socket.onerror = (event) => {
289
+ this.emit("error", event);
290
+ };
291
+ }
292
+
293
+ /**
294
+ * Send data to the remote endpoint.
295
+ *
296
+ * Deno's WebSocket.send() accepts string, ArrayBuffer, or Blob.
297
+ * This method handles the conversion from Buffer (Node.js style)
298
+ * to ArrayBuffer (Deno style).
299
+ *
300
+ * @param {string|Buffer|ArrayBuffer} data - Data to send
301
+ * @throws {Error} If the WebSocket is not in the OPEN state
302
+ *
303
+ * @example
304
+ * // Send text
305
+ * ws.send('Hello, World!')
306
+ *
307
+ * @example
308
+ * // Send JSON
309
+ * ws.send(JSON.stringify({ type: 'message', text: 'Hi!' }))
310
+ *
311
+ * @example
312
+ * // Send binary data
313
+ * ws.send(Buffer.from([0x01, 0x02, 0x03, 0x04]))
314
+ *
315
+ * @example
316
+ * // Safe send with state check
317
+ * if (ws.readyState === ws.OPEN) {
318
+ * ws.send('Safe message')
319
+ * }
320
+ */
321
+ send(data) {
322
+ if (this._readyState !== READY_STATES.OPEN) {
323
+ throw new Error("WebSocket is not open");
324
+ }
325
+
326
+ // Deno's WebSocket.send() accepts string, ArrayBuffer, or Blob
327
+ // Convert Buffer to ArrayBuffer for Deno compatibility
328
+ if (Buffer.isBuffer(data)) {
329
+ // Extract the underlying ArrayBuffer, accounting for Buffer offset
330
+ this._socket.send(
331
+ data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength),
332
+ );
333
+ } else {
334
+ // String or ArrayBuffer - pass through directly
335
+ this._socket.send(data);
336
+ }
337
+ }
338
+
339
+ /**
340
+ * Close the WebSocket connection.
341
+ *
342
+ * Initiates a graceful close by sending a close frame to the
343
+ * remote endpoint with the specified code and reason.
344
+ *
345
+ * Standard close codes (RFC 6455):
346
+ * - `1000` - Normal closure
347
+ * - `1001` - Going away (e.g., server shutdown)
348
+ * - `1002` - Protocol error
349
+ * - `1003` - Unsupported data type
350
+ * - `1008` - Policy violation
351
+ * - `1011` - Unexpected server error
352
+ *
353
+ * @param {number} [code=1000] - Close status code (1000-4999)
354
+ * @param {string} [reason=''] - Human-readable close reason (max 123 bytes)
355
+ *
356
+ * @example
357
+ * // Normal close
358
+ * ws.close()
359
+ *
360
+ * @example
361
+ * // Close with code and reason
362
+ * ws.close(1000, 'Session ended')
363
+ *
364
+ * @example
365
+ * // Close due to error
366
+ * ws.close(1008, 'Policy violation')
367
+ *
368
+ * @example
369
+ * // Server shutdown
370
+ * ws.close(1001, 'Server restarting')
371
+ */
372
+ close(code = 1000, reason = "") {
373
+ // Don't close if already closing or closed
374
+ if (
375
+ this._readyState === READY_STATES.CLOSING ||
376
+ this._readyState === READY_STATES.CLOSED
377
+ ) {
378
+ return;
99
379
  }
380
+
381
+ this._readyState = READY_STATES.CLOSING;
382
+ this._socket.close(code, reason);
383
+ }
100
384
  }
101
385
 
102
386
  /**
103
- * WebSocketServer compatible with Deno's Deno.upgradeWebSocket() pattern
104
- *
105
- * Usage in main.js:
106
- * - Create DenoWebSocketServer
107
- * - Call handleUpgrade() when upgrade request received
108
- * - It uses Deno.upgradeWebSocket() internally
387
+ * WebSocket server adapter for Deno's native WebSocket implementation.
388
+ *
389
+ * This class provides a `ws.WebSocketServer`-compatible interface for use
390
+ * with Deno's unique server architecture. It handles the upgrade process
391
+ * using `Deno.upgradeWebSocket()` and manages client connections.
392
+ *
393
+ * ## Key Differences from ws.WebSocketServer
394
+ *
395
+ * 1. **handleUpgrade Return Value**: Returns `{ response }` which must be
396
+ * returned from the Deno request handler.
397
+ *
398
+ * 2. **Upgrade Mechanism**: Uses `Deno.upgradeWebSocket()` internally
399
+ * instead of raw socket manipulation.
400
+ *
401
+ * 3. **No HTTP Server Integration**: Designed for noServer mode only,
402
+ * integrates with `Deno.serve()`.
403
+ *
404
+ * ## Events
405
+ *
406
+ * | Event | Arguments | Description |
407
+ * |--------------|------------------------------|--------------------------|
408
+ * | `connection` | `(ws: DenoWebSocket, req)` | New client connected |
409
+ *
410
+ * @class DenoWebSocketServer
411
+ * @extends EventEmitter
412
+ *
413
+ * @param {Object} [options={}] - Server configuration
414
+ * @param {boolean} [options.noServer=false] - Run without creating HTTP server
415
+ *
416
+ * @example
417
+ * // Basic setup with Deno.serve()
418
+ * const wss = new DenoWebSocketServer({ noServer: true })
419
+ *
420
+ * wss.on('connection', (ws, req) => {
421
+ * console.log('Client connected')
422
+ * ws.on('message', (data) => console.log('Received:', data))
423
+ * ws.send('Welcome!')
424
+ * })
425
+ *
426
+ * Deno.serve((req) => {
427
+ * const url = new URL(req.url)
428
+ *
429
+ * if (url.pathname === '/ws') {
430
+ * const result = wss.handleUpgrade(req)
431
+ * if (result) {
432
+ * return result.response
433
+ * }
434
+ * return new Response('Upgrade failed', { status: 500 })
435
+ * }
436
+ *
437
+ * return new Response('Hello from Deno!')
438
+ * })
439
+ *
440
+ * @example
441
+ * // Broadcasting to all clients
442
+ * function broadcast(message) {
443
+ * for (const client of wss.clients) {
444
+ * if (client.readyState === client.OPEN) {
445
+ * client.send(message)
446
+ * }
447
+ * }
448
+ * }
449
+ *
450
+ * @example
451
+ * // Graceful shutdown
452
+ * async function shutdown() {
453
+ * wss.close(() => {
454
+ * console.log('All WebSocket connections closed')
455
+ * })
456
+ * }
109
457
  */
110
458
  class DenoWebSocketServer extends EventEmitter {
111
- constructor(options = {}) {
112
- super()
113
- this._noServer = options.noServer || false
114
- this._clients = new Set()
115
- }
459
+ /**
460
+ * Create a new DenoWebSocketServer instance.
461
+ *
462
+ * @param {Object} [options={}] - Configuration options
463
+ * @param {boolean} [options.noServer=false] - Run in noServer mode
464
+ * (this is the typical mode for Deno integration)
465
+ */
466
+ constructor(options = {}) {
467
+ super();
116
468
 
117
469
  /**
118
- * Get all connected clients
119
- * @returns {Set<DenoWebSocket>}
470
+ * Whether this server operates in noServer mode.
471
+ * @type {boolean}
472
+ * @private
120
473
  */
121
- get clients() {
122
- return this._clients
123
- }
474
+ this._noServer = options.noServer || false;
124
475
 
125
476
  /**
126
- * Handle upgrade request using Deno.upgradeWebSocket
127
- * @param {Request} req - Deno Request object
128
- * @param {*} _socket - Not used in Deno (placeholder for API compat)
129
- * @param {*} _head - Not used in Deno (placeholder for API compat)
130
- * @param {function} callback - Called with wrapped WebSocket
131
- * @returns {{ response: Response } | null} - Response to return from handler
477
+ * Set of all connected DenoWebSocket clients.
478
+ * @type {Set<DenoWebSocket>}
479
+ * @private
132
480
  */
133
- handleUpgrade(req, _socket, _head, callback) {
134
- // Check for upgrade header
135
- const upgrade = req.headers.get('upgrade')
136
- if (!upgrade || upgrade.toLowerCase() !== 'websocket') {
137
- return null
138
- }
139
-
140
- try {
141
- // Use Deno's built-in upgrade
142
- const { socket: denoSocket, response } = Deno.upgradeWebSocket(req)
143
-
144
- // Wrap with our adapter
145
- const wrapper = new DenoWebSocket(denoSocket)
146
- this._clients.add(wrapper)
147
-
148
- // Remove from clients on close
149
- wrapper.on('close', () => {
150
- this._clients.delete(wrapper)
151
- })
152
-
153
- // Call the callback with wrapped socket
154
- if (callback) {
155
- callback(wrapper)
156
- }
157
-
158
- // Emit connection event
159
- this.emit('connection', wrapper, req)
160
-
161
- // Return the response for Deno's handler
162
- return { response }
163
- } catch (err) {
164
- console.error('[api-ape] Deno WebSocket upgrade failed:', err)
165
- return null
166
- }
481
+ this._clients = new Set();
482
+ }
483
+
484
+ /**
485
+ * Get all connected WebSocket clients.
486
+ *
487
+ * Returns a Set of DenoWebSocket wrappers for all active connections.
488
+ * Clients are automatically added when connections are established
489
+ * and removed when connections close.
490
+ *
491
+ * @type {Set<DenoWebSocket>}
492
+ * @readonly
493
+ *
494
+ * @example
495
+ * // Count connected clients
496
+ * console.log(`${wss.clients.size} clients connected`)
497
+ *
498
+ * @example
499
+ * // Broadcast to all clients
500
+ * for (const client of wss.clients) {
501
+ * if (client.readyState === client.OPEN) {
502
+ * client.send('Hello everyone!')
503
+ * }
504
+ * }
505
+ *
506
+ * @example
507
+ * // Convert to array for filtering
508
+ * const activeClients = [...wss.clients].filter(c =>
509
+ * c.readyState === c.OPEN
510
+ * )
511
+ */
512
+ get clients() {
513
+ return this._clients;
514
+ }
515
+
516
+ /**
517
+ * Handle an HTTP upgrade request using Deno.upgradeWebSocket.
518
+ *
519
+ * This method uses Deno's built-in `Deno.upgradeWebSocket()` function
520
+ * to upgrade the HTTP connection to a WebSocket. The returned object
521
+ * contains a `response` property that MUST be returned from the Deno
522
+ * request handler.
523
+ *
524
+ * ## Deno Upgrade Flow
525
+ *
526
+ * 1. Check for 'Upgrade: websocket' header
527
+ * 2. Call `Deno.upgradeWebSocket(req)` to get socket and response
528
+ * 3. Wrap the socket in DenoWebSocket adapter
529
+ * 4. Add to clients set and emit 'connection' event
530
+ * 5. Return `{ response }` for the caller to return to Deno
531
+ *
532
+ * @param {Request} req - Deno Request object
533
+ * @param {*} _socket - Not used in Deno (placeholder for API compatibility)
534
+ * @param {*} _head - Not used in Deno (placeholder for API compatibility)
535
+ * @param {Function} [callback] - Called with the DenoWebSocket wrapper
536
+ * @returns {{ response: Response } | null} Object with Response to return,
537
+ * or null if upgrade failed
538
+ *
539
+ * @example
540
+ * // In Deno.serve() handler
541
+ * Deno.serve((req) => {
542
+ * if (new URL(req.url).pathname === '/ws') {
543
+ * const result = wss.handleUpgrade(req, null, null, (ws) => {
544
+ * console.log('WebSocket ready')
545
+ * })
546
+ *
547
+ * if (result) {
548
+ * return result.response
549
+ * }
550
+ * return new Response('Upgrade failed', { status: 500 })
551
+ * }
552
+ *
553
+ * return new Response('Not found', { status: 404 })
554
+ * })
555
+ *
556
+ * @example
557
+ * // Without callback (use 'connection' event instead)
558
+ * wss.on('connection', (ws, req) => {
559
+ * console.log('Client connected from:', req.url)
560
+ * })
561
+ *
562
+ * Deno.serve((req) => {
563
+ * if (shouldUpgrade(req)) {
564
+ * const result = wss.handleUpgrade(req)
565
+ * return result?.response || new Response('Failed', { status: 500 })
566
+ * }
567
+ * return new Response('Hello')
568
+ * })
569
+ */
570
+ handleUpgrade(req, _socket, _head, callback) {
571
+ // Check for upgrade header
572
+ const upgrade = req.headers.get("upgrade");
573
+ if (!upgrade || upgrade.toLowerCase() !== "websocket") {
574
+ return null;
167
575
  }
168
576
 
169
- /**
170
- * Close server and all connections
171
- */
172
- close(callback) {
173
- for (const client of this._clients) {
174
- client.close(1001, 'Server shutting down')
175
- }
176
- this._clients.clear()
177
- if (callback) {
178
- callback()
179
- }
577
+ try {
578
+ // Use Deno's built-in upgrade mechanism
579
+ const { socket: denoSocket, response } = Deno.upgradeWebSocket(req);
580
+
581
+ // Wrap with our adapter for ws-compatible API
582
+ const wrapper = new DenoWebSocket(denoSocket);
583
+ this._clients.add(wrapper);
584
+
585
+ // Remove from clients set when connection closes
586
+ wrapper.on("close", () => {
587
+ this._clients.delete(wrapper);
588
+ });
589
+
590
+ // Invoke callback if provided
591
+ if (callback) {
592
+ callback(wrapper);
593
+ }
594
+
595
+ // Emit connection event for ws library compatibility
596
+ this.emit("connection", wrapper, req);
597
+
598
+ // Return the response for Deno's handler to return
599
+ return { response };
600
+ } catch (err) {
601
+ console.error("[api-ape] Deno WebSocket upgrade failed:", err);
602
+ return null;
180
603
  }
604
+ }
605
+
606
+ /**
607
+ * Close the server and all active connections.
608
+ *
609
+ * Sends a close frame with code 1001 (Going Away) and reason
610
+ * "Server shutting down" to all connected clients, then clears
611
+ * the clients set.
612
+ *
613
+ * @param {Function} [callback] - Called after all connections are closed
614
+ *
615
+ * @example
616
+ * // Graceful shutdown
617
+ * wss.close(() => {
618
+ * console.log('All WebSocket connections closed')
619
+ * Deno.exit(0)
620
+ * })
621
+ *
622
+ * @example
623
+ * // Shutdown with notification
624
+ * for (const client of wss.clients) {
625
+ * client.send(JSON.stringify({ type: 'shutdown', message: 'Server restarting' }))
626
+ * }
627
+ *
628
+ * setTimeout(() => {
629
+ * wss.close(() => console.log('Shutdown complete'))
630
+ * }, 1000)
631
+ *
632
+ * @example
633
+ * // Simple close without callback
634
+ * wss.close()
635
+ */
636
+ close(callback) {
637
+ for (const client of this._clients) {
638
+ client.close(1001, "Server shutting down");
639
+ }
640
+ this._clients.clear();
641
+
642
+ if (callback) {
643
+ callback();
644
+ }
645
+ }
181
646
  }
182
647
 
183
648
  module.exports = {
184
- DenoWebSocket,
185
- DenoWebSocketServer
186
- }
649
+ /**
650
+ * DenoWebSocket class - ws-compatible wrapper for Deno WebSocket.
651
+ * @type {typeof DenoWebSocket}
652
+ */
653
+ DenoWebSocket,
654
+
655
+ /**
656
+ * DenoWebSocketServer class - ws-compatible server for Deno.
657
+ * @type {typeof DenoWebSocketServer}
658
+ */
659
+ DenoWebSocketServer,
660
+ };