Skip to content

Commit ea01275

Browse files
authored
refactor: replace kotlinx-io with okio for file I/O (#123)
* refactor: replace kotlinx-io with okio for file I/O Okio provides a unified cross-platform FileHandle API with built-in random access support (positional read/write, flush, resize), eliminating the need for the RandomAccessHandle abstraction and its per-platform implementations (JvmRandomAccessHandle, IosRandomAccessHandle). - Replace kotlinx-io 0.9.0 with okio 3.16.4 across all modules - Delete RandomAccessHandle interface and JVM/Android/iOS implementations - Rewrite PathFileAccessor to use okio FileHandle (openReadWrite) - Add platformFileSystem expect/actual to abstract FileSystem.SYSTEM (unavailable on WasmJs; throws UnsupportedOperationException there) - Simplify createFileAccessor on JVM/iOS (no more handleFactory param) - Update resolveChildPath to use okio Path division operator - Update DownloadExecution.deduplicatePath to use okio Path/FileSystem - Update FileConfigStore to use okio source/sink/buffer APIs - Remove unused kotlinx-io dependency from library:sqlite - Update GraalVM --initialize-at-build-time from kotlinx.io to okio * feat: add JS and WasmWasi targets with real filesystem support Replace wasmJs with js target in library:api and library:core to enable okio-nodefilesystem (NodeJsFileSystem) for Kotlin/JS. Add wasmWasi target with okio-wasifilesystem (WasiFileSystem) for WASI runtimes. Remove wasmJs from these modules since app/web (the only wasmJs consumer) only uses RemoteKetch and doesn't need core/api. * fix: update CI to run jsNodeTest instead of wasmJsBrowserTest The wasmJs target was replaced with js in library modules, so the wasmJsBrowserTest/wasmJsNodeTest Gradle tasks no longer exist. * refactor: move Ketch creation out of InstanceFactory into platform code InstanceFactory no longer references library:core types (Ketch, DownloadSource, TaskStore, KtorHttpEngine). Instead, each platform (Android, Desktop, iOS) creates the embedded Ketch instance inline and passes it as an embeddedFactory lambda. This removes the library:core dependency from app:shared's commonMain, allowing wasmJs builds to work without the core engine. Also restores wasmJs target on library:api since downstream modules still need it. * Fix kotlin targets in library modules
1 parent 2c1ae06 commit ea01275

64 files changed

Lines changed: 410 additions & 367 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/tests.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,8 @@ jobs:
8585
name: test-results-ios
8686
path: '**/build/test-results/**/TEST-*.xml'
8787

88-
wasmjs-tests:
89-
name: WasmJs Tests
88+
js-tests:
89+
name: JS Tests
9090
runs-on: ubuntu-latest
9191
steps:
9292
- uses: actions/checkout@v6
@@ -98,20 +98,20 @@ jobs:
9898

9999
- uses: gradle/actions/setup-gradle@v5
100100

101-
- name: Run WasmJs tests
102-
run: ./gradlew wasmJsBrowserTest wasmJsNodeTest
101+
- name: Run JS tests
102+
run: ./gradlew jsNodeTest
103103

104104
- name: Upload test results
105105
if: always()
106106
uses: actions/upload-artifact@v6
107107
with:
108-
name: test-results-wasmjs
108+
name: test-results-js
109109
path: '**/build/test-results/**/TEST-*.xml'
110110

111111
publish-results:
112112
name: Publish Test Results
113113
runs-on: ubuntu-latest
114-
needs: [ jvm-tests, android-tests, ios-tests, wasmjs-tests ]
114+
needs: [ jvm-tests, android-tests, ios-tests, js-tests ]
115115
if: always()
116116
steps:
117117
- name: Download test results

app/android/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ dependencies {
8484
implementation(projects.app.shared)
8585
implementation(projects.ai.discover)
8686
implementation(projects.library.core)
87+
implementation(projects.library.ktor)
88+
implementation(projects.library.ftp)
89+
implementation(projects.library.torrent)
8790
implementation(projects.library.sqlite)
8891
implementation(projects.library.server)
8992
implementation(libs.androidx.activity.compose)

app/android/src/main/kotlin/com/linroid/ketch/app/android/KetchService.kt

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,21 @@ import com.linroid.ketch.ai.AiConfig
1616
import com.linroid.ketch.ai.AiModule
1717
import com.linroid.ketch.ai.LlmConfig
1818
import com.linroid.ketch.api.log.KetchLogger
19+
import com.linroid.ketch.api.log.Logger
1920
import com.linroid.ketch.app.instance.InstanceFactory
2021
import com.linroid.ketch.app.instance.InstanceManager
2122
import com.linroid.ketch.app.instance.LocalServerHandle
2223
import com.linroid.ketch.app.instance.ServerState
2324
import com.linroid.ketch.app.state.AiDiscoveryProvider
2425
import com.linroid.ketch.app.state.EmbeddedAiDiscoveryProvider
2526
import com.linroid.ketch.config.FileConfigStore
27+
import com.linroid.ketch.core.Ketch
28+
import com.linroid.ketch.engine.KtorHttpEngine
29+
import com.linroid.ketch.ftp.FtpDownloadSource
2630
import com.linroid.ketch.server.KetchServer
2731
import com.linroid.ketch.sqlite.DriverFactory
2832
import com.linroid.ketch.sqlite.createSqliteTaskStore
33+
import com.linroid.ketch.torrent.TorrentDownloadSource
2934
import kotlinx.coroutines.CoroutineScope
3035
import kotlinx.coroutines.Dispatchers
3136
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -86,9 +91,20 @@ class KetchService : Service() {
8691
?: android.os.Build.MODEL
8792
instanceManager = InstanceManager(
8893
factory = InstanceFactory(
89-
taskStore = taskStore,
90-
downloadConfig = downloadConfig,
9194
deviceName = instanceName,
95+
embeddedFactory = {
96+
Ketch(
97+
httpEngine = KtorHttpEngine(),
98+
taskStore = taskStore,
99+
config = downloadConfig,
100+
name = instanceName,
101+
logger = Logger.console(),
102+
additionalSources = listOf(
103+
FtpDownloadSource(),
104+
TorrentDownloadSource(),
105+
),
106+
)
107+
},
92108
localServerFactory = { ketchApi ->
93109
val serverConfig = config.server
94110
log.i { "Starting local server on port ${serverConfig.port}" }

app/desktop/build.gradle.kts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@ dependencies {
1111
implementation(projects.config)
1212
implementation(projects.app.shared)
1313
implementation(projects.ai.discover)
14-
implementation(projects.library.server)
14+
implementation(projects.library.core)
1515
implementation(projects.library.ktor)
16+
implementation(projects.library.ftp)
17+
implementation(projects.library.torrent)
18+
implementation(projects.library.server)
1619
implementation(projects.library.sqlite)
1720
implementation(compose.desktop.currentOs)
1821
implementation(libs.kotlinx.coroutinesSwing)

app/desktop/src/main/kotlin/com/linroid/ketch/app/desktop/main.kt

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,21 @@ import androidx.compose.ui.window.application
88
import com.linroid.ketch.ai.AiConfig
99
import com.linroid.ketch.ai.AiModule
1010
import com.linroid.ketch.ai.LlmConfig
11+
import com.linroid.ketch.api.log.Logger
1112
import com.linroid.ketch.app.App
1213
import com.linroid.ketch.app.instance.InstanceFactory
1314
import com.linroid.ketch.app.instance.InstanceManager
1415
import com.linroid.ketch.app.instance.LocalServerHandle
1516
import com.linroid.ketch.app.state.EmbeddedAiDiscoveryProvider
1617
import com.linroid.ketch.config.FileConfigStore
1718
import com.linroid.ketch.config.defaultConfigDir
19+
import com.linroid.ketch.core.Ketch
20+
import com.linroid.ketch.engine.KtorHttpEngine
21+
import com.linroid.ketch.ftp.FtpDownloadSource
1822
import com.linroid.ketch.server.KetchServer
1923
import com.linroid.ketch.sqlite.DriverFactory
2024
import com.linroid.ketch.sqlite.createSqliteTaskStore
25+
import com.linroid.ketch.torrent.TorrentDownloadSource
2126
import java.io.File
2227
import java.net.InetAddress
2328

@@ -40,9 +45,20 @@ fun main() = application {
4045
?: InetAddress.getLocalHost().hostName.removeSuffix(".local")
4146
InstanceManager(
4247
factory = InstanceFactory(
43-
taskStore = taskStore,
44-
downloadConfig = downloadConfig,
4548
deviceName = instanceName,
49+
embeddedFactory = {
50+
Ketch(
51+
httpEngine = KtorHttpEngine(),
52+
taskStore = taskStore,
53+
config = downloadConfig,
54+
name = instanceName,
55+
logger = Logger.console(),
56+
additionalSources = listOf(
57+
FtpDownloadSource(),
58+
TorrentDownloadSource(),
59+
),
60+
)
61+
},
4662
localServerFactory = { ketchApi ->
4763
val serverConfig = config.server
4864
val server = KetchServer(

app/shared/build.gradle.kts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,8 @@ kotlin {
4040
sourceSets {
4141
commonMain.dependencies {
4242
implementation(projects.config)
43-
implementation(projects.library.core)
4443
implementation(projects.library.remote)
45-
implementation(projects.library.ktor)
44+
4645
implementation(libs.kotlinx.coroutines.core)
4746
implementation(libs.compose.runtime)
4847
implementation(libs.compose.foundation)
@@ -61,6 +60,8 @@ kotlin {
6160
implementation(libs.kotlinx.coroutines.test)
6261
}
6362
androidMain.dependencies {
63+
implementation(projects.library.core)
64+
implementation(projects.library.ktor)
6465
implementation(projects.ai.discover)
6566
implementation(projects.library.ftp)
6667
implementation(projects.library.torrent)
@@ -69,13 +70,17 @@ kotlin {
6970
implementation(libs.dnssd)
7071
}
7172
iosMain.dependencies {
73+
implementation(projects.library.core)
74+
implementation(projects.library.ktor)
7275
implementation(projects.library.ftp)
7376
implementation(projects.library.torrent)
7477
implementation(projects.library.sqlite)
7578
implementation(libs.ktor.client.darwin)
7679
implementation(libs.dnssd)
7780
}
7881
jvmMain.dependencies {
82+
implementation(projects.library.core)
83+
implementation(projects.library.ktor)
7984
implementation(projects.ai.discover)
8085
implementation(projects.library.ftp)
8186
implementation(projects.library.torrent)

app/shared/src/androidMain/kotlin/com/linroid/ketch/app/instance/PlatformSources.android.kt

Lines changed: 0 additions & 8 deletions
This file was deleted.

app/shared/src/commonMain/kotlin/com/linroid/ketch/app/instance/InstanceEntry.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package com.linroid.ketch.app.instance
22

33
import com.linroid.ketch.api.KetchApi
44
import com.linroid.ketch.config.RemoteConfig
5-
import com.linroid.ketch.core.Ketch
65
import com.linroid.ketch.remote.ConnectionState
76
import com.linroid.ketch.remote.RemoteKetch
87
import kotlinx.coroutines.flow.StateFlow
@@ -13,7 +12,7 @@ interface InstanceEntry {
1312
}
1413

1514
data class EmbeddedInstance(
16-
override val instance: Ketch,
15+
override val instance: KetchApi,
1716
override val label: String,
1817
) : InstanceEntry
1918

Lines changed: 9 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,27 @@
11
package com.linroid.ketch.app.instance
22

33
import com.linroid.ketch.api.KetchApi
4-
import com.linroid.ketch.api.DownloadConfig
5-
import com.linroid.ketch.api.log.Logger
64
import com.linroid.ketch.config.RemoteConfig
7-
import com.linroid.ketch.core.Ketch
8-
import com.linroid.ketch.core.engine.DownloadSource
9-
import com.linroid.ketch.core.task.TaskStore
10-
import com.linroid.ketch.engine.KtorHttpEngine
115
import com.linroid.ketch.remote.RemoteKetch
126

137
/**
148
* Creates [KetchApi] instances for each instance type.
159
*
16-
* @param taskStore persistent storage for download task records.
17-
* Required when using the default embedded instance. Pass `null`
18-
* for remote-only mode (e.g. wasmJs/web).
1910
* @param deviceName label for the embedded instance (e.g. device
2011
* model on Android, hostname on desktop).
21-
* @param embeddedFactory factory for creating the embedded Ketch
12+
* @param embeddedFactory factory for creating the embedded [KetchApi]
2213
* instance. When `null`, no embedded instance is available and
23-
* [InstanceManager] starts in remote-only mode.
24-
* Override in tests to inject fakes.
14+
* [InstanceManager] starts in remote-only mode (e.g. wasmJs/web).
15+
* Each platform provides its own factory that wires up the core
16+
* engine with platform-specific dependencies.
2517
* @param localServerFactory optional factory that starts an HTTP
26-
* server exposing the embedded [KetchApi]. Receives port,
27-
* optional API token, and the embedded KetchApi instance.
28-
* When non-null, server controls appear in the Embedded
29-
* instance entry. Provided by Android and JVM/Desktop.
18+
* server exposing the embedded [KetchApi]. Receives the embedded
19+
* KetchApi instance. When non-null, server controls appear in
20+
* the Embedded instance entry. Provided by Android and JVM/Desktop.
3021
*/
3122
class InstanceFactory(
32-
taskStore: TaskStore? = null,
33-
defaultDirectory: String? = null,
34-
downloadConfig: DownloadConfig = DownloadConfig(
35-
defaultDirectory = defaultDirectory,
36-
),
3723
val deviceName: String = "Embedded",
38-
additionalSources: List<DownloadSource> = platformAdditionalSources(),
39-
private val embeddedFactory: (() -> Ketch)? = taskStore?.let { ts ->
40-
{
41-
createDefaultEmbeddedKetch(
42-
ts, downloadConfig, deviceName, additionalSources,
43-
)
44-
}
45-
},
24+
private val embeddedFactory: (() -> KetchApi)? = null,
4625
private val localServerFactory: ((KetchApi) -> LocalServerHandle)? = null,
4726
) {
4827
/** Whether an embedded instance is available. */
@@ -54,7 +33,7 @@ class InstanceFactory(
5433

5534
private var localServer: LocalServerHandle? = null
5635

57-
/** Create the embedded Ketch instance. */
36+
/** Create the embedded [KetchApi] instance. */
5837
fun createEmbedded(): EmbeddedInstance {
5938
val ketch = embeddedFactory?.invoke()
6039
?: throw UnsupportedOperationException(
@@ -106,21 +85,3 @@ class InstanceFactory(
10685
localServer = null
10786
}
10887
}
109-
110-
internal expect fun platformAdditionalSources(): List<DownloadSource>
111-
112-
private fun createDefaultEmbeddedKetch(
113-
taskStore: TaskStore,
114-
config: DownloadConfig,
115-
name: String,
116-
additionalSources: List<DownloadSource>,
117-
): Ketch {
118-
return Ketch(
119-
httpEngine = KtorHttpEngine(),
120-
taskStore = taskStore,
121-
config = config,
122-
name = name,
123-
logger = Logger.console(),
124-
additionalSources = additionalSources,
125-
)
126-
}

app/shared/src/commonMain/kotlin/com/linroid/ketch/app/instance/InstanceManager.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import com.linroid.ketch.api.ResolvedSource
88
import com.linroid.ketch.api.DownloadConfig
99
import com.linroid.ketch.config.ConfigStore
1010
import com.linroid.ketch.config.RemoteConfig
11-
import com.linroid.ketch.core.Ketch
1211
import com.linroid.ketch.remote.RemoteKetch
1312
import kotlinx.coroutines.CoroutineScope
1413
import kotlinx.coroutines.Dispatchers
@@ -24,7 +23,7 @@ import kotlinx.coroutines.launch
2423
* instance, and lifecycle transitions.
2524
*
2625
* When [InstanceFactory.hasEmbedded] is `true` (Android, iOS,
27-
* JVM/Desktop), an embedded [Ketch] instance is created once
26+
* JVM/Desktop), an embedded [KetchApi] instance is created once
2827
* and reused. An optional HTTP server can be started/stopped
2928
* to expose the same instance over the network.
3029
*

0 commit comments

Comments
 (0)