11import 'dart:async' ;
2+ import 'dart:math' ;
23
34import 'package:flutter/foundation.dart' ;
45
@@ -21,9 +22,24 @@ enum RoomSyncStatus {
2122 notSynced,
2223}
2324
25+ class _PendingRoomLogin {
26+ final String roomPubKeyHex;
27+ final Completer <bool ?> completer;
28+
29+ const _PendingRoomLogin ({
30+ required this .roomPubKeyHex,
31+ required this .completer,
32+ });
33+ }
34+
2435class RoomSyncService extends ChangeNotifier {
2536 static const Duration _loginTimeoutFallback = Duration (seconds: 12 );
2637 static const int _maxAutoLoginAttempts = 3 ;
38+ static const int _autoLoginBurstSize = 2 ;
39+ static const Duration _autoLoginBurstPause = Duration (seconds: 4 );
40+ static const int _autoLoginJitterMinMs = 350 ;
41+ static const int _autoLoginJitterMaxMs = 1250 ;
42+ static const Duration _pushSyncThrottle = Duration (seconds: 20 );
2743
2844 final RoomSyncStore _roomSyncStore;
2945 final StorageService _storageService;
@@ -35,16 +51,18 @@ class RoomSyncService extends ChangeNotifier {
3551 Timer ? _nextSyncTimer;
3652 Timer ? _syncTimeoutTimer;
3753
38- final Map <String , Completer < bool > > _pendingLoginByPrefix = {};
54+ final Map <String , _PendingRoomLogin > _pendingLoginByPrefix = {};
3955 final Set <String > _activeRoomSessions = {};
4056 final Map <String , RoomSyncStateRecord > _states = {};
57+ final Random _random = Random ();
4158
4259 MeshCoreConnectionState ? _lastConnectionState;
4360 Duration _currentInterval = Duration .zero;
4461 bool _started = false ;
4562 bool _syncInFlight = false ;
4663 bool _autoLoginInProgress = false ;
4764 bool _lastRoomSyncEnabled = true ;
65+ DateTime ? _lastPushTriggeredSyncAt;
4866
4967 RoomSyncService ({
5068 required RoomSyncStore roomSyncStore,
@@ -55,7 +73,15 @@ class RoomSyncService extends ChangeNotifier {
5573 Map <String , RoomSyncStateRecord > get states => Map .unmodifiable (_states);
5674
5775 bool isRoomAutoSyncEnabled (String roomPubKeyHex) {
58- return _states[roomPubKeyHex]? .autoSyncEnabled ?? true ;
76+ return _states[roomPubKeyHex]? .autoSyncEnabled ?? false ;
77+ }
78+
79+ int ? roomAclPermissions (String roomPubKeyHex) {
80+ return _states[roomPubKeyHex]? .lastAclPermissions;
81+ }
82+
83+ int ? roomFirmwareLevel (String roomPubKeyHex) {
84+ return _states[roomPubKeyHex]? .lastLoginFirmwareLevel;
5985 }
6086
6187 Future <void > setRoomAutoSyncEnabled (
@@ -195,7 +221,27 @@ class RoomSyncService extends ChangeNotifier {
195221 .toList ();
196222 if (roomContacts.isEmpty) return ;
197223
224+ roomContacts.sort ((a, b) {
225+ final aState = _states[a.publicKeyHex];
226+ final bState = _states[b.publicKeyHex];
227+ final aScore =
228+ aState? .lastSuccessfulSyncAtMs ?? aState? .lastLoginSuccessAtMs ?? 0 ;
229+ final bScore =
230+ bState? .lastSuccessfulSyncAtMs ?? bState? .lastLoginSuccessAtMs ?? 0 ;
231+ return bScore.compareTo (aScore);
232+ });
233+
234+ int processed = 0 ;
198235 for (final room in roomContacts) {
236+ final delay = _nextAutoLoginDelay (processed);
237+ if (delay > Duration .zero) {
238+ await Future .delayed (delay);
239+ }
240+ if (! connector.isConnected ||
241+ ! _roomSyncEnabled ||
242+ ! _roomSyncAutoLoginEnabled) {
243+ break ;
244+ }
199245 final password = savedPasswords[room.publicKeyHex];
200246 if (password == null || password.isEmpty) continue ;
201247 final success = await _loginRoomWithRetries (room, password);
@@ -205,6 +251,7 @@ class RoomSyncService extends ChangeNotifier {
205251 } else {
206252 _recordFailure (room.publicKeyHex);
207253 }
254+ processed++ ;
208255 }
209256 } finally {
210257 _autoLoginInProgress = false ;
@@ -216,15 +263,18 @@ class RoomSyncService extends ChangeNotifier {
216263 Future <bool > _loginRoomWithRetries (Contact room, String password) async {
217264 if (! isRoomAutoSyncEnabled (room.publicKeyHex)) return false ;
218265 for (int attempt = 0 ; attempt < _maxAutoLoginAttempts; attempt++ ) {
219- if (! await _loginRoom (room, password)) continue ;
220- return true ;
266+ final result = await _loginRoom (room, password);
267+ if (result == true ) return true ;
268+ if (result == false ) return false ;
269+ // null indicates timeout/transport failure, so retry.
270+ continue ;
221271 }
222272 return false ;
223273 }
224274
225- Future <bool > _loginRoom (Contact room, String password) async {
275+ Future <bool ? > _loginRoom (Contact room, String password) async {
226276 final connector = _connector;
227- if (connector == null || ! connector.isConnected) return false ;
277+ if (connector == null || ! connector.isConnected) return null ;
228278 if (! isRoomAutoSyncEnabled (room.publicKeyHex)) return false ;
229279 _recordLoginAttempt (room.publicKeyHex);
230280
@@ -240,27 +290,38 @@ class RoomSyncService extends ChangeNotifier {
240290 : _loginTimeoutFallback;
241291
242292 final prefix = _prefixHex (room.publicKey.sublist (0 , 6 ));
243- final completer = Completer <bool >();
244- _pendingLoginByPrefix[prefix] = completer;
293+ final completer = Completer <bool ?>();
294+ _pendingLoginByPrefix[prefix] = _PendingRoomLogin (
295+ roomPubKeyHex: room.publicKeyHex,
296+ completer: completer,
297+ );
245298
246299 try {
247300 await connector.sendFrame (frame);
248301 final result = await completer.future.timeout (
249302 timeout,
250- onTimeout: () => false ,
303+ onTimeout: () => null ,
251304 );
252305 return result;
253306 } catch (_) {
254- return false ;
307+ return null ;
255308 } finally {
256- _pendingLoginByPrefix.remove (prefix);
309+ final currentPending = _pendingLoginByPrefix[prefix];
310+ if (currentPending != null &&
311+ identical (currentPending.completer, completer)) {
312+ _pendingLoginByPrefix.remove (prefix);
313+ }
257314 }
258315 }
259316
260317 void _handleFrame (Uint8List frame) {
261318 if (frame.isEmpty) return ;
262319 final code = frame[0 ];
263320
321+ if (code == pushCodeMsgWaiting) {
322+ _handleQueuedMessagesHint ();
323+ }
324+
264325 if (code == pushCodeLoginSuccess || code == pushCodeLoginFail) {
265326 _handleLoginResponseFrame (frame, code == pushCodeLoginSuccess);
266327 return ;
@@ -271,12 +332,48 @@ class RoomSyncService extends ChangeNotifier {
271332 _markSyncSuccess ();
272333 }
273334
335+ void _handleQueuedMessagesHint () {
336+ final connector = _connector;
337+ if (connector == null || ! connector.isConnected) return ;
338+ if (! _roomSyncEnabled) return ;
339+ if (_syncInFlight) return ;
340+
341+ final hasEnabledActiveRooms = _activeRoomSessions.any (
342+ isRoomAutoSyncEnabled,
343+ );
344+ if (! hasEnabledActiveRooms) return ;
345+
346+ final lastTrigger = _lastPushTriggeredSyncAt;
347+ final now = DateTime .now ();
348+ if (lastTrigger != null &&
349+ now.difference (lastTrigger) < _pushSyncThrottle) {
350+ return ;
351+ }
352+
353+ _lastPushTriggeredSyncAt = now;
354+ _scheduleNextSync (Duration .zero);
355+ }
356+
274357 void _handleLoginResponseFrame (Uint8List frame, bool success) {
275358 if (frame.length < 8 ) return ;
276359 final prefix = _prefixHex (frame.sublist (2 , 8 ));
277360 final pending = _pendingLoginByPrefix[prefix];
278- if (pending != null && ! pending.isCompleted) {
279- pending.complete (success);
361+ if (pending != null && ! pending.completer.isCompleted) {
362+ if (success) {
363+ _recordLoginMetadataFromFrame (pending.roomPubKeyHex, frame);
364+ }
365+ pending.completer.complete (success);
366+ return ;
367+ }
368+ if (! success) return ;
369+
370+ // Manual room logins are handled outside RoomSyncService; in that path we can
371+ // still capture metadata if the prefix resolves uniquely to a room contact.
372+ final roomPubKeyHex = _resolveRoomPubKeyHexByPrefix (prefix);
373+ if (roomPubKeyHex != null ) {
374+ _recordLoginMetadataFromFrame (roomPubKeyHex, frame);
375+ unawaited (_persistStates ());
376+ notifyListeners ();
280377 }
281378 }
282379
@@ -378,7 +475,15 @@ class RoomSyncService extends ChangeNotifier {
378475 }
379476
380477 Future <void > registerManualRoomLogin (String roomPubKeyHex) async {
381- if (! isRoomAutoSyncEnabled (roomPubKeyHex)) return ;
478+ final existing = _states[roomPubKeyHex];
479+ if (existing == null ) {
480+ _states[roomPubKeyHex] = RoomSyncStateRecord (
481+ roomPubKeyHex: roomPubKeyHex,
482+ autoSyncEnabled: true ,
483+ );
484+ } else if (! existing.autoSyncEnabled) {
485+ return ;
486+ }
382487 _activeRoomSessions.add (roomPubKeyHex);
383488 _recordLoginSuccess (roomPubKeyHex);
384489 await _persistStates ();
@@ -407,6 +512,20 @@ class RoomSyncService extends ChangeNotifier {
407512 );
408513 }
409514
515+ void _recordLoginMetadataFromFrame (String roomPubKeyHex, Uint8List frame) {
516+ final existing =
517+ _states[roomPubKeyHex] ??
518+ RoomSyncStateRecord (roomPubKeyHex: roomPubKeyHex);
519+ final serverTimestamp = frame.length >= 12 ? readUint32LE (frame, 8 ) : null ;
520+ final aclPermissions = frame.length >= 13 ? frame[12 ] : null ;
521+ final firmwareLevel = frame.length >= 14 ? frame[13 ] : null ;
522+ _states[roomPubKeyHex] = existing.copyWith (
523+ lastLoginServerTimestamp: serverTimestamp,
524+ lastAclPermissions: aclPermissions,
525+ lastLoginFirmwareLevel: firmwareLevel,
526+ );
527+ }
528+
410529 void _recordFailure (String roomPubKeyHex) {
411530 final existing =
412531 _states[roomPubKeyHex] ??
@@ -422,6 +541,18 @@ class RoomSyncService extends ChangeNotifier {
422541 );
423542 }
424543
544+ Duration _nextAutoLoginDelay (int processedCount) {
545+ if (processedCount <= 0 ) return Duration .zero;
546+ if (processedCount % _autoLoginBurstSize == 0 ) {
547+ return _autoLoginBurstPause;
548+ }
549+ final range = _autoLoginJitterMaxMs - _autoLoginJitterMinMs;
550+ final jitterMs = range <= 0
551+ ? _autoLoginJitterMinMs
552+ : _autoLoginJitterMinMs + _random.nextInt (range + 1 );
553+ return Duration (milliseconds: jitterMs);
554+ }
555+
425556 Future <void > _persistStates () async {
426557 await _roomSyncStore.save (_states);
427558 }
@@ -430,6 +561,22 @@ class RoomSyncService extends ChangeNotifier {
430561 return bytes.map ((b) => b.toRadixString (16 ).padLeft (2 , '0' )).join ();
431562 }
432563
564+ String ? _resolveRoomPubKeyHexByPrefix (String prefixHex) {
565+ final connector = _connector;
566+ if (connector == null ) return null ;
567+ final matches = connector.contacts
568+ .where ((contact) {
569+ if (contact.type != advTypeRoom) return false ;
570+ if (contact.publicKey.length < 6 ) return false ;
571+ return _prefixHex (contact.publicKey.sublist (0 , 6 )) == prefixHex;
572+ })
573+ .map ((contact) => contact.publicKeyHex)
574+ .toSet ()
575+ .toList ();
576+ if (matches.length != 1 ) return null ;
577+ return matches.first;
578+ }
579+
433580 Future <void > _tryLoginRoomByPubKey (String roomPubKeyHex) async {
434581 final connector = _connector;
435582 if (connector == null || ! connector.isConnected) return ;
0 commit comments