forked from agzam/vyatsu-schedule
fix(teacher): register migration, containment period queries, concurrency guards
- Register AppDatabase.MIGRATION_3_4 so v3 databases upgrade without data loss - Query teacher schedule periods by containment (weekStart<=x AND weekEnd>=x) instead of exact weekStart, matching 2-week catalog windows - Clip buildTeacherSchedule days to the requested week range - Add weeksFor() to enumerate weeks of a catalog window for the UI selector - Serialize syncPeriod body with a Mutex to protect the unique index - Rethrow CancellationException before generic catch; version loadRevision checks so stale observers/loads are cancelled - Sync job tracking in PickerViewModel; keep loading true while empty - Drop dead selectTeacher helper
This commit is contained in:
@@ -26,17 +26,24 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.time.DayOfWeek
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalTime
|
||||
import java.time.temporal.TemporalAdjusters
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
class TeacherScheduleRepositoryImpl(
|
||||
private val scheduleDao: TeacherScheduleDao,
|
||||
private val teacherDao: TeacherDao
|
||||
) : TeacherScheduleRepository {
|
||||
|
||||
private val syncMutex = Mutex()
|
||||
|
||||
override fun observeSchedule(period: SchedulePeriod): Flow<TeacherSchedule?> =
|
||||
combine(
|
||||
scheduleDao.observeDaysForPeriod(period.season.name, period.weekStart.toEpochDay()),
|
||||
@@ -50,54 +57,60 @@ class TeacherScheduleRepositoryImpl(
|
||||
.map { names -> names.map { name -> Teacher(name = name) } }
|
||||
|
||||
override suspend fun syncPeriod(date: LocalDate): Flow<BulkSyncProgress> = flow {
|
||||
val targets = withContext(Dispatchers.IO) {
|
||||
teacherDao.getPeriodsForDate(date.toEpochDay())
|
||||
}
|
||||
if (targets.isEmpty()) {
|
||||
emit(BulkSyncProgress(0, 0, 0))
|
||||
return@flow
|
||||
}
|
||||
|
||||
targets.map { it.season to it.weekStart }
|
||||
.distinct()
|
||||
.forEach { (season, weekStart) ->
|
||||
withContext(Dispatchers.IO) {
|
||||
scheduleDao.deletePeriod(season.name, weekStart)
|
||||
}
|
||||
// Unique index (departmentId, season, weekStart) breaks on interleaved
|
||||
// delete+insert from concurrent syncPeriod() callers.
|
||||
syncMutex.withLock {
|
||||
val targets = withContext(Dispatchers.IO) {
|
||||
teacherDao.getPeriodsForDate(date.toEpochDay())
|
||||
}
|
||||
if (targets.isEmpty()) {
|
||||
emit(BulkSyncProgress(0, 0, 0))
|
||||
return@withLock
|
||||
}
|
||||
|
||||
val total = targets.size
|
||||
val semaphore = Semaphore(PARALLELISM)
|
||||
val jobs = coroutineScope {
|
||||
targets.map { target ->
|
||||
async {
|
||||
semaphore.withPermit {
|
||||
try {
|
||||
withContext(Dispatchers.IO) { syncDepartment(target) }
|
||||
true
|
||||
} catch (e: Throwable) {
|
||||
Log.e(
|
||||
TAG,
|
||||
"sync: failed for department ${target.departmentId} " +
|
||||
"season ${target.season} week ${target.weekStart} " +
|
||||
"(${e::class.simpleName}: ${e.message})",
|
||||
e
|
||||
)
|
||||
false
|
||||
targets.map { it.season to it.weekStart }
|
||||
.distinct()
|
||||
.forEach { (season, weekStart) ->
|
||||
withContext(Dispatchers.IO) {
|
||||
scheduleDao.deletePeriod(season.name, weekStart)
|
||||
}
|
||||
}
|
||||
|
||||
val total = targets.size
|
||||
val semaphore = Semaphore(PARALLELISM)
|
||||
val jobs = coroutineScope {
|
||||
targets.map { target ->
|
||||
async {
|
||||
semaphore.withPermit {
|
||||
try {
|
||||
withContext(Dispatchers.IO) { syncDepartment(target) }
|
||||
true
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Throwable) {
|
||||
Log.e(
|
||||
TAG,
|
||||
"sync: failed for department ${target.departmentId} " +
|
||||
"season ${target.season} week ${target.weekStart} " +
|
||||
"(${e::class.simpleName}: ${e.message})",
|
||||
e
|
||||
)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var done = 0
|
||||
var failed = 0
|
||||
for (job in jobs) {
|
||||
if (job.await()) done++ else failed++
|
||||
emit(BulkSyncProgress(total, done + failed, failed))
|
||||
}
|
||||
var done = 0
|
||||
var failed = 0
|
||||
for (job in jobs) {
|
||||
if (job.await()) done++ else failed++
|
||||
emit(BulkSyncProgress(total, done + failed, failed))
|
||||
}
|
||||
|
||||
withContext(Dispatchers.IO) { trimToRecent(keepDistinctPeriods = 2) }
|
||||
withContext(Dispatchers.IO) { trimToRecent(keepDistinctPeriods = 2) }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun hasData(period: SchedulePeriod): Boolean =
|
||||
@@ -105,6 +118,30 @@ class TeacherScheduleRepositoryImpl(
|
||||
scheduleDao.hasSchedulesForPeriod(period.season.name, period.weekStart.toEpochDay())
|
||||
}
|
||||
|
||||
override suspend fun weeksFor(date: LocalDate): List<SchedulePeriod> =
|
||||
withContext(Dispatchers.IO) {
|
||||
teacherDao.getPeriodsForDate(date.toEpochDay())
|
||||
.distinctBy { it.season to (it.weekStart to it.weekEnd) }
|
||||
.flatMap { window ->
|
||||
val windowStart = LocalDate.ofEpochDay(window.weekStart)
|
||||
val windowEnd = LocalDate.ofEpochDay(window.weekEnd)
|
||||
generateSequence(
|
||||
windowStart.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY))
|
||||
) { it.plusWeeks(1) }
|
||||
.takeWhile { it <= windowEnd }
|
||||
.map { monday ->
|
||||
val weekEnd = monday.plusDays(6)
|
||||
SchedulePeriod(
|
||||
weekStart = monday,
|
||||
weekEnd = if (weekEnd.isAfter(windowEnd)) windowEnd else weekEnd,
|
||||
season = window.season
|
||||
)
|
||||
}
|
||||
}
|
||||
.distinctBy { it.season to it.weekStart }
|
||||
.sortedBy { it.weekStart }
|
||||
}
|
||||
|
||||
override suspend fun clearAll() = withContext(Dispatchers.IO) {
|
||||
scheduleDao.deleteAll()
|
||||
}
|
||||
@@ -182,9 +219,13 @@ class TeacherScheduleRepositoryImpl(
|
||||
dayEntities: List<TeacherDayEntity>,
|
||||
lessonEntities: List<TeacherLessonEntity>
|
||||
): TeacherSchedule? {
|
||||
if (dayEntities.isEmpty()) return null
|
||||
val weekDays = dayEntities
|
||||
.filter {
|
||||
it.date >= period.weekStart.toEpochDay() && it.date <= period.weekEnd.toEpochDay()
|
||||
}
|
||||
if (weekDays.isEmpty()) return null
|
||||
val lessonsByDate = lessonEntities.groupBy { it.date }
|
||||
val days = dayEntities
|
||||
val days = weekDays
|
||||
.sortedBy { it.date }
|
||||
.distinctBy { it.date }
|
||||
.map { day ->
|
||||
|
||||
Reference in New Issue
Block a user