155 lines
4.5 KiB
Dart
155 lines
4.5 KiB
Dart
///
|
||
/// 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<RatchetState>? _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<void> open() async {
|
||
if (_box != null && _box!.isOpen) return;
|
||
try {
|
||
_box = await Hive.openBox<RatchetState>(boxName);
|
||
} catch (ex) {
|
||
Llog.log.e('[RatchetStateManager] Failed to open box: $ex');
|
||
rethrow;
|
||
}
|
||
}
|
||
|
||
/// Ensure the box is open, opening it if necessary.
|
||
Future<Box<RatchetState>> _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<RatchetState?> 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<void> save(RatchetState state) async {
|
||
final box = await _ensureBox();
|
||
await box.put(state.hiveKey, state);
|
||
}
|
||
|
||
/// Delete ratchet state for a specific channel + sender.
|
||
Future<void> 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<void> deleteChannel(String channelUuid) async {
|
||
final box = await _ensureBox();
|
||
final prefix = '${channelUuid}_';
|
||
final keysToDelete = <dynamic>[];
|
||
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<List<RatchetState>> listForChannel(String channelUuid) async {
|
||
final box = await _ensureBox();
|
||
final prefix = '${channelUuid}_';
|
||
final results = <RatchetState>[];
|
||
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<bool> 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<void> 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';
|
||
}
|
||
}
|