Refactor repeated usage of coroutineScope
authorLanius Trolling <lanius@laniustrolling.dev>
Sun, 28 Apr 2024 15:52:03 +0000 (11:52 -0400)
committerLanius Trolling <lanius@laniustrolling.dev>
Sun, 28 Apr 2024 15:52:03 +0000 (11:52 -0400)
src/jvmMain/kotlin/info/mechyrdia/data/DataFiles.kt
src/jvmMain/kotlin/info/mechyrdia/data/MigrateFiles.kt
src/jvmMain/kotlin/info/mechyrdia/data/ViewComments.kt
src/jvmMain/kotlin/info/mechyrdia/data/ViewsFiles.kt
src/jvmMain/kotlin/info/mechyrdia/lore/ArticleListing.kt
src/jvmMain/kotlin/info/mechyrdia/lore/Fonts.kt
src/jvmMain/kotlin/info/mechyrdia/lore/ParserLexerAsync.kt
src/jvmMain/kotlin/info/mechyrdia/lore/ParserPreprocess.kt
src/jvmMain/kotlin/info/mechyrdia/lore/ViewsRss.kt
src/jvmMain/kotlin/info/mechyrdia/route/ResourceWebDav.kt

index 77f9563eda7f865b9fa2eb714a7bdf424d31a10f..165e4fd50119576ec93e9431bd1209ec86ea3375 100644 (file)
@@ -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<GridFsEntry>, 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<GridFsEntry>, 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()
                }
        }
        
index 21de217ee435af8f2057dfe6a195aab0f9b600cb..ffad7e9a254477189197aa7878a4b1bfbedce0d7 100644 (file)
@@ -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<String> {
-       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 {
index 930f876b869abf8536ae862f5e51a7ccb2517240..484bad331be41956029a85dd6eebae593c16171d 100644 (file)
@@ -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<Id<Comment>>,
 ) {
        companion object {
+               private suspend fun render(comment: Comment, nations: MutableMap<Id<NationData>, 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<Comment>, nations: MutableMap<Id<NationData>, NationData> = mutableMapOf()): List<CommentRenderData> {
-                       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)
                        }
                }
        }
index 1df19d8afcd216d252237e78b2d23b4de422fb64..2d5fd496799b8daf40802e280c6275e5b9fbae3a 100644 (file)
@@ -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<String, StoredFileType>.sortedAsFiles() = toList()
@@ -38,16 +40,9 @@ private fun Map<String, TreeNode>.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)
index 1b7a65226bab4a3de6ff52fc02f2b006c7906578..83da218d82109e823554b9a89baaa3dc053fc310 100644 (file)
@@ -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<ArticleNode> = 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()
 )
 
index 17c0294a4ce196f89d3242d0c4a0c79b9ad79663..8baa193b39ab3b284ac1183a0db94788149985ca 100644 (file)
@@ -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")
index 6889efc461ca9a601c155711feab40d58698f37e..564f22eeffcfa269ffb238ac21d5c77f22dafce4 100644 (file)
@@ -1,9 +1,5 @@
 package info.mechyrdia.lore
 
-import kotlinx.coroutines.async
-import kotlinx.coroutines.awaitAll
-import kotlinx.coroutines.coroutineScope
-
 data class AsyncLexerTagEnvironment<TContext, TSubject>(
        val context: TContext,
        private val processTags: AsyncLexerTags<TContext, TSubject>,
@@ -57,10 +53,8 @@ fun interface AsyncLexerLineBreakProcessor<TContext, TSubject> {
 
 fun interface AsyncLexerCombiner<TContext, TSubject> {
        suspend fun processAndCombine(env: AsyncLexerTagEnvironment<TContext, TSubject>, nodes: ParserTree): TSubject {
-               return combine(env, coroutineScope {
-                       nodes.map {
-                               async { env.processNode(it) }
-                       }.awaitAll()
+               return combine(env, nodes.mapSuspend {
+                       env.processNode(it)
                })
        }
        
index 88247074a9346ada7f3baa1c409369edcc97322b..5e68d7717549338339447b39cd8a3ea7f3e94b5c 100644 (file)
@@ -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<String, ParserTree> = mapNotNull {
                it.param to it.subNodes
 }.toMap()
 
+suspend fun <T> Iterable<T>.forEachSuspend(processor: suspend (T) -> Unit) = coroutineScope {
+       map {
+               launch {
+                       processor(it)
+               }
+       }.joinAll()
+}
+
 suspend fun <T, R> Iterable<T>.mapSuspend(processor: suspend (T) -> R) = coroutineScope {
        map {
                async {
index 1be68806d9b9484d35fa5a268d4be9ebbffd92c8..30268a3da68efe7b7dd63736a9643c4cd931ddf4 100644 (file)
@@ -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<StoragePathWit
        val stat = statAll(listOf(path, dataPath))
        return if (stat != null)
                listOf(StoragePathWithStat(path, stat))
-       else if (subNodes != null) coroutineScope {
-               subNodes.map { subNode ->
-                       async {
-                               subNode.getPages(path)
-                       }
-               }.awaitAll().flatten()
-       } else emptyList()
+       else subNodes?.mapSuspend { subNode ->
+               subNode.getPages(path)
+       }?.flatten().orEmpty()
 }
 
 suspend fun allPages(): List<StoragePathWithStat> {
-       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()
        )
 }
 
index 215c6f158417ccd97f0817fb4e073841cdbec7ef..67b7e1cd138142945eab8a9c15250fe46645f272 100644 (file)
@@ -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)