mergeCacheWithNewStats method

PersistedStatsCache mergeCacheWithNewStats({
  1. required PersistedStatsCache existingCache,
  2. required ProcessedStats newStats,
  3. required String newLastComputedDate,
})

Merge new stats into an existing cache. Used when incrementally adding new days to the cache.

Implementation

PersistedStatsCache mergeCacheWithNewStats({
  required PersistedStatsCache existingCache,
  required ProcessedStats newStats,
  required String newLastComputedDate,
}) {
  // Merge daily activity — combine by date.
  final dailyActivityMap = <String, DailyActivity>{};
  for (final day in existingCache.dailyActivity) {
    dailyActivityMap[day.date] = day.copyWith();
  }
  for (final day in newStats.dailyActivity) {
    final existing = dailyActivityMap[day.date];
    if (existing != null) {
      existing.messageCount += day.messageCount;
      existing.sessionCount += day.sessionCount;
      existing.toolCallCount += day.toolCallCount;
    } else {
      dailyActivityMap[day.date] = day.copyWith();
    }
  }

  // Merge daily model tokens — combine by date.
  final dailyModelTokensMap = <String, Map<String, int>>{};
  for (final day in existingCache.dailyModelTokens) {
    dailyModelTokensMap[day.date] = Map<String, int>.from(day.tokensByModel);
  }
  for (final day in newStats.dailyModelTokens) {
    final existing = dailyModelTokensMap[day.date];
    if (existing != null) {
      for (final entry in day.tokensByModel.entries) {
        existing[entry.key] = (existing[entry.key] ?? 0) + entry.value;
      }
    } else {
      dailyModelTokensMap[day.date] = Map<String, int>.from(
        day.tokensByModel,
      );
    }
  }

  // Merge model usage.
  final modelUsage = Map<String, ModelUsage>.from(existingCache.modelUsage);
  for (final entry in newStats.modelUsage.entries) {
    final existing = modelUsage[entry.key];
    if (existing != null) {
      modelUsage[entry.key] = existing.merge(entry.value);
    } else {
      modelUsage[entry.key] = entry.value;
    }
  }

  // Merge hour counts.
  final hourCounts = Map<int, int>.from(existingCache.hourCounts);
  for (final entry in newStats.hourCounts.entries) {
    hourCounts[entry.key] = (hourCounts[entry.key] ?? 0) + entry.value;
  }

  // Update session aggregates.
  final totalSessions =
      existingCache.totalSessions + newStats.sessionStats.length;
  final totalMessages =
      existingCache.totalMessages +
      newStats.sessionStats.fold<int>(0, (sum, s) => sum + s.messageCount);

  // Find longest session.
  SessionStats? longestSession = existingCache.longestSession;
  for (final session in newStats.sessionStats) {
    if (longestSession == null ||
        session.duration > longestSession.duration) {
      longestSession = session;
    }
  }

  // Find first session date.
  String? firstSessionDate = existingCache.firstSessionDate;
  for (final session in newStats.sessionStats) {
    if (firstSessionDate == null ||
        session.timestamp.compareTo(firstSessionDate) < 0) {
      firstSessionDate = session.timestamp;
    }
  }

  final sortedDailyActivity = dailyActivityMap.values.toList()
    ..sort((a, b) => a.date.compareTo(b.date));

  final sortedDailyModelTokens =
      dailyModelTokensMap.entries
          .map((e) => DailyModelTokens(date: e.key, tokensByModel: e.value))
          .toList()
        ..sort((a, b) => a.date.compareTo(b.date));

  var result = PersistedStatsCache(
    version: statsCacheVersion,
    lastComputedDate: newLastComputedDate,
    dailyActivity: sortedDailyActivity,
    dailyModelTokens: sortedDailyModelTokens,
    modelUsage: modelUsage,
    totalSessions: totalSessions,
    totalMessages: totalMessages,
    longestSession: longestSession,
    firstSessionDate: firstSessionDate,
    hourCounts: hourCounts,
    totalSpeculationTimeSavedMs:
        existingCache.totalSpeculationTimeSavedMs +
        newStats.totalSpeculationTimeSavedMs,
  );

  if (shotStatsEnabled.value) {
    final shotDistribution = Map<int, int>.from(
      existingCache.shotDistribution ?? {},
    );
    for (final entry in (newStats.shotDistribution ?? {}).entries) {
      shotDistribution[entry.key] =
          (shotDistribution[entry.key] ?? 0) + entry.value;
    }
    result = result.copyWith(shotDistribution: shotDistribution);
  }

  return result;
}