@@ -31,6 +31,11 @@ export { buildApiBaseUrlCandidates, isRemoteDockerHost } from "./controller-reac
3131
3232let selectedApiBaseUrl : string | undefined
3333
34+ type HealthProbeResult = {
35+ readonly apiBaseUrl : string
36+ readonly revision : string | null
37+ }
38+
3439const controllerBootstrapError = ( message : string ) : ControllerBootstrapError => ( {
3540 _tag : "ControllerBootstrapError" ,
3641 message
@@ -43,13 +48,30 @@ const rememberSelectedApiBaseUrl = (value: string): void => {
4348export const resolveApiBaseUrl = ( ) : string =>
4449 resolveExplicitApiBaseUrl ( ) ?? selectedApiBaseUrl ?? resolveConfiguredApiBaseUrl ( )
4550
46- const probeHealth = ( apiBaseUrl : string ) : Effect . Effect < void , ControllerBootstrapError > =>
51+ const parseHealthRevision = ( text : string ) : string | null => {
52+ try {
53+ const parsed : unknown = JSON . parse ( text )
54+ if ( typeof parsed !== "object" || parsed === null || Array . isArray ( parsed ) ) {
55+ return null
56+ }
57+ const revision = Reflect . get ( parsed , "revision" )
58+ return typeof revision === "string" && revision . trim ( ) . length > 0 ? revision . trim ( ) : null
59+ } catch {
60+ return null
61+ }
62+ }
63+
64+ const probeHealth = ( apiBaseUrl : string ) : Effect . Effect < HealthProbeResult , ControllerBootstrapError > =>
4765 Effect . gen ( function * ( _ ) {
4866 const client = yield * _ ( HttpClient . HttpClient )
4967 const response = yield * _ ( client . get ( `${ apiBaseUrl } /health` , { headers : { accept : "application/json" } } ) )
68+ const bodyText = yield * _ ( response . text )
5069
5170 if ( response . status >= 200 && response . status < 300 ) {
52- return
71+ return {
72+ apiBaseUrl,
73+ revision : parseHealthRevision ( bodyText )
74+ }
5375 }
5476
5577 return yield * _ (
@@ -74,6 +96,11 @@ const probeHealth = (apiBaseUrl: string): Effect.Effect<void, ControllerBootstra
7496const findReachableApiBaseUrl = (
7597 candidateUrls : ReadonlyArray < string >
7698) : Effect . Effect < string , ControllerBootstrapError > =>
99+ findReachableHealthProbe ( candidateUrls ) . pipe ( Effect . map ( ( { apiBaseUrl } ) => apiBaseUrl ) )
100+
101+ const findReachableHealthProbe = (
102+ candidateUrls : ReadonlyArray < string >
103+ ) : Effect . Effect < HealthProbeResult , ControllerBootstrapError > =>
77104 Effect . gen ( function * ( _ ) {
78105 if ( candidateUrls . length === 0 ) {
79106 return yield * _ (
@@ -85,14 +112,14 @@ const findReachableApiBaseUrl = (
85112 const healthy = yield * _ (
86113 probeHealth ( candidateUrl ) . pipe (
87114 Effect . match ( {
88- onFailure : ( ) => false ,
89- onSuccess : ( ) => true
115+ onFailure : ( ) => undefined ,
116+ onSuccess : ( result ) => result
90117 } )
91118 )
92119 )
93120
94- if ( healthy ) {
95- return candidateUrl
121+ if ( healthy !== undefined ) {
122+ return healthy
96123 }
97124 }
98125
@@ -176,10 +203,20 @@ const findReachableApiBaseUrlOption = (
176203 } )
177204 )
178205
179- const findReachableDirectApiBaseUrl = (
206+ const findReachableHealthProbeOption = (
207+ candidateUrls : ReadonlyArray < string >
208+ ) : Effect . Effect < HealthProbeResult | undefined , ControllerBootstrapError > =>
209+ findReachableHealthProbe ( candidateUrls ) . pipe (
210+ Effect . match ( {
211+ onFailure : ( ) : HealthProbeResult | undefined => undefined ,
212+ onSuccess : ( probe ) => probe
213+ } )
214+ )
215+
216+ const findReachableDirectHealthProbe = (
180217 explicitApiBaseUrl : string | undefined
181- ) : Effect . Effect < string | undefined , ControllerBootstrapError > =>
182- findReachableApiBaseUrlOption (
218+ ) : Effect . Effect < HealthProbeResult | undefined , ControllerBootstrapError > =>
219+ findReachableHealthProbeOption (
183220 buildApiBaseUrlCandidates ( {
184221 explicitApiBaseUrl,
185222 cachedApiBaseUrl : selectedApiBaseUrl ,
@@ -324,14 +361,25 @@ export const ensureControllerReady = (): Effect.Effect<void, ControllerBootstrap
324361 Effect . gen ( function * ( _ ) {
325362 yield * _ ( failIfRemoteDockerWithoutApiUrl ( ) )
326363 const explicitApiBaseUrl = resolveExplicitApiBaseUrl ( )
327- const reachableBeforeDocker = yield * _ ( findReachableDirectApiBaseUrl ( explicitApiBaseUrl ) )
328-
329- if ( reachableBeforeDocker !== undefined ) {
330- rememberSelectedApiBaseUrl ( reachableBeforeDocker )
331- return
364+ const localControllerRevision = yield * _ ( prepareLocalControllerRevision ( ) )
365+ if ( explicitApiBaseUrl !== undefined ) {
366+ const reachableBeforeDocker = yield * _ ( findReachableDirectHealthProbe ( explicitApiBaseUrl ) )
367+ if ( reachableBeforeDocker !== undefined ) {
368+ rememberSelectedApiBaseUrl ( reachableBeforeDocker . apiBaseUrl )
369+ return
370+ }
371+ yield * _ ( failIfExplicitApiUrlIsUnreachable ( explicitApiBaseUrl ) )
372+ } else {
373+ const reachableBeforeDocker = yield * _ ( findReachableDirectHealthProbe ( undefined ) )
374+ if (
375+ reachableBeforeDocker !== undefined &&
376+ reachableBeforeDocker . revision === localControllerRevision
377+ ) {
378+ rememberSelectedApiBaseUrl ( reachableBeforeDocker . apiBaseUrl )
379+ return
380+ }
332381 }
333382
334- yield * _ ( failIfExplicitApiUrlIsUnreachable ( explicitApiBaseUrl ) )
335383 const bootstrapContext = yield * _ ( loadControllerBootstrapContext ( ) )
336384 const reusedExistingController = yield * _ ( reuseReachableControllerIfPossible ( bootstrapContext ) )
337385 if ( reusedExistingController ) {
0 commit comments