webview.io 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -15,6 +15,8 @@ export type Options = {
15
15
  maxMessagesPerSecond?: number
16
16
  autoReconnect?: boolean
17
17
  messageQueueSize?: number
18
+ connectionPingInterval?: number
19
+ maxConnectionAttempts?: number
18
20
  }
19
21
 
20
22
  export interface RegisteredEvents {
@@ -27,6 +29,7 @@ export type Peer = {
27
29
  origin?: string
28
30
  connected?: boolean
29
31
  lastHeartbeat?: number
32
+ embeddedReady?: boolean
30
33
  }
31
34
 
32
35
  export type MessageData = {
@@ -35,6 +38,7 @@ export type MessageData = {
35
38
  cid: string | undefined
36
39
  timestamp?: number
37
40
  size?: number
41
+ token?: string
38
42
  }
39
43
 
40
44
  export type Message = {
@@ -78,11 +82,18 @@ const ackId = () => {
78
82
  return `${timestamp}_${random}`
79
83
  }
80
84
 
85
+ const generateToken = () => {
86
+ return `${Date.now()}_${Math.random().toString(36).substring(2, 15)}`
87
+ }
88
+
81
89
  const RESERVED_EVENTS = [
82
90
  'ping',
83
91
  'pong',
84
92
  '__heartbeat',
85
- '__heartbeat_response'
93
+ '__heartbeat_response',
94
+ '__embedded_ready',
95
+ '__connection_ack',
96
+ '__webview_ready'
86
97
  ]
87
98
 
88
99
  export default class WIO {
@@ -91,10 +102,15 @@ export default class WIO {
91
102
  options: Options
92
103
  private heartbeatTimer?: NodeJS.Timeout
93
104
  private reconnectTimer?: NodeJS.Timeout
105
+ private connectionAttemptTimer?: NodeJS.Timeout
106
+ private connectionPingInterval?: NodeJS.Timeout
107
+ private embeddedReadyCheckInterval?: NodeJS.Timeout
94
108
  private messageQueue: QueuedMessage[] = []
95
109
  private messageRateTracker: number[] = []
96
110
  private reconnectAttempts: number = 0
97
111
  private maxReconnectAttempts: number = 5
112
+ private connectionToken?: string
113
+ private connectionAttempts: number = 0
98
114
 
99
115
  constructor( options: Options = {} ){
100
116
  if( options && typeof options !== 'object' )
@@ -108,10 +124,12 @@ export default class WIO {
108
124
  maxMessagesPerSecond: 100,
109
125
  autoReconnect: true,
110
126
  messageQueueSize: 50,
127
+ connectionPingInterval: 2000, // 2 seconds
128
+ maxConnectionAttempts: 5,
111
129
  ...options
112
130
  }
113
131
  this.Events = {}
114
- this.peer = { type: 'WEBVIEW', connected: false }
132
+ this.peer = { type: 'WEBVIEW', connected: false, embeddedReady: false }
115
133
 
116
134
  if( options.type )
117
135
  this.peer.type = options.type
@@ -164,7 +182,9 @@ export default class WIO {
164
182
  if( !this.peer.connected ) return
165
183
 
166
184
  this.peer.connected = false
185
+ this.peer.embeddedReady = false
167
186
  this.stopHeartbeat()
187
+ this.stopConnectionAttempt()
168
188
  this.fire('disconnect', { reason: 'CONNECTION_LOST' })
169
189
 
170
190
  this.options.autoReconnect
@@ -184,11 +204,21 @@ export default class WIO {
184
204
  this.reconnectTimer = setTimeout(() => {
185
205
  this.reconnectTimer = undefined
186
206
 
207
+ // Reset connection state
208
+ this.peer.connected = false
209
+ this.peer.embeddedReady = false
210
+ this.connectionAttempts = 0
211
+ this.connectionToken = generateToken()
212
+
187
213
  // Re-initiate connection for WEBVIEW type
188
- this.peer.type === 'WEBVIEW'
189
- && this.emit('ping')
214
+ if( this.peer.type === 'WEBVIEW' ){
215
+ this.startConnectionAttempt()
216
+ }
190
217
 
191
- // For EMBEDDED type, just wait for incoming connection
218
+ // For EMBEDDED type, announce readiness
219
+ if( this.peer.type === 'EMBEDDED' ){
220
+ this.announceEmbeddedReady()
221
+ }
192
222
 
193
223
  // Set timeout for this reconnection attempt
194
224
  setTimeout(() => {
@@ -201,6 +231,98 @@ export default class WIO {
201
231
  }, delay)
202
232
  }
203
233
 
234
+ // Start connection attempt with timeout and retries
235
+ private startConnectionAttempt(){
236
+ this.stopConnectionAttempt()
237
+
238
+ this.debug(`[${this.peer.type}] Starting connection attempt`)
239
+
240
+ // Send initial ping
241
+ this.emit('ping', { token: this.connectionToken })
242
+
243
+ // Set up periodic ping until connected
244
+ this.connectionPingInterval = setInterval(() => {
245
+ if( !this.peer.connected ){
246
+ this.connectionAttempts++
247
+
248
+ if( this.connectionAttempts >= this.options.maxConnectionAttempts! ){
249
+ this.debug(`[${this.peer.type}] Max connection attempts reached`)
250
+ this.stopConnectionAttempt()
251
+ this.fire('connect_timeout', { attempts: this.connectionAttempts })
252
+
253
+ this.options.autoReconnect && this.attemptReconnection()
254
+ return
255
+ }
256
+
257
+ this.debug(`[${this.peer.type}] Connection attempt ${this.connectionAttempts}/${this.options.maxConnectionAttempts}`)
258
+ this.emit('ping', { token: this.connectionToken })
259
+ }
260
+ else {
261
+ this.stopConnectionAttempt()
262
+ }
263
+ }, this.options.connectionPingInterval!)
264
+
265
+ // Set overall timeout
266
+ this.connectionAttemptTimer = setTimeout(() => {
267
+ if( !this.peer.connected ){
268
+ this.debug(`[${this.peer.type}] Connection timeout after ${this.options.connectionTimeout}ms`)
269
+ this.stopConnectionAttempt()
270
+ this.fire('connect_timeout', { attempts: this.connectionAttempts })
271
+
272
+ this.options.autoReconnect && this.attemptReconnection()
273
+ }
274
+ }, this.options.connectionTimeout!)
275
+ }
276
+
277
+ private stopConnectionAttempt(){
278
+ if( this.connectionPingInterval ){
279
+ clearInterval( this.connectionPingInterval )
280
+ this.connectionPingInterval = undefined
281
+ }
282
+
283
+ if( this.connectionAttemptTimer ){
284
+ clearTimeout( this.connectionAttemptTimer )
285
+ this.connectionAttemptTimer = undefined
286
+ }
287
+ }
288
+
289
+ // For EMBEDDED side to announce readiness
290
+ private announceEmbeddedReady(){
291
+ this.stopEmbeddedReadyAnnouncement()
292
+
293
+ let attempts = 0
294
+ const maxAttempts = this.options.maxConnectionAttempts || 5
295
+
296
+ this.debug(`[${this.peer.type}] Announcing embedded ready`)
297
+ this.emit('__embedded_ready')
298
+
299
+ this.embeddedReadyCheckInterval = setInterval(() => {
300
+ if( !this.peer.connected ){
301
+ attempts++
302
+
303
+ if( attempts >= maxAttempts ){
304
+ this.debug(`[${this.peer.type}] Max ready announcement attempts reached`)
305
+ this.stopEmbeddedReadyAnnouncement()
306
+ this.fire('connect_timeout', { attempts })
307
+ return
308
+ }
309
+
310
+ this.debug(`[${this.peer.type}] Ready announcement attempt ${attempts}/${maxAttempts}`)
311
+ this.emit('__embedded_ready')
312
+ }
313
+ else {
314
+ this.stopEmbeddedReadyAnnouncement()
315
+ }
316
+ }, this.options.connectionPingInterval!)
317
+ }
318
+
319
+ private stopEmbeddedReadyAnnouncement(){
320
+ if( this.embeddedReadyCheckInterval ){
321
+ clearInterval( this.embeddedReadyCheckInterval )
322
+ this.embeddedReadyCheckInterval = undefined
323
+ }
324
+ }
325
+
204
326
  // Message rate limiting
205
327
  private checkRateLimit(): boolean {
206
328
  if( !this.options.maxMessagesPerSecond ) return true
@@ -276,31 +398,40 @@ export default class WIO {
276
398
  this.peer.webViewRef = webViewRef
277
399
  this.peer.origin = origin
278
400
  this.peer.connected = false
401
+ this.peer.embeddedReady = false
279
402
  this.reconnectAttempts = 0
403
+ this.connectionAttempts = 0
404
+ this.connectionToken = generateToken()
280
405
 
281
406
  this.debug(`[${this.peer.type}] Initiate connection: WebView origin <${origin}>`)
282
- this.emit('ping')
407
+
408
+ // Start connection attempt with timeout and retries
409
+ this.startConnectionAttempt()
283
410
 
284
411
  return this
285
412
  }
286
413
 
287
414
  /**
288
415
  * Listening to connection from the WebView host
289
- * Note: In React Native context, this is handled by injected JavaScript
290
416
  */
291
417
  listen( hostOrigin?: string ){
292
- this.peer.type = 'EMBEDDED' // iframe.io-rn connection listener is automatically set as EMBEDDED
418
+ this.peer.type = 'EMBEDDED'
293
419
  this.peer.connected = false
420
+ this.peer.embeddedReady = false
294
421
  this.reconnectAttempts = 0
295
422
 
296
423
  this.debug(`[${this.peer.type}] Listening to connect${hostOrigin ? `: Host <${hostOrigin}>` : ''}`)
297
424
 
425
+ // Start announcing readiness
426
+ setTimeout(() => {
427
+ this.announceEmbeddedReady()
428
+ }, 100)
429
+
298
430
  return this
299
431
  }
300
432
 
301
433
  /**
302
434
  * Handle incoming message from WebView
303
- * Called by React Native component via onMessage prop
304
435
  */
305
436
  handleMessage( event: { nativeEvent: { data: string } } ){
306
437
  try {
@@ -309,7 +440,16 @@ export default class WIO {
309
440
  // Enhanced security: check valid message structure
310
441
  if( typeof data !== 'object' || !data.hasOwnProperty('_event') ) return
311
442
 
312
- const { _event, payload, cid, timestamp } = data as Message['data']
443
+ const { _event, payload, cid, timestamp, token } = data as MessageData
444
+
445
+ // Validate origin if specified
446
+ if( this.peer.origin && event.nativeEvent && 'origin' in event.nativeEvent ){
447
+ const messageOrigin = (event.nativeEvent as any).origin
448
+ if( messageOrigin && messageOrigin !== this.peer.origin ){
449
+ this.debug(`[${this.peer.type}] Message from unauthorized origin: ${messageOrigin}`)
450
+ return
451
+ }
452
+ }
313
453
 
314
454
  // Handle heartbeat responses
315
455
  if( _event === '__heartbeat_response' ){
@@ -324,20 +464,88 @@ export default class WIO {
324
464
  return
325
465
  }
326
466
 
467
+ // Handle embedded ready announcement
468
+ if( _event === '__embedded_ready' ){
469
+ this.peer.embeddedReady = true
470
+ this.debug(`[${this.peer.type}] Embedded peer ready`)
471
+
472
+ // If we're WEBVIEW and not connected, send ping
473
+ if( this.peer.type === 'WEBVIEW' && !this.peer.connected ){
474
+ this.emit('ping', { token: this.connectionToken })
475
+ }
476
+ return
477
+ }
478
+
479
+ // Handle webview ready signal
480
+ if( _event === '__webview_ready' ){
481
+ this.debug(`[${this.peer.type}] WebView peer ready`)
482
+ return
483
+ }
484
+
327
485
  this.debug(`[${this.peer.type}] Message: ${_event}`, payload || '')
328
486
 
329
- // Handshake or availability check events
330
- if( _event == 'pong' ){
331
- // WebView is connected
332
- this.peer.connected = true
333
- this.reconnectAttempts = 0
334
- this.peer.lastHeartbeat = Date.now()
335
-
336
- this.startHeartbeat()
337
- this.fire('connect')
338
- this.processMessageQueue()
339
- this.debug(`[${this.peer.type}] connected`)
487
+ // Handshake: ping event
488
+ if( _event === 'ping' ){
489
+ // EMBEDDED receives ping from WEBVIEW
490
+ if( this.peer.type === 'EMBEDDED' ){
491
+ this.connectionToken = token
492
+ this.emit('pong', { token: this.connectionToken })
493
+
494
+ // Don't set fully connected yet - wait for ack
495
+ this.debug(`[${this.peer.type}] Received ping, sent pong`)
496
+ }
497
+ return
498
+ }
499
+
500
+ // Handshake: pong event
501
+ if( _event === 'pong' ){
502
+ // WEBVIEW receives pong from EMBEDDED
503
+ if( this.peer.type === 'WEBVIEW' ){
504
+ // Validate token if provided
505
+ if( token && token !== this.connectionToken ){
506
+ this.debug(`[${this.peer.type}] Invalid connection token in pong`)
507
+ return
508
+ }
340
509
 
510
+ this.peer.connected = true
511
+ this.reconnectAttempts = 0
512
+ this.connectionAttempts = 0
513
+ this.peer.lastHeartbeat = Date.now()
514
+
515
+ // Send connection acknowledgment to complete 3-way handshake
516
+ this.emit('__connection_ack', { token: this.connectionToken })
517
+
518
+ this.stopConnectionAttempt()
519
+ this.startHeartbeat()
520
+ this.fire('connect')
521
+ this.processMessageQueue()
522
+
523
+ this.debug(`[${this.peer.type}] Connected (3-way handshake complete)`)
524
+ }
525
+ return
526
+ }
527
+
528
+ // Handshake: connection ack
529
+ if( _event === '__connection_ack' ){
530
+ // EMBEDDED receives ack from WEBVIEW
531
+ if( this.peer.type === 'EMBEDDED' ){
532
+ // Validate token if provided
533
+ if( token && token !== this.connectionToken ){
534
+ this.debug(`[${this.peer.type}] Invalid connection token in ack`)
535
+ return
536
+ }
537
+
538
+ this.peer.connected = true
539
+ this.reconnectAttempts = 0
540
+ this.peer.lastHeartbeat = Date.now()
541
+
542
+ this.stopEmbeddedReadyAnnouncement()
543
+ this.startHeartbeat()
544
+ this.fire('connect')
545
+ this.processMessageQueue()
546
+
547
+ this.debug(`[${this.peer.type}] Connected (received ack)`)
548
+ }
341
549
  return
342
550
  }
343
551
 
@@ -429,12 +637,13 @@ export default class WIO {
429
637
  this.once(`${_event}--${cid}--@ack`, ({ error, args }) => ackFunction( error, ...args ))
430
638
  }
431
639
 
432
- const messageData = {
640
+ const messageData: MessageData = {
433
641
  _event,
434
642
  payload: sanitizedPayload,
435
643
  cid,
436
644
  timestamp: Date.now(),
437
- size: getMessageSize( sanitizedPayload )
645
+ size: getMessageSize( sanitizedPayload ),
646
+ token: RESERVED_EVENTS.includes(_event) ? this.connectionToken : undefined
438
647
  }
439
648
 
440
649
  this.peer.webViewRef.current?.postMessage( JSON.stringify( newObject( messageData ) ) )
@@ -553,6 +762,8 @@ export default class WIO {
553
762
  // Clean up all resources
554
763
  private cleanup(){
555
764
  this.stopHeartbeat()
765
+ this.stopConnectionAttempt()
766
+ this.stopEmbeddedReadyAnnouncement()
556
767
 
557
768
  if( this.reconnectTimer ){
558
769
  clearTimeout( this.reconnectTimer )
@@ -565,12 +776,15 @@ export default class WIO {
565
776
  this.cleanup()
566
777
 
567
778
  this.peer.connected = false
779
+ this.peer.embeddedReady = false
568
780
  this.peer.webViewRef = undefined
569
781
  this.peer.origin = undefined
570
782
  this.peer.lastHeartbeat = undefined
571
783
  this.messageQueue = []
572
784
  this.messageRateTracker = []
573
785
  this.reconnectAttempts = 0
786
+ this.connectionAttempts = 0
787
+ this.connectionToken = undefined
574
788
 
575
789
  this.removeListeners()
576
790
 
@@ -584,11 +798,13 @@ export default class WIO {
584
798
  getStats(){
585
799
  return {
586
800
  connected: this.isConnected(),
801
+ embeddedReady: this.peer.embeddedReady,
587
802
  peerType: this.peer.type,
588
803
  origin: this.peer.origin,
589
804
  lastHeartbeat: this.peer.lastHeartbeat,
590
805
  queuedMessages: this.messageQueue.length,
591
806
  reconnectAttempts: this.reconnectAttempts,
807
+ connectionAttempts: this.connectionAttempts,
592
808
  activeListeners: Object.keys( this.Events ).length,
593
809
  messageRate: this.messageRateTracker.length
594
810
  }
@@ -610,166 +826,273 @@ export default class WIO {
610
826
  getInjectedJavaScript(): string {
611
827
  return `
612
828
  (function() {
613
- alert('[EMBEDDED] Injected JavaScript')
614
- const RESERVED_EVENTS = ['ping', 'pong', '__heartbeat', '__heartbeat_response'];
615
-
616
- window._wio = {
617
- type: 'EMBEDDED',
618
- connected: false,
619
- Events: {},
620
- messageQueue: [],
621
-
622
- listen: function(){
623
- console.log('[EMBEDDED] Listening for messages...')
624
-
625
- // Listen to messages from React Native
626
- window.addEventListener('message', function( event ){
627
- try {
628
- console.log('[EMBEDDED] Received message:', event.data);
629
- const message = JSON.parse( event.data );
630
- window._wio.handleMessage( message );
631
- }
632
- catch( error ){ console.error('[EMBEDDED] Parse error:', error); }
633
- });
634
-
635
- // Android support
636
- if( typeof document !== 'undefined' ){
637
- document.addEventListener('message', function( event ){
829
+ try {
830
+ console.log('[EMBEDDED] Initializing WIO bridge...');
831
+
832
+ const RESERVED_EVENTS = ['ping', 'pong', '__heartbeat', '__heartbeat_response', '__embedded_ready', '__connection_ack', '__webview_ready'];
833
+
834
+ window._wio = {
835
+ type: 'EMBEDDED',
836
+ connected: false,
837
+ Events: {},
838
+ messageQueue: [],
839
+ connectionToken: null,
840
+ setupComplete: false,
841
+
842
+ listen: function(){
843
+ console.log('[EMBEDDED] Setting up message listeners...');
844
+
845
+ // Listen to messages from React Native
846
+ window.addEventListener('message', function( event ){
638
847
  try {
639
- console.log('[EMBEDDED] Received message:', event.data);
640
- const message = JSON.parse( event.data );
848
+ const message = typeof event.data === 'string' ? JSON.parse( event.data ) : event.data;
641
849
  window._wio.handleMessage( message );
642
850
  }
643
- catch( error ){ console.error('[EMBEDDED] Parse error:', error); }
851
+ catch( error ){
852
+ console.error('[EMBEDDED] Parse error:', error);
853
+ }
644
854
  });
645
- }
646
- },
647
-
648
- ackId: function(){
649
- const
650
- rmin = 100000,
651
- rmax = 999999,
652
- timestamp = Date.now(),
653
- random = Math.floor( Math.random() * ( rmax - rmin + 1 ) + rmin );
654
-
655
- return timestamp + '_' + random;
656
- },
657
-
658
- fire: function( _event, payload, cid ){
659
- if( !this.Events[_event] && !this.Events[_event + '--@once'] ) return;
855
+
856
+ // Android support
857
+ if( typeof document !== 'undefined' ){
858
+ document.addEventListener('message', function( event ){
859
+ try {
860
+ const message = typeof event.data === 'string' ? JSON.parse( event.data ) : event.data;
861
+ window._wio.handleMessage( message );
862
+ }
863
+ catch( error ){
864
+ console.error('[EMBEDDED] Parse error:', error);
865
+ }
866
+ });
867
+ }
868
+
869
+ this.setupComplete = true;
870
+ console.log('[EMBEDDED] Setup complete');
871
+ },
660
872
 
661
- const ackFn = cid
662
- ? ( error, ...args ) => {
663
- this.emit( _event + '--' + cid + '--@ack', { error: error || false, args } );
873
+ ackId: function(){
874
+ const
875
+ rmin = 100000,
876
+ rmax = 999999,
877
+ timestamp = Date.now(),
878
+ random = Math.floor( Math.random() * ( rmax - rmin + 1 ) + rmin );
879
+
880
+ return timestamp + '_' + random;
881
+ },
882
+
883
+ fire: function( _event, payload, cid ){
884
+ if( !this.Events[_event] && !this.Events[_event + '--@once'] ){
885
+ console.log('[EMBEDDED] No listener for:', _event);
886
+ return;
887
+ }
888
+
889
+ const ackFn = cid
890
+ ? ( error, ...args ) => {
891
+ this.emit( _event + '--' + cid + '--@ack', { error: error || false, args } );
892
+ }
893
+ : undefined;
894
+
895
+ let listeners = [];
896
+ if( this.Events[_event + '--@once'] ){
897
+ _event += '--@once';
898
+ listeners = this.Events[_event];
899
+ delete this.Events[_event];
900
+ }
901
+ else listeners = this.Events[_event] || [];
902
+
903
+ listeners.forEach( fn => {
904
+ try {
905
+ payload !== undefined ? fn( payload, ackFn ) : fn( ackFn );
906
+ }
907
+ catch( error ){
908
+ console.error('[EMBEDDED] Listener error:', error);
909
+ }
910
+ });
911
+ },
912
+
913
+ emit: function( _event, payload, fn ){
914
+ if( typeof payload === 'function' ){
915
+ fn = payload;
916
+ payload = undefined;
917
+ }
918
+
919
+ if( !this.connected && !RESERVED_EVENTS.includes(_event) ){
920
+ this.messageQueue.push({ _event, payload, fn, timestamp: Date.now() });
921
+ console.log('[EMBEDDED] Queued message:', _event);
922
+ return;
923
+ }
924
+
925
+ try {
926
+ let cid;
927
+ if( typeof fn === 'function' ){
928
+ cid = this.ackId();
929
+ this.once( _event + '--' + cid + '--@ack', ({ error, args }) => fn( error, ...args ) );
930
+ }
931
+
932
+ const messageData = {
933
+ _event,
934
+ payload,
935
+ cid,
936
+ timestamp: Date.now(),
937
+ token: RESERVED_EVENTS.includes(_event) ? this.connectionToken : undefined
938
+ };
939
+
940
+ if( typeof window.ReactNativeWebView !== 'undefined' ){
941
+ window.ReactNativeWebView.postMessage( JSON.stringify( messageData ) );
664
942
  }
665
- : undefined;
943
+ else {
944
+ console.error('[EMBEDDED] ReactNativeWebView not available');
945
+ }
946
+ }
947
+ catch( error ){
948
+ console.error('[EMBEDDED] Emit error:', error);
949
+ typeof fn === 'function' && fn( String(error) );
950
+ }
951
+ },
666
952
 
667
- let listeners = [];
668
- if( this.Events[_event + '--@once'] ){
953
+ on: function( _event, fn ){
954
+ if( !this.Events[_event] ) this.Events[_event] = [];
955
+ this.Events[_event].push( fn );
956
+ },
957
+
958
+ once: function( _event, fn ){
669
959
  _event += '--@once';
670
- listeners = this.Events[_event];
671
- delete this.Events[_event];
672
- }
673
- else listeners = this.Events[_event] || [];
960
+ if( !this.Events[_event] ) this.Events[_event] = [];
961
+ this.Events[_event].push( fn );
962
+ },
674
963
 
675
- listeners.forEach( fn => {
676
- try { payload !== undefined ? fn( payload, ackFn ) : fn( ackFn ); }
677
- catch( error ){ console.error('[EMBEDDED] Listener error:', error); }
678
- });
679
- },
680
-
681
- emit: function( _event, payload, fn ){
682
- if( typeof payload === 'function' ){
683
- fn = payload;
684
- payload = undefined;
685
- }
964
+ off: function( _event, fn ){
965
+ if( fn && this.Events[_event] ){
966
+ const index = this.Events[_event].indexOf( fn );
967
+ if( index > -1 ){
968
+ this.Events[_event].splice( index, 1 );
969
+ if( this.Events[_event].length === 0 ) delete this.Events[_event];
970
+ }
971
+ }
972
+ else delete this.Events[_event];
973
+ },
686
974
 
687
- if( !this.connected && !RESERVED_EVENTS.includes(_event) ){
688
- this.messageQueue.push({ _event, payload, fn, timestamp: Date.now() });
689
- return;
690
- }
975
+ processMessageQueue: function(){
976
+ if( !this.connected || this.messageQueue.length === 0 ) return;
977
+
978
+ console.log('[EMBEDDED] Processing', this.messageQueue.length, 'queued messages');
979
+ const queue = [...this.messageQueue];
980
+ this.messageQueue = [];
981
+
982
+ queue.forEach( msg => {
983
+ try {
984
+ this.emit( msg._event, msg.payload, msg.fn );
985
+ }
986
+ catch( error ){
987
+ console.error('[EMBEDDED] Queue process error:', error);
988
+ }
989
+ });
990
+ },
691
991
 
692
- try {
693
- let cid;
694
- if( typeof fn === 'function' ){
695
- cid = this.ackId();
696
- this.once( _event + '--' + cid + '--@ack', ({ error, args }) => fn( error, ...args ) );
992
+ handleMessage: function( data ){
993
+ if( !data || !data._event ) return;
994
+
995
+ const { _event, payload, cid, token } = data;
996
+
997
+ console.log('[EMBEDDED] Received:', _event);
998
+
999
+ // Handle heartbeat response
1000
+ if( _event === '__heartbeat_response' ){
1001
+ return;
1002
+ }
1003
+
1004
+ // Handle heartbeat request
1005
+ if( _event === '__heartbeat' ){
1006
+ this.emit('__heartbeat_response', { timestamp: Date.now() });
1007
+ return;
1008
+ }
1009
+
1010
+ // Handle webview ready signal
1011
+ if( _event === '__webview_ready' ){
1012
+ console.log('[EMBEDDED] WebView ready signal received');
1013
+ return;
1014
+ }
1015
+
1016
+ // Handle ping from WEBVIEW
1017
+ if( _event === 'ping' ){
1018
+ console.log('[EMBEDDED] Received ping, sending pong');
1019
+ this.connectionToken = token;
1020
+ this.emit('pong', { token: this.connectionToken });
1021
+ return;
697
1022
  }
698
1023
 
699
- const messageData = {
700
- _event,
701
- payload,
702
- cid,
703
- timestamp: Date.now()
1024
+ // Handle connection acknowledgment
1025
+ if( _event === '__connection_ack' ){
1026
+ if( token && token !== this.connectionToken ){
1027
+ console.error('[EMBEDDED] Invalid connection token in ack');
1028
+ return;
1029
+ }
1030
+
1031
+ console.log('[EMBEDDED] Connection established (received ack)');
1032
+ this.connected = true;
1033
+ this.processMessageQueue();
1034
+ this.fire('connect');
1035
+ return;
1036
+ }
1037
+
1038
+ // Fire event listeners
1039
+ this.fire( _event, payload, cid );
1040
+ },
1041
+
1042
+ announceReady: function(){
1043
+ let attempts = 0;
1044
+ const maxAttempts = 5;
1045
+ const interval = 2000;
1046
+
1047
+ console.log('[EMBEDDED] Starting ready announcements');
1048
+
1049
+ const announce = () => {
1050
+ if( this.connected ){
1051
+ console.log('[EMBEDDED] Connected, stopping announcements');
1052
+ return;
1053
+ }
1054
+
1055
+ attempts++;
1056
+ if( attempts > maxAttempts ){
1057
+ console.log('[EMBEDDED] Max announcement attempts reached');
1058
+ return;
1059
+ }
1060
+
1061
+ console.log('[EMBEDDED] Announcing ready (attempt', attempts + '/' + maxAttempts + ')');
1062
+ this.emit('__embedded_ready');
1063
+
1064
+ setTimeout( announce, interval );
704
1065
  };
705
1066
 
706
- window.ReactNativeWebView.postMessage( JSON.stringify( messageData ) );
707
- }
708
- catch( error ){
709
- console.error('[EMBEDDED] Emit error:', error);
710
- typeof fn === 'function' && fn( String(error) );
1067
+ announce();
711
1068
  }
712
- },
713
-
714
- on: function( _event, fn ){
715
- if( !this.Events[_event] ) this.Events[_event] = [];
716
- this.Events[_event].push( fn );
717
- },
718
-
719
- once: function( _event, fn ){
720
- _event += '--@once';
721
- if( !this.Events[_event] ) this.Events[_event] = [];
722
- this.Events[_event].push( fn );
723
- },
1069
+ };
1070
+
1071
+ // Initialize
1072
+ window._wio.listen();
724
1073
 
725
- off: function( _event, fn ){
726
- if( fn && this.Events[_event] ){
727
- const index = this.Events[_event].indexOf( fn );
728
- if( index > -1 ){
729
- this.Events[_event].splice( index, 1 );
730
- if( this.Events[_event].length === 0 ) delete this.Events[_event];
731
- }
732
- }
733
- else delete this.Events[_event];
734
- },
1074
+ // Start announcing readiness after a short delay
1075
+ setTimeout(() => {
1076
+ window._wio.announceReady();
1077
+ }, 100);
735
1078
 
736
- processMessageQueue: function(){
737
- if( !this.connected || this.messageQueue.length === 0 ) return;
738
-
739
- const queue = [...this.messageQueue];
740
- this.messageQueue = [];
741
-
742
- queue.forEach( msg => {
743
- try { this.emit( msg._event, msg.payload, msg.fn ); }
744
- catch( error ){ console.error('[EMBEDDED] Queue process error:', error); }
745
- });
746
- },
1079
+ console.log('[EMBEDDED] WIO bridge initialized successfully');
1080
+ }
1081
+ catch( error ){
1082
+ console.error('[EMBEDDED] Setup failed:', error);
747
1083
 
748
- handleMessage: function( data ){
749
- if( !data || !data._event ) return;
750
-
751
- const { _event, payload, cid } = data;
752
-
753
- if( _event === '__heartbeat_response' ) return;
754
-
755
- if( _event === '__heartbeat' ){
756
- this.emit('__heartbeat_response', { timestamp: Date.now() });
757
- return;
758
- }
759
-
760
- if( _event === 'ping' ){
761
- this.emit('pong');
762
- this.connected = true;
763
- this.processMessageQueue();
764
- return;
765
- }
766
-
767
- this.fire( _event, payload, cid );
768
- }
769
- };
1084
+ // Create minimal fallback
1085
+ window._wio = {
1086
+ error: error.toString(),
1087
+ emit: function(){ console.error('[EMBEDDED] WIO failed to initialize'); },
1088
+ on: function(){},
1089
+ once: function(){},
1090
+ off: function(){}
1091
+ };
1092
+ }
770
1093
 
771
1094
  true;
772
1095
  })();
773
- `;
1096
+ `
774
1097
  }
775
1098
  }