api-ape 3.0.2 → 4.1.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 +59 -572
- package/client/README.md +73 -14
- package/client/auth/crypto/aead.js +214 -0
- package/client/auth/crypto/constants.js +32 -0
- package/client/auth/crypto/encoding.js +104 -0
- package/client/auth/crypto/files.md +27 -0
- package/client/auth/crypto/kdf.js +217 -0
- package/client/auth/crypto-utils.js +118 -0
- package/client/auth/files.md +52 -0
- package/client/auth/key-recovery.js +288 -0
- package/client/auth/recovery/constants.js +37 -0
- package/client/auth/recovery/files.md +23 -0
- package/client/auth/recovery/key-derivation.js +61 -0
- package/client/auth/recovery/sss-browser.js +189 -0
- package/client/auth/share-storage.js +205 -0
- package/client/auth/storage/constants.js +18 -0
- package/client/auth/storage/db.js +132 -0
- package/client/auth/storage/files.md +27 -0
- package/client/auth/storage/keys.js +173 -0
- package/client/auth/storage/shares.js +200 -0
- package/client/browser.js +190 -23
- package/client/connectSocket.js +418 -988
- package/client/connection/README.md +23 -0
- package/client/connection/fileDownload.js +256 -0
- package/client/connection/fileHandling.js +450 -0
- package/client/connection/fileUtils.js +346 -0
- package/client/connection/files.md +71 -0
- package/client/connection/messageHandler.js +105 -0
- package/client/connection/network.js +350 -0
- package/client/connection/proxy.js +233 -0
- package/client/connection/sender.js +333 -0
- package/client/connection/state.js +321 -0
- package/client/connection/subscriptions.js +151 -0
- package/client/files.md +53 -0
- package/client/index.js +298 -142
- package/client/transports/README.md +50 -0
- package/client/transports/files.md +41 -0
- package/client/transports/streamParser.js +195 -0
- package/client/transports/streaming.js +555 -203
- package/dist/ape.js +6 -1
- package/dist/ape.js.map +4 -4
- package/index.d.ts +38 -16
- package/package.json +31 -6
- package/server/README.md +272 -67
- package/server/adapters/README.md +23 -14
- package/server/adapters/files.md +68 -0
- package/server/adapters/firebase.js +543 -160
- package/server/adapters/index.js +362 -112
- package/server/adapters/mongo.js +530 -140
- package/server/adapters/postgres.js +534 -155
- package/server/adapters/redis.js +508 -143
- package/server/adapters/supabase.js +555 -186
- package/server/client/README.md +43 -0
- package/server/client/connection.js +586 -0
- package/server/client/files.md +40 -0
- package/server/client/index.js +342 -0
- package/server/files.md +54 -0
- package/server/index.js +322 -71
- package/server/lib/README.md +26 -0
- package/server/lib/broadcast/clients.js +219 -0
- package/server/lib/broadcast/files.md +58 -0
- package/server/lib/broadcast/index.js +57 -0
- package/server/lib/broadcast/publishProxy.js +110 -0
- package/server/lib/broadcast/pubsub.js +137 -0
- package/server/lib/broadcast/sendProxy.js +103 -0
- package/server/lib/bun.js +315 -99
- package/server/lib/fileTransfer/README.md +63 -0
- package/server/lib/fileTransfer/files.md +30 -0
- package/server/lib/fileTransfer/streaming.js +435 -0
- package/server/lib/fileTransfer.js +710 -326
- package/server/lib/files.md +111 -0
- package/server/lib/httpUtils.js +283 -0
- package/server/lib/loader.js +208 -7
- package/server/lib/longPolling/README.md +63 -0
- package/server/lib/longPolling/files.md +44 -0
- package/server/lib/longPolling/getHandler.js +365 -0
- package/server/lib/longPolling/postHandler.js +327 -0
- package/server/lib/longPolling.js +174 -219
- package/server/lib/main.js +369 -532
- package/server/lib/runtimes/README.md +42 -0
- package/server/lib/runtimes/bun.js +586 -0
- package/server/lib/runtimes/files.md +56 -0
- package/server/lib/runtimes/node.js +511 -0
- package/server/lib/wiring.js +539 -98
- package/server/lib/ws/README.md +35 -0
- package/server/lib/ws/adapters/README.md +54 -0
- package/server/lib/ws/adapters/bun.js +538 -170
- package/server/lib/ws/adapters/deno.js +623 -149
- package/server/lib/ws/adapters/files.md +42 -0
- package/server/lib/ws/files.md +74 -0
- package/server/lib/ws/frames.js +532 -154
- package/server/lib/ws/index.js +207 -10
- package/server/lib/ws/server.js +385 -92
- package/server/lib/ws/socket.js +549 -181
- package/server/lib/wsProvider.js +363 -89
- package/server/plugins/binary.js +282 -0
- package/server/security/README.md +92 -0
- package/server/security/auth/README.md +319 -0
- package/server/security/auth/adapters/files.md +95 -0
- package/server/security/auth/adapters/ldap/constants.js +37 -0
- package/server/security/auth/adapters/ldap/files.md +19 -0
- package/server/security/auth/adapters/ldap/helpers.js +111 -0
- package/server/security/auth/adapters/ldap.js +353 -0
- package/server/security/auth/adapters/oauth2/constants.js +41 -0
- package/server/security/auth/adapters/oauth2/files.md +19 -0
- package/server/security/auth/adapters/oauth2/helpers.js +123 -0
- package/server/security/auth/adapters/oauth2.js +273 -0
- package/server/security/auth/adapters/opaque-handlers.js +314 -0
- package/server/security/auth/adapters/opaque.js +205 -0
- package/server/security/auth/adapters/saml/constants.js +52 -0
- package/server/security/auth/adapters/saml/files.md +19 -0
- package/server/security/auth/adapters/saml/helpers.js +74 -0
- package/server/security/auth/adapters/saml.js +173 -0
- package/server/security/auth/adapters/totp.js +703 -0
- package/server/security/auth/adapters/webauthn.js +625 -0
- package/server/security/auth/files.md +61 -0
- package/server/security/auth/framework/constants.js +27 -0
- package/server/security/auth/framework/files.md +23 -0
- package/server/security/auth/framework/handlers.js +272 -0
- package/server/security/auth/framework/socket-auth.js +177 -0
- package/server/security/auth/handlers/auth-messages.js +143 -0
- package/server/security/auth/handlers/files.md +28 -0
- package/server/security/auth/index.js +290 -0
- package/server/security/auth/mfa/crypto/aead.js +148 -0
- package/server/security/auth/mfa/crypto/constants.js +35 -0
- package/server/security/auth/mfa/crypto/files.md +27 -0
- package/server/security/auth/mfa/crypto/kdf.js +120 -0
- package/server/security/auth/mfa/crypto/utils.js +68 -0
- package/server/security/auth/mfa/crypto-utils.js +80 -0
- package/server/security/auth/mfa/files.md +77 -0
- package/server/security/auth/mfa/ledger/constants.js +75 -0
- package/server/security/auth/mfa/ledger/errors.js +73 -0
- package/server/security/auth/mfa/ledger/files.md +23 -0
- package/server/security/auth/mfa/ledger/share-record.js +32 -0
- package/server/security/auth/mfa/ledger.js +255 -0
- package/server/security/auth/mfa/recovery/constants.js +67 -0
- package/server/security/auth/mfa/recovery/files.md +19 -0
- package/server/security/auth/mfa/recovery/handlers.js +216 -0
- package/server/security/auth/mfa/recovery.js +191 -0
- package/server/security/auth/mfa/sss/constants.js +21 -0
- package/server/security/auth/mfa/sss/files.md +23 -0
- package/server/security/auth/mfa/sss/gf256.js +103 -0
- package/server/security/auth/mfa/sss/serialization.js +82 -0
- package/server/security/auth/mfa/sss.js +161 -0
- package/server/security/auth/mfa/two-of-three/constants.js +58 -0
- package/server/security/auth/mfa/two-of-three/files.md +23 -0
- package/server/security/auth/mfa/two-of-three/handlers.js +241 -0
- package/server/security/auth/mfa/two-of-three/helpers.js +71 -0
- package/server/security/auth/mfa/two-of-three.js +136 -0
- package/server/security/auth/nonce-manager.js +89 -0
- package/server/security/auth/state-machine-mfa.js +269 -0
- package/server/security/auth/state-machine.js +257 -0
- package/server/security/extractRootDomain.js +144 -16
- package/server/security/files.md +51 -0
- package/server/security/origin.js +197 -15
- package/server/security/reply.js +274 -16
- package/server/socket/README.md +119 -0
- package/server/socket/authMiddleware.js +299 -0
- package/server/socket/files.md +86 -0
- package/server/socket/open.js +154 -8
- package/server/socket/pluginHooks.js +334 -0
- package/server/socket/receive.js +184 -224
- package/server/socket/receiveContext.js +117 -0
- package/server/socket/send.js +416 -78
- package/server/socket/tagUtils.js +402 -0
- package/server/utils/README.md +19 -0
- package/server/utils/deepRequire.js +255 -30
- package/server/utils/files.md +57 -0
- package/server/utils/genId.js +182 -20
- package/server/utils/parseUserAgent.js +313 -251
- package/server/utils/userAgent/README.md +65 -0
- package/server/utils/userAgent/files.md +46 -0
- package/server/utils/userAgent/patterns.js +545 -0
- package/utils/README.md +21 -0
- package/utils/files.md +66 -0
- package/utils/jss/README.md +21 -0
- package/utils/jss/decode.js +471 -0
- package/utils/jss/encode.js +312 -0
- package/utils/jss/files.md +68 -0
- package/utils/jss/plugins.js +210 -0
- package/utils/jss.js +219 -273
- package/utils/messageHash.js +238 -35
- package/dist/api-ape.min.js +0 -2
- package/dist/api-ape.min.js.map +0 -7
- package/server/client.js +0 -311
- package/server/lib/broadcast.js +0 -146
package/client/connectSocket.js
CHANGED
|
@@ -1,1089 +1,519 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Core client socket connection module for api-ape
|
|
3
|
+
*
|
|
4
|
+
* This module manages WebSocket connections with automatic fallback to HTTP streaming
|
|
5
|
+
* when WebSocket connections fail or are blocked (e.g., by corporate firewalls).
|
|
6
|
+
*
|
|
7
|
+
* ## Connection Flow
|
|
8
|
+
* 1. Attempts WebSocket connection first (preferred for low latency)
|
|
9
|
+
* 2. Falls back to HTTP streaming if WebSocket fails within 4 seconds
|
|
10
|
+
* 3. Periodically retries WebSocket even when using HTTP streaming
|
|
11
|
+
* 4. Handles reconnection automatically when connections drop
|
|
12
|
+
*
|
|
13
|
+
* ## Transport Modes
|
|
14
|
+
* - `websocket` - Real-time bidirectional WebSocket connection
|
|
15
|
+
* - `polling` - HTTP streaming fallback (GET for receiving, POST for sending)
|
|
16
|
+
* - `auto` - Automatically selects best transport (default)
|
|
17
|
+
*
|
|
18
|
+
* ## Binary Data Support
|
|
19
|
+
* The module transparently handles binary data (ArrayBuffer, Blob) by:
|
|
20
|
+
* - Converting binary payloads to HTTP uploads
|
|
21
|
+
* - Hydrating responses with linked binary resources
|
|
22
|
+
* - Supporting client-to-client file sharing
|
|
23
|
+
*
|
|
24
|
+
* @module client/connectSocket
|
|
25
|
+
* @see {@link module:client/connection/state} for connection state management
|
|
26
|
+
* @see {@link module:client/transports/streaming} for HTTP fallback transport
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* // Basic usage
|
|
30
|
+
* import connectSocket from './connectSocket.js'
|
|
31
|
+
*
|
|
32
|
+
* const client = connectSocket()
|
|
33
|
+
* connectSocket.autoReconnect()
|
|
34
|
+
*
|
|
35
|
+
* // Send messages
|
|
36
|
+
* client.sender.chat({ message: 'Hello!' })
|
|
37
|
+
* .then(response => console.log(response))
|
|
38
|
+
*
|
|
39
|
+
* // Receive broadcasts
|
|
40
|
+
* client.setOnReceiver('notification', (msg) => {
|
|
41
|
+
* console.log('Received:', msg.data)
|
|
42
|
+
* })
|
|
43
|
+
*
|
|
44
|
+
* // Monitor connection state
|
|
45
|
+
* client.onConnectionChange((state) => {
|
|
46
|
+
* console.log('Connection state:', state)
|
|
47
|
+
* })
|
|
48
|
+
*/
|
|
32
49
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
50
|
+
import jss from "../utils/jss";
|
|
51
|
+
import { createStreamingTransport } from "./transports/streaming";
|
|
52
|
+
import {
|
|
53
|
+
ConnectionState,
|
|
54
|
+
notifyConnectionChange,
|
|
55
|
+
onConnectionChange,
|
|
56
|
+
} from "./connection/state";
|
|
57
|
+
import {
|
|
58
|
+
getSocketUrl,
|
|
59
|
+
checkCaptivePortal,
|
|
60
|
+
scheduleNetworkRetry,
|
|
61
|
+
setupOnlineListeners,
|
|
62
|
+
WS_RETRY_INTERVAL,
|
|
63
|
+
} from "./connection/network";
|
|
64
|
+
import { wrap } from "./connection/proxy";
|
|
65
|
+
import { createWsSend, createSender } from "./connection/sender";
|
|
66
|
+
import { setSendFn, resubscribeAll } from "./connection/subscriptions";
|
|
67
|
+
import {
|
|
68
|
+
processIncomingData,
|
|
69
|
+
dispatchMessage,
|
|
70
|
+
setOnReceiver,
|
|
71
|
+
} from "./connection/messageHandler";
|
|
42
72
|
|
|
43
73
|
/**
|
|
44
|
-
*
|
|
74
|
+
* Configured transport mode
|
|
75
|
+
* @type {'auto'|'websocket'|'polling'}
|
|
76
|
+
* @private
|
|
45
77
|
*/
|
|
46
|
-
|
|
47
|
-
if (typeof window === 'undefined') return false
|
|
48
|
-
return ['localhost', '127.0.0.1', '[::1]'].includes(window.location.hostname)
|
|
49
|
-
}
|
|
78
|
+
let configuredTransport = "auto";
|
|
50
79
|
|
|
51
80
|
/**
|
|
52
|
-
*
|
|
81
|
+
* Currently active transport type
|
|
82
|
+
* @type {'websocket'|'polling'|null}
|
|
83
|
+
* @private
|
|
53
84
|
*/
|
|
54
|
-
|
|
55
|
-
const hostname = window.location.hostname
|
|
56
|
-
const isHttps = window.location.protocol === 'https:'
|
|
57
|
-
const port = window.location.port || (isHttps ? 443 : 80)
|
|
58
|
-
const protocol = isHttps ? 'https' : 'http'
|
|
59
|
-
const portSuffix = (port !== 80 && port !== 443) ? `:${port}` : ''
|
|
60
|
-
return `${protocol}://${hostname}${portSuffix}/api/ape/ping`
|
|
61
|
-
}
|
|
85
|
+
let currentTransport = null;
|
|
62
86
|
|
|
63
87
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
88
|
+
* HTTP streaming transport instance (created lazily)
|
|
89
|
+
* @type {import('./transports/streaming').StreamingTransport|null}
|
|
90
|
+
* @private
|
|
66
91
|
*/
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
const controller = new AbortController()
|
|
70
|
-
const timeoutId = setTimeout(() => controller.abort(), PING_TIMEOUT)
|
|
71
|
-
|
|
72
|
-
const response = await fetch(getPingUrl(), {
|
|
73
|
-
cache: 'no-store',
|
|
74
|
-
signal: controller.signal
|
|
75
|
-
})
|
|
76
|
-
clearTimeout(timeoutId)
|
|
77
|
-
|
|
78
|
-
if (!response.ok) {
|
|
79
|
-
if (isDevMode()) {
|
|
80
|
-
console.error('🦍 [DEV] Ping failed: HTTP', response.status)
|
|
81
|
-
}
|
|
82
|
-
return 'walled'
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const data = await response.json()
|
|
86
|
-
|
|
87
|
-
// Verify response is genuine (not a captive portal redirect page)
|
|
88
|
-
if (data?.ok !== true) {
|
|
89
|
-
if (isDevMode()) {
|
|
90
|
-
console.error('🦍 [DEV] Ping failed: invalid response', data)
|
|
91
|
-
}
|
|
92
|
-
return 'walled'
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Validate timestamp to detect proxy replay attacks
|
|
96
|
-
if (typeof data.ts === 'number') {
|
|
97
|
-
const now = Date.now()
|
|
98
|
-
const skew = Math.abs(now - data.ts)
|
|
99
|
-
if (skew > MAX_PING_CLOCK_SKEW) {
|
|
100
|
-
if (isDevMode()) {
|
|
101
|
-
console.error('🦍 [DEV] Ping failed: timestamp too old/stale (skew:', skew, 'ms)')
|
|
102
|
-
}
|
|
103
|
-
return 'walled'
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return 'ok'
|
|
108
|
-
} catch (err) {
|
|
109
|
-
if (isDevMode()) {
|
|
110
|
-
console.error('🦍 [DEV] Ping failed:', err.message || err)
|
|
111
|
-
}
|
|
112
|
-
return 'walled'
|
|
113
|
-
}
|
|
114
|
-
}
|
|
92
|
+
let streamingTransport = null;
|
|
115
93
|
|
|
116
94
|
/**
|
|
117
|
-
*
|
|
95
|
+
* Timer for periodic WebSocket retry attempts
|
|
96
|
+
* @type {number|null}
|
|
97
|
+
* @private
|
|
118
98
|
*/
|
|
119
|
-
|
|
120
|
-
if (typeof window === 'undefined') return
|
|
121
|
-
|
|
122
|
-
window.addEventListener('online', () => {
|
|
123
|
-
console.log('🦍 Browser went online, checking network...')
|
|
124
|
-
// Trigger reconnection attempt
|
|
125
|
-
attemptConnection()
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
window.addEventListener('offline', () => {
|
|
129
|
-
console.log('🦍 Browser went offline')
|
|
130
|
-
notifyConnectionChange(ConnectionState.Offline)
|
|
131
|
-
})
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Setup listeners on module load (browser only)
|
|
135
|
-
if (typeof window !== 'undefined') {
|
|
136
|
-
setupOnlineListeners()
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
|
|
99
|
+
let wsRetryTimer = null;
|
|
140
100
|
|
|
141
101
|
/**
|
|
142
|
-
*
|
|
102
|
+
* Timeout before falling back to HTTP streaming (ms)
|
|
103
|
+
* @constant {number}
|
|
143
104
|
*/
|
|
144
|
-
|
|
145
|
-
const hostname = window.location.hostname
|
|
146
|
-
const localServers = ["localhost", "127.0.0.1", "[::1]"]
|
|
147
|
-
const isLocal = localServers.includes(hostname)
|
|
148
|
-
const isHttps = window.location.protocol === "https:"
|
|
149
|
-
|
|
150
|
-
// Use window.location.port if available, otherwise fallback (9010 for local dev, 443/80 for prod)
|
|
151
|
-
const port = window.location.port || (isLocal ? 9010 : (isHttps ? 443 : 80))
|
|
152
|
-
|
|
153
|
-
// Build URL - keep /api/ape path
|
|
154
|
-
const protocol = isHttps ? "wss" : "ws"
|
|
155
|
-
const portSuffix = (port !== 80 && port !== 443) ? `:${port}` : ""
|
|
156
|
-
|
|
157
|
-
return `${protocol}://${hostname}${portSuffix}/api/ape`
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
let reconnect = false
|
|
161
|
-
const connectTimeout = 5000
|
|
162
|
-
const totalRequestTimeout = 10000
|
|
163
|
-
//const location = window.location
|
|
164
|
-
|
|
165
|
-
const joinKey = "/"
|
|
166
|
-
// Properties accessed directly on `ape` that should NOT be intercepted
|
|
167
|
-
const reservedKeys = new Set(['on', 'onConnectionChange', 'transport'])
|
|
168
|
-
const handler = {
|
|
169
|
-
get(fn, key) {
|
|
170
|
-
// Skip proxy interception for reserved keys - return actual property
|
|
171
|
-
if (reservedKeys.has(key)) {
|
|
172
|
-
return fn[key]
|
|
173
|
-
}
|
|
174
|
-
const wrapperFn = function (a, b) {
|
|
175
|
-
let path = joinKey + key, body;
|
|
176
|
-
if (2 === arguments.length) {
|
|
177
|
-
path += a
|
|
178
|
-
body = b
|
|
179
|
-
} else {
|
|
180
|
-
body = a
|
|
181
|
-
}
|
|
182
|
-
return fn(path, body)
|
|
183
|
-
}
|
|
184
|
-
return new Proxy(wrapperFn, handler)
|
|
185
|
-
} // END get
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function wrap(api) {
|
|
189
|
-
return new Proxy(api, handler)
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
let __socket = false, ready = false, wsSend = false;
|
|
193
|
-
const waitingOn = {};
|
|
194
|
-
|
|
195
|
-
let aWaitingSend = []
|
|
196
|
-
const receiverArray = [];
|
|
197
|
-
const ofTypesOb = {};
|
|
105
|
+
const WS_FALLBACK_TIMEOUT = 4000;
|
|
198
106
|
|
|
199
107
|
/**
|
|
200
|
-
*
|
|
108
|
+
* Current WebSocket instance, or false if not connected
|
|
109
|
+
* @type {WebSocket|false}
|
|
110
|
+
* @private
|
|
201
111
|
*/
|
|
202
|
-
|
|
203
|
-
console.log('🦍 Switching to HTTP streaming transport')
|
|
204
|
-
currentTransport = 'polling'
|
|
205
|
-
|
|
206
|
-
if (!streamingTransport) {
|
|
207
|
-
streamingTransport = createStreamingTransport()
|
|
208
|
-
|
|
209
|
-
// Handle incoming messages from streaming transport
|
|
210
|
-
streamingTransport.onMessage = async (msg) => {
|
|
211
|
-
const { err, type, data } = msg
|
|
212
|
-
|
|
213
|
-
// Process linked resources and shared files
|
|
214
|
-
let processedData = data
|
|
215
|
-
if (data && !err) {
|
|
216
|
-
try {
|
|
217
|
-
processedData = await fetchLinkedResources(data)
|
|
218
|
-
processedData = await fetchSharedFiles(processedData)
|
|
219
|
-
} catch (fetchErr) {
|
|
220
|
-
console.error(`🦍 Failed to hydrate streaming data:`, fetchErr)
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Dispatch to type-specific handlers
|
|
225
|
-
if (ofTypesOb[type]) {
|
|
226
|
-
ofTypesOb[type].forEach(worker => worker({ err, type, data: processedData }))
|
|
227
|
-
}
|
|
228
|
-
// Dispatch to general handlers
|
|
229
|
-
receiverArray.forEach(worker => worker({ err, type, data: processedData }))
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
streamingTransport.onOpen = () => {
|
|
233
|
-
ready = true
|
|
234
|
-
notifyConnectionChange(ConnectionState.Connected)
|
|
235
|
-
console.log('🦍 HTTP streaming connected')
|
|
236
|
-
|
|
237
|
-
// Flush waiting messages
|
|
238
|
-
aWaitingSend.forEach(({ type, data, resolve, reject, waiting, createdAt, timer }) => {
|
|
239
|
-
clearTimeout(timer)
|
|
240
|
-
const resultPromise = streamingSend(type, data, createdAt)
|
|
241
|
-
if (waiting) {
|
|
242
|
-
resultPromise.then(resolve).catch(reject)
|
|
243
|
-
}
|
|
244
|
-
})
|
|
245
|
-
aWaitingSend = []
|
|
246
|
-
|
|
247
|
-
// Start background WebSocket retry
|
|
248
|
-
startWsRetry()
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
streamingTransport.onClose = () => {
|
|
252
|
-
ready = false
|
|
253
|
-
notifyConnectionChange(ConnectionState.Disconnected)
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
streamingTransport.onError = (err) => {
|
|
257
|
-
console.error('🦍 Streaming error:', err)
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
streamingTransport.connect()
|
|
262
|
-
}
|
|
112
|
+
let __socket = false;
|
|
263
113
|
|
|
264
114
|
/**
|
|
265
|
-
*
|
|
115
|
+
* Whether the connection is ready to send/receive messages
|
|
116
|
+
* @type {boolean}
|
|
117
|
+
* @private
|
|
266
118
|
*/
|
|
267
|
-
|
|
268
|
-
return streamingTransport.send(type, data, createdAt)
|
|
269
|
-
}
|
|
119
|
+
let ready = false;
|
|
270
120
|
|
|
271
121
|
/**
|
|
272
|
-
*
|
|
122
|
+
* Map of pending query IDs to their response callbacks
|
|
123
|
+
* Used to match responses to their original requests
|
|
124
|
+
* @type {Object.<string, function(Error|null, any): void>}
|
|
125
|
+
* @private
|
|
273
126
|
*/
|
|
274
|
-
|
|
275
|
-
if (wsRetryTimer) return
|
|
276
|
-
if (currentTransport !== 'polling') return
|
|
277
|
-
if (configuredTransport === 'polling') return // User explicitly wants polling only
|
|
278
|
-
|
|
279
|
-
wsRetryTimer = setInterval(() => {
|
|
280
|
-
if (currentTransport !== 'polling') {
|
|
281
|
-
clearInterval(wsRetryTimer)
|
|
282
|
-
wsRetryTimer = null
|
|
283
|
-
return
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
console.log('🦍 Attempting WebSocket reconnection...')
|
|
287
|
-
tryWebSocket(true)
|
|
288
|
-
}, WS_RETRY_INTERVAL)
|
|
289
|
-
}
|
|
127
|
+
const waitingOn = {};
|
|
290
128
|
|
|
291
129
|
/**
|
|
292
|
-
*
|
|
293
|
-
* @
|
|
130
|
+
* Queue of messages waiting to be sent when connection becomes ready
|
|
131
|
+
* @type {Array<{type: string, data: any, resolve: function, reject: function, waiting: boolean, createdAt: number, timer: number}>}
|
|
132
|
+
* @private
|
|
294
133
|
*/
|
|
295
|
-
|
|
296
|
-
const ws = new WebSocket(getSocketUrl())
|
|
297
|
-
let fallbackTimer = null
|
|
298
|
-
|
|
299
|
-
// Set fallback timeout (only for initial connection, not retries)
|
|
300
|
-
if (!isRetry && configuredTransport === 'auto') {
|
|
301
|
-
fallbackTimer = setTimeout(() => {
|
|
302
|
-
if (ws.readyState !== WebSocket.OPEN) {
|
|
303
|
-
console.log('🦍 WebSocket timeout, falling back to HTTP streaming')
|
|
304
|
-
ws.close()
|
|
305
|
-
switchToStreaming()
|
|
306
|
-
}
|
|
307
|
-
}, WS_FALLBACK_TIMEOUT)
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
ws.onopen = () => {
|
|
311
|
-
if (fallbackTimer) clearTimeout(fallbackTimer)
|
|
312
|
-
|
|
313
|
-
// If this is a retry and we're in polling mode, switch back to WebSocket
|
|
314
|
-
if (isRetry && currentTransport === 'polling') {
|
|
315
|
-
console.log('🦍 WebSocket reconnected, switching from HTTP streaming')
|
|
316
|
-
if (streamingTransport) {
|
|
317
|
-
streamingTransport.close()
|
|
318
|
-
}
|
|
319
|
-
if (wsRetryTimer) {
|
|
320
|
-
clearInterval(wsRetryTimer)
|
|
321
|
-
wsRetryTimer = null
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
currentTransport = 'websocket'
|
|
326
|
-
__socket = ws
|
|
327
|
-
ready = true
|
|
328
|
-
notifyConnectionChange(ConnectionState.Connected)
|
|
329
|
-
|
|
330
|
-
aWaitingSend.forEach(({ type, data, resolve, reject, waiting, createdAt, timer }) => {
|
|
331
|
-
clearTimeout(timer)
|
|
332
|
-
const resultPromise = wsSend(type, data, createdAt)
|
|
333
|
-
if (waiting) {
|
|
334
|
-
resultPromise.then(resolve).catch(reject)
|
|
335
|
-
}
|
|
336
|
-
})
|
|
337
|
-
aWaitingSend = []
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
ws.onmessage = async function (event) {
|
|
341
|
-
const { err, type, queryId, data } = jss.parse(event.data)
|
|
342
|
-
|
|
343
|
-
// Messages with queryId must fulfill matching promise
|
|
344
|
-
if (queryId) {
|
|
345
|
-
if (waitingOn[queryId]) {
|
|
346
|
-
// Check for linked resources and fetch them before resolving
|
|
347
|
-
if (data && !err) {
|
|
348
|
-
try {
|
|
349
|
-
let hydratedData = await fetchLinkedResources(data)
|
|
350
|
-
hydratedData = await fetchSharedFiles(hydratedData)
|
|
351
|
-
waitingOn[queryId](err, hydratedData)
|
|
352
|
-
} catch (fetchErr) {
|
|
353
|
-
waitingOn[queryId](fetchErr, null)
|
|
354
|
-
}
|
|
355
|
-
} else {
|
|
356
|
-
waitingOn[queryId](err, data)
|
|
357
|
-
}
|
|
358
|
-
delete waitingOn[queryId]
|
|
359
|
-
} else {
|
|
360
|
-
console.error(`🦍 No matching queryId: ${queryId}`)
|
|
361
|
-
}
|
|
362
|
-
return
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// Only messages WITHOUT queryId go to setOnReceiver
|
|
366
|
-
let processedData = data
|
|
367
|
-
if (data && !err) {
|
|
368
|
-
try {
|
|
369
|
-
processedData = await fetchLinkedResources(data)
|
|
370
|
-
processedData = await fetchSharedFiles(processedData)
|
|
371
|
-
} catch (fetchErr) {
|
|
372
|
-
console.error(`🦍 Failed to hydrate broadcast data:`, fetchErr)
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
if (ofTypesOb[type]) {
|
|
377
|
-
ofTypesOb[type].forEach(worker => worker({ err, type, data: processedData }))
|
|
378
|
-
}
|
|
379
|
-
receiverArray.forEach(worker => worker({ err, type, data: processedData }))
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
ws.onerror = function (err) {
|
|
383
|
-
if (fallbackTimer) clearTimeout(fallbackTimer)
|
|
384
|
-
console.error('socket ERROR:', err)
|
|
385
|
-
|
|
386
|
-
// On initial connection error in auto mode, fallback to streaming
|
|
387
|
-
if (!isRetry && configuredTransport === 'auto' && !ready) {
|
|
388
|
-
switchToStreaming()
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
ws.onclose = function (event) {
|
|
393
|
-
if (fallbackTimer) clearTimeout(fallbackTimer)
|
|
394
|
-
console.warn('socket disconnect:', event)
|
|
395
|
-
__socket = false
|
|
396
|
-
ready = false
|
|
397
|
-
|
|
398
|
-
// Only notify disconnected if we're on websocket transport
|
|
399
|
-
if (currentTransport === 'websocket') {
|
|
400
|
-
notifyConnectionChange(ConnectionState.Disconnected)
|
|
401
|
-
setTimeout(() => reconnect && connectSocket(), 500)
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
}
|
|
134
|
+
let aWaitingSend = [];
|
|
405
135
|
|
|
406
136
|
/**
|
|
407
|
-
*
|
|
408
|
-
*
|
|
137
|
+
* Whether auto-reconnect is enabled
|
|
138
|
+
* @type {boolean}
|
|
139
|
+
* @private
|
|
409
140
|
*/
|
|
410
|
-
|
|
411
|
-
const resources = []
|
|
412
|
-
|
|
413
|
-
if (obj === null || obj === undefined || typeof obj !== 'object') {
|
|
414
|
-
return resources
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
if (Array.isArray(obj)) {
|
|
418
|
-
for (let i = 0; i < obj.length; i++) {
|
|
419
|
-
resources.push(...findLinkedResources(obj[i], path ? `${path}.${i}` : String(i)))
|
|
420
|
-
}
|
|
421
|
-
return resources
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
for (const key of Object.keys(obj)) {
|
|
425
|
-
// Check for L-tag in key (from JJS encoding: key<!L>)
|
|
426
|
-
if (key.endsWith('<!L>')) {
|
|
427
|
-
const cleanKey = key.slice(0, -4)
|
|
428
|
-
const hash = obj[key]
|
|
429
|
-
resources.push({
|
|
430
|
-
path: path ? `${path}.${cleanKey}` : cleanKey,
|
|
431
|
-
hash,
|
|
432
|
-
originalKey: key
|
|
433
|
-
})
|
|
434
|
-
} else {
|
|
435
|
-
resources.push(...findLinkedResources(obj[key], path ? `${path}.${key}` : key))
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
return resources
|
|
440
|
-
}
|
|
141
|
+
let reconnect = false;
|
|
441
142
|
|
|
442
143
|
/**
|
|
443
|
-
*
|
|
444
|
-
*
|
|
144
|
+
* WebSocket send function bound to current socket
|
|
145
|
+
* @type {function(string, any, number, boolean=): Promise<any>}
|
|
146
|
+
* @private
|
|
445
147
|
*/
|
|
446
|
-
|
|
447
|
-
const files = []
|
|
448
|
-
|
|
449
|
-
if (obj === null || obj === undefined || typeof obj !== 'object') {
|
|
450
|
-
return files
|
|
451
|
-
}
|
|
148
|
+
const wsSend = createWsSend(() => __socket, waitingOn);
|
|
452
149
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
}
|
|
457
|
-
return files
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
for (const key of Object.keys(obj)) {
|
|
461
|
-
// Check for F-tag in key (client-to-client shared file marker)
|
|
462
|
-
if (key.endsWith('<!F>')) {
|
|
463
|
-
const cleanKey = key.slice(0, -4)
|
|
464
|
-
const hash = obj[key]
|
|
465
|
-
files.push({
|
|
466
|
-
path: path ? `${path}.${cleanKey}` : cleanKey,
|
|
467
|
-
hash,
|
|
468
|
-
originalKey: key
|
|
469
|
-
})
|
|
470
|
-
} else {
|
|
471
|
-
files.push(...findFileTags(obj[key], path ? `${path}.${key}` : key))
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
return files
|
|
150
|
+
// Setup browser online/offline listeners on module load
|
|
151
|
+
if (typeof window !== "undefined") {
|
|
152
|
+
setupOnlineListeners(attemptConnection);
|
|
476
153
|
}
|
|
477
154
|
|
|
478
155
|
/**
|
|
479
|
-
*
|
|
156
|
+
* Flush all queued messages through the provided send function
|
|
157
|
+
*
|
|
158
|
+
* Called when connection becomes ready to send pending messages
|
|
159
|
+
* that were queued while disconnected.
|
|
160
|
+
*
|
|
161
|
+
* @param {function(string, any, number): Promise<any>} sendFn - Send function to use
|
|
162
|
+
* @private
|
|
480
163
|
*/
|
|
481
|
-
function
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
const cleaned = {}
|
|
491
|
-
for (const key of Object.keys(obj)) {
|
|
492
|
-
if (key.endsWith('<!F>')) {
|
|
493
|
-
const cleanKey = key.slice(0, -4)
|
|
494
|
-
cleaned[cleanKey] = obj[key]
|
|
495
|
-
} else {
|
|
496
|
-
cleaned[key] = cleanFileTags(obj[key])
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
return cleaned
|
|
164
|
+
function flushWaitingMessages(sendFn) {
|
|
165
|
+
aWaitingSend.forEach(
|
|
166
|
+
({ type, data, resolve, reject, waiting, createdAt, timer }) => {
|
|
167
|
+
clearTimeout(timer);
|
|
168
|
+
const result = sendFn(type, data, createdAt);
|
|
169
|
+
if (waiting) result.then(resolve).catch(reject);
|
|
170
|
+
},
|
|
171
|
+
);
|
|
172
|
+
aWaitingSend = [];
|
|
500
173
|
}
|
|
501
174
|
|
|
502
175
|
/**
|
|
503
|
-
*
|
|
504
|
-
*
|
|
176
|
+
* Switch from WebSocket to HTTP streaming transport
|
|
177
|
+
*
|
|
178
|
+
* Creates the streaming transport if needed and sets up event handlers.
|
|
179
|
+
* This is called when WebSocket connection fails or times out.
|
|
180
|
+
*
|
|
181
|
+
* @private
|
|
505
182
|
*/
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
if (files.length === 0) {
|
|
510
|
-
return data
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
console.log(`🦍 Fetching ${files.length} shared file(s)`)
|
|
514
|
-
|
|
515
|
-
const cleanedData = cleanFileTags(data)
|
|
516
|
-
|
|
517
|
-
const hostname = window.location.hostname
|
|
518
|
-
const isLocal = ["localhost", "127.0.0.1", "[::1]"].includes(hostname)
|
|
519
|
-
const isHttps = window.location.protocol === "https:"
|
|
520
|
-
const port = window.location.port || (isLocal ? 9010 : (isHttps ? 443 : 80))
|
|
521
|
-
const protocol = isHttps ? "https" : "http"
|
|
522
|
-
const portSuffix = (port !== 80 && port !== 443) ? `:${port}` : ""
|
|
523
|
-
const baseUrl = `${protocol}://${hostname}${portSuffix}`
|
|
524
|
-
|
|
525
|
-
await Promise.all(files.map(async ({ path, hash }) => {
|
|
526
|
-
let retries = 0
|
|
527
|
-
let backoff = 100 // Start with 100ms
|
|
183
|
+
function switchToStreaming() {
|
|
184
|
+
console.log("🦍 Switching to HTTP streaming transport");
|
|
185
|
+
currentTransport = "polling";
|
|
528
186
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
const response = await fetch(`${baseUrl}/api/ape/data/${hash}`, {
|
|
532
|
-
credentials: 'include'
|
|
533
|
-
})
|
|
187
|
+
if (!streamingTransport) {
|
|
188
|
+
streamingTransport = createStreamingTransport();
|
|
534
189
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
throw new Error(`Failed to fetch shared file: ${response.status}`)
|
|
544
|
-
}
|
|
190
|
+
/**
|
|
191
|
+
* Handle incoming messages from streaming transport
|
|
192
|
+
* @param {{type: string, data: any, err: any}} msg - Parsed message
|
|
193
|
+
*/
|
|
194
|
+
streamingTransport.onMessage = async (msg) => {
|
|
195
|
+
const data = await processIncomingData(msg.data, msg.err);
|
|
196
|
+
dispatchMessage(msg.type, msg.err, data);
|
|
197
|
+
};
|
|
545
198
|
|
|
546
|
-
|
|
547
|
-
|
|
199
|
+
/**
|
|
200
|
+
* Handle streaming connection established
|
|
201
|
+
*/
|
|
202
|
+
streamingTransport.onOpen = () => {
|
|
203
|
+
ready = true;
|
|
548
204
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
console.log(`🦍 Shared file ${hash} still uploading (${response.headers.get('X-Ape-Total-Received') || '?'} bytes)`)
|
|
553
|
-
}
|
|
554
|
-
break
|
|
555
|
-
} catch (err) {
|
|
556
|
-
if (retries >= maxRetries - 1) {
|
|
557
|
-
console.error(`🦍 Failed to fetch shared file at ${path}:`, err)
|
|
558
|
-
setValueAtPath(cleanedData, path, null)
|
|
559
|
-
}
|
|
560
|
-
retries++
|
|
561
|
-
await new Promise(r => setTimeout(r, backoff))
|
|
562
|
-
backoff *= 2
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
}))
|
|
205
|
+
// Set up subscription send function for streaming and re-subscribe
|
|
206
|
+
setSendFn((msg) => streamingTransport.sendRaw(msg));
|
|
207
|
+
resubscribeAll();
|
|
566
208
|
|
|
567
|
-
|
|
568
|
-
|
|
209
|
+
notifyConnectionChange(ConnectionState.Connected);
|
|
210
|
+
flushWaitingMessages((t, d, c) => streamingTransport.send(t, d, c));
|
|
211
|
+
startWsRetry();
|
|
212
|
+
};
|
|
569
213
|
|
|
570
|
-
/**
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
214
|
+
/**
|
|
215
|
+
* Handle streaming connection closed
|
|
216
|
+
*/
|
|
217
|
+
streamingTransport.onClose = () => {
|
|
218
|
+
ready = false;
|
|
219
|
+
notifyConnectionChange(ConnectionState.Disconnected);
|
|
220
|
+
};
|
|
576
221
|
|
|
577
|
-
|
|
578
|
-
|
|
222
|
+
/**
|
|
223
|
+
* Handle streaming transport errors
|
|
224
|
+
* @param {Error} err - The error that occurred
|
|
225
|
+
*/
|
|
226
|
+
streamingTransport.onError = (err) =>
|
|
227
|
+
console.error("🦍 Streaming error:", err);
|
|
579
228
|
}
|
|
580
229
|
|
|
581
|
-
|
|
230
|
+
streamingTransport.connect();
|
|
582
231
|
}
|
|
583
232
|
|
|
584
233
|
/**
|
|
585
|
-
*
|
|
234
|
+
* Start periodic WebSocket retry attempts
|
|
235
|
+
*
|
|
236
|
+
* When using HTTP streaming, periodically attempts to upgrade to WebSocket.
|
|
237
|
+
* This allows the connection to upgrade when network conditions improve.
|
|
238
|
+
*
|
|
239
|
+
* @private
|
|
586
240
|
*/
|
|
587
|
-
function
|
|
588
|
-
if (
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
return
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
const cleanKey = key.slice(0, -4)
|
|
600
|
-
cleaned[cleanKey] = obj[key]
|
|
601
|
-
} else {
|
|
602
|
-
cleaned[key] = cleanLinkedKeys(obj[key])
|
|
241
|
+
function startWsRetry() {
|
|
242
|
+
if (
|
|
243
|
+
wsRetryTimer ||
|
|
244
|
+
currentTransport !== "polling" ||
|
|
245
|
+
configuredTransport === "polling"
|
|
246
|
+
)
|
|
247
|
+
return;
|
|
248
|
+
wsRetryTimer = setInterval(() => {
|
|
249
|
+
if (currentTransport !== "polling") {
|
|
250
|
+
clearInterval(wsRetryTimer);
|
|
251
|
+
wsRetryTimer = null;
|
|
252
|
+
return;
|
|
603
253
|
}
|
|
604
|
-
|
|
605
|
-
|
|
254
|
+
tryWebSocket(true);
|
|
255
|
+
}, WS_RETRY_INTERVAL);
|
|
606
256
|
}
|
|
607
257
|
|
|
608
258
|
/**
|
|
609
|
-
*
|
|
259
|
+
* Attempt to establish a WebSocket connection
|
|
260
|
+
*
|
|
261
|
+
* @param {boolean} [isRetry=false] - Whether this is a retry attempt from HTTP streaming mode
|
|
262
|
+
* @private
|
|
263
|
+
*
|
|
264
|
+
* @description
|
|
265
|
+
* Connection flow:
|
|
266
|
+
* 1. Creates WebSocket to server's /api/ape endpoint
|
|
267
|
+
* 2. Sets up fallback timer (only on initial connection with auto transport)
|
|
268
|
+
* 3. On success: marks ready, flushes queued messages
|
|
269
|
+
* 4. On failure: falls back to HTTP streaming (if auto mode)
|
|
270
|
+
* 5. On close: schedules reconnection if auto-reconnect enabled
|
|
610
271
|
*/
|
|
611
|
-
|
|
612
|
-
const
|
|
272
|
+
function tryWebSocket(isRetry = false) {
|
|
273
|
+
const ws = new WebSocket(getSocketUrl());
|
|
274
|
+
let fallbackTimer = null;
|
|
613
275
|
|
|
614
|
-
if
|
|
615
|
-
|
|
276
|
+
// Set up fallback to HTTP streaming if WebSocket doesn't connect in time
|
|
277
|
+
if (!isRetry && configuredTransport === "auto") {
|
|
278
|
+
fallbackTimer = setTimeout(() => {
|
|
279
|
+
if (ws.readyState !== WebSocket.OPEN) {
|
|
280
|
+
ws.close();
|
|
281
|
+
switchToStreaming();
|
|
282
|
+
}
|
|
283
|
+
}, WS_FALLBACK_TIMEOUT);
|
|
616
284
|
}
|
|
617
285
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
const isLocal = ["localhost", "127.0.0.1", "[::1]"].includes(hostname)
|
|
624
|
-
const isHttps = window.location.protocol === "https:"
|
|
625
|
-
const port = window.location.port || (isLocal ? 9010 : (isHttps ? 443 : 80))
|
|
626
|
-
const protocol = isHttps ? "https" : "http"
|
|
627
|
-
const portSuffix = (port !== 80 && port !== 443) ? `:${port}` : ""
|
|
628
|
-
const baseUrl = `${protocol}://${hostname}${portSuffix}`
|
|
629
|
-
|
|
630
|
-
await Promise.all(resources.map(async ({ path, hash }) => {
|
|
631
|
-
try {
|
|
632
|
-
const response = await fetch(`${baseUrl}/api/ape/data/${hash}`, {
|
|
633
|
-
credentials: 'include',
|
|
634
|
-
headers: {
|
|
635
|
-
'X-Ape-Client-Id': clientId || ''
|
|
636
|
-
}
|
|
637
|
-
})
|
|
286
|
+
/**
|
|
287
|
+
* Handle WebSocket connection opened
|
|
288
|
+
*/
|
|
289
|
+
ws.onopen = () => {
|
|
290
|
+
if (fallbackTimer) clearTimeout(fallbackTimer);
|
|
638
291
|
|
|
639
|
-
|
|
640
|
-
|
|
292
|
+
// If retrying from polling mode, close the streaming transport
|
|
293
|
+
if (isRetry && currentTransport === "polling") {
|
|
294
|
+
if (streamingTransport) streamingTransport.close();
|
|
295
|
+
if (wsRetryTimer) {
|
|
296
|
+
clearInterval(wsRetryTimer);
|
|
297
|
+
wsRetryTimer = null;
|
|
641
298
|
}
|
|
642
|
-
|
|
643
|
-
const arrayBuffer = await response.arrayBuffer()
|
|
644
|
-
setValueAtPath(cleanedData, path, arrayBuffer)
|
|
645
|
-
} catch (err) {
|
|
646
|
-
console.error(`🦍 Failed to fetch binary resource at ${path}:`, err)
|
|
647
|
-
setValueAtPath(cleanedData, path, null)
|
|
648
299
|
}
|
|
649
|
-
}))
|
|
650
300
|
|
|
651
|
-
|
|
301
|
+
currentTransport = "websocket";
|
|
302
|
+
__socket = ws;
|
|
303
|
+
ready = true;
|
|
304
|
+
|
|
305
|
+
// Set up subscription send function and re-subscribe to all channels
|
|
306
|
+
setSendFn((msg) => ws.send(jss.stringify(msg)));
|
|
307
|
+
resubscribeAll();
|
|
308
|
+
|
|
309
|
+
notifyConnectionChange(ConnectionState.Connected);
|
|
310
|
+
flushWaitingMessages(wsSend);
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Handle incoming WebSocket messages
|
|
315
|
+
* @param {MessageEvent} event - WebSocket message event
|
|
316
|
+
*/
|
|
317
|
+
ws.onmessage = async (event) => {
|
|
318
|
+
const { err, type, queryId, data } = jss.parse(event.data);
|
|
319
|
+
|
|
320
|
+
// Check if this is a response to a pending request
|
|
321
|
+
if (queryId && waitingOn[queryId]) {
|
|
322
|
+
const hydratedData = await processIncomingData(data, err);
|
|
323
|
+
waitingOn[queryId](err, hydratedData);
|
|
324
|
+
delete waitingOn[queryId];
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Otherwise dispatch as a broadcast/push message
|
|
329
|
+
const processed = await processIncomingData(data, err);
|
|
330
|
+
dispatchMessage(type, err, processed);
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Handle WebSocket errors
|
|
335
|
+
* @param {Event} err - Error event
|
|
336
|
+
*/
|
|
337
|
+
ws.onerror = (err) => {
|
|
338
|
+
if (fallbackTimer) clearTimeout(fallbackTimer);
|
|
339
|
+
// Fall back to streaming on initial connection failure
|
|
340
|
+
if (!isRetry && configuredTransport === "auto" && !ready)
|
|
341
|
+
switchToStreaming();
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Handle WebSocket connection closed
|
|
346
|
+
*/
|
|
347
|
+
ws.onclose = () => {
|
|
348
|
+
if (fallbackTimer) clearTimeout(fallbackTimer);
|
|
349
|
+
__socket = false;
|
|
350
|
+
ready = false;
|
|
351
|
+
|
|
352
|
+
// Only handle reconnection if we were using WebSocket transport
|
|
353
|
+
if (currentTransport === "websocket") {
|
|
354
|
+
notifyConnectionChange(ConnectionState.Disconnected);
|
|
355
|
+
setTimeout(() => reconnect && connectSocket(), 500);
|
|
356
|
+
}
|
|
357
|
+
};
|
|
652
358
|
}
|
|
653
359
|
|
|
654
360
|
/**
|
|
655
|
-
* Attempt to establish connection
|
|
361
|
+
* Attempt to establish a connection to the server
|
|
362
|
+
*
|
|
363
|
+
* This function orchestrates the connection process:
|
|
364
|
+
* 1. Checks if browser is online
|
|
365
|
+
* 2. Detects captive portals (hotel/airport WiFi login pages)
|
|
366
|
+
* 3. Initiates appropriate transport based on configuration
|
|
367
|
+
*
|
|
368
|
+
* @async
|
|
369
|
+
* @private
|
|
656
370
|
*/
|
|
657
371
|
async function attemptConnection() {
|
|
658
|
-
// Check
|
|
659
|
-
if (typeof navigator !==
|
|
660
|
-
notifyConnectionChange(ConnectionState.Offline)
|
|
661
|
-
return
|
|
372
|
+
// Check browser online status first
|
|
373
|
+
if (typeof navigator !== "undefined" && !navigator.onLine) {
|
|
374
|
+
notifyConnectionChange(ConnectionState.Offline);
|
|
375
|
+
return;
|
|
662
376
|
}
|
|
663
377
|
|
|
664
|
-
|
|
665
|
-
notifyConnectionChange(ConnectionState.Connecting)
|
|
666
|
-
const pingResult = await checkCaptivePortal()
|
|
378
|
+
notifyConnectionChange(ConnectionState.Connecting);
|
|
667
379
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
scheduleNetworkRetry()
|
|
672
|
-
return
|
|
380
|
+
// Check for captive portal
|
|
381
|
+
if ((await checkCaptivePortal()) === "walled") {
|
|
382
|
+
notifyConnectionChange(ConnectionState.Walled);
|
|
383
|
+
scheduleNetworkRetry(attemptConnection);
|
|
384
|
+
return;
|
|
673
385
|
}
|
|
674
386
|
|
|
675
|
-
//
|
|
676
|
-
|
|
387
|
+
// Start appropriate transport
|
|
388
|
+
configuredTransport === "polling" ? switchToStreaming() : tryWebSocket(false);
|
|
677
389
|
}
|
|
678
390
|
|
|
679
391
|
/**
|
|
680
|
-
*
|
|
392
|
+
* Create the sender function with current connection state
|
|
393
|
+
* @type {function(string, any): Promise<any>}
|
|
394
|
+
* @private
|
|
681
395
|
*/
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
}
|
|
396
|
+
const sender = createSender(
|
|
397
|
+
() => ready,
|
|
398
|
+
() => wsSend,
|
|
399
|
+
aWaitingSend,
|
|
400
|
+
connectSocket,
|
|
401
|
+
);
|
|
689
402
|
|
|
690
403
|
/**
|
|
691
|
-
*
|
|
404
|
+
* Initialize or retrieve the client connection
|
|
405
|
+
*
|
|
406
|
+
* This is the main entry point for establishing connections.
|
|
407
|
+
* Calling it multiple times returns the same client interface.
|
|
408
|
+
*
|
|
409
|
+
* @returns {ClientInterface} Client interface with sender, receivers, and state management
|
|
410
|
+
*
|
|
411
|
+
* @example
|
|
412
|
+
* const client = connectSocket()
|
|
413
|
+
*
|
|
414
|
+
* // Access proxied sender
|
|
415
|
+
* client.sender.myEndpoint({ data: 'value' })
|
|
416
|
+
*
|
|
417
|
+
* // Subscribe to messages
|
|
418
|
+
* client.setOnReceiver('eventType', handler)
|
|
419
|
+
*
|
|
420
|
+
* // Check current transport
|
|
421
|
+
* console.log(client.transport) // 'websocket' or 'polling'
|
|
692
422
|
*/
|
|
693
|
-
function proceedWithConnection() {
|
|
694
|
-
// Determine which transport to use
|
|
695
|
-
if (configuredTransport === 'polling') {
|
|
696
|
-
switchToStreaming()
|
|
697
|
-
} else {
|
|
698
|
-
// 'auto' or 'websocket' - try WebSocket first
|
|
699
|
-
tryWebSocket(false)
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
|
|
703
423
|
function connectSocket() {
|
|
704
|
-
//
|
|
705
|
-
if (__socket && __socket.readyState !== WebSocket.CLOSED)
|
|
706
|
-
return buildClientInterface()
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
return buildClientInterface()
|
|
710
|
-
}
|
|
711
|
-
if (connectionState === ConnectionState.Connecting) {
|
|
712
|
-
return buildClientInterface()
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
// Start connection with network pre-checks
|
|
716
|
-
attemptConnection()
|
|
717
|
-
|
|
718
|
-
return buildClientInterface()
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
/**
|
|
722
|
-
* Check if value is binary data (ArrayBuffer, typed array, or Blob)
|
|
723
|
-
*/
|
|
724
|
-
function isBinaryData(value) {
|
|
725
|
-
if (value === null || value === undefined) return false
|
|
726
|
-
return value instanceof ArrayBuffer ||
|
|
727
|
-
ArrayBuffer.isView(value) ||
|
|
728
|
-
(typeof Blob !== 'undefined' && value instanceof Blob)
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
/**
|
|
732
|
-
* Get binary type tag (A for ArrayBuffer, B for Blob)
|
|
733
|
-
*/
|
|
734
|
-
function getBinaryTag(value) {
|
|
735
|
-
if (typeof Blob !== 'undefined' && value instanceof Blob) return 'B'
|
|
736
|
-
return 'A'
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
/**
|
|
740
|
-
* Generate a simple hash for binary upload
|
|
741
|
-
*/
|
|
742
|
-
function generateUploadHash(path) {
|
|
743
|
-
let hash = 0
|
|
744
|
-
for (let i = 0; i < path.length; i++) {
|
|
745
|
-
const char = path.charCodeAt(i)
|
|
746
|
-
hash = ((hash << 5) - hash) + char
|
|
747
|
-
hash = hash & hash
|
|
748
|
-
}
|
|
749
|
-
return Math.abs(hash).toString(36)
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
/**
|
|
753
|
-
* Find and extract binary data from payload
|
|
754
|
-
* Returns { processedData, uploads: [{ path, hash, data, tag }] }
|
|
755
|
-
*/
|
|
756
|
-
function processBinaryForUpload(data, path = '') {
|
|
757
|
-
if (data === null || data === undefined) {
|
|
758
|
-
return { processedData: data, uploads: [] }
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
if (isBinaryData(data)) {
|
|
762
|
-
const tag = getBinaryTag(data)
|
|
763
|
-
const hash = generateUploadHash(path || 'root')
|
|
764
|
-
return {
|
|
765
|
-
processedData: { [`__ape_upload__`]: hash },
|
|
766
|
-
uploads: [{ path, hash, data, tag }]
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
if (Array.isArray(data)) {
|
|
771
|
-
const processedArray = []
|
|
772
|
-
const allUploads = []
|
|
773
|
-
|
|
774
|
-
for (let i = 0; i < data.length; i++) {
|
|
775
|
-
const itemPath = path ? `${path}.${i}` : String(i)
|
|
776
|
-
const { processedData, uploads } = processBinaryForUpload(data[i], itemPath)
|
|
777
|
-
processedArray.push(processedData)
|
|
778
|
-
allUploads.push(...uploads)
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
return { processedData: processedArray, uploads: allUploads }
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
if (typeof data === 'object') {
|
|
785
|
-
const processedObj = {}
|
|
786
|
-
const allUploads = []
|
|
787
|
-
|
|
788
|
-
for (const key of Object.keys(data)) {
|
|
789
|
-
const itemPath = path ? `${path}.${key}` : key
|
|
790
|
-
const { processedData, uploads } = processBinaryForUpload(data[key], itemPath)
|
|
791
|
-
|
|
792
|
-
// If this was binary data, mark the key with <!B> or <!A> tag
|
|
793
|
-
if (uploads.length > 0 && processedData?.__ape_upload__) {
|
|
794
|
-
const tag = uploads[uploads.length - 1].tag
|
|
795
|
-
processedObj[`${key}<!${tag}>`] = processedData.__ape_upload__
|
|
796
|
-
} else {
|
|
797
|
-
processedObj[key] = processedData
|
|
798
|
-
}
|
|
799
|
-
allUploads.push(...uploads)
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
return { processedData: processedObj, uploads: allUploads }
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
return { processedData: data, uploads: [] }
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
/**
|
|
809
|
-
* Find and extract binary data for SHARING (client-to-client)
|
|
810
|
-
* Uses <!F> tag instead of <!A>/<!B>
|
|
811
|
-
* Returns { processedData, shares: [{ path, hash, data }] }
|
|
812
|
-
*/
|
|
813
|
-
function processBinaryForSharing(data, path = '') {
|
|
814
|
-
if (data === null || data === undefined) {
|
|
815
|
-
return { processedData: data, shares: [] }
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
if (isBinaryData(data)) {
|
|
819
|
-
const hash = generateUploadHash(path || 'share')
|
|
820
|
-
return {
|
|
821
|
-
processedData: { [`__ape_share__`]: hash },
|
|
822
|
-
shares: [{ path, hash, data }]
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
if (Array.isArray(data)) {
|
|
827
|
-
const processedArray = []
|
|
828
|
-
const allShares = []
|
|
424
|
+
// Return existing interface if already connected
|
|
425
|
+
if (__socket && __socket.readyState !== WebSocket.CLOSED)
|
|
426
|
+
return buildClientInterface();
|
|
427
|
+
if (currentTransport === "polling" && streamingTransport?.isConnected())
|
|
428
|
+
return buildClientInterface();
|
|
829
429
|
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
processedArray.push(processedData)
|
|
834
|
-
allShares.push(...shares)
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
return { processedData: processedArray, shares: allShares }
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
if (typeof data === 'object') {
|
|
841
|
-
const processedObj = {}
|
|
842
|
-
const allShares = []
|
|
843
|
-
|
|
844
|
-
for (const key of Object.keys(data)) {
|
|
845
|
-
const itemPath = path ? `${path}.${key}` : key
|
|
846
|
-
const { processedData, shares } = processBinaryForSharing(data[key], itemPath)
|
|
847
|
-
|
|
848
|
-
// If this was binary data, mark the key with <!F> tag
|
|
849
|
-
if (shares.length > 0 && processedData?.__ape_share__) {
|
|
850
|
-
processedObj[`${key}<!F>`] = processedData.__ape_share__
|
|
851
|
-
} else {
|
|
852
|
-
processedObj[key] = processedData
|
|
853
|
-
}
|
|
854
|
-
allShares.push(...shares)
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
return { processedData: processedObj, shares: allShares }
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
return { processedData: data, shares: [] }
|
|
430
|
+
// Otherwise initiate connection
|
|
431
|
+
attemptConnection();
|
|
432
|
+
return buildClientInterface();
|
|
861
433
|
}
|
|
862
434
|
|
|
863
435
|
/**
|
|
864
|
-
*
|
|
865
|
-
*
|
|
436
|
+
* Build the public client interface object
|
|
437
|
+
*
|
|
438
|
+
* @returns {ClientInterface} The client interface
|
|
439
|
+
* @private
|
|
440
|
+
*
|
|
441
|
+
* @typedef {Object} ClientInterface
|
|
442
|
+
* @property {Proxy} sender - Proxied sender for calling server endpoints
|
|
443
|
+
* @property {function(string|function, function=): void} setOnReceiver - Register message handlers
|
|
444
|
+
* @property {function(function): function} onConnectionChange - Subscribe to connection state changes
|
|
445
|
+
* @property {'websocket'|'polling'|null} transport - Current transport type (read-only)
|
|
866
446
|
*/
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
const response = await fetch(`${baseUrl}/api/ape/data/_share/${hash}`, {
|
|
885
|
-
method: 'PUT',
|
|
886
|
-
credentials: 'include',
|
|
887
|
-
headers: {
|
|
888
|
-
'Content-Type': 'application/octet-stream'
|
|
889
|
-
},
|
|
890
|
-
body: data
|
|
891
|
-
})
|
|
447
|
+
function buildClientInterface() {
|
|
448
|
+
return {
|
|
449
|
+
/**
|
|
450
|
+
* Proxied sender object for calling server endpoints
|
|
451
|
+
*
|
|
452
|
+
* Properties accessed on this object are converted to API paths.
|
|
453
|
+
*
|
|
454
|
+
* @example
|
|
455
|
+
* // Calls /chat endpoint
|
|
456
|
+
* sender.chat({ message: 'Hi' })
|
|
457
|
+
*
|
|
458
|
+
* // Calls /users/123 endpoint
|
|
459
|
+
* sender.users('/123', { action: 'get' })
|
|
460
|
+
*
|
|
461
|
+
* @type {Proxy}
|
|
462
|
+
*/
|
|
463
|
+
sender: wrap(sender),
|
|
892
464
|
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
465
|
+
/**
|
|
466
|
+
* Register a message receiver/handler
|
|
467
|
+
* @see {@link module:client/connection/messageHandler.setOnReceiver}
|
|
468
|
+
*/
|
|
469
|
+
setOnReceiver,
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Subscribe to connection state changes
|
|
473
|
+
* @type {function(function(ConnectionStateValue): void): function(): void}
|
|
474
|
+
*/
|
|
475
|
+
onConnectionChange,
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Current transport type
|
|
479
|
+
* @type {'websocket'|'polling'|null}
|
|
480
|
+
* @readonly
|
|
481
|
+
*/
|
|
482
|
+
get transport() {
|
|
483
|
+
return currentTransport;
|
|
484
|
+
},
|
|
485
|
+
};
|
|
901
486
|
}
|
|
902
487
|
|
|
903
488
|
/**
|
|
904
|
-
*
|
|
489
|
+
* Enable automatic reconnection on connection loss
|
|
490
|
+
*
|
|
491
|
+
* When enabled, the client will automatically attempt to reconnect
|
|
492
|
+
* when the WebSocket connection is closed unexpectedly.
|
|
493
|
+
*
|
|
494
|
+
* @static
|
|
495
|
+
* @memberof connectSocket
|
|
496
|
+
*
|
|
497
|
+
* @example
|
|
498
|
+
* connectSocket.autoReconnect()
|
|
905
499
|
*/
|
|
906
|
-
|
|
907
|
-
if (uploads.length === 0) return
|
|
908
|
-
|
|
909
|
-
// Build base URL
|
|
910
|
-
const hostname = window.location.hostname
|
|
911
|
-
const isLocal = ["localhost", "127.0.0.1", "[::1]"].includes(hostname)
|
|
912
|
-
const isHttps = window.location.protocol === "https:"
|
|
913
|
-
const port = window.location.port || (isLocal ? 9010 : (isHttps ? 443 : 80))
|
|
914
|
-
const protocol = isHttps ? "https" : "http"
|
|
915
|
-
const portSuffix = (port !== 80 && port !== 443) ? `:${port}` : ""
|
|
916
|
-
const baseUrl = `${protocol}://${hostname}${portSuffix}`
|
|
917
|
-
|
|
918
|
-
console.log(`🦍 Uploading ${uploads.length} binary file(s)`)
|
|
919
|
-
|
|
920
|
-
await Promise.all(uploads.map(async ({ hash, data }) => {
|
|
921
|
-
try {
|
|
922
|
-
const response = await fetch(`${baseUrl}/api/ape/data/${queryId}/${hash}`, {
|
|
923
|
-
method: 'PUT',
|
|
924
|
-
credentials: 'include',
|
|
925
|
-
headers: {
|
|
926
|
-
'Content-Type': 'application/octet-stream'
|
|
927
|
-
},
|
|
928
|
-
body: data
|
|
929
|
-
})
|
|
930
|
-
|
|
931
|
-
if (!response.ok) {
|
|
932
|
-
throw new Error(`Upload failed: ${response.status}`)
|
|
933
|
-
}
|
|
934
|
-
} catch (err) {
|
|
935
|
-
console.error(`🦍 Failed to upload binary at ${hash}:`, err)
|
|
936
|
-
throw err
|
|
937
|
-
}
|
|
938
|
-
}))
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
wsSend = function (type, data, createdAt, dirctCall) {
|
|
942
|
-
let rej, promiseIsLive = false;
|
|
943
|
-
const timeLetForReqToBeMade = (createdAt + totalRequestTimeout) - Date.now()
|
|
944
|
-
|
|
945
|
-
const timer = setTimeout(() => {
|
|
946
|
-
if (promiseIsLive) {
|
|
947
|
-
rej(new Error("Request Timedout for :" + type))
|
|
948
|
-
}
|
|
949
|
-
}, timeLetForReqToBeMade);
|
|
950
|
-
|
|
951
|
-
// Process binary data for upload
|
|
952
|
-
const { processedData, uploads } = processBinaryForUpload(data)
|
|
953
|
-
|
|
954
|
-
const payload = {
|
|
955
|
-
type,
|
|
956
|
-
data: processedData,
|
|
957
|
-
//referer:window.location.href,
|
|
958
|
-
createdAt: new Date(createdAt),
|
|
959
|
-
requestedAt: dirctCall ? undefined
|
|
960
|
-
: new Date()
|
|
961
|
-
}
|
|
962
|
-
const message = jss.stringify(payload)
|
|
963
|
-
const queryId = messageHash(message);
|
|
964
|
-
|
|
965
|
-
const replyPromise = new Promise((resolve, reject) => {
|
|
966
|
-
rej = reject
|
|
967
|
-
waitingOn[queryId] = (err, result) => {
|
|
968
|
-
clearTimeout(timer)
|
|
969
|
-
replyPromise.then = next.bind(replyPromise)
|
|
970
|
-
if (err) {
|
|
971
|
-
reject(err)
|
|
972
|
-
} else {
|
|
973
|
-
resolve(result)
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
__socket.send(message);
|
|
977
|
-
|
|
978
|
-
// Upload binary data after sending WS message
|
|
979
|
-
if (uploads.length > 0) {
|
|
980
|
-
uploadBinaryData(queryId, uploads).catch(err => {
|
|
981
|
-
console.error('🦍 Binary upload failed:', err)
|
|
982
|
-
// The server will timeout waiting for the upload
|
|
983
|
-
})
|
|
984
|
-
}
|
|
985
|
-
});
|
|
986
|
-
const next = replyPromise.then;
|
|
987
|
-
replyPromise.then = worker => {
|
|
988
|
-
promiseIsLive = true;
|
|
989
|
-
replyPromise.then = next.bind(replyPromise)
|
|
990
|
-
replyPromise.catch = err.bind(replyPromise)
|
|
991
|
-
return next.call(replyPromise, worker)
|
|
992
|
-
}
|
|
993
|
-
const err = replyPromise.catch;
|
|
994
|
-
replyPromise.catch = worker => {
|
|
995
|
-
promiseIsLive = true;
|
|
996
|
-
replyPromise.catch = err.bind(replyPromise)
|
|
997
|
-
replyPromise.then = next.bind(replyPromise)
|
|
998
|
-
return err.call(replyPromise, worker)
|
|
999
|
-
}
|
|
1000
|
-
return replyPromise
|
|
1001
|
-
} // END wsSend
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
const sender = (type, data) => {
|
|
1005
|
-
if ("string" !== typeof type) {
|
|
1006
|
-
throw new Error("Missing Path vaule")
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
const createdAt = Date.now()
|
|
1010
|
-
|
|
1011
|
-
if (ready) {
|
|
1012
|
-
return wsSend(type, data, createdAt, true)
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
const timeLetForReqToBeMade = (createdAt + connectTimeout) - Date.now() // 5sec for reconnect
|
|
1016
|
-
|
|
1017
|
-
const timer = setTimeout(() => {
|
|
1018
|
-
const errMessage = "Request not sent for :" + type
|
|
1019
|
-
if (payload.waiting) {
|
|
1020
|
-
payload.reject(new Error(errMessage))
|
|
1021
|
-
} else {
|
|
1022
|
-
throw new Error(errMessage)
|
|
1023
|
-
}
|
|
1024
|
-
}, timeLetForReqToBeMade);
|
|
1025
|
-
|
|
1026
|
-
const payload = { type, data, resolve: undefined, reject: undefined, waiting: false, createdAt, timer };
|
|
1027
|
-
const waitingOnOpen = new Promise((res, rej) => { payload.resolve = res; payload.reject = rej; })
|
|
1028
|
-
|
|
1029
|
-
const waitingOnOpenThen = waitingOnOpen.then;
|
|
1030
|
-
const waitingOnOpenCatch = waitingOnOpen.catch;
|
|
1031
|
-
waitingOnOpen.then = worker => {
|
|
1032
|
-
payload.waiting = true;
|
|
1033
|
-
waitingOnOpen.then = waitingOnOpenThen.bind(waitingOnOpen)
|
|
1034
|
-
waitingOnOpen.catch = waitingOnOpenCatch.bind(waitingOnOpen)
|
|
1035
|
-
return waitingOnOpenThen.call(waitingOnOpen, worker)
|
|
1036
|
-
}
|
|
1037
|
-
waitingOnOpen.catch = worker => {
|
|
1038
|
-
payload.waiting = true;
|
|
1039
|
-
waitingOnOpen.catch = waitingOnOpenCatch.bind(waitingOnOpen)
|
|
1040
|
-
waitingOnOpen.then = waitingOnOpenThen.bind(waitingOnOpen)
|
|
1041
|
-
return waitingOnOpenCatch.call(waitingOnOpen, worker)
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
aWaitingSend.push(payload)
|
|
1045
|
-
if (!__socket) {
|
|
1046
|
-
connectSocket()
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
return waitingOnOpen
|
|
1050
|
-
} // END sender
|
|
500
|
+
connectSocket.autoReconnect = () => (reconnect = true);
|
|
1051
501
|
|
|
1052
502
|
/**
|
|
1053
|
-
*
|
|
503
|
+
* Connection state enum reference
|
|
504
|
+
*
|
|
505
|
+
* @static
|
|
506
|
+
* @memberof connectSocket
|
|
507
|
+
* @type {typeof ConnectionState}
|
|
508
|
+
*
|
|
509
|
+
* @example
|
|
510
|
+
* client.onConnectionChange((state) => {
|
|
511
|
+
* if (state === connectSocket.ConnectionState.Connected) {
|
|
512
|
+
* console.log('Connected!')
|
|
513
|
+
* }
|
|
514
|
+
* })
|
|
1054
515
|
*/
|
|
1055
|
-
|
|
1056
|
-
return {
|
|
1057
|
-
sender: wrap(sender),
|
|
1058
|
-
setOnReceiver: (onTypeStFn, handlerFn) => {
|
|
1059
|
-
if ("string" === typeof onTypeStFn) {
|
|
1060
|
-
// Replace handler for this type (prevents duplicates in React StrictMode)
|
|
1061
|
-
ofTypesOb[onTypeStFn] = [handlerFn]
|
|
1062
|
-
} else {
|
|
1063
|
-
// For general receivers, prevent duplicates by checking
|
|
1064
|
-
if (!receiverArray.includes(onTypeStFn)) {
|
|
1065
|
-
receiverArray.push(onTypeStFn)
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
},
|
|
1069
|
-
onConnectionChange: (handler) => {
|
|
1070
|
-
connectionChangeListeners.push(handler)
|
|
1071
|
-
// Immediately call with current state
|
|
1072
|
-
handler(connectionState)
|
|
1073
|
-
// Return unsubscribe function
|
|
1074
|
-
return () => {
|
|
1075
|
-
const idx = connectionChangeListeners.indexOf(handler)
|
|
1076
|
-
if (idx > -1) connectionChangeListeners.splice(idx, 1)
|
|
1077
|
-
}
|
|
1078
|
-
},
|
|
1079
|
-
// Expose current transport type (read-only)
|
|
1080
|
-
get transport() { return currentTransport }
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
connectSocket.autoReconnect = () => reconnect = true
|
|
1085
|
-
connectSocket.ConnectionState = ConnectionState
|
|
1086
|
-
connect = connectSocket
|
|
516
|
+
connectSocket.ConnectionState = ConnectionState;
|
|
1087
517
|
|
|
1088
|
-
export default
|
|
518
|
+
export default connectSocket;
|
|
1089
519
|
export { ConnectionState };
|