webview.io 1.0.0 → 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 +2 -2
- package/src/index.ts +487 -157
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
|
+
}
|
|
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
|
+
}
|
|
340
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,159 +826,273 @@ export default class WIO {
|
|
|
610
826
|
getInjectedJavaScript(): string {
|
|
611
827
|
return `
|
|
612
828
|
(function() {
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
window._wio = {
|
|
616
|
-
type: 'EMBEDDED',
|
|
617
|
-
connected: false,
|
|
618
|
-
Events: {},
|
|
619
|
-
messageQueue: [],
|
|
829
|
+
try {
|
|
830
|
+
console.log('[EMBEDDED] Initializing WIO bridge...');
|
|
620
831
|
|
|
621
|
-
|
|
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
|
-
},
|
|
832
|
+
const RESERVED_EVENTS = ['ping', 'pong', '__heartbeat', '__heartbeat_response', '__embedded_ready', '__connection_ack', '__webview_ready'];
|
|
630
833
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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 ){
|
|
847
|
+
try {
|
|
848
|
+
const message = typeof event.data === 'string' ? JSON.parse( event.data ) : event.data;
|
|
849
|
+
window._wio.handleMessage( message );
|
|
637
850
|
}
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
}
|
|
851
|
+
catch( error ){
|
|
852
|
+
console.error('[EMBEDDED] Parse error:', error);
|
|
853
|
+
}
|
|
854
|
+
});
|
|
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
|
+
},
|
|
659
872
|
|
|
660
|
-
|
|
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
|
+
},
|
|
664
882
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
this.once( _event + '--' + cid + '--@ack', ({ error, args }) => fn( error, ...args ) );
|
|
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;
|
|
670
887
|
}
|
|
671
888
|
|
|
672
|
-
const
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
};
|
|
889
|
+
const ackFn = cid
|
|
890
|
+
? ( error, ...args ) => {
|
|
891
|
+
this.emit( _event + '--' + cid + '--@ack', { error: error || false, args } );
|
|
892
|
+
}
|
|
893
|
+
: undefined;
|
|
678
894
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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];
|
|
895
|
+
let listeners = [];
|
|
896
|
+
if( this.Events[_event + '--@once'] ){
|
|
897
|
+
_event += '--@once';
|
|
898
|
+
listeners = this.Events[_event];
|
|
899
|
+
delete this.Events[_event];
|
|
704
900
|
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
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
|
+
},
|
|
711
912
|
|
|
712
|
-
|
|
713
|
-
|
|
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 ) );
|
|
942
|
+
}
|
|
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
|
+
},
|
|
714
952
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
}
|
|
719
|
-
},
|
|
720
|
-
|
|
721
|
-
handleMessage: function( data ){
|
|
722
|
-
if( !data || !data._event ) return;
|
|
953
|
+
on: function( _event, fn ){
|
|
954
|
+
if( !this.Events[_event] ) this.Events[_event] = [];
|
|
955
|
+
this.Events[_event].push( fn );
|
|
956
|
+
},
|
|
723
957
|
|
|
724
|
-
|
|
958
|
+
once: function( _event, fn ){
|
|
959
|
+
_event += '--@once';
|
|
960
|
+
if( !this.Events[_event] ) this.Events[_event] = [];
|
|
961
|
+
this.Events[_event].push( fn );
|
|
962
|
+
},
|
|
725
963
|
|
|
726
|
-
|
|
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
|
+
},
|
|
727
974
|
|
|
728
|
-
|
|
729
|
-
this.
|
|
730
|
-
|
|
731
|
-
|
|
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
|
+
},
|
|
732
991
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
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;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
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
|
+
},
|
|
739
1041
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
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 );
|
|
1065
|
+
};
|
|
1066
|
+
|
|
1067
|
+
announce();
|
|
759
1068
|
}
|
|
760
|
-
|
|
761
|
-
|
|
1069
|
+
};
|
|
1070
|
+
|
|
1071
|
+
// Initialize
|
|
1072
|
+
window._wio.listen();
|
|
1073
|
+
|
|
1074
|
+
// Start announcing readiness after a short delay
|
|
1075
|
+
setTimeout(() => {
|
|
1076
|
+
window._wio.announceReady();
|
|
1077
|
+
}, 100);
|
|
1078
|
+
|
|
1079
|
+
console.log('[EMBEDDED] WIO bridge initialized successfully');
|
|
762
1080
|
}
|
|
763
|
-
|
|
1081
|
+
catch( error ){
|
|
1082
|
+
console.error('[EMBEDDED] Setup failed:', error);
|
|
1083
|
+
|
|
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
|
+
}
|
|
1093
|
+
|
|
764
1094
|
true;
|
|
765
1095
|
})();
|
|
766
|
-
|
|
1096
|
+
`
|
|
767
1097
|
}
|
|
768
1098
|
}
|