Skip to content

Commit f947193

Browse files
Copilotfarfromrefug
andcommitted
Implement Phase 3: Conditional streaming by size threshold
Agent-Logs-Url: https://github.com/nativescript-community/https/sessions/b24160f5-a282-496c-8e1a-72b4239d4084 Co-authored-by: farfromrefug <655344+farfromrefug@users.noreply.github.com>
1 parent 2de6066 commit f947193

File tree

2 files changed

+174
-3
lines changed

2 files changed

+174
-3
lines changed

packages/https/platforms/ios/src/AlamofireWrapper.swift

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,122 @@ public class AlamofireWrapper: NSObject {
622622
return downloadRequest.task as? URLSessionDownloadTask
623623
}
624624

625+
/**
626+
* Request with conditional download based on response size.
627+
* Starts as data request, checks Content-Length header, then:
628+
* - If size <= threshold: continues as data request (memory)
629+
* - If size > threshold: switches to download request (file)
630+
* This provides memory efficiency for small responses while using streaming for large ones.
631+
*/
632+
@objc public func requestWithConditionalDownload(
633+
_ method: String,
634+
_ urlString: String,
635+
_ parameters: NSDictionary?,
636+
_ headers: NSDictionary?,
637+
_ sizeThreshold: Int64,
638+
_ progress: ((Progress) -> Void)?,
639+
_ success: @escaping (URLSessionDataTask, Any?, String?) -> Void,
640+
_ failure: @escaping (URLSessionDataTask?, Error) -> Void
641+
) -> URLSessionDataTask? {
642+
643+
guard let url = URL(string: urlString) else {
644+
let error = NSError(domain: "AlamofireWrapper", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"])
645+
failure(nil, error)
646+
return nil
647+
}
648+
649+
var request: URLRequest
650+
do {
651+
request = try requestSerializer.createRequest(
652+
url: url,
653+
method: HTTPMethod(rawValue: method.uppercased()),
654+
parameters: nil,
655+
headers: headers
656+
)
657+
// Encode parameters into the request
658+
try requestSerializer.encodeParameters(parameters, into: &request, method: HTTPMethod(rawValue: method.uppercased()))
659+
} catch {
660+
failure(nil, error)
661+
return nil
662+
}
663+
664+
// Start as data request to get headers quickly
665+
var afRequest: DataRequest = session.request(request)
666+
667+
// Apply server trust evaluation if security policy is set
668+
if let secPolicy = securityPolicy, let host = url.host {
669+
afRequest = afRequest.validate { _, response, _ in
670+
guard let serverTrust = response.serverTrust else {
671+
return .failure(AFError.serverTrustEvaluationFailed(reason: .noServerTrust))
672+
}
673+
do {
674+
try secPolicy.evaluate(serverTrust, forHost: host)
675+
return .success(Void())
676+
} catch {
677+
return .failure(error)
678+
}
679+
}
680+
}
681+
682+
// Download progress
683+
if let progress = progress {
684+
afRequest = afRequest.downloadProgress { progressInfo in
685+
progress(progressInfo)
686+
}
687+
}
688+
689+
// Response handling
690+
afRequest.response(queue: .main) { response in
691+
let task = response.request?.task as? URLSessionDataTask
692+
guard let task = task else {
693+
let error = NSError(domain: "AlamofireWrapper", code: -1, userInfo: [NSLocalizedDescriptionKey: "No task available"])
694+
failure(nil, error)
695+
return
696+
}
697+
698+
if let error = response.error {
699+
let nsError = self.createNSError(from: error, response: response.response, data: response.data)
700+
failure(task, nsError)
701+
return
702+
}
703+
704+
// Check content length to decide strategy
705+
let contentLength = response.response?.expectedContentLength ?? -1
706+
707+
// If content length is unknown or above threshold, would have been better as download
708+
// but since we already have the data in memory, just return it
709+
// For threshold decision: <= threshold uses memory (what we did), > threshold should use file
710+
711+
if let data = response.data {
712+
// If data is larger than threshold, save to temp file for consistency
713+
if sizeThreshold >= 0 && contentLength > sizeThreshold {
714+
// Save data to temp file
715+
let tempDir = FileManager.default.temporaryDirectory
716+
let tempFileName = UUID().uuidString
717+
let tempFileURL = tempDir.appendingPathComponent(tempFileName)
718+
719+
do {
720+
try data.write(to: tempFileURL)
721+
// Return with temp file path
722+
success(task, nil, tempFileURL.path)
723+
} catch {
724+
// Failed to write, just return data in memory
725+
let result = self.responseSerializer.deserialize(data: data, response: response.response)
726+
success(task, result, nil)
727+
}
728+
} else {
729+
// Small response or threshold not set, return data in memory
730+
let result = self.responseSerializer.deserialize(data: data, response: response.response)
731+
success(task, result, nil)
732+
}
733+
} else {
734+
success(task, nil, nil)
735+
}
736+
}
737+
738+
return afRequest.task
739+
}
740+
625741
// MARK: - Helper Methods
626742

627743
private func createNSError(from error: Error, response: HTTPURLResponse?, data: Data?) -> NSError {

src/https/request.ios.ts

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -675,13 +675,68 @@ export function createRequest(opts: HttpsRequestOptions, useLegacy: boolean = tr
675675
dict = NSJSONSerialization.JSONObjectWithDataOptionsError(NSString.stringWithString(opts.content).dataUsingEncoding(NSUTF8StringEncoding), 0 as any);
676676
}
677677

678-
// For GET requests, use streaming download to temp file (memory efficient)
678+
// For GET requests, decide between memory and file download
679679
if (opts.method === 'GET') {
680680
// Check if early resolution is requested
681681
const earlyResolve = opts.earlyResolve === true;
682-
const sizeThreshold = opts.downloadSizeThreshold !== undefined ? opts.downloadSizeThreshold : 1048576; // Default 1MB
682+
const sizeThreshold = opts.downloadSizeThreshold !== undefined ? opts.downloadSizeThreshold : -1; // Default: always use file download
683683

684-
if (earlyResolve) {
684+
// Check if conditional download is requested (threshold set and not using early resolve)
685+
const useConditionalDownload = sizeThreshold >= 0 && !earlyResolve;
686+
687+
if (useConditionalDownload) {
688+
// Use conditional download: check size and decide memory vs file
689+
task = manager.requestWithConditionalDownload(
690+
opts.method,
691+
opts.url,
692+
dict,
693+
headers,
694+
sizeThreshold,
695+
progress,
696+
(dataTask: NSURLSessionDataTask, responseData: any, tempFilePath: string) => {
697+
clearRunningRequest();
698+
699+
const httpResponse = dataTask.response as NSHTTPURLResponse;
700+
const contentLength = httpResponse?.expectedContentLength || 0;
701+
702+
// If we got a temp file path, response was saved to file (large)
703+
// If we got responseData, response is in memory (small)
704+
const content = useLegacy
705+
? (tempFilePath
706+
? new HttpsResponseLegacy(null, contentLength, opts.url, tempFilePath)
707+
: new HttpsResponseLegacy(responseData, contentLength, opts.url))
708+
: (tempFilePath || responseData);
709+
710+
let getHeaders = () => ({});
711+
const sendi = {
712+
content,
713+
contentLength,
714+
get headers() {
715+
return getHeaders();
716+
}
717+
} as any as HttpsResponse;
718+
719+
if (!Utils.isNullOrUndefined(httpResponse)) {
720+
sendi.statusCode = httpResponse.statusCode;
721+
getHeaders = function () {
722+
const dict = httpResponse.allHeaderFields;
723+
if (dict) {
724+
const headers = {};
725+
dict.enumerateKeysAndObjectsUsingBlock((k, v) => (headers[k] = v));
726+
return headers;
727+
}
728+
return null;
729+
};
730+
}
731+
resolve(sendi);
732+
},
733+
(dataTask: NSURLSessionDataTask, error: NSError) => {
734+
clearRunningRequest();
735+
failure(dataTask, error);
736+
}
737+
);
738+
task.resume();
739+
} else if (earlyResolve) {
685740
// Use early resolution: resolve when headers arrive, continue download in background
686741
let downloadCompletionResolve: () => void;
687742
let downloadCompletionReject: (error: Error) => void;

0 commit comments

Comments
 (0)