diff --git a/packages/secure_storage/amplify_secure_storage_dart/lib/src/utils/file_key_value_store.dart b/packages/secure_storage/amplify_secure_storage_dart/lib/src/utils/file_key_value_store.dart index 6ce470b42f..63401f569a 100644 --- a/packages/secure_storage/amplify_secure_storage_dart/lib/src/utils/file_key_value_store.dart +++ b/packages/secure_storage/amplify_secure_storage_dart/lib/src/utils/file_key_value_store.dart @@ -1,6 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import 'dart:async'; import 'dart:convert'; import 'dart:io'; @@ -18,7 +19,7 @@ import 'package:path/path.dart' as pkg_path; // without bringing in flutter as a dependency to the tests. class FileKeyValueStore { /// {@macro amplify_secure_storage_dart.file_key_value_store} - const FileKeyValueStore({ + FileKeyValueStore({ required this.path, required this.fileName, this.fs = const local_file.LocalFileSystem(), @@ -32,6 +33,8 @@ class FileKeyValueStore { /// The file will be created if it does not yet exist. final String fileName; + final TaskScheduler _scheduler = TaskScheduler(); + @visibleForTesting final pkg_file.FileSystem fs; @@ -47,9 +50,11 @@ class FileKeyValueStore { required String key, required Object value, }) async { - final data = await readAll(); - data[key] = value; - return writeAll(data); + return _scheduler.schedule(() async { + final data = await readAll(); + data[key] = value; + return writeAll(data); + }); } /// Overwrites the existing data. @@ -67,25 +72,31 @@ class FileKeyValueStore { Future readKey({ required String key, }) async { - final data = await readAll(); - return data[key]; + return _scheduler.schedule(() async { + final data = await readAll(); + return data[key]; + }); } /// Removes a single key from storage. Future removeKey({ required String key, }) async { - final data = await readAll(); - data.remove(key); - await writeAll(data); + return _scheduler.schedule(() async { + final data = await readAll(); + data.remove(key); + await writeAll(data); + }); } /// Returns true if the key exists in storage Future containsKey({ required String key, }) async { - final data = await readAll(); - return data.containsKey(key); + return _scheduler.schedule(() async { + final data = await readAll(); + return data.containsKey(key); + }); } /// Reads all the key-value pairs from storage. @@ -102,3 +113,37 @@ class FileKeyValueStore { return {}; } } + +/// A class for processing async tasks one at a time in the order that they are +/// scheduled. +class TaskScheduler { + final _taskQueue = >[]; + bool _isProcessing = false; + Future schedule(Future Function() task) { + final completer = Completer(); + _taskQueue.add(Task(task, completer)); + _processTasks(); + return completer.future; + } + + Future _processTasks() async { + if (_isProcessing) return; + _isProcessing = true; + while (_taskQueue.isNotEmpty) { + final currentTask = _taskQueue.removeAt(0); + try { + final result = await currentTask.task(); + currentTask.completer.complete(result); + } on Object catch (e, stackTrace) { + currentTask.completer.completeError(e, stackTrace); + } + } + _isProcessing = false; + } +} + +class Task { + Task(this.task, this.completer); + final Future Function() task; + final Completer completer; +} diff --git a/packages/secure_storage/amplify_secure_storage_test/test/file_key_value_store_test.dart b/packages/secure_storage/amplify_secure_storage_test/test/file_key_value_store_test.dart index 8fa16d436c..bf03deac1a 100644 --- a/packages/secure_storage/amplify_secure_storage_test/test/file_key_value_store_test.dart +++ b/packages/secure_storage/amplify_secure_storage_test/test/file_key_value_store_test.dart @@ -4,7 +4,6 @@ @TestOn('vm') import 'package:amplify_secure_storage_dart/src/utils/file_key_value_store.dart'; -import 'package:file/memory.dart'; import 'package:test/test.dart'; void main() { @@ -14,10 +13,13 @@ void main() { storage = FileKeyValueStore( path: 'path', fileName: 'file', - fs: MemoryFileSystem(), ); }); + tearDown(() async { + await storage.file.delete(); + }); + test('readKey & writeKey', () async { // assert initial state is null final value1 = await storage.readKey(key: 'key'); @@ -81,5 +83,30 @@ void main() { final includesKey2 = await storage.containsKey(key: 'key2'); expect(includesKey2, isFalse); }); + + test('parallel writes should occur in the order they are called', () async { + final items = List.generate(1000, ((i) => i)); + final futures = items.map( + (i) async => storage.writeKey(key: 'key', value: i), + ); + await Future.wait(futures); + final value = await storage.readKey(key: 'key'); + expect(value, items.last); + }); + + // Reference: https://github.com/aws-amplify/amplify-flutter/issues/5190 + test('parallel write/remove operations should not corrupt the file', + () async { + final items = List.generate(1000, ((i) => i)); + final futures = items.map( + (i) async { + if (i % 5 == 1) { + await storage.removeKey(key: 'key_${i - 1}'); + } + return storage.writeKey(key: 'key_$i', value: 'value_$i'); + }, + ); + await Future.wait(futures); + }); }); }