Skip to content

Fetch: Download progress #425

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
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
86 changes: 43 additions & 43 deletions 5-network/03-fetch-progress/article.md
Original file line number Diff line number Diff line change
@@ -1,60 +1,60 @@

# Fetch: Download progress
# Fetch: progreso de la descarga

The `fetch` method allows to track *download* progress.
El método `fetch` permite rastrear el progreso de *descarga*.

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.
Ten en cuenta: actualmente no hay forma de que `fetch` rastree el progreso de *carga*. Para ese propósito, utiliza [XMLHttpRequest](info:xmlhttprequest), lo cubriremos más adelante.

To track download progress, we can use `response.body` property. It's `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.
Para rastrear el progreso de la descarga, podemos usar la propiedad `response.body`. Su `ReadableStream`, un objeto especial que proporciona el cuerpo fragmento a fragmento, tal como viene. Las transmisiones legibles se describen en la especificación de la [API de transmisiones](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.
A diferencia de `response.text()`, `response.json()` y otros métodos, `response.body` da control total sobre el proceso de lectura, y podemos contar cuánto se consume en cualquier momento.

Here's the sketch of code that reads the reponse from `response.body`:
Aquí está el bosquejo del código que lee la respuesta de `response.body`:

```js
// instead of response.json() and other methods
// en lugar de response.json() y otros métodos
const reader = response.body.getReader();

// infinite loop while the body is downloading
// bucle infinito mientras el cuerpo se descarga
while(true) {
// done is true for the last chunk
// value is Uint8Array of the chunk bytes
// done es true para el último fragmento
// value es Uint8Array de los bytes del fragmento
const {done, value} = await reader.read();

if (done) {
break;
}

console.log(`Received ${value.length} bytes`)
console.log(`Recibí ${value.length} bytes`)
}
```

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`.
El resultado de la llamada `await reader.read()` es un objeto con dos propiedades:
- **`done`** -- `true` cuando la lectura está completa, de lo contrario `false`.
- **`value`** -- una matriz de tipo bytes: `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.
La API de transmisiones también describe la iteración asincrónica sobre `ReadableStream` con el bucle `for await..of`, pero aún no es ampliamente compatible (consulta [problemas del navegador](https://github.com/whatwg/streams/issues/778#issuecomment-461341033)), por lo que usamos el bucle `while`.
```

We receive response chunks in the loop, until the loading finishes, that is: until `done` becomes `true`.
Recibimos fragmentos de respuesta en el bucle, hasta que finaliza la carga, es decir: hasta que `done` se convierte en `true`.

To log the progress, we just need for every received fragment `value` to add its length to the counter.
Para registrar el progreso, solo necesitamos que cada `value` de fragmento recibido agregue su longitud al contador.

Here's the full working example that gets the response and logs the progress in console, more explanations to follow:
Aquí está el ejemplo funcional completo que obtiene la respuesta y registra el progreso en la consola, seguido de su explicación:

```js run async
// Step 1: start the fetch and obtain a reader
let response = await fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits?per_page=100');
// Paso 1: iniciar la búsqueda y obtener un lector
let response = await fetch('https://api.github.com/repos/javascript-tutorial/es.javascript.info/commits?per_page=100');

const reader = response.body.getReader();

// Step 2: get total length
// Paso 2: obtener la longitud total
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)
// Paso 3: leer los datos
let receivedLength = 0; // cantidad de bytes recibidos hasta el momento
let chunks = []; // matriz de fragmentos binarios recibidos (comprende el cuerpo)
while(true) {
const {done, value} = await reader.read();

Expand All @@ -65,48 +65,48 @@ while(true) {
chunks.push(value);
receivedLength += value.length;

console.log(`Received ${receivedLength} of ${contentLength}`)
console.log(`Recibí ${receivedLength} de ${contentLength}`)
}

// Step 4: concatenate chunks into single Uint8Array
// Paso 4: concatenar fragmentos en un solo 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;
}

// Step 5: decode into a string
// Paso 5: decodificar en un string
let result = new TextDecoder("utf-8").decode(chunksAll);

// We're done!
// ¡Hemos terminado!
let commits = JSON.parse(result);
alert(commits[0].author.login);
```

Let's explain that step-by-step:
Expliquemos eso paso a paso:

1. We perform `fetch` as usual, but instead of calling `response.json()`, we obtain a stream reader `response.body.getReader()`.
1. Realizamos `fetch` como de costumbre, pero en lugar de llamar a `response.json()`, obtenemos un lector de transmisión `response.body.getReader()`.

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.
Ten en cuenta que no podemos usar ambos métodos para leer la misma respuesta: usa un lector o un método de respuesta para obtener el resultado.
2. Antes de leer, podemos averiguar la longitud completa de la respuesta del encabezado `Content-Length`.

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.
Puede estar ausente para solicitudes cross-origin (consulta el capítulo <info:fetch-crossorigin>) y, bueno, técnicamente un servidor no tiene que configurarlo. Pero generalmente está en su lugar.
3. Llama a `await reader.read()` hasta que esté listo.

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.
Recopilamos fragmentos de respuesta en la matriz `chunks`. Eso es importante, porque después de consumir la respuesta, no podremos "releerla" usando `response.json()` u otra forma (puedes intentarlo, habrá un error).
4. Al final, tenemos `chunks` - una matriz de fragmentos de bytes `Uint8Array`. Necesitamos unirlos en un solo resultado. Desafortunadamente, no hay un método simple que los concatene, por lo que hay un código para hacerlo:
1. Creamos `chunksAll = new Uint8Array(selectedLength)` -- una matriz del mismo tipo con la longitud combinada.
2. Luego usa el método `.set(chunk, position)` para copiar cada `chunk` uno tras otro en él.
5. Tenemos el resultado en `chunksAll`. Sin embargo, es una matriz de bytes, no un string.

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.
Para crear un string, necesitamos interpretar estos bytes. El [TextDecoder](info:text-decoder) nativo hace exactamente eso. Luego podemos usar el resultado en `JSON.parse`, si es necesario.

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:
¿Qué pasa si necesitamos contenido binario en lugar de un string? Eso es aún más sencillo. Reemplaza los pasos 4 y 5 con una sola línea que crea un `Blob` de todos los fragmentos:
```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.
Al final tenemos el resultado (como un string o un blob, lo que sea conveniente) y el seguimiento del progreso en el proceso.

Once again, please note, that's not for *upload* progress (no way now with `fetch`), only for *download* progress.
Una vez más, ten en cuenta que eso no es para el progreso de *carga* (hasta ahora eso no es posible con `fetch`), solo para el progreso de *descarga*.
4 changes: 2 additions & 2 deletions 5-network/03-fetch-progress/progress.view/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@
const chunk = await reader.read();

if (chunk.done) {
console.log("done!");
console.log("listo!");
break;
}

chunks.push(chunk.value);
receivedLength += chunk.value.length;
console.log(`${receivedLength}/${contentLength} received`)
console.log(`recibí ${receivedLength}/${contentLength}`)
}


Expand Down