Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@

package it.unibo.alchemist.model

import it.unibo.alchemist.model.observation.Disposable
import it.unibo.alchemist.model.observation.Observable
import it.unibo.alchemist.model.observation.lifecycle.LifecycleOwner
import java.io.Serializable
import org.danilopianini.util.ListSet

Expand All @@ -18,7 +20,9 @@ import org.danilopianini.util.ListSet
*/
sealed interface Actionable<T> :
Comparable<Actionable<T>>,
Serializable {
Serializable,
Disposable,
LifecycleOwner {
/**
* @return true if the reaction can be executed (namely, all the conditions
* are satisfied).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
package it.unibo.alchemist.model

import arrow.core.Option
import it.unibo.alchemist.model.observation.Disposable
import it.unibo.alchemist.model.observation.Observable
import it.unibo.alchemist.model.observation.ObservableMap
import it.unibo.alchemist.model.observation.lifecycle.LifecycleOwner
import java.io.Serializable
import kotlin.reflect.KClass
import kotlin.reflect.full.isSubclassOf
Expand All @@ -25,7 +27,9 @@ import kotlin.reflect.jvm.jvmErasure
interface Node<T> :
Serializable,
Iterable<Reaction<T>>,
Comparable<Node<T>> {
Comparable<Node<T>>,
Disposable,
LifecycleOwner {
/**
* Adds a reaction to this node.
* The reaction is added only in the node,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright (C) 2010-2026, Danilo Pianini and contributors
* listed, for each module, in the respective subproject's build.gradle.kts file.
*
* This file is part of Alchemist, and is distributed under the terms of the
* GNU General Public License, with a linking exception,
* as described in the file LICENSE in the Alchemist distribution's top directory.
*/

package it.unibo.alchemist.model.observation.lifecycle

/**
* Manages the state and listeners.
*/
interface Lifecycle {

/**
* Returns the current state of the Lifecycle.
*/
val currentState: LifecycleState

/**
* Adds a LifecycleObserver that will be notified when the LifecycleOwner changes state.
*/
fun addObserver(observer: (LifecycleState) -> Unit)

/**
* Removes the given observer from the observers list.
*/
fun removeObserver(observer: (LifecycleState) -> Unit)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright (C) 2010-2026, Danilo Pianini and contributors
* listed, for each module, in the respective subproject's build.gradle.kts file.
*
* This file is part of Alchemist, and is distributed under the terms of the
* GNU General Public License, with a linking exception,
* as described in the file LICENSE in the Alchemist distribution's top directory.
*/

package it.unibo.alchemist.model.observation.lifecycle

import it.unibo.alchemist.model.observation.Disposable
import it.unibo.alchemist.model.observation.Observable
import it.unibo.alchemist.model.observation.lifecycle.LifecycleState.DESTROYED
import it.unibo.alchemist.model.observation.lifecycle.LifecycleState.STARTED

/**
* An object which has a [Lifecycle].
*/
interface LifecycleOwner {

/**
* The lifecycle of the provider.
*/
val lifecycle: Lifecycle
}

/**
* Observes this [it.unibo.alchemist.model.observation.Observable] within the context of a [LifecycleOwner]. This
* method is a memory-safe alternative to [it.unibo.alchemist.model.observation.Observable.onChange] in scenarios
* where it is crucial to avoid that the owner subscription leaks. When the
* registrant's state reaches [LifecycleState.DESTROYED], the subscription
* is automatically removed and cleared up. Moreover, the [callback] is only
* invoked if the lifecycle is in an active state ([STARTED]). Finally,
* when the lifecycle moves from an inactive state back to active, the
* [callback] is triggered immediately with the [it.unibo.alchemist.model.observation.Observable.current] value.
*
* @param lifecycleOwner The object controlling the lifecycle of this subscription.
* @param callback The action to perform when the observable emits a value.
* @return a [it.unibo.alchemist.model.observation.Disposable]
* to manually dispose the subscription outside owner's lifecycle.
*/
fun <T> Observable<T>.bindTo(lifecycleOwner: LifecycleOwner, callback: (T) -> Unit): Disposable? =
lifecycleOwner.takeIf { it.lifecycle.currentState != DESTROYED }?.let {
val dataListener: (T) -> Unit = { data ->
// avoid zombie callbacks
if (lifecycleOwner.lifecycle.currentState.isAtLeast(STARTED)) {
callback(data)
}
}

var lifecycleObserver: ((LifecycleState) -> Unit)? = null
lifecycleObserver = { state ->
when (state) {
DESTROYED -> {
this.stopWatching(lifecycleOwner)
lifecycleObserver?.let { lifecycleOwner.lifecycle.removeObserver(it) }
}
STARTED -> callback(this.current)
else -> { /* No action needed for other states */ }
}
}

lifecycleOwner.lifecycle.addObserver(lifecycleObserver)

// set `invokeOnRegistration = false` because we handle the initial data emission manually below
// to strictly respect the `isAtLeast(STARTED)` check.
this.onChange(lifecycleOwner, invokeOnRegistration = false, callback = dataListener)

if (lifecycleOwner.lifecycle.currentState.isAtLeast(STARTED)) {
callback(this.current)
}

return Disposable {
this@bindTo.stopWatching(dataListener)
lifecycleOwner.lifecycle.removeObserver(lifecycleObserver)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright (C) 2010-2026, Danilo Pianini and contributors
* listed, for each module, in the respective subproject's build.gradle.kts file.
*
* This file is part of Alchemist, and is distributed under the terms of the
* GNU General Public License, with a linking exception,
* as described in the file LICENSE in the Alchemist distribution's top directory.
*/

package it.unibo.alchemist.model.observation.lifecycle

/**
* A concrete implementation of [Lifecycle] that handles state transitions and observer notification.
*/
class LifecycleRegistry : Lifecycle {

private val observers = mutableListOf<(LifecycleState) -> Unit>()

override var currentState: LifecycleState = LifecycleState.INITIALIZED
private set

/**
* Transitions the lifecycle to a new [state] and notifies all observers.
*/
fun markState(state: LifecycleState) {
currentState = state
observers.toList().forEach { it(state) }
}

override fun addObserver(observer: (LifecycleState) -> Unit) {
observers.add(observer)
}

override fun removeObserver(observer: (LifecycleState) -> Unit) {
observers.remove(observer)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright (C) 2010-2026, Danilo Pianini and contributors
* listed, for each module, in the respective subproject's build.gradle.kts file.
*
* This file is part of Alchemist, and is distributed under the terms of the
* GNU General Public License, with a linking exception,
* as described in the file LICENSE in the Alchemist distribution's top directory.
*/

package it.unibo.alchemist.model.observation.lifecycle

/**
* Represents the lifecycle state of a component.
*/
enum class LifecycleState {

/**
* Destroyed state for a LifecycleOwner. After this state is reached, this Lifecycle will not emit any more events.
*/
DESTROYED,

/**
* Initialized state for a LifecycleOwner.
*/
INITIALIZED,

/**
* Started state for a LifecycleOwner.
*/
STARTED,

;

/**
* Checks if the current state is at least the given [state].
*/
fun isAtLeast(state: LifecycleState): Boolean = this >= state
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* Copyright (C) 2010-2026, Danilo Pianini and contributors
* listed, for each module, in the respective subproject's build.gradle.kts file.
*
* This file is part of Alchemist, and is distributed under the terms of the
* GNU General Public License, with a linking exception,
* as described in the file LICENSE in the Alchemist distribution's top directory.
*/

package it.unibo.alchemist.model.observation

import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.collections.shouldContain
import io.kotest.matchers.collections.shouldNotContain
import io.kotest.matchers.shouldBe
import it.unibo.alchemist.model.observation.MutableObservable.Companion.observe
import it.unibo.alchemist.model.observation.lifecycle.Lifecycle
import it.unibo.alchemist.model.observation.lifecycle.LifecycleOwner
import it.unibo.alchemist.model.observation.lifecycle.LifecycleRegistry
import it.unibo.alchemist.model.observation.lifecycle.LifecycleState.DESTROYED
import it.unibo.alchemist.model.observation.lifecycle.LifecycleState.STARTED
import it.unibo.alchemist.model.observation.lifecycle.bindTo

class LifecycleTest : FunSpec({

class TestLifecycleOwner : LifecycleOwner {
private val registry = LifecycleRegistry()
override val lifecycle: Lifecycle get() = registry

fun start() = registry.markState(STARTED)
fun destroy() = registry.markState(DESTROYED)
}

context("Lifecycle binding tests") {

test("bindTo should not invoke callback if Lifecycle is INITIALIZED") {
val owner = TestLifecycleOwner()
val observable = observe(10)
var lastValue = -1

observable.bindTo(owner) { lastValue = it }

lastValue shouldBe -1
observable.current = 20
lastValue shouldBe -1
}

test("bindTo should invoke callback immediately if Lifecycle is STARTED") {
val owner = TestLifecycleOwner()
owner.start()
val observable = observe(10)
var lastValue = -1

observable.bindTo(owner) { lastValue = it }

lastValue shouldBe 10
}

test("bindTo should start receiving updates when Lifecycle moves to STARTED") {
val owner = TestLifecycleOwner()
val observable = observe(10)
var lastValue = -1

observable.bindTo(owner) { lastValue = it }

observable.current = 20
lastValue shouldBe -1

owner.start()
lastValue shouldBe 20

observable.current = 30
lastValue shouldBe 30
}

test("bindTo should automatically unsubscribe when Lifecycle is DESTROYED") {
val owner = TestLifecycleOwner()
owner.start()
val observable = observe(10)
var lastValue = -1

observable.bindTo(owner) { lastValue = it }

observable.observers shouldContain owner

owner.destroy()

observable.observers shouldNotContain owner

observable.current = 40
lastValue shouldBe 10
}

test("bindTo should not subscribe at all if Lifecycle is already DESTROYED") {
val owner = TestLifecycleOwner()
owner.destroy()
val observable = observe(10)

observable.bindTo(owner) { }

observable.observers shouldNotContain owner
}

test("Multiple bindings to the same owner should work independently") {
val owner = TestLifecycleOwner()
owner.start()
val obs1 = observe(1)
val obs2 = observe(2)
var val1 = -1
var val2 = -1

obs1.bindTo(owner) { val1 = it }
obs2.bindTo(owner) { val2 = it }

val1 shouldBe 1
val2 shouldBe 2

owner.destroy()

obs1.observers shouldNotContain owner
obs2.observers shouldNotContain owner
}
}
})
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,10 @@ open class Engine<T, P : Position<out P>>(
}

override fun nodeRemoved(node: Node<T>, oldNeighborhood: Neighborhood<T>) {
// copy of reactions due to how [GenericNode.dispose] clears the reactions
val reactions = ArrayList(node.reactions)
schedule {
node.reactions.forEach { removeReaction(it) }
reactions.forEach { removeReaction(it) }
}
}

Expand Down
Loading
Loading