Skip to content

Commit b6cadd2

Browse files
committed
fix: Actor executor fixes
1 parent 245d27d commit b6cadd2

5 files changed

Lines changed: 84 additions & 53 deletions

File tree

Sources/GoodNetworking/Logging/NetworkLogger.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public enum LogLevel: String, CaseIterable {
1919
public protocol NetworkLogger: Sendable {
2020

2121
/// Logs the given message with a specific log level, file name, and line number.
22-
nonisolated func logNetworkEvent(
22+
func logNetworkEvent(
2323
message: Any,
2424
level: LogLevel,
2525
file: String,

Sources/GoodNetworking/Models/Header.swift

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ import Foundation
1111

1212
/// HTTP headers are colon separated name-value pairs used for specifying request
1313
/// details, authentication and more.
14-
public struct HTTPHeader: Equatable, Hashable, HeaderConvertible {
14+
public struct HTTPHeader: Equatable, Hashable, Sendable, HeaderConvertible {
1515

1616
public let name: String
1717
public let value: String
1818

19-
/// Try to initialize HTTPHeader from string value. If string value cannot be parsed
19+
/// Try to initialize ``HTTPHeader`` from string value. If string value cannot be parsed
2020
/// as a valid header, initialization fails with `nil`.
2121
/// - Parameter string: String to parse as a HTTP header
2222
public init?(from string: String) {
@@ -32,7 +32,7 @@ public struct HTTPHeader: Equatable, Hashable, HeaderConvertible {
3232
self.value = String(split[1])
3333
}
3434

35-
/// Initialize HTTPHeader from string value. String must be a valid header,
35+
/// Initialize ``HTTPHeader`` from string value. String must be a valid header,
3636
/// otherwise the initialization will trip an assertion.
3737
/// - Parameter string: String representation of a HTTP header
3838
public init(_ string: String) {
@@ -48,7 +48,7 @@ public struct HTTPHeader: Equatable, Hashable, HeaderConvertible {
4848
self.value = String(split[1])
4949
}
5050

51-
/// Initialize HTTPHeader as a name-value pair.
51+
/// Initialize ``HTTPHeader`` as a name-value pair.
5252
/// - Parameters:
5353
/// - name: Name of the header (part before colon)
5454
/// - value: Value of the header (part after colon)
@@ -82,14 +82,14 @@ extension HTTPHeader: CustomStringConvertible {
8282
// MARK: - HTTPHeaders
8383

8484
/// A collection of multiple headers. Can contain any entities convertible to
85-
/// `HTTPHeader` (`HeaderConvertible`). Final header names
85+
/// ``HTTPHeader`` (``HeaderConvertible``). Final header names
8686
/// and values are resolved at the time the request is sent.
8787
public struct HTTPHeaders: Sendable {
8888

8989
/// List of contained headers
9090
public var headers: [any HeaderConvertible]
9191

92-
/// Create collection of `HTTPHeader`-s from key-value dictionary mapped as
92+
/// Create collection of ``HTTPHeader``-s from key-value dictionary mapped as
9393
/// name-value header pairs.
9494
/// - Parameter headers: Dictionary, where keys are header names
9595
public init(_ headers: [String: String]) {
@@ -122,7 +122,7 @@ public struct HTTPHeaders: Sendable {
122122
}
123123

124124
/// Resolve all headers to their final values.
125-
/// - Returns: Array of resolved headers as `HTTPHeader`-s
125+
/// - Returns: Array of resolved headers as ``HTTPHeader``-s
126126
public func resolve() -> [HTTPHeader] {
127127
headers.map { $0.resolveHeader() }
128128
}
@@ -197,12 +197,16 @@ extension HTTPHeaders: CustomStringConvertible {
197197

198198
// MARK: - HeaderConvertible
199199

200-
/// Allows conforming entities to be converted to HTTPHeader
201-
/// for subsequent use in HTTP requests.
200+
/// Allows conforming entities to be converted to ``HTTPHeader``
201+
/// for use in HTTP requests.
202202
public protocol HeaderConvertible: Sendable {
203203

204-
/// Resolves the final name and value of the header
205-
/// - Returns: Valid HTTP header name-value pair
204+
/// Resolves the final name and value of the header.
205+
///
206+
/// This function will be called every time for each header
207+
/// before a network request is sent.
208+
///
209+
/// - Returns: Valid HTTP header (name-value pair)
206210
func resolveHeader() -> HTTPHeader
207211

208212
}

Sources/GoodNetworking/Models/URLConvertible.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import Foundation
1111

1212
/// `URLConvertible` defines a function that asynchronously resolves the base URL used for network requests.
1313
/// Classes or structs that conform to this protocol can implement their own logic for determining and returning the base URL.
14-
public protocol URLConvertible: Sendable {
14+
public protocol URLConvertible {
1515

1616
/// Resolves and returns the base URL for network requests asynchronously.
1717
///

Sources/GoodNetworking/Session/NetworkSession.swift

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,19 @@ import Foundation
99

1010
// MARK: - Initialization
1111

12-
@NetworkActor public final class NetworkSession: NSObject, Sendable {
12+
/// Main network session
13+
///
14+
/// Session description
15+
///
16+
/// - Base URL: resolved once per request
17+
/// - Session headers: resolved once per session
18+
/// - Request headers: resolved once per request
19+
/// - Interceptor: intercepts every request (adapts, decides if/when to retry)
20+
/// - Are retried requests intercepted again?
21+
/// - Describe in more detail how interceptors work
22+
/// - BaseURL provider pattern
23+
/// - Name is used for identification only, not used by the network session itself
24+
@NetworkActor public final class NetworkSession: NSObject {
1325

1426
nonisolated public let name: String
1527

@@ -39,19 +51,19 @@ import Foundation
3951
logger: any NetworkLogger = PrintNetworkLogger(),
4052
name: String? = nil
4153
) {
54+
self.name = name ?? "NetworkSession"
55+
4256
self.baseUrl = baseUrl
4357
self.sessionHeaders = baseHeaders
4458
self.interceptor = interceptor
4559
self.logger = logger
46-
self.name = name ?? "NetworkSession"
4760

4861
let operationQueue = OperationQueue()
4962
operationQueue.name = "NetworkActorSerialExecutorOperationQueue"
5063
operationQueue.underlyingQueue = NetworkActor.queue
5164

5265
let configuration = URLSessionConfiguration.ephemeral
53-
configuration.httpAdditionalHeaders = baseHeaders.map { $0.resolveHeader() }.reduce(into: [:], { $0[$1.name] = $1.value })
54-
66+
5567
self.configuration = configuration
5668
self.delegateQueue = operationQueue
5769

Sources/GoodNetworking/Utilities/NetworkActor.swift

Lines changed: 51 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -10,54 +10,69 @@ import Foundation
1010
// MARK: - Actor
1111

1212
@globalActor public actor NetworkActor {
13-
13+
14+
// MARK: - Static
15+
1416
public static let shared: NetworkActor = NetworkActor()
1517
public static let queue: DispatchQueue = DispatchQueue(label: "goodnetworking.queue")
16-
17-
private let executor: any SerialExecutor
18-
18+
19+
private static let executor: NetworkActorExecutor = NetworkActorExecutor()
20+
21+
// MARK: - Properties
22+
23+
// MARK: - Computed properties
24+
1925
public nonisolated var unownedExecutor: UnownedSerialExecutor {
20-
UnownedSerialExecutor(ordinary: executor)
21-
}
22-
23-
public init() {
24-
self.executor = NetworkActorSerialExecutor(queue: NetworkActor.queue)
26+
Self.executor.asUnownedSerialExecutor()
2527
}
26-
27-
public static func assumeIsolated<T : Sendable>(_ operation: @NetworkActor () throws -> T) rethrows -> T {
28-
typealias YesActor = @NetworkActor () throws -> T
29-
typealias NoActor = () throws -> T
30-
31-
dispatchPrecondition(condition: .onQueue(queue))
32-
33-
// To do the unsafe cast, we have to pretend it's @escaping.
34-
return try withoutActuallyEscaping(operation) { (_ fn: @escaping YesActor) throws -> T in
35-
let rawFn = unsafeBitCast(fn, to: NoActor.self)
36-
return try rawFn()
28+
29+
// MARK: - Initialization
30+
31+
private init() {}
32+
33+
// MARK: - Isolation
34+
35+
public static func assumeIsolated<T>(
36+
_ block: @NetworkActor () throws -> sending T
37+
) rethrows -> sending T {
38+
typealias YesActor = @NetworkActor () throws -> sending T
39+
typealias NoActor = () throws -> sending T
40+
41+
NetworkActor.preconditionIsolated()
42+
43+
return try withoutActuallyEscaping(block) { (_ fn: @escaping YesActor) throws -> sending T in
44+
try unsafeBitCast(fn, to: NoActor.self)()
3745
}
3846
}
39-
47+
4048
}
4149

42-
// MARK: - Executor
43-
44-
internal final class NetworkActorSerialExecutor: SerialExecutor {
45-
46-
private let queue: DispatchQueue
47-
48-
internal init(queue: DispatchQueue) {
49-
self.queue = queue
50-
}
51-
50+
internal final class NetworkActorExecutor: SerialExecutor {
51+
5252
internal func enqueue(_ job: UnownedJob) {
53-
let executor = self.asUnownedSerialExecutor()
54-
queue.async {
55-
job.runSynchronously(on: executor)
53+
NetworkActor.queue.async {
54+
job.runSynchronously(on: NetworkActor.sharedUnownedExecutor)
5655
}
5756
}
58-
57+
5958
internal func asUnownedSerialExecutor() -> UnownedSerialExecutor {
6059
UnownedSerialExecutor(ordinary: self)
6160
}
62-
61+
62+
// AVAILABLE: (iOS 26.0, macOS 26.0, *)
63+
internal func isIsolatingCurrentContext() -> Bool? {
64+
if OperationQueue.current?.underlyingQueue == NetworkActor.queue {
65+
return true
66+
} else {
67+
return nil
68+
}
69+
}
70+
71+
// AVAILABLE: (iOS 18.0, macOS 15.0, *)
72+
internal func checkIsolated() {
73+
guard isIsolatingCurrentContext() ?? false else {
74+
fatalError()
75+
}
76+
}
77+
6378
}

0 commit comments

Comments
 (0)