11import { useEffect , useRef } from "preact/hooks" ;
2- import { computed , signal } from "@preact/signals" ;
2+ import { useSignal } from "@preact/signals" ;
33import * as yaml from "@std/yaml" ;
44
55interface PlaygroundProps {
@@ -11,9 +11,29 @@ interface PlaygroundProps {
1111
1212type PingResponse =
1313 | { status : "ok" }
14- | { status : "error" ; message ?: string ; error ?: string } ;
14+ | {
15+ status : "error" ;
16+ message : string ;
17+ source : "frontend error" | "frontend api error" | "apalache server down" ;
18+ } ;
1519type VerifyResponse = Record < string , unknown > ;
1620
21+ function extractApiMessage ( payload : unknown ) : string | null {
22+ if ( ! payload || typeof payload !== "object" ) {
23+ return null ;
24+ }
25+
26+ const record = payload as Record < string , unknown > ;
27+ if ( typeof record . message === "string" ) {
28+ return record . message ;
29+ }
30+ if ( typeof record . error === "string" ) {
31+ return record . error ;
32+ }
33+
34+ return null ;
35+ }
36+
1737type MonacoEditor = {
1838 getValue : ( ) => string ;
1939 setValue : ( value : string ) => void ;
@@ -419,37 +439,12 @@ export default function PlaygroundBody(props: PlaygroundProps) {
419439 const editor = useRef < MonacoEditor | null > ( null ) ;
420440 const pollingTimerRef = useRef < number | null > ( null ) ;
421441
422- const loadingText = signal ( "" ) ;
423- const consoleText = signal ( "" ) ;
424- const errorText = signal ( "" ) ;
425-
426- const selectedInv = signal ( "" ) ;
427- const allInvs = signal < string [ ] > ( [ ] ) ;
428-
429- const allInvsOption = computed ( ( ) => {
430- const options = [
431- < option
432- key = "placeholder"
433- value = ""
434- disabled
435- selected = { ! ( allInvs . value && allInvs . value . includes ( selectedInv . value ) ) }
436- >
437- Select an invariant
438- </ option > ,
439- ] ;
440-
441- if ( allInvs . value && allInvs . value . length > 0 ) {
442- options . push (
443- ...allInvs . value . map ( ( inv ) => (
444- < option value = { inv } key = { inv } >
445- { inv }
446- </ option >
447- ) ) ,
448- ) ;
449- }
442+ const loadingText = useSignal ( "" ) ;
443+ const consoleText = useSignal ( props . out ) ;
444+ const errorText = useSignal ( "" ) ;
450445
451- return options ;
452- } ) ;
446+ const selectedInv = useSignal ( props . inv ) ;
447+ const allInvs = useSignal < string [ ] > ( props . invs ) ;
453448 useEffect ( ( ) => {
454449 let isDisposed = false ;
455450
@@ -458,7 +453,12 @@ export default function PlaygroundBody(props: PlaygroundProps) {
458453 if ( rawStoredSnippet ) {
459454 try {
460455 const parsed = JSON . parse ( rawStoredSnippet ) as PlaygroundProps ;
461- if ( parsed . tla . length > 0 ) {
456+ if (
457+ parsed . tla . length > 0 &&
458+ Array . isArray ( parsed . invs ) &&
459+ parsed . invs . length > 0 &&
460+ typeof parsed . inv === "string"
461+ ) {
462462 initTla = parsed ;
463463 }
464464 } catch {
@@ -599,31 +599,56 @@ export default function PlaygroundBody(props: PlaygroundProps) {
599599
600600 async function ping ( ) : Promise < PingResponse > {
601601 try {
602- return await fetch ( "/api/ping" , {
602+ const response = await fetch ( "/api/ping" , {
603603 method : "GET" ,
604604 headers : {
605605 "Content-Type" : "application/json" ,
606606 } ,
607- } ) . then ( ( resp ) => resp . json ( ) as Promise < PingResponse > ) ;
607+ } ) ;
608+
609+ const payload = await response . json ( ) ;
610+ if ( ! response . ok ) {
611+ const message = extractApiMessage ( payload ) ??
612+ `Ping API failed with status ${ response . status } ` ;
613+ const source = message . includes ( "Apalache server unavailable" )
614+ ? "apalache server down"
615+ : "frontend api error" ;
616+ console . error ( `[${ source } ] ${ message } ` ) ;
617+ return { status : "error" , message, source } ;
618+ }
619+
620+ return payload as PingResponse ;
608621 } catch ( error ) {
622+ console . error ( "[frontend error] Failed to call /api/ping" , error ) ;
609623 return {
610624 status : "error" ,
611625 message : error instanceof Error ? error . message : String ( error ) ,
626+ source : "frontend error" ,
612627 } ;
613628 }
614629 }
615630
616631 async function tlaInvariants ( data : { tla : string } ) : Promise < string [ ] > {
617632 try {
618- return await fetch ( "/api/invariants" , {
633+ const response = await fetch ( "/api/invariants" , {
619634 method : "POST" ,
620635 headers : {
621636 "Content-Type" : "application/json" ,
622637 } ,
623638 body : JSON . stringify ( data ) ,
624- } ) . then ( ( resp ) => resp . json ( ) ) ;
639+ } ) ;
640+
641+ if ( ! response . ok ) {
642+ const payload = await response . json ( ) ;
643+ const message = extractApiMessage ( payload ) ??
644+ `Invariants API failed with status ${ response . status } ` ;
645+ console . error ( `[frontend api error] ${ message } ` ) ;
646+ return [ ] ;
647+ }
648+
649+ return await response . json ( ) ;
625650 } catch ( error ) {
626- console . error ( error ) ;
651+ console . error ( "[frontend error] Failed to call /api/invariants" , error ) ;
627652 return [ ] ;
628653 }
629654 }
@@ -632,14 +657,24 @@ export default function PlaygroundBody(props: PlaygroundProps) {
632657 data : { tla : string ; inv : string } ,
633658 ) : Promise < VerifyResponse > {
634659 try {
635- return await fetch ( "/api/verify" , {
660+ const response = await fetch ( "/api/verify" , {
636661 method : "POST" ,
637662 headers : {
638663 "Content-Type" : "application/json" ,
639664 } ,
640665 body : JSON . stringify ( data ) ,
641- } ) . then ( ( resp ) => resp . json ( ) as Promise < VerifyResponse > ) ;
666+ } ) ;
667+
668+ const payload = await response . json ( ) ;
669+ if ( ! response . ok ) {
670+ const message = extractApiMessage ( payload ) ??
671+ `Verify API failed with status ${ response . status } ` ;
672+ console . error ( `[frontend api error] ${ message } ` ) ;
673+ }
674+
675+ return payload as VerifyResponse ;
642676 } catch ( error ) {
677+ console . error ( "[frontend error] Failed to call /api/verify" , error ) ;
643678 return { error : error instanceof Error ? error . message : String ( error ) } ;
644679 }
645680 }
@@ -655,32 +690,45 @@ export default function PlaygroundBody(props: PlaygroundProps) {
655690 const pingResp = await ping ( ) ;
656691 if ( pingResp . status !== "ok" ) {
657692 consoleText . value = "" ;
658- errorText . value = "> Apalache server is down !" ;
693+ errorText . value = `> ${ pingResp . message } ` ;
659694 return ;
660695 }
661696
662697 const invariants = await tlaInvariants ( { tla } ) ;
663698
664699 allInvs . value = invariants ;
665700
666- if ( invariants . includes ( selectedInv . value ) ) {
667- // const spinner = new Spinner(["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"]);
668- const spinner = new Spinner ( [ "..." , " .." , ". ." , ".. " ] ) ;
669- errorText . value = "" ;
670- consoleText . value = "" ;
701+ const invariantToVerify = invariants . includes ( selectedInv . value )
702+ ? selectedInv . value
703+ : ( invariants [ 0 ] ?? "" ) ;
671704
672- const spinnerTimer = globalThis . setInterval ( ( ) => {
673- loadingText . value = `> processing ${ spinner . next ( ) } ` ;
674- } , 200 ) ;
705+ if ( ! invariantToVerify ) {
675706 consoleText . value = "" ;
676- const data = { tla, inv : selectedInv . value } ;
677- const respJson = await tlaVerify ( data ) ;
707+ errorText . value = "> No invariant available to verify" ;
708+ return ;
709+ }
678710
679- globalThis . clearInterval ( spinnerTimer ) ;
680- loadingText . value = "" ;
711+ selectedInv . value = invariantToVerify ;
681712
682- consoleText . value = yaml . stringify ( respJson , { indent : 2 } ) ;
683- }
713+ // const spinner = new Spinner(["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"]);
714+ const spinner = new Spinner ( [ "..." , " .." , ". ." , ".. " ] ) ;
715+ errorText . value = "" ;
716+ consoleText . value = "" ;
717+
718+ const spinnerTimer = globalThis . setInterval ( ( ) => {
719+ loadingText . value = `> processing ${ spinner . next ( ) } ` ;
720+ } , 200 ) ;
721+ consoleText . value = "" ;
722+ const data = { tla, inv : invariantToVerify } ;
723+ const respJson = await tlaVerify ( data ) ;
724+
725+ globalThis . clearInterval ( spinnerTimer ) ;
726+ loadingText . value = "" ;
727+
728+ const rendered = yaml . stringify ( respJson , { indent : 2 } ) . trim ( ) ;
729+ consoleText . value = rendered . length > 0
730+ ? rendered
731+ : JSON . stringify ( respJson , null , 2 ) ;
684732 } ;
685733
686734 return (
@@ -693,7 +741,18 @@ export default function PlaygroundBody(props: PlaygroundProps) {
693741 selectedInv . value = e . currentTarget . value ;
694742 } }
695743 >
696- { allInvsOption }
744+ < option
745+ key = "placeholder"
746+ value = ""
747+ disabled
748+ >
749+ Select an invariant
750+ </ option >
751+ { allInvs . value . map ( ( inv ) => (
752+ < option value = { inv } key = { inv } >
753+ { inv }
754+ </ option >
755+ ) ) }
697756 </ select >
698757 < button
699758 type = "button"
0 commit comments