From bc0ddcb33dd2fc1a335ac6ecbfb68f1e45eac4c0 Mon Sep 17 00:00:00 2001 From: Lanius Trolling Date: Sun, 28 Apr 2024 11:52:03 -0400 Subject: [PATCH] Refactor repeated usage of coroutineScope --- .../kotlin/info/mechyrdia/data/DataFiles.kt | 23 ++--- .../info/mechyrdia/data/MigrateFiles.kt | 43 ++++------ .../info/mechyrdia/data/ViewComments.kt | 47 ++++++----- .../kotlin/info/mechyrdia/data/ViewsFiles.kt | 30 ++----- .../info/mechyrdia/lore/ArticleListing.kt | 11 +-- .../kotlin/info/mechyrdia/lore/Fonts.kt | 8 +- .../info/mechyrdia/lore/ParserLexerAsync.kt | 10 +-- .../info/mechyrdia/lore/ParserPreprocess.kt | 12 ++- .../kotlin/info/mechyrdia/lore/ViewsRss.kt | 83 ++++++++----------- .../info/mechyrdia/route/ResourceWebDav.kt | 24 ++---- 10 files changed, 118 insertions(+), 173 deletions(-) diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/DataFiles.kt b/src/jvmMain/kotlin/info/mechyrdia/data/DataFiles.kt index 77f9563..165e4fd 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/data/DataFiles.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/data/DataFiles.kt @@ -6,6 +6,7 @@ import com.mongodb.reactivestreams.client.gridfs.GridFSBucket import info.mechyrdia.Configuration import info.mechyrdia.FileStorageConfig import info.mechyrdia.lore.StoragePathAttributeKey +import info.mechyrdia.lore.forEachSuspend import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.response.* @@ -318,15 +319,11 @@ private class GridFsStorage(val table: DocumentTable, val bucket: G private suspend fun getSuffix(fullPath: StoragePath, forDir: Boolean = false) = try { val pathParts = fullPath.elements - coroutineScope { - val indices = (if (forDir) 0 else 1)..pathParts.lastIndex - - indices.map { index -> - val path = StoragePath(pathParts.dropLast(index)) - launch { - if (testExact(path)) throw FileAlreadyExistsException(path.toString()) - } - } + val indices = (if (forDir) 0 else 1)..pathParts.lastIndex + + indices.forEachSuspend { index -> + val path = StoragePath(pathParts.dropLast(index)) + if (testExact(path)) throw FileAlreadyExistsException(path.toString()) } null @@ -449,12 +446,8 @@ private class GridFsStorage(val table: DocumentTable, val bucket: G ) ).asFlow().map { it.objectId }.toSet() - coroutineScope { - unusedFiles.map { unusedFile -> - launch { - bucket.delete(unusedFile).awaitFirst() - } - }.joinAll() + unusedFiles.forEachSuspend { unusedFile -> + bucket.delete(unusedFile).awaitFirst() } } diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/MigrateFiles.kt b/src/jvmMain/kotlin/info/mechyrdia/data/MigrateFiles.kt index 21de217..ffad7e9 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/data/MigrateFiles.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/data/MigrateFiles.kt @@ -4,9 +4,7 @@ package info.mechyrdia.data import info.mechyrdia.Configuration import info.mechyrdia.FileStorageConfig -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope +import info.mechyrdia.lore.mapSuspend import kotlinx.coroutines.runBlocking import kotlin.system.exitProcess @@ -48,34 +46,25 @@ private suspend fun migrateDir(path: StoragePath, from: FileStorage, into: FileS val inDir = from.listDir(path) ?: return listOf("[Source Error] Directory at /$path does not exist") - return coroutineScope { - inDir.map { (name, type) -> - async { - val subPath = path / name - when (type) { - StoredFileType.FILE -> migrateFile(subPath, from, into) - StoredFileType.DIRECTORY -> migrateDir(subPath, from, into) - } - } - }.awaitAll().flatten() - } + return inDir.toList().mapSuspend { (name, type) -> + val subPath = path / name + when (type) { + StoredFileType.FILE -> migrateFile(subPath, from, into) + StoredFileType.DIRECTORY -> migrateDir(subPath, from, into) + } + }.flatten() } private suspend fun migrateRoot(from: FileStorage, into: FileStorage): List { - val inRoot = from.listDir(StoragePath.Root) - ?: return listOf("[Source Error] Root directory does not exist") + val inRoot = from.listDir(StoragePath.Root) ?: return listOf("[Source Error] Root directory does not exist") - return coroutineScope { - inRoot.map { (name, type) -> - async { - val subPath = StoragePath.Root / name - when (type) { - StoredFileType.FILE -> migrateFile(subPath, from, into) - StoredFileType.DIRECTORY -> migrateDir(subPath, from, into) - } - } - }.awaitAll().flatten() - } + return inRoot.toList().mapSuspend { (name, type) -> + val subPath = StoragePath.Root / name + when (type) { + StoredFileType.FILE -> migrateFile(subPath, from, into) + StoredFileType.DIRECTORY -> migrateDir(subPath, from, into) + } + }.flatten() } fun interface FileStorageMigrator { diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/ViewComments.kt b/src/jvmMain/kotlin/info/mechyrdia/data/ViewComments.kt index 930f876..484bad3 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/data/ViewComments.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/data/ViewComments.kt @@ -8,7 +8,6 @@ import info.mechyrdia.route.href import info.mechyrdia.route.installCsrfToken import io.ktor.server.application.* import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import kotlinx.html.* import java.time.Instant @@ -31,28 +30,32 @@ data class CommentRenderData( val replyLinks: List>, ) { companion object { + private suspend fun render(comment: Comment, nations: MutableMap, NationData> = mutableMapOf()): CommentRenderData { + val (nationData, pageTitle, htmlResult) = coroutineScope { + val nationDataAsync = async { nations.getNation(comment.submittedBy) } + val pageTitleAsync = async { (StoragePath.articleDir / comment.submittedIn).toFriendlyPathTitle() } + val htmlResultAsync = async { comment.contents.parseAs(ParserTree::toCommentHtml) } + + Triple(nationDataAsync.await(), pageTitleAsync.await(), htmlResultAsync.await()) + } + + return CommentRenderData( + id = comment.id, + submittedBy = nationData, + submittedIn = comment.submittedIn.split('/'), + submittedAt = comment.submittedAt, + submittedInTitle = pageTitle, + numEdits = comment.numEdits, + lastEdit = comment.lastEdit, + contentsRaw = comment.contents, + contentsHtml = htmlResult, + replyLinks = CommentReplyLink.getReplies(comment.id), + ) + } + suspend operator fun invoke(comments: List, nations: MutableMap, NationData> = mutableMapOf()): List { - return coroutineScope { - comments.map { comment -> - async { - val nationDataAsync = async { nations.getNation(comment.submittedBy) } - val pageTitleAsync = async { (StoragePath.articleDir / comment.submittedIn).toFriendlyPathTitle() } - val htmlResult = comment.contents.parseAs(ParserTree::toCommentHtml) - - CommentRenderData( - id = comment.id, - submittedBy = nationDataAsync.await(), - submittedIn = comment.submittedIn.split('/'), - submittedAt = comment.submittedAt, - submittedInTitle = pageTitleAsync.await(), - numEdits = comment.numEdits, - lastEdit = comment.lastEdit, - contentsRaw = comment.contents, - contentsHtml = htmlResult, - replyLinks = CommentReplyLink.getReplies(comment.id), - ) - } - }.awaitAll() + return comments.mapSuspend { comment -> + render(comment, nations) } } } diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/ViewsFiles.kt b/src/jvmMain/kotlin/info/mechyrdia/data/ViewsFiles.kt index 1df19d8..2d5fd49 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/data/ViewsFiles.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/data/ViewsFiles.kt @@ -3,6 +3,7 @@ package info.mechyrdia.data import info.mechyrdia.auth.PageDoNotCacheAttributeKey import info.mechyrdia.lore.adminPage import info.mechyrdia.lore.dateTime +import info.mechyrdia.lore.mapSuspend import info.mechyrdia.lore.redirectHref import info.mechyrdia.route.Root import info.mechyrdia.route.href @@ -13,7 +14,8 @@ import io.ktor.server.application.* import io.ktor.server.html.* import io.ktor.server.plugins.* import io.ktor.server.response.* -import kotlinx.coroutines.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import kotlinx.html.* fun Map.sortedAsFiles() = toList() @@ -38,16 +40,9 @@ private fun Map.sortedAsNodes() = toList() private suspend fun fileTree(path: StoragePath): TreeNode? { return FileStorage.instance.statFile(path)?.let { TreeNode.FileNode(it) - } ?: coroutineScope { - FileStorage.instance.listDir(path)?.map { (name, _) -> - async { - fileTree(path / name)?.let { name to it } - } - }?.awaitAll() - ?.filterNotNull() - ?.toMap() - ?.let { TreeNode.DirNode(it) } - } + } ?: FileStorage.instance.listDir(path)?.keys?.mapSuspend { name -> + fileTree(path / name)?.let { name to it } + }?.filterNotNull()?.toMap()?.let { TreeNode.DirNode(it) } } context(ApplicationCall) @@ -236,16 +231,9 @@ suspend fun ApplicationCall.adminPreviewFile(path: StoragePath) { } private suspend fun fileTreeForCopy(path: StoragePath): TreeNode.DirNode? { - return coroutineScope { - FileStorage.instance.listDir(path)?.map { (name, _) -> - async { - fileTreeForCopy(path / name)?.let { name to it } - } - }?.awaitAll() - ?.filterNotNull() - ?.toMap() - ?.let { TreeNode.DirNode(it) } - } + return FileStorage.instance.listDir(path)?.keys?.mapSuspend { name -> + fileTreeForCopy(path / name)?.let { name to it } + }?.filterNotNull()?.toMap()?.let { TreeNode.DirNode(it) } } context(ApplicationCall) diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ArticleListing.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ArticleListing.kt index 1b7a652..83da218 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/ArticleListing.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/ArticleListing.kt @@ -6,9 +6,6 @@ import info.mechyrdia.data.StoragePath import info.mechyrdia.route.Root import info.mechyrdia.route.href import io.ktor.server.application.* -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope import kotlinx.html.UL import kotlinx.html.a import kotlinx.html.li @@ -23,12 +20,8 @@ suspend fun rootArticleNodeList(): List = StoragePath.articleDir.to suspend fun StoragePath.toArticleNode(): ArticleNode = ArticleNode( name, toFriendlyPageTitle(), - coroutineScope { - val path = this@toArticleNode - FileStorage.instance.listDir(path)?.map { (name, _) -> - val subPath = path / name - async { subPath.toArticleNode() } - }?.awaitAll() + FileStorage.instance.listDir(this)?.keys?.mapSuspend { name -> + (this / name).toArticleNode() }?.sortedAsArticles() ) diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/Fonts.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/Fonts.kt index 17c0294..8baa193 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/Fonts.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/Fonts.kt @@ -7,6 +7,7 @@ import info.mechyrdia.data.* import info.mechyrdia.route.KeyedEnumSerializer import info.mechyrdia.yieldThread import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.withContext import kotlinx.serialization.Serializable import org.slf4j.Logger @@ -81,8 +82,11 @@ object MechyrdiaSansFont { suspend fun renderTextToSvg(text: String, bold: Boolean, italic: Boolean, align: TextAlignment): SvgDoc { val (file, font) = getFont(bold, italic) - val shape = layoutText(text, file, font, align) - return createSvgDocument(shape, 80.0 / file.unitsPerEm, 12.0) + + return runInterruptible(Dispatchers.Default) { + val shape = layoutText(text, file, font, align) + createSvgDocument(shape, 80.0 / file.unitsPerEm, 12.0) + } } private val fontsRoot = StoragePath("fonts") diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ParserLexerAsync.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ParserLexerAsync.kt index 6889efc..564f22e 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/ParserLexerAsync.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/ParserLexerAsync.kt @@ -1,9 +1,5 @@ package info.mechyrdia.lore -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope - data class AsyncLexerTagEnvironment( val context: TContext, private val processTags: AsyncLexerTags, @@ -57,10 +53,8 @@ fun interface AsyncLexerLineBreakProcessor { fun interface AsyncLexerCombiner { suspend fun processAndCombine(env: AsyncLexerTagEnvironment, nodes: ParserTree): TSubject { - return combine(env, coroutineScope { - nodes.map { - async { env.processNode(it) } - }.awaitAll() + return combine(env, nodes.mapSuspend { + env.processNode(it) }) } diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocess.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocess.kt index 8824707..5e68d77 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocess.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocess.kt @@ -4,9 +4,7 @@ import info.mechyrdia.JsonStorageCodec import info.mechyrdia.data.StoragePath import io.ktor.server.application.* import io.ktor.server.request.* -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.* import java.time.Instant import kotlin.math.roundToInt @@ -239,6 +237,14 @@ fun ParserTree.asPreProcessorMap(): Map = mapNotNull { it.param to it.subNodes }.toMap() +suspend fun Iterable.forEachSuspend(processor: suspend (T) -> Unit) = coroutineScope { + map { + launch { + processor(it) + } + }.joinAll() +} + suspend fun Iterable.mapSuspend(processor: suspend (T) -> R) = coroutineScope { map { async { diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/ViewsRss.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/ViewsRss.kt index 1be6880..30268a3 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/ViewsRss.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/ViewsRss.kt @@ -6,9 +6,6 @@ import info.mechyrdia.OwnerNationId import info.mechyrdia.data.* import io.ktor.http.* import io.ktor.server.application.* -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.toList @@ -45,23 +42,15 @@ private suspend fun ArticleNode.getPages(base: StoragePath): List - async { - subNode.getPages(path) - } - }.awaitAll().flatten() - } else emptyList() + else subNodes?.mapSuspend { subNode -> + subNode.getPages(path) + }?.flatten().orEmpty() } suspend fun allPages(): List { - return coroutineScope { - rootArticleNodeList().map { subNode -> - async { - subNode.getPages(StoragePath.articleDir) - } - }.awaitAll().flatten() - } + return rootArticleNodeList().mapSuspend { subNode -> + subNode.getPages(StoragePath.articleDir) + }.flatten() } suspend fun generateRecentPageEdits(): RssChannel { @@ -79,38 +68,34 @@ suspend fun generateRecentPageEdits(): RssChannel { categories = listOf( RssCategory(domain = "https://nationstates.net", category = "Mechyrdia") ), - items = coroutineScope { - pages.map { page -> - async { - val pageLink = page.path.elements.drop(1) - val pageMarkup = FactbookLoader.loadFactbook(pageLink) ?: return@async null - - val pageToC = TableOfContentsBuilder() - pageMarkup.buildToC(pageToC) - val pageOg = pageToC.toOpenGraph() - - val imageEnclosure = pageOg?.image?.let { url -> - val assetPath = url.removePrefix("$MainDomainName/assets/") - val file = StoragePath.assetDir / assetPath - RssItemEnclosure( - url = url, - length = FileStorage.instance.statFile(file)?.size ?: 0L, - type = ContentType.defaultForFileExtension(assetPath.substringAfterLast('.')).toString() - ) - } - - RssItem( - title = pageToC.toPageTitle(), - description = pageOg?.description, - link = "$MainDomainName/lore${pageLink.joinToString(separator = "") { "/$it" }}", - author = null, - comments = "$MainDomainName/lore${pageLink.joinToString(separator = "") { "/$it" }}#comments", - enclosure = imageEnclosure, - pubDate = page.stat.updated - ) - } - }.awaitAll().filterNotNull() - } + items = pages.mapSuspend { page -> + val pageLink = page.path.elements.drop(1) + val pageMarkup = FactbookLoader.loadFactbook(pageLink) ?: return@mapSuspend null + + val pageToC = TableOfContentsBuilder() + pageMarkup.buildToC(pageToC) + val pageOg = pageToC.toOpenGraph() + + val imageEnclosure = pageOg?.image?.let { url -> + val assetPath = url.removePrefix("$MainDomainName/assets/") + val file = StoragePath.assetDir / assetPath + RssItemEnclosure( + url = url, + length = FileStorage.instance.statFile(file)?.size ?: 0L, + type = ContentType.defaultForFileExtension(assetPath.substringAfterLast('.')).toString() + ) + } + + RssItem( + title = pageToC.toPageTitle(), + description = pageOg?.description, + link = "$MainDomainName/lore${pageLink.joinToString(separator = "") { "/$it" }}", + author = null, + comments = "$MainDomainName/lore${pageLink.joinToString(separator = "") { "/$it" }}#comments", + enclosure = imageEnclosure, + pubDate = page.stat.updated + ) + }.filterNotNull() ) } diff --git a/src/jvmMain/kotlin/info/mechyrdia/route/ResourceWebDav.kt b/src/jvmMain/kotlin/info/mechyrdia/route/ResourceWebDav.kt index 215c6f1..67b7e1c 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/route/ResourceWebDav.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/route/ResourceWebDav.kt @@ -3,6 +3,7 @@ package info.mechyrdia.route import info.mechyrdia.auth.WebDavToken import info.mechyrdia.auth.toNationId import info.mechyrdia.data.* +import info.mechyrdia.lore.mapSuspend import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.html.* @@ -10,10 +11,6 @@ import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* import io.ktor.util.* -import io.ktor.utils.io.core.* -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope import kotlinx.html.* import java.net.URI import java.time.Instant @@ -21,7 +18,6 @@ import java.time.ZoneOffset import java.time.ZonedDateTime import java.time.format.DateTimeFormatter import java.util.* -import kotlin.text.String const val WebDavDomainName = "https://dav.mechyrdia.info" @@ -99,13 +95,9 @@ private suspend fun getWebDavPropertiesWithIncludeTags(path: StoragePath, webRoo ) } ?: FileStorage.instance.listDir(path)?.let { subEntries -> val subPaths = subEntries.keys.map { path / it } - val subProps = coroutineScope { - subPaths.map { subPath -> - async { - getWebDavPropertiesWithIncludeTags(subPath, webRoot, depth - 1) - } - }.awaitAll().filterNotNull().flatten() - } + val subProps = subPaths.mapSuspend { subPath -> + getWebDavPropertiesWithIncludeTags(subPath, webRoot, depth - 1) + }.filterNotNull().flatten() val pathWithSuffix = path.elements.joinToString(separator = "") { "$it/" } listOf( @@ -129,11 +121,9 @@ suspend fun FileStorage.copyWebDav(source: StoragePath, target: StoragePath): Bo return when (getType(source)) { StoredFileType.DIRECTORY -> createDir(target) && (listDir(source)?.let { subPaths -> val copyActions = subPaths.keys.map { (source / it) to (target / it) } - coroutineScope { - copyActions.map { (subSource, subTarget) -> - async { copyWebDav(subSource, subTarget) } - }.awaitAll().all { it } - } + copyActions.mapSuspend { (subSource, subTarget) -> + copyWebDav(subSource, subTarget) + }.all { it } } == true) StoredFileType.FILE -> copyFile(source, target) -- 2.25.1