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/README.md +260 -324
- package/dist/index.d.ts +1 -1
- package/dist/index.js +52 -28
- package/package.json +1 -1
- package/src/index.ts +204 -165
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
|
-
|
|
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
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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', {
|
|
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', {
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
423
|
-
return
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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[
|
|
457
|
+
if( this.Events[_event + '--@once'] ){
|
|
434
458
|
// Once triggable event
|
|
435
459
|
_event += '--@once'
|
|
436
|
-
listeners = this.Events[
|
|
460
|
+
listeners = this.Events[_event]
|
|
437
461
|
// Delete once event listeners after fired
|
|
438
|
-
delete this.Events[
|
|
462
|
+
delete this.Events[_event]
|
|
439
463
|
}
|
|
440
|
-
else listeners = this.Events[
|
|
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
|
-
|
|
461
|
-
|
|
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
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
509
|
-
|
|
510
|
-
}
|
|
537
|
+
typeof fn === 'function'
|
|
538
|
+
&& fn( error instanceof Error ? error.message : String(error) )
|
|
511
539
|
}
|
|
512
540
|
|
|
513
|
-
|
|
541
|
+
return this
|
|
514
542
|
}
|
|
515
|
-
|
|
543
|
+
|
|
516
544
|
on( _event: string, fn: Listener ){
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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
|
-
|
|
523
|
-
|
|
524
|
-
|
|
550
|
+
return this
|
|
551
|
+
}
|
|
552
|
+
|
|
525
553
|
once( _event: string, fn: Listener ){
|
|
526
|
-
|
|
554
|
+
// Add Once Event listener
|
|
527
555
|
_event += '--@once'
|
|
528
556
|
|
|
529
|
-
|
|
530
|
-
|
|
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
|
-
|
|
534
|
-
|
|
561
|
+
return this
|
|
562
|
+
}
|
|
535
563
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
if( fn && this.Events[
|
|
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[
|
|
541
|
-
if( index > -1 )
|
|
542
|
-
this.Events[
|
|
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[
|
|
545
|
-
delete this.Events[
|
|
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[
|
|
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
|
-
|
|
555
|
-
|
|
583
|
+
return this
|
|
584
|
+
}
|
|
556
585
|
|
|
557
|
-
|
|
586
|
+
removeListeners( fn?: Listener ){
|
|
558
587
|
// Clear all event listeners
|
|
559
|
-
|
|
560
|
-
|
|
588
|
+
this.Events = {}
|
|
589
|
+
typeof fn == 'function' && fn()
|
|
561
590
|
|
|
562
591
|
this.debug(`[${this.peer.type}] All listeners removed`)
|
|
563
|
-
|
|
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 ){
|
|
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
|
|
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(
|
|
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
|
-
//
|
|
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
|
+
}
|