146 lines
5 KiB
Dart
146 lines
5 KiB
Dart
|
|
import 'dart:io';
|
||
|
|
|
||
|
|
import 'package:halkyon_learn_client/halkyon_learn_client.dart' as protocol;
|
||
|
|
import 'package:halkyon_learn_flutter/src/core/sync/sync_service.dart';
|
||
|
|
import 'package:halkyon_learn_flutter/src/feature/card/data/card_repository.dart';
|
||
|
|
import 'package:halkyon_learn_flutter/src/feature/card/data/sync_card_gateway.dart';
|
||
|
|
import 'package:halkyon_learn_flutter/src/feature/card/model/card.dart';
|
||
|
|
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_repository.dart';
|
||
|
|
import 'package:halkyon_learn_flutter/src/feature/studyset/data/sync_studyset_gateway.dart';
|
||
|
|
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset.dart';
|
||
|
|
import 'package:postgres/postgres.dart' as pg;
|
||
|
|
import 'package:serverpod_auth_core_client/serverpod_auth_core_client.dart';
|
||
|
|
import 'package:serverpod_offline_sync_client/offline_sync.dart';
|
||
|
|
import 'package:yaml/yaml.dart';
|
||
|
|
|
||
|
|
// What the sync tests (ADR 0001, rule 9) share: devices of people signed in
|
||
|
|
// against the running development server, and a look at its database.
|
||
|
|
|
||
|
|
const _serverUrl = 'http://localhost:8080/';
|
||
|
|
|
||
|
|
/// A new anonymous person, as the app signs one in on first launch.
|
||
|
|
Future<AuthSuccess> signInAnonymously() async {
|
||
|
|
final client = protocol.Client(_serverUrl);
|
||
|
|
try {
|
||
|
|
return await client.anonymousIdp.login();
|
||
|
|
} finally {
|
||
|
|
client.close();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Runs [sql] on the running development server's database, as its operator
|
||
|
|
/// could: the connection from `config/development.yaml`, the password from
|
||
|
|
/// `config/passwords.yaml`.
|
||
|
|
Future<pg.Result> queryServer(
|
||
|
|
String sql,
|
||
|
|
Map<String, Object?> parameters,
|
||
|
|
) async {
|
||
|
|
const config = '../halkyon_learn_server/config';
|
||
|
|
final development =
|
||
|
|
loadYaml(File('$config/development.yaml').readAsStringSync()) as YamlMap;
|
||
|
|
final database = development['database'] as YamlMap;
|
||
|
|
final passwords =
|
||
|
|
loadYaml(File('$config/passwords.yaml').readAsStringSync()) as YamlMap;
|
||
|
|
final connection = await pg.Connection.open(
|
||
|
|
pg.Endpoint(
|
||
|
|
host: database['host'] as String,
|
||
|
|
port: database['port'] as int,
|
||
|
|
database: database['name'] as String,
|
||
|
|
username: database['user'] as String,
|
||
|
|
password: (passwords['development'] as YamlMap)['database'] as String,
|
||
|
|
),
|
||
|
|
settings: const pg.ConnectionSettings(sslMode: pg.SslMode.disable),
|
||
|
|
);
|
||
|
|
try {
|
||
|
|
return await connection.execute(pg.Sql.named(sql), parameters: parameters);
|
||
|
|
} finally {
|
||
|
|
await connection.close();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// A device: its own client and session, its own local database, and the
|
||
|
|
/// app's repositories over it. It syncs only when told to.
|
||
|
|
final class SyncDevice(
|
||
|
|
final protocol.Client _client,
|
||
|
|
final OfflineSyncDatabaseSession _db,
|
||
|
|
final SyncService _syncRequests,
|
||
|
|
|
||
|
|
/// The app's studysets on this device's database.
|
||
|
|
final StudysetRepository studysets,
|
||
|
|
|
||
|
|
/// The app's cards on this device's database.
|
||
|
|
final CardRepository cards,
|
||
|
|
) {
|
||
|
|
/// Opens a device signed in as [person], whose local database in
|
||
|
|
/// [database] claims [space] as its personal space.
|
||
|
|
static Future<SyncDevice> open(
|
||
|
|
AuthSuccess person,
|
||
|
|
File database, {
|
||
|
|
required protocol.UuidValue space,
|
||
|
|
}) async {
|
||
|
|
final session = ClientAuthSessionManager(storage: _MemoryAuthStorage());
|
||
|
|
final client = protocol.Client(_serverUrl)..authSessionManager = session;
|
||
|
|
await session.updateSignedInUser(person);
|
||
|
|
final db = await client.createSyncSession(
|
||
|
|
database.path,
|
||
|
|
persistentUserId: space,
|
||
|
|
);
|
||
|
|
// The gateways ask for a sync after every write; these tests sync
|
||
|
|
// explicitly instead, so the requests go nowhere.
|
||
|
|
final syncRequests = SyncService(({required onMerge}) async {});
|
||
|
|
return SyncDevice(
|
||
|
|
client,
|
||
|
|
db,
|
||
|
|
syncRequests,
|
||
|
|
StudysetRepository(SyncStudysetGateway(db, syncRequests)),
|
||
|
|
CardRepository(
|
||
|
|
SyncCardGateway(db, syncRequests, person.authUserId.uuid),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Sends this device's changes and receives everyone else's.
|
||
|
|
Future<void> sync() => _client.offlineSync.syncOnce(_db);
|
||
|
|
|
||
|
|
/// The studyset with [id] on this device, or null.
|
||
|
|
Future<Studyset?> studyset(String id) async {
|
||
|
|
for (final studyset in await studysets.all()) {
|
||
|
|
if (studyset.id == id) {
|
||
|
|
return studyset;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// The card with [id] in [studysetId] on this device, or null.
|
||
|
|
Future<Card?> card(String studysetId, String id) async {
|
||
|
|
for (final card in await cards.cardsOf(studysetId)) {
|
||
|
|
if (card.id == id) {
|
||
|
|
return card;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Closes everything the device opened.
|
||
|
|
Future<void> close() async {
|
||
|
|
await studysets.dispose();
|
||
|
|
await cards.dispose();
|
||
|
|
await _syncRequests.dispose();
|
||
|
|
await _db.close();
|
||
|
|
_client.close();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Keeps a device's session in memory, where the app uses secure storage.
|
||
|
|
final class _MemoryAuthStorage() implements ClientAuthSuccessStorage {
|
||
|
|
AuthSuccess? _session;
|
||
|
|
|
||
|
|
@override
|
||
|
|
Future<AuthSuccess?> get() async => _session;
|
||
|
|
|
||
|
|
@override
|
||
|
|
Future<void> set(AuthSuccess? data) async {
|
||
|
|
_session = data;
|
||
|
|
}
|
||
|
|
}
|