From 77b97f2092f93ca02643f26cf3d75dc367df8d43 Mon Sep 17 00:00:00 2001 From: Martin Chaine Date: Tue, 19 Mar 2024 21:15:27 +0100 Subject: [PATCH] persist pending remote sync actions until there are sent to the server (#142) --- lib/models/db.dart | 8 +- lib/models/remote_action.dart | 31 + lib/models/remote_action.g.dart | 923 ++++++++++++++++++ lib/services/remote_sync.dart | 38 +- .../remote_sync_actions/articles.dart | 28 + lib/services/remote_sync_actions/base.dart | 19 + 6 files changed, 1033 insertions(+), 14 deletions(-) create mode 100644 lib/models/remote_action.dart create mode 100644 lib/models/remote_action.g.dart diff --git a/lib/models/db.dart b/lib/models/db.dart index 7a8ef665..fcec96da 100644 --- a/lib/models/db.dart +++ b/lib/models/db.dart @@ -6,6 +6,7 @@ import '../constants.dart'; import 'app_log.dart'; import 'article.dart'; import 'article_scroll_position.dart'; +import 'remote_action.dart'; typedef DBInstance = Isar; @@ -18,7 +19,12 @@ class DB { final dir = await getApplicationDocumentsDirectory(); _instance = await Isar.open( - [AppLogSchema, ArticleSchema, ArticleScrollPositionSchema], + [ + AppLogSchema, + ArticleSchema, + ArticleScrollPositionSchema, + RemoteActionSchema, + ], directory: dir.path, name: 'frigoligo${devmode ? '-dev' : ''}', ); diff --git a/lib/models/remote_action.dart b/lib/models/remote_action.dart new file mode 100644 index 00000000..9a879119 --- /dev/null +++ b/lib/models/remote_action.dart @@ -0,0 +1,31 @@ +import 'dart:convert'; + +import 'package:isar/isar.dart'; + +import '../services/remote_sync_actions/base.dart'; + +part 'remote_action.g.dart'; + +@collection +class RemoteAction { + RemoteAction(); + + Id? id; + DateTime? createdAt; + int? key; + String? className; + String? jsonParams; + + factory RemoteAction.fromRSA(RemoteSyncAction action) { + return RemoteAction() + ..key = action.hashCode + ..createdAt = DateTime.now() + ..className = action.runtimeType.toString() + ..jsonParams = jsonEncode(action.params()); + } + + RemoteSyncAction toRSA() { + final json = jsonDecode(jsonParams!); + return RemoteSyncAction.fromParams(className!, json); + } +} diff --git a/lib/models/remote_action.g.dart b/lib/models/remote_action.g.dart new file mode 100644 index 00000000..a6c2f73a --- /dev/null +++ b/lib/models/remote_action.g.dart @@ -0,0 +1,923 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'remote_action.dart'; + +// ************************************************************************** +// IsarCollectionGenerator +// ************************************************************************** + +// coverage:ignore-file +// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types + +extension GetRemoteActionCollection on Isar { + IsarCollection get remoteActions => this.collection(); +} + +const RemoteActionSchema = CollectionSchema( + name: r'RemoteAction', + id: -3509153609314200287, + properties: { + r'className': PropertySchema( + id: 0, + name: r'className', + type: IsarType.string, + ), + r'createdAt': PropertySchema( + id: 1, + name: r'createdAt', + type: IsarType.dateTime, + ), + r'jsonParams': PropertySchema( + id: 2, + name: r'jsonParams', + type: IsarType.string, + ), + r'key': PropertySchema( + id: 3, + name: r'key', + type: IsarType.long, + ) + }, + estimateSize: _remoteActionEstimateSize, + serialize: _remoteActionSerialize, + deserialize: _remoteActionDeserialize, + deserializeProp: _remoteActionDeserializeProp, + idName: r'id', + indexes: {}, + links: {}, + embeddedSchemas: {}, + getId: _remoteActionGetId, + getLinks: _remoteActionGetLinks, + attach: _remoteActionAttach, + version: '3.1.4', +); + +int _remoteActionEstimateSize( + RemoteAction object, + List offsets, + Map> allOffsets, +) { + var bytesCount = offsets.last; + { + final value = object.className; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } + { + final value = object.jsonParams; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } + return bytesCount; +} + +void _remoteActionSerialize( + RemoteAction object, + IsarWriter writer, + List offsets, + Map> allOffsets, +) { + writer.writeString(offsets[0], object.className); + writer.writeDateTime(offsets[1], object.createdAt); + writer.writeString(offsets[2], object.jsonParams); + writer.writeLong(offsets[3], object.key); +} + +RemoteAction _remoteActionDeserialize( + Id id, + IsarReader reader, + List offsets, + Map> allOffsets, +) { + final object = RemoteAction(); + object.className = reader.readStringOrNull(offsets[0]); + object.createdAt = reader.readDateTimeOrNull(offsets[1]); + object.id = id; + object.jsonParams = reader.readStringOrNull(offsets[2]); + object.key = reader.readLongOrNull(offsets[3]); + return object; +} + +P _remoteActionDeserializeProp

( + IsarReader reader, + int propertyId, + int offset, + Map> allOffsets, +) { + switch (propertyId) { + case 0: + return (reader.readStringOrNull(offset)) as P; + case 1: + return (reader.readDateTimeOrNull(offset)) as P; + case 2: + return (reader.readStringOrNull(offset)) as P; + case 3: + return (reader.readLongOrNull(offset)) as P; + default: + throw IsarError('Unknown property with id $propertyId'); + } +} + +Id _remoteActionGetId(RemoteAction object) { + return object.id ?? Isar.autoIncrement; +} + +List> _remoteActionGetLinks(RemoteAction object) { + return []; +} + +void _remoteActionAttach( + IsarCollection col, Id id, RemoteAction object) { + object.id = id; +} + +extension RemoteActionQueryWhereSort + on QueryBuilder { + QueryBuilder anyId() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(const IdWhereClause.any()); + }); + } +} + +extension RemoteActionQueryWhere + on QueryBuilder { + QueryBuilder idEqualTo(Id id) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: id, + upper: id, + )); + }); + } + + QueryBuilder idNotEqualTo( + Id id) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: false), + ) + .addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: false), + ); + } else { + return query + .addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: false), + ) + .addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: false), + ); + } + }); + } + + QueryBuilder idGreaterThan( + Id id, + {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: include), + ); + }); + } + + QueryBuilder idLessThan(Id id, + {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: include), + ); + }); + } + + QueryBuilder idBetween( + Id lowerId, + Id upperId, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: lowerId, + includeLower: includeLower, + upper: upperId, + includeUpper: includeUpper, + )); + }); + } +} + +extension RemoteActionQueryFilter + on QueryBuilder { + QueryBuilder + classNameIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'className', + )); + }); + } + + QueryBuilder + classNameIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'className', + )); + }); + } + + QueryBuilder + classNameEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'className', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + classNameGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'className', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + classNameLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'className', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + classNameBetween( + String? lower, + String? upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'className', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + classNameStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'className', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + classNameEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'className', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + classNameContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'className', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + classNameMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'className', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + classNameIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'className', + value: '', + )); + }); + } + + QueryBuilder + classNameIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'className', + value: '', + )); + }); + } + + QueryBuilder + createdAtIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'createdAt', + )); + }); + } + + QueryBuilder + createdAtIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'createdAt', + )); + }); + } + + QueryBuilder + createdAtEqualTo(DateTime? value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'createdAt', + value: value, + )); + }); + } + + QueryBuilder + createdAtGreaterThan( + DateTime? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'createdAt', + value: value, + )); + }); + } + + QueryBuilder + createdAtLessThan( + DateTime? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'createdAt', + value: value, + )); + }); + } + + QueryBuilder + createdAtBetween( + DateTime? lower, + DateTime? upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'createdAt', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder idIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'id', + )); + }); + } + + QueryBuilder + idIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'id', + )); + }); + } + + QueryBuilder idEqualTo( + Id? value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'id', + value: value, + )); + }); + } + + QueryBuilder idGreaterThan( + Id? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'id', + value: value, + )); + }); + } + + QueryBuilder idLessThan( + Id? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'id', + value: value, + )); + }); + } + + QueryBuilder idBetween( + Id? lower, + Id? upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'id', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + jsonParamsIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'jsonParams', + )); + }); + } + + QueryBuilder + jsonParamsIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'jsonParams', + )); + }); + } + + QueryBuilder + jsonParamsEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'jsonParams', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + jsonParamsGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'jsonParams', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + jsonParamsLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'jsonParams', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + jsonParamsBetween( + String? lower, + String? upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'jsonParams', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + jsonParamsStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'jsonParams', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + jsonParamsEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'jsonParams', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + jsonParamsContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'jsonParams', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + jsonParamsMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'jsonParams', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + jsonParamsIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'jsonParams', + value: '', + )); + }); + } + + QueryBuilder + jsonParamsIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'jsonParams', + value: '', + )); + }); + } + + QueryBuilder keyIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'key', + )); + }); + } + + QueryBuilder + keyIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'key', + )); + }); + } + + QueryBuilder keyEqualTo( + int? value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'key', + value: value, + )); + }); + } + + QueryBuilder + keyGreaterThan( + int? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'key', + value: value, + )); + }); + } + + QueryBuilder keyLessThan( + int? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'key', + value: value, + )); + }); + } + + QueryBuilder keyBetween( + int? lower, + int? upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'key', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } +} + +extension RemoteActionQueryObject + on QueryBuilder {} + +extension RemoteActionQueryLinks + on QueryBuilder {} + +extension RemoteActionQuerySortBy + on QueryBuilder { + QueryBuilder sortByClassName() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'className', Sort.asc); + }); + } + + QueryBuilder sortByClassNameDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'className', Sort.desc); + }); + } + + QueryBuilder sortByCreatedAt() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'createdAt', Sort.asc); + }); + } + + QueryBuilder sortByCreatedAtDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'createdAt', Sort.desc); + }); + } + + QueryBuilder sortByJsonParams() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'jsonParams', Sort.asc); + }); + } + + QueryBuilder + sortByJsonParamsDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'jsonParams', Sort.desc); + }); + } + + QueryBuilder sortByKey() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'key', Sort.asc); + }); + } + + QueryBuilder sortByKeyDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'key', Sort.desc); + }); + } +} + +extension RemoteActionQuerySortThenBy + on QueryBuilder { + QueryBuilder thenByClassName() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'className', Sort.asc); + }); + } + + QueryBuilder thenByClassNameDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'className', Sort.desc); + }); + } + + QueryBuilder thenByCreatedAt() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'createdAt', Sort.asc); + }); + } + + QueryBuilder thenByCreatedAtDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'createdAt', Sort.desc); + }); + } + + QueryBuilder thenById() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.asc); + }); + } + + QueryBuilder thenByIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.desc); + }); + } + + QueryBuilder thenByJsonParams() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'jsonParams', Sort.asc); + }); + } + + QueryBuilder + thenByJsonParamsDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'jsonParams', Sort.desc); + }); + } + + QueryBuilder thenByKey() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'key', Sort.asc); + }); + } + + QueryBuilder thenByKeyDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'key', Sort.desc); + }); + } +} + +extension RemoteActionQueryWhereDistinct + on QueryBuilder { + QueryBuilder distinctByClassName( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'className', caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctByCreatedAt() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'createdAt'); + }); + } + + QueryBuilder distinctByJsonParams( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'jsonParams', caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctByKey() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'key'); + }); + } +} + +extension RemoteActionQueryProperty + on QueryBuilder { + QueryBuilder idProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'id'); + }); + } + + QueryBuilder classNameProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'className'); + }); + } + + QueryBuilder createdAtProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'createdAt'); + }); + } + + QueryBuilder jsonParamsProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'jsonParams'); + }); + } + + QueryBuilder keyProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'key'); + }); + } +} diff --git a/lib/services/remote_sync.dart b/lib/services/remote_sync.dart index 4e2fd348..7ba53695 100644 --- a/lib/services/remote_sync.dart +++ b/lib/services/remote_sync.dart @@ -1,6 +1,9 @@ import 'package:flutter/foundation.dart'; +import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; +import '../models/db.dart'; +import '../models/remote_action.dart'; import '../providers/settings.dart'; import '../wallabag/wallabag.dart'; import 'remote_sync_actions/articles.dart'; @@ -12,7 +15,8 @@ final _log = Logger('remote.sync'); class RemoteSyncer with ChangeNotifier { static const _refreshAction = RefreshArticlesAction(); - SettingsProvider settings = + final DBInstance db = DB.get(); + final SettingsProvider settings = SettingsProvider(namespace: kDebugMode ? 'debug' : null); WallabagStorage? _storage; @@ -25,8 +29,7 @@ class RemoteSyncer with ChangeNotifier { void invalidateWallabagInstance() => _storage = null; - final Set _queue = {}; - int get pendingCount => _queue.length; + int get pendingCount => db.remoteActions.countSync(); bool _isWorking = false; bool get isWorking => _isWorking; @@ -58,7 +61,12 @@ class RemoteSyncer with ChangeNotifier { } void add(RemoteSyncAction action) { - _queue.add(action); + final existing = + db.remoteActions.filter().keyEqualTo(action.hashCode).findFirstSync(); + if (existing == null) { + db.writeTxnSync( + () => db.remoteActions.putSync(RemoteAction.fromRSA(action))); + } } Future synchronize({bool withFinalRefresh = false}) async { @@ -90,19 +98,23 @@ class RemoteSyncer with ChangeNotifier { Future _executeActions() async { progressValue = null; - int actionsCount = _queue.length; - List actions = _queue.toList(); + int i = 1; - while (_queue.isNotEmpty) { + int actionsCount = 0; + do { + final actions = db.remoteActions.where().sortByCreatedAt().findAllSync(); + actionsCount += actions.length; for (final action in actions) { - _log.info('running action: $action'); - await action.execute(this); - _queue.remove(action); + final rsa = action.toRSA(); + _log.info('running action: $rsa'); + await rsa.execute(this); + db.writeTxnSync(() => db.remoteActions.deleteSync(action.id!)); progressValue = i / actionsCount; + i++; } - actionsCount += _queue.length; - actions = _queue.toList(); - } + // new actions might be added while the current batch was processed + actionsCount += pendingCount; + } while (i < actionsCount); } static final _instance = RemoteSyncer(); diff --git a/lib/services/remote_sync_actions/articles.dart b/lib/services/remote_sync_actions/articles.dart index b85b7fd8..04d616ee 100644 --- a/lib/services/remote_sync_actions/articles.dart +++ b/lib/services/remote_sync_actions/articles.dart @@ -3,6 +3,12 @@ import 'base.dart'; class RefreshArticlesAction extends RemoteSyncAction { const RefreshArticlesAction() : super('refreshArticles'); + @override + ActionParams params() => {}; + + factory RefreshArticlesAction.fromParams(ActionParams params) => + const RefreshArticlesAction(); + @override Future execute(syncer) async { await syncer.wallabag!.incrementalRefresh( @@ -16,6 +22,12 @@ class DeleteArticleAction extends RemoteSyncAction { final int articleId; + @override + ActionParams params() => {'articleId': articleId}; + + factory DeleteArticleAction.fromParams(ActionParams params) => + DeleteArticleAction(params['articleId'] as int); + @override Future execute(syncer) async { await syncer.wallabag!.deleteArticle(articleId); @@ -36,6 +48,22 @@ class EditArticleAction extends RemoteSyncAction { final bool? starred; final List? tags; + @override + ActionParams params() => { + 'articleId': articleId, + 'archive': archive, + 'starred': starred, + 'tags': tags, + }; + + factory EditArticleAction.fromParams(ActionParams params) => + EditArticleAction( + params['articleId'] as int, + archive: params['archive'] as bool?, + starred: params['starred'] as bool?, + tags: (params['tags'] as List?)?.cast(), + ); + @override Future execute(syncer) async { await syncer.wallabag! diff --git a/lib/services/remote_sync_actions/base.dart b/lib/services/remote_sync_actions/base.dart index f1064822..f45087d2 100644 --- a/lib/services/remote_sync_actions/base.dart +++ b/lib/services/remote_sync_actions/base.dart @@ -1,4 +1,7 @@ import '../remote_sync.dart'; +import 'articles.dart'; + +typedef ActionParams = Map; abstract class RemoteSyncAction { const RemoteSyncAction(String key) : _key = key; @@ -12,6 +15,15 @@ abstract class RemoteSyncAction { @override int get hashCode => _key.hashCode; + ActionParams params(); + + factory RemoteSyncAction.fromParams(String className, ActionParams params) { + if (!actionBuilderRegistry.containsKey(className)) { + throw Exception('unknown RemoteSyncAction: $className'); + } + return actionBuilderRegistry[className]!(params); + } + Future execute(RemoteSyncer syncer); @override @@ -19,3 +31,10 @@ abstract class RemoteSyncAction { return '$runtimeType[$_key]'; } } + +// TODO could not find a way to do it in dart without importing the other file +Map actionBuilderRegistry = { + 'RefreshArticlesAction': RefreshArticlesAction.fromParams, + 'DeleteArticleAction': DeleteArticleAction.fromParams, + 'EditArticleAction': EditArticleAction.fromParams, +};