415 lines
13 KiB
Dart
415 lines
13 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:hive_ce_flutter/hive_flutter.dart';
|
|
import 'package:path_provider/path_provider.dart';
|
|
|
|
import 'package:worker/core/constants/storage_constants.dart';
|
|
// TODO: Re-enable when build_runner generates this file successfully
|
|
// import 'package:worker/hive_registrar.g.dart';
|
|
|
|
/// Hive CE (Community Edition) Database Service
|
|
///
|
|
/// This service manages the initialization, configuration, and lifecycle
|
|
/// of the Hive database for offline-first functionality.
|
|
///
|
|
/// Features:
|
|
/// - Box initialization and registration
|
|
/// - Type adapter registration
|
|
/// - Encryption support
|
|
/// - Database compaction
|
|
/// - Migration handling
|
|
/// - Error recovery
|
|
class HiveService {
|
|
HiveService._internal();
|
|
|
|
// Singleton pattern
|
|
factory HiveService() => _instance;
|
|
|
|
static final HiveService _instance = HiveService._internal();
|
|
|
|
/// Indicates whether Hive has been initialized
|
|
bool _isInitialized = false;
|
|
bool get isInitialized => _isInitialized;
|
|
|
|
/// Encryption cipher (if enabled)
|
|
HiveAesCipher? _encryptionCipher;
|
|
|
|
/// Initialize Hive database
|
|
///
|
|
/// This should be called once during app startup, before any
|
|
/// Hive operations are performed.
|
|
///
|
|
/// [encryptionKey] - Optional 256-bit encryption key for secure storage
|
|
/// If not provided and encryption is enabled, a new key will be generated.
|
|
Future<void> initialize({List<int>? encryptionKey}) async {
|
|
if (_isInitialized) {
|
|
debugPrint('HiveService: Already initialized');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
debugPrint('HiveService: Initializing Hive CE...');
|
|
|
|
// Initialize Hive for Flutter
|
|
await Hive.initFlutter();
|
|
|
|
// Setup encryption if enabled
|
|
if (HiveDatabaseConfig.enableEncryption) {
|
|
_encryptionCipher = HiveAesCipher(
|
|
encryptionKey ?? Hive.generateSecureKey(),
|
|
);
|
|
debugPrint('HiveService: Encryption enabled');
|
|
}
|
|
|
|
// Register all type adapters
|
|
await _registerTypeAdapters();
|
|
|
|
// Open all boxes
|
|
await _openBoxes();
|
|
|
|
// Check and perform migrations if needed
|
|
await _performMigrations();
|
|
|
|
// Perform initial cleanup/compaction if needed
|
|
await _performMaintenance();
|
|
|
|
_isInitialized = true;
|
|
debugPrint('HiveService: Initialization complete');
|
|
} catch (e, stackTrace) {
|
|
debugPrint('HiveService: Initialization failed: $e');
|
|
debugPrint('StackTrace: $stackTrace');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Register all Hive type adapters
|
|
///
|
|
/// Type adapters must be registered before opening boxes.
|
|
/// Uses auto-generated registrar from hive_registrar.g.dart
|
|
Future<void> _registerTypeAdapters() async {
|
|
debugPrint('HiveService: Registering type adapters...');
|
|
|
|
// Register all adapters using the auto-generated extension
|
|
// This automatically registers:
|
|
// - CachedDataAdapter (typeId: 30)
|
|
// - All enum adapters (typeIds: 20-29)
|
|
// TODO: Re-enable when build_runner generates hive_registrar.g.dart successfully
|
|
// Hive.registerAdapters();
|
|
|
|
debugPrint('HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.memberTier) ? "✓" : "✗"} MemberTier adapter');
|
|
debugPrint('HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.userType) ? "✓" : "✗"} UserType adapter');
|
|
debugPrint('HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.orderStatus) ? "✓" : "✗"} OrderStatus adapter');
|
|
debugPrint('HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.projectStatus) ? "✓" : "✗"} ProjectStatus adapter');
|
|
debugPrint('HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.projectType) ? "✓" : "✗"} ProjectType adapter');
|
|
debugPrint('HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.transactionType) ? "✓" : "✗"} TransactionType adapter');
|
|
debugPrint('HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.giftStatus) ? "✓" : "✗"} GiftStatus adapter');
|
|
debugPrint('HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.paymentStatus) ? "✓" : "✗"} PaymentStatus adapter');
|
|
// NotificationType adapter not needed - notification model uses String type
|
|
debugPrint('HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.paymentMethod) ? "✓" : "✗"} PaymentMethod adapter');
|
|
debugPrint('HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.cachedData) ? "✓" : "✗"} CachedData adapter');
|
|
|
|
// TODO: Register actual model type adapters when models are created
|
|
// These will be added to the auto-generated registrar when models are created
|
|
// Example:
|
|
// - UserModel (typeId: 0)
|
|
// - ProductModel (typeId: 1)
|
|
// - CartItemModel (typeId: 2)
|
|
// - OrderModel (typeId: 3)
|
|
// - ProjectModel (typeId: 4)
|
|
// - LoyaltyTransactionModel (typeId: 5)
|
|
// etc.
|
|
|
|
debugPrint('HiveService: Type adapters registered successfully');
|
|
}
|
|
|
|
/// Open all Hive boxes
|
|
///
|
|
/// Opens boxes for immediate access. Some boxes use encryption if enabled.
|
|
Future<void> _openBoxes() async {
|
|
debugPrint('HiveService: Opening boxes...');
|
|
|
|
try {
|
|
// Open non-encrypted boxes
|
|
await Future.wait([
|
|
// Settings and preferences (non-sensitive)
|
|
Hive.openBox<dynamic>(HiveBoxNames.settingsBox),
|
|
|
|
// Cache boxes (non-sensitive)
|
|
Hive.openBox<dynamic>(HiveBoxNames.cacheBox),
|
|
Hive.openBox<dynamic>(HiveBoxNames.syncStateBox),
|
|
|
|
// Product and catalog data (non-sensitive)
|
|
Hive.openBox<dynamic>(HiveBoxNames.productBox),
|
|
Hive.openBox<dynamic>(HiveBoxNames.rewardsBox),
|
|
|
|
// Notification box (non-sensitive)
|
|
Hive.openBox<dynamic>(HiveBoxNames.notificationBox),
|
|
|
|
// Favorites box (non-sensitive)
|
|
Hive.openBox<dynamic>(HiveBoxNames.favoriteBox),
|
|
]);
|
|
|
|
// Open potentially encrypted boxes (sensitive data)
|
|
final encryptedBoxes = [
|
|
HiveBoxNames.userBox,
|
|
HiveBoxNames.cartBox,
|
|
HiveBoxNames.orderBox,
|
|
HiveBoxNames.projectBox,
|
|
HiveBoxNames.loyaltyBox,
|
|
HiveBoxNames.addressBox,
|
|
HiveBoxNames.offlineQueueBox,
|
|
];
|
|
|
|
for (final boxName in encryptedBoxes) {
|
|
await Hive.openBox<dynamic>(
|
|
boxName,
|
|
encryptionCipher: _encryptionCipher,
|
|
);
|
|
}
|
|
|
|
debugPrint('HiveService: All boxes opened successfully');
|
|
} catch (e, stackTrace) {
|
|
debugPrint('HiveService: Error opening boxes: $e');
|
|
debugPrint('StackTrace: $stackTrace');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Perform database migrations
|
|
///
|
|
/// Handles schema version upgrades and data migrations.
|
|
Future<void> _performMigrations() async {
|
|
final settingsBox = Hive.box<dynamic>(HiveBoxNames.settingsBox);
|
|
final currentVersion = settingsBox.get(
|
|
HiveKeys.schemaVersion,
|
|
defaultValue: 0,
|
|
) as int;
|
|
|
|
debugPrint('HiveService: Current schema version: $currentVersion');
|
|
debugPrint('HiveService: Target schema version: ${HiveDatabaseConfig.currentSchemaVersion}');
|
|
|
|
if (currentVersion < HiveDatabaseConfig.currentSchemaVersion) {
|
|
debugPrint('HiveService: Performing migrations...');
|
|
|
|
// Perform migrations sequentially
|
|
for (int version = currentVersion + 1;
|
|
version <= HiveDatabaseConfig.currentSchemaVersion;
|
|
version++) {
|
|
await _migrateToVersion(version);
|
|
}
|
|
|
|
// Update schema version
|
|
await settingsBox.put(
|
|
HiveKeys.schemaVersion,
|
|
HiveDatabaseConfig.currentSchemaVersion,
|
|
);
|
|
|
|
debugPrint('HiveService: Migrations complete');
|
|
} else {
|
|
debugPrint('HiveService: No migrations needed');
|
|
}
|
|
}
|
|
|
|
/// Migrate to a specific version
|
|
Future<void> _migrateToVersion(int version) async {
|
|
debugPrint('HiveService: Migrating to version $version');
|
|
|
|
switch (version) {
|
|
case 1:
|
|
// Initial version - no migration needed
|
|
break;
|
|
|
|
// Future migrations will be added here
|
|
// case 2:
|
|
// await _migrateV1ToV2();
|
|
// break;
|
|
|
|
default:
|
|
debugPrint('HiveService: Unknown migration version: $version');
|
|
}
|
|
}
|
|
|
|
/// Perform database maintenance
|
|
///
|
|
/// Includes compaction, cleanup of expired cache, etc.
|
|
Future<void> _performMaintenance() async {
|
|
debugPrint('HiveService: Performing maintenance...');
|
|
|
|
try {
|
|
// Compact boxes if needed
|
|
await _compactBoxes();
|
|
|
|
// Clear expired cache
|
|
await _clearExpiredCache();
|
|
|
|
// Limit offline queue size
|
|
await _limitOfflineQueue();
|
|
|
|
debugPrint('HiveService: Maintenance complete');
|
|
} catch (e, stackTrace) {
|
|
debugPrint('HiveService: Maintenance error: $e');
|
|
debugPrint('StackTrace: $stackTrace');
|
|
// Don't throw - maintenance errors shouldn't prevent app startup
|
|
}
|
|
}
|
|
|
|
/// Compact boxes to reduce file size
|
|
Future<void> _compactBoxes() async {
|
|
for (final boxName in HiveBoxNames.allBoxes) {
|
|
try {
|
|
if (Hive.isBoxOpen(boxName)) {
|
|
final box = Hive.box<dynamic>(boxName);
|
|
await box.compact();
|
|
debugPrint('HiveService: Compacted box: $boxName');
|
|
}
|
|
} catch (e) {
|
|
debugPrint('HiveService: Error compacting box $boxName: $e');
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Clear expired cache entries
|
|
Future<void> _clearExpiredCache() async {
|
|
final cacheBox = Hive.box<dynamic>(HiveBoxNames.cacheBox);
|
|
|
|
// TODO: Implement cache expiration logic
|
|
// This will be implemented when cache models are created
|
|
|
|
debugPrint('HiveService: Cleared expired cache entries');
|
|
}
|
|
|
|
/// Limit offline queue size
|
|
Future<void> _limitOfflineQueue() async {
|
|
final queueBox = Hive.box<dynamic>(HiveBoxNames.offlineQueueBox);
|
|
|
|
if (queueBox.length > HiveDatabaseConfig.maxOfflineQueueSize) {
|
|
final itemsToRemove = queueBox.length - HiveDatabaseConfig.maxOfflineQueueSize;
|
|
|
|
// Remove oldest items
|
|
for (int i = 0; i < itemsToRemove; i++) {
|
|
await queueBox.deleteAt(0);
|
|
}
|
|
|
|
debugPrint('HiveService: Removed $itemsToRemove old items from offline queue');
|
|
}
|
|
}
|
|
|
|
/// Get a box by name
|
|
///
|
|
/// Returns an already opened box. Throws if box is not open.
|
|
Box<T> getBox<T>(String boxName) {
|
|
if (!Hive.isBoxOpen(boxName)) {
|
|
throw HiveError('Box $boxName is not open');
|
|
}
|
|
return Hive.box<T>(boxName);
|
|
}
|
|
|
|
/// Check if a box is open
|
|
bool isBoxOpen(String boxName) {
|
|
return Hive.isBoxOpen(boxName);
|
|
}
|
|
|
|
/// Clear all data from all boxes
|
|
///
|
|
/// WARNING: This will delete all local data. Use with caution.
|
|
Future<void> clearAllData() async {
|
|
debugPrint('HiveService: Clearing all data...');
|
|
|
|
for (final boxName in HiveBoxNames.allBoxes) {
|
|
try {
|
|
if (Hive.isBoxOpen(boxName)) {
|
|
final box = Hive.box<dynamic>(boxName);
|
|
await box.clear();
|
|
debugPrint('HiveService: Cleared box: $boxName');
|
|
}
|
|
} catch (e) {
|
|
debugPrint('HiveService: Error clearing box $boxName: $e');
|
|
}
|
|
}
|
|
|
|
debugPrint('HiveService: All data cleared');
|
|
}
|
|
|
|
/// Clear user-specific data (logout)
|
|
///
|
|
/// Clears user data while preserving app settings and cache
|
|
Future<void> clearUserData() async {
|
|
debugPrint('HiveService: Clearing user data...');
|
|
|
|
final boxesToClear = [
|
|
HiveBoxNames.userBox,
|
|
HiveBoxNames.cartBox,
|
|
HiveBoxNames.orderBox,
|
|
HiveBoxNames.projectBox,
|
|
HiveBoxNames.loyaltyBox,
|
|
HiveBoxNames.addressBox,
|
|
HiveBoxNames.notificationBox,
|
|
];
|
|
|
|
for (final boxName in boxesToClear) {
|
|
try {
|
|
if (Hive.isBoxOpen(boxName)) {
|
|
final box = Hive.box<dynamic>(boxName);
|
|
await box.clear();
|
|
debugPrint('HiveService: Cleared box: $boxName');
|
|
}
|
|
} catch (e) {
|
|
debugPrint('HiveService: Error clearing box $boxName: $e');
|
|
}
|
|
}
|
|
|
|
debugPrint('HiveService: User data cleared');
|
|
}
|
|
|
|
/// Close all boxes
|
|
///
|
|
/// Should be called when app is terminating
|
|
Future<void> close() async {
|
|
debugPrint('HiveService: Closing all boxes...');
|
|
|
|
try {
|
|
await Hive.close();
|
|
_isInitialized = false;
|
|
debugPrint('HiveService: All boxes closed');
|
|
} catch (e, stackTrace) {
|
|
debugPrint('HiveService: Error closing boxes: $e');
|
|
debugPrint('StackTrace: $stackTrace');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Delete all Hive data from disk
|
|
///
|
|
/// WARNING: This completely removes the database. Use only for testing or reset.
|
|
Future<void> deleteFromDisk() async {
|
|
debugPrint('HiveService: Deleting database from disk...');
|
|
|
|
try {
|
|
// Close all boxes first
|
|
await close();
|
|
|
|
// Delete Hive directory
|
|
final appDocDir = await getApplicationDocumentsDirectory();
|
|
final hiveDir = Directory('${appDocDir.path}/hive');
|
|
|
|
if (await hiveDir.exists()) {
|
|
await hiveDir.delete(recursive: true);
|
|
debugPrint('HiveService: Database deleted from disk');
|
|
}
|
|
} catch (e, stackTrace) {
|
|
debugPrint('HiveService: Error deleting database: $e');
|
|
debugPrint('StackTrace: $stackTrace');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Generate a secure encryption key
|
|
///
|
|
/// Returns a 256-bit encryption key for secure box encryption.
|
|
/// Store this key securely (e.g., in flutter_secure_storage).
|
|
static List<int> generateEncryptionKey() {
|
|
return Hive.generateSecureKey();
|
|
}
|
|
}
|