@@ -740,4 +740,168 @@ export function validateCoordinates(
740740 valid : true ,
741741 coords : { top, bottom, left, right }
742742 } ;
743+ }
744+
745+ export function createBasicAuthToken ( username : string , accessKey : string ) : string {
746+ const credentials = `${ username } :${ accessKey } ` ;
747+ return Buffer . from ( credentials ) . toString ( 'base64' ) ;
748+ }
749+
750+ export async function listenToSmartUISSE (
751+ baseURL : string ,
752+ accessToken : string ,
753+ onEvent ?: ( eventType : string , data : any ) => void
754+ ) : Promise < { abort : ( ) => void } > {
755+ const url = `${ baseURL } /api/v1/sse/smartui` ;
756+
757+ const abortController = new AbortController ( ) ;
758+
759+ try {
760+ const response = await fetch ( url , {
761+ method : 'GET' ,
762+ headers : {
763+ 'Accept' : 'text/event-stream' ,
764+ 'Cache-Control' : 'no-cache' ,
765+ 'Cookie' : `stageAccessToken=Basic ${ accessToken } `
766+ } ,
767+ signal : abortController . signal
768+ } ) ;
769+
770+ if ( ! response . ok ) {
771+ throw new Error ( `HTTP error! status: ${ response . status } ` ) ;
772+ }
773+
774+ onEvent ?.( 'open' , { status : 'connected' } ) ;
775+
776+ const reader = response . body ?. getReader ( ) ;
777+ if ( ! reader ) {
778+ throw new Error ( 'No response body reader available' ) ;
779+ }
780+
781+ const decoder = new TextDecoder ( ) ;
782+ let buffer = '' ;
783+ let currentEvent = '' ;
784+
785+ try {
786+ while ( true ) {
787+ const { done, value } = await reader . read ( ) ;
788+ if ( done ) break ;
789+
790+ const chunk = decoder . decode ( value , { stream : true } ) ;
791+
792+ buffer += chunk ;
793+ const lines = buffer . split ( '\n' ) ;
794+
795+ buffer = lines . pop ( ) || '' ;
796+
797+ for ( const line of lines ) {
798+ if ( line . startsWith ( 'event:' ) ) {
799+ currentEvent = line . substring ( 6 ) . trim ( ) ;
800+ }
801+ else if ( line . startsWith ( 'data:' ) ) {
802+ const data = line . substring ( 5 ) . trim ( ) ;
803+
804+ if ( data ) {
805+ try {
806+ const parsedData = JSON . parse ( data ) ;
807+ onEvent ?.( currentEvent , parsedData ) ;
808+ } catch ( parseError ) {
809+ if ( currentEvent === 'connection' && data === 'connected' ) {
810+ onEvent ?.( currentEvent , { status : 'connected' , message : data } ) ;
811+ } else {
812+ onEvent ?.( currentEvent , data ) ;
813+ }
814+ }
815+ }
816+ }
817+ else if ( line . trim ( ) === '' ) {
818+ currentEvent = '' ;
819+ }
820+ }
821+ }
822+ } catch ( streamError : any ) {
823+ console . error ( 'SSE Streaming error:' , streamError ) ;
824+ onEvent ?.( 'error' , streamError ) ;
825+ } finally {
826+ reader . releaseLock ( ) ;
827+ }
828+
829+ } catch ( error ) {
830+ console . error ( 'SSE Connection error:' , error ) ;
831+ onEvent ?.( 'error' , error ) ;
832+ }
833+
834+ return {
835+ abort : ( ) => abortController . abort ( )
836+ } ;
837+ }
838+
839+ export async function startSSEListener ( ctx : Context ) {
840+ let retryCount = 0 ;
841+ const maxRetries = 3 ;
842+ let currentConnection : { abort : ( ) => void } | null = null ;
843+ let errorCount = 0 ;
844+
845+ const connectWithRetry = async ( ) => {
846+ try {
847+ ctx . log . debug ( `Attempting SSE connection (attempt ${ retryCount + 1 } /${ maxRetries } )` ) ;
848+ const accessKey = ctx . env . LT_ACCESS_KEY ;
849+ const username = ctx . env . LT_USERNAME ;
850+
851+ const basicAuthToken = createBasicAuthToken ( username , accessKey ) ;
852+ ctx . log . debug ( `Basic auth token: ${ basicAuthToken } ` ) ;
853+ currentConnection = await listenToSmartUISSE (
854+ ctx . env . SMARTUI_SSE_URL ,
855+ basicAuthToken ,
856+ ( eventType , data ) => {
857+ switch ( eventType ) {
858+ case 'open' :
859+ ctx . log . debug ( 'Connected to SSE server' ) ;
860+ retryCount = 0 ;
861+ break ;
862+
863+ case 'connection' :
864+ ctx . log . debug ( 'Connection confirmed:' , data ) ;
865+ retryCount = 0 ;
866+ break ;
867+
868+ case 'Dot_buildCompleted' :
869+ ctx . log . debug ( 'Build completed' ) ;
870+ console . log ( 'Build completed' ) ;
871+ currentConnection ?. abort ( ) ;
872+ if ( errorCount > 0 ) {
873+ process . exit ( 1 ) ;
874+ }
875+ process . exit ( 0 ) ;
876+
877+ case 'DOTUIError' :
878+ if ( data . buildId == ctx . build . id ) {
879+ errorCount ++ ;
880+ console . error ( 'Error in build:' , data . message ) ;
881+ }
882+ break ;
883+
884+ case 'error' :
885+ ctx . log . debug ( 'SSE Error occurred:' , data ) ;
886+ currentConnection ?. abort ( ) ;
887+ process . exit ( 0 ) ;
888+
889+ }
890+ }
891+ ) ;
892+
893+ } catch ( error ) {
894+ ctx . log . debug ( `Failed to start SSE listener (attempt ${ retryCount + 1 } ):` , error ) ;
895+ retryCount ++ ;
896+
897+ if ( retryCount < maxRetries ) {
898+ ctx . log . debug ( `Retrying in 2 seconds...` ) ;
899+ setTimeout ( connectWithRetry , 2000 ) ;
900+ } else {
901+ ctx . log . debug ( 'Max retries reached. SSE listener failed.' ) ;
902+ }
903+ }
904+ } ;
905+
906+ connectWithRetry ( ) ;
743907}
0 commit comments