diff --git a/kmp/build.gradle.kts b/kmp/build.gradle.kts index 0cf7b12a..c05ff9ef 100644 --- a/kmp/build.gradle.kts +++ b/kmp/build.gradle.kts @@ -93,8 +93,6 @@ kotlin { // Okio — cross-platform file I/O for asset management implementation("com.squareup.okio:okio:3.17.0") - // ktoml — TOML parsing for .stele-sections manifest - implementation("com.akuleshov7:ktoml-core:0.7.1") } } @@ -103,6 +101,9 @@ kotlin { dependencies { // OpenTelemetry API — JVM/Android only (not available for wasmJs) implementation("io.opentelemetry:opentelemetry-api:1.43.0") + // ktoml — TOML parsing for .stele-sections manifest (excluded from wasmJs: + // ktoml generates WASM bytecode that fails wasm-opt validation) + implementation("com.akuleshov7:ktoml-core:0.7.1") } } @@ -313,6 +314,10 @@ kotlin { // Kable — Kotlin coroutine BLE for iOS/Apple targets (CoreBluetooth wrapper) implementation("com.juul.kable:core:0.32.0") + + // ktoml — TOML parsing for .stele-sections manifest (actual impl for iOS; + // excluded from commonMain to avoid invalid WASM bytecode on wasmJs target) + implementation("com.akuleshov7:ktoml-core:0.7.1") } } diff --git a/kmp/src/androidMain/kotlin/BUILD.bazel b/kmp/src/androidMain/kotlin/BUILD.bazel index d7b2c25b..9a663e8e 100644 --- a/kmp/src/androidMain/kotlin/BUILD.bazel +++ b/kmp/src/androidMain/kotlin/BUILD.bazel @@ -97,6 +97,9 @@ kt_android_library( "@maven//:io_opentelemetry_opentelemetry_sdk", "@maven//:io_opentelemetry_opentelemetry_exporter_logging", + # ktoml — TOML parsing for .stele-sections manifest (from commonMain) + "@maven//:com_akuleshov7_ktoml_core_jvm", + # Ksoup (from commonMain) "@maven//:com_fleeksoft_ksoup_ksoup_jvm", diff --git a/kmp/src/commonMain/kotlin/dev/stapler/stelekit/db/GraphManager.kt b/kmp/src/commonMain/kotlin/dev/stapler/stelekit/db/GraphManager.kt index bdcf42c1..775dcac2 100644 --- a/kmp/src/commonMain/kotlin/dev/stapler/stelekit/db/GraphManager.kt +++ b/kmp/src/commonMain/kotlin/dev/stapler/stelekit/db/GraphManager.kt @@ -378,7 +378,11 @@ class GraphManager( // never hangs permanently. graphScope.launch(PlatformDispatcher.IO) { try { + val t0 = kotlin.time.Clock.System.now().toEpochMilliseconds() + fun elapsed() = kotlin.time.Clock.System.now().toEpochMilliseconds() - t0 + preFlightJob?.await() + logger.info("init[${elapsed()}ms]: preFlightJob done") val dbUrl = driverFactory.getDatabaseUrl(id) val factory = dev.stapler.stelekit.repository.RepositoryFactoryImpl(driverFactory, dbUrl, graphId = id) @@ -396,6 +400,7 @@ class GraphManager( appVersion = deviceInfo?.appVersion ?: "unknown", platform = deviceInfo?.platform ?: "unknown", ) + logger.info("init[${elapsed()}ms]: createRepositorySet done") currentFactory = factory _activeRepositorySet.value = repoSet @@ -404,6 +409,7 @@ class GraphManager( if (writeActor != null) { val db = factory.steleDatabase() UuidMigration(writeActor).runIfNeeded(db) + logger.info("init[${elapsed()}ms]: UuidMigration done") try { MigrationRunner( registry = MigrationRegistry, @@ -417,6 +423,7 @@ class GraphManager( } catch (e: MigrationTamperedError) { logger.error("MigrationRunner: tampered migration detected for graph $id", e) } + logger.info("init[${elapsed()}ms]: content migrations done") } } } catch (e: CancellationException) { diff --git a/kmp/src/commonMain/kotlin/dev/stapler/stelekit/db/MigrationRunner.kt b/kmp/src/commonMain/kotlin/dev/stapler/stelekit/db/MigrationRunner.kt index 4e0df451..d0157479 100644 --- a/kmp/src/commonMain/kotlin/dev/stapler/stelekit/db/MigrationRunner.kt +++ b/kmp/src/commonMain/kotlin/dev/stapler/stelekit/db/MigrationRunner.kt @@ -828,12 +828,24 @@ object MigrationRunner { // driver throws when execSQL is called for result-returning statements. It is set in // ANDROID_PRAGMAS via the rawQuery path (DriverFactory.android.kt) and in // buildMainDbConnectionProps() for JVM so every pool connection inherits it. - driver.execute(null, "ANALYZE blocks", 0).await() - driver.execute(null, "ANALYZE pages", 0).await() - // PRAGMA optimize is still useful for other tables we don't explicitly analyze above. + // ponytail: only ANALYZE when stats are absent; PRAGMA optimize handles stale stats + if (!hasStats(driver, "blocks")) driver.execute(null, "ANALYZE blocks", 0).await() + if (!hasStats(driver, "pages")) driver.execute(null, "ANALYZE pages", 0).await() driver.execute(null, "PRAGMA optimize", 0).await() } + // Returns false if sqlite_stat1 doesn't exist yet or has no row for [table]. + private suspend fun hasStats(driver: SqlDriver, table: String): Boolean = try { + driver.executeQuery( + identifier = null, + sql = "SELECT count(*) FROM sqlite_stat1 WHERE tbl = '$table'", + mapper = { cursor -> cursor.next(); QueryResult.Value((cursor.getLong(0) ?: 0L) > 0L) }, + parameters = 0 + ).await() + } catch (_: Exception) { + false + } + internal suspend fun applyAll(driver: SqlDriver, migrations: List) { // Bootstrap the tracking table — must succeed before anything else. driver.execute( diff --git a/kmp/src/commonMain/kotlin/dev/stapler/stelekit/sections/SectionManifestParser.kt b/kmp/src/commonMain/kotlin/dev/stapler/stelekit/sections/SectionManifestParser.kt index 40f1954d..ab01384f 100644 --- a/kmp/src/commonMain/kotlin/dev/stapler/stelekit/sections/SectionManifestParser.kt +++ b/kmp/src/commonMain/kotlin/dev/stapler/stelekit/sections/SectionManifestParser.kt @@ -3,14 +3,9 @@ package dev.stapler.stelekit.sections import arrow.core.Either import arrow.core.left import arrow.core.right -import com.akuleshov7.ktoml.Toml -import com.akuleshov7.ktoml.TomlInputConfig import dev.stapler.stelekit.error.DomainError import dev.stapler.stelekit.platform.FileSystem import kotlinx.coroutines.CancellationException -import kotlinx.serialization.serializer - -internal val sectionToml = Toml(inputConfig = TomlInputConfig(ignoreUnknownNames = true)) class SectionManifestParser(private val fileSystem: FileSystem) { @@ -23,7 +18,7 @@ class SectionManifestParser(private val fileSystem: FileSystem) { ?: return DomainError.FileSystemError.ReadFailed(path, "could not read manifest").left() return try { - sectionToml.decodeFromString(serializer(), content).right() + (decodeSectionManifestToml(content) ?: SectionManifest()).right() } catch (e: CancellationException) { throw e } catch (e: Exception) { @@ -31,3 +26,8 @@ class SectionManifestParser(private val fileSystem: FileSystem) { } } } + +// ktoml doesn't support Kotlin/Wasm — platform actuals provide real parsing/writing on JVM/iOS, +// stubs on WASM (parser falls back to empty SectionManifest; writer catches the exception). +internal expect fun decodeSectionManifestToml(content: String): SectionManifest? +internal expect fun encodeSectionManifestToml(manifest: SectionManifest): String diff --git a/kmp/src/commonMain/kotlin/dev/stapler/stelekit/sections/SectionManifestWriter.kt b/kmp/src/commonMain/kotlin/dev/stapler/stelekit/sections/SectionManifestWriter.kt index bb7146ff..5055fdb1 100644 --- a/kmp/src/commonMain/kotlin/dev/stapler/stelekit/sections/SectionManifestWriter.kt +++ b/kmp/src/commonMain/kotlin/dev/stapler/stelekit/sections/SectionManifestWriter.kt @@ -6,15 +6,13 @@ import arrow.core.right import dev.stapler.stelekit.error.DomainError import dev.stapler.stelekit.platform.FileSystem import kotlinx.coroutines.CancellationException -import kotlinx.serialization.serializer - class SectionManifestWriter(private val fileSystem: FileSystem) { fun write(graphPath: String, manifest: SectionManifest): Either { val base = if (graphPath.endsWith("/")) graphPath else "$graphPath/" val path = "$base${SectionManifest.FILENAME}" return try { - val content = sectionToml.encodeToString(serializer(), manifest) + val content = encodeSectionManifestToml(manifest) if (fileSystem.writeFile(path, content)) Unit.right() else DomainError.FileSystemError.WriteFailed(path, "write returned false").left() } catch (e: CancellationException) { diff --git a/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/SectionBadge.kt b/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/SectionBadge.kt index 62db9923..65c2ab1c 100644 --- a/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/SectionBadge.kt +++ b/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/SectionBadge.kt @@ -28,8 +28,8 @@ import dev.stapler.stelekit.sections.SectionDefinition @Composable fun SectionBadge( section: SectionDefinition, - onClick: (() -> Unit)? = null, modifier: Modifier = Modifier, + onClick: (() -> Unit)? = null, ) { val dotColor = remember(section.color) { section.color?.let { diff --git a/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/settings/SettingsDialog.kt b/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/settings/SettingsDialog.kt index 2ff5021f..2b9aa7a3 100644 --- a/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/settings/SettingsDialog.kt +++ b/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/settings/SettingsDialog.kt @@ -214,15 +214,18 @@ fun SettingsDialog( onLibsqlDriverToggle = onLibsqlDriverToggle, ) } - SettingsCategory.SECTIONS -> if (sectionManifest != null && - onCreateSection != null && onRenameSection != null && onDeleteSection != null - ) { - SectionsSettings( - manifest = sectionManifest, - onCreateSection = onCreateSection, - onRenameSection = onRenameSection, - onDeleteSection = onDeleteSection, - ) + SettingsCategory.SECTIONS -> { + val canShowSections = sectionManifest != null && + onCreateSection != null && onRenameSection != null && + onDeleteSection != null + if (canShowSections) { + SectionsSettings( + manifest = sectionManifest!!, + onCreateSection = onCreateSection!!, + onRenameSection = onRenameSection!!, + onDeleteSection = onDeleteSection!!, + ) + } } SettingsCategory.DEVICE_SUBSCRIPTIONS -> if (sectionManifest != null && onToggleSectionState != null diff --git a/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/onboarding/DeviceSetupWizard.kt b/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/onboarding/DeviceSetupWizard.kt index daec2f63..4022deeb 100644 --- a/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/onboarding/DeviceSetupWizard.kt +++ b/kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/onboarding/DeviceSetupWizard.kt @@ -108,16 +108,18 @@ private fun ModeSelectionContent( onPersonalMode: () -> Unit, onCustomMode: () -> Unit, ) { - Text("How will you use SteleKit on this device?", style = MaterialTheme.typography.bodyMedium) - Spacer(modifier = Modifier.height(8.dp)) - OutlinedButton(onClick = onWorkMode, modifier = Modifier.fillMaxWidth()) { - Text("Work device — primary section only") - } - OutlinedButton(onClick = onPersonalMode, modifier = Modifier.fillMaxWidth()) { - Text("Personal device — all sections active") - } - OutlinedButton(onClick = onCustomMode, modifier = Modifier.fillMaxWidth()) { - Text("Custom") + Column { + Text("How will you use SteleKit on this device?", style = MaterialTheme.typography.bodyMedium) + Spacer(modifier = Modifier.height(8.dp)) + OutlinedButton(onClick = onWorkMode, modifier = Modifier.fillMaxWidth()) { + Text("Work device — primary section only") + } + OutlinedButton(onClick = onPersonalMode, modifier = Modifier.fillMaxWidth()) { + Text("Personal device — all sections active") + } + OutlinedButton(onClick = onCustomMode, modifier = Modifier.fillMaxWidth()) { + Text("Custom") + } } } @@ -150,39 +152,41 @@ private fun CustomModeContent( customDefaultSection: String, onDefaultChange: (String) -> Unit, ) { - Text("Section access:", style = MaterialTheme.typography.bodyMedium) - sections.forEach { section -> - val state = customStates[section.id] ?: SectionState.ACTIVE - Row( - modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Text(section.displayName, modifier = Modifier.weight(1f)) - TextButton(onClick = { - val next = when (state) { - SectionState.ACTIVE -> SectionState.HIDDEN - SectionState.HIDDEN -> SectionState.REMOVED - SectionState.REMOVED -> SectionState.ACTIVE + Column { + Text("Section access:", style = MaterialTheme.typography.bodyMedium) + sections.forEach { section -> + val state = customStates[section.id] ?: SectionState.ACTIVE + Row( + modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text(section.displayName, modifier = Modifier.weight(1f)) + TextButton(onClick = { + val next = when (state) { + SectionState.ACTIVE -> SectionState.HIDDEN + SectionState.HIDDEN -> SectionState.REMOVED + SectionState.REMOVED -> SectionState.ACTIVE + } + onStateChange(section.id, next) + }) { + Text(state.name.lowercase().replaceFirstChar { it.uppercase() }) } - onStateChange(section.id, next) - }) { - Text(state.name.lowercase().replaceFirstChar { it.uppercase() }) } } - } - Spacer(modifier = Modifier.height(8.dp)) - Text("Default section (for new pages):", style = MaterialTheme.typography.bodyMedium) - Row(verticalAlignment = Alignment.CenterVertically) { - RadioButton(selected = customDefaultSection.isEmpty(), onClick = { onDefaultChange("") }) - Text("Global", style = MaterialTheme.typography.bodyLarge) - } - sections.forEach { section -> + Spacer(modifier = Modifier.height(8.dp)) + Text("Default section (for new pages):", style = MaterialTheme.typography.bodyMedium) Row(verticalAlignment = Alignment.CenterVertically) { - RadioButton( - selected = customDefaultSection == section.id, - onClick = { onDefaultChange(section.id) }, - ) - Text(section.displayName, style = MaterialTheme.typography.bodyLarge) + RadioButton(selected = customDefaultSection.isEmpty(), onClick = { onDefaultChange("") }) + Text("Global", style = MaterialTheme.typography.bodyLarge) + } + sections.forEach { section -> + Row(verticalAlignment = Alignment.CenterVertically) { + RadioButton( + selected = customDefaultSection == section.id, + onClick = { onDefaultChange(section.id) }, + ) + Text(section.displayName, style = MaterialTheme.typography.bodyLarge) + } } } } diff --git a/kmp/src/generated/sqldelight/dev/stapler/stelekit/db/SteleDatabaseQueries.kt b/kmp/src/generated/sqldelight/dev/stapler/stelekit/db/SteleDatabaseQueries.kt index bc598c95..815bd0e3 100644 --- a/kmp/src/generated/sqldelight/dev/stapler/stelekit/db/SteleDatabaseQueries.kt +++ b/kmp/src/generated/sqldelight/dev/stapler/stelekit/db/SteleDatabaseQueries.kt @@ -991,7 +991,11 @@ public class SteleDatabaseQueries( ) } - public fun selectUnloadedPagesBySection(section_id: Collection, value_: Long, value__: Long): Query = selectUnloadedPagesBySection(section_id, value_, value__, ::Pages) + public fun selectUnloadedPagesBySection( + section_id: Collection, + value_: Long, + value__: Long, + ): Query = selectUnloadedPagesBySection(section_id, value_, value__, ::Pages) public fun countUnloadedPages(): Query = Query(-58_485_640, arrayOf("pages"), driver, "SteleDatabase.sq", "countUnloadedPages", "SELECT COUNT(*) FROM pages WHERE is_content_loaded = 0") { cursor -> cursor.getLong(0)!! @@ -5882,7 +5886,7 @@ public class SteleDatabaseQueries( override fun execute(mapper: (SqlCursor) -> QueryResult): QueryResult { val section_idIndexes = createArguments(count = section_id.size) - return driver.executeQuery(null, """SELECT pages.uuid, pages.name, pages.namespace, pages.file_path, pages.created_at, pages.updated_at, pages.properties, pages.version, pages.is_favorite, pages.is_journal, pages.journal_date, pages.is_content_loaded, pages.backlink_count, pages.section_id FROM pages WHERE is_content_loaded = 0 AND section_id IN $section_idIndexes ORDER BY uuid LIMIT ? OFFSET ?""", mapper, section_id.size + 2) { + return driver.executeQuery(null, """SELECT pages.uuid, pages.name, pages.namespace, pages.file_path, pages.created_at, pages.updated_at, pages.properties, pages.version, pages.is_favorite, pages.is_journal, pages.journal_date, pages.is_content_loaded, pages.backlink_count, pages.section_id FROM pages WHERE is_content_loaded = 0 AND section_id IN $section_idIndexes ORDER BY uuid LIMIT ? OFFSET ?""", mapper, 2 + section_id.size) { var parameterIndex = 0 section_id.forEach { section_id_ -> bindString(parameterIndex++, section_id_) diff --git a/kmp/src/generated/sqldelight/dev/stapler/stelekit/db/kmp/SteleDatabaseImpl.kt b/kmp/src/generated/sqldelight/dev/stapler/stelekit/db/kmp/SteleDatabaseImpl.kt index c04b6700..1b0216fd 100644 --- a/kmp/src/generated/sqldelight/dev/stapler/stelekit/db/kmp/SteleDatabaseImpl.kt +++ b/kmp/src/generated/sqldelight/dev/stapler/stelekit/db/kmp/SteleDatabaseImpl.kt @@ -266,6 +266,7 @@ private class SteleDatabaseImpl( driver.execute(null, "CREATE INDEX idx_pages_created_at ON pages(created_at DESC)", 0).await() driver.execute(null, "CREATE INDEX idx_pages_favorite ON pages(name) WHERE is_favorite = 1", 0).await() driver.execute(null, "CREATE INDEX idx_pages_journal_section ON pages(is_journal, journal_date, section_id)", 0).await() + driver.execute(null, "CREATE INDEX idx_pages_section_id ON pages(section_id, name)", 0).await() driver.execute(null, "CREATE INDEX idx_changelog_graph_status ON migration_changelog(graph_id, status)", 0).await() driver.execute(null, "CREATE INDEX idx_changelog_applied_at ON migration_changelog(graph_id, applied_at)", 0).await() driver.execute(null, """ diff --git a/kmp/src/iosMain/kotlin/dev/stapler/stelekit/sections/SectionManifestTomlDecoder.ios.kt b/kmp/src/iosMain/kotlin/dev/stapler/stelekit/sections/SectionManifestTomlDecoder.ios.kt new file mode 100644 index 00000000..5ac71d17 --- /dev/null +++ b/kmp/src/iosMain/kotlin/dev/stapler/stelekit/sections/SectionManifestTomlDecoder.ios.kt @@ -0,0 +1,13 @@ +package dev.stapler.stelekit.sections + +import com.akuleshov7.ktoml.Toml +import com.akuleshov7.ktoml.TomlInputConfig +import kotlinx.serialization.serializer + +private val sectionToml = Toml(inputConfig = TomlInputConfig(ignoreUnknownNames = true)) + +internal actual fun decodeSectionManifestToml(content: String): SectionManifest? = + sectionToml.decodeFromString(serializer(), content) + +internal actual fun encodeSectionManifestToml(manifest: SectionManifest): String = + sectionToml.encodeToString(serializer(), manifest) diff --git a/kmp/src/jvmCommonMain/kotlin/dev/stapler/stelekit/sections/SectionManifestTomlDecoder.jvm.kt b/kmp/src/jvmCommonMain/kotlin/dev/stapler/stelekit/sections/SectionManifestTomlDecoder.jvm.kt new file mode 100644 index 00000000..5ac71d17 --- /dev/null +++ b/kmp/src/jvmCommonMain/kotlin/dev/stapler/stelekit/sections/SectionManifestTomlDecoder.jvm.kt @@ -0,0 +1,13 @@ +package dev.stapler.stelekit.sections + +import com.akuleshov7.ktoml.Toml +import com.akuleshov7.ktoml.TomlInputConfig +import kotlinx.serialization.serializer + +private val sectionToml = Toml(inputConfig = TomlInputConfig(ignoreUnknownNames = true)) + +internal actual fun decodeSectionManifestToml(content: String): SectionManifest? = + sectionToml.decodeFromString(serializer(), content) + +internal actual fun encodeSectionManifestToml(manifest: SectionManifest): String = + sectionToml.encodeToString(serializer(), manifest) diff --git a/kmp/src/wasmJsMain/kotlin/dev/stapler/stelekit/sections/SectionManifestTomlDecoder.js.kt b/kmp/src/wasmJsMain/kotlin/dev/stapler/stelekit/sections/SectionManifestTomlDecoder.js.kt new file mode 100644 index 00000000..ced0f639 --- /dev/null +++ b/kmp/src/wasmJsMain/kotlin/dev/stapler/stelekit/sections/SectionManifestTomlDecoder.js.kt @@ -0,0 +1,8 @@ +package dev.stapler.stelekit.sections + +// ktoml generates invalid WASM bytecode (wasm-opt validation failure) — return null +// so SectionManifestParser falls back to an empty SectionManifest in the browser. +internal actual fun decodeSectionManifestToml(content: String): SectionManifest? = null + +internal actual fun encodeSectionManifestToml(manifest: SectionManifest): String = + throw UnsupportedOperationException("TOML encoding not supported on WASM") diff --git a/kmp/src/wasmJsMain/kotlin/dev/stapler/stelekit/sync/WasmSectionSyncService.kt b/kmp/src/wasmJsMain/kotlin/dev/stapler/stelekit/sync/WasmSectionSyncService.kt index 1e29a38f..519dea4f 100644 --- a/kmp/src/wasmJsMain/kotlin/dev/stapler/stelekit/sync/WasmSectionSyncService.kt +++ b/kmp/src/wasmJsMain/kotlin/dev/stapler/stelekit/sync/WasmSectionSyncService.kt @@ -1,6 +1,6 @@ package dev.stapler.stelekit.sync -import arrow.core.onLeft +import arrow.core.Either import dev.stapler.stelekit.logging.Logger import dev.stapler.stelekit.model.Page import dev.stapler.stelekit.model.PageUuid @@ -13,6 +13,22 @@ import kotlinx.coroutines.await import kotlinx.coroutines.delay import kotlin.time.Clock +// js() calls must be top-level functions in Kotlin/Wasm — not inside a class or companion object. +private fun jsFetchWithToken(url: String, token: String): kotlin.js.Promise = + js("""fetch(url, { headers: { 'Authorization': 'Bearer ' + token, 'Accept': 'application/vnd.github+json' } })""") + +private fun jsFetchAnon(url: String): kotlin.js.Promise = + js("""fetch(url, { headers: { 'Accept': 'application/vnd.github+json' } })""") + +private fun jsFetch(url: String, token: String?): kotlin.js.Promise = + if (token != null) jsFetchWithToken(url, token) else jsFetchAnon(url) + +private fun jsResponseStatus(response: JsAny): Int = js("response.status | 0") +private fun jsResponseHeader(response: JsAny, name: String): String? = + js("response.headers.get(name) || null") +private fun jsResponseText(response: JsAny): kotlin.js.Promise = js("response.text()") +private fun jsStringValue(v: JsAny): String = js("String(v)") + /** * WASM-only service: fetches a GitHub repo tree for a section and inserts INDEX_ONLY stub pages. * @@ -55,8 +71,10 @@ class WasmSectionSyncService(private val pageRepository: PageRepository) { isContentLoaded = false, sectionId = section.id, ) - pageRepository.savePage(stubPage) - .onLeft { logger.error("Failed to save stub for $fullPath: ${it.message}") } + val result = pageRepository.savePage(stubPage) + if (result is Either.Left) { + logger.error("Failed to save stub for $fullPath: ${result.value.message}") + } } } @@ -90,21 +108,6 @@ class WasmSectionSyncService(private val pageRepository: PageRepository) { } } - private fun jsFetchWithToken(url: String, token: String): kotlin.js.Promise = - js("""fetch(url, { headers: { 'Authorization': 'Bearer ' + token, 'Accept': 'application/vnd.github+json' } })""") - - private fun jsFetchAnon(url: String): kotlin.js.Promise = - js("""fetch(url, { headers: { 'Accept': 'application/vnd.github+json' } })""") - - private fun jsFetch(url: String, token: String?): kotlin.js.Promise = - if (token != null) jsFetchWithToken(url, token) else jsFetchAnon(url) - - private fun jsResponseStatus(response: JsAny): Int = js("response.status | 0") - private fun jsResponseHeader(response: JsAny, name: String): String? = - js("response.headers.get(name) || null") - private fun jsResponseText(response: JsAny): kotlin.js.Promise = js("response.text()") - private fun jsStringValue(v: JsAny): String = js("String(v)") - private fun extractTreePaths(json: String): List { val results = mutableListOf() var i = json.indexOf("\"path\"")