watchCalendarEvents method

Stream<List<CalendarEvent>> watchCalendarEvents({
  1. required DateTime startDate,
  2. required DateTime endDate,
})

Watches calendar events overlapping the given date range.

Reads events from the local DB and re-emits when the DB is updated. If the cached window is missing, stale, or doesn't cover the requested range (e.g., semester list just expanded), this stream awaits a refresh before yielding so it can populate or update the cache.

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

Implementation

Stream<List<CalendarEvent>> watchCalendarEvents({
  required DateTime startDate,
  required DateTime endDate,
}) async* {
  const ttl = Duration(days: 1);

  // Gate: refreshCalendarEvents uses delete+insert, which reassigns
  // autoincrement IDs. Drift .watch() re-emits on every ID change, which
  // would re-trigger this branch forever for permanently out-of-window
  // requests that no refresh can ever cover.
  var refreshedForOutOfWindow = false;

  // Lazily computed and cached; re-read once when out-of-window to catch
  // semesters that landed after the stream started.
  (DateTime, DateTime)? window;

  await for (final events in _eventsOverlapping(startDate, endDate).watch()) {
    final user = await _database.select(_database.users).getSingleOrNull();
    if (user == null) {
      yield [];
      continue;
    }

    window ??= await _computeWindow();
    var (windowStart, windowEnd) = window;
    var outOfWindow =
        startDate.isBefore(windowStart) || endDate.isAfter(windowEnd);

    // If out of window, re-compute once in case the semester list expanded
    // since the stream started.
    if (outOfWindow && !refreshedForOutOfWindow) {
      window = await _computeWindow();
      (windowStart, windowEnd) = window;
      outOfWindow =
          startDate.isBefore(windowStart) || endDate.isAfter(windowEnd);
    }

    // calendarFetchedAt is authoritative for cache presence within the
    // window. For out-of-window requests, try once in case the window has
    // widened since the cached stamp (e.g., a newly enrolled semester).
    final shouldRefresh =
        user.calendarFetchedAt == null ||
        (outOfWindow && !refreshedForOutOfWindow);
    if (shouldRefresh) {
      if (outOfWindow) refreshedForOutOfWindow = true;
      try {
        await refreshCalendarEvents();
      } catch (_) {
        // Absorb: yield below so UI exits loading state
      }
    }

    yield events;

    final freshUser = await _database
        .select(_database.users)
        .getSingleOrNull();
    final age = switch (freshUser?.calendarFetchedAt) {
      final t? => DateTime.now().difference(t),
      null => ttl,
    };
    if (age >= ttl) {
      try {
        await refreshCalendarEvents();
      } catch (_) {
        // Absorb: stale data is shown via stream
      }
    }
  }
}