Skip to content

Commit 37493a9

Browse files
Merge pull request #211 from nextcloud/datarequest
search-improvements-api
2 parents 71f75a0 + 397e6ef commit 37493a9

File tree

2 files changed

+141
-8
lines changed

2 files changed

+141
-8
lines changed
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+

Sources/NextcloudKit/NextcloudKit+Search.swift

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,25 @@ import Alamofire
77
import SwiftyJSON
88

99
public extension NextcloudKit {
10+
11+
1012
/// Performs a unified search using multiple providers and returns results asynchronously.
1113
///
1214
/// - Parameters:
1315
/// - timeout: The individual request timeout per provider.
1416
/// - account: The Nextcloud account performing the search.
1517
/// - options: Optional configuration for the request (headers, queue, etc.).
18+
/// - handle: Optional operation handle that receives the underlying DataRequest and URLSessionTask
19+
/// as soon as they are created. Use it to cancel the request while it’s in flight
20+
/// (via handle.cancel()) or to observe the task/request lifecycle.
1621
/// - filter: A closure to filter which `NKSearchProvider` are enabled.
17-
/// - taskHandler: Callback triggered when a `URLSessionTask` is created.
1822
///
1923
/// - Returns: NKSearchProvider, NKError
2024
func unifiedSearchProviders(timeout: TimeInterval = 30,
2125
account: String,
2226
options: NKRequestOptions = NKRequestOptions(),
23-
filter: @escaping (NKSearchProvider) -> Bool = { _ in true },
24-
taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }
27+
handle: NKOperationHandle? = nil,
28+
filter: @escaping (NKSearchProvider) -> Bool = { _ in true }
2529
) async -> (providers: [NKSearchProvider]?, error: NKError) {
2630
let endpoint = "ocs/v2.php/search/providers"
2731
guard let nkSession = nkCommonInstance.nksessions.session(forAccount: account),
@@ -32,11 +36,17 @@ public extension NextcloudKit {
3236

3337
let request = nkSession.sessionData
3438
.request(url, headers: headers, interceptor: NKInterceptor(nkCommonInstance: nkCommonInstance))
35-
.validate(statusCode: 200..<300)
3639
.onURLSessionTaskCreation { task in
3740
task.taskDescription = options.taskDescription
38-
taskHandler(task)
41+
Task {
42+
if let handle {
43+
await handle.set(task: task)
44+
}
45+
}
3946
}
47+
.validate(statusCode: 200..<300)
48+
49+
await handle?.set(request: request)
4050
let response = await request.serializingData().response
4151

4252
switch response.result {
@@ -63,7 +73,9 @@ public extension NextcloudKit {
6373
/// - timeout: The timeout interval for the search request.
6474
/// - account: The Nextcloud account performing the search.
6575
/// - options: Optional request configuration such as headers and queue.
66-
/// - taskHandler: Callback to observe the underlying URLSessionTask.
76+
/// - handle: Optional operation handle that receives the underlying DataRequest and URLSessionTask
77+
/// as soon as they are created. Use it to cancel the request while it’s in flight
78+
/// (via handle.cancel()) or to observe the task/request lifecycle.
6779
///
6880
/// - Returns: NKSearchResult, NKError
6981
func unifiedSearch(providerId: String,
@@ -73,7 +85,7 @@ public extension NextcloudKit {
7385
timeout: TimeInterval = 60,
7486
account: String,
7587
options: NKRequestOptions = NKRequestOptions(),
76-
taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in })
88+
handle: NKOperationHandle? = nil)
7789
async -> (searchResult: NKSearchResult?, error: NKError) {
7890
guard let term = term.urlEncoded,
7991
let nkSession = nkCommonInstance.nksessions.session(forAccount: account),
@@ -105,8 +117,14 @@ public extension NextcloudKit {
105117
.validate(statusCode: 200..<300)
106118
.onURLSessionTaskCreation { task in
107119
task.taskDescription = options.taskDescription
108-
taskHandler(task)
120+
Task {
121+
if let handle {
122+
await handle.set(task: task)
123+
}
124+
}
109125
}
126+
127+
await handle?.set(request: request)
110128
let response = await request.serializingData().response
111129

112130
switch response.result {
@@ -211,3 +229,4 @@ public class NKSearchProvider: NSObject {
211229
return allProvider.compactMap(NKSearchProvider.init)
212230
}
213231
}
232+

0 commit comments

Comments
 (0)