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.
110 lines
4.3 KiB
Dart
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');
|
|
}
|