stream-chat-react-native 9.0.2-beta.1 → 9.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/android/build.gradle +5 -4
  2. package/android/src/main/java/com/streamchatreactnative/StreamChatReactNativePackage.java +20 -4
  3. package/android/src/main/java/com/streamchatreactnative/shared/upload/StreamMultipartUploadFileRequestBody.kt +25 -0
  4. package/android/src/main/java/com/streamchatreactnative/shared/upload/StreamMultipartUploadModels.kt +39 -0
  5. package/android/src/main/java/com/streamchatreactnative/shared/upload/StreamMultipartUploadProgress.kt +80 -0
  6. package/android/src/main/java/com/streamchatreactnative/shared/upload/StreamMultipartUploadRequestParser.kt +110 -0
  7. package/android/src/main/java/com/streamchatreactnative/shared/upload/StreamMultipartUploadSourceResolver.kt +99 -0
  8. package/android/src/main/java/com/streamchatreactnative/shared/upload/StreamMultipartUploader.kt +138 -0
  9. package/android/src/newarch/com/streamchatreactnative/StreamMultipartUploaderModule.kt +122 -0
  10. package/ios/shared/StreamMultipartUploadBodyStream.swift +254 -0
  11. package/ios/shared/StreamMultipartUploadManager.swift +462 -0
  12. package/ios/shared/StreamMultipartUploadModels.swift +69 -0
  13. package/ios/shared/StreamMultipartUploadProgress.swift +48 -0
  14. package/ios/shared/StreamMultipartUploadSourceResolver.swift +391 -0
  15. package/ios/shared/StreamMultipartUploader.h +16 -0
  16. package/ios/shared/StreamMultipartUploader.mm +109 -0
  17. package/ios/shared/StreamMultipartUploaderBridge.swift +145 -0
  18. package/ios/shared/StreamShimmerView.swift +180 -77
  19. package/ios/shared/StreamVideoThumbnailGenerator.swift +13 -2
  20. package/package.json +3 -2
  21. package/src/handlers/index.ts +1 -0
  22. package/src/handlers/multipartUpload.ts +9 -0
  23. package/src/index.js +2 -1
  24. package/src/native/NativeStreamMultipartUploader.ts +52 -0
  25. package/src/native/multipartUploader.ts +5 -0
  26. package/src/optionalDependencies/__tests__/pickDocument.test.ts +86 -0
  27. package/src/optionalDependencies/pickDocument.ts +24 -6
@@ -0,0 +1,254 @@
1
+ import Foundation
2
+
3
+ private enum StreamMultipartBodyElement {
4
+ case data(Data)
5
+ case file(URL)
6
+ }
7
+
8
+ final class StreamMultipartUploadBodyStreamFactory {
9
+ let boundary: String
10
+ let contentLength: Int64?
11
+
12
+ private let elements: [StreamMultipartBodyElement]
13
+
14
+ private init(
15
+ boundary: String,
16
+ contentLength: Int64?,
17
+ elements: [StreamMultipartBodyElement]
18
+ ) {
19
+ self.boundary = boundary
20
+ self.contentLength = contentLength
21
+ self.elements = elements
22
+ }
23
+
24
+ static func create(parts: [StreamMultipartUploadPart]) async throws -> StreamMultipartUploadBodyStreamFactory {
25
+ let boundary = "stream-upload-\(UUID().uuidString)"
26
+ var elements = [StreamMultipartBodyElement]()
27
+ var totalLength: Int64 = 0
28
+ var canComputeLength = true
29
+
30
+ for part in parts {
31
+ switch part {
32
+ case .text(let textPart):
33
+ let data = multipartTextData(boundary: boundary, part: textPart)
34
+ elements.append(.data(data))
35
+ totalLength += Int64(data.count)
36
+ case .file(let filePart):
37
+ let resolvedPart = try await StreamMultipartUploadSourceResolver.resolve(filePart)
38
+ let headerData = multipartFileHeaderData(boundary: boundary, part: resolvedPart)
39
+ let footerData = "\r\n".data(using: .utf8) ?? Data()
40
+
41
+ elements.append(.data(headerData))
42
+ elements.append(.file(resolvedPart.fileURL))
43
+ elements.append(.data(footerData))
44
+
45
+ totalLength += Int64(headerData.count) + Int64(footerData.count)
46
+ if let size = resolvedPart.size {
47
+ totalLength += size
48
+ } else {
49
+ canComputeLength = false
50
+ }
51
+ }
52
+ }
53
+
54
+ let closingBoundary = "--\(boundary)--\r\n".data(using: .utf8) ?? Data()
55
+ elements.append(.data(closingBoundary))
56
+ totalLength += Int64(closingBoundary.count)
57
+
58
+ return StreamMultipartUploadBodyStreamFactory(
59
+ boundary: boundary,
60
+ contentLength: canComputeLength ? totalLength : nil,
61
+ elements: elements
62
+ )
63
+ }
64
+
65
+ func makeStream() -> InputStream {
66
+ StreamMultipartSequentialInputStream(elements: elements)
67
+ }
68
+
69
+ private static func multipartTextData(boundary: String, part: StreamMultipartTextPart) -> Data {
70
+ let payload = [
71
+ "--\(boundary)",
72
+ "Content-Disposition: form-data; name=\(multipartQuotedParameter(part.fieldName))",
73
+ "",
74
+ part.value,
75
+ "",
76
+ ].joined(separator: "\r\n")
77
+
78
+ return payload.data(using: .utf8) ?? Data()
79
+ }
80
+
81
+ private static func multipartFileHeaderData(
82
+ boundary: String,
83
+ part: StreamMultipartResolvedFilePart
84
+ ) -> Data {
85
+ let payload = [
86
+ "--\(boundary)",
87
+ "Content-Disposition: form-data; name=\(multipartQuotedParameter(part.fieldName)); filename=\(multipartQuotedParameter(part.fileName))",
88
+ "Content-Type: \(part.mimeType)",
89
+ "",
90
+ ].joined(separator: "\r\n") + "\r\n"
91
+
92
+ return payload.data(using: .utf8) ?? Data()
93
+ }
94
+
95
+ private static func multipartQuotedParameter(_ value: String) -> String {
96
+ let escaped = value
97
+ .replacingOccurrences(of: "\r", with: "%0D")
98
+ .replacingOccurrences(of: "\n", with: "%0A")
99
+ .replacingOccurrences(of: "\"", with: "%22")
100
+
101
+ return "\"\(escaped)\""
102
+ }
103
+ }
104
+
105
+ private final class StreamMultipartSequentialInputStream: InputStream {
106
+ private let elements: [StreamMultipartBodyElement]
107
+ private var currentIndex = 0
108
+ private var currentStream: InputStream?
109
+ private weak var internalDelegate: StreamDelegate?
110
+ private var internalStatus: Stream.Status = .notOpen
111
+ private var internalError: Error?
112
+ private var scheduledRunLoops: [(runLoop: RunLoop, mode: RunLoop.Mode)] = []
113
+
114
+ init(elements: [StreamMultipartBodyElement]) {
115
+ self.elements = elements
116
+ super.init(data: Data())
117
+ }
118
+
119
+ override var delegate: StreamDelegate? {
120
+ get {
121
+ internalDelegate
122
+ }
123
+ set {
124
+ internalDelegate = newValue
125
+ currentStream?.delegate = newValue
126
+ }
127
+ }
128
+
129
+ override var hasBytesAvailable: Bool {
130
+ guard internalStatus != .closed, internalStatus != .error else {
131
+ return false
132
+ }
133
+
134
+ if let currentStream, currentStream.hasBytesAvailable {
135
+ return true
136
+ }
137
+
138
+ return currentIndex < elements.count
139
+ }
140
+
141
+ override var streamError: Error? {
142
+ internalError
143
+ }
144
+
145
+ override var streamStatus: Stream.Status {
146
+ internalStatus
147
+ }
148
+
149
+ override func open() {
150
+ guard internalStatus == .notOpen else {
151
+ return
152
+ }
153
+
154
+ internalStatus = .opening
155
+ advanceStreamIfNeeded()
156
+ if internalStatus == .error {
157
+ return
158
+ }
159
+ internalStatus = currentStream == nil ? .atEnd : .open
160
+ }
161
+
162
+ override func close() {
163
+ currentStream?.close()
164
+ currentStream = nil
165
+ internalStatus = .closed
166
+ }
167
+
168
+ override func schedule(in aRunLoop: RunLoop, forMode mode: RunLoop.Mode) {
169
+ scheduledRunLoops.append((runLoop: aRunLoop, mode: mode))
170
+ currentStream?.schedule(in: aRunLoop, forMode: mode)
171
+ }
172
+
173
+ override func remove(from aRunLoop: RunLoop, forMode mode: RunLoop.Mode) {
174
+ scheduledRunLoops.removeAll { $0.runLoop == aRunLoop && $0.mode == mode }
175
+ currentStream?.remove(from: aRunLoop, forMode: mode)
176
+ }
177
+
178
+ override func read(_ buffer: UnsafeMutablePointer<UInt8>, maxLength len: Int) -> Int {
179
+ guard internalStatus != .closed else {
180
+ return 0
181
+ }
182
+
183
+ if internalStatus == .notOpen {
184
+ open()
185
+ }
186
+
187
+ while true {
188
+ guard let currentStream else {
189
+ if internalStatus == .error {
190
+ return -1
191
+ }
192
+
193
+ internalStatus = .atEnd
194
+ return 0
195
+ }
196
+
197
+ let bytesRead = currentStream.read(buffer, maxLength: len)
198
+
199
+ if bytesRead > 0 {
200
+ internalStatus = .open
201
+ return bytesRead
202
+ }
203
+
204
+ if bytesRead < 0 {
205
+ internalError = currentStream.streamError
206
+ internalStatus = .error
207
+ return -1
208
+ }
209
+
210
+ currentStream.close()
211
+ self.currentStream = nil
212
+ advanceStreamIfNeeded()
213
+
214
+ if self.currentStream == nil {
215
+ internalStatus = .atEnd
216
+ return 0
217
+ }
218
+ }
219
+ }
220
+
221
+ private func advanceStreamIfNeeded() {
222
+ guard currentStream == nil else {
223
+ return
224
+ }
225
+
226
+ while currentIndex < elements.count {
227
+ let nextElement = elements[currentIndex]
228
+ currentIndex += 1
229
+
230
+ let nextStream: InputStream?
231
+ switch nextElement {
232
+ case .data(let data):
233
+ nextStream = InputStream(data: data)
234
+ case .file(let url):
235
+ nextStream = InputStream(url: url)
236
+ if nextStream == nil {
237
+ internalError = StreamMultipartUploadError.unreadableFile(url.path)
238
+ internalStatus = .error
239
+ return
240
+ }
241
+ }
242
+
243
+ if let nextStream {
244
+ nextStream.delegate = internalDelegate
245
+ for scheduled in scheduledRunLoops {
246
+ nextStream.schedule(in: scheduled.runLoop, forMode: scheduled.mode)
247
+ }
248
+ nextStream.open()
249
+ currentStream = nextStream
250
+ return
251
+ }
252
+ }
253
+ }
254
+ }
@@ -0,0 +1,462 @@
1
+ import Foundation
2
+
3
+ private actor StreamMultipartUploadConcurrencyLimiter {
4
+ private var activeUploads = 0
5
+ private let maxConcurrentUploads: Int
6
+ private var waiterOrder = [UUID]()
7
+ private var waiters = [UUID: CheckedContinuation<Void, Error>]()
8
+
9
+ init(maxConcurrentUploads: Int) {
10
+ self.maxConcurrentUploads = max(1, maxConcurrentUploads)
11
+ }
12
+
13
+ func acquire() async throws {
14
+ if activeUploads < maxConcurrentUploads {
15
+ activeUploads += 1
16
+ return
17
+ }
18
+
19
+ let waiterId = UUID()
20
+
21
+ try await withTaskCancellationHandler {
22
+ try await withCheckedThrowingContinuation { continuation in
23
+ if activeUploads < maxConcurrentUploads {
24
+ activeUploads += 1
25
+ continuation.resume()
26
+ return
27
+ }
28
+
29
+ waiterOrder.append(waiterId)
30
+ waiters[waiterId] = continuation
31
+ }
32
+ } onCancel: {
33
+ Task {
34
+ await self.cancelWaiter(id: waiterId)
35
+ }
36
+ }
37
+ }
38
+
39
+ func release() {
40
+ while !waiterOrder.isEmpty {
41
+ let waiterId = waiterOrder.removeFirst()
42
+
43
+ guard let continuation = waiters.removeValue(forKey: waiterId) else {
44
+ continue
45
+ }
46
+
47
+ continuation.resume()
48
+ return
49
+ }
50
+
51
+ activeUploads = max(0, activeUploads - 1)
52
+ }
53
+
54
+ private func cancelWaiter(id: UUID) {
55
+ waiterOrder.removeAll { $0 == id }
56
+ waiters.removeValue(forKey: id)?.resume(throwing: StreamMultipartUploadError.cancelled)
57
+ }
58
+ }
59
+
60
+ private final class StreamMultipartUploadTaskState {
61
+ let bodyFactory: StreamMultipartUploadBodyStreamFactory
62
+ let progressThrottler: StreamMultipartUploadProgressThrottler
63
+ let task: URLSessionUploadTask
64
+ let uploadId: String
65
+ var completion:
66
+ ((Result<StreamMultipartUploadResponse, Error>) -> Void)?
67
+ var response: HTTPURLResponse?
68
+ var responseData = Data()
69
+ var responseDataError: Error?
70
+
71
+ init(
72
+ bodyFactory: StreamMultipartUploadBodyStreamFactory,
73
+ progressThrottler: StreamMultipartUploadProgressThrottler,
74
+ task: URLSessionUploadTask,
75
+ uploadId: String,
76
+ completion: @escaping (Result<StreamMultipartUploadResponse, Error>) -> Void
77
+ ) {
78
+ self.bodyFactory = bodyFactory
79
+ self.progressThrottler = progressThrottler
80
+ self.task = task
81
+ self.uploadId = uploadId
82
+ self.completion = completion
83
+ }
84
+ }
85
+
86
+ final class StreamMultipartUploadManager: NSObject {
87
+ static let shared = StreamMultipartUploadManager()
88
+ private let maxResponseBodyBytes = 1_048_576
89
+ private let maxConcurrentUploads = min(max(ProcessInfo.processInfo.activeProcessorCount, 2), 4)
90
+
91
+ private lazy var session: URLSession = {
92
+ let delegateQueue = OperationQueue()
93
+ delegateQueue.maxConcurrentOperationCount = 1
94
+ delegateQueue.qualityOfService = .userInitiated
95
+ let configuration = URLSessionConfiguration.ephemeral
96
+ configuration.httpMaximumConnectionsPerHost = maxConcurrentUploads
97
+ configuration.waitsForConnectivity = false
98
+ return URLSession(configuration: configuration, delegate: self, delegateQueue: delegateQueue)
99
+ }()
100
+ private lazy var uploadLimiter = StreamMultipartUploadConcurrencyLimiter(
101
+ maxConcurrentUploads: maxConcurrentUploads
102
+ )
103
+
104
+ private let lock = NSLock()
105
+ private var cancelledUploadIds = Set<String>()
106
+ private var statesByTaskIdentifier = [Int: StreamMultipartUploadTaskState]()
107
+ private var taskIdentifiersByUploadId = [String: Int]()
108
+
109
+ func cancel(uploadId: String) {
110
+ cancel(uploadId: uploadId, recordCancellation: true)
111
+ }
112
+
113
+ func cancelInFlight(uploadId: String) {
114
+ cancel(uploadId: uploadId, recordCancellation: false)
115
+ }
116
+
117
+ private func cancel(uploadId: String, recordCancellation: Bool) {
118
+ lock.lock()
119
+ if recordCancellation {
120
+ cancelledUploadIds.insert(uploadId)
121
+ }
122
+ let taskIdentifier = taskIdentifiersByUploadId[uploadId]
123
+ let task: URLSessionUploadTask?
124
+ if let taskIdentifier {
125
+ task = statesByTaskIdentifier[taskIdentifier]?.task
126
+ } else {
127
+ task = nil
128
+ }
129
+ lock.unlock()
130
+
131
+ task?.cancel()
132
+ }
133
+
134
+ func uploadMultipart(
135
+ uploadId: String,
136
+ url: String,
137
+ method: String,
138
+ headers: [String: String],
139
+ parts: [[String: Any]],
140
+ progress: [String: Any]?,
141
+ timeoutMs: TimeInterval?,
142
+ onProgress: @escaping (Int64, Int64?) -> Void
143
+ ) async throws -> StreamMultipartUploadResponse {
144
+ let request = try parseRequest(
145
+ uploadId: uploadId,
146
+ url: url,
147
+ method: method,
148
+ headers: headers,
149
+ parts: parts,
150
+ progress: progress,
151
+ timeoutMs: timeoutMs
152
+ )
153
+
154
+ try throwIfCancelled(uploadId: uploadId)
155
+ let bodyFactory = try await StreamMultipartUploadBodyStreamFactory.create(parts: request.parts)
156
+ try throwIfCancelled(uploadId: uploadId)
157
+ var urlRequest = URLRequest(url: request.url)
158
+ urlRequest.httpMethod = request.method
159
+ if let timeoutMs = request.timeoutMs, timeoutMs > 0 {
160
+ urlRequest.timeoutInterval = timeoutMs / 1_000
161
+ }
162
+
163
+ request.headers.forEach { key, value in
164
+ if
165
+ key.caseInsensitiveCompare("Content-Type") == .orderedSame ||
166
+ key.caseInsensitiveCompare("Content-Length") == .orderedSame
167
+ {
168
+ return
169
+ }
170
+ urlRequest.setValue(value, forHTTPHeaderField: key)
171
+ }
172
+
173
+ urlRequest.setValue(
174
+ "multipart/form-data; boundary=\(bodyFactory.boundary)",
175
+ forHTTPHeaderField: "Content-Type"
176
+ )
177
+
178
+ if let contentLength = bodyFactory.contentLength {
179
+ urlRequest.setValue(String(contentLength), forHTTPHeaderField: "Content-Length")
180
+ }
181
+
182
+ let progressThrottler =
183
+ StreamMultipartUploadProgressThrottler(options: request.progress, onProgress: onProgress)
184
+ try await uploadLimiter.acquire()
185
+
186
+ return try await withCheckedThrowingContinuation { continuation in
187
+ let task = session.uploadTask(withStreamedRequest: urlRequest)
188
+ let state = StreamMultipartUploadTaskState(
189
+ bodyFactory: bodyFactory,
190
+ progressThrottler: progressThrottler,
191
+ task: task,
192
+ uploadId: uploadId
193
+ ) { result in
194
+ Task {
195
+ await self.uploadLimiter.release()
196
+ }
197
+ continuation.resume(with: result)
198
+ }
199
+
200
+ guard register(state) else {
201
+ task.cancel()
202
+ Task {
203
+ await self.uploadLimiter.release()
204
+ }
205
+ continuation.resume(throwing: StreamMultipartUploadError.cancelled)
206
+ return
207
+ }
208
+
209
+ task.resume()
210
+ }
211
+ }
212
+
213
+ private func parseRequest(
214
+ uploadId: String,
215
+ url: String,
216
+ method: String,
217
+ headers: [String: String],
218
+ parts: [[String: Any]],
219
+ progress: [String: Any]?,
220
+ timeoutMs: TimeInterval?
221
+ ) throws -> StreamMultipartUploadRequest {
222
+ guard let parsedURL = URL(string: url) else {
223
+ throw StreamMultipartUploadError.invalidURL(url)
224
+ }
225
+
226
+ let uploadParts = try parts.enumerated().map { index, rawPart -> StreamMultipartUploadPart in
227
+ guard let fieldName = rawPart["fieldName"] as? String else {
228
+ throw StreamMultipartUploadError.invalidRequest(
229
+ "Multipart part \(index) is missing fieldName"
230
+ )
231
+ }
232
+
233
+ guard let kind = rawPart["kind"] as? String else {
234
+ throw StreamMultipartUploadError.invalidRequest("Multipart part \(index) is missing kind")
235
+ }
236
+
237
+ switch kind {
238
+ case "text":
239
+ guard let value = rawPart["value"] as? String else {
240
+ throw StreamMultipartUploadError.invalidRequest(
241
+ "Multipart text part \(index) is missing value"
242
+ )
243
+ }
244
+ return .text(
245
+ StreamMultipartTextPart(fieldName: fieldName, value: value)
246
+ )
247
+ case "file":
248
+ guard let uri = rawPart["uri"] as? String else {
249
+ throw StreamMultipartUploadError.invalidRequest(
250
+ "Multipart file part \(index) is missing uri"
251
+ )
252
+ }
253
+ guard let fileName = rawPart["fileName"] as? String else {
254
+ throw StreamMultipartUploadError.invalidRequest(
255
+ "Multipart file part \(index) is missing fileName"
256
+ )
257
+ }
258
+ return .file(
259
+ StreamMultipartFilePart(
260
+ fieldName: fieldName,
261
+ fileName: fileName,
262
+ mimeType: rawPart["mimeType"] as? String,
263
+ uri: uri
264
+ )
265
+ )
266
+ default:
267
+ throw StreamMultipartUploadError.invalidRequest("Unsupported multipart kind: \(kind)")
268
+ }
269
+ }
270
+
271
+ if !uploadParts.contains(where: {
272
+ if case .file = $0 {
273
+ return true
274
+ }
275
+ return false
276
+ }) {
277
+ throw StreamMultipartUploadError.invalidRequest(
278
+ "Multipart upload must contain at least one file part"
279
+ )
280
+ }
281
+
282
+ let progressOptions = StreamMultipartUploadProgressOptions(
283
+ count: progress?["count"] as? Int ?? (progress?["count"] as? NSNumber)?.intValue,
284
+ intervalMs: progress?["intervalMs"] as? Double ?? (progress?["intervalMs"] as? NSNumber)?.doubleValue
285
+ )
286
+
287
+ let parsedTimeoutMs = timeoutMs.flatMap { $0 > 0 ? $0 : nil }
288
+
289
+ return StreamMultipartUploadRequest(
290
+ headers: headers,
291
+ method: method,
292
+ parts: uploadParts,
293
+ progress: progress == nil ? nil : progressOptions,
294
+ timeoutMs: parsedTimeoutMs,
295
+ uploadId: uploadId,
296
+ url: parsedURL
297
+ )
298
+ }
299
+
300
+ private func throwIfCancelled(uploadId: String) throws {
301
+ lock.lock()
302
+ let wasCancelled = cancelledUploadIds.remove(uploadId) != nil
303
+ lock.unlock()
304
+
305
+ if wasCancelled {
306
+ throw StreamMultipartUploadError.cancelled
307
+ }
308
+ }
309
+
310
+ private func register(_ state: StreamMultipartUploadTaskState) -> Bool {
311
+ lock.lock()
312
+ if cancelledUploadIds.remove(state.uploadId) != nil {
313
+ lock.unlock()
314
+ return false
315
+ }
316
+
317
+ statesByTaskIdentifier[state.task.taskIdentifier] = state
318
+ taskIdentifiersByUploadId[state.uploadId] = state.task.taskIdentifier
319
+ lock.unlock()
320
+ return true
321
+ }
322
+
323
+ private func removeState(taskIdentifier: Int) -> StreamMultipartUploadTaskState? {
324
+ lock.lock()
325
+ let state = statesByTaskIdentifier.removeValue(forKey: taskIdentifier)
326
+ if let uploadId = state?.uploadId {
327
+ if taskIdentifiersByUploadId[uploadId] == taskIdentifier {
328
+ taskIdentifiersByUploadId.removeValue(forKey: uploadId)
329
+ }
330
+ cancelledUploadIds.remove(uploadId)
331
+ }
332
+ lock.unlock()
333
+ return state
334
+ }
335
+
336
+ private func state(taskIdentifier: Int) -> StreamMultipartUploadTaskState? {
337
+ lock.lock()
338
+ let state = statesByTaskIdentifier[taskIdentifier]
339
+ lock.unlock()
340
+ return state
341
+ }
342
+ }
343
+
344
+ extension StreamMultipartUploadManager: URLSessionDataDelegate, URLSessionTaskDelegate {
345
+ func urlSession(
346
+ _ session: URLSession,
347
+ dataTask: URLSessionDataTask,
348
+ didReceive data: Data
349
+ ) {
350
+ guard let state = state(taskIdentifier: dataTask.taskIdentifier) else {
351
+ return
352
+ }
353
+
354
+ if state.responseData.count + data.count > maxResponseBodyBytes {
355
+ state.responseDataError = StreamMultipartUploadError.responseBodyTooLarge(maxResponseBodyBytes)
356
+ dataTask.cancel()
357
+ return
358
+ }
359
+
360
+ state.responseData.append(data)
361
+ }
362
+
363
+ func urlSession(
364
+ _ session: URLSession,
365
+ dataTask: URLSessionDataTask,
366
+ didReceive response: URLResponse,
367
+ completionHandler: @escaping (URLSession.ResponseDisposition) -> Void
368
+ ) {
369
+ state(taskIdentifier: dataTask.taskIdentifier)?.response = response as? HTTPURLResponse
370
+ completionHandler(.allow)
371
+ }
372
+
373
+ func urlSession(
374
+ _ session: URLSession,
375
+ task: URLSessionTask,
376
+ didCompleteWithError error: Error?
377
+ ) {
378
+ guard let state = removeState(taskIdentifier: task.taskIdentifier) else {
379
+ return
380
+ }
381
+
382
+ if let error {
383
+ if let responseDataError = state.responseDataError {
384
+ state.completion?(.failure(responseDataError))
385
+ state.completion = nil
386
+ return
387
+ }
388
+
389
+ let nsError = error as NSError
390
+
391
+ if nsError.domain == NSURLErrorDomain, nsError.code == NSURLErrorCancelled {
392
+ state.completion?(.failure(StreamMultipartUploadError.cancelled))
393
+ } else {
394
+ state.completion?(.failure(nsError))
395
+ }
396
+ state.completion = nil
397
+ return
398
+ }
399
+
400
+ guard let response = state.response else {
401
+ state.completion?(.failure(StreamMultipartUploadError.missingHTTPResponse))
402
+ state.completion = nil
403
+ return
404
+ }
405
+
406
+ let headers =
407
+ response.allHeaderFields.reduce(into: [String: String]()) { partialResult, entry in
408
+ guard let key = entry.key as? String else {
409
+ return
410
+ }
411
+
412
+ let value = String(describing: entry.value)
413
+ if let existingValue = partialResult[key] {
414
+ partialResult[key] = "\(existingValue), \(value)"
415
+ } else {
416
+ partialResult[key] = value
417
+ }
418
+ }
419
+
420
+ let body = String(decoding: state.responseData, as: UTF8.self)
421
+
422
+ state.completion?(
423
+ .success(
424
+ StreamMultipartUploadResponse(
425
+ body: body,
426
+ headers: headers,
427
+ status: response.statusCode,
428
+ statusText: HTTPURLResponse.localizedString(forStatusCode: response.statusCode)
429
+ )
430
+ )
431
+ )
432
+ state.completion = nil
433
+ }
434
+
435
+ func urlSession(
436
+ _ session: URLSession,
437
+ task: URLSessionTask,
438
+ didSendBodyData bytesSent: Int64,
439
+ totalBytesSent: Int64,
440
+ totalBytesExpectedToSend: Int64
441
+ ) {
442
+ let total: Int64?
443
+ if totalBytesExpectedToSend > 0 {
444
+ total = totalBytesExpectedToSend
445
+ } else {
446
+ total = nil
447
+ }
448
+
449
+ state(taskIdentifier: task.taskIdentifier)?.progressThrottler.dispatch(
450
+ loaded: totalBytesSent,
451
+ total: total
452
+ )
453
+ }
454
+
455
+ func urlSession(
456
+ _ session: URLSession,
457
+ task: URLSessionTask,
458
+ needNewBodyStream completionHandler: @escaping (InputStream?) -> Void
459
+ ) {
460
+ completionHandler(state(taskIdentifier: task.taskIdentifier)?.bodyFactory.makeStream())
461
+ }
462
+ }