From c2e2a66550e2aef290b4c356fb52ea9d60ff7864 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sun, 31 Dec 2023 22:12:15 +0100 Subject: [PATCH] Removed synchronous operations to improve simplicity and DX Attach most of backend management to frontend Fixed bug in `removeOldestTile` logic Removed `TileCannotUpdate` error as it is a library error not a user error Improved documentation and make use of templates and macros to de-duplicate doc-strings Minor internal improvements elsewhere --- lib/flutter_map_tile_caching.dart | 6 - lib/src/backend/exports.dart | 1 - lib/src/backend/impls/objectbox/backend.dart | 48 ++-- lib/src/backend/impls/objectbox/worker.dart | 4 +- lib/src/backend/interfaces/backend.dart | 232 ++++--------------- lib/src/backend/interfaces/no_sync.dart | 181 --------------- lib/src/backend/utils/errors.dart | 28 --- lib/src/bulk_download/instance.dart | 4 +- lib/src/bulk_download/manager.dart | 18 +- lib/src/db/defs/metadata.dart | 23 -- lib/src/db/defs/recovery.dart | 60 ----- lib/src/db/defs/store_descriptor.dart | 19 -- lib/src/db/defs/tile.dart | 26 --- lib/src/db/registry.dart | 207 ----------------- lib/src/db/tools.dart | 54 ----- lib/src/fmtc.dart | 2 + lib/src/store/directory.dart | 18 +- lib/src/store/manage.dart | 49 +--- lib/src/store/statistics.dart | 47 +--- 19 files changed, 93 insertions(+), 934 deletions(-) delete mode 100644 lib/src/backend/interfaces/no_sync.dart delete mode 100644 lib/src/db/defs/metadata.dart delete mode 100644 lib/src/db/defs/recovery.dart delete mode 100644 lib/src/db/defs/store_descriptor.dart delete mode 100644 lib/src/db/defs/tile.dart delete mode 100644 lib/src/db/registry.dart delete mode 100644 lib/src/db/tools.dart diff --git a/lib/flutter_map_tile_caching.dart b/lib/flutter_map_tile_caching.dart index 94d7fe2b..f676e3fe 100644 --- a/lib/flutter_map_tile_caching.dart +++ b/lib/flutter_map_tile_caching.dart @@ -37,12 +37,6 @@ import 'src/backend/exports.dart'; import 'src/bulk_download/instance.dart'; import 'src/bulk_download/rate_limited_stream.dart'; import 'src/bulk_download/tile_loops/shared.dart'; -import 'src/db/defs/metadata.dart'; -import 'src/db/defs/recovery.dart'; -import 'src/db/defs/store_descriptor.dart'; -import 'src/db/defs/tile.dart'; -import 'src/db/registry.dart'; -import 'src/db/tools.dart'; import 'src/errors/browsing.dart'; import 'src/errors/initialisation.dart'; import 'src/misc/exts.dart'; diff --git a/lib/src/backend/exports.dart b/lib/src/backend/exports.dart index f5f38c1a..26cfaf49 100644 --- a/lib/src/backend/exports.dart +++ b/lib/src/backend/exports.dart @@ -1,5 +1,4 @@ export 'impls/objectbox/backend.dart'; export 'interfaces/backend.dart'; export 'interfaces/models.dart'; -export 'interfaces/no_sync.dart'; export 'utils/errors.dart'; diff --git a/lib/src/backend/impls/objectbox/backend.dart b/lib/src/backend/impls/objectbox/backend.dart index f68500e4..4619592b 100644 --- a/lib/src/backend/impls/objectbox/backend.dart +++ b/lib/src/backend/impls/objectbox/backend.dart @@ -9,7 +9,6 @@ import 'package:path_provider/path_provider.dart'; import '../../../misc/exts.dart'; import '../../interfaces/backend.dart'; -import '../../interfaces/no_sync.dart'; import '../../utils/errors.dart'; import 'models/generated/objectbox.g.dart'; import 'models/models.dart'; @@ -30,9 +29,7 @@ abstract interface class ObjectBoxBackendInternal static final _instance = _ObjectBoxBackendImpl._(); } -class _ObjectBoxBackendImpl - with FMTCBackendNoSync - implements ObjectBoxBackendInternal { +class _ObjectBoxBackendImpl implements ObjectBoxBackendInternal { _ObjectBoxBackendImpl._(); void get expectInitialised => _sendPort ?? (throw RootUnavailable()); @@ -73,7 +70,7 @@ class _ObjectBoxBackendImpl @override String get friendlyIdentifier => 'ObjectBox'; - /// {@macro fmtc_backend_initialise} + /// {@macro fmtc.backend.initialise} /// /// This implementation additionally accepts the following [implSpecificArgs]: /// @@ -286,21 +283,11 @@ class _ObjectBoxBackendImpl args: {'storeName': storeName, 'url': url}, ))!['wasOrphaned']; - void _sendRemoveOldestTileCmd(String storeName) { - _sendCmd( - type: _WorkerCmdType.removeOldestTile, - args: { - 'storeName': storeName, - 'number': _dotLength, - }, - ); - _dotLength = 0; - } - @override - void removeOldestTileSync({ + Future removeOldestTile({ required String storeName, - }) { + required int numToRemove, + }) async { // Attempts to avoid flooding worker with requests to delete oldest tile, // and 'batches' them instead @@ -310,7 +297,8 @@ class _ObjectBoxBackendImpl _dotStore = storeName; if (_dotDebouncer.isActive) { _dotDebouncer.cancel(); - _sendRemoveOldestTileCmd(storeName); + _sendROTCmd(storeName); + _dotLength += numToRemove; } } @@ -318,20 +306,22 @@ class _ObjectBoxBackendImpl _dotDebouncer.cancel(); _dotDebouncer = Timer( const Duration(milliseconds: 500), - () => _sendRemoveOldestTileCmd(storeName), + () => _sendROTCmd(storeName), ); + _dotLength += numToRemove; return; } - _dotDebouncer = Timer( - const Duration(seconds: 1), - () => _sendRemoveOldestTileCmd(storeName), - ); + _dotDebouncer = + Timer(const Duration(seconds: 1), () => _sendROTCmd(storeName)); + _dotLength += numToRemove; } - @override - Future removeOldestTile({ - required String storeName, - }) async => - removeOldestTileSync(storeName: storeName); + void _sendROTCmd(String storeName) { + _sendCmd( + type: _WorkerCmdType.removeOldestTile, + args: {'storeName': storeName, 'number': _dotLength}, + ); + _dotLength = 0; + } } diff --git a/lib/src/backend/impls/objectbox/worker.dart b/lib/src/backend/impls/objectbox/worker.dart index ff928bed..5aee80c9 100644 --- a/lib/src/backend/impls/objectbox/worker.dart +++ b/lib/src/backend/impls/objectbox/worker.dart @@ -328,7 +328,9 @@ Future _worker( ); break; case (true, true): // FMTC internal error - throw TileCannotUpdate(url: url); + throw StateError( + 'FMTC ObjectBox backend internal state error: $url', + ); } }, ); diff --git a/lib/src/backend/interfaces/backend.dart b/lib/src/backend/interfaces/backend.dart index c23d0798..94ca7107 100644 --- a/lib/src/backend/interfaces/backend.dart +++ b/lib/src/backend/interfaces/backend.dart @@ -23,7 +23,6 @@ import '../../../flutter_map_tile_caching.dart'; /// /// To end-users: /// * Use [FMTCSettings.backend] to set a custom backend -/// * Not all sync versions of methods are guaranteed to have implementations /// * Avoid calling the [internal] method of a backend abstract interface class FMTCBackend { const FMTCBackend(); @@ -39,56 +38,30 @@ abstract interface class FMTCBackend { abstract interface class FMTCBackendInternal { abstract final String friendlyIdentifier; - /// {@template fmtc_backend_initialise} + /// {@template fmtc.backend.initialise} /// Initialise this backend & create the root /// /// [rootDirectory] defaults to '[getApplicationDocumentsDirectory]/fmtc'. /// /// [maxDatabaseSize] defaults to 1 GB shared across all stores. Specify the /// amount in KB. - /// {@endtemplate} /// /// Some implementations may accept/require additional arguments that may /// be set through [implSpecificArgs]. See their documentation for more /// information. + /// {@endtemplate} /// /// --- /// /// Note to implementers: if you accept implementation specific arguments, - /// override the documentation on this method, and use the - /// 'fmtc_backend_initialise' macro at the top to retain the standard docs. + /// ensure you properly document these. Future initialise({ String? rootDirectory, int? maxDatabaseSize, Map implSpecificArgs = const {}, }); - /// {@macro fmtc_backend_initialise} - /// - /// --- - /// - /// Note to implementers: if you accept implementation specific arguments, - /// override the documentation on this method, and use the - /// 'fmtc_backend_initialise' macro at the top to retain the standard docs. - void initialiseSync({ - String? rootDirectory, - int? maxDatabaseSize, - Map implSpecificArgs = const {}, - }); - - /// Whether [initialiseSync] is implemented - /// - /// If `false`, calling will throw an [SyncOperationUnsupported] error. - abstract final bool supportsSyncInitialise; - - /// Uninitialise this backend, and release whatever resources it is consuming - /// - /// If [deleteRoot] is `true`, then the storage medium will be permanently - /// deleted. - Future destroy({ - bool deleteRoot = false, - }); - + /// {@template fmtc.backend.destroy} /// Uninitialise this backend, and release whatever resources it is consuming /// /// If [deleteRoot] is `true`, then the storage medium will be permanently @@ -99,178 +72,90 @@ abstract interface class FMTCBackendInternal { /// but any operations started after this method call will be lost. A lost /// operation may throw [RootUnavailable]. This parameter may not have a /// noticable/any effect in some implementations. - void destroySync({ - bool deleteRoot = false, - bool immediate = false, + /// {@endtemplate} + Future destroy({ + required bool deleteRoot, + required bool immediate, }); - /// Whether [destroySync] is implemented - /// - /// If `false`, calling will throw an [SyncOperationUnsupported] error. - abstract final bool supportsSyncDestroy; - - /// Whether the store currently exists + /// {@template fmtc.backend.storeExists} + /// Check whether the specified store currently exists + /// {@endtemplate} Future storeExists({ required String storeName, }); - /// Whether the store currently exists - bool storeExistsSync({ - required String storeName, - }); - - /// Whether [storeExistsSync] is implemented - /// - /// If `false`, calling will throw an [SyncOperationUnsupported] error. - abstract final bool supportsSyncStoreExists; - + /// {@template fmtc.backend.createStore} /// Create a new store with the specified name + /// {@endtemplate} Future createStore({ required String storeName, }); - /// Create a new store with the specified name - void createStoreSync({ - required String storeName, - }); - - /// Whether [createStoreSync] is implemented + /// {@template fmtc.backend.deleteStore} + /// Delete the specified store /// - /// If `false`, calling will throw an [SyncOperationUnsupported] error. - abstract final bool supportsSyncCreateStore; - - /// Remove all the tiles from within the specified store - Future resetStore({ + /// This operation cannot be undone! Ensure you confirm with the user that + /// this action is expected. + /// {@endtemplate} + Future deleteStore({ required String storeName, }); + /// {@template fmtc.backend.resetStore} /// Remove all the tiles from within the specified store - void resetStoreSync({ + /// + /// This operation cannot be undone! Ensure you confirm with the user that + /// this action is expected. + /// {@endtemplate} + Future resetStore({ required String storeName, }); - /// Whether [resetStoreSync] is implemented - /// - /// If `false`, calling will throw an [SyncOperationUnsupported] error. - abstract final bool supportsSyncResetStore; - /// Change the name of the store named [currentStoreName] to [newStoreName] Future renameStore({ required String currentStoreName, required String newStoreName, }); - /// Change the name of the store named [currentStoreName] to [newStoreName] - void renameStoreSync({ - required String currentStoreName, - required String newStoreName, - }); - - /// Whether [renameStoreSync] is implemented - /// - /// If `false`, calling will throw an [SyncOperationUnsupported] error. - abstract final bool supportsSyncRenameStore; - - /// Delete the specified store - Future deleteStore({ - required String storeName, - }); - - /// Delete the specified store - void deleteStoreSync({ - required String storeName, - }); - - /// Whether [deleteStoreSync] is implemented - /// - /// If `false`, calling will throw an [SyncOperationUnsupported] error. - abstract final bool supportsSyncDeleteStore; - + /// {@template fmtc.backend.getStoreSize} /// Retrieve the total size (in kibibytes KiB) of the image bytes of all the /// tiles that belong to the specified store /// - /// This does not return any other data that adds to the 'real' store size + /// This does not return any other data that adds to the 'real' store size. + /// {@endtemplate} Future getStoreSize({ required String storeName, }); - /// Retrieve the total size (in kibibytes KiB) of the image bytes of all the - /// tiles that belong to the specified store - /// - /// This does not return any other data that adds to the 'real' store size - double getStoreSizeSync({ - required String storeName, - }); - - /// Whether [getStoreSizeSync] is implemented - /// - /// If `false`, calling will throw an [SyncOperationUnsupported] error. - abstract final bool supportsSyncGetStoreSize; - + /// {@template fmtc.backend.getStoreLength} /// Retrieve the number of tiles that belong to the specified store + /// {@endtemplate} Future getStoreLength({ required String storeName, }); - /// Retrieve the number of tiles that belong to the specified store - int getStoreLengthSync({ - required String storeName, - }); - - /// Whether [getStoreLengthSync] is implemented - /// - /// If `false`, calling will throw an [SyncOperationUnsupported] error. - abstract final bool supportsSyncGetStoreLength; - + /// {@template fmtc.backend.getStoreHits} /// Retrieve the number of times that a tile was successfully retrieved from /// the specified store when browsing + /// {@endtemplate} Future getStoreHits({ required String storeName, }); - /// Retrieve the number of times that a tile was successfully retrieved from - /// the specified store when browsing - int getStoreHitsSync({ - required String storeName, - }); - - /// Whether [getStoreHitsSync] is implemented - /// - /// If `false`, calling will throw an [SyncOperationUnsupported] error. - abstract final bool supportsSyncGetStoreHits; - + /// {@template fmtc.backend.getStoreMisses} /// Retrieve the number of times that a tile was attempted to be retrieved from /// the specified store when browsing, but was not present + /// {@endtemplate} Future getStoreMisses({ required String storeName, }); - /// Retrieve the number of times that a tile was attempted to be retrieved from - /// the specified store when browsing, but was not present - int getStoreMissesSync({ - required String storeName, - }); - - /// Whether [getStoreMissesSync] is implemented - /// - /// If `false`, calling will throw an [SyncOperationUnsupported] error. - abstract final bool supportsSyncGetStoreMisses; - /// Get a raw tile by URL Future readTile({ required String url, }); - /// Get a raw tile by URL - BackendTile? readTileSync({ - required String url, - }); - - /// Whether [readTileSync] is implemented - /// - /// If `false`, calling will throw an [SyncOperationUnsupported] error. - abstract final bool supportsSyncReadTile; - /// Create or update a tile /// /// If the tile already existed, it will be added to the specified store. @@ -285,25 +170,6 @@ abstract interface class FMTCBackendInternal { required Uint8List? bytes, }); - /// Create or update a tile - /// - /// If the tile already existed, it will be added to the specified store. - /// Otherwise, [bytes] must be specified, and the tile will be created and - /// added. - /// - /// If [bytes] is provided and the tile already existed, it will be updated for - /// all stores. - void writeTileSync({ - required String storeName, - required String url, - required Uint8List? bytes, - }); - - /// Whether [writeTileSync] is implemented - /// - /// If `false`, calling will throw an [SyncOperationUnsupported] error. - abstract final bool supportsSyncWriteTile; - /// Remove the tile from the store, deleting it if orphaned /// /// Returns: @@ -315,32 +181,12 @@ abstract interface class FMTCBackendInternal { required String url, }); - /// Remove the tile from the store, deleting it if orphaned - /// - /// Returns: - /// * `null` : if there was no existing tile - /// * `true` : if the tile itself could be deleted (it was orphaned) - /// * `false`: if the tile still belonged to at least store - bool? deleteTileSync({ - required String storeName, - required String url, - }); - - /// Whether [deleteTileSync] is implemented - /// - /// If `false`, calling will throw an [SyncOperationUnsupported] error. - abstract final bool supportsSyncDeleteTile; - + /// {@template fmtc.backend.removeOldestTile} + /// Remove the specified number of tiles from the specified store, in the order + /// of oldest first, and where each tile does not belong to any other store + /// {@endtemplate} Future removeOldestTile({ required String storeName, + required int numToRemove, }); - - void removeOldestTileSync({ - required String storeName, - }); - - /// Whether [removeOldestTileSync] is implemented - /// - /// If `false`, calling will throw an [SyncOperationUnsupported] error. - abstract final bool supportsSyncRemoveOldestTile; } diff --git a/lib/src/backend/interfaces/no_sync.dart b/lib/src/backend/interfaces/no_sync.dart deleted file mode 100644 index ab9a6a42..00000000 --- a/lib/src/backend/interfaces/no_sync.dart +++ /dev/null @@ -1,181 +0,0 @@ -import 'dart:typed_data'; - -import '../utils/errors.dart'; -import 'backend.dart'; -import 'models.dart'; - -/// A shortcut to declare that an [FMTCBackend] does not support any synchronous -/// versions of methods -mixin FMTCBackendNoSync implements FMTCBackendInternal { - /// This synchronous method is unsupported by this implementation - use - /// [initialise] instead - @override - void initialiseSync({ - String? rootDirectory, - int? maxDatabaseSize, - Map implSpecificArgs = const {}, - }) => - throw SyncOperationUnsupported(); - - @override - final supportsSyncInitialise = false; - - /// This synchronous method is unsupported by this implementation - use - /// [destroy] instead - @override - void destroySync({ - bool deleteRoot = false, - bool immediate = false, - }) => - throw SyncOperationUnsupported(); - - @override - final supportsSyncDestroy = false; - - /// This synchronous method is unsupported by this implementation - use - /// [storeExists] instead - @override - bool storeExistsSync({ - required String storeName, - }) => - throw SyncOperationUnsupported(); - - @override - final supportsSyncStoreExists = false; - - /// This synchronous method is unsupported by this implementation - use - /// [createStore] instead - @override - void createStoreSync({ - required String storeName, - }) => - throw SyncOperationUnsupported(); - - @override - final supportsSyncCreateStore = false; - - /// This synchronous method is unsupported by this implementation - use - /// [resetStore] instead - @override - void resetStoreSync({ - required String storeName, - }) => - throw SyncOperationUnsupported(); - - @override - final supportsSyncResetStore = false; - - /// This synchronous method is unsupported by this implementation - use - /// [renameStore] instead - @override - void renameStoreSync({ - required String currentStoreName, - required String newStoreName, - }) => - throw SyncOperationUnsupported(); - - @override - final supportsSyncRenameStore = false; - - /// This synchronous method is unsupported by this implementation - use - /// [deleteStore] instead - @override - void deleteStoreSync({ - required String storeName, - }) => - throw SyncOperationUnsupported(); - - @override - final supportsSyncDeleteStore = false; - - /// This synchronous method is unsupported by this implementation - use - /// [getStoreSize] instead - @override - double getStoreSizeSync({ - required String storeName, - }) => - throw SyncOperationUnsupported(); - - @override - final supportsSyncGetStoreSize = false; - - /// This synchronous method is unsupported by this implementation - use - /// [getStoreLength] instead - @override - int getStoreLengthSync({ - required String storeName, - }) => - throw SyncOperationUnsupported(); - - @override - final supportsSyncGetStoreLength = false; - - /// This synchronous method is unsupported by this implementation - use - /// [getStoreHits] instead - @override - int getStoreHitsSync({ - required String storeName, - }) => - throw SyncOperationUnsupported(); - - @override - final supportsSyncGetStoreHits = false; - - /// This synchronous method is unsupported by this implementation - use - /// [getStoreMisses] instead - @override - int getStoreMissesSync({ - required String storeName, - }) => - throw SyncOperationUnsupported(); - - @override - final supportsSyncGetStoreMisses = false; - - /// This synchronous method is unsupported by this implementation - use - /// [readTile] instead - @override - BackendTile? readTileSync({ - required String url, - }) => - throw SyncOperationUnsupported(); - - @override - final supportsSyncReadTile = false; - - /// This synchronous method is unsupported by this implementation - use - /// [writeTile] instead - @override - void writeTileSync({ - required String storeName, - required String url, - required Uint8List? bytes, - }) => - throw SyncOperationUnsupported(); - - @override - final supportsSyncWriteTile = false; - - /// This synchronous method is unsupported by this implementation - use - /// [deleteTile] instead - @override - bool? deleteTileSync({ - required String storeName, - required String url, - }) => - throw SyncOperationUnsupported(); - - @override - final supportsSyncDeleteTile = false; - - /// This synchronous method is unsupported by this implementation - use - /// [removeOldestTile] instead - @override - void removeOldestTileSync({ - required String storeName, - }) => - throw SyncOperationUnsupported(); - - @override - final supportsSyncRemoveOldestTile = false; -} diff --git a/lib/src/backend/utils/errors.dart b/lib/src/backend/utils/errors.dart index e9be79f2..620620b9 100644 --- a/lib/src/backend/utils/errors.dart +++ b/lib/src/backend/utils/errors.dart @@ -47,31 +47,3 @@ final class StoreAlreadyExists extends FMTCBackendError { String toString() => 'StoreAlreadyExists: The requested store "$storeName" already existed'; } - -/// Indicates that the specified tile could not be updated because it did not -/// already exist -/// -/// If you have this error in your application, please file a bug report. -final class TileCannotUpdate extends FMTCBackendError { - final String url; - - TileCannotUpdate({required this.url}); - - @override - String toString() => - 'TileCannotUpdate: The requested tile ("$url") did not exist, and so cannot be updated'; -} - -/// Indicates that the backend implementation does not support the invoked -/// synchronous operation -/// -/// Use the asynchronous version instead. -/// -/// Note that there is no equivalent error for async operations: if there is no -/// specific async version of an operation, it should redirect to the sync -/// version. -final class SyncOperationUnsupported extends FMTCBackendError { - @override - String toString() => - 'SyncOperationUnsupported: The backend implementation does not support the invoked synchronous operation.'; -} diff --git a/lib/src/bulk_download/instance.dart b/lib/src/bulk_download/instance.dart index b97f9877..ea8111af 100644 --- a/lib/src/bulk_download/instance.dart +++ b/lib/src/bulk_download/instance.dart @@ -9,9 +9,7 @@ class DownloadInstance { static final _instances = {}; static DownloadInstance? registerIfAvailable(Object id) => - _instances.containsKey(id) - ? null - : _instances[id] ??= DownloadInstance._(id); + _instances[id] != null ? null : _instances[id] = DownloadInstance._(id); static bool unregister(Object id) => _instances.remove(id) != null; static DownloadInstance? get(Object id) => _instances[id]; diff --git a/lib/src/bulk_download/manager.dart b/lib/src/bulk_download/manager.dart index 77c8efc1..dde188c8 100644 --- a/lib/src/bulk_download/manager.dart +++ b/lib/src/bulk_download/manager.dart @@ -119,7 +119,7 @@ Future _downloadManager( } // Setup two-way communications with root - final rootreceivePort = ReceivePort(); + final rootReceivePort = ReceivePort(); void send(Object? m) => input.sendPort.send(m); // Setup cancel, pause, and resume handling @@ -131,7 +131,7 @@ Future _downloadManager( final threadPausedStates = generateThreadPausedStates(); final cancelSignal = Completer(); var pauseResumeSignal = Completer()..complete(); - rootreceivePort.listen( + rootReceivePort.listen( (e) async { if (e == null) { try { @@ -172,7 +172,7 @@ Future _downloadManager( ); // Now it's safe, start accepting communications from the root - send(rootreceivePort.sendPort); + send(rootReceivePort.sendPort); // Start download threads & wait for download to complete/cancelled downloadDuration.start(); @@ -183,11 +183,11 @@ Future _downloadManager( if (cancelSignal.isCompleted) return; // Start thread worker isolate & setup two-way communications - final downloadThreadreceivePort = ReceivePort(); + final downloadThreadReceivePort = ReceivePort(); await Isolate.spawn( _singleDownloadThread, ( - sendPort: downloadThreadreceivePort.sendPort, + sendPort: downloadThreadReceivePort.sendPort, storeId: storeId, rootDirectory: input.rootDirectory, options: input.region.options, @@ -197,7 +197,7 @@ Future _downloadManager( obscuredQueryParams: input.obscuredQueryParams, headers: headers, ), - onExit: downloadThreadreceivePort.sendPort, + onExit: downloadThreadReceivePort.sendPort, debugName: '[FMTC] Bulk Download Thread #$threadNo', ); late final SendPort sendPort; @@ -214,7 +214,7 @@ Future _downloadManager( .then((sp) => sp.send(null)), ); - downloadThreadreceivePort.listen( + downloadThreadReceivePort.listen( (evt) async { // Thread is sending tile data if (evt is TileEvent) { @@ -288,7 +288,7 @@ Future _downloadManager( } // Thread ended, goto `onDone` - if (evt == null) return downloadThreadreceivePort.close(); + if (evt == null) return downloadThreadReceivePort.close(); }, onDone: () { try { @@ -323,7 +323,7 @@ Future _downloadManager( ); // Cleanup resources and shutdown - rootreceivePort.close(); + rootReceivePort.close(); tileIsolate.kill(priority: Isolate.immediate); await tileQueue.cancel(immediate: true); Isolate.exit(); diff --git a/lib/src/db/defs/metadata.dart b/lib/src/db/defs/metadata.dart deleted file mode 100644 index 60e46656..00000000 --- a/lib/src/db/defs/metadata.dart +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright © Luka S (JaffaKetchup) under GPL-v3 -// A full license can be found at .\LICENSE - -import 'package:isar/isar.dart'; -import 'package:meta/meta.dart'; - -import '../tools.dart'; - -part 'metadata.g.dart'; - -@internal -@Collection(accessor: 'metadata') -class DbMetadata { - Id get id => DatabaseTools.hash(name); - - final String name; - final String data; - - DbMetadata({ - required this.name, - required this.data, - }); -} diff --git a/lib/src/db/defs/recovery.dart b/lib/src/db/defs/recovery.dart deleted file mode 100644 index 708730c1..00000000 --- a/lib/src/db/defs/recovery.dart +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright © Luka S (JaffaKetchup) under GPL-v3 -// A full license can be found at .\LICENSE - -import 'package:isar/isar.dart'; -import 'package:meta/meta.dart'; - -part 'recovery.g.dart'; - -@internal -enum RegionType { rectangle, circle, line, customPolygon } - -@internal -@Collection(accessor: 'recovery') -class DbRecoverableRegion { - final Id id; - final String storeName; - final DateTime time; - @enumerated - final RegionType type; - - final byte minZoom; - final byte maxZoom; - - final short start; - final short? end; - - final float? nwLat; - final float? nwLng; - final float? seLat; - final float? seLng; - - final float? centerLat; - final float? centerLng; - final float? circleRadius; - - final List? linePointsLat; - final List? linePointsLng; - final float? lineRadius; - - DbRecoverableRegion({ - required this.id, - required this.storeName, - required this.time, - required this.type, - required this.minZoom, - required this.maxZoom, - required this.start, - this.end, - this.nwLat, - this.nwLng, - this.seLat, - this.seLng, - this.centerLat, - this.centerLng, - this.circleRadius, - this.linePointsLat, - this.linePointsLng, - this.lineRadius, - }); -} diff --git a/lib/src/db/defs/store_descriptor.dart b/lib/src/db/defs/store_descriptor.dart deleted file mode 100644 index 5977b398..00000000 --- a/lib/src/db/defs/store_descriptor.dart +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright © Luka S (JaffaKetchup) under GPL-v3 -// A full license can be found at .\LICENSE - -import 'package:isar/isar.dart'; -import 'package:meta/meta.dart'; - -part 'store_descriptor.g.dart'; - -@internal -@Collection(accessor: 'storeDescriptor') -class DbStoreDescriptor { - final Id id = 0; - final String name; - - int hits = 0; - int misses = 0; - - DbStoreDescriptor({required this.name}); -} diff --git a/lib/src/db/defs/tile.dart b/lib/src/db/defs/tile.dart deleted file mode 100644 index 79c7ffe9..00000000 --- a/lib/src/db/defs/tile.dart +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright © Luka S (JaffaKetchup) under GPL-v3 -// A full license can be found at .\LICENSE - -import 'package:isar/isar.dart'; -import 'package:meta/meta.dart'; - -import '../tools.dart'; - -part 'tile.g.dart'; - -@internal -@Collection(accessor: 'tiles') -class DbTile { - Id get id => DatabaseTools.hash(url); - - final String url; - final List bytes; - - @Index() - final DateTime lastModified; - - DbTile({ - required this.url, - required this.bytes, - }) : lastModified = DateTime.now(); -} diff --git a/lib/src/db/registry.dart b/lib/src/db/registry.dart deleted file mode 100644 index 707631d2..00000000 --- a/lib/src/db/registry.dart +++ /dev/null @@ -1,207 +0,0 @@ -// Copyright © Luka S (JaffaKetchup) under GPL-v3 -// A full license can be found at .\LICENSE - -import 'dart:io'; - -import 'package:isar/isar.dart'; -import 'package:meta/meta.dart'; -import 'package:path/path.dart' as p; -import 'package:stream_transform/stream_transform.dart'; - -import '../../flutter_map_tile_caching.dart'; -import '../misc/exts.dart'; -import 'defs/metadata.dart'; -import 'defs/recovery.dart'; -import 'defs/store_descriptor.dart'; -import 'defs/tile.dart'; -import 'tools.dart'; - -/// Manages the stores available -/// -/// It is very important for the [_storeDatabases] state to remain in sync with -/// the actual state of the [directory], otherwise unexpected behaviour may -/// occur. -@internal -class FMTCRegistry { - const FMTCRegistry._({ - required this.directory, - required this.recoveryDatabase, - required Map storeDatabases, - }) : _storeDatabases = storeDatabases; - - static late FMTCRegistry instance; - - final Directory directory; - final Isar recoveryDatabase; - final Map _storeDatabases; - - static Future initialise({ - required Directory directory, - required int databaseMaxSize, - required CompactCondition? databaseCompactCondition, - required void Function(FMTCInitialisationException error)? errorHandler, - required IOSink? initialisationSafetyWriteSink, - required List? safeModeSuccessfulIDs, - required bool debugMode, - }) async { - // Set up initialisation safety features - bool hasLocatedCorruption = false; - - Future deleteDatabaseAndRelatedFiles(String base) async { - try { - await Future.wait( - await directory - .list() - .where((e) => e is File && p.basename(e.path).startsWith(base)) - .map((e) async { - if (await e.exists()) return e.delete(); - }).toList(), - ); - // ignore: empty_catches - } catch (e) {} - } - - Future registerSafeDatabase(String base) async { - initialisationSafetyWriteSink?.writeln(base); - await initialisationSafetyWriteSink?.flush(); - } - - // Prepare open database method - Future?> openIsar(String id, File file) async { - try { - return MapEntry( - int.parse(id), - await Isar.open( - [DbStoreDescriptorSchema, DbTileSchema, DbMetadataSchema], - name: id, - directory: directory.absolute.path, - maxSizeMiB: databaseMaxSize, - compactOnLaunch: databaseCompactCondition, - inspector: debugMode, - ), - ); - } catch (err) { - await deleteDatabaseAndRelatedFiles(p.basename(file.path)); - errorHandler?.call( - FMTCInitialisationException( - 'Failed to initialise a store because Isar failed to open the database.', - FMTCInitialisationExceptionType.isarFailure, - originalError: err, - ), - ); - return null; - } - } - - // Open recovery database - if (!(safeModeSuccessfulIDs?.contains('.recovery') ?? true) && - await (directory >>> '.recovery.isar').exists()) { - await deleteDatabaseAndRelatedFiles('.recovery'); - hasLocatedCorruption = true; - errorHandler?.call( - FMTCInitialisationException( - 'Failed to open a database because it was not listed as safe/stable on last initialisation.', - FMTCInitialisationExceptionType.corruptedDatabase, - storeName: '.recovery', - ), - ); - } - final recoveryDatabase = await Isar.open( - [ - DbRecoverableRegionSchema, - if (debugMode) ...[ - DbStoreDescriptorSchema, - DbTileSchema, - DbMetadataSchema, - ], - ], - name: '.recovery', - directory: directory.absolute.path, - maxSizeMiB: databaseMaxSize, - compactOnLaunch: databaseCompactCondition, - inspector: debugMode, - ); - await registerSafeDatabase('.recovery'); - - // Open store databases - return instance = FMTCRegistry._( - directory: directory, - recoveryDatabase: recoveryDatabase, - storeDatabases: Map.fromEntries( - await directory - .list() - .where( - (e) => - e is File && - !p.basename(e.path).startsWith('.') && - p.extension(e.path) == '.isar', - ) - .asyncMap((file) async { - final id = p.basenameWithoutExtension(file.path); - final path = p.basename(file.path); - - // Check whether the database is safe - if (!hasLocatedCorruption && - safeModeSuccessfulIDs != null && - !safeModeSuccessfulIDs.contains(id)) { - await deleteDatabaseAndRelatedFiles(path); - hasLocatedCorruption = true; - errorHandler?.call( - FMTCInitialisationException( - 'Failed to open a database because it was not listed as safe/stable on last initialisation.', - FMTCInitialisationExceptionType.corruptedDatabase, - ), - ); - return null; - } - - // Open the database - MapEntry? entry = await openIsar(id, file as File); - if (entry == null) return null; - - // Correct the database ID (filename) if the store name doesn't - // match - final storeName = (await entry.value.descriptor).name; - final realId = DatabaseTools.hash(storeName); - if (realId != int.parse(id)) { - await entry.value.close(); - file = await file - .rename(Directory(p.dirname(file.path)) > '$realId.isar'); - entry = await openIsar(realId.toString(), file); - await deleteDatabaseAndRelatedFiles(path); - } - - // Register the database as safe and add it to the registry - await registerSafeDatabase(id); - return entry; - }) - .whereNotNull() - .toList(), - ), - ); - } - - Future uninitialise({bool delete = false}) async { - await Future.wait([ - ...FMTC.instance.rootDirectory.stats.storesAvailable - .map((s) => s.manage.delete()), - recoveryDatabase.close(deleteFromDisk: delete), - ]); - } - - Isar call(String storeName) { - final id = DatabaseTools.hash(storeName); - final isRegistered = _storeDatabases.containsKey(id); - if (!(isRegistered && _storeDatabases[id]!.isOpen)) { - throw FMTCStoreNotReady( - storeName: storeName, - registered: isRegistered, - ); - } - return _storeDatabases[id]!; - } - - Isar register(int id, Isar db) => _storeDatabases[id] = db; - Isar? unregister(int id) => _storeDatabases.remove(id); - Map get storeDatabases => _storeDatabases; -} diff --git a/lib/src/db/tools.dart b/lib/src/db/tools.dart deleted file mode 100644 index f378a041..00000000 --- a/lib/src/db/tools.dart +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright © Luka S (JaffaKetchup) under GPL-v3 -// A full license can be found at .\LICENSE - -import 'package:isar/isar.dart'; -import 'package:meta/meta.dart'; - -import '../../flutter_map_tile_caching.dart'; -import 'defs/store_descriptor.dart'; - -@internal -class DatabaseTools { - static int hash(String string) { - final str = string.trim(); - - // ignore: avoid_js_rounded_ints - int hash = 0xcbf29ce484222325; - int i = 0; - - while (i < str.length) { - final codeUnit = str.codeUnitAt(i++); - hash ^= codeUnit >> 8; - hash *= 0x100000001b3; - hash ^= codeUnit & 0xFF; - hash *= 0x100000001b3; - } - - return hash; - } -} - -@internal -extension IsarExts on Isar { - Future get descriptor async { - final descriptor = await storeDescriptor.get(0); - if (descriptor == null) { - throw FMTCDamagedStoreException( - 'Failed to perform an operation on a store due to the core descriptor being missing.', - FMTCDamagedStoreExceptionType.missingStoreDescriptor, - ); - } - return descriptor; - } - - DbStoreDescriptor get descriptorSync { - final descriptor = storeDescriptor.getSync(0); - if (descriptor == null) { - throw FMTCDamagedStoreException( - 'Failed to perform an operation on a store due to the core descriptor being missing.', - FMTCDamagedStoreExceptionType.missingStoreDescriptor, - ); - } - return descriptor; - } -} diff --git a/lib/src/fmtc.dart b/lib/src/fmtc.dart index 8eda10aa..a283c0f1 100644 --- a/lib/src/fmtc.dart +++ b/lib/src/fmtc.dart @@ -39,6 +39,8 @@ class FlutterMapTileCaching { required bool debugMode, }) : _debugMode = debugMode; + /// {@macro fmtc.backend.initialise} + /// /// Initialise and prepare FMTC, by creating all necessary directories/files /// and configuring the [FlutterMapTileCaching] singleton /// diff --git a/lib/src/store/directory.dart b/lib/src/store/directory.dart index a3b73d05..ced18d77 100644 --- a/lib/src/store/directory.dart +++ b/lib/src/store/directory.dart @@ -3,22 +3,10 @@ part of flutter_map_tile_caching; -/// Represents a store of tiles -/// -/// The tile store itself is a database containing a descriptor, tiles and -/// metadata. -/// -/// The name originates from previous versions of this library, where it -/// represented a real directory instead of a database. -/// -/// Reach through [FlutterMapTileCaching.call]. +/// Container for a [storeName] which includes methods and getters to access +/// functionality based on the specified store class StoreDirectory { - StoreDirectory._( - this.storeName, { - required bool autoCreate, - }) { - if (autoCreate) manage.create(); - } + const StoreDirectory._(this.storeName); /// The user-friendly name of the store directory final String storeName; diff --git a/lib/src/store/manage.dart b/lib/src/store/manage.dart index 86c53914..b16c03fd 100644 --- a/lib/src/store/manage.dart +++ b/lib/src/store/manage.dart @@ -11,70 +11,39 @@ part of flutter_map_tile_caching; /// [StoreAlreadyExists]). It is recommended to check [ready] or [readySync] when /// necessary. final class StoreManagement extends _WithBackendAccess { - StoreManagement._(super.store); + const StoreManagement._(super.store); - /// Whether this store exists + /// {@macro fmtc.backend.storeExists} Future get ready => _backend.storeExists(storeName: _storeName); - /// Whether this store exists - bool get readySync => _backend.storeExistsSync(storeName: _storeName); - - /// Create this store + /// {@macro fmtc.backend.createStore} Future create() => _backend.createStore(storeName: _storeName); - /// Create this store - void createSync() => _backend.createStoreSync(storeName: _storeName); - - /// Delete this store - /// - /// This operation cannot be undone! Ensure you confirm with the user that - /// this action is expected. + /// {@macro fmtc.backend.deleteStore} Future delete() => _backend.deleteStore(storeName: _storeName); - /// Delete this store - /// - /// This operation cannot be undone! Ensure you confirm with the user that - /// this action is expected. - void deleteSync() => _backend.deleteStoreSync(storeName: _storeName); - - /// Removes all tiles from this store - /// - /// This operation cannot be undone! Ensure you confirm with the user that - /// this action is expected. + /// {@macro fmtc.backend.resetStore} Future reset() => _backend.resetStore(storeName: _storeName); - /// Removes all tiles from this store - /// - /// This operation cannot be undone! Ensure you confirm with the user that - /// this action is expected. - void resetSync() => _backend.resetStoreSync(storeName: _storeName); - - /// Rename the store directory + /// Rename the store to [newStoreName] /// /// The old [StoreDirectory] will still retain it's link to the old store, so /// always use the new returned value instead: returns a new [StoreDirectory] /// after a successful renaming operation. - /// - /// This method requires the store to be [ready], else an [FMTCStoreNotReady] - /// error will be raised. Future rename(String newStoreName) async { await _backend.renameStore( currentStoreName: _storeName, newStoreName: newStoreName, ); - // TODO: `autoCreate` and entire shortcut will now be broken by default - // consider whether this bi-synchronousable approach is sustainable - return StoreDirectory._(newStoreName, autoCreate: false); + return StoreDirectory._(newStoreName); } /// Delete all tiles older that were last modified before [expiry] /// /// Ignores [FMTCTileProviderSettings.cachedValidDuration]. - Future pruneTilesOlderThan({required DateTime expiry}) => compute( - _pruneTilesOlderThanWorker, - [_name, _rootDirectory.absolute.path, expiry], - ); + Future pruneTilesOlderThan({required DateTime expiry}) => + _backend.pruneTilesOlderThan(expiry: expiry); /// Retrieves the most recently modified tile from the store, extracts it's /// bytes, and renders them to an [Image] diff --git a/lib/src/store/statistics.dart b/lib/src/store/statistics.dart index a8e9eaaa..a83f70f0 100644 --- a/lib/src/store/statistics.dart +++ b/lib/src/store/statistics.dart @@ -7,49 +7,18 @@ part of flutter_map_tile_caching; final class StoreStats extends _WithBackendAccess { const StoreStats._(super._store); - /// Retrieve the total size of the stored tiles and metadata in kibibytes (KiB) - /// - /// Prefer [storeSizeAsync] to avoid blocking the UI thread. Otherwise, this - /// has slightly better performance. - double get storeSize => _backend.getStoreSizeSync(storeName: _storeName); - - /// Retrieve the total size of the stored tiles and metadata in kibibytes (KiB) - Future get storeSizeAsync => - _backend.getStoreSize(storeName: _storeName); - - /// Retrieve the number of stored tiles synchronously - /// - /// Prefer [storeLengthAsync] to avoid blocking the UI thread. Otherwise, this - /// has slightly better performance. - int get storeLength => _backend.getStoreLengthSync(storeName: _storeName); + /// {@macro fmtc.backend.getStoreSize} + Future get size => _backend.getStoreSize(storeName: _storeName); - /// Retrieve the number of stored tiles asynchronously - Future get storeLengthAsync => - _backend.getStoreLength(storeName: _storeName); + /// {@macro fmtc.backend.getStoreLength} + Future get length => _backend.getStoreLength(storeName: _storeName); - /// Retrieve the number of tiles that were successfully retrieved from the - /// store during browsing synchronously - /// - /// Prefer [cacheHitsAsync] to avoid blocking the UI thread. Otherwise, this - /// has slightly better performance. - int get cacheHits => _backend.getStoreHitsSync(storeName: _storeName); - - /// Retrieve the number of tiles that were successfully retrieved from the - /// store during browsing asynchronously - Future get cacheHitsAsync async => + /// {@macro fmtc.backend.getStoreHits} + Future get cacheHits async => _backend.getStoreHits(storeName: _storeName); - /// Retrieve the number of tiles that were unsuccessfully retrieved from the - /// store during browsing synchronously - /// - /// Prefer [cacheMissesAsync] to avoid blocking the UI thread. Otherwise, this - /// has slightly better performance. - int get cacheMisses => _backend.getStoreMissesSync(storeName: _storeName); - - /// Retrieve the number of tiles that were unsuccessfully retrieved from the - /// store during browsing asynchronously - Future get cacheMissesAsync async => - _backend.getStoreMisses(storeName: _storeName); + /// {@macro fmtc.backend.getStoreMisses} + Future get cacheMisses => _backend.getStoreMisses(storeName: _storeName); /// Watch for changes in the current store ///