feat: check every subscription in the background and add the posts page

This commit is contained in:
randogoth 2026-10-11 13:20:00 +03:00
parent 953ae798eb
commit 1b426ccd8b
15 changed files with 688 additions and 58 deletions

View file

@ -277,6 +277,9 @@ class BrowserController {
currentPage.value = page;
error.value = null;
visitedUrls.value = {...visitedUrls.value, url};
// Opening a post is reading it. Nothing happens when the address is
// not one, so this needs no knowledge of the reader's subscriptions.
await _db.markEntryRead(url);
if (!notModified) {
await _db.saveCachedPage(CachedPagesCompanion.insert(
url: url,

View file

@ -1,18 +1,34 @@
import 'package:actors/actors.dart';
import 'package:drift/drift.dart' show Value;
import 'package:signals/signals.dart';
import '../../database/database.dart';
import '../../mews/document.dart';
import '../actors/fetch_actor.dart';
/// Which sites the reader subscribes to (spec 6). The posts themselves live in
/// the database, so an unread post survives a restart; only the set of
/// addresses is held here, because that is what the chrome watches.
class SubscriptionsController {
SubscriptionsController(this._db);
SubscriptionsController({
required MewsDatabase db,
required Actor<FetchCommand, FetchResult> fetch,
}) : _db = db,
_fetch = fetch;
final MewsDatabase _db;
final Actor<FetchCommand, FetchResult> _fetch;
final subscribedUrls = signal(<String>{});
/// Whether a check is running, so a page can say so instead of looking idle.
final refreshing = signal(false);
/// Called after a check changes something a page would show. Set by the
/// dependency container to the navigator's rerender; holding the navigator
/// directly would make the wiring circular.
void Function()? onChanged;
Future<void> load() async {
final rows = await _db.allSubscriptions();
subscribedUrls.value = rows.map((row) => row.url).toSet();
@ -47,4 +63,85 @@ class SubscriptionsController {
await _db.touchSubscription(page.url, DateTime.now());
return fresh;
}
/// Checks every subscribed site for posts. Sites are checked one after
/// another on purpose: the fetch actor answers concurrent sends, so a
/// parallel pass would open one request per site at once against small
/// servers, for a wait nobody is sitting through.
Future<void> refreshAll() async {
if (refreshing.value) return;
refreshing.value = true;
onChanged?.call();
try {
for (final site in await _db.allSubscriptions()) {
await _check(site.url);
}
} finally {
refreshing.value = false;
onChanged?.call();
}
}
/// Checks one site, on the reader's request.
Future<void> refreshOne(String siteUrl) async {
if (refreshing.value || !isSubscribed(siteUrl)) return;
refreshing.value = true;
onChanged?.call();
try {
await _check(siteUrl);
} finally {
refreshing.value = false;
onChanged?.call();
}
}
Future<void> markRead(String url, {String? siteUrl}) async {
await _db.markEntryRead(url, siteUrl: siteUrl);
onChanged?.call();
}
Future<void> markUnread(String url, {String? siteUrl}) async {
await _db.markEntryUnread(url, siteUrl: siteUrl);
onChanged?.call();
}
/// Fetches one site and stores whatever it publishes. A site that cannot be
/// reached is still recorded as checked and keeps the posts already held, so
/// one site being down never stops the rest of the pass.
Future<void> _check(String siteUrl) async {
try {
final cached = await _db.cachedPage(siteUrl);
final result = await _fetch.send(FetchPage(
url: siteUrl,
etag: cached?.etag,
lastModified: cached?.lastModified,
cachedSource: cached?.source,
));
if (result case FetchPageOk(
:final page,
:final source,
:final etag,
:final lastModified,
:final notModified,
)) {
await _db.describeSubscription(siteUrl,
title: page.title, atomHref: page.atomFeedHref);
await _db.addEntries(siteUrl, page.entries);
if (!notModified) {
// Keep the cache current so the site also reads offline, but do not
// record a visit: the reader did not open it.
await _db.saveCachedPage(CachedPagesCompanion.insert(
url: siteUrl,
source: source,
title: Value(page.title),
etag: Value(etag),
lastModified: Value(lastModified),
));
}
}
} catch (_) {
// Nothing to tell the reader: the site's "last checked" date says it.
}
await _db.touchSubscription(siteUrl, DateTime.now());
}
}