Skip to content

Commit a22bb44

Browse files
authored
Merge pull request #273 from modelix/light-client-for-text-editor
Integration of (light-)model-client with modelix.incremental
2 parents 0b4c823 + 1663afa commit a22bb44

File tree

8 files changed

+751
-47
lines changed

8 files changed

+751
-47
lines changed

light-model-client/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ kotlin {
2828

2929
api(project(":modelql-untyped"))
3030

31+
implementation(libs.modelix.incremental)
3132
implementation(libs.ktor.client.websockets)
3233
implementation(libs.kotlin.stdlib.common)
3334
implementation(libs.kotlin.logging)
@@ -59,6 +60,7 @@ kotlin {
5960
// implementation(project(":model-client"))
6061
implementation(project(":model-server"))
6162
implementation(project(":model-server-lib"))
63+
implementation(libs.modelix.incremental)
6264

6365
implementation(libs.ktor.server.core)
6466
implementation(libs.ktor.server.cors)

light-model-client/src/commonMain/kotlin/org/modelix/client/light/LightModelClient.kt

Lines changed: 86 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import io.ktor.client.engine.HttpClientEngine
55
import io.ktor.client.engine.HttpClientEngineFactory
66
import io.ktor.client.plugins.websocket.WebSockets
77
import kotlinx.coroutines.delay
8+
import org.modelix.incremental.DependencyTracking
9+
import org.modelix.incremental.IStateVariableGroup
10+
import org.modelix.incremental.IStateVariableReference
811
import org.modelix.model.api.ConceptReference
912
import org.modelix.model.api.IBranch
1013
import org.modelix.model.api.IConcept
@@ -60,9 +63,9 @@ class LightModelClient internal constructor(
6063
val autoFilterNonLoadedNodes: Boolean,
6164
val debugName: String = "",
6265
val modelQLClient: ModelQLClient? = null,
63-
) {
66+
) : IStateVariableGroup {
6467

65-
private val nodes: MutableMap<NodeId, NodeData> = HashMap()
68+
private val nodes = NodesMap<NodeData>(this)
6669
private val area = Area()
6770
private var areaListeners: Set<IAreaListener> = emptySet()
6871
private var repositoryId: String? = null
@@ -73,8 +76,7 @@ class LightModelClient internal constructor(
7376
private var temporaryIdsSequence: Long = 0
7477
private var changeSetIdSequence: Int = 0
7578
private val nodesReferencingTemporaryIds = HashSet<NodeId>()
76-
private var writeLevel: Int = 0
77-
private val temporaryNodeAdapters: MutableMap<String, NodeAdapter> = HashMap()
79+
private val temporaryNodeAdapters = NodesMap<NodeAdapter>(this)
7880
private var initialized = false
7981
private var lastUnconfirmedChangeSetId: ChangeSetId? = null
8082
private val unappliedVersions: MutableList<VersionData> = ArrayList()
@@ -91,6 +93,7 @@ class LightModelClient internal constructor(
9193
}
9294
}
9395
transactionManager.afterWrite {
96+
flush()
9497
val changes = object : IAreaChangeList {
9598
override fun visitChanges(visitor: (IAreaChangeEvent) -> Boolean) {}
9699
}
@@ -104,6 +107,10 @@ class LightModelClient internal constructor(
104107
}
105108
}
106109

110+
override fun getGroup(): IStateVariableGroup? {
111+
return null
112+
}
113+
107114
fun dispose() {
108115
connection.disconnect()
109116
}
@@ -154,15 +161,7 @@ class LightModelClient internal constructor(
154161
}
155162

156163
fun <T> runWrite(body: () -> T): T {
157-
return transactionManager.runWrite {
158-
writeLevel++
159-
try {
160-
body()
161-
} finally {
162-
writeLevel--
163-
if (writeLevel == 0) flush()
164-
}
165-
}
164+
return transactionManager.runWrite(body)
166165
}
167166

168167
suspend fun waitForRootNode(timeout: Duration = 30.seconds): INode? {
@@ -202,6 +201,10 @@ class LightModelClient internal constructor(
202201
}
203202
}
204203

204+
fun tryGetParentId(nodeId: NodeId): NodeId? {
205+
return requiresRead { nodes[nodeId]?.parent }
206+
}
207+
205208
fun isInitialized(): Boolean = runRead { initialized }
206209

207210
private fun fullConsistencyCheck() {
@@ -218,10 +221,6 @@ class LightModelClient internal constructor(
218221
// }
219222
}
220223

221-
fun hasTemporaryIds(): Boolean = requiresRead {
222-
temporaryNodeAdapters.isNotEmpty() || nodesReferencingTemporaryIds.isNotEmpty()
223-
}
224-
225224
fun getNode(nodeId: NodeId): NodeAdapter {
226225
return requiresRead {
227226
getNodeData(nodeId) // fail fast if it doesn't exist
@@ -718,6 +717,7 @@ internal interface ITransactionManager {
718717
private class ReadWriteLockTransactionManager : ITransactionManager {
719718
private var writeListener: (() -> Unit)? = null
720719
private val lock = ReadWriteLock()
720+
private var writeLevel = 0
721721
override fun <T> requiresRead(body: () -> T): T {
722722
if (!lock.canRead()) throw IllegalStateException("Not in a read transaction")
723723
return body()
@@ -728,16 +728,18 @@ private class ReadWriteLockTransactionManager : ITransactionManager {
728728
}
729729
override fun <T> runRead(body: () -> T): T = lock.runRead(body)
730730
override fun <T> runWrite(body: () -> T): T {
731-
if (canWrite()) {
732-
return body()
733-
} else {
731+
return lock.runWrite {
732+
writeLevel++
734733
try {
735-
return lock.runWrite(body)
734+
body()
736735
} finally {
737-
try {
738-
writeListener?.invoke()
739-
} catch (ex: Exception) {
740-
mu.KotlinLogging.logger { }.error(ex) { "Exception in write listener" }
736+
writeLevel--
737+
if (writeLevel == 0) {
738+
try {
739+
writeListener?.invoke()
740+
} catch (ex: Exception) {
741+
mu.KotlinLogging.logger { }.error(ex) { "Exception in write listener" }
742+
}
741743
}
742744
}
743745
}
@@ -901,3 +903,62 @@ fun NodeData.asUpdateData(): NodeUpdateData {
901903
fun INode.isLoaded() = isValid
902904
fun <T : INode> Iterable<T>.filterLoaded() = filter { it.isLoaded() }
903905
fun <T : INode> Sequence<T>.filterLoaded() = filter { it.isLoaded() }
906+
907+
data class NodeDataDependency(val client: LightModelClient, val id: NodeId) : IStateVariableReference<NodeData> {
908+
override fun getGroup(): IStateVariableGroup {
909+
return client.tryGetParentId(id)
910+
?.let { NodeDataDependency(client, it) }
911+
?: client
912+
}
913+
914+
override fun read(): NodeData {
915+
return client.getNode(id).getData()
916+
}
917+
}
918+
919+
private class NodesMap<V : Any>(val client: LightModelClient) {
920+
private val map: MutableMap<NodeId, V> = HashMap()
921+
922+
operator fun get(key: NodeId): V? {
923+
DependencyTracking.accessed(NodeDataDependency(client, key))
924+
return map[key]
925+
}
926+
927+
operator fun set(key: NodeId, value: V) {
928+
if (map[key] == value) return
929+
map[key] = value
930+
DependencyTracking.modified(NodeDataDependency(client, key))
931+
}
932+
933+
fun remove(key: NodeId): V? {
934+
if (!map.containsKey(key)) return null
935+
val result = map.remove(key)
936+
DependencyTracking.modified(NodeDataDependency(client, key))
937+
return result
938+
}
939+
940+
fun clear() {
941+
if (map.isEmpty()) return
942+
val removedKeys = map.keys.toList()
943+
map.clear()
944+
for (key in removedKeys) {
945+
DependencyTracking.modified(NodeDataDependency(client, key))
946+
}
947+
}
948+
949+
fun containsKey(key: NodeId): Boolean {
950+
DependencyTracking.accessed(NodeDataDependency(client, key))
951+
return map.containsKey(key)
952+
}
953+
954+
fun getOrPut(key: NodeId, defaultValue: () -> V): V {
955+
DependencyTracking.accessed(NodeDataDependency(client, key))
956+
map[key]?.let { return it }
957+
val createdValue = defaultValue()
958+
map[key] = createdValue
959+
// No modified notification necessary, because only the first access modifies the map, but then there can't be
960+
// any dependency on that key yet.
961+
// DependencyTracking.modified(NodeDataDependency(client, key))
962+
return createdValue
963+
}
964+
}

light-model-client/src/jvmTest/kotlin/org/modelix/client/light/LightModelClientTest.kt

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ import kotlinx.coroutines.delay
2323
import kotlinx.coroutines.launch
2424
import kotlinx.coroutines.withTimeout
2525
import org.modelix.authorization.installAuthentication
26+
import org.modelix.incremental.IncrementalEngine
27+
import org.modelix.incremental.incrementalFunction
28+
import org.modelix.model.api.IProperty
2629
import org.modelix.model.api.addNewChild
2730
import org.modelix.model.api.getDescendants
2831
import org.modelix.model.server.handlers.DeprecatedLightModelServer
@@ -116,6 +119,62 @@ class LightModelClientTest {
116119
assertEquals("xyz", client1.runRead { child1.getPropertyValue("name") })
117120
}
118121

122+
@Test
123+
fun incrementalComputationTest() = runClientTest { createClient ->
124+
val client1 = createClient("1")
125+
val client2 = createClient("2")
126+
127+
val rootNode1 = client1.runRead { client1.getRootNode()!! }
128+
val rootNode2 = client2.runRead { client2.getRootNode()!! }
129+
assertEquals(0, client2.runRead { rootNode2.getChildren("role1").toList().size })
130+
val child1 = client1.runWrite { rootNode1.addNewChild("role1") }
131+
assertEquals(1, client1.runRead { rootNode1.getChildren("role1").toList().size })
132+
wait {
133+
client1.checkException()
134+
client2.checkException()
135+
client2.runRead { rootNode2.getChildren("role1").toList().size == 1 }
136+
}
137+
assertEquals(1, client2.runRead { rootNode2.getChildren("role1").toList().size })
138+
139+
client1.runWrite { rootNode1.setPropertyValue(IProperty.fromName("name"), "abc") }
140+
val engine = IncrementalEngine()
141+
try {
142+
var callCount1 = 0
143+
var callCount2 = 0
144+
val nameWithSuffix1 = engine.incrementalFunction("nameWithSuffix1") { _ ->
145+
callCount1++
146+
val name = rootNode1.getPropertyValue(IProperty.fromName("name"))
147+
name + "Suffix1"
148+
}
149+
val nameWithSuffix2 = engine.incrementalFunction("nameWithSuffix2") { _ ->
150+
callCount2++
151+
val name = rootNode2.getPropertyValue(IProperty.fromName("name"))
152+
name + "Suffix2"
153+
}
154+
assertEquals(callCount1, 0)
155+
assertEquals(callCount2, 0)
156+
assertEquals("abcSuffix1", client1.runRead { nameWithSuffix1() })
157+
wait { "abcSuffix2" == client2.runRead { nameWithSuffix2() } }
158+
assertEquals("abcSuffix2", client2.runRead { nameWithSuffix2() })
159+
assertEquals(callCount1, 1)
160+
assertEquals(callCount2, 1)
161+
assertEquals("abcSuffix1", client1.runRead { nameWithSuffix1() })
162+
assertEquals("abcSuffix2", client2.runRead { nameWithSuffix2() })
163+
assertEquals(callCount1, 1)
164+
assertEquals(callCount2, 1)
165+
client1.runWrite { rootNode1.setPropertyValue(IProperty.fromName("name"), "xxx") }
166+
assertEquals(callCount1, 1)
167+
assertEquals(callCount2, 1)
168+
assertEquals("xxxSuffix1", client1.runRead { nameWithSuffix1() })
169+
wait { "xxxSuffix2" == client2.runRead { nameWithSuffix2() } }
170+
assertEquals("xxxSuffix2", client2.runRead { nameWithSuffix2() })
171+
assertEquals(callCount1, 2)
172+
assertEquals(callCount2, 2)
173+
} finally {
174+
engine.dispose()
175+
}
176+
}
177+
119178
@Test
120179
fun random() = runClientTest { createClient ->
121180
val client1 = createClient("1")

model-api-gen-runtime/src/commonMain/kotlin/org/modelix/metamodel/ITypedNode.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ interface ITypedNode {
1515
fun ITypedNode.untyped() = unwrap()
1616
fun ITypedNode.untypedConcept() = _concept.untyped()
1717
fun ITypedNode.typedConcept() = _concept
18-
fun ITypedNode.getPropertyValue(property: IProperty): String? = unwrap().getPropertyValue(property.name)
18+
fun ITypedNode.getPropertyValue(property: IProperty): String? = unwrap().getPropertyValue(property)
1919
fun ITypedNode.instanceOf(concept: ITypedConcept): Boolean {
2020
return instanceOf(concept._concept)
2121
}

model-client/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ kotlin {
5050
implementation(project(":modelql-client"))
5151
api(project(":modelql-core"))
5252
implementation(kotlin("stdlib-common"))
53+
implementation(libs.modelix.incremental)
5354
implementation(libs.kotlin.collections.immutable)
5455
implementation(libs.kotlin.coroutines.core)
5556
implementation(libs.kotlin.logging)

0 commit comments

Comments
 (0)