@@ -150,8 +150,71 @@ Deno.test("CircuitBreaker transitions to half_open after cooldown", async () =>
150150 // Advance time past cooldown
151151 time . tick ( 1001 ) ;
152152
153+ // isAvailable resolves time-based transitions
154+ assertEquals ( breaker . isAvailable , true ) ;
153155 assertEquals ( breaker . state , "half_open" ) ;
156+ } ) ;
157+
158+ Deno . test ( "CircuitBreaker state getter is pure and does not trigger transitions" , async ( ) => {
159+ using time = new FakeTime ( ) ;
160+
161+ const transitions : Array < [ CircuitState , CircuitState ] > = [ ] ;
162+ const breaker = new CircuitBreaker ( {
163+ failureThreshold : 1 ,
164+ cooldownMs : 1000 ,
165+ onStateChange : ( from , to ) => transitions . push ( [ from , to ] ) ,
166+ } ) ;
167+
168+ // Open the circuit
169+ try {
170+ await breaker . execute ( ( ) => Promise . reject ( new Error ( "fail" ) ) ) ;
171+ } catch { /* expected */ }
172+ assertEquals ( breaker . state , "open" ) ;
173+ assertEquals ( transitions , [ [ "closed" , "open" ] ] ) ;
174+
175+ // Advance time past cooldown
176+ time . tick ( 1001 ) ;
177+
178+ // Reading state should NOT trigger transition or callbacks
179+ assertEquals ( breaker . state , "open" ) ; // Still shows "open" (stale)
180+ assertEquals ( transitions . length , 1 ) ; // No new transitions
181+
182+ // Reading state multiple times should still not trigger
183+ breaker . state ;
184+ breaker . state ;
185+ assertEquals ( transitions . length , 1 ) ;
186+
187+ // Only isAvailable triggers the transition
154188 assertEquals ( breaker . isAvailable , true ) ;
189+ assertEquals ( breaker . state , "half_open" ) ;
190+ assertEquals ( transitions , [ [ "closed" , "open" ] , [ "open" , "half_open" ] ] ) ;
191+ } ) ;
192+
193+ Deno . test ( "CircuitBreaker execute() resolves stale state before checking" , async ( ) => {
194+ using time = new FakeTime ( ) ;
195+
196+ const breaker = new CircuitBreaker ( {
197+ failureThreshold : 1 ,
198+ cooldownMs : 1000 ,
199+ successThreshold : 1 ,
200+ } ) ;
201+
202+ // Open the circuit
203+ try {
204+ await breaker . execute ( ( ) => Promise . reject ( new Error ( "fail" ) ) ) ;
205+ } catch { /* expected */ }
206+ assertEquals ( breaker . state , "open" ) ;
207+
208+ // Advance time past cooldown
209+ time . tick ( 1001 ) ;
210+
211+ // State is stale (still shows open)
212+ assertEquals ( breaker . state , "open" ) ;
213+
214+ // But execute() should resolve the transition and succeed
215+ const result = await breaker . execute ( ( ) => Promise . resolve ( "success" ) ) ;
216+ assertEquals ( result , "success" ) ;
217+ assertEquals ( breaker . state , "closed" ) ; // Now closed after successful execution
155218} ) ;
156219
157220Deno . test ( "CircuitBreaker closes from half_open after success threshold" , async ( ) => {
@@ -168,8 +231,9 @@ Deno.test("CircuitBreaker closes from half_open after success threshold", async
168231 await breaker . execute ( ( ) => Promise . reject ( new Error ( "fail" ) ) ) ;
169232 } catch { /* expected */ }
170233
171- // Enter half_open
234+ // Enter half_open (isAvailable resolves the transition)
172235 time . tick ( 1001 ) ;
236+ assertEquals ( breaker . isAvailable , true ) ;
173237 assertEquals ( breaker . state , "half_open" ) ;
174238
175239 // First success
@@ -195,8 +259,9 @@ Deno.test("CircuitBreaker reopens from half_open on failure", async () => {
195259 await breaker . execute ( ( ) => Promise . reject ( new Error ( "fail" ) ) ) ;
196260 } catch { /* expected */ }
197261
198- // Enter half_open
262+ // Enter half_open (isAvailable resolves the transition)
199263 time . tick ( 1001 ) ;
264+ assertEquals ( breaker . isAvailable , true ) ;
200265 assertEquals ( breaker . state , "half_open" ) ;
201266
202267 // Failure in half_open should reopen
@@ -358,10 +423,9 @@ Deno.test("CircuitBreaker onStateChange callback is invoked", async () => {
358423 } catch { /* expected */ }
359424 assertEquals ( transitions , [ [ "closed" , "open" ] ] ) ;
360425
361- // Half-open
426+ // Half-open (isAvailable resolves the transition)
362427 time . tick ( 1001 ) ;
363- // Access state to trigger transition
364- breaker . state ;
428+ breaker . isAvailable ;
365429 assertEquals ( transitions , [ [ "closed" , "open" ] , [ "open" , "half_open" ] ] ) ;
366430
367431 // Close
@@ -455,8 +519,9 @@ Deno.test("CircuitBreaker half_open limits concurrent requests", async () => {
455519 await breaker . execute ( ( ) => Promise . reject ( new Error ( "fail" ) ) ) ;
456520 } catch { /* expected */ }
457521
458- // Enter half-open
522+ // Enter half-open (isAvailable resolves the transition)
459523 time . tick ( 1001 ) ;
524+ assertEquals ( breaker . isAvailable , true ) ;
460525 assertEquals ( breaker . state , "half_open" ) ;
461526
462527 // Start a slow request
@@ -521,7 +586,8 @@ Deno.test("CircuitBreaker with zero cooldown transitions immediately", async ()
521586 } catch { /* expected */ }
522587
523588 // Should immediately be half_open (or allow immediate transition)
524- // Since cooldown is 0, checking state should transition
589+ // Since cooldown is 0, isAvailable resolves the transition
590+ assertEquals ( breaker . isAvailable , true ) ;
525591 assertEquals ( breaker . state , "half_open" ) ;
526592
527593 // Should be able to close immediately
@@ -658,8 +724,9 @@ Deno.test("CircuitBreaker half_open failure invokes onStateChange and onOpen", a
658724 assertEquals ( transitions , [ [ "closed" , "open" ] ] ) ;
659725 assertEquals ( openCalls , [ 1 ] ) ;
660726
661- // Enter half-open
727+ // Enter half-open (isAvailable resolves the transition)
662728 time . tick ( 1001 ) ;
729+ assertEquals ( breaker . isAvailable , true ) ;
663730 assertEquals ( breaker . state , "half_open" ) ;
664731 assertEquals ( transitions , [ [ "closed" , "open" ] , [ "open" , "half_open" ] ] ) ;
665732
@@ -690,8 +757,9 @@ Deno.test("CircuitBreaker consecutiveSuccesses tracked in half_open getStats", a
690757 await breaker . execute ( ( ) => Promise . reject ( new Error ( "fail" ) ) ) ;
691758 } catch { /* expected */ }
692759
693- // Enter half-open
760+ // Enter half-open (isAvailable resolves the transition)
694761 time . tick ( 1001 ) ;
762+ assertEquals ( breaker . isAvailable , true ) ;
695763 assertEquals ( breaker . state , "half_open" ) ;
696764
697765 // First success
@@ -725,8 +793,9 @@ Deno.test("CircuitBreaker isResultFailure in half_open reopens circuit", async (
725793 await breaker . execute ( ( ) => Promise . reject ( new Error ( "fail" ) ) ) ;
726794 } catch { /* expected */ }
727795
728- // Enter half-open
796+ // Enter half-open (isAvailable resolves the transition)
729797 time . tick ( 1001 ) ;
798+ assertEquals ( breaker . isAvailable , true ) ;
730799 assertEquals ( breaker . state , "half_open" ) ;
731800
732801 // Result failure in half-open should reopen
@@ -750,10 +819,10 @@ Deno.test("CircuitBreaker handles multiple half_open concurrent slots", async ()
750819 await breaker . execute ( ( ) => Promise . reject ( new Error ( "fail" ) ) ) ;
751820 } catch { /* expected */ }
752821
753- // Enter half-open
822+ // Enter half-open (isAvailable resolves the transition)
754823 time . tick ( 1001 ) ;
755- assertEquals ( breaker . state , "half_open" ) ;
756824 assertEquals ( breaker . isAvailable , true ) ;
825+ assertEquals ( breaker . state , "half_open" ) ;
757826
758827 // Start two concurrent requests (should both be allowed)
759828 let resolve1 : ( ( ) => void ) | undefined ;
0 commit comments