-
-
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'; | |
} |
@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();
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);
(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.
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.
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.
The aim of the code above:
Does it look "right"? Felt really gnarly to write.