iframe.io 1.0.4 → 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/README.md +458 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +44 -24
- package/package.json +7 -1
- package/src/index.ts +193 -161
package/README.md
CHANGED
|
@@ -1,2 +1,459 @@
|
|
|
1
1
|
# iframe.io
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
Easy and friendly API to connect and interact between content window and its containing iframe with enhanced features like heartbeat monitoring, automatic reconnection, message queuing, and rate limiting.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🔄 **Bi-directional Communication** - Seamless messaging between parent window and iframe
|
|
8
|
+
- 💓 **Heartbeat Monitoring** - Automatic connection health checking
|
|
9
|
+
- 🔌 **Auto Reconnection** - Automatic reconnection with exponential backoff
|
|
10
|
+
- 📦 **Message Queuing** - Queue messages when connection is lost
|
|
11
|
+
- 🛡️ **Security** - Origin validation and message sanitization
|
|
12
|
+
- ⚡ **Rate Limiting** - Prevent message flooding
|
|
13
|
+
- 🎯 **Event System** - Robust event-driven architecture
|
|
14
|
+
- ✨ **Promise Support** - Async/await compatible methods
|
|
15
|
+
- 📊 **Connection Stats** - Real-time connection statistics
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install iframe.io
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
### Parent Window (WINDOW type)
|
|
26
|
+
|
|
27
|
+
```javascript
|
|
28
|
+
import IOF from 'iframe.io'
|
|
29
|
+
|
|
30
|
+
// Create iframe and get reference
|
|
31
|
+
const iframe = document.createElement('iframe')
|
|
32
|
+
iframe.src = 'https://example.com/iframe-content'
|
|
33
|
+
document.body.appendChild(iframe)
|
|
34
|
+
|
|
35
|
+
// Initialize connection
|
|
36
|
+
const iof = new IOF({ type: 'WINDOW', debug: true })
|
|
37
|
+
|
|
38
|
+
iframe.onload = () => {
|
|
39
|
+
// Initiate connection with iframe
|
|
40
|
+
iof.initiate(iframe.contentWindow, 'https://example.com')
|
|
41
|
+
|
|
42
|
+
// Listen for connection
|
|
43
|
+
iof.on('connect', () => {
|
|
44
|
+
console.log('Connected to iframe!')
|
|
45
|
+
|
|
46
|
+
// Send message
|
|
47
|
+
iof.emit('hello', { message: 'Hello from parent!' })
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// Listen for messages
|
|
51
|
+
iof.on('response', (data) => {
|
|
52
|
+
console.log('Received:', data)
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Iframe Content (IFRAME type)
|
|
58
|
+
|
|
59
|
+
```javascript
|
|
60
|
+
import IOF from 'iframe.io'
|
|
61
|
+
|
|
62
|
+
// Create connection listener
|
|
63
|
+
const iof = new IOF({ type: 'IFRAME', debug: true })
|
|
64
|
+
|
|
65
|
+
// Listen for parent connection
|
|
66
|
+
iof.listen('https://parent-domain.com')
|
|
67
|
+
|
|
68
|
+
// Handle connection
|
|
69
|
+
iof.on('connect', () => {
|
|
70
|
+
console.log('Connected to parent!')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
// Listen for messages
|
|
74
|
+
iof.on('hello', (data, ack) => {
|
|
75
|
+
console.log('Received from parent:', data)
|
|
76
|
+
|
|
77
|
+
// Send response
|
|
78
|
+
iof.emit('response', { message: 'Hello from iframe!' })
|
|
79
|
+
|
|
80
|
+
// Acknowledge receipt (optional)
|
|
81
|
+
if (ack) ack(false, 'Message received')
|
|
82
|
+
})
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## API Reference
|
|
86
|
+
|
|
87
|
+
### Constructor
|
|
88
|
+
|
|
89
|
+
```javascript
|
|
90
|
+
new IOF(options?)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
#### Options
|
|
94
|
+
|
|
95
|
+
| Option | Type | Default | Description |
|
|
96
|
+
|--------|------|---------|-------------|
|
|
97
|
+
| `type` | `'WINDOW' \| 'IFRAME'` | `'IFRAME'` | Connection type |
|
|
98
|
+
| `debug` | `boolean` | `false` | Enable debug logging |
|
|
99
|
+
| `heartbeatInterval` | `number` | `30000` | Heartbeat interval in ms |
|
|
100
|
+
| `connectionTimeout` | `number` | `10000` | Connection timeout in ms |
|
|
101
|
+
| `maxMessageSize` | `number` | `1048576` | Max message size (1MB) |
|
|
102
|
+
| `maxMessagesPerSecond` | `number` | `100` | Rate limit for messages |
|
|
103
|
+
| `autoReconnect` | `boolean` | `true` | Enable auto reconnection |
|
|
104
|
+
| `messageQueueSize` | `number` | `50` | Max queued messages |
|
|
105
|
+
|
|
106
|
+
### Connection Methods
|
|
107
|
+
|
|
108
|
+
#### `initiate(contentWindow, iframeOrigin)`
|
|
109
|
+
|
|
110
|
+
Establish connection with an iframe (WINDOW type only).
|
|
111
|
+
|
|
112
|
+
```javascript
|
|
113
|
+
iof.initiate(iframe.contentWindow, 'https://iframe-origin.com')
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Parameters:**
|
|
117
|
+
- `contentWindow` - The iframe's contentWindow
|
|
118
|
+
- `iframeOrigin` - The iframe's origin URL
|
|
119
|
+
|
|
120
|
+
#### `listen(hostOrigin?)`
|
|
121
|
+
|
|
122
|
+
Listen for connection from parent window (IFRAME type).
|
|
123
|
+
|
|
124
|
+
```javascript
|
|
125
|
+
iof.listen('https://parent-origin.com') // Optional origin restriction
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**Parameters:**
|
|
129
|
+
- `hostOrigin` (optional) - Restrict connections to specific origin
|
|
130
|
+
|
|
131
|
+
#### `disconnect(callback?)`
|
|
132
|
+
|
|
133
|
+
Disconnect and clean up all resources.
|
|
134
|
+
|
|
135
|
+
```javascript
|
|
136
|
+
iof.disconnect(() => {
|
|
137
|
+
console.log('Disconnected!')
|
|
138
|
+
})
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Messaging Methods
|
|
142
|
+
|
|
143
|
+
#### `emit(event, payload?, callback?)`
|
|
144
|
+
|
|
145
|
+
Send a message to the peer.
|
|
146
|
+
|
|
147
|
+
```javascript
|
|
148
|
+
// Simple message
|
|
149
|
+
iof.emit('myEvent', { data: 'hello' })
|
|
150
|
+
|
|
151
|
+
// With acknowledgment
|
|
152
|
+
iof.emit('myEvent', { data: 'hello' }, (error, response) => {
|
|
153
|
+
if (error) {
|
|
154
|
+
console.error('Error:', error)
|
|
155
|
+
} else {
|
|
156
|
+
console.log('Response:', response)
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
// Callback as second parameter
|
|
161
|
+
iof.emit('myEvent', (error, response) => {
|
|
162
|
+
console.log('Response:', response)
|
|
163
|
+
})
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
#### `on(event, listener)`
|
|
167
|
+
|
|
168
|
+
Add event listener.
|
|
169
|
+
|
|
170
|
+
```javascript
|
|
171
|
+
iof.on('myEvent', (data, ack) => {
|
|
172
|
+
console.log('Received:', data)
|
|
173
|
+
|
|
174
|
+
// Send acknowledgment (optional)
|
|
175
|
+
if (ack) ack(false, 'Success response')
|
|
176
|
+
})
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
#### `once(event, listener)`
|
|
180
|
+
|
|
181
|
+
Add one-time event listener.
|
|
182
|
+
|
|
183
|
+
```javascript
|
|
184
|
+
iof.once('myEvent', (data) => {
|
|
185
|
+
console.log('This will only fire once:', data)
|
|
186
|
+
})
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
#### `off(event, listener?)`
|
|
190
|
+
|
|
191
|
+
Remove event listener(s).
|
|
192
|
+
|
|
193
|
+
```javascript
|
|
194
|
+
// Remove specific listener
|
|
195
|
+
iof.off('myEvent', myListener)
|
|
196
|
+
|
|
197
|
+
// Remove all listeners for event
|
|
198
|
+
iof.off('myEvent')
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
#### `removeListeners(callback?)`
|
|
202
|
+
|
|
203
|
+
Remove all event listeners.
|
|
204
|
+
|
|
205
|
+
```javascript
|
|
206
|
+
iof.removeListeners(() => {
|
|
207
|
+
console.log('All listeners removed')
|
|
208
|
+
})
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Async Methods
|
|
212
|
+
|
|
213
|
+
#### `emitAsync(event, payload?)`
|
|
214
|
+
|
|
215
|
+
Send message and return Promise.
|
|
216
|
+
|
|
217
|
+
```javascript
|
|
218
|
+
try {
|
|
219
|
+
const response = await iof.emitAsync('getData', { id: 123 })
|
|
220
|
+
console.log('Response:', response)
|
|
221
|
+
} catch (error) {
|
|
222
|
+
console.error('Error:', error)
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
#### `onceAsync(event)`
|
|
227
|
+
|
|
228
|
+
Wait for single event occurrence.
|
|
229
|
+
|
|
230
|
+
```javascript
|
|
231
|
+
const data = await iof.onceAsync('dataReady')
|
|
232
|
+
console.log('Data received:', data)
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
#### `connectAsync(timeout?)`
|
|
236
|
+
|
|
237
|
+
Wait for connection to be established.
|
|
238
|
+
|
|
239
|
+
```javascript
|
|
240
|
+
try {
|
|
241
|
+
await iof.connectAsync(5000) // 5 second timeout
|
|
242
|
+
console.log('Connected!')
|
|
243
|
+
} catch (error) {
|
|
244
|
+
console.error('Connection timeout')
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Utility Methods
|
|
249
|
+
|
|
250
|
+
#### `isConnected()`
|
|
251
|
+
|
|
252
|
+
Check if connection is active.
|
|
253
|
+
|
|
254
|
+
```javascript
|
|
255
|
+
if (iof.isConnected()) {
|
|
256
|
+
console.log('Connection is active')
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
#### `getStats()`
|
|
261
|
+
|
|
262
|
+
Get connection statistics.
|
|
263
|
+
|
|
264
|
+
```javascript
|
|
265
|
+
const stats = iof.getStats()
|
|
266
|
+
console.log('Stats:', stats)
|
|
267
|
+
/*
|
|
268
|
+
{
|
|
269
|
+
connected: true,
|
|
270
|
+
peerType: 'WINDOW',
|
|
271
|
+
origin: 'https://example.com',
|
|
272
|
+
lastHeartbeat: 1640995200000,
|
|
273
|
+
queuedMessages: 0,
|
|
274
|
+
reconnectAttempts: 0,
|
|
275
|
+
activeListeners: 5,
|
|
276
|
+
messageRate: 10
|
|
277
|
+
}
|
|
278
|
+
*/
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
#### `clearQueue()`
|
|
282
|
+
|
|
283
|
+
Clear queued messages.
|
|
284
|
+
|
|
285
|
+
```javascript
|
|
286
|
+
iof.clearQueue()
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## Events
|
|
290
|
+
|
|
291
|
+
### Built-in Events
|
|
292
|
+
|
|
293
|
+
| Event | Description | Data |
|
|
294
|
+
|-------|-------------|------|
|
|
295
|
+
| `connect` | Connection established | - |
|
|
296
|
+
| `disconnect` | Connection lost | `{ reason: string }` |
|
|
297
|
+
| `reconnecting` | Reconnection attempt | `{ attempt: number, delay: number }` |
|
|
298
|
+
| `reconnection_failed` | All reconnection attempts failed | `{ attempts: number }` |
|
|
299
|
+
| `error` | Error occurred | `{ type: string, ...details }` |
|
|
300
|
+
|
|
301
|
+
### Error Types
|
|
302
|
+
|
|
303
|
+
- `RATE_LIMIT_EXCEEDED` - Message rate limit exceeded
|
|
304
|
+
- `MESSAGE_HANDLING_ERROR` - Error processing received message
|
|
305
|
+
- `EMIT_ERROR` - Error sending message
|
|
306
|
+
- `LISTENER_ERROR` - Error in event listener
|
|
307
|
+
- `INVALID_ORIGIN` - Message from unauthorized origin
|
|
308
|
+
- `ORIGIN_MISMATCH` - Origin doesn't match established connection
|
|
309
|
+
- `NO_CONNECTION` - Attempted to send without connection
|
|
310
|
+
|
|
311
|
+
## Advanced Usage
|
|
312
|
+
|
|
313
|
+
### Message Acknowledgments
|
|
314
|
+
|
|
315
|
+
```javascript
|
|
316
|
+
// Sender
|
|
317
|
+
iof.emit('processData', { data: [...] }, (error, result) => {
|
|
318
|
+
if (error) {
|
|
319
|
+
console.error('Processing failed:', error)
|
|
320
|
+
} else {
|
|
321
|
+
console.log('Processing result:', result)
|
|
322
|
+
}
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
// Receiver
|
|
326
|
+
iof.on('processData', async (data, ack) => {
|
|
327
|
+
try {
|
|
328
|
+
const result = await processData(data.data)
|
|
329
|
+
ack(false, result) // Success: ack(error, ...args)
|
|
330
|
+
} catch (error) {
|
|
331
|
+
ack(error.message) // Error: ack(errorMessage)
|
|
332
|
+
}
|
|
333
|
+
})
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### Connection Monitoring
|
|
337
|
+
|
|
338
|
+
```javascript
|
|
339
|
+
const iof = new IOF({
|
|
340
|
+
heartbeatInterval: 10000, // 10 seconds
|
|
341
|
+
connectionTimeout: 5000, // 5 seconds
|
|
342
|
+
autoReconnect: true
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
iof.on('disconnect', ({ reason }) => {
|
|
346
|
+
console.log('Connection lost:', reason)
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
iof.on('reconnecting', ({ attempt, delay }) => {
|
|
350
|
+
console.log(`Reconnecting (${attempt}/5) in ${delay}ms`)
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
iof.on('reconnection_failed', ({ attempts }) => {
|
|
354
|
+
console.log(`Failed to reconnect after ${attempts} attempts`)
|
|
355
|
+
})
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### Message Queuing
|
|
359
|
+
|
|
360
|
+
```javascript
|
|
361
|
+
const iof = new IOF({
|
|
362
|
+
messageQueueSize: 100,
|
|
363
|
+
autoReconnect: true
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
// Messages sent while disconnected are automatically queued
|
|
367
|
+
iof.emit('queuedMessage', { data: 'This will be queued if disconnected' })
|
|
368
|
+
|
|
369
|
+
// Check queue status
|
|
370
|
+
const stats = iof.getStats()
|
|
371
|
+
console.log(`Queued messages: ${stats.queuedMessages}`)
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### Rate Limiting
|
|
375
|
+
|
|
376
|
+
```javascript
|
|
377
|
+
const iof = new IOF({
|
|
378
|
+
maxMessagesPerSecond: 50,
|
|
379
|
+
maxMessageSize: 512 * 1024 // 512KB
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
iof.on('error', ({ type, limit, current }) => {
|
|
383
|
+
if (type === 'RATE_LIMIT_EXCEEDED') {
|
|
384
|
+
console.log(`Rate limit exceeded: ${current}/${limit} messages per second`)
|
|
385
|
+
}
|
|
386
|
+
})
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### Security Best Practices
|
|
390
|
+
|
|
391
|
+
```javascript
|
|
392
|
+
// Always specify allowed origins
|
|
393
|
+
iof.listen('https://trusted-parent.com')
|
|
394
|
+
|
|
395
|
+
// Handle security errors
|
|
396
|
+
iof.on('error', ({ type, expected, received }) => {
|
|
397
|
+
if (type === 'INVALID_ORIGIN') {
|
|
398
|
+
console.warn(`Rejected message from ${received}, expected ${expected}`)
|
|
399
|
+
}
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
// Validate message content
|
|
403
|
+
iof.on('userData', (data, ack) => {
|
|
404
|
+
if (!isValidUserData(data)) {
|
|
405
|
+
return ack('Invalid data format')
|
|
406
|
+
}
|
|
407
|
+
// Process valid data...
|
|
408
|
+
})
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
## Browser Support
|
|
412
|
+
|
|
413
|
+
- Modern browsers with postMessage API support
|
|
414
|
+
- Chrome 2+
|
|
415
|
+
- Firefox 3+
|
|
416
|
+
- Safari 4+
|
|
417
|
+
- IE 8+
|
|
418
|
+
- Edge (all versions)
|
|
419
|
+
|
|
420
|
+
## TypeScript Support
|
|
421
|
+
|
|
422
|
+
The library is written in TypeScript and includes full type definitions:
|
|
423
|
+
|
|
424
|
+
```typescript
|
|
425
|
+
import IOF, { Options, PeerType, AckFunction, Listener } from 'iframe.io'
|
|
426
|
+
|
|
427
|
+
const options: Options = {
|
|
428
|
+
type: 'WINDOW',
|
|
429
|
+
debug: true,
|
|
430
|
+
heartbeatInterval: 15000
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const iof = new IOF(options)
|
|
434
|
+
|
|
435
|
+
// Type-safe event handling
|
|
436
|
+
iof.on('myEvent', (data: { message: string }, ack?: AckFunction) => {
|
|
437
|
+
console.log(data.message)
|
|
438
|
+
ack?.(false, 'success')
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
// Type-safe async emissions
|
|
442
|
+
const response = await iof.emitAsync<{ query: string }, { result: any }>('search', {
|
|
443
|
+
query: 'typescript'
|
|
444
|
+
})
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
## License
|
|
448
|
+
|
|
449
|
+
MIT License - see LICENSE file for details.
|
|
450
|
+
|
|
451
|
+
## Contributing
|
|
452
|
+
|
|
453
|
+
Contributions are welcome! Please read our contributing guidelines and submit pull requests to our repository.
|
|
454
|
+
|
|
455
|
+
## Support
|
|
456
|
+
|
|
457
|
+
- Create an issue on GitHub for bug reports
|
|
458
|
+
- Check existing issues for common problems
|
|
459
|
+
- Review the documentation for usage examples
|
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
|
|
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 (
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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', {
|
|
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', {
|
|
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
|
-
|
|
380
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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",
|
|
@@ -34,6 +34,12 @@
|
|
|
34
34
|
},
|
|
35
35
|
"keywords": [
|
|
36
36
|
"realtime",
|
|
37
|
+
"embeddable",
|
|
38
|
+
"widget",
|
|
39
|
+
"components",
|
|
40
|
+
"micro-frontend",
|
|
41
|
+
"Secure",
|
|
42
|
+
"cross-origin",
|
|
37
43
|
"iframe",
|
|
38
44
|
"browser",
|
|
39
45
|
"events",
|
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' ){
|
|
@@ -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(
|
|
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', {
|
|
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', {
|
|
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(
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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[
|
|
451
|
+
if( this.Events[_event + '--@once'] ){
|
|
434
452
|
// Once triggable event
|
|
435
453
|
_event += '--@once'
|
|
436
|
-
listeners = this.Events[
|
|
454
|
+
listeners = this.Events[_event]
|
|
437
455
|
// Delete once event listeners after fired
|
|
438
|
-
delete this.Events[
|
|
456
|
+
delete this.Events[_event]
|
|
439
457
|
}
|
|
440
|
-
else listeners = this.Events[
|
|
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
|
-
|
|
461
|
-
|
|
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
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
509
|
-
|
|
510
|
-
}
|
|
531
|
+
typeof fn === 'function'
|
|
532
|
+
&& fn( error instanceof Error ? error.message : String(error) )
|
|
511
533
|
}
|
|
512
534
|
|
|
513
|
-
|
|
535
|
+
return this
|
|
514
536
|
}
|
|
515
|
-
|
|
537
|
+
|
|
516
538
|
on( _event: string, fn: Listener ){
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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
|
-
|
|
523
|
-
|
|
524
|
-
|
|
544
|
+
return this
|
|
545
|
+
}
|
|
546
|
+
|
|
525
547
|
once( _event: string, fn: Listener ){
|
|
526
|
-
|
|
548
|
+
// Add Once Event listener
|
|
527
549
|
_event += '--@once'
|
|
528
550
|
|
|
529
|
-
|
|
530
|
-
|
|
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
|
-
|
|
534
|
-
|
|
555
|
+
return this
|
|
556
|
+
}
|
|
535
557
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
if( fn && this.Events[
|
|
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[
|
|
541
|
-
if( index > -1 )
|
|
542
|
-
this.Events[
|
|
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[
|
|
545
|
-
delete this.Events[
|
|
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[
|
|
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
|
-
|
|
555
|
-
|
|
577
|
+
return this
|
|
578
|
+
}
|
|
556
579
|
|
|
557
|
-
|
|
580
|
+
removeListeners( fn?: Listener ){
|
|
558
581
|
// Clear all event listeners
|
|
559
|
-
|
|
560
|
-
|
|
582
|
+
this.Events = {}
|
|
583
|
+
typeof fn == 'function' && fn()
|
|
561
584
|
|
|
562
585
|
this.debug(`[${this.peer.type}] All listeners removed`)
|
|
563
|
-
|
|
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 ){
|
|
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
|
|
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(
|
|
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
|
+
}
|