@@ -9,12 +9,32 @@ import com.urik.keyboard.utils.CacheMemoryManager
99import com.urik.keyboard.utils.ErrorLogger
1010import com.urik.keyboard.utils.ManagedCache
1111import kotlinx.coroutines.CoroutineDispatcher
12+ import kotlinx.coroutines.CoroutineScope
1213import kotlinx.coroutines.Dispatchers
14+ import kotlinx.coroutines.Job
15+ import kotlinx.coroutines.SupervisorJob
16+ import kotlinx.coroutines.delay
17+ import kotlinx.coroutines.launch
18+ import kotlinx.coroutines.sync.Mutex
19+ import kotlinx.coroutines.sync.withLock
1320import kotlinx.coroutines.withContext
1421import java.util.concurrent.ConcurrentHashMap
1522import javax.inject.Inject
1623import javax.inject.Singleton
1724
25+ private data class PendingFrequencyUpdate (
26+ val languageTag : String ,
27+ val wordNormalized : String ,
28+ var incrementCount : Int = 1 ,
29+ )
30+
31+ private data class PendingBigramUpdate (
32+ val languageTag : String ,
33+ val wordANormalized : String ,
34+ val wordBNormalized : String ,
35+ var incrementCount : Int = 1 ,
36+ )
37+
1838@Singleton
1939class WordFrequencyRepository
2040 @Inject
@@ -41,6 +61,18 @@ class WordFrequencyRepository
4161 @Volatile
4262 private var topBigramsCache = ConcurrentHashMap <String , Map <String , List <String >>>()
4363
64+ private val writeScope = CoroutineScope (SupervisorJob () + ioDispatcher)
65+ private var frequencyFlushJob: Job ? = null
66+ private var bigramFlushJob: Job ? = null
67+ private val frequencyWriteMutex = Mutex ()
68+ private val bigramWriteMutex = Mutex ()
69+ private val pendingFrequencyUpdates = ConcurrentHashMap <String , PendingFrequencyUpdate >()
70+ private val pendingBigramUpdates = ConcurrentHashMap <String , PendingBigramUpdate >()
71+
72+ private companion object {
73+ const val WRITE_DEBOUNCE_MS = 300L
74+ }
75+
4476 private fun normalizeWord (
4577 word : String ,
4678 languageTag : String ,
@@ -51,38 +83,67 @@ class WordFrequencyRepository
5183 normalizedWord : String ,
5284 ): String = " ${languageTag} _$normalizedWord "
5385
54- suspend fun incrementFrequency (
86+ fun incrementFrequency (
5587 word : String ,
5688 languageTag : String ,
57- ): Result <Unit > =
58- withContext(ioDispatcher) {
59- try {
60- if (word.isBlank()) {
61- return @withContext Result .success(Unit )
62- }
89+ ): Result <Unit > {
90+ if (word.isBlank()) {
91+ return Result .success(Unit )
92+ }
6393
64- val normalized = normalizeWord(word, languageTag)
65- val cacheKey = buildCacheKey(languageTag, normalized)
94+ val normalized = normalizeWord(word, languageTag)
95+ val cacheKey = buildCacheKey(languageTag, normalized)
6696
67- userWordFrequencyDao.incrementFrequency(
68- languageTag = languageTag,
69- wordNormalized = normalized,
70- lastUsed = System .currentTimeMillis(),
71- )
97+ frequencyCache.invalidate(cacheKey)
7298
73- frequencyCache.invalidate(cacheKey)
99+ pendingFrequencyUpdates.compute(cacheKey) { _, existing ->
100+ existing?.apply { incrementCount++ }
101+ ? : PendingFrequencyUpdate (languageTag, normalized)
102+ }
74103
75- Result .success(Unit )
76- } catch (e: Exception ) {
77- ErrorLogger .logException(
78- component = " WordFrequencyRepository" ,
79- severity = ErrorLogger .Severity .HIGH ,
80- exception = e,
81- context = mapOf (" operation" to " incrementFrequency" ),
82- )
83- Result .failure(e)
104+ scheduleFrequencyFlush()
105+
106+ return Result .success(Unit )
107+ }
108+
109+ private fun scheduleFrequencyFlush () {
110+ frequencyFlushJob?.cancel()
111+ frequencyFlushJob =
112+ writeScope.launch {
113+ delay(WRITE_DEBOUNCE_MS )
114+ flushPendingFrequencyUpdates()
115+ }
116+ }
117+
118+ private suspend fun flushPendingFrequencyUpdates () {
119+ frequencyWriteMutex.withLock {
120+ if (pendingFrequencyUpdates.isEmpty()) return
121+
122+ val updates = pendingFrequencyUpdates.toMap()
123+ pendingFrequencyUpdates.clear()
124+
125+ val timestamp = System .currentTimeMillis()
126+
127+ updates.values.forEach { update ->
128+ try {
129+ repeat(update.incrementCount) {
130+ userWordFrequencyDao.incrementFrequency(
131+ languageTag = update.languageTag,
132+ wordNormalized = update.wordNormalized,
133+ lastUsed = timestamp,
134+ )
135+ }
136+ } catch (e: Exception ) {
137+ ErrorLogger .logException(
138+ component = " WordFrequencyRepository" ,
139+ severity = ErrorLogger .Severity .HIGH ,
140+ exception = e,
141+ context = mapOf (" operation" to " flushPendingFrequencyUpdates" ),
142+ )
143+ }
84144 }
85145 }
146+ }
86147
87148 suspend fun getFrequency (
88149 word : String ,
@@ -179,65 +240,80 @@ class WordFrequencyRepository
179240 }
180241 }
181242
182- suspend fun clearAllFrequencies (): Result <Unit > =
183- withContext(ioDispatcher) {
184- try {
185- userWordFrequencyDao.clearAll()
186- frequencyCache.invalidateAll()
187- Result .success(Unit )
188- } catch (e: Exception ) {
189- ErrorLogger .logException(
190- component = " WordFrequencyRepository" ,
191- severity = ErrorLogger .Severity .HIGH ,
192- exception = e,
193- context = mapOf (" operation" to " clearAllFrequencies" ),
194- )
195- Result .failure(e)
196- }
197- }
198-
199243 fun clearCache () {
200244 frequencyCache.invalidateAll()
201245 bigramCache.invalidateAll()
202246 topBigramsCache.clear()
247+ pendingFrequencyUpdates.clear()
248+ pendingBigramUpdates.clear()
203249 }
204250
205- suspend fun recordBigram (
251+ fun recordBigram (
206252 wordA : String ,
207253 wordB : String ,
208254 languageTag : String ,
209- ): Result <Unit > =
210- withContext(ioDispatcher) {
211- try {
212- if (wordA.isBlank() || wordB.isBlank()) {
213- return @withContext Result .success(Unit )
214- }
255+ ): Result <Unit > {
256+ if (wordA.isBlank() || wordB.isBlank()) {
257+ return Result .success(Unit )
258+ }
215259
216- val normalizedA = normalizeWord(wordA, languageTag)
217- val normalizedB = normalizeWord(wordB, languageTag)
218- val cacheKey = buildBigramCacheKey(languageTag, normalizedA)
260+ val normalizedA = normalizeWord(wordA, languageTag)
261+ val normalizedB = normalizeWord(wordB, languageTag)
262+ val cacheKey = buildBigramCacheKey(languageTag, normalizedA)
263+ val bigramKey = " ${languageTag} _${normalizedA} _$normalizedB "
219264
220- userWordBigramDao.incrementBigram(
221- languageTag = languageTag,
222- wordANormalized = normalizedA,
223- wordBNormalized = normalizedB,
224- lastUsed = System .currentTimeMillis(),
225- )
265+ bigramCache.invalidate(cacheKey)
266+ topBigramsCache.remove(languageTag)
267+
268+ pendingBigramUpdates.compute(bigramKey) { _, existing ->
269+ existing?.apply { incrementCount++ }
270+ ? : PendingBigramUpdate (languageTag, normalizedA, normalizedB)
271+ }
226272
227- bigramCache.invalidate(cacheKey)
228- topBigramsCache.remove(languageTag)
273+ scheduleBigramFlush()
229274
230- Result .success(Unit )
231- } catch (e: Exception ) {
232- ErrorLogger .logException(
233- component = " WordFrequencyRepository" ,
234- severity = ErrorLogger .Severity .HIGH ,
235- exception = e,
236- context = mapOf (" operation" to " recordBigram" ),
237- )
238- Result .failure(e)
275+ return Result .success(Unit )
276+ }
277+
278+ private fun scheduleBigramFlush () {
279+ bigramFlushJob?.cancel()
280+ bigramFlushJob =
281+ writeScope.launch {
282+ delay(WRITE_DEBOUNCE_MS )
283+ flushPendingBigramUpdates()
284+ }
285+ }
286+
287+ private suspend fun flushPendingBigramUpdates () {
288+ bigramWriteMutex.withLock {
289+ if (pendingBigramUpdates.isEmpty()) return
290+
291+ val updates = pendingBigramUpdates.toMap()
292+ pendingBigramUpdates.clear()
293+
294+ val timestamp = System .currentTimeMillis()
295+
296+ updates.values.forEach { update ->
297+ try {
298+ repeat(update.incrementCount) {
299+ userWordBigramDao.incrementBigram(
300+ languageTag = update.languageTag,
301+ wordANormalized = update.wordANormalized,
302+ wordBNormalized = update.wordBNormalized,
303+ lastUsed = timestamp,
304+ )
305+ }
306+ } catch (e: Exception ) {
307+ ErrorLogger .logException(
308+ component = " WordFrequencyRepository" ,
309+ severity = ErrorLogger .Severity .HIGH ,
310+ exception = e,
311+ context = mapOf (" operation" to " flushPendingBigramUpdates" ),
312+ )
313+ }
239314 }
240315 }
316+ }
241317
242318 suspend fun getBigramPredictions (
243319 wordA : String ,
@@ -308,24 +384,6 @@ class WordFrequencyRepository
308384 }
309385 }
310386
311- suspend fun clearAllBigrams (): Result <Unit > =
312- withContext(ioDispatcher) {
313- try {
314- userWordBigramDao.clearAll()
315- bigramCache.invalidateAll()
316- topBigramsCache.clear()
317- Result .success(Unit )
318- } catch (e: Exception ) {
319- ErrorLogger .logException(
320- component = " WordFrequencyRepository" ,
321- severity = ErrorLogger .Severity .HIGH ,
322- exception = e,
323- context = mapOf (" operation" to " clearAllBigrams" ),
324- )
325- Result .failure(e)
326- }
327- }
328-
329387 private fun buildBigramCacheKey (
330388 languageTag : String ,
331389 normalizedWordA : String ,
0 commit comments