ssap_app/node_modules/expo/ios/Fetch/NativeResponse.swift

228 lines
6.7 KiB
Swift

// Copyright 2015-present 650 Industries. All rights reserved.
import ExpoModulesCore
/**
A SharedObject for response.
*/
internal final class NativeResponse: SharedObject, ExpoURLSessionTaskDelegate, @unchecked Sendable {
internal let sink: ResponseSink
private let dispatchQueue: DispatchQueue
private(set) var state: ResponseState = .intialized {
didSet {
dispatchQueue.async { [weak self] in
guard let self else {
return
}
self.stateChangeOnceListeners.removeAll { $0(self.state) == true }
}
}
}
private typealias StateChangeListener = (ResponseState) -> Bool
private var stateChangeOnceListeners: [StateChangeListener] = []
private(set) var responseInit: NativeResponseInit?
private(set) var redirected = false
private(set) var error: Error?
var redirectMode: NativeRequestRedirect = .follow
var bodyUsed: Bool {
return self.sink.bodyUsed
}
init(dispatchQueue: DispatchQueue) {
self.sink = ResponseSink()
self.dispatchQueue = dispatchQueue
}
func startStreaming() -> Data? {
if isInvalidState(.responseReceived, .bodyCompleted) {
return nil
}
if state == .responseReceived {
state = .bodyStreamingStarted
let queuedData = self.sink.finalize()
emit(event: "didReceiveResponseData", arguments: queuedData)
} else if state == .bodyCompleted {
let queuedData = self.sink.finalize()
return queuedData
}
return nil
}
func cancelStreaming() {
if isInvalidState(.bodyStreamingStarted) {
return
}
state = .bodyStreamingCanceled
}
func emitRequestCanceled() {
let error = FetchRequestCanceledException()
self.error = error
if state == .bodyStreamingStarted {
emit(event: "didFailWithError", arguments: error.localizedDescription)
}
state = .errorReceived
emit(event: "readyForJSFinalization")
}
/**
Waits for given states and when it meets the requirement, executes the callback.
*/
func waitFor(states: [ResponseState], callback: @escaping @Sendable (ResponseState) -> Void) {
if states.contains(state) {
callback(state)
return
}
dispatchQueue.async { [weak self] () in
guard let self else {
return
}
self.stateChangeOnceListeners.append { newState in
if states.contains(newState) {
callback(newState)
return true
}
return false
}
}
}
/**
Check valid state machine
*/
private func isInvalidState(_ validStates: ResponseState...) -> Bool {
if validStates.contains(state) {
return false
}
let validStatesString = validStates.map { "\($0.rawValue)" }.joined(separator: ",")
log.error("Invalid state - currentState[\(state.rawValue)] validStates[\(validStatesString)]")
return true
}
/**
Factory of NativeResponseInit
*/
private static func createResponseInit(response: URLResponse) -> NativeResponseInit? {
guard let httpResponse = response as? HTTPURLResponse else {
return NativeResponseInit(
headers: [], status: 200, statusText: "", url: response.url?.absoluteString ?? ""
)
}
let status = httpResponse.statusCode
let statusText = HTTPURLResponse.localizedString(forStatusCode: status)
let headers = httpResponse.allHeaderFields.reduce(into: [[String]]()) { result, header in
if let key = header.key as? String, let value = header.value as? String {
result.append([key, value])
}
}
let url = httpResponse.url?.absoluteString ?? ""
return NativeResponseInit(
headers: headers, status: status, statusText: statusText, url: url
)
}
// MARK: - ExpoURLSessionTaskDelegate implementations
func urlSessionDidStart(_ session: ExpoURLSessionTask) {
if isInvalidState(.intialized) {
return
}
state = .started
}
func urlSession(_ session: ExpoURLSessionTask, didReceive response: URLResponse) {
if isInvalidState(.started) {
return
}
responseInit = Self.createResponseInit(response: response)
state = .responseReceived
}
func urlSession(_ session: ExpoURLSessionTask, didReceive data: Data) {
if isInvalidState(.responseReceived, .bodyStreamingStarted, .bodyStreamingCanceled) {
return
}
if state == .responseReceived {
self.sink.appendBufferBody(data: data)
} else if state == .bodyStreamingStarted {
emit(event: "didReceiveResponseData", arguments: data)
}
// no-op in .bodyStreamingCanceled state
}
func urlSession(
_ session: ExpoURLSessionTask,
task: URLSessionTask,
willPerformHTTPRedirection response: HTTPURLResponse,
newRequest request: URLRequest,
completionHandler: @escaping (URLRequest?) -> Void
) {
let shouldFollowRedirects = self.redirectMode == .follow
completionHandler(shouldFollowRedirects ? request : nil)
self.redirected = shouldFollowRedirects
if self.redirectMode == .error {
let error = FetchRedirectException()
self.error = error
if state == .bodyStreamingStarted {
emit(event: "didFailWithError", arguments: error.localizedDescription)
}
state = .errorReceived
emit(event: "readyForJSFinalization")
}
}
func urlSession(_ session: ExpoURLSessionTask, task: URLSessionTask, didCompleteWithError error: (any Error)?) {
if isInvalidState(.started, .responseReceived, .bodyStreamingStarted, .bodyStreamingCanceled) {
return
}
if state == .started,
let urlError = error as? URLError,
urlError.code.rawValue == CFNetworkErrors.cfurlErrorFileDoesNotExist.rawValue,
let url = task.currentRequest?.url,
url.scheme == "file" {
// When requesting a local file that does not exist,
// the `urlSession(_:didReceive:)` method will not be called.
// Instead of throwing an exception, we generate a 404 response.
responseInit = NativeResponseInit(
headers: [], status: 404, statusText: "File not found", url: url.absoluteString)
// First, set the state to .responseReceived, and then to .errorReceived in the next loop.
// This simulates the state transition similar to HTTP requests.
state = .responseReceived
dispatchQueue.async { [weak self] in
guard let self else {
return
}
self.urlSession(session, task: task, didCompleteWithError: error)
}
return
}
if state == .bodyStreamingStarted {
if let error {
emit(event: "didFailWithError", arguments: error.localizedDescription)
} else {
emit(event: "didComplete")
}
}
if let error {
self.error = error
state = .errorReceived
} else {
state = .bodyCompleted
}
emit(event: "readyForJSFinalization")
}
}