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
@@ -1,9 +1,10 @@
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')
7
+ const { createLongPollingHandler } = require('./longPolling')
7
8
  const { parse: parseUrl } = require('url')
8
9
 
9
10
  let created = false
@@ -32,7 +33,7 @@ function matchRoute(pathname, pattern) {
32
33
  }
33
34
 
34
35
  /**
35
- * Send JSON response
36
+ * Send JSON response (Node.js style)
36
37
  */
37
38
  function sendJson(res, statusCode, data) {
38
39
  res.writeHead(statusCode, { 'Content-Type': 'application/json' })
@@ -40,10 +41,12 @@ function sendJson(res, statusCode, data) {
40
41
  }
41
42
 
42
43
  /**
43
- * Get cookie value from request
44
+ * Get cookie value from request headers
44
45
  */
45
- function getCookie(req, name) {
46
- const cookies = req.headers.cookie
46
+ function getCookie(headers, name) {
47
+ const cookies = typeof headers.get === 'function'
48
+ ? headers.get('cookie')
49
+ : headers.cookie
47
50
  if (!cookies) return null
48
51
  const match = cookies.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`))
49
52
  return match ? match[1] : null
@@ -52,42 +55,71 @@ function getCookie(req, name) {
52
55
  /**
53
56
  * Check if request is from localhost
54
57
  */
55
- function isLocalhost(req) {
56
- const host = req.headers.host?.split(':')[0] || ''
57
- 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)
58
61
  }
59
62
 
60
63
  /**
61
64
  * Check if connection is secure (HTTPS)
62
65
  */
63
66
  function isSecure(req) {
64
- return req.socket?.encrypted || req.headers['x-forwarded-proto'] === 'https'
65
- }
66
-
67
- module.exports = function (server, { where, onConnent, fileTransferOptions }) {
68
-
69
- if (created) {
70
- throw new Error("Api-Ape already started")
67
+ if (typeof req.headers?.get === 'function') {
68
+ return req.headers.get('x-forwarded-proto') === 'https'
71
69
  }
72
- created = true
70
+ return req.socket?.encrypted || req.headers?.['x-forwarded-proto'] === 'https'
71
+ }
73
72
 
73
+ /**
74
+ * Create core api-ape handlers (shared between runtimes)
75
+ */
76
+ function createApeCore({ where, onConnent, fileTransferOptions }) {
74
77
  const controllers = loader(where)
75
78
  const fileTransfer = getFileTransferManager(fileTransferOptions)
79
+ const wiringHandler = wiring(controllers, onConnent, fileTransfer)
80
+ const { handleStreamGet, handleStreamPost } = createLongPollingHandler(controllers, onConnent, fileTransfer)
76
81
 
77
- // Create WebSocket server attached to the HTTP server
78
- const wss = new WebSocketServer({ noServer: true })
79
-
80
- // Handle WebSocket connections
81
82
  const wsPath = `/${where}/ape`
82
- const wiringHandler = wiring(controllers, onConnent, fileTransfer)
83
+ const pollPath = `/${where}/ape/poll`
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`
83
89
 
84
- wss.on('connection', wiringHandler)
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
+ }
105
+
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
114
+
115
+ const wss = new WebSocketServer({ noServer: true })
116
+ wss.on('connection', core.wiringHandler)
85
117
 
86
118
  // Handle HTTP upgrade requests for WebSocket
87
119
  server.on('upgrade', (req, socket, head) => {
88
120
  const { pathname } = parseUrl(req.url)
89
121
 
90
- if (pathname === wsPath) {
122
+ if (pathname === core.wsPath) {
91
123
  wss.handleUpgrade(req, socket, head, (ws) => {
92
124
  wss.emit('connection', ws, req)
93
125
  })
@@ -104,8 +136,8 @@ module.exports = function (server, { where, onConnent, fileTransferOptions }) {
104
136
  server.on('request', (req, res) => {
105
137
  const { pathname } = parseUrl(req.url)
106
138
 
107
- // Serve bundled client at /api/ape.js (or /{where}/ape.js)
108
- if (pathname === `/${where}/ape.js`) {
139
+ // Serve bundled client
140
+ if (pathname === core.clientPath) {
109
141
  const filePath = path.join(__dirname, '../../dist/ape.js')
110
142
  fs.readFile(filePath, (err, data) => {
111
143
  if (err) {
@@ -118,21 +150,52 @@ module.exports = function (server, { where, onConnent, fileTransferOptions }) {
118
150
  return
119
151
  }
120
152
 
121
- // File download endpoint - GET /api/ape/data/:hash
122
- const downloadMatch = matchRoute(pathname, `/${where}/ape/data/:hash`)
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
+ })
164
+ return
165
+ }
166
+
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)
175
+ return
176
+ }
177
+
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)
123
186
  if (req.method === 'GET' && downloadMatch) {
124
187
  const { hash } = downloadMatch
125
- const hostId = getCookie(req, 'apeHostId') || req.headers['x-ape-host-id']
188
+ const clientId = getCookie(req.headers, 'apeClientId') || req.headers['x-ape-client-id']
126
189
 
127
- if (!hostId) {
190
+ if (!clientId) {
128
191
  return sendJson(res, 401, { error: 'Missing session identifier' })
129
192
  }
130
193
 
131
- if (!isLocalhost(req) && !isSecure(req)) {
194
+ if (!isLocalhost(req.headers.host) && !isSecure(req)) {
132
195
  return sendJson(res, 403, { error: 'HTTPS required for file transfers' })
133
196
  }
134
197
 
135
- const result = fileTransfer.getDownload(hash, hostId)
198
+ const result = core.fileTransfer.getDownload(hash, clientId)
136
199
 
137
200
  if (!result) {
138
201
  return sendJson(res, 404, { error: 'Download not found or unauthorized' })
@@ -146,17 +209,17 @@ module.exports = function (server, { where, onConnent, fileTransferOptions }) {
146
209
  return
147
210
  }
148
211
 
149
- // File upload endpoint - PUT /api/ape/data/:queryId/:pathHash
150
- const uploadMatch = matchRoute(pathname, `/${where}/ape/data/:queryId/:pathHash`)
212
+ // File upload
213
+ const uploadMatch = matchRoute(pathname, core.uploadPattern)
151
214
  if (req.method === 'PUT' && uploadMatch) {
152
215
  const { queryId, pathHash } = uploadMatch
153
- const hostId = getCookie(req, 'apeHostId') || req.headers['x-ape-host-id']
216
+ const clientId = getCookie(req.headers, 'apeClientId') || req.headers['x-ape-client-id']
154
217
 
155
- if (!hostId) {
218
+ if (!clientId) {
156
219
  return sendJson(res, 401, { error: 'Missing session identifier' })
157
220
  }
158
221
 
159
- if (!isLocalhost(req) && !isSecure(req)) {
222
+ if (!isLocalhost(req.headers.host) && !isSecure(req)) {
160
223
  return sendJson(res, 403, { error: 'HTTPS required for file transfers' })
161
224
  }
162
225
 
@@ -164,7 +227,7 @@ module.exports = function (server, { where, onConnent, fileTransferOptions }) {
164
227
  req.on('data', chunk => chunks.push(chunk))
165
228
  req.on('end', () => {
166
229
  const data = Buffer.concat(chunks)
167
- const success = fileTransfer.receiveUpload(queryId, pathHash, data, hostId)
230
+ const success = core.fileTransfer.receiveUpload(queryId, pathHash, data, clientId)
168
231
 
169
232
  if (success) {
170
233
  sendJson(res, 200, { success: true })
@@ -183,4 +246,284 @@ module.exports = function (server, { where, onConnent, fileTransferOptions }) {
183
246
  listener.call(server, req, res)
184
247
  }
185
248
  })
186
- }
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