Skip to content
Draft
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ plugins {
alias(libs.plugins.detekt)
alias(libs.plugins.kotlinx.kover)
alias(libs.plugins.npm.publish) apply false
id("org.jetbrains.gradle.plugin.idea-ext") version "1.1.9" apply false
}

group = "org.modelix"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ internal class MutableModelTreeJsImpl(

private val model = CompositeModel(trees.map { it.asModel() })
private val changeListeners = trees.map { tree ->
ChangeListener(tree) { change ->
ChangeListener(tree, model) { change ->
changeHandlers.forEach { it(change) }
}.also { tree.addListener(it) }
// TODO missing removeListener call
Expand All @@ -57,11 +57,22 @@ internal class MutableModelTreeJsImpl(
private fun IWritableNode.toJS() = toNodeJs(AutoTransactionsNode(this, model).asLegacyNode())
}

internal class ChangeListener(private val tree: IMutableModelTree, private val changeCallback: (ChangeJS) -> Unit) :
IGenericMutableModelTree.Listener<INodeReference> {
internal class ChangeListener(
private val tree: IMutableModelTree,
private val model: CompositeModel,
private val changeCallback: (ChangeJS) -> Unit
) : IGenericMutableModelTree.Listener<INodeReference> {

fun nodeIdToInode(nodeId: INodeReference): INodeJS {
return toNodeJs(NodeInMutableModel(tree, nodeId).asLegacyNode())
// Use the composite model to resolve nodes from any tree
val node = model.tryResolveNode(nodeId)
if (node == null) {
// Log or handle the case where node cannot be resolved
println("Warning: Could not resolve node $nodeId in composite model")
// Fall back to the old behavior for this tree
return toNodeJs(NodeInMutableModel(tree, nodeId).asLegacyNode())
}
return toNodeJs(AutoTransactionsNode(node, model).asLegacyNode())
}

override fun treeChanged(oldTree: IGenericModelTree<INodeReference>, newTree: IGenericModelTree<INodeReference>) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,4 +201,138 @@ class MutableModelTreeJsTest {
// Assert
assertEquals(1, childrenChanged)
}

@Test
fun compositeModelResolvesNodesFromMultipleTrees() {
// Arrange - Create two separate models
val model1Json = """
{
"root": {
"id": "model1Root",
"children": [
{
"id": "model1Child"
}
]
}
}
""".trimIndent()

val model2Json = """
{
"root": {
"id": "model2Root",
"children": [
{
"id": "model2Child"
}
]
}
}
""".trimIndent()

// Create a composite branch with both models
val branch = loadModelsFromJsonAsBranch(arrayOf(model1Json, model2Json))
val rootNodes = branch.getRootNodes()

// Act & Assert - Verify both models are accessible
assertEquals(2, rootNodes.size, "Should have 2 root nodes")

val model1Child = rootNodes[0].getAllChildren()[0]
val model2Child = rootNodes[1].getAllChildren()[0]

// Act - Get references from each model
val model1ChildRef = model1Child.getReference()
val model2ChildRef = model2Child.getReference()

// Assert - Both nodes should be resolvable from the composite branch
val resolved1 = branch.resolveNode(model1ChildRef)
val resolved2 = branch.resolveNode(model2ChildRef)

assertEquals(model1Child, resolved1, "Should resolve node from first model")
assertEquals(model2Child, resolved2, "Should resolve node from second model")
}

@Test
fun compositeModelChangeListenerResolvesNodesFromAllTrees() {
// Arrange - Create two separate models
val model1Json = """
{
"root": {
"id": "model1Root"
}
}
""".trimIndent()

val model2Json = """
{
"root": {
"id": "model2Root"
}
}
""".trimIndent()

val branch = loadModelsFromJsonAsBranch(arrayOf(model1Json, model2Json))
val rootNodes = branch.getRootNodes()

var changesReceived = 0
var changeNodeFromModel1 = false
var changeNodeFromModel2 = false

branch.addListener { change ->
changesReceived++
when (change) {
is PropertyChanged -> {
// Verify that the node in the change event can be accessed
val changedNode = change.node
// Check which model the changed node belongs to
if (changedNode == rootNodes[0]) {
changeNodeFromModel1 = true
} else if (changedNode == rootNodes[1]) {
changeNodeFromModel2 = true
}
}
else -> {}
}
}

// Act - Make changes to both models
rootNodes[0].setPropertyValue("prop1", "value1")
rootNodes[1].setPropertyValue("prop2", "value2")

// Assert - Both changes should be detected
assertEquals(2, changesReceived, "Should receive 2 change events")
assert(changeNodeFromModel1) { "Should detect change in model 1" }
assert(changeNodeFromModel2) { "Should detect change in model 2" }
}

@Test
fun compositeModelReturnsConsistentNodeWrappersForSameNode() {
// Arrange - Create a model with a child node
val modelJson = """
{
"root": {
"id": "rootNode",
"children": [
{
"id": "childNode"
}
]
}
}
""".trimIndent()

val branch = loadModelsFromJsonAsBranch(arrayOf(modelJson))
val rootNode = branch.rootNode
val childNode = rootNode.getAllChildren()[0]
val childRef = childNode.getReference()

// Act - Resolve the same node reference multiple times
val resolved1 = branch.resolveNode(childRef)
val resolved2 = branch.resolveNode(childRef)

// Assert - Should return the exact same wrapper object (identity check)
// This is critical for Vue.js reactivity cache to work correctly
assertEquals(resolved1, resolved2, "Should return equal nodes")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,17 @@ fun runWithNettyServer(
nettyServer.stop()
}
}

fun runTestApplication(block: suspend ApplicationTestBuilder.() -> Unit) {
val previousDevMode = System.getProperty("io.ktor.development")
System.setProperty("io.ktor.development", "false")
try {
io.ktor.server.testing.testApplication(block)
} finally {
if (previousDevMode == null) {
System.clearProperty("io.ktor.development")
} else {
System.setProperty("io.ktor.development", previousDevMode)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.bodyAsText
import io.ktor.http.HttpStatusCode
import io.ktor.server.testing.ApplicationTestBuilder
import io.ktor.server.testing.testApplication
import io.mockk.clearAllMocks
import io.mockk.coEvery
import io.mockk.every
Expand All @@ -42,6 +41,7 @@ import org.modelix.model.persistent.CPNode
import org.modelix.model.persistent.CPNodeRef
import org.modelix.model.server.handlers.RepositoriesManager
import org.modelix.model.server.installDefaultServerPlugins
import org.modelix.model.server.runTestApplication
import kotlin.test.BeforeTest
import kotlin.test.Test

Expand Down Expand Up @@ -308,7 +308,7 @@ class DiffViewTest {
val v1 = createCLVersion { it }
val v2 = createCLVersion(v1) { it }

private fun runDiffViewTest(block: suspend ApplicationTestBuilder.() -> Unit) = testApplication {
private fun runDiffViewTest(block: suspend ApplicationTestBuilder.() -> Unit) = runTestApplication {
application {
installDefaultServerPlugins()
DiffView(repositoriesManager).init(this)
Expand Down
2 changes: 1 addition & 1 deletion vue-model-api/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { useModelsFromJson } from "./useModelsFromJson";
export { useModelClient } from "./useModelClient";
export { useReplicatedModel } from "./useReplicatedModel";
export { useReplicatedModels, useReplicatedModel } from "./useReplicatedModels";
export { useReadonlyVersion } from "./useReadonlyVersion";
Loading
Loading