1
- import { ViewController , createDuplicateIframeWarning , createURL , createModalNotReadyError } from '@magic-sdk/provider' ;
1
+ import {
2
+ createDuplicateIframeWarning ,
3
+ createModalLostError ,
4
+ createModalNotReadyError ,
5
+ createURL ,
6
+ ViewController ,
7
+ } from '@magic-sdk/provider' ;
2
8
import { MagicIncomingWindowMessage , MagicOutgoingWindowMessage } from '@magic-sdk/types' ;
3
9
4
10
/**
@@ -45,8 +51,7 @@ function checkForSameSrcInstances(parameters: string) {
45
51
46
52
const SECOND = 1000 ;
47
53
const MINUTE = 60 * SECOND ;
48
- const RESPONSE_DELAY = 15 * SECOND ; // 15 seconds
49
- const PING_INTERVAL = 2 * MINUTE ; // 2 minutes
54
+ const PING_INTERVAL = 5 * MINUTE ; // 5 minutes
50
55
const INITIAL_HEARTBEAT_DELAY = 60 * MINUTE ; // 1 hour
51
56
52
57
/**
@@ -55,9 +60,9 @@ const INITIAL_HEARTBEAT_DELAY = 60 * MINUTE; // 1 hour
55
60
export class IframeController extends ViewController {
56
61
private iframe ! : Promise < HTMLIFrameElement > ;
57
62
private activeElement : any = null ;
58
- private lastPingTime = Date . now ( ) ;
59
- private intervalTimer : ReturnType < typeof setInterval > | null = null ;
60
- private timeoutTimer : ReturnType < typeof setTimeout > | null = null ;
63
+ private lastPongTime : null | number = null ;
64
+ private heartbeatIntervalTimer : ReturnType < typeof setInterval > | null = null ;
65
+ private heartbeatDebounce = debounce ( ( ) => this . heartBeatCheck ( ) , INITIAL_HEARTBEAT_DELAY ) ;
61
66
62
67
private getIframeSrc ( ) {
63
68
return createURL ( `/send?params=${ encodeURIComponent ( this . parameters ) } ` , this . endpoint ) . href ;
@@ -97,23 +102,23 @@ export class IframeController extends ViewController {
97
102
this . iframe . then ( iframe => {
98
103
if ( iframe instanceof HTMLIFrameElement ) {
99
104
iframe . addEventListener ( 'load' , async ( ) => {
100
- await this . startHeartBeat ( ) ;
105
+ this . heartbeatDebounce ( ) ;
101
106
} ) ;
102
107
}
103
108
} ) ;
104
109
105
110
window . addEventListener ( 'message' , ( event : MessageEvent ) => {
106
- if ( event . origin === this . endpoint ) {
107
- if ( event . data && event . data . msgType && this . messageHandlers . size ) {
108
- const isPongMessage = event . data . msgType . includes ( MagicIncomingWindowMessage . MAGIC_PONG ) ;
111
+ if ( event . origin === this . endpoint && event . data . msgType ) {
112
+ if ( event . data . msgType . includes ( MagicIncomingWindowMessage . MAGIC_PONG ) ) {
113
+ // Mark the Pong time
114
+ this . lastPongTime = Date . now ( ) ;
115
+ }
109
116
110
- if ( isPongMessage ) {
111
- this . lastPingTime = Date . now ( ) ;
112
- }
117
+ if ( event . data && this . messageHandlers . size ) {
113
118
// If the response object is undefined, we ensure it's at least an
114
119
// empty object before passing to the event listener.
115
- /* istanbul ignore next */
116
120
event . data . response = event . data . response ?? { } ;
121
+ this . stopHeartBeat ( ) ;
117
122
for ( const handler of this . messageHandlers . values ( ) ) {
118
123
handler ( event ) ;
119
124
}
@@ -145,56 +150,107 @@ export class IframeController extends ViewController {
145
150
}
146
151
147
152
protected async _post ( data : any ) {
148
- const iframe = await this . iframe ;
153
+ const iframe = await this . checkIframeExistsInDOM ( ) ;
154
+
155
+ if ( ! iframe ) {
156
+ this . init ( ) ;
157
+ throw createModalLostError ( ) ;
158
+ }
159
+
149
160
if ( iframe && iframe . contentWindow ) {
150
161
iframe . contentWindow . postMessage ( data , this . endpoint ) ;
151
162
} else {
152
163
throw createModalNotReadyError ( ) ;
153
164
}
154
165
}
155
166
167
+ /**
168
+ * Sends periodic pings to check the connection.
169
+ * If no pong is received or it’s stale, the iframe is reloaded.
170
+ */
171
+ /* istanbul ignore next */
156
172
private heartBeatCheck ( ) {
157
- this . intervalTimer = setInterval ( async ( ) => {
158
- const message = { msgType : `${ MagicOutgoingWindowMessage . MAGIC_PING } -${ this . parameters } ` , payload : [ ] } ;
173
+ let firstPing = true ;
159
174
175
+ // Helper function to send a ping message.
176
+ const sendPing = async ( ) => {
177
+ const message = {
178
+ msgType : `${ MagicOutgoingWindowMessage . MAGIC_PING } -${ this . parameters } ` ,
179
+ payload : [ ] ,
180
+ } ;
160
181
await this . _post ( message ) ;
182
+ } ;
161
183
162
- const timeSinceLastPing = Date . now ( ) - this . lastPingTime ;
163
-
164
- if ( timeSinceLastPing > RESPONSE_DELAY ) {
165
- await this . reloadIframe ( ) ;
166
- this . lastPingTime = Date . now ( ) ;
184
+ this . heartbeatIntervalTimer = setInterval ( async ( ) => {
185
+ // If no pong has ever been received.
186
+ if ( ! this . lastPongTime ) {
187
+ if ( ! firstPing ) {
188
+ // On subsequent ping with no previous pong response, reload the iframe.
189
+ this . reloadIframe ( ) ;
190
+ firstPing = true ;
191
+ return ;
192
+ }
193
+ } else {
194
+ // If we have a pong, check how long ago it was received.
195
+ const timeSinceLastPong = Date . now ( ) - this . lastPongTime ;
196
+ if ( timeSinceLastPong > PING_INTERVAL * 2 ) {
197
+ // If the pong is too stale, reload the iframe.
198
+ this . reloadIframe ( ) ;
199
+ firstPing = true ;
200
+ return ;
201
+ }
167
202
}
168
- } , PING_INTERVAL ) ;
169
- }
170
203
171
- private async startHeartBeat ( ) {
172
- const iframe = await this . iframe ;
173
-
174
- if ( iframe ) {
175
- this . timeoutTimer = setTimeout ( ( ) => this . heartBeatCheck ( ) , INITIAL_HEARTBEAT_DELAY ) ;
176
- }
204
+ // Send a new ping message and update the counter.
205
+ await sendPing ( ) ;
206
+ firstPing = false ;
207
+ } , PING_INTERVAL ) ;
177
208
}
178
209
210
+ // Debounce revival mechanism
211
+ // Kill any existing PingPong interval
179
212
private stopHeartBeat ( ) {
180
- if ( this . timeoutTimer ) {
181
- clearTimeout ( this . timeoutTimer ) ;
182
- this . timeoutTimer = null ;
183
- }
213
+ this . heartbeatDebounce ( ) ;
214
+ this . lastPongTime = null ;
184
215
185
- if ( this . intervalTimer ) {
186
- clearInterval ( this . intervalTimer ) ;
187
- this . intervalTimer = null ;
216
+ if ( this . heartbeatIntervalTimer ) {
217
+ clearInterval ( this . heartbeatIntervalTimer ) ;
218
+ this . heartbeatIntervalTimer = null ;
188
219
}
189
220
}
190
221
191
222
private async reloadIframe ( ) {
192
223
const iframe = await this . iframe ;
193
224
225
+ // Reset HeartBeat
226
+ this . stopHeartBeat ( ) ;
227
+
194
228
if ( iframe ) {
229
+ // reload the iframe source
195
230
iframe . src = this . getIframeSrc ( ) ;
196
231
} else {
197
- throw createModalNotReadyError ( ) ;
232
+ this . init ( ) ;
233
+ console . warn ( 'Magic SDK: Modal lost, re-initiating' ) ;
198
234
}
199
235
}
236
+
237
+ async checkIframeExistsInDOM ( ) {
238
+ // Check if the iframe is already in the DOM
239
+ const iframes : HTMLIFrameElement [ ] = [ ] . slice . call ( document . querySelectorAll ( '.magic-iframe' ) ) ;
240
+ return iframes . find ( iframe => iframe . src . includes ( encodeURIComponent ( this . parameters ) ) ) ;
241
+ }
242
+ }
243
+
244
+ function debounce < T extends ( ...args : unknown [ ] ) => void > ( func : T , delay : number ) {
245
+ let timeoutId : ReturnType < typeof setTimeout > | null = null ;
246
+
247
+ return function ( ...args : Parameters < T > ) : void {
248
+ if ( timeoutId ) {
249
+ clearTimeout ( timeoutId ) ;
250
+ }
251
+
252
+ timeoutId = setTimeout ( ( ) => {
253
+ func ( ...args ) ;
254
+ } , delay ) ;
255
+ } ;
200
256
}
0 commit comments