watchStudents method

Stream<List<Student>> watchStudents(
  1. int courseOfferingId
)

Watches the I-School Plus roster (classmates) for a course offering.

Emits the cached roster immediately (ordered by student ID), then triggers a background network fetch if the roster is empty or stale. The stream re-emits automatically when the DB is updated.

Network errors during background refresh are absorbed — the stream continues showing stale (or empty) data rather than erroring.

Implementation

Stream<List<Student>> watchStudents(int courseOfferingId) async* {
  const ttl = Duration(days: 1);

  final query =
      _database.select(_database.courseOfferingStudents).join([
          innerJoin(
            _database.students,
            _database.students.id.equalsExp(
              _database.courseOfferingStudents.student,
            ),
          ),
        ])
        ..where(
          _database.courseOfferingStudents.courseOffering.equals(
            courseOfferingId,
          ),
        )
        ..orderBy([OrderingTerm.asc(_database.students.studentId)]);

  await for (final rows in query.watch()) {
    final students = [
      for (final row in rows) row.readTable(_database.students),
    ];

    if (students.isEmpty) {
      try {
        await refreshStudents(courseOfferingId);
      } catch (_) {
        // Absorb: yield empty below so UI exits loading state
      }
    }

    yield students;

    final offering = await (_database.select(
      _database.courseOfferings,
    )..where((o) => o.id.equals(courseOfferingId))).getSingleOrNull();
    if (offering == null) return;

    final age = switch (offering.rosterFetchedAt) {
      final t? => DateTime.now().difference(t),
      null => ttl,
    };
    if (age >= ttl) {
      try {
        await refreshStudents(courseOfferingId);
      } catch (_) {
        // Absorb: stale data is shown via stream
      }
    }
  }
}