Line data Source code
1 : // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file 2 : // for details. All rights reserved. Use of this source code is governed by a 3 : // BSD-style license that can be found in the LICENSE file. 4 : 5 : import 'dart:async'; 6 : 7 : import 'package:async/async.dart'; 8 : 9 : /// Runs asynchronous functions and caches the result for a period of time. 10 : /// 11 : /// This class exists to cover the pattern of having potentially expensive code 12 : /// such as file I/O, network access, or isolate computation that's unlikely to 13 : /// change quickly run fewer times. For example: 14 : /// 15 : /// ```dart 16 : /// final _usersCache = new AsyncCache<List<String>>(const Duration(hours: 1)); 17 : /// 18 : /// /// Uses the cache if it exists, otherwise calls the closure: 19 : /// Future<List<String>> get onlineUsers => _usersCache.fetch(() { 20 : /// // Actually fetch online users here. 21 : /// }); 22 : /// ``` 23 : /// 24 : /// This class's timing can be mocked using [`fake_async`][fake_async]. 25 : /// 26 : /// [fake_async]: https://pub.dev/packages/fake_async 27 : class AsyncCache<T> { 28 : /// How long cached values stay fresh. 29 : final Duration _duration; 30 : 31 : /// Cached results of a previous [fetchStream] call. 32 : StreamSplitter<T>? _cachedStreamSplitter; 33 : 34 : /// Cached results of a previous [fetch] call. 35 : Future<T>? _cachedValueFuture; 36 : 37 : /// Fires when the cache should be considered stale. 38 : Timer? _stale; 39 : 40 : /// Creates a cache that invalidates its contents after [duration] has passed. 41 : /// 42 : /// The [duration] starts counting after the Future returned by [fetch] 43 : /// completes, or after the Stream returned by [fetchStream] emits a done 44 : /// event. 45 0 : AsyncCache(this._duration); 46 : 47 : /// Creates a cache that invalidates after an in-flight request is complete. 48 : /// 49 : /// An ephemeral cache guarantees that a callback function will only be 50 : /// executed at most once concurrently. This is useful for requests for which 51 : /// data is updated frequently but stale data is acceptable. 52 0 : factory AsyncCache.ephemeral() => AsyncCache(Duration.zero); 53 : 54 : /// Returns a cached value from a previous call to [fetch], or runs [callback] 55 : /// to compute a new one. 56 : /// 57 : /// If [fetch] has been called recently enough, returns its previous return 58 : /// value. Otherwise, runs [callback] and returns its new return value. 59 0 : Future<T> fetch(Future<T> Function() callback) async { 60 0 : if (_cachedStreamSplitter != null) { 61 0 : throw StateError('Previously used to cache via `fetchStream`'); 62 : } 63 0 : final result = _cachedValueFuture ??= callback(); 64 : try { 65 0 : return await result; 66 : } finally { 67 0 : _startStaleTimer(); 68 : } 69 : } 70 : 71 : /// Returns a cached stream from a previous call to [fetchStream], or runs 72 : /// [callback] to compute a new stream. 73 : /// 74 : /// If [fetchStream] has been called recently enough, returns a copy of its 75 : /// previous return value. Otherwise, runs [callback] and returns its new 76 : /// return value. 77 0 : Stream<T> fetchStream(Stream<T> Function() callback) { 78 0 : if (_cachedValueFuture != null) { 79 0 : throw StateError('Previously used to cache via `fetch`'); 80 : } 81 0 : var splitter = _cachedStreamSplitter ??= StreamSplitter( 82 0 : callback().transform(StreamTransformer.fromHandlers(handleDone: (sink) { 83 0 : _startStaleTimer(); 84 0 : sink.close(); 85 : }))); 86 0 : return splitter.split(); 87 : } 88 : 89 : /// Removes any cached value. 90 0 : void invalidate() { 91 : // TODO: This does not return a future, but probably should. 92 0 : _cachedValueFuture = null; 93 : // TODO: This does not await, but probably should. 94 0 : _cachedStreamSplitter?.close(); 95 0 : _cachedStreamSplitter = null; 96 0 : _stale?.cancel(); 97 0 : _stale = null; 98 : } 99 : 100 0 : void _startStaleTimer() { 101 0 : _stale = Timer(_duration, invalidate); 102 : } 103 : }