Skip to content

Commit fca069d

Browse files
authored
Fix crud uploads for websockets (#212)
1 parent e5300be commit fca069d

File tree

6 files changed

+102
-92
lines changed

6 files changed

+102
-92
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## 1.2.2
4+
5+
* Supabase: Avoid creating `Json` serializers multiple times.
6+
* Fix local writes not being uploaded correctly when using WebSockets as a transport protocol.
7+
38
## 1.2.1
49

510
* [Supabase Connector] Fixed issue where only `400` HTTP status code errors where reported as connection errors. The connector now reports errors for codes `>=400`.

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,7 @@ abstract class BaseSyncIntegrationTest(
467467
val uploadStarted = CompletableDeferred<Unit>()
468468
testConnector.uploadDataCallback = { db ->
469469
db.getCrudBatch()?.let { batch ->
470+
logger.v { "connector: uploading crud batch" }
470471
uploadStarted.complete(Unit)
471472
completeUpload.await()
472473
batch.complete.invoke(null)
@@ -478,7 +479,7 @@ abstract class BaseSyncIntegrationTest(
478479
turbineScope {
479480
val turbine = database.currentStatus.asFlow().testIn(this)
480481
syncLines.send(SyncLine.KeepAlive(1234))
481-
turbine.waitFor { it.connected }
482+
turbine.waitFor { it.connected && !it.uploading }
482483
turbine.cancelAndIgnoreRemainingEvents()
483484
}
484485

@@ -675,7 +676,7 @@ class NewSyncIntegrationTest : BaseSyncIntegrationTest(true) {
675676
verifyNoMoreCalls(connector)
676677

677678
syncLines.send(SyncLine.KeepAlive(tokenExpiresIn = 10))
678-
prefetchCalled.complete(Unit)
679+
prefetchCalled.await()
679680
// Should still be connected before prefetch completes
680681
database.currentStatus.connected shouldBe true
681682

core/src/commonMain/kotlin/com/powersync/bucket/BucketStorage.kt

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import com.powersync.sync.Instruction
66
import com.powersync.sync.LegacySyncImplementation
77
import com.powersync.sync.SyncDataBatch
88
import com.powersync.sync.SyncLocalDatabaseResult
9+
import kotlinx.serialization.Serializable
10+
import kotlinx.serialization.json.JsonObject
911

1012
internal interface BucketStorage {
1113
fun getMaxOpId(): String
@@ -50,13 +52,31 @@ internal interface BucketStorage {
5052
partialPriority: BucketPriority? = null,
5153
): SyncLocalDatabaseResult
5254

53-
suspend fun control(
54-
op: String,
55-
payload: String?,
56-
): List<Instruction>
55+
suspend fun control(args: PowerSyncControlArguments): List<Instruction>
56+
}
57+
58+
internal sealed interface PowerSyncControlArguments {
59+
@Serializable
60+
class Start(
61+
val parameters: JsonObject,
62+
) : PowerSyncControlArguments
63+
64+
data object Stop : PowerSyncControlArguments
5765

58-
suspend fun control(
59-
op: String,
60-
payload: ByteArray,
61-
): List<Instruction>
66+
data class TextLine(
67+
val line: String,
68+
) : PowerSyncControlArguments
69+
70+
class BinaryLine(
71+
val line: ByteArray,
72+
) : PowerSyncControlArguments {
73+
override fun toString(): String = "BinaryLine"
74+
}
75+
76+
data object CompletedUpload : PowerSyncControlArguments
6277
}
78+
79+
@Serializable
80+
internal class StartSyncIteration(
81+
val parameters: JsonObject,
82+
)

core/src/commonMain/kotlin/com/powersync/bucket/BucketStorageImpl.kt

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -365,22 +365,21 @@ internal class BucketStorageImpl(
365365
return JsonUtil.json.decodeFromString<List<Instruction>>(result)
366366
}
367367

368-
override suspend fun control(
369-
op: String,
370-
payload: String?,
371-
): List<Instruction> =
368+
override suspend fun control(args: PowerSyncControlArguments): List<Instruction> =
372369
db.writeTransaction { tx ->
373-
logger.v { "powersync_control($op, $payload)" }
370+
logger.v { "powersync_control: $args" }
374371

375-
tx.get("SELECT powersync_control(?, ?) AS r", listOf(op, payload), ::handleControlResult)
376-
}
372+
val (op: String, data: Any?) =
373+
when (args) {
374+
is PowerSyncControlArguments.Start -> "start" to JsonUtil.json.encodeToString(args)
375+
PowerSyncControlArguments.Stop -> "stop" to null
377376

378-
override suspend fun control(
379-
op: String,
380-
payload: ByteArray,
381-
): List<Instruction> =
382-
db.writeTransaction { tx ->
383-
logger.v { "powersync_control($op, binary payload)" }
384-
tx.get("SELECT powersync_control(?, ?) AS r", listOf(op, payload), ::handleControlResult)
377+
PowerSyncControlArguments.CompletedUpload -> "completed_upload" to null
378+
379+
is PowerSyncControlArguments.BinaryLine -> "line_binary" to args.line
380+
is PowerSyncControlArguments.TextLine -> "line_text" to args.line
381+
}
382+
383+
tx.get("SELECT powersync_control(?, ?) AS r", listOf(op, data), ::handleControlResult)
385384
}
386385
}

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

Lines changed: 52 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import com.powersync.bucket.BucketChecksum
88
import com.powersync.bucket.BucketRequest
99
import com.powersync.bucket.BucketStorage
1010
import com.powersync.bucket.Checkpoint
11+
import com.powersync.bucket.PowerSyncControlArguments
1112
import com.powersync.bucket.WriteCheckpointResponse
1213
import com.powersync.connectors.PowerSyncBackendConnector
1314
import com.powersync.db.crud.CrudEntry
@@ -47,7 +48,6 @@ import kotlinx.coroutines.flow.flow
4748
import kotlinx.coroutines.launch
4849
import kotlinx.coroutines.withContext
4950
import kotlinx.datetime.Clock
50-
import kotlinx.serialization.Serializable
5151
import kotlinx.serialization.json.JsonElement
5252
import kotlinx.serialization.json.JsonObject
5353
import kotlinx.serialization.json.encodeToJsonElement
@@ -297,67 +297,71 @@ internal class SyncStream(
297297
*/
298298
private inner class ActiveIteration(
299299
val scope: CoroutineScope,
300-
var hadSyncLine: Boolean = false,
301300
var fetchLinesJob: Job? = null,
302301
var credentialsInvalidation: Job? = null,
303302
) {
304-
suspend fun start() {
305-
@Serializable
306-
class StartParameters(
307-
val parameters: JsonObject,
308-
)
309-
310-
control("start", JsonUtil.json.encodeToString(StartParameters(params)))
311-
fetchLinesJob?.join()
312-
}
313-
314-
suspend fun stop() {
315-
control("stop")
316-
fetchLinesJob?.join()
317-
}
303+
// Using a channel for control invocations so that they're handled by a single coroutine,
304+
// avoiding races between concurrent jobs like fetching credentials.
305+
private val controlInvocations = Channel<PowerSyncControlArguments>()
318306

319-
private suspend fun control(
320-
op: String,
321-
payload: String? = null,
322-
) {
323-
val instructions = bucketStorage.control(op, payload)
324-
handleInstructions(instructions)
307+
private suspend fun invokeControl(args: PowerSyncControlArguments) {
308+
val instructions = bucketStorage.control(args)
309+
instructions.forEach { handleInstruction(it) }
325310
}
326311

327-
private suspend fun control(
328-
op: String,
329-
payload: ByteArray,
330-
) {
331-
val instructions = bucketStorage.control(op, payload)
332-
handleInstructions(instructions)
312+
suspend fun start() {
313+
invokeControl(PowerSyncControlArguments.Start(params))
314+
315+
var hadSyncLine = false
316+
for (line in controlInvocations) {
317+
val instructions = bucketStorage.control(line)
318+
instructions.forEach { handleInstruction(it) }
319+
320+
if (!hadSyncLine && (line is PowerSyncControlArguments.TextLine || line is PowerSyncControlArguments.BinaryLine)) {
321+
// Trigger a crud upload when receiving the first sync line: We could have
322+
// pending local writes made while disconnected, so in addition to listening on
323+
// updates to `ps_crud`, we also need to trigger a CRUD upload in some other
324+
// cases. We do this on the first sync line because the client is likely to be
325+
// online in that case.
326+
hadSyncLine = true
327+
triggerCrudUploadAsync()
328+
}
329+
}
333330
}
334331

335-
private suspend fun handleInstructions(instructions: List<Instruction>) {
336-
instructions.forEach { handleInstruction(it) }
332+
suspend fun stop() {
333+
invokeControl(PowerSyncControlArguments.Stop)
334+
fetchLinesJob?.join()
337335
}
338336

339337
private suspend fun handleInstruction(instruction: Instruction) {
340338
when (instruction) {
341339
is Instruction.EstablishSyncStream -> {
342340
fetchLinesJob?.cancelAndJoin()
343341
fetchLinesJob =
344-
scope.launch {
345-
launch {
346-
logger.v { "listening for completed uploads" }
347-
348-
for (completion in completedCrudUploads) {
349-
control("completed_upload")
342+
scope
343+
.launch {
344+
launch {
345+
logger.v { "listening for completed uploads" }
346+
for (completion in completedCrudUploads) {
347+
controlInvocations.send(PowerSyncControlArguments.CompletedUpload)
348+
}
350349
}
351-
}
352350

353-
launch {
354-
connect(instruction)
351+
launch {
352+
connect(instruction)
353+
}
354+
}.also {
355+
it.invokeOnCompletion {
356+
controlInvocations.close()
357+
}
355358
}
356-
}
357359
}
358360
Instruction.CloseSyncStream -> {
361+
logger.v { "Closing sync stream connection" }
359362
fetchLinesJob!!.cancelAndJoin()
360363
fetchLinesJob = null
364+
logger.v { "Sync stream connection shut down" }
361365
}
362366
Instruction.FlushSileSystem -> {
363367
// We have durable file systems, so flushing is not necessary
@@ -389,9 +393,10 @@ internal class SyncStream(
389393
val job =
390394
scope.launch {
391395
connector.prefetchCredentials().join()
396+
logger.v { "Stopping because new credentials are available" }
392397

393398
// Token has been refreshed, start another iteration
394-
stop()
399+
controlInvocations.send(PowerSyncControlArguments.Stop)
395400
}
396401
job.invokeOnCompletion {
397402
credentialsInvalidation = null
@@ -409,36 +414,16 @@ internal class SyncStream(
409414
}
410415
}
411416

412-
/**
413-
* Triggers a crud upload when called for the first time.
414-
*
415-
* We could have pending local writes made while disconnected, so in addition to listening
416-
* on updates to `ps_crud`, we also need to trigger a CRUD upload in some other cases. We
417-
* do this on the first sync line because the client is likely to be online in that case.
418-
*/
419-
private fun triggerCrudUploadIfFirstLine() {
420-
if (!hadSyncLine) {
421-
triggerCrudUploadAsync()
422-
hadSyncLine = true
423-
}
424-
}
425-
426-
private suspend fun line(text: String) {
427-
triggerCrudUploadIfFirstLine()
428-
control("line_text", text)
429-
}
430-
431-
private suspend fun line(blob: ByteArray) {
432-
triggerCrudUploadIfFirstLine()
433-
control("line_binary", blob)
434-
}
435-
436417
private suspend fun connect(start: Instruction.EstablishSyncStream) {
437418
when (val method = options.method) {
438419
ConnectionMethod.Http ->
439-
connectViaHttp(start.request).collect(this::line)
420+
connectViaHttp(start.request).collect {
421+
controlInvocations.send(PowerSyncControlArguments.TextLine(it))
422+
}
440423
is ConnectionMethod.WebSocket ->
441-
connectViaWebSocket(start.request, method).collect(this::line)
424+
connectViaWebSocket(start.request, method).collect {
425+
controlInvocations.send(PowerSyncControlArguments.BinaryLine(it))
426+
}
442427
}
443428
}
444429
}

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ development=true
1919
RELEASE_SIGNING_ENABLED=true
2020
# Library config
2121
GROUP=com.powersync
22-
LIBRARY_VERSION=1.2.1
22+
LIBRARY_VERSION=1.2.2
2323
GITHUB_REPO=https://github.com/powersync-ja/powersync-kotlin.git
2424
# POM
2525
POM_URL=https://github.com/powersync-ja/powersync-kotlin/

0 commit comments

Comments
 (0)