Skip to content

Commit e036ca0

Browse files
committed
AsyncTimeoutSequence
1 parent 1e1f9ce commit e036ca0

File tree

3 files changed

+275
-0
lines changed

3 files changed

+275
-0
lines changed

Sources/AsyncTimeoutSequence.swift

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
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+
func timeout(seconds: TimeInterval) -> AsyncTimeoutSequence<Self> {
37+
AsyncTimeoutSequence(base: self, seconds: seconds)
38+
}
39+
40+
@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
41+
func timeout(duration: Duration) -> AsyncTimeoutSequence<Self> {
42+
AsyncTimeoutSequence(base: self, duration: duration)
43+
}
44+
}
45+
46+
public struct AsyncTimeoutSequence<Base: AsyncSequence>: AsyncSequence where Base.Element: Sendable {
47+
private let base: Base
48+
private let interval: TimeoutInterval
49+
50+
public init(base: Base, seconds: TimeInterval) {
51+
self.base = base
52+
self.interval = .timeInterval(seconds)
53+
}
54+
55+
@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
56+
public init(base: Base, duration: Duration) {
57+
self.base = base
58+
self.interval = .duration(.init(duration))
59+
}
60+
61+
public func makeAsyncIterator() -> AsyncIterator {
62+
AsyncIterator(
63+
iterator: base.makeAsyncIterator(),
64+
interval: interval
65+
)
66+
}
67+
68+
public struct AsyncIterator: AsyncIteratorProtocol {
69+
private var iterator: Base.AsyncIterator
70+
private let interval: TimeoutInterval
71+
72+
init(iterator: Base.AsyncIterator, interval: TimeoutInterval) {
73+
self.iterator = iterator
74+
self.interval = interval
75+
}
76+
77+
public mutating func next() async throws -> Base.Element? {
78+
switch interval {
79+
case .timeInterval(let seconds):
80+
return try await withThrowingTimeout(seconds: seconds) {
81+
try await self.iterator.next()
82+
}
83+
84+
case .duration(let durationBox):
85+
guard #available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) else {
86+
fatalError("cannot occur")
87+
}
88+
return try await withThrowingTimeout(after: .now + durationBox.value) {
89+
try await self.iterator.next()
90+
}
91+
}
92+
}
93+
}
94+
}
95+
96+
enum TimeoutInterval {
97+
case timeInterval(TimeInterval)
98+
case duration(DurationBox)
99+
100+
struct DurationBox {
101+
private let storage: Any
102+
103+
@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
104+
var value: Duration {
105+
storage as! Duration
106+
}
107+
108+
@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
109+
init(_ duration: Duration) {
110+
self.storage = duration
111+
}
112+
}
113+
}

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)