You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
meshcore-client/lib/helpers/pending_reactions.dart

90 lines
3.0 KiB

import '../utils/app_logger.dart';
import 'reaction_helper.dart';
class _Pending {
final String scopeKey;
final ReactionInfo info;
final String reactingSender;
final DateTime queuedAt;
const _Pending(this.scopeKey, this.info, this.reactingSender, this.queuedAt);
}
/// Reactions that arrived before the message they point at.
///
/// Out-of-order delivery is normal on a mesh, so a reaction with no local
/// target is not junk, it is early. Before this existed the connector consumed
/// such a reaction and dropped it with no badge, no message and no log, which
/// is indistinguishable from nobody having reacted (GH #382).
///
/// Entries are bounded and expire. A drop is logged at warn so it surfaces in
/// the in-app log and the file log rather than vanishing (SAFELANE 6). Whether
/// dropping is the right end state is still open; the ratio of late matches to
/// expiries in the log is the evidence for revisiting it.
class PendingReactions {
static const int maxEntries = 50;
static const Duration ttl = Duration(minutes: 15);
final List<_Pending> _entries = [];
int get length => _entries.length;
void add(
String scopeKey,
ReactionInfo info,
String reactingSender,
DateTime now,
) {
expire(now);
_entries.add(_Pending(scopeKey, info, reactingSender, now));
if (_entries.length > maxEntries) {
_drop(_entries.removeAt(0), now, 'queue full');
}
appLogger.info(
'Queued reaction ${info.emoji} from $reactingSender for unseen target '
'${info.targetHash} in $scopeKey (${_entries.length}/$maxEntries pending)',
tag: 'Reactions',
);
}
/// Re-attempt every live entry for [scopeKey]. [apply] receives the queued
/// reaction and the name of whoever sent it, and reports whether it found its
/// target; matched entries are removed, the rest stay.
void retry(
String scopeKey,
bool Function(ReactionInfo info, String reactingSender) apply,
DateTime now,
) {
expire(now);
_entries.removeWhere((entry) {
if (entry.scopeKey != scopeKey) return false;
if (!apply(entry.info, entry.reactingSender)) return false;
appLogger.info(
'Late-matched reaction ${entry.info.emoji} from ${entry.reactingSender} '
'to ${entry.info.targetHash} in $scopeKey after '
'${now.difference(entry.queuedAt).inSeconds}s',
tag: 'Reactions',
);
return true;
});
}
void expire(DateTime now) {
_entries.removeWhere((entry) {
if (now.difference(entry.queuedAt) < ttl) return false;
_drop(entry, now, 'expired after ${ttl.inMinutes}m');
return true;
});
}
void _drop(_Pending entry, DateTime now, String why) {
appLogger.warn(
'Dropping unmatched reaction ${entry.info.emoji} from '
'${entry.reactingSender} for target ${entry.info.targetHash} in '
'${entry.scopeKey} ($why, age ${now.difference(entry.queuedAt).inSeconds}s). '
'The message it points at was never seen locally.',
tag: 'Reactions',
);
}
}

Powered by TurnKey Linux.