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/README.md
CHANGED
|
@@ -24,18 +24,30 @@ yarn add api-ape
|
|
|
24
24
|
|
|
25
25
|
## Quick Start
|
|
26
26
|
|
|
27
|
-
### Server (
|
|
27
|
+
### Server (Node.js)
|
|
28
28
|
|
|
29
29
|
```js
|
|
30
|
-
const
|
|
30
|
+
const { createServer } = require('http')
|
|
31
31
|
const ape = require('api-ape')
|
|
32
32
|
|
|
33
|
-
const
|
|
33
|
+
const server = createServer()
|
|
34
34
|
|
|
35
35
|
// Wire up api-ape - loads controllers from ./api folder
|
|
36
|
-
ape(
|
|
36
|
+
ape(server, { where: 'api' })
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
server.listen(3000)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**With Express:**
|
|
42
|
+
```js
|
|
43
|
+
const express = require('express')
|
|
44
|
+
const ape = require('api-ape')
|
|
45
|
+
|
|
46
|
+
const app = express()
|
|
47
|
+
const server = app.listen(3000) // Get the HTTP server
|
|
48
|
+
|
|
49
|
+
// Pass the HTTP server (not the Express app)
|
|
50
|
+
ape(server, { where: 'api' })
|
|
39
51
|
```
|
|
40
52
|
|
|
41
53
|
### Create a Controller
|
|
@@ -86,12 +98,13 @@ Include the bundled client and start calling:
|
|
|
86
98
|
|
|
87
99
|
### Server
|
|
88
100
|
|
|
89
|
-
#### `ape(
|
|
101
|
+
#### `ape(server, options)`
|
|
90
102
|
|
|
91
|
-
Initialize api-ape on
|
|
103
|
+
Initialize api-ape on a Node.js HTTP/HTTPS server.
|
|
92
104
|
|
|
93
105
|
| Option | Type | Description |
|
|
94
106
|
|--------|------|-------------|
|
|
107
|
+
| `server` | `http.Server` | Node.js HTTP or HTTPS server instance |
|
|
95
108
|
| `where` | `string` | Directory containing controller files (default: `'api'`) |
|
|
96
109
|
| `onConnent` | `function` | Connection lifecycle hook (see [Connection Lifecycle](#connection-lifecycle)) |
|
|
97
110
|
|
|
@@ -146,7 +159,7 @@ ape.on('notification', ({ data, err, type }) => {
|
|
|
146
159
|
### Default Options
|
|
147
160
|
|
|
148
161
|
```js
|
|
149
|
-
ape(
|
|
162
|
+
ape(server, {
|
|
150
163
|
where: 'api', // Controller directory
|
|
151
164
|
onConnent: undefined // Lifecycle hook (optional)
|
|
152
165
|
})
|
|
@@ -157,7 +170,7 @@ ape(app, {
|
|
|
157
170
|
Customize behavior per connection:
|
|
158
171
|
|
|
159
172
|
```js
|
|
160
|
-
ape(
|
|
173
|
+
ape(server, {
|
|
161
174
|
where: 'api',
|
|
162
175
|
onConnent(socket, req, hostId) {
|
|
163
176
|
return {
|
|
@@ -328,7 +341,7 @@ docker-compose up --build
|
|
|
328
341
|
|
|
329
342
|
### CORS Errors in Browser
|
|
330
343
|
|
|
331
|
-
Ensure your
|
|
344
|
+
Ensure your server allows WebSocket connections from your origin. api-ape uses the `ws` library which handles WebSocket upgrades on the HTTP server level.
|
|
332
345
|
|
|
333
346
|
### Controller Not Found
|
|
334
347
|
|
|
@@ -343,12 +356,46 @@ The client automatically reconnects with exponential backoff. If connections dro
|
|
|
343
356
|
* Verify network stability
|
|
344
357
|
* Check server logs for errors
|
|
345
358
|
|
|
346
|
-
### Binary Data / File
|
|
359
|
+
### Binary Data / File Transfers
|
|
360
|
+
|
|
361
|
+
api-ape supports transparent binary file transfers. Simply return `Buffer` data from controllers:
|
|
362
|
+
|
|
363
|
+
```js
|
|
364
|
+
// api/files/download.js
|
|
365
|
+
module.exports = function(filename) {
|
|
366
|
+
return {
|
|
367
|
+
name: filename,
|
|
368
|
+
data: fs.readFileSync(`./uploads/${filename}`) // Buffer
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
The client receives `ArrayBuffer` automatically:
|
|
374
|
+
|
|
375
|
+
```js
|
|
376
|
+
const result = await ape.files.download('image.png')
|
|
377
|
+
console.log(result.data) // ArrayBuffer
|
|
378
|
+
|
|
379
|
+
// Display as image
|
|
380
|
+
const blob = new Blob([result.data])
|
|
381
|
+
img.src = URL.createObjectURL(blob)
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
**Uploads work the same way:**
|
|
385
|
+
|
|
386
|
+
```js
|
|
387
|
+
// Client
|
|
388
|
+
const arrayBuffer = await file.arrayBuffer()
|
|
389
|
+
await ape.files.upload({ name: file.name, data: arrayBuffer })
|
|
390
|
+
|
|
391
|
+
// Server (api/files/upload.js)
|
|
392
|
+
module.exports = function({ name, data }) {
|
|
393
|
+
fs.writeFileSync(`./uploads/${name}`, data) // data is Buffer
|
|
394
|
+
return { success: true }
|
|
395
|
+
}
|
|
396
|
+
```
|
|
347
397
|
|
|
348
|
-
|
|
349
|
-
* Sending file URLs instead of raw data
|
|
350
|
-
* Using a separate file upload endpoint
|
|
351
|
-
* Chunking large payloads
|
|
398
|
+
Binary data is transferred via temporary HTTP endpoints (`/api/ape/data/:hash`) with session verification and auto-cleanup.
|
|
352
399
|
|
|
353
400
|
### TypeScript Support
|
|
354
401
|
|
|
@@ -426,7 +473,7 @@ api-ape/
|
|
|
426
473
|
│ └── connectSocket.js # WebSocket client with auto-reconnect
|
|
427
474
|
├── server/
|
|
428
475
|
│ ├── lib/
|
|
429
|
-
│ │ ├── main.js #
|
|
476
|
+
│ │ ├── main.js # HTTP server integration
|
|
430
477
|
│ │ ├── loader.js # Auto-loads controller files
|
|
431
478
|
│ │ ├── broadcast.js # Client tracking & broadcast
|
|
432
479
|
│ │ └── wiring.js # WebSocket handler setup
|
package/client/README.md
CHANGED
|
@@ -67,3 +67,35 @@ ape.configure({
|
|
|
67
67
|
Default port detection:
|
|
68
68
|
- Local (`localhost`, `127.0.0.1`): `9010`
|
|
69
69
|
- Remote: Uses current page port or `443`/`80`
|
|
70
|
+
|
|
71
|
+
## File Transfers
|
|
72
|
+
|
|
73
|
+
Binary data is automatically handled. The client fetches binary resources and uploads binary data transparently.
|
|
74
|
+
|
|
75
|
+
### Receiving Binary Data
|
|
76
|
+
|
|
77
|
+
```js
|
|
78
|
+
// Server returns Buffer, client receives ArrayBuffer
|
|
79
|
+
const result = await ape.files.download('image.png')
|
|
80
|
+
console.log(result.data) // ArrayBuffer
|
|
81
|
+
|
|
82
|
+
// Display as image
|
|
83
|
+
const blob = new Blob([result.data])
|
|
84
|
+
img.src = URL.createObjectURL(blob)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Uploading Binary Data
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
const file = input.files[0]
|
|
91
|
+
const arrayBuffer = await file.arrayBuffer()
|
|
92
|
+
|
|
93
|
+
// Binary data is uploaded automatically
|
|
94
|
+
await ape.files.upload({
|
|
95
|
+
name: file.name,
|
|
96
|
+
data: arrayBuffer // Sent via HTTP PUT
|
|
97
|
+
})
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Binary transfers use `/api/ape/data/:hash` endpoints with session verification.
|
|
101
|
+
|
package/client/browser.js
CHANGED
|
@@ -4,7 +4,7 @@ import connectSocket from './connectSocket.js'
|
|
|
4
4
|
const port = window.location.port || (window.location.protocol === 'https:' ? 443 : 80)
|
|
5
5
|
connectSocket.configure({ port: parseInt(port, 10) })
|
|
6
6
|
|
|
7
|
-
const { sender, setOnReciver } = connectSocket()
|
|
7
|
+
const { sender, setOnReciver, onConnectionChange } = connectSocket()
|
|
8
8
|
connectSocket.autoReconnect()
|
|
9
9
|
|
|
10
10
|
// Global API - use defineProperty to bypass Proxy interception
|
|
@@ -15,3 +15,9 @@ Object.defineProperty(window.ape, 'on', {
|
|
|
15
15
|
enumerable: false,
|
|
16
16
|
configurable: false
|
|
17
17
|
})
|
|
18
|
+
Object.defineProperty(window.ape, 'onConnectionChange', {
|
|
19
|
+
value: onConnectionChange,
|
|
20
|
+
writable: false,
|
|
21
|
+
enumerable: false,
|
|
22
|
+
configurable: false
|
|
23
|
+
})
|
package/client/connectSocket.js
CHANGED
|
@@ -1,9 +1,27 @@
|
|
|
1
1
|
import messageHash from '../utils/messageHash'
|
|
2
2
|
import jss from '../utils/jss'
|
|
3
3
|
|
|
4
|
-
|
|
5
4
|
let connect;
|
|
6
5
|
|
|
6
|
+
// Connection state enum
|
|
7
|
+
const ConnectionState = {
|
|
8
|
+
Disconnected: 'disconnected',
|
|
9
|
+
Connecting: 'connecting',
|
|
10
|
+
Connected: 'connected',
|
|
11
|
+
Closing: 'closing'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Connection state tracking
|
|
15
|
+
let connectionState = ConnectionState.Disconnected
|
|
16
|
+
const connectionChangeListeners = []
|
|
17
|
+
|
|
18
|
+
function notifyConnectionChange(newState) {
|
|
19
|
+
if (connectionState !== newState) {
|
|
20
|
+
connectionState = newState
|
|
21
|
+
connectionChangeListeners.forEach(fn => fn(newState))
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
7
25
|
// Configuration
|
|
8
26
|
let configuredPort = null
|
|
9
27
|
let configuredHost = null
|
|
@@ -82,11 +100,13 @@ const ofTypesOb = {};
|
|
|
82
100
|
function connectSocket() {
|
|
83
101
|
|
|
84
102
|
if (!__socket) {
|
|
103
|
+
notifyConnectionChange(ConnectionState.Connecting)
|
|
85
104
|
__socket = new WebSocket(getSocketUrl())
|
|
86
105
|
|
|
87
106
|
__socket.onopen = event => {
|
|
88
107
|
//console.log('socket connected()');
|
|
89
108
|
ready = true;
|
|
109
|
+
notifyConnectionChange(ConnectionState.Connected)
|
|
90
110
|
aWaitingSend.forEach(({ type, data, next, err, waiting, createdAt, timer }) => {
|
|
91
111
|
clearTimeout(timer)
|
|
92
112
|
//TODO: clear throw of wait for server
|
|
@@ -101,14 +121,148 @@ function connectSocket() {
|
|
|
101
121
|
aWaitingSend = []
|
|
102
122
|
} // END onopen
|
|
103
123
|
|
|
104
|
-
|
|
124
|
+
/**
|
|
125
|
+
* Find all L-tagged (binary link) properties in data
|
|
126
|
+
* Returns array of { path, hash }
|
|
127
|
+
*/
|
|
128
|
+
function findLinkedResources(obj, path = '') {
|
|
129
|
+
const resources = []
|
|
130
|
+
|
|
131
|
+
if (obj === null || obj === undefined || typeof obj !== 'object') {
|
|
132
|
+
return resources
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (Array.isArray(obj)) {
|
|
136
|
+
for (let i = 0; i < obj.length; i++) {
|
|
137
|
+
resources.push(...findLinkedResources(obj[i], path ? `${path}.${i}` : String(i)))
|
|
138
|
+
}
|
|
139
|
+
return resources
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
for (const key of Object.keys(obj)) {
|
|
143
|
+
// Check for L-tag in key (from JJS encoding: key<!L>)
|
|
144
|
+
if (key.endsWith('<!L>')) {
|
|
145
|
+
const cleanKey = key.slice(0, -4)
|
|
146
|
+
const hash = obj[key]
|
|
147
|
+
resources.push({
|
|
148
|
+
path: path ? `${path}.${cleanKey}` : cleanKey,
|
|
149
|
+
hash,
|
|
150
|
+
originalKey: key
|
|
151
|
+
})
|
|
152
|
+
} else {
|
|
153
|
+
resources.push(...findLinkedResources(obj[key], path ? `${path}.${key}` : key))
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return resources
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Set a value at a nested path in an object
|
|
162
|
+
*/
|
|
163
|
+
function setValueAtPath(obj, path, value) {
|
|
164
|
+
const parts = path.split('.')
|
|
165
|
+
let current = obj
|
|
166
|
+
|
|
167
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
168
|
+
current = current[parts[i]]
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
current[parts[parts.length - 1]] = value
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Clean up L-tagged keys (rename key<!L> to key)
|
|
176
|
+
*/
|
|
177
|
+
function cleanLinkedKeys(obj) {
|
|
178
|
+
if (obj === null || obj === undefined || typeof obj !== 'object') {
|
|
179
|
+
return obj
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (Array.isArray(obj)) {
|
|
183
|
+
return obj.map(cleanLinkedKeys)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const cleaned = {}
|
|
187
|
+
for (const key of Object.keys(obj)) {
|
|
188
|
+
if (key.endsWith('<!L>')) {
|
|
189
|
+
const cleanKey = key.slice(0, -4)
|
|
190
|
+
cleaned[cleanKey] = obj[key] // Value will be replaced after fetch
|
|
191
|
+
} else {
|
|
192
|
+
cleaned[key] = cleanLinkedKeys(obj[key])
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return cleaned
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Fetch binary resources and hydrate data object
|
|
200
|
+
*/
|
|
201
|
+
async function fetchLinkedResources(data, hostId) {
|
|
202
|
+
const resources = findLinkedResources(data)
|
|
203
|
+
|
|
204
|
+
if (resources.length === 0) {
|
|
205
|
+
return data
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
console.log(`🦍 Fetching ${resources.length} binary resource(s)`)
|
|
209
|
+
|
|
210
|
+
// Clean the data first (remove <!L> suffixes from keys)
|
|
211
|
+
const cleanedData = cleanLinkedKeys(data)
|
|
212
|
+
|
|
213
|
+
// Build base URL for fetches
|
|
214
|
+
const hostname = configuredHost || window.location.hostname
|
|
215
|
+
const isLocal = ["localhost", "127.0.0.1", "[::1]"].includes(hostname)
|
|
216
|
+
const isHttps = window.location.protocol === "https:"
|
|
217
|
+
const defaultPort = isLocal ? 9010 : (window.location.port || (isHttps ? 443 : 80))
|
|
218
|
+
const port = configuredPort || defaultPort
|
|
219
|
+
const protocol = isHttps ? "https" : "http"
|
|
220
|
+
const portSuffix = (isLocal || (port !== 80 && port !== 443)) ? `:${port}` : ""
|
|
221
|
+
const baseUrl = `${protocol}://${hostname}${portSuffix}`
|
|
222
|
+
|
|
223
|
+
// Fetch all resources in parallel
|
|
224
|
+
await Promise.all(resources.map(async ({ path, hash }) => {
|
|
225
|
+
try {
|
|
226
|
+
const response = await fetch(`${baseUrl}/api/ape/data/${hash}`, {
|
|
227
|
+
credentials: 'include',
|
|
228
|
+
headers: {
|
|
229
|
+
'X-Ape-Host-Id': hostId || ''
|
|
230
|
+
}
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
if (!response.ok) {
|
|
234
|
+
throw new Error(`Failed to fetch binary resource: ${response.status}`)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const arrayBuffer = await response.arrayBuffer()
|
|
238
|
+
setValueAtPath(cleanedData, path, arrayBuffer)
|
|
239
|
+
} catch (err) {
|
|
240
|
+
console.error(`🦍 Failed to fetch binary resource at ${path}:`, err)
|
|
241
|
+
setValueAtPath(cleanedData, path, null)
|
|
242
|
+
}
|
|
243
|
+
}))
|
|
244
|
+
|
|
245
|
+
return cleanedData
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
__socket.onmessage = async function (event) {
|
|
105
249
|
//console.log('WebSocket message:', event);
|
|
106
250
|
const { err, type, queryId, data } = jss.parse(event.data)
|
|
107
251
|
|
|
108
252
|
// Messages with queryId must fulfill matching promise
|
|
109
253
|
if (queryId) {
|
|
110
254
|
if (waitingOn[queryId]) {
|
|
111
|
-
|
|
255
|
+
// Check for linked resources and fetch them before resolving
|
|
256
|
+
if (data && !err) {
|
|
257
|
+
try {
|
|
258
|
+
const hydratedData = await fetchLinkedResources(data)
|
|
259
|
+
waitingOn[queryId](err, hydratedData)
|
|
260
|
+
} catch (fetchErr) {
|
|
261
|
+
waitingOn[queryId](fetchErr, null)
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
waitingOn[queryId](err, data)
|
|
265
|
+
}
|
|
112
266
|
delete waitingOn[queryId]
|
|
113
267
|
} else {
|
|
114
268
|
// No matching promise - error and ignore
|
|
@@ -118,10 +272,20 @@ function connectSocket() {
|
|
|
118
272
|
}
|
|
119
273
|
|
|
120
274
|
// Only messages WITHOUT queryId go to setOnReciver
|
|
275
|
+
// Also hydrate broadcast messages
|
|
276
|
+
let processedData = data
|
|
277
|
+
if (data && !err) {
|
|
278
|
+
try {
|
|
279
|
+
processedData = await fetchLinkedResources(data)
|
|
280
|
+
} catch (fetchErr) {
|
|
281
|
+
console.error(`🦍 Failed to hydrate broadcast data:`, fetchErr)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
121
285
|
if (ofTypesOb[type]) {
|
|
122
|
-
ofTypesOb[type].forEach(worker => worker({ err, type, data }))
|
|
286
|
+
ofTypesOb[type].forEach(worker => worker({ err, type, data: processedData }))
|
|
123
287
|
} // if ofTypesOb[type]
|
|
124
|
-
reciverOnAr.forEach(worker => worker({ err, type, data }))
|
|
288
|
+
reciverOnAr.forEach(worker => worker({ err, type, data: processedData }))
|
|
125
289
|
|
|
126
290
|
} // END onmessage
|
|
127
291
|
|
|
@@ -133,10 +297,138 @@ function connectSocket() {
|
|
|
133
297
|
console.warn('socket disconnect:', event);
|
|
134
298
|
__socket = false
|
|
135
299
|
ready = false;
|
|
300
|
+
notifyConnectionChange(ConnectionState.Disconnected)
|
|
136
301
|
setTimeout(() => reconnect && connectSocket(), 500);
|
|
137
302
|
} // END onclose
|
|
138
303
|
|
|
139
304
|
} // END if ! __socket
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Check if value is binary data (ArrayBuffer, typed array, or Blob)
|
|
308
|
+
*/
|
|
309
|
+
function isBinaryData(value) {
|
|
310
|
+
if (value === null || value === undefined) return false
|
|
311
|
+
return value instanceof ArrayBuffer ||
|
|
312
|
+
ArrayBuffer.isView(value) ||
|
|
313
|
+
(typeof Blob !== 'undefined' && value instanceof Blob)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Get binary type tag (A for ArrayBuffer, B for Blob)
|
|
318
|
+
*/
|
|
319
|
+
function getBinaryTag(value) {
|
|
320
|
+
if (typeof Blob !== 'undefined' && value instanceof Blob) return 'B'
|
|
321
|
+
return 'A'
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Generate a simple hash for binary upload
|
|
326
|
+
*/
|
|
327
|
+
function generateUploadHash(path) {
|
|
328
|
+
let hash = 0
|
|
329
|
+
for (let i = 0; i < path.length; i++) {
|
|
330
|
+
const char = path.charCodeAt(i)
|
|
331
|
+
hash = ((hash << 5) - hash) + char
|
|
332
|
+
hash = hash & hash
|
|
333
|
+
}
|
|
334
|
+
return Math.abs(hash).toString(36)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Find and extract binary data from payload
|
|
339
|
+
* Returns { processedData, uploads: [{ path, hash, data, tag }] }
|
|
340
|
+
*/
|
|
341
|
+
function processBinaryForUpload(data, path = '') {
|
|
342
|
+
if (data === null || data === undefined) {
|
|
343
|
+
return { processedData: data, uploads: [] }
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (isBinaryData(data)) {
|
|
347
|
+
const tag = getBinaryTag(data)
|
|
348
|
+
const hash = generateUploadHash(path || 'root')
|
|
349
|
+
return {
|
|
350
|
+
processedData: { [`__ape_upload__`]: hash },
|
|
351
|
+
uploads: [{ path, hash, data, tag }]
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (Array.isArray(data)) {
|
|
356
|
+
const processedArray = []
|
|
357
|
+
const allUploads = []
|
|
358
|
+
|
|
359
|
+
for (let i = 0; i < data.length; i++) {
|
|
360
|
+
const itemPath = path ? `${path}.${i}` : String(i)
|
|
361
|
+
const { processedData, uploads } = processBinaryForUpload(data[i], itemPath)
|
|
362
|
+
processedArray.push(processedData)
|
|
363
|
+
allUploads.push(...uploads)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return { processedData: processedArray, uploads: allUploads }
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (typeof data === 'object') {
|
|
370
|
+
const processedObj = {}
|
|
371
|
+
const allUploads = []
|
|
372
|
+
|
|
373
|
+
for (const key of Object.keys(data)) {
|
|
374
|
+
const itemPath = path ? `${path}.${key}` : key
|
|
375
|
+
const { processedData, uploads } = processBinaryForUpload(data[key], itemPath)
|
|
376
|
+
|
|
377
|
+
// If this was binary data, mark the key with <!B> or <!A> tag
|
|
378
|
+
if (uploads.length > 0 && processedData?.__ape_upload__) {
|
|
379
|
+
const tag = uploads[uploads.length - 1].tag
|
|
380
|
+
processedObj[`${key}<!${tag}>`] = processedData.__ape_upload__
|
|
381
|
+
} else {
|
|
382
|
+
processedObj[key] = processedData
|
|
383
|
+
}
|
|
384
|
+
allUploads.push(...uploads)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return { processedData: processedObj, uploads: allUploads }
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return { processedData: data, uploads: [] }
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Upload binary data via HTTP PUT
|
|
395
|
+
*/
|
|
396
|
+
async function uploadBinaryData(queryId, uploads) {
|
|
397
|
+
if (uploads.length === 0) return
|
|
398
|
+
|
|
399
|
+
// Build base URL
|
|
400
|
+
const hostname = configuredHost || window.location.hostname
|
|
401
|
+
const isLocal = ["localhost", "127.0.0.1", "[::1]"].includes(hostname)
|
|
402
|
+
const isHttps = window.location.protocol === "https:"
|
|
403
|
+
const defaultPort = isLocal ? 9010 : (window.location.port || (isHttps ? 443 : 80))
|
|
404
|
+
const port = configuredPort || defaultPort
|
|
405
|
+
const protocol = isHttps ? "https" : "http"
|
|
406
|
+
const portSuffix = (isLocal || (port !== 80 && port !== 443)) ? `:${port}` : ""
|
|
407
|
+
const baseUrl = `${protocol}://${hostname}${portSuffix}`
|
|
408
|
+
|
|
409
|
+
console.log(`🦍 Uploading ${uploads.length} binary file(s)`)
|
|
410
|
+
|
|
411
|
+
await Promise.all(uploads.map(async ({ hash, data }) => {
|
|
412
|
+
try {
|
|
413
|
+
const response = await fetch(`${baseUrl}/api/ape/data/${queryId}/${hash}`, {
|
|
414
|
+
method: 'PUT',
|
|
415
|
+
credentials: 'include',
|
|
416
|
+
headers: {
|
|
417
|
+
'Content-Type': 'application/octet-stream'
|
|
418
|
+
},
|
|
419
|
+
body: data
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
if (!response.ok) {
|
|
423
|
+
throw new Error(`Upload failed: ${response.status}`)
|
|
424
|
+
}
|
|
425
|
+
} catch (err) {
|
|
426
|
+
console.error(`🦍 Failed to upload binary at ${hash}:`, err)
|
|
427
|
+
throw err
|
|
428
|
+
}
|
|
429
|
+
}))
|
|
430
|
+
}
|
|
431
|
+
|
|
140
432
|
wsSend = function (type, data, createdAt, dirctCall) {
|
|
141
433
|
let rej, promiseIsLive = false;
|
|
142
434
|
const timeLetForReqToBeMade = (createdAt + totalRequestTimeout) - Date.now()
|
|
@@ -146,9 +438,13 @@ function connectSocket() {
|
|
|
146
438
|
rej(new Error("Request Timedout for :" + type))
|
|
147
439
|
}
|
|
148
440
|
}, timeLetForReqToBeMade);
|
|
441
|
+
|
|
442
|
+
// Process binary data for upload
|
|
443
|
+
const { processedData, uploads } = processBinaryForUpload(data)
|
|
444
|
+
|
|
149
445
|
const payload = {
|
|
150
446
|
type,
|
|
151
|
-
data,
|
|
447
|
+
data: processedData,
|
|
152
448
|
//referer:window.location.href,
|
|
153
449
|
createdAt: new Date(createdAt),
|
|
154
450
|
requestedAt: dirctCall ? undefined
|
|
@@ -169,6 +465,14 @@ function connectSocket() {
|
|
|
169
465
|
}
|
|
170
466
|
}
|
|
171
467
|
__socket.send(message);
|
|
468
|
+
|
|
469
|
+
// Upload binary data after sending WS message
|
|
470
|
+
if (uploads.length > 0) {
|
|
471
|
+
uploadBinaryData(queryId, uploads).catch(err => {
|
|
472
|
+
console.error('🦍 Binary upload failed:', err)
|
|
473
|
+
// The server will timeout waiting for the upload
|
|
474
|
+
})
|
|
475
|
+
}
|
|
172
476
|
});
|
|
173
477
|
const next = replyPromise.then;
|
|
174
478
|
replyPromise.then = worker => {
|
|
@@ -248,13 +552,24 @@ function connectSocket() {
|
|
|
248
552
|
reciverOnAr.push(onTypeStFn)
|
|
249
553
|
}
|
|
250
554
|
}
|
|
251
|
-
} // END setOnReciver
|
|
555
|
+
}, // END setOnReciver
|
|
556
|
+
onConnectionChange: (handler) => {
|
|
557
|
+
connectionChangeListeners.push(handler)
|
|
558
|
+
// Immediately call with current state
|
|
559
|
+
handler(connectionState)
|
|
560
|
+
// Return unsubscribe function
|
|
561
|
+
return () => {
|
|
562
|
+
const idx = connectionChangeListeners.indexOf(handler)
|
|
563
|
+
if (idx > -1) connectionChangeListeners.splice(idx, 1)
|
|
564
|
+
}
|
|
565
|
+
}
|
|
252
566
|
} // END return
|
|
253
567
|
} // END connectSocket
|
|
254
568
|
|
|
255
569
|
connectSocket.autoReconnect = () => reconnect = true
|
|
256
570
|
connectSocket.configure = configure
|
|
571
|
+
connectSocket.ConnectionState = ConnectionState
|
|
257
572
|
connect = connectSocket
|
|
258
573
|
|
|
259
574
|
export default connect;
|
|
260
|
-
export { configure };
|
|
575
|
+
export { configure, ConnectionState };
|