Skip to content

Commit cf4de2c

Browse files
[Fix] Sync Status Updates (#138)
1 parent 025b1d2 commit cf4de2c

File tree

9 files changed

+151
-71
lines changed

9 files changed

+151
-71
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## 1.0.0-BETA27
44

55
* Improved watch query internals. Added the ability to throttle watched queries.
6+
* Fixed `uploading` and `downloading` sync status indicators.
67

78
## 1.0.0-BETA26
89

core/src/commonIntegrationTest/kotlin/com/powersync/SyncIntegrationTest.kt

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,14 @@ class SyncIntegrationTest {
207207
SyncLine.FullCheckpoint(
208208
Checkpoint(
209209
lastOpId = "4",
210-
checksums = listOf(BucketChecksum(bucket = "bkt", priority = BucketPriority(1), checksum = 0)),
210+
checksums =
211+
listOf(
212+
BucketChecksum(
213+
bucket = "bkt",
214+
priority = BucketPriority(1),
215+
checksum = 0,
216+
),
217+
),
211218
),
212219
),
213220
)
@@ -228,4 +235,39 @@ class SyncIntegrationTest {
228235
database.close()
229236
syncLines.close()
230237
}
238+
239+
@Test
240+
fun setsDownloadingState() =
241+
runTest {
242+
val syncStream = syncStream()
243+
database.connect(syncStream, 1000L)
244+
245+
turbineScope(timeout = 10.0.seconds) {
246+
val turbine = database.currentStatus.asFlow().testIn(this)
247+
turbine.waitFor { it.connected && !it.downloading }
248+
249+
syncLines.send(
250+
SyncLine.FullCheckpoint(
251+
Checkpoint(
252+
lastOpId = "1",
253+
checksums =
254+
listOf(
255+
BucketChecksum(
256+
bucket = "bkt",
257+
checksum = 0,
258+
),
259+
),
260+
),
261+
),
262+
)
263+
turbine.waitFor { it.downloading }
264+
265+
syncLines.send(SyncLine.CheckpointComplete(lastOpId = "1"))
266+
turbine.waitFor { !it.downloading }
267+
turbine.cancel()
268+
}
269+
270+
database.close()
271+
syncLines.close()
272+
}
231273
}

core/src/commonMain/kotlin/com/powersync/db/PowerSyncDatabaseImpl.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ internal class PowerSyncDatabaseImpl(
128128
currentStatus.update(
129129
connected = it.connected,
130130
connecting = it.connecting,
131+
uploading = it.uploading,
131132
downloading = it.downloading,
132133
lastSyncedAt = it.lastSyncedAt,
133134
hasSynced = it.hasSynced,

core/src/commonMain/kotlin/com/powersync/sync/SyncStream.kt

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,6 @@ internal class SyncStream(
129129
var checkedCrudItem: CrudEntry? = null
130130

131131
while (true) {
132-
status.update(uploading = true)
133132
/**
134133
* This is the first item in the FIFO CRUD queue.
135134
*/
@@ -146,6 +145,7 @@ internal class SyncStream(
146145
}
147146

148147
checkedCrudItem = nextCrudItem
148+
status.update(uploading = true)
149149
uploadCrud()
150150
} else {
151151
// Uploading is completed
@@ -256,6 +256,8 @@ internal class SyncStream(
256256
state = handleInstruction(line, value, state)
257257
}
258258

259+
status.update(downloading = false)
260+
259261
return state
260262
}
261263

@@ -268,7 +270,12 @@ internal class SyncStream(
268270
is SyncLine.FullCheckpoint -> handleStreamingSyncCheckpoint(line, state)
269271
is SyncLine.CheckpointDiff -> handleStreamingSyncCheckpointDiff(line, state)
270272
is SyncLine.CheckpointComplete -> handleStreamingSyncCheckpointComplete(state)
271-
is SyncLine.CheckpointPartiallyComplete -> handleStreamingSyncCheckpointPartiallyComplete(line, state)
273+
is SyncLine.CheckpointPartiallyComplete ->
274+
handleStreamingSyncCheckpointPartiallyComplete(
275+
line,
276+
state,
277+
)
278+
272279
is SyncLine.KeepAlive -> handleStreamingKeepAlive(line, state)
273280
is SyncLine.SyncDataBucket -> handleStreamingSyncData(line, state)
274281
SyncLine.UnknownSyncLine -> {
@@ -283,6 +290,8 @@ internal class SyncStream(
283290
): SyncStreamState {
284291
val (checkpoint) = line
285292
state.targetCheckpoint = checkpoint
293+
status.update(downloading = true)
294+
286295
val bucketsToDelete = state.bucketSet!!.toMutableList()
287296
val newBuckets = mutableSetOf<String>()
288297

@@ -323,7 +332,12 @@ internal class SyncStream(
323332
}
324333

325334
state.validatedCheckpoint = state.targetCheckpoint
326-
status.update(lastSyncedAt = Clock.System.now(), hasSynced = true, clearDownloadError = true)
335+
status.update(
336+
lastSyncedAt = Clock.System.now(),
337+
downloading = false,
338+
hasSynced = true,
339+
clearDownloadError = true,
340+
)
327341

328342
return state
329343
}
@@ -374,6 +388,8 @@ internal class SyncStream(
374388
throw Exception("Checkpoint diff without previous checkpoint")
375389
}
376390

391+
status.update(downloading = true)
392+
377393
val newBuckets = mutableMapOf<String, BucketChecksum>()
378394

379395
state.targetCheckpoint!!.checksums.forEach { checksum ->
@@ -410,6 +426,7 @@ internal class SyncStream(
410426
data: SyncLine.SyncDataBucket,
411427
state: SyncStreamState,
412428
): SyncStreamState {
429+
status.update(downloading = true)
413430
bucketStorage.saveSyncData(SyncDataBatch(listOf(data)))
414431
return state
415432
}

demos/supabase-todolist/shared/src/commonMain/kotlin/com/powersync/demos/App.kt

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,28 @@ import com.powersync.demos.screens.HomeScreen
2424
import com.powersync.demos.screens.SignInScreen
2525
import com.powersync.demos.screens.SignUpScreen
2626
import com.powersync.demos.screens.TodosScreen
27+
import kotlinx.coroutines.flow.debounce
2728
import kotlinx.coroutines.runBlocking
2829

29-
3030
@Composable
31-
fun App(factory: DatabaseDriverFactory, modifier: Modifier = Modifier) {
32-
val supabase = remember {
33-
SupabaseConnector(
34-
powerSyncEndpoint = Config.POWERSYNC_URL,
35-
supabaseUrl = Config.SUPABASE_URL,
36-
supabaseKey = Config.SUPABASE_ANON_KEY
37-
)
38-
}
31+
fun App(
32+
factory: DatabaseDriverFactory,
33+
modifier: Modifier = Modifier,
34+
) {
35+
val supabase =
36+
remember {
37+
SupabaseConnector(
38+
powerSyncEndpoint = Config.POWERSYNC_URL,
39+
supabaseUrl = Config.SUPABASE_URL,
40+
supabaseKey = Config.SUPABASE_ANON_KEY,
41+
)
42+
}
3943
val db = remember { PowerSyncDatabase(factory, schema) }
40-
val status by db.currentStatus.asFlow().collectAsState(initial = db.currentStatus)
44+
// Debouncing the status flow prevents flicker
45+
val status by db.currentStatus
46+
.asFlow()
47+
.debounce(200)
48+
.collectAsState(initial = db.currentStatus)
4149

4250
// This assumes that the buckets for lists has a priority of 1 (but it will work fine with sync
4351
// rules not defining any priorities at all too). When giving lists a higher priority than
@@ -48,9 +56,10 @@ fun App(factory: DatabaseDriverFactory, modifier: Modifier = Modifier) {
4856
}
4957

5058
val navController = remember { NavController(Screen.Home) }
51-
val authViewModel = remember {
52-
AuthViewModel(supabase, db, navController)
53-
}
59+
val authViewModel =
60+
remember {
61+
AuthViewModel(supabase, db, navController)
62+
}
5463

5564
val authState by authViewModel.authState.collectAsState()
5665
val currentScreen by navController.currentScreen.collectAsState()
@@ -81,7 +90,7 @@ fun App(factory: DatabaseDriverFactory, modifier: Modifier = Modifier) {
8190

8291
when (currentScreen) {
8392
is Screen.Home -> {
84-
if(authState == AuthState.SignedOut) {
93+
if (authState == AuthState.SignedOut) {
8594
navController.navigate(Screen.SignIn)
8695
}
8796

@@ -93,14 +102,13 @@ fun App(factory: DatabaseDriverFactory, modifier: Modifier = Modifier) {
93102
HomeScreen(
94103
modifier = modifier.background(MaterialTheme.colors.background),
95104
items = items,
96-
isConnected = status.connected,
97105
onSignOutSelected = { handleSignOut() },
98106
inputText = listsInputText,
99107
onItemClicked = handleOnItemClicked,
100108
onItemDeleteClicked = lists.value::onItemDeleteClicked,
101109
onAddItemClicked = lists.value::onAddItemClicked,
102110
onInputTextChanged = lists.value::onInputTextChanged,
103-
hasSynced = hasSyncedLists
111+
syncStatus = status,
104112
)
105113
}
106114

@@ -113,7 +121,7 @@ fun App(factory: DatabaseDriverFactory, modifier: Modifier = Modifier) {
113121
modifier = modifier.background(MaterialTheme.colors.background),
114122
navController = navController,
115123
items = todoItems,
116-
isConnected = status.connected,
124+
syncStatus = status,
117125
inputText = todosInputText,
118126
onItemClicked = todos.value::onItemClicked,
119127
onItemDoneChanged = todos.value::onItemDoneChanged,
@@ -133,24 +141,24 @@ fun App(factory: DatabaseDriverFactory, modifier: Modifier = Modifier) {
133141
}
134142

135143
is Screen.SignIn -> {
136-
if(authState == AuthState.SignedIn) {
144+
if (authState == AuthState.SignedIn) {
137145
navController.navigate(Screen.Home)
138146
}
139147

140148
SignInScreen(
141149
navController,
142-
authViewModel
150+
authViewModel,
143151
)
144152
}
145153

146154
is Screen.SignUp -> {
147-
if(authState == AuthState.SignedIn) {
155+
if (authState == AuthState.SignedIn) {
148156
navController.navigate(Screen.Home)
149157
}
150158

151159
SignUpScreen(
152160
navController,
153-
authViewModel
161+
authViewModel,
154162
)
155163
}
156164
}

demos/supabase-todolist/shared/src/commonMain/kotlin/com/powersync/demos/components/WifiIcon.kt

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,29 @@ package com.powersync.demos.components
22

33
import androidx.compose.material.Icon
44
import androidx.compose.material.icons.Icons
5-
import androidx.compose.material.icons.filled.Wifi
6-
import androidx.compose.material.icons.filled.WifiOff
5+
import androidx.compose.material.icons.filled.Cloud
6+
import androidx.compose.material.icons.filled.CloudOff
7+
import androidx.compose.material.icons.filled.CloudSync
8+
import androidx.compose.material.icons.filled.LeakAdd
9+
import androidx.compose.material.icons.filled.Thunderstorm
710
import androidx.compose.runtime.Composable
11+
import com.powersync.sync.SyncStatusData
812

913
@Composable
10-
fun WifiIcon(isConnected: Boolean) {
11-
val icon = if (isConnected) {
12-
Icons.Filled.Wifi
13-
} else {
14-
Icons.Filled.WifiOff
15-
}
14+
fun WifiIcon(status: SyncStatusData) {
15+
val icon =
16+
when {
17+
status.downloading || status.uploading -> Icons.Filled.CloudSync
18+
status.connected -> Icons.Filled.Cloud
19+
!status.connected -> Icons.Filled.CloudOff
20+
status.connecting -> Icons.Filled.LeakAdd
21+
else -> {
22+
Icons.Filled.Thunderstorm
23+
}
24+
}
1625

1726
Icon(
1827
imageVector = icon,
19-
contentDescription = if (isConnected) "Online" else "Offline",
28+
contentDescription = status.toString(),
2029
)
21-
}
30+
}

demos/supabase-todolist/shared/src/commonMain/kotlin/com/powersync/demos/screens/HomeScreen.kt

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,14 @@ import com.powersync.demos.components.ListContent
2222
import com.powersync.demos.components.Menu
2323
import com.powersync.demos.components.WifiIcon
2424
import com.powersync.demos.powersync.ListItem
25+
import com.powersync.sync.SyncStatusData
2526

2627
@Composable
2728
internal fun HomeScreen(
2829
modifier: Modifier = Modifier,
2930
items: List<ListItem>,
3031
inputText: String,
31-
isConnected: Boolean,
32-
hasSynced: Boolean?,
32+
syncStatus: SyncStatusData,
3333
onSignOutSelected: () -> Unit,
3434
onItemClicked: (item: ListItem) -> Unit,
3535
onItemDeleteClicked: (item: ListItem) -> Unit,
@@ -40,46 +40,49 @@ internal fun HomeScreen(
4040
TopAppBar(
4141
title = {
4242
Text(
43-
"Todo Lists",
44-
textAlign = TextAlign.Center,
45-
modifier = Modifier.fillMaxWidth().padding(end = 36.dp)
46-
) },
47-
navigationIcon = { Menu(
48-
true,
49-
onSignOutSelected
50-
) },
43+
"Todo Lists",
44+
textAlign = TextAlign.Center,
45+
modifier = Modifier.fillMaxWidth().padding(end = 36.dp),
46+
)
47+
},
48+
navigationIcon = {
49+
Menu(
50+
true,
51+
onSignOutSelected,
52+
)
53+
},
5154
actions = {
52-
WifiIcon(isConnected)
55+
WifiIcon(syncStatus)
5356
Spacer(modifier = Modifier.width(16.dp))
54-
}
57+
},
5558
)
5659

5760
when {
58-
hasSynced == null || hasSynced == false -> {
59-
Box(
60-
modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.background),
61-
contentAlignment = Alignment.Center
62-
) {
63-
Text(
64-
text = "Busy with initial sync...",
65-
style = MaterialTheme.typography.h6
66-
)
67-
}
61+
syncStatus.hasSynced == null || syncStatus.hasSynced == false -> {
62+
Box(
63+
modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.background),
64+
contentAlignment = Alignment.Center,
65+
) {
66+
Text(
67+
text = "Busy with initial sync...",
68+
style = MaterialTheme.typography.h6,
69+
)
6870
}
69-
else -> {
71+
}
7072

73+
else -> {
7174
Input(
7275
text = inputText,
7376
onAddClicked = onAddItemClicked,
7477
onTextChanged = onInputTextChanged,
75-
screen = Screen.Home
78+
screen = Screen.Home,
7679
)
7780

7881
Box(Modifier.weight(1F)) {
7982
ListContent(
8083
items = items,
8184
onItemClicked = onItemClicked,
82-
onItemDeleteClicked = onItemDeleteClicked
85+
onItemDeleteClicked = onItemDeleteClicked,
8386
)
8487
}
8588
}

0 commit comments

Comments
 (0)