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/README.md +208 -37
- package/dist/index.d.ts +15 -2
- package/dist/index.js +198 -23
- package/package.json +1 -1
- package/src/index.ts +485 -162
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
|
-
|
|
214
|
+
if( this.peer.type === 'WEBVIEW' ){
|
|
215
|
+
this.startConnectionAttempt()
|
|
216
|
+
}
|
|
190
217
|
|
|
191
|
-
// For EMBEDDED type,
|
|
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
|
-
|
|
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'
|
|
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
|
|
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
|
|
330
|
-
if( _event
|
|
331
|
-
//
|
|
332
|
-
this.peer.
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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
|
-
|
|
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 ){
|
|
851
|
+
catch( error ){
|
|
852
|
+
console.error('[EMBEDDED] Parse error:', error);
|
|
853
|
+
}
|
|
644
854
|
});
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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
|
-
|
|
662
|
-
|
|
663
|
-
|
|
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
|
-
|
|
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
|
-
|
|
668
|
-
|
|
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
|
-
|
|
671
|
-
|
|
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
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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
|
-
|
|
688
|
-
this.messageQueue.
|
|
689
|
-
|
|
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
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
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
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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
|
-
|
|
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
|
-
|
|
715
|
-
|
|
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
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
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
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
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
|
}
|