api-ape 2.1.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 (87) hide show
  1. package/README.md +184 -195
  2. package/client/README.md +37 -30
  3. package/client/browser.js +4 -14
  4. package/client/connectSocket.js +167 -42
  5. package/client/index.js +171 -0
  6. package/client/transports/streaming.js +3 -16
  7. package/dist/ape.js +2 -1049
  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 +67 -23
  12. package/package.json +27 -8
  13. package/server/README.md +52 -11
  14. package/server/lib/broadcast.js +25 -8
  15. package/server/lib/bun.js +122 -0
  16. package/server/lib/longPolling.js +28 -23
  17. package/server/lib/main.js +372 -46
  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/socket/receive.js +14 -1
  27. package/server/socket/send.js +6 -6
  28. package/server/utils/parseUserAgent.js +286 -0
  29. package/example/Bun/README.md +0 -74
  30. package/example/Bun/api/message.ts +0 -11
  31. package/example/Bun/index.html +0 -76
  32. package/example/Bun/package.json +0 -9
  33. package/example/Bun/server.ts +0 -59
  34. package/example/Bun/styles.css +0 -128
  35. package/example/ExpressJs/README.md +0 -95
  36. package/example/ExpressJs/api/message.js +0 -11
  37. package/example/ExpressJs/backend.js +0 -39
  38. package/example/ExpressJs/index.html +0 -88
  39. package/example/ExpressJs/package-lock.json +0 -834
  40. package/example/ExpressJs/package.json +0 -10
  41. package/example/ExpressJs/styles.css +0 -128
  42. package/example/NextJs/.dockerignore +0 -29
  43. package/example/NextJs/Dockerfile +0 -52
  44. package/example/NextJs/Dockerfile.dev +0 -27
  45. package/example/NextJs/README.md +0 -113
  46. package/example/NextJs/ape/client.js +0 -66
  47. package/example/NextJs/ape/embed.js +0 -12
  48. package/example/NextJs/ape/index.js +0 -23
  49. package/example/NextJs/ape/logic/chat.js +0 -62
  50. package/example/NextJs/ape/onConnect.js +0 -69
  51. package/example/NextJs/ape/onDisconnect.js +0 -13
  52. package/example/NextJs/ape/onError.js +0 -9
  53. package/example/NextJs/ape/onReceive.js +0 -15
  54. package/example/NextJs/ape/onSend.js +0 -15
  55. package/example/NextJs/api/message.js +0 -44
  56. package/example/NextJs/docker-compose.yml +0 -22
  57. package/example/NextJs/next-env.d.ts +0 -5
  58. package/example/NextJs/next.config.js +0 -8
  59. package/example/NextJs/package-lock.json +0 -6400
  60. package/example/NextJs/package.json +0 -24
  61. package/example/NextJs/pages/Info.tsx +0 -153
  62. package/example/NextJs/pages/_app.tsx +0 -6
  63. package/example/NextJs/pages/index.tsx +0 -275
  64. package/example/NextJs/public/favicon.ico +0 -0
  65. package/example/NextJs/public/vercel.svg +0 -4
  66. package/example/NextJs/server.js +0 -36
  67. package/example/NextJs/styles/Chat.module.css +0 -448
  68. package/example/NextJs/styles/Home.module.css +0 -129
  69. package/example/NextJs/styles/globals.css +0 -26
  70. package/example/NextJs/tsconfig.json +0 -20
  71. package/example/README.md +0 -117
  72. package/example/Vite/README.md +0 -68
  73. package/example/Vite/ape/client.ts +0 -66
  74. package/example/Vite/ape/onConnect.ts +0 -52
  75. package/example/Vite/api/message.ts +0 -57
  76. package/example/Vite/index.html +0 -16
  77. package/example/Vite/package.json +0 -19
  78. package/example/Vite/server.ts +0 -62
  79. package/example/Vite/src/App.vue +0 -170
  80. package/example/Vite/src/components/Info.vue +0 -352
  81. package/example/Vite/src/main.ts +0 -5
  82. package/example/Vite/src/style.css +0 -200
  83. package/example/Vite/src/vite-env.d.ts +0 -7
  84. package/example/Vite/vite.config.ts +0 -20
  85. package/todo.md +0 -85
  86. package/utils/jss.test.js +0 -261
  87. package/utils/messageHash.test.js +0 -56
@@ -1,6 +1,6 @@
1
1
  const loader = require('./loader')
2
2
  const wiring = require('./wiring')
3
- const { WebSocketServer } = require('ws')
3
+ const { getWebSocketProvider, isBun, isDeno, getRuntime } = require('./wsProvider')
4
4
  const path = require('path')
5
5
  const fs = require('fs')
6
6
  const { getFileTransferManager } = require('./fileTransfer')
@@ -33,7 +33,7 @@ function matchRoute(pathname, pattern) {
33
33
  }
34
34
 
35
35
  /**
36
- * Send JSON response
36
+ * Send JSON response (Node.js style)
37
37
  */
38
38
  function sendJson(res, statusCode, data) {
39
39
  res.writeHead(statusCode, { 'Content-Type': 'application/json' })
@@ -41,10 +41,12 @@ function sendJson(res, statusCode, data) {
41
41
  }
42
42
 
43
43
  /**
44
- * Get cookie value from request
44
+ * Get cookie value from request headers
45
45
  */
46
- function getCookie(req, name) {
47
- const cookies = req.headers.cookie
46
+ function getCookie(headers, name) {
47
+ const cookies = typeof headers.get === 'function'
48
+ ? headers.get('cookie')
49
+ : headers.cookie
48
50
  if (!cookies) return null
49
51
  const match = cookies.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`))
50
52
  return match ? match[1] : null
@@ -53,46 +55,71 @@ function getCookie(req, name) {
53
55
  /**
54
56
  * Check if request is from localhost
55
57
  */
56
- function isLocalhost(req) {
57
- const host = req.headers.host?.split(':')[0] || ''
58
- return ['localhost', '127.0.0.1', '[::1]'].includes(host)
58
+ function isLocalhost(host) {
59
+ const hostname = host?.split(':')[0] || ''
60
+ return ['localhost', '127.0.0.1', '[::1]'].includes(hostname)
59
61
  }
60
62
 
61
63
  /**
62
64
  * Check if connection is secure (HTTPS)
63
65
  */
64
66
  function isSecure(req) {
65
- return req.socket?.encrypted || req.headers['x-forwarded-proto'] === 'https'
66
- }
67
-
68
- module.exports = function (server, { where, onConnent, fileTransferOptions }) {
69
-
70
- if (created) {
71
- throw new Error("Api-Ape already started")
67
+ if (typeof req.headers?.get === 'function') {
68
+ return req.headers.get('x-forwarded-proto') === 'https'
72
69
  }
73
- created = true
70
+ return req.socket?.encrypted || req.headers?.['x-forwarded-proto'] === 'https'
71
+ }
74
72
 
73
+ /**
74
+ * Create core api-ape handlers (shared between runtimes)
75
+ */
76
+ function createApeCore({ where, onConnent, fileTransferOptions }) {
75
77
  const controllers = loader(where)
76
78
  const fileTransfer = getFileTransferManager(fileTransferOptions)
79
+ const wiringHandler = wiring(controllers, onConnent, fileTransfer)
80
+ const { handleStreamGet, handleStreamPost } = createLongPollingHandler(controllers, onConnent, fileTransfer)
77
81
 
78
- // Create WebSocket server attached to the HTTP server
79
- const wss = new WebSocketServer({ noServer: true })
80
-
81
- // Handle WebSocket connections
82
82
  const wsPath = `/${where}/ape`
83
83
  const pollPath = `/${where}/ape/poll`
84
- const wiringHandler = wiring(controllers, onConnent, fileTransfer)
84
+ const pingPath = `/${where}/ape/ping`
85
+ const clientPath = `/${where}/ape.js`
86
+ const clientMapPath = `/${where}/ape.js.map`
87
+ const downloadPattern = `/${where}/ape/data/:hash`
88
+ const uploadPattern = `/${where}/ape/data/:queryId/:pathHash`
89
+
90
+ return {
91
+ controllers,
92
+ fileTransfer,
93
+ wiringHandler,
94
+ handleStreamGet,
95
+ handleStreamPost,
96
+ wsPath,
97
+ pollPath,
98
+ pingPath,
99
+ clientPath,
100
+ clientMapPath,
101
+ downloadPattern,
102
+ uploadPattern
103
+ }
104
+ }
85
105
 
86
- // Create long polling handler for WebSocket fallback
87
- const { handleStreamGet, handleStreamPost } = createLongPollingHandler(controllers, onConnent, fileTransfer)
106
+ /**
107
+ * Node.js / Express integration
108
+ * Uses server.on('upgrade') and server.on('request')
109
+ */
110
+ function initNodeServer(server, options) {
111
+ const { WebSocketServer } = getWebSocketProvider()
112
+ const core = createApeCore(options)
113
+ const { where } = options
88
114
 
89
- wss.on('connection', wiringHandler)
115
+ const wss = new WebSocketServer({ noServer: true })
116
+ wss.on('connection', core.wiringHandler)
90
117
 
91
118
  // Handle HTTP upgrade requests for WebSocket
92
119
  server.on('upgrade', (req, socket, head) => {
93
120
  const { pathname } = parseUrl(req.url)
94
121
 
95
- if (pathname === wsPath) {
122
+ if (pathname === core.wsPath) {
96
123
  wss.handleUpgrade(req, socket, head, (ws) => {
97
124
  wss.emit('connection', ws, req)
98
125
  })
@@ -109,8 +136,8 @@ module.exports = function (server, { where, onConnent, fileTransferOptions }) {
109
136
  server.on('request', (req, res) => {
110
137
  const { pathname } = parseUrl(req.url)
111
138
 
112
- // Serve bundled client at /api/ape.js (or /{where}/ape.js)
113
- if (pathname === `/${where}/ape.js`) {
139
+ // Serve bundled client
140
+ if (pathname === core.clientPath) {
114
141
  const filePath = path.join(__dirname, '../../dist/ape.js')
115
142
  fs.readFile(filePath, (err, data) => {
116
143
  if (err) {
@@ -123,33 +150,52 @@ module.exports = function (server, { where, onConnent, fileTransferOptions }) {
123
150
  return
124
151
  }
125
152
 
126
- // Long polling endpoints - GET /api/ape/poll (streaming receive)
127
- if (pathname === pollPath && req.method === 'GET') {
128
- handleStreamGet(req, res)
153
+ // Serve source map for debugging
154
+ if (pathname === core.clientMapPath) {
155
+ const filePath = path.join(__dirname, '../../dist/ape.js.map')
156
+ fs.readFile(filePath, (err, data) => {
157
+ if (err) {
158
+ sendJson(res, 404, { error: 'Source map not found' })
159
+ return
160
+ }
161
+ res.writeHead(200, { 'Content-Type': 'application/json' })
162
+ res.end(data)
163
+ })
129
164
  return
130
165
  }
131
166
 
132
- // Long polling endpoints - POST /api/ape/poll (send messages)
133
- if (pathname === pollPath && req.method === 'POST') {
134
- handleStreamPost(req, res, controllers)
167
+ // Ping endpoint for captive portal detection
168
+ if (pathname === core.pingPath && req.method === 'GET') {
169
+ return sendJson(res, 200, { ok: true, ts: Date.now() })
170
+ }
171
+
172
+ // Long polling - GET
173
+ if (pathname === core.pollPath && req.method === 'GET') {
174
+ core.handleStreamGet(req, res)
135
175
  return
136
176
  }
137
177
 
138
- // File download endpoint - GET /api/ape/data/:hash
139
- const downloadMatch = matchRoute(pathname, `/${where}/ape/data/:hash`)
178
+ // Long polling - POST
179
+ if (pathname === core.pollPath && req.method === 'POST') {
180
+ core.handleStreamPost(req, res, core.controllers)
181
+ return
182
+ }
183
+
184
+ // File download
185
+ const downloadMatch = matchRoute(pathname, core.downloadPattern)
140
186
  if (req.method === 'GET' && downloadMatch) {
141
187
  const { hash } = downloadMatch
142
- const hostId = getCookie(req, 'apeHostId') || req.headers['x-ape-host-id']
188
+ const clientId = getCookie(req.headers, 'apeClientId') || req.headers['x-ape-client-id']
143
189
 
144
- if (!hostId) {
190
+ if (!clientId) {
145
191
  return sendJson(res, 401, { error: 'Missing session identifier' })
146
192
  }
147
193
 
148
- if (!isLocalhost(req) && !isSecure(req)) {
194
+ if (!isLocalhost(req.headers.host) && !isSecure(req)) {
149
195
  return sendJson(res, 403, { error: 'HTTPS required for file transfers' })
150
196
  }
151
197
 
152
- const result = fileTransfer.getDownload(hash, hostId)
198
+ const result = core.fileTransfer.getDownload(hash, clientId)
153
199
 
154
200
  if (!result) {
155
201
  return sendJson(res, 404, { error: 'Download not found or unauthorized' })
@@ -163,17 +209,17 @@ module.exports = function (server, { where, onConnent, fileTransferOptions }) {
163
209
  return
164
210
  }
165
211
 
166
- // File upload endpoint - PUT /api/ape/data/:queryId/:pathHash
167
- const uploadMatch = matchRoute(pathname, `/${where}/ape/data/:queryId/:pathHash`)
212
+ // File upload
213
+ const uploadMatch = matchRoute(pathname, core.uploadPattern)
168
214
  if (req.method === 'PUT' && uploadMatch) {
169
215
  const { queryId, pathHash } = uploadMatch
170
- const hostId = getCookie(req, 'apeHostId') || req.headers['x-ape-host-id']
216
+ const clientId = getCookie(req.headers, 'apeClientId') || req.headers['x-ape-client-id']
171
217
 
172
- if (!hostId) {
218
+ if (!clientId) {
173
219
  return sendJson(res, 401, { error: 'Missing session identifier' })
174
220
  }
175
221
 
176
- if (!isLocalhost(req) && !isSecure(req)) {
222
+ if (!isLocalhost(req.headers.host) && !isSecure(req)) {
177
223
  return sendJson(res, 403, { error: 'HTTPS required for file transfers' })
178
224
  }
179
225
 
@@ -181,7 +227,7 @@ module.exports = function (server, { where, onConnent, fileTransferOptions }) {
181
227
  req.on('data', chunk => chunks.push(chunk))
182
228
  req.on('end', () => {
183
229
  const data = Buffer.concat(chunks)
184
- const success = fileTransfer.receiveUpload(queryId, pathHash, data, hostId)
230
+ const success = core.fileTransfer.receiveUpload(queryId, pathHash, data, clientId)
185
231
 
186
232
  if (success) {
187
233
  sendJson(res, 200, { success: true })
@@ -200,4 +246,284 @@ module.exports = function (server, { where, onConnent, fileTransferOptions }) {
200
246
  listener.call(server, req, res)
201
247
  }
202
248
  })
203
- }
249
+
250
+ return { wss, core }
251
+ }
252
+
253
+ /**
254
+ * Bun integration
255
+ * Returns fetch and websocket handlers to spread into Bun.serve()
256
+ */
257
+ function initBunServer(options) {
258
+ const { BunWebSocket } = require('./ws/adapters/bun')
259
+ const core = createApeCore(options)
260
+ const clients = new Map()
261
+
262
+ /**
263
+ * Fetch handler for Bun.serve()
264
+ * Handles all api-ape routes, returns null for non-ape routes
265
+ */
266
+ function fetch(req, server) {
267
+ const url = new URL(req.url)
268
+ const pathname = url.pathname
269
+
270
+ // WebSocket upgrade
271
+ if (pathname === core.wsPath) {
272
+ const upgrade = req.headers.get('upgrade')
273
+ if (upgrade?.toLowerCase() === 'websocket') {
274
+ const success = server.upgrade(req, { data: { req } })
275
+ if (success) return undefined
276
+ return new Response('WebSocket upgrade failed', { status: 500 })
277
+ }
278
+ }
279
+
280
+ // Serve client bundle
281
+ if (pathname === core.clientPath) {
282
+ try {
283
+ const filePath = path.join(__dirname, '../../dist/ape.js')
284
+ const data = fs.readFileSync(filePath)
285
+ return new Response(data, {
286
+ headers: { 'Content-Type': 'application/javascript' }
287
+ })
288
+ } catch {
289
+ return new Response('Client bundle not found', { status: 500 })
290
+ }
291
+ }
292
+
293
+ // Serve source map for debugging
294
+ if (pathname === core.clientMapPath) {
295
+ try {
296
+ const filePath = path.join(__dirname, '../../dist/ape.js.map')
297
+ const data = fs.readFileSync(filePath)
298
+ return new Response(data, {
299
+ headers: { 'Content-Type': 'application/json' }
300
+ })
301
+ } catch {
302
+ return new Response('Source map not found', { status: 404 })
303
+ }
304
+ }
305
+
306
+ // Ping endpoint for captive portal detection
307
+ if (pathname === core.pingPath && req.method === 'GET') {
308
+ return new Response(JSON.stringify({ ok: true, ts: Date.now() }), {
309
+ headers: { 'Content-Type': 'application/json' }
310
+ })
311
+ }
312
+
313
+ // Not an api-ape route
314
+ return null
315
+ }
316
+
317
+ /**
318
+ * WebSocket handlers for Bun.serve()
319
+ */
320
+ const websocket = {
321
+ open(ws) {
322
+ const wrapper = new BunWebSocket(ws)
323
+ clients.set(ws, wrapper)
324
+ const { req } = ws.data || {}
325
+ core.wiringHandler(wrapper, req)
326
+ },
327
+
328
+ message(ws, message) {
329
+ const wrapper = clients.get(ws)
330
+ if (wrapper) {
331
+ wrapper._onMessage(message)
332
+ }
333
+ },
334
+
335
+ close(ws, code, reason) {
336
+ const wrapper = clients.get(ws)
337
+ if (wrapper) {
338
+ wrapper._onClose(code, reason)
339
+ clients.delete(ws)
340
+ }
341
+ },
342
+
343
+ error(ws, error) {
344
+ const wrapper = clients.get(ws)
345
+ if (wrapper) {
346
+ wrapper._onError(error)
347
+ }
348
+ }
349
+ }
350
+
351
+ return { fetch, websocket, clients, core }
352
+ }
353
+
354
+ /**
355
+ * Check if server is a Bun server (has reload method and Bun globals)
356
+ */
357
+ function isBunServer(server) {
358
+ return isBun() && typeof server?.reload === 'function'
359
+ }
360
+
361
+ /**
362
+ * Initialize Bun server using server.reload() to hook in
363
+ * This allows same signature: ape(server, { where: 'api' })
364
+ */
365
+ function initBunServerWithReload(server, options) {
366
+ const { BunWebSocket } = require('./ws/adapters/bun')
367
+ const core = createApeCore(options)
368
+ const clients = new Map()
369
+
370
+ // Check if WebSocket support is enabled on Bun server
371
+ // Bun requires websocket handlers to be defined at Bun.serve() creation
372
+ const hasWebSocketSupport = typeof server.upgrade === 'function'
373
+
374
+ if (!hasWebSocketSupport && options.transport !== 'longpolling') {
375
+ throw new Error(`
376
+ 🦍 api-ape: Bun WebSocket support not enabled!
377
+
378
+ To enable WebSocket support in Bun, add a 'websocket' property when creating your server:
379
+
380
+ const server = Bun.serve({
381
+ port: 3000,
382
+ fetch(req) { ... },
383
+ websocket: { message() {} } // <-- Required for api-ape
384
+ })
385
+
386
+ ape(server, { where: 'api' })
387
+
388
+ If you only want HTTP long-polling (no WebSocket), pass:
389
+ ape(server, { where: 'api', transport: 'longpolling' })
390
+ `)
391
+ }
392
+
393
+ // Store original fetch handler
394
+ const originalFetch = server.fetch
395
+
396
+ /**
397
+ * Wrapped fetch handler that handles api-ape routes first
398
+ */
399
+ function wrappedFetch(req, server) {
400
+ const url = new URL(req.url)
401
+ const pathname = url.pathname
402
+
403
+ // WebSocket upgrade
404
+ if (pathname === core.wsPath) {
405
+ const upgrade = req.headers.get('upgrade')
406
+ if (upgrade?.toLowerCase() === 'websocket') {
407
+ const success = server.upgrade(req, { data: { req } })
408
+ if (success) return undefined
409
+ return new Response('WebSocket upgrade failed', { status: 500 })
410
+ }
411
+ }
412
+
413
+ // Serve client bundle
414
+ if (pathname === core.clientPath) {
415
+ try {
416
+ const filePath = path.join(__dirname, '../../dist/ape.js')
417
+ const data = fs.readFileSync(filePath)
418
+ return new Response(data, {
419
+ headers: { 'Content-Type': 'application/javascript' }
420
+ })
421
+ } catch {
422
+ return new Response('Client bundle not found', { status: 500 })
423
+ }
424
+ }
425
+
426
+ // Serve source map for debugging
427
+ if (pathname === core.clientMapPath) {
428
+ try {
429
+ const filePath = path.join(__dirname, '../../dist/ape.js.map')
430
+ const data = fs.readFileSync(filePath)
431
+ return new Response(data, {
432
+ headers: { 'Content-Type': 'application/json' }
433
+ })
434
+ } catch {
435
+ return new Response('Source map not found', { status: 404 })
436
+ }
437
+ }
438
+
439
+ // Ping endpoint for captive portal detection
440
+ if (pathname === core.pingPath && req.method === 'GET') {
441
+ return new Response(JSON.stringify({ ok: true, ts: Date.now() }), {
442
+ headers: { 'Content-Type': 'application/json' }
443
+ })
444
+ }
445
+
446
+ // Pass to original fetch handler
447
+ if (originalFetch) {
448
+ return originalFetch(req, server)
449
+ }
450
+
451
+ return new Response('Not Found', { status: 404 })
452
+ }
453
+
454
+ /**
455
+ * WebSocket handlers
456
+ */
457
+ const websocket = {
458
+ open(ws) {
459
+ const wrapper = new BunWebSocket(ws)
460
+ clients.set(ws, wrapper)
461
+ const { req } = ws.data || {}
462
+ core.wiringHandler(wrapper, req)
463
+ },
464
+
465
+ message(ws, message) {
466
+ const wrapper = clients.get(ws)
467
+ if (wrapper) {
468
+ wrapper._onMessage(message)
469
+ }
470
+ },
471
+
472
+ close(ws, code, reason) {
473
+ const wrapper = clients.get(ws)
474
+ if (wrapper) {
475
+ wrapper._onClose(code, reason)
476
+ clients.delete(ws)
477
+ }
478
+ },
479
+
480
+ error(ws, error) {
481
+ const wrapper = clients.get(ws)
482
+ if (wrapper) {
483
+ wrapper._onError(error)
484
+ }
485
+ }
486
+ }
487
+
488
+ // Use server.reload() to hook in our handlers
489
+ server.reload({
490
+ fetch: wrappedFetch,
491
+ websocket
492
+ })
493
+
494
+ return { clients, core }
495
+ }
496
+
497
+ /**
498
+ * Main api-ape entry point
499
+ * Unified signature for all runtimes:
500
+ * ape(server, { where: 'api' })
501
+ *
502
+ * Works with:
503
+ * - Node.js http.Server
504
+ * - Express server
505
+ * - Bun.serve() server
506
+ */
507
+ module.exports = function (server, options) {
508
+ if (created) {
509
+ throw new Error("Api-Ape already started")
510
+ }
511
+ created = true
512
+
513
+ // Bun server - use server.reload() to hook in
514
+ if (isBunServer(server)) {
515
+ return initBunServerWithReload(server, options)
516
+ }
517
+
518
+ // Node.js / Express - server is an http.Server with .on() method
519
+ if (server && typeof server.on === 'function') {
520
+ return initNodeServer(server, options)
521
+ }
522
+
523
+ throw new Error('Unsupported server type. Expected http.Server (Node.js) or Bun.serve() server.')
524
+ }
525
+
526
+ // Export runtime detection utilities
527
+ module.exports.isBun = isBun
528
+ module.exports.isDeno = isDeno
529
+ module.exports.getRuntime = getRuntime
@@ -3,11 +3,9 @@ const socketOpen = require('../socket/open')
3
3
  const socketReceive = require('../socket/receive')
4
4
  const socketSend = require('../socket/send')
5
5
  const makeid = require('../utils/genId')
6
- const UAParser = require('ua-parser-js');
6
+ const parseUserAgent = require('../utils/parseUserAgent');
7
7
  const { addClient, removeClient } = require('./broadcast')
8
8
 
9
- const parser = new UAParser();
10
-
11
9
  // connent, beforeSend, beforeReceive, error, afterSend, afterReceive, disconnent
12
10
 
13
11
 
@@ -40,13 +38,22 @@ module.exports = function wiring(controllers, onConnent, fileTransfer) {
40
38
  }
41
39
  } // END sentBufferFn
42
40
 
43
- const hostId = makeid(20)
44
- const agent = parser.setUA(req.headers['user-agent']).getResult()
41
+ const clientId = makeid(20)
42
+ const agent = parseUserAgent(req.headers['user-agent'])
45
43
  const sharedValues = {
46
44
  socket, req, agent, send: (type, data, err) => sentBufferFn(false, type, data, err)
47
45
  }
48
- sharedValues.send.toString = () => hostId
46
+ sharedValues.send.toString = () => clientId
47
+
48
+ // Track this client for broadcast BEFORE calling onConnent
49
+ // This ensures ape.online() returns the correct count when sending init
50
+ const clientInfo = { clientId, send: null, embed: null }
51
+ addClient(clientInfo)
49
52
 
53
+ // Remove client on disconnect (set up early, will work once send is assigned)
54
+ socket.on('close', () => {
55
+ removeClient(clientInfo)
56
+ })
50
57
 
51
58
  let result = onConnent(socket, req, sharedValues.send)
52
59
  if (!result || !result.then) {
@@ -57,6 +64,7 @@ module.exports = function wiring(controllers, onConnent, fileTransfer) {
57
64
  const isOk = socketOpen(socket, req, onError)
58
65
 
59
66
  if (!isOk) {
67
+ removeClient(clientInfo) // Clean up if connection fails
60
68
  return;
61
69
  }
62
70
 
@@ -65,7 +73,7 @@ module.exports = function wiring(controllers, onConnent, fileTransfer) {
65
73
  const ape = {
66
74
  socket,
67
75
  req,
68
- hostId,
76
+ clientId,
69
77
  checkReply,
70
78
  events: { onReceive, onSend, onError, onDisconnent },
71
79
  controllers,
@@ -76,13 +84,12 @@ module.exports = function wiring(controllers, onConnent, fileTransfer) {
76
84
  send = socketSend(ape)
77
85
  ape.send = send
78
86
 
79
- // Track this client for broadcast
80
- const clientInfo = { hostId, send, embed }
81
- addClient(clientInfo)
87
+ // Update clientInfo with real send function and embed
88
+ clientInfo.send = send
89
+ clientInfo.embed = embed
82
90
 
83
- // Remove client on disconnect
91
+ // Call onDisconnent when socket closes
84
92
  socket.on('close', () => {
85
- removeClient(clientInfo)
86
93
  onDisconnent()
87
94
  })
88
95