iframe.io 1.0.2 → 1.0.4

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
@@ -1,12 +1,17 @@
1
-
2
1
  export type PeerType = 'WINDOW' | 'IFRAME'
3
2
 
4
- export type CallbackFunction = ( error: boolean | string, ...args: any[] ) => void
5
- export type Listener = ( payload?: any, callback?: CallbackFunction ) => void
3
+ export type AckFunction = ( error: boolean | string, ...args: any[] ) => void
4
+ export type Listener = ( payload?: any, ack?: AckFunction ) => void
6
5
 
7
6
  export type Options = {
8
7
  type?: PeerType
9
- // debug?: boolean
8
+ debug?: boolean
9
+ heartbeatInterval?: number
10
+ connectionTimeout?: number
11
+ maxMessageSize?: number
12
+ maxMessagesPerSecond?: number
13
+ autoReconnect?: boolean
14
+ messageQueueSize?: number
10
15
  }
11
16
 
12
17
  export interface RegisteredEvents {
@@ -17,12 +22,16 @@ export type Peer = {
17
22
  type: PeerType
18
23
  source?: Window
19
24
  origin?: string
25
+ connected?: boolean
26
+ lastHeartbeat?: number
20
27
  }
21
28
 
22
29
  export type MessageData = {
23
30
  _event: string
24
31
  payload: any
25
- cid: boolean
32
+ cid: string | undefined
33
+ timestamp?: number
34
+ size?: number
26
35
  }
27
36
 
28
37
  export type Message = {
@@ -31,65 +40,288 @@ export type Message = {
31
40
  source: Window
32
41
  }
33
42
 
43
+ export type QueuedMessage = {
44
+ _event: string
45
+ payload: any
46
+ fn?: AckFunction
47
+ timestamp: number
48
+ }
49
+
34
50
  function newObject( data: object ){
35
51
  return JSON.parse( JSON.stringify( data ) )
36
52
  }
37
53
 
38
- const callbackId = () => {
39
- const rmin = 10, rmax = 9999
40
- return Date.now() + String( Math.floor( Math.random() * ( rmax - rmin + 1 )+( rmin + 1 ) ) )
54
+ function getMessageSize( data: any ): number {
55
+ try {
56
+ return JSON.stringify( data ).length
57
+ } catch {
58
+ return 0
59
+ }
60
+ }
61
+
62
+ function sanitizePayload( payload: any, maxSize: number ): any {
63
+ if( !payload ) return payload
64
+
65
+ const size = getMessageSize( payload )
66
+ if( size > maxSize ) {
67
+ throw new Error(`Message size ${size} exceeds limit ${maxSize}`)
68
+ }
69
+
70
+ // Basic sanitization - remove functions and undefined values
71
+ return JSON.parse( JSON.stringify( payload ) )
72
+ }
73
+
74
+ const ackId = () => {
75
+ const rmin = 100000, rmax = 999999
76
+ const timestamp = Date.now()
77
+ const random = Math.floor( Math.random() * ( rmax - rmin + 1 ) + rmin )
78
+ return `${timestamp}_${random}`
41
79
  }
42
80
 
43
81
  export default class IOF {
44
82
  Events: RegisteredEvents
45
83
  peer: Peer
46
84
  options: Options
47
-
48
- constructor( options: Options ){
85
+ private messageListener?: (event: MessageEvent) => void
86
+ private heartbeatTimer?: NodeJS.Timeout
87
+ private reconnectTimer?: NodeJS.Timeout
88
+ private messageQueue: QueuedMessage[] = []
89
+ private messageRateTracker: number[] = []
90
+ private reconnectAttempts: number = 0
91
+ private maxReconnectAttempts: number = 5
92
+
93
+ constructor( options: Options = {} ){
49
94
  if( options && typeof options !== 'object' )
50
95
  throw new Error('Invalid Options')
51
96
 
52
- this.options = options
97
+ this.options = {
98
+ debug: false,
99
+ heartbeatInterval: 30000, // 30 seconds
100
+ connectionTimeout: 10000, // 10 seconds
101
+ maxMessageSize: 1024 * 1024, // 1MB
102
+ maxMessagesPerSecond: 100,
103
+ autoReconnect: true,
104
+ messageQueueSize: 50,
105
+ ...options
106
+ }
53
107
  this.Events = {}
54
- this.peer = { type: 'IFRAME' }
108
+ this.peer = { type: 'IFRAME', connected: false }
55
109
 
56
110
  if( options.type )
57
111
  this.peer.type = options.type.toUpperCase() as PeerType
58
112
  }
59
113
 
60
- debug( ...args: any[] ){ console.debug( ...args ) }
114
+ debug( ...args: any[] ){
115
+ this.options.debug && console.debug( ...args )
116
+ }
61
117
 
118
+ isConnected(): boolean {
119
+ return !!this.peer.connected && !!this.peer.source
120
+ }
121
+
122
+ // Enhanced connection health monitoring
123
+ private startHeartbeat(){
124
+ if( !this.options.heartbeatInterval ) return
125
+
126
+ this.heartbeatTimer = setInterval(() => {
127
+ if( this.isConnected() ){
128
+ const now = Date.now()
129
+
130
+ // Check if peer is still responsive
131
+ if( this.peer.lastHeartbeat && ( now - this.peer.lastHeartbeat ) > ( this.options.heartbeatInterval! * 2 ) ){
132
+ this.debug(`[${this.peer.type}] Heartbeat timeout detected`)
133
+ this.handleConnectionLoss()
134
+ return
135
+ }
136
+
137
+ // Send heartbeat
138
+ try { this.emit('__heartbeat', { timestamp: now }) }
139
+ catch( error ){
140
+ this.debug(`[${this.peer.type}] Heartbeat send failed:`, error )
141
+ this.handleConnectionLoss()
142
+ }
143
+ }
144
+ }, this.options.heartbeatInterval )
145
+ }
146
+ private stopHeartbeat(){
147
+ if( !this.heartbeatTimer ) return
148
+
149
+ clearInterval( this.heartbeatTimer )
150
+ this.heartbeatTimer = undefined
151
+ }
152
+
153
+ // Handle connection loss and potential reconnection
154
+ private handleConnectionLoss(){
155
+ if( !this.peer.connected ) return
156
+
157
+ this.peer.connected = false
158
+ this.stopHeartbeat()
159
+ this.fire('disconnect', { reason: 'CONNECTION_LOST' })
160
+
161
+ this.options.autoReconnect
162
+ && this.reconnectAttempts < this.maxReconnectAttempts
163
+ && this.attemptReconnection()
164
+ }
165
+ private attemptReconnection(){
166
+ if( this.reconnectTimer ) return
167
+
168
+ this.reconnectAttempts++
169
+ const delay = Math.min( 1000 * Math.pow( 2, this.reconnectAttempts - 1 ), 30000 ) // Exponential backoff, max 30s
170
+
171
+ this.debug(`[${this.peer.type}] Attempting reconnection ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay}ms`)
172
+ this.fire('reconnecting', { attempt: this.reconnectAttempts, delay })
173
+
174
+ this.reconnectTimer = setTimeout(() => {
175
+ this.reconnectTimer = undefined
176
+
177
+ // Re-initiate connection for WINDOW type
178
+ this.peer.type === 'WINDOW'
179
+ && this.peer.source
180
+ && this.peer.origin
181
+ && this.emit('ping')
182
+
183
+ // For IFRAME type, just wait for incoming connection
184
+
185
+ // Set timeout for this reconnection attempt
186
+ setTimeout( () => {
187
+ if( !this.peer.connected ){
188
+ this.reconnectAttempts < this.maxReconnectAttempts
189
+ ? this.attemptReconnection()
190
+ : this.fire('reconnection_failed', { attempts: this.reconnectAttempts })
191
+ }
192
+ }, this.options.connectionTimeout! )
193
+ }, delay )
194
+ }
195
+
196
+ // Message rate limiting
197
+ private checkRateLimit(): boolean {
198
+ if( !this.options.maxMessagesPerSecond ) return true
199
+
200
+ const
201
+ now = Date.now(),
202
+ aSecondAgo = now - 1000
203
+
204
+ // Clean old entries
205
+ this.messageRateTracker = this.messageRateTracker.filter( timestamp => timestamp > aSecondAgo )
206
+
207
+ // Check if limit exceeded
208
+ if( this.messageRateTracker.length >= this.options.maxMessagesPerSecond ){
209
+ this.fire('error', {
210
+ type: 'RATE_LIMIT_EXCEEDED',
211
+ limit: this.options.maxMessagesPerSecond,
212
+ current: this.messageRateTracker.length
213
+ })
214
+
215
+ return false
216
+ }
217
+
218
+ this.messageRateTracker.push( now )
219
+ return true
220
+ }
221
+ // Queue messages when not connected
222
+ private queueMessage( _event: string, payload?: any, fn?: AckFunction ){
223
+ if( this.messageQueue.length >= this.options.messageQueueSize! ){
224
+ // Remove oldest message
225
+ const removed = this.messageQueue.shift()
226
+ this.debug(`[${this.peer.type}] Message queue full, removed oldest message:`, removed?._event )
227
+ }
228
+
229
+ this.messageQueue.push({
230
+ _event,
231
+ payload,
232
+ fn,
233
+ timestamp: Date.now()
234
+ })
235
+
236
+ this.debug(`[${this.peer.type}] Queued message: ${_event} (queue size: ${this.messageQueue.length})`)
237
+ }
238
+
239
+ // Process queued messages when connection is established
240
+ private processMessageQueue(){
241
+ if( !this.isConnected() || this.messageQueue.length === 0 ) return
242
+
243
+ this.debug(`[${this.peer.type}] Processing ${this.messageQueue.length} queued messages`)
244
+
245
+ const queue = [...this.messageQueue]
246
+ this.messageQueue = []
247
+
248
+ queue.forEach( message => {
249
+ try { this.emit( message._event, message.payload, message.fn ) }
250
+ catch( error ){ this.debug(`[${this.peer.type}] Failed to send queued message:`, error ) }
251
+ })
252
+ }
253
+
254
+ /**
255
+ * Establish a connection with an iframe containing
256
+ * in the current window
257
+ */
62
258
  initiate( contentWindow: MessageEventSource, iframeOrigin: string ){
63
- // Establish a connection with an iframe containing in the current window
64
259
  if( !contentWindow || !iframeOrigin )
65
260
  throw new Error('Invalid Connection initiation arguments')
66
261
 
67
262
  if( this.peer.type === 'IFRAME' )
68
263
  throw new Error('Expect IFRAME to <listen> and WINDOW to <initiate> a connection')
69
264
 
265
+ // Clean up existing listener if any
266
+ this.cleanup()
267
+
70
268
  this.peer.source = contentWindow as Window
71
269
  this.peer.origin = iframeOrigin
270
+ this.peer.connected = false
271
+ this.reconnectAttempts = 0
72
272
 
73
- window.addEventListener( 'message', ({ origin, data, source }) => {
74
- // Check valid message
75
- if( origin !== this.peer.origin
76
- || !source
77
- || typeof data !== 'object'
78
- || !data.hasOwnProperty('_event') ) return
79
-
80
- const { _event, payload, cid } = data as Message['data']
81
- this.debug( `[${this.peer.type}] Message: ${_event}`, payload || '' )
82
-
83
- // Handshake or availability check events
84
- if( _event == 'pong' ){
85
- // Content Window is connected to iframe
86
- this.fire('connect')
87
- return this.debug(`[${this.peer.type}] connected`)
273
+ this.messageListener = ({ origin, data, source }) => {
274
+ try {
275
+ // Enhanced security: check valid message structure
276
+ if( origin !== this.peer.origin
277
+ || !source
278
+ || typeof data !== 'object'
279
+ || !data.hasOwnProperty('_event') ) return
280
+
281
+ const { _event, payload, cid, timestamp } = data as Message['data']
282
+
283
+ // Handle heartbeat responses
284
+ if( _event === '__heartbeat_response' ){
285
+ this.peer.lastHeartbeat = Date.now()
286
+ return
287
+ }
288
+
289
+ // Handle heartbeat requests
290
+ if( _event === '__heartbeat' ){
291
+ this.emit('__heartbeat_response', { timestamp: Date.now() })
292
+ this.peer.lastHeartbeat = Date.now()
293
+ return
294
+ }
295
+
296
+ this.debug( `[${this.peer.type}] Message: ${_event}`, payload || '' )
297
+
298
+ // Handshake or availability check events
299
+ if( _event == 'pong' ){
300
+ // Content Window is connected to iframe
301
+ this.peer.connected = true
302
+ this.reconnectAttempts = 0
303
+ this.peer.lastHeartbeat = Date.now()
304
+ this.startHeartbeat()
305
+ this.fire('connect')
306
+ this.processMessageQueue()
307
+
308
+ return this.debug(`[${this.peer.type}] connected`)
309
+ }
310
+
311
+ // Fire available event listeners
312
+ this.fire( _event, payload, cid )
88
313
  }
314
+ catch( error ){
315
+ this.debug(`[${this.peer.type}] Message handling error:`, error )
316
+ this.fire('error', {
317
+ type: 'MESSAGE_HANDLING_ERROR',
318
+ error: error instanceof Error ? error.message : String(error),
319
+ origin
320
+ })
321
+ }
322
+ }
89
323
 
90
- // Fire available event listeners
91
- this.fire( _event, payload, cid )
92
- }, false )
324
+ window.addEventListener( 'message', this.messageListener, false )
93
325
 
94
326
  this.debug(`[${this.peer.type}] Initiate connection: IFrame origin <${iframeOrigin}>`)
95
327
  this.emit('ping')
@@ -97,62 +329,105 @@ export default class IOF {
97
329
  return this
98
330
  }
99
331
 
332
+ /**
333
+ * Listening to connection from the content window
334
+ */
100
335
  listen( hostOrigin?: string ){
101
- // Listening to connection from the content window
102
-
103
336
  this.peer.type = 'IFRAME' // iframe.io connection listener is automatically set as IFRAME
337
+ this.peer.connected = false
338
+ this.reconnectAttempts = 0
104
339
  this.debug(`[${this.peer.type}] Listening to connect${hostOrigin ? `: Host <${hostOrigin}>` : ''}`)
105
340
 
106
- window.addEventListener( 'message', ({ origin, data, source }) => {
107
- // Check host origin where event must only come from.
108
- if( hostOrigin && hostOrigin !== origin )
109
- throw new Error('Invalid Event Origin')
110
-
111
- // Check valid message
112
- if( !source
113
- || typeof data !== 'object'
114
- || !data.hasOwnProperty('_event') ) return
115
-
116
- // Define peer source window and origin
117
- if( !this.peer.source ){
118
- this.peer = { ...this.peer, source: source as Window, origin }
119
- this.debug(`[${this.peer.type}] Connect to ${origin}`)
120
- }
121
-
122
- // Origin different from handshaked source origin
123
- else if( origin !== this.peer.origin )
124
- throw new Error('Invalid Origin')
125
-
126
- const { _event, payload, cid } = data
127
- this.debug( `[${this.peer.type}] Message: ${_event}`, payload || '' )
341
+ // Clean up existing listener if any
342
+ this.cleanup()
128
343
 
129
- // Handshake or availability check events
130
- if( _event == 'ping' ){
131
- this.emit('pong')
344
+ this.messageListener = ({ origin, data, source }) => {
345
+ try {
346
+ // Enhanced security: check host origin where event must only come from
347
+ if( hostOrigin && hostOrigin !== origin ){
348
+ this.fire('error', { type: 'INVALID_ORIGIN', expected: hostOrigin, received: origin })
349
+ return
350
+ }
351
+
352
+ // Enhanced security: check valid message structure
353
+ if( !source
354
+ || typeof data !== 'object'
355
+ || !data.hasOwnProperty('_event') ) return
356
+
357
+ // Define peer source window and origin
358
+ if( !this.peer.source ){
359
+ this.peer = { ...this.peer, source: source as Window, origin }
360
+ this.debug(`[${this.peer.type}] Connect to ${origin}`)
361
+ }
362
+
363
+ // Origin different from handshaked source origin
364
+ else if( origin !== this.peer.origin ){
365
+ this.fire('error', { type: 'ORIGIN_MISMATCH', expected: this.peer.origin, received: origin })
366
+ return
367
+ }
368
+
369
+ const { _event, payload, cid, timestamp } = data
370
+
371
+ // Handle heartbeat responses
372
+ if( _event === '__heartbeat_response' ){
373
+ this.peer.lastHeartbeat = Date.now()
374
+ return
375
+ }
376
+
377
+ // Handle heartbeat requests
378
+ if( _event === '__heartbeat' ){
379
+ this.emit('__heartbeat_response', { timestamp: Date.now() })
380
+ this.peer.lastHeartbeat = Date.now()
381
+ return
382
+ }
383
+
384
+ this.debug(`[${this.peer.type}] Message: ${_event}`, payload || '')
385
+
386
+ // Handshake or availability check events
387
+ if( _event == 'ping' ){
388
+ this.emit('pong')
389
+
390
+ // Iframe is connected to content window
391
+ this.peer.connected = true
392
+ this.reconnectAttempts = 0
393
+ this.peer.lastHeartbeat = Date.now()
394
+ this.startHeartbeat()
395
+ this.fire('connect')
396
+ this.processMessageQueue()
397
+
398
+ return this.debug(`[${this.peer.type}] connected`)
399
+ }
132
400
 
133
- // Iframe is connected to content window
134
- this.fire('connect')
135
- return this.debug(`[${this.peer.type}] connected`)
401
+ // Fire available event listeners
402
+ this.fire( _event, payload, cid )
136
403
  }
404
+ catch( error ){
405
+ this.debug(`[${this.peer.type}] Message handling error:`, error )
406
+ this.fire('error', {
407
+ type: 'MESSAGE_HANDLING_ERROR',
408
+ error: error instanceof Error ? error.message : String(error),
409
+ origin
410
+ })
411
+ }
412
+ }
137
413
 
138
- // Fire available event listeners
139
- this.fire( _event, payload, cid )
140
- }, false )
414
+ window.addEventListener( 'message', this.messageListener, false )
141
415
 
142
416
  return this
143
417
  }
144
418
 
145
- fire( _event: string, payload?: MessageData['payload'], cid?: boolean ){
146
- // Volatile event
419
+ fire( _event: string, payload?: MessageData['payload'], cid?: string ){
420
+ // Volatile event - check if any listeners exist
147
421
  if( !this.Events[ _event ]
148
422
  && !this.Events[ _event +'--@once'] )
149
423
  return this.debug(`[${this.peer.type}] No <${_event}> listener defined`)
150
424
 
151
- const callbackFn = cid ?
152
- ( error: boolean | string, ...args: any[] ): void => {
153
- this.emit(`${_event}--${cid}--@callback`, { error: error || false, args } )
154
- return
155
- } : undefined
425
+ const ackFn = cid
426
+ ? ( error: boolean | string, ...args: any[] ): void => {
427
+ this.emit(`${_event}--${cid}--@ack`, { error: error || false, args } )
428
+ return
429
+ }
430
+ : undefined
156
431
  let listeners: Listener[] = []
157
432
 
158
433
  if( this.Events[ _event +'--@once'] ){
@@ -164,30 +439,76 @@ export default class IOF {
164
439
  }
165
440
  else listeners = this.Events[ _event ]
166
441
 
167
- // Fire listeners
168
- listeners.map( fn => payload !== undefined ? fn( payload, callbackFn ) : fn( callbackFn ) )
442
+ // Fire listeners with error handling
443
+ listeners.forEach( fn => {
444
+ try { payload !== undefined ? fn( payload, ackFn ) : fn( ackFn ) }
445
+ catch( error ){
446
+ this.debug(`[${this.peer.type}] Listener error for ${_event}:`, error )
447
+ this.fire('error', {
448
+ type: 'LISTENER_ERROR',
449
+ event: _event,
450
+ error: error instanceof Error ? error.message : String(error)
451
+ })
452
+ }
453
+ })
169
454
  }
170
455
 
171
- emit( _event: string, payload?: MessageData['payload'], fn?: CallbackFunction ){
172
-
173
- if( !this.peer.source )
174
- throw new Error('No Connection initiated')
456
+ emit<T = any>( _event: string, payload?: T | AckFunction, fn?: AckFunction ){
457
+ // Check rate limiting
458
+ if( !this.checkRateLimit() ) return this
459
+
460
+ // Queue message if not connected (except for connection-related events)
461
+ if( !this.isConnected() && !['ping', 'pong', '__heartbeat', '__heartbeat_response'].includes(_event) ){
462
+ this.queueMessage( _event, payload, fn )
463
+ return this
464
+ }
465
+
466
+ if( !this.peer.source ){
467
+ this.fire('error', { type: 'NO_CONNECTION', event: _event })
468
+ return this
469
+ }
175
470
 
176
471
  if( typeof payload == 'function' ){
177
- fn = payload
178
- payload = null
472
+ fn = payload as AckFunction
473
+ payload = undefined
179
474
  }
180
475
 
181
- // Acknowledge/callback event listener
182
- let cid
183
- if( typeof fn === 'function' ){
184
- const callbackFunction = fn
476
+ try {
477
+ // Enhanced security: sanitize and validate payload
478
+ const sanitizedPayload = payload ? sanitizePayload( payload, this.options.maxMessageSize! ) : payload
479
+
480
+ // Acknowledge event listener
481
+ let cid: string | undefined
482
+ if( typeof fn === 'function' ){
483
+ const ackFunction = fn
185
484
 
186
- cid = callbackId()
187
- this.once(`${_event}--${cid}--@callback`, ({ error, args }) => callbackFunction( error, ...args ) )
485
+ cid = ackId()
486
+ this.once(`${_event}--${cid}--@ack`, ({ error, args }) => ackFunction( error, ...args ) )
487
+ }
488
+
489
+ const messageData = {
490
+ _event,
491
+ payload: sanitizedPayload,
492
+ cid,
493
+ timestamp: Date.now(),
494
+ size: getMessageSize( sanitizedPayload )
495
+ }
496
+
497
+ this.peer.source.postMessage( newObject( messageData ), this.peer.origin as string )
498
+ }
499
+ catch( error ){
500
+ this.debug(`[${this.peer.type}] Emit error:`, error )
501
+ this.fire('error', {
502
+ type: 'EMIT_ERROR',
503
+ event: _event,
504
+ error: error instanceof Error ? error.message : String(error)
505
+ })
506
+
507
+ // Call acknowledgment with error if provided
508
+ if( typeof fn === 'function' ){
509
+ fn( error instanceof Error ? error.message : String(error) )
510
+ }
188
511
  }
189
-
190
- this.peer.source.postMessage( newObject({ _event, payload, cid }), this.peer.origin as string )
191
512
 
192
513
  return this
193
514
  }
@@ -214,10 +535,22 @@ export default class IOF {
214
535
 
215
536
  off( _event: string, fn?: Listener ){
216
537
  // Remove Event listener
217
- delete this.Events[ _event ]
218
- typeof fn == 'function' && fn()
538
+ if( fn && this.Events[ _event ] ){
539
+ // Remove specific listener if provided
540
+ const index = this.Events[ _event ].indexOf( fn )
541
+ if( index > -1 ) {
542
+ this.Events[ _event ].splice( index, 1 )
543
+ // Remove event array if empty
544
+ if( this.Events[ _event ].length === 0 )
545
+ delete this.Events[ _event ]
546
+ }
547
+ }
548
+ // Remove all listeners for event
549
+ else delete this.Events[ _event ]
219
550
 
551
+ typeof fn == 'function' && fn()
220
552
  this.debug(`[${this.peer.type}] <${_event}> listener off`)
553
+
221
554
  return this
222
555
  }
223
556
 
@@ -229,4 +562,96 @@ export default class IOF {
229
562
  this.debug(`[${this.peer.type}] All listeners removed`)
230
563
  return this
231
564
  }
565
+
566
+ emitAsync<T = any, R = any>( _event: string, payload?: T ): Promise<R> {
567
+ return new Promise(( resolve, reject ) => {
568
+ try {
569
+ this.emit( _event, payload, ( error, ...args ) => {
570
+ error
571
+ ? reject( new Error( typeof error === 'string' ? error : 'Ack error' ) )
572
+ : resolve( args.length === 0 ? undefined : args.length === 1 ? args[0] : args )
573
+ })
574
+ }
575
+ catch( error ){ reject( error ) }
576
+ })
577
+ }
578
+
579
+ onceAsync<T = any>( _event: string ): Promise<T> {
580
+ return new Promise( resolve => this.once( _event, resolve ) )
581
+ }
582
+
583
+ connectAsync( timeout: number = 5000 ): Promise<void> {
584
+ return new Promise(( resolve, reject ) => {
585
+ if( this.isConnected() ) return resolve()
586
+
587
+ const timeoutId = setTimeout(() => {
588
+ this.off('connect', connectHandler )
589
+ reject( new Error('Connection timeout') )
590
+ }, timeout )
591
+
592
+ const connectHandler = () => {
593
+ clearTimeout( timeoutId )
594
+ resolve()
595
+ }
596
+
597
+ this.once('connect', connectHandler )
598
+ })
599
+ }
600
+
601
+ // Clean up all resources
602
+ private cleanup(){
603
+ if( this.messageListener ){
604
+ window.removeEventListener( 'message', this.messageListener )
605
+ this.messageListener = undefined
606
+ }
607
+
608
+ this.stopHeartbeat()
609
+
610
+ if( this.reconnectTimer ){
611
+ clearTimeout( this.reconnectTimer )
612
+ this.reconnectTimer = undefined
613
+ }
614
+ }
615
+
616
+ disconnect( fn?: () => void ){
617
+ // Clean disconnect method
618
+ this.cleanup()
619
+
620
+ this.peer.connected = false
621
+ this.peer.source = undefined
622
+ this.peer.origin = undefined
623
+ this.peer.lastHeartbeat = undefined
624
+ this.messageQueue = []
625
+ this.messageRateTracker = []
626
+ this.reconnectAttempts = 0
627
+ this.removeListeners()
628
+
629
+ typeof fn == 'function' && fn()
630
+ this.debug(`[${this.peer.type}] Disconnected`)
631
+
632
+ return this
633
+ }
634
+
635
+ // Get connection statistics
636
+ getStats(){
637
+ return {
638
+ connected: this.isConnected(),
639
+ peerType: this.peer.type,
640
+ origin: this.peer.origin,
641
+ lastHeartbeat: this.peer.lastHeartbeat,
642
+ queuedMessages: this.messageQueue.length,
643
+ reconnectAttempts: this.reconnectAttempts,
644
+ activeListeners: Object.keys( this.Events ).length,
645
+ messageRate: this.messageRateTracker.length
646
+ }
647
+ }
648
+
649
+ // Clear message queue manually
650
+ clearQueue(){
651
+ const queueSize = this.messageQueue.length
652
+ this.messageQueue = []
653
+
654
+ this.debug(`[${this.peer.type}] Cleared ${queueSize} queued messages`)
655
+ return this
656
+ }
232
657
  }