Skip to content

Commit 5804bc3

Browse files
authored
Android: stop over-fetching after local edits and on WS echoes (#307)
1 parent 7815163 commit 5804bc3

6 files changed

Lines changed: 61 additions & 57 deletions

File tree

android/app/src/main/java/com/dkhalife/tasks/data/sync/SyncCoordinator.kt

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,12 @@ class SyncCoordinator @Inject constructor(
5353
private val mutex = Mutex()
5454

5555
@Volatile
56-
private var activeJob: Job? = null
56+
private var activeFullJob: Job? = null
5757

5858
fun syncOnce() {
5959
if (!networkMonitor.isOnline.value) return
60-
if (activeJob?.isActive == true) return
61-
activeJob = scope.launch {
60+
if (activeFullJob?.isActive == true) return
61+
activeFullJob = scope.launch {
6262
mutex.withLock {
6363
try {
6464
flushOutbox()
@@ -72,14 +72,41 @@ class SyncCoordinator @Inject constructor(
7272

7373
suspend fun syncOnceBlocking(): Boolean {
7474
if (!networkMonitor.isOnline.value) return false
75-
return mutex.withLock {
76-
try {
77-
flushOutbox()
78-
refreshAll()
79-
true
80-
} catch (e: Exception) {
81-
telemetryManager.logError(TAG, "Sync cycle failed: ${e.message}", e)
82-
false
75+
activeFullJob?.takeIf { it.isActive }?.let {
76+
it.join()
77+
return true
78+
}
79+
val job = scope.launch {
80+
mutex.withLock {
81+
try {
82+
flushOutbox()
83+
refreshAll()
84+
} catch (e: Exception) {
85+
telemetryManager.logError(TAG, "Sync cycle failed: ${e.message}", e)
86+
}
87+
}
88+
}
89+
activeFullJob = job
90+
job.join()
91+
return true
92+
}
93+
94+
/**
95+
* Flush-only path for locally-initiated mutations. No server refresh — the server's
96+
* WebSocket echo (handled by [WebSocketSyncBridge]) triggers reconciliation. Always
97+
* launches a coroutine so newly-enqueued rows are never stranded; concurrent flushes
98+
* serialize through [mutex] and each pass drains the full outbox, so redundant calls
99+
* collapse into cheap no-ops rather than missed work.
100+
*/
101+
fun flushPending() {
102+
if (!networkMonitor.isOnline.value) return
103+
scope.launch {
104+
mutex.withLock {
105+
try {
106+
flushOutbox()
107+
} catch (e: Exception) {
108+
telemetryManager.logError(TAG, "Outbox flush failed: ${e.message}", e)
109+
}
83110
}
84111
}
85112
}

android/app/src/main/java/com/dkhalife/tasks/data/sync/WebSocketSyncBridge.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class WebSocketSyncBridge @Inject constructor(
3737
webSocketManager.messages
3838
.debounce(DEBOUNCE_MS)
3939
.collect { message ->
40-
if (message.action in TASK_EVENTS) {
40+
if (message.action in SYNC_EVENTS) {
4141
syncAll()
4242
}
4343
}
@@ -71,13 +71,16 @@ class WebSocketSyncBridge @Inject constructor(
7171
companion object {
7272
private const val TAG = "WebSocketSyncBridge"
7373
private const val DEBOUNCE_MS = 500L
74-
private val TASK_EVENTS = setOf(
74+
private val SYNC_EVENTS = setOf(
7575
"task_created",
7676
"task_updated",
7777
"task_deleted",
7878
"task_completed",
7979
"task_uncompleted",
80-
"task_skipped"
80+
"task_skipped",
81+
"label_created",
82+
"label_updated",
83+
"label_deleted",
8184
)
8285
}
8386
}

android/app/src/main/java/com/dkhalife/tasks/repo/LabelRepository.kt

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ class LabelRepository @Inject constructor(
8080
)
8181
)
8282
}
83-
syncCoordinator.syncOnce()
83+
syncCoordinator.flushPending()
8484
Result.success(placeholderId)
8585
} catch (e: Exception) {
8686
telemetryManager.logError(TAG, "Failed to create label locally: ${e.message}", e)
@@ -114,7 +114,7 @@ class LabelRepository @Inject constructor(
114114
)
115115
}
116116
}
117-
syncCoordinator.syncOnce()
117+
syncCoordinator.flushPending()
118118
Result.success(Unit)
119119
} catch (e: Exception) {
120120
telemetryManager.logError(TAG, "Failed to update label locally: ${e.message}", e)
@@ -144,19 +144,16 @@ class LabelRepository @Inject constructor(
144144
enqueued = true
145145
}
146146
}
147-
if (enqueued) syncCoordinator.syncOnce()
147+
if (enqueued) syncCoordinator.flushPending()
148148
Result.success(Unit)
149149
} catch (e: Exception) {
150150
telemetryManager.logError(TAG, "Failed to delete label locally: ${e.message}", e)
151151
Result.failure(e)
152152
}
153153
}
154154

155-
fun updateLabelsFromWebSocket(@Suppress("UNUSED_PARAMETER") labels: List<Label>) {
156-
syncCoordinator.syncOnce()
157-
}
158-
159155
companion object {
160156
private const val TAG = "LabelRepository"
161157
}
162158
}
159+

android/app/src/main/java/com/dkhalife/tasks/repo/TaskRepository.kt

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ class TaskRepository @Inject constructor(
153153
)
154154
)
155155
}
156-
syncCoordinator.syncOnce()
156+
syncCoordinator.flushPending()
157157
Result.success(placeholderId)
158158
} catch (e: Exception) {
159159
telemetryManager.logError(TAG, "Failed to create task locally: ${e.message}", e)
@@ -197,7 +197,7 @@ class TaskRepository @Inject constructor(
197197
)
198198
}
199199
}
200-
syncCoordinator.syncOnce()
200+
syncCoordinator.flushPending()
201201
Result.success(Unit)
202202
} catch (e: Exception) {
203203
telemetryManager.logError(TAG, "Failed to update task locally: ${e.message}", e)
@@ -226,7 +226,7 @@ class TaskRepository @Inject constructor(
226226
enqueued = true
227227
}
228228
}
229-
if (enqueued) syncCoordinator.syncOnce()
229+
if (enqueued) syncCoordinator.flushPending()
230230
Result.success(Unit)
231231
} catch (e: Exception) {
232232
telemetryManager.logError(TAG, "Failed to delete task locally: ${e.message}", e)
@@ -256,7 +256,7 @@ class TaskRepository @Inject constructor(
256256
)
257257
)
258258
}
259-
syncCoordinator.syncOnce()
259+
syncCoordinator.flushPending()
260260
Result.success(Unit)
261261
} catch (e: Exception) {
262262
telemetryManager.logError(TAG, "Failed to update due date locally: ${e.message}", e)
@@ -277,20 +277,16 @@ class TaskRepository @Inject constructor(
277277
)
278278
)
279279
}
280-
syncCoordinator.syncOnce()
280+
syncCoordinator.flushPending()
281281
Result.success(Unit)
282282
} catch (e: Exception) {
283283
telemetryManager.logError(TAG, "Failed to enqueue $opType locally: ${e.message}", e)
284284
Result.failure(e)
285285
}
286286
}
287287

288-
fun updateTasksFromWebSocket(@Suppress("UNUSED_PARAMETER") tasks: List<Task>) {
289-
// WebSocket pushes trigger a full sync via SyncCoordinator; DB updates flow from there.
290-
syncCoordinator.syncOnce()
291-
}
292-
293288
companion object {
294289
private const val TAG = "TaskRepository"
295290
}
296291
}
292+

android/app/src/main/java/com/dkhalife/tasks/viewmodel/LabelViewModel.kt

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import androidx.lifecycle.viewModelScope
55
import com.dkhalife.tasks.model.*
66
import com.dkhalife.tasks.repo.LabelRepository
77
import com.dkhalife.tasks.telemetry.TelemetryManager
8-
import com.dkhalife.tasks.ws.WebSocketManager
98
import dagger.hilt.android.lifecycle.HiltViewModel
109
import kotlinx.coroutines.flow.MutableStateFlow
1110
import kotlinx.coroutines.flow.StateFlow
@@ -15,7 +14,6 @@ import javax.inject.Inject
1514
@HiltViewModel
1615
class LabelViewModel @Inject constructor(
1716
private val labelRepository: LabelRepository,
18-
private val webSocketManager: WebSocketManager,
1917
private val telemetryManager: TelemetryManager
2018
) : ViewModel() {
2119

@@ -29,17 +27,6 @@ class LabelViewModel @Inject constructor(
2927

3028
init {
3129
refreshLabels()
32-
collectWebSocketMessages()
33-
}
34-
35-
private fun collectWebSocketMessages() {
36-
viewModelScope.launch {
37-
webSocketManager.messages.collect { message ->
38-
when (message.action) {
39-
"label_created", "label_updated", "label_deleted" -> refreshLabels()
40-
}
41-
}
42-
}
4330
}
4431

4532
fun refreshLabels() {

android/app/src/main/java/com/dkhalife/tasks/viewmodel/TaskListViewModel.kt

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -116,20 +116,14 @@ class TaskListViewModel @Inject constructor(
116116
private fun collectWebSocketMessages() {
117117
viewModelScope.launch {
118118
webSocketManager.messages.collect { message ->
119+
// The in-progress task list is kept fresh by WebSocketSyncBridge -> SyncCoordinator,
120+
// which updates the Room DB that `tasks` observes. Completed tasks are fetched via a
121+
// separate endpoint not covered by the coordinator, so refresh them here when
122+
// relevant WebSocket task actions are received.
119123
when (message.action) {
120-
"task_created", "task_updated", "task_skipped" -> refreshTasks()
121-
"task_completed" -> {
122-
refreshTasks()
123-
refreshCompletedTasks()
124-
}
125-
"task_uncompleted" -> {
126-
refreshTasks()
127-
refreshCompletedTasks()
128-
}
129-
"task_deleted" -> {
130-
refreshTasks()
131-
refreshCompletedTasks()
132-
}
124+
"task_completed",
125+
"task_uncompleted",
126+
"task_deleted" -> refreshCompletedTasks()
133127
}
134128
}
135129
}

0 commit comments

Comments
 (0)