diff --git a/README.md b/README.md index b09983227..772263b3d 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,20 @@ networking.firewall.allowedTCPPorts = [ 12345 ]; The images are automatically published to docker.woost.space once a day from `master`. +# nodejs Stack traces in tests + +TODO: Why is this not working? + +```bash +npm install source-map-support +``` + +then run tests using + +``` +SOURCEMAPS=true start nsbt +``` + # Deployment Requirements: * docker diff --git a/build.sbt b/build.sbt index eae3b4e60..23d86d71b 100644 --- a/build.sbt +++ b/build.sbt @@ -160,6 +160,9 @@ lazy val webSettings = Seq( scalaJSStage in Test := FastOptStage, scalaJSLinkerConfig in (Compile, fastOptJS) ~= { _.withSourceMap(withSourceMaps) }, scalaJSLinkerConfig in (Compile, fullOptJS) ~= { _.withSourceMap(withSourceMaps) }, + scalaJSLinkerConfig in (Test, fastOptJS) ~= { _.withSourceMap(withSourceMaps) }, + scalaJSLinkerConfig in (Test, fullOptJS) ~= { _.withSourceMap(withSourceMaps) }, + npmDevDependencies in Compile ++= Deps.npm.webpackDependencies, version in webpack := Deps.webpackVersion, version in startWebpackDevServer := Deps.webpackDevServerVersion, diff --git a/webApp/src/main/scala/wust/webApp/state/GlobalState.scala b/webApp/src/main/scala/wust/webApp/state/GlobalState.scala index b5b20f1f7..7e4a8d1d7 100644 --- a/webApp/src/main/scala/wust/webApp/state/GlobalState.scala +++ b/webApp/src/main/scala/wust/webApp/state/GlobalState.scala @@ -2,6 +2,7 @@ package wust.webApp.state import scala.util.Try import com.github.ghik.silencer.silent +import graphstate.GraphState import acyclic.file import monix.reactive.Observable import monix.reactive.subjects.PublishSubject @@ -125,8 +126,7 @@ object GlobalState { val g = graph.addNodes( // these nodes are obviously not in the graph for an assumed user, since the user is not persisted yet. // if we start with an assumed user and just create new channels we will never get a graph from the backend. - user().toNode :: - Nil + user().toNode :: Nil ) scribe.debug(" graph with added usernode: " + g) g @@ -136,6 +136,13 @@ object GlobalState { } } + val graphState = Var(new GraphState(Graph.empty)) + eventProcessor.graphEvents.foreach { + case LocalGraphUpdateEvent.NewChanges(changes) => graphState.now.update(changes) + case LocalGraphUpdateEvent.NewGraph(graph) => graphState() = new GraphState(graph) + } + + val viewConfig: Rx[ViewConfig] = { var lastViewConfig: ViewConfig = ViewConfig(View.Empty, Page.empty) diff --git a/webApp/src/main/scala/wust/webApp/state/graphstate/EdgeState.scala b/webApp/src/main/scala/wust/webApp/state/graphstate/EdgeState.scala new file mode 100644 index 000000000..8d3859971 --- /dev/null +++ b/webApp/src/main/scala/wust/webApp/state/graphstate/EdgeState.scala @@ -0,0 +1,78 @@ +package wust.webApp.state.graphstate + +import acyclic.file +import rx._ +import flatland._ +import wust.ids._ +import wust.util.algorithm._ +import wust.util.collection._ +import wust.util.macros.InlineList +import wust.graph._ +import wust.util.time.time + +import scala.collection.{ breakOut, immutable, mutable } + +object EdgeState { + @inline def edgeKey(edge: Edge): (NodeId, NodeId) = edge.sourceId -> edge.targetId + + def apply(nodeState: NodeState, edges: Array[Edge]): EdgeState = { + val edgeState = new EdgeState(nodeState) + edgeState.update(GraphChanges(addEdges = edges)) + edgeState + } +} + +final class EdgeState(nodeState: NodeState) { + + val edgesNow: mutable.ArrayBuffer[Edge] = mutable.ArrayBuffer.empty + val idToIdxHashMap: mutable.HashMap[(NodeId, NodeId), Int] = mutable.HashMap.empty + val edgesRx = new LazyReactiveCollection[Edge](getCurrent = idx => edgesNow(idx)) + var edgesIdxNow: InterleavedArrayInt = InterleavedArrayInt.empty + + @inline def idToIdxFold[T](endPoints: (NodeId, NodeId))(default: => T)(f: Int => T): T = { + idToIdxHashMap.get(endPoints) match { + case Some(idx) => f(idx) + case None => default + } + } + @inline def idToIdxForeach[U](endPoints: (NodeId, NodeId))(f: Int => U): Unit = idToIdxFold(endPoints)(())(f(_)) + @inline def idToIdxMap[T](endPoints: (NodeId, NodeId))(f: Int => T): Option[T] = idToIdxFold(endPoints)(Option.empty[T])(idx => Some(f(idx))) + @inline def idToIdxOrThrow(endPoints: (NodeId, NodeId)): Int = idToIdxHashMap(endPoints) + def idToIdx(endPoints: (NodeId, NodeId)): Option[Int] = idToIdxFold[Option[Int]](endPoints)(None)(Some(_)) + + def update(changes: GraphChanges): Unit = { + // register new and updated edges + + edgesNow.sizeHint(edgesNow.length + changes.addEdges.length) + edgesRx.sizeHint(edgesRx.length + changes.addEdges.length) + idToIdxHashMap.sizeHint(idToIdxHashMap.size + changes.addEdges.length) + + val addEdgeIdxBuilder = InterleavedArrayInt.builder + changes.addEdges.foreachElement { edge => + val key = EdgeState.edgeKey(edge) + + idToIdxFold(key){ + val newIdx = edgesNow.length + edgesNow += edge + edgesRx.grow() + idToIdxHashMap(key) = newIdx + nodeState.idToIdxForeach(edge.sourceId){ sourceIdx => + nodeState.idToIdxForeach(edge.targetId){ targetIdx => + addEdgeIdxBuilder.add(sourceIdx, targetIdx) + } + } + }{ idx => + edgesNow(idx) = edge + edgesRx(idx)() = edge + } + } + + assert(edgesNow.length == idToIdxHashMap.size) + + time("graphstate:edgestate:updateInterleaved") { + edgesIdxNow = new InterleavedArrayInt(edgesIdxNow.interleaved ++ addEdgeIdxBuilder.result().interleaved) + } + assert(edgesRx.length == edgesNow.length) + assert(edgesIdxNow.elementCount == edgesNow.length) + } +} diff --git a/webApp/src/main/scala/wust/webApp/state/graphstate/GraphState.scala b/webApp/src/main/scala/wust/webApp/state/graphstate/GraphState.scala new file mode 100644 index 000000000..af5faec1b --- /dev/null +++ b/webApp/src/main/scala/wust/webApp/state/graphstate/GraphState.scala @@ -0,0 +1,49 @@ +package wust.webApp.state.graphstate + +import acyclic.file +import rx._ +import flatland._ +import wust.ids._ +import wust.util.algorithm._ +import wust.util.collection._ +import wust.util.macros.InlineList +import wust.graph._ +import wust.util.time.time + +import scala.collection.{ breakOut, immutable, mutable } + +//TODO: idempotence +//TODO: commutativity (e.g. store edges without loaded nodes until the nodes appear) +//TODO: be immune to inconsistency + +final class GraphState(initialGraph: Graph) { + import initialGraph.{ nodes, edges } + val nodeState = new NodeState + val edgeState = new EdgeState(nodeState) + + val children = new LayerState(edgeState, edgeDistributors.ifMyEdgeChild) + val read = new LayerState(edgeState, edgeDistributors.ifMyEdgeRead) + + update(GraphChanges(addNodes = initialGraph.nodes, addEdges = initialGraph.edges)) + + def update(changes: GraphChanges) = { + time("graphstate") { + val layerChanges = time("graphstate:nodestate") {nodeState.update(changes)} + time("graphstate:edgestate") {edgeState.update(changes)} + + children.update(layerChanges) + read.update(layerChanges) + } + } +} + +object edgeDistributors { + def ifMyEdgeChild(code: (NodeId, NodeId) => Unit): Edge => Unit = { + case edge: Edge.Child => code(edge.parentId, edge.childId) + case _ => + } + def ifMyEdgeRead(code: (NodeId, NodeId) => Unit): Edge => Unit = { + case edge: Edge.Read => code(edge.nodeId, edge.userId) + case _ => + } +} diff --git a/webApp/src/main/scala/wust/webApp/state/graphstate/LayerState.scala b/webApp/src/main/scala/wust/webApp/state/graphstate/LayerState.scala new file mode 100644 index 000000000..3187f4f0c --- /dev/null +++ b/webApp/src/main/scala/wust/webApp/state/graphstate/LayerState.scala @@ -0,0 +1,133 @@ +package wust.webApp.state.graphstate + +import scala.reflect.ClassTag +import scala.scalajs.js.JSConverters._ +import acyclic.file +import rx._ +import flatland._ +import wust.ids._ +import wust.util.algorithm._ +import wust.util.collection._ +import wust.util.macros.InlineList +import wust.graph._ +import wust.util.time.time +import scala.scalajs.js + +import scala.collection.{ breakOut, immutable, mutable } +import scala.scalajs.js.WrappedArray + +final class InterleavedJSArrayIntBuilder { + @inline private def extractHi(l: Long): Int = (l >> 32).toInt + @inline private def extractLo(l: Long): Int = l.toInt + @inline private def combineToLong(a: Int, b: Int): Long = ((a.toLong) << 32) | (b & 0xffffffffL) + + val self = new js.Array[Long] + + @inline def add(a: Int, b: Int): Unit = { + self += combineToLong(a, b) + } + + @inline def result() = self +} + +final class LayerState(val edgeState: EdgeState, ifMyEdge: ((NodeId, NodeId) => Unit) => (Edge => Unit)) { + import edgeState.edgesIdxNow + + var edgeLookupNow: NestedArrayIntValues = NestedArrayInt.empty + var edgeRevLookupNow: NestedArrayIntValues = NestedArrayInt.empty + val edgeLookupRx = new LazyReactiveCollection[Array[Int]](getCurrent = idx => edgeLookupNow(idx).toArray) + val edgeRevLookupRx = new LazyReactiveCollection[Array[Int]](getCurrent = idx => edgeRevLookupNow(idx).toArray) + + var lookupNow: NestedArrayIntMapped = edgeLookupNow.viewMapInt(edgesIdxNow.right) + var revLookupNow: NestedArrayIntMapped = edgeRevLookupNow.viewMapInt(edgesIdxNow.left) + def lookupRx(idx: Int)(implicit ctx: Ctx.Owner) = edgeLookupRx(idx).map(_.map(edgesIdxNow.right)) + def revLookupRx(idx: Int)(implicit ctx: Ctx.Owner) = edgeRevLookupRx(idx).map(_.map(edgesIdxNow.left)) + + // @inline def ifMyEdge(code: (NodeId, NodeId) => Unit): PartialFunction[Edge, Unit] + @inline def update(changes: LayerChanges): Unit = { + // time("graphstate:update") { + val affectedSourceNodes = new mutable.ArrayBuilder.ofInt + val affectedTargetNodes = new mutable.ArrayBuilder.ofInt + val addElemBuilder = new InterleavedJSArrayIntBuilder + val addRevElemBuilder = new InterleavedJSArrayIntBuilder + time("graphstate:update:addEdges") { + changes.addEdges.foreachElement { + ifMyEdge { (sourceId, targetId) => + edgeState.idToIdxForeach(sourceId -> targetId) { edgeIdx => + val sourceIdx = edgesIdxNow.left(edgeIdx) + val targetIdx = edgesIdxNow.right(edgeIdx) + addElemBuilder.add(sourceIdx, edgeIdx) + addRevElemBuilder.add(targetIdx, edgeIdx) + affectedSourceNodes += sourceIdx + affectedTargetNodes += targetIdx + } + } + } + } + val addElem = time("graphstate:update:addElem") { + val interleavedLong = addElemBuilder.result() + // use native js Array.sort with compare function for long (faster than scala.util.Sorting.quickSort or java.util.Arrays.sort) + interleavedLong.sort(_ compare _) + new InterleavedArrayInt(interleavedLong.toArray) // TODO: is there a way to avoid copying? Either convert js.Array to scala array or call js.Array.sort on scala Array? + } + val addRevElem = time("graphstate:update:addRevElem") { + val interleavedLong = addRevElemBuilder.result() + // use native js Array.sort with compare function for long (faster than scala.util.Sorting.quickSort or java.util.Arrays.sort) + interleavedLong.sort(_ compare _) + new InterleavedArrayInt(interleavedLong.toArray) // TODO: is there a way to avoid copying? Either convert js.Array to scala array or call js.Array.sort on scala Array? + } + + val delElemBuilder = new mutable.ArrayBuilder.ofRef[(Int, Int)] + val delRevElemBuilder = new mutable.ArrayBuilder.ofRef[(Int, Int)] + time("graphstate:update:delEdges") { + changes.delEdges.foreach { + ifMyEdge { (sourceId, targetId) => + edgeState.idToIdxForeach(sourceId -> targetId) { edgeIdx => + val sourceIdx = edgesIdxNow.left(edgeIdx) + val targetIdx = edgesIdxNow.right(edgeIdx) + delElemBuilder += sourceIdx -> lookupNow.indexOf(sourceIdx)(edgeIdx) // Remove the first occurence of the sourceId/targetId combination + delRevElemBuilder += targetIdx -> lookupNow.indexOf(targetIdx)(edgeIdx) // Remove the first occurence of the sourceId/targetId combination + affectedSourceNodes += sourceIdx + affectedTargetNodes += targetIdx + } + } + } + } + val delElem = time("graphstate:update:delElem") { InterleavedArrayInt(delElemBuilder.result().sortBy(_._1)) } + val delRevElem = time("graphstate:update:delRevElem") { InterleavedArrayInt(delRevElemBuilder.result().sortBy(_._1)) } + + time("graphstate:update:change") { + // NestedArray.changed() parameters: + // addIdx: Int, // how many nodes are added + // addElem: InterleavedArrayInt // Array[idx -> elem] + // delElem: InterleavedArrayInt // Array[idx -> position] + if (scala.scalajs.LinkingInfo.developmentMode) { + edgeLookupNow = edgeLookupNow.changedWithAssertions(changes.addIdx, addElem, delElem) + edgeRevLookupNow = edgeRevLookupNow.changedWithAssertions(changes.addIdx, addRevElem, delRevElem) + } else { + edgeLookupNow = edgeLookupNow.changed(changes.addIdx, addElem, delElem) + edgeRevLookupNow = edgeRevLookupNow.changed(changes.addIdx, addRevElem, delRevElem) + } + } + + time("graphstate:update:update-rx") { + lookupNow = edgeLookupNow.viewMapInt(edgesIdxNow.right) + revLookupNow = edgeRevLookupNow.viewMapInt(edgesIdxNow.left) + + edgeLookupRx.sizeHint(edgeLookupRx.length + changes.addIdx) + edgeRevLookupRx.sizeHint(edgeRevLookupRx.length + changes.addIdx) + loop(changes.addIdx) { _ => + edgeLookupRx.grow() + edgeRevLookupRx.grow() + } + + affectedSourceNodes.result().foreachElement { sourceNodeIdx => + edgeLookupRx.refresh(sourceNodeIdx) + } + affectedTargetNodes.result().foreachElement { targetNodeIdx => + edgeRevLookupRx.refresh(targetNodeIdx) + } + } + // } + } +} diff --git a/webApp/src/main/scala/wust/webApp/state/graphstate/LazyReactiveCollection.scala b/webApp/src/main/scala/wust/webApp/state/graphstate/LazyReactiveCollection.scala new file mode 100644 index 000000000..3a7b496b3 --- /dev/null +++ b/webApp/src/main/scala/wust/webApp/state/graphstate/LazyReactiveCollection.scala @@ -0,0 +1,44 @@ +package wust.webApp.state.graphstate + +import scala.reflect.ClassTag +import scala.scalajs.js.JSConverters._ +import acyclic.file +import rx._ +import flatland._ +import wust.ids._ +import wust.util.algorithm._ +import wust.util.collection._ +import wust.util.macros.InlineList +import wust.graph._ +import wust.util.time.time +import scala.scalajs.js + +import scala.collection.{ breakOut, immutable, mutable } +import scala.scalajs.js.WrappedArray + +@inline final class LazyReactiveCollection[T](getCurrent: Int => T) { + val self: mutable.ArrayBuffer[Var[T]] = mutable.ArrayBuffer.empty + + @inline def length = self.length + + @inline def sizeHint(n:Int) = self.sizeHint(n) + + @inline def grow(): Unit = { self += null } + + @inline def refresh(idx: Int): Unit = { + if (self(idx) != null) { + self(idx)() = getCurrent(idx) + } + } + + @inline def apply(idx: Int): Var[T] = { + if (self(idx) == null) { + val value = Var(getCurrent(idx)) + self(idx) = value + value + } else { + self(idx) + } + } +} + diff --git a/webApp/src/main/scala/wust/webApp/state/graphstate/NodeState.scala b/webApp/src/main/scala/wust/webApp/state/graphstate/NodeState.scala new file mode 100644 index 000000000..7ff4feda5 --- /dev/null +++ b/webApp/src/main/scala/wust/webApp/state/graphstate/NodeState.scala @@ -0,0 +1,77 @@ +package wust.webApp.state.graphstate + +import acyclic.file +import rx._ +import flatland._ +import wust.ids._ +import wust.util.algorithm._ +import wust.util.collection._ +import wust.util.macros.InlineList +import wust.graph._ +import wust.util.time.time + +import scala.collection.{ breakOut, immutable, mutable } + +object NodeState { + def apply(nodes: Array[Node]):NodeState = { + val nodeState = new NodeState + nodeState.update(GraphChanges(addNodes = nodes)) + nodeState + } +} + +final class NodeState { + val nodesNow: mutable.ArrayBuffer[Node] = mutable.ArrayBuffer.empty // faster than js.Array + val idToIdxHashMap: mutable.HashMap[NodeId, Int] = mutable.HashMap.empty + val nodesRx = new LazyReactiveCollection[Node](idx => nodesNow(idx)) + + @inline def idToIdxFold[T](id: NodeId)(default: => T)(f: Int => T): T = { + idToIdxHashMap.get(id) match { + case Some(idx) => f(idx) + case None => default + } + } + @inline def idToIdxForeach[U](id: NodeId)(f: Int => U): Unit = idToIdxFold(id)(())(f(_)) + @inline def idToIdxMap[T](id: NodeId)(f: Int => T): Option[T] = idToIdxFold(id)(Option.empty[T])(idx => Some(f(idx))) + @inline def idToIdxOrThrow(nodeId: NodeId): Int = idToIdxHashMap(nodeId) + def idToIdx(nodeId: NodeId): Option[Int] = idToIdxFold[Option[Int]](nodeId)(None)(Some(_)) + def nodesByIdOrThrow(nodeId: NodeId): Node = nodesNow(idToIdxOrThrow(nodeId)) + def nodesById(nodeId: NodeId): Option[Node] = idToIdxFold[Option[Node]](nodeId)(None)(idx => Some(nodesNow(idx))) + + def update(changes: GraphChanges): LayerChanges = { + // register new and updated nodes + + var addIdx = 0 // counts the number of newly added nodes + + nodesNow.sizeHint(nodesNow.length + changes.addNodes.length) + idToIdxHashMap.sizeHint(idToIdxHashMap.size + changes.addNodes.length) + nodesRx.sizeHint(nodesRx.length + changes.addNodes.length) + + changes.addNodes.foreachElement { node => + val nodeId = node.id + idToIdxFold(nodeId){ + // add new node and update idToIdxHashMap + val newIdx = nodesNow.length + nodesNow += node + nodesRx.grow() + idToIdxHashMap(nodeId) = newIdx + addIdx += 1 + }{ idx => + // already exists, update node + nodesNow(idx) = node + nodesRx.refresh(idx) + } + } + + assert(nodesNow.length == idToIdxHashMap.size) + assert(nodesNow.length == nodesRx.length) + LayerChanges(addIdx, changes.addEdges, changes.delEdges) + } +} + +final case class LayerChanges( + addIdx: Int, + addEdges: Array[Edge] = Array.empty, + delEdges: Array[Edge] = Array.empty +) + diff --git a/webApp/src/test/scala/wust/webApp/state/EdgeStateSpec.scala b/webApp/src/test/scala/wust/webApp/state/EdgeStateSpec.scala new file mode 100644 index 000000000..74961320d --- /dev/null +++ b/webApp/src/test/scala/wust/webApp/state/EdgeStateSpec.scala @@ -0,0 +1,96 @@ +package wust.webApp.state.graphstate + +import org.scalatest._ +import wust.graph._ +import wust.ids._ + +class EdgeStateSpec extends FreeSpec with MustMatchers { + def parent(childId:Cuid, parentId:Cuid) = Edge.Child(ParentId(NodeId(parentId)), ChildId(NodeId(childId))) + def child(parentId:Cuid, childId:Cuid, deletedAt:Option[EpochMilli] = None) = Edge.Child(ParentId(NodeId(parentId)), deletedAt, ChildId(NodeId(childId))) + def user(id:Cuid) = Node.User(UserId(NodeId(id)), NodeData.User(id.toString, false, 0), NodeMeta.User) + implicit def stringToCuid(id:String):Cuid = Cuid.fromBase58String("5Q4is6Gc5NbA7T7W7PvAUw".dropRight(id.length) + id).right.get + implicit def idToNode(id: String): Node = Node.Content(id = NodeId(id:Cuid), data = NodeData.PlainText("content"), role = NodeRole.default, meta = NodeMeta(NodeAccess.ReadWrite)) + def idToNode(id: String, content:String): Node = Node.Content(id = NodeId(id:Cuid), data = NodeData.PlainText(content), role = NodeRole.default, meta = NodeMeta(NodeAccess.ReadWrite)) + + "factory" in { + val nodes = Array[Node]("B", "C", "D", "E", "F") + val nodeState = NodeState(nodes) + val edges = Array[Edge](child("B", "C"), child("B", "D"), child("E", "F"), child("F", "E")) + val edgeState = EdgeState(nodeState, edges) + + assert(edgeState.edgesNow(edgeState.idToIdxOrThrow(NodeId("B":Cuid) -> NodeId("C":Cuid))) == child("B", "C")) + assert(edgeState.edgesNow(edgeState.idToIdxOrThrow(NodeId("B":Cuid) -> NodeId("D":Cuid))) == child("B", "D")) + assert(edgeState.edgesNow(edgeState.idToIdxOrThrow(NodeId("E":Cuid) -> NodeId("F":Cuid))) == child("E", "F")) + assert(edgeState.edgesNow(edgeState.idToIdxOrThrow(NodeId("F":Cuid) -> NodeId("E":Cuid))) == child("F", "E")) + + assert(edgeState.edgesIdxNow.toIndexedSeq(edgeState.idToIdxOrThrow(NodeId("B":Cuid) -> NodeId("C":Cuid))).productIterator.asInstanceOf[Iterator[Int]].map(nodeState.nodesNow).toList == List[Node]("B", "C")) + assert(edgeState.edgesIdxNow.toIndexedSeq(edgeState.idToIdxOrThrow(NodeId("B":Cuid) -> NodeId("D":Cuid))).productIterator.asInstanceOf[Iterator[Int]].map(nodeState.nodesNow).toList == List[Node]("B", "D")) + assert(edgeState.edgesIdxNow.toIndexedSeq(edgeState.idToIdxOrThrow(NodeId("E":Cuid) -> NodeId("F":Cuid))).productIterator.asInstanceOf[Iterator[Int]].map(nodeState.nodesNow).toList == List[Node]("E", "F")) + assert(edgeState.edgesIdxNow.toIndexedSeq(edgeState.idToIdxOrThrow(NodeId("F":Cuid) -> NodeId("E":Cuid))).productIterator.asInstanceOf[Iterator[Int]].map(nodeState.nodesNow).toList == List[Node]("F", "E")) + + assert(edgeState.edgesRx(edgeState.idToIdxOrThrow(NodeId("B":Cuid) -> NodeId("C":Cuid))).now == child("B", "C")) + assert(edgeState.edgesRx(edgeState.idToIdxOrThrow(NodeId("B":Cuid) -> NodeId("D":Cuid))).now == child("B", "D")) + assert(edgeState.edgesRx(edgeState.idToIdxOrThrow(NodeId("E":Cuid) -> NodeId("F":Cuid))).now == child("E", "F")) + assert(edgeState.edgesRx(edgeState.idToIdxOrThrow(NodeId("F":Cuid) -> NodeId("E":Cuid))).now == child("F", "E")) + } + + "add edges" in { + val nodes = Array[Node]("A", "B", "C", "D", "E", "F") + val nodeState = NodeState(nodes) + val edges = Array[Edge](child("B", "C"), child("B", "D"), child("E", "F"), child("F", "E")) + val edgeState = EdgeState(nodeState, edges) + + edgeState.update(GraphChanges(addEdges = Array(child("A", "C"), child("D", "E")))) + // old edges are still there + assert(edgeState.edgesNow(edgeState.idToIdxOrThrow(NodeId("B":Cuid) -> NodeId("C":Cuid))) == child("B", "C")) + assert(edgeState.edgesNow(edgeState.idToIdxOrThrow(NodeId("B":Cuid) -> NodeId("D":Cuid))) == child("B", "D")) + assert(edgeState.edgesNow(edgeState.idToIdxOrThrow(NodeId("E":Cuid) -> NodeId("F":Cuid))) == child("E", "F")) + assert(edgeState.edgesNow(edgeState.idToIdxOrThrow(NodeId("F":Cuid) -> NodeId("E":Cuid))) == child("F", "E")) + + assert(edgeState.edgesIdxNow.toIndexedSeq(edgeState.idToIdxOrThrow(NodeId("B":Cuid) -> NodeId("C":Cuid))).productIterator.asInstanceOf[Iterator[Int]].map(nodeState.nodesNow).toList == List[Node]("B", "C")) + assert(edgeState.edgesIdxNow.toIndexedSeq(edgeState.idToIdxOrThrow(NodeId("B":Cuid) -> NodeId("D":Cuid))).productIterator.asInstanceOf[Iterator[Int]].map(nodeState.nodesNow).toList == List[Node]("B", "D")) + assert(edgeState.edgesIdxNow.toIndexedSeq(edgeState.idToIdxOrThrow(NodeId("E":Cuid) -> NodeId("F":Cuid))).productIterator.asInstanceOf[Iterator[Int]].map(nodeState.nodesNow).toList == List[Node]("E", "F")) + assert(edgeState.edgesIdxNow.toIndexedSeq(edgeState.idToIdxOrThrow(NodeId("F":Cuid) -> NodeId("E":Cuid))).productIterator.asInstanceOf[Iterator[Int]].map(nodeState.nodesNow).toList == List[Node]("F", "E")) + + assert(edgeState.edgesRx(edgeState.idToIdxOrThrow(NodeId("B":Cuid) -> NodeId("C":Cuid))).now == child("B", "C")) + assert(edgeState.edgesRx(edgeState.idToIdxOrThrow(NodeId("B":Cuid) -> NodeId("D":Cuid))).now == child("B", "D")) + assert(edgeState.edgesRx(edgeState.idToIdxOrThrow(NodeId("E":Cuid) -> NodeId("F":Cuid))).now == child("E", "F")) + assert(edgeState.edgesRx(edgeState.idToIdxOrThrow(NodeId("F":Cuid) -> NodeId("E":Cuid))).now == child("F", "E")) + + // added edges + assert(edgeState.edgesNow(edgeState.idToIdxOrThrow(NodeId("A":Cuid) -> NodeId("C":Cuid))) == child("A", "C")) + assert(edgeState.edgesNow(edgeState.idToIdxOrThrow(NodeId("D":Cuid) -> NodeId("E":Cuid))) == child("D", "E")) + + assert(edgeState.edgesIdxNow.toIndexedSeq(edgeState.idToIdxOrThrow(NodeId("A":Cuid) -> NodeId("C":Cuid))).productIterator.asInstanceOf[Iterator[Int]].map(nodeState.nodesNow).toList == List[Node]("A", "C")) + assert(edgeState.edgesIdxNow.toIndexedSeq(edgeState.idToIdxOrThrow(NodeId("D":Cuid) -> NodeId("E":Cuid))).productIterator.asInstanceOf[Iterator[Int]].map(nodeState.nodesNow).toList == List[Node]("D", "E")) + + assert(edgeState.edgesRx(edgeState.idToIdxOrThrow(NodeId("A":Cuid) -> NodeId("C":Cuid))).now == child("A", "C")) + assert(edgeState.edgesRx(edgeState.idToIdxOrThrow(NodeId("D":Cuid) -> NodeId("E":Cuid))).now == child("D", "E")) + } + + "update edge" in { + val nodes = Array[Node]("B", "C", "D", "E", "F") + val nodeState = NodeState(nodes) + val edges = Array[Edge](child("B", "C"), child("B", "D"), child("E", "F"), child("F", "E")) + val edgeState = EdgeState(nodeState, edges) + + edgeState.update(GraphChanges(addEdges = Array(child("B", "C", Some(EpochMilli(0L))), child("F", "E", Some(EpochMilli(1L)))))) + + // old edges are still there + assert(edgeState.edgesNow(edgeState.idToIdxOrThrow(NodeId("B":Cuid) -> NodeId("D":Cuid))) == child("B", "D")) + assert(edgeState.edgesNow(edgeState.idToIdxOrThrow(NodeId("E":Cuid) -> NodeId("F":Cuid))) == child("E", "F")) + + assert(edgeState.edgesIdxNow.toIndexedSeq(edgeState.idToIdxOrThrow(NodeId("B":Cuid) -> NodeId("D":Cuid))).productIterator.asInstanceOf[Iterator[Int]].map(nodeState.nodesNow).toList == List[Node]("B", "D")) + assert(edgeState.edgesIdxNow.toIndexedSeq(edgeState.idToIdxOrThrow(NodeId("E":Cuid) -> NodeId("F":Cuid))).productIterator.asInstanceOf[Iterator[Int]].map(nodeState.nodesNow).toList == List[Node]("E", "F")) + + assert(edgeState.edgesRx(edgeState.idToIdxOrThrow(NodeId("B":Cuid) -> NodeId("D":Cuid))).now == child("B", "D")) + assert(edgeState.edgesRx(edgeState.idToIdxOrThrow(NodeId("E":Cuid) -> NodeId("F":Cuid))).now == child("E", "F")) + + // new and updated edges: + assert(edgeState.edgesNow(edgeState.idToIdxOrThrow(NodeId("B":Cuid) -> NodeId("C":Cuid))) == child("B", "C", Some(EpochMilli(0L)))) + assert(edgeState.edgesNow(edgeState.idToIdxOrThrow(NodeId("F":Cuid) -> NodeId("E":Cuid))) == child("F", "E", Some(EpochMilli(1L)))) + + assert(edgeState.edgesRx(edgeState.idToIdxOrThrow(NodeId("B":Cuid) -> NodeId("C":Cuid))).now == child("B", "C", Some(EpochMilli(0L)))) + assert(edgeState.edgesRx(edgeState.idToIdxOrThrow(NodeId("F":Cuid) -> NodeId("E":Cuid))).now == child("F", "E", Some(EpochMilli(1L)))) + } +} diff --git a/webApp/src/test/scala/wust/webApp/state/GraphStateSpec.scala b/webApp/src/test/scala/wust/webApp/state/GraphStateSpec.scala new file mode 100644 index 000000000..c3946956f --- /dev/null +++ b/webApp/src/test/scala/wust/webApp/state/GraphStateSpec.scala @@ -0,0 +1,165 @@ +package wust.webApp.state.graphstate + +import org.scalatest._ +import wust.graph._ +import wust.ids._ +import rx._ + +class GraphStateSpec extends FreeSpec with MustMatchers { + def parent(childId: Cuid, parentId: Cuid) = Edge.Child(ParentId(NodeId(parentId)), ChildId(NodeId(childId))) + def child(parentId: Cuid, childId: Cuid) = Edge.Child(ParentId(NodeId(parentId)), ChildId(NodeId(childId))) + def user(id: Cuid) = Node.User(UserId(NodeId(id)), NodeData.User(id.toString, false, 0), NodeMeta.User) + implicit def stringToCuid(id: String): Cuid = Cuid.fromBase58String("5Q4is6Gc5NbA7T7W7PvAUw".dropRight(id.length) + id).right.get + implicit def idToNode(id: String): Node = Node.Content(id = NodeId(id: Cuid), data = NodeData.PlainText("content"), role = NodeRole.default, meta = NodeMeta(NodeAccess.ReadWrite)) + def idToNode(id: String, content: String): Node = Node.Content(id = NodeId(id: Cuid), data = NodeData.PlainText(content), role = NodeRole.default, meta = NodeMeta(NodeAccess.ReadWrite)) + + implicit val ctx: Ctx.Owner = Ctx.Owner.safe() + + "GraphState factory" in { + val graph = Graph( + nodes = Array("A", "B", "C", "D", "E", "F"), + edges = Array(child("B", "C"), child("B", "D"), child("F", "E"), child("E", "F")) + ) + + val graphState = new GraphState(graph) + import graphState.{ nodeState, edgeState, children } + + assert(children.lookupNow(nodeState.idToIdxOrThrow(NodeId("A": Cuid))).map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]()) + assert(children.lookupNow(nodeState.idToIdxOrThrow(NodeId("B": Cuid))).map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]("C", "D")) + assert(children.lookupNow(nodeState.idToIdxOrThrow(NodeId("C": Cuid))).map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]()) + assert(children.lookupNow(nodeState.idToIdxOrThrow(NodeId("D": Cuid))).map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]()) + assert(children.lookupNow(nodeState.idToIdxOrThrow(NodeId("E": Cuid))).map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]("F")) + assert(children.lookupNow(nodeState.idToIdxOrThrow(NodeId("F": Cuid))).map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]("E")) + + assert(children.lookupRx(nodeState.idToIdxOrThrow(NodeId("A": Cuid))).now.map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]()) + assert(children.lookupRx(nodeState.idToIdxOrThrow(NodeId("B": Cuid))).now.map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]("C", "D")) + assert(children.lookupRx(nodeState.idToIdxOrThrow(NodeId("C": Cuid))).now.map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]()) + assert(children.lookupRx(nodeState.idToIdxOrThrow(NodeId("D": Cuid))).now.map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]()) + assert(children.lookupRx(nodeState.idToIdxOrThrow(NodeId("E": Cuid))).now.map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]("F")) + assert(children.lookupRx(nodeState.idToIdxOrThrow(NodeId("F": Cuid))).now.map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]("E")) + + assert(children.revLookupNow(nodeState.idToIdxOrThrow(NodeId("A": Cuid))).map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]()) + assert(children.revLookupNow(nodeState.idToIdxOrThrow(NodeId("B": Cuid))).map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]()) + assert(children.revLookupNow(nodeState.idToIdxOrThrow(NodeId("C": Cuid))).map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]("B")) + assert(children.revLookupNow(nodeState.idToIdxOrThrow(NodeId("D": Cuid))).map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]("B")) + assert(children.revLookupNow(nodeState.idToIdxOrThrow(NodeId("E": Cuid))).map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]("F")) + assert(children.revLookupNow(nodeState.idToIdxOrThrow(NodeId("F": Cuid))).map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]("E")) + + assert(children.revLookupRx(nodeState.idToIdxOrThrow(NodeId("A": Cuid))).now.map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]()) + assert(children.revLookupRx(nodeState.idToIdxOrThrow(NodeId("B": Cuid))).now.map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]()) + assert(children.revLookupRx(nodeState.idToIdxOrThrow(NodeId("C": Cuid))).now.map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]("B")) + assert(children.revLookupRx(nodeState.idToIdxOrThrow(NodeId("D": Cuid))).now.map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]("B")) + assert(children.revLookupRx(nodeState.idToIdxOrThrow(NodeId("E": Cuid))).now.map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]("F")) + assert(children.revLookupRx(nodeState.idToIdxOrThrow(NodeId("F": Cuid))).now.map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]("E")) + } + + "GraphState add nodes" in { + val graph = Graph( + nodes = Array("A", "B", "C", "D", "E", "F"), + edges = Array(child("B", "C"), child("B", "D"), child("E", "F"), child("F", "E")) + ) + + val graphState = new GraphState(graph) + graphState.update(GraphChanges(addNodes = Array[Node]("G", "H"))) + import graphState.{ nodeState, children } + + assert(children.lookupNow(nodeState.idToIdxOrThrow(NodeId("A": Cuid))).map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]()) + assert(children.lookupNow(nodeState.idToIdxOrThrow(NodeId("B": Cuid))).map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]("C", "D")) + assert(children.lookupNow(nodeState.idToIdxOrThrow(NodeId("C": Cuid))).map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]()) + assert(children.lookupNow(nodeState.idToIdxOrThrow(NodeId("D": Cuid))).map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]()) + assert(children.lookupNow(nodeState.idToIdxOrThrow(NodeId("E": Cuid))).map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]("F")) + assert(children.lookupNow(nodeState.idToIdxOrThrow(NodeId("F": Cuid))).map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]("E")) + assert(children.lookupNow(nodeState.idToIdxOrThrow(NodeId("G": Cuid))).map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]()) + + assert(children.lookupRx(nodeState.idToIdxOrThrow(NodeId("A": Cuid))).now.map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]()) + assert(children.lookupRx(nodeState.idToIdxOrThrow(NodeId("B": Cuid))).now.map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]("C", "D")) + assert(children.lookupRx(nodeState.idToIdxOrThrow(NodeId("C": Cuid))).now.map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]()) + assert(children.lookupRx(nodeState.idToIdxOrThrow(NodeId("D": Cuid))).now.map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]()) + assert(children.lookupRx(nodeState.idToIdxOrThrow(NodeId("E": Cuid))).now.map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]("F")) + assert(children.lookupRx(nodeState.idToIdxOrThrow(NodeId("F": Cuid))).now.map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]("E")) + assert(children.lookupRx(nodeState.idToIdxOrThrow(NodeId("G": Cuid))).now.map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]()) + + assert(children.revLookupNow(nodeState.idToIdxOrThrow(NodeId("A": Cuid))).map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]()) + assert(children.revLookupNow(nodeState.idToIdxOrThrow(NodeId("B": Cuid))).map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]()) + assert(children.revLookupNow(nodeState.idToIdxOrThrow(NodeId("C": Cuid))).map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]("B")) + assert(children.revLookupNow(nodeState.idToIdxOrThrow(NodeId("D": Cuid))).map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]("B")) + assert(children.revLookupNow(nodeState.idToIdxOrThrow(NodeId("E": Cuid))).map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]("F")) + assert(children.revLookupNow(nodeState.idToIdxOrThrow(NodeId("F": Cuid))).map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]("E")) + assert(children.revLookupNow(nodeState.idToIdxOrThrow(NodeId("G": Cuid))).map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]()) + + assert(children.revLookupRx(nodeState.idToIdxOrThrow(NodeId("A": Cuid))).now.map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]()) + assert(children.revLookupRx(nodeState.idToIdxOrThrow(NodeId("B": Cuid))).now.map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]()) + assert(children.revLookupRx(nodeState.idToIdxOrThrow(NodeId("C": Cuid))).now.map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]("B")) + assert(children.revLookupRx(nodeState.idToIdxOrThrow(NodeId("D": Cuid))).now.map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]("B")) + assert(children.revLookupRx(nodeState.idToIdxOrThrow(NodeId("E": Cuid))).now.map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]("F")) + assert(children.revLookupRx(nodeState.idToIdxOrThrow(NodeId("F": Cuid))).now.map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]("E")) + assert(children.revLookupRx(nodeState.idToIdxOrThrow(NodeId("G": Cuid))).now.map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]()) + } + + "GraphState add edge" in { + val graph = Graph( + nodes = Array("A", "B", "C", "D", "E", "F"), + edges = Array(child("B", "C"), child("B", "D"), child("E", "F"), child("F", "E")) + ) + + val graphState = new GraphState(graph) + import graphState.{ nodeState, children } + + assert(children.lookupNow(nodeState.idToIdxOrThrow(NodeId("A": Cuid))).map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]()) + assert(children.lookupNow(nodeState.idToIdxOrThrow(NodeId("C": Cuid))).map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]()) + assert(children.lookupRx(nodeState.idToIdxOrThrow(NodeId("A": Cuid))).now.map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]()) + assert(children.lookupRx(nodeState.idToIdxOrThrow(NodeId("C": Cuid))).now.map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]()) + + assert(children.revLookupNow(nodeState.idToIdxOrThrow(NodeId("A": Cuid))).map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]()) + assert(children.revLookupNow(nodeState.idToIdxOrThrow(NodeId("C": Cuid))).map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]("B")) + assert(children.revLookupRx(nodeState.idToIdxOrThrow(NodeId("A": Cuid))).now.map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]()) + assert(children.revLookupRx(nodeState.idToIdxOrThrow(NodeId("C": Cuid))).now.map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]("B")) + + graphState.update(GraphChanges(addEdges = Array(child("A", "C")))) + + assert(children.lookupNow(nodeState.idToIdxOrThrow(NodeId("A": Cuid))).map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]("C")) + assert(children.lookupNow(nodeState.idToIdxOrThrow(NodeId("C": Cuid))).map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]()) + assert(children.lookupRx(nodeState.idToIdxOrThrow(NodeId("A": Cuid))).now.map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]("C")) + assert(children.lookupRx(nodeState.idToIdxOrThrow(NodeId("C": Cuid))).now.map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]()) + + assert(children.revLookupNow(nodeState.idToIdxOrThrow(NodeId("A": Cuid))).map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]()) + assert(children.revLookupNow(nodeState.idToIdxOrThrow(NodeId("C": Cuid))).map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]("B", "A")) + assert(children.revLookupRx(nodeState.idToIdxOrThrow(NodeId("A": Cuid))).now.map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]()) + assert(children.revLookupRx(nodeState.idToIdxOrThrow(NodeId("C": Cuid))).now.map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]("B", "A")) + } + + "GraphState add nodes with edge" in { + val graph = Graph( + nodes = Array("A", "B", "C", "D", "E", "F"), + edges = Array(child("B", "C"), child("B", "D"), child("E", "F"), child("F", "E")) + ) + + val graphState = new GraphState(graph) + import graphState.{ nodeState, children } + + graphState.update(GraphChanges(addNodes = Array("G", "H"), addEdges = Array(child("G", "H")))) + assert(children.lookupNow(nodeState.idToIdxOrThrow(NodeId("G": Cuid))).map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]("H")) + assert(children.lookupRx(nodeState.idToIdxOrThrow(NodeId("G": Cuid))).now.map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]("H")) + assert(children.revLookupNow(nodeState.idToIdxOrThrow(NodeId("H": Cuid))).map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]("G")) + assert(children.revLookupRx(nodeState.idToIdxOrThrow(NodeId("H": Cuid))).now.map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]("G")) + } + + "GraphState delete edge" in { + val graph = Graph( + nodes = Array("A", "B", "C", "D", "E", "F"), + edges = Array(child("B", "C"), child("B", "D"), child("E", "F"), child("F", "E")) + ) + + val graphState = new GraphState(graph) + import graphState.{ nodeState, children } + + graphState.update(GraphChanges(delEdges = Array(child("E", "F")))) + assert(children.lookupNow(nodeState.idToIdxOrThrow(NodeId("E": Cuid))).map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]()) + assert(children.lookupRx(nodeState.idToIdxOrThrow(NodeId("E": Cuid))).now.map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]()) + assert(children.revLookupNow(nodeState.idToIdxOrThrow(NodeId("F": Cuid))).map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]()) + assert(children.revLookupRx(nodeState.idToIdxOrThrow(NodeId("F": Cuid))).now.map(idx => nodeState.nodesNow(idx).id).toList == List[Cuid]()) + } + + //TODO: delete for author LabeledProperty edges, where edges can have the same source/target combination + //TODO: addEdges overwrite edge +} diff --git a/webApp/src/test/scala/wust/webApp/state/NodeStateSpec.scala b/webApp/src/test/scala/wust/webApp/state/NodeStateSpec.scala new file mode 100644 index 000000000..2bae9702c --- /dev/null +++ b/webApp/src/test/scala/wust/webApp/state/NodeStateSpec.scala @@ -0,0 +1,77 @@ +package wust.webApp.state.graphstate + +import org.scalatest._ +import wust.graph._ +import wust.ids._ + +class NodeStateSpec extends FreeSpec with MustMatchers { + def parent(childId:Cuid, parentId:Cuid) = Edge.Child(ParentId(NodeId(parentId)), ChildId(NodeId(childId))) + def child(parentId:Cuid, childId:Cuid) = Edge.Child(ParentId(NodeId(parentId)), ChildId(NodeId(childId))) + def user(id:Cuid) = Node.User(UserId(NodeId(id)), NodeData.User(id.toString, false, 0), NodeMeta.User) + implicit def stringToCuid(id:String):Cuid = Cuid.fromBase58String("5Q4is6Gc5NbA7T7W7PvAUw".dropRight(id.length) + id).right.get + implicit def idToNode(id: String): Node = Node.Content(id = NodeId(id:Cuid), data = NodeData.PlainText("content"), role = NodeRole.default, meta = NodeMeta(NodeAccess.ReadWrite)) + def idToNode(id: String, content:String): Node = Node.Content(id = NodeId(id:Cuid), data = NodeData.PlainText(content), role = NodeRole.default, meta = NodeMeta(NodeAccess.ReadWrite)) + + "factory" in { + val nodes = Array[Node]("A", "B", "C") + val nodeState = NodeState(nodes) + + assert(nodeState.nodesNow(nodeState.idToIdxOrThrow(NodeId("A":Cuid))) == idToNode("A")) + assert(nodeState.nodesNow(nodeState.idToIdxOrThrow(NodeId("B":Cuid))) == idToNode("B")) + assert(nodeState.nodesNow(nodeState.idToIdxOrThrow(NodeId("C":Cuid))) == idToNode("C")) + + assert(nodeState.nodesRx(nodeState.idToIdxOrThrow(NodeId("A":Cuid))).now == idToNode("A")) + assert(nodeState.nodesRx(nodeState.idToIdxOrThrow(NodeId("B":Cuid))).now == idToNode("B")) + assert(nodeState.nodesRx(nodeState.idToIdxOrThrow(NodeId("C":Cuid))).now == idToNode("C")) + } + + "add nodes" in { + val nodes = Array[Node]("A", "B", "C") + val nodeState = NodeState(nodes) + + nodeState.update(GraphChanges(addNodes = Array("G","H"))) + + assert(nodeState.nodesNow(nodeState.idToIdxOrThrow(NodeId("A":Cuid))) == idToNode("A")) + assert(nodeState.nodesNow(nodeState.idToIdxOrThrow(NodeId("B":Cuid))) == idToNode("B")) + assert(nodeState.nodesNow(nodeState.idToIdxOrThrow(NodeId("C":Cuid))) == idToNode("C")) + assert(nodeState.nodesNow(nodeState.idToIdxOrThrow(NodeId("G":Cuid))) == idToNode("G")) + assert(nodeState.nodesNow(nodeState.idToIdxOrThrow(NodeId("H":Cuid))) == idToNode("H")) + + assert(nodeState.nodesRx(nodeState.idToIdxOrThrow(NodeId("A":Cuid))).now == idToNode("A")) + assert(nodeState.nodesRx(nodeState.idToIdxOrThrow(NodeId("B":Cuid))).now == idToNode("B")) + assert(nodeState.nodesRx(nodeState.idToIdxOrThrow(NodeId("C":Cuid))).now == idToNode("C")) + assert(nodeState.nodesRx(nodeState.idToIdxOrThrow(NodeId("G":Cuid))).now == idToNode("G")) + assert(nodeState.nodesRx(nodeState.idToIdxOrThrow(NodeId("H":Cuid))).now == idToNode("H")) + } + + + "update nodes" in { + val nodes = Array[Node]("A", "B", "C") + val nodeState = NodeState(nodes) + + nodeState.update(GraphChanges(addNodes = Array[Node](idToNode("A", "changed")))) + + assert(nodeState.nodesNow(nodeState.idToIdxOrThrow(NodeId("A":Cuid))) == idToNode("A", "changed")) + assert(nodeState.nodesRx(nodeState.idToIdxOrThrow(NodeId("A":Cuid))).now == idToNode("A", "changed")) + } + + "update: return LayerChanges" in { + val nodes = Array[Node]("A", "B", "C") + + val nodeState = NodeState(nodes) + val layerChanges = nodeState.update(GraphChanges( + addNodes = Array("G","H"), + addEdges = Array(child("G", "H")), + delEdges = Array(child("X", "Y")) + )) + + val expected = LayerChanges( + addIdx = 2, + addEdges = Array(child("G", "H")), + delEdges = Array(child("X", "Y")) + ) + assert(layerChanges.addIdx == expected.addIdx) + assert(layerChanges.addEdges.toList == expected.addEdges.toList) + assert(layerChanges.delEdges.toList == expected.delEdges.toList) + } +}