api-ape 2.1.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 +184 -195
- package/client/README.md +37 -30
- package/client/browser.js +4 -14
- package/client/connectSocket.js +167 -42
- package/client/index.js +171 -0
- package/client/transports/streaming.js +3 -16
- package/dist/ape.js +2 -1049
- 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 +67 -23
- package/package.json +27 -8
- package/server/README.md +52 -11
- package/server/lib/broadcast.js +25 -8
- package/server/lib/bun.js +122 -0
- package/server/lib/longPolling.js +28 -23
- package/server/lib/main.js +372 -46
- 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/socket/receive.js +14 -1
- package/server/socket/send.js +6 -6
- 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/browser.js
CHANGED
|
@@ -1,33 +1,23 @@
|
|
|
1
1
|
import connectSocket from './connectSocket.js'
|
|
2
2
|
|
|
3
|
-
// Auto-configure for current page
|
|
4
|
-
const port = window.location.port || (window.location.protocol === 'https:' ? 443 : 80)
|
|
5
|
-
connectSocket.configure({ port: parseInt(port, 10) })
|
|
6
|
-
|
|
7
3
|
const { sender, setOnReciver, onConnectionChange, getTransport } = connectSocket()
|
|
8
4
|
connectSocket.autoReconnect()
|
|
9
5
|
|
|
10
6
|
// Global API - use defineProperty to bypass Proxy interception
|
|
11
|
-
window.
|
|
12
|
-
Object.defineProperty(window.
|
|
7
|
+
window.api = sender
|
|
8
|
+
Object.defineProperty(window.api, 'on', {
|
|
13
9
|
value: setOnReciver,
|
|
14
10
|
writable: false,
|
|
15
11
|
enumerable: false,
|
|
16
12
|
configurable: false
|
|
17
13
|
})
|
|
18
|
-
Object.defineProperty(window.
|
|
14
|
+
Object.defineProperty(window.api, 'onConnectionChange', {
|
|
19
15
|
value: onConnectionChange,
|
|
20
16
|
writable: false,
|
|
21
17
|
enumerable: false,
|
|
22
18
|
configurable: false
|
|
23
19
|
})
|
|
24
|
-
Object.defineProperty(window.
|
|
25
|
-
value: connectSocket.configure,
|
|
26
|
-
writable: false,
|
|
27
|
-
enumerable: false,
|
|
28
|
-
configurable: false
|
|
29
|
-
})
|
|
30
|
-
Object.defineProperty(window.ape, 'getTransport', {
|
|
20
|
+
Object.defineProperty(window.api, 'getTransport', {
|
|
31
21
|
value: getTransport,
|
|
32
22
|
writable: false,
|
|
33
23
|
enumerable: false,
|
package/client/connectSocket.js
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
import messageHash from '../utils/messageHash'
|
|
2
2
|
import jss from '../utils/jss'
|
|
3
|
-
import { createStreamingTransport
|
|
3
|
+
import { createStreamingTransport } from './transports/streaming'
|
|
4
4
|
|
|
5
5
|
let connect;
|
|
6
6
|
|
|
7
7
|
// Connection state enum
|
|
8
8
|
const ConnectionState = {
|
|
9
|
+
Offline: 'offline', // navigator.onLine = false
|
|
10
|
+
Walled: 'walled', // Captive portal detected (ping failed)
|
|
9
11
|
Disconnected: 'disconnected',
|
|
10
12
|
Connecting: 'connecting',
|
|
11
13
|
Connected: 'connected',
|
|
12
14
|
Closing: 'closing'
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
// Connection state tracking
|
|
16
|
-
let connectionState =
|
|
17
|
+
// Connection state tracking - start with offline check
|
|
18
|
+
let connectionState = (typeof navigator !== 'undefined' && !navigator.onLine)
|
|
19
|
+
? ConnectionState.Offline
|
|
20
|
+
: ConnectionState.Disconnected
|
|
17
21
|
const connectionChangeListeners = []
|
|
18
22
|
|
|
19
23
|
function notifyConnectionChange(newState) {
|
|
@@ -24,50 +28,129 @@ function notifyConnectionChange(newState) {
|
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
// Configuration
|
|
27
|
-
let configuredPort = null
|
|
28
|
-
let configuredHost = null
|
|
29
31
|
let configuredTransport = 'auto' // 'auto' | 'websocket' | 'polling'
|
|
30
32
|
|
|
31
33
|
// Transport state
|
|
32
34
|
let currentTransport = null // 'websocket' | 'polling'
|
|
33
35
|
let streamingTransport = null
|
|
34
36
|
let wsRetryTimer = null
|
|
37
|
+
let networkCheckTimer = null
|
|
35
38
|
const WS_FALLBACK_TIMEOUT = 4000 // Time to wait for WS before fallback
|
|
36
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)
|
|
37
42
|
|
|
38
43
|
/**
|
|
39
|
-
*
|
|
40
|
-
* @param {object} opts
|
|
41
|
-
* @param {number} [opts.port] - WebSocket port (default: 9010 for local, 443/80 for remote)
|
|
42
|
-
* @param {string} [opts.host] - WebSocket host (default: auto-detect from window.location)
|
|
43
|
-
* @param {string} [opts.transport] - Transport mode: 'auto' | 'websocket' | 'polling'
|
|
44
|
+
* Check if running in dev/local mode
|
|
44
45
|
*/
|
|
45
|
-
function
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
46
|
+
function isDevMode() {
|
|
47
|
+
if (typeof window === 'undefined') return false
|
|
48
|
+
return ['localhost', '127.0.0.1', '[::1]'].includes(window.location.hostname)
|
|
49
|
+
}
|
|
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'
|
|
56
115
|
}
|
|
57
116
|
}
|
|
58
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
|
+
|
|
59
143
|
/**
|
|
60
144
|
* Get WebSocket URL - auto-detects from window.location, keeps /api/ape path
|
|
61
145
|
*/
|
|
62
146
|
function getSocketUrl() {
|
|
63
|
-
const hostname =
|
|
147
|
+
const hostname = window.location.hostname
|
|
64
148
|
const localServers = ["localhost", "127.0.0.1", "[::1]"]
|
|
65
149
|
const isLocal = localServers.includes(hostname)
|
|
66
150
|
const isHttps = window.location.protocol === "https:"
|
|
67
151
|
|
|
68
152
|
// Default port: 9010 for local dev, otherwise use window.location.port or implicit 443/80
|
|
69
|
-
const
|
|
70
|
-
const port = configuredPort || defaultPort
|
|
153
|
+
const port = isLocal ? 9010 : (window.location.port || (isHttps ? 443 : 80))
|
|
71
154
|
|
|
72
155
|
// Build URL - keep /api/ape path
|
|
73
156
|
const protocol = isHttps ? "wss" : "ws"
|
|
@@ -83,7 +166,7 @@ const totalRequestTimeout = 10000
|
|
|
83
166
|
|
|
84
167
|
const joinKey = "/"
|
|
85
168
|
// Properties accessed directly on `ape` that should NOT be intercepted
|
|
86
|
-
const reservedKeys = new Set(['on', 'onConnectionChange', '
|
|
169
|
+
const reservedKeys = new Set(['on', 'onConnectionChange', 'getTransport'])
|
|
87
170
|
const handler = {
|
|
88
171
|
get(fn, key) {
|
|
89
172
|
// Skip proxy interception for reserved keys - return actual property
|
|
@@ -387,7 +470,7 @@ function cleanLinkedKeys(obj) {
|
|
|
387
470
|
/**
|
|
388
471
|
* Fetch binary resources and hydrate data object
|
|
389
472
|
*/
|
|
390
|
-
async function fetchLinkedResources(data,
|
|
473
|
+
async function fetchLinkedResources(data, clientId) {
|
|
391
474
|
const resources = findLinkedResources(data)
|
|
392
475
|
|
|
393
476
|
if (resources.length === 0) {
|
|
@@ -398,11 +481,10 @@ async function fetchLinkedResources(data, hostId) {
|
|
|
398
481
|
|
|
399
482
|
const cleanedData = cleanLinkedKeys(data)
|
|
400
483
|
|
|
401
|
-
const hostname =
|
|
484
|
+
const hostname = window.location.hostname
|
|
402
485
|
const isLocal = ["localhost", "127.0.0.1", "[::1]"].includes(hostname)
|
|
403
486
|
const isHttps = window.location.protocol === "https:"
|
|
404
|
-
const
|
|
405
|
-
const port = configuredPort || defaultPort
|
|
487
|
+
const port = isLocal ? 9010 : (window.location.port || (isHttps ? 443 : 80))
|
|
406
488
|
const protocol = isHttps ? "https" : "http"
|
|
407
489
|
const portSuffix = (isLocal || (port !== 80 && port !== 443)) ? `:${port}` : ""
|
|
408
490
|
const baseUrl = `${protocol}://${hostname}${portSuffix}`
|
|
@@ -412,7 +494,7 @@ async function fetchLinkedResources(data, hostId) {
|
|
|
412
494
|
const response = await fetch(`${baseUrl}/api/ape/data/${hash}`, {
|
|
413
495
|
credentials: 'include',
|
|
414
496
|
headers: {
|
|
415
|
-
'X-Ape-
|
|
497
|
+
'X-Ape-Client-Id': clientId || ''
|
|
416
498
|
}
|
|
417
499
|
})
|
|
418
500
|
|
|
@@ -431,17 +513,46 @@ async function fetchLinkedResources(data, hostId) {
|
|
|
431
513
|
return cleanedData
|
|
432
514
|
}
|
|
433
515
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
if (
|
|
440
|
-
|
|
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
|
|
441
524
|
}
|
|
442
525
|
|
|
526
|
+
// Perform captive portal check
|
|
443
527
|
notifyConnectionChange(ConnectionState.Connecting)
|
|
528
|
+
const pingResult = await checkCaptivePortal()
|
|
444
529
|
|
|
530
|
+
if (pingResult === 'walled') {
|
|
531
|
+
notifyConnectionChange(ConnectionState.Walled)
|
|
532
|
+
// Retry network check periodically
|
|
533
|
+
scheduleNetworkRetry()
|
|
534
|
+
return
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Network is good, proceed with socket connection
|
|
538
|
+
proceedWithConnection()
|
|
539
|
+
}
|
|
540
|
+
|
|
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() {
|
|
445
556
|
// Determine which transport to use
|
|
446
557
|
if (configuredTransport === 'polling') {
|
|
447
558
|
switchToStreaming()
|
|
@@ -449,6 +560,22 @@ function connectSocket() {
|
|
|
449
560
|
// 'auto' or 'websocket' - try WebSocket first
|
|
450
561
|
tryWebSocket(false)
|
|
451
562
|
}
|
|
563
|
+
}
|
|
564
|
+
|
|
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
|
+
}
|
|
576
|
+
|
|
577
|
+
// Start connection with network pre-checks
|
|
578
|
+
attemptConnection()
|
|
452
579
|
|
|
453
580
|
return buildClientInterface()
|
|
454
581
|
}
|
|
@@ -547,11 +674,10 @@ async function uploadBinaryData(queryId, uploads) {
|
|
|
547
674
|
if (uploads.length === 0) return
|
|
548
675
|
|
|
549
676
|
// Build base URL
|
|
550
|
-
const hostname =
|
|
677
|
+
const hostname = window.location.hostname
|
|
551
678
|
const isLocal = ["localhost", "127.0.0.1", "[::1]"].includes(hostname)
|
|
552
679
|
const isHttps = window.location.protocol === "https:"
|
|
553
|
-
const
|
|
554
|
-
const port = configuredPort || defaultPort
|
|
680
|
+
const port = isLocal ? 9010 : (window.location.port || (isHttps ? 443 : 80))
|
|
555
681
|
const protocol = isHttps ? "https" : "http"
|
|
556
682
|
const portSuffix = (isLocal || (port !== 80 && port !== 443)) ? `:${port}` : ""
|
|
557
683
|
const baseUrl = `${protocol}://${hostname}${portSuffix}`
|
|
@@ -723,9 +849,8 @@ function buildClientInterface() {
|
|
|
723
849
|
}
|
|
724
850
|
|
|
725
851
|
connectSocket.autoReconnect = () => reconnect = true
|
|
726
|
-
connectSocket.configure = configure
|
|
727
852
|
connectSocket.ConnectionState = ConnectionState
|
|
728
853
|
connect = connectSocket
|
|
729
854
|
|
|
730
855
|
export default connect;
|
|
731
|
-
export {
|
|
856
|
+
export { ConnectionState };
|
package/client/index.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified api-ape export for browser
|
|
3
|
+
*
|
|
4
|
+
* Auto-detects browser environment, initializes client, and buffers
|
|
5
|
+
* calls until the connection is ready. No more getApeClient().then()!
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* import api from 'api-ape'
|
|
9
|
+
*
|
|
10
|
+
* // Properties are proxied - calls buffer until connected
|
|
11
|
+
* api.message({ user: 'Bob', text: 'Hello!' })
|
|
12
|
+
*
|
|
13
|
+
* // Subscribe to broadcasts
|
|
14
|
+
* api.on('message', (data) => console.log(data))
|
|
15
|
+
*
|
|
16
|
+
* // Check connection state
|
|
17
|
+
* api.onConnectionChange((state) => console.log(state))
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
// Only run this in browser environments
|
|
21
|
+
const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined'
|
|
22
|
+
|
|
23
|
+
let clientPromise = null
|
|
24
|
+
let resolvedClient = null
|
|
25
|
+
const bufferedCalls = []
|
|
26
|
+
const bufferedReceivers = []
|
|
27
|
+
const connectionChangeHandlers = []
|
|
28
|
+
let currentConnectionState = 'disconnected'
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Initialize the client (called once on first use)
|
|
32
|
+
*/
|
|
33
|
+
function getClient() {
|
|
34
|
+
if (clientPromise) return clientPromise
|
|
35
|
+
|
|
36
|
+
if (!isBrowser) {
|
|
37
|
+
// Return a dummy object for SSR
|
|
38
|
+
return Promise.resolve(null)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
clientPromise = (async () => {
|
|
42
|
+
const connectSocket = (await import('./connectSocket.js')).default
|
|
43
|
+
|
|
44
|
+
// Connect
|
|
45
|
+
const client = connectSocket()
|
|
46
|
+
connectSocket.autoReconnect()
|
|
47
|
+
|
|
48
|
+
// Track connection state
|
|
49
|
+
client.onConnectionChange((state) => {
|
|
50
|
+
currentConnectionState = state
|
|
51
|
+
connectionChangeHandlers.forEach(fn => fn(state))
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
resolvedClient = client
|
|
55
|
+
|
|
56
|
+
// Flush buffered receivers
|
|
57
|
+
bufferedReceivers.forEach(({ type, handler }) => {
|
|
58
|
+
client.setOnReciver(type, handler)
|
|
59
|
+
})
|
|
60
|
+
bufferedReceivers.length = 0
|
|
61
|
+
|
|
62
|
+
// Flush buffered calls
|
|
63
|
+
bufferedCalls.forEach(({ method, args, resolve, reject }) => {
|
|
64
|
+
try {
|
|
65
|
+
const result = client.sender[method](...args)
|
|
66
|
+
if (result && typeof result.then === 'function') {
|
|
67
|
+
result.then(resolve).catch(reject)
|
|
68
|
+
} else {
|
|
69
|
+
resolve(result)
|
|
70
|
+
}
|
|
71
|
+
} catch (err) {
|
|
72
|
+
reject(err)
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
bufferedCalls.length = 0
|
|
76
|
+
|
|
77
|
+
return client
|
|
78
|
+
})()
|
|
79
|
+
|
|
80
|
+
return clientPromise
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Create a sender proxy that buffers calls until client is ready
|
|
85
|
+
*/
|
|
86
|
+
const senderProxy = new Proxy({}, {
|
|
87
|
+
get(target, prop) {
|
|
88
|
+
// Reserved properties
|
|
89
|
+
if (prop === 'on') return on
|
|
90
|
+
if (prop === 'onConnectionChange') return onConnectionChange
|
|
91
|
+
if (prop === 'getTransport') return () => resolvedClient?.getTransport?.() || null
|
|
92
|
+
if (prop === 'then' || prop === 'catch') return undefined // Not a Promise
|
|
93
|
+
|
|
94
|
+
// Return a function that either calls directly or buffers
|
|
95
|
+
return (...args) => {
|
|
96
|
+
// If client is ready, call directly
|
|
97
|
+
if (resolvedClient) {
|
|
98
|
+
return resolvedClient.sender[prop](...args)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Buffer the call and return a Promise
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
bufferedCalls.push({ method: prop, args, resolve, reject })
|
|
104
|
+
// Ensure client is initializing
|
|
105
|
+
getClient()
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Subscribe to broadcasts from the server
|
|
113
|
+
* @param {string} type - Broadcast type to listen for
|
|
114
|
+
* @param {Function} handler - Handler function
|
|
115
|
+
*/
|
|
116
|
+
function on(type, handler) {
|
|
117
|
+
if (resolvedClient) {
|
|
118
|
+
resolvedClient.setOnReciver(type, handler)
|
|
119
|
+
} else {
|
|
120
|
+
bufferedReceivers.push({ type, handler })
|
|
121
|
+
getClient()
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Subscribe to connection state changes
|
|
127
|
+
* @param {Function} handler - Called with state: 'offline' | 'walled' | 'disconnected' | 'connecting' | 'connected'
|
|
128
|
+
* @returns {Function} Unsubscribe function
|
|
129
|
+
*/
|
|
130
|
+
function onConnectionChange(handler) {
|
|
131
|
+
connectionChangeHandlers.push(handler)
|
|
132
|
+
// Immediately call with current state
|
|
133
|
+
handler(currentConnectionState)
|
|
134
|
+
|
|
135
|
+
// If client exists, also register with it
|
|
136
|
+
if (resolvedClient) {
|
|
137
|
+
return resolvedClient.onConnectionChange(handler)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Ensure client is initializing
|
|
141
|
+
getClient()
|
|
142
|
+
|
|
143
|
+
// Return unsubscribe function
|
|
144
|
+
return () => {
|
|
145
|
+
const idx = connectionChangeHandlers.indexOf(handler)
|
|
146
|
+
if (idx > -1) connectionChangeHandlers.splice(idx, 1)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Define properties on the proxy to avoid Proxy interception issues
|
|
151
|
+
Object.defineProperty(senderProxy, 'on', {
|
|
152
|
+
value: on,
|
|
153
|
+
writable: false,
|
|
154
|
+
enumerable: false,
|
|
155
|
+
configurable: false
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
Object.defineProperty(senderProxy, 'onConnectionChange', {
|
|
159
|
+
value: onConnectionChange,
|
|
160
|
+
writable: false,
|
|
161
|
+
enumerable: false,
|
|
162
|
+
configurable: false
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
// Auto-initialize in browser
|
|
166
|
+
if (isBrowser) {
|
|
167
|
+
getClient()
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export default senderProxy
|
|
171
|
+
export { on, onConnectionChange, getClient }
|
|
@@ -5,29 +5,16 @@ import jss from '../../utils/jss'
|
|
|
5
5
|
* Uses fetch + ReadableStream for receiving, POST for sending
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
// Configuration
|
|
9
|
-
let configuredPort = null
|
|
10
|
-
let configuredHost = null
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Configure transport connection options
|
|
14
|
-
*/
|
|
15
|
-
function configure(opts = {}) {
|
|
16
|
-
if (opts.port) configuredPort = opts.port
|
|
17
|
-
if (opts.host) configuredHost = opts.host
|
|
18
|
-
}
|
|
19
|
-
|
|
20
8
|
/**
|
|
21
9
|
* Get base URL for polling endpoints
|
|
22
10
|
*/
|
|
23
11
|
function getPollUrl() {
|
|
24
|
-
const hostname =
|
|
12
|
+
const hostname = window.location.hostname
|
|
25
13
|
const localServers = ["localhost", "127.0.0.1", "[::1]"]
|
|
26
14
|
const isLocal = localServers.includes(hostname)
|
|
27
15
|
const isHttps = window.location.protocol === "https:"
|
|
28
16
|
|
|
29
|
-
const
|
|
30
|
-
const port = configuredPort || defaultPort
|
|
17
|
+
const port = isLocal ? 9010 : (window.location.port || (isHttps ? 443 : 80))
|
|
31
18
|
|
|
32
19
|
const protocol = isHttps ? "https" : "http"
|
|
33
20
|
const portSuffix = (isLocal || (port !== 80 && port !== 443)) ? `:${port}` : ""
|
|
@@ -250,4 +237,4 @@ function createStreamingTransport() {
|
|
|
250
237
|
}
|
|
251
238
|
}
|
|
252
239
|
|
|
253
|
-
export { createStreamingTransport,
|
|
240
|
+
export { createStreamingTransport, getPollUrl }
|