|
1 | 1 | package com.threegap.bitnagil.data.routine.repositoryImpl |
2 | 2 |
|
| 3 | +import com.threegap.bitnagil.data.di.IoDispatcher |
| 4 | +import com.threegap.bitnagil.data.routine.datasource.RoutineLocalDataSource |
3 | 5 | import com.threegap.bitnagil.data.routine.datasource.RoutineRemoteDataSource |
4 | 6 | import com.threegap.bitnagil.data.routine.model.request.toDto |
5 | 7 | import com.threegap.bitnagil.data.routine.model.response.toDomain |
6 | 8 | import com.threegap.bitnagil.domain.routine.model.Routine |
| 9 | +import com.threegap.bitnagil.domain.routine.model.RoutineCompletionInfo |
7 | 10 | import com.threegap.bitnagil.domain.routine.model.RoutineCompletionInfos |
8 | 11 | import com.threegap.bitnagil.domain.routine.model.RoutineEditInfo |
9 | 12 | import com.threegap.bitnagil.domain.routine.model.RoutineRegisterInfo |
10 | 13 | import com.threegap.bitnagil.domain.routine.model.RoutineSchedule |
11 | | -import com.threegap.bitnagil.domain.routine.model.WriteRoutineEvent |
12 | 14 | import com.threegap.bitnagil.domain.routine.repository.RoutineRepository |
| 15 | +import kotlinx.coroutines.CoroutineDispatcher |
| 16 | +import kotlinx.coroutines.CoroutineScope |
| 17 | +import kotlinx.coroutines.FlowPreview |
| 18 | +import kotlinx.coroutines.SupervisorJob |
| 19 | +import kotlinx.coroutines.channels.BufferOverflow |
13 | 20 | import kotlinx.coroutines.flow.Flow |
14 | 21 | import kotlinx.coroutines.flow.MutableSharedFlow |
15 | | -import kotlinx.coroutines.flow.asSharedFlow |
| 22 | +import kotlinx.coroutines.flow.debounce |
| 23 | +import kotlinx.coroutines.flow.emitAll |
| 24 | +import kotlinx.coroutines.flow.filterNotNull |
| 25 | +import kotlinx.coroutines.flow.flow |
| 26 | +import kotlinx.coroutines.flow.onEach |
| 27 | +import kotlinx.coroutines.launch |
16 | 28 | import javax.inject.Inject |
| 29 | +import javax.inject.Singleton |
17 | 30 |
|
| 31 | +@Singleton |
18 | 32 | class RoutineRepositoryImpl @Inject constructor( |
19 | 33 | private val routineRemoteDataSource: RoutineRemoteDataSource, |
| 34 | + private val routineLocalDataSource: RoutineLocalDataSource, |
| 35 | + @param:IoDispatcher private val dispatcher: CoroutineDispatcher, |
20 | 36 | ) : RoutineRepository { |
21 | | - override suspend fun fetchWeeklyRoutines(startDate: String, endDate: String): Result<RoutineSchedule> = |
| 37 | + |
| 38 | + private val repositoryScope = CoroutineScope(SupervisorJob() + dispatcher) |
| 39 | + private val pendingChangesByDate = mutableMapOf<String, MutableMap<String, RoutineCompletionInfo>>() |
| 40 | + private val originalStatesByDate = mutableMapOf<String, MutableMap<String, RoutineCompletionInfo>>() |
| 41 | + private val syncTrigger = MutableSharedFlow<String>( |
| 42 | + extraBufferCapacity = 1, |
| 43 | + onBufferOverflow = BufferOverflow.DROP_LATEST, |
| 44 | + ) |
| 45 | + |
| 46 | + init { |
| 47 | + @OptIn(FlowPreview::class) |
| 48 | + repositoryScope.launch { |
| 49 | + syncTrigger |
| 50 | + .debounce(500L) |
| 51 | + .collect { dateKey -> flushPendingChanges(dateKey) } |
| 52 | + } |
| 53 | + } |
| 54 | + |
| 55 | + override fun observeWeeklyRoutines(startDate: String, endDate: String): Flow<RoutineSchedule> = flow { |
| 56 | + if (routineLocalDataSource.lastFetchRange != (startDate to endDate)) { |
| 57 | + routineLocalDataSource.clearCache() |
| 58 | + fetchAndSave(startDate, endDate) |
| 59 | + } |
| 60 | + emitAll( |
| 61 | + routineLocalDataSource.routineSchedule |
| 62 | + .onEach { if (it == null) fetchAndSave(startDate, endDate) } |
| 63 | + .filterNotNull(), |
| 64 | + ) |
| 65 | + } |
| 66 | + |
| 67 | + private suspend fun fetchAndSave(startDate: String, endDate: String) { |
22 | 68 | routineRemoteDataSource.fetchWeeklyRoutines(startDate, endDate) |
23 | | - .map { it.toDomain() } |
| 69 | + .onSuccess { routineLocalDataSource.saveSchedule(it.toDomain(), startDate, endDate) } |
| 70 | + .onFailure { throw it } |
| 71 | + } |
| 72 | + |
| 73 | + override suspend fun applyRoutineToggle(dateKey: String, routineId: String, completionInfo: RoutineCompletionInfo) { |
| 74 | + if (originalStatesByDate[dateKey]?.containsKey(routineId) != true) { |
| 75 | + routineLocalDataSource.getCompletionInfo(dateKey, routineId)?.let { |
| 76 | + originalStatesByDate.getOrPut(dateKey) { mutableMapOf() }[routineId] = it |
| 77 | + } |
| 78 | + } |
| 79 | + routineLocalDataSource.applyOptimisticToggle(dateKey, routineId, completionInfo) |
| 80 | + pendingChangesByDate.getOrPut(dateKey) { mutableMapOf() }[routineId] = completionInfo |
| 81 | + syncTrigger.emit(dateKey) |
| 82 | + } |
24 | 83 |
|
25 | | - override suspend fun syncRoutineCompletion(routineCompletionInfos: RoutineCompletionInfos): Result<Unit> = |
26 | | - routineRemoteDataSource.syncRoutineCompletion(routineCompletionInfos.toDto()) |
| 84 | + private suspend fun flushPendingChanges(dateKey: String) { |
| 85 | + val snapshot = pendingChangesByDate.remove(dateKey) |
| 86 | + val originals = originalStatesByDate.remove(dateKey) |
| 87 | + val actualChanges = snapshot?.filter { (routineId, pending) -> originals?.get(routineId) != pending } |
| 88 | + if (actualChanges.isNullOrEmpty()) return |
| 89 | + |
| 90 | + val syncRequest = RoutineCompletionInfos(routineCompletionInfos = actualChanges.values.toList()) |
| 91 | + routineRemoteDataSource.syncRoutineCompletion(syncRequest.toDto()) |
| 92 | + .onFailure { |
| 93 | + val range = routineLocalDataSource.lastFetchRange ?: return |
| 94 | + fetchAndSave(range.first, range.second) |
| 95 | + } |
| 96 | + } |
| 97 | + |
| 98 | + private suspend fun refreshCache() { |
| 99 | + val range = routineLocalDataSource.lastFetchRange ?: return |
| 100 | + fetchAndSave(range.first, range.second) |
| 101 | + } |
27 | 102 |
|
28 | 103 | override suspend fun getRoutine(routineId: String): Result<Routine> = |
29 | 104 | routineRemoteDataSource.getRoutine(routineId).map { it.toDomain() } |
30 | 105 |
|
31 | 106 | override suspend fun deleteRoutine(routineId: String): Result<Unit> = |
32 | | - routineRemoteDataSource.deleteRoutine(routineId) |
| 107 | + routineRemoteDataSource.deleteRoutine(routineId).also { |
| 108 | + if (it.isSuccess) refreshCache() |
| 109 | + } |
33 | 110 |
|
34 | 111 | override suspend fun deleteRoutineForDay(routineId: String): Result<Unit> = |
35 | | - routineRemoteDataSource.deleteRoutineForDay(routineId) |
36 | | - |
37 | | - override suspend fun registerRoutine(routineRegisterInfo: RoutineRegisterInfo): Result<Unit> { |
38 | | - val request = routineRegisterInfo.toDto() |
39 | | - return routineRemoteDataSource.registerRoutine(request).also { |
40 | | - if (it.isSuccess) { |
41 | | - _writeRoutineEventFlow.emit(WriteRoutineEvent.AddRoutine) |
42 | | - } |
| 112 | + routineRemoteDataSource.deleteRoutineForDay(routineId).also { |
| 113 | + if (it.isSuccess) refreshCache() |
43 | 114 | } |
44 | | - } |
45 | 115 |
|
46 | | - override suspend fun editRoutine(routineEditInfo: RoutineEditInfo): Result<Unit> { |
47 | | - val request = routineEditInfo.toDto() |
48 | | - return routineRemoteDataSource.editRoutine(request).also { |
49 | | - if (it.isSuccess) { |
50 | | - _writeRoutineEventFlow.emit(WriteRoutineEvent.EditRoutine(routineEditInfo.id)) |
51 | | - } |
| 116 | + override suspend fun registerRoutine(routineRegisterInfo: RoutineRegisterInfo): Result<Unit> = |
| 117 | + routineRemoteDataSource.registerRoutine(routineRegisterInfo.toDto()).also { |
| 118 | + if (it.isSuccess) refreshCache() |
52 | 119 | } |
53 | | - } |
54 | 120 |
|
55 | | - private val _writeRoutineEventFlow = MutableSharedFlow<WriteRoutineEvent>() |
56 | | - override suspend fun getWriteRoutineEventFlow(): Flow<WriteRoutineEvent> = _writeRoutineEventFlow.asSharedFlow() |
| 121 | + override suspend fun editRoutine(routineEditInfo: RoutineEditInfo): Result<Unit> = |
| 122 | + routineRemoteDataSource.editRoutine(routineEditInfo.toDto()).also { |
| 123 | + if (it.isSuccess) refreshCache() |
| 124 | + } |
57 | 125 | } |
0 commit comments