summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSho Sakuma <me@m1sk9.dev>2026-08-02 19:38:18 +0900
committerSho Sakuma <me@m1sk9.dev>2026-08-02 19:38:18 +0900
commit7e7d0875087185e6c68bb29f1d60bd51b300bfef (patch)
tree4c17b939b7d73813c5eebe8a29f382fc736e76af
parent288d1e4a4babc136e8b3837d19970cc37023eb8e (diff)
downloadLunaticChat-7e7d0875087185e6c68bb29f1d60bd51b300bfef.tar.gz
LunaticChat-7e7d0875087185e6c68bb29f1d60bd51b300bfef.tar.bz2
LunaticChat-7e7d0875087185e6c68bb29f1d60bd51b300bfef.zip
refactor: extract the debounced async save
YamlPlayerSettingsStorage and ConversionCache each carried a line-for-line identical AtomicBoolean-plus-runDelayed debounce, five-second constant included, so changing the save cadence meant changing it twice and noticing that it was written twice. Extracting it also removed the only reason those two classes held a JavaPlugin: they took the whole plugin to reach the scheduler. They now take the collaborator they actually use, which is both narrower and testable without a running server. ChannelStorage is deliberately left alone - it saves through runNow with no debounce at all, and giving it one would change when writes happen. Co-Authored-By: Claude <noreply@anthropic.com>
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/DebouncedSaver.kt36
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt4
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/converter/ConversionCache.kt23
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/YamlPlayerSettingsStorage.kt21
4 files changed, 45 insertions, 39 deletions
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/DebouncedSaver.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/DebouncedSaver.kt
new file mode 100644
index 0000000..bc31785
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/DebouncedSaver.kt
@@ -0,0 +1,36 @@
+package dev.m1sk9.lunaticChat.paper
+
+import org.bukkit.plugin.java.JavaPlugin
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicBoolean
+
+/**
+ * Coalesces a burst of save requests into one asynchronous write.
+ *
+ * The first [request] after an idle period schedules the write [delaySeconds] later; requests
+ * arriving before it fires are absorbed by it, so a player toggling a setting repeatedly costs one
+ * file write rather than one per toggle.
+ */
+class DebouncedSaver(
+ private val plugin: JavaPlugin,
+ private val delaySeconds: Long = 5,
+) {
+ private val pending = AtomicBoolean(false)
+
+ /**
+ * Schedules [save] to run asynchronously, unless a write is already pending.
+ */
+ fun request(save: () -> Unit) {
+ if (!pending.compareAndSet(false, true)) return
+
+ plugin.server.asyncScheduler.runDelayed(
+ plugin,
+ {
+ pending.set(false)
+ save()
+ },
+ delaySeconds,
+ TimeUnit.SECONDS,
+ )
+ }
+}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt
index 5ed8ed6..6213348 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt
@@ -169,7 +169,7 @@ class ServiceInitializer(
val storage =
YamlPlayerSettingsStorage(
settingsFile = settingsFile,
- plugin = plugin,
+ saver = DebouncedSaver(plugin),
logger = logger,
)
@@ -194,7 +194,7 @@ class ServiceInitializer(
ConversionCache(
cacheFile = plugin.dataFolder.resolve(configuration.features.japaneseConversion.cacheFilePath).toPath(),
maxEntries = configuration.features.japaneseConversion.cacheMaxEntries,
- plugin = plugin,
+ saver = DebouncedSaver(plugin),
logger = logger,
)
cache.loadFromDisk()
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/converter/ConversionCache.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/converter/ConversionCache.kt
index bab4e8d..ad0a9e0 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/converter/ConversionCache.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/converter/ConversionCache.kt
@@ -1,12 +1,10 @@
package dev.m1sk9.lunaticChat.paper.converter
import dev.m1sk9.lunaticChat.engine.converter.CacheData
+import dev.m1sk9.lunaticChat.paper.DebouncedSaver
import kotlinx.serialization.json.Json
-import org.bukkit.plugin.java.JavaPlugin
import java.nio.file.Path
import java.util.concurrent.ConcurrentHashMap
-import java.util.concurrent.TimeUnit
-import java.util.concurrent.atomic.AtomicBoolean
import java.util.logging.Logger
import kotlin.io.path.bufferedReader
import kotlin.io.path.exists
@@ -15,11 +13,10 @@ import kotlin.io.path.writeText
class ConversionCache(
private val cacheFile: Path,
private val maxEntries: Int = 500,
- private val plugin: JavaPlugin,
+ private val saver: DebouncedSaver,
private val logger: Logger,
) {
private val conversionMemoryCache = ConcurrentHashMap<String, String>()
- private val conversionSaveQueue = AtomicBoolean(false)
companion object {
private const val CACHE_VERSION = "1"
@@ -84,7 +81,7 @@ class ConversionCache(
}
conversionMemoryCache[key] = value
- queueSaveToDisk()
+ saver.request(::saveToDisk)
}
/**
@@ -108,20 +105,6 @@ class ConversionCache(
}
}
- private fun queueSaveToDisk() {
- if (conversionSaveQueue.compareAndSet(false, true)) {
- plugin.server.asyncScheduler.runDelayed(
- plugin,
- {
- conversionSaveQueue.set(false)
- saveToDisk()
- },
- 5,
- TimeUnit.SECONDS,
- )
- }
- }
-
// FIXME: ConcurrentHashMap keys are unordered, so evicting "oldest" entries
// actually evicts random entries. Consider using LinkedHashMap with access-order
// or implement proper LRU cache with timestamp tracking.
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/YamlPlayerSettingsStorage.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/YamlPlayerSettingsStorage.kt
index 5513303..7a727b0 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/YamlPlayerSettingsStorage.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/YamlPlayerSettingsStorage.kt
@@ -2,10 +2,8 @@ package dev.m1sk9.lunaticChat.paper.settings
import com.charleskorn.kaml.Yaml
import dev.m1sk9.lunaticChat.engine.settings.PlayerSettingsData
-import org.bukkit.plugin.java.JavaPlugin
+import dev.m1sk9.lunaticChat.paper.DebouncedSaver
import java.nio.file.Path
-import java.util.concurrent.TimeUnit
-import java.util.concurrent.atomic.AtomicBoolean
import java.util.logging.Logger
import kotlin.io.path.bufferedReader
import kotlin.io.path.exists
@@ -16,16 +14,15 @@ import kotlin.io.path.writeText
* Provides async save with debouncing.
*
* @property settingsFile The path to the YAML settings file
- * @property plugin The plugin instance for scheduling async tasks
+ * @property saver Coalesces bursts of save requests into one asynchronous write
* @property logger The logger for logging operations
*/
class YamlPlayerSettingsStorage(
private val settingsFile: Path,
- private val plugin: JavaPlugin,
+ private val saver: DebouncedSaver,
private val logger: Logger,
) {
private val yaml = Yaml.default
- private val saveFlag = AtomicBoolean(false)
/**
* Loads player settings from the YAML file.
@@ -72,16 +69,6 @@ class YamlPlayerSettingsStorage(
* @param data The settings data to save
*/
fun queueAsyncSave(data: PlayerSettingsData) {
- if (saveFlag.compareAndSet(false, true)) {
- plugin.server.asyncScheduler.runDelayed(
- plugin,
- {
- saveFlag.set(false)
- saveToDisk(data)
- },
- 5,
- TimeUnit.SECONDS,
- )
- }
+ saver.request { saveToDisk(data) }
}
}