api-ape 2.2.3 → 3.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.
package/README.md CHANGED
@@ -3,6 +3,7 @@
3
3
  [![npm version](https://img.shields.io/npm/v/api-ape.svg)](https://www.npmjs.com/package/api-ape)
4
4
  [![GitHub issues](https://img.shields.io/github/issues/codemeasandwich/api-ape)](https://github.com/codemeasandwich/api-ape/issues)
5
5
  [![Zero Dependencies](https://img.shields.io/badge/dependencies-0-brightgreen.svg)](#zero-dependencies)
6
+ [![Dependabot](https://img.shields.io/dependabot/codemeasandwich/api-ape)](https://github.com/codemeasandwich/api-ape/security/dependabot)
6
7
  [![CSRF protected](https://img.shields.io/badge/CSRF%20🚷-protected-green.svg)](#csrf-protection)
7
8
  [![Bundle Size](https://img.shields.io/bundlephobia/minzip/api-ape)](https://bundlephobia.com/package/api-ape)
8
9
  [![JJS Encoding](https://img.shields.io/badge/encoding-JJS-blue.svg)](#jjs-encoding)
@@ -32,7 +33,7 @@ yarn add api-ape
32
33
 
33
34
  ```js
34
35
  const { createServer } = require('http')
35
- const ape = require('api-ape')
36
+ const { ape } = require('api-ape')
36
37
 
37
38
  const server = createServer()
38
39
 
@@ -45,7 +46,7 @@ server.listen(3000)
45
46
  **With Express:**
46
47
  ```js
47
48
  const express = require('express')
48
- const ape = require('api-ape')
49
+ const { ape } = require('api-ape')
49
50
 
50
51
  const app = express()
51
52
  const server = app.listen(3000) // Get the HTTP server
@@ -116,6 +117,57 @@ api.onConnectionChange((state) => {
116
117
  * **HTTP streaming fallback** — Automatically falls back to long polling when WebSockets are blocked
117
118
  * **JJS Encoding** — Extended JSON supporting Date, RegExp, Error, Set, Map, undefined, and circular refs
118
119
  * **Connection lifecycle hooks** — Customize behavior on connect, receive, send, error, and disconnect
120
+ * **🌲 Forest** — Distributed mesh for horizontal scaling across multiple servers
121
+
122
+ ---
123
+
124
+ ## 🌲 Forest: Distributed Mesh
125
+
126
+ **Forest** enables horizontal scaling by coordinating multiple api-ape servers through a shared database. Messages are routed directly to the server hosting the destination client — no broadcast spam.
127
+
128
+ ```js
129
+ import { createClient } from 'redis';
130
+ const redis = createClient();
131
+ await redis.connect();
132
+
133
+ // Join the mesh — that's it!
134
+ ape.joinVia(redis);
135
+ ```
136
+
137
+ ### Supported Backends
138
+
139
+ | Backend | Push Mechanism | Best For |
140
+ |---------|---------------|----------|
141
+ | **Redis** | PUB/SUB | Most deployments |
142
+ | **MongoDB** | Change Streams | Mongo-native stacks |
143
+ | **PostgreSQL** | LISTEN/NOTIFY | SQL shops |
144
+ | **Supabase** | Realtime | Supabase users |
145
+ | **Firebase** | Native push | Serverless/edge |
146
+
147
+ ### How It Works
148
+
149
+ ```
150
+ ┌─────────────┐ ┌─────────────┐
151
+ │ Server A │ │ Server B │
152
+ │ client-1 │ │ client-2 │
153
+ └──────┬──────┘ └──────▲──────┘
154
+ │ │
155
+ │ 1. sendTo("client-2") │
156
+ │ → lookup: client-2 → srv-B │
157
+ │ │
158
+ │ 2. channels.push("srv-B", msg) │
159
+ └──────────┬───────────────────────┘
160
+
161
+ ┌──────▼──────┐
162
+ │ Database │
163
+ │ (message │
164
+ │ bus) │
165
+ └─────────────┘
166
+ ```
167
+
168
+ APE creates its own namespaced keys/tables (`ape:*` or `ape_*`). No schema conflicts with your data.
169
+
170
+ 👉 **[See detailed Forest documentation](server/README.md#forest-distributed-mesh)**
119
171
 
120
172
  ---
121
173
 
@@ -131,7 +183,7 @@ Initialize api-ape on a Node.js HTTP/HTTPS server.
131
183
  |--------|------|-------------|
132
184
  | `server` | `http.Server` | Node.js HTTP or HTTPS server instance |
133
185
  | `where` | `string` | Directory containing controller files (default: `'api'`) |
134
- | `onConnent` | `function` | Connection lifecycle hook (see [Connection Lifecycle](#connection-lifecycle)) |
186
+ | `onConnect` | `function` | Connection lifecycle hook (see [Connection Lifecycle](#connection-lifecycle)) |
135
187
 
136
188
  #### Controller Context (`this`)
137
189
 
@@ -141,14 +193,24 @@ Inside controller functions, `this` provides:
141
193
  |----------|-------------|
142
194
  | `this.broadcast(type, data)` | Send to **ALL** connected clients |
143
195
  | `this.broadcastOthers(type, data)` | Send to all **EXCEPT** the caller |
144
- | `this.online()` | Get count of connected clients |
145
- | `this.getClients()` | Get array of connected clientIds |
146
196
  | `this.clientId` | Unique ID of the calling client (generated by api-ape) |
147
197
  | `this.sessionId` | Session ID from cookie (set by outer framework, may be `null`) |
148
198
  | `this.req` | Original HTTP request |
149
199
  | `this.socket` | WebSocket instance |
150
200
  | `this.agent` | Parsed user-agent (browser, OS, device) |
151
201
 
202
+ #### `ape.clients`
203
+
204
+ A read-only Map of connected clients. Each client provides:
205
+
206
+ | Property | Description |
207
+ |----------|-------------|
208
+ | `clientId` | Unique client identifier |
209
+ | `sessionId` | Session ID from cookie (may be `null`) |
210
+ | `embed` | Embedded values from onConnect |
211
+ | `agent` | Parsed user-agent (browser, OS, device) |
212
+ | `sendTo(type, data)` | Send a message to this specific client |
213
+
152
214
  ### Client
153
215
 
154
216
  #### `api.<path>.<method>(...args)`
@@ -178,13 +240,12 @@ api.on('notification', ({ data, err, type }) => {
178
240
  })
179
241
  ```
180
242
 
181
- #### `api.getTransport()`
243
+ #### `api.transport`
182
244
 
183
- Get the currently active transport type.
245
+ Get the currently active transport type. This is a read-only property.
184
246
 
185
247
  ```js
186
- const transport = api.getTransport()
187
- console.log(transport) // 'websocket' | 'polling' | null
248
+ console.log(api.transport) // 'websocket' | 'polling' | null
188
249
  ```
189
250
 
190
251
  #### `api.onConnectionChange(handler)`
@@ -229,7 +290,7 @@ Customize behavior per connection:
229
290
  ```js
230
291
  ape(server, {
231
292
  where: 'api',
232
- onConnent(socket, req, clientId) {
293
+ onConnect(socket, req, clientId) {
233
294
  return {
234
295
  // Embed values into `this` for all controllers
235
296
  embed: {
@@ -249,7 +310,7 @@ ape(server, {
249
310
  },
250
311
 
251
312
  onError: (errStr) => console.error(errStr),
252
- onDisconnent: () => console.log('Client left')
313
+ onDisconnect: () => console.log('Client left')
253
314
  }
254
315
  }
255
316
  })
@@ -281,16 +342,27 @@ module.exports = function(announcement) {
281
342
  }
282
343
  ```
283
344
 
284
- ### Get Online Count
345
+ ### Using ape.clients
285
346
 
286
347
  ```js
287
- // api/stats.js
288
- module.exports = function() {
289
- return {
290
- online: this.online(),
291
- clients: this.getClients()
292
- }
348
+ const { ape } = require('api-ape')
349
+
350
+ // Get online count
351
+ console.log('Online:', ape.clients.size)
352
+
353
+ // Get all client IDs
354
+ const clientIds = Array.from(ape.clients.keys())
355
+
356
+ // Send to a specific client
357
+ const client = ape.clients.get(someClientId)
358
+ if (client) {
359
+ client.sendTo('notification', { message: 'Hello!' })
293
360
  }
361
+
362
+ // Iterate all clients
363
+ ape.clients.forEach((client, clientId) => {
364
+ console.log(`Client ${clientId}:`, client.sessionId, client.agent.browser.name)
365
+ })
294
366
  ```
295
367
 
296
368
  ### Access Request Data
@@ -454,7 +526,7 @@ This prevents malicious sites from making requests to your api-ape server while
454
526
  ### Security Considerations
455
527
 
456
528
  * Validate all input in controllers
457
- * Use authentication/authorization in `onConnent` hooks
529
+ * Use authentication/authorization in `onConnect` hooks
458
530
  * Sanitize data before broadcasting
459
531
  * Keep dependencies up to date
460
532
 
package/client/browser.js CHANGED
@@ -1,25 +1,25 @@
1
1
  import connectSocket from './connectSocket.js'
2
2
 
3
- const { sender, setOnReciver, onConnectionChange, getTransport } = connectSocket()
3
+ const client = connectSocket()
4
4
  connectSocket.autoReconnect()
5
5
 
6
6
  // Global API - use defineProperty to bypass Proxy interception
7
- window.api = sender
7
+ window.api = client.sender
8
8
  Object.defineProperty(window.api, 'on', {
9
- value: setOnReciver,
9
+ value: client.setOnReceiver,
10
10
  writable: false,
11
11
  enumerable: false,
12
12
  configurable: false
13
13
  })
14
14
  Object.defineProperty(window.api, 'onConnectionChange', {
15
- value: onConnectionChange,
15
+ value: client.onConnectionChange,
16
16
  writable: false,
17
17
  enumerable: false,
18
18
  configurable: false
19
19
  })
20
- Object.defineProperty(window.api, 'getTransport', {
21
- value: getTransport,
22
- writable: false,
20
+ // Read-only transport property - only ape can change this internally
21
+ Object.defineProperty(window.api, 'transport', {
22
+ get: () => client.transport,
23
23
  enumerable: false,
24
24
  configurable: false
25
25
  })
@@ -160,13 +160,13 @@ function getSocketUrl() {
160
160
  }
161
161
 
162
162
  let reconnect = false
163
- const connentTimeout = 5000
163
+ const connectTimeout = 5000
164
164
  const totalRequestTimeout = 10000
165
165
  //const location = window.location
166
166
 
167
167
  const joinKey = "/"
168
168
  // Properties accessed directly on `ape` that should NOT be intercepted
169
- const reservedKeys = new Set(['on', 'onConnectionChange', 'getTransport'])
169
+ const reservedKeys = new Set(['on', 'onConnectionChange', 'transport'])
170
170
  const handler = {
171
171
  get(fn, key) {
172
172
  // Skip proxy interception for reserved keys - return actual property
@@ -193,10 +193,9 @@ function wrap(api) {
193
193
 
194
194
  let __socket = false, ready = false, wsSend = false;
195
195
  const waitingOn = {};
196
- const reciverOn = [];
197
196
 
198
197
  let aWaitingSend = []
199
- const reciverOnAr = [];
198
+ const receiverArray = [];
200
199
  const ofTypesOb = {};
201
200
 
202
201
  /**
@@ -213,12 +212,23 @@ function switchToStreaming() {
213
212
  streamingTransport.onMessage = async (msg) => {
214
213
  const { err, type, data } = msg
215
214
 
215
+ // Process linked resources and shared files
216
+ let processedData = data
217
+ if (data && !err) {
218
+ try {
219
+ processedData = await fetchLinkedResources(data)
220
+ processedData = await fetchSharedFiles(processedData)
221
+ } catch (fetchErr) {
222
+ console.error(`🦍 Failed to hydrate streaming data:`, fetchErr)
223
+ }
224
+ }
225
+
216
226
  // Dispatch to type-specific handlers
217
227
  if (ofTypesOb[type]) {
218
- ofTypesOb[type].forEach(worker => worker({ err, type, data }))
228
+ ofTypesOb[type].forEach(worker => worker({ err, type, data: processedData }))
219
229
  }
220
230
  // Dispatch to general handlers
221
- reciverOnAr.forEach(worker => worker({ err, type, data }))
231
+ receiverArray.forEach(worker => worker({ err, type, data: processedData }))
222
232
  }
223
233
 
224
234
  streamingTransport.onOpen = () => {
@@ -227,11 +237,11 @@ function switchToStreaming() {
227
237
  console.log('🦍 HTTP streaming connected')
228
238
 
229
239
  // Flush waiting messages
230
- aWaitingSend.forEach(({ type, data, next, err, waiting, createdAt, timer }) => {
240
+ aWaitingSend.forEach(({ type, data, resolve, reject, waiting, createdAt, timer }) => {
231
241
  clearTimeout(timer)
232
242
  const resultPromise = streamingSend(type, data, createdAt)
233
243
  if (waiting) {
234
- resultPromise.then(next).catch(err)
244
+ resultPromise.then(resolve).catch(reject)
235
245
  }
236
246
  })
237
247
  aWaitingSend = []
@@ -319,11 +329,11 @@ function tryWebSocket(isRetry = false) {
319
329
  ready = true
320
330
  notifyConnectionChange(ConnectionState.Connected)
321
331
 
322
- aWaitingSend.forEach(({ type, data, next, err, waiting, createdAt, timer }) => {
332
+ aWaitingSend.forEach(({ type, data, resolve, reject, waiting, createdAt, timer }) => {
323
333
  clearTimeout(timer)
324
334
  const resultPromise = wsSend(type, data, createdAt)
325
335
  if (waiting) {
326
- resultPromise.then(next).catch(err)
336
+ resultPromise.then(resolve).catch(reject)
327
337
  }
328
338
  })
329
339
  aWaitingSend = []
@@ -338,7 +348,8 @@ function tryWebSocket(isRetry = false) {
338
348
  // Check for linked resources and fetch them before resolving
339
349
  if (data && !err) {
340
350
  try {
341
- const hydratedData = await fetchLinkedResources(data)
351
+ let hydratedData = await fetchLinkedResources(data)
352
+ hydratedData = await fetchSharedFiles(hydratedData)
342
353
  waitingOn[queryId](err, hydratedData)
343
354
  } catch (fetchErr) {
344
355
  waitingOn[queryId](fetchErr, null)
@@ -353,11 +364,12 @@ function tryWebSocket(isRetry = false) {
353
364
  return
354
365
  }
355
366
 
356
- // Only messages WITHOUT queryId go to setOnReciver
367
+ // Only messages WITHOUT queryId go to setOnReceiver
357
368
  let processedData = data
358
369
  if (data && !err) {
359
370
  try {
360
371
  processedData = await fetchLinkedResources(data)
372
+ processedData = await fetchSharedFiles(processedData)
361
373
  } catch (fetchErr) {
362
374
  console.error(`🦍 Failed to hydrate broadcast data:`, fetchErr)
363
375
  }
@@ -366,7 +378,7 @@ function tryWebSocket(isRetry = false) {
366
378
  if (ofTypesOb[type]) {
367
379
  ofTypesOb[type].forEach(worker => worker({ err, type, data: processedData }))
368
380
  }
369
- reciverOnAr.forEach(worker => worker({ err, type, data: processedData }))
381
+ receiverArray.forEach(worker => worker({ err, type, data: processedData }))
370
382
  }
371
383
 
372
384
  ws.onerror = function (err) {
@@ -429,6 +441,134 @@ function findLinkedResources(obj, path = '') {
429
441
  return resources
430
442
  }
431
443
 
444
+ /**
445
+ * Find all F-tagged (shared file) properties in data
446
+ * Returns array of { path, hash, originalKey }
447
+ */
448
+ function findFileTags(obj, path = '') {
449
+ const files = []
450
+
451
+ if (obj === null || obj === undefined || typeof obj !== 'object') {
452
+ return files
453
+ }
454
+
455
+ if (Array.isArray(obj)) {
456
+ for (let i = 0; i < obj.length; i++) {
457
+ files.push(...findFileTags(obj[i], path ? `${path}.${i}` : String(i)))
458
+ }
459
+ return files
460
+ }
461
+
462
+ for (const key of Object.keys(obj)) {
463
+ // Check for F-tag in key (client-to-client shared file marker)
464
+ if (key.endsWith('<!F>')) {
465
+ const cleanKey = key.slice(0, -4)
466
+ const hash = obj[key]
467
+ files.push({
468
+ path: path ? `${path}.${cleanKey}` : cleanKey,
469
+ hash,
470
+ originalKey: key
471
+ })
472
+ } else {
473
+ files.push(...findFileTags(obj[key], path ? `${path}.${key}` : key))
474
+ }
475
+ }
476
+
477
+ return files
478
+ }
479
+
480
+ /**
481
+ * Clean up F-tagged keys (rename key<!F> to key)
482
+ */
483
+ function cleanFileTags(obj) {
484
+ if (obj === null || obj === undefined || typeof obj !== 'object') {
485
+ return obj
486
+ }
487
+
488
+ if (Array.isArray(obj)) {
489
+ return obj.map(cleanFileTags)
490
+ }
491
+
492
+ const cleaned = {}
493
+ for (const key of Object.keys(obj)) {
494
+ if (key.endsWith('<!F>')) {
495
+ const cleanKey = key.slice(0, -4)
496
+ cleaned[cleanKey] = obj[key]
497
+ } else {
498
+ cleaned[key] = cleanFileTags(obj[key])
499
+ }
500
+ }
501
+ return cleaned
502
+ }
503
+
504
+ /**
505
+ * Fetch shared files (client-to-client transfers)
506
+ * Retries if upload is still in progress
507
+ */
508
+ async function fetchSharedFiles(data, maxRetries = 5) {
509
+ const files = findFileTags(data)
510
+
511
+ if (files.length === 0) {
512
+ return data
513
+ }
514
+
515
+ console.log(`🦍 Fetching ${files.length} shared file(s)`)
516
+
517
+ const cleanedData = cleanFileTags(data)
518
+
519
+ const hostname = window.location.hostname
520
+ const isLocal = ["localhost", "127.0.0.1", "[::1]"].includes(hostname)
521
+ const isHttps = window.location.protocol === "https:"
522
+ const port = isLocal ? 9010 : (window.location.port || (isHttps ? 443 : 80))
523
+ const protocol = isHttps ? "https" : "http"
524
+ const portSuffix = (isLocal || (port !== 80 && port !== 443)) ? `:${port}` : ""
525
+ const baseUrl = `${protocol}://${hostname}${portSuffix}`
526
+
527
+ await Promise.all(files.map(async ({ path, hash }) => {
528
+ let retries = 0
529
+ let backoff = 100 // Start with 100ms
530
+
531
+ while (retries < maxRetries) {
532
+ try {
533
+ const response = await fetch(`${baseUrl}/api/ape/data/${hash}`, {
534
+ credentials: 'include'
535
+ })
536
+
537
+ if (!response.ok) {
538
+ // 404 might mean file not uploaded yet, retry
539
+ if (response.status === 404 && retries < maxRetries - 1) {
540
+ retries++
541
+ await new Promise(r => setTimeout(r, backoff))
542
+ backoff *= 2 // Exponential backoff
543
+ continue
544
+ }
545
+ throw new Error(`Failed to fetch shared file: ${response.status}`)
546
+ }
547
+
548
+ const arrayBuffer = await response.arrayBuffer()
549
+ setValueAtPath(cleanedData, path, arrayBuffer)
550
+
551
+ // Check if upload is still in progress
552
+ const isComplete = response.headers.get('X-Ape-Complete') === '1'
553
+ if (!isComplete) {
554
+ console.log(`🦍 Shared file ${hash} still uploading (${response.headers.get('X-Ape-Total-Received') || '?'} bytes)`)
555
+ }
556
+ break
557
+ } catch (err) {
558
+ if (retries >= maxRetries - 1) {
559
+ console.error(`🦍 Failed to fetch shared file at ${path}:`, err)
560
+ setValueAtPath(cleanedData, path, null)
561
+ }
562
+ retries++
563
+ await new Promise(r => setTimeout(r, backoff))
564
+ backoff *= 2
565
+ }
566
+ }
567
+ }))
568
+
569
+ return cleanedData
570
+ }
571
+
432
572
  /**
433
573
  * Set a value at a nested path in an object
434
574
  */
@@ -667,6 +807,101 @@ function processBinaryForUpload(data, path = '') {
667
807
  return { processedData: data, uploads: [] }
668
808
  }
669
809
 
810
+ /**
811
+ * Find and extract binary data for SHARING (client-to-client)
812
+ * Uses <!F> tag instead of <!A>/<!B>
813
+ * Returns { processedData, shares: [{ path, hash, data }] }
814
+ */
815
+ function processBinaryForSharing(data, path = '') {
816
+ if (data === null || data === undefined) {
817
+ return { processedData: data, shares: [] }
818
+ }
819
+
820
+ if (isBinaryData(data)) {
821
+ const hash = generateUploadHash(path || 'share')
822
+ return {
823
+ processedData: { [`__ape_share__`]: hash },
824
+ shares: [{ path, hash, data }]
825
+ }
826
+ }
827
+
828
+ if (Array.isArray(data)) {
829
+ const processedArray = []
830
+ const allShares = []
831
+
832
+ for (let i = 0; i < data.length; i++) {
833
+ const itemPath = path ? `${path}.${i}` : String(i)
834
+ const { processedData, shares } = processBinaryForSharing(data[i], itemPath)
835
+ processedArray.push(processedData)
836
+ allShares.push(...shares)
837
+ }
838
+
839
+ return { processedData: processedArray, shares: allShares }
840
+ }
841
+
842
+ if (typeof data === 'object') {
843
+ const processedObj = {}
844
+ const allShares = []
845
+
846
+ for (const key of Object.keys(data)) {
847
+ const itemPath = path ? `${path}.${key}` : key
848
+ const { processedData, shares } = processBinaryForSharing(data[key], itemPath)
849
+
850
+ // If this was binary data, mark the key with <!F> tag
851
+ if (shares.length > 0 && processedData?.__ape_share__) {
852
+ processedObj[`${key}<!F>`] = processedData.__ape_share__
853
+ } else {
854
+ processedObj[key] = processedData
855
+ }
856
+ allShares.push(...shares)
857
+ }
858
+
859
+ return { processedData: processedObj, shares: allShares }
860
+ }
861
+
862
+ return { processedData: data, shares: [] }
863
+ }
864
+
865
+ /**
866
+ * Upload shared files via HTTP PUT
867
+ * Uses different endpoint pattern for streaming files
868
+ */
869
+ async function uploadSharedFiles(shares) {
870
+ if (shares.length === 0) return
871
+
872
+ // Build base URL
873
+ const hostname = window.location.hostname
874
+ const isLocal = ["localhost", "127.0.0.1", "[::1]"].includes(hostname)
875
+ const isHttps = window.location.protocol === "https:"
876
+ const port = isLocal ? 9010 : (window.location.port || (isHttps ? 443 : 80))
877
+ const protocol = isHttps ? "https" : "http"
878
+ const portSuffix = (isLocal || (port !== 80 && port !== 443)) ? `:${port}` : ""
879
+ const baseUrl = `${protocol}://${hostname}${portSuffix}`
880
+
881
+ console.log(`🦍 Uploading ${shares.length} shared file(s)`)
882
+
883
+ await Promise.all(shares.map(async ({ hash, data }) => {
884
+ try {
885
+ // For shared files, use upload pattern with hash as both queryId and pathHash
886
+ const response = await fetch(`${baseUrl}/api/ape/data/_share/${hash}`, {
887
+ method: 'PUT',
888
+ credentials: 'include',
889
+ headers: {
890
+ 'Content-Type': 'application/octet-stream'
891
+ },
892
+ body: data
893
+ })
894
+
895
+ if (!response.ok) {
896
+ throw new Error(`Shared upload failed: ${response.status}`)
897
+ }
898
+ } catch (err) {
899
+ console.error(`🦍 Failed to upload shared file ${hash}:`, err)
900
+ throw err
901
+ }
902
+ }))
903
+ }
904
+
670
905
  /**
671
906
  * Upload binary data via HTTP PUT
672
907
  */
@@ -779,19 +1014,19 @@ const sender = (type, data) => {
779
1014
  return wsSend(type, data, createdAt, true)
780
1015
  }
781
1016
 
782
- const timeLetForReqToBeMade = (createdAt + connentTimeout) - Date.now() // 5sec for reconnent
1017
+ const timeLetForReqToBeMade = (createdAt + connectTimeout) - Date.now() // 5sec for reconnect
783
1018
 
784
1019
  const timer = setTimeout(() => {
785
1020
  const errMessage = "Request not sent for :" + type
786
1021
  if (payload.waiting) {
787
- payload.err(new Error(errMessage))
1022
+ payload.reject(new Error(errMessage))
788
1023
  } else {
789
1024
  throw new Error(errMessage)
790
1025
  }
791
1026
  }, timeLetForReqToBeMade);
792
1027
 
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; })
1028
+ const payload = { type, data, resolve: undefined, reject: undefined, waiting: false, createdAt, timer };
1029
+ const waitingOnOpen = new Promise((res, rej) => { payload.resolve = res; payload.reject = rej; })
795
1030
 
796
1031
  const waitingOnOpenThen = waitingOnOpen.then;
797
1032
  const waitingOnOpenCatch = waitingOnOpen.catch;
@@ -822,14 +1057,14 @@ const sender = (type, data) => {
822
1057
  function buildClientInterface() {
823
1058
  return {
824
1059
  sender: wrap(sender),
825
- setOnReciver: (onTypeStFn, handlerFn) => {
1060
+ setOnReceiver: (onTypeStFn, handlerFn) => {
826
1061
  if ("string" === typeof onTypeStFn) {
827
1062
  // Replace handler for this type (prevents duplicates in React StrictMode)
828
1063
  ofTypesOb[onTypeStFn] = [handlerFn]
829
1064
  } else {
830
1065
  // For general receivers, prevent duplicates by checking
831
- if (!reciverOnAr.includes(onTypeStFn)) {
832
- reciverOnAr.push(onTypeStFn)
1066
+ if (!receiverArray.includes(onTypeStFn)) {
1067
+ receiverArray.push(onTypeStFn)
833
1068
  }
834
1069
  }
835
1070
  },
@@ -843,8 +1078,8 @@ function buildClientInterface() {
843
1078
  if (idx > -1) connectionChangeListeners.splice(idx, 1)
844
1079
  }
845
1080
  },
846
- // Expose current transport type
847
- getTransport: () => currentTransport
1081
+ // Expose current transport type (read-only)
1082
+ get transport() { return currentTransport }
848
1083
  }
849
1084
  }
850
1085
 
package/client/index.js CHANGED
@@ -55,7 +55,7 @@ function getClient() {
55
55
 
56
56
  // Flush buffered receivers
57
57
  bufferedReceivers.forEach(({ type, handler }) => {
58
- client.setOnReciver(type, handler)
58
+ client.setOnReceiver(type, handler)
59
59
  })
60
60
  bufferedReceivers.length = 0
61
61
 
@@ -88,7 +88,7 @@ const senderProxy = new Proxy({}, {
88
88
  // Reserved properties
89
89
  if (prop === 'on') return on
90
90
  if (prop === 'onConnectionChange') return onConnectionChange
91
- if (prop === 'getTransport') return () => resolvedClient?.getTransport?.() || null
91
+ if (prop === 'transport') return resolvedClient?.transport || null
92
92
  if (prop === 'then' || prop === 'catch') return undefined // Not a Promise
93
93
 
94
94
  // Return a function that either calls directly or buffers
@@ -115,7 +115,7 @@ const senderProxy = new Proxy({}, {
115
115
  */
116
116
  function on(type, handler) {
117
117
  if (resolvedClient) {
118
- resolvedClient.setOnReciver(type, handler)
118
+ resolvedClient.setOnReceiver(type, handler)
119
119
  } else {
120
120
  bufferedReceivers.push({ type, handler })
121
121
  getClient()