@@ -40,12 +40,88 @@ import anki.collection.opChanges
4040import anki.import_export.ImportResponse
4141import com.ichi2.anki.common.crashreporting.CrashReportService
4242import com.ichi2.anki.utils.ext.ifNotZero
43+ import kotlinx.coroutines.CoroutineDispatcher
44+ import kotlinx.coroutines.CoroutineScope
45+ import kotlinx.coroutines.CoroutineStart
46+ import kotlinx.coroutines.Dispatchers
47+ import kotlinx.coroutines.Job
48+ import kotlinx.coroutines.SupervisorJob
49+ import kotlinx.coroutines.cancel
50+ import kotlinx.coroutines.channels.BufferOverflow
51+ import kotlinx.coroutines.flow.MutableSharedFlow
52+ import kotlinx.coroutines.launch
4353import org.jetbrains.annotations.Contract
4454import timber.log.Timber
4555import java.lang.ref.WeakReference
4656import java.util.concurrent.CopyOnWriteArrayList
4757
4858object ChangeManager {
59+ @get:VisibleForTesting
60+ @set:VisibleForTesting
61+ internal var dispatcher: CoroutineDispatcher = Dispatchers .Main
62+ set(value) {
63+ if (field == value) return
64+ field = value
65+
66+ resetCollector()
67+ }
68+
69+ private var scope: CoroutineScope ? = null
70+ private var collectorJob: Job ? = null
71+
72+ private val changeFlow =
73+ MutableSharedFlow <Pair <OpChanges , Any ?>>(
74+ extraBufferCapacity = 64 ,
75+ onBufferOverflow = BufferOverflow .DROP_OLDEST ,
76+ )
77+
78+ private fun ensureCollector () {
79+ if (collectorJob?.isActive == true ) return
80+
81+ val currentScope = CoroutineScope (dispatcher + SupervisorJob ())
82+ scope = currentScope
83+ collectorJob =
84+ currentScope.launch(start = CoroutineStart .UNDISPATCHED ) {
85+ changeFlow.collect { (changes, handler) ->
86+ notifySubscribers(changes, handler)
87+ }
88+ }
89+ }
90+
91+ private fun resetCollector () {
92+ collectorJob?.cancel()
93+ collectorJob = null
94+ scope?.cancel()
95+ scope = null
96+ }
97+
98+ fun <T : Any > publish (
99+ changes : T ,
100+ handler : Any? = null,
101+ ) {
102+ ensureCollector()
103+ val opChanges =
104+ when (changes) {
105+ is OpChanges -> changes
106+ is OpChangesWithCount -> changes.changes
107+ is OpChangesWithId -> changes.changes
108+ is OpChangesAfterUndo -> changes.changes
109+ is OpChangesOnly -> changes.changes
110+ is ImportResponse -> changes.changes
111+ else -> {
112+ Timber .e(" unhandled change type: ${changes::class } " )
113+ return
114+ }
115+ }
116+
117+ val success = changeFlow.tryEmit(Pair (opChanges, handler))
118+ if (! success) {
119+ scope?.launch {
120+ changeFlow.emit(Pair (opChanges, handler))
121+ }
122+ }
123+ }
124+
49125 // do not make this a 'fun interface' - lambdas may immediately be GCed
50126 // due to the use of WeakReference
51127 interface Subscriber {
@@ -78,6 +154,7 @@ object ChangeManager {
78154 subscriber : Subscriber ,
79155 owner : LifecycleOwner ? = subscriber as ? LifecycleOwner ,
80156 ) {
157+ ensureCollector()
81158 val weakRef = WeakReference (subscriber)
82159
83160 if (owner == null ) {
@@ -156,7 +233,10 @@ object ChangeManager {
156233 is OpChangesAfterUndo -> changes.changes
157234 is OpChangesOnly -> changes.changes
158235 is ImportResponse -> changes.changes
159- else -> TODO (" unhandled change type of class '${changes::class } '" )
236+ else -> {
237+ Timber .e(" unhandled change type: ${changes::class } " )
238+ return
239+ }
160240 }
161241 notifySubscribers(opChanges, initiator)
162242 }
0 commit comments