diff options
| author | Sho Sakuma <me@m1sk9.dev> | 2026-08-02 20:06:20 +0900 |
|---|---|---|
| committer | Sho Sakuma <me@m1sk9.dev> | 2026-08-02 20:06:20 +0900 |
| commit | 16789f7ad3aadac756904ffc8bda0a7c6271a273 (patch) | |
| tree | ecdbb838840c6adb9e11605f593f8b12dee1108b | |
| parent | 7bcaaf9a305c2a8608a420c8f05521bd2de089dd (diff) | |
| download | LunaticChat-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>
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 = |
