@@ -8,6 +8,7 @@ import com.powersync.bucket.BucketChecksum
8
8
import com.powersync.bucket.BucketRequest
9
9
import com.powersync.bucket.BucketStorage
10
10
import com.powersync.bucket.Checkpoint
11
+ import com.powersync.bucket.PowerSyncControlArguments
11
12
import com.powersync.bucket.WriteCheckpointResponse
12
13
import com.powersync.connectors.PowerSyncBackendConnector
13
14
import com.powersync.db.crud.CrudEntry
@@ -39,9 +40,11 @@ import kotlinx.coroutines.NonCancellable
39
40
import kotlinx.coroutines.cancelAndJoin
40
41
import kotlinx.coroutines.channels.BufferOverflow
41
42
import kotlinx.coroutines.channels.Channel
43
+ import kotlinx.coroutines.channels.consume
42
44
import kotlinx.coroutines.coroutineScope
43
45
import kotlinx.coroutines.delay
44
46
import kotlinx.coroutines.flow.Flow
47
+ import kotlinx.coroutines.flow.collect
45
48
import kotlinx.coroutines.flow.emitAll
46
49
import kotlinx.coroutines.flow.flow
47
50
import kotlinx.coroutines.launch
@@ -297,43 +300,41 @@ internal class SyncStream(
297
300
*/
298
301
private inner class ActiveIteration (
299
302
val scope : CoroutineScope ,
300
- var hadSyncLine : Boolean = false ,
301
303
var fetchLinesJob : Job ? = null ,
302
304
var credentialsInvalidation : Job ? = null ,
303
305
) {
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
- }
306
+ // Using a channel for control invocations so that they're handled by a single coroutine,
307
+ // avoiding races between concurrent jobs like fetching credentials.
308
+ private val controlInvocations = Channel <PowerSyncControlArguments >()
313
309
314
- suspend fun stop () {
315
- control(" stop" )
316
- fetchLinesJob?.join()
317
- }
318
-
319
- private suspend fun control (
320
- op : String ,
321
- payload : String? = null,
322
- ) {
323
- val instructions = bucketStorage.control(op, payload)
324
- handleInstructions(instructions)
310
+ private suspend fun invokeControl (args : PowerSyncControlArguments ) {
311
+ val instructions = bucketStorage.control(args)
312
+ instructions.forEach { handleInstruction(it) }
325
313
}
326
314
327
- private suspend fun control (
328
- op : String ,
329
- payload : ByteArray ,
330
- ) {
331
- val instructions = bucketStorage.control(op, payload)
332
- handleInstructions(instructions)
315
+ suspend fun start () {
316
+ invokeControl(PowerSyncControlArguments .Start (params))
317
+
318
+ var hadSyncLine = false
319
+ for (line in controlInvocations) {
320
+ val instructions = bucketStorage.control(line)
321
+ instructions.forEach { handleInstruction(it) }
322
+
323
+ if (! hadSyncLine && (line is PowerSyncControlArguments .TextLine || line is PowerSyncControlArguments .BinaryLine )) {
324
+ // Trigger a crud upload when receiving the first sync line: We could have
325
+ // pending local writes made while disconnected, so in addition to listening on
326
+ // updates to `ps_crud`, we also need to trigger a CRUD upload in some other
327
+ // cases. We do this on the first sync line because the client is likely to be
328
+ // online in that case.
329
+ hadSyncLine = true
330
+ triggerCrudUploadAsync()
331
+ }
332
+ }
333
333
}
334
334
335
- private suspend fun handleInstructions (instructions : List <Instruction >) {
336
- instructions.forEach { handleInstruction(it) }
335
+ suspend fun stop () {
336
+ invokeControl(PowerSyncControlArguments .Stop )
337
+ fetchLinesJob?.join()
337
338
}
338
339
339
340
private suspend fun handleInstruction (instruction : Instruction ) {
@@ -344,20 +345,25 @@ internal class SyncStream(
344
345
scope.launch {
345
346
launch {
346
347
logger.v { " listening for completed uploads" }
347
-
348
348
for (completion in completedCrudUploads) {
349
- control( " completed_upload " )
349
+ controlInvocations.send( PowerSyncControlArguments . CompletedUpload )
350
350
}
351
351
}
352
352
353
353
launch {
354
354
connect(instruction)
355
355
}
356
+ }.also {
357
+ it.invokeOnCompletion {
358
+ controlInvocations.close()
359
+ }
356
360
}
357
361
}
358
362
Instruction .CloseSyncStream -> {
363
+ logger.v { " Closing sync stream connection" }
359
364
fetchLinesJob!! .cancelAndJoin()
360
365
fetchLinesJob = null
366
+ logger.v { " Sync stream connection shut down" }
361
367
}
362
368
Instruction .FlushSileSystem -> {
363
369
// We have durable file systems, so flushing is not necessary
@@ -389,6 +395,7 @@ internal class SyncStream(
389
395
val job =
390
396
scope.launch {
391
397
connector.prefetchCredentials().join()
398
+ logger.v { " Stopping because new credentials are available" }
392
399
393
400
// Token has been refreshed, start another iteration
394
401
stop()
@@ -409,36 +416,16 @@ internal class SyncStream(
409
416
}
410
417
}
411
418
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
- control(" line_text" , text)
428
- triggerCrudUploadIfFirstLine()
429
- }
430
-
431
- private suspend fun line (blob : ByteArray ) {
432
- control(" line_binary" , blob)
433
- triggerCrudUploadIfFirstLine()
434
- }
435
-
436
419
private suspend fun connect (start : Instruction .EstablishSyncStream ) {
437
420
when (val method = options.method) {
438
421
ConnectionMethod .Http ->
439
- connectViaHttp(start.request).collect(this ::line)
422
+ connectViaHttp(start.request).collect {
423
+ controlInvocations.send(PowerSyncControlArguments .TextLine (it))
424
+ }
440
425
is ConnectionMethod .WebSocket ->
441
- connectViaWebSocket(start.request, method).collect(this ::line)
426
+ connectViaWebSocket(start.request, method).collect {
427
+ controlInvocations.send(PowerSyncControlArguments .BinaryLine (it))
428
+ }
442
429
}
443
430
}
444
431
}
0 commit comments