watchSyllabuses method
- required String courseNumber,
- required SyllabusLanguage language,
Watches every submitted language syllabus for the offering identified
by courseNumber.
Emits a non-empty cached aggregate immediately. Missing or stale cache entries are fetched before the first emission when no teacher has a submitted syllabus, or revalidated after cached submissions are emitted. Unsubmitted syllabuses retain cache metadata but are omitted from results. Refresh failures are absorbed so stale data remains visible and an empty list leaves loading. Missing offerings and offerings without teachers emit an empty list without making a request.
Implementation
// TODO: a looked-up offering is not persisted, so it has no row for
// Syllabuses.courseOffering to reference and this emits an empty list for
// it. Only reachable once the UI can open a course the student is not
// enrolled in; needs SyllabusDetail to accept DTO-backed rows, like
// CourseOfferingDetail.
Stream<List<TeacherSyllabusDetail>> watchSyllabuses({
required String courseNumber,
required SyllabusLanguage language,
}) async* {
const ttl = Duration(minutes: 15);
final context = await _readSyllabusContext(courseNumber);
if (context == null || context.teachers.isEmpty) {
yield [];
return;
}
final offering = context.offering;
final associatedTeachers = context.teachers;
final teacherIds = associatedTeachers
.map((entry) => entry.teacher.id)
.toList();
final syllabuses = _database.syllabuses;
final sections = _database.syllabusSections;
final teachers = _database.teachers;
final query =
_database.select(syllabuses).join([
innerJoin(teachers, teachers.id.equalsExp(syllabuses.teacher)),
leftOuterJoin(
sections,
sections.syllabus.equalsExp(syllabuses.id),
),
])
..where(
syllabuses.courseOffering.equals(offering.id) &
syllabuses.teacher.isIn(teacherIds) &
syllabuses.language.equalsValue(language),
)
..orderBy([
.asc(teachers.code),
.asc(sections.position),
]);
var attemptedRefresh = false;
await for (final rows in query.watch()) {
final details = _mapSyllabusRows(rows);
final cachedByTeacher = {
for (final row in rows)
row.readTable(_database.syllabuses).teacher: row.readTable(
_database.syllabuses,
),
};
final now = DateTime.now();
final needsRefresh = associatedTeachers.any((associatedTeacher) {
final cached = cachedByTeacher[associatedTeacher.teacher.id];
return cached == null || now.difference(cached.fetchedAt) >= ttl;
});
if (details.isEmpty) {
if (needsRefresh && !attemptedRefresh) {
attemptedRefresh = true;
try {
await refreshSyllabuses(
courseNumber: courseNumber,
language: language,
);
yield _mapSyllabusRows(await query.get());
continue;
} catch (_) {
// Absorb: yield an empty list so the UI exits its loading state.
}
}
yield [];
continue;
}
yield details;
if (needsRefresh && !attemptedRefresh) {
attemptedRefresh = true;
try {
await refreshSyllabuses(
courseNumber: courseNumber,
language: language,
);
} catch (_) {
// Absorb: the cached details emitted above remain visible.
}
}
}
}