dueTodayItems function
Joined view of due cards with their topic + domain metadata. Keeps the join in one place so the Home screen widget tree stays presentational. Topics are batch-fetched in a single round-trip (was N+1 by topic).
Implementation
@riverpod
Future<List<DueTodayItem>> dueTodayItems(Ref ref) async {
final cards = await ref.watch(dueCardsProvider.future);
if (cards.isEmpty) return const [];
final ds = ref.watch(supabaseDatasourceProvider);
// De-dupe topic IDs — multiple cards can share a topic — and batch the
// fetch so we make one HTTP round-trip regardless of due-list size.
final topicIds = cards.map((c) => c.topicId).toSet();
final fetched = await ds.getTopicsByIds(topicIds);
final topics = {for (final t in fetched) t.id: t};
// Map domainId → (slug, name) once.
final domainIds = topics.values
.map((t) => t.domainId)
.whereType<String>()
.toSet();
final allDomains = await ds.getMyDomains();
final domainsById = {for (final d in allDomains) d.id: d};
final missing = domainIds
.where((id) => !domainsById.containsKey(id))
.toList();
if (missing.isNotEmpty) {
final extra = await ds.getDomainsByIds(missing.toSet());
for (final d in extra) {
domainsById[d.id] = d;
}
}
final out = <DueTodayItem>[];
for (final c in cards) {
final t = topics[c.topicId];
if (t == null) continue;
final domain = domainsById[t.domainId];
out.add(
DueTodayItem(
cardId: c.id,
topicId: c.topicId,
topicTitle: t.title,
domainSlug: domain?.slug ?? '',
domainName: domain?.name ?? '',
),
);
}
return out;
}