Fix scrollback: burst-proof stick-to-bottom, cap-proof layout wait, serialized buffer updates, atomic history saves

This commit is contained in:
weishaupt
2026-10-04 16:29:21 -04:00
parent 6709db3b13
commit 4f717709d2
3 changed files with 140 additions and 86 deletions
@@ -3,6 +3,9 @@ package data
import org.json.JSONArray
import org.json.JSONObject
import java.io.File
import java.nio.file.AtomicMoveNotSupportedException
import java.nio.file.Files
import java.nio.file.StandardCopyOption
/**
* Persists each buffer's scrollback to a JSON file under rootDir/history/, so messages
@@ -25,14 +28,31 @@ class MessageHistoryRepository(private val rootDir: File) {
val arr = JSONArray(file.readText())
(0 until arr.length()).map { i -> fromJson(arr.getJSONObject(i)) }
} catch (e: Exception) {
// Rename the bad file aside rather than silently returning empty - returning
// empty here looks to the caller exactly like "no history yet", and the next
// debounced save would then happily overwrite - and permanently lose - whatever
// was in the corrupt file.
file.renameTo(File(file.parentFile, "${file.name}.corrupt"))
emptyList()
}
}
// @Synchronized because flushAllSaves (on shutdown) calls this directly for every
// buffer while a debounced save for the same key can still be in flight on its own
// coroutine - without this, the two writes race and whichever's atomic move lands
// second wins, which isn't necessarily the one with the more complete message list.
@Synchronized
fun saveMessages(key: BufferKey, messages: List<IrcMessage>) {
val arr = JSONArray()
messages.takeLast(500).forEach { arr.put(toJson(it)) }
fileFor(key).writeText(arr.toString())
val file = fileFor(key)
val tmp = File(file.parentFile, "${file.name}.tmp")
tmp.writeText(arr.toString())
try {
Files.move(tmp.toPath(), file.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE)
} catch (e: AtomicMoveNotSupportedException) {
Files.move(tmp.toPath(), file.toPath(), StandardCopyOption.REPLACE_EXISTING)
}
}
private fun toJson(m: IrcMessage): JSONObject = JSONObject().apply {