1
1
import _Helpers
2
- import Foundation
2
+ @ preconcurrency import Foundation
3
3
4
4
#if canImport(FoundationNetworking)
5
5
import FoundationNetworking
@@ -20,21 +20,23 @@ public actor FunctionsClient {
20
20
var headers : [ String : String ]
21
21
/// The Region to invoke the functions in.
22
22
let region : String ?
23
- /// The fetch handler used to make requests.
24
- let fetch : FetchHandler
23
+
24
+ private let http : HTTPClient
25
25
26
26
/// Initializes a new instance of `FunctionsClient`.
27
27
///
28
28
/// - Parameters:
29
29
/// - url: The base URL for the functions.
30
30
/// - headers: Headers to be included in the requests. (Default: empty dictionary)
31
31
/// - region: The Region to invoke the functions in.
32
+ /// - logger: SupabaseLogger instance to use.
32
33
/// - fetch: The fetch handler used to make requests. (Default: URLSession.shared.data(for:))
33
34
@_disfavoredOverload
34
35
public init (
35
36
url: URL ,
36
37
headers: [ String : String ] = [ : ] ,
37
38
region: String ? = nil ,
39
+ logger: ( any SupabaseLogger ) ? = nil ,
38
40
fetch: @escaping FetchHandler = { try await URLSession . shared. data ( for: $0) }
39
41
) {
40
42
self . url = url
@@ -43,7 +45,7 @@ public actor FunctionsClient {
43
45
self . headers [ " X-Client-Info " ] = " functions-swift/ \( version) "
44
46
}
45
47
self . region = region
46
- self . fetch = fetch
48
+ http = HTTPClient ( logger : logger , fetchHandler : fetch)
47
49
}
48
50
49
51
/// Initializes a new instance of `FunctionsClient`.
@@ -52,20 +54,16 @@ public actor FunctionsClient {
52
54
/// - url: The base URL for the functions.
53
55
/// - headers: Headers to be included in the requests. (Default: empty dictionary)
54
56
/// - region: The Region to invoke the functions in.
57
+ /// - logger: SupabaseLogger instance to use.
55
58
/// - fetch: The fetch handler used to make requests. (Default: URLSession.shared.data(for:))
56
59
public init (
57
60
url: URL ,
58
61
headers: [ String : String ] = [ : ] ,
59
62
region: FunctionRegion ? = nil ,
63
+ logger: ( any SupabaseLogger ) ? = nil ,
60
64
fetch: @escaping FetchHandler = { try await URLSession . shared. data ( for: $0) }
61
65
) {
62
- self . url = url
63
- self . headers = headers
64
- if headers [ " X-Client-Info " ] == nil {
65
- self . headers [ " X-Client-Info " ] = " functions-swift/ \( version) "
66
- }
67
- self . region = region? . rawValue
68
- self . fetch = fetch
66
+ self . init ( url: url, headers: headers, region: region? . rawValue, logger: logger, fetch: fetch)
69
67
}
70
68
71
69
/// Updates the authorization header.
@@ -92,10 +90,10 @@ public actor FunctionsClient {
92
90
options: FunctionInvokeOptions = . init( ) ,
93
91
decode: ( Data , HTTPURLResponse ) throws -> Response
94
92
) async throws -> Response {
95
- let ( data , response) = try await rawInvoke (
93
+ let response = try await rawInvoke (
96
94
functionName: functionName, invokeOptions: options
97
95
)
98
- return try decode ( data, response)
96
+ return try decode ( response . data, response . response)
99
97
}
100
98
101
99
/// Invokes a function and decodes the response as a specific type.
@@ -130,33 +128,101 @@ public actor FunctionsClient {
130
128
private func rawInvoke(
131
129
functionName: String ,
132
130
invokeOptions: FunctionInvokeOptions
133
- ) async throws -> ( Data , HTTPURLResponse ) {
131
+ ) async throws -> Response {
132
+ var request = Request (
133
+ path: functionName,
134
+ method: . post,
135
+ headers: invokeOptions. headers. merging ( headers) { invoke, _ in invoke } ,
136
+ body: invokeOptions. body
137
+ )
138
+
139
+ if let region = invokeOptions. region ?? region {
140
+ request. headers [ " x-region " ] = region
141
+ }
142
+
143
+ let response = try await http. fetch ( request, baseURL: url)
144
+
145
+ guard 200 ..< 300 ~= response. statusCode else {
146
+ throw FunctionsError . httpError ( code: response. statusCode, data: response. data)
147
+ }
148
+
149
+ let isRelayError = response. response. value ( forHTTPHeaderField: " x-relay-error " ) == " true "
150
+ if isRelayError {
151
+ throw FunctionsError . relayError
152
+ }
153
+
154
+ return response
155
+ }
156
+
157
+ /// Invokes a function with streamed response.
158
+ ///
159
+ /// Function MUST return a `text/event-stream` content type for this method to work.
160
+ ///
161
+ /// - Parameters:
162
+ /// - functionName: The name of the function to invoke.
163
+ /// - invokeOptions: Options for invoking the function.
164
+ /// - Returns: A stream of Data.
165
+ ///
166
+ /// - Warning: Experimental method.
167
+ /// - Note: This method doesn't use the same underlying `URLSession` as the remaining methods in the library.
168
+ public func _invokeWithStreamedResponse(
169
+ _ functionName: String ,
170
+ options invokeOptions: FunctionInvokeOptions = . init( )
171
+ ) -> AsyncThrowingStream < Data , any Error > {
172
+ let ( stream, continuation) = AsyncThrowingStream < Data , any Error > . makeStream ( )
173
+ let delegate = StreamResponseDelegate ( continuation: continuation)
174
+
175
+ let session = URLSession ( configuration: . default, delegate: delegate, delegateQueue: nil )
176
+
134
177
let url = url. appendingPathComponent ( functionName)
135
178
var urlRequest = URLRequest ( url: url)
136
179
urlRequest. allHTTPHeaderFields = invokeOptions. headers. merging ( headers) { invoke, _ in invoke }
137
180
urlRequest. httpMethod = ( invokeOptions. method ?? . post) . rawValue
138
181
urlRequest. httpBody = invokeOptions. body
139
182
140
- let region = invokeOptions. region ?? region
141
- if let region {
142
- urlRequest. setValue ( region, forHTTPHeaderField: " x-region " )
183
+ let task = session. dataTask ( with: urlRequest) { data, response, _ in
184
+ guard let httpResponse = response as? HTTPURLResponse else {
185
+ continuation. finish ( throwing: URLError ( . badServerResponse) )
186
+ return
187
+ }
188
+
189
+ guard 200 ..< 300 ~= httpResponse. statusCode else {
190
+ let error = FunctionsError . httpError ( code: httpResponse. statusCode, data: data ?? Data ( ) )
191
+ continuation. finish ( throwing: error)
192
+ return
193
+ }
194
+
195
+ let isRelayError = httpResponse. value ( forHTTPHeaderField: " x-relay-error " ) == " true "
196
+ if isRelayError {
197
+ continuation. finish ( throwing: FunctionsError . relayError)
198
+ }
143
199
}
144
200
145
- let ( data , response ) = try await fetch ( urlRequest )
201
+ task . resume ( )
146
202
147
- guard let httpResponse = response as? HTTPURLResponse else {
148
- throw URLError ( . badServerResponse)
149
- }
203
+ continuation. onTermination = { _ in
204
+ task. cancel ( )
150
205
151
- guard 200 ..< 300 ~= httpResponse . statusCode else {
152
- throw FunctionsError . httpError ( code : httpResponse . statusCode , data : data )
206
+ // Hold a strong reference to delegate until continuation terminates.
207
+ _ = delegate
153
208
}
154
209
155
- let isRelayError = httpResponse. value ( forHTTPHeaderField: " x-relay-error " ) == " true "
156
- if isRelayError {
157
- throw FunctionsError . relayError
158
- }
210
+ return stream
211
+ }
212
+ }
213
+
214
+ final class StreamResponseDelegate : NSObject , URLSessionDataDelegate , Sendable {
215
+ let continuation : AsyncThrowingStream < Data , any Error > . Continuation
216
+
217
+ init ( continuation: AsyncThrowingStream < Data , any Error > . Continuation ) {
218
+ self . continuation = continuation
219
+ }
220
+
221
+ func urlSession( _: URLSession , dataTask _: URLSessionDataTask , didReceive data: Data ) {
222
+ continuation. yield ( data)
223
+ }
159
224
160
- return ( data, httpResponse)
225
+ func urlSession( _: URLSession , task _: URLSessionTask , didCompleteWithError error: ( any Error ) ? ) {
226
+ continuation. finish ( throwing: error)
161
227
}
162
228
}
0 commit comments