fix(alarm): stop corrupt entries and unreadable payloads from wiping saved alarms
A single malformed alarm entry (bad/missing id, wrong type) used to discard the ENTIRE persisted list on next load, and a fully unparseable payload let the periodic recalculation silently overwrite it with an empty one -- both destroyed valid alarms with no user action. Adds a shared per-entry tolerant-parse helper (persistencia_tolerante.dart) that skips and logs only the bad entry; ServicioAlarmas now normalizes its cached raw after a partial load (no dirty-guard thrash) and sets a degraded-read flag after a total decode failure that suppresses automatic writes until a good read or an explicit user mutation restores authority.
This commit is contained in:
@@ -5,6 +5,7 @@ import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../modelos/alarma_musical.dart';
|
||||
import '../modelos/emisora.dart';
|
||||
import 'persistencia_tolerante.dart';
|
||||
import 'servicio_programacion_alarmas.dart';
|
||||
|
||||
class ConfiguracionAlarmas {
|
||||
@@ -41,6 +42,14 @@ class ServicioAlarmas {
|
||||
String? _cacheRaw;
|
||||
Future<void> _cola = Future<void>.value();
|
||||
|
||||
// persistence-resilience (D4): set when the top-level payload could not
|
||||
// be decoded at all (vs. an individual entry inside it). While true, the
|
||||
// AUTOMATIC writer (recalcularTodas) must never reach disk -- only an
|
||||
// explicit user mutation (which always funnels through _guardar) may
|
||||
// overwrite a payload we could not read. Cleared by _guardar and by the
|
||||
// next successful (even if partial) read.
|
||||
bool _lecturaAlarmasDegradada = false;
|
||||
|
||||
Future<T> _enCola<T>(Future<T> Function() accion) {
|
||||
final resultado = _cola.then((_) => accion());
|
||||
_cola = resultado.then((_) {}, onError: (_) {});
|
||||
@@ -63,12 +72,29 @@ class ServicioAlarmas {
|
||||
final raw = prefs.getString(_keyConfig);
|
||||
final config = _parsear(raw);
|
||||
_cache = config;
|
||||
_cacheRaw = raw;
|
||||
return config;
|
||||
}
|
||||
|
||||
/// Parses the persisted [raw] payload with per-entry tolerance
|
||||
/// (persistence-resilience D1/D2) and updates [_cacheRaw] /
|
||||
/// [_lecturaAlarmasDegradada] as a side effect, since each outcome needs
|
||||
/// a DIFFERENT cached-raw value:
|
||||
/// - top-level decode failure (bad JSON, or any container-level field of
|
||||
/// the wrong shape) -> DEGRADED: empty config, [_cacheRaw] keeps the
|
||||
/// corrupt [raw] untouched, [_lecturaAlarmasDegradada] set so
|
||||
/// `recalcularTodas` never overwrites it (D4).
|
||||
/// - decodes, but one or more entries are malformed -> PARTIAL: only the
|
||||
/// survivors are kept, [_cacheRaw] is normalized to their own
|
||||
/// serialization -- NOT the corrupt raw (D3) -- so the automatic
|
||||
/// writer's dirty-guard compares against a coherent baseline instead of
|
||||
/// re-firing on every 60s tick.
|
||||
/// - fully healthy payload -> unchanged behavior; clears the flag if a
|
||||
/// previous read had set it (suppression lifts on next successful
|
||||
/// read).
|
||||
ConfiguracionAlarmas _parsear(String? raw) {
|
||||
if (raw == null || raw.trim().isEmpty) {
|
||||
_lecturaAlarmasDegradada = false;
|
||||
_cacheRaw = raw;
|
||||
return const ConfiguracionAlarmas(
|
||||
alarmas: [],
|
||||
vacaciones: [],
|
||||
@@ -77,30 +103,44 @@ class ServicioAlarmas {
|
||||
}
|
||||
try {
|
||||
final data = jsonDecode(raw) as Map<String, dynamic>;
|
||||
return ConfiguracionAlarmas(
|
||||
alarmas:
|
||||
(data['alarmas'] as List? ?? const [])
|
||||
.whereType<Map>()
|
||||
.map(
|
||||
(e) => AlarmaMusical.fromJson(Map<String, dynamic>.from(e)),
|
||||
)
|
||||
.toList(),
|
||||
vacaciones:
|
||||
(data['vacaciones'] as List? ?? const [])
|
||||
.whereType<Map>()
|
||||
.map(
|
||||
(e) => RangoVacaciones.fromJson(Map<String, dynamic>.from(e)),
|
||||
)
|
||||
.toList(),
|
||||
excepciones:
|
||||
(data['excepciones'] as List? ?? const [])
|
||||
.whereType<Map>()
|
||||
.map(
|
||||
(e) => ExcepcionAlarma.fromJson(Map<String, dynamic>.from(e)),
|
||||
)
|
||||
.toList(),
|
||||
final alarmas = parseListaTolerante<AlarmaMusical>(
|
||||
data['alarmas'] as List?,
|
||||
AlarmaMusical.fromJson,
|
||||
subsistema: 'alarmas',
|
||||
coleccion: 'alarmas',
|
||||
);
|
||||
final vacaciones = parseListaTolerante<RangoVacaciones>(
|
||||
data['vacaciones'] as List?,
|
||||
RangoVacaciones.fromJson,
|
||||
subsistema: 'alarmas',
|
||||
coleccion: 'vacaciones',
|
||||
);
|
||||
final excepciones = parseListaTolerante<ExcepcionAlarma>(
|
||||
data['excepciones'] as List?,
|
||||
ExcepcionAlarma.fromJson,
|
||||
subsistema: 'alarmas',
|
||||
coleccion: 'excepciones',
|
||||
);
|
||||
final config = ConfiguracionAlarmas(
|
||||
alarmas: alarmas.validas,
|
||||
vacaciones: vacaciones.validas,
|
||||
excepciones: excepciones.validas,
|
||||
);
|
||||
_lecturaAlarmasDegradada = false;
|
||||
final huboSaltos =
|
||||
alarmas.saltadas > 0 ||
|
||||
vacaciones.saltadas > 0 ||
|
||||
excepciones.saltadas > 0;
|
||||
_cacheRaw = huboSaltos ? _serializar(config) : raw;
|
||||
return config;
|
||||
} catch (e) {
|
||||
_lecturaAlarmasDegradada = true;
|
||||
_cacheRaw = raw;
|
||||
registrarSaltoPersistencia(
|
||||
subsistema: 'alarmas',
|
||||
detalle: _keyConfig,
|
||||
razon: e.toString(),
|
||||
);
|
||||
} catch (_) {
|
||||
return const ConfiguracionAlarmas(
|
||||
alarmas: [],
|
||||
vacaciones: [],
|
||||
@@ -200,6 +240,11 @@ class ServicioAlarmas {
|
||||
|
||||
Future<ConfiguracionAlarmas> recalcularTodas() => _enCola(() async {
|
||||
final config = await _configActual();
|
||||
// persistence-resilience (D4): a degraded top-level read must never
|
||||
// let this AUTOMATIC writer reach disk -- only an explicit user
|
||||
// mutation (guardarAlarma/eliminarAlarma/etc., via _guardar) may
|
||||
// persist over a payload we could not read.
|
||||
if (_lecturaAlarmasDegradada) return config;
|
||||
final alarmas = _recalcularLista(
|
||||
config.alarmas,
|
||||
config.vacaciones,
|
||||
@@ -427,6 +472,10 @@ class ServicioAlarmas {
|
||||
await prefs.setString(_keyConfig, serializado);
|
||||
_cache = config;
|
||||
_cacheRaw = serializado;
|
||||
// persistence-resilience (D4): every explicit mutation funnels through
|
||||
// here, so this is where write authority is restored after a degraded
|
||||
// read -- user intent wins over a suppressed automatic writer.
|
||||
_lecturaAlarmasDegradada = false;
|
||||
}
|
||||
|
||||
String _serializar(ConfiguracionAlarmas config) => jsonEncode({
|
||||
|
||||
Reference in New Issue
Block a user