@@ -15,12 +15,43 @@ internal let defaultPollingConfiguration = (
15
15
pollingInterval: Duration . milliseconds ( 1 )
16
16
)
17
17
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
+
18
46
/// A type describing an error thrown when polling fails.
19
47
@_spi ( Experimental)
20
48
public struct PollingFailedError : Error , Sendable , Codable {
21
49
/// A user-specified comment describing this confirmation
22
50
public var comment : Comment ?
23
51
52
+ /// Why polling failed, either cancelled, or because the stop condition failed.
53
+ public var reason : PollingFailureReason
54
+
24
55
/// A ``SourceContext`` indicating where and how this confirmation was called
25
56
@_spi ( ForToolsIntegrationOnly)
26
57
public var sourceContext : SourceContext
@@ -30,13 +61,16 @@ public struct PollingFailedError: Error, Sendable, Codable {
30
61
/// - Parameters:
31
62
/// - comment: A user-specified comment describing this confirmation.
32
63
/// Defaults to `nil`.
64
+ /// - reason: The reason why polling failed.
33
65
/// - sourceContext: A ``SourceContext`` indicating where and how this
34
66
/// confirmation was called.
35
- public init (
67
+ init (
36
68
comment: Comment ? = nil ,
37
- sourceContext: SourceContext
69
+ reason: PollingFailureReason ,
70
+ sourceContext: SourceContext ,
38
71
) {
39
72
self . comment = comment
73
+ self . reason = reason
40
74
self . sourceContext = sourceContext
41
75
}
42
76
}
@@ -46,29 +80,14 @@ extension PollingFailedError: CustomIssueRepresentable {
46
80
if let comment {
47
81
issue. comments. append ( comment)
48
82
}
49
- issue. kind = . pollingConfirmationFailed
83
+ issue. kind = . pollingConfirmationFailed(
84
+ reason: reason
85
+ )
50
86
issue. sourceContext = sourceContext
51
87
return issue
52
88
}
53
89
}
54
90
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
-
72
91
/// Poll expression within the duration based on the given stop condition
73
92
///
74
93
/// - Parameters:
@@ -229,23 +248,46 @@ private func getValueFromTrait<TraitKind, Value>(
229
248
}
230
249
231
250
extension 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
+ }
232
260
/// Process the result of a polled expression and decide whether to continue
233
261
/// polling.
234
262
///
235
263
/// - Parameters:
236
264
/// - expressionResult: The result of the polled expression.
265
+ /// - wasLastPollingAttempt: If this was the last time we're attempting to
266
+ /// poll.
237
267
///
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 > {
244
274
switch self {
245
275
case . firstPass:
246
- return result
276
+ if let result {
277
+ return . succeeded( result)
278
+ } else {
279
+ return . continuePolling
280
+ }
247
281
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
+ }
249
291
}
250
292
}
251
293
@@ -344,11 +386,30 @@ private struct Poller {
344
386
345
387
let iterations = max ( Int ( duration. seconds ( ) / interval. seconds ( ) ) , 1 )
346
388
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) :
348
392
return value
349
- } else {
350
- throw PollingFailedError ( comment: comment, sourceContext: sourceContext)
393
+ case . cancelled:
394
+ failureReason = . cancelled
395
+ case . failed:
396
+ failureReason = . stopConditionFailed( stopCondition)
351
397
}
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 )
352
413
}
353
414
354
415
/// This function contains the logic for continuously polling an expression,
@@ -363,26 +424,27 @@ private struct Poller {
363
424
iterations: Int ,
364
425
isolation: isolated ( any Actor ) ? = #isolation,
365
426
expression: @escaping ( ) async -> sending R?
366
- ) async -> R ? {
367
- var lastResult : R ?
427
+ ) async -> PollingResult < R > {
368
428
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
376
438
}
377
439
do {
378
440
try await Task . sleep ( for: interval)
379
441
} catch {
380
442
// `Task.sleep` should only throw an error if it's cancelled
381
443
// during the sleep period.
382
- return nil
444
+ return . cancelled
383
445
}
384
446
}
385
- return lastResult
447
+ return . failed
386
448
}
387
449
}
388
450
0 commit comments