├── example ├── .gitignore ├── src │ └── main │ │ ├── res │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── values │ │ │ ├── dimens.xml │ │ │ ├── colors.xml │ │ │ ├── strings.xml │ │ │ └── themes.xml │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ ├── values-night │ │ │ └── themes.xml │ │ ├── drawable-v24 │ │ │ └── ic_launcher_foreground.xml │ │ ├── layout │ │ │ └── activity_login.xml │ │ └── drawable │ │ │ └── ic_launcher_background.xml │ │ ├── kotlin │ │ └── com │ │ │ └── example │ │ │ └── myapplication │ │ │ └── ui │ │ │ └── login │ │ │ ├── Globals.kt │ │ │ ├── LoginActivityBG.kt │ │ │ └── LoginExtent.kt │ │ └── AndroidManifest.xml ├── proguard-rules.pro └── build.gradle ├── gradle └── wrapper │ ├── .gitignore │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── tutorial-javafx └── src │ └── main │ └── kotlin │ └── module-info.java ├── behavior-graph ├── src │ ├── commonMain │ │ └── kotlin │ │ │ └── behaviorgraph │ │ │ ├── Transient.kt │ │ │ ├── Thunk.kt │ │ │ ├── OrderingState.kt │ │ │ ├── EventLoopPhase.kt │ │ │ ├── RelinkingOrder.kt │ │ │ ├── DateProvider.kt │ │ │ ├── BehaviorGraphException.kt │ │ │ ├── ExtentRemoveStrategy.kt │ │ │ ├── PlatformSpecific.kt │ │ │ ├── LinkType.kt │ │ │ ├── EventLoopState.kt │ │ │ ├── Linkable.kt │ │ │ ├── Action.kt │ │ │ ├── SideEffect.kt │ │ │ ├── Event.kt │ │ │ ├── BehaviorQueue.kt │ │ │ ├── Resource.kt │ │ │ ├── Moment.kt │ │ │ ├── TypedMoment.kt │ │ │ ├── Behavior.kt │ │ │ ├── ExtentLifetime.kt │ │ │ ├── State.kt │ │ │ └── Extent.kt │ ├── iosArm64Main │ │ └── kotlin │ │ │ └── behaviorgraph │ │ │ └── PlatformSpecificIosArm64Main.kt │ ├── jsMain │ │ └── kotlin │ │ │ └── behaviorgraph │ │ │ └── PlatformSpecificJS.kt │ ├── jvmMain │ │ └── kotlin │ │ │ └── behaviorgraph │ │ │ └── PlatformSpecificJVM.kt │ ├── commonTest │ │ └── kotlin │ │ │ └── behaviorgraph │ │ │ ├── AbstractBehaviorGraphTest.kt │ │ │ ├── BehaviorQueueTest.kt │ │ │ ├── DependenciesTest.kt │ │ │ ├── ExtentTest.kt │ │ │ ├── GraphCheckTests.kt │ │ │ ├── MomentTest.kt │ │ │ ├── EffectsActionsEventsTest.kt │ │ │ └── StateTest.kt │ └── jvmTest │ │ └── kotlin │ │ └── behaviorgraph │ │ └── ConcurrencyTests.kt └── build.gradle ├── tutorial-1 ├── build.gradle └── src │ └── main │ └── java │ └── Tutorial.java ├── settings.gradle ├── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── tutorial-2-swing ├── build.gradle └── src │ └── main │ └── java │ ├── Tutorial.java │ ├── Thermostat.java │ └── TutorialUI.java ├── tutorial-3-swing ├── build.gradle └── src │ └── main │ └── java │ ├── Tutorial.java │ ├── ItemUI.java │ ├── ItemExtent.java │ ├── ListUI.java │ └── ListExtent.java ├── code-walkthrough ├── build.gradle └── src │ └── main │ └── java │ ├── CodeWalkthrough.java │ └── LoginForm.java ├── gradle.properties ├── CONTRIBUTING.md ├── gradlew ├── Code_of_Conduct.md ├── LICENSE.txt └── README.md /example/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /gradle/wrapper/.gitignore: -------------------------------------------------------------------------------- 1 | !gradle-wrapper.jar 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/bgkotlin/master/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /tutorial-javafx/src/main/kotlin/module-info.java: -------------------------------------------------------------------------------- 1 | module tutorial.javafx { 2 | requires behaviorgraph; 3 | requires kotlin.stdlib; 4 | } -------------------------------------------------------------------------------- /example/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/bgkotlin/master/example/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/bgkotlin/master/example/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/bgkotlin/master/example/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/bgkotlin/master/example/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/bgkotlin/master/example/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/bgkotlin/master/example/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/bgkotlin/master/example/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/bgkotlin/master/example/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/bgkotlin/master/example/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yahoo/bgkotlin/master/example/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /behavior-graph/src/commonMain/kotlin/behaviorgraph/Transient.kt: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright Yahoo 2021 3 | // 4 | package behaviorgraph 5 | 6 | internal interface Transient { 7 | fun clear() 8 | } 9 | -------------------------------------------------------------------------------- /behavior-graph/src/commonMain/kotlin/behaviorgraph/Thunk.kt: -------------------------------------------------------------------------------- 1 | package behaviorgraph 2 | 3 | fun interface Thunk { 4 | fun invoke(); 5 | } 6 | 7 | fun interface ExtentThunk { 8 | fun invoke(ctx: T) 9 | } -------------------------------------------------------------------------------- /tutorial-1/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'application' 3 | } 4 | 5 | application { 6 | mainClass = 'Tutorial' 7 | } 8 | 9 | dependencies { 10 | implementation project(':behavior-graph') 11 | } 12 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'behavior-graph' 2 | include 'example' 3 | include 'behavior-graph' 4 | include 'tutorial-1' 5 | include 'tutorial-2-swing' 6 | include 'code-walkthrough' 7 | include 'tutorial-3-swing' 8 | 9 | -------------------------------------------------------------------------------- /example/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | -------------------------------------------------------------------------------- /behavior-graph/src/commonMain/kotlin/behaviorgraph/OrderingState.kt: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright Yahoo 2021 3 | // 4 | package behaviorgraph 5 | 6 | internal enum class OrderingState { 7 | Untracked, 8 | NeedsOrdering, 9 | Clearing, 10 | Ordering, 11 | Ordered 12 | } 13 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Aug 23 11:41:29 PDT 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /behavior-graph/src/commonMain/kotlin/behaviorgraph/EventLoopPhase.kt: -------------------------------------------------------------------------------- 1 | package behaviorgraph 2 | 3 | internal enum class EventLoopPhase { 4 | Queued, 5 | Action, 6 | Updates, 7 | SideEffects; 8 | 9 | val processingChanges: Boolean get() = (this == Action || this == Updates) 10 | } -------------------------------------------------------------------------------- /behavior-graph/src/commonMain/kotlin/behaviorgraph/RelinkingOrder.kt: -------------------------------------------------------------------------------- 1 | package behaviorgraph 2 | 3 | /** 4 | * See [BehaviorBuilder.dynamicDemands] and [BehaviorBuilder.dynamicSupplies] for more information. 5 | */ 6 | enum class RelinkingOrder { 7 | RelinkingOrderPrior, 8 | RelinkingOrderSubsequent 9 | } -------------------------------------------------------------------------------- /behavior-graph/src/commonMain/kotlin/behaviorgraph/DateProvider.kt: -------------------------------------------------------------------------------- 1 | package behaviorgraph 2 | 3 | /** 4 | * A Graph [Event] saves the current time when it runs. 5 | * This optional interface lets this time be overridden, typically for testing purposes. 6 | */ 7 | interface DateProvider { 8 | fun now(): Long 9 | } -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | I confirm that this contribution is made under the terms of the license found in the root directory of this repository's source tree and that I have the authority necessary to make this contribution on behalf of its copyright owner. 4 | -------------------------------------------------------------------------------- /example/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /example/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /behavior-graph/src/commonMain/kotlin/behaviorgraph/BehaviorGraphException.kt: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright Yahoo 2021 3 | // 4 | package behaviorgraph 5 | 6 | open class BehaviorGraphException : RuntimeException { 7 | constructor(message: String, ex: Exception?): super(message, ex) 8 | constructor(message: String): super(message) 9 | constructor(ex: Exception): super(ex) 10 | } 11 | -------------------------------------------------------------------------------- /example/src/main/kotlin/com/example/myapplication/ui/login/Globals.kt: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright Yahoo 2021 3 | // 4 | package com.example.myapplication.ui.login 5 | 6 | import behaviorgraph.Graph 7 | 8 | object Globals { 9 | init { 10 | } 11 | 12 | var graph = Graph() 13 | 14 | init { 15 | reinit() 16 | } 17 | 18 | @JvmStatic 19 | fun reinit() { 20 | graph = Graph() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /example/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | local.properties 2 | .gradle 3 | .idea 4 | build 5 | .DS_Store 6 | .kotlin 7 | 8 | # Compiled class file 9 | *.class 10 | 11 | # Log file 12 | *.log 13 | 14 | # BlueJ files 15 | *.ctxt 16 | 17 | # Mobile Tools for Java (J2ME) 18 | .mtj.tmp/ 19 | 20 | # Package Files # 21 | *.jar 22 | *.war 23 | *.nar 24 | *.ear 25 | *.zip 26 | *.tar.gz 27 | *.rar 28 | 29 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 30 | hs_err_pid* 31 | -------------------------------------------------------------------------------- /behavior-graph/src/commonMain/kotlin/behaviorgraph/ExtentRemoveStrategy.kt: -------------------------------------------------------------------------------- 1 | package behaviorgraph 2 | 3 | /** 4 | * When removing an extent the default is to remove only that extent. 5 | * We can specify `ContainedLifetimes` to indicate that we want all extents with the same or 6 | * shorter lifetimes to be removed as well. This can be convenient when removing a subtree of extents. 7 | */ 8 | enum class ExtentRemoveStrategy { 9 | ExtentOnly, 10 | ContainedLifetimes 11 | } 12 | -------------------------------------------------------------------------------- /tutorial-2-swing/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'application' 3 | } 4 | 5 | group 'com.yahoo.behaviorgraph' 6 | 7 | repositories { 8 | mavenCentral() 9 | } 10 | 11 | application { 12 | mainClass = 'Tutorial' 13 | } 14 | 15 | dependencies { 16 | implementation project(':behavior-graph') 17 | } 18 | 19 | dependencies { 20 | testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0' 21 | testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0' 22 | } 23 | 24 | test { 25 | useJUnitPlatform() 26 | } -------------------------------------------------------------------------------- /tutorial-3-swing/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'application' 3 | } 4 | 5 | group 'com.yahoo.behaviorgraph' 6 | 7 | repositories { 8 | mavenCentral() 9 | } 10 | 11 | application { 12 | mainClass = 'Tutorial' 13 | } 14 | 15 | dependencies { 16 | implementation project(':behavior-graph') 17 | } 18 | 19 | dependencies { 20 | testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0' 21 | testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0' 22 | } 23 | 24 | test { 25 | useJUnitPlatform() 26 | } -------------------------------------------------------------------------------- /code-walkthrough/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'application' 3 | } 4 | 5 | group 'com.yahoo.behaviorgraph' 6 | 7 | repositories { 8 | mavenCentral() 9 | } 10 | 11 | //application { 12 | // mainClass = 'Tutorial' 13 | //} 14 | 15 | dependencies { 16 | implementation project(':behavior-graph') 17 | } 18 | 19 | dependencies { 20 | testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0' 21 | testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0' 22 | } 23 | 24 | test { 25 | useJUnitPlatform() 26 | } -------------------------------------------------------------------------------- /behavior-graph/src/commonMain/kotlin/behaviorgraph/PlatformSpecific.kt: -------------------------------------------------------------------------------- 1 | package behaviorgraph 2 | 3 | import kotlinx.coroutines.sync.Mutex 4 | 5 | internal interface PlatformSpecific { 6 | fun assert(condition: Boolean, lazyMessage: () -> String) 7 | fun safeAddToActionQueue(action: RunnableAction, queue: MutableList, mutex: Mutex) 8 | fun nameResources(focus: Any) 9 | fun setCurrentThread(state: EventLoopState) 10 | fun runningOnCurrentThread(state: EventLoopState?): Boolean 11 | fun defaultNameForExtent(extent: Extent<*>): String 12 | } -------------------------------------------------------------------------------- /behavior-graph/src/commonMain/kotlin/behaviorgraph/LinkType.kt: -------------------------------------------------------------------------------- 1 | package behaviorgraph 2 | 3 | /** 4 | * [Linkable] types are objects that can be demanded. It can be one of two types. 5 | * The default **Reactive** type means that when the corresponding resource is updated, 6 | * the demanding behavior will be activated to run. 7 | * An **Order** type means the behavior will not be activated when that resource updates 8 | * but because it is still dependent on it is guaranteed to run afterwards if it activates 9 | * from another resource updating. 10 | */ 11 | enum class LinkType { 12 | Reactive, 13 | Order 14 | } -------------------------------------------------------------------------------- /behavior-graph/src/commonMain/kotlin/behaviorgraph/EventLoopState.kt: -------------------------------------------------------------------------------- 1 | package behaviorgraph 2 | 3 | internal data class EventLoopState(val action: RunnableAction, val actionUpdates: MutableList = mutableListOf(), var currentSideEffect: SideEffect? = null, var phase: EventLoopPhase = EventLoopPhase.Queued) { 4 | internal var thread: Any? = null 5 | override fun toString(): String { 6 | var rows = mutableListOf("Action") 7 | actionUpdates?.forEach { resource -> 8 | rows.add(" " + resource.toString()) 9 | } 10 | return rows.joinToString("\n") 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /example/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | My Application 3 | 4 | Email 5 | Password 6 | Sign in or register 7 | Sign in 8 | "Welcome !" 9 | Not a valid username 10 | Password must be >5 characters 11 | "Login failed" 12 | 13 | -------------------------------------------------------------------------------- /behavior-graph/src/commonMain/kotlin/behaviorgraph/Linkable.kt: -------------------------------------------------------------------------------- 1 | package behaviorgraph 2 | 3 | /** 4 | * A behavior demands a set of **Demandables**. 5 | * A [Resource], [Moment], and [State] are all Demandables. 6 | * By default these Demandables have a [LinkType] of `Reactive` which means a demanding behavior will 7 | * run when the resource is updated. An `Order` type will let the behavior access the demanded resource 8 | * but it will not necessarily reun when that resource is updated. 9 | */ 10 | interface Linkable { 11 | val resource: Resource 12 | val type: LinkType 13 | } 14 | 15 | data class DemandLink(override val resource: Resource, override val type: LinkType) : Linkable -------------------------------------------------------------------------------- /code-walkthrough/src/main/java/CodeWalkthrough.java: -------------------------------------------------------------------------------- 1 | import behaviorgraph.Graph; 2 | 3 | import javax.swing.*; 4 | 5 | public class CodeWalkthrough { 6 | JButton loginButton = new JButton(); 7 | 8 | public static void main(String[] args) { 9 | Graph graph = new Graph(); 10 | LoginForm loginForm = new LoginForm(graph); 11 | loginForm.addToGraphWithAction(); 12 | } 13 | 14 | public void configureUI() { 15 | Graph graph = new Graph(); 16 | LoginForm loginForm = new LoginForm(graph); 17 | loginButton.addActionListener(e -> { 18 | graph.action(() -> { 19 | loginForm.loginClick.updateWithAction(); 20 | }); 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tutorial-2-swing/src/main/java/Tutorial.java: -------------------------------------------------------------------------------- 1 | import behaviorgraph.Graph; 2 | 3 | public class Tutorial { 4 | public static void main(String[] args) { 5 | //Schedule a job for the event-dispatching thread: 6 | //creating and showing this application's GUI. 7 | javax.swing.SwingUtilities.invokeLater(new Runnable() { 8 | TutorialUI ui; 9 | Thermostat tm; 10 | Graph graph; 11 | public void run() { 12 | ui = new TutorialUI(); 13 | graph = new Graph(); 14 | tm = new Thermostat(graph, ui); 15 | tm.addToGraphWithAction(); 16 | ui.createAndShowGUI(); 17 | } 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tutorial-3-swing/src/main/java/Tutorial.java: -------------------------------------------------------------------------------- 1 | import behaviorgraph.Graph; 2 | 3 | public class Tutorial { 4 | public static void main(String[] args) { 5 | //Schedule a job for the event-dispatching thread: 6 | //creating and showing this application's GUI. 7 | javax.swing.SwingUtilities.invokeLater(new Runnable() { 8 | ListUI ui; 9 | ListExtent list; 10 | Graph graph; 11 | public void run() { 12 | ui = new ListUI(); 13 | graph = new Graph(); 14 | list = new ListExtent(graph, ui); 15 | list.addToGraphWithAction(); 16 | ui.createAndShowGUI(); 17 | } 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /example/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /example/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | -------------------------------------------------------------------------------- /example/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /example/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | -------------------------------------------------------------------------------- /tutorial-3-swing/src/main/java/ItemUI.java: -------------------------------------------------------------------------------- 1 | import javax.swing.*; 2 | import java.awt.*; 3 | import java.awt.font.TextAttribute; 4 | import java.util.Map; 5 | 6 | public class ItemUI extends JPanel { 7 | JCheckBox completedCheckbox; 8 | JLabel itemText; 9 | JButton itemDelete; 10 | 11 | public ItemUI() { 12 | super(new FlowLayout(FlowLayout.LEFT)); 13 | completedCheckbox = new JCheckBox(); 14 | add(completedCheckbox); 15 | 16 | itemText = new JLabel(); 17 | add(itemText); 18 | 19 | itemDelete = new JButton("Delete"); 20 | add(itemDelete); 21 | } 22 | 23 | public void setCompleted(boolean completed) { 24 | Map textAttributes = itemText.getFont().getAttributes(); 25 | textAttributes.put(TextAttribute.STRIKETHROUGH, completed ? Boolean.TRUE : Boolean.FALSE); 26 | itemText.setFont(itemText.getFont().deriveFont(textAttributes)); 27 | } 28 | 29 | public void setSelected(boolean selected) { 30 | this.setBackground(selected ? Color.GREEN : null); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /behavior-graph/src/commonMain/kotlin/behaviorgraph/Action.kt: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright Yahoo 2021 3 | // 4 | package behaviorgraph 5 | 6 | import kotlinx.coroutines.CompletableJob 7 | import kotlinx.coroutines.Job 8 | 9 | /** 10 | * An __Action__ is a block of code which initiates a Behavior Graph [Event]. 11 | * You create actions with the [action], [actionAsync] methods on an [Extent]. 12 | * You can also use [Graph.action], [Graph.actionAsync] on your [Graph] instance. 13 | * You do not create an action directly. 14 | * The block of code in actions is run by the Behavior Graph runtime. 15 | */ 16 | interface Action { 17 | val debugName: String? 18 | } 19 | 20 | abstract class RunnableAction: Action { 21 | abstract fun runAction() 22 | internal val job: CompletableJob = Job() 23 | } 24 | 25 | internal class GraphAction(val thunk: Thunk, override val debugName: String? = null): RunnableAction() { 26 | override fun runAction() { 27 | thunk.invoke() 28 | } 29 | } 30 | 31 | internal class ExtentAction(val thunk: ExtentThunk, val context: T, override val debugName: String? = null): 32 | RunnableAction() { 33 | override fun runAction() { 34 | thunk.invoke(context) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /behavior-graph/src/commonMain/kotlin/behaviorgraph/SideEffect.kt: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright Yahoo 2021 3 | // 4 | package behaviorgraph 5 | 6 | import kotlinx.coroutines.CoroutineDispatcher 7 | 8 | /** 9 | * SideEffects are blocks of code that are guaranteed to run at the end of the current event. 10 | * Use them to create output to external APIs. 11 | */ 12 | interface SideEffect { 13 | val debugName: String? 14 | val behavior: Behavior<*>? 15 | val dispatcher: CoroutineDispatcher? 16 | } 17 | 18 | internal interface RunnableSideEffect: SideEffect { 19 | fun run() 20 | } 21 | 22 | internal class GraphSideEffect( 23 | val thunk: Thunk, 24 | override val behavior: Behavior<*>?, 25 | override val debugName: String? = null, 26 | override val dispatcher: CoroutineDispatcher? = null 27 | ): RunnableSideEffect { 28 | override fun run() { 29 | thunk.invoke() 30 | } 31 | } 32 | 33 | internal class ExtentSideEffect( 34 | val thunk: ExtentThunk, 35 | val context: T, 36 | override val behavior: Behavior?, 37 | override val debugName: String? = null, 38 | override val dispatcher: CoroutineDispatcher? = null 39 | ): RunnableSideEffect { 40 | override fun run() { 41 | thunk.invoke(context) 42 | } 43 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | android.defaults.buildfeatures.buildconfig=true 21 | android.nonTransitiveRClass=false 22 | android.nonFinalResIds=false 23 | -------------------------------------------------------------------------------- /behavior-graph/src/iosArm64Main/kotlin/behaviorgraph/PlatformSpecificIosArm64Main.kt: -------------------------------------------------------------------------------- 1 | package behaviorgraph 2 | 3 | import kotlinx.coroutines.runBlocking 4 | import kotlinx.coroutines.sync.Mutex 5 | 6 | @OptIn(kotlin.experimental.ExperimentalNativeApi::class) 7 | internal actual fun makePlatformSpecific(): PlatformSpecific { 8 | return object : PlatformSpecific { 9 | override fun assert(condition: Boolean, lazyMessage: () -> String) { 10 | kotlin.assert(condition, lazyMessage) 11 | } 12 | 13 | override fun safeAddToActionQueue(action: RunnableAction, queue: MutableList, mutex: Mutex) { 14 | runBlocking { 15 | try { 16 | mutex.lock() 17 | queue.add(action) 18 | } finally { 19 | mutex.unlock() 20 | } 21 | } 22 | } 23 | 24 | override fun nameResources(focus: Any) { 25 | } 26 | 27 | override fun setCurrentThread(state: EventLoopState) { 28 | // NO Op 29 | } 30 | 31 | override fun runningOnCurrentThread(state: EventLoopState?): Boolean { 32 | return state != null 33 | } 34 | 35 | override fun defaultNameForExtent(extent: Extent<*>): String { 36 | return "Extent" 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /code-walkthrough/src/main/java/LoginForm.java: -------------------------------------------------------------------------------- 1 | import behaviorgraph.Extent; 2 | import behaviorgraph.Graph; 3 | import behaviorgraph.Moment; 4 | import behaviorgraph.State; 5 | 6 | public class LoginForm extends Extent { 7 | State loginEnabled = state(false); 8 | State email = state(""); 9 | State password = state(""); 10 | Moment loginClick = moment(); 11 | State loggingIn = state(false); 12 | 13 | public LoginForm(Graph graph) { 14 | super(graph); 15 | 16 | behavior() 17 | .supplies(loggingIn) 18 | .demands(loginClick) 19 | .runs(ctx -> { 20 | if (loginClick.justUpdated() && !this.loggingIn.value()) { 21 | loggingIn.update(true); 22 | } 23 | }); 24 | 25 | behavior() 26 | .supplies(loginEnabled) 27 | .demands(email, password, loggingIn) 28 | .runs(ctx -> { 29 | boolean emailValid = validateEmail(email.value()); 30 | boolean passwordValid = password.value().length() > 0; 31 | boolean enabled = emailValid && passwordValid && !loggingIn.value(); 32 | loginEnabled.update(enabled); 33 | 34 | sideEffect(ctx1 -> { 35 | // loginButton.setEnabled(loginEnabled.value()); 36 | }); 37 | }); 38 | } 39 | 40 | private boolean validateEmail(String email) { 41 | // ... validate email code goes here 42 | return true; 43 | } 44 | } 45 | 46 | -------------------------------------------------------------------------------- /behavior-graph/src/commonMain/kotlin/behaviorgraph/Event.kt: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright Yahoo 2021 3 | // 4 | package behaviorgraph 5 | 6 | /** 7 | * An **Event** is a single run pass through a [Graph] instance. It starts with 8 | * an [Action], proceeds through running each relevant [Behavior]. And completes 9 | * after the last [GraphSideEffect] is run. Every resource that is updated during this 10 | * pass will point to the same `Event` instance. 11 | * 12 | * You do not create Events explicitly. The Graph instance will create one when it runs. 13 | * 14 | * `InitialEvent` is the Zero event which occurs _before_ all other events. 15 | * 16 | */ 17 | class Event internal constructor(sequence: Long, timestamp: Long) { 18 | /** 19 | * Each Event is assigned a monotonically increasing number. You can use this information to quickly determine the order in which resources update. 20 | */ 21 | val sequence: Long 22 | 23 | /** 24 | * Each event is given a timestamp. By default this is the current time in milliseconds but can be overridden with the [DateProvider] instance passed on [Graph] construction. 25 | */ 26 | val timestamp: Long 27 | 28 | init { 29 | this.sequence = sequence 30 | this.timestamp = timestamp 31 | } 32 | 33 | companion object { 34 | /** 35 | * `InitialEvent` is the Zero event which occurs _before_ all other events. 36 | * New [State] resources automatically are given this event to pair with their initial values. 37 | */ 38 | val InitialEvent: Event = Event(0, 0) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /behavior-graph/src/jsMain/kotlin/behaviorgraph/PlatformSpecificJS.kt: -------------------------------------------------------------------------------- 1 | package behaviorgraph 2 | 3 | import kotlinx.coroutines.sync.Mutex 4 | 5 | internal actual fun makePlatformSpecific(): PlatformSpecific { 6 | return object : PlatformSpecific { 7 | override fun assert(condition: Boolean, lazyMessage: () -> String) { 8 | if (!condition) { 9 | throw AssertionError(lazyMessage()) 10 | } 11 | } 12 | 13 | override fun safeAddToActionQueue(action: RunnableAction, queue: MutableList, mutex: Mutex) { 14 | // JS is single threaded, so we don't need to lock the queue 15 | queue.add(action) 16 | } 17 | 18 | override fun nameResources(focus: Any) { 19 | val dynamicFocus = focus.asDynamic() 20 | val keys = js("Object").keys(dynamicFocus) as Array 21 | for (key in keys) { 22 | // iterate through each field and see if it is a resource subclass 23 | val field = dynamicFocus[key] 24 | if (field != null && field["__bg_isResource"] != null) { 25 | // sometimes fields aren't accessible to reflection, try enabling that 26 | if (field["debugName"] == null) { 27 | field["debugName"] = key 28 | } 29 | } 30 | } 31 | } 32 | 33 | override fun setCurrentThread(state: EventLoopState) { 34 | // NO Op 35 | } 36 | 37 | override fun runningOnCurrentThread(state: EventLoopState?): Boolean { 38 | return state != null 39 | } 40 | 41 | override fun defaultNameForExtent(extent: Extent<*>): String { 42 | return extent.asDynamic().constructor.name as? String ?: "Extent" 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /example/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | 31 | -------------------------------------------------------------------------------- /tutorial-1/src/main/java/Tutorial.java: -------------------------------------------------------------------------------- 1 | import behaviorgraph.Extent; 2 | import behaviorgraph.Graph; 3 | import behaviorgraph.Moment; 4 | import behaviorgraph.State; 5 | 6 | public class Tutorial { 7 | public static void main(String[] args) { 8 | Graph g = new Graph(); 9 | TutorialExtent e = new TutorialExtent(g); 10 | 11 | e.addToGraphWithAction(); 12 | 13 | g.action(() -> { 14 | e.person.update("World"); 15 | e.greeting.update("Hello"); 16 | }); 17 | g.action(() -> { 18 | e.greeting.update("Goodbye"); 19 | }); 20 | g.action(() -> { 21 | e.button.update(); 22 | e.greeting.update("Nevermind"); 23 | e.loggingEnabled.update(false); 24 | }); 25 | } 26 | } 27 | 28 | class TutorialExtent extends Extent { 29 | 30 | State person = state("Nobody"); 31 | State greeting = state("Greetings"); 32 | Moment button = moment(); 33 | State message = state(null); 34 | Moment sentMessage = moment(); 35 | State loggingEnabled = state(true); 36 | 37 | public TutorialExtent(Graph g) { 38 | super(g); 39 | 40 | behavior() 41 | .supplies(message, sentMessage) 42 | .demands(person, greeting, button) 43 | .runs(ctx -> { 44 | message.update(greeting.value() + ", " + person.value() + "!"); 45 | if (button.justUpdated()) { 46 | System.out.println(message.value()); 47 | sentMessage.update(); 48 | } 49 | }); 50 | 51 | behavior() 52 | .demands(message, sentMessage, loggingEnabled) 53 | .runs(ctx -> { 54 | if (loggingEnabled.value()) { 55 | if (message.justUpdated()) { 56 | System.out.println("Message changed to: " + message.value() + " : " + message.event().getTimestamp()); 57 | } 58 | if (sentMessage.justUpdated()) { 59 | System.out.println("Message sent: " + message.value() + " : " + message.event().getTimestamp()); 60 | } 61 | } 62 | }); 63 | } 64 | 65 | } -------------------------------------------------------------------------------- /example/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | } 5 | 6 | repositories { 7 | mavenLocal() 8 | } 9 | 10 | android { 11 | 12 | compileSdkVersion 33 13 | 14 | defaultConfig { 15 | applicationId 'com.yahoo.demo.BehaviorGraphExample' 16 | minSdkVersion 19 17 | targetSdkVersion 33 18 | versionCode 1 19 | versionName "1.0" 20 | 21 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 22 | } 23 | 24 | buildTypes { 25 | release { 26 | minifyEnabled true 27 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 28 | } 29 | } 30 | compileOptions { 31 | sourceCompatibility JavaVersion.VERSION_11 32 | targetCompatibility JavaVersion.VERSION_11 33 | } 34 | kotlinOptions { 35 | jvmTarget = '11' 36 | } 37 | buildFeatures { 38 | viewBinding true 39 | } 40 | 41 | sourceSets.all { 42 | java.srcDir("src/$name/kotlin") 43 | } 44 | namespace 'com.example.myapplication' 45 | } 46 | 47 | dependencies { 48 | 49 | implementation project(':behavior-graph') 50 | //implementation 'com.yahoo.behavior-graph:bgkotlin:0.5.0-RC1' 51 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 52 | implementation "androidx.core:core-ktx:1.10.1" 53 | implementation 'androidx.appcompat:appcompat:1.3.1' 54 | implementation 'com.google.android.material:material:1.4.0' 55 | //implementation 'androidx.annotation:annotation:1.3.0' 56 | implementation 'androidx.constraintlayout:constraintlayout:2.1.0' 57 | implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1' 58 | implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1' 59 | testImplementation 'junit:junit:4.13.2' 60 | testImplementation 'org.robolectric:robolectric:4.4' 61 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 62 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 63 | } 64 | -------------------------------------------------------------------------------- /tutorial-3-swing/src/main/java/ItemExtent.java: -------------------------------------------------------------------------------- 1 | import behaviorgraph.Extent; 2 | import behaviorgraph.Graph; 3 | import behaviorgraph.State; 4 | 5 | import java.awt.event.ItemEvent; 6 | import java.awt.event.MouseAdapter; 7 | import java.awt.event.MouseEvent; 8 | 9 | public class ItemExtent extends Extent { 10 | ListExtent list; 11 | State itemText; 12 | ItemUI itemUI; 13 | State completed; 14 | 15 | public ItemExtent(Graph g, String inText, ListExtent inList) { 16 | super(g); 17 | 18 | list = inList; 19 | itemText = state(inText); 20 | itemUI = new ItemUI(); 21 | completed = state(false); 22 | itemUI.completedCheckbox.addItemListener(itemEvent -> { 23 | completed.updateWithAction(itemEvent.getStateChange() == ItemEvent.SELECTED); 24 | }); 25 | itemUI.itemDelete.addActionListener(actionEvent -> { 26 | list.removeItem.updateWithAction(this); 27 | }); 28 | itemUI.addMouseListener(new MouseAdapter() { 29 | @Override 30 | public void mouseClicked(MouseEvent e) { 31 | list.selectRequest.updateWithAction(ItemExtent.this); 32 | } 33 | }); 34 | behavior() 35 | .demands(itemText, getDidAdd()) 36 | .runs(ctx -> { 37 | sideEffect(ctx1 -> { 38 | itemUI.itemText.setText(itemText.value()); 39 | }); 40 | }); 41 | 42 | behavior() 43 | .demands(completed, getDidAdd()) 44 | .runs(ctx -> { 45 | sideEffect(ctx1 -> { 46 | itemUI.setCompleted(completed.value()); 47 | }); 48 | }); 49 | 50 | behavior() 51 | .demands(list.selected, getDidAdd()) 52 | .runs(ctx -> { 53 | var selected = list.selected.value() == this; 54 | sideEffect(ctx1 -> { 55 | itemUI.setSelected(selected); 56 | }); 57 | }); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /behavior-graph/src/jvmMain/kotlin/behaviorgraph/PlatformSpecificJVM.kt: -------------------------------------------------------------------------------- 1 | package behaviorgraph 2 | 3 | import kotlinx.coroutines.runBlocking 4 | import kotlinx.coroutines.sync.Mutex 5 | 6 | internal actual fun makePlatformSpecific(): PlatformSpecific { 7 | return object : PlatformSpecific { 8 | override fun assert(condition: Boolean, lazyMessage: () -> String) { 9 | kotlin.assert(condition, lazyMessage) 10 | } 11 | 12 | override fun safeAddToActionQueue(action: RunnableAction, queue: MutableList, mutex: Mutex) { 13 | runBlocking { 14 | try { 15 | mutex.lock() 16 | queue.add(action) 17 | } finally { 18 | mutex.unlock() 19 | } 20 | } 21 | } 22 | 23 | override fun nameResources(focus: Any) { 24 | try { 25 | focus.javaClass.declaredFields.forEach { field -> 26 | // iterate through each field and see if its a resource subclass 27 | if ((Resource::class.java as Class).isAssignableFrom(field.type)) { 28 | // sometimes fields aren't accessible to reflection, try enabling that 29 | field.isAccessible = true // throws error if not possible 30 | val resource = field.get(focus) as Resource 31 | if (resource.debugName == null) { 32 | resource.debugName = field.name 33 | } 34 | } 35 | } 36 | } catch (ex: Exception) { 37 | // throws error if we cannot make fields accessible for security reasons 38 | // catching the error is fine here, it just means we won't get debug names 39 | } 40 | } 41 | 42 | override fun setCurrentThread(state: EventLoopState) { 43 | state.thread = Thread.currentThread() 44 | } 45 | 46 | override fun runningOnCurrentThread(state: EventLoopState?): Boolean { 47 | return state?.thread == Thread.currentThread() 48 | } 49 | 50 | override fun defaultNameForExtent(extent: Extent<*>): String { 51 | return extent.javaClass.simpleName 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /behavior-graph/src/commonTest/kotlin/behaviorgraph/AbstractBehaviorGraphTest.kt: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright Yahoo 2021 3 | // 4 | package behaviorgraph 5 | 6 | import kotlinx.coroutines.test.TestDispatcher 7 | import kotlinx.coroutines.test.UnconfinedTestDispatcher 8 | //import java.lang.reflect.Field 9 | import kotlin.test.* 10 | //import kotlin.reflect.KClass 11 | 12 | @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) 13 | abstract class AbstractBehaviorGraphTest 14 | { 15 | open class TestExtent(g: Graph) : Extent(g) 16 | 17 | var testDispatcher: TestDispatcher = UnconfinedTestDispatcher() 18 | lateinit var g: Graph 19 | protected lateinit var setupExt: TestExtent 20 | lateinit var ext: TestExtent 21 | lateinit var r_a: State 22 | lateinit var r_b: State 23 | lateinit var r_c: State 24 | 25 | // /** 26 | // * Usage: assertExpectedException(Exception::class) {...lambda to run} 27 | // * expects to match on the exact expectedClass 28 | // */ 29 | // protected fun assertExpectedException(expectedClass: KClass<*>, lambda: () -> Unit) { 30 | // try { 31 | // lambda() 32 | // } catch (e: Exception) { 33 | // if ( e.javaClass == expectedClass.java) { 34 | // return 35 | // } 36 | // fail("unexpected exception. Expected: $expectedClass but found $e") 37 | // } 38 | // fail("did not catch expected exception: $expectedClass") 39 | // } 40 | 41 | protected fun assertNoThrow(lambda: () -> Unit) { 42 | try { 43 | lambda() 44 | } catch (e: Exception) { 45 | fail("Unexpected exception") 46 | } 47 | assertTrue(true, "Did not throw") 48 | } 49 | 50 | // protected fun reflectionGetField(obj: Any, name: String): Any? { 51 | // val field: Field = obj.javaClass.getDeclaredField(name) 52 | // field.trySetAccessible() 53 | // return field.get(obj) 54 | // } 55 | // 56 | @BeforeTest 57 | open fun setUp() { 58 | g = Graph() 59 | g.defaultSideEffectDispatcher = testDispatcher 60 | setupExt = TestExtent(g) 61 | ext = TestExtent(g) 62 | r_a = setupExt.state(0, "r_a") 63 | r_b = setupExt.state( 0, "r_b") 64 | r_c = setupExt.state(0, "r_c") 65 | setupExt.addToGraphWithAction() 66 | setupExt.addChildLifetime(ext) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | First, thanks for taking the time to contribute to our project! There are many ways you can help out. 3 | 4 | ### Questions 5 | 6 | If you have a question that needs an answer, create an issue, and label it as a question. 7 | 8 | ### Issues for bugs or feature requests 9 | 10 | If you encounter any bugs in the code, or want to request a new feature or enhancement, please create an issue to report it. Kindly add a label to indicate what type of issue it is. 11 | 12 | ### Contribute Code 13 | We welcome your pull requests for bug fixes. To implement something new, please create an issue first so we can discuss it together. 14 | 15 | ***Creating a Pull Request*** 16 | Please follow [best practices](https://github.com/trein/dev-best-practices/wiki/Git-Commit-Best-Practices) for creating git commits. In addition: 17 | 18 | - Make sure your code respects existing formatting conventions. In general, follow 19 | the same coding style as the code that you are modifying. 20 | - Bugfixes must include a unit test or integration test reproducing the issue. 21 | - Try to keep pull requests short and submit separate ones for unrelated 22 | features, but feel free to combine simple bugfixes/tests into one pull request. 23 | - Keep the number of commits small and combine commits for related changes. 24 | - Keep formatting changes in separate commits to make code reviews easier and 25 | distinguish them from actual code changes. 26 | 27 | When your code is ready to be submitted, submit a pull request to begin the code review process. 28 | 29 | We only seek to accept code that you are authorized to contribute to the project. We have added a pull request template on our projects so that your contributions are made with the following confirmation: 30 | 31 | > I confirm that this contribution is made under the terms of the license found in the root directory of this repository's source tree and that I have the authority necessary to make this contribution on behalf of its copyright owner. 32 | 33 | ## Code of Conduct 34 | 35 | We encourage inclusive and professional interactions on our project. We welcome everyone to open an issue, improve the documentation, report bug or submit a pull request. By participating in this project, you agree to abide by the [Code of Conduct](Code-Of-Conduct.md). If you feel there is a conduct issue related to this project, please raise it per the Code of Conduct process and we will address it. 36 | 37 | -------------------------------------------------------------------------------- /tutorial-3-swing/src/main/java/ListUI.java: -------------------------------------------------------------------------------- 1 | import javax.swing.*; 2 | import java.awt.*; 3 | 4 | public class ListUI { 5 | JTextField newItemText; 6 | JButton save; 7 | JPanel itemsPanel; 8 | JLabel remainingItems; 9 | JLabel actionLabel; 10 | JFrame frame; 11 | public ListUI() { 12 | newItemText = new JTextField("", 20); 13 | save = new JButton("Save"); 14 | itemsPanel = new JPanel(); 15 | remainingItems = new JLabel(); 16 | actionLabel = new JLabel("Add Item:"); 17 | } 18 | 19 | public void createAndShowGUI() { 20 | //Create and set up the window. 21 | frame = new JFrame("Behavior Graph Tutorial 3"); 22 | frame.setResizable(false); 23 | frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 24 | 25 | GridBagConstraints c; 26 | BoxLayout layout; 27 | 28 | Container mainPane = frame.getContentPane(); 29 | layout = new BoxLayout(mainPane, BoxLayout.PAGE_AXIS); 30 | 31 | JLabel title = new JLabel("Todo List"); 32 | title.setFont(new Font("Helvetica", Font.BOLD, 16)); 33 | mainPane.add(title); 34 | mainPane.setLayout(layout); 35 | 36 | JPanel headerPane = new JPanel(); 37 | headerPane.setBackground(Color.LIGHT_GRAY); 38 | headerPane.add(actionLabel); 39 | headerPane.add(newItemText); 40 | headerPane.add(save); 41 | mainPane.add(headerPane); 42 | 43 | BoxLayout itemsLayout = new BoxLayout(itemsPanel, BoxLayout.PAGE_AXIS); 44 | itemsPanel.setLayout(itemsLayout); 45 | mainPane.add(itemsPanel); 46 | 47 | JPanel footerPane = new JPanel(); 48 | footerPane.setBackground(Color.LIGHT_GRAY); 49 | footerPane.add(remainingItems); 50 | mainPane.add(footerPane); 51 | 52 | //Display the window. 53 | frame.pack(); 54 | frame.setVisible(true); 55 | } 56 | 57 | public void addItem(ItemUI inItem) { 58 | itemsPanel.add(inItem); 59 | frame.pack(); 60 | } 61 | 62 | public void removeItem(ItemUI inItem) { 63 | itemsPanel.remove(inItem); 64 | frame.pack(); 65 | } 66 | 67 | public void setSelected(ItemExtent itemExtent) { 68 | newItemText.setText(itemExtent == null ? "" : itemExtent.itemText.value()); 69 | actionLabel.setText(itemExtent == null ? "Add Item:" : "Edit Item:"); 70 | newItemText.requestFocus(); 71 | } 72 | 73 | public void setRemainingCount(long count) { 74 | remainingItems.setText("Remaining items: " + count); 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /tutorial-2-swing/src/main/java/Thermostat.java: -------------------------------------------------------------------------------- 1 | import behaviorgraph.Extent; 2 | import behaviorgraph.Graph; 3 | import behaviorgraph.Moment; 4 | import behaviorgraph.State; 5 | 6 | public class Thermostat extends Extent { 7 | State desiredTemperature; 8 | State currentTemperature; 9 | State heatOn; 10 | 11 | Moment up; 12 | Moment down; 13 | TutorialUI ui; 14 | 15 | public Thermostat(Graph g, TutorialUI uiParam) { 16 | super(g); 17 | ui = uiParam; 18 | 19 | up = moment(); 20 | ui.upButton.addActionListener(e -> { 21 | up.updateWithAction(); 22 | }); 23 | 24 | down = moment(); 25 | ui.downButton.addActionListener(e -> { 26 | down.updateWithAction(); 27 | }); 28 | 29 | desiredTemperature = state(60); 30 | currentTemperature = state(60); 31 | heatOn = state(false); 32 | 33 | // Set the desired Temperature 34 | behavior() 35 | .supplies(desiredTemperature) 36 | .demands(up, down, getDidAdd()) 37 | .runs(ctx -> { 38 | if (up.justUpdated()) { 39 | desiredTemperature.update(desiredTemperature.value() + 1); 40 | } else if (down.justUpdated()) { 41 | desiredTemperature.update(desiredTemperature.value() - 1); 42 | } 43 | sideEffect(ctx1 -> { 44 | ui.desiredTemp.setText(desiredTemperature.value().toString()); 45 | }); 46 | }); 47 | 48 | // Update current temperature display 49 | behavior() 50 | .demands(currentTemperature, getDidAdd()) 51 | .runs(ctx -> { 52 | sideEffect(ctx1 -> { 53 | ui.currentTemp.setText(currentTemperature.value().toString()); 54 | }); 55 | }); 56 | 57 | // Determine if heat is on 58 | behavior() 59 | .supplies(heatOn) 60 | .demands(currentTemperature, desiredTemperature, getDidAdd()) 61 | .runs(ctx -> { 62 | boolean on = desiredTemperature.value() > currentTemperature.value(); 63 | heatOn.update(on); 64 | sideEffect(ctx1 -> { 65 | ui.heatStatus.setText("Heat " + (heatOn.value() ? "On" : "Off")); 66 | }); 67 | }); 68 | 69 | // Control heating equipment 70 | behavior() 71 | .demands(heatOn) 72 | .runs(ctx -> { 73 | if (heatOn.justUpdatedTo(true)) { 74 | sideEffect(ctx1 -> { 75 | ui.turnOnHeat(e -> { 76 | currentTemperature.updateWithAction(currentTemperature.value() + 1); 77 | }); 78 | }); 79 | } else if (heatOn.justUpdatedTo(false)) { 80 | sideEffect(ctx1 -> { 81 | ui.turnOffHeat(); 82 | }); 83 | } 84 | }); 85 | 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /behavior-graph/src/commonMain/kotlin/behaviorgraph/BehaviorQueue.kt: -------------------------------------------------------------------------------- 1 | package behaviorgraph 2 | 3 | internal class BehaviorQueue { 4 | // Keeps track of activated behaviors and when asked 5 | // runs the next one with the lowest order. 6 | // Uses a binary heap 7 | 8 | private var behaviors: MutableList> = mutableListOf() 9 | 10 | fun add(behavior: Behavior<*>) { 11 | behaviors.add(behavior) 12 | heapUp(behaviors.size - 1) 13 | } 14 | 15 | fun peek(): Behavior<*>? { 16 | return behaviors.firstOrNull() 17 | } 18 | 19 | fun pop(): Behavior<*>? { 20 | if (behaviors.isEmpty()) return null 21 | 22 | val min = behaviors[0] 23 | // swap with last to remove from end 24 | val last = behaviors.removeAt(behaviors.size - 1) 25 | if (behaviors.isNotEmpty()) { 26 | behaviors[0] = last 27 | heapDown(0) 28 | } 29 | return min 30 | } 31 | 32 | private fun heapUp(index: Int) { 33 | var currentIndex = index 34 | var parentIndex = (currentIndex - 1) / 2 35 | 36 | while (currentIndex > 0 && behaviors[currentIndex] < behaviors[parentIndex]) { 37 | swap(currentIndex, parentIndex) 38 | currentIndex = parentIndex 39 | parentIndex = (currentIndex - 1) / 2 40 | } 41 | } 42 | 43 | private fun heapDown(index: Int) { 44 | var currentIndex = index 45 | 46 | while (true) { 47 | val leftChildIndex = (2 * currentIndex) + 1 48 | val rightChildIndex = (2 * currentIndex) + 2 49 | var smallestIndex = currentIndex 50 | 51 | if (leftChildIndex < behaviors.size && behaviors[leftChildIndex] < behaviors[smallestIndex]) { 52 | smallestIndex = leftChildIndex 53 | } 54 | 55 | if (rightChildIndex < behaviors.size && behaviors[rightChildIndex] < behaviors[smallestIndex]) { 56 | smallestIndex = rightChildIndex 57 | } 58 | 59 | if (smallestIndex == currentIndex) { 60 | break 61 | } 62 | 63 | swap(currentIndex, smallestIndex) 64 | currentIndex = smallestIndex 65 | } 66 | } 67 | 68 | private fun swap(i: Int, j: Int) { 69 | val temp = behaviors[i] 70 | behaviors[i] = behaviors[j] 71 | behaviors[j] = temp 72 | } 73 | 74 | fun reheap() { 75 | val oldBehaviors = behaviors 76 | behaviors = mutableListOf() 77 | for (b in oldBehaviors) { 78 | add(b) 79 | } 80 | } 81 | 82 | fun clear() { 83 | behaviors.clear() 84 | } 85 | 86 | val size: Int 87 | get() { return behaviors.size } 88 | } -------------------------------------------------------------------------------- /behavior-graph/src/commonTest/kotlin/behaviorgraph/BehaviorQueueTest.kt: -------------------------------------------------------------------------------- 1 | package behaviorgraph 2 | 3 | import kotlin.test.* 4 | 5 | internal class BehaviorQueueTest { 6 | 7 | lateinit var q: BehaviorQueue 8 | lateinit var g: Graph 9 | lateinit var ext: Extent 10 | 11 | @BeforeTest 12 | fun setup() { 13 | q = BehaviorQueue() 14 | g = Graph() 15 | ext = Extent(g) 16 | } 17 | 18 | fun makeBehavior(order: Long): Behavior { 19 | val b = Behavior(ext, null, null, {}) 20 | b.order = order 21 | return b 22 | } 23 | 24 | @Test 25 | fun behaviorQueueIsEmpty() { 26 | assertEquals(0, q.size) 27 | assertNull(q.pop()) 28 | } 29 | 30 | @Test 31 | fun behaviorQueueCanAddAndPop() { 32 | val b1 = makeBehavior(0) 33 | q.add(b1) 34 | assertEquals(b1, q.pop()) 35 | } 36 | 37 | @Test 38 | fun poppingMeansItMovesToTheNext() { 39 | val b1 = makeBehavior(1) 40 | q.add(b1) 41 | assertEquals(b1, q.pop()) 42 | assertNull(q.pop()) 43 | } 44 | 45 | @Test 46 | fun popsLowestOrderFirst() { 47 | val b1 = makeBehavior(1) 48 | val b2 = makeBehavior(0) 49 | q.add(b1) 50 | q.add(b2) 51 | assertEquals(b2, q.pop()) 52 | assertEquals(b1, q.pop()) 53 | } 54 | 55 | @Test 56 | fun stopSearchingWhenWeHaveFoundMinimumOrder() { 57 | val b1 = makeBehavior(7) 58 | val b2 = makeBehavior(8) 59 | q.add(b1) 60 | q.add(b2) 61 | q.pop() 62 | val b3 = makeBehavior(3) 63 | q.add(b3) 64 | assertEquals(b3, q.pop()) 65 | } 66 | 67 | @Test 68 | fun clearingRestsEverything() { 69 | // |> Given we have queue that has been used 70 | val b1 = makeBehavior(1) 71 | val b2 = makeBehavior(1) 72 | q.add(b1) 73 | q.add(b2) 74 | 75 | // |> When we clear it 76 | q.clear() 77 | 78 | // |> Then it should be empty 79 | assertNull(q.pop()) 80 | assertEquals(0, q.size) 81 | 82 | // |> And when we add more items 83 | q.add(b2) 84 | val popped = q.pop() 85 | 86 | // |> Then we should get first new item 87 | assertEquals(b2, popped) 88 | } 89 | 90 | @Test 91 | fun reheapMakesSureOrderIsCorrect() { 92 | val b1 = makeBehavior(9) 93 | val b2 = makeBehavior(9) 94 | val b3 = makeBehavior(9) 95 | q.add(b1) 96 | q.add(b2) 97 | q.add(b3) 98 | 99 | b2.order = 5 100 | q.reheap() 101 | 102 | assertEquals(q.pop(), b2) 103 | } 104 | 105 | } -------------------------------------------------------------------------------- /example/src/main/kotlin/com/example/myapplication/ui/login/LoginActivityBG.kt: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright Yahoo 2021 3 | // 4 | package com.example.myapplication.ui.login 5 | 6 | import android.os.Bundle 7 | import android.text.Editable 8 | import android.text.TextWatcher 9 | import android.widget.Button 10 | import android.widget.EditText 11 | import android.widget.TextView 12 | import androidx.appcompat.app.AppCompatActivity 13 | import com.example.myapplication.R 14 | 15 | 16 | class LoginActivityBG : AppCompatActivity() { 17 | lateinit var usernameEditText: EditText 18 | lateinit var passwordEditText: EditText 19 | lateinit var loginButton: Button 20 | lateinit var loginExtent: LoginExtent 21 | lateinit var emailFeedbackTextView: TextView 22 | lateinit var passwordFeedbackTextView: TextView 23 | lateinit var loginStatusTextView: TextView 24 | lateinit var loginSucceededButton: Button 25 | lateinit var loginFailedButton: Button 26 | 27 | 28 | override fun onCreate(savedInstanceState: Bundle?) { 29 | super.onCreate(savedInstanceState) 30 | setContentView(R.layout.activity_login) 31 | usernameEditText = findViewById(R.id.username) 32 | passwordEditText = findViewById(R.id.password) 33 | loginButton = findViewById(R.id.login) 34 | 35 | emailFeedbackTextView = findViewById(R.id.emailFeedback) 36 | passwordFeedbackTextView = findViewById(R.id.passwordFeedback) 37 | loginStatusTextView = findViewById(R.id.loginStatus) 38 | loginSucceededButton = findViewById(R.id.loginSucceededButton) 39 | loginFailedButton = findViewById(R.id.loginFailedButton) 40 | 41 | loginExtent = LoginExtent(this, Globals.graph) 42 | loginExtent 43 | Globals.graph.action("init") { 44 | loginExtent.addToGraph() 45 | } 46 | 47 | usernameEditText.afterTextChanged { 48 | loginExtent.email.updateWithAction(it) 49 | } 50 | 51 | passwordEditText.afterTextChanged { 52 | loginExtent.password.updateWithAction(it) 53 | } 54 | 55 | loginButton.setOnClickListener() { 56 | loginExtent.loginClick.updateWithAction() 57 | } 58 | 59 | loginSucceededButton.setOnClickListener() { 60 | loginExtent.loginComplete.updateWithAction(true) 61 | } 62 | 63 | loginFailedButton.setOnClickListener() { 64 | loginExtent.loginComplete.updateWithAction(false) 65 | } 66 | 67 | } 68 | } 69 | 70 | /** 71 | * Extension function to simplify setting an afterTextChanged action to EditText components. 72 | */ 73 | fun EditText.afterTextChanged(afterTextChanged: (String) -> Unit) { 74 | this.addTextChangedListener(object : TextWatcher { 75 | override fun afterTextChanged(editable: Editable?) { 76 | afterTextChanged.invoke(editable.toString()) 77 | } 78 | 79 | override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} 80 | override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} 81 | }) 82 | } 83 | -------------------------------------------------------------------------------- /behavior-graph/src/commonMain/kotlin/behaviorgraph/Resource.kt: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright Yahoo 2021 3 | // 4 | package behaviorgraph 5 | 6 | import kotlin.jvm.JvmOverloads 7 | import kotlin.jvm.JvmName 8 | 9 | import kotlin.js.JsName 10 | /** 11 | * A Resource is a node which connects between Behaviors. It can be demanded or supplied. 12 | * This is the base class for [Moment] [TypedMoment] and [State] resources. 13 | * In almost all scenarios you are interested in one of those. 14 | * 15 | * You may wish to use this Resource to enforce ordering between two behaviors without 16 | * implying any other relationship. 17 | */ 18 | open class Resource @JvmOverloads constructor(val extent: Extent<*>, @JsName("debugName") var debugName: String? = null): Linkable { 19 | val graph: Graph = extent.graph 20 | @JsName("__bg_isResource") val isResource: Boolean = true // field for javascript based reflection 21 | internal var subsequents: MutableSet> = mutableSetOf() 22 | var suppliedBy: Behavior<*>? = null 23 | internal set 24 | 25 | override val resource get() = this 26 | override val type get() = LinkType.Reactive 27 | 28 | init { 29 | extent.addResource(this) 30 | } 31 | 32 | @get:JvmName("order") 33 | val order: Linkable get() = DemandLink(this, LinkType.Order) 34 | 35 | internal open val internalJustUpdated: Boolean get() = false 36 | 37 | internal fun assertValidUpdater() { 38 | val currentBehavior = graph.currentBehavior 39 | val currentEvent = graph.currentEvent 40 | if (!graph.processingChangesOnCurrentThread) { 41 | graph.bgassert(false) { 42 | "Resource must be updated inside a behavior or action. \nResource=$this" 43 | } 44 | } 45 | if (!graph.validateDependencies) { return } 46 | if (suppliedBy != null && currentBehavior != suppliedBy) { 47 | graph.bgassert(false) { 48 | "Supplied resource can only be updated by its supplying behavior. \nResource=$this \nBehavior Trying to Update=$currentBehavior" 49 | } 50 | } 51 | if (suppliedBy == null && currentBehavior != null) { 52 | graph.bgassert(false) { 53 | "Unsupplied resource can only be updated in an action. \nResource=$this \nBehaviorTrying to Update=$currentBehavior" 54 | } 55 | } 56 | } 57 | 58 | internal fun assertValidAccessor() { 59 | if (!graph.validateDependencies) { return } 60 | // allow access to state from alternate threads while running 61 | if (!graph.platformSpecific.runningOnCurrentThread(graph.eventLoopState)) { return } 62 | val currentBehavior = graph.currentBehavior 63 | if (currentBehavior != null && currentBehavior != suppliedBy && !(currentBehavior.demands?.contains(this) ?: false)) { 64 | graph.bgassert(false) { 65 | "Cannot access the value or event of a resource inside a behavior unless it is supplied or demanded. \nResource=$this \nAccessing Behavior=$currentBehavior" 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /behavior-graph/src/commonMain/kotlin/behaviorgraph/Moment.kt: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright Yahoo 2021 3 | // 4 | package behaviorgraph 5 | 6 | import kotlinx.coroutines.Job 7 | import kotlin.jvm.JvmOverloads 8 | import kotlin.jvm.JvmName 9 | 10 | /** 11 | * A Moment is a type of resource is a type of Resource for tracking information that exists at a 12 | * single moment in time. A Button press is an example of a moment. It happens and then it is over. 13 | * Use [TypedMoment] if you wish to associate additional data with a Moment. 14 | */ 15 | class Moment @JvmOverloads constructor(extent: Extent<*>, debugName: String? = null): Resource(extent, debugName), 16 | Transient { 17 | private var _happened = false 18 | private var _happenedWhen: Event? = null 19 | 20 | /** 21 | * If this Moment has ever been update what was the last Event it was updated. 22 | * A behavior must demand this resource to access this property. 23 | */ 24 | @get:JvmName("event") 25 | val event: Event? 26 | get() { 27 | assertValidAccessor() 28 | return this._happenedWhen 29 | } 30 | 31 | /** 32 | * Is there a current event and was this Moment resource updated during this event. 33 | * A behavior must demand this resource to access this property. 34 | */ 35 | @get:JvmName("justUpdated") 36 | val justUpdated: Boolean 37 | get() { 38 | assertValidAccessor() 39 | return _happened 40 | } 41 | 42 | override val internalJustUpdated: Boolean get() = justUpdated 43 | 44 | /** 45 | * Mark this Moment resource as updated an activate any dependent behaviors. 46 | * A behavior must supply this resource in order to update it. 47 | */ 48 | fun update() { 49 | assertValidUpdater() 50 | _happened = true 51 | _happenedWhen = graph.currentEvent 52 | graph.resourceTouched(this) 53 | graph.trackTransient(this) 54 | } 55 | 56 | /** 57 | * Create a new action and call [update]. 58 | */ 59 | @JvmOverloads 60 | fun updateWithAction(debugName: String? = null): Job { 61 | return graph.action(debugName) { 62 | update() 63 | } 64 | } 65 | 66 | override fun clear() { 67 | _happened = false 68 | } 69 | 70 | override fun toString(): String { 71 | val localDebugName = debugName ?: "" 72 | val localType = super.toString() 73 | val localUpdated = if (_happened) "Updated" else "Not Updated" 74 | val localSequence = _happenedWhen?.sequence ?: "NA" 75 | return "$localDebugName $localType == $localUpdated ($localSequence)" 76 | } 77 | 78 | fun observeUpdates(onUpdated: (Event) -> Unit): Behavior<*> { 79 | val extent = this.extent as Extent 80 | val observer = extent.behavior() 81 | .demands(this) 82 | .runs { _ -> 83 | this.extent.sideEffect { 84 | onUpdated(this.event!!) 85 | } 86 | } 87 | if (this.extent.addedToGraphWhen != null) { 88 | this.extent.graph.addLateBehavior(observer) 89 | } 90 | return observer 91 | } 92 | } -------------------------------------------------------------------------------- /tutorial-3-swing/src/main/java/ListExtent.java: -------------------------------------------------------------------------------- 1 | import behaviorgraph.*; 2 | 3 | import java.util.ArrayList; 4 | 5 | public class ListExtent extends Extent { 6 | TypedMoment save = typedMoment(); 7 | State> allItems = state(new ArrayList<>()); 8 | TypedMoment removeItem = typedMoment(); 9 | TypedMoment selectRequest = typedMoment(); 10 | State selected = state(null); 11 | 12 | public ListExtent(Graph graph, ListUI listUI) { 13 | super(graph); 14 | 15 | listUI.save.addActionListener(actionEvent -> { 16 | save.updateWithAction(listUI.newItemText.getText()); 17 | }); 18 | 19 | behavior() 20 | .supplies(allItems) 21 | .demands(save, removeItem) 22 | .runs(ctx -> { 23 | if (save.justUpdated() && selected.traceValue() == null) { 24 | var item = new ItemExtent(graph, save.value(), this); 25 | addChildLifetime(item); 26 | item.addToGraph(); 27 | allItems.value().add(item); 28 | allItems.updateForce(allItems.value()); 29 | sideEffect(ctx1 -> { 30 | listUI.addItem(item.itemUI); 31 | listUI.newItemText.setText(""); 32 | }); 33 | } else if (removeItem.justUpdated()) { 34 | var item = removeItem.value(); 35 | item.removeFromGraph(); 36 | allItems.value().remove(item); 37 | allItems.updateForce(allItems.value()); 38 | sideEffect(ctx1 -> { 39 | listUI.removeItem(item.itemUI); 40 | }); 41 | } 42 | }); 43 | 44 | behavior() 45 | .demands(allItems, getDidAdd()) 46 | .dynamicDemands(new Linkable[]{allItems}, (ctx, demands) -> { 47 | for (ItemExtent item: allItems.value()) { 48 | demands.add(item.completed); 49 | } 50 | }) 51 | .runs(ctx -> { 52 | sideEffect(ctx1 -> { 53 | long count = allItems.value().stream().filter(itemExtent -> !itemExtent.completed.value()).count(); 54 | listUI.setRemainingCount(count); 55 | }); 56 | }); 57 | 58 | behavior() 59 | .supplies(selected) 60 | .demands(selectRequest, save) 61 | .runs(ctx -> { 62 | if (selectRequest.justUpdated()) { 63 | if (selected.value() == selectRequest.value()) { 64 | selected.update(null); 65 | } else { 66 | selected.update(selectRequest.value()); 67 | } 68 | } else if (save.justUpdated()) { 69 | selected.update(null); 70 | } 71 | 72 | if (selected.justUpdated()) { 73 | sideEffect(ctx1 -> { 74 | listUI.setSelected(selected.value()); 75 | }); 76 | } 77 | }); 78 | 79 | behavior() 80 | .dynamicSupplies(new Linkable[]{allItems}, (ctx, supplies) -> { 81 | for (ItemExtent item: allItems.value()) { 82 | supplies.add(item.itemText); 83 | } 84 | }) 85 | .demands(save) 86 | .runs(ctx -> { 87 | if (save.justUpdated() && selected.traceValue() != null) { 88 | selected.traceValue().itemText.update(save.value()); 89 | } 90 | }); 91 | 92 | 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /example/src/main/kotlin/com/example/myapplication/ui/login/LoginExtent.kt: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright Yahoo 2021 3 | // 4 | package com.example.myapplication.ui.login 5 | 6 | import android.text.TextUtils 7 | import android.util.Patterns 8 | import behaviorgraph.Extent 9 | import behaviorgraph.Graph 10 | 11 | class LoginExtent(var loginActivityBG: LoginActivityBG, graph: Graph) : Extent(graph) { 12 | val email = state("") 13 | val password = state("") 14 | val emailValid = state(false) 15 | val passwordValid = state(false) 16 | val loginEnabled = state(false) 17 | val loggingIn = state(false) 18 | val loginClick = moment() 19 | val loginComplete = typedMoment() 20 | 21 | init { 22 | behavior() 23 | .supplies(emailValid) 24 | .demands(email, didAdd) 25 | .runs { 26 | emailValid.update(validateEmail(email.value)) 27 | sideEffect(null) { 28 | loginActivityBG.emailFeedbackTextView.text = 29 | if (emailValid.value) { 30 | "✅" 31 | } else { 32 | "❌" 33 | } 34 | } 35 | } 36 | 37 | behavior() 38 | .supplies(passwordValid) 39 | .demands(password, didAdd) 40 | .runs { 41 | passwordValid.update(password.value.isNotEmpty()) 42 | sideEffect("passwordFeedback") { 43 | loginActivityBG.passwordFeedbackTextView.text = 44 | if (passwordValid.value) { 45 | "✅" 46 | } else { 47 | "❌" 48 | } 49 | 50 | } 51 | } 52 | 53 | behavior() 54 | .supplies(loginEnabled) 55 | .demands(emailValid, passwordValid, loggingIn, didAdd) 56 | .runs { 57 | val enabled = 58 | emailValid.value && passwordValid.value && !loggingIn.value 59 | loginEnabled.update(enabled) 60 | sideEffect("enable login button") { extent -> 61 | loginActivityBG.loginButton.isEnabled = extent.loginEnabled.value 62 | } 63 | } 64 | 65 | behavior() 66 | .supplies(loggingIn) 67 | .demands(loginClick, loginComplete) 68 | .runs { 69 | if (loginClick.justUpdated && loginEnabled.traceValue) { 70 | loggingIn.update(true) 71 | } else if (loginComplete.justUpdated && loggingIn.value) { 72 | loggingIn.update(false) 73 | } 74 | 75 | if (loggingIn.justUpdatedTo(true)) { 76 | sideEffect("login api call") { extent -> 77 | } 78 | } 79 | } 80 | 81 | behavior() 82 | .demands(loggingIn, loginComplete, didAdd) 83 | .runs { 84 | sideEffect("login status") { 85 | var status = "" 86 | if (loggingIn.value) { 87 | status = "Logging in..."; 88 | } else if (loggingIn.justUpdatedTo(false)) { 89 | if (loginComplete.value == true) { 90 | status = "Login Success"; 91 | } else if (loginComplete.value == false) { 92 | status = "Login Failed"; 93 | } 94 | } 95 | loginActivityBG.loginStatusTextView.text = status 96 | 97 | } 98 | } 99 | } 100 | 101 | private fun validateEmail(target: CharSequence?): Boolean { 102 | return !TextUtils.isEmpty(target) && Patterns.EMAIL_ADDRESS.matcher(target).matches() 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /behavior-graph/src/commonTest/kotlin/behaviorgraph/DependenciesTest.kt: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright Yahoo 2021 3 | // 4 | package behaviorgraph 5 | 6 | import kotlin.test.* 7 | 8 | class DependenciesTest : AbstractBehaviorGraphTest() { 9 | @Test 10 | fun aActivatesB() { 11 | ext.behavior() 12 | .supplies(r_b) 13 | .demands(r_a) 14 | .runs { 15 | r_b.update(2 * r_a.value) 16 | } 17 | ext.addToGraphWithAction() 18 | r_a.updateWithAction(1) 19 | 20 | assertEquals(2L, r_b.value) 21 | assertEquals(r_b.event, r_a.event) 22 | } 23 | 24 | @Test 25 | fun behaviorActivatedAncePerEventLoop() { 26 | var called = 0 27 | ext.behavior() 28 | .supplies(r_c) 29 | .demands(r_a, r_b) 30 | .runs { 31 | called += 1 32 | } 33 | ext.addToGraphWithAction() 34 | 35 | g.action { 36 | r_a.update(1) 37 | r_b.update(2) 38 | } 39 | 40 | assertEquals(1, called) //once for initial add and once for adds 41 | } 42 | 43 | @Test 44 | fun duplicatesAreFilteredOut() { 45 | val b1 = ext.behavior() 46 | .supplies(r_b, r_b) 47 | .demands(r_a, r_a) 48 | .runs {} 49 | ext.addToGraphWithAction() 50 | 51 | assertEquals(1, b1.demands!!.size) 52 | assertEquals(1, b1.supplies!!.size) 53 | assertEquals(1, r_a.subsequents.size) 54 | } 55 | 56 | @Test 57 | fun orderingResourcesArentCalled() { 58 | // |> Given a behavior with an ordering demand 59 | var run = false 60 | ext.behavior().demands(r_a, r_b.order).runs { 61 | run = true 62 | } 63 | ext.addToGraphWithAction() 64 | 65 | // |> When that demand is updated 66 | r_b.updateWithAction(1) 67 | 68 | // |> Then that behavior doesn't run 69 | assertFalse(run) 70 | } 71 | 72 | @Test 73 | fun checkCanUpdateResourceInADifferentExtent() { 74 | val parentExt = TestExtent(g) 75 | val ext2 = TestExtent(g) 76 | parentExt.addChildLifetime(ext2) 77 | 78 | val parent_r: State = parentExt.state(0, "parent_r") 79 | val parent_r2: State = parentExt.state(0, "parent_r2") 80 | val ext2_r1: State = ext2.state(0, "ext2_r1") 81 | 82 | parentExt.behavior() 83 | .supplies(parent_r2) 84 | .demands(parent_r) 85 | .runs { 86 | parent_r2.update(parent_r.value) 87 | } 88 | 89 | ext2.behavior() 90 | .supplies(parent_r) 91 | .demands(ext2_r1) 92 | .runs { 93 | parent_r.update(ext2_r1.value) 94 | } 95 | 96 | parentExt.addToGraphWithAction() 97 | ext2.addToGraphWithAction() 98 | 99 | g.action("update ext2_r1") { 100 | ext2_r1.update(33) 101 | } 102 | 103 | assertEquals(33L, parent_r2.value) 104 | } 105 | 106 | @Test 107 | fun toStringWorksInBehaviorsThatDontDemandMentionedResources() { 108 | // We want to be able to debug things 109 | // So toString methods shouldn't trigger any demand verification checks 110 | 111 | val s1 = ext.state(0) 112 | val m1 = ext.moment() 113 | val tm1 = ext.typedMoment() 114 | val m2 = ext.moment() 115 | 116 | ext.behavior() 117 | .demands(s1) 118 | .supplies(m2) 119 | .performs { 120 | m2.update() 121 | } 122 | 123 | var s1_string = "" 124 | var m1_string = "" 125 | var tm1_string = "" 126 | ext.behavior() 127 | .demands(m2) 128 | .performs { 129 | s1_string = s1.toString() 130 | m1_string = m1.toString() 131 | tm1_string = tm1.toString() 132 | } 133 | 134 | ext.addToGraphWithAction() 135 | ext.action { 136 | s1.update(1) 137 | m1.update() 138 | tm1.update(1) 139 | } 140 | 141 | assertTrue(s1_string.isNotEmpty()) 142 | assertTrue(m1_string.isNotEmpty()) 143 | assertTrue(tm1_string.isNotEmpty()) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /behavior-graph/src/commonMain/kotlin/behaviorgraph/TypedMoment.kt: -------------------------------------------------------------------------------- 1 | package behaviorgraph 2 | 3 | import kotlin.jvm.JvmOverloads 4 | import kotlin.jvm.JvmName 5 | 6 | /** 7 | * A TypedMoment is a [Moment] resource that has associated data. 8 | * It is a type of Resource for tracking information that exists at a 9 | * single moment in time. A network call returning is an example of a TypedMoment. It happens, 10 | * when it happens it contains the results of the network call. At then end of the current event 11 | * it is no longer relevant. 12 | * Use [Moment] if you have no additional information. 13 | */ 14 | class TypedMoment @JvmOverloads constructor(extent: Extent<*>, debugName: String? = null): Resource(extent, debugName), 15 | Transient { 16 | data class Happened(val value: T, val event: Event) 17 | private var _happened: Happened? = null 18 | /** 19 | * Is there a current event and if the moment updated then what is the associated data. 20 | * Will return null if the moment did not update this event. 21 | * Be careful: It is possible that T is also an optional type itself 22 | * So this typedMoment could be called with .update(null). 23 | * In that case justUpdated will be true and value will also be null. 24 | * A behavior must demand this resource to access its value. 25 | */ 26 | @get:JvmName("value") 27 | val value: T? 28 | get() { 29 | assertValidAccessor() 30 | return this._happened?.value 31 | } 32 | 33 | /** 34 | * If this Moment has ever been update what was the last Event it was updated. 35 | * A behavior must demand this resource to access this property. 36 | */ 37 | @get:JvmName("event") 38 | val event: Event? 39 | get() { 40 | assertValidAccessor() 41 | return this._happened?.event 42 | } 43 | 44 | 45 | /** 46 | * Create a new action and call [update]. 47 | */ 48 | @JvmOverloads 49 | fun updateWithAction(value: T, debugName: String? = null) { 50 | graph.action(debugName) { update(value) } 51 | } 52 | 53 | /** 54 | * Mark this TypedMoment resource as updated, associate a value with that update and activate any dependent behaviors. 55 | * A behavior must supply this resource in order to update it. 56 | */ 57 | fun update(value: T) { 58 | assertValidUpdater() 59 | graph.currentEvent?.let { 60 | _happened = Happened(value, it) 61 | graph.resourceTouched(this) 62 | graph.trackTransient(this) 63 | } 64 | } 65 | 66 | override fun clear() { 67 | _happened = null 68 | } 69 | 70 | override fun toString(): String { 71 | val localDebugName = debugName ?: "" 72 | val localType = super.toString() 73 | val localUpdated = _happened?.value ?: "NA" 74 | val localSequence = _happened?.event?.sequence ?: "NA" 75 | return "$localDebugName $localType == $localUpdated ($localSequence)" 76 | } 77 | 78 | /** 79 | * Is there a current event and was this Moment resource updated during this event. 80 | * A behavior must demand this resource to access this property. 81 | */ 82 | @get:JvmName("justUpdated") 83 | val justUpdated: Boolean get() { 84 | assertValidAccessor() 85 | return _happened != null 86 | } 87 | 88 | override val internalJustUpdated: Boolean get() = justUpdated 89 | 90 | /** 91 | * Checks if [justUpdated] and if the associated value is `==` to the passed in value. 92 | */ 93 | fun justUpdatedTo(value: T): Boolean { 94 | return this.justUpdated && this._happened?.value == value 95 | } 96 | 97 | fun observeUpdates(onUpdated: (Pair) -> Unit): Behavior<*> { 98 | val extent = this.extent as Extent 99 | val observer = extent.behavior() 100 | .demands(this) 101 | .runs { _ -> 102 | this._happened?.let { happened -> 103 | this.extent.sideEffect { 104 | onUpdated(Pair(happened.value, happened.event)) 105 | } 106 | } 107 | } 108 | if (this.extent.addedToGraphWhen != null) { 109 | this.extent.graph.addLateBehavior(observer) 110 | } 111 | return observer 112 | } 113 | } -------------------------------------------------------------------------------- /behavior-graph/src/commonMain/kotlin/behaviorgraph/Behavior.kt: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright Yahoo 2021 3 | // 4 | package behaviorgraph 5 | 6 | /** 7 | * A behavior is a block of code together with its dependency relationships (links). They are one of the two node types in a behavior graph. You define behaviors using the behavior() factory method of an Extent. 8 | * 9 | * Behaviors have both static and dynamic links. You provide static links when you create the behavior. Behavior Graph will update dynamic links per special methods on BehaviorBuilder or you can update them directly on a behavior. 10 | * @property extent A behavior always has an [Extent] with which it is created. 11 | */ 12 | class Behavior( 13 | val extent: Extent, demands: List?, supplies: List?, 14 | internal var thunk: ExtentThunk 15 | ) : Comparable> { 16 | /** 17 | * The current set of all Resources which the behavior demands. 18 | */ 19 | var demands: MutableSet? = null 20 | internal set 21 | internal var orderingDemands: MutableSet? = null 22 | 23 | /** 24 | * The current set of all Resources which the behavior supplies. 25 | */ 26 | var supplies: Set? = null 27 | internal set 28 | internal var enqueuedWhen: Long? = null 29 | internal var removedWhen: Long? = null 30 | internal var orderingState = OrderingState.Untracked 31 | var order: Long = 0 32 | internal set 33 | 34 | internal var untrackedDemands: List? 35 | internal var untrackedDynamicDemands: List? = null 36 | internal var untrackedSupplies: List? 37 | internal var untrackedDynamicSupplies: List? = null 38 | 39 | init { 40 | this.untrackedDemands = demands 41 | this.untrackedSupplies = supplies 42 | } 43 | 44 | override fun compareTo(other: Behavior<*>): Int { 45 | return order.compareTo(other.order) 46 | } 47 | 48 | override fun toString(): String { 49 | val rows = mutableListOf("Behavior") 50 | supplies?.forEachIndexed { index, resource -> 51 | if (index == 0) { 52 | rows.add(" Supplies:") 53 | } 54 | rows.add(" " + resource.toString()) 55 | } 56 | demands?.forEachIndexed { index, resource -> 57 | if (index == 0) { 58 | rows.add(" Demands:") 59 | } 60 | rows.add(" " + resource.toString()) 61 | } 62 | return rows.joinToString("\n") 63 | } 64 | 65 | /** 66 | * Provide an array of Demandables. undefined is also an element type to make for easier use of optional chaining. Providing null is equivalent to saying there are no dynamic demands. 67 | */ 68 | fun setDynamicDemands(vararg newDemands: Linkable) { 69 | setDynamicDemands(newDemands.asList()) 70 | } 71 | 72 | /** 73 | * Provide an array of Demandables. undefined is also an element type to make for easier use of optional chaining. Providing null is equivalent to saying there are no dynamic demands. 74 | */ 75 | fun setDynamicDemands(newDemands: List?) { 76 | this.extent.graph.updateDemands(this, newDemands?.filterNotNull()) 77 | } 78 | 79 | /** 80 | * Provide an array of Resources to supply. undefined is also an element type to make for easier use of optional chaining. Providing null is equivalent to saying there are no dynamic supplies. 81 | */ 82 | fun setDynamicSupplies(vararg newSupplies: Linkable) { 83 | setDynamicSupplies(newSupplies.asList()) 84 | } 85 | 86 | /** 87 | * Provide an array of Resources to supply. undefined is also an element type to make for easier use of optional chaining. Providing null is equivalent to saying there are no dynamic supplies. 88 | */ 89 | fun setDynamicSupplies(newSupplies: List?) { 90 | this.extent.graph.updateSupplies(this, newSupplies?.filterNotNull()) 91 | } 92 | 93 | /** 94 | * Remove the behavior from the graph independent of extent lifetime (supports observer patterns) 95 | */ 96 | fun removeEarly() { 97 | this.extent.graph.markBehaviorForRemoval(this) 98 | } 99 | 100 | /** 101 | * Add behavior to the graph independent of extent lifetime (supports observer patterns) 102 | */ 103 | fun addLate() { 104 | this.extent.graph.addLateBehavior(this) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /behavior-graph/src/commonMain/kotlin/behaviorgraph/ExtentLifetime.kt: -------------------------------------------------------------------------------- 1 | package behaviorgraph 2 | 3 | internal class ExtentLifetime( 4 | extent: Extent<*> 5 | ){ 6 | var addedToGraphWhen: Long? = null 7 | val extents: MutableSet> = mutableSetOf() 8 | var children: MutableSet? = null 9 | var parent: ExtentLifetime? = null 10 | 11 | init { 12 | extents.add(extent) 13 | if (extent.addedToGraphWhen != null) { 14 | addedToGraphWhen = extent.addedToGraphWhen 15 | } 16 | } 17 | 18 | fun unify(extent: Extent<*>) { 19 | if (extent.addedToGraphWhen != null) { 20 | extent.graph.bgassert(false) { 21 | "Same lifetime relationship must be established before adding any extent to graph. \nExtent=$extent" 22 | } 23 | // disabled asserts just allows this 24 | } 25 | if (extent.lifetime != null) { 26 | // merge existing lifetimes and children into one lifetime heirarchy 27 | // move children first 28 | extent.lifetime?.children?.forEach {lifetime -> 29 | addChildLifetime(lifetime) 30 | } 31 | // then make any extents in other lifetime part of this one 32 | extent.lifetime?.extents?.forEach { it -> 33 | it.lifetime = this 34 | extents.add(it) 35 | } 36 | } else { 37 | extent.lifetime = this 38 | extents.add(extent) 39 | } 40 | } 41 | 42 | fun addChild(extent: Extent<*>) { 43 | if (extent.lifetime == null) { 44 | extent.lifetime = ExtentLifetime(extent) 45 | } 46 | extent.lifetime?.let { 47 | addChildLifetime(it) 48 | } 49 | } 50 | 51 | fun addChildLifetime(lifetime: ExtentLifetime) { 52 | var myLifetime: ExtentLifetime? = this 53 | while (myLifetime != null) { 54 | // check up the chain of parents to prevent circular lifetime 55 | if (myLifetime == lifetime) { 56 | val extent = myLifetime.extents.first() 57 | if (extent != null) { 58 | extent.graph.bgassert(false) { 59 | "Extent lifetime cannot be a child of itself." 60 | } 61 | } 62 | return 63 | } 64 | myLifetime = myLifetime.parent 65 | } 66 | lifetime.parent = this 67 | if (children == null) { 68 | children = mutableSetOf() 69 | } 70 | children?.add(lifetime) 71 | } 72 | 73 | fun hasCompatibleLifetime(lifetime: ExtentLifetime?): Boolean { 74 | if (this == lifetime) { 75 | // unified 76 | return true 77 | } else if (lifetime != null) { 78 | // parents 79 | val thisParent = parent 80 | if (thisParent != null) { 81 | // parent is a weak reference so we get it here 82 | val refParent = thisParent 83 | if (refParent != null) { 84 | return refParent.hasCompatibleLifetime(lifetime) 85 | } 86 | } 87 | } 88 | return false 89 | } 90 | 91 | fun getAllContainedExtents(): List> { 92 | val resultExtents = mutableListOf>() 93 | resultExtents.addAll(extents) 94 | children?.forEach { childLifetime -> resultExtents.addAll(childLifetime.getAllContainedExtents()) } 95 | return resultExtents 96 | } 97 | 98 | fun getAllContainingExtents(): List> { 99 | val resultExtents = mutableListOf>() 100 | resultExtents.addAll(extents) 101 | parent?.let { 102 | resultExtents.addAll(it.getAllContainingExtents()) 103 | } 104 | return resultExtents 105 | } 106 | 107 | fun clearExtentRelationship(removedExtent: Extent<*>) { 108 | // Removed extents no longer participate in their lifetime 109 | // Unwind those to prevent memory leaks 110 | 111 | // removed extent is no longer part of it's lifetime 112 | extents.remove(removedExtent) 113 | // and empty lifetimes are no longer part of a parent child relationship 114 | if (extents.isEmpty()) { 115 | parent?.children?.remove(this) 116 | parent = null 117 | } 118 | removedExtent.lifetime = null 119 | } 120 | } -------------------------------------------------------------------------------- /behavior-graph/build.gradle: -------------------------------------------------------------------------------- 1 | // https://docs.gradle.org/current/samples/sample_building_kotlin_libraries.html 2 | // https://github.com/Kotlin/dokka/blob/master/examples/gradle/dokka-library-publishing-example/build.gradle.kts 3 | // https://docs.gradle.org/current/userguide/signing_plugin.html 4 | // https://www.gnupg.org/gph/en/manual.html 5 | 6 | plugins { 7 | //id "org.jetbrains.kotlin.jvm" 8 | // id "java-library" 9 | // id "com.android.library" 10 | id "org.jetbrains.dokka" 11 | id "com.vanniktech.maven.publish" version "0.31.0" 12 | id "org.jetbrains.kotlin.multiplatform" 13 | } 14 | 15 | java { 16 | //withSourcesJar() 17 | sourceCompatibility = JavaVersion.VERSION_11 18 | targetCompatibility = JavaVersion.VERSION_11 19 | } 20 | 21 | kotlin { 22 | jvm() { 23 | compilations.all { 24 | kotlinOptions { 25 | jvmTarget = "11" 26 | } 27 | } 28 | } 29 | // js() { 30 | // browser() 31 | // nodejs() 32 | // } 33 | // iosArm64() 34 | // androidTarget() 35 | 36 | sourceSets { 37 | commonMain { 38 | dependencies { 39 | api "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 40 | api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" 41 | } 42 | } 43 | 44 | jvmMain { 45 | dependencies { 46 | api "org.jetbrains.kotlin:kotlin-stdlib-jdk8" 47 | api "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:$coroutines_version" 48 | } 49 | } 50 | 51 | // jsMain { 52 | // dependencies { 53 | // api "org.jetbrains.kotlin:kotlin-stdlib-js" 54 | // api "org.jetbrains.kotlinx:kotlinx-coroutines-core-js:$coroutines_version" 55 | // } 56 | // } 57 | 58 | commonTest { 59 | dependencies { 60 | implementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" 61 | implementation "org.jetbrains.kotlin:kotlin-test-common:$kotlin_version" 62 | implementation "org.jetbrains.kotlin:kotlin-test-annotations-common:$kotlin_version" 63 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version" 64 | } 65 | } 66 | 67 | jvmTest { 68 | dependencies { 69 | implementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" 70 | implementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version" 71 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version" 72 | } 73 | } 74 | 75 | // jsTest { 76 | // dependencies { 77 | // implementation "org.jetbrains.kotlin:kotlin-test-js:$kotlin_version" 78 | // implementation "org.jetbrains.kotlinx:kotlinx-coroutines-test-js:$coroutines_version" 79 | // } 80 | // } 81 | } 82 | } 83 | 84 | 85 | //tasks.named('jar') { 86 | // manifest { 87 | // attributes('Automatic-Module-Name': 'behaviorgraph') 88 | // } 89 | //} 90 | 91 | //def dokkaJavadocJar = tasks.register("dokkaJavadocJar", org.gradle.jvm.tasks.Jar) { 92 | // it.dependsOn(dokkaJavadoc) 93 | // it.from(dokkaJavadoc.outputDirectory) 94 | // it.archiveClassifier = "javadoc" 95 | // 96 | //} 97 | 98 | import com.vanniktech.maven.publish.KotlinMultiplatform 99 | import com.vanniktech.maven.publish.JavadocJar 100 | import com.vanniktech.maven.publish.SonatypeHost 101 | 102 | mavenPublishing { 103 | configure(new KotlinMultiplatform(new JavadocJar.Dokka("dokkaHtml"), true)) 104 | publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL) 105 | coordinates("com.yahoo.behaviorgraph", "bgjvm", "$bg_version") 106 | pom { 107 | name = 'Behavior Graph' 108 | description = 'Behavior Graph lets you build your programs out of small, easily understood pieces in a way that lets the computer do more of the work for you.' 109 | url = 'https://github.com/yahoo/bgkotlin' 110 | licenses { 111 | license { 112 | name = 'The Apache License, Version 2.0' 113 | url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' 114 | } 115 | } 116 | developers { 117 | developer { 118 | id = 'slevin' 119 | name = 'Sean Levin' 120 | email = 'slevin@yahooinc.com' 121 | } 122 | } 123 | scm { 124 | connection = 'scm:git:git://github.com/yahoo/bgkotlin.git' 125 | developerConnection = 'scm:git:ssh://github.com:yahoo/bgkotlin.git' 126 | url = 'https://github.com/yahoo/bgkotlin' 127 | } 128 | } 129 | 130 | signAllPublications() 131 | } -------------------------------------------------------------------------------- /tutorial-2-swing/src/main/java/TutorialUI.java: -------------------------------------------------------------------------------- 1 | import javax.swing.*; 2 | import java.awt.*; 3 | import java.awt.event.ActionListener; 4 | 5 | public class TutorialUI { 6 | JButton upButton; 7 | JButton downButton; 8 | JLabel heatStatus; 9 | JLabel currentTemp; 10 | JLabel desiredTemp; 11 | Timer heatTimer; 12 | 13 | public TutorialUI() { 14 | upButton = new JButton("Up"); 15 | downButton = new JButton("Down"); 16 | heatStatus = new JLabel(); 17 | currentTemp = new JLabel(); 18 | desiredTemp = new JLabel(); 19 | } 20 | 21 | public void createAndShowGUI() { 22 | //Create and set up the window. 23 | JFrame frame = new JFrame("Behavior Graph Tutorial 2"); 24 | frame.setResizable(false); 25 | frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 26 | 27 | GridBagConstraints c; 28 | GridBagLayout layout; 29 | 30 | Container mainPane = frame.getContentPane(); 31 | layout = new GridBagLayout(); 32 | mainPane.setLayout(layout); 33 | JPanel innerPane = new JPanel(); 34 | c = new GridBagConstraints(); 35 | c.insets = new Insets(8,8,8,8); 36 | c.fill = GridBagConstraints.BOTH; 37 | c.gridwidth = 1; 38 | c.gridheight = 1; 39 | innerPane.setSize(300,400); 40 | mainPane.add(innerPane, c); 41 | 42 | layout = new GridBagLayout(); 43 | innerPane.setLayout(layout); 44 | layout.rowHeights = new int[]{30,30,30,30}; 45 | c = new GridBagConstraints(); 46 | JLabel label = new JLabel("Tempwell"); 47 | label.setFont(new Font("Helvetica", Font.BOLD, 30)); 48 | c.fill = GridBagConstraints.BOTH; 49 | c.gridwidth = 3; 50 | c.gridheight = 2; 51 | c.gridx = 0; 52 | c.gridy = 0; 53 | innerPane.add(label, c); 54 | 55 | c = new GridBagConstraints(); 56 | JPanel display = new JPanel(); 57 | display.setBackground(Color.LIGHT_GRAY); 58 | c.insets = new Insets(4,4,4,4); 59 | c.fill = GridBagConstraints.BOTH; 60 | c.gridwidth = 2; 61 | c.gridheight = 2; 62 | c.gridx = 0; 63 | c.gridy = 2; 64 | innerPane.add(display, c); 65 | 66 | c = new GridBagConstraints(); 67 | c.anchor = GridBagConstraints.WEST; 68 | c.gridwidth = 1; 69 | c.gridheight = 1; 70 | c.gridx = 2; 71 | c.gridy = 2; 72 | innerPane.add(upButton, c); 73 | 74 | c = new GridBagConstraints(); 75 | c.anchor = GridBagConstraints.WEST; 76 | c.gridwidth = 1; 77 | c.gridheight = 1; 78 | c.gridx = 2; 79 | c.gridy = 3; 80 | innerPane.add(downButton, c); 81 | 82 | 83 | layout = new GridBagLayout(); 84 | layout.columnWidths = new int[]{100, 100}; 85 | layout.rowHeights = new int[]{25,25,25}; 86 | display.setLayout(layout); 87 | c = new GridBagConstraints(); 88 | c.insets = new Insets(2,4,2,4); 89 | heatStatus.setFont(new Font("Helvetica", Font.BOLD, 16)); 90 | c.anchor = GridBagConstraints.WEST; 91 | c.gridwidth = 1; 92 | c.gridheight = 1; 93 | c.gridx = 0; 94 | c.gridy = 0; 95 | display.add(heatStatus, c); 96 | 97 | c = new GridBagConstraints(); 98 | JLabel currentLabel = new JLabel("Current"); 99 | currentLabel.setFont(new Font("Helvetica", Font.PLAIN, 14)); 100 | c.gridwidth = 1; 101 | c.gridheight = 1; 102 | c.gridx = 0; 103 | c.gridy = 1; 104 | display.add(currentLabel, c); 105 | 106 | c = new GridBagConstraints(); 107 | JLabel desiredLabel = new JLabel("Desired"); 108 | currentLabel.setFont(new Font("Helvetica", Font.PLAIN, 14)); 109 | c.gridwidth = 1; 110 | c.gridheight = 1; 111 | c.gridx = 1; 112 | c.gridy = 1; 113 | display.add(desiredLabel, c); 114 | 115 | c = new GridBagConstraints(); 116 | currentTemp.setFont(new Font("Helvetica", Font.BOLD, 20)); 117 | c.gridwidth = 1; 118 | c.gridheight = 1; 119 | c.gridx = 0; 120 | c.gridy = 2; 121 | display.add(currentTemp, c); 122 | 123 | c = new GridBagConstraints(); 124 | desiredTemp.setFont(new Font("Helvetica", Font.BOLD, 20)); 125 | c.gridwidth = 1; 126 | c.gridheight = 1; 127 | c.gridx = 1; 128 | c.gridy = 2; 129 | display.add(desiredTemp, c); 130 | 131 | //Display the window. 132 | frame.pack(); 133 | frame.setVisible(true); 134 | } 135 | 136 | public void turnOnHeat(ActionListener listener) { 137 | heatTimer = new Timer(1500, listener); 138 | heatTimer.setRepeats(true); 139 | heatTimer.start(); 140 | } 141 | 142 | public void turnOffHeat() { 143 | if (heatTimer != null) { 144 | heatTimer.stop(); 145 | heatTimer = null; 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /behavior-graph/src/commonTest/kotlin/behaviorgraph/ExtentTest.kt: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright Yahoo 2021 3 | // 4 | package behaviorgraph 5 | 6 | import kotlin.test.* 7 | 8 | class ExtentTest : AbstractBehaviorGraphTest() { 9 | class TestExtentLocal(g: Graph) : TestExtent(g) { 10 | val r1 = this.state(0) 11 | var r2 = this.state(0, "custom_r2") 12 | val b1 = this.behavior().demands(r1).supplies(r2).runs { 13 | r2.update(r1.value * 2) 14 | } 15 | 16 | fun injectNumber(num: Int) { 17 | r1.updateWithAction(num) 18 | } 19 | } 20 | 21 | @Test 22 | fun getsClassName() { 23 | val e = TestExtentLocal(g) 24 | assertEquals("TestExtentLocal", e.debugName) 25 | } 26 | 27 | @Test 28 | fun containedComponentsPickedUp() { 29 | val e = TestExtentLocal(g) 30 | e.addToGraphWithAction() 31 | 32 | assertSame(g, e.r1.graph) 33 | assertSame(e, e.b1.extent) 34 | assertEquals(0, e.r2.value) 35 | 36 | e.injectNumber(2) 37 | 38 | assertEquals(2, e.r1.value) 39 | assertEquals(4, e.r2.value) 40 | } 41 | 42 | @Test 43 | fun containedComponentsNamedIfNeeded() { 44 | val e = TestExtentLocal(g) 45 | e.addToGraphWithAction() 46 | 47 | // we use regex because kotlin name mangling modifies the field name 48 | // when converting to js 49 | 50 | // property names 51 | assertTrue(e.r1.debugName?.contains(Regex("r1")) == true) 52 | // custom name not overridden 53 | assertTrue(e.r2.debugName?.contains(Regex("custom_r2")) == true) 54 | } 55 | 56 | @Test 57 | fun automaticNamingCanBeDisabled() { 58 | g.automaticResourceNaming = false 59 | val e = TestExtentLocal(g) 60 | e.addToGraphWithAction() 61 | 62 | // won't get automatic name 63 | assertEquals(null, e.r1.debugName) 64 | // custom name still works 65 | assertEquals("custom_r2", e.r2.debugName) 66 | } 67 | 68 | @Test 69 | fun addedResourceIsUpdatedOnAdding() { 70 | val e = TestExtent(g) 71 | var runOnAdd = false 72 | e.behavior().demands(e.didAdd).runs { 73 | runOnAdd = true 74 | } 75 | e.addToGraphWithAction() 76 | 77 | assertTrue(runOnAdd) 78 | } 79 | 80 | //checks below 81 | @Test 82 | fun checkCannotAddExtentToGraphMultipleTimes() { 83 | assertFails { setupExt.addToGraphWithAction() } 84 | } 85 | 86 | @Test 87 | fun checkExtentCannotBeAddedToGraphOutsideEvent() { 88 | val e = TestExtentLocal(g) 89 | assertFails { 90 | e.addToGraph() 91 | } 92 | } 93 | 94 | @Test 95 | fun checkExtentCannotBeRemovedFromGraphOutsideEvent() { 96 | val e = TestExtentLocal(g) 97 | e.addToGraphWithAction() 98 | assertFails { 99 | e.removeFromGraph() 100 | } 101 | } 102 | 103 | class NonSubclass(g: Graph) { 104 | val extent = Extent(g, this) 105 | val r1 = extent.state(0) 106 | var r2 = extent.state(0) 107 | 108 | init { 109 | extent.behavior().demands(r1).supplies(r2).runs { 110 | r2.update(r1.value * 2) 111 | } 112 | } 113 | 114 | fun injectNumber(num: Int) { 115 | r1.updateWithAction(num) 116 | } 117 | } 118 | 119 | @Test 120 | fun nonExtentSubclassAlsoWorks() { 121 | val nonSubclass = NonSubclass(g) 122 | nonSubclass.extent.addToGraphWithAction() 123 | nonSubclass.injectNumber(2) 124 | assertEquals(nonSubclass.r2.value, 4) 125 | } 126 | 127 | @Test 128 | fun extentMethodsReturnContextObjects() { 129 | val nonSubclass = NonSubclass(g) 130 | // add a behavior to test that nonSubclass is the one that's run 131 | nonSubclass.extent.behavior() 132 | .demands(nonSubclass.extent.didAdd) 133 | .runs { 134 | assertEquals(it, nonSubclass) 135 | it.extent.sideEffect { 136 | assertEquals(it, nonSubclass) 137 | it.extent.action { 138 | assertEquals(it, nonSubclass) 139 | } 140 | it.extent.action { 141 | assertEquals(it, nonSubclass) 142 | } 143 | } 144 | } 145 | nonSubclass.extent.addToGraphWithAction() 146 | } 147 | 148 | @Test 149 | fun contextObjectNamesItsResources() { 150 | val nonSubclass = NonSubclass(g) 151 | nonSubclass.extent.addToGraphWithAction() 152 | 153 | // we use regex because kotlin name mangling modifies the field name 154 | // when converting to js 155 | assertEquals(nonSubclass.r1.debugName?.contains(Regex("r1")), true) 156 | assertEquals(nonSubclass.r2.debugName?.contains(Regex("r2")), true) 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /example/src/main/res/layout/activity_login.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 27 | 28 | 37 | 38 | 47 | 48 | 64 | 65 |