@@ -23,9 +23,7 @@ import kotlinx.datetime.Clock
2323import to.bitkit.R
2424import to.bitkit.data.AppDb
2525import to.bitkit.data.CacheStore
26- import to.bitkit.data.SettingsData
2726import to.bitkit.data.SettingsStore
28- import to.bitkit.data.WidgetsData
2927import to.bitkit.data.WidgetsStore
3028import to.bitkit.data.backup.VssBackupClient
3129import to.bitkit.data.resetPin
@@ -48,6 +46,7 @@ import to.bitkit.services.LightningService
4846import to.bitkit.ui.shared.toast.ToastEventBus
4947import to.bitkit.utils.Logger
5048import to.bitkit.utils.jsonLogOf
49+ import java.util.concurrent.ConcurrentHashMap
5150import javax.inject.Inject
5251import javax.inject.Singleton
5352
@@ -72,6 +71,9 @@ class BackupRepo @Inject constructor(
7271 private val statusObserverJobs = mutableListOf<Job >()
7372 private val dataListenerJobs = mutableListOf<Job >()
7473 private var periodicCheckJob: Job ? = null
74+
75+ private val runningBackups = ConcurrentHashMap .newKeySet<BackupCategory >()
76+
7577 private var isObserving = false
7678 private var lastNotificationTime = 0L
7779
@@ -97,6 +99,22 @@ class BackupRepo @Inject constructor(
9799 Logger .debug(" Start observing backup statuses and data store changes" , context = TAG )
98100
99101 scope.launch { vssBackupClient.setup() }
102+
103+ scope.launch {
104+ BackupCategory .entries.forEach { category ->
105+ if (category !in runningBackups) {
106+ cacheStore.updateBackupStatus(category) { status ->
107+ if (status.running) {
108+ Logger .debug(" Clearing stale running flag for: '$category '" , context = TAG )
109+ status.copy(running = false )
110+ } else {
111+ status
112+ }
113+ }
114+ }
115+ }
116+ }
117+
100118 startBackupStatusObservers()
101119 startDataStoreListeners()
102120 startPeriodicBackupFailureCheck()
@@ -269,29 +287,40 @@ class BackupRepo @Inject constructor(
269287 }
270288
271289 private fun scheduleBackup (category : BackupCategory ) {
272- // Cancel existing backup job for this category
273290 backupJobs[category]?.cancel()
274291
275292 Logger .verbose(" Scheduling backup for: '$category '" , context = TAG )
276293
277294 backupJobs[category] = scope.launch {
278- // Set running immediately to prevent UI showing failure during debounce
295+ runningBackups + = category
279296 cacheStore.updateBackupStatus(category) {
280297 it.copy(running = true )
281298 }
282299
283300 delay(BACKUP_DEBOUNCE )
284301
285- // Double-check if backup is still needed
286302 val status = cacheStore.backupStatuses.first()[category] ? : BackupItemStatus ()
287- if (status.shouldBackup ()) {
303+ if (status.isRequired && ! shouldSkipBackup ()) {
288304 triggerBackup(category)
289305 } else {
290- // Backup no longer needed, reset running flag
306+ Logger .debug(" Backup no longer needed for: '$category '" , context = TAG )
307+ runningBackups - = category
291308 cacheStore.updateBackupStatus(category) {
292309 it.copy(running = false )
293310 }
294311 }
312+ }.also { job ->
313+ job.invokeOnCompletion { exception ->
314+ if (exception != null ) {
315+ Logger .debug(" Backup job cancelled for: '$category '" , context = TAG )
316+ scope.launch {
317+ runningBackups - = category
318+ cacheStore.updateBackupStatus(category) {
319+ it.copy(running = false )
320+ }
321+ }
322+ }
323+ }
295324 }
296325 }
297326
@@ -335,12 +364,14 @@ class BackupRepo @Inject constructor(
335364 suspend fun triggerBackup (category : BackupCategory ) = withContext(ioDispatcher) {
336365 Logger .debug(" Backup starting for: '$category '" , context = TAG )
337366
367+ runningBackups + = category
338368 cacheStore.updateBackupStatus(category) {
339369 it.copy(running = true , required = currentTimeMillis())
340370 }
341371
342372 vssBackupClient.putObject(key = category.name, data = getBackupDataBytes(category))
343373 .onSuccess {
374+ runningBackups - = category
344375 cacheStore.updateBackupStatus(category) {
345376 it.copy(
346377 running = false ,
@@ -350,6 +381,7 @@ class BackupRepo @Inject constructor(
350381 Logger .info(" Backup succeeded for: '$category '" , context = TAG )
351382 }
352383 .onFailure { e ->
384+ runningBackups - = category
353385 cacheStore.updateBackupStatus(category) {
354386 it.copy(running = false )
355387 }
@@ -452,34 +484,34 @@ class BackupRepo @Inject constructor(
452484 val tagMetadata = parsed.tagMetadata.map { it.toTagMetadataEntity() }
453485 db.tagMetadataDao().upsert(tagMetadata)
454486 Logger .debug(" Restored ${tagMetadata.size} pre-activity metadata" , TAG )
487+ parsed.createdAt
455488 }
456489
457490 performRestore(BackupCategory .SETTINGS ) { dataBytes ->
458- val parsed = json.decodeFromString<SettingsData >(String (dataBytes)).resetPin()
459- settingsStore.update { parsed }
460491 val parsed = json.decodeFromString<SettingsBackupV1 >(String (dataBytes))
461492 settingsStore.restoreFromBackup(parsed)
493+ parsed.createdAt
462494 }
463495 performRestore(BackupCategory .WIDGETS ) { dataBytes ->
464- val parsed = json.decodeFromString<WidgetsData >(String (dataBytes))
465- widgetsStore.update { parsed }
466496 val parsed = json.decodeFromString<WidgetsBackupV1 >(String (dataBytes))
467497 widgetsStore.restoreFromBackup(parsed)
498+ parsed.createdAt
468499 }
469500 performRestore(BackupCategory .WALLET ) { dataBytes ->
470501 val parsed = json.decodeFromString<WalletBackupV1 >(String (dataBytes))
471502 db.transferDao().upsert(parsed.transfers)
472503 Logger .debug(" Restored ${parsed.transfers.size} transfers" , context = TAG )
504+ parsed.createdAt
473505 }
474506 performRestore(BackupCategory .BLOCKTANK ) { dataBytes ->
475507 val parsed = json.decodeFromString<BlocktankBackupV1 >(String (dataBytes))
476- blocktankRepo.restoreFromBackup(parsed).onSuccess {
477- Logger .debug(" Restored ${parsed.orders.size} orders, ${parsed.cjitEntries.size} CJITs" , TAG )
478- }
508+ blocktankRepo.restoreFromBackup(parsed)
509+ parsed.createdAt
479510 }
480511 performRestore(BackupCategory .ACTIVITY ) { dataBytes ->
481512 val parsed = json.decodeFromString<ActivityBackupV1 >(String (dataBytes))
482513 activityRepo.restoreFromBackup(parsed)
514+ parsed.createdAt
483515 }
484516
485517 Logger .info(" Full restore success" , context = TAG )
@@ -503,24 +535,25 @@ class BackupRepo @Inject constructor(
503535
504536 private suspend fun performRestore (
505537 category : BackupCategory ,
506- restoreAction : suspend (ByteArray ) -> Unit ,
538+ restoreAction : suspend (dataBytes: ByteArray ) -> Long ,
507539 ): Result <Unit > = runCatching {
540+ var createdAtTimestamp = currentTimeMillis()
541+
508542 vssBackupClient.getObject(category.name).map { it?.value }
509543 .onSuccess { dataBytes ->
510544 if (dataBytes == null ) {
511545 Logger .warn(" Restore null for: '$category '" , context = TAG )
512546 } else {
513- restoreAction(dataBytes)
547+ createdAtTimestamp = restoreAction(dataBytes)
514548 Logger .info(" Restore success for: '$category '" , context = TAG )
515549 }
516550 }
517551 .onFailure {
518552 Logger .debug(" Restore error for: '$category '" , context = TAG )
519553 }
520554
521- val now = currentTimeMillis()
522555 cacheStore.updateBackupStatus(category) {
523- it.copy(running = false , synced = now , required = now )
556+ it.copy(running = false , synced = createdAtTimestamp , required = createdAtTimestamp )
524557 }
525558 }
526559
0 commit comments