api-ape 1.0.2 → 2.0.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 +63 -16
- package/client/README.md +32 -0
- package/client/browser.js +7 -1
- package/client/connectSocket.js +323 -8
- package/dist/ape.js +289 -44
- package/example/Bun/README.md +74 -0
- package/example/Bun/api/message.ts +11 -0
- package/example/Bun/index.html +76 -0
- package/example/Bun/package.json +9 -0
- package/example/Bun/server.ts +59 -0
- package/example/Bun/styles.css +128 -0
- package/example/ExpressJs/README.md +5 -7
- package/example/ExpressJs/backend.js +23 -21
- package/example/NextJs/ape/client.js +3 -3
- package/example/NextJs/ape/onConnect.js +5 -5
- package/example/NextJs/package-lock.json +1353 -60
- package/example/NextJs/package.json +0 -1
- package/example/NextJs/pages/index.tsx +21 -10
- package/example/NextJs/server.js +7 -11
- package/example/README.md +51 -0
- package/example/Vite/README.md +68 -0
- package/example/Vite/ape/client.ts +66 -0
- package/example/Vite/ape/onConnect.ts +52 -0
- package/example/Vite/api/message.ts +57 -0
- package/example/Vite/index.html +16 -0
- package/example/Vite/package.json +19 -0
- package/example/Vite/server.ts +62 -0
- package/example/Vite/src/App.vue +170 -0
- package/example/Vite/src/components/Info.vue +352 -0
- package/example/Vite/src/main.ts +5 -0
- package/example/Vite/src/style.css +200 -0
- package/example/Vite/src/vite-env.d.ts +7 -0
- package/example/Vite/vite.config.ts +20 -0
- package/index.d.ts +31 -3
- package/package.json +2 -3
- package/server/README.md +44 -0
- package/server/index.js +10 -2
- package/server/lib/fileTransfer.js +247 -0
- package/server/lib/main.js +172 -9
- package/server/lib/wiring.js +4 -2
- package/server/socket/receive.js +118 -3
- package/server/socket/send.js +97 -2
package/server/lib/main.js
CHANGED
|
@@ -1,23 +1,186 @@
|
|
|
1
1
|
const loader = require('./loader')
|
|
2
2
|
const wiring = require('./wiring')
|
|
3
|
-
const
|
|
4
|
-
const path = require('path')
|
|
3
|
+
const { WebSocketServer } = require('ws')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
const fs = require('fs')
|
|
6
|
+
const { getFileTransferManager } = require('./fileTransfer')
|
|
7
|
+
const { parse: parseUrl } = require('url')
|
|
5
8
|
|
|
6
9
|
let created = false
|
|
7
10
|
|
|
8
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Parse URL path parameters like /api/ape/data/:hash
|
|
13
|
+
* Returns null if pattern doesn't match, or object with params if it does
|
|
14
|
+
*/
|
|
15
|
+
function matchRoute(pathname, pattern) {
|
|
16
|
+
const patternParts = pattern.split('/')
|
|
17
|
+
const pathParts = pathname.split('/')
|
|
18
|
+
|
|
19
|
+
if (patternParts.length !== pathParts.length) {
|
|
20
|
+
return null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const params = {}
|
|
24
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
25
|
+
if (patternParts[i].startsWith(':')) {
|
|
26
|
+
params[patternParts[i].slice(1)] = pathParts[i]
|
|
27
|
+
} else if (patternParts[i] !== pathParts[i]) {
|
|
28
|
+
return null
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return params
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Send JSON response
|
|
36
|
+
*/
|
|
37
|
+
function sendJson(res, statusCode, data) {
|
|
38
|
+
res.writeHead(statusCode, { 'Content-Type': 'application/json' })
|
|
39
|
+
res.end(JSON.stringify(data))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get cookie value from request
|
|
44
|
+
*/
|
|
45
|
+
function getCookie(req, name) {
|
|
46
|
+
const cookies = req.headers.cookie
|
|
47
|
+
if (!cookies) return null
|
|
48
|
+
const match = cookies.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`))
|
|
49
|
+
return match ? match[1] : null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check if request is from localhost
|
|
54
|
+
*/
|
|
55
|
+
function isLocalhost(req) {
|
|
56
|
+
const host = req.headers.host?.split(':')[0] || ''
|
|
57
|
+
return ['localhost', '127.0.0.1', '[::1]'].includes(host)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Check if connection is secure (HTTPS)
|
|
62
|
+
*/
|
|
63
|
+
function isSecure(req) {
|
|
64
|
+
return req.socket?.encrypted || req.headers['x-forwarded-proto'] === 'https'
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = function (server, { where, onConnent, fileTransferOptions }) {
|
|
9
68
|
|
|
10
69
|
if (created) {
|
|
11
70
|
throw new Error("Api-Ape already started")
|
|
12
71
|
}
|
|
13
|
-
created = true
|
|
14
|
-
|
|
72
|
+
created = true
|
|
73
|
+
|
|
15
74
|
const controllers = loader(where)
|
|
75
|
+
const fileTransfer = getFileTransferManager(fileTransferOptions)
|
|
76
|
+
|
|
77
|
+
// Create WebSocket server attached to the HTTP server
|
|
78
|
+
const wss = new WebSocketServer({ noServer: true })
|
|
79
|
+
|
|
80
|
+
// Handle WebSocket connections
|
|
81
|
+
const wsPath = `/${where}/ape`
|
|
82
|
+
const wiringHandler = wiring(controllers, onConnent, fileTransfer)
|
|
16
83
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
84
|
+
wss.on('connection', wiringHandler)
|
|
85
|
+
|
|
86
|
+
// Handle HTTP upgrade requests for WebSocket
|
|
87
|
+
server.on('upgrade', (req, socket, head) => {
|
|
88
|
+
const { pathname } = parseUrl(req.url)
|
|
89
|
+
|
|
90
|
+
if (pathname === wsPath) {
|
|
91
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
92
|
+
wss.emit('connection', ws, req)
|
|
93
|
+
})
|
|
94
|
+
} else {
|
|
95
|
+
socket.destroy()
|
|
96
|
+
}
|
|
20
97
|
})
|
|
21
98
|
|
|
22
|
-
|
|
99
|
+
// Store original request listeners to chain after api-ape handlers
|
|
100
|
+
const originalListeners = server.listeners('request').slice()
|
|
101
|
+
server.removeAllListeners('request')
|
|
102
|
+
|
|
103
|
+
// Handle HTTP requests for api-ape routes
|
|
104
|
+
server.on('request', (req, res) => {
|
|
105
|
+
const { pathname } = parseUrl(req.url)
|
|
106
|
+
|
|
107
|
+
// Serve bundled client at /api/ape.js (or /{where}/ape.js)
|
|
108
|
+
if (pathname === `/${where}/ape.js`) {
|
|
109
|
+
const filePath = path.join(__dirname, '../../dist/ape.js')
|
|
110
|
+
fs.readFile(filePath, (err, data) => {
|
|
111
|
+
if (err) {
|
|
112
|
+
sendJson(res, 500, { error: 'Failed to read client bundle' })
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
res.writeHead(200, { 'Content-Type': 'application/javascript' })
|
|
116
|
+
res.end(data)
|
|
117
|
+
})
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// File download endpoint - GET /api/ape/data/:hash
|
|
122
|
+
const downloadMatch = matchRoute(pathname, `/${where}/ape/data/:hash`)
|
|
123
|
+
if (req.method === 'GET' && downloadMatch) {
|
|
124
|
+
const { hash } = downloadMatch
|
|
125
|
+
const hostId = getCookie(req, 'apeHostId') || req.headers['x-ape-host-id']
|
|
126
|
+
|
|
127
|
+
if (!hostId) {
|
|
128
|
+
return sendJson(res, 401, { error: 'Missing session identifier' })
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!isLocalhost(req) && !isSecure(req)) {
|
|
132
|
+
return sendJson(res, 403, { error: 'HTTPS required for file transfers' })
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const result = fileTransfer.getDownload(hash, hostId)
|
|
136
|
+
|
|
137
|
+
if (!result) {
|
|
138
|
+
return sendJson(res, 404, { error: 'Download not found or unauthorized' })
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
res.writeHead(200, {
|
|
142
|
+
'Content-Type': result.contentType,
|
|
143
|
+
'Content-Length': result.data.length || result.data.byteLength
|
|
144
|
+
})
|
|
145
|
+
res.end(result.data)
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// File upload endpoint - PUT /api/ape/data/:queryId/:pathHash
|
|
150
|
+
const uploadMatch = matchRoute(pathname, `/${where}/ape/data/:queryId/:pathHash`)
|
|
151
|
+
if (req.method === 'PUT' && uploadMatch) {
|
|
152
|
+
const { queryId, pathHash } = uploadMatch
|
|
153
|
+
const hostId = getCookie(req, 'apeHostId') || req.headers['x-ape-host-id']
|
|
154
|
+
|
|
155
|
+
if (!hostId) {
|
|
156
|
+
return sendJson(res, 401, { error: 'Missing session identifier' })
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!isLocalhost(req) && !isSecure(req)) {
|
|
160
|
+
return sendJson(res, 403, { error: 'HTTPS required for file transfers' })
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const chunks = []
|
|
164
|
+
req.on('data', chunk => chunks.push(chunk))
|
|
165
|
+
req.on('end', () => {
|
|
166
|
+
const data = Buffer.concat(chunks)
|
|
167
|
+
const success = fileTransfer.receiveUpload(queryId, pathHash, data, hostId)
|
|
168
|
+
|
|
169
|
+
if (success) {
|
|
170
|
+
sendJson(res, 200, { success: true })
|
|
171
|
+
} else {
|
|
172
|
+
sendJson(res, 404, { error: 'Upload not expected or unauthorized' })
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
req.on('error', (err) => {
|
|
176
|
+
sendJson(res, 500, { error: err.message })
|
|
177
|
+
})
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Not an api-ape route - pass to original handlers
|
|
182
|
+
for (const listener of originalListeners) {
|
|
183
|
+
listener.call(server, req, res)
|
|
184
|
+
}
|
|
185
|
+
})
|
|
23
186
|
}
|
package/server/lib/wiring.js
CHANGED
|
@@ -26,7 +26,7 @@ function defaultEvents(events = {}) {
|
|
|
26
26
|
//============================================== wiring
|
|
27
27
|
//=====================================================
|
|
28
28
|
|
|
29
|
-
module.exports = function wiring(controllers, onConnent) {
|
|
29
|
+
module.exports = function wiring(controllers, onConnent, fileTransfer) {
|
|
30
30
|
onConnent = onConnent || (() => { });
|
|
31
31
|
return function webSocketHandler(socket, req) {
|
|
32
32
|
|
|
@@ -70,7 +70,8 @@ module.exports = function wiring(controllers, onConnent) {
|
|
|
70
70
|
events: { onReceive, onSend, onError, onDisconnent },
|
|
71
71
|
controllers,
|
|
72
72
|
sharedValues,
|
|
73
|
-
embedValues: embed
|
|
73
|
+
embedValues: embed,
|
|
74
|
+
fileTransfer // Pass file transfer manager
|
|
74
75
|
}// END ape
|
|
75
76
|
send = socketSend(ape)
|
|
76
77
|
ape.send = send
|
|
@@ -92,3 +93,4 @@ module.exports = function wiring(controllers, onConnent) {
|
|
|
92
93
|
|
|
93
94
|
} // END webSocketHandler
|
|
94
95
|
} // END wiring
|
|
96
|
+
|
package/server/socket/receive.js
CHANGED
|
@@ -2,8 +2,95 @@ const messageHash = require('../../utils/messageHash')
|
|
|
2
2
|
const { broadcast, online, getClients } = require('../lib/broadcast')
|
|
3
3
|
const jss = require('../../utils/jss')
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Find B/A tagged properties in data (indicating pending uploads)
|
|
7
|
+
* Returns array of { path, hash, tag }
|
|
8
|
+
*/
|
|
9
|
+
function findUploadTags(obj, path = '') {
|
|
10
|
+
const uploads = []
|
|
11
|
+
|
|
12
|
+
if (obj === null || obj === undefined || typeof obj !== 'object') {
|
|
13
|
+
return uploads
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (Array.isArray(obj)) {
|
|
17
|
+
for (let i = 0; i < obj.length; i++) {
|
|
18
|
+
uploads.push(...findUploadTags(obj[i], path ? `${path}.${i}` : String(i)))
|
|
19
|
+
}
|
|
20
|
+
return uploads
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
for (const key of Object.keys(obj)) {
|
|
24
|
+
// Check for B or A tag (binary upload markers)
|
|
25
|
+
const bMatch = key.match(/^(.+)<!B>$/)
|
|
26
|
+
const aMatch = key.match(/^(.+)<!A>$/)
|
|
27
|
+
|
|
28
|
+
if (bMatch) {
|
|
29
|
+
uploads.push({
|
|
30
|
+
path: path ? `${path}.${bMatch[1]}` : bMatch[1],
|
|
31
|
+
hash: obj[key],
|
|
32
|
+
tag: 'B',
|
|
33
|
+
originalKey: key
|
|
34
|
+
})
|
|
35
|
+
} else if (aMatch) {
|
|
36
|
+
uploads.push({
|
|
37
|
+
path: path ? `${path}.${aMatch[1]}` : aMatch[1],
|
|
38
|
+
hash: obj[key],
|
|
39
|
+
tag: 'A',
|
|
40
|
+
originalKey: key
|
|
41
|
+
})
|
|
42
|
+
} else {
|
|
43
|
+
uploads.push(...findUploadTags(obj[key], path ? `${path}.${key}` : key))
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return uploads
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Clean upload tags from data (rename key<!B> to key)
|
|
52
|
+
*/
|
|
53
|
+
function cleanUploadTags(obj) {
|
|
54
|
+
if (obj === null || obj === undefined || typeof obj !== 'object') {
|
|
55
|
+
return obj
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (Array.isArray(obj)) {
|
|
59
|
+
return obj.map(cleanUploadTags)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const cleaned = {}
|
|
63
|
+
for (const key of Object.keys(obj)) {
|
|
64
|
+
const bMatch = key.match(/^(.+)<!B>$/)
|
|
65
|
+
const aMatch = key.match(/^(.+)<!A>$/)
|
|
66
|
+
|
|
67
|
+
if (bMatch) {
|
|
68
|
+
cleaned[bMatch[1]] = obj[key] // Will be replaced with actual data
|
|
69
|
+
} else if (aMatch) {
|
|
70
|
+
cleaned[aMatch[1]] = obj[key] // Will be replaced with actual data
|
|
71
|
+
} else {
|
|
72
|
+
cleaned[key] = cleanUploadTags(obj[key])
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return cleaned
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Set value at nested path
|
|
80
|
+
*/
|
|
81
|
+
function setValueAtPath(obj, path, value) {
|
|
82
|
+
const parts = path.split('.')
|
|
83
|
+
let current = obj
|
|
84
|
+
|
|
85
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
86
|
+
current = current[parts[i]]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
current[parts[parts.length - 1]] = value
|
|
90
|
+
}
|
|
91
|
+
|
|
5
92
|
module.exports = function receiveHandler(ape) {
|
|
6
|
-
const { send, checkReply, events, controllers, sharedValues, hostId, embedValues } = ape
|
|
93
|
+
const { send, checkReply, events, controllers, sharedValues, hostId, embedValues, fileTransfer } = ape
|
|
7
94
|
|
|
8
95
|
// Build `this` context for controllers
|
|
9
96
|
// Includes: client metadata + api-ape utilities
|
|
@@ -18,7 +105,7 @@ module.exports = function receiveHandler(ape) {
|
|
|
18
105
|
hostId
|
|
19
106
|
}
|
|
20
107
|
|
|
21
|
-
return function onReceive(msg) {
|
|
108
|
+
return async function onReceive(msg) {
|
|
22
109
|
// Convert Buffer to string - WebSocket messages may arrive as binary
|
|
23
110
|
const msgString = typeof msg === 'string' ? msg : msg.toString('utf8');
|
|
24
111
|
const queryId = messageHash(msgString);
|
|
@@ -31,6 +118,34 @@ module.exports = function receiveHandler(ape) {
|
|
|
31
118
|
// Call onReceive hook - it should return a finish callback
|
|
32
119
|
const onFinish = events.onReceive(queryId, data, type) || (() => { })
|
|
33
120
|
|
|
121
|
+
// Check for pending uploads (B/A tags)
|
|
122
|
+
let processedData = data
|
|
123
|
+
if (fileTransfer && data) {
|
|
124
|
+
const uploadTags = findUploadTags(data)
|
|
125
|
+
|
|
126
|
+
if (uploadTags.length > 0) {
|
|
127
|
+
console.log(`📤 Waiting for ${uploadTags.length} upload(s) for ${type}`)
|
|
128
|
+
|
|
129
|
+
// Clean the data object
|
|
130
|
+
processedData = cleanUploadTags(data)
|
|
131
|
+
|
|
132
|
+
// Wait for all uploads
|
|
133
|
+
try {
|
|
134
|
+
await Promise.all(uploadTags.map(async ({ path, hash }) => {
|
|
135
|
+
const uploadData = await fileTransfer.registerUpload(queryId, hash, hostId)
|
|
136
|
+
setValueAtPath(processedData, path, uploadData)
|
|
137
|
+
}))
|
|
138
|
+
} catch (uploadErr) {
|
|
139
|
+
console.error(`📤 Upload wait failed:`, uploadErr)
|
|
140
|
+
send(queryId, false, false, uploadErr)
|
|
141
|
+
if (typeof onFinish === 'function') {
|
|
142
|
+
onFinish(uploadErr, true)
|
|
143
|
+
}
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
34
149
|
const result = new Promise((resolve, reject) => {
|
|
35
150
|
try {
|
|
36
151
|
const controller = controllers[type]
|
|
@@ -38,7 +153,7 @@ module.exports = function receiveHandler(ape) {
|
|
|
38
153
|
throw `TypeError: "${type}" was not found`
|
|
39
154
|
}
|
|
40
155
|
checkReply(queryId, createdAt)
|
|
41
|
-
resolve(controller.call(that,
|
|
156
|
+
resolve(controller.call(that, processedData))
|
|
42
157
|
} catch (err) {
|
|
43
158
|
reject(err)
|
|
44
159
|
}
|
package/server/socket/send.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const jss = require('../../utils/jss')
|
|
2
|
+
const { FileTransferManager } = require('../lib/fileTransfer')
|
|
2
3
|
|
|
3
4
|
function checkSocketState(socket) {
|
|
4
5
|
if (socket.readyState !== socket.OPEN) {
|
|
@@ -17,7 +18,88 @@ function checkSocketState(socket) {
|
|
|
17
18
|
} // END if
|
|
18
19
|
} // END checkSocketState
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Check if value is binary data (Buffer, ArrayBuffer, or typed array)
|
|
23
|
+
*/
|
|
24
|
+
function isBinaryData(value) {
|
|
25
|
+
if (value === null || value === undefined) return false
|
|
26
|
+
return Buffer.isBuffer(value) ||
|
|
27
|
+
value instanceof ArrayBuffer ||
|
|
28
|
+
ArrayBuffer.isView(value)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Detect content type from binary data
|
|
33
|
+
*/
|
|
34
|
+
function detectContentType(data) {
|
|
35
|
+
// Could be enhanced with magic number detection
|
|
36
|
+
return 'application/octet-stream'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Process data object, replacing binary values with L-tagged hashes
|
|
41
|
+
* Returns { processedData, binaryEntries }
|
|
42
|
+
*/
|
|
43
|
+
function processBinaryData(data, queryId, fileTransfer, hostId, path = '') {
|
|
44
|
+
if (data === null || data === undefined) {
|
|
45
|
+
return { processedData: data, binaryEntries: [] }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (isBinaryData(data)) {
|
|
49
|
+
// This is binary data - register and return hash
|
|
50
|
+
const hash = FileTransferManager.generateHash(queryId, path || 'root')
|
|
51
|
+
const contentType = detectContentType(data)
|
|
52
|
+
fileTransfer.registerDownload(hash, data, contentType, hostId)
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
processedData: { [`__ape_link__`]: hash },
|
|
56
|
+
binaryEntries: [{ path, hash }]
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (Array.isArray(data)) {
|
|
61
|
+
const processedArray = []
|
|
62
|
+
const allBinaryEntries = []
|
|
63
|
+
|
|
64
|
+
for (let i = 0; i < data.length; i++) {
|
|
65
|
+
const itemPath = path ? `${path}.${i}` : String(i)
|
|
66
|
+
const { processedData, binaryEntries } = processBinaryData(
|
|
67
|
+
data[i], queryId, fileTransfer, hostId, itemPath
|
|
68
|
+
)
|
|
69
|
+
processedArray.push(processedData)
|
|
70
|
+
allBinaryEntries.push(...binaryEntries)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { processedData: processedArray, binaryEntries: allBinaryEntries }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (typeof data === 'object') {
|
|
77
|
+
const processedObj = {}
|
|
78
|
+
const allBinaryEntries = []
|
|
79
|
+
|
|
80
|
+
for (const key of Object.keys(data)) {
|
|
81
|
+
const itemPath = path ? `${path}.${key}` : key
|
|
82
|
+
const { processedData, binaryEntries } = processBinaryData(
|
|
83
|
+
data[key], queryId, fileTransfer, hostId, itemPath
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
// If this was binary data, mark the key with <!L> tag
|
|
87
|
+
if (binaryEntries.length > 0 && processedData?.__ape_link__) {
|
|
88
|
+
processedObj[`${key}<!L>`] = processedData.__ape_link__
|
|
89
|
+
} else {
|
|
90
|
+
processedObj[key] = processedData
|
|
91
|
+
}
|
|
92
|
+
allBinaryEntries.push(...binaryEntries)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { processedData: processedObj, binaryEntries: allBinaryEntries }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Primitive value - return as-is
|
|
99
|
+
return { processedData: data, binaryEntries: [] }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = function sendHandler({ socket, events, hostId, fileTransfer }) {
|
|
21
103
|
|
|
22
104
|
return function send(queryId, type, data, err) {
|
|
23
105
|
if (!type && !queryId) {
|
|
@@ -43,11 +125,24 @@ module.exports = function sendHandler({ socket, events, hostId }) {
|
|
|
43
125
|
}
|
|
44
126
|
return;
|
|
45
127
|
}
|
|
128
|
+
|
|
129
|
+
// Process binary data if fileTransfer is available
|
|
130
|
+
let processedData = data
|
|
131
|
+
if (fileTransfer && data && !err) {
|
|
132
|
+
const { processedData: processed, binaryEntries } = processBinaryData(
|
|
133
|
+
data, queryId || type, fileTransfer, hostId
|
|
134
|
+
)
|
|
135
|
+
processedData = processed
|
|
136
|
+
if (binaryEntries.length > 0) {
|
|
137
|
+
console.log(`📦 Registered ${binaryEntries.length} binary download(s) for ${queryId || type}`)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
46
141
|
if (err) {
|
|
47
142
|
socket.send(jss.stringify({ err: err.message || err, type, queryId }))
|
|
48
143
|
if (typeof onFinish === 'function') onFinish(err, true)
|
|
49
144
|
} else {
|
|
50
|
-
socket.send(jss.stringify({ data, type, queryId }))
|
|
145
|
+
socket.send(jss.stringify({ data: processedData, type, queryId }))
|
|
51
146
|
if (typeof onFinish === 'function') onFinish(false, data)
|
|
52
147
|
}
|
|
53
148
|
|