Some checks are pending
Backend CI / test (push) Waiting to run
Backend CI / build (push) Blocked by required conditions
Mobile App CI / test (push) Waiting to run
Mobile App CI / build-android (push) Blocked by required conditions
Mobile App CI / build-ios (push) Blocked by required conditions
Web App CI / test (push) Waiting to run
Web App CI / build (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
Deploy Telegram Bot / Deploy Telegram Bot (push) Waiting to run
197 lines
No EOL
5.5 KiB
Dart
197 lines
No EOL
5.5 KiB
Dart
import 'dart:async';
|
|
import 'dart:collection';
|
|
import 'dart:math';
|
|
|
|
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 = 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: 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).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: DateTime.now()),
|
|
);
|
|
}
|
|
}
|
|
} |