api-ape 1.0.2 → 2.0.0

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