Skip to content

Commit 3064fff

Browse files
authored
Merge pull request #5 from swhitty/AsyncTimeoutSequence
AsyncTimeoutSequence
2 parents 1e1f9ce + bc4e271 commit 3064fff

File tree

4 files changed

+281
-4
lines changed

4 files changed

+281
-4
lines changed

Sources/AsyncTimeoutSequence.swift

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
//
2+
// AsyncTimeoutSequence.swift
3+
// swift-timeout
4+
//
5+
// Created by Simon Whitty on 03/06/2025.
6+
// Copyright 2025 Simon Whitty
7+
//
8+
// Distributed under the permissive MIT license
9+
// Get the latest version from here:
10+
//
11+
// https://github.com/swhitty/swift-timeout
12+
//
13+
// Permission is hereby granted, free of charge, to any person obtaining a copy
14+
// of this software and associated documentation files (the "Software"), to deal
15+
// in the Software without restriction, including without limitation the rights
16+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17+
// copies of the Software, and to permit persons to whom the Software is
18+
// furnished to do so, subject to the following conditions:
19+
//
20+
// The above copyright notice and this permission notice shall be included in all
21+
// copies or substantial portions of the Software.
22+
//
23+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29+
// SOFTWARE.
30+
//
31+
32+
import Foundation
33+
34+
public extension AsyncSequence where Element: Sendable {
35+
36+
/// Creates an asynchronous sequence that throws error if any iteration
37+
/// takes longer than provided `TimeInterval`.
38+
func timeout(seconds: TimeInterval) -> AsyncTimeoutSequence<Self> {
39+
AsyncTimeoutSequence(base: self, seconds: seconds)
40+
}
41+
42+
/// Creates an asynchronous sequence that throws error if any iteration
43+
/// takes longer than provided `Duration`.
44+
@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
45+
func timeout(duration: Duration) -> AsyncTimeoutSequence<Self> {
46+
AsyncTimeoutSequence(base: self, duration: duration)
47+
}
48+
}
49+
50+
public struct AsyncTimeoutSequence<Base: AsyncSequence>: AsyncSequence where Base.Element: Sendable {
51+
public typealias Element = Base.Element
52+
53+
private let base: Base
54+
private let interval: TimeoutInterval
55+
56+
public init(base: Base, seconds: TimeInterval) {
57+
self.base = base
58+
self.interval = .timeInterval(seconds)
59+
}
60+
61+
@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
62+
public init(base: Base, duration: Duration) {
63+
self.base = base
64+
self.interval = .duration(.init(duration))
65+
}
66+
67+
public func makeAsyncIterator() -> AsyncIterator {
68+
AsyncIterator(
69+
iterator: base.makeAsyncIterator(),
70+
interval: interval
71+
)
72+
}
73+
74+
public struct AsyncIterator: AsyncIteratorProtocol {
75+
private var iterator: Base.AsyncIterator
76+
private let interval: TimeoutInterval
77+
78+
init(iterator: Base.AsyncIterator, interval: TimeoutInterval) {
79+
self.iterator = iterator
80+
self.interval = interval
81+
}
82+
83+
public mutating func next() async throws -> Base.Element? {
84+
switch interval {
85+
case .timeInterval(let seconds):
86+
return try await withThrowingTimeout(seconds: seconds) {
87+
try await self.iterator.next()
88+
}
89+
90+
case .duration(let durationBox):
91+
guard #available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) else {
92+
fatalError("cannot occur")
93+
}
94+
return try await withThrowingTimeout(after: .now + durationBox.value) {
95+
try await self.iterator.next()
96+
}
97+
}
98+
}
99+
}
100+
}
101+
102+
enum TimeoutInterval {
103+
case timeInterval(TimeInterval)
104+
case duration(DurationBox)
105+
106+
struct DurationBox {
107+
private let storage: Any
108+
109+
@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
110+
var value: Duration {
111+
storage as! Duration
112+
}
113+
114+
@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
115+
init(_ duration: Duration) {
116+
self.storage = duration
117+
}
118+
}
119+
}

Sources/Task+SleepIndefinitely.swift

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@
2929
// SOFTWARE.
3030
//
3131

32-
#if compiler(>=6)
33-
3432
package extension Task<Never, Never> {
3533

3634
private typealias State = (isCancelled: Bool, continuation: CheckedContinuation<Void, any Error>?)
@@ -61,5 +59,3 @@ package extension Task<Never, Never> {
6159
fatalError("can never occur")
6260
}
6361
}
64-
65-
#endif

Tests/AsyncTimeoutSequenceTests.swift

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
//
2+
// AsyncTimeoutSequenceTests.swift
3+
// swift-timeout
4+
//
5+
// Created by Simon Whitty on 03/06/2025.
6+
// Copyright 2025 Simon Whitty
7+
//
8+
// Distributed under the permissive MIT license
9+
// Get the latest version from here:
10+
//
11+
// https://github.com/swhitty/swift-timeout
12+
//
13+
// Permission is hereby granted, free of charge, to any person obtaining a copy
14+
// of this software and associated documentation files (the "Software"), to deal
15+
// in the Software without restriction, including without limitation the rights
16+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17+
// copies of the Software, and to permit persons to whom the Software is
18+
// furnished to do so, subject to the following conditions:
19+
//
20+
// The above copyright notice and this permission notice shall be included in all
21+
// copies or substantial portions of the Software.
22+
//
23+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29+
// SOFTWARE.
30+
//
31+
32+
#if canImport(Testing)
33+
@testable import Timeout
34+
import Testing
35+
36+
struct AsyncTimeoutSequenceTests {
37+
38+
@Test
39+
func timeoutSeconds() async throws {
40+
let (stream, continuation) = AsyncStream<Int>.makeStream()
41+
let t = Task {
42+
continuation.yield(1)
43+
try await Task.sleep(nanoseconds: 1_000)
44+
continuation.yield(2)
45+
try await Task.sleepIndefinitely()
46+
}
47+
defer { t.cancel() }
48+
var iterator = stream.timeout(seconds: 0.1).makeAsyncIterator()
49+
50+
#expect(try await iterator.next() == 1)
51+
#expect(try await iterator.next() == 2)
52+
await #expect(throws: TimeoutError.self) {
53+
try await iterator.next()
54+
}
55+
}
56+
57+
@Test
58+
func timeoutDuration() async throws {
59+
let (stream, continuation) = AsyncStream<Int>.makeStream()
60+
let t = Task {
61+
continuation.yield(1)
62+
try await Task.sleep(nanoseconds: 1_000)
63+
continuation.yield(2)
64+
try await Task.sleepIndefinitely()
65+
}
66+
defer { t.cancel() }
67+
var iterator = stream.timeout(duration: .milliseconds(100)).makeAsyncIterator()
68+
69+
#expect(try await iterator.next() == 1)
70+
#expect(try await iterator.next() == 2)
71+
await #expect(throws: TimeoutError.self) {
72+
try await iterator.next()
73+
}
74+
}
75+
}
76+
#endif
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
//
2+
// AsyncTimeoutSequenceXCTests.swift
3+
// swift-timeout
4+
//
5+
// Created by Simon Whitty on 03/06/2025.
6+
// Copyright 2025 Simon Whitty
7+
//
8+
// Distributed under the permissive MIT license
9+
// Get the latest version from here:
10+
//
11+
// https://github.com/swhitty/swift-timeout
12+
//
13+
// Permission is hereby granted, free of charge, to any person obtaining a copy
14+
// of this software and associated documentation files (the "Software"), to deal
15+
// in the Software without restriction, including without limitation the rights
16+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17+
// copies of the Software, and to permit persons to whom the Software is
18+
// furnished to do so, subject to the following conditions:
19+
//
20+
// The above copyright notice and this permission notice shall be included in all
21+
// copies or substantial portions of the Software.
22+
//
23+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29+
// SOFTWARE.
30+
//
31+
32+
#if !canImport(Testing)
33+
@testable import Timeout
34+
import XCTest
35+
36+
final class AsyncTimeoutSequenceXCTests: XCTestCase {
37+
38+
func testTimeoutSeconds() async throws {
39+
let (stream, continuation) = AsyncStream<Int>.makeStream()
40+
let t = Task {
41+
continuation.yield(1)
42+
try await Task.sleep(nanoseconds: 1_000)
43+
continuation.yield(2)
44+
try await Task.sleepIndefinitely()
45+
}
46+
defer { t.cancel() }
47+
var iterator = stream.timeout(seconds: 0.1).makeAsyncIterator()
48+
49+
var val = try await iterator.next()
50+
XCTAssertEqual(val, 1)
51+
val = try await iterator.next()
52+
XCTAssertEqual(val, 2)
53+
54+
do {
55+
_ = try await iterator.next()
56+
XCTFail("expected error")
57+
} catch {
58+
XCTAssertTrue(error is TimeoutError)
59+
}
60+
}
61+
62+
func testTimeoutDuration() async throws {
63+
let (stream, continuation) = AsyncStream<Int>.makeStream()
64+
let t = Task {
65+
continuation.yield(1)
66+
try await Task.sleep(nanoseconds: 1_000)
67+
continuation.yield(2)
68+
try await Task.sleepIndefinitely()
69+
}
70+
defer { t.cancel() }
71+
var iterator = stream.timeout(duration: .milliseconds(100)).makeAsyncIterator()
72+
73+
var val = try await iterator.next()
74+
XCTAssertEqual(val, 1)
75+
val = try await iterator.next()
76+
XCTAssertEqual(val, 2)
77+
78+
do {
79+
_ = try await iterator.next()
80+
XCTFail("expected error")
81+
} catch {
82+
XCTAssertTrue(error is TimeoutError)
83+
}
84+
}
85+
}
86+
#endif

0 commit comments

Comments
 (0)