Skip to content

Commit

Permalink
Merge pull request #501 from lyskouski/CR-498
Browse files Browse the repository at this point in the history
[#498] [CR] LocalStorage limitations
  • Loading branch information
lyskouski authored Oct 4, 2024
2 parents 68cb90e + 6357598 commit aaaa036
Show file tree
Hide file tree
Showing 10 changed files with 214 additions and 123 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"curr",
"lerp",
"LTRB",
"readwrite",
"sublist",
"unfocus",
"writeln"
Expand Down
2 changes: 1 addition & 1 deletion integration_test/stress/initialization_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ void main() {
final endTime = DateTime.now();
final duration = endTime.difference(startTime);
file.writeAsString(
"${duration.inMilliseconds} ms | ${await TransactionLog.getSize()} | ${TransactionLog.increment}\n",
"${duration.inMilliseconds} ms | ${await TransactionLog.getSize()} | ${TransactionLog.amount}\n",
mode: FileMode.append,
);
}, timeout: const Timeout(Duration(minutes: 30)));
Expand Down
132 changes: 12 additions & 120 deletions lib/_classes/storage/transaction_log.dart
Original file line number Diff line number Diff line change
@@ -1,100 +1,33 @@
// Copyright 2023 The terCAD team. All rights reserved.
// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file.

import 'dart:io';
import 'dart:convert';
import 'dart:async';
import 'dart:math';
import 'package:app_finance/_classes/controller/encryption_handler.dart';
import 'package:app_finance/_classes/storage/app_data.dart';
import 'package:app_finance/_classes/storage/app_preferences.dart';
import 'package:app_finance/_classes/storage/transaction_log/abstract_storage_web.dart'
if (dart.library.io) 'package:app_finance/_classes/storage/transaction_log/abstract_storage.dart';
import 'package:app_finance/_classes/storage/transaction_log/interface_storage.dart';
import 'package:app_finance/_ext/data_ext.dart';
import 'package:path_provider/path_provider.dart';
import 'package:flutter/foundation.dart' show kIsWeb;

class TransactionLog {
static int increment = 0;
class TransactionLog extends AbstractStorage implements InterfaceStorage {
static int amount = 0;

static bool _isLocked = false;
static Future<String> getSize() => AbstractStorage.getSize();

static File? _logFile;
static void clear() => AbstractStorage.clear();

static const filePath = '.terCAD/app-finance.log';
static Stream<String> read() => AbstractStorage.read();

static Future<File> get logFle async {
if (_logFile != null) {
return Future.value(_logFile);
}
List<File> scope = [
await getApplicationDocumentsDirectory(),
await getApplicationSupportDirectory(),
Directory.systemTemp,
await getTemporaryDirectory(),
].map((dir) => File('${dir.absolute.path}/$filePath')).toList();
File? file = scope.where((f) => f.existsSync()).firstOrNull;
int i = 0;
while (i < scope.length && file == null) {
try {
File tmp = scope[i];
if (!tmp.existsSync()) {
tmp.createSync(recursive: true);
tmp.writeAsString("\n", mode: FileMode.append);
}
file = tmp;
} catch (e) {
i++;
}
}
if (file == null) {
throw Exception('Write access denied for: $scope.');
}
return _logFile = file;
}

static String _formatBytes(int bytes) {
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
if (bytes == 0) return '0 B';
final i = (log(bytes) / log(1024)).floor();
return '${(bytes / pow(1024, i)).toStringAsFixed(2)} ${sizes[i]}';
}

static Future<String> getSize() async {
int? size;
if (kIsWeb) {
size = increment * 256;
} else {
size = (await logFle).lengthSync();
}
return _formatBytes(size);
}

static void saveRaw(String line) {
int retrial = 1;
while (_isLocked && retrial < 1000) {
sleep(Duration(microseconds: retrial * 10));
retrial++;
}
_isLocked = true;
try {
if (kIsWeb) {
AppPreferences.set('log$increment', line);
} else if (_logFile != null) {
_logFile!.writeAsStringSync("$line\n", mode: FileMode.append);
}
_isLocked = false;
} catch (e) {
_isLocked = false;
rethrow;
}
}
static void saveRaw(String line) => AbstractStorage.saveRaw(line);

static void save(dynamic content) {
String line = content.toString();
if (EncryptionHandler.doEncrypt()) {
line = EncryptionHandler.encrypt(line);
}
saveRaw(line);
increment++;
amount++;
}

static void init(AppData store, String type, Map<String, dynamic> data) {
Expand All @@ -104,54 +37,13 @@ class TransactionLog {
}
}

static Stream<String> _loadWeb() async* {
int attempts = 0;
do {
int i = increment + attempts;
var line = AppPreferences.get('log$i');
if (line == null) {
attempts++;
} else {
increment += attempts + 1;
attempts = 0;
}
yield line ?? '';
} while (attempts < 10);
}

static Stream<String> read() async* {
Stream<String> lines;
increment = 0;
if (kIsWeb) {
lines = _loadWeb();
} else {
lines = (await logFle).openRead().transform(utf8.decoder).transform(const LineSplitter());
}
await for (var line in lines) {
if (!kIsWeb) {
increment++;
}
yield line;
}
}

static clear() {
if (kIsWeb) {
while (increment > 0) {
AppPreferences.clear('log$increment');
increment--;
}
} else {
_logFile?.deleteSync();
_logFile?.createSync();
}
}

static Future<bool> load(AppData store) async {
bool isEncrypted = EncryptionHandler.doEncrypt();
bool isOK = true;
amount = 0;
await for (var line in read()) {
isOK &= add(store, line, isEncrypted);
amount++;
}
return isOK;
}
Expand Down
75 changes: 75 additions & 0 deletions lib/_classes/storage/transaction_log/abstract_storage.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright 2024 The terCAD team. All rights reserved.
// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file.

import 'dart:convert';
import 'dart:io';

import 'package:app_finance/_classes/storage/transaction_log/interface_storage.dart';
import 'package:app_finance/_ext/int_ext.dart';
import 'package:path_provider/path_provider.dart';

abstract class AbstractStorage implements InterfaceStorage {
static File? _logFile;
static bool _isLocked = false;

static const filePath = '.terCAD/app-finance.log';

static Future<File> get logFle async {
if (_logFile != null) {
return Future.value(_logFile);
}
List<File> scope = [
await getApplicationDocumentsDirectory(),
await getApplicationSupportDirectory(),
Directory.systemTemp,
await getTemporaryDirectory(),
].map((dir) => File('${dir.absolute.path}/$filePath')).toList();
File? file = scope.where((f) => f.existsSync()).firstOrNull;
int i = 0;
while (i < scope.length && file == null) {
try {
File tmp = scope[i];
if (!tmp.existsSync()) {
tmp.createSync(recursive: true);
tmp.writeAsString("\n", mode: FileMode.append);
}
file = tmp;
} catch (e) {
i++;
}
}
if (file == null) {
throw Exception('Write access denied for: $scope.');
}
return _logFile = file;
}

static Future<String> getSize() async {
int size = (await logFle).lengthSync();
return size.toByteSize();
}

static void saveRaw(String line) {
int retrial = 1;
while (_isLocked && retrial < 1000) {
sleep(Duration(microseconds: retrial * 10));
retrial++;
}
_isLocked = true;
_logFile!.writeAsStringSync("$line\n", mode: FileMode.append);
_isLocked = false;
}

static Stream<String> read() async* {
Stream<String> lines = (await logFle).openRead().transform(utf8.decoder).transform(const LineSplitter());

await for (var line in lines) {
yield line;
}
}

static void clear() {
_logFile?.deleteSync();
_logFile?.createSync();
}
}
77 changes: 77 additions & 0 deletions lib/_classes/storage/transaction_log/abstract_storage_web.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright 2024 The terCAD team. All rights reserved.
// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file.

import 'package:app_finance/_classes/storage/app_preferences.dart';
import 'package:app_finance/_classes/storage/transaction_log/interface_storage.dart';
import 'package:app_finance/_ext/int_ext.dart';
import 'package:idb_shim/idb_browser.dart';

abstract class AbstractStorage implements InterfaceStorage {
static int increment = 0;
static const String storeName = 'records';
static Database? db;

static Future<void> _initIndexedDB() async {
IdbFactory? idbFactory = getIdbFactory();
if (idbFactory == null) {
return;
}
db = await idbFactory.open('fingrom.db', version: 1, onUpgradeNeeded: (VersionChangeEvent event) {
event.database.createObjectStore(storeName, keyPath: 'id');
});
}

static Future<String> getSize() async {
int size = increment * 256;
return size.toByteSize();
}

static void saveRaw(String line) {
if (db != null) {
var store = db!.transaction(storeName, 'readwrite').objectStore(storeName);
store.put({'id': 'log$increment', 'line': line});
} else {
AppPreferences.set('log$increment', line);
}
increment++;
}

static Stream<String> readRaw(Function callback) async* {
int attempts = 0;
do {
int i = increment + attempts;
String? line = await callback(i);
if (line == null) {
attempts++;
} else {
increment += attempts + 1;
attempts = 0;
}
yield line ?? '';
} while (attempts < 10);
}

static Stream<String> read() async* {
increment = 0;
await _initIndexedDB();
await for (var line in readRaw((i) => AppPreferences.get('log$i'))) {
yield line;
}
if (db != null) {
var store = db!.transaction(storeName, 'readonly').objectStore(storeName);
await for (var line in readRaw((i) async => ((await store.getObject('log$i')) as Map?)?['line'])) {
yield line;
}
}
}

static void clear() {
while (increment > 0) {
AppPreferences.clear('log$increment');
if (db != null) {
db!.transaction(storeName, 'readwrite').objectStore(storeName).delete('log$increment');
}
increment--;
}
}
}
12 changes: 12 additions & 0 deletions lib/_classes/storage/transaction_log/interface_storage.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright 2024 The terCAD team. All rights reserved.
// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file.

abstract interface class InterfaceStorage {
static Future<String> getSize() async => Future.value('');

static void saveRaw(String line) {}

static Stream<String> read() async* {}

static void clear() {}
}
9 changes: 9 additions & 0 deletions lib/_ext/int_ext.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright 2023 The terCAD team. All rights reserved.
// Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file.

import 'dart:math';

import 'package:flutter/material.dart';

extension IntExt on int {
Expand All @@ -14,4 +16,11 @@ extension IntExt on int {
return IconData(this, fontFamily: fontFamily);
}
}

String toByteSize() {
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
if (this == 0) return '0 B';
final i = (log(this) / log(1024)).floor();
return '${(this / pow(1024, i)).toStringAsFixed(2)} ${sizes[i]}';
}
}
Loading

0 comments on commit aaaa036

Please sign in to comment.