webview.io 1.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/src/index.ts ADDED
@@ -0,0 +1,768 @@
1
+ import type { RefObject } from 'react'
2
+ import type { WebView } from 'react-native-webview'
3
+
4
+ export type PeerType = 'WEBVIEW' | 'EMBEDDED'
5
+
6
+ export type AckFunction = ( error: boolean | string, ...args: any[] ) => void
7
+ export type Listener = ( payload?: any, ack?: AckFunction ) => void
8
+
9
+ export type Options = {
10
+ type?: PeerType
11
+ debug?: boolean
12
+ heartbeatInterval?: number
13
+ connectionTimeout?: number
14
+ maxMessageSize?: number
15
+ maxMessagesPerSecond?: number
16
+ autoReconnect?: boolean
17
+ messageQueueSize?: number
18
+ }
19
+
20
+ export interface RegisteredEvents {
21
+ [index: string]: Listener[]
22
+ }
23
+
24
+ export type Peer = {
25
+ type: PeerType
26
+ webViewRef?: RefObject<WebView>
27
+ origin?: string
28
+ connected?: boolean
29
+ lastHeartbeat?: number
30
+ }
31
+
32
+ export type MessageData = {
33
+ _event: string
34
+ payload: any
35
+ cid: string | undefined
36
+ timestamp?: number
37
+ size?: number
38
+ }
39
+
40
+ export type Message = {
41
+ data: MessageData
42
+ }
43
+
44
+ export type QueuedMessage = {
45
+ _event: string
46
+ payload: any
47
+ fn?: AckFunction
48
+ timestamp: number
49
+ }
50
+
51
+ function newObject( data: object ){
52
+ return JSON.parse( JSON.stringify( data ) )
53
+ }
54
+
55
+ function getMessageSize( data: any ): number {
56
+ try { return JSON.stringify( data ).length }
57
+ catch { return 0 }
58
+ }
59
+
60
+ function sanitizePayload( payload: any, maxSize: number ): any {
61
+ if( !payload ) return payload
62
+
63
+ const size = getMessageSize( payload )
64
+ if( size > maxSize )
65
+ throw new Error(`Message size ${size} exceeds limit ${maxSize}`)
66
+
67
+ // Basic sanitization - remove functions and undefined values
68
+ return JSON.parse( JSON.stringify( payload ) )
69
+ }
70
+
71
+ const ackId = () => {
72
+ const
73
+ rmin = 100000,
74
+ rmax = 999999,
75
+ timestamp = Date.now(),
76
+ random = Math.floor( Math.random() * ( rmax - rmin + 1 ) + rmin )
77
+
78
+ return `${timestamp}_${random}`
79
+ }
80
+
81
+ const RESERVED_EVENTS = [
82
+ 'ping',
83
+ 'pong',
84
+ '__heartbeat',
85
+ '__heartbeat_response'
86
+ ]
87
+
88
+ export default class WIO {
89
+ Events: RegisteredEvents
90
+ peer: Peer
91
+ options: Options
92
+ private heartbeatTimer?: NodeJS.Timeout
93
+ private reconnectTimer?: NodeJS.Timeout
94
+ private messageQueue: QueuedMessage[] = []
95
+ private messageRateTracker: number[] = []
96
+ private reconnectAttempts: number = 0
97
+ private maxReconnectAttempts: number = 5
98
+
99
+ constructor( options: Options = {} ){
100
+ if( options && typeof options !== 'object' )
101
+ throw new Error('Invalid Options')
102
+
103
+ this.options = {
104
+ debug: false,
105
+ heartbeatInterval: 30000, // 30 seconds
106
+ connectionTimeout: 10000, // 10 seconds
107
+ maxMessageSize: 1024 * 1024, // 1MB
108
+ maxMessagesPerSecond: 100,
109
+ autoReconnect: true,
110
+ messageQueueSize: 50,
111
+ ...options
112
+ }
113
+ this.Events = {}
114
+ this.peer = { type: 'WEBVIEW', connected: false }
115
+
116
+ if( options.type )
117
+ this.peer.type = options.type
118
+ }
119
+
120
+ debug( ...args: any[] ){
121
+ this.options.debug && console.debug( ...args )
122
+ }
123
+
124
+ isConnected(): boolean {
125
+ return !!this.peer.connected && !!this.peer.webViewRef
126
+ }
127
+
128
+ // Enhanced connection health monitoring
129
+ private startHeartbeat(){
130
+ if( !this.options.heartbeatInterval ) return
131
+
132
+ this.heartbeatTimer = setInterval(() => {
133
+ if( this.isConnected() ){
134
+ const now = Date.now()
135
+
136
+ // Check if peer is still responsive
137
+ if( this.peer.lastHeartbeat
138
+ && ( now - this.peer.lastHeartbeat ) > ( this.options.heartbeatInterval! * 2 ) ){
139
+ this.debug(`[${this.peer.type}] Heartbeat timeout detected`)
140
+ this.handleConnectionLoss()
141
+
142
+ return
143
+ }
144
+
145
+ // Send heartbeat
146
+ try { this.emit('__heartbeat', { timestamp: now }) }
147
+ catch( error ){
148
+ this.debug(`[${this.peer.type}] Heartbeat send failed:`, error)
149
+ this.handleConnectionLoss()
150
+ }
151
+ }
152
+ }, this.options.heartbeatInterval )
153
+ }
154
+
155
+ private stopHeartbeat(){
156
+ if( !this.heartbeatTimer ) return
157
+
158
+ clearInterval( this.heartbeatTimer )
159
+ this.heartbeatTimer = undefined
160
+ }
161
+
162
+ // Handle connection loss and potential reconnection
163
+ private handleConnectionLoss(){
164
+ if( !this.peer.connected ) return
165
+
166
+ this.peer.connected = false
167
+ this.stopHeartbeat()
168
+ this.fire('disconnect', { reason: 'CONNECTION_LOST' })
169
+
170
+ this.options.autoReconnect
171
+ && this.reconnectAttempts < this.maxReconnectAttempts
172
+ && this.attemptReconnection()
173
+ }
174
+
175
+ private attemptReconnection(){
176
+ if( this.reconnectTimer ) return
177
+
178
+ this.reconnectAttempts++
179
+ const delay = Math.min( 1000 * Math.pow( 2, this.reconnectAttempts - 1 ), 30000 ) // Exponential backoff, max 30s
180
+
181
+ this.debug(`[${this.peer.type}] Attempting reconnection ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay}ms`)
182
+ this.fire('reconnecting', { attempt: this.reconnectAttempts, delay })
183
+
184
+ this.reconnectTimer = setTimeout(() => {
185
+ this.reconnectTimer = undefined
186
+
187
+ // Re-initiate connection for WEBVIEW type
188
+ this.peer.type === 'WEBVIEW'
189
+ && this.emit('ping')
190
+
191
+ // For EMBEDDED type, just wait for incoming connection
192
+
193
+ // Set timeout for this reconnection attempt
194
+ setTimeout(() => {
195
+ if( this.peer.connected ) return
196
+
197
+ this.reconnectAttempts < this.maxReconnectAttempts
198
+ ? this.attemptReconnection()
199
+ : this.fire('reconnection_failed', { attempts: this.reconnectAttempts })
200
+ }, this.options.connectionTimeout!)
201
+ }, delay)
202
+ }
203
+
204
+ // Message rate limiting
205
+ private checkRateLimit(): boolean {
206
+ if( !this.options.maxMessagesPerSecond ) return true
207
+
208
+ const
209
+ now = Date.now(),
210
+ aSecondAgo = now - 1000
211
+
212
+ // Clean old entries
213
+ this.messageRateTracker = this.messageRateTracker.filter( timestamp => timestamp > aSecondAgo )
214
+
215
+ // Check if limit exceeded
216
+ if( this.messageRateTracker.length >= this.options.maxMessagesPerSecond ){
217
+ this.fire('error', {
218
+ type: 'RATE_LIMIT_EXCEEDED',
219
+ limit: this.options.maxMessagesPerSecond,
220
+ current: this.messageRateTracker.length
221
+ })
222
+
223
+ return false
224
+ }
225
+
226
+ this.messageRateTracker.push( now )
227
+ return true
228
+ }
229
+
230
+ // Queue messages when not connected
231
+ private queueMessage( _event: string, payload?: any, fn?: AckFunction ){
232
+ if( this.messageQueue.length >= this.options.messageQueueSize! ){
233
+ // Remove oldest message
234
+ const removed = this.messageQueue.shift()
235
+ this.debug(`[${this.peer.type}] Message queue full, removed oldest message:`, removed?._event)
236
+ }
237
+
238
+ this.messageQueue.push({
239
+ _event,
240
+ payload,
241
+ fn,
242
+ timestamp: Date.now()
243
+ })
244
+
245
+ this.debug(`[${this.peer.type}] Queued message: ${_event} (queue size: ${this.messageQueue.length})`)
246
+ }
247
+
248
+ // Process queued messages when connection is established
249
+ private processMessageQueue(){
250
+ if( !this.isConnected() || this.messageQueue.length === 0 ) return
251
+
252
+ this.debug(`[${this.peer.type}] Processing ${this.messageQueue.length} queued messages`)
253
+
254
+ const queue = [...this.messageQueue]
255
+ this.messageQueue = []
256
+
257
+ queue.forEach( message => {
258
+ try { this.emit( message._event, message.payload, message.fn ) }
259
+ catch( error ){ this.debug(`[${this.peer.type}] Failed to send queued message:`, error) }
260
+ })
261
+ }
262
+
263
+ /**
264
+ * Establish a connection with WebView
265
+ */
266
+ initiate( webViewRef: RefObject<WebView>, origin: string ){
267
+ if( !webViewRef || !origin )
268
+ throw new Error('Invalid Connection initiation arguments')
269
+
270
+ if( this.peer.type === 'EMBEDDED' )
271
+ throw new Error('Expect EMBEDDED to <listen> and WEBVIEW to <initiate> a connection')
272
+
273
+ // Clean up existing resources if any
274
+ this.cleanup()
275
+
276
+ this.peer.webViewRef = webViewRef
277
+ this.peer.origin = origin
278
+ this.peer.connected = false
279
+ this.reconnectAttempts = 0
280
+
281
+ this.debug(`[${this.peer.type}] Initiate connection: WebView origin <${origin}>`)
282
+ this.emit('ping')
283
+
284
+ return this
285
+ }
286
+
287
+ /**
288
+ * Listening to connection from the WebView host
289
+ * Note: In React Native context, this is handled by injected JavaScript
290
+ */
291
+ listen( hostOrigin?: string ){
292
+ this.peer.type = 'EMBEDDED' // iframe.io-rn connection listener is automatically set as EMBEDDED
293
+ this.peer.connected = false
294
+ this.reconnectAttempts = 0
295
+
296
+ this.debug(`[${this.peer.type}] Listening to connect${hostOrigin ? `: Host <${hostOrigin}>` : ''}`)
297
+
298
+ return this
299
+ }
300
+
301
+ /**
302
+ * Handle incoming message from WebView
303
+ * Called by React Native component via onMessage prop
304
+ */
305
+ handleMessage( event: { nativeEvent: { data: string } } ){
306
+ try {
307
+ const data = JSON.parse( event.nativeEvent.data )
308
+
309
+ // Enhanced security: check valid message structure
310
+ if( typeof data !== 'object' || !data.hasOwnProperty('_event') ) return
311
+
312
+ const { _event, payload, cid, timestamp } = data as Message['data']
313
+
314
+ // Handle heartbeat responses
315
+ if( _event === '__heartbeat_response' ){
316
+ this.peer.lastHeartbeat = Date.now()
317
+ return
318
+ }
319
+
320
+ // Handle heartbeat requests
321
+ if( _event === '__heartbeat' ){
322
+ this.emit('__heartbeat_response', { timestamp: Date.now() })
323
+ this.peer.lastHeartbeat = Date.now()
324
+ return
325
+ }
326
+
327
+ this.debug(`[${this.peer.type}] Message: ${_event}`, payload || '')
328
+
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`)
340
+
341
+ return
342
+ }
343
+
344
+ // Fire available event listeners
345
+ this.fire( _event, payload, cid )
346
+ }
347
+ catch( error ){
348
+ this.debug(`[${this.peer.type}] Message handling error:`, error)
349
+ this.fire('error', {
350
+ type: 'MESSAGE_HANDLING_ERROR',
351
+ error: error instanceof Error ? error.message : String(error)
352
+ })
353
+ }
354
+ }
355
+
356
+ fire( _event: string, payload?: MessageData['payload'], cid?: string ){
357
+ // Volatile event - check if any listeners exist
358
+ if( !this.Events[_event] && !this.Events[_event + '--@once'] ){
359
+ this.debug(`[${this.peer.type}] No <${_event}> listener defined`)
360
+ return
361
+ }
362
+
363
+ const ackFn = cid
364
+ ? ( error: boolean | string, ...args: any[] ): void => {
365
+ this.emit(`${_event}--${cid}--@ack`, { error: error || false, args })
366
+ return
367
+ }
368
+ : undefined
369
+ let listeners: Listener[] = []
370
+
371
+ if( this.Events[_event + '--@once'] ){
372
+ // Once triggable event
373
+ _event += '--@once'
374
+ listeners = this.Events[_event]
375
+ // Delete once event listeners after fired
376
+ delete this.Events[_event]
377
+ }
378
+ else listeners = this.Events[_event]
379
+
380
+ // Fire listeners with error handling
381
+ listeners.forEach( fn => {
382
+ try { payload !== undefined ? fn( payload, ackFn ) : fn( ackFn ) }
383
+ catch( error ){
384
+ this.debug(`[${this.peer.type}] Listener error for ${_event}:`, error)
385
+ this.fire('error', {
386
+ type: 'LISTENER_ERROR',
387
+ event: _event,
388
+ error: error instanceof Error ? error.message : String(error)
389
+ })
390
+ }
391
+ })
392
+ }
393
+
394
+ emit<T = any>( _event: string, payload?: T | AckFunction, fn?: AckFunction ){
395
+ // Check rate limiting
396
+ if( !this.checkRateLimit() ) return this
397
+
398
+ /**
399
+ * Queue message if not connected: Except for
400
+ * connection-related events
401
+ */
402
+ if( !this.isConnected() && !RESERVED_EVENTS.includes(_event) ){
403
+ this.queueMessage( _event, payload, fn )
404
+ return this
405
+ }
406
+
407
+ if( !this.peer.webViewRef ){
408
+ this.fire('error', { type: 'NO_CONNECTION', event: _event })
409
+ return this
410
+ }
411
+
412
+ if( typeof payload == 'function' ){
413
+ fn = payload as AckFunction
414
+ payload = undefined
415
+ }
416
+
417
+ try {
418
+ // Enhanced security: sanitize and validate payload
419
+ const sanitizedPayload = payload
420
+ ? sanitizePayload( payload, this.options.maxMessageSize! )
421
+ : payload
422
+
423
+ // Acknowledge event listener
424
+ let cid: string | undefined
425
+ if( typeof fn === 'function' ){
426
+ const ackFunction = fn
427
+
428
+ cid = ackId()
429
+ this.once(`${_event}--${cid}--@ack`, ({ error, args }) => ackFunction( error, ...args ))
430
+ }
431
+
432
+ const messageData = {
433
+ _event,
434
+ payload: sanitizedPayload,
435
+ cid,
436
+ timestamp: Date.now(),
437
+ size: getMessageSize( sanitizedPayload )
438
+ }
439
+
440
+ this.peer.webViewRef.current?.postMessage( JSON.stringify( newObject( messageData ) ) )
441
+ }
442
+ catch( error ){
443
+ this.debug(`[${this.peer.type}] Emit error:`, error)
444
+ this.fire('error', {
445
+ type: 'EMIT_ERROR',
446
+ event: _event,
447
+ error: error instanceof Error ? error.message : String(error)
448
+ })
449
+
450
+ // Call acknowledgment with error if provided
451
+ typeof fn === 'function'
452
+ && fn( error instanceof Error ? error.message : String(error) )
453
+ }
454
+
455
+ return this
456
+ }
457
+
458
+ on( _event: string, fn: Listener ){
459
+ // Add Event listener
460
+ if( !this.Events[_event] ) this.Events[_event] = []
461
+ this.Events[_event].push( fn )
462
+
463
+ this.debug(`[${this.peer.type}] New <${_event}> listener on`)
464
+ return this
465
+ }
466
+
467
+ once( _event: string, fn: Listener ){
468
+ // Add Once Event listener
469
+ _event += '--@once'
470
+
471
+ if( !this.Events[_event] ) this.Events[_event] = []
472
+ this.Events[_event].push( fn )
473
+
474
+ this.debug(`[${this.peer.type}] New <${_event} once> listener on`)
475
+ return this
476
+ }
477
+
478
+ off( _event: string, fn?: Listener ){
479
+ // Remove Event listener
480
+ if( fn && this.Events[_event] ){
481
+ // Remove specific listener if provided
482
+ const index = this.Events[_event].indexOf( fn )
483
+ if( index > -1 ){
484
+ this.Events[_event].splice( index, 1 )
485
+
486
+ // Remove event array if empty
487
+ if( this.Events[_event].length === 0 )
488
+ delete this.Events[_event]
489
+ }
490
+ }
491
+ // Remove all listeners for event
492
+ else delete this.Events[_event]
493
+
494
+ typeof fn == 'function' && fn()
495
+ this.debug(`[${this.peer.type}] <${_event}> listener off`)
496
+
497
+ return this
498
+ }
499
+
500
+ removeListeners( fn?: Listener ){
501
+ // Clear all event listeners
502
+ this.Events = {}
503
+ typeof fn == 'function' && fn()
504
+
505
+ this.debug(`[${this.peer.type}] All listeners removed`)
506
+ return this
507
+ }
508
+
509
+ emitAsync<T = any, R = any>( _event: string, payload?: T, timeout: number = 5000 ): Promise<R> {
510
+ return new Promise(( resolve, reject ) => {
511
+ const timeoutId = setTimeout(() => {
512
+ reject( new Error(`Event '${_event}' acknowledgment timeout after ${timeout}ms`) )
513
+ }, timeout )
514
+
515
+ try {
516
+ this.emit( _event, payload, ( error, ...args ) => {
517
+ clearTimeout( timeoutId )
518
+
519
+ error
520
+ ? reject( new Error( typeof error === 'string' ? error : 'Ack error' ) )
521
+ : resolve( args.length === 0 ? undefined : args.length === 1 ? args[0] : args as any )
522
+ })
523
+ }
524
+ catch( error ){
525
+ clearTimeout( timeoutId )
526
+ reject( error )
527
+ }
528
+ })
529
+ }
530
+
531
+ onceAsync<T = any>( _event: string ): Promise<T> {
532
+ return new Promise( resolve => this.once( _event, resolve ) )
533
+ }
534
+
535
+ connectAsync( timeout?: number ): Promise<void> {
536
+ return new Promise(( resolve, reject ) => {
537
+ if( this.isConnected() ) return resolve()
538
+
539
+ const timeoutId = setTimeout(() => {
540
+ this.off('connect', connectHandler)
541
+ reject( new Error('Connection timeout') )
542
+ }, timeout || this.options.connectionTimeout)
543
+
544
+ const connectHandler = () => {
545
+ clearTimeout( timeoutId )
546
+ resolve()
547
+ }
548
+
549
+ this.once('connect', connectHandler)
550
+ })
551
+ }
552
+
553
+ // Clean up all resources
554
+ private cleanup(){
555
+ this.stopHeartbeat()
556
+
557
+ if( this.reconnectTimer ){
558
+ clearTimeout( this.reconnectTimer )
559
+ this.reconnectTimer = undefined
560
+ }
561
+ }
562
+
563
+ disconnect( fn?: () => void ){
564
+ // Cleanup on disconnect
565
+ this.cleanup()
566
+
567
+ this.peer.connected = false
568
+ this.peer.webViewRef = undefined
569
+ this.peer.origin = undefined
570
+ this.peer.lastHeartbeat = undefined
571
+ this.messageQueue = []
572
+ this.messageRateTracker = []
573
+ this.reconnectAttempts = 0
574
+
575
+ this.removeListeners()
576
+
577
+ typeof fn == 'function' && fn()
578
+ this.debug(`[${this.peer.type}] Disconnected`)
579
+
580
+ return this
581
+ }
582
+
583
+ // Get connection statistics
584
+ getStats(){
585
+ return {
586
+ connected: this.isConnected(),
587
+ peerType: this.peer.type,
588
+ origin: this.peer.origin,
589
+ lastHeartbeat: this.peer.lastHeartbeat,
590
+ queuedMessages: this.messageQueue.length,
591
+ reconnectAttempts: this.reconnectAttempts,
592
+ activeListeners: Object.keys( this.Events ).length,
593
+ messageRate: this.messageRateTracker.length
594
+ }
595
+ }
596
+
597
+ // Clear message queue manually
598
+ clearQueue(){
599
+ const queueSize = this.messageQueue.length
600
+ this.messageQueue = []
601
+
602
+ this.debug(`[${this.peer.type}] Cleared ${queueSize} queued messages`)
603
+ return this
604
+ }
605
+
606
+ /**
607
+ * Get injected JavaScript for WebView
608
+ * Sets up the EMBEDDED side of the bridge
609
+ */
610
+ getInjectedJavaScript(): string {
611
+ return `
612
+ (function() {
613
+ const RESERVED_EVENTS = ['ping', 'pong', '__heartbeat', '__heartbeat_response'];
614
+
615
+ window._wio = {
616
+ type: 'EMBEDDED',
617
+ connected: false,
618
+ Events: {},
619
+ messageQueue: [],
620
+
621
+ ackId: function(){
622
+ const
623
+ rmin = 100000,
624
+ rmax = 999999,
625
+ timestamp = Date.now(),
626
+ random = Math.floor( Math.random() * ( rmax - rmin + 1 ) + rmin );
627
+
628
+ return timestamp + '_' + random;
629
+ },
630
+
631
+ fire: function( _event, payload, cid ){
632
+ if( !this.Events[_event] && !this.Events[_event + '--@once'] ) return;
633
+
634
+ const ackFn = cid
635
+ ? ( error, ...args ) => {
636
+ this.emit( _event + '--' + cid + '--@ack', { error: error || false, args } );
637
+ }
638
+ : undefined;
639
+
640
+ let listeners = [];
641
+ if( this.Events[_event + '--@once'] ){
642
+ _event += '--@once';
643
+ listeners = this.Events[_event];
644
+ delete this.Events[_event];
645
+ }
646
+ else listeners = this.Events[_event] || [];
647
+
648
+ listeners.forEach( fn => {
649
+ try { payload !== undefined ? fn( payload, ackFn ) : fn( ackFn ); }
650
+ catch( error ){ console.error('[EMBEDDED] Listener error:', error); }
651
+ });
652
+ },
653
+
654
+ emit: function( _event, payload, fn ){
655
+ if( typeof payload === 'function' ){
656
+ fn = payload;
657
+ payload = undefined;
658
+ }
659
+
660
+ if( !this.connected && !RESERVED_EVENTS.includes(_event) ){
661
+ this.messageQueue.push({ _event, payload, fn, timestamp: Date.now() });
662
+ return;
663
+ }
664
+
665
+ try {
666
+ let cid;
667
+ if( typeof fn === 'function' ){
668
+ cid = this.ackId();
669
+ this.once( _event + '--' + cid + '--@ack', ({ error, args }) => fn( error, ...args ) );
670
+ }
671
+
672
+ const messageData = {
673
+ _event,
674
+ payload,
675
+ cid,
676
+ timestamp: Date.now()
677
+ };
678
+
679
+ window.ReactNativeWebView.postMessage( JSON.stringify( messageData ) );
680
+ }
681
+ catch( error ){
682
+ console.error('[EMBEDDED] Emit error:', error);
683
+ typeof fn === 'function' && fn( String(error) );
684
+ }
685
+ },
686
+
687
+ on: function( _event, fn ){
688
+ if( !this.Events[_event] ) this.Events[_event] = [];
689
+ this.Events[_event].push( fn );
690
+ },
691
+
692
+ once: function( _event, fn ){
693
+ _event += '--@once';
694
+ if( !this.Events[_event] ) this.Events[_event] = [];
695
+ this.Events[_event].push( fn );
696
+ },
697
+
698
+ off: function( _event, fn ){
699
+ if( fn && this.Events[_event] ){
700
+ const index = this.Events[_event].indexOf( fn );
701
+ if( index > -1 ){
702
+ this.Events[_event].splice( index, 1 );
703
+ if( this.Events[_event].length === 0 ) delete this.Events[_event];
704
+ }
705
+ }
706
+ else delete this.Events[_event];
707
+ },
708
+
709
+ processMessageQueue: function(){
710
+ if( !this.connected || this.messageQueue.length === 0 ) return;
711
+
712
+ const queue = [...this.messageQueue];
713
+ this.messageQueue = [];
714
+
715
+ queue.forEach( msg => {
716
+ try { this.emit( msg._event, msg.payload, msg.fn ); }
717
+ catch( error ){ console.error('[EMBEDDED] Queue process error:', error); }
718
+ });
719
+ },
720
+
721
+ handleMessage: function( data ){
722
+ if( !data || !data._event ) return;
723
+
724
+ const { _event, payload, cid } = data;
725
+
726
+ if( _event === '__heartbeat_response' ) return;
727
+
728
+ if( _event === '__heartbeat' ){
729
+ this.emit('__heartbeat_response', { timestamp: Date.now() });
730
+ return;
731
+ }
732
+
733
+ if( _event === 'ping' ){
734
+ this.emit('pong');
735
+ this.connected = true;
736
+ this.processMessageQueue();
737
+ return;
738
+ }
739
+
740
+ this.fire( _event, payload, cid );
741
+ }
742
+ };
743
+
744
+ // Listen to messages from React Native
745
+ window.addEventListener('message', function( event ){
746
+ try {
747
+ const message = JSON.parse( event.data );
748
+ window._wio.handleMessage( message );
749
+ }
750
+ catch( error ){ console.error('[EMBEDDED] Parse error:', error); }
751
+ });
752
+
753
+ // Android support
754
+ if( typeof document !== 'undefined' ){
755
+ document.addEventListener('message', function( event ){
756
+ try {
757
+ const message = JSON.parse( event.data );
758
+ window._wio.handleMessage( message );
759
+ }
760
+ catch( error ){ console.error('[EMBEDDED] Parse error:', error); }
761
+ });
762
+ }
763
+
764
+ true;
765
+ })();
766
+ `;
767
+ }
768
+ }