summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSho Sakuma <me@m1sk9.dev>2026-08-02 20:06:20 +0900
committerSho Sakuma <me@m1sk9.dev>2026-08-02 20:06:20 +0900
commit16789f7ad3aadac756904ffc8bda0a7c6271a273 (patch)
treeecdbb838840c6adb9e11605f593f8b12dee1108b
parent7bcaaf9a305c2a8608a420c8f05521bd2de089dd (diff)
downloadLunaticChat-16789f7ad3aadac756904ffc8bda0a7c6271a273.tar.gz
LunaticChat-16789f7ad3aadac756904ffc8bda0a7c6271a273.tar.bz2
LunaticChat-16789f7ad3aadac756904ffc8bda0a7c6271a273.zip
fix: let a debounced settings write see changes made while it waits
DebouncedSaver keeps only the first callback of a burst, and queueAsyncSave closed over the snapshot taken when it was called. So if one player toggled a setting and a second toggled two seconds later, the write that fired at five seconds persisted the first snapshot and dropped the second player's change - it survived in memory until some later toggle happened to trigger another write, and was lost on a crash. Passing a supplier instead means the snapshot is taken when the write runs, which is what "batched into a single save" was always meant to mean. ConversionCache already had this shape by passing ::saveToDisk. The staleness predates the refactor, but bc8010b claimed to have closed this window; it only moved where the snapshot was built, not when. Co-Authored-By: Claude <noreply@anthropic.com>
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/PlayerSettingsManager.kt2
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/YamlPlayerSettingsStorage.kt8
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/settings/PlayerSettingsManagerTest.kt23
3 files changed, 29 insertions, 4 deletions
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/PlayerSettingsManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/PlayerSettingsManager.kt
index de35c55..9ee8d5a 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/PlayerSettingsManager.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/PlayerSettingsManager.kt
@@ -61,7 +61,7 @@ class PlayerSettingsManager(
*/
fun updateSettings(settings: PlayerChatSettings) {
this.settings[settings.uuid] = settings
- storage.queueAsyncSave(snapshot())
+ storage.queueAsyncSave(::snapshot)
logger.fine("Updated settings for player ${settings.uuid}")
}
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 7a727b0..f4d8a6f 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
@@ -66,9 +66,11 @@ class YamlPlayerSettingsStorage(
* Queues an async save operation with 5-second debouncing.
* Multiple save requests within 5 seconds are batched into a single save.
*
- * @param data The settings data to save
+ * @param data Supplies the settings to write. It is called when the write runs rather than
+ * when it is queued, so the batched write persists every change made during the delay - not
+ * just the one that started it.
*/
- fun queueAsyncSave(data: PlayerSettingsData) {
- saver.request { saveToDisk(data) }
+ fun queueAsyncSave(data: () -> PlayerSettingsData) {
+ saver.request { saveToDisk(data()) }
}
}
diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/settings/PlayerSettingsManagerTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/settings/PlayerSettingsManagerTest.kt
index bc716c9..b7e1385 100644
--- a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/settings/PlayerSettingsManagerTest.kt
+++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/settings/PlayerSettingsManagerTest.kt
@@ -6,6 +6,7 @@ import dev.m1sk9.lunaticChat.paper.TestUtils
import dev.m1sk9.lunaticChat.paper.TestUtils.createTestUUID
import io.mockk.every
import io.mockk.mockk
+import io.mockk.slot
import io.mockk.verify
import kotlin.test.Test
import kotlin.test.assertEquals
@@ -82,6 +83,28 @@ class PlayerSettingsManagerTest {
}
@Test
+ fun `a queued save writes changes made after it was queued`() {
+ val (manager, storage, _) = createManager()
+ manager.initialize()
+
+ // Only the first request of a debounced burst survives; the write it schedules must still
+ // see every later change, or those changes exist only in memory until the next write.
+ val scheduled = slot<() -> PlayerSettingsData>()
+ every { storage.queueAsyncSave(capture(scheduled)) } returns Unit
+
+ val first = createTestUUID(1)
+ val second = createTestUUID(2)
+ manager.updateSettings(PlayerChatSettings(uuid = first, japaneseConversionEnabled = false))
+ val pendingWrite = scheduled.captured
+ manager.updateSettings(PlayerChatSettings(uuid = second, japaneseConversionEnabled = false))
+
+ val written = pendingWrite()
+
+ assertEquals(false, written.japaneseConversion[first])
+ assertEquals(false, written.japaneseConversion[second])
+ }
+
+ @Test
fun `updateSettings should overwrite existing settings`() {
val playerId = createTestUUID(1)
val data =