diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts index e0fea0c..6f78b5c 100644 --- a/core/data/build.gradle.kts +++ b/core/data/build.gradle.kts @@ -2,9 +2,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.kotlinMultiplatform) - alias(libs.plugins.kotlinSerialization) alias(libs.plugins.androidMultiplatformLibrary) - alias(libs.plugins.ksp) } kotlin { @@ -27,26 +25,8 @@ kotlin { jvm() sourceSets { - androidMain.dependencies { - implementation(libs.koin.compose) - implementation(libs.ktor.client.okhttp) - } commonMain.dependencies { - implementation(libs.okio) - implementation(libs.koin.core) - implementation(libs.ktor.client.core) - implementation(libs.ktor.client.contentnegotiation) - implementation(libs.ktor.serialization.kotlinx.json) - implementation(libs.kotlinx.coroutines.core) - implementation(libs.kotlinx.datetime) - implementation(libs.kotlinx.serialization.json) - implementation(libs.kotlinx.serialization.protobuf) - implementation(projects.core) - implementation(projects.core.room) - } - iosMain.dependencies { - implementation(libs.ktor.client.darwin) } } } diff --git a/core/data/client/build.gradle.kts b/core/data/client/build.gradle.kts new file mode 100644 index 0000000..e9848f3 --- /dev/null +++ b/core/data/client/build.gradle.kts @@ -0,0 +1,54 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.kotlinSerialization) + alias(libs.plugins.androidMultiplatformLibrary) + alias(libs.plugins.ksp) +} + +kotlin { + android { + namespace = "moe.lava.banksia.core.data.client" + compileSdk = libs.versions.android.compileSdk.get().toInt() + + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + } + } + + compilerOptions { + freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime") + } + + iosArm64() + iosSimulatorArm64() + + jvm() + + sourceSets { + androidMain.dependencies { + implementation(libs.koin.compose) + implementation(libs.ktor.client.okhttp) + } + commonMain.dependencies { + api(projects.core.data) + + implementation(libs.okio) + implementation(libs.koin.core) + implementation(libs.ktor.client.core) + implementation(libs.ktor.client.contentnegotiation) + implementation(libs.ktor.serialization.kotlinx.json) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.datetime) + implementation(libs.kotlinx.serialization.json) + implementation(libs.kotlinx.serialization.protobuf) + + implementation(projects.core) + implementation(projects.core.room) + } + iosMain.dependencies { + implementation(libs.ktor.client.darwin) + } + } +} diff --git a/core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/DataDiModule.kt b/core/data/client/src/commonMain/kotlin/moe/lava/banksia/core/data/ClientDataDiModule.kt similarity index 81% rename from core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/DataDiModule.kt rename to core/data/client/src/commonMain/kotlin/moe/lava/banksia/core/data/ClientDataDiModule.kt index 6529a92..01e961c 100644 --- a/core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/DataDiModule.kt +++ b/core/data/client/src/commonMain/kotlin/moe/lava/banksia/core/data/ClientDataDiModule.kt @@ -8,6 +8,9 @@ import io.ktor.client.plugins.plugin import io.ktor.serialization.kotlinx.json.json import kotlinx.serialization.json.Json import moe.lava.banksia.core.Constants +import moe.lava.banksia.core.data.repositories.ClientRouteRepository +import moe.lava.banksia.core.data.repositories.ClientStopRepository +import moe.lava.banksia.core.data.repositories.ClientStopTimeRepository import moe.lava.banksia.core.data.repositories.RouteRepository import moe.lava.banksia.core.data.repositories.StopRepository import moe.lava.banksia.core.data.repositories.StopTimeRepository @@ -21,9 +24,10 @@ import moe.lava.banksia.core.room.roomDiModule import moe.lava.banksia.core.util.log import moe.lava.banksia.data.ptv.PtvService import org.koin.core.module.dsl.singleOf +import org.koin.dsl.bind import org.koin.dsl.module -val dataDiModule = module { +val clientDataDiModule = module { includes(roomDiModule) // HTTP Clients @@ -56,7 +60,7 @@ val dataDiModule = module { singleOf(::StopTimeRemoteDataSource) // Repositories - singleOf(::RouteRepository) - singleOf(::StopRepository) - singleOf(::StopTimeRepository) + singleOf(::ClientRouteRepository) bind RouteRepository::class + singleOf(::ClientStopRepository) bind StopRepository::class + singleOf(::ClientStopTimeRepository) bind StopTimeRepository::class } diff --git a/core/data/client/src/commonMain/kotlin/moe/lava/banksia/core/data/repositories/ClientRouteRepository.kt b/core/data/client/src/commonMain/kotlin/moe/lava/banksia/core/data/repositories/ClientRouteRepository.kt new file mode 100644 index 0000000..70a8905 --- /dev/null +++ b/core/data/client/src/commonMain/kotlin/moe/lava/banksia/core/data/repositories/ClientRouteRepository.kt @@ -0,0 +1,25 @@ +package moe.lava.banksia.core.data.repositories + +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import moe.lava.banksia.core.data.sources.route.RouteLocalDataSource +import moe.lava.banksia.core.data.sources.route.RouteRemoteDataSource + +internal class ClientRouteRepository internal constructor( + private val local: RouteLocalDataSource, + private val remote: RouteRemoteDataSource, +) : RouteRepository { + private val mutex = Mutex() + override suspend fun getAll() = mutex.withLock { + local + .getAll() + .map { it.asModel() } + .ifEmpty { + remote + .getAll() + .also { local.save(*it.toTypedArray()) } + } + } + + override suspend fun get(id: String) = mutex.withLock { local.get(id)?.asModel() ?: remote.get(id) } +} diff --git a/core/data/client/src/commonMain/kotlin/moe/lava/banksia/core/data/repositories/ClientStopRepository.kt b/core/data/client/src/commonMain/kotlin/moe/lava/banksia/core/data/repositories/ClientStopRepository.kt new file mode 100644 index 0000000..a5fd300 --- /dev/null +++ b/core/data/client/src/commonMain/kotlin/moe/lava/banksia/core/data/repositories/ClientStopRepository.kt @@ -0,0 +1,22 @@ +package moe.lava.banksia.core.data.repositories + +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import moe.lava.banksia.core.data.sources.stop.StopLocalDataSource +import moe.lava.banksia.core.data.sources.stop.StopRemoteDataSource + +internal class ClientStopRepository internal constructor( + private val local: StopLocalDataSource, + private val remote: StopRemoteDataSource, +) : StopRepository { + private val mutex = Mutex() + + override suspend fun get(id: String) = mutex.withLock { local.get(id)?.asModel() ?: remote.get(id) } + override suspend fun getByRoute(id: String) = mutex.withLock { + local + .getByRoute(id) + .map { it.asModel() } + .ifEmpty { null } + ?: remote.getByRoute(id) + } +} diff --git a/core/data/client/src/commonMain/kotlin/moe/lava/banksia/core/data/repositories/ClientStopTimeRepository.kt b/core/data/client/src/commonMain/kotlin/moe/lava/banksia/core/data/repositories/ClientStopTimeRepository.kt new file mode 100644 index 0000000..aea3159 --- /dev/null +++ b/core/data/client/src/commonMain/kotlin/moe/lava/banksia/core/data/repositories/ClientStopTimeRepository.kt @@ -0,0 +1,16 @@ +package moe.lava.banksia.core.data.repositories + +import moe.lava.banksia.core.data.sources.stoptime.StopTimeLocalDataSource +import moe.lava.banksia.core.data.sources.stoptime.StopTimeRemoteDataSource +import moe.lava.banksia.core.model.StopTimeDated + +internal class ClientStopTimeRepository internal constructor( + private val local: StopTimeLocalDataSource, + private val remote: StopTimeRemoteDataSource, +) : StopTimeRepository { + override suspend fun getForStop(id: String): List { + return local + .getAtStop(id) + .ifEmpty { remote.getAtStop(id) } + } +} diff --git a/core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/sources/route/RouteLocalDataSource.kt b/core/data/client/src/commonMain/kotlin/moe/lava/banksia/core/data/sources/route/RouteLocalDataSource.kt similarity index 100% rename from core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/sources/route/RouteLocalDataSource.kt rename to core/data/client/src/commonMain/kotlin/moe/lava/banksia/core/data/sources/route/RouteLocalDataSource.kt diff --git a/core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/sources/route/RouteRemoteDataSource.kt b/core/data/client/src/commonMain/kotlin/moe/lava/banksia/core/data/sources/route/RouteRemoteDataSource.kt similarity index 100% rename from core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/sources/route/RouteRemoteDataSource.kt rename to core/data/client/src/commonMain/kotlin/moe/lava/banksia/core/data/sources/route/RouteRemoteDataSource.kt diff --git a/core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/sources/stop/StopLocalDataSource.kt b/core/data/client/src/commonMain/kotlin/moe/lava/banksia/core/data/sources/stop/StopLocalDataSource.kt similarity index 100% rename from core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/sources/stop/StopLocalDataSource.kt rename to core/data/client/src/commonMain/kotlin/moe/lava/banksia/core/data/sources/stop/StopLocalDataSource.kt diff --git a/core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/sources/stop/StopRemoteDataSource.kt b/core/data/client/src/commonMain/kotlin/moe/lava/banksia/core/data/sources/stop/StopRemoteDataSource.kt similarity index 100% rename from core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/sources/stop/StopRemoteDataSource.kt rename to core/data/client/src/commonMain/kotlin/moe/lava/banksia/core/data/sources/stop/StopRemoteDataSource.kt diff --git a/core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/sources/stoptime/StopTimeLocalDataSource.kt b/core/data/client/src/commonMain/kotlin/moe/lava/banksia/core/data/sources/stoptime/StopTimeLocalDataSource.kt similarity index 100% rename from core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/sources/stoptime/StopTimeLocalDataSource.kt rename to core/data/client/src/commonMain/kotlin/moe/lava/banksia/core/data/sources/stoptime/StopTimeLocalDataSource.kt diff --git a/core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/sources/stoptime/StopTimeRemoteDataSource.kt b/core/data/client/src/commonMain/kotlin/moe/lava/banksia/core/data/sources/stoptime/StopTimeRemoteDataSource.kt similarity index 100% rename from core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/sources/stoptime/StopTimeRemoteDataSource.kt rename to core/data/client/src/commonMain/kotlin/moe/lava/banksia/core/data/sources/stoptime/StopTimeRemoteDataSource.kt diff --git a/core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/sources/trip/TripRemoteDataSource.kt b/core/data/client/src/commonMain/kotlin/moe/lava/banksia/core/data/sources/trip/TripRemoteDataSource.kt similarity index 100% rename from core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/sources/trip/TripRemoteDataSource.kt rename to core/data/client/src/commonMain/kotlin/moe/lava/banksia/core/data/sources/trip/TripRemoteDataSource.kt diff --git a/core/data/server/build.gradle.kts b/core/data/server/build.gradle.kts new file mode 100644 index 0000000..eaa309b --- /dev/null +++ b/core/data/server/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + alias(libs.plugins.kotlinJvm) + alias(libs.plugins.kotlinSerialization) +} + +kotlin { + compilerOptions { + freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime") + } +} + +dependencies { + implementation(libs.okio) + implementation(libs.koin.core) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.datetime) + + api(projects.core.data) + implementation(projects.core) + implementation(projects.core.room) +} diff --git a/core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/repositories/RouteRepository.kt b/core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/repositories/RouteRepository.kt index fdeb95f..fbb663f 100644 --- a/core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/repositories/RouteRepository.kt +++ b/core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/repositories/RouteRepository.kt @@ -1,25 +1,8 @@ package moe.lava.banksia.core.data.repositories -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import moe.lava.banksia.core.data.sources.route.RouteLocalDataSource -import moe.lava.banksia.core.data.sources.route.RouteRemoteDataSource +import moe.lava.banksia.core.model.Route -class RouteRepository internal constructor( - private val local: RouteLocalDataSource, - private val remote: RouteRemoteDataSource, -) { - private val mutex = Mutex() - suspend fun getAll() = mutex.withLock { - local - .getAll() - .map { it.asModel() } - .ifEmpty { - remote - .getAll() - .also { local.save(*it.toTypedArray()) } - } - } - - suspend fun get(id: String) = mutex.withLock { local.get(id)?.asModel() ?: remote.get(id) } +interface RouteRepository { + suspend fun get(id: String): Route + suspend fun getAll(): List } diff --git a/core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/repositories/StopRepository.kt b/core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/repositories/StopRepository.kt index d83a9cd..c663f89 100644 --- a/core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/repositories/StopRepository.kt +++ b/core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/repositories/StopRepository.kt @@ -1,22 +1,8 @@ package moe.lava.banksia.core.data.repositories -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import moe.lava.banksia.core.data.sources.stop.StopLocalDataSource -import moe.lava.banksia.core.data.sources.stop.StopRemoteDataSource +import moe.lava.banksia.core.model.Stop -class StopRepository internal constructor( - private val local: StopLocalDataSource, - private val remote: StopRemoteDataSource, -) { - private val mutex = Mutex() - - suspend fun get(id: String) = mutex.withLock { local.get(id)?.asModel() ?: remote.get(id) } - suspend fun getByRoute(id: String) = mutex.withLock { - local - .getByRoute(id) - .map { it.asModel() } - .ifEmpty { null } - ?: remote.getByRoute(id) - } +interface StopRepository { + suspend fun get(id: String): Stop + suspend fun getByRoute(id: String): List } diff --git a/core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/repositories/StopTimeRepository.kt b/core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/repositories/StopTimeRepository.kt index 34aa570..87d01ac 100644 --- a/core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/repositories/StopTimeRepository.kt +++ b/core/data/src/commonMain/kotlin/moe/lava/banksia/core/data/repositories/StopTimeRepository.kt @@ -1,16 +1,7 @@ package moe.lava.banksia.core.data.repositories -import moe.lava.banksia.core.data.sources.stoptime.StopTimeLocalDataSource -import moe.lava.banksia.core.data.sources.stoptime.StopTimeRemoteDataSource import moe.lava.banksia.core.model.StopTimeDated -class StopTimeRepository internal constructor( - private val local: StopTimeLocalDataSource, - private val remote: StopTimeRemoteDataSource, -) { - suspend fun getForStop(id: String): List { - return local - .getAtStop(id) - .ifEmpty { remote.getAtStop(id) } - } +interface StopTimeRepository { + suspend fun getForStop(id: String): List } diff --git a/core/room/src/androidMain/kotlin/moe/lava/banksia/core/room/DatabaseManager.android.kt b/core/room/src/androidMain/kotlin/moe/lava/banksia/core/room/DatabaseManager.android.kt new file mode 100644 index 0000000..999ee4b --- /dev/null +++ b/core/room/src/androidMain/kotlin/moe/lava/banksia/core/room/DatabaseManager.android.kt @@ -0,0 +1,22 @@ +package moe.lava.banksia.core.room + +import android.content.Context +import androidx.room.Room +import org.koin.core.component.KoinComponent +import org.koin.core.component.get +import org.koin.core.component.inject + +actual class DatabaseManager actual constructor() : KoinComponent { + private val ctx by inject() + + actual val database by lazy { + val ctx = get().applicationContext + val dbFile = ctx.getDatabasePath("room.db") + val builder = Room.databaseBuilder( + context = ctx, + name = dbFile.absolutePath, + ) + + Database.build(builder) + } +} diff --git a/core/room/src/androidMain/kotlin/moe/lava/banksia/core/room/RoomDiModule.android.kt b/core/room/src/androidMain/kotlin/moe/lava/banksia/core/room/RoomDiModule.android.kt deleted file mode 100644 index 8cd01e6..0000000 --- a/core/room/src/androidMain/kotlin/moe/lava/banksia/core/room/RoomDiModule.android.kt +++ /dev/null @@ -1,21 +0,0 @@ -package moe.lava.banksia.core.room - -import android.content.Context -import androidx.room.Room -import androidx.room.RoomDatabase -import org.koin.core.parameter.ParametersHolder -import org.koin.core.scope.Scope - -class AndroidDatabaseBuilder(val ctx: Context) : PlatformDatabaseBuilder { - override fun getBuilder(): RoomDatabase.Builder { - val appContext = ctx.applicationContext - val dbFile = appContext.getDatabasePath("room.db") - return Room.databaseBuilder( - context = appContext, - name = dbFile.absolutePath - ) - } -} - -internal actual fun Scope.provideDatabaseBuilder(p: ParametersHolder): PlatformDatabaseBuilder = - AndroidDatabaseBuilder(get()) diff --git a/core/room/src/commonMain/kotlin/moe/lava/banksia/core/room/DatabaseManager.kt b/core/room/src/commonMain/kotlin/moe/lava/banksia/core/room/DatabaseManager.kt new file mode 100644 index 0000000..bfe32a9 --- /dev/null +++ b/core/room/src/commonMain/kotlin/moe/lava/banksia/core/room/DatabaseManager.kt @@ -0,0 +1,7 @@ +package moe.lava.banksia.core.room + +import org.koin.core.component.KoinComponent + +expect class DatabaseManager() : KoinComponent { + val database: Database +} diff --git a/core/room/src/commonMain/kotlin/moe/lava/banksia/core/room/RoomDiModule.kt b/core/room/src/commonMain/kotlin/moe/lava/banksia/core/room/RoomDiModule.kt index 67d4fd2..85c56fc 100644 --- a/core/room/src/commonMain/kotlin/moe/lava/banksia/core/room/RoomDiModule.kt +++ b/core/room/src/commonMain/kotlin/moe/lava/banksia/core/room/RoomDiModule.kt @@ -1,26 +1,18 @@ package moe.lava.banksia.core.room -import androidx.room.RoomDatabase -import org.koin.core.parameter.ParametersHolder -import org.koin.core.scope.Scope +import org.koin.core.module.dsl.singleOf import org.koin.dsl.module val roomDiModule = module { - single { provideDatabaseBuilder(it) } - single { Database.build(get().getBuilder()) } + singleOf(::DatabaseManager) + factory { get().database } - single { get().versionMetadataDao } - single { get().routeDao } - single { get().serviceDao } - single { get().serviceExceptionDao } - single { get().shapeDao } - single { get().stopDao } - single { get().stopTimeDao } - single { get().tripDao } + factory { get().versionMetadataDao } + factory { get().routeDao } + factory { get().serviceDao } + factory { get().serviceExceptionDao } + factory { get().shapeDao } + factory { get().stopDao } + factory { get().stopTimeDao } + factory { get().tripDao } } - -internal interface PlatformDatabaseBuilder { - fun getBuilder(): RoomDatabase.Builder -} - -internal expect fun Scope.provideDatabaseBuilder(p: ParametersHolder): PlatformDatabaseBuilder diff --git a/core/room/src/commonMain/kotlin/moe/lava/banksia/core/room/dao/StopDao.kt b/core/room/src/commonMain/kotlin/moe/lava/banksia/core/room/dao/StopDao.kt index 99f5a2d..7edb560 100644 --- a/core/room/src/commonMain/kotlin/moe/lava/banksia/core/room/dao/StopDao.kt +++ b/core/room/src/commonMain/kotlin/moe/lava/banksia/core/room/dao/StopDao.kt @@ -15,7 +15,7 @@ interface StopDao { @Query(""" SELECT * FROM Stop WHERE platformCode <> "" - AND parent == "" + AND parent IS NULL """) suspend fun getAllParentless(): List diff --git a/core/room/src/iosMain/kotlin/moe/lava/banksia/core/room/DatabaseManager.ios.kt b/core/room/src/iosMain/kotlin/moe/lava/banksia/core/room/DatabaseManager.ios.kt new file mode 100644 index 0000000..34e370e --- /dev/null +++ b/core/room/src/iosMain/kotlin/moe/lava/banksia/core/room/DatabaseManager.ios.kt @@ -0,0 +1,8 @@ +package moe.lava.banksia.core.room + +import org.koin.core.component.KoinComponent + +actual class DatabaseManager actual constructor() : KoinComponent { + actual val database: Database + get() = TODO("Not yet implemented") +} diff --git a/core/room/src/iosMain/kotlin/moe/lava/banksia/core/room/RoomDiModule.ios.kt b/core/room/src/iosMain/kotlin/moe/lava/banksia/core/room/RoomDiModule.ios.kt deleted file mode 100644 index 3c74852..0000000 --- a/core/room/src/iosMain/kotlin/moe/lava/banksia/core/room/RoomDiModule.ios.kt +++ /dev/null @@ -1,14 +0,0 @@ -package moe.lava.banksia.core.room - -import androidx.room.RoomDatabase -import org.koin.core.parameter.ParametersHolder -import org.koin.core.scope.Scope - -class IosDatabaseBuilder() : PlatformDatabaseBuilder { - override fun getBuilder(): RoomDatabase.Builder { - TODO("Not yet implemented") - } -} - -internal actual fun Scope.provideDatabaseBuilder(p: ParametersHolder): PlatformDatabaseBuilder = - IosDatabaseBuilder() diff --git a/core/room/src/jvmMain/kotlin/moe/lava/banksia/core/room/DatabaseManager.jvm.kt b/core/room/src/jvmMain/kotlin/moe/lava/banksia/core/room/DatabaseManager.jvm.kt new file mode 100644 index 0000000..360d2dd --- /dev/null +++ b/core/room/src/jvmMain/kotlin/moe/lava/banksia/core/room/DatabaseManager.jvm.kt @@ -0,0 +1,69 @@ +package moe.lava.banksia.core.room + +import androidx.room.Room +import androidx.room.RoomDatabase +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import moe.lava.banksia.core.util.error +import org.koin.core.component.KoinComponent +import java.io.File +import kotlin.system.exitProcess + +actual class DatabaseManager : KoinComponent { + private var liveDatabase: Database = Database.build(getBuilder()) + actual val database get() = liveDatabase + + private fun getBuilder(path: String = "./data/room.db"): RoomDatabase.Builder { + val dbFile = File(path) + return Room.databaseBuilder( + name = dbFile.absolutePath, + ).setJournalMode(RoomDatabase.JournalMode.TRUNCATE) + } + + fun makeAlt() = Database.build(getBuilder("./data/room_alt.db")) + + private fun deleteAll(file: File): Boolean { + val r1 = file.takeIf { it.exists() }?.delete() + val r2 = File(file.parentFile, file.name + ".lck").takeIf { it.exists() }?.delete() + val r3 = File(file.parentFile, file.name + "-journal").takeIf { it.exists() }?.delete() + return r1 != false && r2 != false && r3 != false + } + + private fun renameAll(from: File, to: File): Boolean { + val r1 = from.takeIf { it.exists() }?.renameTo(to) + val r2 = File(from.parentFile, from.name + ".lck").takeIf { it.exists() }?.renameTo(File(to.parentFile, to.name + ".lck")) + val r3 = File(from.parentFile, from.name + "-journal").takeIf { it.exists() }?.renameTo(File(to.parentFile, to.name + "-journal")) + return r1 != false && r2 != false && r3 != false + } + + fun swap(scope: CoroutineScope = CoroutineScope(Dispatchers.IO)) { + val live = File("./data/room.db") + val alt = File("./data/room_alt.db") + val old = File("./data/room_old.db") + + if (!renameAll(live, old)) { + error("DatabaseManager", "Failed to rename database from live to old (${live.absolutePath} -> ${old.absolutePath})") + return + } + if (!renameAll(alt, live)) { + error("DatabaseManager", "Failed to rename database from alt to live, trying to undo.. (${alt.absolutePath} -> ${live.absolutePath})") + if (!live.renameTo(old)) { + error("DatabaseManager", "Failed to undo, critical failure, exiting..") + exitProcess(1) + } + return + } + val oldDatabase = liveDatabase + liveDatabase = Database.build(getBuilder()) + + scope.launch { + delay(5000) + if (!deleteAll(old)) { + error("DatabaseManager", "Failed to unlink old database, stray files! (${old.absolutePath})") + } + oldDatabase.close() + } + } +} diff --git a/core/room/src/jvmMain/kotlin/moe/lava/banksia/core/room/RoomDiModule.jvm.kt b/core/room/src/jvmMain/kotlin/moe/lava/banksia/core/room/RoomDiModule.jvm.kt deleted file mode 100644 index 38c35ce..0000000 --- a/core/room/src/jvmMain/kotlin/moe/lava/banksia/core/room/RoomDiModule.jvm.kt +++ /dev/null @@ -1,19 +0,0 @@ -package moe.lava.banksia.core.room - -import androidx.room.Room -import androidx.room.RoomDatabase -import org.koin.core.parameter.ParametersHolder -import org.koin.core.scope.Scope -import java.io.File - -class JvmDatabaseBuilder() : PlatformDatabaseBuilder { - override fun getBuilder(): RoomDatabase.Builder { - val dbFile = File("./data/room.db") - return Room.databaseBuilder( - name = dbFile.absolutePath, - ) - } -} - -internal actual fun Scope.provideDatabaseBuilder(p: ParametersHolder): PlatformDatabaseBuilder = - JvmDatabaseBuilder() diff --git a/server/build.gradle.kts b/server/build.gradle.kts index 0883c85..4150bb2 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -5,7 +5,7 @@ plugins { application } -group = "moe.lava.banksia" +group = "moe.lava.banksia.server" version = "1.0.0" application { mainClass.set("moe.lava.banksia.server.ApplicationKt") diff --git a/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/GtfsParser.kt b/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/GtfsParser.kt index e726a46..b95b232 100644 --- a/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/GtfsParser.kt +++ b/server/gtfs/src/main/kotlin/moe/lava/banksia/server/gtfs/GtfsParser.kt @@ -135,7 +135,7 @@ class GtfsParser( .forEach { fd -> log.info("parsing stop times for ${fd.parent}...") parseStopTimes(fd, trips) { seq -> - seq.chunked(10000) + seq.chunked(1000000) .forEach { emit(GtfsData.StopTimeChunk(it)) } } } @@ -170,7 +170,7 @@ class GtfsParser( id = stop_id, name = stop_name, pos = Point(stop_lat, stop_lon), - parent = parent_station, + parent = parent_station.ifEmpty { null }, hasWheelChairBoarding = wheelchair_boarding == "1", level = level_id, platformCode = platform_code, diff --git a/server/src/main/kotlin/moe/lava/banksia/server/Application.kt b/server/src/main/kotlin/moe/lava/banksia/server/Application.kt index 2fe7bf6..0981b80 100644 --- a/server/src/main/kotlin/moe/lava/banksia/server/Application.kt +++ b/server/src/main/kotlin/moe/lava/banksia/server/Application.kt @@ -28,7 +28,7 @@ import moe.lava.banksia.core.util.serialise import moe.lava.banksia.server.di.ServerModules import moe.lava.banksia.server.gtfsrt.GtfsrtService import org.koin.dsl.module -import org.koin.ktor.ext.inject +import org.koin.ktor.ext.get import org.koin.ktor.plugin.Koin import kotlin.time.Clock @@ -46,19 +46,29 @@ fun Application.module() { modules(ServerModules) } - val gtfsr by inject() @Suppress("KotlinConstantConditions") - launch { gtfsr.start(this, !Constants.devMode) } + launch { get().start(this, !Constants.devMode) } routing { if (Constants.devMode) { get("/fixup") { call.respondText("received") - val fixer by inject() - fixer.addParentsToStops() + get().addParentsToStops() } } - get("/update") { + get("/manage/fixup") { + val key = call.parameters["key"] + if (key != Constants.updateKey) { + call.respond(HttpStatusCode.Forbidden) + return@get + } + + call.respondText("fixing") + launch(context = Dispatchers.IO) { + get().addParentsToStops() + } + } + get("/manage/update") { val key = call.parameters["key"] if (key != Constants.updateKey) { call.respond(HttpStatusCode.Forbidden) @@ -70,16 +80,13 @@ fun Application.module() { ?: "https://opendata.transport.vic.gov.au/dataset/3f4e292e-7f8a-4ffe-831f-1953be0fe448/resource/${datasetUuid}/download/gtfs.zip" call.respondText("received") launch(context = Dispatchers.IO) { - val fixer by inject() - val importer by inject() - importer.import(datasetUrl) - - fixer.addParentsToStops() + get().import(datasetUrl) + get().addParentsToStops() } } get("/metadata/{type?}") { - val dao by inject() + val dao = get() val type = call.parameters["type"] if (type == null) { call.respond(dao.getAll().map { it.asModel() }) @@ -96,7 +103,7 @@ fun Application.module() { get("/routes") { val routes = withContext(context = Dispatchers.IO) { - inject().value.getAll() + get().getAll() } val res = routes.map { it.asModel() } call.respond(res) @@ -104,7 +111,7 @@ fun Application.module() { get("/routes/{route_id}") { val routeId = call.parameters["route_id"]!! val route = withContext(context = Dispatchers.IO) { - inject().value.get(routeId) + get().get(routeId) } if (route != null) call.respond(route.asModel()) @@ -113,7 +120,7 @@ fun Application.module() { } get("/stops") { val routes = withContext(context = Dispatchers.IO) { - inject().value.getAll() + get().getAll() } val res = routes.map { it.asModel() } call.respond(res) @@ -121,7 +128,7 @@ fun Application.module() { get("/stops/{stop_id}") { val stopId = call.parameters["stop_id"]!! val stop = withContext(context = Dispatchers.IO) { - inject().value.get(stopId) + get().get(stopId) } if (stop != null) call.respond(stop.asModel()) @@ -132,7 +139,7 @@ fun Application.module() { val routeId = call.parameters["route_id"]!! val useParent = call.queryParameters["parent"] !in listOf("false", "0") val stops = withContext(Dispatchers.IO) { - val routeDao by inject() + val routeDao = get() if (useParent) routeDao.stopsParent(routeId) else @@ -146,7 +153,7 @@ fun Application.module() { ?.let { LocalDate.parse(it, LocalDate.Formats.ISO) } ?: Clock.System.todayIn(TimeZone.currentSystemDefault()) val times = withContext(context = Dispatchers.IO) { - inject().value + get() .getForStopDated( stopId, listOf(date.dayOfWeek).serialise(), diff --git a/server/src/main/kotlin/moe/lava/banksia/server/GtfsDataFixer.kt b/server/src/main/kotlin/moe/lava/banksia/server/GtfsDataFixer.kt index b2620f4..c74930d 100644 --- a/server/src/main/kotlin/moe/lava/banksia/server/GtfsDataFixer.kt +++ b/server/src/main/kotlin/moe/lava/banksia/server/GtfsDataFixer.kt @@ -24,14 +24,14 @@ class GtfsDataFixer( name = name, lat = avgLat, lng = avgLng, - parent = "", + parent = null, hasWheelChairBoarding = stops.all { it.hasWheelChairBoarding }, level = "", platformCode = "", ) log("datafixer", "inserting ${parentId} for ${stops.size} children") dao.insertAll(parent) - database.stopDao.updateParents(stops.map { it.id }, parentId) + dao.updateParents(stops.map { it.id }, parentId) } } } diff --git a/server/src/main/kotlin/moe/lava/banksia/server/GtfsImporter.kt b/server/src/main/kotlin/moe/lava/banksia/server/GtfsImporter.kt index a012da7..5c8dc37 100644 --- a/server/src/main/kotlin/moe/lava/banksia/server/GtfsImporter.kt +++ b/server/src/main/kotlin/moe/lava/banksia/server/GtfsImporter.kt @@ -1,7 +1,5 @@ package moe.lava.banksia.server -import androidx.room.immediateTransaction -import androidx.room.useWriterConnection import io.ktor.util.logging.Logger import moe.lava.banksia.core.model.Route import moe.lava.banksia.core.model.Service @@ -11,6 +9,7 @@ import moe.lava.banksia.core.model.Stop import moe.lava.banksia.core.model.StopTime import moe.lava.banksia.core.model.Trip import moe.lava.banksia.core.room.Database +import moe.lava.banksia.core.room.DatabaseManager import moe.lava.banksia.core.room.entity.asEntity import moe.lava.banksia.server.gtfs.GtfsData import moe.lava.banksia.server.gtfs.GtfsParser @@ -18,74 +17,66 @@ import kotlin.time.Clock class GtfsImporter( private val parser: GtfsParser, - private val database: Database, + private val dbm: DatabaseManager, private val log: Logger, ) { suspend fun import(url: String, date: Long = Clock.System.now().epochSeconds) { - database.useWriterConnection { transactor -> - transactor.immediateTransaction { - database.routeDao.deleteAll() - database.serviceDao.deleteAll() - database.serviceExceptionDao.deleteAll() - database.shapeDao.deleteAll() - database.stopDao.deleteAll() - database.stopTimeDao.deleteAll() - database.tripDao.deleteAll() + val database = dbm.makeAlt() - parser.update(url).collect { chunk -> - when (chunk) { - is GtfsData.RouteChunk -> addRoutes(chunk.routes) - is GtfsData.ServiceChunk -> addServices(chunk.services) - is GtfsData.ServiceExceptionChunk -> addServiceExceptions(chunk.exceptions) - is GtfsData.ShapeChunk -> addShapes(chunk.shapes) - is GtfsData.StopChunk -> addStops(chunk.stops) - is GtfsData.StopTimeChunk -> addStopTimes(chunk.stopTimes) - is GtfsData.TripChunk -> addTrips(chunk.trips) - } - } - - updateMetadata(date) + parser.update(url).collect { chunk -> + when (chunk) { + is GtfsData.RouteChunk -> database.addRoutes(chunk.routes) + is GtfsData.ServiceChunk -> database.addServices(chunk.services) + is GtfsData.ServiceExceptionChunk -> database.addServiceExceptions(chunk.exceptions) + is GtfsData.ShapeChunk -> database.addShapes(chunk.shapes) + is GtfsData.StopChunk -> database.addStops(chunk.stops) + is GtfsData.StopTimeChunk -> database.addStopTimes(chunk.stopTimes) + is GtfsData.TripChunk -> database.addTrips(chunk.trips) } } + + database.updateMetadata(date) + database.close() + dbm.swap() } - private suspend fun updateMetadata(date: Long) { - val dao = database.versionMetadataDao + private suspend fun Database.updateMetadata(date: Long) { + val dao = versionMetadataDao log.info("updating metadata...") dao.update(date, listOf("routes", "stops", "shapes", "trips", "stop_times")) log.info("done") } - private suspend fun addRoutes(routes: List) { - val dao = database.routeDao + private suspend fun Database.addRoutes(routes: List) { + val dao = routeDao log.info("inserting routes...") dao.insertOrReplaceAll(*routes.map { it.asEntity() }.toTypedArray()) log.info("done") } - private suspend fun addServices(services: List) { - val dao = database.serviceDao + private suspend fun Database.addServices(services: List) { + val dao = serviceDao log.info("inserting services...") dao.insertOrReplaceAll(*services.map { it.asEntity() }.toTypedArray()) log.info("done") } - private suspend fun addServiceExceptions(exceptions: List) { - val dao = database.serviceExceptionDao + private suspend fun Database.addServiceExceptions(exceptions: List) { + val dao = serviceExceptionDao log.info("inserting exceptions...") dao.insertOrReplaceAll(*exceptions.map { it.asEntity() }.toTypedArray()) log.info("done") } - private suspend fun addShapes(shapes: List) { - val dao = database.shapeDao + private suspend fun Database.addShapes(shapes: List) { + val dao = shapeDao log.info("inserting shapes...") dao.insertOrReplaceAll(*shapes.map { it.asEntity() }.toTypedArray()) log.info("done") } - private suspend fun addStops(stops: List) { - val dao = database.stopDao + private suspend fun Database.addStops(stops: List) { + val dao = stopDao log.info("inserting stops...") stops .groupBy { it.id } @@ -102,15 +93,15 @@ class GtfsImporter( log.info("done") } - private suspend fun addStopTimes(stopTimes: List) { - val dao = database.stopTimeDao + private suspend fun Database.addStopTimes(stopTimes: List) { + val dao = stopTimeDao log.info("inserting ${stopTimes.size} stoptimes...") dao.insertOrReplaceAll(*stopTimes.map { it.asEntity() }.toTypedArray()) log.info("done") } - private suspend fun addTrips(trips: List) { - val dao = database.tripDao + private suspend fun Database.addTrips(trips: List) { + val dao = tripDao log.info("inserting ${trips.size} trips...") dao.insertOrReplaceAll(*trips.map { it.asEntity() }.toTypedArray()) log.info("done") diff --git a/server/src/main/kotlin/moe/lava/banksia/server/di/ServerModules.kt b/server/src/main/kotlin/moe/lava/banksia/server/di/ServerModules.kt index 7c7fc0b..a8a7541 100644 --- a/server/src/main/kotlin/moe/lava/banksia/server/di/ServerModules.kt +++ b/server/src/main/kotlin/moe/lava/banksia/server/di/ServerModules.kt @@ -6,6 +6,7 @@ import moe.lava.banksia.server.GtfsDataFixer import moe.lava.banksia.server.GtfsImporter import moe.lava.banksia.server.gtfs.GtfsParser import moe.lava.banksia.server.gtfsrt.GtfsrtService +import org.koin.core.module.dsl.factoryOf import org.koin.core.module.dsl.singleOf import org.koin.dsl.module @@ -16,6 +17,6 @@ val ServerModules = module { singleOf(::GtfsParser) singleOf(::GtfsrtService) - singleOf(::GtfsDataFixer) - singleOf(::GtfsImporter) + factoryOf(::GtfsDataFixer) + factoryOf(::GtfsImporter) } diff --git a/settings.gradle.kts b/settings.gradle.kts index 335422d..9abcf7f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -37,6 +37,8 @@ include(":server:gtfs") include(":server:gtfs_rt") include(":core") include(":core:data") +include(":core:data:client") +include(":core:data:server") include(":core:room") include(":ui") include(":ui:maps") diff --git a/ui/build.gradle.kts b/ui/build.gradle.kts index 018c8e6..9c5c7bd 100644 --- a/ui/build.gradle.kts +++ b/ui/build.gradle.kts @@ -68,7 +68,7 @@ kotlin { implementation(libs.ui.backhandler) implementation(projects.core) - implementation(projects.core.data) + implementation(projects.core.data.client) implementation(projects.ui.maps) implementation(projects.ui.shared) } diff --git a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/di/AppModule.kt b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/di/AppModule.kt index cff36fb..a2b4d7e 100644 --- a/ui/src/commonMain/kotlin/moe/lava/banksia/ui/di/AppModule.kt +++ b/ui/src/commonMain/kotlin/moe/lava/banksia/ui/di/AppModule.kt @@ -1,12 +1,12 @@ package moe.lava.banksia.ui.di -import moe.lava.banksia.core.data.dataDiModule +import moe.lava.banksia.core.data.clientDataDiModule import moe.lava.banksia.ui.screens.map.MapScreenViewModel import org.koin.core.module.dsl.viewModelOf import org.koin.dsl.module val AppModule = module { - includes(dataDiModule) + includes(clientDataDiModule) // ViewModel viewModelOf(::MapScreenViewModel)