Skip to content

Commit 7fa7e17

Browse files
committed
Merge branch 'main' of https://github.com/nextcloud/NextcloudKit into assistant-improvements
2 parents 7f82439 + 37493a9 commit 7fa7e17

2 files changed

Lines changed: 200 additions & 180 deletions

File tree

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// SPDX-FileCopyrightText: Nextcloud GmbH
2+
// SPDX-FileCopyrightText: 2026 Marino Faggiana
3+
// SPDX-License-Identifier: GPL-3.0-or-later
4+
5+
import Foundation
6+
import Alamofire
7+
8+
/// An operation handle that exposes the underlying Alamofire DataRequest and URLSessionTask
9+
/// as soon as they are created, allowing clients to cancel an in-flight operation, observe
10+
/// its lifecycle, and react to state changes via an async events stream.
11+
///
12+
/// Concurrency & thread-safety:
13+
/// NKOperationHandle is an actor, so interactions are serialized and safe across concurrency domains.
14+
///
15+
/// Features:
16+
/// - Store and expose the underlying `DataRequest` and `URLSessionTask`.
17+
/// - Cancel the operation at any time using `cancel()` (prefers `DataRequest.cancel()`; falls back to `URLSessionTask.cancel()`).
18+
/// - Observe lifecycle events with `events()` returning `AsyncStream<NKOperationEvent>`.
19+
/// Emitted events include:
20+
/// - `.didSetRequest(DataRequest)` when the request is created and stored
21+
/// - `.didSetTask(URLSessionTask)` when the task is created and stored
22+
/// - `.didCancel` after `cancel()` is invoked
23+
/// - `.didClear` when references are cleared via `clear()`
24+
/// - Check whether an operation is currently active using `isActive()`.
25+
/// - Explicitly release stored references using `clear()`.
26+
///
27+
/// Typical usage:
28+
/// ```swift
29+
/// let handle = NKOperationHandle()
30+
/// Task {
31+
/// for await event in await handle.events() {
32+
/// switch event {
33+
/// case .didSetTask(let task):
34+
/// print("Task available:", task)
35+
/// case .didSetRequest(let request):
36+
/// print("Request available:", request)
37+
/// case .didCancel:
38+
/// print("Operation cancelled")
39+
/// case .didClear:
40+
/// print("Handle cleared")
41+
/// }
42+
/// }
43+
/// }
44+
/// // Pass `handle` to an API that creates a network request.
45+
/// // The API will call `set(request:)` and/or `set(task:)` when available.
46+
/// // You can cancel at any time:
47+
/// await handle.cancel()
48+
/// ```
49+
///
50+
/// Notes:
51+
/// - The events stream is created lazily the first time `events()` is called and is finished in `clear()`.
52+
/// - If you don't need event observation, you can ignore `events()` and use only `cancel()`/`isActive()`.
53+
public actor NKOperationHandle {
54+
private(set) var request: DataRequest?
55+
private(set) var task: URLSessionTask?
56+
57+
public enum NKOperationEvent {
58+
case didSetRequest(DataRequest)
59+
case didSetTask(URLSessionTask)
60+
case didCancel
61+
case didClear
62+
}
63+
64+
private var eventsStream: AsyncStream<NKOperationEvent>?
65+
private var eventsContinuation: AsyncStream<NKOperationEvent>.Continuation?
66+
67+
public init() {}
68+
69+
public func events() -> AsyncStream<NKOperationEvent> {
70+
if let eventsStream { return eventsStream }
71+
let (stream, continuation) = AsyncStream<NKOperationEvent>.makeStream()
72+
self.eventsStream = stream
73+
self.eventsContinuation = continuation
74+
return stream
75+
}
76+
77+
public func set(request: DataRequest) {
78+
self.request = request
79+
eventsContinuation?.yield(.didSetRequest(request))
80+
}
81+
public func set(task: URLSessionTask) {
82+
self.task = task
83+
eventsContinuation?.yield(.didSetTask(task))
84+
}
85+
public func currentRequest() -> DataRequest? {
86+
request
87+
}
88+
public func currentTask() -> URLSessionTask? {
89+
task
90+
}
91+
92+
public func cancel() {
93+
if let request = request {
94+
request.cancel()
95+
} else {
96+
task?.cancel()
97+
}
98+
eventsContinuation?.yield(.didCancel)
99+
}
100+
101+
public func clear() {
102+
eventsContinuation?.yield(.didClear)
103+
request = nil
104+
task = nil
105+
eventsContinuation?.finish()
106+
eventsContinuation = nil
107+
eventsStream = nil
108+
}
109+
110+
public func isActive() -> Bool {
111+
return request != nil || task != nil
112+
}
113+
}
114+

0 commit comments

Comments
 (0)