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