208 lines
6.5 KiB
Dart
208 lines
6.5 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
|
|
import 'questions_db.dart';
|
|
import 'signalr/questions_hub_connections.dart';
|
|
import 'trading/prospective_guess_assignments_db.dart';
|
|
|
|
/// Creates questions in Postgres and delivers them over SignalR.
|
|
class QuestionService {
|
|
QuestionService({
|
|
required QuestionsDb questionsDb,
|
|
required QuestionsHubConnections hubConnections,
|
|
}) : _questionsDb = questionsDb,
|
|
_hubConnections = hubConnections;
|
|
|
|
final QuestionsDb _questionsDb;
|
|
final QuestionsHubConnections _hubConnections;
|
|
|
|
ProspectiveGuessAssignmentsDb get _assignmentsDb =>
|
|
ProspectiveGuessAssignmentsDb(_questionsDb.connection);
|
|
|
|
/// Called at login: ensures one unanswered prospective question when possible.
|
|
Future<Map<String, dynamic>?> bootstrapOnLogin(
|
|
String firebaseUid,
|
|
) async {
|
|
await _questionsDb.ensureUserExists(firebaseUid);
|
|
return ensureProspectiveQuestionQueued(firebaseUid);
|
|
}
|
|
|
|
/// Creates the next slot-based prospective question when the queue is empty.
|
|
Future<Map<String, dynamic>?> ensureProspectiveQuestionQueued(
|
|
String firebaseUid, {
|
|
DateTime? now,
|
|
}) async {
|
|
await _questionsDb.ensureUserExists(firebaseUid);
|
|
|
|
final String? pendingQuestionId =
|
|
await _assignmentsDb.findPendingQuestionId(firebaseUid);
|
|
if (pendingQuestionId != null) {
|
|
final Map<String, dynamic>? question = await _questionsDb.findQuestionById(
|
|
questionId: pendingQuestionId,
|
|
assignedUserId: firebaseUid,
|
|
);
|
|
if (question != null && question['userResponse'] == null) {
|
|
final int unansweredCount =
|
|
await _questionsDb.countUnansweredQuestions(firebaseUid);
|
|
return _questionsDb.toClientPayload(
|
|
question,
|
|
unansweredCount: unansweredCount > 0 ? unansweredCount : 1,
|
|
);
|
|
}
|
|
}
|
|
|
|
final int queued =
|
|
await _questionsDb.countUnansweredQuestions(firebaseUid);
|
|
if (queued > 0) {
|
|
final Map<String, dynamic>? existing =
|
|
await _questionsDb.findUnansweredQuestion(firebaseUid);
|
|
if (existing == null) {
|
|
return null;
|
|
}
|
|
return _questionsDb.toClientPayload(
|
|
existing,
|
|
unansweredCount: queued,
|
|
);
|
|
}
|
|
|
|
final Map<String, dynamic>? prospective =
|
|
await _createProspectiveQuestionWithAssignment(
|
|
firebaseUid,
|
|
now: now,
|
|
);
|
|
if (prospective == null) {
|
|
return null;
|
|
}
|
|
|
|
return prospective;
|
|
}
|
|
|
|
/// Picks, creates, and records one prospective question when none is queued.
|
|
///
|
|
/// Retries when a concurrent request claims the same user/slot/symbol first.
|
|
Future<Map<String, dynamic>?> _createProspectiveQuestionWithAssignment(
|
|
String firebaseUid, {
|
|
DateTime? now,
|
|
}) async {
|
|
for (var attempt = 0; attempt < 8; attempt++) {
|
|
final Map<String, dynamic>? picked =
|
|
await _questionsDb.pickProspectiveQuestionForUser(
|
|
firebaseUid,
|
|
now: now,
|
|
);
|
|
if (picked == null) {
|
|
return null;
|
|
}
|
|
|
|
final DateTime olderSlotStart = DateTime.parse(
|
|
picked['olderSlotStart']! as String,
|
|
);
|
|
final DateTime newerSlotStart = DateTime.parse(
|
|
picked['newerSlotStart']! as String,
|
|
);
|
|
final String symbol = picked['symbol']! as String;
|
|
|
|
if (await _assignmentsDb.hasAssignmentForSymbolSlotPair(
|
|
firebaseUid: firebaseUid,
|
|
olderSlotStart: olderSlotStart,
|
|
newerSlotStart: newerSlotStart,
|
|
symbol: symbol,
|
|
)) {
|
|
continue;
|
|
}
|
|
|
|
final Map<String, dynamic> question = await _questionsDb.createQuestion(
|
|
assignedUserId: firebaseUid,
|
|
questionText: picked['questionText']! as String,
|
|
correctAnswer: picked['correctAnswer']! as num,
|
|
sourceTag: 'market_history:prospective',
|
|
pipelineKey: 'trading',
|
|
pipelineStep: 'guess_weekly_move:await_answer',
|
|
metadata: <String, dynamic>{
|
|
'prospective_question_id': picked['id'],
|
|
'symbol': symbol,
|
|
'older_slot_start': picked['olderSlotStart'],
|
|
'newer_slot_start': picked['newerSlotStart'],
|
|
'price_delta_pct': picked['priceDeltaPct'],
|
|
},
|
|
);
|
|
|
|
final bool assigned = await _assignmentsDb.insertPendingIfAbsent(
|
|
firebaseUid: firebaseUid,
|
|
olderSlotStart: olderSlotStart,
|
|
newerSlotStart: newerSlotStart,
|
|
symbol: symbol,
|
|
prospectiveQuestionId: picked['id']! as String,
|
|
questionId: question['id']! as String,
|
|
);
|
|
if (!assigned) {
|
|
await _questionsDb.deleteUnansweredQuestion(
|
|
questionId: question['id']! as String,
|
|
assignedUserId: firebaseUid,
|
|
);
|
|
continue;
|
|
}
|
|
|
|
return _questionsDb.toClientPayload(
|
|
question,
|
|
unansweredCount: 1,
|
|
);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Inserts a question and pushes it to connected SignalR clients.
|
|
Future<Map<String, dynamic>> createAndDeliverQuestion({
|
|
required String assignedUserId,
|
|
required String questionText,
|
|
required num correctAnswer,
|
|
String? sourceTag,
|
|
String? pipelineKey,
|
|
String? pipelineStep,
|
|
Map<String, dynamic>? metadata,
|
|
}) async {
|
|
final Map<String, dynamic> question = await _questionsDb.createQuestion(
|
|
assignedUserId: assignedUserId,
|
|
questionText: questionText,
|
|
correctAnswer: correctAnswer,
|
|
sourceTag: sourceTag,
|
|
pipelineKey: pipelineKey,
|
|
pipelineStep: pipelineStep,
|
|
metadata: metadata,
|
|
);
|
|
final int unansweredCount =
|
|
await _questionsDb.countUnansweredQuestions(assignedUserId);
|
|
final Map<String, dynamic> payload = _questionsDb.toClientPayload(
|
|
question,
|
|
unansweredCount: unansweredCount,
|
|
);
|
|
await _hubConnections.pushQuestion(assignedUserId, payload);
|
|
return payload;
|
|
}
|
|
|
|
/// On SignalR connect: deliver an existing unanswered question only (no create).
|
|
Future<void> deliverPendingQuestionOnConnect(
|
|
QuestionsHubConnection connection,
|
|
) async {
|
|
try {
|
|
final String uid = connection.firebaseUid;
|
|
final Map<String, dynamic>? question =
|
|
await _questionsDb.findUnansweredQuestion(uid);
|
|
if (question == null) {
|
|
return;
|
|
}
|
|
final int unansweredCount =
|
|
await _questionsDb.countUnansweredQuestions(uid);
|
|
final Map<String, dynamic> payload = _questionsDb.toClientPayload(
|
|
question,
|
|
unansweredCount: unansweredCount,
|
|
);
|
|
await _hubConnections.pushQuestionToConnection(connection, payload);
|
|
} catch (e, st) {
|
|
stderr.writeln(
|
|
'Failed to deliver pending question for ${connection.firebaseUid}: $e\n$st',
|
|
);
|
|
}
|
|
}
|
|
}
|