look ahead
This commit is contained in:
@@ -52,6 +52,13 @@ TxMetaStoreData mergeTxMetaStores(TxMetaStoreData a, TxMetaStoreData b) {
|
||||
existing == null ? e.value : _mergeExact(existing, e.value);
|
||||
}
|
||||
|
||||
final recurringTemplates = <String, RecurringTemplate>{
|
||||
...a.recurringTemplates,
|
||||
};
|
||||
for (final e in b.recurringTemplates.entries) {
|
||||
recurringTemplates.putIfAbsent(e.key, () => e.value);
|
||||
}
|
||||
|
||||
final labelsByClientId = <String, TxLabel>{
|
||||
for (final label in a.customLabels) label.id: label,
|
||||
};
|
||||
@@ -66,6 +73,7 @@ TxMetaStoreData mergeTxMetaStores(TxMetaStoreData a, TxMetaStoreData b) {
|
||||
labelsByMerchant: labelsByMerchant,
|
||||
notesByMerchant: notesByMerchant,
|
||||
exactTemplates: exactTemplates,
|
||||
recurringTemplates: recurringTemplates,
|
||||
customLabels: labelsByClientId.values.toList(growable: false),
|
||||
labelParents: labelParents,
|
||||
);
|
||||
@@ -170,6 +178,7 @@ class PbSyncService {
|
||||
}) async {
|
||||
final out = <RecordModel>[];
|
||||
var page = 1;
|
||||
var lastReported = -1;
|
||||
while (true) {
|
||||
final result = await _withTimeout(
|
||||
_pb.collection(collection).getList(
|
||||
@@ -181,8 +190,13 @@ class PbSyncService {
|
||||
label: '$collection list page $page',
|
||||
);
|
||||
out.addAll(result.items);
|
||||
onPage?.call(out.length);
|
||||
if (result.items.length < _listPageSize) break;
|
||||
final finished = result.items.length < _listPageSize;
|
||||
if (onPage != null &&
|
||||
(finished || out.length - lastReported >= 400)) {
|
||||
lastReported = out.length;
|
||||
onPage(out.length);
|
||||
}
|
||||
if (finished) break;
|
||||
page++;
|
||||
}
|
||||
return out;
|
||||
@@ -213,7 +227,17 @@ class PbSyncService {
|
||||
report(0.36);
|
||||
onStatus?.call('Pulling templates…');
|
||||
final exactRecords = await _fullList('exact_templates', filter);
|
||||
report(0.45);
|
||||
report(0.42);
|
||||
onStatus?.call('Pulling recurring templates…');
|
||||
List<RecordModel> recurringRecords = const [];
|
||||
try {
|
||||
recurringRecords = await _fullList('recurring_templates', filter);
|
||||
} catch (error) {
|
||||
debugPrint(
|
||||
'PbSyncService pull: recurring_templates unavailable ($error)',
|
||||
);
|
||||
}
|
||||
report(0.48);
|
||||
|
||||
final customLabels = <TxLabel>[];
|
||||
final labelParents = <String, String>{};
|
||||
@@ -269,10 +293,35 @@ class PbSyncService {
|
||||
);
|
||||
}
|
||||
|
||||
final recurringTemplates = <String, RecurringTemplate>{};
|
||||
for (final r in recurringRecords) {
|
||||
final clientId = r.getStringValue('client_id');
|
||||
if (clientId.isEmpty) continue;
|
||||
final sourceTx = r.getStringValue('source_tx_id');
|
||||
final labelId = r.getStringValue('label_id');
|
||||
recurringTemplates[clientId] = RecurringTemplate(
|
||||
id: clientId,
|
||||
name: r.getStringValue('name').isEmpty
|
||||
? 'Recurring'
|
||||
: r.getStringValue('name'),
|
||||
dayOfMonth: r.getIntValue('day_of_month', 1).clamp(1, 28),
|
||||
currency: r.getStringValue('currency').isEmpty
|
||||
? 'EUR'
|
||||
: r.getStringValue('currency'),
|
||||
source: _parseTemplateSource(r.getStringValue('source')),
|
||||
amountRule: _parseAmountRule(r.getStringValue('amount_rule')),
|
||||
merchantKey: r.getStringValue('merchant_key'),
|
||||
sourceTxId: sourceTx.isEmpty ? null : sourceTx,
|
||||
labelId: labelId.isEmpty ? null : labelId,
|
||||
avgCount: r.getIntValue('avg_count', 5).clamp(1, 36),
|
||||
autoFromRecurring: r.getBoolValue('auto_from_recurring'),
|
||||
);
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
'PbSyncService pull: labels=${labelRecords.length} '
|
||||
'tx_meta=${metaRecords.length} merchants=${merchantRecords.length} '
|
||||
'exact=${exactRecords.length}',
|
||||
'exact=${exactRecords.length} recurring=${recurringRecords.length}',
|
||||
);
|
||||
|
||||
return TxMetaStoreData(
|
||||
@@ -280,6 +329,7 @@ class PbSyncService {
|
||||
labelsByMerchant: labelsByMerchant,
|
||||
notesByMerchant: notesByMerchant,
|
||||
exactTemplates: exactTemplates,
|
||||
recurringTemplates: recurringTemplates,
|
||||
customLabels: customLabels,
|
||||
labelParents: labelParents,
|
||||
);
|
||||
@@ -347,6 +397,15 @@ class PbSyncService {
|
||||
report(0.96);
|
||||
onStatus?.call('Syncing templates…');
|
||||
await _syncExactTemplates(userId, filter, store);
|
||||
report(0.98);
|
||||
onStatus?.call('Syncing recurring templates…');
|
||||
try {
|
||||
await _syncRecurringTemplates(userId, filter, store);
|
||||
} catch (error) {
|
||||
debugPrint(
|
||||
'PbSyncService push: recurring_templates unavailable ($error)',
|
||||
);
|
||||
}
|
||||
report(1.0);
|
||||
return txSummary;
|
||||
}
|
||||
@@ -563,7 +622,11 @@ class PbSyncService {
|
||||
]);
|
||||
done = end;
|
||||
onProgress?.call(done / total);
|
||||
onStatus?.call('Syncing $collection ($done/$total)…');
|
||||
// Status less often than progress — SyncController already throttles,
|
||||
// but fewer string allocs/churn still help.
|
||||
if (done == total || done == end && (done ~/ _writeConcurrency) % 4 == 0) {
|
||||
onStatus?.call('Syncing $collection ($done/$total)…');
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.isNotEmpty) {
|
||||
@@ -603,6 +666,12 @@ class PbSyncService {
|
||||
filter =
|
||||
'user="$userId" && exact_key="${key.replaceAll('"', '\\"')}"';
|
||||
}
|
||||
case 'recurring_templates':
|
||||
final key = body['client_id'];
|
||||
if (userId != null && key is String) {
|
||||
filter =
|
||||
'user="$userId" && client_id="${key.replaceAll('"', '\\"')}"';
|
||||
}
|
||||
case 'transactions':
|
||||
final key = body['tx_id'];
|
||||
if (userId != null && key is String) {
|
||||
@@ -853,6 +922,61 @@ class PbSyncService {
|
||||
await _runWriteOps('exact_templates', ops);
|
||||
}
|
||||
|
||||
Future<void> _syncRecurringTemplates(
|
||||
String userId,
|
||||
String filter,
|
||||
TxMetaStoreData store,
|
||||
) async {
|
||||
final existing = {
|
||||
for (final r in await _fullList('recurring_templates', filter))
|
||||
r.getStringValue('client_id'): r,
|
||||
};
|
||||
final keep = <String>{};
|
||||
final ops = <_BatchOp>[];
|
||||
|
||||
for (final e in store.recurringTemplates.entries) {
|
||||
final t = e.value;
|
||||
final clientId = _clip(t.id.isNotEmpty ? t.id : e.key, 120);
|
||||
if (clientId.isEmpty) continue;
|
||||
keep.add(clientId);
|
||||
final body = <String, dynamic>{
|
||||
'user': userId,
|
||||
'client_id': clientId,
|
||||
'name': _clip(t.name.isEmpty ? 'Recurring' : t.name, 400),
|
||||
'day_of_month': t.dayOfMonth.clamp(1, 28),
|
||||
'currency': _clip(
|
||||
t.currency.isEmpty ? 'EUR' : t.currency.toUpperCase(),
|
||||
8,
|
||||
),
|
||||
'source': t.source.name,
|
||||
'amount_rule': t.amountRule.name,
|
||||
'merchant_key': _clip(t.merchantKey, 500),
|
||||
'source_tx_id': _clip(t.sourceTxId ?? '', 2000),
|
||||
'label_id': _clip(t.labelId ?? '', 80),
|
||||
'avg_count': t.avgCount.clamp(1, 36),
|
||||
'auto_from_recurring': t.autoFromRecurring,
|
||||
};
|
||||
final remote = existing[clientId];
|
||||
if (remote == null) {
|
||||
ops.add(_BatchOp.create(body));
|
||||
} else if (!_recurringUnchanged(remote, t)) {
|
||||
ops.add(_BatchOp.update(remote.id, body));
|
||||
}
|
||||
}
|
||||
|
||||
for (final e in existing.entries) {
|
||||
if (!keep.contains(e.key)) {
|
||||
ops.add(_BatchOp.delete(e.value.id));
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
'PbSyncService recurring_templates: remote=${existing.length} '
|
||||
'ops=${ops.length}',
|
||||
);
|
||||
await _runWriteOps('recurring_templates', ops);
|
||||
}
|
||||
|
||||
bool _labelUnchanged(RecordModel remote, Map<String, dynamic> body) {
|
||||
return remote.getStringValue('name') == body['name'] &&
|
||||
remote.getIntValue('color') == body['color'] &&
|
||||
@@ -882,6 +1006,34 @@ class PbSyncService {
|
||||
remote.getBoolValue('recurring') == t.recurring &&
|
||||
remote.getBoolValue('internal_transfer') == t.internalTransfer;
|
||||
}
|
||||
|
||||
bool _recurringUnchanged(RecordModel remote, RecurringTemplate t) {
|
||||
return remote.getStringValue('name') == t.name &&
|
||||
remote.getIntValue('day_of_month') == t.dayOfMonth &&
|
||||
remote.getStringValue('currency') == t.currency &&
|
||||
remote.getStringValue('source') == t.source.name &&
|
||||
remote.getStringValue('amount_rule') == t.amountRule.name &&
|
||||
remote.getStringValue('merchant_key') == t.merchantKey &&
|
||||
remote.getStringValue('source_tx_id') == (t.sourceTxId ?? '') &&
|
||||
remote.getStringValue('label_id') == (t.labelId ?? '') &&
|
||||
remote.getIntValue('avg_count', 5) == t.avgCount &&
|
||||
remote.getBoolValue('auto_from_recurring') == t.autoFromRecurring;
|
||||
}
|
||||
}
|
||||
|
||||
RecurringTemplateSource _parseTemplateSource(String raw) {
|
||||
return switch (raw) {
|
||||
'label' => RecurringTemplateSource.label,
|
||||
_ => RecurringTemplateSource.transaction,
|
||||
};
|
||||
}
|
||||
|
||||
RecurringAmountRule _parseAmountRule(String raw) {
|
||||
return switch (raw) {
|
||||
'avgLastTxs' => RecurringAmountRule.avgLastTxs,
|
||||
'labelAvgMonths' => RecurringAmountRule.labelAvgMonths,
|
||||
_ => RecurringAmountRule.lastValue,
|
||||
};
|
||||
}
|
||||
|
||||
List<String> _jsonStringList(dynamic raw) {
|
||||
|
||||
Reference in New Issue
Block a user