mirror of
https://codeberg.org/shroff/phylum.git
synced 2026-01-08 20:49:45 -06:00
[client] WIP: Better local change tracking
This commit is contained in:
@@ -3,7 +3,7 @@ import 'dart:async';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:offtheline/offtheline.dart';
|
||||
import 'package:phylum/libphylum/actions/action_resource_create.dart';
|
||||
import 'package:phylum/libphylum/db/resource_helpers.dart';
|
||||
import 'package:phylum/libphylum/db/db.dart';
|
||||
import 'package:phylum/libphylum/phylum_api_types.dart';
|
||||
|
||||
class ResourceCopyAction extends ResourceCreateAction with JsonApiAction {
|
||||
@@ -94,11 +94,11 @@ class ResourceCopyAction extends ResourceCreateAction with JsonApiAction {
|
||||
|
||||
@override
|
||||
Future<void> applyOptimisticUpdate() async {
|
||||
await account.db.createResource(resourceId, parent, localName, dir, timestamp);
|
||||
await account.localChangeTracker.create(resourceId, parent, localName, dir, timestamp);
|
||||
if (!dir) {
|
||||
await account.db.updateResource(
|
||||
await account.localChangeTracker.update(
|
||||
resourceId,
|
||||
(o) => o(
|
||||
ResourcesCompanion(
|
||||
contentType: Value(contentType),
|
||||
contentLength: Value(contentLength),
|
||||
contentSha256: Value(contentSha256),
|
||||
@@ -106,15 +106,15 @@ class ResourceCopyAction extends ResourceCreateAction with JsonApiAction {
|
||||
);
|
||||
}
|
||||
if (deletedId != null) {
|
||||
await account.db.updateResource(deletedId!, (u) => u(parent: Value(null)));
|
||||
await account.localChangeTracker.update(deletedId!, ResourcesCompanion(parent: Value(null)));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> revertOptimisticUpdate() async {
|
||||
await account.db.deleteResource(resourceId);
|
||||
await account.localChangeTracker.revert(resourceId);
|
||||
if (deletedId != null) {
|
||||
await account.serverResourceTracker.revert(deletedId!);
|
||||
await account.localChangeTracker.revert(deletedId!);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'dart:async';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:offtheline/offtheline.dart';
|
||||
import 'package:phylum/libphylum/db/db.dart';
|
||||
import 'package:phylum/libphylum/db/resource_helpers.dart';
|
||||
import 'package:phylum/libphylum/phylum_api_types.dart';
|
||||
|
||||
import 'action_resource.dart';
|
||||
@@ -56,12 +55,12 @@ class ResourceDeleteAction extends ResourceAction with JsonApiAction {
|
||||
|
||||
@override
|
||||
Future<void> applyOptimisticUpdate() {
|
||||
return account.db.updateResource(resourceId, (o) => o(deleted: const Value(true)));
|
||||
return account.localChangeTracker.update(resourceId, ResourcesCompanion(deleted: const Value(true)));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> revertOptimisticUpdate() {
|
||||
return account.db.deleteResource(resourceId);
|
||||
return account.localChangeTracker.revert(resourceId);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'dart:async';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:offtheline/offtheline.dart';
|
||||
import 'package:phylum/libphylum/actions/action_resource_create.dart';
|
||||
import 'package:phylum/libphylum/db/resource_helpers.dart';
|
||||
import 'package:phylum/libphylum/db/db.dart';
|
||||
import 'package:phylum/libphylum/phylum_api_types.dart';
|
||||
|
||||
class ResourceMkdirAction extends ResourceCreateAction with JsonApiAction {
|
||||
@@ -70,17 +70,17 @@ class ResourceMkdirAction extends ResourceCreateAction with JsonApiAction {
|
||||
|
||||
@override
|
||||
Future<void> applyOptimisticUpdate() async {
|
||||
await account.db.createResource(resourceId, parent, localName, true, timestamp);
|
||||
await account.localChangeTracker.create(resourceId, parent, localName, true, timestamp);
|
||||
if (deletedId != null) {
|
||||
await account.db.updateResource(deletedId!, (u) => u(parent: Value(null)));
|
||||
await account.localChangeTracker.update(deletedId!, ResourcesCompanion(parent: Value(null)));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> revertOptimisticUpdate() async {
|
||||
await account.db.deleteResource(resourceId);
|
||||
await account.localChangeTracker.revert(resourceId);
|
||||
if (deletedId != null) {
|
||||
await account.serverResourceTracker.revert(deletedId!);
|
||||
await account.localChangeTracker.revert(deletedId!);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:drift/drift.dart';
|
||||
import 'package:offtheline/offtheline.dart';
|
||||
import 'package:phylum/libphylum/actions/action_resource_bind.dart';
|
||||
import 'package:phylum/libphylum/actions/action_resource_create.dart';
|
||||
import 'package:phylum/libphylum/db/resource_helpers.dart';
|
||||
import 'package:phylum/libphylum/db/db.dart';
|
||||
import 'package:phylum/libphylum/phylum_api_types.dart';
|
||||
|
||||
class ResourceMoveAction extends ResourceBindAction with JsonApiAction {
|
||||
@@ -74,24 +74,24 @@ class ResourceMoveAction extends ResourceBindAction with JsonApiAction {
|
||||
|
||||
@override
|
||||
Future<void> applyOptimisticUpdate() async {
|
||||
await account.db.updateResource(
|
||||
await account.localChangeTracker.update(
|
||||
resourceId,
|
||||
(o) => o(
|
||||
ResourcesCompanion(
|
||||
name: Value.absentIfNull(localName),
|
||||
parent: Value.absentIfNull(parent),
|
||||
modified: Value(modified),
|
||||
),
|
||||
);
|
||||
if (deletedId != null) {
|
||||
await account.db.updateResource(deletedId!, (u) => u(parent: Value(null)));
|
||||
await account.localChangeTracker.update(deletedId!, ResourcesCompanion(parent: Value(null)));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> revertOptimisticUpdate() async {
|
||||
await account.serverResourceTracker.revert(resourceId);
|
||||
await account.localChangeTracker.revert(resourceId);
|
||||
if (deletedId != null) {
|
||||
await account.serverResourceTracker.revert(deletedId!);
|
||||
await account.localChangeTracker.revert(deletedId!);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:convert';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:offtheline/offtheline.dart';
|
||||
import 'package:phylum/libphylum/db/db.dart';
|
||||
import 'package:phylum/libphylum/db/resource_helpers.dart';
|
||||
import 'package:phylum/libphylum/phylum_api_types.dart';
|
||||
import 'package:phylum/util/permissions.dart';
|
||||
@@ -92,9 +93,9 @@ class ResourceShareAction extends ResourceAction with JsonApiAction {
|
||||
p[username] = permission;
|
||||
}
|
||||
final serialized = p.isEmpty ? null : json.encode(p);
|
||||
account.db.updateResource(
|
||||
return account.localChangeTracker.update(
|
||||
resourceId,
|
||||
(o) => o(
|
||||
ResourcesCompanion(
|
||||
permissions: Value(serialized),
|
||||
modified: Value(modified),
|
||||
),
|
||||
@@ -103,7 +104,7 @@ class ResourceShareAction extends ResourceAction with JsonApiAction {
|
||||
|
||||
@override
|
||||
Future<void> revertOptimisticUpdate() {
|
||||
return account.serverResourceTracker.revert(resourceId);
|
||||
return account.localChangeTracker.revert(resourceId);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'package:http/http.dart';
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
import 'package:offtheline/offtheline.dart';
|
||||
import 'package:phylum/libphylum/actions/action_resource_create.dart';
|
||||
import 'package:phylum/libphylum/db/resource_helpers.dart';
|
||||
import 'package:phylum/libphylum/db/db.dart';
|
||||
import 'package:phylum/libphylum/phylum_api_types.dart';
|
||||
|
||||
class ResourceUploadAction extends ResourceCreateAction with FileUploadApiAction {
|
||||
@@ -20,7 +20,8 @@ class ResourceUploadAction extends ResourceCreateAction with FileUploadApiAction
|
||||
String get endpoint => '/api/v1/fs/upload/$resourceId';
|
||||
|
||||
@override
|
||||
Future<MultipartFile> get file => MultipartFile.fromPath('contents', path, filename: resourceName, contentType: MediaType.parse(contentType));
|
||||
Future<MultipartFile> get file =>
|
||||
MultipartFile.fromPath('contents', path, filename: resourceName, contentType: MediaType.parse(contentType));
|
||||
|
||||
final String path;
|
||||
final int size;
|
||||
@@ -78,25 +79,25 @@ class ResourceUploadAction extends ResourceCreateAction with FileUploadApiAction
|
||||
|
||||
@override
|
||||
Future<void> applyOptimisticUpdate() async {
|
||||
await account.db.createResource(resourceId, parent, localName, false, timestamp);
|
||||
await account.db.updateResource(
|
||||
await account.localChangeTracker.create(resourceId, parent, localName, false, timestamp);
|
||||
await account.localChangeTracker.update(
|
||||
resourceId,
|
||||
(o) => o(
|
||||
ResourcesCompanion(
|
||||
contentType: Value(contentType),
|
||||
contentLength: Value(size),
|
||||
contentSha256: Value("<unknown>"),
|
||||
),
|
||||
);
|
||||
if (deletedId != null) {
|
||||
await account.db.updateResource(deletedId!, (u) => u(parent: Value(null)));
|
||||
await account.localChangeTracker.update(deletedId!, ResourcesCompanion(parent: Value(null)));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> revertOptimisticUpdate() async {
|
||||
await account.db.deleteResource(resourceId);
|
||||
await account.localChangeTracker.revert(resourceId);
|
||||
if (deletedId != null) {
|
||||
await account.serverResourceTracker.revert(deletedId!);
|
||||
await account.localChangeTracker.revert(deletedId!);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,9 @@ extension ResourceHelpers on AppDatabase {
|
||||
}
|
||||
|
||||
Stream<List<Resource>> watchChildren(String id) {
|
||||
return (_selectChildren(id)..orderBy([(u) => OrderingTerm.desc(u.dir), (u) => OrderingTerm.asc(u.name.collate(Collate.noCase))])).watch();
|
||||
return (_selectChildren(id)
|
||||
..orderBy([(u) => OrderingTerm.desc(u.dir), (u) => OrderingTerm.asc(u.name.collate(Collate.noCase))]))
|
||||
.watch();
|
||||
}
|
||||
|
||||
Stream<List<ParentsResult>> watchParents(String id) {
|
||||
@@ -41,26 +43,4 @@ extension ResourceHelpers on AppDatabase {
|
||||
SimpleSelectStatement<Resources, Resource> _selectResources(Iterable<String> ids) {
|
||||
return resources.select()..where((f) => f.id.isIn(ids));
|
||||
}
|
||||
|
||||
Future<int> createResource(String id, String parent, String name, bool dir, DateTime timestamp) {
|
||||
return resources.insertOne(
|
||||
ResourcesCompanion.insert(
|
||||
id: id,
|
||||
name: name,
|
||||
parent: Value(parent),
|
||||
dir: dir,
|
||||
created: Value(timestamp),
|
||||
modified: Value(timestamp),
|
||||
),
|
||||
mode: InsertMode.insertOrReplace,
|
||||
);
|
||||
}
|
||||
|
||||
Future<int> updateResource(String id, Insertable<Resource> Function($ResourcesUpdateCompanionBuilder o) fn) {
|
||||
return managers.resources.filter((f) => f.id.equals(id)).update(fn);
|
||||
}
|
||||
|
||||
Future<int> deleteResource(String id) {
|
||||
return resources.deleteWhere((f) => f.id.equals(id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
@@ -6,27 +7,82 @@ import 'package:phylum/libphylum/db/db.dart';
|
||||
import 'package:phylum/libphylum/db/resource_helpers.dart';
|
||||
import 'package:phylum/libphylum/phylum_account.dart';
|
||||
|
||||
class ServerResourceStateTracker {
|
||||
class LocalChangeTracker {
|
||||
final PhylumAccount account;
|
||||
late final Box<Resource?> _box;
|
||||
late final Box<Resource?> _serverDataBox;
|
||||
// Map<String, Resource?> _pending = {};
|
||||
|
||||
ServerResourceStateTracker({required this.account});
|
||||
LocalChangeTracker({required this.account});
|
||||
|
||||
Future<void> initialize() async {
|
||||
if (!Hive.isAdapterRegistered(const ResourceAdapter().typeId)) {
|
||||
Hive.registerAdapter(const ResourceAdapter());
|
||||
}
|
||||
_box = await account.openBox('server_data');
|
||||
_serverDataBox = await account.openBox('server_data');
|
||||
}
|
||||
|
||||
Resource? getServerResource(String id) {
|
||||
return _box.get(id);
|
||||
Future<int> create(String id, String parent, String name, bool dir, DateTime timestamp) {
|
||||
// This will create a key with null value, which is exactly what we want
|
||||
_retainOriginal(id);
|
||||
return account.db.resources.insertOne(
|
||||
ResourcesCompanion.insert(
|
||||
id: id,
|
||||
name: name,
|
||||
parent: Value(parent),
|
||||
dir: dir,
|
||||
created: Value(timestamp),
|
||||
modified: Value(timestamp),
|
||||
),
|
||||
mode: InsertMode.insertOrReplace,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> revert(String id) async {
|
||||
final r = _box.get(id);
|
||||
if (r != null) {
|
||||
await account.db.resources.replaceOne(r);
|
||||
Future<void> update(String id, Insertable<Resource> update) {
|
||||
_retainOriginal(id);
|
||||
return (account.db.resources.update()..where((f) => f.id.equals(id))).write(update);
|
||||
}
|
||||
|
||||
Future<void> _retainOriginal(String id) async {
|
||||
final pending = Zone.current[#pending];
|
||||
pending[id] ??= _serverDataBox.get(id) ?? await account.db.getResource(id);
|
||||
}
|
||||
|
||||
Future<void> track(Future<void> Function() fn, {bool clearUntouched = true}) async {
|
||||
final pending = await runZonedGuarded(() async {
|
||||
await fn();
|
||||
return (Zone.current[#pending] as Map).cast<String, Resource?>();
|
||||
}, (err, stack) {}, zoneValues: {
|
||||
#pending: <String, Resource?>{},
|
||||
});
|
||||
if (pending != null) {
|
||||
var untouchedKeys = const <String>{};
|
||||
if (clearUntouched) {
|
||||
untouchedKeys = Set.from(_serverDataBox.keys);
|
||||
untouchedKeys.retainWhere((key) => !pending.containsKey(key));
|
||||
}
|
||||
// Avoid unnecessary disk writes
|
||||
pending.removeWhere((key, value) => _serverDataBox.containsKey(key) && _serverDataBox.get(key) == value);
|
||||
await _serverDataBox.putAll(pending);
|
||||
await _serverDataBox.deleteAll(untouchedKeys);
|
||||
await _serverDataBox.flush();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> revert(String id) {
|
||||
final r = _serverDataBox.get(id);
|
||||
|
||||
if (r == null) {
|
||||
return account.db.resources.deleteWhere((f) => f.id.equals(id));
|
||||
} else {
|
||||
return account.db.resources.replaceOne(r);
|
||||
}
|
||||
}
|
||||
|
||||
void processServerUpdates(Iterable<ResourcesCompanion> updates) {
|
||||
for (final r in updates) {
|
||||
if (_serverDataBox.containsKey(r.id.value)) {
|
||||
_serverDataBox.put(r.id.value, _serverDataBox.get(r.id.value)?.copyWithCompanion(r));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,30 +90,11 @@ class ServerResourceStateTracker {
|
||||
return Interceptor(tracker: this, clearUntouched: clearUntouched);
|
||||
}
|
||||
|
||||
Future<void> _trackServerResources(Map<String, Resource?> serverResources, bool clearUntouched) async {
|
||||
var untouchedKeys = const <String>{};
|
||||
if (clearUntouched) {
|
||||
untouchedKeys = Set.from(_box.keys);
|
||||
untouchedKeys.retainWhere((key) => !serverResources.containsKey(key));
|
||||
}
|
||||
// Avoid unnecessary disk writes
|
||||
serverResources.removeWhere((key, value) => _box.containsKey(key) && _box.get(key) == value);
|
||||
await _box.putAll(serverResources);
|
||||
await _box.deleteAll(untouchedKeys);
|
||||
await _box.flush();
|
||||
}
|
||||
|
||||
void update(Iterable<ResourcesCompanion> updates) {
|
||||
for (final r in updates) {
|
||||
if (_box.containsKey(r.id.value)) {
|
||||
_box.put(r.id.value, _box.get(r.id.value)?.copyWithCompanion(r));
|
||||
}
|
||||
}
|
||||
}
|
||||
Future<void> _trackServerResources(Map<String, Resource?> serverResources, bool clearUntouched) async {}
|
||||
}
|
||||
|
||||
class Interceptor extends QueryInterceptor {
|
||||
final ServerResourceStateTracker tracker;
|
||||
final LocalChangeTracker tracker;
|
||||
final bool clearUntouched;
|
||||
late final AppDatabase db;
|
||||
final Map<String, Resource?> serverData = {};
|
||||
@@ -89,7 +126,7 @@ class Interceptor extends QueryInterceptor {
|
||||
}
|
||||
|
||||
Future<void> retainOriginal(String id) async {
|
||||
serverData[id] ??= tracker._box.get(id) ?? await db.getResource(id);
|
||||
serverData[id] ??= tracker._serverDataBox.get(id) ?? await db.getResource(id);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -6,7 +6,7 @@ import 'package:phylum/libphylum/phylum_api_types.dart';
|
||||
import 'package:phylum/libphylum/repositories/my_lists_repository.dart';
|
||||
import 'package:phylum/libphylum/repositories/resource_repository.dart';
|
||||
import 'package:phylum/libphylum/repositories/user_repository.dart';
|
||||
import 'package:phylum/libphylum/server_data_tracker.dart';
|
||||
import 'package:phylum/libphylum/local_updates_tracker.dart';
|
||||
import 'package:phylum/util/logging.dart';
|
||||
|
||||
const _persistKeyAccessToken = 'accessToken';
|
||||
@@ -19,7 +19,7 @@ class PhylumAccount extends Account<PhylumAccount> {
|
||||
late final resourceRepository = ResourceRepository(account: this);
|
||||
late final userRepository = UserRepository(account: this);
|
||||
late final myListsRepository = MyListsRepository(account: this);
|
||||
late final serverResourceTracker = ServerResourceStateTracker(account: this);
|
||||
late final localChangeTracker = LocalChangeTracker(account: this);
|
||||
|
||||
final _dispatcher = SimulatedBadNetworkDispatcher.good(dropRate: 0);
|
||||
// final _dispatcher = HttpClientDispatcher();
|
||||
@@ -62,7 +62,7 @@ class PhylumAccount extends Account<PhylumAccount> {
|
||||
// Set Authorization header
|
||||
_accessToken = _initialAccessToken ?? accessToken;
|
||||
|
||||
await serverResourceTracker.initialize();
|
||||
await localChangeTracker.initialize();
|
||||
await userRepository.initialize();
|
||||
await myListsRepository.initialize();
|
||||
|
||||
@@ -79,20 +79,13 @@ class PhylumAccount extends Account<PhylumAccount> {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> transaction(
|
||||
Future<void> Function() fn, {
|
||||
bool retainServerData = false,
|
||||
bool clearUntouched = true,
|
||||
}) {
|
||||
if (retainServerData) {
|
||||
return db.runWithInterceptor(
|
||||
() => db.transaction(fn),
|
||||
interceptor: serverResourceTracker.newInterceptor(clearUntouched),
|
||||
);
|
||||
}
|
||||
return db.transaction(fn);
|
||||
Future<void> localChanges(Future<void> Function() fn, {bool clearUntouched = true}) {
|
||||
return localChangeTracker.track(fn, clearUntouched: clearUntouched);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> transaction(Future<void> Function() fn) => db.transaction(fn);
|
||||
|
||||
@override
|
||||
Future<void> cleanup() async {
|
||||
await super.cleanup();
|
||||
|
||||
@@ -219,19 +219,20 @@ class ResourceRepository {
|
||||
}
|
||||
batch.insertAllOnConflictUpdate(db.resources, parsed);
|
||||
});
|
||||
account.serverResourceTracker.update(parsed);
|
||||
account.serverResourceTracker.update(previousChildren);
|
||||
account.localChangeTracker.processServerUpdates(parsed);
|
||||
account.localChangeTracker.processServerUpdates(previousChildren);
|
||||
}
|
||||
|
||||
Future<ResourcesCompanion> processResourceUpdateResponse(String id, Map<String, dynamic> data) async {
|
||||
final r = parsePartialResourceObject(data, id);
|
||||
final update = (account.db.resources.update()..where((o) => o.id.equals(r.id.value)));
|
||||
await update.write(r);
|
||||
account.serverResourceTracker.update([r]);
|
||||
account.localChangeTracker.processServerUpdates([r]);
|
||||
return r;
|
||||
}
|
||||
|
||||
Future<(Iterable<ResourcesCompanion>, Iterable<ResourcesCompanion>)> parseFullResourceObject(Map<String, dynamic> data) async {
|
||||
Future<(Iterable<ResourcesCompanion>, Iterable<ResourcesCompanion>)> parseFullResourceObject(
|
||||
Map<String, dynamic> data) async {
|
||||
final ancestors = (data['ancestors'] as List).cast<Map>().map(
|
||||
(a) => parseResourceAncestor(a.cast<String, dynamic>()).copyWith(dir: Value(true)),
|
||||
);
|
||||
|
||||
@@ -618,8 +618,8 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: ab233bd87a0c7e0d77ce211a5d9c929fd18f33b6
|
||||
resolved-ref: ab233bd87a0c7e0d77ce211a5d9c929fd18f33b6
|
||||
ref: "3471261985806a93ced64fa7b425240c7d57d79b"
|
||||
resolved-ref: "3471261985806a93ced64fa7b425240c7d57d79b"
|
||||
url: "https://codeberg.org/shroff/offtheline.git"
|
||||
source: git
|
||||
version: "0.16.0"
|
||||
|
||||
@@ -24,7 +24,7 @@ dependencies:
|
||||
offtheline:
|
||||
git:
|
||||
url: https://codeberg.org/shroff/offtheline.git
|
||||
ref: ab233bd87a0c7e0d77ce211a5d9c929fd18f33b6
|
||||
ref: 3471261985806a93ced64fa7b425240c7d57d79b
|
||||
open_file:
|
||||
path:
|
||||
path_provider:
|
||||
|
||||
Reference in New Issue
Block a user