Skip to content

Instantly share code, notes, and snippets.

@jakearchibald
Last active November 28, 2020 16:19
Show Gist options
  • Save jakearchibald/785f79b0dea5bfe0c448 to your computer and use it in GitHub Desktop.
Save jakearchibald/785f79b0dea5bfe0c448 to your computer and use it in GitHub Desktop.
// Attempt to fetch data from the cache
var cachedFetch = fetchGalleryData({
useCache: true
});
// Attempt to update from the network at the same time
showSpinner();
var liveFetchResolved = false;
var liveUpdate = fetchGalleryData({
useCache: false
}).then(function(data) {
liveFetchResolved = true;
updateGallery(data);
})
// always hide the spinner after network activity
.then(hideSpinner, hideSpinner);
// React to the cached fetch:
var cachedUpdate = cachedFetch.then(function(data) {
// Don't update from cache if network won
// Using a var for this doesn't feel Promise-like
if (!liveFetchResolved) {
updateGallery(data);
}
});
// Cater for no cache or live data
// "race" feels like the wrong method name for this usage
// TODO: this doesn't do what I think it does
// A) because my use of hideSpinner means liveUpdate never fails
// B) race will reject on the first rejection, not if all reject
Promise.race([cachedUpdate, liveUpdate]).catch(showNoDataError);
// Here are loose definitions for the functions above
// you don't really need to read this unless the above is confusing
function fetchGalleryData(opts) {
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open('get', 'http://api.example.com/gallery.json');
xhr.responseType = 'json';
if (opts.useCache) {
// we'll pick this up in the ServiceWorker
xhr.setRequestHeader('x-use-cache', 'true');
}
xhr.onload = function() {
resolve(xhr.response);
};
xhr.onabort = xhr.ontimeout = xhr.onerror = reject;
});
}
var gallery = document.querySelector('.gallery');
function updateGallery(data) {
gallery.innerHTML = data.html;
}
var spinner = document.querySelector('.spinner');
function showSpinner() {
spinner.style.display = 'none';
}
function hideSpinner() {
spinner.style.display = 'none';
}
var noDataError = document.querySelector('.no-data-error');
function showNoDataError() {
noDataError.style.display = 'block';
}
@jakearchibald
Copy link
Author

The aim of the code above:

  • Show a spinner
  • Fetch cached data & fresh data at the same time
  • Update the gallery with data as it arrives, except:
  • Don't update the page with cached data if fresh data was used first
  • Fail silently if the network update fails but cached data was provided
  • Show an error if no data is shown (cache fails, network fails)
  • Hide spinner

Does it look "right"? Felt really gnarly to write.

@jakearchibald
Copy link
Author

@domenic asks how I'd code it if it were all sync:

showSpinner();

var updatedFromCache = false;

try {
  updateGallery(fetchGalleryData({
    useCache: true
  }));
  updatedFromCache = true;
} catch(e) {}

try {
  updateGallery(fetchGalleryData({
    useCache: false
  }));
} catch(e) {
  if (!updatedFromCache) {
    showNoDataError();
  }
}

hideSpinner();

@domenic
Copy link

domenic commented Nov 4, 2013

In that case:

showSpinner();

var updatedFromCache = false;

fetchGalleryData({ useCache: true })
  .then(data => {
    updateGallery(data);
    updatedFromCache = true;
  })
  .catch(e => {})
  .then(() => fetchGalleryData({ useCache: false }))
  .then(updateGallery)
  .catch(e => {
    if (!updatedFromCache) {
      showNoDataError();
    }
  })
  .then(hideSpinner);

@jakearchibald
Copy link
Author

(from IRC)

The above does the job, but it'd be faster to start both fetches at the same time, but prevent the cached response updating the page after the fresh network response.

@domenic
Copy link

domenic commented Nov 4, 2013

In that case, using finally as in domenic/promises-unwrapping#18 and a custom any function:

showSpinner();

var updatedFromFresh = false;

var cacheUpdate = fetchGalleryData({ useCache: true })
  .then(data => {
    if (!updatedFromFresh) {
      updateGallery(data);
    }
  });

var freshUpdate = fetchGalleryData({ useCache: false }))
  .then(data => {
    updateGallery(data);
    updatedFromFresh = true;
   })
   .finally(hideSpinner);

// Needs to be written
any([cacheUpdate, freshUpdate]).catch(showNoDataError);

Note that it's important to use .finally(hideSpinner) instead of .then(hideSpinner, hideSpinner), since .finally propagates the rejection, so that if the fresh update fails, freshUpdate is rejected. This is important to ensure that if both updates fail, showNoDataError is called; if we did .then(hideSpinner, hideSpinner), freshUpdate would always fulfill.

@jakearchibald
Copy link
Author

For completeness, here's the solution using today's Promises and ES5:

var updatedFromFresh = false;
showSpinner();

var cacheUpdate = fetchGalleryData({ useCache: true })
  .then(function(data) {
    if (!updatedFromFresh) {
      updateGallery(data);
    }
  });

var freshUpdate = fetchGalleryData({ useCache: false })
  .then(function(data) {
    updateGallery(data);
    updatedFromFresh = true;
  });

cacheUpdate.catch(function() {
  return freshUpdate;
}).catch(showNoDataError).then(hideSpinner);

There's a small behaviour change: hideSpinner is called when both the cache & fresh update has resolved/rejected. The previous example may hide the spinner before the cache update has happened, which is likely if the user has no connection at all.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment