]> gitweb.hamatoma.de Git - flutter_bones.git/commitdiff
daily work: PersistanceCache
authorHamatoma <author@hamatoma.de>
Fri, 30 Oct 2020 23:33:27 +0000 (00:33 +0100)
committerHamatoma <author@hamatoma.de>
Fri, 30 Oct 2020 23:33:27 +0000 (00:33 +0100)
lib/flutter_bones.dart
lib/src/persistence/persistence_cache.dart [new file with mode: 0644]
test/persistence_cache_test.dart [new file with mode: 0644]

index 86d4457086b145497354283bc9de777d52f72096..76cf4063ca7937a72a102af21cb83d964fc6f96a 100644 (file)
@@ -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 (file)
index 0000000..d3e299e
--- /dev/null
@@ -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 = <CacheEntry>[];
+  final map = <String, CacheEntry>{};
+  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:
+  /// "<id>.<module>.<sqlName>;<colText> <colValue>;<param1> <param2>..."
+  /// 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 = <String, dynamic>{};
+      params.forEach((element) {
+        final keyValue = element.split('=');
+        params2[keyValue[0]] = keyValue[1];
+      });
+      final entry = CacheEntry(key, CacheEntryType.combobox, [
+        hasUndef ? ['-'] : <String>[],
+        hasUndef ? <dynamic>[null] : <dynamic>[]
+      ]);
+      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 (file)
index 0000000..0488167
--- /dev/null
@@ -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);
+}