From: Hamatoma Date: Fri, 30 Oct 2020 23:33:27 +0000 (+0100) Subject: daily work: PersistanceCache X-Git-Url: https://gitweb.hamatoma.de/?a=commitdiff_plain;h=9eab3d302cb77e813d7f2fd7ab450cdb0a94ea11;p=flutter_bones.git daily work: PersistanceCache --- diff --git a/lib/flutter_bones.dart b/lib/flutter_bones.dart index 86d4457..76cf406 100644 --- a/lib/flutter_bones.dart +++ b/lib/flutter_bones.dart @@ -30,6 +30,7 @@ export 'src/page/configuration/configuration_create_page.dart'; export 'src/page/user_page.dart'; export 'src/persistence/persistence.dart'; export 'src/persistence/rest_persistence.dart'; +export 'src/persistence/persistence_cache.dart'; export 'src/widget/raised_button_bone.dart'; export 'src/widget/simple_form.dart'; export 'src/widget/text_form_field_bone.dart'; diff --git a/lib/src/persistence/persistence_cache.dart b/lib/src/persistence/persistence_cache.dart new file mode 100644 index 0000000..d3e299e --- /dev/null +++ b/lib/src/persistence/persistence_cache.dart @@ -0,0 +1,119 @@ +import 'package:dart_bones/dart_bones.dart'; +import 'package:flutter_bones/flutter_bones.dart'; +import 'package:flutter_bones/src/model/combo_base_model.dart'; +import 'package:meta/meta.dart'; +import 'persistence.dart'; + +/// A cache for objects coming from a persistence layer. +/// Uses the least recently used algorithm to avoid overflow. +class PersistenceCache { + static PersistenceCache _instance; + //static final regExpCombobox = RegExp(r'^\w\.\w+\.\w+;\w+ \w+;( ?:\w+=\S*)*$'); + static final regExpCombobox = + RegExp(r'^\w+\.\w+\.\w+;\w+ \w+;( ?:\w+=\S*)*$'); + final leastReasentlyUsed = []; + final map = {}; + final maxEntries; + factory PersistenceCache() { + return _instance; + } + final ApplicationData application; + BaseLogger logger; + + PersistenceCache.internal(this.application, this.maxEntries) { + this.logger = this.application.logger; + } + + /// True constructor (of the singleton instance). + /// [application]: offers all needed data: logger, persistence... + /// [maxEntries]: the maximal entries of the cache. If a new entry exceeds + /// that number the least recently used entry is removed before. + factory PersistenceCache.create(ApplicationData application, + {int maxEntries = 256}) { + _instance = PersistenceCache.internal(application, maxEntries); + return _instance; + } + + /// Returns the data (text, values) of a combobox specified by [key]. + /// If the data are unavailable (e.g. a asynchronous request is running) the + /// result is null. + /// [key] has the format: + /// "..; ; ..." + /// e.g. "x99.role.list;role_name role_id;:role_name=% :role_active=T" + /// If [hasUndef] is true the first combobox list entry is '-' with the value null. + ComboboxData combobox(String key, {bool hasUndef = false}) { + ComboboxData rc; + if (regExpCombobox.firstMatch(key) == null) { + logger.error('wrong key syntax: $key'); + } else if (map.containsKey(key)) { + final entry = updateLRU(key); + if (entry.ready) { + rc = ComboboxData(entry.list[0], entry.list[1]); + } + } else { + final parts = key.split(';'); + final idModuleName = parts[0].split('.'); + final nameValue = parts[1].split(' '); + final params = parts[2].split(' '); + final params2 = {}; + params.forEach((element) { + final keyValue = element.split('='); + params2[keyValue[0]] = keyValue[1]; + }); + final entry = CacheEntry(key, CacheEntryType.combobox, [ + hasUndef ? ['-'] : [], + hasUndef ? [null] : [] + ]); + insert(entry); + application.persistence + .list( + module: idModuleName[1], + sqlName: idModuleName[2], + params: params2) + .then((rows) { + rows.forEach((row) { + entry.list[0].add(row[nameValue[0]]); + entry.list[1].add(row[nameValue[1]]); + }); + entry.ready = true; + rc = ComboboxData(entry.list[0], entry.list[1]); + }); + } + return rc; + } + + /// Inserts an [cacheEntry] into the cache. + void insert(CacheEntry cacheEntry) { + if (leastReasentlyUsed.length >= maxEntries) { + map.remove(leastReasentlyUsed[0].key); + leastReasentlyUsed.removeAt(0); + } + leastReasentlyUsed.add(cacheEntry); + map[cacheEntry.key] = cacheEntry; + } + + /// Moves the entry specified by [key] at the top of the Least Recently Used + /// stack. + /// Returns the entry specified by [key]. + CacheEntry updateLRU(String key) { + final entry = map[key]; + leastReasentlyUsed.remove(entry); + leastReasentlyUsed.add(entry); + return entry; + } +} + +/// Specifies an cache entry. +class CacheEntry { + final String key; + final CacheEntryType entryType; + final List list; + bool ready; + CacheEntry(this.key, this.entryType, this.list, {this.ready = false}); +} + +enum CacheEntryType { + combobox, + record, + list, +} diff --git a/test/persistence_cache_test.dart b/test/persistence_cache_test.dart new file mode 100644 index 0000000..0488167 --- /dev/null +++ b/test/persistence_cache_test.dart @@ -0,0 +1,71 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility that Flutter provides. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:dart_bones/dart_bones.dart'; +import 'package:flutter_bones/flutter_bones.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() async { + final logger = MemoryLogger(LEVEL_FINE); + PersistenceCache cache; + setUpAll(() { + final configuration = BaseConfiguration({ + 'client': { + 'host': 'localhost', + 'port': 58011, + 'schema': 'http', + 'application': 'unittest', + 'version': '1.0.0', + } + }, logger); + final persistence = RestPersistence.fromConfig(configuration, logger); + final appData = + ApplicationData(configuration, null, null, persistence, logger); + cache = PersistenceCache.create(appData, maxEntries: 2); + }); + group('basics', () { + test('combobox', () async { + final cache2 = PersistenceCache(); + expect(cache2, isNotNull); + const key = 'id1.role.list;role_name role_id;:role_name=%'; + const key2 = 'id2.role.list;role_name role_id;:role_name=%'; + var data = cache.combobox(key, hasUndef: true); + var data2 = cache.combobox(key2, hasUndef: false); + expect(data, isNull); + expect(data2, isNull); + await sleep(200); + data = cache.combobox(key); + await sleep(200); + data2 = cache.combobox(key2); + expect(logger.errors.length, equals(0)); + expect(data, isNotNull); + expect(data.texts.length, greaterThan(4)); + expect(data.texts.length, equals(data.values.length)); + expect(data2, isNotNull); + expect(data2.texts.length, greaterThan(4)); + expect(data2.texts.length, equals(data2.values.length)); + expect(data.texts.length, equals(data2.values.length + 1)); + expect(cache.leastReasentlyUsed.length, equals(2)); + const key3 = 'id3.role.list;role_name role_id;:role_name=%'; + var data3 = cache.combobox(key3, hasUndef: false); + expect(cache.leastReasentlyUsed.length, equals(2)); + expect(cache.map.containsKey(key), isFalse); + expect(cache.map.containsKey(key2), isTrue); + }); + test('error', () { + logger.clear(); + const key = 'id1.role.list;role_name+role_id;:role_name=%'; + expect(cache.combobox(key), isNull); + expect(logger.errors.length, equals(1)); + expect(logger.contains('wrong key syntax: id1.role.list;role_name+role_id;:role_name=%'), isTrue); + }); + }); +} + +Future sleep(int millisec) { + return Future.delayed(Duration(milliseconds: millisec), () => null); +}