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` for /// [parseListaTolerante], a `Map` 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 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` -> `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> parseListaTolerante( List? datos, T Function(Map entrada) parser, { required String subsistema, required String coleccion, }) { if (datos == null || datos.isEmpty) { return (validas: [], saltadas: 0); } final validas = []; 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.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?`). This helper never calls /// `jsonDecode` itself — see [parseListaTolerante] for the same rationale. ResultadoTolerante> parseMapaTolerante( Map? datos, V Function(dynamic valor) parser, { required String subsistema, required String coleccion, }) { if (datos == null || datos.isEmpty) { return (validas: {}, saltadas: 0); } final validas = {}; 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'); }