provisional.kt
src/jvmMain/kotlin/afield/document/workspace/provisional.kt · 459 lines
package and imports — 90 lines, from line 1
package afield.document.workspace
import afield.app.protocol.handlers
import afield.app.protocol.send
import afield.data.table.div
import afield.data.table.minus
import afield.data.table.plus
import afield.data.table.rem
import afield.data.table.times
import afield.ui.notation.with
import afield.app.protocol.Message
import afield.app.protocol.ProvisionalEntry
import afield.app.protocol.ProvisionalSet
import afield.app.protocol._send
import afield.app.protocol.computeIfAbsent
import afield.app.protocol.payload
import afield.app.server.thisSessionInfo
import afield.data.table.get
import afield.document.graph.Box
import afield.document.graph.box
import afield.document.graph.doResync
import afield.graphics.vulkan.*
import afield.media.stream.*
import afield.runtime.lifecycle.Runloop
import afield.runtime.lifecycle.map
import afield.tools.automation.FlightRecorder
import afield.ui.output.markdown
import afield.ui.output.templates.filter
import afield.app.diagnostics.logging.lprintln
import afield.ai.learning.*
import afield.ai.retrieval.*
import afield.animation.interpolation.*
import afield.animation.keyframes.*
import afield.animation.motion.*
import afield.app.commands.*
import afield.app.diagnostics.*
import afield.audio.analysis.*
import afield.audio.synth.*
import afield.data.collections.*
import afield.data.formats.*
import afield.data.graph.*
import afield.data.sorting.*
import afield.document.context.*
import afield.document.graph.*
import afield.document.properties.*
import afield.document.workspace.*
import afield.geometry.mesh.*
import afield.geometry.path.*
import afield.geometry.spatial.*
import afield.graphics.color.*
import afield.graphics.interaction.*
import afield.graphics.render.*
import afield.graphics.shader.*
import afield.graphics.text.*
import afield.graphics.vulkan.buffers.*
import afield.io.osc.*
import afield.io.process.*
import afield.lang.lambda.*
import afield.lang.python.*
import afield.lang.remotejs.*
import afield.lang.stacksl.*
import afield.math.autodiff.*
import afield.math.control.*
import afield.math.estimation.*
import afield.math.functions.*
import afield.math.linalg.*
import afield.math.optimization.*
import afield.math.statistics.*
import afield.math.units.*
import afield.media.image.*
import afield.media.video.*
import afield.runtime.execution.*
import afield.runtime.languages.*
import afield.runtime.lifecycle.*
import afield.simulation.agents.*
import afield.simulation.automata.*
import afield.simulation.particles.*
import afield.study.build.*
import afield.study.evidence.*
import afield.study.search.*
import afield.study.source.*
import afield.study.ui.*
import afield.ui.controls.*
import afield.ui.output.*
import afield.ui.visualization.*
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.util.concurrent.ConcurrentHashMap
Provisional edits: proposed text for a box property, typically from AI4, held per document until someone
accepts or rejects it. Entries are kept in memory, keyed by document, box uid and property, and every
change to the entries sends the document's whole ProvisionalSet to its browsers as set-provisional.
acceptProvisional writes the proposed text into the box, records the acceptance (with any
AiProvisionalArchive markdown attached by setProvisionalArchive) in the flight recorder, and drops the
entry; rejectProvisional only drops it. acceptAllProvisional and rejectAllProvisional do the same for
a whole document. When the decided property is a shader source (GLSL, Slang or stackSL) and the box already has the
matching live shader module, that module is rebuilt afterwards, once per box and shader kind, on the
runloop; rejecting reloads it too.
private val provisionalByDocument =
ConcurrentHashMap<String, ConcurrentHashMap<String, ConcurrentHashMap<String, String>>>()
private val provisionalArchiveByDocument =
ConcurrentHashMap<String, ConcurrentHashMap<String, ConcurrentHashMap<String, AiProvisionalArchive>>>()
data class AiProvisionalArchive
Markdown kept beside a provisional entry and recorded with it when the entry is accepted.
body — 67 lines, from line 112
data class AiProvisionalArchive(
/**
* The reasoning to keep with the acceptance, usually the conversation that produced the edit. Blank
* markdown is not stored at all, so an archive has to carry something to survive.
*/
val markdown: String,
/**
* Who proposed the edit. Both archives built in the tree leave it at `AI4`, and an acceptance is
* recorded from [markdown] alone, so nothing reads this yet.
*/
val source: String = "AI4"
)
private val shaderProvisionalProperties = setOf(
"vertex",
"fragment",
"stackSL",
"vertex_fragment",
"mesh",
"task",
"compute",
"raygen",
"miss",
"closesthit",
"anyhit",
"intersection",
"callable",
"vertex_s",
"fragment_s",
"mesh_s",
"task_s",
"compute_s",
"raygen_s",
"miss_s",
"closesthit_s",
"anyhit_s",
"intersection_s",
"callable_s"
)
private val graphicsShaderProperties = setOf("vertex", "fragment", "vertex_fragment", "vertex_s", "fragment_s")
private val computeShaderProperties = setOf("compute", "compute_s")
private val meshShaderProperties = setOf("task", "mesh", "fragment", "task_s", "mesh_s", "fragment_s")
private val rayShaderProperties = setOf(
"raygen",
"miss",
"closesthit",
"anyhit",
"intersection",
"callable",
"raygen_s",
"miss_s",
"closesthit_s",
"anyhit_s",
"intersection_s",
"callable_s"
)
private fun currentDocument(): String? =
try {
thisSessionInfo.get()?.filename
} catch (_: Exception) {
null
}
private fun documentKeyFor(box: Box): String? =
if (box.isInsideInitialized()) box.inside.filename else currentDocument()
Rebuilds the live shader modules whose source a provisional decision changed, one reload per box and
shader kind however many of its properties were decided (accept-all on a box with both vertex and
fragment pending compiles it once, against both new stages).
Launched on the runloop and not awaited. The callers are the provisional-accept / -reject /
-accept-all / -reject-all handlers, which run on the Netty event loop serving that browser's /ws
socket; waiting there for the runloop to come free and then for a GLSL/Slang compile plus pipeline
rebuild stalled every message on the socket, keepalive pongs included, until clients dropped it
("1011 keepalive ping timeout"). Nothing after the call needs the reload: the handlers send no reply,
the property is written and the provisional set sent before this runs, and a reload reports its own
errors onto the shader property. The runloop runs launches in order, so a later decision or alt-r
on the same box still follows this one.
source — 41 lines, from line 194
@OptIn(DelicateCoroutinesApi::class)
private fun reloadShaderRuntimeForProvisionalProperties(decided: List<Pair<Box, String>>) {
val propertiesByBox = decided
.filter { (_, property) -> property in shaderProvisionalProperties }
.groupBy({ it.first }, { it.second })
if (propertiesByBox.isEmpty()) return
GlobalScope.launch(Runloop.runLoop.main) {
propertiesByBox.forEach { (target, properties) -> reloadShaderRuntime(target, properties.toSet()) }
}
}
private fun reloadShaderRuntime(target: Box, properties: Set<String>) {
fun changed(kind: Set<String>) = properties.any { it in kind }
runCatching {
if (changed(graphicsShaderProperties) && runCatching { target._shader != null }.getOrDefault(false)) {
reloadShader(target)
}
if (changed(computeShaderProperties) && runCatching { target._computeShader != null }.getOrDefault(false)) {
reloadComputeShader(target)
}
if (changed(meshShaderProperties) && runCatching { target._meshShader != null }.getOrDefault(false)) {
reloadMeshShader(target)
}
if (changed(rayShaderProperties) && runCatching { target._rayTracingShader != null }.getOrDefault(false)) {
reloadRayTracingShader(target)
}
}.onFailure { throwable ->
lprintln(
"warning,ai4",
"Unable to refresh shader runtime after provisional decision for ${target.id}/${properties.joinToString("+")}: ${
throwable.message ?: throwable::class.java.name
}"
)
}
}
private fun documentMap(document: String): ConcurrentHashMap<String, ConcurrentHashMap<String, String>> =
provisionalByDocument.computeIfAbsent(document) { ConcurrentHashMap() }
private fun propertyMap(document: String, uid: String): ConcurrentHashMap<String, String> =
documentMap(document).computeIfAbsent(uid) { ConcurrentHashMap() }
fun markProvisional(box: Box, property: String, contents: String? = null)
Proposes contents for one of a box's properties, against the document the box belongs to. With no
contents it proposes the property's current text, which makes accepting a no-op on the box and leaves the
proposal as a thing to approve rather than a change to apply. Does nothing when the box belongs to no
document, and when the property is absent or does not hold text.
fun markProvisional(box: Box, property: String, contents: String? = null) {
val document = documentKeyFor(box) ?: return
val value = contents ?: (box.getPropertyRaw(property) as? String) ?: return
setProvisional(document, box.id, property, value)
}
fun setProvisional(document: String, uid: String, property: String, contents: String)
Proposes contents for a box property by document path and box uid, replacing any proposal already held
for that property, and sends the document's whole set to its browsers. Neither the box nor the property
has to exist yet; a proposal for a box the session does not have is dropped when it is decided.
fun setProvisional(document: String, uid: String, property: String, contents: String) {
val props = propertyMap(document, uid)
props[property] = contents
sendProvisionalSet(document)
}
fun setProvisionalArchive(document: String, uid: String, property: String, archive: AiProvisionalArchive)
Attaches the reasoning behind a proposal to one box property, to be recorded if that property is accepted. An archive with blank markdown is ignored. Archives are held apart from the proposals, and one is dropped when its own proposal is decided, so an archive attached to a property with no proposal stays until the document's proposals are cleared as a whole.
fun setProvisionalArchive(document: String, uid: String, property: String, archive: AiProvisionalArchive) {
if (archive.markdown.isBlank()) return
val props = provisionalArchiveByDocument
.computeIfAbsent(document) { ConcurrentHashMap() }
.computeIfAbsent(uid) { ConcurrentHashMap() }
props[property] = archive
}
fun setProvisionalArchive(document: String, entries: List<ProvisionalEntry>, archive: AiProvisionalArchive)
Attaches one archive to every property in entries, which is how a single conversation covers the several
boxes it edited. Each entry gets the same markdown, so accepting any one of them records the whole of it.
fun setProvisionalArchive(document: String, entries: List<ProvisionalEntry>, archive: AiProvisionalArchive) {
entries.forEach { entry ->
setProvisionalArchive(document, entry.uid, entry.property, archive)
}
}
private fun provisionalArchive(document: String, uid: String, property: String): AiProvisionalArchive? =
provisionalArchiveByDocument[document]?.get(uid)?.get(property)
private fun removeProvisionalArchive(document: String, uid: String, property: String) {
val docMap = provisionalArchiveByDocument[document] ?: return
val props = docMap[uid] ?: return
props.remove(property)
if (props.isEmpty()) docMap.remove(uid)
if (docMap.isEmpty()) provisionalArchiveByDocument.remove(document)
}
fun clearProvisional(box: Box, property: String): Boolean
Drops the proposal for one of a box's properties without applying it, and reports whether there was one. False for a box that belongs to no document. This is removeProvisional addressed by box; note that the other function of this name takes a document path and drops every proposal in it.
fun clearProvisional(box: Box, property: String): Boolean {
val document = documentKeyFor(box) ?: return false
return removeProvisional(document, box.id, property)
}
fun removeProvisional(document: String, uid: String, property: String): Boolean
Drops one proposal and its archive without applying it, reporting whether there was one to drop. The document's set is sent to its browsers on every path through this, including the ones that found nothing, so a browser showing a stale proposal is corrected either way.
fun removeProvisional(document: String, uid: String, property: String): Boolean {
val docMap = provisionalByDocument[document] ?: run {
sendProvisionalSet(document)
return false
}
val props = docMap[uid] ?: run {
sendProvisionalSet(document)
return false
}
val removed = props.remove(property) != null
if (props.isEmpty()) docMap.remove(uid)
if (docMap.isEmpty()) provisionalByDocument.remove(document)
removeProvisionalArchive(document, uid, property)
sendProvisionalSet(document)
return removed
}
fun clearProvisional(document: String)
Drops every proposal and archive held for a document, applying none of them, and tells its browsers. Takes a document path, unlike the overload taking a box and a property, which drops exactly one.
fun clearProvisional(document: String) {
provisionalByDocument.remove(document)
provisionalArchiveByDocument.remove(document)
sendProvisionalSet(document)
}
fun provisionalSet(document: String): ProvisionalSet
A document's proposals as the wire type the frontend receives, which is provisionalEntries in an envelope.
fun provisionalSet(document: String): ProvisionalSet =
ProvisionalSet(provisionalEntries(document))
fun provisionalEntries(document: String): List<ProvisionalEntry>
Every proposal held for a document, each naming a box uid, a property and the proposed text. Empty for a document with none. The order follows the concurrent maps these are held in, so treat it as arbitrary.
fun provisionalEntries(document: String): List<ProvisionalEntry> {
val docMap = provisionalByDocument[document] ?: return emptyList()
return docMap.flatMap { (uid, props) ->
props.map { (property, contents) ->
ProvisionalEntry(uid = uid, property = property, contents = contents)
}
}
}
fun acceptProvisional(document: String, uid: String, property: String): Boolean
Writes one proposal into its box, records the acceptance with any archive attached to it, and drops the proposal. Reports false and changes nothing when there is no such proposal; when the box is not in the current session it also drops the proposal, since nothing could accept it later. A decided shader property has its live module rebuilt afterwards.
fun acceptProvisional(document: String, uid: String, property: String): Boolean {
val docMap = provisionalByDocument[document] ?: return false
val props = docMap[uid] ?: return false
val contents = props[property] ?: return false
val target = thisSessionInfo.get().boxes[uid] ?: run {
removeProvisional(document, uid, property)
return false
}
target.setPropertyRaw(property, contents, doResync = true)
FlightRecorder.recordAiProvisionalAccepted(
box = target,
property = property,
acceptedValue = contents,
archiveMarkdown = provisionalArchive(document, uid, property)?.markdown
)
removeProvisional(document, uid, property)
reloadShaderRuntimeForProvisionalProperties(listOf(target to property))
return true
}
fun recordAcceptedProvisionalValue(document: String, uid: String, property: String, acceptedValue: String): Boolean
Records an acceptance of acceptedValue, with any archive attached to the property, and does nothing
else: the proposal stays and the box is not written. The editor sends this when someone takes part of a
proposal hunk by hunk, so what was accepted is the editor's text rather than the proposal verbatim.
Reports false when the proposal or the box is gone.
fun recordAcceptedProvisionalValue(document: String, uid: String, property: String, acceptedValue: String): Boolean {
val docMap = provisionalByDocument[document] ?: return false
val props = docMap[uid] ?: return false
if (!props.containsKey(property)) return false
val target = thisSessionInfo.get().boxes[uid] ?: return false
FlightRecorder.recordAiProvisionalAccepted(
box = target,
property = property,
acceptedValue = acceptedValue,
archiveMarkdown = provisionalArchive(document, uid, property)?.markdown
)
return true
}
fun rejectProvisional(document: String, uid: String, property: String): Boolean
Drops one proposal without writing it, and reports whether there was one. A rejected shader property has its live module rebuilt too, so the box goes back to running the source it had before the proposal.
fun rejectProvisional(document: String, uid: String, property: String): Boolean {
val target = thisSessionInfo.get().boxes[uid]
val removed = removeProvisional(document, uid, property)
if (removed && target != null) {
reloadShaderRuntimeForProvisionalProperties(listOf(target to property))
}
return removed
}
fun acceptAllProvisional(document: String)
Writes every proposal a document holds into its box and records each acceptance, then clears the document. A proposal whose box is not in the session is cleared along with the rest without being applied. Each box and shader kind is rebuilt once, however many of its properties were accepted.
fun acceptAllProvisional(document: String) {
val entries = provisionalEntries(document)
if (entries.isEmpty()) {
sendProvisionalSet(document)
return
}
val accepted = entries.mapNotNull { entry ->
val target = thisSessionInfo.get().boxes[entry.uid] ?: return@mapNotNull null
target.setPropertyRaw(entry.property, entry.contents, doResync = true)
FlightRecorder.recordAiProvisionalAccepted(
box = target,
property = entry.property,
acceptedValue = entry.contents,
archiveMarkdown = provisionalArchive(document, entry.uid, entry.property)?.markdown
)
target to entry.property
}
clearProvisional(document)
reloadShaderRuntimeForProvisionalProperties(accepted)
}
fun rejectAllProvisional(document: String)
Drops every proposal a document holds, writing none of them, and rebuilds the live shader modules of the boxes whose shader sources were among them.
fun rejectAllProvisional(document: String) {
val entries = provisionalEntries(document)
clearProvisional(document)
val sessionBoxes = thisSessionInfo.get().boxes
reloadShaderRuntimeForProvisionalProperties(
entries.mapNotNull { entry -> sessionBoxes[entry.uid]?.let { it to entry.property } }
)
}
fun sendProvisionalSet(document: String)
Sends a document's whole proposal set to every browser open on it, as set-provisional. The set is sent
whole rather than as a delta, so this is also how a browser is corrected after any change.
fun sendProvisionalSet(document: String) {
val message = Message("set-provisional").payload(provisionalSet(document))
_send(message, document = Regex.escape(document))
}