11package com.powersync.db.internal
22
33import com.powersync.ExperimentalPowerSyncAPI
4- import com.powersync.PowerSyncException
54import com.powersync.db.SqlCursor
65import com.powersync.db.ThrowableLockCallback
76import com.powersync.db.ThrowableTransactionCallback
@@ -15,8 +14,9 @@ import kotlinx.coroutines.Dispatchers
1514import kotlinx.coroutines.IO
1615import kotlinx.coroutines.flow.Flow
1716import kotlinx.coroutines.flow.SharedFlow
18- import kotlinx.coroutines.flow.channelFlow
19- import kotlinx.coroutines.flow.filter
17+ import kotlinx.coroutines.flow.emitAll
18+ import kotlinx.coroutines.flow.flow
19+ import kotlinx.coroutines.flow.map
2020import kotlinx.coroutines.flow.onSubscription
2121import kotlinx.coroutines.flow.transform
2222import kotlinx.coroutines.withContext
@@ -79,75 +79,69 @@ internal class InternalDatabaseImpl(
7979 tables : Set <String >,
8080 throttleMs : Long ,
8181 triggerImmediately : Boolean ,
82- ): Flow <Set <String >> =
83- channelFlow {
84- // Match all possible internal table combinations
85- val watchedTables =
86- tables.flatMap { listOf (it, " ps_data__$it " , " ps_data_local__$it " ) }.toSet()
87-
88- // Accumulate updates between throttles
89- val batchedUpdates = AtomicMutableSet <String >()
82+ ): Flow <Set <String >> {
83+ // Match all possible internal table combinations
84+ val watchedTables =
85+ tables.flatMap { listOf (it, " ps_data__$it " , " ps_data_local__$it " ) }.toSet()
9086
91- updatesOnTables()
92- .onSubscription {
93- if (triggerImmediately) {
94- // Emit an initial event (if requested). No changes would be detected at this point
95- send(setOf ())
96- }
97- }.transform { updates ->
98- val intersection = updates.intersect(watchedTables)
99- if (intersection.isNotEmpty()) {
100- // Transform table names using friendlyTableName
101- val friendlyTableNames = intersection.map { friendlyTableName(it) }.toSet()
102- batchedUpdates.addAll(friendlyTableNames)
103- emit(Unit )
104- }
105- }
106- // Throttling here is a feature which prevents watch queries from spamming updates.
107- // Throttling by design discards and delays events within the throttle window. Discarded events
108- // still trigger a trailing edge update.
109- // Backpressure is avoided on the throttling and consumer level by buffering the last upstream value.
110- .throttle(throttleMs.milliseconds)
111- .collect {
112- // Emit the transformed tables which have changed
113- val copy = batchedUpdates.toSetAndClear()
114- send(copy)
115- }
87+ return rawChangedTables(watchedTables, throttleMs, triggerImmediately).map {
88+ it.mapTo(mutableSetOf (), ::friendlyTableName)
11689 }
90+ }
11791
11892 override fun <RowType : Any > watch (
11993 sql : String ,
12094 parameters : List <Any ?>? ,
12195 throttleMs : Long ,
12296 mapper : (SqlCursor ) -> RowType ,
12397 ): Flow <List <RowType >> =
124- // Use a channel flow here since we throttle (buffer used under the hood)
125- // This causes some emissions to be from different scopes.
126- channelFlow {
98+ flow {
12799 // Fetch the tables asynchronously with getAll
128100 val tables =
129101 getSourceTables(sql, parameters)
130102 .filter { it.isNotBlank() }
131103 .toSet()
132104
105+ val queries =
106+ rawChangedTables(tables, throttleMs, triggerImmediately = true ).map {
107+ getAll(sql, parameters = parameters, mapper = mapper)
108+ }
109+ emitAll(queries)
110+ }
111+
112+ private fun rawChangedTables (
113+ tableNames : Set <String >,
114+ throttleMs : Long ,
115+ triggerImmediately : Boolean ,
116+ ): Flow <Set <String >> =
117+ flow {
118+ val batchedUpdates = AtomicMutableSet <String >()
119+
133120 updatesOnTables()
134- // onSubscription here is very important.
135- // This ensures that the initial result and all updates are emitted.
136121 .onSubscription {
137- send(getAll(sql, parameters = parameters, mapper = mapper))
138- }.filter {
139- // Only trigger updates on relevant tables
140- it.intersect(tables).isNotEmpty()
122+ if (triggerImmediately) {
123+ // Emit an initial event (if requested). No changes would be detected at this point
124+ emit(initialUpdateSentinel)
125+ }
126+ }.transform { updates ->
127+ if (updates == = initialUpdateSentinel) {
128+ // This should always be emitted despite being empty and not intersecting with
129+ // the tables we care about.
130+ emit(Unit )
131+ } else {
132+ val intersection = updates.intersect(tableNames)
133+ if (intersection.isNotEmpty()) {
134+ batchedUpdates.addAll(intersection)
135+ emit(Unit )
136+ }
137+ }
141138 }
142139 // Throttling here is a feature which prevents watch queries from spamming updates.
143140 // Throttling by design discards and delays events within the throttle window. Discarded events
144141 // still trigger a trailing edge update.
145142 // Backpressure is avoided on the throttling and consumer level by buffering the last upstream value.
146- // Note that the buffered upstream "value" only serves to trigger the getAll query. We don't buffer watch results.
147143 .throttle(throttleMs.milliseconds)
148- .collect {
149- send(getAll(sql, parameters = parameters, mapper = mapper))
150- }
144+ .collect { emit(batchedUpdates.toSetAndClear()) }
151145 }
152146
153147 override suspend fun <T > useConnection (
@@ -259,6 +253,10 @@ internal class InternalDatabaseImpl(
259253 val p2 : Long ,
260254 val p3 : Long ,
261255 )
256+
257+ private companion object {
258+ val initialUpdateSentinel = emptySet<String >()
259+ }
262260}
263261
264262/* *
0 commit comments