简体   繁体   中英

Async flow control without RxJS

I'm working on a mobile app and I want it to be offline-first. Let's say I want to show the "Company" page so I request the company datas.

Here's what I do, I look for the company data in the local storage (indexedDB in my case) and at the same time I call the server for the same data (to get the potential updates). In each callback I update the view with the datas fetched. First updated from local datas then from remote datas.

To avoid a race condition I have a boolean called "remoteDataAlreadyFetched" that I check in my local storage callback (in case remote datas arrive before the storage response)

My question is: how should I handle this ? 2 separate promises in my view controller (one for local, one for remote) ? Should I use an observable ? This looks overkill since there won't be more than 2 responses (local and remote). Am I missing something ?

Thanks a lot for your help

EDIT:

This is how I would do it with RxJS. Don't know if it's a bad practice to use Observables in this situation...

public static getOfflineFirstObservable(localCall, remoteCall): Observable<any> {
  const data$ = new BehaviorSubject(null);
  var hasFetchedRemoteData = false;

  localCall().then(localDatas => {
    if (!hasFetchedRemoteData) data$.next(localDatas);
  });

  remoteCall().then(remoteDatas => {
    data$.next(remoteDatas);
    hasFetchedRemoteData = true;
  });

  return data$.asObservable();
}

Very interesting problem, so you are trying to make sure about the order of your two async operations.

I implement something that realize this, check my code below or just run them ;)

// two promise with no guaranteed order:
var localPromise = new Promise(resolve => setTimeout(resolve, Math.random() * 1000))
var remotePromise = new Promise(resolve => setTimeout(resolve, Math.random() * 1000))

// wrap them
var localFirst = Promise.resolve(localPromise)
    // can also add your handler for localdata
    .then(() => Promise.resolve(remotePromise))
var remoteFirst = Promise.resolve(remotePromise)

// viola
Promise.race([localFirst, remoteFirst]).then(console.log)

If you're offline then trying to get remote data will result in rejected promises (with promise.race). If all you're trying to do is get item from cache first (if it's there) and if it's not try to get it remote you can do the following:

const cacheBuilder = promises => fetcher => setFn => getFn => url => {
  //using url as key but can be url+JSON.stringify(parameters)+method(GET/POST)
  const key = url;
  //get from cache first if exist
  return getFn(key).catch(()=>{
    if(promises[key]){
      return promises[key];//return active promise
    }
    promises[key]=fetcher(url);
    return promises[key];

  })
  .then(
    result=>{
      if(!promises[key]){//update cache, this will cause requests to server
        fetcher(url).then(result=>setFn(key,result)).catch(ignore=>ignore);
      }
      promises[key]=undefined;
      setFn(key,result);
      return result;
    }
  );
}

const cacheFirst = cacheBuilder(
  {}//store active promises here
)(
  //fetch function (can be $.get or something else)
  //  I am only using url here but you could use (url,params,method,headers) as well
  url=>
    //remove "console.log ||" it's to show that multiple active fetches share promises
    //  asking for fetch("/") multiple times while first time is not resolved
    //  will not cause multiple requests
    console.log("fetching:",url) ||
    fetch(url)
    .then(response=>response.text())
    .then(result=>result.substr(0,10))
)(
  //how to set an item in local storage
  (key,value)=>{
    newStorage = JSON.parse(localStorage.getItem("netCache")||"{}");
    newStorage[key]=value;
    localStorage.setItem("netCache",JSON.stringify(newStorage));
  }
)(
  //how to get an item based on key (can be url or url + JSON.stringify(parameters) or url+params+method...)
  key=>
    Promise.resolve(
      JSON.parse(localStorage.getItem("netCache")||"{}")[key] ||
      Promise.reject("Not in cache")
    )
);

Promise.all([//should not cause multiple requests, should have only one request made
  cacheFirst("/"),
  cacheFirst("/"),
  cacheFirst("/"),
  cacheFirst("/")
]).then(
  ()=>cacheFirst("/")//should come from cache as well, no request made
)

Here is an example where all implementation is in one function without passing fetch, getter and setter:

const cacheFirst = (promises => url => {
  //using url as key but can be url+JSON.stringify(parameters)+method(GET/POST)
  const key = url;
  const fetcher = url=>
    fetch(url)
    .then(response=>response.text())
    .then(result=>result.substr(0,10));
  const setFn = (key,value)=>{
    newStorage = JSON.parse(localStorage.getItem("netCache")||"{}");
    newStorage[key]=value;
    localStorage.setItem("netCache",JSON.stringify(newStorage));
  }
  const getFn = key=>
    Promise.resolve(
      JSON.parse(localStorage.getItem("netCache")||"{}")[key] ||
      Promise.reject("Not in cache")
    );
  //get from cache first if exist
  return getFn(key).catch(()=>{
    if(promises[key]){
      return promises[key];//return active promise
    }
    promises[key]=fetcher(url);
    return promises[key];
  })
  .then(
    result=>{
      //update cache if result didn't came from request, this will cause requests to server
      if(!promises[key]){
        fetcher(url)
        .then(result=>setFn(key,result))
        .catch(ignore=>ignore);
      }
      promises[key]=undefined;
      setFn(key,result);
      return result;
    }
  );
})({})//IIFE passing in the promises object to store active promises

I've finally decided to create 2 separate promises. I get the local data (if exists) on page load and immediately try to fetch the remote data. So the user can see a content right away but still see a spinner on the top right indicating that's remote data have been requested. When the request comes back the view is updated.

I've decided to do that because the page also has a pull to refresh so it seemed smarter to separate the 2 calls (local and remote) to be able to call the server as many time as I wanted.

Thanks for your answers. @HMR Your solution is really interesting but didn't really fit my needs. Thanks a lot

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM