@@ -15,12 +15,43 @@ internal let defaultPollingConfiguration = (
1515 pollingInterval: Duration . milliseconds ( 1 )
1616)
1717
18+ /// A type defining when to stop polling.
19+ /// This also determines what happens if the duration elapses during polling.
20+ @_spi ( Experimental)
21+ public enum PollingStopCondition : Sendable , Equatable , Codable {
22+ /// Evaluates the expression until the first time it returns true.
23+ /// If it does not pass once by the time the timeout is reached, then a
24+ /// failure will be reported.
25+ case firstPass
26+
27+ /// Evaluates the expression until the first time it returns false.
28+ /// If the expression returns false, then a failure will be reported.
29+ /// If the expression only returns true before the timeout is reached, then
30+ /// no failure will be reported.
31+ /// If the expression does not finish evaluating before the timeout is
32+ /// reached, then a failure will be reported.
33+ case stopsPassing
34+ }
35+
36+ /// A type describing why polling failed
37+ @_spi ( Experimental)
38+ public enum PollingFailureReason : Sendable , Codable {
39+ /// The polling failed because it was cancelled using `Task.cancel`.
40+ case cancelled
41+
42+ /// The polling failed because the stop condition failed.
43+ case stopConditionFailed( PollingStopCondition )
44+ }
45+
1846/// A type describing an error thrown when polling fails.
1947@_spi ( Experimental)
2048public struct PollingFailedError : Error , Sendable , Codable {
2149 /// A user-specified comment describing this confirmation
2250 public var comment : Comment ?
2351
52+ /// Why polling failed, either cancelled, or because the stop condition failed.
53+ public var reason : PollingFailureReason
54+
2455 /// A ``SourceContext`` indicating where and how this confirmation was called
2556 @_spi ( ForToolsIntegrationOnly)
2657 public var sourceContext : SourceContext
@@ -30,13 +61,16 @@ public struct PollingFailedError: Error, Sendable, Codable {
3061 /// - Parameters:
3162 /// - comment: A user-specified comment describing this confirmation.
3263 /// Defaults to `nil`.
64+ /// - reason: The reason why polling failed.
3365 /// - sourceContext: A ``SourceContext`` indicating where and how this
3466 /// confirmation was called.
35- public init (
67+ init (
3668 comment: Comment ? = nil ,
37- sourceContext: SourceContext
69+ reason: PollingFailureReason ,
70+ sourceContext: SourceContext ,
3871 ) {
3972 self . comment = comment
73+ self . reason = reason
4074 self . sourceContext = sourceContext
4175 }
4276}
@@ -46,29 +80,14 @@ extension PollingFailedError: CustomIssueRepresentable {
4680 if let comment {
4781 issue. comments. append ( comment)
4882 }
49- issue. kind = . pollingConfirmationFailed
83+ issue. kind = . pollingConfirmationFailed(
84+ reason: reason
85+ )
5086 issue. sourceContext = sourceContext
5187 return issue
5288 }
5389}
5490
55- /// A type defining when to stop polling early.
56- /// This also determines what happens if the duration elapses during polling.
57- public enum PollingStopCondition : Sendable , Equatable {
58- /// Evaluates the expression until the first time it returns true.
59- /// If it does not pass once by the time the timeout is reached, then a
60- /// failure will be reported.
61- case firstPass
62-
63- /// Evaluates the expression until the first time it returns false.
64- /// If the expression returns false, then a failure will be reported.
65- /// If the expression only returns true before the timeout is reached, then
66- /// no failure will be reported.
67- /// If the expression does not finish evaluating before the timeout is
68- /// reached, then a failure will be reported.
69- case stopsPassing
70- }
71-
7291/// Poll expression within the duration based on the given stop condition
7392///
7493/// - Parameters:
@@ -229,23 +248,46 @@ private func getValueFromTrait<TraitKind, Value>(
229248}
230249
231250extension PollingStopCondition {
251+ /// The result of processing polling.
252+ enum PollingProcessResult < R> {
253+ /// Continue to poll.
254+ case continuePolling
255+ /// Polling succeeded.
256+ case succeeded( R )
257+ /// Polling failed.
258+ case failed
259+ }
232260 /// Process the result of a polled expression and decide whether to continue
233261 /// polling.
234262 ///
235263 /// - Parameters:
236264 /// - expressionResult: The result of the polled expression.
265+ /// - wasLastPollingAttempt: If this was the last time we're attempting to
266+ /// poll.
237267 ///
238- /// - Returns: A poll result (if polling should stop), or nil (if polling
239- /// should continue) .
240- @ available ( _clockAPI , * )
241- fileprivate func shouldStopPolling (
242- expressionResult result : Bool
243- ) -> Bool {
268+ /// - Returns: A process result. Whether to continue polling, stop because
269+ /// polling failed, or stop because polling succeeded .
270+ fileprivate func process < R > (
271+ expressionResult result : R ? ,
272+ wasLastPollingAttempt : Bool
273+ ) -> PollingProcessResult < R > {
244274 switch self {
245275 case . firstPass:
246- return result
276+ if let result {
277+ return . succeeded( result)
278+ } else {
279+ return . continuePolling
280+ }
247281 case . stopsPassing:
248- return !result
282+ if let result {
283+ if wasLastPollingAttempt {
284+ return . succeeded( result)
285+ } else {
286+ return . continuePolling
287+ }
288+ } else {
289+ return . failed
290+ }
249291 }
250292 }
251293
@@ -344,11 +386,30 @@ private struct Poller {
344386
345387 let iterations = max ( Int ( duration. seconds ( ) / interval. seconds ( ) ) , 1 )
346388
347- if let value = await poll ( iterations: iterations, expression: body) {
389+ let failureReason : PollingFailureReason
390+ switch await poll ( iterations: iterations, expression: body) {
391+ case let . succeeded( value) :
348392 return value
349- } else {
350- throw PollingFailedError ( comment: comment, sourceContext: sourceContext)
393+ case . cancelled:
394+ failureReason = . cancelled
395+ case . failed:
396+ failureReason = . stopConditionFailed( stopCondition)
351397 }
398+ throw PollingFailedError (
399+ comment: comment,
400+ reason: failureReason,
401+ sourceContext: sourceContext
402+ )
403+ }
404+
405+ /// The result of polling.
406+ private enum PollingResult < R> {
407+ /// Polling was cancelled using `Task.Cancel`. This is treated as a failure.
408+ case cancelled
409+ /// The stop condition failed.
410+ case failed
411+ /// The stop condition passed.
412+ case succeeded( R )
352413 }
353414
354415 /// This function contains the logic for continuously polling an expression,
@@ -363,26 +424,27 @@ private struct Poller {
363424 iterations: Int ,
364425 isolation: isolated ( any Actor ) ? = #isolation,
365426 expression: @escaping ( ) async -> sending R?
366- ) async -> R ? {
367- var lastResult : R ?
427+ ) async -> PollingResult < R > {
368428 for iteration in 0 ..< iterations {
369- lastResult = await expression ( )
370- if stopCondition. shouldStopPolling ( expressionResult: lastResult != nil ) {
371- return lastResult
372- }
373- if iteration == ( iterations - 1 ) {
374- // don't bother sleeping if it's the last iteration.
375- break
429+ switch stopCondition. process (
430+ expressionResult: await expression ( ) ,
431+ wasLastPollingAttempt: iteration == ( iterations - 1 )
432+ ) {
433+ case . continuePolling: break
434+ case let . succeeded( value) :
435+ return . succeeded( value)
436+ case . failed:
437+ return . failed
376438 }
377439 do {
378440 try await Task . sleep ( for: interval)
379441 } catch {
380442 // `Task.sleep` should only throw an error if it's cancelled
381443 // during the sleep period.
382- return nil
444+ return . cancelled
383445 }
384446 }
385- return lastResult
447+ return . failed
386448 }
387449}
388450
0 commit comments