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

@ -8,6 +8,8 @@ import 'package:mews/application/controllers/subscriptions_controller.dart';
import 'package:mews/database/database.dart';
import 'package:mews/mews/document.dart';
import 'fake_fetch.dart';
void main() {
late MewsDatabase db;
@ -61,7 +63,7 @@ void main() {
});
test('subscribing takes on the back catalogue without flagging it', () async {
final subscriptions = SubscriptionsController(db);
final subscriptions = SubscriptionsController(db: db, fetch: FakeFetch(const {}));
await subscriptions.load();
final firstPost = MewsEntry(
@ -97,7 +99,7 @@ void main() {
});
test('unread posts survive a restart', () async {
final subscriptions = SubscriptionsController(db);
final subscriptions = SubscriptionsController(db: db, fetch: FakeFetch(const {}));
await subscriptions.load();
await subscriptions.toggle(_page('https://example.com/', const []));
await subscriptions.refreshSite(_page('https://example.com/', [
@ -109,14 +111,14 @@ void main() {
// A fresh controller over the same database sees the same unread post,
// which the old in-memory count could not do.
final restarted = SubscriptionsController(db);
final restarted = SubscriptionsController(db: db, fetch: FakeFetch(const {}));
await restarted.load();
expect(restarted.isSubscribed('https://example.com/'), isTrue);
expect(await db.unreadCount(), 1);
});
test('leaving a site takes its posts with it', () async {
final subscriptions = SubscriptionsController(db);
final subscriptions = SubscriptionsController(db: db, fetch: FakeFetch(const {}));
await subscriptions.load();
await subscriptions.toggle(_page('https://example.com/', const []));
await subscriptions.refreshSite(_page('https://example.com/', [
@ -132,6 +134,79 @@ void main() {
expect(await db.unreadCount(), 0);
});
test('a check finds new posts on every subscribed site', () async {
const siteA = 'https://a.example/';
const siteB = 'https://b.example/';
final sources = {
siteA: _entriesPage('Site A', const []),
siteB: _entriesPage('Site B', const []),
};
final subscriptions =
SubscriptionsController(db: db, fetch: FakeFetch(sources));
await subscriptions.load();
await subscriptions.toggle(_page(siteA, const []));
await subscriptions.toggle(_page(siteB, const []));
// Both sites publish something after the reader subscribed.
sources[siteA] = _entriesPage('Site A', const [
('2026-10-09', 'a-one.html', 'A newer post'),
]);
sources[siteB] = _entriesPage('Site B', const [
('2026-10-10', 'b-one.html', 'Another post'),
('2026-10-09', 'b-two.html', 'And one more'),
]);
await subscriptions.refreshAll();
expect(await db.unreadCount(), 3);
expect(await db.unreadCount(siteUrl: siteA), 1);
expect(subscriptions.refreshing.value, isFalse);
// Every site was recorded as checked.
for (final site in await db.allSubscriptions()) {
expect(site.lastCheckedAt, isNotNull);
}
});
test('one unreachable site does not stop the rest of the check', () async {
const reachable = 'https://a.example/';
const unreachable = 'https://down.example/';
final subscriptions = SubscriptionsController(
db: db,
// Only the first site answers; the other has no source at all.
fetch: FakeFetch({
reachable: _entriesPage('Site A', const [
('2026-10-09', 'a-one.html', 'A newer post'),
]),
}),
);
await subscriptions.load();
await subscriptions.toggle(_page(unreachable, const []));
await subscriptions.toggle(_page(reachable, const []));
await subscriptions.refreshAll();
expect(await db.unreadCount(siteUrl: reachable), 1);
expect(subscriptions.refreshing.value, isFalse);
// The site that could not be reached is still marked as checked.
final down = (await db.allSubscriptions())
.firstWhere((s) => s.url == unreachable);
expect(down.lastCheckedAt, isNotNull);
});
test('a check already running is not started again', () async {
final subscriptions = SubscriptionsController(
db: db,
fetch: FakeFetch({'https://a.example/': _entriesPage('Site A', const [])}),
);
await subscriptions.load();
await subscriptions.toggle(_page('https://a.example/', const []));
var redraws = 0;
subscriptions.onChanged = () => redraws++;
await Future.wait([subscriptions.refreshAll(), subscriptions.refreshAll()]);
// One pass: it starts and finishes once, so two redraws, not four.
expect(redraws, 2);
});
test('caches pages for offline reopening', () async {
await db.saveCachedPage(CachedPagesCompanion.insert(
url: 'https://example.com/page.html',
@ -161,3 +236,20 @@ MewsPage _page(String url, List<MewsEntry> entries) => MewsPage(
entries: entries,
specVersion: '0.1',
);
/// A Mews page whose list holds dated links (spec 6.1), one per entry given as
/// (date, file name, title).
String _entriesPage(
String title, List<(String, String, String)> entries) =>
'''
<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml">
<head><title>$title</title><meta name="mews-profile" content="0.1" /></head>
<body>
<h1>$title</h1>
<ul>
${entries.map((e) => ' <li><a href="${e.$2}">${e.$1} ${e.$3}</a></li>').join('\n')}
</ul>
</body>
</html>
''';