@@ -362,6 +362,23 @@ export class WebViewer {
362362 this . #canvas. style . height = options . height ?? "360px" ;
363363 parent . append ( this . #canvas) ;
364364
365+ // Show loading spinner
366+ const loader = document . createElement ( "div" ) ;
367+ loader . id = "rerun-loader" ;
368+ loader . innerHTML = `
369+ <style>
370+ @keyframes rerun-spin { to { transform: rotate(360deg); } }
371+ </style>
372+ <div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; background-color: #1c1c1c; font-family: sans-serif; color: white;">
373+ <div style="width: 40px; height: 40px; border: 3px solid #444; border-top-color: white; border-radius: 50%; animation: rerun-spin 1s linear infinite;"></div>
374+ <div style="margin-top: 16px;">Loading Rerun…</div>
375+ </div>
376+ ` ;
377+ loader . style . position = "absolute" ;
378+ loader . style . inset = "0" ;
379+ parent . style . position = "relative" ;
380+ parent . append ( loader ) ;
381+
365382 // This yield appears to be necessary to ensure that the canvas is attached to the DOM
366383 // and visible. Without it we get occasionally get a panic about a failure to find a canvas
367384 // element with the given ID.
@@ -372,7 +389,14 @@ export class WebViewer {
372389 delete ( options as any ) . base_url ;
373390 }
374391
375- let WebHandle_class = await load ( base_url ) ;
392+ let WebHandle_class : typeof wasm_bindgen . WebHandle ;
393+ try {
394+ WebHandle_class = await load ( base_url ) ;
395+ } catch ( e ) {
396+ loader . remove ( ) ;
397+ this . #fail( "Failed to load rerun" , String ( e ) ) ;
398+ throw e ;
399+ }
376400 if ( this . #state !== "starting" ) return ;
377401
378402 const fullscreen = this . #allow_fullscreen
@@ -404,18 +428,33 @@ export class WebViewer {
404428 try {
405429 await this . #handle. start ( this . #canvas) ;
406430 } catch ( e ) {
407- this . stop ( ) ;
431+ loader . remove ( ) ;
432+ this . #fail( "Failed to start" , String ( e ) ) ;
408433 throw e ;
409434 }
410435 if ( this . #state !== "starting" ) return ;
411436
437+ loader . remove ( ) ;
412438 this . #state = "ready" ;
413439 this . #dispatch_event( "ready" ) ;
414440
415441 if ( rrd ) {
416442 this . open ( rrd ) ;
417443 }
418444
445+ let self = this ;
446+
447+ function check_for_panic ( ) {
448+ if ( self . #handle?. has_panicked ( ) ) {
449+ self . #fail( "Rerun has crashed." , self . #handle?. panic_message ( ) ) ;
450+ } else {
451+ let delay_ms = 1000 ;
452+ setTimeout ( check_for_panic , delay_ms ) ;
453+ }
454+ }
455+
456+ check_for_panic ( ) ;
457+
419458 return ;
420459 }
421460
@@ -566,7 +605,7 @@ export class WebViewer {
566605 try {
567606 this . #handle. add_receiver ( url , options . follow_if_http ) ;
568607 } catch ( e ) {
569- this . stop ( ) ;
608+ this . #fail ( "Failed to open recording" , String ( e ) ) ;
570609 throw e ;
571610 }
572611 }
@@ -589,7 +628,7 @@ export class WebViewer {
589628 try {
590629 this . #handle. remove_receiver ( url ) ;
591630 } catch ( e ) {
592- this . stop ( ) ;
631+ this . #fail ( "Failed to close recording" , String ( e ) ) ;
593632 throw e ;
594633 }
595634 }
@@ -624,6 +663,39 @@ export class WebViewer {
624663 this . #allow_fullscreen = false ;
625664 }
626665
666+ #fail( message : string , error_message ?: string ) {
667+ console . error ( "WebViewer failure:" , message , error_message ) ;
668+ if ( this . canvas ?. parentElement ) {
669+ const parent = this . canvas . parentElement ;
670+ parent . innerHTML = `
671+ <div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: white; font-family: sans-serif; background-color: #1c1c1c;">
672+ <h1 id="fail-message"></h1>
673+ <pre id="fail-error" style="text-align: left;"></pre>
674+ <button id="fail-clear-cache">Clear caches and reload</button>
675+ </div>
676+ ` ;
677+
678+ document . getElementById ( "fail-message" ) ! . textContent = message ;
679+
680+ const errorEl = document . getElementById ( "fail-error" ) ! ;
681+ if ( error_message ) {
682+ errorEl . textContent = error_message ;
683+ } else {
684+ errorEl . remove ( ) ;
685+ }
686+
687+ document . getElementById ( "fail-clear-cache" ) ! . addEventListener ( "click" , async ( ) => {
688+ if ( "caches" in window ) {
689+ const keys = await caches . keys ( ) ;
690+ await Promise . all ( keys . map ( ( key ) => caches . delete ( key ) ) ) ;
691+ }
692+ window . location . reload ( ) ;
693+ } ) ;
694+ }
695+
696+ this . stop ( ) ;
697+ }
698+
627699 /**
628700 * Opens a new channel for sending log messages.
629701 *
@@ -643,7 +715,7 @@ export class WebViewer {
643715 try {
644716 this . #handle. open_channel ( id , channel_name ) ;
645717 } catch ( e ) {
646- this . stop ( ) ;
718+ this . #fail ( "Failed to open channel" , String ( e ) ) ;
647719 throw e ;
648720 }
649721
@@ -657,7 +729,7 @@ export class WebViewer {
657729 try {
658730 this . #handle. send_rrd_to_channel ( id , data ) ;
659731 } catch ( e ) {
660- this . stop ( ) ;
732+ this . #fail ( "Failed to send data" , String ( e ) ) ;
661733 throw e ;
662734 }
663735 } ;
@@ -672,7 +744,7 @@ export class WebViewer {
672744 try {
673745 this . #handle. send_table_to_channel ( id , data ) ;
674746 } catch ( e ) {
675- this . stop ( ) ;
747+ this . #fail ( "Failed to send table" , String ( e ) ) ;
676748 throw e ;
677749 }
678750 }
@@ -687,7 +759,7 @@ export class WebViewer {
687759 try {
688760 this . #handle. close_channel ( id ) ;
689761 } catch ( e ) {
690- this . stop ( ) ;
762+ this . #fail ( "Failed to close channel" , String ( e ) ) ;
691763 throw e ;
692764 }
693765 } ;
@@ -713,7 +785,7 @@ export class WebViewer {
713785 try {
714786 this . #handle. override_panel_state ( panel , state ) ;
715787 } catch ( e ) {
716- this . stop ( ) ;
788+ this . #fail ( "Failed to override panel state" , String ( e ) ) ;
717789 throw e ;
718790 }
719791 }
@@ -733,7 +805,7 @@ export class WebViewer {
733805 try {
734806 this . #handle. toggle_panel_overrides ( value as boolean | undefined ) ;
735807 } catch ( e ) {
736- this . stop ( ) ;
808+ this . #fail ( "Failed to toggle panel overrides" , String ( e ) ) ;
737809 throw e ;
738810 }
739811 }
0 commit comments