1

I have an app that makes a lot of asynchronous fetch calls, some of which are identical.

I have a function that supersets fetch (say fetchPlus), by creating a pseudo-unique identifier per request based on the arguments. That way, I can store the result in sessionStorage and access it.

function fetchCacheStore(hash) {
    const storeItem = 'fetch_' + hash;
    return {
        getCache: function () {
            return JSON.parse(sessionStorage.getItem(storeItem));
        },

        setCache: function (data) {
            sessionStorage.setItem(storeItem, JSON.stringify(data));
            setTimeout(function () { sessionStorage.removeItem(storeItem); }, 25); // Clear the cache item shortly after
        },
    };
}

function fetchPlus() {
    const stringHasher = function (s) { // Adapted from https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript/22429679#comment94234739_7616484
        for (var i = h = 0; i < s.length; i++) {
            h = Math.imul(31, h) + s.charCodeAt(i) | 0;
        }
        return btoa(h);
    }

    let thisCallDetails = JSON.stringify(Array.prototype.slice.call(arguments).sort());
    let fetchCallHash = stringHasher(thisCallDetails);
    let fetchCache = fetchCacheStore(fetchCallHash);
    let fetchCacheGet = fetchCache.getCache();

    let promise;

    if (fetchCacheGet === null) { // The data is not cached
        promise = fetch(...arguments); // Create the fetch call
        promise.then(data => {
            data.close.json().then(content => {
                fetchCache.setCache(content);
            });
        }); // Store the result in the cache
    } else {
        let dataHeaders = { "status": 200, "Content-Type": "application/json" };
        promise = new Response(fetchCacheGet, dataHeaders); // Programatically create a Response
    }

    return promise;
}

Everything works well aside from the fact that when the data exists in sessionStorage, I am returning a JSON object directly, and not a Response, so in my code, when I do a call like so:

fetchPlus(url, params)
    .then(response => response.json())
    .then(data => …)

I end up with an error letting me know that I can't run json() on response.

The line promise = new Response(fetchCacheGet, dataHeaders); is probably incorrect, but I am not sure how to "reverse" the data into being the data spit out from the original fetch call. Maybe I'm missing something obvious. Or maybe this is all wrong.

I'm open to suggestions but this app is already set up so removing all the .then(response => response.json()) from the codebase is not an option.

Also, I am aware my code isn't the best in class, so do forgive me. Once again, open to suggestions as long as it's constructive.

I'd love help to make this work if anyone has a few minutes to spare.

UPDATE: Functioning code

Thanks to @AuxTaxo's answer below, I've solved my issue. For anyone interested, here is the updated code:

function fetchCacheStore(hash) {
const storeItem = 'fetch_' + hash;
return {
    getCache: function () {
        return sessionStorage.getItem(storeItem);
    },

    setCache: function (data) {
        sessionStorage.setItem(storeItem, data);
        setTimeout(function () { sessionStorage.removeItem(storeItem); }, 1000); // Clear the cache item after a short while
    },
};
}

function fetchPlus() {
const stringHasher = function (s) { // Adapted from https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript/22429679#comment94234739_7616484
    for (var i = h = 0; i < s.length; i++) {
        h = Math.imul(31, h) + s.charCodeAt(i) | 0;
    }
    return btoa(h);
}

let thisCallDetails = JSON.stringify(Array.prototype.slice.call(arguments).sort());
let fetchCallHash = stringHasher(thisCallDetails);
let fetchCache = fetchCacheStore(fetchCallHash);
let fetchCacheGet = fetchCache.getCache();

let promise;

if (fetchCacheGet === null) { // The data is not cached
    promise = fetch(...arguments); // Create the fetch call
    promise.then(data => {
        data.clone().text().then(content => {
            fetchCache.setCache(content) // Store the result in the cache
        });
    });
} else {
    let dataHeaders = { "status": 200, headers: { "Content-Type": "application/json" } };
    // Programatically create a Response object, which works as a Promise
    promise = Promise.race([new Response(fetchCacheGet, dataHeaders)]);
}

return promise;
}

// Used as: `fetchPlus(url, params).then(response => response.json()).then(data => { /* … */ })`*
chriskirknielsen
  • 2,839
  • 2
  • 14
  • 24

2 Answers2

7

I assume data.close.json().then is a typo of data.clone().json().then.

new Response() expects (among other options) a string, but you're passing it an object. The response body ends up being set to "[object Object]", which .json() chokes on.

You can patch the problem by stringifying your object before passing it to the Response constructor, but a better solution would be to work with strings for as long as possible. Your Response bodies are strings, and Storage objects store strings, so store the result of response.text() in your cache instead of response.json().

Also, you're only caching the result for 25 milliseconds, so sessionStorage's advantage of keeping data across page refreshes doesn't seem useful. Just use a plain object as your cache. And dataHeaders should be { "status": 200, headers: { "Content-Type": "application/json" } }.

AuxTaco
  • 4,883
  • 1
  • 12
  • 27
  • Thanks @AuxTaco, that makes a lot more sense. I got it to work with some tweaks (had to wrap the `Response` in a `Promise`), and indeed, no need to parse JSON to stringify right after. :) 25ms is a bit short, so I bumped it up to 1000 — I make a lot of requests with the database so the results can differ in a short amount of time. Thank you for your help! – chriskirknielsen Jun 13 '19 at 14:53
1
  function fetchPlus() {
    ...

    let promise;
    if (fetchCacheGet === null) { // The data is not cached
      promise = fetch(...arguments)
        .then(data => {
          return data.json()
            .then(content => {
              // read the response and cache
              fetchCache.setCache(content);
              const init = {
                'status': 200,
                'statusText': 'SuperSmashingGreat!'
              };
              return new Response(JSON.stringify(content), init); <-- recreate the response and it.
            });
        }); // Store the result in the cache
    } else {
      let dataHeaders = {
        'status': 200,
        'statusText': 'SuperSmashingGreat!'
      };
      promise = new Response(JSON.stringify(fetchCacheGet), dataHeaders); // Programatically create a Response
    }

    return promise;
  }
macphilips
  • 527
  • 4
  • 14