Skip to content

Commit d43224a

Browse files
Copilotfarfromrefug
andcommitted
Add early resolution documentation and fix scope issue
Agent-Logs-Url: https://github.com/nativescript-community/https/sessions/7bc451f5-53da-42f8-b904-b8680baa893e Co-authored-by: farfromrefug <655344+farfromrefug@users.noreply.github.com>
1 parent b321446 commit d43224a

File tree

2 files changed

+345
-5
lines changed

2 files changed

+345
-5
lines changed

docs/EARLY_RESOLUTION.md

Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
# Early Resolution Feature
2+
3+
## Overview
4+
5+
The early resolution feature allows iOS GET requests to resolve immediately when headers are received, before the full download completes. This enables you to:
6+
7+
1. **Inspect status code and headers** before committing to a large download
8+
2. **Cancel requests early** if the response doesn't meet criteria (e.g., wrong content type, too large)
9+
3. **Show progress UI** sooner since you know the download size immediately
10+
11+
## Usage
12+
13+
### Basic Example
14+
15+
```typescript
16+
import { request } from '@nativescript-community/https';
17+
18+
async function downloadFile() {
19+
// Request resolves as soon as headers arrive
20+
const response = await request({
21+
method: 'GET',
22+
url: 'https://example.com/large-video.mp4',
23+
earlyResolve: true, // Enable early resolution
24+
tag: 'my-download' // For cancellation
25+
});
26+
27+
console.log('Headers received!');
28+
console.log('Status:', response.statusCode);
29+
console.log('Content-Length:', response.contentLength);
30+
console.log('Content-Type:', response.headers['Content-Type']);
31+
32+
// Check if we want to proceed with download
33+
if (response.statusCode !== 200) {
34+
console.log('Bad status code, cancelling...');
35+
cancel('my-download');
36+
return;
37+
}
38+
39+
if (response.contentLength > 100 * 1024 * 1024) {
40+
console.log('File too large, cancelling...');
41+
cancel('my-download');
42+
return;
43+
}
44+
45+
// toFile() waits for download to complete, then moves file
46+
console.log('Download accepted, waiting for completion...');
47+
const file = await response.content.toFile('~/Videos/video.mp4');
48+
console.log('Download complete:', file.path);
49+
}
50+
```
51+
52+
### With Progress Tracking
53+
54+
```typescript
55+
const response = await request({
56+
method: 'GET',
57+
url: 'https://example.com/large-file.zip',
58+
earlyResolve: true,
59+
onProgress: (current, total) => {
60+
const percent = (current / total * 100).toFixed(1);
61+
console.log(`Download progress: ${percent}% (${current}/${total} bytes)`);
62+
}
63+
});
64+
65+
// Check headers immediately
66+
if (response.headers['Content-Type'] !== 'application/zip') {
67+
console.log('Wrong content type!');
68+
return;
69+
}
70+
71+
// Wait for full download
72+
await response.content.toFile('~/Downloads/file.zip');
73+
```
74+
75+
### Conditional Download Based on Headers
76+
77+
```typescript
78+
async function smartDownload(url: string) {
79+
const response = await request({
80+
method: 'GET',
81+
url,
82+
earlyResolve: true
83+
});
84+
85+
const contentType = response.headers['Content-Type'] || '';
86+
const contentLength = response.contentLength;
87+
88+
// Decide what to do based on headers
89+
if (contentType.includes('application/json')) {
90+
// Small JSON, use toJSON()
91+
const data = await response.content.toJSON();
92+
return data;
93+
} else if (contentType.includes('image/')) {
94+
// Image, use toImage()
95+
const image = await response.content.toImage();
96+
return image;
97+
} else {
98+
// Large file, save to disk
99+
const filename = `download_${Date.now()}`;
100+
await response.content.toFile(`~/Downloads/${filename}`);
101+
return filename;
102+
}
103+
}
104+
```
105+
106+
## How It Works
107+
108+
### Without Early Resolution (Default)
109+
110+
```
111+
1. await request() starts
112+
2. [HTTP connection established]
113+
3. [Headers received]
114+
4. [Full download to temp file: 0% ... 100%]
115+
5. await request() resolves ← You get response here
116+
6. response.content.toFile() ← Instant file move
117+
```
118+
119+
### With Early Resolution (earlyResolve: true)
120+
121+
```
122+
1. await request() starts
123+
2. [HTTP connection established]
124+
3. [Headers received]
125+
4. await request() resolves ← You get response here (immediately!)
126+
5. [Download continues in background: 0% ... 100%]
127+
6. response.content.toFile() ← Waits for download, then moves file
128+
└─ If download not done: waits
129+
└─ If download done: instant file move
130+
```
131+
132+
## Important Notes
133+
134+
### 1. Download Continues in Background
135+
136+
When `earlyResolve: true`, the promise resolves immediately but the download continues in the background. The download will complete even if you don't call `toFile()` or other content methods.
137+
138+
### 2. Content Methods Wait for Completion
139+
140+
All content access methods wait for the download to complete:
141+
142+
```typescript
143+
const response = await request({ ..., earlyResolve: true });
144+
// ↑ Resolves immediately with headers
145+
146+
await response.content.toFile('...'); // Waits for download
147+
await response.content.toJSON(); // Waits for download
148+
await response.content.toImage(); // Waits for download
149+
await response.content.toString(); // Waits for download
150+
```
151+
152+
### 3. Cancellation
153+
154+
You can cancel the download after inspecting headers:
155+
156+
```typescript
157+
const response = await request({
158+
method: 'GET',
159+
url: '...',
160+
earlyResolve: true,
161+
tag: 'my-download'
162+
});
163+
164+
if (response.contentLength > MAX_SIZE) {
165+
cancel('my-download'); // Cancels background download
166+
}
167+
```
168+
169+
### 4. GET Requests Only
170+
171+
Currently, early resolution only works with GET requests. Other HTTP methods (POST, PUT, DELETE) will ignore the `earlyResolve` option.
172+
173+
### 5. Memory Efficiency Maintained
174+
175+
Even with early resolution, downloads still stream to a temp file (not loaded into memory). This maintains the memory efficiency of the streaming download feature.
176+
177+
## Configuration Options
178+
179+
### earlyResolve
180+
181+
- **Type:** `boolean`
182+
- **Default:** `false`
183+
- **Platform:** iOS only
184+
- **Description:** Resolve the request promise when headers arrive, before download completes
185+
186+
```typescript
187+
{
188+
earlyResolve: true // Resolve on headers, download continues
189+
}
190+
```
191+
192+
### downloadSizeThreshold
193+
194+
- **Type:** `number` (bytes)
195+
- **Default:** `1048576` (1 MB)
196+
- **Platform:** iOS only
197+
- **Description:** Response size threshold for file download vs memory loading
198+
199+
```typescript
200+
{
201+
downloadSizeThreshold: 5 * 1024 * 1024 // 5 MB threshold
202+
}
203+
```
204+
205+
Responses larger than this will be downloaded to temp file (memory efficient). Responses smaller will be loaded into memory (faster for small responses).
206+
207+
## Comparison with Android
208+
209+
Android's `ResponseBody` naturally provides this behavior:
210+
- The request completes immediately when headers arrive
211+
- The body stream is available but not consumed
212+
- Calling methods like `toFile()` consumes the stream
213+
214+
iOS with `earlyResolve: true` mimics this behavior:
215+
- The request resolves when headers arrive
216+
- The download continues in background
217+
- Calling methods like `toFile()` waits for completion
218+
219+
This makes the iOS and Android APIs more consistent when using `earlyResolve: true`.
220+
221+
## Error Handling
222+
223+
If the download fails after headers are received:
224+
225+
```typescript
226+
try {
227+
const response = await request({
228+
method: 'GET',
229+
url: '...',
230+
earlyResolve: true
231+
});
232+
233+
console.log('Headers OK:', response.statusCode);
234+
235+
// If download fails after headers, toFile() will throw
236+
await response.content.toFile('...');
237+
238+
} catch (error) {
239+
console.error('Download failed:', error);
240+
}
241+
```
242+
243+
## Performance Considerations
244+
245+
### When to Use Early Resolution
246+
247+
**Good use cases:**
248+
- Large downloads where you want to check headers first
249+
- Conditional downloads based on content type or size
250+
- Downloads where user might cancel based on file size
251+
- APIs that return metadata in headers (file size, checksum, etc.)
252+
253+
**Not recommended:**
254+
- Small API responses (< 1MB) where early resolution adds complexity
255+
- Requests where you always need the full content
256+
- Simple requests where you don't inspect headers
257+
258+
### Performance Impact
259+
260+
Early resolution has minimal performance impact:
261+
- No additional network requests
262+
- No memory overhead
263+
- Download happens at the same speed
264+
- Slight overhead from promise/callback management (negligible)
265+
266+
## Example: Download Manager with Early Resolution
267+
268+
```typescript
269+
class DownloadManager {
270+
async download(url: string, destination: string) {
271+
try {
272+
// Get headers first
273+
const response = await request({
274+
method: 'GET',
275+
url,
276+
earlyResolve: true,
277+
tag: url,
278+
onProgress: (current, total) => {
279+
this.updateProgress(url, current, total);
280+
}
281+
});
282+
283+
// Validate headers
284+
if (response.statusCode !== 200) {
285+
throw new Error(`HTTP ${response.statusCode}`);
286+
}
287+
288+
const fileSize = response.contentLength;
289+
const contentType = response.headers['Content-Type'];
290+
291+
console.log(`Downloading ${fileSize} bytes (${contentType})`);
292+
293+
// Check storage space
294+
if (fileSize > this.getAvailableSpace()) {
295+
cancel(url);
296+
throw new Error('Insufficient storage space');
297+
}
298+
299+
// Proceed with download
300+
const file = await response.content.toFile(destination);
301+
console.log('Downloaded:', file.path);
302+
303+
return file;
304+
305+
} catch (error) {
306+
console.error('Download failed:', error);
307+
throw error;
308+
}
309+
}
310+
311+
private updateProgress(url: string, current: number, total: number) {
312+
const percent = (current / total * 100).toFixed(1);
313+
console.log(`[${url}] ${percent}%`);
314+
}
315+
316+
private getAvailableSpace(): number {
317+
// Implementation depends on platform
318+
return 1024 * 1024 * 1024; // Example: 1GB
319+
}
320+
}
321+
```
322+
323+
## Future Enhancements
324+
325+
Potential improvements for future versions:
326+
327+
1. **Streaming to custom destination:** Start writing to destination file immediately instead of temp file
328+
2. **Partial downloads:** Resume interrupted downloads
329+
3. **Multiple callbacks:** Progress callbacks that fire at different stages
330+
4. **Background downloads:** Downloads that survive app termination (iOS background tasks)
331+
332+
## See Also
333+
334+
- [iOS Streaming Implementation](./IOS_STREAMING_IMPLEMENTATION.md)
335+
- [iOS/Android Behavior Parity](./IOS_ANDROID_BEHAVIOR_PARITY.md)
336+
- [Usage Examples](./USAGE_EXAMPLE.md)

src/https/request.ios.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,9 @@ export function createRequest(opts: HttpsRequestOptions, useLegacy: boolean = tr
690690
downloadCompletionReject = rej;
691691
});
692692

693+
// Track the content object so we can update it when download completes
694+
let responseContent: HttpsResponseLegacy | undefined;
695+
693696
const downloadTask = manager.downloadToTempWithEarlyHeaders(
694697
opts.method,
695698
opts.url,
@@ -704,9 +707,10 @@ export function createRequest(opts: HttpsRequestOptions, useLegacy: boolean = tr
704707
const httpResponse = response as NSHTTPURLResponse;
705708

706709
// Create response WITHOUT temp file path (download still in progress)
707-
const content = useLegacy
708-
? new HttpsResponseLegacy(null, contentLength, opts.url, undefined, downloadCompletionPromise)
709-
: undefined;
710+
if (useLegacy) {
711+
responseContent = new HttpsResponseLegacy(null, contentLength, opts.url, undefined, downloadCompletionPromise);
712+
}
713+
const content = useLegacy ? responseContent : undefined;
710714

711715
let getHeaders = () => ({});
712716
const sendi = {
@@ -739,8 +743,8 @@ export function createRequest(opts: HttpsRequestOptions, useLegacy: boolean = tr
739743
downloadCompletionReject(new Error(error.localizedDescription));
740744
} else {
741745
// Update the response content with temp file path
742-
if (useLegacy && content instanceof HttpsResponseLegacy) {
743-
(content as any).tempFilePath = tempFilePath;
746+
if (responseContent) {
747+
(responseContent as any).tempFilePath = tempFilePath;
744748
}
745749
downloadCompletionResolve();
746750
}

0 commit comments

Comments
 (0)