add LogRing — bounded retention for Logger records (T-54)
The Logger only live-broadcasts; an output panel opening late would see no history. LogRing is a drop-oldest sink (default 2000 records) that keeps the recent window plus per-source and per-level bookkeeping — distinct sources for the panel's filter dropdown, level counts for the status-bar health badge. Flutter-free; the dock UI wraps it. First slice of the D-87 dock. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
/// Bounded in-memory retention for [Logger] records — the history the
|
||||
/// output dock (T-54 / D-87) reads on open.
|
||||
///
|
||||
/// The [Logger] only live-broadcasts to its stream; a panel that opens late
|
||||
/// would see nothing. [LogRing] is a sink that keeps the last [capacity]
|
||||
/// records (drop-oldest, same shape as the D-85 event ring) plus enough
|
||||
/// bookkeeping to drive the panel's filter dropdown (distinct [sources]) and
|
||||
/// the status-bar health badge (level counts).
|
||||
///
|
||||
/// Flutter-free (only `dart:async`/`dart:collection` + the [LogRecord] type)
|
||||
/// so it unit-tests without a widget harness. The UI wraps it; the ring
|
||||
/// itself holds no view concerns.
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
|
||||
import 'log.dart';
|
||||
|
||||
class LogRing {
|
||||
LogRing({this.capacity = 2000}) : assert(capacity > 0, 'capacity must be positive');
|
||||
|
||||
/// Maximum retained records. Oldest are dropped past this.
|
||||
final int capacity;
|
||||
|
||||
final ListQueue<LogRecord> _records = ListQueue<LogRecord>();
|
||||
// Per-source / per-level counts over the *retained* window, so [sources]
|
||||
// and [countAtLeast] stay accurate as records age out.
|
||||
final Map<String, int> _sourceCounts = {};
|
||||
final List<int> _levelCounts = List<int>.filled(LogLevel.values.length, 0);
|
||||
final StreamController<void> _changes = StreamController<void>.broadcast();
|
||||
|
||||
/// Fires after any mutation (add or clear). Listeners re-read [records].
|
||||
Stream<void> get changes => _changes.stream;
|
||||
|
||||
/// Snapshot of retained records, oldest first.
|
||||
List<LogRecord> get records => List<LogRecord>.unmodifiable(_records);
|
||||
|
||||
/// Distinct sources currently retained, sorted — drives the filter dropdown.
|
||||
List<String> get sources => _sourceCounts.keys.toList()..sort();
|
||||
|
||||
int get length => _records.length;
|
||||
bool get isEmpty => _records.isEmpty;
|
||||
|
||||
/// Append a record (the [Logger] sink). Drops the oldest past [capacity].
|
||||
void add(LogRecord r) {
|
||||
_records.addLast(r);
|
||||
_sourceCounts.update(r.source, (n) => n + 1, ifAbsent: () => 1);
|
||||
_levelCounts[r.level.index]++;
|
||||
while (_records.length > capacity) {
|
||||
_decr(_records.removeFirst());
|
||||
}
|
||||
_notify();
|
||||
}
|
||||
|
||||
/// Drop everything (the panel's "Clear" action).
|
||||
void clear() {
|
||||
if (_records.isEmpty) return;
|
||||
_records.clear();
|
||||
_sourceCounts.clear();
|
||||
for (var i = 0; i < _levelCounts.length; i++) {
|
||||
_levelCounts[i] = 0;
|
||||
}
|
||||
_notify();
|
||||
}
|
||||
|
||||
/// Count of retained records at [level] or higher — e.g.
|
||||
/// `countAtLeast(LogLevel.warn)` for the status-badge warn+error total.
|
||||
int countAtLeast(LogLevel level) {
|
||||
var n = 0;
|
||||
for (var i = level.index; i < _levelCounts.length; i++) {
|
||||
n += _levelCounts[i];
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
void _decr(LogRecord r) {
|
||||
final c = _sourceCounts[r.source];
|
||||
if (c != null) {
|
||||
if (c <= 1) {
|
||||
_sourceCounts.remove(r.source);
|
||||
} else {
|
||||
_sourceCounts[r.source] = c - 1;
|
||||
}
|
||||
}
|
||||
_levelCounts[r.level.index]--;
|
||||
}
|
||||
|
||||
void _notify() {
|
||||
if (!_changes.isClosed) _changes.add(null);
|
||||
}
|
||||
|
||||
void dispose() => _changes.close();
|
||||
}
|
||||
Reference in New Issue
Block a user