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.dartlang.org/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 after an in-flight request is complete.
41 : ///
42 : /// An ephemeral cache guarantees that a callback function will only be
43 : /// executed at most once concurrently. This is useful for requests for which
44 : /// data is updated frequently but stale data is acceptable.
45 0 : factory AsyncCache.ephemeral() => new AsyncCache(Duration.ZERO);
46 :
47 : /// Creates a cache that invalidates its contents after [duration] has passed.
48 : ///
49 : /// The [duration] starts counting after the Future returned by [fetch]
50 : /// completes, or after the Stream returned by [fetchStream] emits a done
51 : /// event.
52 0 : AsyncCache(this._duration);
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 : Future<T> fetch(Future<T> callback()) async {
60 0 : if (_cachedStreamSplitter != null) {
61 0 : throw new StateError('Previously used to cache via `fetchStream`');
62 : }
63 0 : if (_cachedValueFuture == null) {
64 0 : _cachedValueFuture = callback();
65 0 : await _cachedValueFuture;
66 0 : _startStaleTimer();
67 : }
68 0 : return _cachedValueFuture;
69 0 : }
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 : Stream<T> fetchStream(Stream<T> callback()) {
78 0 : if (_cachedValueFuture != null) {
79 0 : throw new StateError('Previously used to cache via `fetch`');
80 : }
81 0 : if (_cachedStreamSplitter == null) {
82 0 : _cachedStreamSplitter = new StreamSplitter(callback()
83 0 : .transform(new StreamTransformer.fromHandlers(handleDone: (sink) {
84 0 : _startStaleTimer();
85 0 : sink.close();
86 : })));
87 : }
88 0 : return _cachedStreamSplitter.split();
89 : }
90 :
91 : /// Removes any cached value.
92 : void invalidate() {
93 0 : _cachedValueFuture = null;
94 0 : _cachedStreamSplitter?.close();
95 0 : _cachedStreamSplitter = null;
96 0 : _stale?.cancel();
97 0 : _stale = null;
98 : }
99 :
100 : void _startStaleTimer() {
101 0 : _stale = new Timer(_duration, invalidate);
102 : }
103 : }
|