iframe.io 1.0.5 → 1.0.6

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/dist/index.d.ts CHANGED
@@ -75,7 +75,7 @@ export default class IOF {
75
75
  once(_event: string, fn: Listener): this;
76
76
  off(_event: string, fn?: Listener): this;
77
77
  removeListeners(fn?: Listener): this;
78
- emitAsync<T = any, R = any>(_event: string, payload?: T): Promise<R>;
78
+ emitAsync<T = any, R = any>(_event: string, payload?: T, timeout?: number): Promise<R>;
79
79
  onceAsync<T = any>(_event: string): Promise<T>;
80
80
  connectAsync(timeout?: number): Promise<void>;
81
81
  private cleanup;
package/dist/index.js CHANGED
@@ -35,18 +35,21 @@ function sanitizePayload(payload, maxSize) {
35
35
  if (!payload)
36
36
  return payload;
37
37
  var size = getMessageSize(payload);
38
- if (size > maxSize) {
38
+ if (size > maxSize)
39
39
  throw new Error("Message size ".concat(size, " exceeds limit ").concat(maxSize));
40
- }
41
40
  // Basic sanitization - remove functions and undefined values
42
41
  return JSON.parse(JSON.stringify(payload));
43
42
  }
44
43
  var ackId = function () {
45
- var rmin = 100000, rmax = 999999;
46
- var timestamp = Date.now();
47
- var random = Math.floor(Math.random() * (rmax - rmin + 1) + rmin);
44
+ var rmin = 100000, rmax = 999999, timestamp = Date.now(), random = Math.floor(Math.random() * (rmax - rmin + 1) + rmin);
48
45
  return "".concat(timestamp, "_").concat(random);
49
46
  };
47
+ var RESERVED_EVENTS = [
48
+ 'ping',
49
+ 'pong',
50
+ '__heartbeat',
51
+ '__heartbeat_response'
52
+ ];
50
53
  var IOF = /** @class */ (function () {
51
54
  function IOF(options) {
52
55
  if (options === void 0) { options = {}; }
@@ -81,7 +84,8 @@ var IOF = /** @class */ (function () {
81
84
  if (_this.isConnected()) {
82
85
  var now = Date.now();
83
86
  // Check if peer is still responsive
84
- if (_this.peer.lastHeartbeat && (now - _this.peer.lastHeartbeat) > (_this.options.heartbeatInterval * 2)) {
87
+ if (_this.peer.lastHeartbeat
88
+ && (now - _this.peer.lastHeartbeat) > (_this.options.heartbeatInterval * 2)) {
85
89
  _this.debug("[".concat(_this.peer.type, "] Heartbeat timeout detected"));
86
90
  _this.handleConnectionLoss();
87
91
  return;
@@ -132,11 +136,11 @@ var IOF = /** @class */ (function () {
132
136
  // For IFRAME type, just wait for incoming connection
133
137
  // Set timeout for this reconnection attempt
134
138
  setTimeout(function () {
135
- if (!_this.peer.connected) {
136
- _this.reconnectAttempts < _this.maxReconnectAttempts
137
- ? _this.attemptReconnection()
138
- : _this.fire('reconnection_failed', { attempts: _this.reconnectAttempts });
139
- }
139
+ if (_this.peer.connected)
140
+ return;
141
+ _this.reconnectAttempts < _this.maxReconnectAttempts
142
+ ? _this.attemptReconnection()
143
+ : _this.fire('reconnection_failed', { attempts: _this.reconnectAttempts });
140
144
  }, _this.options.connectionTimeout);
141
145
  }, delay);
142
146
  };
@@ -273,7 +277,11 @@ var IOF = /** @class */ (function () {
273
277
  try {
274
278
  // Enhanced security: check host origin where event must only come from
275
279
  if (hostOrigin && hostOrigin !== origin) {
276
- _this.fire('error', { type: 'INVALID_ORIGIN', expected: hostOrigin, received: origin });
280
+ _this.fire('error', {
281
+ type: 'INVALID_ORIGIN',
282
+ expected: hostOrigin,
283
+ received: origin
284
+ });
277
285
  return;
278
286
  }
279
287
  // Enhanced security: check valid message structure
@@ -288,7 +296,11 @@ var IOF = /** @class */ (function () {
288
296
  }
289
297
  // Origin different from handshaked source origin
290
298
  else if (origin !== _this.peer.origin) {
291
- _this.fire('error', { type: 'ORIGIN_MISMATCH', expected: _this.peer.origin, received: origin });
299
+ _this.fire('error', {
300
+ type: 'ORIGIN_MISMATCH',
301
+ expected: _this.peer.origin,
302
+ received: origin
303
+ });
292
304
  return;
293
305
  }
294
306
  var _event = data._event, payload = data.payload, cid = data.cid, timestamp = data.timestamp;
@@ -334,8 +346,7 @@ var IOF = /** @class */ (function () {
334
346
  IOF.prototype.fire = function (_event, payload, cid) {
335
347
  var _this = this;
336
348
  // Volatile event - check if any listeners exist
337
- if (!this.Events[_event]
338
- && !this.Events[_event + '--@once'])
349
+ if (!this.Events[_event] && !this.Events[_event + '--@once'])
339
350
  return this.debug("[".concat(this.peer.type, "] No <").concat(_event, "> listener defined"));
340
351
  var ackFn = cid
341
352
  ? function (error) {
@@ -376,8 +387,11 @@ var IOF = /** @class */ (function () {
376
387
  // Check rate limiting
377
388
  if (!this.checkRateLimit())
378
389
  return this;
379
- // Queue message if not connected (except for connection-related events)
380
- if (!this.isConnected() && !['ping', 'pong', '__heartbeat', '__heartbeat_response'].includes(_event)) {
390
+ /**
391
+ * Queue message if not connected: Except for
392
+ * connection-related events
393
+ */
394
+ if (!this.isConnected() && !RESERVED_EVENTS.includes(_event)) {
381
395
  this.queueMessage(_event, payload, fn);
382
396
  return this;
383
397
  }
@@ -391,7 +405,9 @@ var IOF = /** @class */ (function () {
391
405
  }
392
406
  try {
393
407
  // Enhanced security: sanitize and validate payload
394
- var sanitizedPayload = payload ? sanitizePayload(payload, this.options.maxMessageSize) : payload;
408
+ var sanitizedPayload = payload
409
+ ? sanitizePayload(payload, this.options.maxMessageSize)
410
+ : payload;
395
411
  // Acknowledge event listener
396
412
  var cid = void 0;
397
413
  if (typeof fn === 'function') {
@@ -419,9 +435,8 @@ var IOF = /** @class */ (function () {
419
435
  error: error instanceof Error ? error.message : String(error)
420
436
  });
421
437
  // Call acknowledgment with error if provided
422
- if (typeof fn === 'function') {
423
- fn(error instanceof Error ? error.message : String(error));
424
- }
438
+ typeof fn === 'function'
439
+ && fn(error instanceof Error ? error.message : String(error));
425
440
  }
426
441
  return this;
427
442
  };
@@ -468,21 +483,27 @@ var IOF = /** @class */ (function () {
468
483
  this.debug("[".concat(this.peer.type, "] All listeners removed"));
469
484
  return this;
470
485
  };
471
- IOF.prototype.emitAsync = function (_event, payload) {
486
+ IOF.prototype.emitAsync = function (_event, payload, timeout) {
472
487
  var _this = this;
488
+ if (timeout === void 0) { timeout = 5000; }
473
489
  return new Promise(function (resolve, reject) {
490
+ var timeoutId = setTimeout(function () {
491
+ reject(new Error("Event '".concat(_event, "' acknowledgment timeout after ").concat(timeout, "ms")));
492
+ }, timeout);
474
493
  try {
475
494
  _this.emit(_event, payload, function (error) {
476
495
  var args = [];
477
496
  for (var _i = 1; _i < arguments.length; _i++) {
478
497
  args[_i - 1] = arguments[_i];
479
498
  }
499
+ clearTimeout(timeoutId);
480
500
  error
481
501
  ? reject(new Error(typeof error === 'string' ? error : 'Ack error'))
482
502
  : resolve(args.length === 0 ? undefined : args.length === 1 ? args[0] : args);
483
503
  });
484
504
  }
485
505
  catch (error) {
506
+ clearTimeout(timeoutId);
486
507
  reject(error);
487
508
  }
488
509
  });
@@ -493,14 +514,13 @@ var IOF = /** @class */ (function () {
493
514
  };
494
515
  IOF.prototype.connectAsync = function (timeout) {
495
516
  var _this = this;
496
- if (timeout === void 0) { timeout = 5000; }
497
517
  return new Promise(function (resolve, reject) {
498
518
  if (_this.isConnected())
499
519
  return resolve();
500
520
  var timeoutId = setTimeout(function () {
501
521
  _this.off('connect', connectHandler);
502
522
  reject(new Error('Connection timeout'));
503
- }, timeout);
523
+ }, timeout || _this.options.connectionTimeout);
504
524
  var connectHandler = function () {
505
525
  clearTimeout(timeoutId);
506
526
  resolve();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iframe.io",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "Easy and friendly API to connect and interact between content window and its containing iframe",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
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' ){
@@ -312,16 +323,16 @@ export default class IOF {
312
323
  this.fire( _event, payload, cid )
313
324
  }
314
325
  catch( error ){
315
- this.debug(`[${this.peer.type}] Message handling error:`, error )
316
- this.fire('error', {
326
+ this.debug(`[${this.peer.type}] Message handling error:`, error)
327
+ this.fire('error', {
317
328
  type: 'MESSAGE_HANDLING_ERROR',
318
329
  error: error instanceof Error ? error.message : String(error),
319
- origin
330
+ origin
320
331
  })
321
332
  }
322
333
  }
323
334
 
324
- window.addEventListener( 'message', this.messageListener, false )
335
+ window.addEventListener('message', this.messageListener, false)
325
336
 
326
337
  this.debug(`[${this.peer.type}] Initiate connection: IFrame origin <${iframeOrigin}>`)
327
338
  this.emit('ping')
@@ -345,10 +356,14 @@ export default class IOF {
345
356
  try {
346
357
  // Enhanced security: check host origin where event must only come from
347
358
  if( hostOrigin && hostOrigin !== origin ){
348
- this.fire('error', { type: 'INVALID_ORIGIN', expected: hostOrigin, received: origin })
359
+ this.fire('error', {
360
+ type: 'INVALID_ORIGIN',
361
+ expected: hostOrigin,
362
+ received: origin
363
+ })
349
364
  return
350
365
  }
351
-
366
+
352
367
  // Enhanced security: check valid message structure
353
368
  if( !source
354
369
  || typeof data !== 'object'
@@ -362,25 +377,29 @@ export default class IOF {
362
377
 
363
378
  // Origin different from handshaked source origin
364
379
  else if( origin !== this.peer.origin ){
365
- this.fire('error', { type: 'ORIGIN_MISMATCH', expected: this.peer.origin, received: origin })
380
+ this.fire('error', {
381
+ type: 'ORIGIN_MISMATCH',
382
+ expected: this.peer.origin,
383
+ received: origin
384
+ })
366
385
  return
367
386
  }
368
-
387
+
369
388
  const { _event, payload, cid, timestamp } = data
370
-
389
+
371
390
  // Handle heartbeat responses
372
391
  if( _event === '__heartbeat_response' ){
373
392
  this.peer.lastHeartbeat = Date.now()
374
393
  return
375
394
  }
376
-
395
+
377
396
  // Handle heartbeat requests
378
397
  if( _event === '__heartbeat' ){
379
398
  this.emit('__heartbeat_response', { timestamp: Date.now() })
380
399
  this.peer.lastHeartbeat = Date.now()
381
400
  return
382
401
  }
383
-
402
+
384
403
  this.debug(`[${this.peer.type}] Message: ${_event}`, payload || '')
385
404
 
386
405
  // Handshake or availability check events
@@ -394,7 +413,7 @@ export default class IOF {
394
413
  this.startHeartbeat()
395
414
  this.fire('connect')
396
415
  this.processMessageQueue()
397
-
416
+
398
417
  return this.debug(`[${this.peer.type}] connected`)
399
418
  }
400
419
 
@@ -402,49 +421,48 @@ export default class IOF {
402
421
  this.fire( _event, payload, cid )
403
422
  }
404
423
  catch( error ){
405
- this.debug(`[${this.peer.type}] Message handling error:`, error )
406
- this.fire('error', {
424
+ this.debug(`[${this.peer.type}] Message handling error:`, error)
425
+ this.fire('error', {
407
426
  type: 'MESSAGE_HANDLING_ERROR',
408
427
  error: error instanceof Error ? error.message : String(error),
409
- origin
428
+ origin
410
429
  })
411
430
  }
412
431
  }
413
432
 
414
- window.addEventListener( 'message', this.messageListener, false )
433
+ window.addEventListener('message', this.messageListener, false)
415
434
 
416
435
  return this
417
436
  }
418
437
 
419
438
  fire( _event: string, payload?: MessageData['payload'], cid?: string ){
420
439
  // Volatile event - check if any listeners exist
421
- if( !this.Events[ _event ]
422
- && !this.Events[ _event +'--@once'] )
440
+ if( !this.Events[_event] && !this.Events[_event + '--@once'] )
423
441
  return this.debug(`[${this.peer.type}] No <${_event}> listener defined`)
424
442
 
425
443
  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
444
+ ? ( error: boolean | string, ...args: any[] ): void => {
445
+ this.emit(`${_event}--${cid}--@ack`, { error: error || false, args })
446
+ return
447
+ }
448
+ : undefined
431
449
  let listeners: Listener[] = []
432
450
 
433
- if( this.Events[ _event +'--@once'] ){
451
+ if( this.Events[_event + '--@once'] ){
434
452
  // Once triggable event
435
453
  _event += '--@once'
436
- listeners = this.Events[ _event ]
454
+ listeners = this.Events[_event]
437
455
  // Delete once event listeners after fired
438
- delete this.Events[ _event ]
456
+ delete this.Events[_event]
439
457
  }
440
- else listeners = this.Events[ _event ]
441
-
458
+ else listeners = this.Events[_event]
459
+
442
460
  // Fire listeners with error handling
443
461
  listeners.forEach( fn => {
444
462
  try { payload !== undefined ? fn( payload, ackFn ) : fn( ackFn ) }
445
463
  catch( error ){
446
- this.debug(`[${this.peer.type}] Listener error for ${_event}:`, error )
447
- this.fire('error', {
464
+ this.debug(`[${this.peer.type}] Listener error for ${_event}:`, error)
465
+ this.fire('error', {
448
466
  type: 'LISTENER_ERROR',
449
467
  event: _event,
450
468
  error: error instanceof Error ? error.message : String(error)
@@ -456,123 +474,137 @@ export default class IOF {
456
474
  emit<T = any>( _event: string, payload?: T | AckFunction, fn?: AckFunction ){
457
475
  // Check rate limiting
458
476
  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) ){
477
+
478
+ /**
479
+ * Queue message if not connected: Except for
480
+ * connection-related events
481
+ */
482
+ if( !this.isConnected() && !RESERVED_EVENTS.includes(_event) ){
462
483
  this.queueMessage( _event, payload, fn )
463
484
  return this
464
485
  }
465
-
486
+
466
487
  if( !this.peer.source ){
467
488
  this.fire('error', { type: 'NO_CONNECTION', event: _event })
468
489
  return this
469
490
  }
470
491
 
471
- if( typeof payload == 'function' ){
472
- fn = payload as AckFunction
473
- payload = undefined
474
- }
492
+ if( typeof payload == 'function' ){
493
+ fn = payload as AckFunction
494
+ payload = undefined
495
+ }
475
496
 
476
497
  try {
477
498
  // Enhanced security: sanitize and validate payload
478
- const sanitizedPayload = payload ? sanitizePayload( payload, this.options.maxMessageSize! ) : payload
479
-
499
+ const sanitizedPayload = payload
500
+ ? sanitizePayload( payload, this.options.maxMessageSize! )
501
+ : payload
502
+
480
503
  // Acknowledge event listener
481
504
  let cid: string | undefined
482
505
  if( typeof fn === 'function' ){
483
506
  const ackFunction = fn
484
507
 
485
508
  cid = ackId()
486
- this.once(`${_event}--${cid}--@ack`, ({ error, args }) => ackFunction( error, ...args ) )
509
+ this.once(`${_event}--${cid}--@ack`, ({ error, args }) => ackFunction( error, ...args ))
487
510
  }
488
-
489
- const messageData = {
490
- _event,
491
- payload: sanitizedPayload,
511
+
512
+ const messageData = {
513
+ _event,
514
+ payload: sanitizedPayload,
492
515
  cid,
493
516
  timestamp: Date.now(),
494
517
  size: getMessageSize( sanitizedPayload )
495
518
  }
496
-
519
+
497
520
  this.peer.source.postMessage( newObject( messageData ), this.peer.origin as string )
498
521
  }
499
522
  catch( error ){
500
- this.debug(`[${this.peer.type}] Emit error:`, error )
501
- this.fire('error', {
523
+ this.debug(`[${this.peer.type}] Emit error:`, error)
524
+ this.fire('error', {
502
525
  type: 'EMIT_ERROR',
503
526
  event: _event,
504
527
  error: error instanceof Error ? error.message : String(error)
505
528
  })
506
-
529
+
507
530
  // Call acknowledgment with error if provided
508
- if( typeof fn === 'function' ){
509
- fn( error instanceof Error ? error.message : String(error) )
510
- }
531
+ typeof fn === 'function'
532
+ && fn( error instanceof Error ? error.message : String(error) )
511
533
  }
512
534
 
513
- return this
535
+ return this
514
536
  }
515
-
537
+
516
538
  on( _event: string, fn: Listener ){
517
- // Add Event listener
518
- if( !this.Events[ _event ] ) this.Events[ _event ] = []
519
- this.Events[ _event ].push( fn )
520
-
539
+ // Add Event listener
540
+ if( !this.Events[_event] ) this.Events[_event] = []
541
+ this.Events[_event].push( fn )
542
+
521
543
  this.debug(`[${this.peer.type}] New <${_event}> listener on`)
522
- return this
523
- }
524
-
544
+ return this
545
+ }
546
+
525
547
  once( _event: string, fn: Listener ){
526
- // Add Once Event listener
548
+ // Add Once Event listener
527
549
  _event += '--@once'
528
550
 
529
- if( !this.Events[ _event ] ) this.Events[ _event ] = []
530
- this.Events[ _event ].push( fn )
531
-
551
+ if( !this.Events[_event] ) this.Events[_event] = []
552
+ this.Events[_event].push( fn )
553
+
532
554
  this.debug(`[${this.peer.type}] New <${_event} once> listener on`)
533
- return this
534
- }
555
+ return this
556
+ }
535
557
 
536
- off( _event: string, fn?: Listener ){
537
- // Remove Event listener
538
- if( fn && this.Events[ _event ] ){
558
+ off( _event: string, fn?: Listener ){
559
+ // Remove Event listener
560
+ if( fn && this.Events[_event] ){
539
561
  // Remove specific listener if provided
540
- const index = this.Events[ _event ].indexOf( fn )
541
- if( index > -1 ) {
542
- this.Events[ _event ].splice( index, 1 )
562
+ const index = this.Events[_event].indexOf( fn )
563
+ if( index > -1 ){
564
+ this.Events[_event].splice( index, 1 )
565
+
543
566
  // Remove event array if empty
544
- if( this.Events[ _event ].length === 0 )
545
- delete this.Events[ _event ]
567
+ if( this.Events[_event].length === 0 )
568
+ delete this.Events[_event]
546
569
  }
547
570
  }
548
571
  // Remove all listeners for event
549
- else delete this.Events[ _event ]
550
-
572
+ else delete this.Events[_event]
573
+
551
574
  typeof fn == 'function' && fn()
552
575
  this.debug(`[${this.peer.type}] <${_event}> listener off`)
553
576
 
554
- return this
555
- }
577
+ return this
578
+ }
556
579
 
557
- removeListeners( fn?: Listener ){
580
+ removeListeners( fn?: Listener ){
558
581
  // Clear all event listeners
559
- this.Events = {}
560
- typeof fn == 'function' && fn()
582
+ this.Events = {}
583
+ typeof fn == 'function' && fn()
561
584
 
562
585
  this.debug(`[${this.peer.type}] All listeners removed`)
563
- return this
564
- }
586
+ return this
587
+ }
565
588
 
566
- emitAsync<T = any, R = any>( _event: string, payload?: T ): Promise<R> {
589
+ emitAsync<T = any, R = any>( _event: string, payload?: T, timeout: number = 5000 ): Promise<R> {
567
590
  return new Promise(( resolve, reject ) => {
591
+ const timeoutId = setTimeout(() => {
592
+ reject( new Error(`Event '${_event}' acknowledgment timeout after ${timeout}ms`) )
593
+ }, timeout)
594
+
568
595
  try {
569
596
  this.emit( _event, payload, ( error, ...args ) => {
597
+ clearTimeout( timeoutId )
598
+
570
599
  error
571
600
  ? reject( new Error( typeof error === 'string' ? error : 'Ack error' ) )
572
601
  : resolve( args.length === 0 ? undefined : args.length === 1 ? args[0] : args )
573
602
  })
574
603
  }
575
- catch( error ){ reject( error ) }
604
+ catch( error ){
605
+ clearTimeout( timeoutId )
606
+ reject( error )
607
+ }
576
608
  })
577
609
  }
578
610
 
@@ -580,33 +612,33 @@ export default class IOF {
580
612
  return new Promise( resolve => this.once( _event, resolve ) )
581
613
  }
582
614
 
583
- connectAsync( timeout: number = 5000 ): Promise<void> {
615
+ connectAsync( timeout?: number ): Promise<void> {
584
616
  return new Promise(( resolve, reject ) => {
585
617
  if( this.isConnected() ) return resolve()
586
618
 
587
619
  const timeoutId = setTimeout(() => {
588
- this.off('connect', connectHandler )
620
+ this.off('connect', connectHandler)
589
621
  reject( new Error('Connection timeout') )
590
- }, timeout )
622
+ }, timeout || this.options.connectionTimeout)
591
623
 
592
624
  const connectHandler = () => {
593
625
  clearTimeout( timeoutId )
594
626
  resolve()
595
627
  }
596
628
 
597
- this.once('connect', connectHandler )
629
+ this.once('connect', connectHandler)
598
630
  })
599
631
  }
600
632
 
601
633
  // Clean up all resources
602
634
  private cleanup(){
603
635
  if( this.messageListener ){
604
- window.removeEventListener( 'message', this.messageListener )
636
+ window.removeEventListener('message', this.messageListener)
605
637
  this.messageListener = undefined
606
638
  }
607
-
639
+
608
640
  this.stopHeartbeat()
609
-
641
+
610
642
  if( this.reconnectTimer ){
611
643
  clearTimeout( this.reconnectTimer )
612
644
  this.reconnectTimer = undefined
@@ -616,7 +648,7 @@ export default class IOF {
616
648
  disconnect( fn?: () => void ){
617
649
  // Clean disconnect method
618
650
  this.cleanup()
619
-
651
+
620
652
  this.peer.connected = false
621
653
  this.peer.source = undefined
622
654
  this.peer.origin = undefined
@@ -625,10 +657,10 @@ export default class IOF {
625
657
  this.messageRateTracker = []
626
658
  this.reconnectAttempts = 0
627
659
  this.removeListeners()
628
-
660
+
629
661
  typeof fn == 'function' && fn()
630
662
  this.debug(`[${this.peer.type}] Disconnected`)
631
-
663
+
632
664
  return this
633
665
  }
634
666
 
@@ -654,4 +686,4 @@ export default class IOF {
654
686
  this.debug(`[${this.peer.type}] Cleared ${queueSize} queued messages`)
655
687
  return this
656
688
  }
657
- }
689
+ }