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/dist/index.d.ts +66 -11
- package/dist/index.js +435 -72
- package/package.json +1 -1
- package/src/index.ts +515 -90
package/src/index.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
|
|
2
1
|
export type PeerType = 'WINDOW' | 'IFRAME'
|
|
3
2
|
|
|
4
|
-
export type
|
|
5
|
-
export type Listener = ( payload?: any,
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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 =
|
|
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[] ){
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
//
|
|
134
|
-
this.fire(
|
|
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
|
-
|
|
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?:
|
|
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
|
|
152
|
-
( error: boolean | string, ...args: any[] ): void => {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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.
|
|
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?:
|
|
172
|
-
|
|
173
|
-
if( !this.
|
|
174
|
-
|
|
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 =
|
|
472
|
+
fn = payload as AckFunction
|
|
473
|
+
payload = undefined
|
|
179
474
|
}
|
|
180
475
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
187
|
-
|
|
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
|
-
|
|
218
|
-
|
|
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
|
}
|