api-ape 2.2.2 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -17
- package/client/browser.js +7 -7
- package/client/connectSocket.js +257 -22
- package/client/index.js +3 -3
- package/dist/ape.js +1 -1
- package/dist/ape.js.map +3 -3
- package/dist/api-ape.min.js +1 -1
- package/dist/api-ape.min.js.map +3 -3
- package/index.d.ts +183 -19
- package/package.json +2 -2
- package/server/README.md +311 -5
- package/server/adapters/README.md +275 -0
- package/server/adapters/firebase.js +172 -0
- package/server/adapters/index.js +144 -0
- package/server/adapters/mongo.js +161 -0
- package/server/adapters/postgres.js +177 -0
- package/server/adapters/redis.js +154 -0
- package/server/adapters/supabase.js +199 -0
- package/server/index.js +3 -3
- package/server/lib/broadcast.js +115 -49
- package/server/lib/bun.js +4 -4
- package/server/lib/fileTransfer.js +129 -0
- package/server/lib/longPolling.js +22 -13
- package/server/lib/main.js +40 -8
- package/server/lib/wiring.js +23 -19
- package/server/socket/receive.js +46 -0
- package/server/socket/send.js +7 -0
package/server/index.js
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const ape = require('./lib/main')
|
|
7
|
-
const { broadcast,
|
|
7
|
+
const { broadcast, clients } = require('./lib/broadcast')
|
|
8
8
|
|
|
9
9
|
// Attach broadcast utilities to the main function for clean exports
|
|
10
10
|
ape.broadcast = broadcast
|
|
11
|
-
ape.
|
|
12
|
-
ape.getClients = getClients
|
|
11
|
+
ape.clients = clients
|
|
13
12
|
|
|
14
13
|
module.exports = ape
|
|
14
|
+
|
package/server/lib/broadcast.js
CHANGED
|
@@ -1,40 +1,120 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Client tracking and broadcast utilities for api-ape
|
|
3
|
+
* Provides a Map of connected clients with sendTo functionality
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
//
|
|
7
|
-
const
|
|
6
|
+
// Internal Map of connected clients: clientId -> ClientWrapper
|
|
7
|
+
const _clients = new Map()
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
10
|
+
* Create a ClientWrapper that exposes client info and sendTo function
|
|
11
|
+
* @param {object} clientInfo - Raw client info from wiring/longPolling
|
|
12
|
+
*/
|
|
13
|
+
function createClientWrapper(clientInfo) {
|
|
14
|
+
return {
|
|
15
|
+
get clientId() { return clientInfo.clientId },
|
|
16
|
+
get sessionId() { return clientInfo.sessionId || null },
|
|
17
|
+
get embed() { return clientInfo.embed || {} },
|
|
18
|
+
get agent() { return clientInfo.agent || {} },
|
|
19
|
+
/**
|
|
20
|
+
* Send a message to this specific client
|
|
21
|
+
* @param {string} type - Message type
|
|
22
|
+
* @param {any} data - Data to send
|
|
23
|
+
*/
|
|
24
|
+
sendTo(type, data) {
|
|
25
|
+
if (clientInfo.send) {
|
|
26
|
+
try {
|
|
27
|
+
clientInfo.send(false, type, data, false)
|
|
28
|
+
} catch (e) {
|
|
29
|
+
console.error(`📢 sendTo failed for ${clientInfo.clientId}:`, e.message)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Read-only proxy for the clients Map
|
|
38
|
+
* Allows: get, has, keys, values, entries, forEach, size, iteration
|
|
39
|
+
* Prevents: set, delete, clear (throws error if attempted)
|
|
40
|
+
*/
|
|
41
|
+
const clients = new Proxy(_clients, {
|
|
42
|
+
get(target, prop) {
|
|
43
|
+
// Prevent mutation methods
|
|
44
|
+
if (prop === 'set' || prop === 'delete' || prop === 'clear') {
|
|
45
|
+
return () => {
|
|
46
|
+
throw new Error(`ape.clients.${prop}() is not allowed. Clients are managed internally by api-ape.`)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Allow size property
|
|
51
|
+
if (prop === 'size') {
|
|
52
|
+
return target.size
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Bind methods to target
|
|
56
|
+
const value = target[prop]
|
|
57
|
+
if (typeof value === 'function') {
|
|
58
|
+
return value.bind(target)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return value
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Add a client to the connected map (internal use only)
|
|
67
|
+
* @param {object} clientInfo - { clientId, sessionId, agent, embed, send }
|
|
11
68
|
*/
|
|
12
69
|
function addClient(clientInfo) {
|
|
13
|
-
|
|
14
|
-
|
|
70
|
+
const wrapper = createClientWrapper(clientInfo)
|
|
71
|
+
_clients.set(clientInfo.clientId, wrapper)
|
|
72
|
+
|
|
73
|
+
// Store reference to raw info so we can update embed later if needed
|
|
74
|
+
wrapper._raw = clientInfo
|
|
75
|
+
|
|
76
|
+
console.log(`🟢 Client added: ${clientInfo.clientId} (total: ${_clients.size})`)
|
|
15
77
|
}
|
|
16
78
|
|
|
17
79
|
/**
|
|
18
|
-
* Remove a client from the connected
|
|
19
|
-
*
|
|
80
|
+
* Remove a client from the connected map (internal use only)
|
|
81
|
+
* @param {string|object} clientIdOrInfo - clientId string or { clientId } object
|
|
20
82
|
*/
|
|
21
|
-
function removeClient(
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
83
|
+
function removeClient(clientIdOrInfo) {
|
|
84
|
+
const clientId = typeof clientIdOrInfo === 'string'
|
|
85
|
+
? clientIdOrInfo
|
|
86
|
+
: clientIdOrInfo.clientId
|
|
87
|
+
|
|
88
|
+
if (_clients.has(clientId)) {
|
|
89
|
+
_clients.delete(clientId)
|
|
90
|
+
console.log(`🔴 Client removed: ${clientId} (total: ${_clients.size})`)
|
|
91
|
+
} else {
|
|
92
|
+
console.log(`⚠️ Client not found for removal: ${clientId} (total: ${_clients.size})`)
|
|
28
93
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Update a client's embed values after onConnect resolves (internal use only)
|
|
98
|
+
* @param {string} clientId
|
|
99
|
+
* @param {object} embed
|
|
100
|
+
*/
|
|
101
|
+
function updateClientEmbed(clientId, embed) {
|
|
102
|
+
const wrapper = _clients.get(clientId)
|
|
103
|
+
if (wrapper && wrapper._raw) {
|
|
104
|
+
wrapper._raw.embed = embed
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Update a client's send function after it's ready (internal use only)
|
|
110
|
+
* @param {string} clientId
|
|
111
|
+
* @param {function} send
|
|
112
|
+
*/
|
|
113
|
+
function updateClientSend(clientId, send) {
|
|
114
|
+
const wrapper = _clients.get(clientId)
|
|
115
|
+
if (wrapper && wrapper._raw) {
|
|
116
|
+
wrapper._raw.send = send
|
|
36
117
|
}
|
|
37
|
-
console.log(`⚠️ Client not found for removal: ${clientInfo.clientId} (total: ${connectedClients.size})`)
|
|
38
118
|
}
|
|
39
119
|
|
|
40
120
|
/**
|
|
@@ -44,37 +124,23 @@ function removeClient(clientInfo) {
|
|
|
44
124
|
* @param {string} [excludeClientId] - Optional clientId to exclude (e.g., sender)
|
|
45
125
|
*/
|
|
46
126
|
function broadcast(type, data, excludeClientId) {
|
|
47
|
-
console.log(`📢 Broadcasting "${type}" to ${
|
|
48
|
-
|
|
49
|
-
if (excludeClientId &&
|
|
127
|
+
console.log(`📢 Broadcasting "${type}" to ${_clients.size} clients`, excludeClientId ? `(excluding ${excludeClientId})` : '')
|
|
128
|
+
_clients.forEach((wrapper, clientId) => {
|
|
129
|
+
if (excludeClientId && clientId === excludeClientId) {
|
|
50
130
|
return // Skip excluded client
|
|
51
131
|
}
|
|
52
|
-
|
|
53
|
-
client.send(false, type, data, false)
|
|
54
|
-
} catch (e) {
|
|
55
|
-
console.error(`📢 Broadcast failed to ${client.clientId}:`, e.message)
|
|
56
|
-
}
|
|
132
|
+
wrapper.sendTo(type, data)
|
|
57
133
|
})
|
|
58
134
|
}
|
|
59
135
|
|
|
60
|
-
/**
|
|
61
|
-
* Get count of online clients
|
|
62
|
-
*/
|
|
63
|
-
function online() {
|
|
64
|
-
return connectedClients.size
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Get all connected client clientIds
|
|
69
|
-
*/
|
|
70
|
-
function getClients() {
|
|
71
|
-
return Array.from(connectedClients).map(c => c.clientId)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
136
|
module.exports = {
|
|
137
|
+
// Public: read-only clients Map
|
|
138
|
+
clients,
|
|
139
|
+
// Public: broadcast function
|
|
140
|
+
broadcast,
|
|
141
|
+
// Internal: client management (used by wiring.js and longPolling.js)
|
|
75
142
|
addClient,
|
|
76
143
|
removeClient,
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
getClients
|
|
144
|
+
updateClientEmbed,
|
|
145
|
+
updateClientSend
|
|
80
146
|
}
|
package/server/lib/bun.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* ```ts
|
|
7
7
|
* import { apeBun } from 'api-ape/bun'
|
|
8
8
|
*
|
|
9
|
-
* const ape = apeBun({ where: 'api',
|
|
9
|
+
* const ape = apeBun({ where: 'api', onConnect: ... })
|
|
10
10
|
*
|
|
11
11
|
* Bun.serve({
|
|
12
12
|
* port: 3000,
|
|
@@ -25,15 +25,15 @@ const { BunWebSocket, BunWebSocketServer } = require('./ws/adapters/bun')
|
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* Create api-ape handlers for Bun.serve()
|
|
28
|
-
* @param {{ where: string,
|
|
28
|
+
* @param {{ where: string, onConnect?: Function, fileTransferOptions?: Object }} options
|
|
29
29
|
*/
|
|
30
|
-
function apeBun({ where,
|
|
30
|
+
function apeBun({ where, onConnect, fileTransferOptions }) {
|
|
31
31
|
const controllers = loader(where)
|
|
32
32
|
const fileTransfer = getFileTransferManager(fileTransferOptions)
|
|
33
33
|
const wss = new BunWebSocketServer({ noServer: true })
|
|
34
34
|
|
|
35
35
|
const wsPath = `/${where}/ape`
|
|
36
|
-
const wiringHandler = wiring(controllers,
|
|
36
|
+
const wiringHandler = wiring(controllers, onConnect, fileTransfer)
|
|
37
37
|
|
|
38
38
|
// Handle connections
|
|
39
39
|
wss.on('connection', wiringHandler)
|
|
@@ -28,10 +28,124 @@ class FileTransferManager {
|
|
|
28
28
|
// Map<`${queryId}/${pathHash}`, { sessionHostId, createdAt, resolver, rejector, timer, data }>
|
|
29
29
|
this.pendingUploads = new Map()
|
|
30
30
|
|
|
31
|
+
// Map<fileId, StreamingFileEntry> - for client-to-client streaming
|
|
32
|
+
// StreamingFileEntry: { uploaderId, chunks[], totalReceived, isComplete, createdAt, timer }
|
|
33
|
+
this.streamingFiles = new Map()
|
|
34
|
+
|
|
31
35
|
// Cleanup interval
|
|
32
36
|
this._cleanupInterval = setInterval(() => this._cleanup(), 30000)
|
|
33
37
|
}
|
|
34
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Register a streaming file (client-to-client transfer)
|
|
41
|
+
* Called when <!F> tag is detected in incoming message
|
|
42
|
+
* @param {string} fileId - Unique file identifier
|
|
43
|
+
* @param {string} uploaderId - Client ID of uploader
|
|
44
|
+
* @returns {string} The fileId
|
|
45
|
+
*/
|
|
46
|
+
registerStreamingFile(fileId, uploaderId) {
|
|
47
|
+
// Clear existing entry if any
|
|
48
|
+
if (this.streamingFiles.has(fileId)) {
|
|
49
|
+
const existing = this.streamingFiles.get(fileId)
|
|
50
|
+
if (existing.timer) clearTimeout(existing.timer)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const entry = {
|
|
54
|
+
uploaderId,
|
|
55
|
+
chunks: [],
|
|
56
|
+
totalReceived: 0,
|
|
57
|
+
isComplete: false,
|
|
58
|
+
createdAt: Date.now(),
|
|
59
|
+
timer: setTimeout(() => {
|
|
60
|
+
this.streamingFiles.delete(fileId)
|
|
61
|
+
console.log(`📦 Streaming file expired: ${fileId}`)
|
|
62
|
+
}, this.startTimeout + this.completeTimeout)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.streamingFiles.set(fileId, entry)
|
|
66
|
+
console.log(`📦 Registered streaming file: ${fileId} from ${uploaderId}`)
|
|
67
|
+
return fileId
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Append a chunk to a streaming file
|
|
72
|
+
* @param {string} fileId - File identifier
|
|
73
|
+
* @param {Buffer} chunk - Data chunk
|
|
74
|
+
* @returns {boolean} True if accepted
|
|
75
|
+
*/
|
|
76
|
+
appendChunk(fileId, chunk) {
|
|
77
|
+
const entry = this.streamingFiles.get(fileId)
|
|
78
|
+
if (!entry) {
|
|
79
|
+
console.warn(`📦 Streaming file not found: ${fileId}`)
|
|
80
|
+
return false
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
entry.chunks.push(chunk)
|
|
84
|
+
entry.totalReceived += chunk.length
|
|
85
|
+
return true
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Mark streaming file as complete
|
|
90
|
+
* @param {string} fileId - File identifier
|
|
91
|
+
* @param {Buffer} data - Complete file data (if not chunked)
|
|
92
|
+
* @returns {boolean} True if successful
|
|
93
|
+
*/
|
|
94
|
+
completeStreamingUpload(fileId, data) {
|
|
95
|
+
const entry = this.streamingFiles.get(fileId)
|
|
96
|
+
if (!entry) {
|
|
97
|
+
console.warn(`📦 Streaming file not found for completion: ${fileId}`)
|
|
98
|
+
return false
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (data) {
|
|
102
|
+
entry.chunks = [data]
|
|
103
|
+
entry.totalReceived = data.length
|
|
104
|
+
}
|
|
105
|
+
entry.isComplete = true
|
|
106
|
+
|
|
107
|
+
// Reset timer for cleanup after completion
|
|
108
|
+
clearTimeout(entry.timer)
|
|
109
|
+
entry.timer = setTimeout(() => {
|
|
110
|
+
this.streamingFiles.delete(fileId)
|
|
111
|
+
console.log(`📦 Streaming file cleaned up: ${fileId}`)
|
|
112
|
+
}, this.completeTimeout)
|
|
113
|
+
|
|
114
|
+
console.log(`📦 Streaming upload complete: ${fileId} (${entry.totalReceived} bytes)`)
|
|
115
|
+
return true
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get streaming file data (available bytes so far)
|
|
120
|
+
* @param {string} fileId - File identifier
|
|
121
|
+
* @param {number} offset - Byte offset to start from (for resumable downloads)
|
|
122
|
+
* @returns {{ data: Buffer, isComplete: boolean, totalReceived: number } | null}
|
|
123
|
+
*/
|
|
124
|
+
getStreamingFile(fileId, offset = 0) {
|
|
125
|
+
const entry = this.streamingFiles.get(fileId)
|
|
126
|
+
if (!entry) {
|
|
127
|
+
return null
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Concatenate chunks
|
|
131
|
+
const data = Buffer.concat(entry.chunks)
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
data: offset > 0 ? data.slice(offset) : data,
|
|
135
|
+
isComplete: entry.isComplete,
|
|
136
|
+
totalReceived: entry.totalReceived
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Check if a file ID is a streaming file
|
|
142
|
+
* @param {string} fileId - File identifier
|
|
143
|
+
* @returns {boolean}
|
|
144
|
+
*/
|
|
145
|
+
isStreamingFile(fileId) {
|
|
146
|
+
return this.streamingFiles.has(fileId)
|
|
147
|
+
}
|
|
148
|
+
|
|
35
149
|
/**
|
|
36
150
|
* Register a binary download
|
|
37
151
|
* @param {string} hash - Unique hash for this download
|
|
@@ -210,6 +324,15 @@ class FileTransferManager {
|
|
|
210
324
|
console.log(`📤 Cleanup stale upload: ${key}`)
|
|
211
325
|
}
|
|
212
326
|
}
|
|
327
|
+
|
|
328
|
+
// Cleanup streaming files
|
|
329
|
+
for (const [fileId, entry] of this.streamingFiles) {
|
|
330
|
+
if (now - entry.createdAt > maxAge) {
|
|
331
|
+
clearTimeout(entry.timer)
|
|
332
|
+
this.streamingFiles.delete(fileId)
|
|
333
|
+
console.log(`📦 Cleanup stale streaming file: ${fileId}`)
|
|
334
|
+
}
|
|
335
|
+
}
|
|
213
336
|
}
|
|
214
337
|
|
|
215
338
|
/**
|
|
@@ -228,6 +351,12 @@ class FileTransferManager {
|
|
|
228
351
|
|
|
229
352
|
this.pendingDownloads.clear()
|
|
230
353
|
this.pendingUploads.clear()
|
|
354
|
+
|
|
355
|
+
// Clear streaming file timers
|
|
356
|
+
for (const entry of this.streamingFiles.values()) {
|
|
357
|
+
clearTimeout(entry.timer)
|
|
358
|
+
}
|
|
359
|
+
this.streamingFiles.clear()
|
|
231
360
|
}
|
|
232
361
|
}
|
|
233
362
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
const { addClient, removeClient, broadcast } = require('./broadcast')
|
|
1
|
+
const { addClient, removeClient, broadcast, clients, updateClientEmbed } = require('./broadcast')
|
|
2
2
|
const makeid = require('../utils/genId')
|
|
3
3
|
const jss = require('../../utils/jss')
|
|
4
|
+
const parseUserAgent = require('../utils/parseUserAgent')
|
|
4
5
|
|
|
5
6
|
// Active streaming connections: clientId -> { res, messageQueue, heartbeatTimer }
|
|
6
7
|
const streamClients = new Map()
|
|
@@ -45,7 +46,7 @@ function sendJson(res, statusCode, data) {
|
|
|
45
46
|
/**
|
|
46
47
|
* Create long polling handler
|
|
47
48
|
*/
|
|
48
|
-
function createLongPollingHandler(controllers,
|
|
49
|
+
function createLongPollingHandler(controllers, onConnect, fileTransfer) {
|
|
49
50
|
|
|
50
51
|
/**
|
|
51
52
|
* Handle GET /api/ape/poll - Streaming receive
|
|
@@ -116,26 +117,33 @@ function createLongPollingHandler(controllers, onConnent, fileTransfer) {
|
|
|
116
117
|
}
|
|
117
118
|
}, 20000)
|
|
118
119
|
|
|
119
|
-
//
|
|
120
|
-
const
|
|
121
|
-
|
|
120
|
+
// Extract sessionId from cookies
|
|
121
|
+
const sessionIdMatch = (req.headers.cookie || '').match(/(?:^|;\s*)sessionId=([^;]*)/)
|
|
122
|
+
const sessionId = sessionIdMatch ? sessionIdMatch[1] : null
|
|
123
|
+
|
|
124
|
+
// Parse user agent
|
|
125
|
+
const agent = parseUserAgent(req.headers['user-agent'])
|
|
126
|
+
|
|
127
|
+
// Register client for broadcasts with full metadata
|
|
128
|
+
addClient({ clientId, sessionId, agent, send, embed: null })
|
|
122
129
|
streamClients.set(clientId, clientState)
|
|
123
130
|
|
|
124
|
-
// Call
|
|
125
|
-
if (
|
|
126
|
-
Promise.resolve(
|
|
131
|
+
// Call onConnect hook if provided
|
|
132
|
+
if (onConnect) {
|
|
133
|
+
Promise.resolve(onConnect(null, req, send))
|
|
127
134
|
.then(handlers => {
|
|
128
135
|
if (handlers) {
|
|
129
|
-
if (handlers.
|
|
130
|
-
clientState.onDisconnect = handlers.
|
|
136
|
+
if (handlers.onDisconnect) {
|
|
137
|
+
clientState.onDisconnect = handlers.onDisconnect
|
|
131
138
|
}
|
|
132
139
|
if (handlers.embed) {
|
|
133
140
|
clientState.embed = handlers.embed
|
|
141
|
+
updateClientEmbed(clientId, handlers.embed)
|
|
134
142
|
}
|
|
135
143
|
}
|
|
136
144
|
})
|
|
137
145
|
.catch(err => {
|
|
138
|
-
console.error('
|
|
146
|
+
console.error('onConnect error:', err)
|
|
139
147
|
})
|
|
140
148
|
}
|
|
141
149
|
|
|
@@ -193,8 +201,9 @@ function createLongPollingHandler(controllers, onConnent, fileTransfer) {
|
|
|
193
201
|
req,
|
|
194
202
|
broadcast: (t, d) => broadcast(t, d),
|
|
195
203
|
broadcastOthers: (t, d) => broadcast(t, d, clientId),
|
|
196
|
-
|
|
197
|
-
|
|
204
|
+
// Use clients Map for count and list
|
|
205
|
+
online: () => clients.size,
|
|
206
|
+
getClients: () => Array.from(clients.keys())
|
|
198
207
|
}
|
|
199
208
|
|
|
200
209
|
// Execute controller
|
package/server/lib/main.js
CHANGED
|
@@ -73,11 +73,11 @@ function isSecure(req) {
|
|
|
73
73
|
/**
|
|
74
74
|
* Create core api-ape handlers (shared between runtimes)
|
|
75
75
|
*/
|
|
76
|
-
function createApeCore({ where,
|
|
76
|
+
function createApeCore({ where, onConnect, fileTransferOptions }) {
|
|
77
77
|
const controllers = loader(where)
|
|
78
78
|
const fileTransfer = getFileTransferManager(fileTransferOptions)
|
|
79
|
-
const wiringHandler = wiring(controllers,
|
|
80
|
-
const { handleStreamGet, handleStreamPost } = createLongPollingHandler(controllers,
|
|
79
|
+
const wiringHandler = wiring(controllers, onConnect, fileTransfer)
|
|
80
|
+
const { handleStreamGet, handleStreamPost } = createLongPollingHandler(controllers, onConnect, fileTransfer)
|
|
81
81
|
|
|
82
82
|
const wsPath = `/${where}/ape`
|
|
83
83
|
const pollPath = `/${where}/ape/poll`
|
|
@@ -185,6 +185,26 @@ function initNodeServer(server, options) {
|
|
|
185
185
|
const downloadMatch = matchRoute(pathname, core.downloadPattern)
|
|
186
186
|
if (req.method === 'GET' && downloadMatch) {
|
|
187
187
|
const { hash } = downloadMatch
|
|
188
|
+
|
|
189
|
+
// Check for streaming file first (client-to-client sharing, no session check)
|
|
190
|
+
const streamingFile = core.fileTransfer.getStreamingFile(hash)
|
|
191
|
+
if (streamingFile) {
|
|
192
|
+
if (!isLocalhost(req.headers.host) && !isSecure(req)) {
|
|
193
|
+
return sendJson(res, 403, { error: 'HTTPS required for file transfers' })
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Return available data (may be partial if upload still in progress)
|
|
197
|
+
res.writeHead(200, {
|
|
198
|
+
'Content-Type': 'application/octet-stream',
|
|
199
|
+
'Content-Length': streamingFile.data.length,
|
|
200
|
+
'X-Ape-Complete': streamingFile.isComplete ? '1' : '0',
|
|
201
|
+
'X-Ape-Total-Received': String(streamingFile.totalReceived)
|
|
202
|
+
})
|
|
203
|
+
res.end(streamingFile.data)
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Session-bound download (original behavior)
|
|
188
208
|
const clientId = getCookie(req.headers, 'apeClientId') || req.headers['x-ape-client-id']
|
|
189
209
|
|
|
190
210
|
if (!clientId) {
|
|
@@ -213,11 +233,6 @@ function initNodeServer(server, options) {
|
|
|
213
233
|
const uploadMatch = matchRoute(pathname, core.uploadPattern)
|
|
214
234
|
if (req.method === 'PUT' && uploadMatch) {
|
|
215
235
|
const { queryId, pathHash } = uploadMatch
|
|
216
|
-
const clientId = getCookie(req.headers, 'apeClientId') || req.headers['x-ape-client-id']
|
|
217
|
-
|
|
218
|
-
if (!clientId) {
|
|
219
|
-
return sendJson(res, 401, { error: 'Missing session identifier' })
|
|
220
|
-
}
|
|
221
236
|
|
|
222
237
|
if (!isLocalhost(req.headers.host) && !isSecure(req)) {
|
|
223
238
|
return sendJson(res, 403, { error: 'HTTPS required for file transfers' })
|
|
@@ -227,6 +242,23 @@ function initNodeServer(server, options) {
|
|
|
227
242
|
req.on('data', chunk => chunks.push(chunk))
|
|
228
243
|
req.on('end', () => {
|
|
229
244
|
const data = Buffer.concat(chunks)
|
|
245
|
+
|
|
246
|
+
// Check if this is a streaming file upload (client-to-client)
|
|
247
|
+
// For streaming files, queryId might be the fileId directly
|
|
248
|
+
if (core.fileTransfer.isStreamingFile(pathHash)) {
|
|
249
|
+
const success = core.fileTransfer.completeStreamingUpload(pathHash, data)
|
|
250
|
+
if (success) {
|
|
251
|
+
return sendJson(res, 200, { success: true, streaming: true })
|
|
252
|
+
}
|
|
253
|
+
return sendJson(res, 404, { error: 'Streaming file not found' })
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Regular upload (session-bound)
|
|
257
|
+
const clientId = getCookie(req.headers, 'apeClientId') || req.headers['x-ape-client-id']
|
|
258
|
+
if (!clientId) {
|
|
259
|
+
return sendJson(res, 401, { error: 'Missing session identifier' })
|
|
260
|
+
}
|
|
261
|
+
|
|
230
262
|
const success = core.fileTransfer.receiveUpload(queryId, pathHash, data, clientId)
|
|
231
263
|
|
|
232
264
|
if (success) {
|
package/server/lib/wiring.js
CHANGED
|
@@ -4,9 +4,9 @@ const socketReceive = require('../socket/receive')
|
|
|
4
4
|
const socketSend = require('../socket/send')
|
|
5
5
|
const makeid = require('../utils/genId')
|
|
6
6
|
const parseUserAgent = require('../utils/parseUserAgent');
|
|
7
|
-
const { addClient, removeClient } = require('./broadcast')
|
|
7
|
+
const { addClient, removeClient, updateClientEmbed, updateClientSend } = require('./broadcast')
|
|
8
8
|
|
|
9
|
-
//
|
|
9
|
+
// connect, beforeSend, beforeReceive, error, afterSend, afterReceive, disconnect
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
function defaultEvents(events = {}) {
|
|
@@ -15,7 +15,7 @@ function defaultEvents(events = {}) {
|
|
|
15
15
|
onReceive: () => { },
|
|
16
16
|
onSend: () => { },
|
|
17
17
|
onError: (errSt) => console.error(errSt),
|
|
18
|
-
|
|
18
|
+
onDisconnect: () => { },
|
|
19
19
|
} // END fallBackEvents
|
|
20
20
|
return Object.assign({}, fallBackEvents, events)
|
|
21
21
|
} // END defaultEvents
|
|
@@ -24,8 +24,8 @@ function defaultEvents(events = {}) {
|
|
|
24
24
|
//============================================== wiring
|
|
25
25
|
//=====================================================
|
|
26
26
|
|
|
27
|
-
module.exports = function wiring(controllers,
|
|
28
|
-
|
|
27
|
+
module.exports = function wiring(controllers, onConnect, fileTransfer) {
|
|
28
|
+
onConnect = onConnect || (() => { });
|
|
29
29
|
return function webSocketHandler(socket, req) {
|
|
30
30
|
|
|
31
31
|
let send;
|
|
@@ -40,31 +40,35 @@ module.exports = function wiring(controllers, onConnent, fileTransfer) {
|
|
|
40
40
|
|
|
41
41
|
const clientId = makeid(20)
|
|
42
42
|
const agent = parseUserAgent(req.headers['user-agent'])
|
|
43
|
+
|
|
44
|
+
// Extract sessionId from cookies (set by outer framework)
|
|
45
|
+
const sessionIdMatch = (req.headers.cookie || '').match(/(?:^|;\s*)sessionId=([^;]*)/)
|
|
46
|
+
const sessionId = sessionIdMatch ? sessionIdMatch[1] : null
|
|
47
|
+
|
|
43
48
|
const sharedValues = {
|
|
44
49
|
socket, req, agent, send: (type, data, err) => sentBufferFn(false, type, data, err)
|
|
45
50
|
}
|
|
46
51
|
sharedValues.send.toString = () => clientId
|
|
47
52
|
|
|
48
|
-
// Track this client for broadcast BEFORE calling
|
|
49
|
-
// This ensures ape.
|
|
50
|
-
|
|
51
|
-
addClient(clientInfo)
|
|
53
|
+
// Track this client for broadcast BEFORE calling onConnect
|
|
54
|
+
// This ensures ape.clients.size returns the correct count when sending init
|
|
55
|
+
addClient({ clientId, sessionId, agent, send: null, embed: null })
|
|
52
56
|
|
|
53
57
|
// Remove client on disconnect (set up early, will work once send is assigned)
|
|
54
58
|
socket.on('close', () => {
|
|
55
|
-
removeClient(
|
|
59
|
+
removeClient(clientId)
|
|
56
60
|
})
|
|
57
61
|
|
|
58
|
-
let result =
|
|
62
|
+
let result = onConnect(socket, req, sharedValues.send)
|
|
59
63
|
if (!result || !result.then) {
|
|
60
64
|
result = Promise.resolve(result)
|
|
61
65
|
}
|
|
62
66
|
result.then(defaultEvents)
|
|
63
|
-
.then(({ embed, onReceive, onSend, onError,
|
|
67
|
+
.then(({ embed, onReceive, onSend, onError, onDisconnect }) => {
|
|
64
68
|
const isOk = socketOpen(socket, req, onError)
|
|
65
69
|
|
|
66
70
|
if (!isOk) {
|
|
67
|
-
removeClient(
|
|
71
|
+
removeClient(clientId) // Clean up if connection fails
|
|
68
72
|
return;
|
|
69
73
|
}
|
|
70
74
|
|
|
@@ -75,7 +79,7 @@ module.exports = function wiring(controllers, onConnent, fileTransfer) {
|
|
|
75
79
|
req,
|
|
76
80
|
clientId,
|
|
77
81
|
checkReply,
|
|
78
|
-
events: { onReceive, onSend, onError,
|
|
82
|
+
events: { onReceive, onSend, onError, onDisconnect },
|
|
79
83
|
controllers,
|
|
80
84
|
sharedValues,
|
|
81
85
|
embedValues: embed,
|
|
@@ -84,13 +88,13 @@ module.exports = function wiring(controllers, onConnent, fileTransfer) {
|
|
|
84
88
|
send = socketSend(ape)
|
|
85
89
|
ape.send = send
|
|
86
90
|
|
|
87
|
-
// Update
|
|
88
|
-
|
|
89
|
-
|
|
91
|
+
// Update client with real send function and embed values
|
|
92
|
+
updateClientSend(clientId, send)
|
|
93
|
+
updateClientEmbed(clientId, embed)
|
|
90
94
|
|
|
91
|
-
// Call
|
|
95
|
+
// Call onDisconnect when socket closes
|
|
92
96
|
socket.on('close', () => {
|
|
93
|
-
|
|
97
|
+
onDisconnect()
|
|
94
98
|
})
|
|
95
99
|
|
|
96
100
|
sentBufferAr.forEach(args => send(...args))
|