mnemo_cards/mnemo_cards_backend/lib/statistics/session_tracker.dart
Dmitry 1ab5071345
Some checks are pending
Backend CI / test (push) Waiting to run
Backend CI / build (push) Blocked by required conditions
Deploy Admin Panel / Deploy Admin Panel (push) Waiting to run
Deploy Admin Panel / Admin Panel Verification (push) Blocked by required conditions
Deploy Mnemo Cards / Deploy Backend (push) Waiting to run
Deploy Mnemo Cards / Deploy Web App (push) Blocked by required conditions
Deploy Mnemo Cards / Final Verification (push) Blocked by required conditions
f
2025-12-14 02:35:14 +03:00

198 lines
No EOL
5.6 KiB
Dart

import 'dart:async';
import 'dart:collection';
import 'dart:math';
import 'package:drift_postgres/drift_postgres.dart';
import 'package:injectable/injectable.dart';
import 'package:mnemo_cards_backend/database/database.dart';
import 'package:drift/drift.dart' as drift;
@lazySingleton
class SessionTracker {
final AppDatabase _db;
/// Active sessions cache: userId -> sessionId
final Map<String, String> _activeSessions = {};
/// Session timers for automatic timeout: sessionId -> timer
final Map<String, Timer> _sessionTimers = {};
/// Session timeout duration (30 minutes by default)
static const Duration _sessionTimeout = Duration(minutes: 30);
SessionTracker(this._db);
/// Get or create an active session for a user
///
/// Returns the session ID of the active session.
/// If no active session exists, creates a new one.
Future<String> getOrCreateSession(
String userId, {
String? packId,
String? testId,
}) async {
// Check if user already has an active session
if (_activeSessions.containsKey(userId)) {
final sessionId = _activeSessions[userId]!;
// Reset timeout timer
_resetSessionTimer(sessionId);
// Update session activity
await _updateSessionActivity(sessionId);
return sessionId;
}
// Create new session
final sessionId = _generateSessionId();
final now = PgDateTime(DateTime.now());
await _db.statisticsDao.createSession(
StudySessionsCompanion.insert(
userId: userId,
sessionId: drift.Value(sessionId),
startTime: now,
packId: drift.Value(packId),
testId: drift.Value(testId),
),
);
// Cache active session
_activeSessions[userId] = sessionId;
// Start timeout timer
_startSessionTimer(sessionId);
return sessionId;
}
/// End a session manually
Future<void> endSession(
String sessionId, {
int? wordsLearned,
int? testsCompleted,
double? accuracy,
}) async {
// Cancel timer
_sessionTimers[sessionId]?.cancel();
_sessionTimers.remove(sessionId);
// Find user for this session
final matchingEntries = _activeSessions.entries
.where((entry) => entry.value == sessionId)
.toList();
final userId = matchingEntries.isNotEmpty ? matchingEntries.first.key : null;
if (userId != null) {
_activeSessions.remove(userId);
}
// Get session by sessionId to find its database ID
final session = await _db.statisticsDao.getSessionBySessionId(sessionId);
if (session == null) return;
// Update session in database using database ID
await _db.statisticsDao.endSession(
session.id,
wordsLearned: wordsLearned,
testsCompleted: testsCompleted,
accuracy: accuracy,
);
}
/// Update session statistics
Future<void> updateSessionStats(
String sessionId, {
int? wordsLearned,
int? testsCompleted,
double? accuracy,
}) async {
// Find session by sessionId
final session = await _db.statisticsDao.getSessionBySessionId(sessionId);
if (session == null) return;
// Update session
final updatedSession = session.copyWith(
wordsLearned: wordsLearned ?? session.wordsLearned,
testsCompleted: testsCompleted ?? session.testsCompleted,
accuracy: accuracy ?? session.accuracy,
updatedAt: PgDateTime(DateTime.now()),
);
await _db.statisticsDao.updateSession(updatedSession);
}
/// Get active session for user
Future<StudySession?> getActiveSession(String userId) async {
final activeSessions = await _db.statisticsDao.getActiveSessions(userId);
return activeSessions.isNotEmpty ? activeSessions.first : null;
}
/// Get session history for user
Future<List<StudySession>> getUserSessions(
String userId, {
int? limit,
DateTime? fromDate,
DateTime? toDate,
}) async {
return await _db.statisticsDao.getSessionsByUserId(
userId,
limit: limit,
fromDate: fromDate,
toDate: toDate,
);
}
/// Clean up expired sessions (called by cron job)
Future<void> cleanupExpiredSessions() async {
// End all active sessions that have timed out
final now = DateTime.now();
final expiredSessions = <String>[];
for (final entry in _activeSessions.entries) {
final sessionId = entry.value;
final session = await _db.statisticsDao.getSessionBySessionId(sessionId);
if (session != null &&
session.endTime == null &&
now.difference(session.startTime.dateTime).inMinutes > _sessionTimeout.inMinutes) {
expiredSessions.add(sessionId);
}
}
for (final sessionId in expiredSessions) {
await endSession(sessionId);
}
}
/// Generate a unique session ID
String _generateSessionId() {
final random = Random();
final bytes = List<int>.generate(16, (i) => random.nextInt(256));
return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join();
}
/// Start timeout timer for session
void _startSessionTimer(String sessionId) {
_sessionTimers[sessionId] = Timer(_sessionTimeout, () async {
await endSession(sessionId);
});
}
/// Reset timeout timer for session
void _resetSessionTimer(String sessionId) {
_sessionTimers[sessionId]?.cancel();
_startSessionTimer(sessionId);
}
/// Update session activity timestamp
Future<void> _updateSessionActivity(String sessionId) async {
final session = await _db.statisticsDao.getSessionBySessionId(sessionId);
if (session != null) {
await _db.statisticsDao.updateSession(
session.copyWith(updatedAt: PgDateTime(DateTime.now())),
);
}
}
}