1- import 'dart:async' ;
21import 'dart:convert' ;
32import 'dart:io' ;
43import 'dart:math' ;
54
65import 'package:collection/collection.dart' ;
6+ import 'package:file/file.dart' as pf;
77import 'package:flutter/widgets.dart' ;
88import 'package:flutter_cache_manager/src/storage/cache_info_repositories/cache_info_repository.dart' ;
99import 'package:flutter_cache_manager/src/storage/cache_info_repositories/helper_methods.dart' ;
@@ -31,6 +31,9 @@ class JsonCacheInfoRepository extends CacheInfoRepository
3131 final Map <String , CacheObject > _cacheObjects = {};
3232 final Map <int , Map <String , dynamic >> _jsonCache = {};
3333
34+ bool _dirty = false ;
35+ Future <void > _writeQueue = Future .value ();
36+
3437 @override
3538 Future <bool > open () async {
3639 if (! shouldOpenOnNewConnection ()) {
@@ -77,7 +80,7 @@ class JsonCacheInfoRepository extends CacheInfoRepository
7780 if (cacheObject.id == null ) {
7881 throw ArgumentError ('Updated objects should have an existing id.' );
7982 }
80- _put (cacheObject, setTouchedToNow);
83+ await _put (cacheObject, setTouchedToNow);
8184 return 1 ;
8285 }
8386
@@ -104,32 +107,34 @@ class JsonCacheInfoRepository extends CacheInfoRepository
104107
105108 @override
106109 Future <int > delete (int id) async {
107- final cacheObject = _cacheObjects.values.firstWhereOrNull (
108- (element) => element.id == id,
109- );
110- if (cacheObject == null ) {
110+ if (! _removeById (id)) {
111111 return 0 ;
112112 }
113- _remove (cacheObject );
113+ await _schedulePersist ( );
114114 return 1 ;
115115 }
116116
117117 @override
118118 Future <int > deleteAll (Iterable <int > ids) async {
119119 var deleted = 0 ;
120120 for (final id in ids) {
121- deleted += await delete (id);
121+ if (_removeById (id)) deleted++ ;
122+ }
123+ if (deleted > 0 ) {
124+ await _schedulePersist ();
122125 }
123126 return deleted;
124127 }
125128
126129 @override
127130 Future <bool > close () async {
128- if (! shouldClose ()) {
129- return false ;
131+ final shouldCloseRepo = shouldClose ();
132+ if (_dirty) {
133+ await _schedulePersist ();
134+ } else {
135+ await _writeQueue;
130136 }
131- await _saveFile ();
132- return true ;
137+ return shouldCloseRepo;
133138 }
134139
135140 Future <void > _readFile (File file) async {
@@ -162,33 +167,85 @@ class JsonCacheInfoRepository extends CacheInfoRepository
162167 }
163168 }
164169
165- CacheObject _put (CacheObject cacheObject, bool setTouchedToNow) {
170+ Future <CacheObject > _put (
171+ CacheObject cacheObject,
172+ bool setTouchedToNow,
173+ ) async {
166174 final map = cacheObject.toMap (setTouchedToNow: setTouchedToNow);
167175 _jsonCache[cacheObject.id! ] = map;
168176 final updatedCacheObject = CacheObject .fromMap (map);
169177 _cacheObjects[cacheObject.key] = updatedCacheObject;
170- _cacheUpdated ();
178+ await _schedulePersist ();
171179 return updatedCacheObject;
172180 }
173181
174- void _remove (CacheObject cacheObject) {
182+ bool _removeById (int id) {
183+ final cacheObject = _cacheObjects.values.firstWhereOrNull (
184+ (element) => element.id == id,
185+ );
186+ if (cacheObject == null ) {
187+ return false ;
188+ }
175189 _cacheObjects.remove (cacheObject.key);
176190 _jsonCache.remove (cacheObject.id);
177- _cacheUpdated () ;
191+ return true ;
178192 }
179193
180- void _cacheUpdated () {
181- timer? .cancel ();
182- timer = Timer (timerDuration, _saveFile);
194+ /// Queues a write of the current cache info.
195+ ///
196+ /// The returned future completes when the changes are on disk. Changes made
197+ /// while a write is in progress are written by that same write or the one
198+ /// directly after it, so a burst of changes doesn't cause a write per change.
199+ Future <void > _schedulePersist () {
200+ _dirty = true ;
201+ _writeQueue = _writeQueue.then ((_) => _flushIfDirty ());
202+ return _writeQueue;
183203 }
184204
185- Timer ? timer;
186- Duration timerDuration = const Duration (seconds: 3 );
205+ Future <void > _flushIfDirty () async {
206+ while (_dirty) {
207+ _dirty = false ;
208+ try {
209+ await _saveFile ();
210+ } on Object catch (e, stacktrace) {
211+ // Keep the changes dirty so a later change or close retries the write,
212+ // but stop here to avoid retrying a persistent failure in a loop.
213+ _dirty = true ;
214+ FlutterError .reportError (
215+ FlutterErrorDetails (
216+ exception: e,
217+ stack: stacktrace,
218+ library: 'flutter cache manager' ,
219+ context: ErrorDescription (
220+ 'Thrown when writing the file containing cache info. '
221+ 'The cache info could not be persisted and may be lost when the '
222+ 'app is closed.' ,
223+ ),
224+ ),
225+ );
226+ return ;
227+ }
228+ }
229+ }
187230
188231 Future <void > _saveFile () async {
189- timer? .cancel ();
190- timer = null ;
191- await _file! .writeAsString (jsonEncode (_jsonCache.values.toList ()));
232+ final file = await _getFile ();
233+ final content = jsonEncode (_jsonCache.values.toList ());
234+ final tempFile = _createSiblingFile ('${file .path }.tmp' );
235+ await tempFile.writeAsString (content, flush: true );
236+ await tempFile.rename (file.path);
237+ }
238+
239+ /// Creates a file next to [_file] on the same file system.
240+ ///
241+ /// [_file] can be backed by an alternative [pf.FileSystem] , in which case a
242+ /// plain [File] would resolve against the local file system instead.
243+ File _createSiblingFile (String siblingPath) {
244+ final file = _file! ;
245+ if (file is pf.File ) {
246+ return file.fileSystem.file (siblingPath);
247+ }
248+ return File (siblingPath);
192249 }
193250
194251 @override
@@ -197,6 +254,10 @@ class JsonCacheInfoRepository extends CacheInfoRepository
197254 if (await file.exists ()) {
198255 await file.delete ();
199256 }
257+ final tempFile = _createSiblingFile ('${file .path }.tmp' );
258+ if (await tempFile.exists ()) {
259+ await tempFile.delete ();
260+ }
200261 }
201262
202263 @override
@@ -206,18 +267,20 @@ class JsonCacheInfoRepository extends CacheInfoRepository
206267 }
207268
208269 Future <File > _getFile () async {
209- if (_file == null ) {
210- if (path != null ) {
211- directory = File (path! ).parent;
212- } else {
213- directory ?? = await getApplicationSupportDirectory ();
214- }
215- await directory! .create (recursive: true );
216- if (path == null || ! path! .endsWith ('.json' )) {
217- path = join (directory! .path, '$databaseName .json' );
218- }
219- _file = File (path! );
270+ if (_file != null ) {
271+ return _file! ;
272+ }
273+
274+ if (path != null ) {
275+ directory = File (path! ).parent;
276+ } else {
277+ directory ?? = await getApplicationSupportDirectory ();
278+ }
279+ await directory! .create (recursive: true );
280+ if (path == null || ! path! .endsWith ('.json' )) {
281+ path = join (directory! .path, '$databaseName .json' );
220282 }
283+ _file = File (path! );
221284 return _file! ;
222285 }
223286}
0 commit comments