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.
- package/README.md +184 -195
- package/client/README.md +37 -30
- package/client/browser.js +4 -14
- package/client/connectSocket.js +167 -42
- package/client/index.js +171 -0
- package/client/transports/streaming.js +3 -16
- package/dist/ape.js +2 -1049
- package/dist/ape.js.map +7 -0
- package/dist/api-ape.min.js +2 -0
- package/dist/api-ape.min.js.map +7 -0
- package/index.d.ts +67 -23
- package/package.json +27 -8
- package/server/README.md +52 -11
- package/server/lib/broadcast.js +25 -8
- package/server/lib/bun.js +122 -0
- package/server/lib/longPolling.js +28 -23
- package/server/lib/main.js +372 -46
- package/server/lib/wiring.js +19 -12
- package/server/lib/ws/adapters/bun.js +225 -0
- package/server/lib/ws/adapters/deno.js +186 -0
- package/server/lib/ws/frames.js +217 -0
- package/server/lib/ws/index.js +15 -0
- package/server/lib/ws/server.js +109 -0
- package/server/lib/ws/socket.js +222 -0
- package/server/lib/wsProvider.js +135 -0
- package/server/socket/receive.js +14 -1
- package/server/socket/send.js +6 -6
- package/server/utils/parseUserAgent.js +286 -0
- package/example/Bun/README.md +0 -74
- package/example/Bun/api/message.ts +0 -11
- package/example/Bun/index.html +0 -76
- package/example/Bun/package.json +0 -9
- package/example/Bun/server.ts +0 -59
- package/example/Bun/styles.css +0 -128
- package/example/ExpressJs/README.md +0 -95
- package/example/ExpressJs/api/message.js +0 -11
- package/example/ExpressJs/backend.js +0 -39
- package/example/ExpressJs/index.html +0 -88
- package/example/ExpressJs/package-lock.json +0 -834
- package/example/ExpressJs/package.json +0 -10
- package/example/ExpressJs/styles.css +0 -128
- package/example/NextJs/.dockerignore +0 -29
- package/example/NextJs/Dockerfile +0 -52
- package/example/NextJs/Dockerfile.dev +0 -27
- package/example/NextJs/README.md +0 -113
- package/example/NextJs/ape/client.js +0 -66
- package/example/NextJs/ape/embed.js +0 -12
- package/example/NextJs/ape/index.js +0 -23
- package/example/NextJs/ape/logic/chat.js +0 -62
- package/example/NextJs/ape/onConnect.js +0 -69
- package/example/NextJs/ape/onDisconnect.js +0 -13
- package/example/NextJs/ape/onError.js +0 -9
- package/example/NextJs/ape/onReceive.js +0 -15
- package/example/NextJs/ape/onSend.js +0 -15
- package/example/NextJs/api/message.js +0 -44
- package/example/NextJs/docker-compose.yml +0 -22
- package/example/NextJs/next-env.d.ts +0 -5
- package/example/NextJs/next.config.js +0 -8
- package/example/NextJs/package-lock.json +0 -6400
- package/example/NextJs/package.json +0 -24
- package/example/NextJs/pages/Info.tsx +0 -153
- package/example/NextJs/pages/_app.tsx +0 -6
- package/example/NextJs/pages/index.tsx +0 -275
- package/example/NextJs/public/favicon.ico +0 -0
- package/example/NextJs/public/vercel.svg +0 -4
- package/example/NextJs/server.js +0 -36
- package/example/NextJs/styles/Chat.module.css +0 -448
- package/example/NextJs/styles/Home.module.css +0 -129
- package/example/NextJs/styles/globals.css +0 -26
- package/example/NextJs/tsconfig.json +0 -20
- package/example/README.md +0 -117
- package/example/Vite/README.md +0 -68
- package/example/Vite/ape/client.ts +0 -66
- package/example/Vite/ape/onConnect.ts +0 -52
- package/example/Vite/api/message.ts +0 -57
- package/example/Vite/index.html +0 -16
- package/example/Vite/package.json +0 -19
- package/example/Vite/server.ts +0 -62
- package/example/Vite/src/App.vue +0 -170
- package/example/Vite/src/components/Info.vue +0 -352
- package/example/Vite/src/main.ts +0 -5
- package/example/Vite/src/style.css +0 -200
- package/example/Vite/src/vite-env.d.ts +0 -7
- package/example/Vite/vite.config.ts +0 -20
- package/todo.md +0 -85
- package/utils/jss.test.js +0 -261
- package/utils/messageHash.test.js +0 -56
package/package.json
CHANGED
|
@@ -1,17 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "api-ape",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.2",
|
|
4
4
|
"description": "Remote Procedure Events (RPE) - A lightweight WebSocket framework for building real-time APIs. Call server functions from the browser like local methods with automatic reconnection, HTTP streaming fallback, and extended JSON encoding.",
|
|
5
5
|
"main": "index.js",
|
|
6
|
+
"browser": "./client/index.js",
|
|
6
7
|
"types": "index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"browser": "./client/index.js",
|
|
11
|
+
"default": "./index.js"
|
|
12
|
+
},
|
|
13
|
+
"./client": "./client/index.js",
|
|
14
|
+
"./client/*": "./client/*",
|
|
15
|
+
"./server": "./server/index.js",
|
|
16
|
+
"./server/*": "./server/*"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist/",
|
|
20
|
+
"client/",
|
|
21
|
+
"server/",
|
|
22
|
+
"utils/",
|
|
23
|
+
"index.js",
|
|
24
|
+
"index.d.ts",
|
|
25
|
+
"!**/*.test.js"
|
|
26
|
+
],
|
|
7
27
|
"scripts": {
|
|
28
|
+
"prepare": "cp .hooks/* .git/hooks/ 2>/dev/null && chmod +x .git/hooks/* || true",
|
|
8
29
|
"test": "jest --no-cache",
|
|
9
30
|
"test:watch": "jest --watch --runInBand",
|
|
10
31
|
"test:cover": "jest --coverage --no-cache --detectOpenHandles",
|
|
11
32
|
"demo:express": "cd example/ExpressJs && npm install && npm start",
|
|
12
33
|
"demo:nextjs": "cd example/NextJs && docker-compose up --build",
|
|
13
34
|
"demo:vite": "cd example/Vite && npm install && npm run dev",
|
|
14
|
-
"demo:bun": "cd example/Bun && npm install && npm start"
|
|
35
|
+
"demo:bun": "cd example/Bun && npm install && npm start",
|
|
36
|
+
"publish": "bash scripts/publish.sh"
|
|
15
37
|
},
|
|
16
38
|
"repository": {
|
|
17
39
|
"type": "git",
|
|
@@ -20,7 +42,7 @@
|
|
|
20
42
|
"keywords": [
|
|
21
43
|
"websocket",
|
|
22
44
|
"realtime",
|
|
23
|
-
"
|
|
45
|
+
"CSRF",
|
|
24
46
|
"api",
|
|
25
47
|
"remote-procedure-call",
|
|
26
48
|
"real-time",
|
|
@@ -34,7 +56,7 @@
|
|
|
34
56
|
"jjs",
|
|
35
57
|
"rpe",
|
|
36
58
|
"nodejs",
|
|
37
|
-
"
|
|
59
|
+
"library"
|
|
38
60
|
],
|
|
39
61
|
"author": "brian shannon",
|
|
40
62
|
"license": "MIT",
|
|
@@ -42,10 +64,7 @@
|
|
|
42
64
|
"url": "https://github.com/codemeasandwich/api-ape/issues"
|
|
43
65
|
},
|
|
44
66
|
"homepage": "https://github.com/codemeasandwich/api-ape#readme",
|
|
45
|
-
"dependencies": {
|
|
46
|
-
"ua-parser-js": "^1.0.37",
|
|
47
|
-
"ws": "^8.14.0"
|
|
48
|
-
},
|
|
67
|
+
"dependencies": {},
|
|
49
68
|
"devDependencies": {
|
|
50
69
|
"esbuild": "^0.27.2",
|
|
51
70
|
"jest": "^29.3.1"
|
package/server/README.md
CHANGED
|
@@ -13,7 +13,16 @@ server/
|
|
|
13
13
|
│ ├── broadcast.js # Client tracking & broadcast utilities
|
|
14
14
|
│ ├── fileTransfer.js # Binary file transfer manager
|
|
15
15
|
│ ├── longPolling.js # HTTP streaming fallback handler
|
|
16
|
-
│
|
|
16
|
+
│ ├── wiring.js # WebSocket handler setup
|
|
17
|
+
│ ├── wsProvider.js # Runtime detection (Node 24+ native / polyfill)
|
|
18
|
+
│ └── ws/ # RFC 6455 WebSocket polyfill (zero dependencies)
|
|
19
|
+
│ ├── index.js # Module entry point
|
|
20
|
+
│ ├── frames.js # Frame encoding/decoding
|
|
21
|
+
│ ├── socket.js # WebSocket connection class
|
|
22
|
+
│ ├── server.js # WebSocketServer class
|
|
23
|
+
│ └── adapters/ # Runtime-specific adapters
|
|
24
|
+
│ ├── bun.js # Bun native WebSocket
|
|
25
|
+
│ └── deno.js # Deno native WebSocket
|
|
17
26
|
├── socket/
|
|
18
27
|
│ ├── receive.js # Incoming message handler
|
|
19
28
|
│ └── send.js # Outgoing message handler
|
|
@@ -76,8 +85,9 @@ ape(app, {
|
|
|
76
85
|
| `this.broadcast(type, data)` | Send to ALL connected clients |
|
|
77
86
|
| `this.broadcastOthers(type, data)` | Send to all EXCEPT the caller |
|
|
78
87
|
| `this.online()` | Get count of connected clients |
|
|
79
|
-
| `this.getClients()` | Get array of connected
|
|
80
|
-
| `this.
|
|
88
|
+
| `this.getClients()` | Get array of connected clientIds |
|
|
89
|
+
| `this.clientId` | Unique ID of the calling client (generated by api-ape) |
|
|
90
|
+
| `this.sessionId` | Session ID from cookie (set by outer framework, may be `null`) |
|
|
81
91
|
| `this.req` | Original HTTP request |
|
|
82
92
|
| `this.socket` | WebSocket instance |
|
|
83
93
|
| `this.agent` | Parsed user-agent |
|
|
@@ -102,15 +112,15 @@ Drop JS files in your `where` directory:
|
|
|
102
112
|
|
|
103
113
|
```
|
|
104
114
|
api/
|
|
105
|
-
├── hello.js →
|
|
106
|
-
├── users.js →
|
|
115
|
+
├── hello.js → api.hello(data)
|
|
116
|
+
├── users.js → api.users(data)
|
|
107
117
|
├── posts/
|
|
108
|
-
│ ├── index.js →
|
|
109
|
-
│ ├── list.js →
|
|
110
|
-
│ └── create.js →
|
|
118
|
+
│ ├── index.js → api.posts(data) # index.js maps to parent folder
|
|
119
|
+
│ ├── list.js → api.posts.list(data)
|
|
120
|
+
│ └── create.js → api.posts.create(data)
|
|
111
121
|
```
|
|
112
122
|
|
|
113
|
-
**Note**: Both `api/users.js` and `api/users/index.js` map to the same endpoint `
|
|
123
|
+
**Note**: Both `api/users.js` and `api/users/index.js` map to the same endpoint `api.users(data)`. Use `index.js` when you want to group related files in a folder.
|
|
114
124
|
|
|
115
125
|
**⚠️ Duplicate Detection**: If both files exist, api-ape will throw an error on startup:
|
|
116
126
|
```
|
|
@@ -159,7 +169,7 @@ api-ape automatically provides HTTP streaming endpoints as a fallback when WebSo
|
|
|
159
169
|
|
|
160
170
|
Long-lived HTTP streaming connection for receiving server messages.
|
|
161
171
|
|
|
162
|
-
- **Session**: Cookie-based (`
|
|
172
|
+
- **Session**: Cookie-based (`apeClientId`)
|
|
163
173
|
- **Response**: Streaming JSON messages
|
|
164
174
|
- **Heartbeat**: Every 20 seconds
|
|
165
175
|
- **Auto-reconnect**: Client reconnects after 25 seconds
|
|
@@ -168,7 +178,7 @@ Long-lived HTTP streaming connection for receiving server messages.
|
|
|
168
178
|
|
|
169
179
|
Send messages to server when using HTTP streaming transport.
|
|
170
180
|
|
|
171
|
-
- **Session**: Cookie-based (`
|
|
181
|
+
- **Session**: Cookie-based (`apeClientId`)
|
|
172
182
|
- **Body**: JJS-encoded message
|
|
173
183
|
- **Response**: JJS-encoded result
|
|
174
184
|
|
|
@@ -180,3 +190,34 @@ Send messages to server when using HTTP streaming transport.
|
|
|
180
190
|
4. Automatically upgrades back to WebSocket when available
|
|
181
191
|
|
|
182
192
|
The fallback is **completely transparent** to your controllers - they work identically with both transports.
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Zero-Dependency WebSocket
|
|
197
|
+
|
|
198
|
+
api-ape includes its own RFC 6455 WebSocket implementation with **zero npm dependencies**.
|
|
199
|
+
|
|
200
|
+
### Runtime Detection
|
|
201
|
+
|
|
202
|
+
The server automatically detects and uses the best available WebSocket implementation:
|
|
203
|
+
|
|
204
|
+
1. **Deno**: Uses native `Deno.upgradeWebSocket()` API
|
|
205
|
+
2. **Bun**: Uses native `Bun.serve()` WebSocket handlers
|
|
206
|
+
3. **Node.js 24+** (stable): Uses native `node:ws` module
|
|
207
|
+
4. **Earlier Node.js**: Uses built-in RFC 6455 polyfill
|
|
208
|
+
|
|
209
|
+
```javascript
|
|
210
|
+
// Automatic - no configuration needed
|
|
211
|
+
ape(server, { where: 'api' })
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Polyfill Features
|
|
215
|
+
|
|
216
|
+
The built-in polyfill implements:
|
|
217
|
+
|
|
218
|
+
- Full RFC 6455 handshake (SHA-1 + GUID)
|
|
219
|
+
- Text and binary frames
|
|
220
|
+
- Frame fragmentation
|
|
221
|
+
- Ping/pong heartbeats
|
|
222
|
+
- Proper close handshake
|
|
223
|
+
- Masking (client→server)
|
package/server/lib/broadcast.js
CHANGED
|
@@ -11,31 +11,48 @@ const connectedClients = new Set()
|
|
|
11
11
|
*/
|
|
12
12
|
function addClient(clientInfo) {
|
|
13
13
|
connectedClients.add(clientInfo)
|
|
14
|
+
console.log(`🟢 Client added: ${clientInfo.clientId} (total: ${connectedClients.size})`)
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Remove a client from the connected set
|
|
19
|
+
* Accepts either the client object or { clientId } for lookup
|
|
18
20
|
*/
|
|
19
21
|
function removeClient(clientInfo) {
|
|
20
|
-
connectedClients.
|
|
22
|
+
const sizeBefore = connectedClients.size
|
|
23
|
+
// If exact reference found, delete it
|
|
24
|
+
if (connectedClients.has(clientInfo)) {
|
|
25
|
+
connectedClients.delete(clientInfo)
|
|
26
|
+
console.log(`🔴 Client removed (ref): ${clientInfo.clientId} (total: ${connectedClients.size})`)
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
// Otherwise search by clientId (needed for long polling cleanup)
|
|
30
|
+
for (const client of connectedClients) {
|
|
31
|
+
if (client.clientId === clientInfo.clientId) {
|
|
32
|
+
connectedClients.delete(client)
|
|
33
|
+
console.log(`🔴 Client removed (lookup): ${clientInfo.clientId} (total: ${connectedClients.size})`)
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
console.log(`⚠️ Client not found for removal: ${clientInfo.clientId} (total: ${connectedClients.size})`)
|
|
21
38
|
}
|
|
22
39
|
|
|
23
40
|
/**
|
|
24
41
|
* Broadcast to all connected clients
|
|
25
42
|
* @param {string} type - Message type
|
|
26
43
|
* @param {any} data - Data to send
|
|
27
|
-
* @param {string} [
|
|
44
|
+
* @param {string} [excludeClientId] - Optional clientId to exclude (e.g., sender)
|
|
28
45
|
*/
|
|
29
|
-
function broadcast(type, data,
|
|
30
|
-
console.log(`📢 Broadcasting "${type}" to ${connectedClients.size} clients`,
|
|
46
|
+
function broadcast(type, data, excludeClientId) {
|
|
47
|
+
console.log(`📢 Broadcasting "${type}" to ${connectedClients.size} clients`, excludeClientId ? `(excluding ${excludeClientId})` : '')
|
|
31
48
|
connectedClients.forEach(client => {
|
|
32
|
-
if (
|
|
49
|
+
if (excludeClientId && client.clientId === excludeClientId) {
|
|
33
50
|
return // Skip excluded client
|
|
34
51
|
}
|
|
35
52
|
try {
|
|
36
53
|
client.send(false, type, data, false)
|
|
37
54
|
} catch (e) {
|
|
38
|
-
console.error(`📢 Broadcast failed to ${client.
|
|
55
|
+
console.error(`📢 Broadcast failed to ${client.clientId}:`, e.message)
|
|
39
56
|
}
|
|
40
57
|
})
|
|
41
58
|
}
|
|
@@ -48,10 +65,10 @@ function online() {
|
|
|
48
65
|
}
|
|
49
66
|
|
|
50
67
|
/**
|
|
51
|
-
* Get all connected client
|
|
68
|
+
* Get all connected client clientIds
|
|
52
69
|
*/
|
|
53
70
|
function getClients() {
|
|
54
|
-
return Array.from(connectedClients).map(c => c.
|
|
71
|
+
return Array.from(connectedClients).map(c => c.clientId)
|
|
55
72
|
}
|
|
56
73
|
|
|
57
74
|
module.exports = {
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bun-specific api-ape integration
|
|
3
|
+
* Returns handlers for use with Bun.serve()
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { apeBun } from 'api-ape/bun'
|
|
8
|
+
*
|
|
9
|
+
* const ape = apeBun({ where: 'api', onConnent: ... })
|
|
10
|
+
*
|
|
11
|
+
* Bun.serve({
|
|
12
|
+
* port: 3000,
|
|
13
|
+
* fetch: ape.fetch,
|
|
14
|
+
* websocket: ape.websocket
|
|
15
|
+
* })
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const loader = require('./loader')
|
|
20
|
+
const wiring = require('./wiring')
|
|
21
|
+
const path = require('path')
|
|
22
|
+
const fs = require('fs')
|
|
23
|
+
const { getFileTransferManager } = require('./fileTransfer')
|
|
24
|
+
const { BunWebSocket, BunWebSocketServer } = require('./ws/adapters/bun')
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Create api-ape handlers for Bun.serve()
|
|
28
|
+
* @param {{ where: string, onConnent?: Function, fileTransferOptions?: Object }} options
|
|
29
|
+
*/
|
|
30
|
+
function apeBun({ where, onConnent, fileTransferOptions }) {
|
|
31
|
+
const controllers = loader(where)
|
|
32
|
+
const fileTransfer = getFileTransferManager(fileTransferOptions)
|
|
33
|
+
const wss = new BunWebSocketServer({ noServer: true })
|
|
34
|
+
|
|
35
|
+
const wsPath = `/${where}/ape`
|
|
36
|
+
const wiringHandler = wiring(controllers, onConnent, fileTransfer)
|
|
37
|
+
|
|
38
|
+
// Handle connections
|
|
39
|
+
wss.on('connection', wiringHandler)
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Bun fetch handler - handles HTTP requests and WebSocket upgrades
|
|
43
|
+
*/
|
|
44
|
+
function fetch(req, server) {
|
|
45
|
+
const url = new URL(req.url)
|
|
46
|
+
const pathname = url.pathname
|
|
47
|
+
|
|
48
|
+
// WebSocket upgrade
|
|
49
|
+
if (pathname === wsPath) {
|
|
50
|
+
const upgrade = req.headers.get('upgrade')
|
|
51
|
+
if (upgrade?.toLowerCase() === 'websocket') {
|
|
52
|
+
// Use Bun's native upgrade
|
|
53
|
+
const success = server.upgrade(req, {
|
|
54
|
+
data: { req }
|
|
55
|
+
})
|
|
56
|
+
if (success) {
|
|
57
|
+
return undefined // Bun handles the response
|
|
58
|
+
}
|
|
59
|
+
return new Response('WebSocket upgrade failed', { status: 500 })
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Serve client bundle
|
|
64
|
+
if (pathname === `/${where}/ape.js`) {
|
|
65
|
+
try {
|
|
66
|
+
const filePath = path.join(__dirname, '../../dist/ape.js')
|
|
67
|
+
const data = fs.readFileSync(filePath)
|
|
68
|
+
return new Response(data, {
|
|
69
|
+
headers: { 'Content-Type': 'application/javascript' }
|
|
70
|
+
})
|
|
71
|
+
} catch {
|
|
72
|
+
return new Response('Client bundle not found', { status: 500 })
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Not an api-ape route
|
|
77
|
+
return null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Bun websocket handlers
|
|
82
|
+
*/
|
|
83
|
+
const websocket = {
|
|
84
|
+
open(ws) {
|
|
85
|
+
const wrapper = new BunWebSocket(ws)
|
|
86
|
+
wss._clients.set(ws, wrapper)
|
|
87
|
+
|
|
88
|
+
const { req } = ws.data || {}
|
|
89
|
+
wiringHandler(wrapper, req)
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
message(ws, message) {
|
|
93
|
+
const wrapper = wss._clients.get(ws)
|
|
94
|
+
if (wrapper) {
|
|
95
|
+
wrapper._onMessage(message)
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
close(ws, code, reason) {
|
|
100
|
+
const wrapper = wss._clients.get(ws)
|
|
101
|
+
if (wrapper) {
|
|
102
|
+
wrapper._onClose(code, reason)
|
|
103
|
+
wss._clients.delete(ws)
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
error(ws, error) {
|
|
108
|
+
const wrapper = wss._clients.get(ws)
|
|
109
|
+
if (wrapper) {
|
|
110
|
+
wrapper._onError(error)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
fetch,
|
|
117
|
+
websocket,
|
|
118
|
+
wss
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = { apeBun }
|
|
@@ -2,35 +2,35 @@ const { addClient, removeClient, broadcast } = require('./broadcast')
|
|
|
2
2
|
const makeid = require('../utils/genId')
|
|
3
3
|
const jss = require('../../utils/jss')
|
|
4
4
|
|
|
5
|
-
// Active streaming connections:
|
|
5
|
+
// Active streaming connections: clientId -> { res, messageQueue, heartbeatTimer }
|
|
6
6
|
const streamClients = new Map()
|
|
7
7
|
|
|
8
8
|
// Pending message handlers for POST requests: queryId -> { resolve, reject, timer }
|
|
9
9
|
const pendingRequests = new Map()
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* Set
|
|
12
|
+
* Set apeClientId cookie if not present
|
|
13
13
|
*/
|
|
14
|
-
function
|
|
14
|
+
function ensureClientId(req, res) {
|
|
15
15
|
const cookies = req.headers.cookie || ''
|
|
16
|
-
const match = cookies.match(/(?:^|;\s*)
|
|
16
|
+
const match = cookies.match(/(?:^|;\s*)apeClientId=([^;]*)/)
|
|
17
17
|
|
|
18
18
|
if (match) {
|
|
19
19
|
return match[1]
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
// Generate new
|
|
23
|
-
const
|
|
24
|
-
res.setHeader('Set-Cookie', `
|
|
25
|
-
return
|
|
22
|
+
// Generate new clientId and set cookie
|
|
23
|
+
const clientId = makeid(20)
|
|
24
|
+
res.setHeader('Set-Cookie', `apeClientId=${clientId}; Path=/; HttpOnly; SameSite=Strict`)
|
|
25
|
+
return clientId
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
|
-
* Get
|
|
29
|
+
* Get clientId from cookie
|
|
30
30
|
*/
|
|
31
|
-
function
|
|
31
|
+
function getClientId(req) {
|
|
32
32
|
const cookies = req.headers.cookie || ''
|
|
33
|
-
const match = cookies.match(/(?:^|;\s*)
|
|
33
|
+
const match = cookies.match(/(?:^|;\s*)apeClientId=([^;]*)/)
|
|
34
34
|
return match ? match[1] : null
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -52,7 +52,7 @@ function createLongPollingHandler(controllers, onConnent, fileTransfer) {
|
|
|
52
52
|
* Keeps connection open and writes JSON messages as they arrive
|
|
53
53
|
*/
|
|
54
54
|
function handleStreamGet(req, res) {
|
|
55
|
-
const
|
|
55
|
+
const clientId = ensureClientId(req, res)
|
|
56
56
|
|
|
57
57
|
// Set up streaming response headers
|
|
58
58
|
res.writeHead(200, {
|
|
@@ -81,7 +81,7 @@ function createLongPollingHandler(controllers, onConnent, fileTransfer) {
|
|
|
81
81
|
cleanup()
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
|
-
send.toString = () =>
|
|
84
|
+
send.toString = () => clientId
|
|
85
85
|
|
|
86
86
|
// Clean up on close
|
|
87
87
|
const cleanup = () => {
|
|
@@ -92,8 +92,8 @@ function createLongPollingHandler(controllers, onConnent, fileTransfer) {
|
|
|
92
92
|
clearInterval(clientState.heartbeatTimer)
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
streamClients.delete(
|
|
96
|
-
removeClient({
|
|
95
|
+
streamClients.delete(clientId)
|
|
96
|
+
removeClient({ clientId })
|
|
97
97
|
|
|
98
98
|
// Notify disconnect handler if registered
|
|
99
99
|
if (clientState.onDisconnect) {
|
|
@@ -117,9 +117,9 @@ function createLongPollingHandler(controllers, onConnent, fileTransfer) {
|
|
|
117
117
|
}, 20000)
|
|
118
118
|
|
|
119
119
|
// Register client for broadcasts
|
|
120
|
-
const clientInfo = {
|
|
120
|
+
const clientInfo = { clientId, send }
|
|
121
121
|
addClient(clientInfo)
|
|
122
|
-
streamClients.set(
|
|
122
|
+
streamClients.set(clientId, clientState)
|
|
123
123
|
|
|
124
124
|
// Call onConnent hook if provided
|
|
125
125
|
if (onConnent) {
|
|
@@ -154,9 +154,9 @@ function createLongPollingHandler(controllers, onConnent, fileTransfer) {
|
|
|
154
154
|
* Process message through controllers, return response
|
|
155
155
|
*/
|
|
156
156
|
function handleStreamPost(req, res, controllers) {
|
|
157
|
-
const
|
|
157
|
+
const clientId = getClientId(req)
|
|
158
158
|
|
|
159
|
-
if (!
|
|
159
|
+
if (!clientId) {
|
|
160
160
|
return sendJson(res, 401, { error: 'Missing session. GET /api/ape/poll first.' })
|
|
161
161
|
}
|
|
162
162
|
|
|
@@ -178,16 +178,21 @@ function createLongPollingHandler(controllers, onConnent, fileTransfer) {
|
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
// Get client state for embed values
|
|
181
|
-
const clientState = streamClients.get(
|
|
181
|
+
const clientState = streamClients.get(clientId)
|
|
182
182
|
const embedValues = clientState?.embed || {}
|
|
183
183
|
|
|
184
|
+
// Extract sessionId from cookies (set by outer framework)
|
|
185
|
+
const sessionIdMatch = (req.headers.cookie || '').match(/(?:^|;\s*)sessionId=([^;]*)/)
|
|
186
|
+
const sessionId = sessionIdMatch ? sessionIdMatch[1] : null
|
|
187
|
+
|
|
184
188
|
// Build controller context
|
|
185
189
|
const context = {
|
|
186
190
|
...embedValues,
|
|
187
|
-
|
|
191
|
+
clientId,
|
|
192
|
+
sessionId, // Session ID from cookie (set by outer framework)
|
|
188
193
|
req,
|
|
189
194
|
broadcast: (t, d) => broadcast(t, d),
|
|
190
|
-
broadcastOthers: (t, d) => broadcast(t, d,
|
|
195
|
+
broadcastOthers: (t, d) => broadcast(t, d, clientId),
|
|
191
196
|
online: () => streamClients.size,
|
|
192
197
|
getClients: () => Array.from(streamClients.keys())
|
|
193
198
|
}
|
|
@@ -218,4 +223,4 @@ function createLongPollingHandler(controllers, onConnent, fileTransfer) {
|
|
|
218
223
|
}
|
|
219
224
|
}
|
|
220
225
|
|
|
221
|
-
module.exports = { createLongPollingHandler,
|
|
226
|
+
module.exports = { createLongPollingHandler, getClientId, ensureClientId }
|