Recuperación anulable

El problema original de GitHub sobre “anular la recuperación” fue abierto en 2015. Ahora, si quito el 2015 del 2017 (el año actual), obtengo 2. Esto demuestra un error en matemáticas, porque 2015 fue de hecho "por siempre" hace.

En 2015 empezamos a explorar la anulación de recuperaciones en curso y, después de 780 comentarios en GitHub, se creó un par de inicios falsos y 5 solicitudes de extracción. Por último, tenemos el aterrizaje de recuperación anulable El primero es Firefox 57.

Actualización: No, me equivoqué. Edge 16 ya cuenta con compatibilidad para anular. Felicitaciones a los ¡Equipo de Edge!

Más adelante me adentraré en la historia, pero primero, la API:

El control y la maniobra de la señal

Conoce la AbortController y la AbortSignal:

const controller = new AbortController();
const signal = controller.signal;

El controlador solo tiene un método:

controller.abort();

Cuando haces esto, notifica a la señal:

signal.addEventListener('abort', () => {
    // Logs true:
    console.log(signal.aborted);
});

El estándar del DOM proporciona esta API, y esa es toda la API. Es genéricamente deliberadamente para que otros estándares web y bibliotecas de JavaScript puedan usarla.

Anular indicadores y recuperar

La recuperación puede tardar un AbortSignal. Por ejemplo, a continuación, se muestra cómo establecer un tiempo de espera de recuperación después de 5 segundos:

const controller = new AbortController();
const signal = controller.signal;

setTimeout(() => controller.abort(), 5000);

fetch(url, { signal }).then(response => {
    return response.text();
}).then(text => {
    console.log(text);
});

Cuando anulas una recuperación, se anulan la solicitud y la respuesta, por lo que cualquier lectura del cuerpo de la respuesta (como response.text()).

Aquí te presentamos una demostración: Al momento de la redacción, el único navegador que es compatible con esto es Firefox 57. Además, prepárate, nadie con experiencia de diseño participó en la creación de la demostración.

Como alternativa, el indicador se puede proporcionar a un objeto de solicitud y, luego, pasarse a recuperar:

const controller = new AbortController();
const signal = controller.signal;
const request = new Request(url, { signal });

fetch(request);

Esto funciona porque request.signal es un AbortSignal.

Reacciona a una recuperación anulada

Cuando anulas una operación asíncrona, la promesa se rechaza con un DOMException llamado AbortError:

fetch(url, { signal }).then(response => {
    return response.text();
}).then(text => {
    console.log(text);
}).catch(err => {
    if (err.name === 'AbortError') {
    console.log('Fetch aborted');
    } else {
    console.error('Uh oh, an error!', err);
    }
});

A menudo, no querrás mostrar un mensaje de error si el usuario anuló la operación, ya que no es una "error" si hiciste con éxito lo que el usuario solicitó. Para evitar esto, usa una sentencia if, como el una de las anteriores para manejar específicamente los errores de anulación.

Este es un ejemplo que le da al usuario un botón para cargar contenido y un botón para anular. Si el botón de errores, se muestra un error, a menos que sea un error de anulación:

// This will allow us to abort the fetch.
let controller;

// Abort if the user clicks:
abortBtn.addEventListener('click', () => {
    if (controller) controller.abort();
});

// Load the content:
loadBtn.addEventListener('click', async () => {
    controller = new AbortController();
    const signal = controller.signal;

    // Prevent another click until this fetch is done
    loadBtn.disabled = true;
    abortBtn.disabled = false;

    try {
    // Fetch the content & use the signal for aborting
    const response = await fetch(contentUrl, { signal });
    // Add the content to the page
    output.innerHTML = await response.text();
    }
    catch (err) {
    // Avoid showing an error message if the fetch was aborted
    if (err.name !== 'AbortError') {
        output.textContent = "Oh no! Fetching failed.";
    }
    }

    // These actions happen no matter how the fetch ends
    loadBtn.disabled = false;
    abortBtn.disabled = true;
});

Aquí te presentamos una demostración: Al momento de la redacción, los únicos navegadores que compatibles son Edge 16 y Firefox 57.

Una señal, muchas recuperaciones

Se puede usar un solo indicador para anular muchas recuperaciones a la vez:

async function fetchStory({ signal } = {}) {
    const storyResponse = await fetch('/story.json', { signal });
    const data = await storyResponse.json();

    const chapterFetches = data.chapterUrls.map(async url => {
    const response = await fetch(url, { signal });
    return response.text();
    });

    return Promise.all(chapterFetches);
}

En el ejemplo anterior, se usa el mismo indicador para la recuperación inicial y la del capítulo paralelo recuperaciones de datos. A continuación, te mostramos cómo usar fetchStory:

const controller = new AbortController();
const signal = controller.signal;

fetchStory({ signal }).then(chapters => {
    console.log(chapters);
});

En este caso, si llamas a controller.abort(), se anularán las recuperaciones en curso.

El futuro

Otros navegadores

Edge hizo un gran trabajo al enviar esto primero, y Firefox está muy activo. Sus ingenieros implementada desde el paquete de pruebas mientras se realizaba la especificación mientras se está escribiendo. Para otros navegadores, sigue estos pasos:

En un service worker

Necesito terminar las especificaciones de los componentes del service worker, pero este es el plan:

Como mencioné antes, cada objeto Request tiene una propiedad signal. Dentro de un service worker, fetchEvent.request.signal indicará que se anuló si la página ya no está interesada en la respuesta. Como resultado, un código como este simplemente funciona:

addEventListener('fetch', event => {
    event.respondWith(fetch(event.request));
});

Si la página anula la recuperación, los indicadores de fetchEvent.request.signal anulan, de modo que la recuperación dentro del service worker también se anula.

Si recuperas un valor distinto de event.request, deberás pasar el indicador a tu recuperaciones personalizadas

addEventListener('fetch', event => {
    const url = new URL(event.request.url);

    if (event.request.method == 'GET' && url.pathname == '/about/') {
    // Modify the URL
    url.searchParams.set('from-service-worker', 'true');
    // Fetch, but pass the signal through
    event.respondWith(
        fetch(url, { signal: event.request.signal })
    );
    }
});

Sigue las especificaciones para realizar un seguimiento de esto; agregaré vínculos tickets del navegador una vez que esté listo para la implementación.

La historia

Sí... tardó mucho tiempo en combinarse esta API relativamente simple. Esto se debe a los siguientes motivos:

No coincide con la API

Como puedes ver, la conversación sobre GitHub dura bastantes. Hay mucho matiz en ese hilo (y algo de falta de matices), pero el desacuerdo clave es uno quería que el método abort existiera en el objeto que muestra fetch(), mientras que el otro quisiera una separación entre obtener la respuesta y afectarla.

Estos requisitos son incompatibles, por lo que un grupo no iba a conseguir lo que quería. Si se trata tú, ¡perdón! Si eso te hace sentir mejor, yo también estaba en ese grupo. Pero ver que AbortSignal encaja en de seguridad de otras APIs hace que parezca la elección correcta. Además, permitir que las promesas encadenadas volverse anulables se volverían muy complicados, sino imposibles.

Si quisieras mostrar un objeto que proporcione una respuesta, pero que también pueda anularse, podrías crear un wrapper simple:

function abortableFetch(request, opts) {
    const controller = new AbortController();
    const signal = controller.signal;

    return {
    abort: () => controller.abort(),
    ready: fetch(request, { ...opts, signal })
    };
}

Inicios falsos en TC39

Se realizó un esfuerzo para diferenciar una acción cancelada de un error. Esto incluyó una tercera promesa para indicar "cancelada" y una nueva sintaxis para controlar la cancelación tanto en modo síncrono como asíncrono código:

Qué no debes hacer

No es un código real; se retiró la propuesta

    try {
      // Start spinner, then:
      await someAction();
    }
    catch cancel (reason) {
      // Maybe do nothing?
    }
    catch (err) {
      // Show error message
    }
    finally {
      // Stop spinner
    }

Lo más común que se debe hacer cuando se cancela una acción es nada. La propuesta anterior separa la cancelación de los errores, de modo que no tuvieras que controlar los errores de anulación específicamente. catch cancel dejó escuchas sobre acciones canceladas, pero la mayoría de las veces no necesitarás hacerlo.

Esto llegó a la etapa 1 en las TC39, pero no se logró el consenso y se retiró la propuesta.

Nuestra propuesta alternativa, AbortController, no requería ninguna sintaxis nueva, por lo que no tenía sentido para especificarlo dentro de TC39. Todo lo que necesitábamos de JavaScript ya estaba ahí, así que definimos el interfaces de la plataforma web, específicamente el estándar DOM. Una vez que tomamos esa decisión, el resto se reunieron relativamente rápido.

Gran cambio de especificaciones

XMLHttpRequest se puede anular durante años, pero la especificación era bastante imprecisa. No estaba claro en puntos en los que la actividad de red subyacente podría evitarse o finalizar, o qué ocurrió si hubo una condición de carrera entre la llamada a abort() y la finalización de la recuperación.

Queríamos hacerlo bien esta vez, pero eso generó un gran cambio de especificaciones que requirió muchos (eso es mi culpa, y un gran agradecimiento a Anne van Kesteren y Domenic Denicola por arrastrarme) y un conjunto de pruebas decente.

¡Pero estamos aquí ahora! Tenemos una nueva primitiva web para anular las acciones asíncronas, y varias recuperaciones pueden controlarse al mismo tiempo. Más adelante, veremos cómo habilitar los cambios de prioridad durante el ciclo de vida de una recuperación y un nivel API para observar el progreso de la recuperación.