api-ape 2.0.0 → 2.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +203 -124
- package/client/README.md +37 -30
- package/client/browser.js +10 -8
- package/client/connectSocket.js +662 -381
- package/client/index.js +171 -0
- package/client/transports/streaming.js +240 -0
- package/dist/ape.js +2 -699
- 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 +71 -18
- package/package.json +50 -15
- package/server/README.md +99 -13
- package/server/lib/broadcast.js +25 -8
- package/server/lib/bun.js +122 -0
- package/server/lib/longPolling.js +226 -0
- package/server/lib/main.js +381 -38
- 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/security/origin.js +16 -4
- package/server/socket/receive.js +14 -1
- package/server/socket/send.js +6 -6
- package/server/utils/deepRequire.js +25 -10
- 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/client/connectSocket.js
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
import messageHash from '../utils/messageHash'
|
|
2
2
|
import jss from '../utils/jss'
|
|
3
|
+
import { createStreamingTransport } from './transports/streaming'
|
|
3
4
|
|
|
4
5
|
let connect;
|
|
5
6
|
|
|
6
7
|
// Connection state enum
|
|
7
8
|
const ConnectionState = {
|
|
9
|
+
Offline: 'offline', // navigator.onLine = false
|
|
10
|
+
Walled: 'walled', // Captive portal detected (ping failed)
|
|
8
11
|
Disconnected: 'disconnected',
|
|
9
12
|
Connecting: 'connecting',
|
|
10
13
|
Connected: 'connected',
|
|
11
14
|
Closing: 'closing'
|
|
12
15
|
}
|
|
13
16
|
|
|
14
|
-
// Connection state tracking
|
|
15
|
-
let connectionState =
|
|
17
|
+
// Connection state tracking - start with offline check
|
|
18
|
+
let connectionState = (typeof navigator !== 'undefined' && !navigator.onLine)
|
|
19
|
+
? ConnectionState.Offline
|
|
20
|
+
: ConnectionState.Disconnected
|
|
16
21
|
const connectionChangeListeners = []
|
|
17
22
|
|
|
18
23
|
function notifyConnectionChange(newState) {
|
|
@@ -23,32 +28,129 @@ function notifyConnectionChange(newState) {
|
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
// Configuration
|
|
26
|
-
let
|
|
27
|
-
|
|
31
|
+
let configuredTransport = 'auto' // 'auto' | 'websocket' | 'polling'
|
|
32
|
+
|
|
33
|
+
// Transport state
|
|
34
|
+
let currentTransport = null // 'websocket' | 'polling'
|
|
35
|
+
let streamingTransport = null
|
|
36
|
+
let wsRetryTimer = null
|
|
37
|
+
let networkCheckTimer = null
|
|
38
|
+
const WS_FALLBACK_TIMEOUT = 4000 // Time to wait for WS before fallback
|
|
39
|
+
const WS_RETRY_INTERVAL = 30000 // Retry WebSocket while in polling mode
|
|
40
|
+
const PING_TIMEOUT = 3000 // Timeout for ping check
|
|
41
|
+
const MAX_PING_CLOCK_SKEW = 60000 // Max allowed time difference (60s)
|
|
28
42
|
|
|
29
43
|
/**
|
|
30
|
-
*
|
|
31
|
-
* @param {object} opts
|
|
32
|
-
* @param {number} [opts.port] - WebSocket port (default: 9010 for local, 443/80 for remote)
|
|
33
|
-
* @param {string} [opts.host] - WebSocket host (default: auto-detect from window.location)
|
|
44
|
+
* Check if running in dev/local mode
|
|
34
45
|
*/
|
|
35
|
-
function
|
|
36
|
-
if (
|
|
37
|
-
|
|
46
|
+
function isDevMode() {
|
|
47
|
+
if (typeof window === 'undefined') return false
|
|
48
|
+
return ['localhost', '127.0.0.1', '[::1]'].includes(window.location.hostname)
|
|
38
49
|
}
|
|
39
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Build ping URL for captive portal detection
|
|
53
|
+
*/
|
|
54
|
+
function getPingUrl() {
|
|
55
|
+
const hostname = window.location.hostname
|
|
56
|
+
const localServers = ['localhost', '127.0.0.1', '[::1]']
|
|
57
|
+
const isLocal = localServers.includes(hostname)
|
|
58
|
+
const isHttps = window.location.protocol === 'https:'
|
|
59
|
+
const port = isLocal ? 9010 : (window.location.port || (isHttps ? 443 : 80))
|
|
60
|
+
const protocol = isHttps ? 'https' : 'http'
|
|
61
|
+
const portSuffix = (isLocal || (port !== 80 && port !== 443)) ? `:${port}` : ''
|
|
62
|
+
return `${protocol}://${hostname}${portSuffix}/api/ape/ping`
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Check for captive portal by pinging /api/ape/ping
|
|
67
|
+
* Returns 'ok' if real internet, 'walled' if captive portal detected
|
|
68
|
+
*/
|
|
69
|
+
async function checkCaptivePortal() {
|
|
70
|
+
try {
|
|
71
|
+
const controller = new AbortController()
|
|
72
|
+
const timeoutId = setTimeout(() => controller.abort(), PING_TIMEOUT)
|
|
73
|
+
|
|
74
|
+
const response = await fetch(getPingUrl(), {
|
|
75
|
+
cache: 'no-store',
|
|
76
|
+
signal: controller.signal
|
|
77
|
+
})
|
|
78
|
+
clearTimeout(timeoutId)
|
|
79
|
+
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
if (isDevMode()) {
|
|
82
|
+
console.error('🦍 [DEV] Ping failed: HTTP', response.status)
|
|
83
|
+
}
|
|
84
|
+
return 'walled'
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const data = await response.json()
|
|
88
|
+
|
|
89
|
+
// Verify response is genuine (not a captive portal redirect page)
|
|
90
|
+
if (data?.ok !== true) {
|
|
91
|
+
if (isDevMode()) {
|
|
92
|
+
console.error('🦍 [DEV] Ping failed: invalid response', data)
|
|
93
|
+
}
|
|
94
|
+
return 'walled'
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Validate timestamp to detect proxy replay attacks
|
|
98
|
+
if (typeof data.ts === 'number') {
|
|
99
|
+
const now = Date.now()
|
|
100
|
+
const skew = Math.abs(now - data.ts)
|
|
101
|
+
if (skew > MAX_PING_CLOCK_SKEW) {
|
|
102
|
+
if (isDevMode()) {
|
|
103
|
+
console.error('🦍 [DEV] Ping failed: timestamp too old/stale (skew:', skew, 'ms)')
|
|
104
|
+
}
|
|
105
|
+
return 'walled'
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return 'ok'
|
|
110
|
+
} catch (err) {
|
|
111
|
+
if (isDevMode()) {
|
|
112
|
+
console.error('🦍 [DEV] Ping failed:', err.message || err)
|
|
113
|
+
}
|
|
114
|
+
return 'walled'
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Setup navigator.onLine event listeners
|
|
120
|
+
*/
|
|
121
|
+
function setupOnlineListeners() {
|
|
122
|
+
if (typeof window === 'undefined') return
|
|
123
|
+
|
|
124
|
+
window.addEventListener('online', () => {
|
|
125
|
+
console.log('🦍 Browser went online, checking network...')
|
|
126
|
+
// Trigger reconnection attempt
|
|
127
|
+
attemptConnection()
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
window.addEventListener('offline', () => {
|
|
131
|
+
console.log('🦍 Browser went offline')
|
|
132
|
+
notifyConnectionChange(ConnectionState.Offline)
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Setup listeners on module load (browser only)
|
|
137
|
+
if (typeof window !== 'undefined') {
|
|
138
|
+
setupOnlineListeners()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
|
|
40
143
|
/**
|
|
41
144
|
* Get WebSocket URL - auto-detects from window.location, keeps /api/ape path
|
|
42
145
|
*/
|
|
43
146
|
function getSocketUrl() {
|
|
44
|
-
const hostname =
|
|
147
|
+
const hostname = window.location.hostname
|
|
45
148
|
const localServers = ["localhost", "127.0.0.1", "[::1]"]
|
|
46
149
|
const isLocal = localServers.includes(hostname)
|
|
47
150
|
const isHttps = window.location.protocol === "https:"
|
|
48
151
|
|
|
49
152
|
// Default port: 9010 for local dev, otherwise use window.location.port or implicit 443/80
|
|
50
|
-
const
|
|
51
|
-
const port = configuredPort || defaultPort
|
|
153
|
+
const port = isLocal ? 9010 : (window.location.port || (isHttps ? 443 : 80))
|
|
52
154
|
|
|
53
155
|
// Build URL - keep /api/ape path
|
|
54
156
|
const protocol = isHttps ? "wss" : "ws"
|
|
@@ -64,7 +166,7 @@ const totalRequestTimeout = 10000
|
|
|
64
166
|
|
|
65
167
|
const joinKey = "/"
|
|
66
168
|
// Properties accessed directly on `ape` that should NOT be intercepted
|
|
67
|
-
const reservedKeys = new Set(['on'])
|
|
169
|
+
const reservedKeys = new Set(['on', 'onConnectionChange', 'getTransport'])
|
|
68
170
|
const handler = {
|
|
69
171
|
get(fn, key) {
|
|
70
172
|
// Skip proxy interception for reserved keys - return actual property
|
|
@@ -97,449 +199,627 @@ let aWaitingSend = []
|
|
|
97
199
|
const reciverOnAr = [];
|
|
98
200
|
const ofTypesOb = {};
|
|
99
201
|
|
|
100
|
-
|
|
202
|
+
/**
|
|
203
|
+
* Switch to streaming transport (HTTP long polling fallback)
|
|
204
|
+
*/
|
|
205
|
+
function switchToStreaming() {
|
|
206
|
+
console.log('🦍 Switching to HTTP streaming transport')
|
|
207
|
+
currentTransport = 'polling'
|
|
101
208
|
|
|
102
|
-
if (!
|
|
103
|
-
|
|
104
|
-
__socket = new WebSocket(getSocketUrl())
|
|
209
|
+
if (!streamingTransport) {
|
|
210
|
+
streamingTransport = createStreamingTransport()
|
|
105
211
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
212
|
+
// Handle incoming messages from streaming transport
|
|
213
|
+
streamingTransport.onMessage = async (msg) => {
|
|
214
|
+
const { err, type, data } = msg
|
|
215
|
+
|
|
216
|
+
// Dispatch to type-specific handlers
|
|
217
|
+
if (ofTypesOb[type]) {
|
|
218
|
+
ofTypesOb[type].forEach(worker => worker({ err, type, data }))
|
|
219
|
+
}
|
|
220
|
+
// Dispatch to general handlers
|
|
221
|
+
reciverOnAr.forEach(worker => worker({ err, type, data }))
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
streamingTransport.onOpen = () => {
|
|
225
|
+
ready = true
|
|
109
226
|
notifyConnectionChange(ConnectionState.Connected)
|
|
227
|
+
console.log('🦍 HTTP streaming connected')
|
|
228
|
+
|
|
229
|
+
// Flush waiting messages
|
|
110
230
|
aWaitingSend.forEach(({ type, data, next, err, waiting, createdAt, timer }) => {
|
|
111
231
|
clearTimeout(timer)
|
|
112
|
-
|
|
113
|
-
const resultPromise = wsSend(type, data, createdAt)
|
|
232
|
+
const resultPromise = streamingSend(type, data, createdAt)
|
|
114
233
|
if (waiting) {
|
|
115
|
-
resultPromise.then(next)
|
|
116
|
-
.catch(err)
|
|
234
|
+
resultPromise.then(next).catch(err)
|
|
117
235
|
}
|
|
118
236
|
})
|
|
119
|
-
// cloudfler drops the connetion and the client has to remake,
|
|
120
|
-
// we clear the array as we dont need this info every RE-connent
|
|
121
237
|
aWaitingSend = []
|
|
122
|
-
} // END onopen
|
|
123
|
-
|
|
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
238
|
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
}
|
|
239
|
+
// Start background WebSocket retry
|
|
240
|
+
startWsRetry()
|
|
241
|
+
}
|
|
141
242
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
}
|
|
243
|
+
streamingTransport.onClose = () => {
|
|
244
|
+
ready = false
|
|
245
|
+
notifyConnectionChange(ConnectionState.Disconnected)
|
|
246
|
+
}
|
|
156
247
|
|
|
157
|
-
|
|
248
|
+
streamingTransport.onError = (err) => {
|
|
249
|
+
console.error('🦍 Streaming error:', err)
|
|
158
250
|
}
|
|
251
|
+
}
|
|
159
252
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
*/
|
|
163
|
-
function setValueAtPath(obj, path, value) {
|
|
164
|
-
const parts = path.split('.')
|
|
165
|
-
let current = obj
|
|
253
|
+
streamingTransport.connect()
|
|
254
|
+
}
|
|
166
255
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
256
|
+
/**
|
|
257
|
+
* Send via streaming transport
|
|
258
|
+
*/
|
|
259
|
+
function streamingSend(type, data, createdAt) {
|
|
260
|
+
return streamingTransport.send(type, data, createdAt)
|
|
261
|
+
}
|
|
170
262
|
|
|
171
|
-
|
|
263
|
+
/**
|
|
264
|
+
* Start background retry for WebSocket (while in polling mode)
|
|
265
|
+
*/
|
|
266
|
+
function startWsRetry() {
|
|
267
|
+
if (wsRetryTimer) return
|
|
268
|
+
if (currentTransport !== 'polling') return
|
|
269
|
+
if (configuredTransport === 'polling') return // User explicitly wants polling only
|
|
270
|
+
|
|
271
|
+
wsRetryTimer = setInterval(() => {
|
|
272
|
+
if (currentTransport !== 'polling') {
|
|
273
|
+
clearInterval(wsRetryTimer)
|
|
274
|
+
wsRetryTimer = null
|
|
275
|
+
return
|
|
172
276
|
}
|
|
173
277
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (obj === null || obj === undefined || typeof obj !== 'object') {
|
|
179
|
-
return obj
|
|
180
|
-
}
|
|
278
|
+
console.log('🦍 Attempting WebSocket reconnection...')
|
|
279
|
+
tryWebSocket(true)
|
|
280
|
+
}, WS_RETRY_INTERVAL)
|
|
281
|
+
}
|
|
181
282
|
|
|
182
|
-
|
|
183
|
-
|
|
283
|
+
/**
|
|
284
|
+
* Try to establish WebSocket connection
|
|
285
|
+
* @param {boolean} isRetry - If true, this is a background retry attempt
|
|
286
|
+
*/
|
|
287
|
+
function tryWebSocket(isRetry = false) {
|
|
288
|
+
const ws = new WebSocket(getSocketUrl())
|
|
289
|
+
let fallbackTimer = null
|
|
290
|
+
|
|
291
|
+
// Set fallback timeout (only for initial connection, not retries)
|
|
292
|
+
if (!isRetry && configuredTransport === 'auto') {
|
|
293
|
+
fallbackTimer = setTimeout(() => {
|
|
294
|
+
if (ws.readyState !== WebSocket.OPEN) {
|
|
295
|
+
console.log('🦍 WebSocket timeout, falling back to HTTP streaming')
|
|
296
|
+
ws.close()
|
|
297
|
+
switchToStreaming()
|
|
184
298
|
}
|
|
299
|
+
}, WS_FALLBACK_TIMEOUT)
|
|
300
|
+
}
|
|
185
301
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
302
|
+
ws.onopen = () => {
|
|
303
|
+
if (fallbackTimer) clearTimeout(fallbackTimer)
|
|
304
|
+
|
|
305
|
+
// If this is a retry and we're in polling mode, switch back to WebSocket
|
|
306
|
+
if (isRetry && currentTransport === 'polling') {
|
|
307
|
+
console.log('🦍 WebSocket reconnected, switching from HTTP streaming')
|
|
308
|
+
if (streamingTransport) {
|
|
309
|
+
streamingTransport.close()
|
|
310
|
+
}
|
|
311
|
+
if (wsRetryTimer) {
|
|
312
|
+
clearInterval(wsRetryTimer)
|
|
313
|
+
wsRetryTimer = null
|
|
194
314
|
}
|
|
195
|
-
return cleaned
|
|
196
315
|
}
|
|
197
316
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const resources = findLinkedResources(data)
|
|
317
|
+
currentTransport = 'websocket'
|
|
318
|
+
__socket = ws
|
|
319
|
+
ready = true
|
|
320
|
+
notifyConnectionChange(ConnectionState.Connected)
|
|
203
321
|
|
|
204
|
-
|
|
205
|
-
|
|
322
|
+
aWaitingSend.forEach(({ type, data, next, err, waiting, createdAt, timer }) => {
|
|
323
|
+
clearTimeout(timer)
|
|
324
|
+
const resultPromise = wsSend(type, data, createdAt)
|
|
325
|
+
if (waiting) {
|
|
326
|
+
resultPromise.then(next).catch(err)
|
|
206
327
|
}
|
|
328
|
+
})
|
|
329
|
+
aWaitingSend = []
|
|
330
|
+
}
|
|
207
331
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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) {
|
|
249
|
-
//console.log('WebSocket message:', event);
|
|
250
|
-
const { err, type, queryId, data } = jss.parse(event.data)
|
|
251
|
-
|
|
252
|
-
// Messages with queryId must fulfill matching promise
|
|
253
|
-
if (queryId) {
|
|
254
|
-
if (waitingOn[queryId]) {
|
|
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)
|
|
332
|
+
ws.onmessage = async function (event) {
|
|
333
|
+
const { err, type, queryId, data } = jss.parse(event.data)
|
|
334
|
+
|
|
335
|
+
// Messages with queryId must fulfill matching promise
|
|
336
|
+
if (queryId) {
|
|
337
|
+
if (waitingOn[queryId]) {
|
|
338
|
+
// Check for linked resources and fetch them before resolving
|
|
339
|
+
if (data && !err) {
|
|
340
|
+
try {
|
|
341
|
+
const hydratedData = await fetchLinkedResources(data)
|
|
342
|
+
waitingOn[queryId](err, hydratedData)
|
|
343
|
+
} catch (fetchErr) {
|
|
344
|
+
waitingOn[queryId](fetchErr, null)
|
|
265
345
|
}
|
|
266
|
-
delete waitingOn[queryId]
|
|
267
346
|
} else {
|
|
268
|
-
|
|
269
|
-
console.error(`🦍 No matching queryId: ${queryId}`)
|
|
347
|
+
waitingOn[queryId](err, data)
|
|
270
348
|
}
|
|
271
|
-
|
|
349
|
+
delete waitingOn[queryId]
|
|
350
|
+
} else {
|
|
351
|
+
console.error(`🦍 No matching queryId: ${queryId}`)
|
|
272
352
|
}
|
|
353
|
+
return
|
|
354
|
+
}
|
|
273
355
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
console.error(`🦍 Failed to hydrate broadcast data:`, fetchErr)
|
|
282
|
-
}
|
|
356
|
+
// Only messages WITHOUT queryId go to setOnReciver
|
|
357
|
+
let processedData = data
|
|
358
|
+
if (data && !err) {
|
|
359
|
+
try {
|
|
360
|
+
processedData = await fetchLinkedResources(data)
|
|
361
|
+
} catch (fetchErr) {
|
|
362
|
+
console.error(`🦍 Failed to hydrate broadcast data:`, fetchErr)
|
|
283
363
|
}
|
|
364
|
+
}
|
|
284
365
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
366
|
+
if (ofTypesOb[type]) {
|
|
367
|
+
ofTypesOb[type].forEach(worker => worker({ err, type, data: processedData }))
|
|
368
|
+
}
|
|
369
|
+
reciverOnAr.forEach(worker => worker({ err, type, data: processedData }))
|
|
370
|
+
}
|
|
289
371
|
|
|
290
|
-
|
|
372
|
+
ws.onerror = function (err) {
|
|
373
|
+
if (fallbackTimer) clearTimeout(fallbackTimer)
|
|
374
|
+
console.error('socket ERROR:', err)
|
|
291
375
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
376
|
+
// On initial connection error in auto mode, fallback to streaming
|
|
377
|
+
if (!isRetry && configuredTransport === 'auto' && !ready) {
|
|
378
|
+
switchToStreaming()
|
|
379
|
+
}
|
|
380
|
+
}
|
|
295
381
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
382
|
+
ws.onclose = function (event) {
|
|
383
|
+
if (fallbackTimer) clearTimeout(fallbackTimer)
|
|
384
|
+
console.warn('socket disconnect:', event)
|
|
385
|
+
__socket = false
|
|
386
|
+
ready = false
|
|
387
|
+
|
|
388
|
+
// Only notify disconnected if we're on websocket transport
|
|
389
|
+
if (currentTransport === 'websocket') {
|
|
300
390
|
notifyConnectionChange(ConnectionState.Disconnected)
|
|
301
|
-
setTimeout(() => reconnect && connectSocket(), 500)
|
|
302
|
-
} // END onclose
|
|
303
|
-
|
|
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
|
|
391
|
+
setTimeout(() => reconnect && connectSocket(), 500)
|
|
333
392
|
}
|
|
334
|
-
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Find all L-tagged (binary link) properties in data
|
|
398
|
+
* Returns array of { path, hash }
|
|
399
|
+
*/
|
|
400
|
+
function findLinkedResources(obj, path = '') {
|
|
401
|
+
const resources = []
|
|
402
|
+
|
|
403
|
+
if (obj === null || obj === undefined || typeof obj !== 'object') {
|
|
404
|
+
return resources
|
|
335
405
|
}
|
|
336
406
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
*/
|
|
341
|
-
function processBinaryForUpload(data, path = '') {
|
|
342
|
-
if (data === null || data === undefined) {
|
|
343
|
-
return { processedData: data, uploads: [] }
|
|
407
|
+
if (Array.isArray(obj)) {
|
|
408
|
+
for (let i = 0; i < obj.length; i++) {
|
|
409
|
+
resources.push(...findLinkedResources(obj[i], path ? `${path}.${i}` : String(i)))
|
|
344
410
|
}
|
|
411
|
+
return resources
|
|
412
|
+
}
|
|
345
413
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
414
|
+
for (const key of Object.keys(obj)) {
|
|
415
|
+
// Check for L-tag in key (from JJS encoding: key<!L>)
|
|
416
|
+
if (key.endsWith('<!L>')) {
|
|
417
|
+
const cleanKey = key.slice(0, -4)
|
|
418
|
+
const hash = obj[key]
|
|
419
|
+
resources.push({
|
|
420
|
+
path: path ? `${path}.${cleanKey}` : cleanKey,
|
|
421
|
+
hash,
|
|
422
|
+
originalKey: key
|
|
423
|
+
})
|
|
424
|
+
} else {
|
|
425
|
+
resources.push(...findLinkedResources(obj[key], path ? `${path}.${key}` : key))
|
|
353
426
|
}
|
|
427
|
+
}
|
|
354
428
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
const allUploads = []
|
|
429
|
+
return resources
|
|
430
|
+
}
|
|
358
431
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
432
|
+
/**
|
|
433
|
+
* Set a value at a nested path in an object
|
|
434
|
+
*/
|
|
435
|
+
function setValueAtPath(obj, path, value) {
|
|
436
|
+
const parts = path.split('.')
|
|
437
|
+
let current = obj
|
|
438
|
+
|
|
439
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
440
|
+
current = current[parts[i]]
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
current[parts[parts.length - 1]] = value
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Clean up L-tagged keys (rename key<!L> to key)
|
|
448
|
+
*/
|
|
449
|
+
function cleanLinkedKeys(obj) {
|
|
450
|
+
if (obj === null || obj === undefined || typeof obj !== 'object') {
|
|
451
|
+
return obj
|
|
452
|
+
}
|
|
365
453
|
|
|
366
|
-
|
|
454
|
+
if (Array.isArray(obj)) {
|
|
455
|
+
return obj.map(cleanLinkedKeys)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const cleaned = {}
|
|
459
|
+
for (const key of Object.keys(obj)) {
|
|
460
|
+
if (key.endsWith('<!L>')) {
|
|
461
|
+
const cleanKey = key.slice(0, -4)
|
|
462
|
+
cleaned[cleanKey] = obj[key]
|
|
463
|
+
} else {
|
|
464
|
+
cleaned[key] = cleanLinkedKeys(obj[key])
|
|
367
465
|
}
|
|
466
|
+
}
|
|
467
|
+
return cleaned
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Fetch binary resources and hydrate data object
|
|
472
|
+
*/
|
|
473
|
+
async function fetchLinkedResources(data, clientId) {
|
|
474
|
+
const resources = findLinkedResources(data)
|
|
368
475
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
476
|
+
if (resources.length === 0) {
|
|
477
|
+
return data
|
|
478
|
+
}
|
|
372
479
|
|
|
373
|
-
|
|
374
|
-
const itemPath = path ? `${path}.${key}` : key
|
|
375
|
-
const { processedData, uploads } = processBinaryForUpload(data[key], itemPath)
|
|
480
|
+
console.log(`🦍 Fetching ${resources.length} binary resource(s)`)
|
|
376
481
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
482
|
+
const cleanedData = cleanLinkedKeys(data)
|
|
483
|
+
|
|
484
|
+
const hostname = window.location.hostname
|
|
485
|
+
const isLocal = ["localhost", "127.0.0.1", "[::1]"].includes(hostname)
|
|
486
|
+
const isHttps = window.location.protocol === "https:"
|
|
487
|
+
const port = isLocal ? 9010 : (window.location.port || (isHttps ? 443 : 80))
|
|
488
|
+
const protocol = isHttps ? "https" : "http"
|
|
489
|
+
const portSuffix = (isLocal || (port !== 80 && port !== 443)) ? `:${port}` : ""
|
|
490
|
+
const baseUrl = `${protocol}://${hostname}${portSuffix}`
|
|
491
|
+
|
|
492
|
+
await Promise.all(resources.map(async ({ path, hash }) => {
|
|
493
|
+
try {
|
|
494
|
+
const response = await fetch(`${baseUrl}/api/ape/data/${hash}`, {
|
|
495
|
+
credentials: 'include',
|
|
496
|
+
headers: {
|
|
497
|
+
'X-Ape-Client-Id': clientId || ''
|
|
383
498
|
}
|
|
384
|
-
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
if (!response.ok) {
|
|
502
|
+
throw new Error(`Failed to fetch binary resource: ${response.status}`)
|
|
385
503
|
}
|
|
386
504
|
|
|
387
|
-
|
|
505
|
+
const arrayBuffer = await response.arrayBuffer()
|
|
506
|
+
setValueAtPath(cleanedData, path, arrayBuffer)
|
|
507
|
+
} catch (err) {
|
|
508
|
+
console.error(`🦍 Failed to fetch binary resource at ${path}:`, err)
|
|
509
|
+
setValueAtPath(cleanedData, path, null)
|
|
388
510
|
}
|
|
511
|
+
}))
|
|
389
512
|
|
|
390
|
-
|
|
513
|
+
return cleanedData
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Attempt to establish connection with network pre-checks
|
|
518
|
+
*/
|
|
519
|
+
async function attemptConnection() {
|
|
520
|
+
// Check if browser is online
|
|
521
|
+
if (typeof navigator !== 'undefined' && !navigator.onLine) {
|
|
522
|
+
notifyConnectionChange(ConnectionState.Offline)
|
|
523
|
+
return
|
|
391
524
|
}
|
|
392
525
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
async function uploadBinaryData(queryId, uploads) {
|
|
397
|
-
if (uploads.length === 0) return
|
|
526
|
+
// Perform captive portal check
|
|
527
|
+
notifyConnectionChange(ConnectionState.Connecting)
|
|
528
|
+
const pingResult = await checkCaptivePortal()
|
|
398
529
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
const protocol = isHttps ? "https" : "http"
|
|
406
|
-
const portSuffix = (isLocal || (port !== 80 && port !== 443)) ? `:${port}` : ""
|
|
407
|
-
const baseUrl = `${protocol}://${hostname}${portSuffix}`
|
|
530
|
+
if (pingResult === 'walled') {
|
|
531
|
+
notifyConnectionChange(ConnectionState.Walled)
|
|
532
|
+
// Retry network check periodically
|
|
533
|
+
scheduleNetworkRetry()
|
|
534
|
+
return
|
|
535
|
+
}
|
|
408
536
|
|
|
409
|
-
|
|
537
|
+
// Network is good, proceed with socket connection
|
|
538
|
+
proceedWithConnection()
|
|
539
|
+
}
|
|
410
540
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
541
|
+
/**
|
|
542
|
+
* Schedule a retry of network check (for walled/offline states)
|
|
543
|
+
*/
|
|
544
|
+
function scheduleNetworkRetry() {
|
|
545
|
+
if (networkCheckTimer) return
|
|
546
|
+
networkCheckTimer = setTimeout(() => {
|
|
547
|
+
networkCheckTimer = null
|
|
548
|
+
attemptConnection()
|
|
549
|
+
}, WS_RETRY_INTERVAL)
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Proceed with WebSocket/polling connection after network checks pass
|
|
554
|
+
*/
|
|
555
|
+
function proceedWithConnection() {
|
|
556
|
+
// Determine which transport to use
|
|
557
|
+
if (configuredTransport === 'polling') {
|
|
558
|
+
switchToStreaming()
|
|
559
|
+
} else {
|
|
560
|
+
// 'auto' or 'websocket' - try WebSocket first
|
|
561
|
+
tryWebSocket(false)
|
|
430
562
|
}
|
|
563
|
+
}
|
|
431
564
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
565
|
+
function connectSocket() {
|
|
566
|
+
// Skip if already connected or connecting
|
|
567
|
+
if (__socket && __socket.readyState !== WebSocket.CLOSED) {
|
|
568
|
+
return buildClientInterface()
|
|
569
|
+
}
|
|
570
|
+
if (currentTransport === 'polling' && streamingTransport?.isConnected()) {
|
|
571
|
+
return buildClientInterface()
|
|
572
|
+
}
|
|
573
|
+
if (connectionState === ConnectionState.Connecting) {
|
|
574
|
+
return buildClientInterface()
|
|
575
|
+
}
|
|
435
576
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
rej(new Error("Request Timedout for :" + type))
|
|
439
|
-
}
|
|
440
|
-
}, timeLetForReqToBeMade);
|
|
441
|
-
|
|
442
|
-
// Process binary data for upload
|
|
443
|
-
const { processedData, uploads } = processBinaryForUpload(data)
|
|
444
|
-
|
|
445
|
-
const payload = {
|
|
446
|
-
type,
|
|
447
|
-
data: processedData,
|
|
448
|
-
//referer:window.location.href,
|
|
449
|
-
createdAt: new Date(createdAt),
|
|
450
|
-
requestedAt: dirctCall ? undefined
|
|
451
|
-
: new Date()
|
|
452
|
-
}
|
|
453
|
-
const message = jss.stringify(payload)
|
|
454
|
-
const queryId = messageHash(message);
|
|
577
|
+
// Start connection with network pre-checks
|
|
578
|
+
attemptConnection()
|
|
455
579
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
580
|
+
return buildClientInterface()
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Check if value is binary data (ArrayBuffer, typed array, or Blob)
|
|
585
|
+
*/
|
|
586
|
+
function isBinaryData(value) {
|
|
587
|
+
if (value === null || value === undefined) return false
|
|
588
|
+
return value instanceof ArrayBuffer ||
|
|
589
|
+
ArrayBuffer.isView(value) ||
|
|
590
|
+
(typeof Blob !== 'undefined' && value instanceof Blob)
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Get binary type tag (A for ArrayBuffer, B for Blob)
|
|
595
|
+
*/
|
|
596
|
+
function getBinaryTag(value) {
|
|
597
|
+
if (typeof Blob !== 'undefined' && value instanceof Blob) return 'B'
|
|
598
|
+
return 'A'
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Generate a simple hash for binary upload
|
|
603
|
+
*/
|
|
604
|
+
function generateUploadHash(path) {
|
|
605
|
+
let hash = 0
|
|
606
|
+
for (let i = 0; i < path.length; i++) {
|
|
607
|
+
const char = path.charCodeAt(i)
|
|
608
|
+
hash = ((hash << 5) - hash) + char
|
|
609
|
+
hash = hash & hash
|
|
610
|
+
}
|
|
611
|
+
return Math.abs(hash).toString(36)
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Find and extract binary data from payload
|
|
616
|
+
* Returns { processedData, uploads: [{ path, hash, data, tag }] }
|
|
617
|
+
*/
|
|
618
|
+
function processBinaryForUpload(data, path = '') {
|
|
619
|
+
if (data === null || data === undefined) {
|
|
620
|
+
return { processedData: data, uploads: [] }
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (isBinaryData(data)) {
|
|
624
|
+
const tag = getBinaryTag(data)
|
|
625
|
+
const hash = generateUploadHash(path || 'root')
|
|
626
|
+
return {
|
|
627
|
+
processedData: { [`__ape_upload__`]: hash },
|
|
628
|
+
uploads: [{ path, hash, data, tag }]
|
|
483
629
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (Array.isArray(data)) {
|
|
633
|
+
const processedArray = []
|
|
634
|
+
const allUploads = []
|
|
635
|
+
|
|
636
|
+
for (let i = 0; i < data.length; i++) {
|
|
637
|
+
const itemPath = path ? `${path}.${i}` : String(i)
|
|
638
|
+
const { processedData, uploads } = processBinaryForUpload(data[i], itemPath)
|
|
639
|
+
processedArray.push(processedData)
|
|
640
|
+
allUploads.push(...uploads)
|
|
490
641
|
}
|
|
491
|
-
return replyPromise
|
|
492
|
-
} // END wsSend
|
|
493
642
|
|
|
643
|
+
return { processedData: processedArray, uploads: allUploads }
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (typeof data === 'object') {
|
|
647
|
+
const processedObj = {}
|
|
648
|
+
const allUploads = []
|
|
494
649
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
650
|
+
for (const key of Object.keys(data)) {
|
|
651
|
+
const itemPath = path ? `${path}.${key}` : key
|
|
652
|
+
const { processedData, uploads } = processBinaryForUpload(data[key], itemPath)
|
|
653
|
+
|
|
654
|
+
// If this was binary data, mark the key with <!B> or <!A> tag
|
|
655
|
+
if (uploads.length > 0 && processedData?.__ape_upload__) {
|
|
656
|
+
const tag = uploads[uploads.length - 1].tag
|
|
657
|
+
processedObj[`${key}<!${tag}>`] = processedData.__ape_upload__
|
|
658
|
+
} else {
|
|
659
|
+
processedObj[key] = processedData
|
|
660
|
+
}
|
|
661
|
+
allUploads.push(...uploads)
|
|
498
662
|
}
|
|
499
663
|
|
|
500
|
-
|
|
664
|
+
return { processedData: processedObj, uploads: allUploads }
|
|
665
|
+
}
|
|
501
666
|
|
|
502
|
-
|
|
503
|
-
|
|
667
|
+
return { processedData: data, uploads: [] }
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Upload binary data via HTTP PUT
|
|
672
|
+
*/
|
|
673
|
+
async function uploadBinaryData(queryId, uploads) {
|
|
674
|
+
if (uploads.length === 0) return
|
|
675
|
+
|
|
676
|
+
// Build base URL
|
|
677
|
+
const hostname = window.location.hostname
|
|
678
|
+
const isLocal = ["localhost", "127.0.0.1", "[::1]"].includes(hostname)
|
|
679
|
+
const isHttps = window.location.protocol === "https:"
|
|
680
|
+
const port = isLocal ? 9010 : (window.location.port || (isHttps ? 443 : 80))
|
|
681
|
+
const protocol = isHttps ? "https" : "http"
|
|
682
|
+
const portSuffix = (isLocal || (port !== 80 && port !== 443)) ? `:${port}` : ""
|
|
683
|
+
const baseUrl = `${protocol}://${hostname}${portSuffix}`
|
|
684
|
+
|
|
685
|
+
console.log(`🦍 Uploading ${uploads.length} binary file(s)`)
|
|
686
|
+
|
|
687
|
+
await Promise.all(uploads.map(async ({ hash, data }) => {
|
|
688
|
+
try {
|
|
689
|
+
const response = await fetch(`${baseUrl}/api/ape/data/${queryId}/${hash}`, {
|
|
690
|
+
method: 'PUT',
|
|
691
|
+
credentials: 'include',
|
|
692
|
+
headers: {
|
|
693
|
+
'Content-Type': 'application/octet-stream'
|
|
694
|
+
},
|
|
695
|
+
body: data
|
|
696
|
+
})
|
|
697
|
+
|
|
698
|
+
if (!response.ok) {
|
|
699
|
+
throw new Error(`Upload failed: ${response.status}`)
|
|
700
|
+
}
|
|
701
|
+
} catch (err) {
|
|
702
|
+
console.error(`🦍 Failed to upload binary at ${hash}:`, err)
|
|
703
|
+
throw err
|
|
504
704
|
}
|
|
705
|
+
}))
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
wsSend = function (type, data, createdAt, dirctCall) {
|
|
709
|
+
let rej, promiseIsLive = false;
|
|
710
|
+
const timeLetForReqToBeMade = (createdAt + totalRequestTimeout) - Date.now()
|
|
505
711
|
|
|
506
|
-
|
|
712
|
+
const timer = setTimeout(() => {
|
|
713
|
+
if (promiseIsLive) {
|
|
714
|
+
rej(new Error("Request Timedout for :" + type))
|
|
715
|
+
}
|
|
716
|
+
}, timeLetForReqToBeMade);
|
|
717
|
+
|
|
718
|
+
// Process binary data for upload
|
|
719
|
+
const { processedData, uploads } = processBinaryForUpload(data)
|
|
720
|
+
|
|
721
|
+
const payload = {
|
|
722
|
+
type,
|
|
723
|
+
data: processedData,
|
|
724
|
+
//referer:window.location.href,
|
|
725
|
+
createdAt: new Date(createdAt),
|
|
726
|
+
requestedAt: dirctCall ? undefined
|
|
727
|
+
: new Date()
|
|
728
|
+
}
|
|
729
|
+
const message = jss.stringify(payload)
|
|
730
|
+
const queryId = messageHash(message);
|
|
507
731
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
732
|
+
const replyPromise = new Promise((resolve, reject) => {
|
|
733
|
+
rej = reject
|
|
734
|
+
waitingOn[queryId] = (err, result) => {
|
|
735
|
+
clearTimeout(timer)
|
|
736
|
+
replyPromise.then = next.bind(replyPromise)
|
|
737
|
+
if (err) {
|
|
738
|
+
reject(err)
|
|
512
739
|
} else {
|
|
513
|
-
|
|
740
|
+
resolve(result)
|
|
514
741
|
}
|
|
515
|
-
}, timeLetForReqToBeMade);
|
|
516
|
-
|
|
517
|
-
const payload = { type, data, next: undefined, err: undefined, waiting: false, createdAt, timer };
|
|
518
|
-
const waitingOnOpen = new Promise((res, er) => { payload.next = res; payload.err = er; })
|
|
519
|
-
|
|
520
|
-
const waitingOnOpenThen = waitingOnOpen.then;
|
|
521
|
-
const waitingOnOpenCatch = waitingOnOpen.catch;
|
|
522
|
-
waitingOnOpen.then = worker => {
|
|
523
|
-
payload.waiting = true;
|
|
524
|
-
waitingOnOpen.then = waitingOnOpenThen.bind(waitingOnOpen)
|
|
525
|
-
waitingOnOpen.catch = waitingOnOpenCatch.bind(waitingOnOpen)
|
|
526
|
-
return waitingOnOpenThen.call(waitingOnOpen, worker)
|
|
527
742
|
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
743
|
+
__socket.send(message);
|
|
744
|
+
|
|
745
|
+
// Upload binary data after sending WS message
|
|
746
|
+
if (uploads.length > 0) {
|
|
747
|
+
uploadBinaryData(queryId, uploads).catch(err => {
|
|
748
|
+
console.error('🦍 Binary upload failed:', err)
|
|
749
|
+
// The server will timeout waiting for the upload
|
|
750
|
+
})
|
|
533
751
|
}
|
|
752
|
+
});
|
|
753
|
+
const next = replyPromise.then;
|
|
754
|
+
replyPromise.then = worker => {
|
|
755
|
+
promiseIsLive = true;
|
|
756
|
+
replyPromise.then = next.bind(replyPromise)
|
|
757
|
+
replyPromise.catch = err.bind(replyPromise)
|
|
758
|
+
return next.call(replyPromise, worker)
|
|
759
|
+
}
|
|
760
|
+
const err = replyPromise.catch;
|
|
761
|
+
replyPromise.catch = worker => {
|
|
762
|
+
promiseIsLive = true;
|
|
763
|
+
replyPromise.catch = err.bind(replyPromise)
|
|
764
|
+
replyPromise.then = next.bind(replyPromise)
|
|
765
|
+
return err.call(replyPromise, worker)
|
|
766
|
+
}
|
|
767
|
+
return replyPromise
|
|
768
|
+
} // END wsSend
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
const sender = (type, data) => {
|
|
772
|
+
if ("string" !== typeof type) {
|
|
773
|
+
throw new Error("Missing Path vaule")
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const createdAt = Date.now()
|
|
534
777
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
778
|
+
if (ready) {
|
|
779
|
+
return wsSend(type, data, createdAt, true)
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const timeLetForReqToBeMade = (createdAt + connentTimeout) - Date.now() // 5sec for reconnent
|
|
783
|
+
|
|
784
|
+
const timer = setTimeout(() => {
|
|
785
|
+
const errMessage = "Request not sent for :" + type
|
|
786
|
+
if (payload.waiting) {
|
|
787
|
+
payload.err(new Error(errMessage))
|
|
788
|
+
} else {
|
|
789
|
+
throw new Error(errMessage)
|
|
538
790
|
}
|
|
791
|
+
}, timeLetForReqToBeMade);
|
|
792
|
+
|
|
793
|
+
const payload = { type, data, next: undefined, err: undefined, waiting: false, createdAt, timer };
|
|
794
|
+
const waitingOnOpen = new Promise((res, er) => { payload.next = res; payload.err = er; })
|
|
795
|
+
|
|
796
|
+
const waitingOnOpenThen = waitingOnOpen.then;
|
|
797
|
+
const waitingOnOpenCatch = waitingOnOpen.catch;
|
|
798
|
+
waitingOnOpen.then = worker => {
|
|
799
|
+
payload.waiting = true;
|
|
800
|
+
waitingOnOpen.then = waitingOnOpenThen.bind(waitingOnOpen)
|
|
801
|
+
waitingOnOpen.catch = waitingOnOpenCatch.bind(waitingOnOpen)
|
|
802
|
+
return waitingOnOpenThen.call(waitingOnOpen, worker)
|
|
803
|
+
}
|
|
804
|
+
waitingOnOpen.catch = worker => {
|
|
805
|
+
payload.waiting = true;
|
|
806
|
+
waitingOnOpen.catch = waitingOnOpenCatch.bind(waitingOnOpen)
|
|
807
|
+
waitingOnOpen.then = waitingOnOpenThen.bind(waitingOnOpen)
|
|
808
|
+
return waitingOnOpenCatch.call(waitingOnOpen, worker)
|
|
809
|
+
}
|
|
539
810
|
|
|
540
|
-
|
|
541
|
-
|
|
811
|
+
aWaitingSend.push(payload)
|
|
812
|
+
if (!__socket) {
|
|
813
|
+
connectSocket()
|
|
814
|
+
}
|
|
542
815
|
|
|
816
|
+
return waitingOnOpen
|
|
817
|
+
} // END sender
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Build the client interface object
|
|
821
|
+
*/
|
|
822
|
+
function buildClientInterface() {
|
|
543
823
|
return {
|
|
544
824
|
sender: wrap(sender),
|
|
545
825
|
setOnReciver: (onTypeStFn, handlerFn) => {
|
|
@@ -552,7 +832,7 @@ function connectSocket() {
|
|
|
552
832
|
reciverOnAr.push(onTypeStFn)
|
|
553
833
|
}
|
|
554
834
|
}
|
|
555
|
-
},
|
|
835
|
+
},
|
|
556
836
|
onConnectionChange: (handler) => {
|
|
557
837
|
connectionChangeListeners.push(handler)
|
|
558
838
|
// Immediately call with current state
|
|
@@ -562,14 +842,15 @@ function connectSocket() {
|
|
|
562
842
|
const idx = connectionChangeListeners.indexOf(handler)
|
|
563
843
|
if (idx > -1) connectionChangeListeners.splice(idx, 1)
|
|
564
844
|
}
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
|
|
845
|
+
},
|
|
846
|
+
// Expose current transport type
|
|
847
|
+
getTransport: () => currentTransport
|
|
848
|
+
}
|
|
849
|
+
}
|
|
568
850
|
|
|
569
851
|
connectSocket.autoReconnect = () => reconnect = true
|
|
570
|
-
connectSocket.configure = configure
|
|
571
852
|
connectSocket.ConnectionState = ConnectionState
|
|
572
853
|
connect = connectSocket
|
|
573
854
|
|
|
574
855
|
export default connect;
|
|
575
|
-
export {
|
|
856
|
+
export { ConnectionState };
|