webview.io 1.0.1 → 1.0.3

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
@@ -5,13 +5,15 @@ Easy and friendly API to connect and interact between React Native applications
5
5
  ## Features
6
6
 
7
7
  - **Bidirectional Communication**: Seamless messaging between React Native and WebView
8
- - **Enhanced Security**: Origin validation, message sanitization, and payload size limits
8
+ - **Robust Connection Handshake**: Three-way handshake with token validation for reliable connections
9
+ - **Enhanced Security**: Origin validation, message sanitization, token-based authentication, and payload size limits
9
10
  - **Auto-Reconnection**: Automatic reconnection with exponential backoff strategy
11
+ - **Connection Timeout & Retries**: Configurable timeout with periodic connection attempts
10
12
  - **Heartbeat Monitoring**: Connection health monitoring with configurable intervals
11
13
  - **Message Queuing**: Queue messages when disconnected and replay on reconnection
12
14
  - **Rate Limiting**: Configurable message rate limiting to prevent spam
13
15
  - **Promise Support**: Modern async/await APIs with timeout handling
14
- - **Connection Statistics**: Real-time connection and performance metrics
16
+ - **Connection Statistics**: Real-time connection and performance metrics including readiness state
15
17
  - **Comprehensive Error Handling**: Detailed error types and handling mechanisms
16
18
 
17
19
  ## Installation
@@ -67,6 +69,11 @@ function MapComponent() {
67
69
  wioRef.current.emit('hello', { message: 'Hello from React Native!' })
68
70
  })
69
71
 
72
+ // Handle connection timeout
73
+ wioRef.current.on('connect_timeout', ({ attempts }) => {
74
+ console.log(`Connection failed after ${attempts} attempts`)
75
+ })
76
+
70
77
  // Listen for messages
71
78
  wioRef.current.on('response', (data) => {
72
79
  console.log('Received:', data)
@@ -128,6 +135,8 @@ const wio = new WIO({
128
135
  debug: false, // Enable debug logging
129
136
  heartbeatInterval: 30000, // Heartbeat interval in ms (30s)
130
137
  connectionTimeout: 10000, // Connection timeout in ms (10s)
138
+ connectionPingInterval: 2000, // Ping interval during connection (2s)
139
+ maxConnectionAttempts: 5, // Max connection attempts before timeout
131
140
  maxMessageSize: 1024 * 1024, // Max message size in bytes (1MB)
132
141
  maxMessagesPerSecond: 100, // Rate limit (100 messages/second)
133
142
  autoReconnect: true, // Enable automatic reconnection
@@ -135,6 +144,48 @@ const wio = new WIO({
135
144
  })
136
145
  ```
137
146
 
147
+ ## Connection Architecture
148
+
149
+ ### Three-Way Handshake
150
+
151
+ webview.io uses a robust three-way handshake protocol with token validation to ensure reliable connections:
152
+
153
+ ```
154
+ 1. WEBVIEW → ping (with token) → EMBEDDED
155
+ - WEBVIEW initiates connection with unique token
156
+ - Sends periodic pings until acknowledged
157
+
158
+ 2. WEBVIEW ← pong (with token) ← EMBEDDED
159
+ - EMBEDDED responds with same token
160
+ - EMBEDDED announces readiness periodically
161
+
162
+ 3. WEBVIEW → __connection_ack (with token) → EMBEDDED
163
+ - WEBVIEW confirms receipt of pong
164
+ - Both sides now confirmed connected
165
+ ```
166
+
167
+ This prevents:
168
+ - Race conditions during initialization
169
+ - Accepting connections from wrong peers
170
+ - Silent connection failures
171
+ - Message loss during handshake
172
+
173
+ ### Connection Flow Details
174
+
175
+ **WEBVIEW Side:**
176
+ - Sends initial `ping` with unique connection token
177
+ - Retries every 2 seconds (configurable via `connectionPingInterval`)
178
+ - Times out after 10 seconds (configurable via `connectionTimeout`)
179
+ - Maximum 5 attempts (configurable via `maxConnectionAttempts`)
180
+ - Fires `connect_timeout` event if all attempts fail
181
+
182
+ **EMBEDDED Side:**
183
+ - Automatically announces `__embedded_ready` when loaded
184
+ - Retries announcement every 2 seconds until connected
185
+ - Handles race condition if loaded before React Native
186
+ - Responds to `ping` with `pong` containing same token
187
+ - Confirms connection upon receiving `__connection_ack`
188
+
138
189
  ## Async/Await Support
139
190
 
140
191
  ### Send Messages with Acknowledgments
@@ -177,6 +228,21 @@ console.log('User data received:', userData)
177
228
 
178
229
  ## Enhanced Connection Management
179
230
 
231
+ ### Connection Timeout Handling
232
+
233
+ ```javascript
234
+ // Handle connection timeout (new event)
235
+ wio.on('connect_timeout', ({ attempts }) => {
236
+ console.log(`Failed to connect after ${attempts} attempts`)
237
+ // Optionally retry manually or show error to user
238
+ })
239
+
240
+ // With auto-reconnect enabled, timeout triggers reconnection
241
+ wio.on('reconnecting', ({ attempt, delay }) => {
242
+ console.log(`Reconnection attempt ${attempt}, waiting ${delay}ms`)
243
+ })
244
+ ```
245
+
180
246
  ### Auto-Reconnection
181
247
 
182
248
  ```javascript
@@ -201,11 +267,13 @@ const stats = wio.getStats()
201
267
  console.log(stats)
202
268
  // {
203
269
  // connected: true,
270
+ // embeddedReady: true, // NEW: EMBEDDED peer readiness state
204
271
  // peerType: 'WEBVIEW',
205
272
  // origin: 'https://example.com',
206
273
  // lastHeartbeat: 1609459200000,
207
274
  // queuedMessages: 0,
208
275
  // reconnectAttempts: 0,
276
+ // connectionAttempts: 0, // NEW: Current connection attempts
209
277
  // activeListeners: 5,
210
278
  // messageRate: 2
211
279
  // }
@@ -213,18 +281,24 @@ console.log(stats)
213
281
 
214
282
  ## Security Features
215
283
 
284
+ ### Token-Based Authentication
285
+
286
+ Each connection uses a unique token for validation:
287
+
288
+ ```javascript
289
+ // Automatically generated and validated during handshake
290
+ // Prevents accepting messages from previous connections
291
+ // Tokens are only used for connection-related events
292
+ ```
293
+
216
294
  ### Origin Validation
217
295
 
218
296
  ```javascript
219
- // Strict origin checking (WebView side)
220
- window._wio.listen('react-native') // Only accept from React Native
297
+ // Strict origin checking (React Native side)
298
+ wio.initiate(webViewRef, 'https://trusted-domain.com')
221
299
 
222
- // Error handling for invalid origins (React Native side)
223
- wio.on('error', (error) => {
224
- if (error.type === 'INVALID_ORIGIN') {
225
- console.log(`Rejected message from ${error.received}`)
226
- }
227
- })
300
+ // Messages from other origins are automatically rejected
301
+ // No additional configuration needed
228
302
  ```
229
303
 
230
304
  ### Message Sanitization
@@ -257,17 +331,8 @@ wio.on('error', (error) => {
257
331
  ```javascript
258
332
  wio.on('error', (error) => {
259
333
  switch (error.type) {
260
- case 'INVALID_ORIGIN':
261
- console.error(`Invalid origin: expected ${error.expected}, got ${error.received}`)
262
- break
263
- case 'ORIGIN_MISMATCH':
264
- console.error(`Origin mismatch: expected ${error.expected}, got ${error.received}`)
265
- break
266
- case 'RATE_LIMIT_EXCEEDED':
267
- console.warn(`Rate limit exceeded: ${error.current}/${error.limit} messages/second`)
268
- break
269
334
  case 'MESSAGE_HANDLING_ERROR':
270
- console.error(`Error handling event ${error.event}: ${error.error}`)
335
+ console.error(`Error handling message: ${error.error}`)
271
336
  break
272
337
  case 'EMIT_ERROR':
273
338
  console.error(`Error sending event ${error.event}: ${error.error}`)
@@ -275,6 +340,9 @@ wio.on('error', (error) => {
275
340
  case 'LISTENER_ERROR':
276
341
  console.error(`Error in listener for ${error.event}: ${error.error}`)
277
342
  break
343
+ case 'RATE_LIMIT_EXCEEDED':
344
+ console.warn(`Rate limit exceeded: ${error.current}/${error.limit} messages/second`)
345
+ break
278
346
  case 'NO_CONNECTION':
279
347
  console.error(`Attempted to send ${error.event} without connection`)
280
348
  break
@@ -308,15 +376,15 @@ console.log(`${stats.queuedMessages} messages queued`)
308
376
  - **`onceAsync(event)`** - Wait for single event (Promise)
309
377
 
310
378
  #### Utility Methods
311
- - **`getStats()`** - Get connection statistics
379
+ - **`getStats()`** - Get connection statistics including readiness states
312
380
  - **`clearQueue()`** - Clear queued messages
313
381
  - **`getInjectedJavaScript()`** - Get JavaScript to inject into WebView
314
382
 
315
383
  #### Connection Methods
316
- - **`initiate(webViewRef, origin)`** - Establish connection (WEBVIEW peer only)
317
- - **`listen(hostOrigin?)`** - Listen for connection (EMBEDDED peer only)
384
+ - **`initiate(webViewRef, origin)`** - Establish connection with retry logic (WEBVIEW peer only)
385
+ - **`listen(hostOrigin?)`** - Listen for connection with readiness announcement (EMBEDDED peer only)
318
386
  - **`handleMessage(event)`** - Handle incoming message from WebView
319
- - **`disconnect(callback?)`** - Disconnect and cleanup
387
+ - **`disconnect(callback?)`** - Disconnect and cleanup all resources
320
388
  - **`isConnected()`** - Check connection status
321
389
 
322
390
  #### Messaging Methods
@@ -329,11 +397,18 @@ console.log(`${stats.queuedMessages} messages queued`)
329
397
  ### Events
330
398
 
331
399
  #### Connection Events
332
- - **`connect`** - Connection established
400
+ - **`connect`** - Connection established (after 3-way handshake)
333
401
  - **`disconnect`** - Connection lost with reason
402
+ - **`connect_timeout`** - Initial connection failed after all attempts (NEW)
334
403
  - **`reconnecting`** - Reconnection attempt started
335
404
  - **`reconnection_failed`** - All reconnection attempts failed
336
405
 
406
+ #### Internal Events (handled automatically)
407
+ - **`__embedded_ready`** - EMBEDDED peer announces readiness
408
+ - **`__connection_ack`** - Final acknowledgment in 3-way handshake
409
+ - **`__heartbeat`** - Connection health check
410
+ - **`__heartbeat_response`** - Heartbeat response
411
+
337
412
  #### Error Events
338
413
  - **`error`** - Various error conditions with detailed error objects
339
414
 
@@ -341,7 +416,7 @@ console.log(`${stats.queuedMessages} messages queued`)
341
416
 
342
417
  ```javascript
343
418
  import React, { useRef, useEffect, useState } from 'react'
344
- import { View, TouchableOpacity, Text, StyleSheet } from 'react-native'
419
+ import { View, TouchableOpacity, Text, StyleSheet, ActivityIndicator } from 'react-native'
345
420
  import { WebView } from 'react-native-webview'
346
421
  import WIO from 'webview.io'
347
422
 
@@ -349,24 +424,41 @@ function App() {
349
424
  const webViewRef = useRef(null)
350
425
  const wioRef = useRef(null)
351
426
  const [isConnected, setIsConnected] = useState(false)
427
+ const [isConnecting, setIsConnecting] = useState(false)
428
+ const [connectionAttempts, setConnectionAttempts] = useState(0)
352
429
 
353
430
  useEffect(() => {
354
431
  wioRef.current = new WIO({
355
432
  type: 'WEBVIEW',
356
- debug: true
433
+ debug: true,
434
+ connectionTimeout: 10000,
435
+ connectionPingInterval: 2000,
436
+ maxConnectionAttempts: 5
357
437
  })
358
438
 
359
439
  wioRef.current.initiate(webViewRef, 'https://your-app.com')
440
+ setIsConnecting(true)
360
441
 
361
442
  wioRef.current
362
443
  .on('connect', () => {
363
444
  console.log('Connected!')
364
445
  setIsConnected(true)
446
+ setIsConnecting(false)
447
+ setConnectionAttempts(0)
365
448
  })
366
- .on('disconnect', () => {
367
- console.log('Disconnected!')
449
+ .on('disconnect', ({ reason }) => {
450
+ console.log('Disconnected:', reason)
368
451
  setIsConnected(false)
369
452
  })
453
+ .on('connect_timeout', ({ attempts }) => {
454
+ console.log(`Connection timeout after ${attempts} attempts`)
455
+ setIsConnecting(false)
456
+ setConnectionAttempts(attempts)
457
+ })
458
+ .on('reconnecting', ({ attempt, delay }) => {
459
+ console.log(`Reconnecting (${attempt})...`)
460
+ setIsConnecting(true)
461
+ })
370
462
  .on('location:picked', (location) => {
371
463
  console.log('User picked location:', location)
372
464
  })
@@ -378,13 +470,19 @@ function App() {
378
470
 
379
471
  const getLocation = async () => {
380
472
  try {
381
- const location = await wioRef.current.emitAsync('get:location')
473
+ const location = await wioRef.current.emitAsync('get:location', null, 5000)
382
474
  console.log('Got location:', location)
383
475
  } catch (error) {
384
476
  console.error('Failed to get location:', error)
385
477
  }
386
478
  }
387
479
 
480
+ const retry = () => {
481
+ setConnectionAttempts(0)
482
+ setIsConnecting(true)
483
+ wioRef.current.initiate(webViewRef, 'https://your-app.com')
484
+ }
485
+
388
486
  return (
389
487
  <View style={styles.container}>
390
488
  <WebView
@@ -396,10 +494,29 @@ function App() {
396
494
  />
397
495
 
398
496
  <View style={styles.controls}>
399
- <Text>Status: {isConnected ? 'Connected' : 'Disconnected'}</Text>
400
- <TouchableOpacity onPress={getLocation}>
401
- <Text>Get Location</Text>
402
- </TouchableOpacity>
497
+ <View style={styles.status}>
498
+ {isConnecting && <ActivityIndicator />}
499
+ <Text>
500
+ Status: {isConnected ? 'Connected' : isConnecting ? 'Connecting...' : 'Disconnected'}
501
+ </Text>
502
+ {connectionAttempts > 0 && (
503
+ <Text style={styles.error}>
504
+ Failed after {connectionAttempts} attempts
505
+ </Text>
506
+ )}
507
+ </View>
508
+
509
+ {isConnected && (
510
+ <TouchableOpacity style={styles.button} onPress={getLocation}>
511
+ <Text>Get Location</Text>
512
+ </TouchableOpacity>
513
+ )}
514
+
515
+ {!isConnected && !isConnecting && (
516
+ <TouchableOpacity style={styles.button} onPress={retry}>
517
+ <Text>Retry Connection</Text>
518
+ </TouchableOpacity>
519
+ )}
403
520
  </View>
404
521
  </View>
405
522
  )
@@ -407,7 +524,10 @@ function App() {
407
524
 
408
525
  const styles = StyleSheet.create({
409
526
  container: { flex: 1 },
410
- controls: { padding: 16 }
527
+ controls: { padding: 16 },
528
+ status: { flexDirection: 'row', alignItems: 'center', gap: 8, marginBottom: 12 },
529
+ error: { color: 'red', marginTop: 4 },
530
+ button: { padding: 12, backgroundColor: '#007AFF', borderRadius: 8, alignItems: 'center' }
411
531
  })
412
532
  ```
413
533
 
@@ -422,6 +542,9 @@ const options: Options = {
422
542
  type: 'WEBVIEW',
423
543
  debug: true,
424
544
  heartbeatInterval: 30000,
545
+ connectionTimeout: 10000,
546
+ connectionPingInterval: 2000,
547
+ maxConnectionAttempts: 5,
425
548
  maxMessageSize: 512 * 1024
426
549
  }
427
550
 
@@ -449,8 +572,6 @@ const response = await wio.emitAsync<{ query: string }, ApiResponse>(
449
572
 
450
573
  | Error Type | Description |
451
574
  |------------|-------------|
452
- | `INVALID_ORIGIN` | Message from unexpected origin |
453
- | `ORIGIN_MISMATCH` | Origin changed during session |
454
575
  | `MESSAGE_HANDLING_ERROR` | Error processing incoming message |
455
576
  | `EMIT_ERROR` | Error sending message |
456
577
  | `LISTENER_ERROR` | Error in event listener |
@@ -469,8 +590,55 @@ const response = await wio.emitAsync<{ query: string }, ApiResponse>(
469
590
  - **Rate Limiting**: Respect the `maxMessagesPerSecond` limit (default 100/sec)
470
591
  - **Queue Size**: Monitor queued messages to avoid memory issues
471
592
  - **Heartbeat**: Adjust `heartbeatInterval` based on your reliability needs
593
+ - **Connection Timeout**: Configure `connectionTimeout` and `maxConnectionAttempts` based on network conditions
472
594
  - **Battery Impact**: Consider disabling heartbeat or increasing interval for battery-sensitive applications
473
595
 
596
+ ## Troubleshooting
597
+
598
+ ### Connection Never Establishes
599
+
600
+ **Symptoms:** `connect_timeout` event fires, connection never succeeds
601
+
602
+ **Solutions:**
603
+ 1. Check that `injectedJavaScript` is properly set on WebView
604
+ 2. Verify `javaScriptEnabled={true}` is set
605
+ 3. Check browser console for JavaScript errors in WebView
606
+ 4. Ensure origin matches exactly (including protocol)
607
+ 5. Increase `connectionTimeout` for slow networks
608
+ 6. Check that WebView content loads successfully
609
+
610
+ ### Messages Not Received
611
+
612
+ **Symptoms:** `emit()` called but listener never fires
613
+
614
+ **Solutions:**
615
+ 1. Verify connection is established (`isConnected()` returns true)
616
+ 2. Check that listener is registered before message is sent
617
+ 3. Look for errors in `error` event listener
618
+ 4. Check if rate limiting is being exceeded
619
+ 5. Verify message size is under `maxMessageSize`
620
+
621
+ ### Frequent Disconnections
622
+
623
+ **Symptoms:** Constant `disconnect`/`reconnect` cycle
624
+
625
+ **Solutions:**
626
+ 1. Increase `heartbeatInterval` to reduce network overhead
627
+ 2. Check network stability
628
+ 3. Look for memory issues or crashes in WebView
629
+ 4. Verify no conflicting JavaScript in WebView
630
+ 5. Check React Native debugger for errors
631
+
632
+ ### Race Conditions on Load
633
+
634
+ **Symptoms:** Sometimes connects, sometimes doesn't
635
+
636
+ **Solutions:**
637
+ - Already handled! The three-way handshake with readiness announcements prevents this
638
+ - EMBEDDED announces readiness periodically until connected
639
+ - WEBVIEW retries connection attempts automatically
640
+ - No action needed from your side
641
+
474
642
  ## Differences from iframe.io
475
643
 
476
644
  `webview.io` is adapted specifically for React Native and differs from `iframe.io` in the following ways:
@@ -480,6 +648,9 @@ const response = await wio.emitAsync<{ query: string }, ApiResponse>(
480
648
  - **Message Handling**: Requires explicit `handleMessage()` call in `onMessage` prop
481
649
  - **Injected Script**: Uses `getInjectedJavaScript()` to setup bridge in WebView
482
650
  - **No DOM Dependencies**: Works in React Native environment without DOM APIs
651
+ - **Enhanced Handshake**: Three-way handshake with token validation for mobile reliability
652
+ - **Connection Retry**: Built-in retry logic for spotty mobile connections
653
+ - **Readiness Protocol**: Handles race conditions common in mobile WebView loading
483
654
 
484
655
  ## License
485
656
 
package/dist/index.d.ts CHANGED
@@ -12,6 +12,8 @@ export type Options = {
12
12
  maxMessagesPerSecond?: number;
13
13
  autoReconnect?: boolean;
14
14
  messageQueueSize?: number;
15
+ connectionPingInterval?: number;
16
+ maxConnectionAttempts?: number;
15
17
  };
16
18
  export interface RegisteredEvents {
17
19
  [index: string]: Listener[];
@@ -22,6 +24,7 @@ export type Peer = {
22
24
  origin?: string;
23
25
  connected?: boolean;
24
26
  lastHeartbeat?: number;
27
+ embeddedReady?: boolean;
25
28
  };
26
29
  export type MessageData = {
27
30
  _event: string;
@@ -29,6 +32,7 @@ export type MessageData = {
29
32
  cid: string | undefined;
30
33
  timestamp?: number;
31
34
  size?: number;
35
+ token?: string;
32
36
  };
33
37
  export type Message = {
34
38
  data: MessageData;
@@ -45,10 +49,15 @@ export default class WIO {
45
49
  options: Options;
46
50
  private heartbeatTimer?;
47
51
  private reconnectTimer?;
52
+ private connectionAttemptTimer?;
53
+ private connectionPingInterval?;
54
+ private embeddedReadyCheckInterval?;
48
55
  private messageQueue;
49
56
  private messageRateTracker;
50
57
  private reconnectAttempts;
51
58
  private maxReconnectAttempts;
59
+ private connectionToken?;
60
+ private connectionAttempts;
52
61
  constructor(options?: Options);
53
62
  debug(...args: any[]): void;
54
63
  isConnected(): boolean;
@@ -56,6 +65,10 @@ export default class WIO {
56
65
  private stopHeartbeat;
57
66
  private handleConnectionLoss;
58
67
  private attemptReconnection;
68
+ private startConnectionAttempt;
69
+ private stopConnectionAttempt;
70
+ private announceEmbeddedReady;
71
+ private stopEmbeddedReadyAnnouncement;
59
72
  private checkRateLimit;
60
73
  private queueMessage;
61
74
  private processMessageQueue;
@@ -65,12 +78,10 @@ export default class WIO {
65
78
  initiate(webViewRef: RefObject<WebView>, origin: string): this;
66
79
  /**
67
80
  * Listening to connection from the WebView host
68
- * Note: In React Native context, this is handled by injected JavaScript
69
81
  */
70
82
  listen(hostOrigin?: string): this;
71
83
  /**
72
84
  * Handle incoming message from WebView
73
- * Called by React Native component via onMessage prop
74
85
  */
75
86
  handleMessage(event: {
76
87
  nativeEvent: {
@@ -90,11 +101,13 @@ export default class WIO {
90
101
  disconnect(fn?: () => void): this;
91
102
  getStats(): {
92
103
  connected: boolean;
104
+ embeddedReady: boolean | undefined;
93
105
  peerType: PeerType;
94
106
  origin: string | undefined;
95
107
  lastHeartbeat: number | undefined;
96
108
  queuedMessages: number;
97
109
  reconnectAttempts: number;
110
+ connectionAttempts: number;
98
111
  activeListeners: number;
99
112
  messageRate: number;
100
113
  };