/// <summary> /// This method attempts to returned a cached value, and fetch one from /// the web. Optionally, it can continue to query to see if an update is required. /// /// If the cached value exists, it is returned. Then the predicate is queried to see /// if the remote value should be refreshed. /// /// If there is no cached value, then the value is fetched. /// /// Once the above is done, as the retrySequence comes in, the predicate will /// be called to see if a refresh is needed. If so, the data will be re-fetched. /// /// In all cases any remotely fetched data is cached. /// /// This also means that await'ing this method is a Bad Idea(tm), always /// use Subscribe. 1-infinity values can be returned depending on the arguments. /// </summary> /// <param name="key">The key to store the returned result under.</param> /// <param name="fetchFunc">A sequence that will return the new values of the data</param> /// <param name="fetchPredicate">A Func to determine whether /// the updated item should be fetched. Only called once a cached version exists.</param> /// <param name="retrySequence">Sequence that will trigger a predicate call followed by /// a fetch call if the predicate indicates so.</param> /// <param name="This">The blob cache against which we operate</param> /// <returns>An Observable stream containing one or more /// results (possibly a cached version, then the latest version(s))</returns> public static IObservable <T> GetAndFetchLatest <T>(this IBlobCache This, string key, Func <IObservable <T> > fetchFunc, Func <DateTimeOffset, IObservable <bool> > fetchPredicate, IObservable <Unit> retrySequence = null, DateTimeOffset?absoluteExpiration = null ) { if (fetchPredicate == null) { throw new ArgumentException("fetchPredicate"); } if (fetchFunc == null) { throw new ArgumentException("fetchFunc"); } // We are going to get the cache value if we can. And then we will run updates after that. // If we have nothing cached, then we will run the fetch directly. Otherwise we will run the // fetch sequence. var getOldKey = This.GetObjectCreatedAt <T>(key); var refetchIfNeeded = getOldKey .Where(dt => dt != null && dt.HasValue && dt.Value != null) .SelectMany(dt => fetchPredicate(dt.Value)) .Where(doit => doit == true) .Select(_ => default(Unit)); var fetchRequired = getOldKey .Where(dt => dt == null || !dt.HasValue || dt.Value == null) .Select(_ => default(Unit)); // Next, get the item... var fetchFromCache = Observable.Defer(() => This.GetObject <T>(key)) .Catch <T, KeyNotFoundException>(_ => Observable.Empty <T>()); var fetchFromRemote = fetchRequired.Concat(refetchIfNeeded) .SelectMany(_ => fetchFunc()) .SelectMany(x => This.InsertObject <T>(key, x, absoluteExpiration).Select(_ => x)); var items = fetchFromCache.Concat(fetchFromRemote); // Once we have these, we also have to kick off a second set of fetches for our retry sequence. if (retrySequence == null) { return(items); } var getAfter = retrySequence .SelectMany(_ => This.GetObjectCreatedAt <T>(key)) .SelectMany(dt => fetchPredicate(dt.Value)) .Where(doit => doit == true) .SelectMany(_ => fetchFunc()) .SelectMany(x => This.InsertObject <T>(key, x, absoluteExpiration).Select(_ => x)); return(items.Concat(getAfter)); }
/// <summary> /// This method attempts to returned a cached value, while /// simultaneously calling a Func to return the latest value. When the /// latest data comes back, it replaces what was previously in the /// cache. /// /// This method is best suited for loading dynamic data from the /// Internet, while still showing the user earlier data. /// /// This method returns an IObservable that may return *two* results /// (first the cached data, then the latest data). Therefore, it's /// important for UI applications that in your Subscribe method, you /// write the code to merge the second result when it comes in. /// /// This also means that await'ing this method is a Bad Idea(tm), always /// use Subscribe. /// </summary> /// <param name="key">The key to store the returned result under.</param> /// <param name="fetchFunc"></param> /// <param name="fetchPredicate">An optional Func to determine whether /// the updated item should be fetched. If the cached version isn't found, /// this parameter is ignored and the item is always fetched.</param> /// <param name="absoluteExpiration">An optional expiration date.</param> /// <param name="shouldInvalidateOnError">If this is true, the cache will /// be cleared when an exception occurs in fetchFunc</param> /// <returns>An Observable stream containing either one or two /// results (possibly a cached version, then the latest version)</returns> public static IObservable <T> GetAndFetchLatest <T>(this IBlobCache This, string key, Func <IObservable <T> > fetchFunc, Func <DateTimeOffset, bool> fetchPredicate = null, DateTimeOffset?absoluteExpiration = null, bool shouldInvalidateOnError = false) { var fetch = Observable.Defer(() => This.GetObjectCreatedAt <T>(key)) .Select(x => fetchPredicate == null || x == null || fetchPredicate(x.Value)) .Where(x => x != false) .SelectMany(_ => { var fetchObs = fetchFunc().Catch <T, Exception>(ex => { var shouldInvalidate = shouldInvalidateOnError ? This.InvalidateObject <T>(key) : Observable.Return(Unit.Default); return(shouldInvalidate.SelectMany(__ => Observable.Throw <T>(ex))); }); return(fetchObs .SelectMany(x => This.InvalidateObject <T>(key).Select(__ => x)) .SelectMany(x => This.InsertObject <T>(key, x, absoluteExpiration).Select(__ => x))); }); var result = This.GetObject <T>(key).Select(x => new Tuple <T, bool>(x, true)) .Catch(Observable.Return(new Tuple <T, bool>(default(T), false))); return(result.SelectMany(x => { return x.Item2 ? Observable.Return(x.Item1) : Observable.Empty <T>(); }).Concat(fetch).Multicast(new ReplaySubject <T>()).RefCount()); }
public static IObservable <T> GetAndRequestFetch <T>(this IBlobCache This, string key, Func <IObservable <T> > fetchFunc, Func <DateTimeOffset?, IObservable <bool> > fetchPredicate, IObservable <Unit> requestSequence = null, DateTimeOffset?absoluteExpiration = null ) { if (fetchPredicate == null) { throw new ArgumentException("fetchPredicate"); } if (fetchFunc == null) { throw new ArgumentException("fetchFunc"); } // We are going to get the cache value if we can. And then we will run updates after that. // If we have nothing cached, then we will run the fetch directly. Otherwise we will run the // fetch sequence. var getOldKey = This.GetObjectCreatedAt <T>(key); // Next, get the item... var fetchFromCache = Observable.Defer(() => This.GetObject <T>(key)) .Catch <T, KeyNotFoundException>(_ => Observable.Empty <T>()); var items = fetchFromCache; // Once we have these, we also have to kick off a second set of fetches for our retry sequence. if (requestSequence == null) { return(items); } // TODO: How can we make this atomic. THe problem is that fetchPredicate may depend on the object having been // inserted, but because they are on different threads we may get race conditions. So there must be a way... var getAfter = requestSequence .SelectMany(_ => This.GetObjectCreatedAt <T>(key)) .SelectMany(dt => fetchPredicate(dt)) .Where(doit => doit == true) .SelectMany(_ => fetchFunc()) .SelectMany(x => This.InsertObject <T>(key, x, absoluteExpiration).Select(_ => x)); return(items.Concat(getAfter)); }
public static IObservable <T?> GetAndFetchLatest <T>( this IBlobCache blobCache, string key, Func <IObservable <T> > fetchFunc, Func <DateTimeOffset, bool>?fetchPredicate = null, DateTimeOffset?absoluteExpiration = null, bool shouldInvalidateOnError = false, Func <T, bool>?cacheValidationPredicate = null) { if (blobCache is null) { throw new ArgumentNullException(nameof(blobCache)); } #pragma warning disable CS8604 // Possible null reference argument. var fetch = Observable.Defer(() => blobCache.GetObjectCreatedAt <T>(key)) .Select(x => fetchPredicate is null || x is null || fetchPredicate(x.Value)) .Where(x => x) .SelectMany(_ => { var fetchObs = fetchFunc().Catch <T, Exception>(ex => { var shouldInvalidate = shouldInvalidateOnError ? blobCache.InvalidateObject <T>(key) : Observable.Return(Unit.Default); return(shouldInvalidate.SelectMany(__ => Observable.Throw <T>(ex))); }); return(fetchObs .SelectMany(x => cacheValidationPredicate is not null && !cacheValidationPredicate(x) ? Observable.Return(default(T)) : blobCache.InvalidateObject <T>(key).Select(__ => x)) .SelectMany(x => cacheValidationPredicate is not null && !cacheValidationPredicate(x) ? Observable.Return(default(T)) : blobCache.InsertObject(key, x, absoluteExpiration).Select(__ => x))); }); if (fetch is null) { return(Observable.Throw <T>(new Exception("Could not find a valid way to fetch the value"))); } var result = blobCache.GetObject <T>(key).Select(x => (x, true)) .Catch(Observable.Return((default(T), false))); #pragma warning restore CS8604 // Possible null reference argument. return(result.SelectMany(x => x.Item2 ? Observable.Return(x.Item1) : Observable.Empty <T>()) .Concat(fetch) .Multicast(new ReplaySubject <T?>()) .RefCount()); }
/// <summary> /// This method attempts to returned a cached value, while /// simultaneously calling a Func to return the latest value. When the /// latest data comes back, it replaces what was previously in the /// cache. /// /// This method is best suited for loading dynamic data from the /// Internet, while still showing the user earlier data. /// /// This method returns an IObservable that may return *two* results /// (first the cached data, then the latest data). Therefore, it's /// important for UI applications that in your Subscribe method, you /// write the code to merge the second result when it comes in. /// /// This also means that await'ing this method is a Bad Idea(tm), always /// use Subscribe. /// </summary> /// <param name="key">The key to store the returned result under.</param> /// <param name="fetchFunc"></param> /// <param name="fetchPredicate">An optional Func to determine whether /// the updated item should be fetched. If the cached version isn't found, /// this parameter is ignored and the item is always fetched.</param> /// <param name="absoluteExpiration">An optional expiration date.</param> /// <param name="shouldInvalidateOnError">If this is true, the cache will /// be cleared when an exception occurs in fetchFunc</param> /// <returns>An Observable stream containing either one or two /// results (possibly a cached version, then the latest version)</returns> public static IObservable <T> GetAndFetchLatest <T>(this IBlobCache This, string key, Func <IObservable <T> > fetchFunc, Func <DateTimeOffset, bool> fetchPredicate = null, DateTimeOffset?absoluteExpiration = null, bool shouldInvalidateOnError = false) { var fetch = Observable.Defer(() => This.GetObjectCreatedAt <T>(key)) .Select(x => fetchPredicate == null || x == null || fetchPredicate(x.Value)) .Where(x => x != false) .SelectMany(async _ => { var ret = default(T); try { ret = await fetchFunc(); } catch (Exception) { if (shouldInvalidateOnError) { This.InvalidateObject <T>(key); } throw; } await This.InvalidateObject <T>(key); await This.InsertObject(key, ret, absoluteExpiration); return(ret); }); var result = This.GetObjectAsync <T>(key).Select(x => new Tuple <T, bool>(x, true)) .Catch(Observable.Return(new Tuple <T, bool>(default(T), false))); return(result.SelectMany(x => { return x.Item2 ? Observable.Return(x.Item1) : Observable.Empty <T>(); }).Concat(fetch).Multicast(new ReplaySubject <T>()).RefCount()); }
/// <summary> /// Returns the time that this particular object was put into the cache. /// </summary> /// <param name="file"></param> /// <returns></returns> public static IObservable <DateTimeOffset?> GetCacheCreateTime(this IFile file, IBlobCache cache = null) { cache = cache ?? Blobs.LocalStorage; return(cache.GetObjectCreatedAt <string>(file.FileDateKey())); }