api-ape 2.0.0 → 2.2.2

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 (89) hide show
  1. package/README.md +203 -124
  2. package/client/README.md +37 -30
  3. package/client/browser.js +10 -8
  4. package/client/connectSocket.js +662 -381
  5. package/client/index.js +171 -0
  6. package/client/transports/streaming.js +240 -0
  7. package/dist/ape.js +2 -699
  8. package/dist/ape.js.map +7 -0
  9. package/dist/api-ape.min.js +2 -0
  10. package/dist/api-ape.min.js.map +7 -0
  11. package/index.d.ts +71 -18
  12. package/package.json +50 -15
  13. package/server/README.md +99 -13
  14. package/server/lib/broadcast.js +25 -8
  15. package/server/lib/bun.js +122 -0
  16. package/server/lib/longPolling.js +226 -0
  17. package/server/lib/main.js +381 -38
  18. package/server/lib/wiring.js +19 -12
  19. package/server/lib/ws/adapters/bun.js +225 -0
  20. package/server/lib/ws/adapters/deno.js +186 -0
  21. package/server/lib/ws/frames.js +217 -0
  22. package/server/lib/ws/index.js +15 -0
  23. package/server/lib/ws/server.js +109 -0
  24. package/server/lib/ws/socket.js +222 -0
  25. package/server/lib/wsProvider.js +135 -0
  26. package/server/security/origin.js +16 -4
  27. package/server/socket/receive.js +14 -1
  28. package/server/socket/send.js +6 -6
  29. package/server/utils/deepRequire.js +25 -10
  30. package/server/utils/parseUserAgent.js +286 -0
  31. package/example/Bun/README.md +0 -74
  32. package/example/Bun/api/message.ts +0 -11
  33. package/example/Bun/index.html +0 -76
  34. package/example/Bun/package.json +0 -9
  35. package/example/Bun/server.ts +0 -59
  36. package/example/Bun/styles.css +0 -128
  37. package/example/ExpressJs/README.md +0 -95
  38. package/example/ExpressJs/api/message.js +0 -11
  39. package/example/ExpressJs/backend.js +0 -39
  40. package/example/ExpressJs/index.html +0 -88
  41. package/example/ExpressJs/package-lock.json +0 -834
  42. package/example/ExpressJs/package.json +0 -10
  43. package/example/ExpressJs/styles.css +0 -128
  44. package/example/NextJs/.dockerignore +0 -29
  45. package/example/NextJs/Dockerfile +0 -52
  46. package/example/NextJs/Dockerfile.dev +0 -27
  47. package/example/NextJs/README.md +0 -113
  48. package/example/NextJs/ape/client.js +0 -66
  49. package/example/NextJs/ape/embed.js +0 -12
  50. package/example/NextJs/ape/index.js +0 -23
  51. package/example/NextJs/ape/logic/chat.js +0 -62
  52. package/example/NextJs/ape/onConnect.js +0 -69
  53. package/example/NextJs/ape/onDisconnect.js +0 -13
  54. package/example/NextJs/ape/onError.js +0 -9
  55. package/example/NextJs/ape/onReceive.js +0 -15
  56. package/example/NextJs/ape/onSend.js +0 -15
  57. package/example/NextJs/api/message.js +0 -44
  58. package/example/NextJs/docker-compose.yml +0 -22
  59. package/example/NextJs/next-env.d.ts +0 -5
  60. package/example/NextJs/next.config.js +0 -8
  61. package/example/NextJs/package-lock.json +0 -6400
  62. package/example/NextJs/package.json +0 -24
  63. package/example/NextJs/pages/Info.tsx +0 -153
  64. package/example/NextJs/pages/_app.tsx +0 -6
  65. package/example/NextJs/pages/index.tsx +0 -275
  66. package/example/NextJs/public/favicon.ico +0 -0
  67. package/example/NextJs/public/vercel.svg +0 -4
  68. package/example/NextJs/server.js +0 -36
  69. package/example/NextJs/styles/Chat.module.css +0 -448
  70. package/example/NextJs/styles/Home.module.css +0 -129
  71. package/example/NextJs/styles/globals.css +0 -26
  72. package/example/NextJs/tsconfig.json +0 -20
  73. package/example/README.md +0 -117
  74. package/example/Vite/README.md +0 -68
  75. package/example/Vite/ape/client.ts +0 -66
  76. package/example/Vite/ape/onConnect.ts +0 -52
  77. package/example/Vite/api/message.ts +0 -57
  78. package/example/Vite/index.html +0 -16
  79. package/example/Vite/package.json +0 -19
  80. package/example/Vite/server.ts +0 -62
  81. package/example/Vite/src/App.vue +0 -170
  82. package/example/Vite/src/components/Info.vue +0 -352
  83. package/example/Vite/src/main.ts +0 -5
  84. package/example/Vite/src/style.css +0 -200
  85. package/example/Vite/src/vite-env.d.ts +0 -7
  86. package/example/Vite/vite.config.ts +0 -20
  87. package/todo.md +0 -85
  88. package/utils/jss.test.js +0 -261
  89. package/utils/messageHash.test.js +0 -56
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Bun Native WebSocket Adapter
3
+ * Wraps Bun's ServerWebSocket to be compatible with the ws library API
4
+ *
5
+ * Bun uses a different pattern:
6
+ * - Bun.serve({ websocket: { open, message, close, ... } })
7
+ * - server.upgrade(req) to upgrade connections
8
+ *
9
+ * This adapter provides a ws-compatible WebSocketServer that works
10
+ * with the existing api-ape code.
11
+ */
12
+
13
+ const { EventEmitter } = require('events')
14
+
15
+ // WebSocket ready states (matching ws library)
16
+ const READY_STATES = {
17
+ CONNECTING: 0,
18
+ OPEN: 1,
19
+ CLOSING: 2,
20
+ CLOSED: 3
21
+ }
22
+
23
+ /**
24
+ * Wrapper around Bun's ServerWebSocket to provide ws-compatible API
25
+ */
26
+ class BunWebSocket extends EventEmitter {
27
+ constructor(bunSocket) {
28
+ super()
29
+ this._socket = bunSocket
30
+ this._readyState = READY_STATES.OPEN
31
+
32
+ // Define constants on instance (matching ws library API)
33
+ this.CONNECTING = READY_STATES.CONNECTING
34
+ this.OPEN = READY_STATES.OPEN
35
+ this.CLOSING = READY_STATES.CLOSING
36
+ this.CLOSED = READY_STATES.CLOSED
37
+ }
38
+
39
+ get readyState() {
40
+ return this._readyState
41
+ }
42
+
43
+ /**
44
+ * Send data to the client
45
+ * @param {string|Buffer|ArrayBuffer} data - Data to send
46
+ */
47
+ send(data) {
48
+ if (this._readyState !== READY_STATES.OPEN) {
49
+ throw new Error('WebSocket is not open')
50
+ }
51
+ this._socket.send(data)
52
+ }
53
+
54
+ /**
55
+ * Close the WebSocket connection
56
+ * @param {number} code - Status code
57
+ * @param {string} reason - Close reason
58
+ */
59
+ close(code = 1000, reason = '') {
60
+ if (this._readyState === READY_STATES.CLOSING ||
61
+ this._readyState === READY_STATES.CLOSED) {
62
+ return
63
+ }
64
+ this._readyState = READY_STATES.CLOSING
65
+ this._socket.close(code, reason)
66
+ }
67
+
68
+ /**
69
+ * Called by BunWebSocketServer when message received
70
+ * @internal
71
+ */
72
+ _onMessage(data) {
73
+ // Convert to Buffer for consistency with ws library
74
+ const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data)
75
+ this.emit('message', buffer)
76
+ }
77
+
78
+ /**
79
+ * Called by BunWebSocketServer when connection closed
80
+ * @internal
81
+ */
82
+ _onClose(code, reason) {
83
+ this._readyState = READY_STATES.CLOSED
84
+ this.emit('close', code, reason)
85
+ }
86
+
87
+ /**
88
+ * Called by BunWebSocketServer on error
89
+ * @internal
90
+ */
91
+ _onError(error) {
92
+ this.emit('error', error)
93
+ }
94
+ }
95
+
96
+ /**
97
+ * WebSocketServer compatible with Bun's server.upgrade() pattern
98
+ *
99
+ * Usage in main.js:
100
+ * - Create BunWebSocketServer
101
+ * - It provides the websocket handlers for Bun.serve()
102
+ * - Call handleUpgrade() when upgrade request received
103
+ */
104
+ class BunWebSocketServer extends EventEmitter {
105
+ constructor(options = {}) {
106
+ super()
107
+ this._noServer = options.noServer || false
108
+ this._clients = new Map() // Map socket -> BunWebSocket wrapper
109
+
110
+ // Bun websocket handler configuration
111
+ // This will be used by the integration in main.js
112
+ this.websocketHandlers = {
113
+ open: (ws) => this._handleOpen(ws),
114
+ message: (ws, message) => this._handleMessage(ws, message),
115
+ close: (ws, code, reason) => this._handleClose(ws, code, reason),
116
+ error: (ws, error) => this._handleError(ws, error)
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Get all connected clients
122
+ * @returns {Set<BunWebSocket>}
123
+ */
124
+ get clients() {
125
+ return new Set(this._clients.values())
126
+ }
127
+
128
+ /**
129
+ * Handle upgrade request - called from main.js
130
+ * For Bun, we need to return info for the upgrade
131
+ * @param {Request} req - Bun Request object
132
+ * @param {*} server - Bun server instance
133
+ * @param {*} head - Not used in Bun
134
+ * @param {function} callback - Called with wrapped WebSocket
135
+ */
136
+ handleUpgrade(req, server, head, callback) {
137
+ // In Bun, we store the callback and req info
138
+ // The actual upgrade happens via server.upgrade()
139
+ // The callback will be called in _handleOpen when Bun calls our open handler
140
+
141
+ // Store pending upgrade info keyed by some identifier
142
+ // Bun's server.upgrade() succeeds/fails synchronously
143
+ const upgraded = server.upgrade(req, {
144
+ data: { callback, req }
145
+ })
146
+
147
+ if (!upgraded) {
148
+ // Upgrade failed
149
+ return false
150
+ }
151
+
152
+ return true
153
+ }
154
+
155
+ /**
156
+ * Handle Bun websocket open event
157
+ * @internal
158
+ */
159
+ _handleOpen(bunSocket) {
160
+ const wrapper = new BunWebSocket(bunSocket)
161
+ this._clients.set(bunSocket, wrapper)
162
+
163
+ // Get the callback from upgrade data
164
+ const { callback, req } = bunSocket.data || {}
165
+
166
+ if (callback) {
167
+ callback(wrapper)
168
+ }
169
+
170
+ // Emit connection event
171
+ this.emit('connection', wrapper, req)
172
+ }
173
+
174
+ /**
175
+ * Handle Bun websocket message event
176
+ * @internal
177
+ */
178
+ _handleMessage(bunSocket, message) {
179
+ const wrapper = this._clients.get(bunSocket)
180
+ if (wrapper) {
181
+ wrapper._onMessage(message)
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Handle Bun websocket close event
187
+ * @internal
188
+ */
189
+ _handleClose(bunSocket, code, reason) {
190
+ const wrapper = this._clients.get(bunSocket)
191
+ if (wrapper) {
192
+ wrapper._onClose(code, reason)
193
+ this._clients.delete(bunSocket)
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Handle Bun websocket error event
199
+ * @internal
200
+ */
201
+ _handleError(bunSocket, error) {
202
+ const wrapper = this._clients.get(bunSocket)
203
+ if (wrapper) {
204
+ wrapper._onError(error)
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Close server and all connections
210
+ */
211
+ close(callback) {
212
+ for (const [bunSocket, wrapper] of this._clients) {
213
+ wrapper.close(1001, 'Server shutting down')
214
+ }
215
+ this._clients.clear()
216
+ if (callback) {
217
+ callback()
218
+ }
219
+ }
220
+ }
221
+
222
+ module.exports = {
223
+ BunWebSocket,
224
+ BunWebSocketServer
225
+ }
@@ -0,0 +1,186 @@
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.
11
+ */
12
+
13
+ const { EventEmitter } = require('events')
14
+
15
+ // WebSocket ready states (matching ws library)
16
+ const READY_STATES = {
17
+ CONNECTING: 0,
18
+ OPEN: 1,
19
+ CLOSING: 2,
20
+ CLOSED: 3
21
+ }
22
+
23
+ /**
24
+ * Wrapper around Deno's native WebSocket to provide ws-compatible API
25
+ * Deno's WebSocket uses onmessage/onclose properties instead of EventEmitter
26
+ */
27
+ 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
+ }
42
+
43
+ get readyState() {
44
+ return this._readyState
45
+ }
46
+
47
+ /**
48
+ * Setup Deno WebSocket event handlers
49
+ * @internal
50
+ */
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
+ }
70
+
71
+ /**
72
+ * Send data to the client
73
+ * @param {string|Buffer|ArrayBuffer} data - Data to send
74
+ */
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
+ }
86
+
87
+ /**
88
+ * Close the WebSocket connection
89
+ * @param {number} code - Status code
90
+ * @param {string} reason - Close reason
91
+ */
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)
99
+ }
100
+ }
101
+
102
+ /**
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
109
+ */
110
+ class DenoWebSocketServer extends EventEmitter {
111
+ constructor(options = {}) {
112
+ super()
113
+ this._noServer = options.noServer || false
114
+ this._clients = new Set()
115
+ }
116
+
117
+ /**
118
+ * Get all connected clients
119
+ * @returns {Set<DenoWebSocket>}
120
+ */
121
+ get clients() {
122
+ return this._clients
123
+ }
124
+
125
+ /**
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
132
+ */
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
+ }
167
+ }
168
+
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
+ }
180
+ }
181
+ }
182
+
183
+ module.exports = {
184
+ DenoWebSocket,
185
+ DenoWebSocketServer
186
+ }
@@ -0,0 +1,217 @@
1
+ /**
2
+ * RFC 6455 WebSocket Frame Encoding/Decoding
3
+ *
4
+ * Frame format:
5
+ * 0 1 2 3
6
+ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
7
+ * +-+-+-+-+-------+-+-------------+-------------------------------+
8
+ * |F|R|R|R| opcode|M| Payload len | Extended payload length |
9
+ * |I|S|S|S| (4) |A| (7) | (16/64) |
10
+ * |N|V|V|V| |S| | (if payload len==126/127) |
11
+ * | |1|2|3| |K| | |
12
+ * +-+-+-+-+-------+-+-------------+-------------------------------+
13
+ * | Extended payload length continued, if payload len == 127 |
14
+ * +-------------------------------+-------------------------------+
15
+ * | | Masking-key, if MASK set to 1 |
16
+ * +-------------------------------+-------------------------------+
17
+ * | Masking-key (continued) | Payload Data |
18
+ * +-------------------------------+-------------------------------+
19
+ */
20
+
21
+ const crypto = require('crypto')
22
+
23
+ // WebSocket GUID for handshake (RFC 6455 Section 1.3)
24
+ const WS_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
25
+
26
+ // Opcodes
27
+ const OPCODES = {
28
+ CONTINUATION: 0x00,
29
+ TEXT: 0x01,
30
+ BINARY: 0x02,
31
+ CLOSE: 0x08,
32
+ PING: 0x09,
33
+ PONG: 0x0A
34
+ }
35
+
36
+ /**
37
+ * Generate the Sec-WebSocket-Accept header value from client key
38
+ * @param {string} clientKey - The Sec-WebSocket-Key from client
39
+ * @returns {string} Base64 encoded accept key
40
+ */
41
+ function generateAcceptKey(clientKey) {
42
+ return crypto
43
+ .createHash('sha1')
44
+ .update(clientKey + WS_GUID)
45
+ .digest('base64')
46
+ }
47
+
48
+ /**
49
+ * Unmask payload data (client → server messages are always masked)
50
+ * @param {Buffer} payload - The masked payload
51
+ * @param {Buffer} maskKey - 4-byte mask key
52
+ * @returns {Buffer} Unmasked payload
53
+ */
54
+ function unmaskPayload(payload, maskKey) {
55
+ const result = Buffer.alloc(payload.length)
56
+ for (let i = 0; i < payload.length; i++) {
57
+ result[i] = payload[i] ^ maskKey[i & 3]
58
+ }
59
+ return result
60
+ }
61
+
62
+ /**
63
+ * Parse a WebSocket frame from buffer
64
+ * @param {Buffer} buffer - Raw data buffer
65
+ * @returns {{ frame: Object, bytesConsumed: number } | null} Parsed frame or null if incomplete
66
+ */
67
+ function parseFrame(buffer) {
68
+ if (buffer.length < 2) return null
69
+
70
+ let offset = 0
71
+ const firstByte = buffer[offset++]
72
+ const secondByte = buffer[offset++]
73
+
74
+ const fin = (firstByte & 0x80) !== 0
75
+ const opcode = firstByte & 0x0F
76
+ const masked = (secondByte & 0x80) !== 0
77
+ let payloadLength = secondByte & 0x7F
78
+
79
+ // Extended payload length
80
+ if (payloadLength === 126) {
81
+ if (buffer.length < offset + 2) return null
82
+ payloadLength = buffer.readUInt16BE(offset)
83
+ offset += 2
84
+ } else if (payloadLength === 127) {
85
+ if (buffer.length < offset + 8) return null
86
+ // JavaScript can't handle full 64-bit, use lower 32 bits
87
+ const high = buffer.readUInt32BE(offset)
88
+ const low = buffer.readUInt32BE(offset + 4)
89
+ if (high !== 0) {
90
+ throw new Error('Payload too large')
91
+ }
92
+ payloadLength = low
93
+ offset += 8
94
+ }
95
+
96
+ // Mask key (4 bytes, only if masked)
97
+ let maskKey = null
98
+ if (masked) {
99
+ if (buffer.length < offset + 4) return null
100
+ maskKey = buffer.slice(offset, offset + 4)
101
+ offset += 4
102
+ }
103
+
104
+ // Payload
105
+ if (buffer.length < offset + payloadLength) return null
106
+ let payload = buffer.slice(offset, offset + payloadLength)
107
+ offset += payloadLength
108
+
109
+ // Unmask if needed
110
+ if (masked && maskKey) {
111
+ payload = unmaskPayload(payload, maskKey)
112
+ }
113
+
114
+ return {
115
+ frame: { fin, opcode, payload },
116
+ bytesConsumed: offset
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Build a WebSocket frame
122
+ * Server → client frames are never masked (per RFC 6455)
123
+ * @param {Buffer|string} data - Payload data
124
+ * @param {number} opcode - Frame opcode
125
+ * @param {boolean} fin - Is this the final frame?
126
+ * @returns {Buffer} Complete frame buffer
127
+ */
128
+ function buildFrame(data, opcode = OPCODES.TEXT, fin = true) {
129
+ const payload = Buffer.isBuffer(data) ? data : Buffer.from(data)
130
+ const payloadLength = payload.length
131
+
132
+ // Calculate header size
133
+ let headerSize = 2 // First two bytes
134
+ let extendedLengthSize = 0
135
+
136
+ if (payloadLength > 65535) {
137
+ extendedLengthSize = 8
138
+ } else if (payloadLength > 125) {
139
+ extendedLengthSize = 2
140
+ }
141
+
142
+ const frame = Buffer.alloc(headerSize + extendedLengthSize + payloadLength)
143
+ let offset = 0
144
+
145
+ // First byte: FIN + opcode
146
+ frame[offset++] = (fin ? 0x80 : 0x00) | opcode
147
+
148
+ // Second byte: mask bit (0) + payload length
149
+ if (payloadLength > 65535) {
150
+ frame[offset++] = 127
151
+ // Write 64-bit length (high 32 bits = 0)
152
+ frame.writeUInt32BE(0, offset)
153
+ offset += 4
154
+ frame.writeUInt32BE(payloadLength, offset)
155
+ offset += 4
156
+ } else if (payloadLength > 125) {
157
+ frame[offset++] = 126
158
+ frame.writeUInt16BE(payloadLength, offset)
159
+ offset += 2
160
+ } else {
161
+ frame[offset++] = payloadLength
162
+ }
163
+
164
+ // Payload (no masking for server → client)
165
+ payload.copy(frame, offset)
166
+
167
+ return frame
168
+ }
169
+
170
+ /**
171
+ * Build a close frame with optional status code and reason
172
+ * @param {number} code - Status code (1000 = normal)
173
+ * @param {string} reason - Close reason
174
+ * @returns {Buffer} Close frame
175
+ */
176
+ function buildCloseFrame(code = 1000, reason = '') {
177
+ const reasonBuffer = Buffer.from(reason)
178
+ const payload = Buffer.alloc(2 + reasonBuffer.length)
179
+ payload.writeUInt16BE(code, 0)
180
+ reasonBuffer.copy(payload, 2)
181
+ return buildFrame(payload, OPCODES.CLOSE)
182
+ }
183
+
184
+ /**
185
+ * Build a pong frame in response to ping
186
+ * @param {Buffer} data - Ping payload to echo back
187
+ * @returns {Buffer} Pong frame
188
+ */
189
+ function buildPongFrame(data) {
190
+ return buildFrame(data, OPCODES.PONG)
191
+ }
192
+
193
+ /**
194
+ * Parse close frame payload
195
+ * @param {Buffer} payload - Close frame payload
196
+ * @returns {{ code: number, reason: string }}
197
+ */
198
+ function parseClosePayload(payload) {
199
+ if (payload.length >= 2) {
200
+ return {
201
+ code: payload.readUInt16BE(0),
202
+ reason: payload.slice(2).toString('utf8')
203
+ }
204
+ }
205
+ return { code: 1005, reason: '' }
206
+ }
207
+
208
+ module.exports = {
209
+ OPCODES,
210
+ generateAcceptKey,
211
+ parseFrame,
212
+ buildFrame,
213
+ buildCloseFrame,
214
+ buildPongFrame,
215
+ parseClosePayload,
216
+ unmaskPayload
217
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * WebSocket polyfill entry point
3
+ * Provides WebSocketServer compatible with the ws library API
4
+ */
5
+
6
+ const { WebSocketServer } = require('./server')
7
+ const { WebSocket, READY_STATES } = require('./socket')
8
+ const { OPCODES } = require('./frames')
9
+
10
+ module.exports = {
11
+ WebSocketServer,
12
+ WebSocket,
13
+ READY_STATES,
14
+ OPCODES
15
+ }