|
| 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