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:
parent
151f848360
commit
1781553f1d
5 changed files with 44 additions and 137 deletions
|
|
@ -88,7 +88,17 @@ class ContentKeys(
|
||||||
Future<void> refresh() => _refreshing.run();
|
Future<void> refresh() => _refreshing.run();
|
||||||
|
|
||||||
Future<void> _refresh() async {
|
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 {
|
try {
|
||||||
// Kept only once it says yes; a no is asked again next time.
|
// Kept only once it says yes; a no is asked again next time.
|
||||||
if (await registerHasKeys!() == true) {
|
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();
|
_rows = await _gateway.all();
|
||||||
// Remembered, so withholding them later changes nothing here.
|
// Remembered, so withholding them later changes nothing here.
|
||||||
if (_rows.isNotEmpty) {
|
if (_rows.isNotEmpty) {
|
||||||
|
|
|
||||||
|
|
@ -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('');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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 {
|
test("another account's device opens nothing of it", () async {
|
||||||
final laptop = await device(alice);
|
final laptop = await device(alice);
|
||||||
final keyId = await laptop.newStudysetKey('studyset-1');
|
final keyId = await laptop.newStudysetKey('studyset-1');
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue