Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 55 additions & 47 deletions 5-network/03-fetch-progress/article.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@

# Fetch: Download progress
# متد Fetch: پیشرفت دانلود

The `fetch` method allows to track *download* progress.
متد `fetch` به ما اجازه می‌دهد پیشرفت *دانلود* را دنبال کنیم.

Please note: there's currently no way for `fetch` to track *upload* progress. For that purpose, please use [XMLHttpRequest](info:xmlhttprequest), we'll cover it later.
لطفاً توجه کنید: در حال حاضر هیچ راهی وجود ندارد که `fetch` بتواند پیشرفت *آپلود* را ردیابی کند. برای این کار باید از [XMLHttpRequest](info:xmlhttprequest) استفاده کنید که بعداً بررسی خواهد شد.

To track download progress, we can use `response.body` property. It's a `ReadableStream` -- a special object that provides body chunk-by-chunk, as it comes. Readable streams are described in the [Streams API](https://streams.spec.whatwg.org/#rs-class) specification.
برای دنبال‌کردن پیشرفت دانلود، می‌توانیم از ویژگی `response.body` استفاده کنیم. این یک `ReadableStream` است -- یک شیء خاص که بدنه را به صورت تکه‌تکه (chunk-by-chunk) و هم‌زمان با دریافت فراهم می‌کند. جریان‌های قابل خواندن در [Streams API](https://streams.spec.whatwg.org/#rs-class) توضیح داده شده‌اند.

Unlike `response.text()`, `response.json()` and other methods, `response.body` gives full control over the reading process, and we can count how much is consumed at any moment.
برخلاف `response.text()`، `response.json()` و سایر متدها، ویژگی `response.body` کنترل کامل روی فرایند خواندن می‌دهد و می‌توانیم در هر لحظه مقدار داده‌ی مصرف‌شده را اندازه‌گیری کنیم.

Here's the sketch of code that reads the response from `response.body`:
در ادامه اسکلت کدی را می‌بینید که پاسخ را از `response.body` می‌خواند:

```js
// instead of response.json() and other methods
// به‌جای response.json() و سایر متدها
const reader = response.body.getReader();

// infinite loop while the body is downloading
// حلقه بی‌نهایت تا زمانی که بدنه در حال دانلود است
while(true) {
// done is true for the last chunk
// value is Uint8Array of the chunk bytes
// done برای آخرین chunk برابر true می‌شود
// value یک Uint8Array از بایت‌های chunk است
const {done, value} = await reader.read();

if (done) {
Expand All @@ -29,32 +29,34 @@ while(true) {
}
```

The result of `await reader.read()` call is an object with two properties:
- **`done`** -- `true` when the reading is complete, otherwise `false`.
- **`value`** -- a typed array of bytes: `Uint8Array`.
نتیجه‌ی فراخوانی `await reader.read()` یک شیء با دو ویژگی است:

* ویژگی **`done`** -- زمانی `true` می‌شود که خواندن کامل شده باشد، در غیر این صورت `false` است.
* ویژگی **`value`** -- یک آرایه تایپ‌شده از بایت‌ها: `Uint8Array`.

```smart
Streams API also describes asynchronous iteration over `ReadableStream` with `for await..of` loop, but it's not yet widely supported (see [browser issues](https://github.com/whatwg/streams/issues/778#issuecomment-461341033)), so we use `while` loop.
توجه: Streams API همچنین پیمایش ناهمزمان (`async iteration`) روی `ReadableStream` را با حلقه‌ی `for await..of` تعریف می‌کند، اما هنوز به طور گسترده پشتیبانی نمی‌شود (به [مشکلات مرورگرها](https://github.com/whatwg/streams/issues/778#issuecomment-461341033) مراجعه کنید)، بنابراین از حلقه‌ی `while` استفاده می‌کنیم.
```

We receive response chunks in the loop, until the loading finishes, that is: until `done` becomes `true`.
ما در حلقه، تکه‌های پاسخ را دریافت می‌کنیم تا زمانی که بارگذاری تمام شود؛ یعنی تا زمانی که `done` برابر `true` شود.

To log the progress, we just need for every received fragment `value` to add its length to the counter.
برای ثبت پیشرفت، کافی است در هر بار دریافت قطعه‌ی `value`، طول آن را به شمارنده اضافه کنیم.

Here's the full working example that gets the response and logs the progress in console, more explanations to follow:
در ادامه یک مثال کامل داریم که پاسخ را دریافت می‌کند و پیشرفت را در کنسول لاگ می‌کند. توضیحات بیشتر بعد از آن آمده است:

```js run async
// Step 1: start the fetch and obtain a reader
// مرحله 1: شروع fetch و گرفتن reader
let response = await fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits?per_page=100');

const reader = response.body.getReader();

// Step 2: get total length
// مرحله 2: گرفتن طول کل داده
const contentLength = +response.headers.get('Content-Length');

// Step 3: read the data
let receivedLength = 0; // received that many bytes at the moment
let chunks = []; // array of received binary chunks (comprises the body)
// مرحله 3: خواندن داده
let receivedLength = 0; // تا این لحظه این مقدار بایت دریافت شده
let chunks = []; // آرایه‌ای از تکه‌های باینری دریافت‌شده (بدنه)

while(true) {
const {done, value} = await reader.read();

Expand All @@ -68,47 +70,53 @@ while(true) {
console.log(`Received ${receivedLength} of ${contentLength}`)
}

// Step 4: concatenate chunks into single Uint8Array
let chunksAll = new Uint8Array(receivedLength); // (4.1)
// مرحله 4: اتصال chunkها به یک Uint8Array واحد
let chunksAll = new Uint8Array(receivedLength); // (مرحله 4.1)
let position = 0;
for(let chunk of chunks) {
chunksAll.set(chunk, position); // (4.2)
position += chunk.length;
chunksAll.set(chunk, position); // (مرحله 4.2)
position += chunk.length;
}

// Step 5: decode into a string
// مرحله 5: تبدیل به رشته
let result = new TextDecoder("utf-8").decode(chunksAll);

// We're done!
// کار تمام شد!
let commits = JSON.parse(result);
alert(commits[0].author.login);
```

Let's explain that step-by-step:
بیایید مرحله به مرحله توضیح بدهیم:

1. ما مانند حالت عادی `fetch` انجام می‌دهیم، اما به جای `response.json()`، یک stream reader می‌گیریم: `response.body.getReader()`.

توجه کنید که نمی‌توان هم‌زمان از هر دو روش برای خواندن یک پاسخ استفاده کرد: یا باید از reader استفاده کنیم یا از متدهای آماده‌ی response.

2. قبل از خواندن، می‌توانیم از هدر `Content-Length` طول کل پاسخ را به دست آوریم.

این مقدار ممکن است در درخواست‌های cross-origin وجود نداشته باشد (به فصل <info:fetch-crossorigin> مراجعه کنید) و در عمل هم سرور مجبور به ارسال آن نیست. اما معمولاً وجود دارد.

3. تابع `await reader.read()` را تا زمانی که تمام شود اجرا می‌کنیم.

ما تکه‌های پاسخ را در آرایه‌ی `chunks` جمع می‌کنیم. این مهم است چون بعد از مصرف شدن پاسخ، دیگر نمی‌توان آن را دوباره با `response.json()` یا روش‌های مشابه خواند (اگر امتحان کنید خطا خواهید گرفت).

1. We perform `fetch` as usual, but instead of calling `response.json()`, we obtain a stream reader `response.body.getReader()`.
4. در پایان، ما `chunks` را داریم -- آرایه‌ای از قطعات بایت `Uint8Array`. باید آن‌ها را به یک خروجی واحد تبدیل کنیم. متأسفانه متد مستقیمی برای اتصال آن‌ها وجود ندارد، بنابراین این کار را دستی انجام می‌دهیم:

Please note, we can't use both these methods to read the same response: either use a reader or a response method to get the result.
2. Prior to reading, we can figure out the full response length from the `Content-Length` header.
1. یک `Uint8Array` جدید با طول کل ایجاد می‌کنیم: `chunksAll = new Uint8Array(receivedLength)`
2. سپس با `.set(chunk, position)` هر قطعه را پشت سر هم داخل آن کپی می‌کنیم.

It may be absent for cross-origin requests (see chapter <info:fetch-crossorigin>) and, well, technically a server doesn't have to set it. But usually it's at place.
3. Call `await reader.read()` until it's done.
5. در نهایت داده‌ی نهایی در `chunksAll` قرار دارد، اما هنوز رشته نیست.

We gather response chunks in the array `chunks`. That's important, because after the response is consumed, we won't be able to "re-read" it using `response.json()` or another way (you can try, there'll be an error).
4. At the end, we have `chunks` -- an array of `Uint8Array` byte chunks. We need to join them into a single result. Unfortunately, there's no single method that concatenates those, so there's some code to do that:
1. We create `chunksAll = new Uint8Array(receivedLength)` -- a same-typed array with the combined length.
2. Then use `.set(chunk, position)` method to copy each `chunk` one after another in it.
5. We have the result in `chunksAll`. It's a byte array though, not a string.
برای تبدیل به رشته، باید این بایت‌ها را تفسیر کنیم. کلاس داخلی [TextDecoder](info:text-decoder) دقیقاً همین کار را انجام می‌دهد. سپس در صورت نیاز می‌توانیم آن را با `JSON.parse` تبدیل کنیم.

To create a string, we need to interpret these bytes. The built-in [TextDecoder](info:text-decoder) does exactly that. Then we can `JSON.parse` it, if necessary.
اگر به جای رشته داده‌ی باینری بخواهیم، کار ساده‌تر است: کافی است مراحل 4 و 5 را با یک خط جایگزین کنیم:

What if we need binary content instead of a string? That's even simpler. Replace steps 4 and 5 with a single line that creates a `Blob` from all chunks:
```js
let blob = new Blob(chunks);
```
```js
let blob = new Blob(chunks);
```

At the end we have the result (as a string or a blob, whatever is convenient), and progress-tracking in the process.
در نهایت ما نتیجه را (به صورت رشته یا Blob) داریم و هم‌زمان در طول فرایند، پیشرفت دانلود را هم دنبال می‌کنیم.

Once again, please note, that's not for *upload* progress (no way now with `fetch`), only for *download* progress.
دوباره توجه کنید: این روش برای *پیشرفت آپلود* نیست (فعلاً با `fetch` ممکن نیست)، فقط برای *پیشرفت دانلود* است.

Also, if the size is unknown, we should check `receivedLength` in the loop and break it once it reaches a certain limit. So that the `chunks` won't overflow the memory.
همچنین اگر اندازه‌ی داده مشخص نباشد، باید در حلقه مقدار `receivedLength` را بررسی کنیم و اگر از یک حدی بیشتر شد، حلقه را متوقف کنیم تا آرایه‌ی `chunks` باعث مصرف بیش از حد حافظه نشود.