import 'dart:async'; import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:p_tits_pas/models/carte_bulle.dart'; import 'package:p_tits_pas/services/api/api_config.dart'; import 'package:p_tits_pas/services/api/tokenService.dart'; import 'cards_stream_connector_stub.dart' if (dart.library.html) 'cards_stream_connector_web.dart' if (dart.library.io) 'cards_stream_connector_io.dart'; /// Événement temps réel file d'attention — ticket #195. class CardsStreamEvent { final String type; final String? cardId; final CarteBulle? card; const CardsStreamEvent({ required this.type, this.cardId, this.card, }); } /// Client SSE `GET /cards/stream` avec reconnexion. class CardsStreamClient { final void Function(CardsStreamEvent event) onEvent; final VoidCallback? onReconnectSync; CardsStreamClient({ required this.onEvent, this.onReconnectSync, }); final CardsStreamConnector _connector = CardsStreamConnector(); bool _started = false; bool _stopRequested = false; int _attempt = 0; Timer? _retryTimer; bool _hadConnection = false; void start() { if (_started) return; _started = true; _stopRequested = false; _attempt = 0; unawaited(_connect()); } void stop() { _stopRequested = true; _started = false; _retryTimer?.cancel(); _retryTimer = null; _connector.close(); } Future _connect() async { if (_stopRequested) return; final token = await TokenService.getToken(); if (token == null || token.isEmpty) { _scheduleRetry(); return; } final base = '${ApiConfig.baseUrl}${ApiConfig.cardsStream}'.replaceAll( RegExp(r'/+$'), '', ); // Web (EventSource) : token en query. IO : Bearer dans le connecteur. final url = kIsWeb ? Uri.parse(base).replace( queryParameters: {'access_token': token}, ).toString() : base; await _connector.connect( url: url, bearerToken: kIsWeb ? null : token, onEvent: _handleRaw, onError: (_) { if (_stopRequested) return; _connector.close(); _scheduleRetry(); }, ); } void _handleRaw(String eventType, String data) { if (_stopRequested) return; // Premier événement (souvent heartbeat) = connexion OK. if (!_hadConnection) { _hadConnection = true; _attempt = 0; } else if (eventType == 'heartbeat') { _attempt = 0; return; } if (eventType == 'heartbeat') return; try { final decoded = jsonDecode(data); if (decoded is! Map) return; final map = Map.from(decoded); final type = (map['event']?.toString().isNotEmpty == true) ? map['event'].toString() : eventType; final cardId = map['card_id']?.toString(); final payload = map['data']; CarteBulle? card; if (type == 'response.added' && payload is Map) { final nested = payload['card']; if (nested is Map) { card = CarteBulle.fromJson(Map.from(nested)); } } else if ((type == 'card.created' || type == 'card.updated') && payload is Map) { card = CarteBulle.fromJson(Map.from(payload)); } onEvent( CardsStreamEvent( type: type, cardId: cardId ?? card?.id, card: card, ), ); } catch (_) { // Événement mal formé : ignorer. } } void _scheduleRetry() { if (_stopRequested) return; _retryTimer?.cancel(); final delaySec = (1 << _attempt.clamp(0, 4)).clamp(1, 30); _attempt++; _retryTimer = Timer(Duration(seconds: delaySec), () { if (_stopRequested) return; if (_hadConnection) { // Resync REST après coupure pour ne pas manquer d'événements. onReconnectSync?.call(); } unawaited(_connect()); }); } }