Files
pluriwave/lib/servicios/persistencia_tolerante.dart
T
FreeTLab 65c1ac2085
Build & Deploy PluriWave / Análisis de código (push) Successful in 38s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 1m38s
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.
2026-07-11 12:27:39 +02:00

110 lines
4.3 KiB
Dart

import 'package:flutter/foundation.dart';
/// Shared per-entry tolerant-parse primitives (persistence-resilience D1).
///
/// Every subsystem that persists a collection (alarms, custom stations, EQ
/// presets/device names) decodes its own top-level payload first — total
/// top-level decode failure is a subsystem-specific policy (see each
/// caller's own degraded-read handling) and is intentionally OUT of scope
/// here. What IS shared is what happens once the container decoded fine but
/// one entry inside it did not: that single entry must be skipped and
/// logged, never allowed to take its valid siblings down with it, and never
/// replaced by a fabricated identity.
/// Result of a tolerant, per-entry parse pass over a persisted collection.
///
/// [validas] holds whatever survived (a `List<T>` for
/// [parseListaTolerante], a `Map<String, V>` for [parseMapaTolerante]).
/// [saltadas] counts how many entries were skipped because they failed to
/// parse — callers use this to detect a PARTIAL read (>0) versus a fully
/// healthy one (0) without inspecting [validas] itself.
typedef ResultadoTolerante<T> = ({T validas, int saltadas});
/// Parses each entry of the ALREADY-decoded [datos] independently via
/// [parser]; an entry that is not a [Map], or whose [parser] throws (e.g. a
/// missing/invalid identity field), is skipped and logged instead of
/// aborting the whole parse. Order of survivors matches [datos]' order.
///
/// [datos] must already be the decoded top-level list (e.g. the result of
/// `jsonDecode(raw) as Map<String, dynamic>` -> `map['alarmas'] as List?`).
/// This helper never calls `jsonDecode` itself — total-failure policy for
/// an unparseable top-level payload differs per subsystem and stays with
/// the caller.
ResultadoTolerante<List<T>> parseListaTolerante<T>(
List<dynamic>? datos,
T Function(Map<String, dynamic> entrada) parser, {
required String subsistema,
required String coleccion,
}) {
if (datos == null || datos.isEmpty) {
return (validas: <T>[], saltadas: 0);
}
final validas = <T>[];
var saltadas = 0;
for (var indice = 0; indice < datos.length; indice++) {
try {
final entrada = datos[indice];
if (entrada is! Map) {
throw const FormatException('la entrada no es un mapa');
}
validas.add(parser(Map<String, dynamic>.from(entrada)));
} catch (e) {
saltadas++;
registrarSaltoPersistencia(
subsistema: subsistema,
detalle: '$coleccion[$indice]',
razon: e.toString(),
);
}
}
return (validas: validas, saltadas: saltadas);
}
/// Parses each VALUE of the ALREADY-decoded [datos] independently via
/// [parser]; a value whose [parser] throws is skipped and logged, and only
/// its key is dropped from the result. Keys of surviving entries are
/// preserved byte-for-byte (needed by callers whose keys carry meaning,
/// e.g. EQ's colon-delimited `stationUuid:deviceId` matrix keys).
///
/// [datos] must already be the decoded top-level map (e.g. the result of
/// `jsonDecode(raw) as Map<String, dynamic>?`). This helper never calls
/// `jsonDecode` itself — see [parseListaTolerante] for the same rationale.
ResultadoTolerante<Map<String, V>> parseMapaTolerante<V>(
Map<String, dynamic>? datos,
V Function(dynamic valor) parser, {
required String subsistema,
required String coleccion,
}) {
if (datos == null || datos.isEmpty) {
return (validas: <String, V>{}, saltadas: 0);
}
final validas = <String, V>{};
var saltadas = 0;
for (final entrada in datos.entries) {
try {
validas[entrada.key] = parser(entrada.value);
} catch (e) {
saltadas++;
registrarSaltoPersistencia(
subsistema: subsistema,
detalle: '$coleccion[${entrada.key}]',
razon: e.toString(),
);
}
}
return (validas: validas, saltadas: saltadas);
}
/// Logs a persistence skip or degraded read with enough context to
/// diagnose it later: which subsystem, which entry/key/reason. Developer
/// facing only — this MUST NOT surface as user-visible copy or gain an
/// l10n key (persistence-resilience spec, "Diagnostics are developer-facing
/// only").
void registrarSaltoPersistencia({
required String subsistema,
required String detalle,
required String razon,
}) {
debugPrint('[PluriWave][persistencia] $subsistema $detalle: $razon');
}