watchApplicationCatalog method

Stream<List<PortalApplicationCategoryData>> watchApplicationCatalog()

Watches the current user's cached application catalog.

Cached data and favorite changes are emitted through the same Drift stream. Missing data is fetched before the first empty value is yielded; stale data is yielded immediately and refreshed in the background. Network errors are absorbed so cached data remains available.

Implementation

Stream<List<PortalApplicationCategoryData>> watchApplicationCatalog() async* {
  const ttl = Duration(days: 1);

  final user = await _database.select(_database.users).getSingleOrNull();
  if (user == null) {
    yield const [];
    return;
  }

  var skipNextStalenessCheck = user.applicationCatalogFetchedAt == null;
  if (skipNextStalenessCheck) {
    try {
      await refreshApplicationCatalog();
    } catch (_) {
      // Absorb: the initial query snapshot below yields empty or stale data.
    }
  }

  final categories = _database.portalApplicationCategories;
  final applications = _database.portalApplications;
  final favorites = _database.portalApplicationFavorites;
  final query =
      _database.select(categories).join([
          leftOuterJoin(
            applications,
            applications.category.equalsExp(categories.id),
          ),
          leftOuterJoin(
            favorites,
            favorites.user.equalsExp(categories.user) &
                favorites.applicationCode.equalsExp(applications.code),
          ),
        ])
        ..where(categories.user.equals(user.id))
        ..orderBy([
          OrderingTerm.asc(categories.position),
          OrderingTerm.asc(applications.position),
        ]);

  await for (final rows in query.watch()) {
    final data = _mapCatalogRows(rows);
    final currentUser = await (_database.select(
      _database.users,
    )..where((row) => row.id.equals(user.id))).getSingleOrNull();
    if (currentUser == null) {
      yield const [];
      return;
    }

    yield data;

    if (skipNextStalenessCheck) {
      skipNextStalenessCheck = false;
      continue;
    }
    final freshUser = await (_database.select(
      _database.users,
    )..where((row) => row.id.equals(user.id))).getSingleOrNull();
    final age = switch (freshUser?.applicationCatalogFetchedAt) {
      final fetchedAt? => DateTime.now().difference(fetchedAt),
      null => ttl,
    };
    if (age >= ttl) {
      try {
        await refreshApplicationCatalog();
      } catch (_) {
        // Absorb: stale data is already visible through the stream.
      }
    }
  }
}