@@ -64,7 +64,7 @@ const openapiPath = `/api-reference`
64
64
65
65
const allowedOrigins = [ env . NEXT_PUBLIC_URL ! . replace ( / \/ $ / , '' ) , 'http://localhost:7664' ]
66
66
67
- let onFirstStateMessage = ( ) => { }
67
+ let onFirstStateMessage = ( ) => { }
68
68
const firstStateReceived = new Promise < void > ( ( resolve ) => {
69
69
onFirstStateMessage = resolve
70
70
} )
@@ -170,43 +170,80 @@ async function websocketIdHandling(websocketId: string) {
170
170
if ( globalThis . websocketHandlingDone ) return
171
171
globalThis . websocketHandlingDone = true
172
172
173
- console . log ( 'connecting over preview websocketId' , websocketId )
174
- const websocketUrl = `wss:// ${ WEBSITE_DOMAIN } /_tunnel/client?id= ${ websocketId } `
175
- const ws = new WebSocket ( websocketUrl )
176
- ws . onopen = ( ) => {
177
- useDocsState . setState ( {
178
- websocketServerPreviewConnected : true ,
179
- } )
180
- ws . send ( JSON . stringify ( { type : 'ready' } ) )
181
- }
182
- ws . onclose = ( ) => {
183
- useDocsState . setState ( {
184
- websocketServerPreviewConnected : false ,
185
- } )
173
+ let ws : WebSocket | null = null
174
+ let reconnectTimeout : NodeJS . Timeout | null = null
175
+ let pingInterval : NodeJS . Timeout | null = null
176
+
177
+ const clearTimers = ( ) => {
178
+ if ( reconnectTimeout ) {
179
+ clearTimeout ( reconnectTimeout )
180
+ reconnectTimeout = null
181
+ }
182
+ if ( pingInterval ) {
183
+ clearInterval ( pingInterval )
184
+ pingInterval = null
185
+ }
186
186
}
187
- ws . onmessage = async ( event ) => {
188
- let data : IframeRpcMessage
189
- try {
190
- data = JSON . parse ( event . data )
191
- } catch {
192
- console . error ( `websocket sent invalid json` , event . data )
193
- return
187
+
188
+ const connect = ( ) => {
189
+ console . log ( 'connecting over preview websocketId' , websocketId )
190
+ const websocketUrl = `wss://${ WEBSITE_DOMAIN } /_tunnel/downstream?id=${ websocketId } `
191
+ ws = new WebSocket ( websocketUrl )
192
+
193
+ ws . onopen = ( ) => {
194
+ clearTimers ( )
195
+ useDocsState . setState ( {
196
+ websocketConnectionState : 'CONNECTED' ,
197
+ } )
198
+ ws ! . send ( JSON . stringify ( { type : 'ready' } ) )
199
+
200
+ // Start ping interval
201
+ pingInterval = setInterval ( ( ) => {
202
+ if ( ws && ws . readyState === WebSocket . OPEN ) {
203
+ ws . send ( JSON . stringify ( { type : 'ping' } ) )
204
+ }
205
+ } , 5 * 1000 )
194
206
}
195
- const { id, revalidate, state : partialState } = data || { }
196
- if ( partialState ) {
197
- await setDocsStateForMessage ( partialState )
207
+
208
+ ws . onclose = ( event ) => {
209
+ clearTimers ( )
210
+
211
+ // Check if upstream is not connected based on close code
212
+ if ( event . code === 4008 ) {
213
+ useDocsState . setState ( {
214
+ websocketConnectionState : 'UPSTREAM_NOT_CONNECTED' ,
215
+ } )
216
+ } else {
217
+ useDocsState . setState ( {
218
+ websocketConnectionState : 'NOT_CONNECTED' ,
219
+ } )
220
+ }
221
+
222
+ // Always attempt to reconnect after 5 seconds
223
+ reconnectTimeout = setTimeout ( ( ) => connect ( ) , 5000 )
198
224
}
199
- if ( revalidate ) {
200
- await revalidator ?. revalidate ( )
225
+
226
+ ws . onmessage = async ( event ) => {
227
+ let data : IframeRpcMessage
228
+ try {
229
+ data = JSON . parse ( event . data )
230
+ } catch {
231
+ console . error ( `websocket sent invalid json` , event . data )
232
+ return
233
+ }
234
+ const { id, revalidate, state : partialState } = data || { }
235
+ if ( partialState ) {
236
+ await setDocsStateForMessage ( partialState )
237
+ }
238
+ if ( revalidate ) {
239
+ await revalidator ?. revalidate ( )
240
+ }
241
+ ws ! . send ( JSON . stringify ( { id } satisfies IframeRpcMessage ) )
201
242
}
202
- ws . send ( JSON . stringify ( { id } satisfies IframeRpcMessage ) )
203
243
}
204
- // ping interval
205
- setInterval ( ( ) => {
206
- if ( ws . readyState === WebSocket . OPEN ) {
207
- ws . send ( JSON . stringify ( { type : 'ping' } ) )
208
- }
209
- } , 1000 )
244
+
245
+ // Start initial connection
246
+ connect ( )
210
247
}
211
248
212
249
if ( typeof window !== 'undefined' ) {
@@ -293,11 +330,11 @@ export function CSSVariables({ docsJson }: { docsJson: DocsJsonType }) {
293
330
const toCssBlock = ( obj : Record < string , string > | undefined ) =>
294
331
obj
295
332
? Object . entries ( obj )
296
- . map ( ( [ key , value ] ) => {
297
- const cssVar = key . startsWith ( '--' ) ? key : `--${ key } `
298
- return `${ cssVar } : ${ value } !important;`
299
- } )
300
- . join ( '\n ' )
333
+ . map ( ( [ key , value ] ) => {
334
+ const cssVar = key . startsWith ( '--' ) ? key : `--${ key } `
335
+ return `${ cssVar } : ${ value } !important;`
336
+ } )
337
+ . join ( '\n ' )
301
338
: ''
302
339
303
340
// Don't render if both empty
@@ -552,7 +589,7 @@ function DocsLayoutWrapper({ children, docsJson }: { children: React.ReactNode;
552
589
}
553
590
554
591
const noop = ( callback : ( ) => void ) => {
555
- return ( ) => { }
592
+ return ( ) => { }
556
593
}
557
594
558
595
function PreviewBanner ( { websocketId } : { websocketId ?: string } ) {
@@ -564,7 +601,7 @@ function PreviewBanner({ websocketId }: { websocketId?: string }) {
564
601
globalNavigate ( url . pathname + url . search )
565
602
}
566
603
567
- const websocketServerPreviewConnected = useDocsState ( ( state ) => state . websocketServerPreviewConnected )
604
+ const websocketConnectionState = useDocsState ( ( state ) => state . websocketConnectionState )
568
605
569
606
const shouldShow = useSyncExternalStore (
570
607
noop ,
@@ -575,19 +612,41 @@ function PreviewBanner({ websocketId }: { websocketId?: string }) {
575
612
return null
576
613
}
577
614
615
+ const getStatusColor = ( ) => {
616
+ switch ( websocketConnectionState ) {
617
+ case 'CONNECTED' :
618
+ return 'bg-green-500'
619
+ case 'UPSTREAM_NOT_CONNECTED' :
620
+ return 'bg-yellow-500'
621
+ case 'NOT_CONNECTED' :
622
+ default :
623
+ return 'bg-red-500'
624
+ }
625
+ }
626
+
627
+ const getStatusMessage = ( ) => {
628
+ switch ( websocketConnectionState ) {
629
+ case 'CONNECTED' :
630
+ return 'Connected to local preview. Added content will be highlighted green'
631
+ case 'UPSTREAM_NOT_CONNECTED' :
632
+ return 'Waiting for upstream server. Please run the preview server'
633
+ case 'NOT_CONNECTED' :
634
+ default :
635
+ return 'Server disconnected. Please restart the preview server'
636
+ }
637
+ }
638
+
578
639
return (
579
640
< Banner className = 'sticky top-0 z-50 bg-fd-muted text-fd-accent-foreground isolate px-4 py-1 flex items-center justify-between' >
580
641
< div className = 'flex items-center gap-2' >
581
642
< div
582
643
className = { cn (
583
644
'w-2 h-2 rounded-full animate-pulse' ,
584
- websocketServerPreviewConnected ? 'bg-green-500' : 'bg-red-500' ,
645
+ getStatusColor ( ) ,
585
646
) }
586
647
> </ div >
587
648
< span className = 'font-medium text-sm' >
588
- { websocketServerPreviewConnected
589
- ? 'Connected to local preview. Added content will be highlighted green'
590
- : 'Server disconnected. Please restart the preview server' }
649
+ { getStatusMessage ( ) }
591
650
</ span >
592
651
</ div >
593
652
< button
0 commit comments