Skip to content

Commit 061a27c

Browse files
committed
Fixes for strict-concurrency (Transferring workarounds)
1 parent 68d1e6e commit 061a27c

10 files changed

+103
-19
lines changed

FlyingFox/Tests/HTTPServerTests.swift

+6-1
Original file line numberDiff line numberDiff line change
@@ -532,7 +532,7 @@ extension HTTPServer {
532532

533533

534534
#if canImport(Darwin)
535-
extension URLSessionWebSocketTask.Message: Equatable {
535+
extension URLSessionWebSocketTask.Message {
536536
public static func == (lhs: URLSessionWebSocketTask.Message, rhs: URLSessionWebSocketTask.Message) -> Bool {
537537
switch (lhs, rhs) {
538538
case (.string(let lval), .string(let rval)):
@@ -544,6 +544,11 @@ extension URLSessionWebSocketTask.Message: Equatable {
544544
}
545545
}
546546
}
547+
#if compiler(>=6)
548+
extension URLSessionWebSocketTask.Message: @retroactive Equatable { }
549+
#else
550+
extension URLSessionWebSocketTask.Message: Equatable { }
551+
#endif
547552
#endif
548553

549554
extension Task where Success == Never, Failure == Never {

FlyingFox/Tests/Handlers/WebSocketHTTPHandlerTests.swift

+18-4
Original file line numberDiff line numberDiff line change
@@ -205,9 +205,23 @@ private extension WebSocketHTTPHandler {
205205

206206
private struct MockHandler: WSHandler {
207207
func makeFrames(for client: AsyncThrowingStream<WSFrame, any Error>) async throws -> AsyncStream<WSFrame> {
208-
var iterator = client.makeAsyncIterator()
209-
return AsyncStream<WSFrame> {
210-
try? await iterator.next()
211-
}
208+
UnsafeFrames(source: client).makeStream()
209+
}
210+
}
211+
212+
private final class UnsafeFrames: @unchecked Sendable {
213+
214+
private var iterator: AsyncThrowingStream<WSFrame, any Error>.Iterator
215+
216+
init(source: AsyncThrowingStream<WSFrame, any Error>) {
217+
self.iterator = source.makeAsyncIterator()
218+
}
219+
220+
func makeStream() -> AsyncStream<WSFrame> {
221+
AsyncStream<WSFrame> { await self.nextFrame() }
222+
}
223+
224+
func nextFrame() async -> WSFrame? {
225+
try? await iterator.next()
212226
}
213227
}

FlyingFox/Tests/WebSocket/WSFrameValidatorTests.swift

+19-2
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,24 @@ final class WSFrameValidatorTests: XCTestCase {
9696
private extension WSFrameValidator {
9797

9898
static func validate(_ frames: [WSFrame]) async throws -> AsyncThrowingStream<WSFrame, any Swift.Error> {
99-
var iterator = validateFrames(from: AsyncThrowingStream.make(frames)).makeAsyncIterator()
100-
return AsyncThrowingStream { try await iterator.next() }
99+
UnsafeFrames(frames: frames).makeStream()
101100
}
102101
}
102+
103+
private final class UnsafeFrames: @unchecked Sendable {
104+
105+
var iterator: AsyncThrowingCompactMapSequence<AsyncThrowingStream<WSFrame, any Error>, WSFrame>.Iterator
106+
107+
init(frames: [WSFrame]) {
108+
self.iterator = WSFrameValidator.validateFrames(from: AsyncThrowingStream.make(frames)).makeAsyncIterator()
109+
}
110+
111+
func makeStream() -> AsyncThrowingStream<WSFrame, any Error> {
112+
AsyncThrowingStream { try await self.nextFrame() }
113+
}
114+
115+
func nextFrame() async throws -> WSFrame? {
116+
try await iterator.next()
117+
}
118+
}
119+

FlyingSocks/Sources/AllocatedLock.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ package struct AllocatedLock<State>: @unchecked Sendable {
5353
}
5454

5555
@inlinable
56-
package func withLock<R>(_ body: @Sendable (inout State) throws -> R) rethrows -> R where R: Sendable {
56+
package func withLock<R>(_ body: @Sendable (inout State) throws -> R) rethrows -> R {
5757
storage.lock()
5858
defer { storage.unlock() }
5959
return try body(&storage.state)

FlyingSocks/Sources/AsyncBufferedCollection.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131

3232
import Foundation
3333

34-
package struct AsyncBufferedCollection<C: Collection>: AsyncBufferedSequence {
34+
package struct AsyncBufferedCollection<C: Collection>: AsyncBufferedSequence where C: Sendable, C.Element: Sendable {
3535
package typealias Element = C.Element
3636

3737
private let collection: C

FlyingSocks/Sources/AsyncBufferedEmptySequence.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
// SOFTWARE.
3030
//
3131

32-
package struct AsyncBufferedEmptySequence<Element>: Sendable, AsyncBufferedSequence {
32+
package struct AsyncBufferedEmptySequence<Element: Sendable>: Sendable, AsyncBufferedSequence {
3333

3434
private let completeImmediately: Bool
3535

FlyingSocks/Sources/AsyncBufferedSequence.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
//
3131

3232
/// AsyncSequence that is buffered and can optionally receive contiguous elements in chunks, instead of just one-at-a-time.
33-
public protocol AsyncBufferedSequence<Element>: AsyncSequence where AsyncIterator: AsyncBufferedIteratorProtocol {
33+
public protocol AsyncBufferedSequence<Element>: AsyncSequence, Sendable where AsyncIterator: AsyncBufferedIteratorProtocol, Element: Sendable {
3434

3535
}
3636

FlyingSocks/Sources/AsyncSharedReplaySequence.swift

+11-4
Original file line numberDiff line numberDiff line change
@@ -189,10 +189,9 @@ extension AsyncSharedReplaySequence {
189189

190190
private func requestNextChunk(atMost count: Int) async throws -> (some Collection<Element>)? {
191191
do {
192-
var iterator = try state.makeAsyncIterator()
193-
194-
let chunk = try await iterator.nextBuffer(suggested: count)
195-
state = .iterating(iterator)
192+
var iterator = try Transferring(state.makeAsyncIterator())
193+
let chunk = try await iterator.nextBuffer(suggested: count)?.value
194+
state = .iterating(iterator.value)
196195
return chunk
197196
} catch {
198197
state = .error(error)
@@ -202,6 +201,14 @@ extension AsyncSharedReplaySequence {
202201
}
203202
}
204203

204+
private extension Transferring where Value: AsyncBufferedIteratorProtocol {
205+
206+
mutating func nextBuffer(suggested count: Int) async throws -> Transferring<Value.Buffer>? {
207+
guard let buffer = try await value.nextBuffer(suggested: count) else { return nil }
208+
return Transferring<Value.Buffer>(buffer)
209+
}
210+
}
211+
205212
extension AsyncSharedReplaySequence.SharedBuffer.State {
206213

207214
mutating func makeAsyncIterator() throws -> Base.AsyncIterator {

FlyingSocks/Sources/ConsumingAsyncSequence.swift

+7-4
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,7 @@ extension ConsumingAsyncSequence {
9292
guard let element = try await iterator.next() else {
9393
return nil
9494
}
95-
let newState = State.iterating(iterator, index: index + 1)
96-
state.withLock { $0 = newState }
95+
setState(.iterating(iterator, index: index + 1))
9796
return element
9897
}
9998

@@ -102,10 +101,14 @@ extension ConsumingAsyncSequence {
102101
guard let buffer = try await iterator.nextBuffer(suggested: count) else {
103102
return nil
104103
}
105-
let newState = State.iterating(iterator, index: index + buffer.count)
106-
state.withLock { $0 = newState }
104+
setState(.iterating(iterator, index: index + buffer.count))
107105
return buffer
108106
}
107+
108+
func setState(_ state: State) {
109+
let t = Transferring(state)
110+
self.state.withLock { $0 = t.value }
111+
}
109112
}
110113
}
111114

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//
2+
// Transferring.swift
3+
// FlyingFox
4+
//
5+
// Created by Simon Whitty on 09/08/2024.
6+
// Copyright © 2024 Simon Whitty. All rights reserved.
7+
//
8+
// Distributed under the permissive MIT license
9+
// Get the latest version from here:
10+
//
11+
// https://github.com/swhitty/FlyingFox
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+
package struct Transferring<Value>: @unchecked Sendable {
33+
package var value: Value
34+
35+
package init(_ value: Value) {
36+
self.value = value
37+
}
38+
}

0 commit comments

Comments
 (0)