util_settings.kt
src/jvmMain/kotlin/afield/app/settings/util_settings.kt · 224 lines
package afield.app.settings
import afield.data.table.div
import afield.data.table.get
import afield.data.table.minus
import afield.data.table.plus
import afield.data.table.rem
import afield.data.table.times
import afield.runtime.execution.plus
import afield.runtime.lifecycle.plus
import afield.runtime.lifecycle.rem
import afield.ui.notation.with
import afield.audio.synth.*
import afield.document.graph.HasProperties
import afield.graphics.vulkan.*
import afield.media.stream.*
import afield.media.video.*
import afield.app.settings.PersistOnExit.needingPersistance
import afield.document.persistence.SavingContext.Companion.format
import afield.app.diagnostics.logging.lprintln
import afield.data.collections.invoke
import afield.math.linalg.invoke
import java.io.File
import java.lang.reflect.*
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference
import kotlin.concurrent.thread
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
Property delegates that keep a single String, Int or Double value across Field restarts,
one small file per property name under .field/ in the working directory.
Declare a property by PersistentString { "default" } (or PersistentInt, PersistentDouble).
The first read loads .field/<propertyName> through PersistOnExit.load and falls back to the
default when the file is missing; a write records the value in
PersistOnExit.needingPersistance. A daemon thread writes pending values about once a second,
and a shutdown hook writes whatever is left on a graceful exit.
The file is named by the Kotlin property name alone, so two persistent properties with the same name share one file.
class PersistentString(val defaultValue: (() -> String?)
class PersistentString(val defaultValue: (() -> String?) = { null }) {
var first = true
var value: String? = null
operator fun getValue(f: Any?, property: KProperty<*>): String?
operator fun getValue(f: Any?, property: KProperty<*>): String? {
if (first) {
first = false
value = PersistOnExit.load<String>(property.name)
if (value == null) {
value = defaultValue.invoke()
}
}
return value
}
operator fun setValue(f: Any?, property: KProperty<*>, v: String?)
operator fun setValue(f: Any?, property: KProperty<*>, v: String?) {
value = v as String
needingPersistance[property.name] = v
}
}
class PersistentInt(val defaultValue: (() -> Int?)
class PersistentInt(val defaultValue: (() -> Int?) = { null }) {
var first = true
var value: Int? = null
operator fun getValue(f: HasProperties, property: KProperty<*>): Int?
operator fun getValue(f: HasProperties, property: KProperty<*>): Int? {
if (first) {
first = false
value = PersistOnExit.load<Int>(property.name)
if (value == null) {
value = defaultValue.invoke()
}
}
return value!!
}
operator fun setValue(f: HasProperties, property: KProperty<*>, v: Int?)
operator fun setValue(f: HasProperties, property: KProperty<*>, v: Int?) {
value = v as Int
needingPersistance[property.name] = v
}
}
class PersistentDouble(val defaultValue: (() -> Double)?
class PersistentDouble(val defaultValue: (() -> Double)? = { 0.0 }) {
var first = true
var value: Double? = null
operator fun getValue(f: HasProperties, property: KProperty<*>): Double
operator fun getValue(f: HasProperties, property: KProperty<*>): Double {
if (first) {
first = false
value = PersistOnExit.load<Double>(property.name)
if (value == null) {
value = defaultValue?.invoke()
}
}
return value!!
}
operator fun setValue(f: HasProperties, property: KProperty<*>, v: Double)
operator fun setValue(f: HasProperties, property: KProperty<*>, v: Double) {
value = v
needingPersistance[property.name] = v
}
}
object PersistOnExit
object PersistOnExit {
var needingPersistance = ConcurrentHashMap<String, Any>()
private fun drainPending() {
needingPersistance.entries.removeIf {
save(it.key, it.value)
true
}
}
var checkThread
Daemon: this is a best-effort background flusher and must never be the reason a JVM stays alive (otherwise every jvm-main smoke test that touches a persisted property would hang until the launcher force-exits). A shutdown hook flushes any last-second changes on graceful exit so we keep the "persist on exit" guarantee.
var checkThread = thread(isDaemon = true) {
while (true) {
Thread.sleep(1000)
if (needingPersistance.size > 0) lprintln("info,persist", "persistence number is ${needingPersistance.size}")
drainPending()
}
}
init
init {
Runtime.getRuntime().addShutdownHook(Thread({ drainPending() }, "afield-persist-flush"))
}
fun save(name: String, t: Any?)
fun save(name: String, t: Any?) {
if (t == null) return
try {
val f = System.getProperty("user.dir") + "/.field"
File(f).mkdirs()
val p = f + "/$name"
var text: String? = null
when (t::class) {
String::class -> {
text = format.encodeToString<String>(t as String)
}
Int::class -> {
text = format.encodeToString<Int>(t as Int)
}
Double::class -> {
text = format.encodeToString<Double>(t as Double)
}
Float::class -> {
text = format.encodeToString<Float>(t as Float)
}
else -> {
throw IllegalArgumentException("unknown type ${t::class}")
}
}
lprintln("info,persist", "saving $name to $text with path $p")
File(p).writeText(text)
} catch (e: Exception) {
lprintln("error,persist", "problem saving $name — ${e.message}")
}
}
inline fun <reified T> load
inline fun <reified T> load(
name: String,
storageDirectory: File = File(System.getProperty("user.dir"), ".field"),
): T? {
try {
val source = File(storageDirectory, name)
if (!source.exists()) return null
val text = source.readText()
when (T::class) {
String::class -> {
return format.decodeFromString<String>(text) as T
}
Int::class -> {
return format.decodeFromString<Int>(text) as T
}
Double::class -> {
return format.decodeFromString<Double>(text) as T
}
Float::class -> {
return format.decodeFromString<Float>(text) as T
}
else -> {
throw IllegalArgumentException("unknown type ${T::class}")
}
}
} catch (e: Exception) {
lprintln("error,persist", "problem loading $name — ${e.message}")
return null
}
}
}