├── .classpath ├── .project ├── LICENSE ├── NOTICE ├── README.md ├── lib ├── hamcrest.jar ├── joda-convert-1.2.jar ├── joda-time-2.0-sources.jar ├── joda-time-2.0.jar ├── jsr166y.jar ├── junit.jar └── scalatest-1.2.1-SNAPSHOT.jar ├── src └── scala │ └── react │ ├── Debug.scala │ ├── Domain.scala │ ├── EngineModule.scala │ ├── EventModule.scala │ ├── FlowModule.scala │ ├── ReactiveModule.scala │ ├── SchedulerModule.scala │ ├── SignalModule.scala │ ├── TopoQueue.scala │ └── monitor │ ├── IO.scala │ ├── LogDebug.scala │ ├── LogFormat.scala │ └── PagedWriter.scala └── test └── scala └── react └── test ├── AllTests.java ├── CombinatorTests.scala ├── FlowTests.scala ├── ReactiveTests.scala ├── ReactorTests.scala ├── RouterTests.scala ├── TopoQueueTests.scala └── utils ├── JTestUtils.java ├── TestDomain.scala └── TestUtils.scala /.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | scala-react-2 4 | 5 | 6 | 7 | 8 | 9 | org.scala-ide.sdt.core.scalabuilder 10 | 11 | 12 | 13 | 14 | 15 | org.scala-ide.sdt.core.scalanature 16 | org.eclipse.jdt.core.javanature 17 | 18 | 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | scala.react 2 | Copyright 2012 Ingo Maier 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # scala.react 2 | 3 | Scala.react is a reactive programming library for Scala. -------------------------------------------------------------------------------- /lib/hamcrest.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ingoem/scala-react/5713070438623307d12cc218617cfa50f4489df6/lib/hamcrest.jar -------------------------------------------------------------------------------- /lib/joda-convert-1.2.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ingoem/scala-react/5713070438623307d12cc218617cfa50f4489df6/lib/joda-convert-1.2.jar -------------------------------------------------------------------------------- /lib/joda-time-2.0-sources.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ingoem/scala-react/5713070438623307d12cc218617cfa50f4489df6/lib/joda-time-2.0-sources.jar -------------------------------------------------------------------------------- /lib/joda-time-2.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ingoem/scala-react/5713070438623307d12cc218617cfa50f4489df6/lib/joda-time-2.0.jar -------------------------------------------------------------------------------- /lib/jsr166y.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ingoem/scala-react/5713070438623307d12cc218617cfa50f4489df6/lib/jsr166y.jar -------------------------------------------------------------------------------- /lib/junit.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ingoem/scala-react/5713070438623307d12cc218617cfa50f4489df6/lib/junit.jar -------------------------------------------------------------------------------- /lib/scalatest-1.2.1-SNAPSHOT.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ingoem/scala-react/5713070438623307d12cc218617cfa50f4489df6/lib/scalatest-1.2.1-SNAPSHOT.jar -------------------------------------------------------------------------------- /src/scala/react/Debug.scala: -------------------------------------------------------------------------------- 1 | package scala.react 2 | 3 | import java.util.WeakHashMap 4 | import java.text.SimpleDateFormat 5 | import scala.annotation.elidable 6 | import scala.collection.mutable.ArrayBuffer 7 | 8 | abstract class Debug[D <: Domain](val domain: D) { 9 | import domain._ 10 | 11 | def setName(node: Node, name: String) 12 | def getName(node: Node): String 13 | 14 | 15 | // The thread on which this engine is currently running, `null` if not in a turn. 16 | @volatile private var thread: Thread = null 17 | 18 | /** 19 | * Returns `true` if the current thread is the thread the engine is running on. 20 | */ 21 | @elidable(800) protected def isInTurn = Thread.currentThread == thread 22 | @elidable(800) protected def isInTurn_=(b: Boolean) { 23 | thread = if (b) Thread.currentThread else null 24 | } 25 | 26 | @elidable(800) def enterTurn(id: Long) { 27 | assert(!isInTurn, "Tried to run a turn before the previous one was finished.") 28 | logEnterTurn(id) 29 | isInTurn = true 30 | } 31 | @elidable(800) def leaveTurn(id: Long) { 32 | assert(isInTurn) 33 | logLeaveTurn(id) 34 | isInTurn = false 35 | } 36 | 37 | @elidable(800) def assertInTurn() = assert(isInTurn, "This method must be run on its domain " + this) 38 | 39 | @elidable(800) def logStart() 40 | @elidable(800) def logEnterTurn(id: Long) 41 | @elidable(800) def logLeaveTurn(id: Long) 42 | @elidable(800) def logTock(n: Node) 43 | @elidable(800) def logLevelMismatch(accessor: Node, accessed: Node) 44 | @elidable(800) def logTodo(t: Any) 45 | } 46 | 47 | class NilDebug[D <: Domain](dom: D) extends Debug(dom) { 48 | import domain._ 49 | 50 | def setName(node: Node, name: String) { } 51 | def getName(node: Node): String = "" 52 | 53 | def logStart() {} 54 | def logEnterTurn(id: Long) {} 55 | def logLeaveTurn(id: Long) {} 56 | def logTock(n: Node) {} 57 | def logLevelMismatch(accessor: Node, accessed: Node) {} 58 | def logTodo(t: Any) {} 59 | } 60 | 61 | abstract class AbstractDebug[D <: Domain](dom: D) extends Debug(dom) { 62 | import domain._ 63 | private val names = new WeakHashMap[Node, String] 64 | def setName(node: Node, name: String) { names.put(node, name) } 65 | def getName(node: Node): String = names.get(node) 66 | } 67 | 68 | abstract class PrintDebug[D <: Domain](dom: D) extends AbstractDebug(dom) { 69 | import domain._ 70 | 71 | private val dateFormat = new SimpleDateFormat("d MMM yyyy HH:mm:ss.SSS") 72 | 73 | def log(s: String) 74 | 75 | private def time() = System.currentTimeMillis() 76 | private def timePrefix = "+" + (time()-startTime) + "ms: " 77 | private var startTime = 0L 78 | 79 | def logStart() { 80 | startTime = time() 81 | log("Start domain " + domain + " at " + dateFormat.format(startTime)) 82 | } 83 | def logEnterTurn(id: Long) = log(timePrefix + "enter turn " + id) 84 | def logLeaveTurn(id: Long) = log(timePrefix + "leave turn " + id) 85 | def logTock(n: Node) = log("Tock " + n) 86 | def logLevelMismatch(accessor: Node, accessed: Node) = log("Level mismatch when " + accessor + " accessed " + accessed) 87 | def logTodo(t: Any) = log("Todo " + t) 88 | } 89 | 90 | 91 | class ConsoleDebug[D <: Domain](dom: D) extends PrintDebug(dom) { 92 | def log(s: String) = println(s) 93 | } -------------------------------------------------------------------------------- /src/scala/react/Domain.scala: -------------------------------------------------------------------------------- 1 | package scala.react 2 | import scala.annotation.elidable 3 | 4 | /** 5 | * Defines all reactive classes by mixing in all reactive modules. 6 | * 7 | * Clients usually create an 'object myDomain extends Domain' and import it. 8 | * 9 | * Except the scheduler interface, no method in neither class is guaranteed to be thread-safe. 10 | */ 11 | abstract class Domain extends ReactiveModule 12 | with SignalModule 13 | with EventModule 14 | with FlowModule 15 | with SchedulerModule { domain => 16 | 17 | protected val scheduler: Scheduler 18 | protected def engine: Engine 19 | 20 | val debug: Debug[this.type] = 21 | System.getProperty("scala.react.debug", "no").toLowerCase match { 22 | case "no" => new NilDebug[this.type](domain) 23 | case "print" => new ConsoleDebug[this.type](this) 24 | case "log" => new monitor.LogDebug[this.type](this) 25 | } 26 | 27 | /** 28 | * A reactive version of `scala.App`, running all initialization code on this domain. 29 | * Automatically starts the domain in the main method. 30 | * 31 | * @see scala.App 32 | * @see scala.DelayedInit 33 | */ 34 | trait ReactiveApp extends App { 35 | def main() {} 36 | 37 | override def main(args: Array[String]) { 38 | domain schedule { super.main(args) } 39 | domain.start() 40 | main() 41 | } 42 | } 43 | 44 | /** 45 | * Starts processing events. Thread-safe. 46 | */ 47 | def start() { 48 | debug.logStart() 49 | scheduler.start() 50 | } 51 | } 52 | 53 | -------------------------------------------------------------------------------- /src/scala/react/EngineModule.scala: -------------------------------------------------------------------------------- 1 | package scala.react 2 | 3 | import java.util.ArrayDeque 4 | import java.util.concurrent.ConcurrentLinkedQueue 5 | import scala.collection.mutable.ArrayStack 6 | 7 | abstract class EngineModule { self: Domain => 8 | // Dependent stack stuff is inlined, since it needs to be as efficient as possible 9 | private var depStack = new Array[Node](4) 10 | private var depStackSize = 0 11 | 12 | protected def depStackPush(n: Node) { 13 | if (depStackSize == depStack.length) { 14 | val newarr = new Array[Node](depStack.length * 2) 15 | System.arraycopy(depStack, 0, newarr, 0, depStack.length) 16 | depStack = newarr 17 | } 18 | depStack(depStackSize) = n 19 | depStackSize += 1 20 | } 21 | 22 | protected def depStackPop() { 23 | if (depStackSize == 0) sys.error("Stack empty") 24 | depStackSize -= 1 25 | depStack(depStackSize) = null 26 | } 27 | 28 | protected def depStackTop: Node = depStack(depStackSize-1) 29 | 30 | 31 | // ----------------------------------------------------------------- 32 | 33 | 34 | /** 35 | * Nodes that throw this exception must ensure that the dependent stack is in clean state, i.e., 36 | * no leftovers on the stack. Otherwise dynamic dependency tracking goes havoc. 37 | */ 38 | private[react] object LevelMismatch extends util.control.NoStackTrace { 39 | var accessed: Node = _ 40 | } 41 | 42 | @inline def runTurn(op: => Unit) { 43 | op 44 | engine.runTurn 45 | } 46 | 47 | @inline def runReadTurn(leaf: LeafNode) { 48 | engine.runReadTurn(leaf) 49 | } 50 | 51 | abstract class Propagator { 52 | // the current turn index, first usable turn has index 2. Values 0 and 1 are reserved. 53 | private var turn = 1 54 | 55 | // the current propagation level at which this engine is or previously was 56 | protected var level = 0 57 | 58 | private var propQueue: PropQueue[StrictNode] = System.getProperty("scala.react.propqueue", "prio").toLowerCase match { 59 | case "prio" => new PriorityQueue[StrictNode] { 60 | def priority(n: StrictNode) = n.level 61 | } 62 | case "topo" => new TopoQueue[StrictNode] { 63 | def depth(n: StrictNode) = n.level 64 | } 65 | } 66 | 67 | def currentTurn: Long = turn 68 | def currentLevel: Int = level 69 | 70 | protected def applyTodos() 71 | 72 | def runTurn() { 73 | turn += 1 74 | debug.enterTurn(currentTurn) 75 | 76 | try { 77 | propagate() 78 | } catch { 79 | case e => uncaughtException(e) 80 | } finally { 81 | propQueue.clear 82 | level = 0 83 | debug.leaveTurn(currentTurn) 84 | } 85 | } 86 | 87 | def runReadTurn(leaf: LeafNode) { 88 | turn += 1 89 | debug.enterTurn(currentTurn) 90 | 91 | try { 92 | level = leaf.level 93 | leaf.invalidate() 94 | tryTock(leaf) 95 | } catch { 96 | case e => uncaughtException(e) 97 | } finally { 98 | propQueue.clear 99 | level = 0 100 | debug.leaveTurn(currentTurn) 101 | } 102 | } 103 | 104 | protected def propagate() { 105 | val queue = propQueue 106 | applyTodos() 107 | while (!queue.isEmpty) { 108 | val node = queue.dequeue 109 | assert(node.level >= level) 110 | if (node.level > level) 111 | level = node.level 112 | tryTock(node) 113 | assert(depStackSize == 0) 114 | } 115 | } 116 | 117 | def newNode(n: Node) {} 118 | 119 | def defer(dep: StrictNode) { 120 | debug.assertInTurn() 121 | val depLevel = dep.level 122 | if (depLevel == level) // fast path 123 | tryTock(dep) 124 | else if (depLevel < level) // can happen for signals that are created on the fly 125 | hoist(dep, level + 1) 126 | else propQueue += dep // default path 127 | } 128 | 129 | /** 130 | * Exceptions are caught in the main propagation loop and passed to this method. 131 | * Override to customize exception handling. The default behavior is to print a stack 132 | * trace and `sys.exit`. 133 | */ 134 | protected def uncaughtException(e: Throwable) { 135 | Console.err.println("Uncaught exception in turn!") 136 | e.printStackTrace() 137 | sys.exit(-1) 138 | } 139 | 140 | /** 141 | * Try to tock a node. Hoists the node, if on wrong level. 142 | */ 143 | protected def tryTock(dep: StrictNode) = try { 144 | debug.logTock(dep) 145 | dep.tock() 146 | } catch { 147 | case lm @ LevelMismatch => 148 | val lvl = lm.accessed.level 149 | debug.logLevelMismatch(dep, lm.accessed) 150 | hoist(dep, lvl + 1) 151 | } 152 | 153 | def levelMismatch(accessed: Node) { 154 | LevelMismatch.accessed = accessed 155 | throw LevelMismatch 156 | } 157 | 158 | /** 159 | * Change the level of the given node and reschedule for later in this turn. 160 | */ 161 | def hoist(dep: StrictNode, newLevel: Int) { 162 | dep.level = newLevel 163 | dep match { 164 | case dep: DependencyNode => dep.hoistDependents(newLevel + 1) 165 | case _ => 166 | } 167 | propQueue reinsert dep 168 | } 169 | 170 | def hoist(dep: Node, newLevel: Int) { 171 | dep.level = newLevel 172 | dep match { 173 | case dep: DependencyNode => dep.hoistDependents(newLevel + 1) 174 | case _ => 175 | } 176 | dep match { 177 | case dep: StrictNode => propQueue reinsert dep 178 | case _ => 179 | } 180 | } 181 | } 182 | 183 | class Engine extends Propagator { 184 | // todos that can be scheduled externally, thread-safe 185 | private val asyncTodos = new ConcurrentLinkedQueue[AnyRef] // Runnable or ()=>Unit 186 | // todos that can be scheduled only inside a turn, no need for thead-safety 187 | private val localTodos = new ArrayDeque[Tickable] 188 | 189 | def schedule(r: Runnable) = { 190 | asyncTodos.add(r) 191 | scheduler.ensureTurnIsScheduled() 192 | } 193 | def schedule(op: => Unit) = { 194 | asyncTodos.add(() => op) 195 | scheduler.ensureTurnIsScheduled() 196 | } 197 | 198 | def tickNextTurn(t: Tickable) = { 199 | //assert(!localTodos.contains(t)) 200 | // TODO: make localTodos a set? 201 | if (!localTodos.contains(t)) { 202 | localTodos add t 203 | scheduler.ensureTurnIsScheduled() 204 | } 205 | } 206 | protected def applyTodos() { 207 | var t = asyncTodos.poll() 208 | while (t ne null) { 209 | debug.logTodo(t) 210 | t match { 211 | case r: Runnable => r.run() 212 | case f: Function0[_] => f() 213 | } 214 | 215 | t = asyncTodos.poll() 216 | } 217 | 218 | while (!localTodos.isEmpty) { 219 | val t = localTodos.poll() 220 | debug.logTodo(t) 221 | t.tick() 222 | } 223 | } 224 | 225 | //def newLocalChannel[P](r: Reactive[P, Any], p: P): Channel[P] = new LocalDelayChannel(r, p) 226 | } 227 | 228 | class LocalDelayChannel[P](val node: Reactive[P, Any], p: P) extends Tickable with Channel[P] { 229 | private var pulse: P = p 230 | private var added = false 231 | def push(r: Reactive[P, Any], p: P) { 232 | pulse = p 233 | if (!added) { 234 | added = true 235 | engine.tickNextTurn(this) 236 | } 237 | } 238 | def get(r: Reactive[P, Any]) = pulse 239 | 240 | def tick() { 241 | added = false 242 | node.tick() 243 | } 244 | } 245 | } -------------------------------------------------------------------------------- /src/scala/react/EventModule.scala: -------------------------------------------------------------------------------- 1 | package scala.react 2 | 3 | import scala.util.continuations._ 4 | 5 | trait EventModule { module: Domain => 6 | object Events { 7 | def once[A](a: A): Events[A] = { 8 | val es = EventSource[A] 9 | es << a 10 | es 11 | } 12 | 13 | /** 14 | * Creates a flow event that runs through the given `op` once. 15 | */ 16 | def flow[A](op: EventsFlowOps[A] => Unit @suspendable): Events[A] = new FlowEvents[A] { 17 | def body = op(this) 18 | } 19 | 20 | /** 21 | * Creates a flow event that runs through the given `op` repeatedly until disposed, e.g., by 22 | * calling `halt`. 23 | */ 24 | def loop[A](op: EventsFlowOps[A] => Unit @suspendable): Events[A] = new FlowEvents[A] { 25 | def body = while (!isDisposed) op(this) 26 | } 27 | 28 | /** 29 | * An event stream that never emits. 30 | */ 31 | def never[A]: Events[A] = Never 32 | object Never extends Events[Nothing] with MuteNode 33 | 34 | class MergedAny(val inputs: Iterable[Reactive[Any, Any]]) extends Events[Unit] 35 | with DependentN 36 | with LazyNode { 37 | inputs foreach { _ subscribe this } 38 | 39 | def doValidatePulse() { 40 | if (inputs exists { _.isEmitting }) emit() 41 | else mute() 42 | } 43 | } 44 | 45 | def mergeAny(inputs: Reactive[Any, Any]*): Events[Unit] = new MergedAny(inputs) 46 | } 47 | 48 | /** 49 | * An event stream. Can emit in a propagation turn, but holds no value otherwise. 50 | */ 51 | abstract class Events[+A] extends TotalReactive[A, Unit] with SimpleReactive[A, Unit] { outer => 52 | def getValue: Unit = () 53 | 54 | protected class Mapped[B](f: A => B) extends LazyEvents1[A, B](outer) { 55 | protected[this] def react(a: A) { emit(f(a)) } 56 | } 57 | 58 | protected class Filtered(pred: A => Boolean) extends StrictEvents1[A, A](outer) { 59 | protected[this] def react(a: A) { if (pred(a)) emit(a) } 60 | } 61 | 62 | protected class Collected[B](f: PartialFunction[A, B]) extends StrictEvents1[A, B](outer) { 63 | protected[this] def react(a: A) { if (f isDefinedAt a) emit(f(a)) } 64 | } 65 | 66 | protected class Taken(private var count: Int) extends StrictEvents1[A, A](outer) { 67 | protected[this] def react(a: A) { 68 | if (count > 0) { 69 | count -= 1 70 | emit(a) 71 | } else disconnect() 72 | } 73 | } 74 | 75 | protected class Dropped(private var count: Int) extends StrictEvents1[A, A](outer) { 76 | protected[this] def react(a: A) { 77 | if (count > 0) count -= 1 78 | else emit(a) 79 | } 80 | } 81 | 82 | protected class Delayed(delay: Int) extends StrictEvents1[A, A](outer) { 83 | if(delay <= 0) throw new IllegalArgumentException("Delay must be >= 1.") 84 | 85 | private val buffer = new Array[Any](delay) // ring buffer 86 | private var head = 0 87 | private var size = 0 // only used until we have seen delay number of events 88 | 89 | protected[this] def react(a: A) { 90 | if(size < delay) { 91 | buffer(size) = a 92 | size += 1 93 | } else { 94 | emit(buffer(head).asInstanceOf[A]) 95 | buffer(head) = a 96 | head = (head + 1) % delay 97 | } 98 | } 99 | } 100 | 101 | protected class Scanned[B](init: B)(f: (B, A) => B) extends StrictEvents1[A, B](outer) { 102 | pulse = init 103 | 104 | protected[this] def react(a: A) { emit(f(this.pulse, a)) } 105 | } 106 | 107 | protected class ScannedMutable[B](init: B)(f: (B, A) => B) extends StrictEvents1[A, B](outer) { 108 | pulse = init 109 | 110 | protected[this] def react(a: A) { 111 | mutable { 112 | emit(f(this.pulse, a)) 113 | } 114 | } 115 | } 116 | 117 | protected class Scanned1[B >: A](f: (B, B) => B) extends StrictEvents1[B, B](outer) { 118 | private var started = false 119 | protected[this] def react(a: B) = { 120 | if (started) emit(f(this.pulse, a)) 121 | else { 122 | started = true 123 | emit(a) 124 | } 125 | } 126 | } 127 | 128 | protected class Merged[B >: A](val input1: Events[B], val input2: Events[B]) extends Events[B] 129 | with Dependent2[B, Unit] 130 | with LazyNode { 131 | input1 subscribe this 132 | input2 subscribe this 133 | 134 | def doValidatePulse() { 135 | input1.ifEmittingElse { emit _ } { 136 | input2.ifEmittingElse { emit _ } { mute() } 137 | } 138 | } 139 | } 140 | 141 | protected class Flattened[B](isEvents: A => Events[B]) extends LazyEvents[B] with ManagesDependencies { 142 | private var cur: Events[B] = null 143 | 144 | // level = (if(cur==null) outer.level else math.max(outer.level, cur.level)) + 1 145 | 146 | outer.subscribe(this) 147 | def doValidatePulse() { 148 | outer.ifEmitting { e => 149 | if (cur != null) cur.unsubscribe(this) 150 | cur = isEvents(e) 151 | cur.subscribe(this) 152 | } 153 | if (cur != null) cur.ifEmitting { x => 154 | emit(x) 155 | } 156 | } 157 | 158 | protected override def disconnect() { 159 | outer.unsubscribe(this) 160 | cur.unsubscribe(this) 161 | super.disconnect() 162 | } 163 | } 164 | 165 | /** 166 | * An event stream that emits every value from this stream mapped by the given function. 167 | */ 168 | def map[B](f: A => B): Events[B] = new Mapped(f) 169 | /** 170 | * An event stream that emits all values from this stream for which the given predicate 171 | * evaluates to `true`. 172 | */ 173 | def filter(p: A => Boolean): Events[A] = new Filtered(p) 174 | /** 175 | * An event stream that emits all values from this stream for which the given partial function 176 | * is defined. Additionally maps, i.e., `es collect { case e if p(e) => f(e) }` is equivalent to 177 | * `es filter p map f`. 178 | */ 179 | def collect[B](f: PartialFunction[A, B]): Events[B] = new Collected(f) 180 | 181 | /** 182 | * An event stream that applies `init` and the value from this stream to the given 183 | * function and emits the result at the time of the second event. At the time of the 184 | * third event, it applies the last result and third value from this stream to the given 185 | * function and emits the result ... and so on. 186 | */ 187 | def scan[B](init: B)(f: (B, A) => B): Events[B] = new Scanned(init)(f) 188 | 189 | /** 190 | * An event stream that applies the first and second value from this stream to the given 191 | * function and emits the result at the time of the second event. At the time of the third 192 | * event, it applies the last result and third value from this stream to the given function 193 | * and emits the result ... and so on. 194 | */ 195 | def scan1[B >: A](f: (B, B) => B): Events[B] = new Scanned1(f) 196 | 197 | /** 198 | * A scan that can skip events from this event stream. 199 | * 200 | * Wraps the call to `f` in a `mutable` block, i.e., a call to `mute` inside `f` is safe and 201 | * indicates that the current event from this event stream should be skipped and the resulting 202 | * event stream should not emit in that turn. 203 | */ 204 | def scanMutable[B](init: B)(f: (B, A) => B): Events[B] = new ScannedMutable(init)(f) 205 | 206 | /** 207 | * An event stream that emits the first `count` events from this stream. 208 | */ 209 | def take(count: Int): Events[A] = new Taken(count) 210 | 211 | /** 212 | * An event stream that emits all event from this stream after the first `count` events. 213 | */ 214 | def drop(count: Int): Events[A] = new Dropped(count) 215 | 216 | def delay(count: Int): Events[A] = new Delayed(count) 217 | 218 | /** 219 | * An event stream that emits the values from both this and the given stream. It is left-biased, 220 | * i.e., emits a value from this stream if both input stream emit at the same time. 221 | */ 222 | def merge[B >: A](that: Events[B]): Events[B] = new Merged(this, that) 223 | 224 | /** 225 | * A signal that initially holds `init` and then always the latest emitted value from this 226 | * stream. 227 | */ 228 | def hold[B >: A](init: B): Signal[B] = new HoldSignal(init)(this) 229 | 230 | def flatten[B](implicit isEvents: A => Events[B]): Events[B] = new Flattened[B](isEvents) 231 | } 232 | 233 | /** 234 | * An event stream with a single input. 235 | */ 236 | abstract class Events1[+A, +B](protected val input: Reactive[A, Any]) 237 | extends Events[B] 238 | with Dependent1[A, Any] { 239 | 240 | connect() 241 | 242 | def doValidatePulse() { 243 | input.ifEmitting(react _) 244 | setPulseValid() 245 | } 246 | protected[this] def react(a: A) 247 | } 248 | 249 | abstract class LazyEvents[+A] 250 | extends Events[A] 251 | with LazyNode 252 | 253 | abstract class StrictEvents[+A] 254 | extends Events[A] 255 | with LazyNode 256 | 257 | abstract class LazyEvents1[+A, +B](input: Reactive[A, Any]) 258 | extends Events1[A, B](input) 259 | with LazyNode 260 | 261 | abstract class StrictEvents1[+A, +B](input: Reactive[A, Any]) 262 | extends Events1[A, B](input) 263 | with StrictNode 264 | 265 | protected[this] class ChangeEvents[P](input: Reactive[P, Any]) extends LazyEvents1[P, P](input) { 266 | def react(p: P) { emit(p) } 267 | } 268 | 269 | object EventSource { 270 | def apply[A](implicit owner: Owner): EventSource[A] = new EventSource[A](owner) 271 | } 272 | 273 | /** 274 | * An externally mutable event stream. 275 | */ 276 | class EventSource[A] protected (val owner: Owner) extends Events[A] with SimpleSource[A, Unit] with StrictNode { 277 | protected[react] val channel = owner.newChannel(this, null.asInstanceOf[A]) 278 | 279 | /** 280 | * Lets this stream emit the given value, either in the next turn, if the owner is the domain or 281 | * in the current turn if the owner is a router. 282 | */ 283 | def <<(a: A) { 284 | if (owner eq DomainOwner) channel.push(this, a) 285 | else { 286 | emit(a) 287 | engine.defer(this) 288 | } 289 | } 290 | 291 | override def reactiveDescriptor = "EventSource" 292 | } 293 | } -------------------------------------------------------------------------------- /src/scala/react/FlowModule.scala: -------------------------------------------------------------------------------- 1 | package scala.react 2 | 3 | import scala.util.continuations._ 4 | 5 | trait FlowModule { module: Domain => 6 | private val idContinuation = ()=>() 7 | 8 | trait FlowNode extends StrictNode { 9 | protected[react] var _continue = initialContinuation 10 | 11 | protected def continue() = _continue() 12 | protected def initialContinuation: () => Unit = () => reset { 13 | body() 14 | //TODO: dispose() 15 | } 16 | 17 | override def dispose() { 18 | _continue = idContinuation 19 | super.dispose() 20 | } 21 | 22 | def body(): Unit @suspendable 23 | 24 | protected[react] def shiftAndContinue[A](body: (A => Unit) => Unit) = 25 | shift { (k: A => Unit) => 26 | // we need to save the continuation before we run it, so we always have the very latest 27 | // continuation when a level mismatch exception is thrown. 28 | _continue = () => body(k) 29 | continue() 30 | } 31 | 32 | /** 33 | * Continues with the given continuation in the next cycle. 34 | */ 35 | protected[react] def continueLater(k: => Unit) = { 36 | _continue = () => k 37 | engine.tickNextTurn(this) 38 | } 39 | 40 | /** 41 | * Continues with the given continuation and returns the remaining continuation. 42 | */ 43 | protected[react] def continueWith(k: => Unit): (()=>Unit) = { 44 | _continue = () => k 45 | continue() 46 | _continue 47 | } 48 | } 49 | 50 | 51 | trait FlowOps { this: FlowNode => 52 | 53 | /** 54 | * Suspends the reactive and continues execution in the next turn. 55 | */ 56 | def pause: Unit @suspendable = shift { (k: Unit => Unit) => 57 | continueLater { k() } 58 | } 59 | 60 | /** 61 | * Waits for a pulse from the given reactive and returns it. Returns immediately, if the 62 | * reactive is currently emitting. 63 | */ 64 | def await[B](input: Reactive[B, Any]): B @suspendable = shiftAndContinue[B] { k => 65 | input.ifEmittingElse { p => 66 | /*if (!reactive.isDisposed)*/ k(p) 67 | } { 68 | input subscribe this 69 | } 70 | } 71 | 72 | /** 73 | * Waits for the next pulse from the given reactive and returns it. Never returns immediately, 74 | * even if the reactive is currently emitting. Equivalent to `pause; await input` 75 | */ 76 | def awaitNext[B](input: Reactive[B, Any]): B @suspendable = { 77 | pause 78 | await(input) 79 | } 80 | 81 | /** 82 | * Halts the reactive and clears internal data structures. 83 | * Once halted, a reactive cannot resume. 84 | */ 85 | def halt: Unit @suspendable = shift { (k: Unit => Unit) => dispose() } 86 | 87 | /** 88 | * Repeatedly executes `body` until the given reactive `r` emits. 89 | * Immediately returns if `r` is currently emitting. Otherwise, it finishes the iteration 90 | * in which `r` emits, i.e., the execution of the body is always completed before 91 | * returning. Equivalent to 92 | * 93 | * {{{ 94 | * var done = false 95 | * var e: A 96 | * par { 97 | * e = next(es) 98 | * done = true 99 | * } { 100 | * while (!done) { body } 101 | * } 102 | * e 103 | * }}} 104 | */ 105 | def loopEndUntil[A](r: Reactive[A, Any])(body: => Unit @suspendable): A @suspendable = { 106 | var done = false 107 | par { 108 | await(r) 109 | done = true 110 | } { 111 | while (!done) { body } 112 | } 113 | //assert(r.isEmitting) 114 | r.getPulse 115 | } 116 | 117 | /** 118 | * Same as `loopEndUntil` but aborts the execution of `body` in the turn the given reactive 119 | * emits. Equivalent to 120 | * 121 | * {{{ 122 | * var e: A 123 | * abortOn(r) { 124 | * while (true) { body } 125 | * } 126 | * e 127 | * }}} 128 | */ 129 | def loopUntil[A](r: Reactive[A, Any])(body: => Unit @suspendable): A @suspendable = { 130 | abortOn(r) { 131 | while (true) { body } 132 | } 133 | //assert(r.isEmitting) 134 | r.getPulse 135 | } 136 | 137 | /** 138 | * Runs the given `body` until the given reactive emits. Does not run `body` if the reactive 139 | * is currently emitting. 140 | */ 141 | def abortOn[A](input: Reactive[A, Any])(body: => Unit @suspendable): Unit @suspendable = par { 142 | await(input) 143 | join 144 | } { 145 | body 146 | join 147 | } 148 | 149 | //private var _join = () => () 150 | private var _join: scala.runtime.IntRef = null 151 | 152 | /** 153 | * Halts the execution of the current branch and, if present, joins the innermost 154 | * enclosing `par` expression. Only halts the current branch if there is no enclosing `par`. 155 | */ 156 | def join: Unit @suspendable = shiftAndContinue[Unit] { k => 157 | if(_join == null) throw new IllegalStateException("Join called in wrong context.") 158 | _join.elem = 0 159 | } 160 | 161 | /** 162 | * First runs the `left` branch until it pauses, finishes or calls `join`. Then does 163 | * the same with the `right` branch. If both branches finish, or at least one branch called 164 | * `join`, this method immediately returns. If no branch called `join` and at least one branch 165 | * is not finished yet, this method pauses and next turn, will continue evaluating each branch 166 | * where it stopped previously. It proceeds so until both branches are finsihed or at least one 167 | * calls `join`. 168 | */ 169 | def par(left: => Unit @suspendable)(right: => Unit @suspendable): Unit @suspendable = 170 | shiftAndContinue[Unit] { exitK => 171 | val latch = new scala.runtime.IntRef(2) 172 | 173 | def cont(left: => Unit)(right: => Unit): Unit = { 174 | val oldLatch = _join // stack joins for nested pars 175 | _join = latch 176 | val leftK = continueWith(left) 177 | if (latch.elem > 0) { 178 | val rightK = continueWith(right) 179 | _join = oldLatch 180 | if (latch.elem > 0) { 181 | _continue = () => cont { leftK() } { rightK() } 182 | } else exitK() 183 | } else exitK() 184 | } 185 | 186 | cont { reset { left; latch.elem -= 1 } } { reset { right; latch.elem -= 1 } } 187 | } 188 | } 189 | 190 | trait SimpleFlowOps[P, V, R <: SimpleFlowReactive[P, V, R]] extends FlowOps { this: R => 191 | 192 | } 193 | 194 | trait SimpleFlowReactive[P, V, R <: SimpleFlowReactive[P, V, R]] extends OpaqueReactive[P, V] 195 | with FlowNode 196 | with SimpleReactive[P, V] 197 | with SimpleFlowOps[P, V, R] { this: R => 198 | override protected[react] def emit(p: Any) = super.emit(p) 199 | def react() { continue() } 200 | } 201 | 202 | trait SignalFlowOps[A] extends SimpleFlowOps[A, A, FlowSignal[A]] { this: FlowSignal[A] => 203 | def update(a: A): Unit @suspendable = emit(a) 204 | def previous = getValue 205 | } 206 | 207 | trait EventsFlowOps[A] extends SimpleFlowOps[A, Unit, FlowEvents[A]] { this: FlowEvents[A] => 208 | def <<(a: A): Unit @suspendable = emit(a) 209 | } 210 | 211 | abstract class FlowSignal[A](init: A) extends Signal[A] 212 | with SimpleFlowReactive[A, A, FlowSignal[A]] 213 | with SignalFlowOps[A] { 214 | pulse = init 215 | tick() 216 | 217 | def toStrict: Signal[A] = this 218 | 219 | override def reactiveDescriptor = "FlowSignal" 220 | } 221 | 222 | abstract class FlowEvents[A] extends Events[A] 223 | with SimpleFlowReactive[A, Unit, FlowEvents[A]] 224 | with EventsFlowOps[A] { 225 | tick() 226 | override def reactiveDescriptor = "FlowEvents" 227 | } 228 | 229 | object Reactor { 230 | def flow[A](op: FlowOps => Unit @suspendable): Reactor = new Reactor { 231 | def body = op(this) 232 | } 233 | 234 | /** 235 | * Creates a flow event that runs through the given `op` repeatedly until disposed, e.g., by 236 | * calling `halt`. 237 | */ 238 | def loop[A](op: FlowOps => Unit @suspendable): Reactor = new Reactor { 239 | def body = while (!isDisposed) op(this) 240 | } 241 | } 242 | 243 | abstract class Reactor extends FlowNode with LeafNode with FlowOps { 244 | tick() 245 | def react() { continue() } 246 | override def reactiveDescriptor = "Reactor" 247 | } 248 | } -------------------------------------------------------------------------------- /src/scala/react/ReactiveModule.scala: -------------------------------------------------------------------------------- 1 | package scala.react 2 | import java.lang.ref.WeakReference 3 | 4 | /** 5 | * Defines the base classes for the reactive framework. 6 | * 7 | * Note: The main backbone of the hierarchy is implemented in classes instead of traits for 8 | * efficiency reasons. 9 | */ 10 | abstract class ReactiveModule extends EngineModule { module: Domain => 11 | object MuteControl extends scala.util.control.ControlThrowable 12 | 13 | /** 14 | * Control flow operator similar to `break`. May only be used inside a `mutable` block inside of 15 | * a `Reactive` implementation and indicates that the current reactive may not emit in 16 | * the current turn. 17 | */ 18 | def mute: Nothing = throw MuteControl 19 | 20 | /** 21 | * Control flow operator similar to `breakable`. Retutrns `true` if `mute` was not called in `op`, 22 | * returns `false` otherwise. Some combinators use this internally, e.g., `Events.scanMutable`. 23 | */ 24 | @inline final def mutable[B](op: => B): Boolean = try { 25 | op 26 | true 27 | } catch { 28 | case MuteControl => false 29 | } 30 | 31 | /** 32 | * Runs the given `op` on maximum level in this turn, i.e., `op` is safe to query any other 33 | * node. 34 | */ 35 | def doLater(op: => Unit) { 36 | new DoLaterNode { def react() = op } 37 | } 38 | 39 | private abstract class DoLaterNode extends LeafNode { 40 | tick() 41 | } 42 | 43 | //type Dependents = PoolingWeakListSet[Node] 44 | //def newDependents(): Dependents = PoolingWeakListSet.empty 45 | 46 | abstract class Tickable { 47 | protected[react] def tick() 48 | } 49 | 50 | private object Node { 51 | final private val TickNumMask: Long = ~(TickInvalidMask | TickMuteMask) 52 | final private val TickInvalidMask: Long = 1L << 63 53 | final private val TickMuteMask: Long = 1L << 62 54 | } 55 | 56 | abstract class Node extends Tickable { 57 | /* lastTick indicates whether this node's pulse is valid (lastTick > 0) or invalid (< 0). Id 0 58 | * is reserved. This is used for multiple purposes. 59 | * When ticked, its sign bit is set, when validated the sign bit is cleared. Multiple ticks 60 | * are hence idempotent, and we can use this fact to make sure we don't put strict nodes on the 61 | * queue multiple times in the same turn. When emitting, lastTick is set to the current 62 | * turn id. Therefore, we can use this field to determine when this node has been emitting the 63 | * last time. 64 | */ 65 | private var tickBits: Long = 1L 66 | 67 | private[react] val refs = new WeakReference(this) 68 | 69 | import Node._ 70 | protected def isPulseValid = (tickBits & TickInvalidMask) == 0 71 | def lastTicked = tickBits & TickNumMask 72 | 73 | /** 74 | * Makes sure that the pulse of this node is consistent. Might be called multiple times in a 75 | * single turn. Calls to this method must happen from a level above this node. In other words, 76 | * if checkTopology() would throw an exception when this method is called, the behavior of 77 | * this method is undefined. 78 | */ 79 | protected def validatePulse() { 80 | if (!isPulseValid) { 81 | doValidatePulse() 82 | setPulseValid() 83 | } 84 | } 85 | 86 | protected def doValidatePulse() 87 | 88 | protected def setPulseValid() { 89 | if (!isPulseValid) setNotEmitting() 90 | } 91 | 92 | /** 93 | * Mark as invalid and return 'true' if this node has not been invalidated in this turn yet, 94 | * otherwise do nothing but return `false`. 95 | */ 96 | final protected[react] def invalidate(): Boolean = { 97 | val mask = engine.currentTurn | TickInvalidMask 98 | if (tickBits == mask) false 99 | else { 100 | tickBits = mask 101 | true 102 | } 103 | } 104 | 105 | def isEmitting: Boolean = tickBits == engine.currentTurn 106 | 107 | protected def setEmitting() { 108 | assert((engine.currentTurn & TickInvalidMask & TickMuteMask) == 0) // overflow 109 | tickBits = engine.currentTurn 110 | } 111 | protected def setNotEmitting() { 112 | tickBits = engine.currentTurn | TickMuteMask 113 | } 114 | 115 | engine.newNode(this) 116 | 117 | /** 118 | * The topological depth value of this node. 119 | * 120 | * Implementation note: this could be a def instead of a var for some nodes, which might reduce 121 | * object size. Benchmarking, however, revealed that this method then becomes a major bottleneck, 122 | * because of virtual calls and potential indirections because of traits and when accessing 123 | * the level of a dependency. Lesson learned: stick with a var for all nodes. 124 | */ 125 | protected[react] var level = 0 126 | 127 | def name: String = debug.getName(this) 128 | def name_=(nme: String): this.type = { debug.setName(this, nme); this } 129 | 130 | def engine: Engine = module.engine 131 | 132 | /** 133 | * Called exactly once per turn for all nodes in the propagation set. Not called, if not in the 134 | * propagation set. As this method may be called out of topolical order, implementations should 135 | * not access this node's dependencies. 136 | */ 137 | protected[react] def tick() 138 | 139 | /** 140 | * Ticks this nodes dependents. Does nothing, if or when this nodes does not have dependents. 141 | */ 142 | protected def tickDependents() 143 | 144 | /** 145 | * Throws an exception if accessed from a level <= the level of this node. No-op otherwise. 146 | */ 147 | def checkTopology() = 148 | if (level >= engine.currentLevel) engine.levelMismatch(this) 149 | 150 | /** 151 | * Makes sure that the value of this node is consistent. Might be called multiple times in a 152 | * single turn. Calls to this method must happen from a level above this node. In other words, 153 | * if checkTopology() would throw an exception when this method is called, the behavior of 154 | * this method is undefined. 155 | */ 156 | protected def validateValue() 157 | 158 | def isDisposed: Boolean = tickBits == 0 159 | 160 | /** 161 | * Disconnects this node from the dependency graph. 162 | * 163 | * Subclasses that override this method should always call `super.disconnect()` 164 | * so effects of this method are chained. 165 | */ 166 | protected def disconnect() { tickBits = 0 } 167 | 168 | def dispose() { 169 | disconnect() 170 | } 171 | 172 | def reactiveDescriptor: String = getClass.getName 173 | 174 | override def toString = { 175 | val nme = name 176 | val prefix = if (nme != null && nme != "") name + ":" else "" 177 | prefix + reactiveDescriptor + "@" + level 178 | } 179 | } 180 | 181 | /** 182 | * A node that does nothing. 183 | */ 184 | trait MuteNode extends Node { 185 | protected[react] def tick() {} 186 | override def isEmitting = false 187 | override protected def tickDependents() {} 188 | override def checkTopology() {} 189 | override protected def validatePulse() {} 190 | override def doValidatePulse() {} 191 | override protected def validateValue() {} 192 | } 193 | 194 | object NilNode extends MuteNode 195 | 196 | /** 197 | * A dependent node that is strictly evaluated, i.e., in every turn in which it is ticked. 198 | * 199 | * When ticked, it lets the engine decide when it is safe to evaluate. It does not tick 200 | * its dependents until the engine tells this node to evaluate. 201 | */ 202 | trait StrictNode extends Node { 203 | protected[react] def tick() { 204 | if (invalidate()) engine.defer(this) 205 | } 206 | 207 | /** 208 | * Called by the engine when this node defered itself in `tick`. Always called in topological 209 | * order, so implementations may safely access its dependencies. 210 | */ 211 | protected[react] def tock() { 212 | validatePulse() 213 | if (isEmitting) tickDependents() 214 | } 215 | } 216 | 217 | /** 218 | * A dependent node that is evaluated not before queried, i.e., it might not be evaluated 219 | * in every turn in which it was ticked. 220 | * 221 | * A node that when ticked, always invalidates itself and ticks its dependents. It evaluates not 222 | * before it is queried. 223 | */ 224 | trait LazyNode extends Node { 225 | invalidate() 226 | 227 | protected[react] def tick() { 228 | if (invalidate()) tickDependents() 229 | } 230 | } 231 | 232 | trait LeafNode extends StrictNode { 233 | level = Int.MaxValue 234 | protected def tickDependents() {} 235 | protected def isValueValid = true 236 | protected def doValidatePulse() { react(); setNotEmitting() } 237 | protected def validateValue() {} 238 | 239 | protected def react() 240 | } 241 | 242 | /** 243 | * A node that maintains a set of dependents, which are stored weakly. Methods that traverse 244 | * the dependent set clear it from stale weak refs. 245 | */ 246 | abstract class DependencyNode extends Node { 247 | // Null if empty, a single weak ref or an array of weak refs if not empty. 248 | // This turns out to be the fastest and one of the most space efficient ways to handle weak 249 | // dependencies (few indirections, no garbage as long as array is not resized). 250 | private var dependents: AnyRef = null 251 | 252 | // Stuff in here is all inlined, since it is absolutely performance critical 253 | 254 | /** 255 | * Tick all dependents of this node. 256 | */ 257 | protected def tickDependents() { 258 | dependents match { 259 | case null => 260 | case wref: WeakReference[Node] => 261 | val d = wref.get 262 | if ((d eq null) || !d.isInstanceOf[ManagesDependencies] /*|| d.isDisposed*/) dependents = null 263 | if (d ne null) d.tick() 264 | case arr: Array[WeakReference[Node]] => 265 | var i = 0 266 | while (i < arr.length) { 267 | val wref = arr(i) 268 | if (wref ne null) { 269 | val d = wref.get 270 | if ((d eq null) || !d.isInstanceOf[ManagesDependencies] /*|| d.isDisposed*/) { 271 | arr(i) = null 272 | } 273 | if (d ne null) d.tick() 274 | } 275 | i += 1 276 | } 277 | } 278 | } 279 | 280 | /** 281 | * Transitively hoist all dependents to new level. 282 | */ 283 | private[react] def hoistDependents(newLevel: Int) { 284 | // impl note: in contrast to tickDependents(), don't clear unmanaged dependencies 285 | dependents match { 286 | case null => 287 | case wref: WeakReference[Node] => 288 | val d = wref.get 289 | if (d eq null) dependents = null 290 | else engine.hoist(d, newLevel) 291 | case arr: Array[WeakReference[Node]] => 292 | var i = 0 293 | while (i < arr.length) { 294 | val wref = arr(i) 295 | if (wref ne null) { 296 | val d = wref.get 297 | if (d eq null) arr(i) = null 298 | else engine.hoist(d, newLevel) 299 | } 300 | i += 1 301 | } 302 | } 303 | } 304 | 305 | /** 306 | * Add the given node to this node's dependents, if not already present. 307 | */ 308 | protected[react] def subscribe(dep: Node) { 309 | if ((dep ne NilNode) && !isDisposed) { 310 | dependents match { 311 | case null => dependents = dep.refs 312 | case wref: WeakReference[Node] => 313 | if (wref eq dep.refs) return 314 | val d = wref.get 315 | if (d eq null) dependents = dep.refs 316 | else if (d eq dep) return 317 | else { 318 | val newDeps = new Array[WeakReference[Node]](4) 319 | newDeps(0) = wref 320 | newDeps(1) = dep.refs.asInstanceOf[WeakReference[Node]] 321 | dependents = newDeps 322 | } 323 | case arr: Array[WeakReference[Node]] => 324 | val oldSize = arr.length 325 | var i = 0 326 | while (i < oldSize) { 327 | val wref = arr(i) 328 | if (wref eq dep.refs) return 329 | if (wref eq null) { 330 | arr(i) = dep.refs.asInstanceOf[WeakReference[Node]] 331 | return 332 | } /*else { 333 | val d = wref.get 334 | if(d == null || d.isDisposed) { 335 | arr(i) = dep.refs.asInstanceOf[WeakReference[Node]] 336 | return 337 | } 338 | }*/ 339 | i += 1 340 | } 341 | 342 | 343 | 344 | val newDeps = new Array[WeakReference[Node]](oldSize * 2) 345 | compat.Platform.arraycopy(arr, 0, newDeps, 0, oldSize) 346 | newDeps(oldSize) = dep.refs.asInstanceOf[WeakReference[Node]] 347 | dependents = newDeps 348 | } 349 | } 350 | } 351 | 352 | /** 353 | * Removes the given node from this node's dependent set if present. 354 | */ 355 | protected[react] def unsubscribe(dep: Node) { 356 | if (dep ne NilNode) { 357 | dependents match { 358 | case null => 359 | case wref: WeakReference[Node] => 360 | val d = wref.get 361 | if ((d eq null) || (d eq dep)) dependents = null 362 | case arr: Array[WeakReference[Node]] => 363 | var i = 0 364 | while (i < arr.length) { 365 | val wref = arr(i) 366 | if (wref ne null) { 367 | val d = wref.get 368 | if (d eq null) arr(i) = null 369 | else if (d eq dep) { 370 | arr(i) = null 371 | return 372 | } 373 | } 374 | i += 1 375 | } 376 | } 377 | } 378 | } 379 | 380 | protected override def disconnect() { 381 | dependents = null 382 | super.disconnect() 383 | } 384 | } 385 | 386 | /** 387 | * A marker trait indicating that this node takes care of subscribing and unsubscribing itself to its 388 | * dependencies. 389 | * 390 | * Implementation note: do not turn this trait into a boolean method. An instanceof check 391 | * is much faster than a virtual trait call. 392 | */ 393 | trait ManagesDependencies extends Node 394 | 395 | /** 396 | * Convenience implementation for a dependent with a single input. 397 | */ 398 | trait Dependent1[+P, +V] extends Node with ManagesDependencies { 399 | protected def input: Reactive[P, V] 400 | //level = input.level + 1 401 | 402 | protected def connect() { 403 | input.subscribe(this) 404 | } 405 | 406 | protected override def disconnect() { 407 | input.unsubscribe(this) 408 | super.disconnect() 409 | } 410 | } 411 | 412 | trait Dependent2[+P, +V] extends Node with ManagesDependencies { 413 | protected def input1: Reactive[P, V] 414 | protected def input2: Reactive[P, V] 415 | //level = math.max(input1.level, input2.level) + 1 416 | 417 | protected def connect() { 418 | input1.subscribe(this) 419 | input2.subscribe(this) 420 | } 421 | 422 | protected override def disconnect() { 423 | input1.unsubscribe(this) 424 | input2.unsubscribe(this) 425 | super.disconnect() 426 | } 427 | } 428 | 429 | trait DependentN extends Node with ManagesDependencies { 430 | protected def inputs: Iterable[Reactive[Any, Any]] 431 | // level = inputs.foldLeft(0) { (l, r) => math.max(l, r.level) } + 1 432 | 433 | protected def connect() { 434 | inputs foreach { _ subscribe this } 435 | } 436 | 437 | protected override def disconnect() { 438 | inputs foreach { _ unsubscribe this } 439 | super.disconnect() 440 | } 441 | } 442 | 443 | /** 444 | * A node that emits pulses and holds values. It has three properties that can change from turn 445 | * to turn and are updated either strictly or lazily: 446 | * 447 | * - whether this reactive is emitting 448 | * - if it is emitting, what pulse it is emitting 449 | * - what value it currently holds. 450 | * 451 | * A reactive must change its value only in a turn in which it is also emitting. It can, 452 | * however, choose not to change its value when it is emitting. 453 | */ 454 | abstract class Reactive[+P, +V] extends DependencyNode with Emitter[P, V] { 455 | /** 456 | * Returns the value of this reactive if defined. The result is undefined, if this reactive 457 | * isn't currently emitting. 458 | */ 459 | def getValue: V 460 | 461 | def valueNow: Option[V] = ifDefinedElse[Option[V]](Some(_))(None) 462 | 463 | def isDefined: Boolean 464 | 465 | protected def validateValue() { 466 | if (!isValueValid) doValidateValue() 467 | } 468 | 469 | protected def doValidateValue() 470 | protected def isValueValid: Boolean 471 | 472 | @inline final def ifDefined[U](f: V => U) { 473 | checkTopology() 474 | validateValue() 475 | if (isDefined) f(getValue) 476 | } 477 | 478 | @inline final def ifDefinedElse[B](f: V => B)(default: => B): B = { 479 | checkTopology() 480 | validateValue() 481 | if (isDefined) f(getValue) else default 482 | } 483 | 484 | /** 485 | * Returns a pulse value which is only valid if this reactive is currently emitting. 486 | * The result is undefined (it may throw an exception), if this reactive isn't currently 487 | * emitting. 488 | */ 489 | def getPulse: P 490 | 491 | def pulseNow: Option[P] = ifEmittingElse[Option[P]](Some(_))(None) 492 | 493 | /** 494 | * Runs the given function with the current value of this pulse, if this pulse is currently 495 | * present. 496 | */ 497 | @inline final def ifEmitting[U](f: P => U) { 498 | checkTopology() 499 | validatePulse() 500 | assert(isPulseValid) 501 | if (isEmitting) f(getPulse) 502 | } 503 | 504 | /** 505 | * Runs the given function with the current value of this pulse and return the result, 506 | * if this pulse is currently present. Otheriwse, returns the given default value. 507 | */ 508 | @inline final def ifEmittingElse[B](f: P => B)(default: => B): B = { 509 | checkTopology() 510 | validatePulse() 511 | assert(isPulseValid) 512 | if (isEmitting) f(getPulse) else default 513 | } 514 | 515 | def changes: Events[P] = new ChangeEvents[P](this) 516 | 517 | override def toString = { 518 | val s = if (isPulseValid) "" else "" 519 | val p = if (isEmitting) "p=" + getPulse else "" 520 | val v = if (isDefined) "v=" + getValue else "" 521 | super.toString + "(" + s + "," + p + "," + v + ")" 522 | } 523 | } 524 | 525 | /** 526 | * A reactive that always has a value, i.e., for which `isDefined == true` always holds. 527 | */ 528 | abstract class TotalReactive[+P, +V] extends Reactive[P, V] { 529 | def now: V = { 530 | checkTopology() 531 | validateValue() 532 | getValue 533 | } 534 | 535 | def apply(): V = { 536 | checkTopology() 537 | subscribe(depStackTop) 538 | validateValue() 539 | getValue 540 | } 541 | 542 | def isDefined: Boolean = true 543 | } 544 | 545 | /** 546 | * A standard interface for an implementation of a reactive that can emit pulses. Any reactive 547 | * that emits pulses must extend this trait or one of its subtraits. 548 | */ 549 | trait Emitter[+P, +V] extends Node { this: Reactive[P, V] => 550 | /** 551 | * Sets this reactive's state to 'not emitting'. 552 | */ 553 | protected[this] def mute() { 554 | assert(!isEmitting) 555 | freePulse() 556 | setNotEmitting() 557 | } 558 | 559 | /** 560 | * Lets this reactive emit the given pulse, i.e., sets the pulse and the state to 'emitting'. 561 | * 562 | * Unfortunately, this needs to stay untyped until SI-3272 is fixed, if that'll ever happen. 563 | */ 564 | protected[this] def emit(p: Any) 565 | 566 | /** 567 | * Frees garbage from pulse 568 | */ 569 | protected[react] def freePulse() 570 | } 571 | 572 | /** 573 | * An emitter whose pulses represent the current value as opposed to a change delta. Therefore, 574 | * the current value depends on the most recent pulse only and not on a previous value or pulse. 575 | */ 576 | trait SimpleEmitter[+P, +V] extends Emitter[P, V] { this: Reactive[P, V] => 577 | // TODO: I'd like to move most of that stuff here to Emitter, but that triggers SI-3272 or 578 | // some variation. 579 | protected[this] var pulse: P = _ 580 | 581 | def getPulse = pulse 582 | 583 | protected[this] def emit(p: Any) { 584 | //val oldPulse = getPulse 585 | //val oldValue = getValue 586 | pulse = p.asInstanceOf[P] 587 | val doEmit = true //!isDefined //|| shouldEmit(oldValue, getPulse) 588 | if (doEmit) setEmitting() 589 | //else { 590 | // pulse = oldPulse 591 | // mute() 592 | // } 593 | } 594 | 595 | protected[react] def freePulse() { pulse = null.asInstanceOf[P] } 596 | } 597 | 598 | /** 599 | * An emitter whose pulses represent a delta to its previous value. Therefore, the current value 600 | * depends on the most recent pulse and a previous value. 601 | */ 602 | trait DeltaEmitter[+P, +V] extends Emitter[P, V] { this: Reactive[P, V] => 603 | 604 | } 605 | 606 | trait SimpleReactive[+P, +V] extends Reactive[P, V] with SimpleEmitter[P, V] { 607 | protected def isValueValid = isPulseValid 608 | 609 | protected def doValidateValue() { doValidatePulse() } 610 | } 611 | 612 | trait DeltaReactive[+P, +V] extends Reactive[P, V] with DeltaEmitter[P, V] 613 | 614 | /** 615 | * A reactive that accesses dependencies in a manner unpredictable by the framework. Usually 616 | * this means it evaluates some closure when notified, such as a `FuncSignal`. 617 | */ 618 | trait OpaqueReactive[P, V] extends Reactive[P, V] { 619 | level = math.max(1, level) 620 | 621 | protected def doValidatePulse() { 622 | depStackPush(this) 623 | try { 624 | // eval and let reactives consulted in `op` obtain our dependent 625 | react() 626 | } finally { 627 | // always leave the dependent stack in a clean state. 628 | depStackPop() 629 | } 630 | } 631 | 632 | protected def react() 633 | } 634 | 635 | /** 636 | * Objects that create observers must inherit this trait. Maintains an internal list of created 637 | * observers. Once an instance of this trait is disposed, all it observers it maintains are 638 | * disposed. 639 | * 640 | * It is safe to create instances of this trait off-turn. Observers must be created in a turn, 641 | * however. 642 | */ 643 | trait Observing { outer => 644 | private var _obRefs: AnyRef = null 645 | 646 | protected abstract class Observer extends LeafNode { 647 | ref() 648 | 649 | protected override def disconnect() { 650 | super.disconnect() 651 | unref() 652 | } 653 | 654 | protected def ref() { 655 | _obRefs = _obRefs match { 656 | case null => this 657 | case ob: Observer => collection.immutable.Set(ob, this) 658 | case obs: Set[_] => obs.asInstanceOf[Set[Observer]] + this 659 | case _ => sys.error("Impossible case") 660 | } 661 | } 662 | 663 | protected def unref() { 664 | _obRefs = _obRefs match { 665 | case null => null 666 | case x if x eq this => null 667 | case obs: Set[_] => obs.asInstanceOf[Set[Observer]] - this 668 | case _ => sys.error("Impossible case") 669 | } 670 | } 671 | 672 | override def reactiveDescriptor = "Observer@" + System.identityHashCode(this) 673 | } 674 | 675 | class Observer1[P, V](val input: Reactive[P, V], op: P => Unit) extends Observer with Dependent1[P, V] { 676 | input.subscribe(this) 677 | def react() { 678 | if (!isDisposed) input.ifEmitting(op) 679 | } 680 | } 681 | 682 | class ObserverOnce1[P, V](input: Reactive[P, V], op: P => Unit) extends Observer1[P, V](input, op) { 683 | override def react() { 684 | super.react() 685 | disconnect() 686 | } 687 | } 688 | 689 | class ObserverDynamic(op: => Unit) extends Observer { 690 | def react() { 691 | depStackPush(this) 692 | try { 693 | op 694 | } finally { 695 | depStackPop() 696 | } 697 | } 698 | } 699 | 700 | /** 701 | * Observes pulses from the given reactive, starting in the current turn. 702 | */ 703 | protected def observe[P, V](r: Reactive[P, V])(op: P => Unit): Observer = { 704 | val ob = new Observer1(r, op) 705 | ob.tick() 706 | ob 707 | } 708 | 709 | /** 710 | * Observes exactly one pulse from the given reactive, starting in the current turn. 711 | */ 712 | protected def observeOnce[P, V](r: Reactive[P, V])(op: P => Unit): Observer = { 713 | new ObserverOnce1(r, op) 714 | } 715 | 716 | /** 717 | * Runs `op` once, which might access signals through `Signal.apply`. Repeatedly runs `op` 718 | * when some of the signals accessed change. Might run `op` even if no signal has changed. 719 | */ 720 | protected def observeDynamic(op: => Unit): Observer = { 721 | val ob = new ObserverDynamic(op) 722 | ob.tick() 723 | ob 724 | } 725 | } 726 | 727 | trait Source[P, V] extends Reactive[P, V] { 728 | protected[this] def owner: Owner 729 | level = owner.level 730 | protected[react] def channel: Channel[P] 731 | } 732 | 733 | trait SimpleSource[P, V] extends SimpleReactive[P, V] with Source[P, V] { 734 | protected def doValidatePulse() { 735 | val x = channel.get(this) 736 | emit(x) 737 | } 738 | } 739 | 740 | trait Channel[P] { 741 | def get(r: Reactive[P, Any]): P 742 | def push(r: Reactive[P, Any], p: P) 743 | } 744 | 745 | sealed trait Owner { 746 | protected[react] def level: Int 747 | def newChannel[P, V](r: Reactive[P, V], p: P): Channel[P] 748 | } 749 | 750 | object DomainOwner extends Owner { 751 | def level = 0 752 | def newChannel[P, V](r: Reactive[P, V], p: P): Channel[P] = new LocalDelayChannel(r, p) 753 | } 754 | 755 | abstract class Router(inputNodes: Reactive[Any, Any]*) extends DependentN with StrictNode with Owner { 756 | protected val inputs = inputNodes.toSeq 757 | connect() 758 | protected implicit def owner: Owner = this 759 | 760 | invalidate() 761 | 762 | def newChannel[P, V](r: Reactive[P, V], p: P): Channel[P] = null 763 | 764 | protected def tickDependents() {} 765 | 766 | protected def doValidatePulse() { 767 | react() 768 | } 769 | 770 | protected def validateValue() {} 771 | 772 | //def get[P](r: Reactive[P, Any]): P = r.getPulse 773 | //def push[P](r: Reactive[P, Any], p: P) = r emit p 774 | 775 | def react() 776 | } 777 | 778 | implicit def owner: Owner = DomainOwner 779 | } -------------------------------------------------------------------------------- /src/scala/react/SchedulerModule.scala: -------------------------------------------------------------------------------- 1 | package scala.react 2 | 3 | import java.util.concurrent.atomic.AtomicBoolean 4 | import java.util.concurrent.{ Executors, ExecutorService } 5 | import javax.swing.SwingUtilities 6 | 7 | trait SchedulerModule { self: Domain => 8 | 9 | 10 | def schedule(r: Runnable) { engine.schedule(r) } 11 | def schedule(op: => Unit) { engine.schedule(op) } 12 | 13 | /** 14 | * The scheduler is responsible for scheduling propagation turns. Turns are not necessarily run 15 | * immediately, but are scheduled with `ensureTurnIsScheduled()`. 16 | */ 17 | abstract class Scheduler { 18 | /** 19 | * Ensure that a turn will be started at the discretion of this scheduler. 20 | * Repeated invocations of this method before the turn is started may not result 21 | * in additional turns being scheduled. 22 | */ 23 | def ensureTurnIsScheduled() 24 | 25 | /** 26 | * Starts this scheduler. Called exactly once before any other method on this 27 | * scheduler is invoked. 28 | */ 29 | def start() { 30 | ensureTurnIsScheduled() 31 | } 32 | } 33 | 34 | class ManualScheduler extends Scheduler { 35 | def ensureTurnIsScheduled() {} 36 | } 37 | 38 | /** 39 | * A scheduler with a thread-safe API. 40 | */ 41 | abstract class ThreadSafeScheduler extends Scheduler { 42 | private val isScheduled = new AtomicBoolean(false) 43 | 44 | private val runnable = new Runnable { 45 | def run { 46 | // TODO: do we really need to CAS twice? 47 | if (isScheduled.compareAndSet(true, false)) engine.runTurn() 48 | } 49 | } 50 | 51 | def ensureTurnIsScheduled() { 52 | if (isScheduled.compareAndSet(false, true)) { 53 | schedule(runnable) 54 | } 55 | } 56 | 57 | /** 58 | * To be implemented by subclasses. 59 | * 60 | * This uses a Runnable and not a Scala closure to avoid wrapping them when interacting 61 | * with existing Java frameworks. 62 | */ 63 | protected def schedule(r: Runnable) 64 | } 65 | 66 | /** 67 | * A scheduler running turns on a java.util.concurrent thread-pool. 68 | */ 69 | class ThreadPoolScheduler(pool: ExecutorService) extends ThreadSafeScheduler { 70 | def this() = this(Executors.newCachedThreadPool()) 71 | protected def schedule(r: Runnable) = pool.execute(r) 72 | } 73 | 74 | /** 75 | * A scheduler running turns on the Swing event dispatcher thread (EDT). 76 | */ 77 | class SwingScheduler extends ThreadSafeScheduler { 78 | def schedule(r: Runnable) = SwingUtilities.invokeLater(r) 79 | } 80 | } -------------------------------------------------------------------------------- /src/scala/react/SignalModule.scala: -------------------------------------------------------------------------------- 1 | package scala.react 2 | 3 | import scala.util.continuations._ 4 | 5 | trait SignalModule { module: Domain => 6 | def Strict[A](op: =>A): Signal[A] = new StrictOpSignal(op) 7 | def Lazy[A](op: =>A): Signal[A] = new LazyOpSignal(op) 8 | 9 | object Signal { 10 | /** 11 | * Creates a flow signal that runs through the given `op` once. 12 | */ 13 | def flow[A](init: A)(op: SignalFlowOps[A]=>Unit @suspendable): Signal[A] = new FlowSignal(init) { 14 | def body = op(this) 15 | } 16 | 17 | /** 18 | * Creates a flow signal that runs through the given `op` repeatedly until disposed, e.g., by 19 | * calling `halt`. 20 | */ 21 | def loop[A](init: A)(op: SignalFlowOps[A]=>Unit @suspendable): Signal[A] = new FlowSignal(init) { 22 | def body = while(!isDisposed) op(this) 23 | } 24 | } 25 | 26 | /** 27 | * A time-varying value. 28 | */ 29 | abstract class Signal[+A] extends TotalReactive[A, A] with SimpleReactive[A, A] { outer => 30 | /* A signal's pulse and value are really the same, except the flag that indicates whether 31 | * the signal is emitting or not. 32 | */ 33 | def getValue: A = getPulse 34 | 35 | def toStrict: Signal[A] 36 | 37 | // Fix types, when/if SI-3272 gets fixed 38 | protected def shouldEmit(oldVal: Any, newVal: Any): Boolean = oldVal != newVal 39 | 40 | override protected[this] def emit(p: Any) { 41 | if(shouldEmit(getValue, p)) super.emit(p) 42 | } 43 | 44 | override protected[react] def freePulse() { 45 | // the pulse is the value for signals, so don't do anything here. 46 | } 47 | } 48 | 49 | abstract class FuncSignal[A] extends Signal[A] with OpaqueReactive[A, A] { 50 | protected def react() { 51 | val v = eval 52 | emit(v) 53 | } 54 | 55 | protected def eval: A 56 | } 57 | 58 | abstract class StrictFuncSignal[A] extends FuncSignal[A] with StrictNode { 59 | tick() 60 | 61 | def toStrict: Signal[A] = this 62 | } 63 | 64 | class StrictOpSignal[A](op: => A) extends StrictFuncSignal[A] { 65 | protected def eval = op 66 | 67 | override def reactiveDescriptor = "Strict Signal" 68 | } 69 | 70 | abstract class LazyFuncSignal[A] extends FuncSignal[A] with LazyNode 71 | 72 | class LazyOpSignal[A](op: => A) extends LazyFuncSignal[A] { 73 | protected def eval = op 74 | 75 | def toStrict: Signal[A] = new StrictOpSignal(op) 76 | } 77 | 78 | /** 79 | * A signal with a single input. 80 | */ 81 | abstract class Signal1[+A, +B](protected val input: Reactive[A, Any]) 82 | extends Signal[B] 83 | with Dependent1[A, Any] { 84 | 85 | input.subscribe(this) 86 | 87 | def doValidatePulse() { 88 | input.ifEmitting(pulsate _) 89 | setPulseValid() 90 | } 91 | protected[this] def pulsate(a: A) 92 | } 93 | 94 | abstract class StrictSignal1[+A, +B](input: Reactive[A, Any]) 95 | extends Signal1[A, B](input) 96 | with StrictNode 97 | 98 | protected[this] class HoldSignal[P](init: P)(input: Reactive[P, Any]) extends StrictSignal1[P, P](input) { 99 | pulse = init 100 | 101 | def pulsate(p: P) { emit(p) } 102 | 103 | def toStrict = this 104 | } 105 | 106 | object Var { 107 | /** 108 | * Creates a mutable signal with the given owner. 109 | */ 110 | def apply[A](init: A)(implicit owner: Owner): Var[A] = new Var(init, owner) 111 | } 112 | 113 | /** 114 | * An externally mutable signal. 115 | */ 116 | class Var[A] protected(init: A, val owner: Owner) extends Signal[A] with SimpleSource[A, A] with StrictNode { 117 | pulse = init 118 | 119 | protected[react] val channel = owner.newChannel(this, init) 120 | 121 | def toStrict = this 122 | 123 | /** 124 | * Sets the value of this signal, either in the next turn, if the owner is the domain or 125 | * in the current turn if the owner is a router. 126 | */ 127 | def update(a: A) { 128 | if(owner eq DomainOwner) channel.push(this, a) 129 | else { 130 | emit(a) 131 | if(isEmitting) engine.defer(this) 132 | } 133 | } 134 | 135 | override def reactiveDescriptor = "Var" 136 | } 137 | 138 | /** 139 | * A constant signal. 140 | */ 141 | case class Val[A](a: A) extends Signal[A] with MuteNode { 142 | pulse = a 143 | 144 | def toStrict = this 145 | override def reactiveDescriptor = "Val" 146 | } 147 | } -------------------------------------------------------------------------------- /src/scala/react/TopoQueue.scala: -------------------------------------------------------------------------------- 1 | package scala.react 2 | import java.util.Arrays 3 | 4 | abstract class PropQueue[A >: Null: Manifest] { 5 | def clear() 6 | def dequeue(): A 7 | def isEmpty: Boolean 8 | def +=(elem: A) 9 | def reinsert(elem: A) 10 | } 11 | 12 | /** 13 | * A slightly specialized priority queue. 14 | */ 15 | abstract class PriorityQueue[A >: Null: Manifest] extends PropQueue[A] { 16 | 17 | private var array = new Array[A](16) 18 | private var size0 = 1 // array(0) unused 19 | 20 | protected def ensureSize(n: Int) { 21 | if (n > array.length) { 22 | var newsize = array.length * 2 23 | while (n > newsize) 24 | newsize = newsize * 2 25 | 26 | val newar = new Array[A](newsize) 27 | compat.Platform.arraycopy(array, 0, newar, 0, size0) 28 | array = newar 29 | } 30 | } 31 | 32 | protected def swap(a: Int, b: Int) { 33 | val h = array(a) 34 | array(a) = array(b) 35 | array(b) = h 36 | } 37 | 38 | protected def fixUp(m: Int): Unit = { 39 | val as = array 40 | var k = m 41 | while (k > 1 && priority(as(k / 2)) > priority(as(k))) { 42 | swap(k, k / 2) 43 | k = k / 2 44 | } 45 | } 46 | 47 | protected def fixDown(m: Int, n: Int): Unit = { 48 | val as = array 49 | var k = m 50 | while (n >= 2 * k) { 51 | var j = 2 * k 52 | if (j < n && priority(as(j)) > priority(as(j + 1))) 53 | j += 1 54 | if (priority(as(k)) <= priority(as(j))) 55 | return 56 | else { 57 | swap(k, j) 58 | k = j 59 | } 60 | } 61 | } 62 | 63 | def +=(elem: A) { 64 | ensureSize(size0 * 3/2 + 1) 65 | array(size0) = elem 66 | fixUp(size0) 67 | size0 += 1 68 | } 69 | 70 | def dequeue(): A = if (size0 > 1) { 71 | size0 -= 1 72 | val res = array(1) 73 | array(1) = array(size0) 74 | array(size0) = null // need to clear, don't want to keep nodes alive longer than necessary 75 | fixDown(1, size0 - 1) 76 | res 77 | } else 78 | throw new NoSuchElementException("no element to remove from heap") 79 | 80 | def priority(a: A): Int 81 | 82 | def isEmpty = size0 == 1 83 | def clear() { size0 = 1 } 84 | 85 | /*private def remove(elem: A) { 86 | var i = 1 87 | while(i < size0) { 88 | if (array(i) == elem) { 89 | size0 -= 1 90 | array(i) = null 91 | if(i == size0) { 92 | return 93 | } else { 94 | array(i) = null 95 | } 96 | compat.Platform.arraycopy(array, i+1, array, i, size0-i) 97 | 98 | } 99 | return 100 | } 101 | }*/ 102 | 103 | private def indexOf(elem: A): Int = { 104 | var i = 1 105 | while(i < size0) { 106 | if (array(i) == elem) return i 107 | i += 1 108 | } 109 | -1 110 | } 111 | 112 | def reinsert(elem: A) { 113 | //remove(elem) 114 | val idx = indexOf(elem) 115 | if(idx == -1) this += elem 116 | else { 117 | fixDown(idx, size0-1) 118 | } 119 | 120 | } 121 | 122 | override def toString() = 123 | "PrioQueue" + array.mkString("[", ",", "]") 124 | } 125 | 126 | /** 127 | * A fast, special purpose priority queue. Priorities are topological depth values. 128 | * This queue assumes that between initialization and `clear` calls, elements are inserted with 129 | * monotonically increasing depths as it happens during topologically ordered graph traversal 130 | * (it might and probaly will crash otherwise). 131 | */ 132 | abstract class TopoQueue[A >: Null: Manifest] extends PropQueue[A] { 133 | private var curDepth, curIndex, maxDepth = 0 134 | private var sizes: Array[Int] = _ 135 | private var levels: Array[Array[A]] = _ 136 | private var maxLevelSize = 0 137 | private var maxLevel: Array[A] = _ 138 | 139 | compact() 140 | 141 | def compact() { 142 | sizes = Array.fill(initDepth)(0) 143 | levels = Array.fill(initDepth) { new Array[A](initWidth) } 144 | maxLevel = new Array[A](initWidth) 145 | } 146 | 147 | def initDepth = 2 148 | def initWidth = 2 149 | 150 | /** 151 | * The depth value of the given element. To be implemented by subclasses. 152 | */ 153 | def depth(a: A): Int 154 | 155 | def clear() { 156 | assert(isEmpty) 157 | curDepth = 0 158 | curIndex = 0 159 | maxDepth = 0 160 | maxLevelSize = 0 161 | // it should be slightly faster to do this here than to adapt sizes during traversal: 162 | Arrays.fill(sizes, 0) 163 | } 164 | 165 | def +=(a: A) { 166 | val d = depth(a) 167 | assert(d >= curDepth) 168 | if (d < Int.MaxValue) { 169 | ensureDepthCapacity(d) 170 | val l = ensureLevelCapacity(d) 171 | l(sizes(d)) = a 172 | sizes(d) += 1 173 | if (maxDepth < d) maxDepth = d 174 | } else { 175 | val l = ensureLevelCapacity(d) 176 | l(maxLevelSize) = a 177 | maxLevelSize += 1 178 | } 179 | } 180 | 181 | /** 182 | * Removes the element with the least depth and makes sure that this queue 183 | * does not hold on to its reference. 184 | */ 185 | def dequeue(): A = { 186 | assert(!isEmpty) 187 | while (curDepth < sizes.length && curIndex >= sizes(curDepth)) { 188 | curDepth += 1 189 | curIndex = 0 190 | } 191 | val level = if (curDepth < sizes.length) levels(curDepth) else { curDepth = Int.MaxValue; maxLevel } 192 | val a = level(curIndex) 193 | assert(a != null) 194 | level(curIndex) = null 195 | curIndex += 1 196 | a 197 | } 198 | 199 | /** 200 | * `true` if there are more elements after the current index. 201 | */ 202 | def isEmpty: Boolean = if (maxLevelSize == 0) 203 | (curDepth == maxDepth) && (curIndex == sizes(curDepth)) 204 | else (curDepth == Int.MaxValue) && (curIndex == maxLevelSize) 205 | 206 | // ensure that level d has capacity to store new elements 207 | private def ensureLevelCapacity(d: Int): Array[A] = 208 | if (d == Int.MaxValue) { 209 | val arr = maxLevel 210 | val oldSize = arr.length 211 | if (oldSize <= maxLevelSize) { 212 | var newsize = oldSize * 2 213 | 214 | val newArr = new Array[A](newsize) 215 | compat.Platform.arraycopy(arr, 0, newArr, 0, oldSize) 216 | maxLevel = newArr 217 | newArr 218 | } else arr 219 | } else { 220 | val arr = levels(d) 221 | val oldSize = arr.length 222 | if (oldSize <= sizes(d)) { 223 | var newsize = oldSize * 2 224 | 225 | val newArr = new Array[A](newsize) 226 | compat.Platform.arraycopy(arr, 0, newArr, 0, oldSize) 227 | levels(d) = newArr 228 | newArr 229 | } else arr 230 | } 231 | 232 | // ensure that we have levels allocated with indices at least up to d 233 | private def ensureDepthCapacity(d: Int) = { 234 | val arr = levels 235 | val oldSize = arr.length 236 | if (d >= oldSize) { 237 | var newSize = oldSize * 2 238 | while (d >= newSize) newSize *= 2 239 | 240 | val newArr = new Array[Array[A]](newSize) 241 | compat.Platform.arraycopy(arr, 0, newArr, 0, oldSize) 242 | 243 | val newSizeArr = new Array[Int](newSize) 244 | compat.Platform.arraycopy(sizes, 0, newSizeArr, 0, oldSize) 245 | 246 | // fill new levels and sizes with fresh arrays 247 | var i = oldSize 248 | while (i < newSize) { 249 | newArr(i) = new Array[A](initWidth) 250 | i += 1 251 | } 252 | levels = newArr 253 | sizes = newSizeArr 254 | } 255 | } 256 | 257 | def reinsert(elem: A) = sys.error("not implemented") 258 | } -------------------------------------------------------------------------------- /src/scala/react/monitor/IO.scala: -------------------------------------------------------------------------------- 1 | package scala.react.monitor 2 | 3 | import java.io._ 4 | import java.nio.channels.FileChannel 5 | import java.nio.{ByteBuffer, MappedByteBuffer} 6 | 7 | object IO { 8 | def readChannel(file: File): FileChannel = new RandomAccessFile(file, "r").getChannel 9 | def writeChannel(file: File): FileChannel = new RandomAccessFile(file, "rw").getChannel 10 | 11 | def mapForReading(ch: FileChannel, offset: Long, size: Int): MappedByteBuffer = 12 | ch.map(FileChannel.MapMode.READ_ONLY, offset, size) 13 | 14 | def mapForWriting(ch: FileChannel, offset: Long, size: Int): MappedByteBuffer = 15 | ch.map(FileChannel.MapMode.READ_WRITE, offset, size) 16 | 17 | def newLogFile: File = File.createTempFile("scalareact", "log") 18 | 19 | def unmap(buf: ByteBuffer) { 20 | buf match { 21 | case buf : sun.nio.ch.DirectBuffer => 22 | buf.cleaner.clean() 23 | case _ => 24 | throw new RuntimeException("Cannot unmap given buffer") 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/scala/react/monitor/LogDebug.scala: -------------------------------------------------------------------------------- 1 | package scala.react 2 | package monitor 3 | 4 | import java.io._ 5 | import java.nio.MappedByteBuffer 6 | import java.nio.BufferOverflowException 7 | import java.nio.channels.FileChannel 8 | import java.util.WeakHashMap 9 | import scala.collection.mutable.ArrayBuffer 10 | 11 | class LogDebug[D <: Domain](dom: D) extends AbstractDebug(dom) { 12 | import domain._ 13 | import LogFormat._ 14 | 15 | private val log = new PagedWriter(IO.newLogFile) 16 | private val header = IO.mapForWriting(log.channel, 0, 8) 17 | 18 | private val ids = new WeakHashMap[Node, Long] 19 | private var idCounter = 0L // 0 is reserved 20 | private var idSize = 1.toByte // in bytes 21 | private def uniqueId(n: Node): Long = { 22 | var id = ids.get(n) 23 | if (id == 0) { 24 | idCounter += 1 25 | id = idCounter 26 | ids.put(n, id) 27 | 28 | if (id <= Byte.MaxValue) { 29 | writeTag(NewNodeTag) 30 | log putByte id.toByte 31 | } else if (id <= Short.MaxValue) { 32 | if (idSize == Byte.MaxValue + 1) { 33 | idSize = 2 34 | writeTag(NewNodeGenTag) 35 | log putByte idSize 36 | } 37 | writeTag(NewNodeTag) 38 | log putShort id.toShort 39 | } else if (id <= Int.MaxValue) { 40 | if (idSize == Short.MaxValue + 1) { 41 | idSize = 4 42 | writeTag(NewNodeGenTag) 43 | log putByte idSize 44 | } 45 | writeTag(NewNodeTag) 46 | log putInt id.toInt 47 | } else { 48 | if (idSize == Int.MaxValue + 1) { 49 | idSize = 8 50 | writeTag(NewNodeGenTag) 51 | log putByte idSize 52 | } 53 | writeTag(NewNodeTag) 54 | log putLong id 55 | } 56 | } 57 | id 58 | } 59 | 60 | private def writeTag(tag: Byte) { 61 | log putByte tag 62 | } 63 | 64 | private def writeNodeId(id: Long) { 65 | idSize match { 66 | case 1 => log putByte id.toByte 67 | case 2 => log putShort id.toShort 68 | case 4 => log putInt id.toInt 69 | case 8 => log putLong id 70 | } 71 | } 72 | 73 | private var t0 = 0L 74 | private var t = 0L 75 | private var lastSnapshot = 0L 76 | 77 | private def time() = System.nanoTime - t0 78 | 79 | def logStart() { 80 | val startTime = System.currentTimeMillis 81 | t0 = time() 82 | writeHeader(0) 83 | log putLong startTime 84 | } 85 | 86 | def writeHeader(l: Long) { 87 | header.rewind() 88 | header putLong l 89 | } 90 | 91 | def logEnterTurn(id: Long) = { 92 | t = time() 93 | log putLong id 94 | log putLong t 95 | } 96 | def logLeaveTurn(id: Long) = { 97 | t = time() 98 | if (t - lastSnapshot > 1000000000) { 99 | lastSnapshot = t 100 | writeHeader(t) 101 | } 102 | writeTag(TodoTag) 103 | log putInt todoCounter 104 | todoCounter = 0 105 | writeTag(EndTurnTag) 106 | log putLong t 107 | } 108 | def logTock(n: Node) = { 109 | val id = uniqueId(n) 110 | writeTag(TockTag) 111 | writeNodeId(id) 112 | } 113 | def logLevelMismatch(accessor: Node, accessed: Node) = { 114 | writeTag(MismatchTag) 115 | writeNodeId(uniqueId(accessor)) 116 | writeNodeId(uniqueId(accessed)) 117 | } 118 | private var todoCounter = 0 119 | def logTodo(t: Any) = { 120 | todoCounter += 1 121 | } 122 | } 123 | 124 | -------------------------------------------------------------------------------- /src/scala/react/monitor/LogFormat.scala: -------------------------------------------------------------------------------- 1 | package scala.react 2 | package monitor 3 | 4 | object LogFormat { 5 | val TockTag: Byte = 1 6 | val TodoTag: Byte = 2 7 | val MismatchTag: Byte = 3 8 | val NewNodeTag: Byte = 4 9 | val NewNodeGenTag: Byte = 5 10 | val EndTurnTag: Byte = -1 11 | } 12 | -------------------------------------------------------------------------------- /src/scala/react/monitor/PagedWriter.scala: -------------------------------------------------------------------------------- 1 | package scala.react.monitor 2 | 3 | import java.io.File 4 | import java.nio.channels.FileChannel 5 | import java.nio.BufferOverflowException 6 | 7 | /** 8 | * A writer with a MappedByteBuffer-like interface that can be used to write to memory mapped files 9 | * for which the size is unknown in advance. The file size is increased by `chunkSize` (plus/minus 10 | * alignment) everytime it runs out of space. 11 | */ 12 | class PagedWriter(file: File, chunkSize: Int = 1000000) { 13 | private var offset = 0L 14 | val channel = IO.writeChannel(file) 15 | private var buf = IO.mapForWriting(channel, offset, chunkSize) 16 | 17 | private def remap() { 18 | offset += buf.position 19 | buf = channel.map(FileChannel.MapMode.READ_WRITE, offset, chunkSize) 20 | } 21 | 22 | def putLong(v: Long): Unit = try { 23 | buf putLong v 24 | } catch { 25 | case e: BufferOverflowException => 26 | remap() 27 | putLong(v) 28 | } 29 | 30 | def putByte(v: Byte): Unit = try { 31 | buf put v 32 | } catch { 33 | case e: BufferOverflowException => 34 | remap() 35 | putByte(v) 36 | } 37 | 38 | def putShort(v: Short): Unit = try { 39 | buf putShort v 40 | } catch { 41 | case e: BufferOverflowException => 42 | remap() 43 | putShort(v) 44 | } 45 | 46 | def putInt(v: Int): Unit = try { 47 | buf putInt v 48 | } catch { 49 | case e: BufferOverflowException => 50 | remap() 51 | putInt(v) 52 | } 53 | } -------------------------------------------------------------------------------- /test/scala/react/test/AllTests.java: -------------------------------------------------------------------------------- 1 | package scala.react.test; 2 | 3 | import org.junit.runner.RunWith; 4 | import org.junit.runners.Suite; 5 | 6 | @RunWith(Suite.class) 7 | @Suite.SuiteClasses({ 8 | TopoQueueTests.class, 9 | ReactiveTests.class, 10 | CombinatorTests.class, 11 | FlowTests.class, 12 | RouterTests.class, 13 | ReactorTests.class 14 | }) 15 | public final class AllTests { 16 | } 17 | -------------------------------------------------------------------------------- /test/scala/react/test/CombinatorTests.scala: -------------------------------------------------------------------------------- 1 | package scala.react 2 | package test 3 | 4 | import utils._ 5 | import dom._ 6 | import org.junit.Test 7 | import org.junit.Assert._ 8 | 9 | class CombinatorTests extends TestUtils with Observing { 10 | @Test def eventsMap() = testTurns(6) { 11 | val x = EventSource[Int] 12 | val y = x map (_ * 2) 13 | observe(y)(log(_)) 14 | 15 | turn { 16 | log.assert() 17 | x << 1 18 | log.assert() 19 | } 20 | turn { 21 | log.assert(2) 22 | x << 1 23 | log.assert() 24 | } 25 | turn { 26 | log.assert(2) 27 | } 28 | turn { 29 | log.assert() 30 | x << 3 31 | log.assert() 32 | } 33 | turn { 34 | log.assert(6) 35 | } 36 | } 37 | 38 | 39 | @Test def eventsMapLazy() = testTurns(5) { 40 | val x = EventSource[Int] 41 | val y = x map (_ * 2) 42 | 43 | turn { 44 | x << 1 45 | doLater { 46 | assertEquals(Some(2), y.pulseNow) 47 | } 48 | } 49 | turn { 50 | x << 2 51 | } 52 | turn { 53 | x << 3 54 | observe(y) { log(_) } 55 | } 56 | turn { 57 | log.assert(6) 58 | } 59 | } 60 | 61 | @Test def eventsCollect() = testTurns(7) { 62 | val x = EventSource[Int] 63 | val y = x collect { 64 | case x if x % 2 == 0 => x 65 | } 66 | observe(y)(log(_)) 67 | 68 | turn { 69 | log.assert() 70 | x << 2 71 | log.assert() 72 | } 73 | turn { 74 | log.assert(2) 75 | x << 1 76 | log.assert() 77 | } 78 | turn { 79 | log.assert() 80 | } 81 | turn { 82 | log.assert() 83 | x << 3 84 | log.assert() 85 | } 86 | turn { 87 | log.assert() 88 | x << 22 89 | log.assert() 90 | } 91 | turn { 92 | log.assert(22) 93 | } 94 | } 95 | 96 | @Test def eventsFilter() = testTurns(7) { 97 | val x = EventSource[Int] 98 | val y = x filter (_ % 2 == 0) 99 | observe(y)(log(_)) 100 | 101 | turn { 102 | log.assert() 103 | x << 2 104 | log.assert() 105 | } 106 | turn { 107 | log.assert(2) 108 | x << 1 109 | log.assert() 110 | } 111 | turn { 112 | log.assert() 113 | } 114 | turn { 115 | log.assert() 116 | x << 3 117 | log.assert() 118 | } 119 | turn { 120 | log.assert() 121 | x << 22 122 | log.assert() 123 | } 124 | turn { 125 | log.assert(22) 126 | } 127 | } 128 | 129 | @Test def eventsTake() = testTurns(6) { 130 | val x = EventSource[Int] 131 | val y = x take 2 132 | observe(y)(log(_)) 133 | 134 | turn { 135 | log.assert() 136 | x << 2 137 | log.assert() 138 | } 139 | turn { 140 | log.assert(2) 141 | x << 11 142 | log.assert() 143 | } 144 | turn { 145 | log.assert(11) 146 | x << 3 147 | log.assert() 148 | } 149 | turn { 150 | log.assert() 151 | x << 4 152 | log.assert() 153 | } 154 | turn { 155 | log.assert() 156 | } 157 | } 158 | 159 | @Test def eventsDelay2() = testTurns(10) { 160 | val x = EventSource[Int] 161 | val y = x delay 2 162 | observe(y)(log(_)) 163 | 164 | turn { 165 | log.assert() 166 | x << 1 167 | log.assert() 168 | } 169 | turn { 170 | log.assert() 171 | x << 2 172 | log.assert() 173 | } 174 | turn { 175 | log.assert() 176 | x << 3 177 | log.assert() 178 | } 179 | turn { 180 | log.assert(1) 181 | x << 4 182 | log.assert() 183 | } 184 | turn { 185 | log.assert(2) 186 | } 187 | turn { 188 | log.assert() 189 | } 190 | turn { 191 | log.assert() 192 | x << 5 193 | log.assert() 194 | } 195 | turn { 196 | log.assert(3) 197 | x << 6 198 | log.assert() 199 | } 200 | turn { 201 | log.assert(4) 202 | } 203 | } 204 | 205 | @Test def eventsScan() = testTurns(6) { 206 | val x = EventSource[Int] 207 | val y = x.scan(1){ _ + _ } 208 | observe(y)(log(_)) 209 | 210 | turn { 211 | log.assert() 212 | x << 2 213 | log.assert() 214 | } 215 | turn { 216 | log.assert(3) 217 | x << 5 218 | log.assert() 219 | } 220 | turn { 221 | log.assert(8) 222 | } 223 | turn { 224 | log.assert() 225 | x << 5 226 | log.assert() 227 | } 228 | turn { 229 | log.assert(13) 230 | } 231 | } 232 | 233 | @Test def eventsScan1() = testTurns(6) { 234 | val x = EventSource[Int] 235 | val y = x.scan1 { _ + _ } 236 | observe(y)(log(_)) 237 | 238 | turn { 239 | log.assert() 240 | x << 2 241 | log.assert() 242 | } 243 | turn { 244 | log.assert(2) 245 | x << 5 246 | log.assert() 247 | } 248 | turn { 249 | log.assert(7) 250 | } 251 | turn { 252 | log.assert() 253 | x << 5 254 | log.assert() 255 | } 256 | turn { 257 | log.assert(12) 258 | } 259 | } 260 | 261 | @Test def eventsScanMutable() = testTurns(8) { 262 | val x = EventSource[Int] 263 | val y = x.scanMutable("0") { (acc, e) => 264 | if(e == 2) mute else acc + "," + e 265 | } 266 | observe(y)(log(_)) 267 | 268 | turn { 269 | log.assert() 270 | x << 1 271 | log.assert() 272 | } 273 | turn { 274 | log.assert("0,1") 275 | x << 3 276 | log.assert() 277 | } 278 | turn { 279 | log.assert("0,1,3") 280 | } 281 | turn { 282 | log.assert() 283 | x << 2 284 | log.assert() 285 | } 286 | turn { 287 | log.assert() 288 | x << 4 289 | log.assert() 290 | } 291 | turn { 292 | log.assert("0,1,3,4") 293 | } 294 | turn { 295 | log.assert() 296 | } 297 | } 298 | 299 | @Test def signalChanges() = testTurns(6) { 300 | val x = Var(0) 301 | val y = x.changes 302 | observe(y)(log(_)) 303 | 304 | turn { 305 | log.assert() 306 | x()= 1 307 | log.assert() 308 | } 309 | turn { 310 | log.assert(1) 311 | x()= 1 312 | log.assert() 313 | } 314 | turn { 315 | log.assert() 316 | } 317 | turn { 318 | log.assert() 319 | x()= 2 320 | log.assert() 321 | } 322 | turn { 323 | log.assert(2) 324 | } 325 | } 326 | 327 | @Test def eventsHold() = testTurns(6) { 328 | val x = EventSource[Int] 329 | val y = x.hold(1) 330 | observe(y)(log(_)) 331 | 332 | turn { 333 | log.assert() 334 | x << 1 335 | log.assert() 336 | } 337 | turn { 338 | log.assert() 339 | x << 2 340 | log.assert() 341 | } 342 | turn { 343 | log.assert(2) 344 | } 345 | turn { 346 | log.assert() 347 | x << 3 348 | log.assert() 349 | } 350 | turn { 351 | log.assert(3) 352 | } 353 | } 354 | 355 | @Test def strictLazyReorder() = testTurns(6) { 356 | val switch = Var(false) 357 | val a0 = Var(0) // level 0 358 | val a1 = Strict { a0() + 1 } // level 1 359 | //val s2 = Strict { if(s0() % 2 == 0) s0() + 1 else s1() + 2 } // level 1 then 2 360 | val a2 = Strict { a1() + 1 } // level 2 361 | val a3 = Strict { a2() + 1 } // level 3 362 | val a14 = Strict { if(switch()) a3() + 1 else a0() + 1 } // level 1 or 4 363 | val l25 = Lazy { a14() + 1 } // level 2 or 5 364 | // this should always observe the correct state in l25, which needs to be lifted 365 | // transitively when a14 switches branches 366 | val res = Strict { 367 | val x = a0() + l25() + 1 368 | log("eval" + x) // log to make sure this signal isn't evaluated redundantly 369 | x 370 | } 371 | observe(res)(log(_)) 372 | 373 | turn { 374 | log.assert("eval3", 3) 375 | assertEquals(0, a0.level) 376 | assertEquals(1, a1.level) 377 | assertEquals(2, a2.level) 378 | assertEquals(3, a3.level) 379 | assertEquals(1, a14.level) 380 | assertEquals(1, l25.level) // not evaluated, should be something > 0 381 | assertEquals(2, res.level) 382 | 383 | a0()= 1 384 | log.assert() 385 | } 386 | turn { 387 | log.assert("eval5", 5) 388 | assertEquals(0, a0.level) 389 | assertEquals(1, a1.level) 390 | assertEquals(2, a2.level) 391 | assertEquals(3, a3.level) 392 | assertEquals(1, a14.level) 393 | assertEquals(1, l25.level) 394 | assertEquals(2, res.level) 395 | 396 | a0()= 2 397 | switch()= true 398 | 399 | log.assert() 400 | } 401 | turn { 402 | log.assert("eval10", 10) 403 | assertEquals(0, a0.level) 404 | assertEquals(1, a1.level) 405 | assertEquals(2, a2.level) 406 | assertEquals(3, a3.level) 407 | assertEquals(4, a14.level) 408 | assertEquals(5, l25.level) 409 | assertEquals(6, res.level) 410 | } 411 | turn { 412 | log.assert() 413 | a0()= 3 414 | log.assert() 415 | } 416 | turn { 417 | log.assert("eval12", 12) 418 | } 419 | } 420 | } -------------------------------------------------------------------------------- /test/scala/react/test/FlowTests.scala: -------------------------------------------------------------------------------- 1 | package scala.react 2 | package test 3 | 4 | import org.junit.Test 5 | import org.junit.Assert._ 6 | import org.scalatest.junit.ShouldMatchersForJUnit._ 7 | import test.utils._ 8 | import dom._ 9 | 10 | class FlowTests extends TestUtils { 11 | 12 | @Test def simpleSignal() = testTurns(2) { 13 | val x = Signal.flow(1) { self => 14 | log("a") 15 | } 16 | logReactive(x) 17 | 18 | postTurn { 19 | log.assert("a") 20 | assertEquals(1, x.now) 21 | } 22 | 23 | turn { 24 | postTurn { 25 | log.assert() 26 | } 27 | } 28 | } 29 | 30 | @Test def simpleSeqSignal() = testTurns(4) { 31 | val x = Signal.flow(1) { self => 32 | log("a") 33 | self()= 2 34 | self.pause 35 | log("b") 36 | self()= 3 37 | self.pause 38 | log("d") 39 | } 40 | logReactive(x) 41 | 42 | postTurn { 43 | log.assert("a", 2) 44 | assertEquals(2, x.now) 45 | } 46 | 47 | turn { 48 | postTurn { 49 | log.assert("b", 3) 50 | assertEquals(3, x.now) 51 | } 52 | } 53 | turn { 54 | postTurn { 55 | log.assert("d") 56 | } 57 | } 58 | turn { 59 | postTurn { 60 | log.assert() 61 | } 62 | } 63 | } 64 | 65 | @Test def simpleSeqSignalWithPause() = testTurns(5) { 66 | val x = Signal.flow(0) { self => 67 | log("a") 68 | self()= 1 69 | self.pause 70 | log("b") 71 | self.pause 72 | log("c") 73 | self()= 2 74 | self.pause 75 | log("d") 76 | } 77 | logReactive(x) 78 | 79 | postTurn { 80 | log.assert("a", 1) 81 | } 82 | 83 | turn { 84 | postTurn { 85 | log.assert("b") 86 | } 87 | } 88 | turn { 89 | postTurn { 90 | log.assert("c", 2) 91 | } 92 | } 93 | turn { 94 | postTurn { 95 | log.assert("d") 96 | } 97 | } 98 | turn { 99 | postTurn { 100 | log.assert() 101 | } 102 | } 103 | } 104 | 105 | @Test def simpleNext() = testTurns(6) { 106 | val es1 = EventSource[Int] 107 | val es2 = EventSource[Int] 108 | val x = Signal.loop(1) { self => 109 | log("a") 110 | self()= (self await es1) 111 | log("b") 112 | self.pause 113 | log("c") 114 | self() = (self await es1) 115 | log("d") 116 | self.pause 117 | log("e") 118 | self() = (self await es2) 119 | log("f") 120 | self.pause 121 | } 122 | logReactive(x) 123 | 124 | postTurn { 125 | log.assert("a") 126 | assertEquals(1, x.now) 127 | } 128 | 129 | turn { 130 | es1 << 2 131 | postTurn { 132 | log.assert("b", 2) 133 | assertEquals(2, x.now) 134 | } 135 | } 136 | turn { 137 | } 138 | turn { 139 | es1 << 3 140 | postTurn { 141 | log.assert("c", "d", 3) 142 | assertEquals(3, x.now) 143 | } 144 | } 145 | turn { 146 | es1 << 4 147 | postTurn { 148 | log.assert("e") 149 | assertEquals(3, x.now) 150 | } 151 | } 152 | turn { 153 | es2 << 5 154 | postTurn { 155 | log.assert("f", 5) 156 | assertEquals(5, x.now) 157 | } 158 | } 159 | } 160 | 161 | @Test def finishedFlowIsNotMute() = testTurns(2) { 162 | val x = Signal.flow(0) { self => 163 | log("a") 164 | self()= 1 165 | log("b") 166 | } 167 | logReactive(x) 168 | 169 | postTurn { 170 | log.assert("a", "b", 1) 171 | } 172 | 173 | turn { 174 | postTurn { 175 | log.assert() 176 | } 177 | } 178 | } 179 | 180 | @Test def repeatedEmit() = testTurns(2) { 181 | val x = Signal.flow(0) { self => 182 | log("a") 183 | self()= 1 184 | log("b") 185 | self()= 2 186 | log("c") 187 | } 188 | logReactive(x) 189 | 190 | postTurn { 191 | log.assert("a", "b", "c", 2) 192 | } 193 | 194 | turn { 195 | postTurn { 196 | log.assert() 197 | } 198 | } 199 | } 200 | 201 | @Test def parJoin() = testTurns(3) { 202 | val x = Signal.flow(1) { self => 203 | log("1") 204 | self.par { 205 | log("a1") 206 | self.pause 207 | log("a2") 208 | self.join 209 | log("a3") 210 | } { 211 | log("b1") 212 | self.pause 213 | log("b2") 214 | self.pause 215 | log("b3") 216 | } 217 | log("2") 218 | } 219 | logReactive(x) 220 | 221 | postTurn { 222 | log.assert("1", "a1", "b1") 223 | } 224 | 225 | turn { 226 | postTurn { 227 | log.assert("a2", "2") 228 | } 229 | } 230 | turn {} 231 | } 232 | 233 | @Test def nestedParsJoin() = testTurns(3) { 234 | val x = Signal.flow(1) { self => 235 | log("1") 236 | self.par { 237 | log("a1") 238 | self.par { 239 | log("c1") 240 | self.pause 241 | log("c2") 242 | self.join 243 | log("c3") // should never be reached 244 | } { 245 | log("d1") 246 | self.pause 247 | log("d2") // should never be reached 248 | } 249 | log("a2") 250 | } { 251 | log("b1") 252 | self.pause 253 | log("b2") 254 | } 255 | log("2") 256 | } 257 | logReactive(x) 258 | 259 | postTurn { 260 | log.assert("1", "a1", "c1", "d1", "b1") 261 | } 262 | 263 | turn { 264 | postTurn { 265 | log.assert("c2", "a2", "b2", "2") 266 | } 267 | } 268 | turn {} 269 | } 270 | 271 | @Test def loopUntilSignal() = testTurns(6) { 272 | val es1 = EventSource[Int] 273 | val es2 = EventSource[Int] 274 | val x = Signal.flow(1) { self => 275 | log("a") 276 | var i = 2 277 | val e1 = self.loopUntil(es1) { 278 | log("b") 279 | self()= i 280 | self.pause 281 | i += 1 282 | log("c") 283 | } 284 | log("d") 285 | self()= e1 286 | self.pause 287 | log("e") 288 | val e2 = self.loopUntil(es2) { 289 | log("f") 290 | self()= i 291 | self.pause 292 | i += 1 293 | log("g") 294 | } 295 | log("h") 296 | }.name = "subject" 297 | logReactive(x) 298 | 299 | postTurn { 300 | log.assert("a", "b", 2) 301 | } 302 | 303 | turn { 304 | postTurn { 305 | log.assert("c", "b", 3) 306 | } 307 | } 308 | turn { 309 | es1 << 1 310 | postTurn { 311 | log.assert("d", 1) 312 | } 313 | } 314 | turn { 315 | postTurn { 316 | log.assert("e", "f", 3) // last i+=1 should have been skipped 317 | } 318 | } 319 | turn { 320 | es1 << 2 321 | postTurn { 322 | log.assert("g", "f", 4) 323 | } 324 | } 325 | turn { 326 | es2 << 2 327 | postTurn { 328 | log.assert("h") 329 | } 330 | } 331 | } 332 | 333 | @Test def loopEndUntilSignal() = testTurns(6) { 334 | val es1 = EventSource[Int] 335 | val es2 = EventSource[Int] 336 | val x = Signal.flow(1) { self => 337 | log("a") 338 | var i = 2 339 | val e1 = self.loopEndUntil(es1) { 340 | log("b") 341 | self()= i 342 | self.pause 343 | i += 1 344 | log("c") 345 | } 346 | log("d") 347 | self()= e1 348 | self.pause 349 | log("e") 350 | val e2 = self.loopEndUntil(es2) { 351 | log("f") 352 | self()= i 353 | self.pause 354 | i += 1 355 | log("g") 356 | } 357 | log("h") 358 | }.name = "subject" 359 | logReactive(x) 360 | 361 | postTurn { 362 | log.assert("a", "b", 2) 363 | } 364 | 365 | turn { 366 | postTurn { 367 | log.assert("c", "b", 3) 368 | } 369 | } 370 | turn { 371 | es1 << 1 372 | postTurn { 373 | log.assert("c", "d", 1) 374 | } 375 | } 376 | turn { 377 | postTurn { 378 | log.assert("e", "f", 4) // last i+=1 should have been executed 379 | } 380 | } 381 | turn { 382 | es1 << 2 383 | postTurn { 384 | log.assert("g", "f", 5) 385 | } 386 | } 387 | turn { 388 | es2 << 2 389 | postTurn { 390 | log.assert("g", "h") 391 | } 392 | } 393 | } 394 | 395 | } -------------------------------------------------------------------------------- /test/scala/react/test/ReactiveTests.scala: -------------------------------------------------------------------------------- 1 | package scala.react 2 | package test 3 | 4 | import org.junit.Test 5 | import org.junit.Assert._ 6 | import org.scalatest.junit.ShouldMatchersForJUnit._ 7 | import test.utils._ 8 | import dom._ 9 | 10 | class ReactiveTests extends TestUtils { 11 | 12 | @Test def testTurnCount() = testTurns(1) { 13 | } 14 | 15 | @Test def testTurnOrder() = testTurns(4) { 16 | log(1) 17 | 18 | turn { 19 | log.assert(1) 20 | log.assert() 21 | log(2) 22 | } 23 | turn { 24 | log.assert(2) 25 | log.assert() 26 | log(3) 27 | } 28 | turn { 29 | log.assert(3) 30 | log.assert() 31 | } 32 | } 33 | 34 | @Test(expected=classOf[AssertionError]) 35 | def testTurnOrderFailCount() = testTurns(3) { 36 | log(1) 37 | 38 | turn { 39 | log.assert(1) 40 | log.assert() 41 | log(2) 42 | } 43 | turn { 44 | log.assert(2) 45 | log.assert() 46 | log(3) 47 | } 48 | turn { 49 | log.assert(3) 50 | log.assert() 51 | } 52 | } 53 | 54 | @Test(expected=classOf[AssertionError]) 55 | def testTurnOrderFailLog() = testTurns(2) { 56 | turn { 57 | log.assert(1) 58 | } 59 | } 60 | 61 | @Test def testDoLater() = testTurns(5) { 62 | doLater { log(1) } 63 | log.assert() 64 | postTurn { 65 | log.assert(1) 66 | } 67 | 68 | turn { 69 | log.assert() 70 | } 71 | turn { 72 | log.assert() 73 | doLater{ log(2) } 74 | postTurn { 75 | log.assert(2) 76 | } 77 | } 78 | turn { 79 | log.assert() 80 | doLater{ log(10) } 81 | doLater{ log(11) } 82 | postTurn { 83 | log.assert(10, 11) 84 | } 85 | } 86 | turn { 87 | log.assert() 88 | } 89 | } 90 | 91 | @Test def simpleInvalidate() = testTurns(1) { 92 | new Observer { 93 | assertTrue(invalidate()) 94 | assertFalse(invalidate()) 95 | 96 | def react() {} 97 | } 98 | } 99 | 100 | @Test def simpleObserveVar() = testTurns(7) { 101 | val x = Var(1) 102 | logReactive(x) 103 | 104 | doLater { assertEquals(1, x.now) } 105 | 106 | turn { 107 | log.assert() 108 | doLater { 109 | x() = 2 110 | } 111 | } 112 | turn { 113 | log.assert() 114 | postTurn { log.assert(2) } 115 | } 116 | turn { 117 | log.assert() 118 | } 119 | turn { 120 | log.assert() 121 | x() = 3 122 | doLater { log.assert() } 123 | } 124 | turn { 125 | log.assert(3) 126 | x() = 4 127 | x() = 5 128 | doLater { log.assert() } 129 | } 130 | turn { 131 | log.assert(5) 132 | } 133 | } 134 | 135 | @Test def simpleObserveVarImmediate() = testTurns(2) { 136 | val x = Var(1) 137 | x() = 2 138 | observe(x)(log(_)) 139 | log.assert() 140 | doLater { log.assert(2) } 141 | 142 | turn { 143 | log.assert() 144 | } 145 | } 146 | 147 | @Test def simpleObserveEventSource() = testTurns(6) { 148 | val x = EventSource[Int] 149 | observe(x)(log(_)) 150 | 151 | turn { 152 | log.assert() 153 | x << 1 154 | log.assert() 155 | } 156 | turn { 157 | log.assert(1) 158 | x << 1 159 | log.assert() 160 | } 161 | turn { 162 | log.assert(1) 163 | } 164 | turn { 165 | log.assert() 166 | x << 3 167 | log.assert() 168 | } 169 | turn { 170 | log.assert(3) 171 | } 172 | } 173 | 174 | @Test def eventsOnce() = testTurns(4) { 175 | val x = Events.once(1) 176 | observe(x)(log(_)) 177 | log.assert() 178 | 179 | turn { 180 | log.assert(1) 181 | } 182 | turn { 183 | log.assert() 184 | } 185 | turn { 186 | log.assert() 187 | } 188 | } 189 | 190 | @Test def eventsNever() = testTurns(3) { 191 | val x = Events.never[Int] 192 | observe(x)(log(_)) 193 | log.assert() 194 | 195 | turn { 196 | log.assert() 197 | } 198 | turn { 199 | log.assert() 200 | } 201 | } 202 | 203 | @Test def simpleObserveOnce() = testTurns(9) { 204 | val x = Var(1) 205 | observeOnce(x)(log(_)) 206 | 207 | turn { 208 | log.assert() 209 | x() = 2 210 | log.assert() 211 | } 212 | turn { 213 | log.assert(2) 214 | x() = 2 215 | log.assert() 216 | } 217 | turn { 218 | 219 | } 220 | turn { 221 | log.assert() 222 | x() = 3 223 | } 224 | turn { 225 | log.assert() 226 | } 227 | 228 | turn { 229 | log.assert() 230 | x() = 4 231 | observeOnce(x)(log(_)) 232 | postTurn { log.assert(4) } 233 | } 234 | turn { 235 | log.assert() 236 | x() = 5 237 | } 238 | turn { 239 | log.assert() 240 | } 241 | } 242 | 243 | @Test def strictSignalInit() = testTurns(3) { 244 | val x = Var(1) 245 | val y = Strict { 246 | log("y") 247 | log(x.now) 248 | log(x.getPulse) 249 | log(x()) 250 | } 251 | 252 | turn { 253 | log.assert("y", 1, 1, 1) 254 | x() = 2 255 | log.assert() 256 | } 257 | turn { 258 | log.assert("y", 2, 2, 2) 259 | } 260 | } 261 | 262 | @Test def strictSignalHigherOrder() = testTurns(3) { 263 | // Testing that: 264 | // - we don't have assertions in the engine that break when nodes are created on the fly and/or 265 | // below the current engine level 266 | // - the dependent stack stays in a reasonable state in the presence of HO nodes and level 267 | // mismatches 268 | val x = Var(1) // level 0 269 | val y = Strict { x() + 1 } // level 1 270 | val z: Signal[Signal[Int]] = Strict { 271 | y() // lift this sig to level 2 272 | Strict { 273 | x()+1 // let this one be below the outer signal => below current engine level on first creation 274 | }.name = "inner" 275 | }.name = "outer" 276 | 277 | turn { 278 | x() = 2 279 | println(x) 280 | } 281 | turn { 282 | x() = 3 283 | } 284 | } 285 | 286 | @Test def strictSignalAdaptsLevel() = testTurns(3) { 287 | val x = Var(1) 288 | val y = Strict { x() + 1 } 289 | val z = Strict { 290 | log("z") 291 | log(y.now) 292 | log(y.getPulse) 293 | log(y()) 294 | } 295 | 296 | turn { 297 | log.assert("z", "z", 2, 2, 2) // redundant "z", because of hoisting 298 | x() = 2 299 | log.assert() 300 | } 301 | turn { 302 | log.assert("z", 3, 3, 3) 303 | } 304 | } 305 | 306 | @Test def strictSignalAdaptsLevel2() = testTurns(5) { 307 | val x = Var(1) 308 | val y = Strict { x() + 1 } 309 | val which = Var(true) 310 | val z = Strict { 311 | if(which()) { log("then"); x() } 312 | else { log("else"); y() } 313 | } 314 | logReactive(z) 315 | 316 | postTurn { 317 | log.assert("then", 1) 318 | } 319 | 320 | turn { 321 | log.assert() 322 | x() = 2 323 | log.assert() 324 | postTurn { 325 | log.assert("then", 2) 326 | } 327 | } 328 | turn { 329 | log.assert() 330 | which() = false 331 | log.assert() 332 | postTurn { 333 | log.assert("else", "else", 3) 334 | } 335 | } 336 | 337 | turn { 338 | log.assert() 339 | x() = 3 340 | log.assert() 341 | postTurn { 342 | log.assert("else", 4) 343 | } 344 | } 345 | 346 | turn { 347 | log.assert() 348 | which() = true 349 | log.assert() 350 | postTurn { 351 | log.assert("then", 3) 352 | } 353 | } 354 | } 355 | 356 | 357 | 358 | @Test def strictSignalRedundantEval() = testTurns(3) { 359 | val x = Var(1) 360 | val y = Strict { x() } 361 | val z = Strict { x() } 362 | val sum = Strict{ y() + z() } 363 | observe(sum){ log(_) } 364 | 365 | turn { 366 | log.clear() 367 | x() = 2 368 | log.assert() 369 | } 370 | turn { 371 | log.assert(4) 372 | } 373 | } 374 | 375 | @Test def lazySignal() = testTurns(10) { 376 | val x = Var(1) 377 | val y = Lazy { 378 | log("y") 379 | val v = x.now 380 | log(v) // should adapt level 381 | log(x.getPulse) 382 | log(x()) 383 | v 384 | } 385 | var doEval = Var(false) 386 | val z = Strict { 387 | if (doEval()) log("z " + y()) else log("z mute") 388 | } 389 | log.assert() 390 | 391 | turn { 392 | log.assert("z mute") 393 | x() = 2 394 | postTurn { log.assert() } 395 | } 396 | turn { 397 | // lazy signal is not connected 398 | log.assert() 399 | doLater { 400 | log("ob " + y.now) 401 | } 402 | } 403 | turn { 404 | log.assert("y", 2, 2, 2, "ob 2") 405 | // this should not cause y to validate, but invalidate it 406 | x() = 3 407 | } 408 | turn { 409 | // y should not have been evaluated 410 | log.assert() 411 | // should cause z to eval y 412 | doEval() = true 413 | } 414 | turn { 415 | log.assert("y", 3, 3, 3, "z 3") 416 | x() = 4 417 | log.assert() 418 | } 419 | turn { 420 | log.assert("y", 4, 4, 4, "z 4") 421 | doEval() = false 422 | } 423 | turn { 424 | log.assert("z mute") 425 | x() = 5 426 | log.assert() 427 | } 428 | turn { 429 | log.clear() // z might have been notified, because of a stale dependency 430 | x() = 6 431 | log.assert() 432 | } 433 | turn { 434 | // stale dependency should have been cleared 435 | log.assert() 436 | } 437 | } 438 | 439 | } -------------------------------------------------------------------------------- /test/scala/react/test/ReactorTests.scala: -------------------------------------------------------------------------------- 1 | package scala.react 2 | package test 3 | 4 | import utils._ 5 | import dom._ 6 | import org.junit.Test 7 | import org.junit.Assert._ 8 | 9 | class ReactorTests extends TestUtils with Observing { 10 | @Test def simple() = testTurns(3) { 11 | val x = EventSource[Int] 12 | val r = Reactor.flow { self => 13 | log(1) 14 | self await x 15 | log(2) 16 | } 17 | 18 | turn { 19 | log.assert(1) 20 | x << 1 21 | log.assert() 22 | } 23 | turn { 24 | log.assert(2) 25 | x << 2 26 | log.assert() 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /test/scala/react/test/RouterTests.scala: -------------------------------------------------------------------------------- 1 | package scala.react 2 | package test 3 | 4 | import utils._ 5 | import dom._ 6 | import org.junit.Test 7 | import org.junit.Assert._ 8 | 9 | class RouterTests extends TestUtils with Observing { 10 | @Test def holdRouter() = testTurns(6) { 11 | val x = EventSource[Int] 12 | val r = new Router(x) { 13 | private val v = Var(1) 14 | def out: Signal[Int] = v 15 | def react() { 16 | x.ifEmitting { e => 17 | v() = e 18 | } 19 | } 20 | } 21 | observe(r.out)(log(_)) 22 | 23 | turn { 24 | log.assert() 25 | x << 1 26 | log.assert() 27 | } 28 | turn { 29 | log.assert() 30 | x << 2 31 | log.assert() 32 | } 33 | turn { 34 | log.assert(2) 35 | } 36 | turn { 37 | log.assert() 38 | x << 3 39 | log.assert() 40 | } 41 | turn { 42 | log.assert(3) 43 | } 44 | } 45 | 46 | /*@Test def bidirectionalRouter() = testTurns(6) { 47 | val x = Var(1) 48 | val y = Var(2) 49 | val r = new Router(x) { 50 | private val v1 = Var(1) 51 | private val v2 = Var(2) 52 | def out1: Signal[Int] = v1 53 | def out2: Signal[Int] = v2 54 | 55 | def react() { 56 | x.ifEmitting { e => 57 | v1() = e * v2.now 58 | } 59 | y.ifEmitting { e => 60 | v2() = e * v1.now 61 | } 62 | } 63 | } 64 | observe(r.out1)(log(_)) 65 | observe(r.out2)(log(_)) 66 | 67 | turn { 68 | log.assert() 69 | x()= 1 70 | log.assert() 71 | } 72 | turn { 73 | log.assert() 74 | x()= 2 75 | log.assert() 76 | } 77 | turn { 78 | log.assert(2) 79 | } 80 | turn { 81 | log.assert() 82 | x()= 3 83 | log.assert() 84 | } 85 | turn { 86 | log.assert(3) 87 | } 88 | }*/ 89 | } -------------------------------------------------------------------------------- /test/scala/react/test/TopoQueueTests.scala: -------------------------------------------------------------------------------- 1 | package scala.react 2 | package test 3 | 4 | import org.junit.Test 5 | import org.junit.Assert._ 6 | 7 | class TopoQueueTests { 8 | case class Node(i: Int) 9 | 10 | class Q extends TopoQueue[Node] { 11 | def depth(n: Node) = n.i 12 | } 13 | 14 | @Test def simple() { 15 | val q = new Q 16 | assertTrue(q.isEmpty) 17 | q.clear() 18 | assertTrue(q.isEmpty) 19 | q += Node(0) 20 | assertFalse(q.isEmpty) 21 | q += Node(0) 22 | q += Node(1) 23 | assertFalse(q.isEmpty) 24 | q += Node(5) 25 | 26 | assertEquals(Node(0), q.dequeue) 27 | assertFalse(q.isEmpty) 28 | assertEquals(Node(0), q.dequeue) 29 | assertEquals(Node(1), q.dequeue) 30 | assertFalse(q.isEmpty) 31 | assertEquals(Node(5), q.dequeue) 32 | assertTrue(q.isEmpty) 33 | q.clear() 34 | assertTrue(q.isEmpty) 35 | } 36 | 37 | @Test def withMaxLevel() { 38 | val q = new Q 39 | assertTrue(q.isEmpty) 40 | q += Node(Int.MaxValue) 41 | assertFalse(q.isEmpty) 42 | assertEquals(Node(Int.MaxValue), q.dequeue) 43 | assertTrue(q.isEmpty) 44 | q.clear() 45 | assertTrue(q.isEmpty) 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /test/scala/react/test/utils/JTestUtils.java: -------------------------------------------------------------------------------- 1 | package scala.react.test.utils; 2 | 3 | import org.junit.Rule; 4 | import org.junit.rules.TestName; 5 | 6 | public class JTestUtils { 7 | // This needs to be a public member, so needs to be in a Java class. 8 | @Rule public TestName testName = new TestName(); 9 | } -------------------------------------------------------------------------------- /test/scala/react/test/utils/TestDomain.scala: -------------------------------------------------------------------------------- 1 | package scala.react 2 | package test.utils 3 | 4 | import java.util.ArrayDeque 5 | import org.junit.Assert._ 6 | 7 | object dom extends TestDomain 8 | 9 | class TestDomain extends Domain { 10 | var engine = new TestEngine 11 | val scheduler = new ManualScheduler 12 | 13 | private val postTurnTodos = new ArrayDeque[() => Unit] 14 | def schedulePostTurn(op: => Unit) = postTurnTodos add (() => op) 15 | 16 | private def reset() { 17 | turnQueue.clear() 18 | postTurnTodos.clear() 19 | engine = new TestEngine 20 | } 21 | 22 | // add some test hooks to the standard engine 23 | class TestEngine extends Engine { 24 | override def runTurn() = super.runTurn() 25 | override def propagate() = { 26 | super.propagate() 27 | level = Int.MaxValue 28 | while (!postTurnTodos.isEmpty) 29 | postTurnTodos.poll().apply() 30 | } 31 | override def uncaughtException(e: Throwable) = { 32 | e.printStackTrace() 33 | postTurnTodos.clear() 34 | throw e 35 | } 36 | } 37 | 38 | def testTurns(turnsExpected: Int)(op: => Unit) { 39 | reset() 40 | dom.start() 41 | turn { op } 42 | while (!turnQueue.isEmpty) { 43 | val t = turnQueue.poll() 44 | dom schedule { t() } 45 | dom.engine.runTurn() 46 | } 47 | assertEquals(turnsExpected, engine.currentTurn - 1) 48 | } 49 | 50 | private val turnQueue = new ArrayDeque[() => Unit] 51 | 52 | // First run the given op, and then a turn. 53 | def turn(op: => Unit) { 54 | turnQueue add (() => op) 55 | } 56 | 57 | // run at the very end of current turn 58 | def postTurn(op: => Unit) = dom.schedulePostTurn(op) 59 | 60 | override def toString = "TestDomain" 61 | } -------------------------------------------------------------------------------- /test/scala/react/test/utils/TestUtils.scala: -------------------------------------------------------------------------------- 1 | package scala.react 2 | package test.utils 3 | 4 | import org.junit.{ After, Before, Rule } 5 | import org.junit.Assert._ 6 | import java.util.ArrayDeque 7 | import org.scalatest.junit.ShouldMatchersForJUnit._ 8 | import scala.collection.mutable._ 9 | import dom._ 10 | import org.junit.BeforeClass 11 | import org.junit.rules.TestName 12 | 13 | abstract class TestUtils extends JTestUtils with Observing { 14 | val log = new Log 15 | 16 | class Log { 17 | private val buf = new ArrayBuffer[Any] with SynchronizedBuffer[Any] 18 | def apply(a: Any) { 19 | buf += a 20 | } 21 | def assert(as: Any*) { buf.toArray should equal(as.toArray); clear() } 22 | def clear() = buf.clear 23 | } 24 | 25 | 26 | @Before def setup() { 27 | log.clear() 28 | println("======= Running " + testName.getMethodName) 29 | } 30 | 31 | @After def tearDown() { 32 | log.assert() 33 | println("======= done with " + testName.getMethodName) 34 | } 35 | 36 | def logReactive[P](r: Reactive[P, Any]): Observer = observe(r){ log(_) }.name = "(Logger of " + r + ")" 37 | } --------------------------------------------------------------------------------