/// /// Ratchet State Manager – Hive Persistence for Ratchet Chain State /// /// Manages the lifecycle of [RatchetState] objects in a dedicated Hive box. /// Each entry is keyed by ``'{channelUuid}_{senderIdentifier}'``. /// /// Phase 2 of the Hydra Apocalypse Ratchet. /// import 'package:hive_ce/hive.dart'; import 'package:letusmsg/data/ratchet_state_data.dart'; import 'package:letusmsg/utilz/Llog.dart'; /// Manages persistence of [RatchetState] objects in Hive. /// /// Provides CRUD operations for ratchet states and channel-level cleanup. /// The box is opened lazily on first access and remains open for the app /// lifetime. /// /// ## Key Format /// ``'{channelUuid}_{senderIdentifier}'`` — deterministic from the /// [RatchetState.hiveKey] property. /// /// ## Usage /// ```dart /// final manager = RatchetStateManager(); /// await manager.open(); /// /// // Save after ratchet advance /// await manager.save(state); /// /// // Load for decrypt /// final state = await manager.load('chan-uuid', 'sender-id'); /// ``` class RatchetStateManager { static const String boxName = 'ratchet_state'; Box? _box; /// Whether the Hive box is currently open. bool get isOpen => _box?.isOpen ?? false; /// Open the ratchet state Hive box. /// /// Safe to call multiple times — returns immediately if already open. Future open() async { if (_box != null && _box!.isOpen) return; try { _box = await Hive.openBox(boxName); } catch (ex) { Llog.log.e('[RatchetStateManager] Failed to open box: $ex'); rethrow; } } /// Ensure the box is open, opening it if necessary. Future> _ensureBox() async { if (_box == null || !_box!.isOpen) { await open(); } return _box!; } /// Load ratchet state for a specific channel + sender. /// /// Returns ``null`` if no state exists (channel not yet initialized with /// ratchet, or pre-Phase 2 channel). Future load( String channelUuid, String senderIdentifier, ) async { final box = await _ensureBox(); final key = _buildKey(channelUuid, senderIdentifier); return box.get(key); } /// Save ratchet state after advancing the chain. /// /// Must be called after every send/receive operation to persist the /// updated chain keys and counters. Future save(RatchetState state) async { final box = await _ensureBox(); await box.put(state.hiveKey, state); } /// Delete ratchet state for a specific channel + sender. Future delete( String channelUuid, String senderIdentifier, ) async { final box = await _ensureBox(); final key = _buildKey(channelUuid, senderIdentifier); await box.delete(key); } /// Delete all ratchet states for a channel (all senders). /// /// Called when a channel is deleted or left. Iterates all keys in the box /// and removes those matching the channel prefix. Future deleteChannel(String channelUuid) async { final box = await _ensureBox(); final prefix = '${channelUuid}_'; final keysToDelete = []; for (final key in box.keys) { if (key.toString().startsWith(prefix)) { keysToDelete.add(key); } } if (keysToDelete.isNotEmpty) { await box.deleteAll(keysToDelete); Llog.log.d('[RatchetStateManager] Deleted ${keysToDelete.length} ' 'ratchet state(s) for channel $channelUuid'); } } /// List all ratchet states for a channel (all senders). /// /// Useful for debugging and testing. Future> listForChannel(String channelUuid) async { final box = await _ensureBox(); final prefix = '${channelUuid}_'; final results = []; for (final key in box.keys) { if (key.toString().startsWith(prefix)) { final state = box.get(key); if (state != null) results.add(state); } } return results; } /// Check if a ratchet state exists for a channel + sender. Future exists( String channelUuid, String senderIdentifier, ) async { final box = await _ensureBox(); return box.containsKey(_buildKey(channelUuid, senderIdentifier)); } /// Close the Hive box. /// /// Called during app shutdown or cleanup. Future close() async { if (_box != null && _box!.isOpen) { await _box!.close(); } _box = null; } /// Build the composite Hive key from channel + sender identifiers. static String _buildKey(String channelUuid, String senderIdentifier) { return '${channelUuid}_$senderIdentifier'; } }