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.
Files changed (89) hide show
  1. package/README.md +203 -124
  2. package/client/README.md +37 -30
  3. package/client/browser.js +10 -8
  4. package/client/connectSocket.js +662 -381
  5. package/client/index.js +171 -0
  6. package/client/transports/streaming.js +240 -0
  7. package/dist/ape.js +2 -699
  8. package/dist/ape.js.map +7 -0
  9. package/dist/api-ape.min.js +2 -0
  10. package/dist/api-ape.min.js.map +7 -0
  11. package/index.d.ts +71 -18
  12. package/package.json +50 -15
  13. package/server/README.md +99 -13
  14. package/server/lib/broadcast.js +25 -8
  15. package/server/lib/bun.js +122 -0
  16. package/server/lib/longPolling.js +226 -0
  17. package/server/lib/main.js +381 -38
  18. package/server/lib/wiring.js +19 -12
  19. package/server/lib/ws/adapters/bun.js +225 -0
  20. package/server/lib/ws/adapters/deno.js +186 -0
  21. package/server/lib/ws/frames.js +217 -0
  22. package/server/lib/ws/index.js +15 -0
  23. package/server/lib/ws/server.js +109 -0
  24. package/server/lib/ws/socket.js +222 -0
  25. package/server/lib/wsProvider.js +135 -0
  26. package/server/security/origin.js +16 -4
  27. package/server/socket/receive.js +14 -1
  28. package/server/socket/send.js +6 -6
  29. package/server/utils/deepRequire.js +25 -10
  30. package/server/utils/parseUserAgent.js +286 -0
  31. package/example/Bun/README.md +0 -74
  32. package/example/Bun/api/message.ts +0 -11
  33. package/example/Bun/index.html +0 -76
  34. package/example/Bun/package.json +0 -9
  35. package/example/Bun/server.ts +0 -59
  36. package/example/Bun/styles.css +0 -128
  37. package/example/ExpressJs/README.md +0 -95
  38. package/example/ExpressJs/api/message.js +0 -11
  39. package/example/ExpressJs/backend.js +0 -39
  40. package/example/ExpressJs/index.html +0 -88
  41. package/example/ExpressJs/package-lock.json +0 -834
  42. package/example/ExpressJs/package.json +0 -10
  43. package/example/ExpressJs/styles.css +0 -128
  44. package/example/NextJs/.dockerignore +0 -29
  45. package/example/NextJs/Dockerfile +0 -52
  46. package/example/NextJs/Dockerfile.dev +0 -27
  47. package/example/NextJs/README.md +0 -113
  48. package/example/NextJs/ape/client.js +0 -66
  49. package/example/NextJs/ape/embed.js +0 -12
  50. package/example/NextJs/ape/index.js +0 -23
  51. package/example/NextJs/ape/logic/chat.js +0 -62
  52. package/example/NextJs/ape/onConnect.js +0 -69
  53. package/example/NextJs/ape/onDisconnect.js +0 -13
  54. package/example/NextJs/ape/onError.js +0 -9
  55. package/example/NextJs/ape/onReceive.js +0 -15
  56. package/example/NextJs/ape/onSend.js +0 -15
  57. package/example/NextJs/api/message.js +0 -44
  58. package/example/NextJs/docker-compose.yml +0 -22
  59. package/example/NextJs/next-env.d.ts +0 -5
  60. package/example/NextJs/next.config.js +0 -8
  61. package/example/NextJs/package-lock.json +0 -6400
  62. package/example/NextJs/package.json +0 -24
  63. package/example/NextJs/pages/Info.tsx +0 -153
  64. package/example/NextJs/pages/_app.tsx +0 -6
  65. package/example/NextJs/pages/index.tsx +0 -275
  66. package/example/NextJs/public/favicon.ico +0 -0
  67. package/example/NextJs/public/vercel.svg +0 -4
  68. package/example/NextJs/server.js +0 -36
  69. package/example/NextJs/styles/Chat.module.css +0 -448
  70. package/example/NextJs/styles/Home.module.css +0 -129
  71. package/example/NextJs/styles/globals.css +0 -26
  72. package/example/NextJs/tsconfig.json +0 -20
  73. package/example/README.md +0 -117
  74. package/example/Vite/README.md +0 -68
  75. package/example/Vite/ape/client.ts +0 -66
  76. package/example/Vite/ape/onConnect.ts +0 -52
  77. package/example/Vite/api/message.ts +0 -57
  78. package/example/Vite/index.html +0 -16
  79. package/example/Vite/package.json +0 -19
  80. package/example/Vite/server.ts +0 -62
  81. package/example/Vite/src/App.vue +0 -170
  82. package/example/Vite/src/components/Info.vue +0 -352
  83. package/example/Vite/src/main.ts +0 -5
  84. package/example/Vite/src/style.css +0 -200
  85. package/example/Vite/src/vite-env.d.ts +0 -7
  86. package/example/Vite/vite.config.ts +0 -20
  87. package/todo.md +0 -85
  88. package/utils/jss.test.js +0 -261
  89. package/utils/messageHash.test.js +0 -56
@@ -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 = ConnectionState.Disconnected
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 configuredPort = null
27
- let configuredHost = null
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
- * Configure api-ape client connection
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 configure(opts = {}) {
36
- if (opts.port) configuredPort = opts.port
37
- if (opts.host) configuredHost = opts.host
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 = configuredHost || window.location.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 defaultPort = isLocal ? 9010 : (window.location.port || (isHttps ? 443 : 80))
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
- function connectSocket() {
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 (!__socket) {
103
- notifyConnectionChange(ConnectionState.Connecting)
104
- __socket = new WebSocket(getSocketUrl())
209
+ if (!streamingTransport) {
210
+ streamingTransport = createStreamingTransport()
105
211
 
106
- __socket.onopen = event => {
107
- //console.log('socket connected()');
108
- ready = true;
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
- //TODO: clear throw of wait for server
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
- if (obj === null || obj === undefined || typeof obj !== 'object') {
132
- return resources
133
- }
134
-
135
- if (Array.isArray(obj)) {
136
- for (let i = 0; i < obj.length; i++) {
137
- resources.push(...findLinkedResources(obj[i], path ? `${path}.${i}` : String(i)))
138
- }
139
- return resources
140
- }
239
+ // Start background WebSocket retry
240
+ startWsRetry()
241
+ }
141
242
 
142
- for (const key of Object.keys(obj)) {
143
- // Check for L-tag in key (from JJS encoding: key<!L>)
144
- if (key.endsWith('<!L>')) {
145
- const cleanKey = key.slice(0, -4)
146
- const hash = obj[key]
147
- resources.push({
148
- path: path ? `${path}.${cleanKey}` : cleanKey,
149
- hash,
150
- originalKey: key
151
- })
152
- } else {
153
- resources.push(...findLinkedResources(obj[key], path ? `${path}.${key}` : key))
154
- }
155
- }
243
+ streamingTransport.onClose = () => {
244
+ ready = false
245
+ notifyConnectionChange(ConnectionState.Disconnected)
246
+ }
156
247
 
157
- return resources
248
+ streamingTransport.onError = (err) => {
249
+ console.error('🦍 Streaming error:', err)
158
250
  }
251
+ }
159
252
 
160
- /**
161
- * Set a value at a nested path in an object
162
- */
163
- function setValueAtPath(obj, path, value) {
164
- const parts = path.split('.')
165
- let current = obj
253
+ streamingTransport.connect()
254
+ }
166
255
 
167
- for (let i = 0; i < parts.length - 1; i++) {
168
- current = current[parts[i]]
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
- current[parts[parts.length - 1]] = value
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
- * Clean up L-tagged keys (rename key<!L> to key)
176
- */
177
- function cleanLinkedKeys(obj) {
178
- if (obj === null || obj === undefined || typeof obj !== 'object') {
179
- return obj
180
- }
278
+ console.log('🦍 Attempting WebSocket reconnection...')
279
+ tryWebSocket(true)
280
+ }, WS_RETRY_INTERVAL)
281
+ }
181
282
 
182
- if (Array.isArray(obj)) {
183
- return obj.map(cleanLinkedKeys)
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
- const cleaned = {}
187
- for (const key of Object.keys(obj)) {
188
- if (key.endsWith('<!L>')) {
189
- const cleanKey = key.slice(0, -4)
190
- cleaned[cleanKey] = obj[key] // Value will be replaced after fetch
191
- } else {
192
- cleaned[key] = cleanLinkedKeys(obj[key])
193
- }
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
- * Fetch binary resources and hydrate data object
200
- */
201
- async function fetchLinkedResources(data, hostId) {
202
- const resources = findLinkedResources(data)
317
+ currentTransport = 'websocket'
318
+ __socket = ws
319
+ ready = true
320
+ notifyConnectionChange(ConnectionState.Connected)
203
321
 
204
- if (resources.length === 0) {
205
- return data
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
- console.log(`🦍 Fetching ${resources.length} binary resource(s)`)
209
-
210
- // Clean the data first (remove <!L> suffixes from keys)
211
- const cleanedData = cleanLinkedKeys(data)
212
-
213
- // Build base URL for fetches
214
- const hostname = configuredHost || window.location.hostname
215
- const isLocal = ["localhost", "127.0.0.1", "[::1]"].includes(hostname)
216
- const isHttps = window.location.protocol === "https:"
217
- const defaultPort = isLocal ? 9010 : (window.location.port || (isHttps ? 443 : 80))
218
- const port = configuredPort || defaultPort
219
- const protocol = isHttps ? "https" : "http"
220
- const portSuffix = (isLocal || (port !== 80 && port !== 443)) ? `:${port}` : ""
221
- const baseUrl = `${protocol}://${hostname}${portSuffix}`
222
-
223
- // Fetch all resources in parallel
224
- await Promise.all(resources.map(async ({ path, hash }) => {
225
- try {
226
- const response = await fetch(`${baseUrl}/api/ape/data/${hash}`, {
227
- credentials: 'include',
228
- headers: {
229
- 'X-Ape-Host-Id': hostId || ''
230
- }
231
- })
232
-
233
- if (!response.ok) {
234
- throw new Error(`Failed to fetch binary resource: ${response.status}`)
235
- }
236
-
237
- const arrayBuffer = await response.arrayBuffer()
238
- setValueAtPath(cleanedData, path, arrayBuffer)
239
- } catch (err) {
240
- console.error(`🦍 Failed to fetch binary resource at ${path}:`, err)
241
- setValueAtPath(cleanedData, path, null)
242
- }
243
- }))
244
-
245
- return cleanedData
246
- }
247
-
248
- __socket.onmessage = async function (event) {
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
- // No matching promise - error and ignore
269
- console.error(`🦍 No matching queryId: ${queryId}`)
347
+ waitingOn[queryId](err, data)
270
348
  }
271
- return
349
+ delete waitingOn[queryId]
350
+ } else {
351
+ console.error(`🦍 No matching queryId: ${queryId}`)
272
352
  }
353
+ return
354
+ }
273
355
 
274
- // Only messages WITHOUT queryId go to setOnReciver
275
- // Also hydrate broadcast messages
276
- let processedData = data
277
- if (data && !err) {
278
- try {
279
- processedData = await fetchLinkedResources(data)
280
- } catch (fetchErr) {
281
- console.error(`🦍 Failed to hydrate broadcast data:`, fetchErr)
282
- }
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
- if (ofTypesOb[type]) {
286
- ofTypesOb[type].forEach(worker => worker({ err, type, data: processedData }))
287
- } // if ofTypesOb[type]
288
- reciverOnAr.forEach(worker => worker({ err, type, data: processedData }))
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
- } // END onmessage
372
+ ws.onerror = function (err) {
373
+ if (fallbackTimer) clearTimeout(fallbackTimer)
374
+ console.error('socket ERROR:', err)
291
375
 
292
- __socket.onerror = function (err) {
293
- console.error('socket ERROR:', err);
294
- } // END onerror
376
+ // On initial connection error in auto mode, fallback to streaming
377
+ if (!isRetry && configuredTransport === 'auto' && !ready) {
378
+ switchToStreaming()
379
+ }
380
+ }
295
381
 
296
- __socket.onclose = function (event) {
297
- console.warn('socket disconnect:', event);
298
- __socket = false
299
- ready = false;
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
- return Math.abs(hash).toString(36)
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
- * Find and extract binary data from payload
339
- * Returns { processedData, uploads: [{ path, hash, data, tag }] }
340
- */
341
- function processBinaryForUpload(data, path = '') {
342
- if (data === null || data === undefined) {
343
- return { processedData: data, uploads: [] }
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
- if (isBinaryData(data)) {
347
- const tag = getBinaryTag(data)
348
- const hash = generateUploadHash(path || 'root')
349
- return {
350
- processedData: { [`__ape_upload__`]: hash },
351
- uploads: [{ path, hash, data, tag }]
352
- }
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
- if (Array.isArray(data)) {
356
- const processedArray = []
357
- const allUploads = []
429
+ return resources
430
+ }
358
431
 
359
- for (let i = 0; i < data.length; i++) {
360
- const itemPath = path ? `${path}.${i}` : String(i)
361
- const { processedData, uploads } = processBinaryForUpload(data[i], itemPath)
362
- processedArray.push(processedData)
363
- allUploads.push(...uploads)
364
- }
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
- return { processedData: processedArray, uploads: allUploads }
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
- if (typeof data === 'object') {
370
- const processedObj = {}
371
- const allUploads = []
476
+ if (resources.length === 0) {
477
+ return data
478
+ }
372
479
 
373
- for (const key of Object.keys(data)) {
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
- // If this was binary data, mark the key with <!B> or <!A> tag
378
- if (uploads.length > 0 && processedData?.__ape_upload__) {
379
- const tag = uploads[uploads.length - 1].tag
380
- processedObj[`${key}<!${tag}>`] = processedData.__ape_upload__
381
- } else {
382
- processedObj[key] = processedData
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
- allUploads.push(...uploads)
499
+ })
500
+
501
+ if (!response.ok) {
502
+ throw new Error(`Failed to fetch binary resource: ${response.status}`)
385
503
  }
386
504
 
387
- return { processedData: processedObj, uploads: allUploads }
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
- return { processedData: data, uploads: [] }
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
- * Upload binary data via HTTP PUT
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
- // Build base URL
400
- const hostname = configuredHost || window.location.hostname
401
- const isLocal = ["localhost", "127.0.0.1", "[::1]"].includes(hostname)
402
- const isHttps = window.location.protocol === "https:"
403
- const defaultPort = isLocal ? 9010 : (window.location.port || (isHttps ? 443 : 80))
404
- const port = configuredPort || defaultPort
405
- const protocol = isHttps ? "https" : "http"
406
- const portSuffix = (isLocal || (port !== 80 && port !== 443)) ? `:${port}` : ""
407
- const baseUrl = `${protocol}://${hostname}${portSuffix}`
530
+ if (pingResult === 'walled') {
531
+ notifyConnectionChange(ConnectionState.Walled)
532
+ // Retry network check periodically
533
+ scheduleNetworkRetry()
534
+ return
535
+ }
408
536
 
409
- console.log(`🦍 Uploading ${uploads.length} binary file(s)`)
537
+ // Network is good, proceed with socket connection
538
+ proceedWithConnection()
539
+ }
410
540
 
411
- await Promise.all(uploads.map(async ({ hash, data }) => {
412
- try {
413
- const response = await fetch(`${baseUrl}/api/ape/data/${queryId}/${hash}`, {
414
- method: 'PUT',
415
- credentials: 'include',
416
- headers: {
417
- 'Content-Type': 'application/octet-stream'
418
- },
419
- body: data
420
- })
421
-
422
- if (!response.ok) {
423
- throw new Error(`Upload failed: ${response.status}`)
424
- }
425
- } catch (err) {
426
- console.error(`🦍 Failed to upload binary at ${hash}:`, err)
427
- throw err
428
- }
429
- }))
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
- wsSend = function (type, data, createdAt, dirctCall) {
433
- let rej, promiseIsLive = false;
434
- const timeLetForReqToBeMade = (createdAt + totalRequestTimeout) - Date.now()
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
- const timer = setTimeout(() => {
437
- if (promiseIsLive) {
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
- const replyPromise = new Promise((resolve, reject) => {
457
- rej = reject
458
- waitingOn[queryId] = (err, result) => {
459
- clearTimeout(timer)
460
- replyPromise.then = next.bind(replyPromise)
461
- if (err) {
462
- reject(err)
463
- } else {
464
- resolve(result)
465
- }
466
- }
467
- __socket.send(message);
468
-
469
- // Upload binary data after sending WS message
470
- if (uploads.length > 0) {
471
- uploadBinaryData(queryId, uploads).catch(err => {
472
- console.error('🦍 Binary upload failed:', err)
473
- // The server will timeout waiting for the upload
474
- })
475
- }
476
- });
477
- const next = replyPromise.then;
478
- replyPromise.then = worker => {
479
- promiseIsLive = true;
480
- replyPromise.then = next.bind(replyPromise)
481
- replyPromise.catch = err.bind(replyPromise)
482
- return next.call(replyPromise, worker)
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
- const err = replyPromise.catch;
485
- replyPromise.catch = worker => {
486
- promiseIsLive = true;
487
- replyPromise.catch = err.bind(replyPromise)
488
- replyPromise.then = next.bind(replyPromise)
489
- return err.call(replyPromise, worker)
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
- const sender = (type, data) => {
496
- if ("string" !== typeof type) {
497
- throw new Error("Missing Path vaule")
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
- const createdAt = Date.now()
664
+ return { processedData: processedObj, uploads: allUploads }
665
+ }
501
666
 
502
- if (ready) {
503
- return wsSend(type, data, createdAt, true)
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
- const timeLetForReqToBeMade = (createdAt + connentTimeout) - Date.now() // 5sec for reconnent
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
- const timer = setTimeout(() => {
509
- const errMessage = "Request not sent for :" + type
510
- if (payload.waiting) {
511
- payload.err(new Error(errMessage))
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
- throw new Error(errMessage)
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
- waitingOnOpen.catch = worker => {
529
- payload.waiting = true;
530
- waitingOnOpen.catch = waitingOnOpenCatch.bind(waitingOnOpen)
531
- waitingOnOpen.then = waitingOnOpenThen.bind(waitingOnOpen)
532
- return waitingOnOpenCatch.call(waitingOnOpen, worker)
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
- aWaitingSend.push(payload)
536
- if (!__socket) {
537
- connectSocket()
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
- return waitingOnOpen
541
- } // END sender
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
- }, // END setOnReciver
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
- } // END return
567
- } // END connectSocket
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 { configure, ConnectionState };
856
+ export { ConnectionState };