perf(learn): local reads no longer wait on the instance

Every read of the person's content refreshes their keys, and a refresh
asked the instance whether the account has keys until it once answered
yes, which an account signing in with SimpleLogin alone never does. So
on a poor connection every read waited on a request: the library's
progress, a request per studyset in a row, took over five minutes, and
Home was slow too.

A device holding the keys already knows the account has them, so the
keys now open first and the instance is asked only while no other sign
says so (a device without the keys still asks at every refresh).

Whats-New: The library and Home open quickly again without a good connection.
Whats-New-De: Die Bibliothek und die Startseite öffnen sich ohne gute Verbindung wieder schnell.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-10 22:39:10 +01:00
commit 1781553f1d
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
5 changed files with 44 additions and 137 deletions

View file

@ -88,7 +88,17 @@ class ContentKeys(
Future<void> refresh() => _refreshing.run();
Future<void> _refresh() async {
if (ledger.registerHasKeys != true && registerHasKeys != null) {
if (!_unlocked) {
final account = await _accountKeys();
if (account != null) {
await cipher.unlock(account);
_unlocked = true;
}
}
// Asked only while nothing else says the account has keys: its yes
// would change nothing then, and every read refreshes, so reads waited
// on the instance, minutes on a poor connection (10 Nov 2026).
if (!accountHasKeys && registerHasKeys != null) {
try {
// Kept only once it says yes; a no is asked again next time.
if (await registerHasKeys!() == true) {
@ -102,13 +112,6 @@ class ContentKeys(
);
}
}
if (!_unlocked) {
final account = await _accountKeys();
if (account != null) {
await cipher.unlock(account);
_unlocked = true;
}
}
_rows = await _gateway.all();
// Remembered, so withholding them later changes nothing here.
if (_rows.isNotEmpty) {

View file

@ -1,24 +0,0 @@
import 'dart:io';
/// Whether the person answered the library's offer to make every studyset
/// private (ADR 0019, D4), on this device: asked once, whatever the answer.
abstract interface class PrivateOfferStore() {
/// Whether it was answered.
Future<bool> answered();
/// Notes that it was.
Future<void> answer();
}
/// [PrivateOfferStore] as a mark in the person's folder: the file is there
/// once the offer was answered.
class FilePrivateOfferStore(final File _mark) implements PrivateOfferStore {
@override
Future<bool> answered() async => _mark.existsSync();
@override
Future<void> answer() async {
await _mark.parent.create(recursive: true);
await _mark.writeAsString('');
}
}

View file

@ -65,6 +65,39 @@ void main() {
);
});
test('a device holding the keys never asks the instance whether the '
'account has them, which reads would otherwise wait on (offline: '
'minutes); one without asks at every refresh', () async {
var asked = 0;
Future<bool?> register() async {
asked++;
return false;
}
final laptop = ContentKeys(
synced,
FakeContentCipher(),
() async => alice,
registerHasKeys: register,
);
addTearDown(laptop.dispose);
await laptop.refresh();
await laptop.refresh();
expect(asked, 0);
expect(laptop.accountHasKeys, isTrue);
final keyless = ContentKeys(
FakeContentKeyGateway(),
FakeContentCipher(),
() async => null,
registerHasKeys: register,
);
addTearDown(keyless.dispose);
await keyless.refresh();
await keyless.refresh();
expect(asked, 2);
});
test("another account's device opens nothing of it", () async {
final laptop = await device(alice);
final keyId = await laptop.newStudysetKey('studyset-1');

View file

@ -1,92 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_payload.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_content.dart';
import 'package:material_ui/material_ui.dart';
import '../../../support/fake_studyset_gateway.dart';
import '../../../support/memory_private_offer_store.dart';
import '../../../support/pump_app.dart';
import '../../../support/test_dependencies.dart';
/// The library's one offer to make the studysets from before private (ADR
/// 0019, D4).
void main() {
late FakeStudysetGateway gateway;
late MemoryPrivateOfferStore offer;
setUp(() {
gateway = FakeStudysetGateway()..canMakePrivate = true;
offer = MemoryPrivateOfferStore();
gateway
..receiveRemoteChange(
'japanese',
encodeStudysetPayload(StudysetContent.titled('Japanese')),
)
..receiveRemoteChange(
'kanji',
encodeStudysetPayload(StudysetContent.titled('Kanji')),
parentId: 'japanese',
)
..receiveRemoteChange(
'diary',
encodeStudysetPayload(StudysetContent.titled('Diary')),
isPrivate: true,
);
});
Future<void> showLibrary(WidgetTester tester) async {
tester.view.physicalSize = const Size(1200, 1600);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
await pumpApp(
tester,
testDependencies(studysetGateway: gateway, privateOffer: offer),
location: RoutePaths.library,
);
await tester.pump();
await tester.pump();
}
testWidgets('makes every top-level studyset that is not private private '
'at once, then never asks again', (tester) async {
await showLibrary(tester);
expect(find.text('Make your studysets private?'), findsOneWidget);
await tester.tap(find.text('Make all private'));
await tester.pump();
await tester.pump();
expect(gateway.privacyChanges, [('japanese', true)]);
expect(offer.isAnswered, isTrue);
expect(find.text('Make your studysets private?'), findsNothing);
});
testWidgets('not now leaves them as they are, and asks no more', (
tester,
) async {
await showLibrary(tester);
await tester.tap(find.text('Not now'));
await tester.pump();
await tester.pump();
expect(gateway.privacyChanges, isEmpty);
expect(offer.isAnswered, isTrue);
expect(find.text('Make your studysets private?'), findsNothing);
});
testWidgets('is never shown once answered', (tester) async {
offer.isAnswered = true;
await showLibrary(tester);
expect(find.text('Make your studysets private?'), findsNothing);
});
testWidgets("is never shown on a device without the account's keys", (
tester,
) async {
gateway.canMakePrivate = false;
await showLibrary(tester);
expect(find.text('Make your studysets private?'), findsNothing);
});
}

View file

@ -1,13 +0,0 @@
import 'package:halkyon_learn_flutter/src/feature/studyset/data/private_offer_store.dart';
/// [PrivateOfferStore] in memory.
class MemoryPrivateOfferStore() implements PrivateOfferStore {
/// Whether the offer was answered.
bool isAnswered = false;
@override
Future<bool> answered() async => isAnswered;
@override
Future<void> answer() async => isAnswered = true;
}