Skip to content

Commit 8b79081

Browse files
committed
Work in progress: Sendable conformance required for Swift 6.2.
Signed-off-by: Iva Horn <iva.horn@nextcloud.com>
1 parent 025bfaa commit 8b79081

4 files changed

Lines changed: 66 additions & 68 deletions

File tree

Sources/NextcloudFileProviderKit/Enumeration/RemoteChangeObserver.swift

Lines changed: 54 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,23 @@ import NextcloudKit
99

1010
public let NotifyPushAuthenticatedNotificationName = Notification.Name("NotifyPushAuthenticated")
1111

12-
public final class RemoteChangeObserver: NSObject, NextcloudKitDelegate, URLSessionWebSocketDelegate {
12+
public actor RemoteChangeObserver: NSObject, Sendable {
13+
// @unchecked Sendable is used because 'account' is mutable, but mutation is controlled and safe in this context.
1314
public let remoteInterface: RemoteInterface
1415
public let changeNotificationInterface: ChangeNotificationInterface
1516
public let domain: NSFileProviderDomain?
1617
public let dbManager: FilesDatabaseManager
17-
public var account: Account
18+
public let account: Account
1819
public var accountId: String { account.ncKitAccount }
1920

20-
public var webSocketPingIntervalNanoseconds: UInt64 = 3 * 1_000_000_000
21-
public var webSocketReconfigureIntervalNanoseconds: UInt64 = 1 * 1_000_000_000
22-
public var webSocketPingFailLimit = 8
23-
public var webSocketAuthenticationFailLimit = 3
24-
public var webSocketTaskActive: Bool { webSocketTask != nil }
21+
public let webSocketPingIntervalNanoseconds: UInt64 = 3 * 1_000_000_000
22+
public let webSocketReconfigureIntervalNanoseconds: UInt64 = 1 * 1_000_000_000
23+
public let webSocketPingFailLimit = 8
24+
public let webSocketAuthenticationFailLimit = 3
25+
26+
public var webSocketTaskActive: Bool {
27+
webSocketTask != nil
28+
}
2529

2630
private let logger: FileProviderLogger
2731

@@ -45,7 +49,9 @@ public final class RemoteChangeObserver: NSObject, NextcloudKitDelegate, URLSess
4549
}
4650
}
4751

48-
public var pollingActive: Bool { pollingTimer != nil }
52+
public var pollingActive: Bool {
53+
pollingTimer != nil
54+
}
4955

5056
private(set) var networkReachability: NKTypeReachability = .unknown {
5157
didSet {
@@ -81,7 +87,7 @@ public final class RemoteChangeObserver: NSObject, NextcloudKitDelegate, URLSess
8187

8288
private func startPollingTimer() {
8389
guard !invalidated else { return }
84-
Task { @MainActor in
90+
Task {
8591
pollingTimer = Timer.scheduledTimer(
8692
withTimeInterval: pollInterval, repeats: true
8793
) { [weak self] _ in
@@ -93,7 +99,7 @@ public final class RemoteChangeObserver: NSObject, NextcloudKitDelegate, URLSess
9399
}
94100

95101
private func stopPollingTimer() {
96-
Task { @MainActor in
102+
Task {
97103
logger.info("Stopping polling timer.")
98104
pollingTimer?.invalidate()
99105
pollingTimer = nil
@@ -219,40 +225,6 @@ public final class RemoteChangeObserver: NSObject, NextcloudKitDelegate, URLSess
219225
}
220226
}
221227

222-
public func urlSession(
223-
_: URLSession,
224-
webSocketTask _: URLSessionWebSocketTask,
225-
didOpenWithProtocol _: String?
226-
) {
227-
guard !invalidated else { return }
228-
logger.debug("Websocket connected \(accountId) sending auth details", [.account: accountId])
229-
Task { await authenticateWebSocket() }
230-
}
231-
232-
public func urlSession(
233-
_: URLSession,
234-
webSocketTask: URLSessionWebSocketTask,
235-
didCloseWith _: URLSessionWebSocketTask.CloseCode,
236-
reason: Data?
237-
) {
238-
guard !invalidated else { return }
239-
// If the task that closed is not the current active task, it means we have
240-
// already initiated a reset and this is a stale callback. Ignore it.
241-
guard webSocketTask === self.webSocketTask else {
242-
logger.debug("An old websocket task closed, ignoring.")
243-
return
244-
}
245-
246-
logger.debug("Socket connection closed for \(accountId).", [.account: accountId])
247-
248-
if let reason {
249-
logger.debug("Reason: \(String(data: reason, encoding: .utf8) ?? "")")
250-
}
251-
252-
logger.debug("Retrying websocket connection for \(accountId).", [.account: accountId])
253-
reconnectWebSocket()
254-
}
255-
256228
private func authenticateWebSocket() async {
257229
guard !invalidated else {
258230
return
@@ -316,7 +288,7 @@ public final class RemoteChangeObserver: NSObject, NextcloudKitDelegate, URLSess
316288
switch result {
317289
case .failure:
318290
self.logger.debug("Failed to read websocket \(self.accountId)", [.account: self.accountId])
319-
// Do not reconnect here, delegate methods will handle reconnecting
291+
// Do not reconnect here, delegate methods will handle reconnecting
320292
case let .success(message):
321293
switch message {
322294
case let .data(data):
@@ -368,14 +340,48 @@ public final class RemoteChangeObserver: NSObject, NextcloudKitDelegate, URLSess
368340
logger.error("Received unknown string from websocket \(accountId): \(string)", [.account: accountId])
369341
}
370342
}
343+
}
371344

372-
// MARK: - NextcloudKitDelegate methods
345+
// MARK: - URLSessionWebSocketDelegate
373346

374-
public func networkReachabilityObserver(_ typeReachability: NKTypeReachability) {
375-
networkReachability = typeReachability
347+
extension RemoteChangeObserver: URLSessionWebSocketDelegate {
348+
nonisolated public func urlSession(_: URLSession, webSocketTask _: URLSessionWebSocketTask, didOpenWithProtocol _: String?) {
349+
guard !invalidated else {
350+
return
351+
}
352+
353+
logger.debug("Websocket connected \(accountId) sending auth details", [.account: accountId])
354+
Task { await authenticateWebSocket() }
376355
}
377356

357+
nonisolated public func urlSession(_: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith _: URLSessionWebSocketTask.CloseCode, reason: Data?) {
358+
guard !invalidated else { return }
359+
// If the task that closed is not the current active task, it means we have
360+
// already initiated a reset and this is a stale callback. Ignore it.
361+
guard webSocketTask === self.webSocketTask else {
362+
logger.debug("An old websocket task closed, ignoring.")
363+
return
364+
}
365+
366+
logger.debug("Socket connection closed for \(accountId).", [.account: accountId])
367+
368+
if let reason {
369+
logger.debug("Reason: \(String(data: reason, encoding: .utf8) ?? "")")
370+
}
371+
372+
logger.debug("Retrying websocket connection for \(accountId).", [.account: accountId])
373+
reconnectWebSocket()
374+
}
375+
378376
public func urlSessionDidFinishEvents(forBackgroundURLSession _: URLSession) {}
377+
}
378+
379+
// MARK: - NextcloudKitDelegate methods
380+
381+
extension RemoteChangeObserver: NextcloudKitDelegate {
382+
public func networkReachabilityObserver(_ typeReachability: NKTypeReachability) {
383+
networkReachability = typeReachability
384+
}
379385

380386
public func downloadProgress(
381387
_: Float,

Sources/NextcloudFileProviderKit/Extensions/RandomAccessCollection+Extensions.swift

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,13 @@ extension RandomAccessCollection {
3131
}
3232

3333
/// Performs an asynchronous `forEach` operation on the collection in concurrent chunks.
34-
func concurrentChunkedForEach(
35-
into size: Int = defaultChunkSize, operation: @escaping (Element) async -> Void
36-
) async {
34+
func concurrentChunkedForEach(into size: Int = defaultChunkSize, operation: @escaping (Element) async -> Void) async {
3735
await withTaskGroup(of: Void.self) { group in
3836
for chunk in chunked(into: size) {
37+
let chunkArray = Array(chunk) // Convert to Array to ensure Sendable
38+
3939
group.addTask {
40-
for element in chunk {
40+
for element in chunkArray {
4141
await operation(element)
4242
}
4343
}
@@ -46,17 +46,19 @@ extension RandomAccessCollection {
4646
}
4747

4848
/// Performs an asynchronous `compactMap` operation on the collection in concurrent chunks.
49-
func concurrentChunkedCompactMap<T>(
50-
into size: Int = defaultChunkSize, transform: @escaping (Element) throws -> T?
51-
) async throws -> [T] {
49+
func concurrentChunkedCompactMap<T>(into size: Int = defaultChunkSize, transform: @escaping (Element) throws -> T?) async throws -> [T] where T: Sendable {
5250
try await withThrowingTaskGroup(of: [T].self) { group in
5351
var results = [T]()
5452
// Reserving capacity is still a good optimization, though we can't know the exact final count.
5553
results.reserveCapacity(Int(self.count))
5654

5755
for chunk in chunked(into: size) {
56+
let chunkArray = Array(chunk) // Convert to Array to ensure Sendable
57+
5858
group.addTask {
59-
try chunk.compactMap { try transform($0) }
59+
try chunkArray.compactMap {
60+
try transform($0)
61+
}
6062
}
6163
}
6264

Sources/NextcloudFileProviderKit/Interface/ChangeNotificationInterface.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@
33

44
import Foundation
55

6-
public protocol ChangeNotificationInterface {
6+
public protocol ChangeNotificationInterface: Sendable {
77
func notifyChange()
88
}

Sources/NextcloudFileProviderKit/Interface/RemoteInterface.swift

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,13 @@ import Foundation
77
import NextcloudCapabilitiesKit
88
import NextcloudKit
99

10-
public enum EnumerateDepth: String {
11-
case target = "0"
12-
case targetAndDirectChildren = "1"
13-
case targetAndAllChildren = "infinity"
14-
}
15-
16-
public enum AuthenticationAttemptResultState: Int {
17-
case authenticationError, connectionError, success
18-
}
19-
2010
///
2111
/// Abstraction of the Nextcloud server APIs to call from the file provider extension.
2212
///
2313
/// Usually, the shared `NextcloudKit` instance is conforming to this and provided as an argument.
2414
/// NextcloudKit is not mockable as of writing, hence this protocol was defined to enable testing.
2515
///
26-
public protocol RemoteInterface {
16+
public protocol RemoteInterface: Sendable {
2717
func setDelegate(_ delegate: NextcloudKitDelegate)
2818

2919
func createFolder(

0 commit comments

Comments
 (0)