summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Booleans.kt32
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/BuildScope.kt208
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combinators.kt278
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combine.kt201
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/DeferredValue.kt37
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/EffectScope.kt49
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Events.kt350
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Filter.kt83
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/GroupBy.kt178
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Incremental.kt157
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosNetwork.kt110
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosScope.kt35
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Merge.kt258
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Modes.kt103
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Selector.kt88
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/State.kt358
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/StateScope.kt538
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Switch.kt111
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/ToColdFlow.kt99
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TransactionScope.kt61
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Transactional.kt18
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/BuildScopeImpl.kt68
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/FilterNode.kt13
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/IncrementalImpl.kt50
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Init.kt6
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxDeferred.kt24
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxPrompt.kt13
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Network.kt19
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateImpl.kt7
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateScopeImpl.kt12
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/HeteroMap.kt7
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/Util.kt2
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Either.kt108
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/MapPatch.kt33
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Maybe.kt100
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/These.kt125
-rw-r--r--packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosSamples.kt774
-rw-r--r--packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosTests.kt69
38 files changed, 3169 insertions, 1613 deletions
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Booleans.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Booleans.kt
new file mode 100644
index 000000000000..ca02576f7093
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Booleans.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.kairos
+
+/** Returns a [State] that is `true` only when all of [states] are `true`. */
+@ExperimentalKairosApi
+fun allOf(vararg states: State<Boolean>): State<Boolean> = combine(*states) { it.allTrue() }
+
+/** Returns a [State] that is `true` when any of [states] are `true`. */
+@ExperimentalKairosApi
+fun anyOf(vararg states: State<Boolean>): State<Boolean> = combine(*states) { it.anyTrue() }
+
+/** Returns a [State] containing the inverse of the Boolean held by the original [State]. */
+@ExperimentalKairosApi fun not(state: State<Boolean>): State<Boolean> = state.mapCheapUnsafe { !it }
+
+private fun Iterable<Boolean>.allTrue() = all { it }
+
+private fun Iterable<Boolean>.anyTrue() = any { it }
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/BuildScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/BuildScope.kt
index b6918703b404..bd2173cd2393 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/BuildScope.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/BuildScope.kt
@@ -17,17 +17,14 @@
package com.android.systemui.kairos
import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.just
import com.android.systemui.kairos.util.map
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.Job
import kotlinx.coroutines.awaitCancellation
-import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -36,9 +33,8 @@ import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.dropWhile
import kotlinx.coroutines.flow.scan
-import kotlinx.coroutines.launch
-/** A function that modifies the KairosNetwork. */
+/** A computation that can modify the Kairos network. */
typealias BuildSpec<A> = BuildScope.() -> A
/**
@@ -56,17 +52,7 @@ inline operator fun <A> BuildScope.invoke(block: BuildScope.() -> A) = run(block
/** Operations that add inputs and outputs to a Kairos network. */
@ExperimentalKairosApi
-interface BuildScope : StateScope {
-
- /**
- * A [KairosNetwork] handle that is bound to this [BuildScope].
- *
- * It supports all of the standard functionality by which external code can interact with this
- * Kairos network, but all [activated][KairosNetwork.activateSpec] [BuildSpec]s are bound as
- * children to this [BuildScope], such that when this [BuildScope] is destroyed, all children
- * are also destroyed.
- */
- val kairosNetwork: KairosNetwork
+interface BuildScope : HasNetwork, StateScope {
/**
* Defers invoking [block] until after the current [BuildScope] code-path completes, returning a
@@ -110,11 +96,21 @@ interface BuildScope : StateScope {
* executed if this [BuildScope] is still active by that time. It can be deactivated due to a
* -Latest combinator, for example.
*
- * Shorthand for:
- * ```kotlin
- * events.observe { effect { ... } }
+ * [Disposing][DisposableHandle.dispose] of the returned [DisposableHandle] will stop the
+ * observation of new emissions. It will however *not* cancel any running effects from previous
+ * emissions. To achieve this behavior, use [launchScope] or [asyncScope] to create a child
+ * build scope:
+ * ``` kotlin
+ * val job = launchScope {
+ * events.observe { x ->
+ * launchEffect { longRunningEffect(x) }
+ * }
+ * }
+ * // cancels observer and any running effects:
+ * job.cancel()
* ```
*/
+ // TODO: remove disposable handle return? might add more confusion than convenience
fun <A> Events<A>.observe(
coroutineContext: CoroutineContext = EmptyCoroutineContext,
block: EffectScope.(A) -> Unit = {},
@@ -129,7 +125,7 @@ interface BuildScope : StateScope {
* same key are undone (any registered [observers][observe] are unregistered, and any pending
* [side-effects][effect] are cancelled).
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
+ * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the
* previously-active [BuildSpec] will be undone with no replacement.
*/
fun <K, A, B> Events<Map<K, Maybe<BuildSpec<A>>>>.applyLatestSpecForKey(
@@ -138,32 +134,32 @@ interface BuildScope : StateScope {
): Pair<Events<Map<K, Maybe<A>>>, DeferredValue<Map<K, B>>>
/**
- * Creates an instance of an [Events] with elements that are from [builder].
+ * Creates an instance of an [Events] with elements that are emitted from [builder].
*
* [builder] is run in its own coroutine, allowing for ongoing work that can emit to the
- * provided [MutableState].
+ * provided [EventProducerScope].
*
* By default, [builder] is only running while the returned [Events] is being
* [observed][observe]. If you want it to run at all times, simply add a no-op observer:
- * ```kotlin
- * events { ... }.apply { observe() }
+ * ``` kotlin
+ * events { ... }.apply { observe() }
* ```
*/
- fun <T> events(
- name: String? = null,
- builder: suspend EventProducerScope<T>.() -> Unit,
- ): Events<T>
+ // TODO: eventually this should be defined on KairosNetwork + an extension on HasNetwork
+ // - will require modifying InputNode so that it can be manually killed, as opposed to using
+ // takeUntil (which requires a StateScope).
+ fun <T> events(builder: suspend EventProducerScope<T>.() -> Unit): Events<T>
/**
* Creates an instance of an [Events] with elements that are emitted from [builder].
*
* [builder] is run in its own coroutine, allowing for ongoing work that can emit to the
- * provided [MutableState].
+ * provided [CoalescingEventProducerScope].
*
* By default, [builder] is only running while the returned [Events] is being
* [observed][observe]. If you want it to run at all times, simply add a no-op observer:
- * ```kotlin
- * events { ... }.apply { observe() }
+ * ``` kotlin
+ * events { ... }.apply { observe() }
* ```
*
* In the event of backpressure, emissions are *coalesced* into batches. When a value is
@@ -171,6 +167,7 @@ interface BuildScope : StateScope {
* [coalesce]. Once the batch is consumed by the kairos network in the next transaction, the
* batch is reset back to [getInitialValue].
*/
+ // TODO: see TODO for [events]
fun <In, Out> coalescingEvents(
getInitialValue: () -> Out,
coalesce: (old: Out, new: In) -> Out,
@@ -186,6 +183,7 @@ interface BuildScope : StateScope {
*
* The return value from [block] can be accessed via the returned [DeferredValue].
*/
+ // TODO: return a DisposableHandle instead of Job?
fun <A> asyncScope(block: BuildSpec<A>): Pair<DeferredValue<A>, Job>
// TODO: once we have context params, these can all become extensions:
@@ -198,9 +196,9 @@ interface BuildScope : StateScope {
* outside of the current Kairos transaction; when [transform] returns, the returned value is
* emitted from the result [Events] in a new transaction.
*
- * Shorthand for:
- * ```kotlin
- * events.mapLatestBuild { a -> asyncEvent { transform(a) } }.flatten()
+ * ``` kotlin
+ * fun <A, B> Events<A>.mapAsyncLatest(transform: suspend (A) -> B): Events<B> =
+ * mapLatestBuild { a -> asyncEvent { transform(a) } }.flatten()
* ```
*/
fun <A, B> Events<A>.mapAsyncLatest(transform: suspend (A) -> B): Events<B> =
@@ -219,42 +217,19 @@ interface BuildScope : StateScope {
/**
* Returns a [StateFlow] whose [value][StateFlow.value] tracks the current
* [value of this State][State.sample], and will emit at the same rate as [State.changes].
- *
- * Note that the [value][StateFlow.value] is not available until the *end* of the current
- * transaction. If you need the current value before this time, then use [State.sample].
*/
fun <A> State<A>.toStateFlow(): StateFlow<A> {
- val uninitialized = Any()
- var initialValue: Any? = uninitialized
- val innerStateFlow = MutableStateFlow<Any?>(uninitialized)
- deferredBuildScope {
- initialValue = sample()
- changes.observe {
- innerStateFlow.value = it
- initialValue = null
- }
- }
-
- @Suppress("UNCHECKED_CAST")
- fun getValue(innerValue: Any?): A =
- when {
- innerValue !== uninitialized -> innerValue as A
- initialValue !== uninitialized -> initialValue as A
- else ->
- error(
- "Attempted to access StateFlow.value before Kairos transaction has completed."
- )
- }
-
+ val innerStateFlow = MutableStateFlow(sampleDeferred())
+ changes.observe { innerStateFlow.value = deferredOf(it) }
return object : StateFlow<A> {
override val replayCache: List<A>
- get() = innerStateFlow.replayCache.map(::getValue)
+ get() = innerStateFlow.replayCache.map { it.value }
override val value: A
- get() = getValue(innerStateFlow.value)
+ get() = innerStateFlow.value.value
override suspend fun collect(collector: FlowCollector<A>): Nothing {
- innerStateFlow.collect { collector.emit(getValue(it)) }
+ innerStateFlow.collect { collector.emit(it.value) }
}
}
}
@@ -365,14 +340,14 @@ interface BuildScope : StateScope {
initialSpec: BuildSpec<A>
): Pair<Events<B>, DeferredValue<A>> {
val (events, result) =
- mapCheap { spec -> mapOf(Unit to just(spec)) }
+ mapCheap { spec -> mapOf(Unit to Maybe.present(spec)) }
.applyLatestSpecForKey(initialSpecs = mapOf(Unit to initialSpec), numKeys = 1)
val outEvents: Events<B> =
events.mapMaybe {
checkNotNull(it[Unit]) { "applyLatest: expected result, but none present in: $it" }
}
val outInit: DeferredValue<A> = deferredBuildScope {
- val initResult: Map<Unit, A> = result.get()
+ val initResult: Map<Unit, A> = result.value
check(Unit in initResult) {
"applyLatest: expected initial result, but none present in: $initResult"
}
@@ -425,7 +400,7 @@ interface BuildScope : StateScope {
transform: BuildScope.(A) -> B,
): Pair<Events<B>, DeferredValue<B>> =
mapCheap { buildSpec { transform(it) } }
- .applyLatestSpec(initialSpec = buildSpec { transform(initialValue.get()) })
+ .applyLatestSpec(initialSpec = buildSpec { transform(initialValue.value) })
/**
* Returns an [Events] containing the results of applying each [BuildSpec] emitted from the
@@ -436,7 +411,7 @@ interface BuildScope : StateScope {
* same key are undone (any registered [observers][observe] are unregistered, and any pending
* [side-effects][effect] are cancelled).
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
+ * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the
* previously-active [BuildSpec] will be undone with no replacement.
*/
fun <K, A, B> Events<Map<K, Maybe<BuildSpec<A>>>>.applyLatestSpecForKey(
@@ -445,6 +420,17 @@ interface BuildScope : StateScope {
): Pair<Events<Map<K, Maybe<A>>>, DeferredValue<Map<K, B>>> =
applyLatestSpecForKey(deferredOf(initialSpecs), numKeys)
+ /**
+ * Returns an [Incremental] containing the results of applying each [BuildSpec] emitted from the
+ * original [Incremental].
+ *
+ * When each [BuildSpec] is applied, changes from the previously-active [BuildSpec] with the
+ * same key are undone (any registered [observers][observe] are unregistered, and any pending
+ * [side-effects][effect] are cancelled).
+ *
+ * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the
+ * previously-active [BuildSpec] will be undone with no replacement.
+ */
fun <K, V> Incremental<K, BuildSpec<V>>.applyLatestSpecForKey(
numKeys: Int? = null
): Incremental<K, V> {
@@ -460,7 +446,7 @@ interface BuildScope : StateScope {
* same key are undone (any registered [observers][observe] are unregistered, and any pending
* [side-effects][effect] are cancelled).
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
+ * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the
* previously-active [BuildSpec] will be undone with no replacement.
*/
fun <K, V> Events<Map<K, Maybe<BuildSpec<V>>>>.applyLatestSpecForKey(
@@ -476,7 +462,7 @@ interface BuildScope : StateScope {
* same key are undone (any registered [observers][observe] are unregistered, and any pending
* [side-effects][effect] are cancelled).
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
+ * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the
* previously-active [BuildSpec] will be undone with no replacement.
*/
fun <K, V> Events<Map<K, Maybe<BuildSpec<V>>>>.holdLatestSpecForKey(
@@ -495,7 +481,7 @@ interface BuildScope : StateScope {
* same key are undone (any registered [observers][observe] are unregistered, and any pending
* [side-effects][effect] are cancelled).
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
+ * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the
* previously-active [BuildSpec] will be undone with no replacement.
*/
fun <K, V> Events<Map<K, Maybe<BuildSpec<V>>>>.holdLatestSpecForKey(
@@ -513,7 +499,7 @@ interface BuildScope : StateScope {
* registered [observers][observe] are unregistered, and any pending [side-effects][effect] are
* cancelled).
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
+ * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the
* previously-active [BuildScope] will be undone with no replacement.
*/
fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestBuildForKey(
@@ -524,7 +510,7 @@ interface BuildScope : StateScope {
map { patch -> patch.mapValues { (k, v) -> v.map { buildSpec { transform(k, it) } } } }
.applyLatestSpecForKey(
deferredBuildScope {
- initialValues.get().mapValues { (k, v) -> buildSpec { transform(k, v) } }
+ initialValues.value.mapValues { (k, v) -> buildSpec { transform(k, v) } }
},
numKeys = numKeys,
)
@@ -539,7 +525,7 @@ interface BuildScope : StateScope {
* registered [observers][observe] are unregistered, and any pending [side-effects][effect] are
* cancelled).
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
+ * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the
* previously-active [BuildScope] will be undone with no replacement.
*/
fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestBuildForKey(
@@ -558,7 +544,7 @@ interface BuildScope : StateScope {
* registered [observers][observe] are unregistered, and any pending [side-effects][effect] are
* cancelled).
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
+ * If the [Maybe] value for an associated key is [absent][Maybe.absent], then the
* previously-active [BuildScope] will be undone with no replacement.
*/
fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestBuildForKey(
@@ -570,7 +556,7 @@ interface BuildScope : StateScope {
fun <R> Events<R>.nextDeferred(): Deferred<R> {
lateinit var next: CompletableDeferred<R>
val job = launchScope { nextOnly().observe { next.complete(it) } }
- next = CompletableDeferred<R>(parent = job)
+ next = CompletableDeferred(parent = job)
return next
}
@@ -581,12 +567,11 @@ interface BuildScope : StateScope {
}
/** Returns an [Events] that emits whenever this [Flow] emits. */
- fun <A> Flow<A>.toEvents(name: String? = null): Events<A> =
- events(name) { collect { emit(it) } }
+ fun <A> Flow<A>.toEvents(): Events<A> = events { collect { emit(it) } }
/**
* Shorthand for:
- * ```kotlin
+ * ``` kotlin
* flow.toEvents().holdState(initialValue)
* ```
*/
@@ -594,7 +579,7 @@ interface BuildScope : StateScope {
/**
* Shorthand for:
- * ```kotlin
+ * ``` kotlin
* flow.scan(initialValue, operation).toEvents().holdState(initialValue)
* ```
*/
@@ -603,7 +588,7 @@ interface BuildScope : StateScope {
/**
* Shorthand for:
- * ```kotlin
+ * ``` kotlin
* flow.scan(initialValue) { a, f -> f(a) }.toEvents().holdState(initialValue)
* ```
*/
@@ -679,6 +664,13 @@ interface BuildScope : StateScope {
* Invokes [block] on the value held in this [State]. [block] receives an [BuildScope] that can
* be used to make further modifications to the Kairos network, and/or perform side-effects via
* [effect].
+ *
+ * ``` kotlin
+ * fun <A> State<A>.observeBuild(block: BuildScope.(A) -> Unit = {}): Job = launchScope {
+ * block(sample())
+ * changes.observeBuild(block)
+ * }
+ * ```
*/
fun <A> State<A>.observeBuild(block: BuildScope.(A) -> Unit = {}): Job = launchScope {
block(sample())
@@ -706,12 +698,9 @@ interface BuildScope : StateScope {
* outside of the current Kairos transaction; when it completes, the returned [Events] emits in a
* new transaction.
*
- * Shorthand for:
- * ```
- * events { emitter: MutableEvents<A> ->
- * val a = block()
- * emitter.emit(a)
- * }
+ * ``` kotlin
+ * fun <A> BuildScope.asyncEvent(block: suspend () -> A): Events<A> =
+ * events { emit(block()) }.apply { observe() }
* ```
*/
@ExperimentalKairosApi
@@ -730,9 +719,12 @@ fun <A> BuildScope.asyncEvent(block: suspend () -> A): Events<A> =
* executed if this [BuildScope] is still active by that time. It can be deactivated due to a
* -Latest combinator, for example.
*
- * Shorthand for:
- * ```kotlin
- * launchScope { now.observe { block() } }
+ * ``` kotlin
+ * fun BuildScope.effect(
+ * context: CoroutineContext = EmptyCoroutineContext,
+ * block: EffectScope.() -> Unit,
+ * ): Job =
+ * launchScope { now.observe(context) { block() } }
* ```
*/
@ExperimentalKairosApi
@@ -748,13 +740,14 @@ fun BuildScope.effect(
* done because the current [BuildScope] might be deactivated within this transaction, perhaps due
* to a -Latest combinator. If this happens, then the coroutine will never actually be started.
*
- * Shorthand for:
- * ```kotlin
- * effect { effectCoroutineScope.launch { block() } }
+ * ``` kotlin
+ * fun BuildScope.launchEffect(block: suspend KairosScope.() -> Unit): Job =
+ * effect { effectCoroutineScope.launch { block() } }
* ```
*/
@ExperimentalKairosApi
-fun BuildScope.launchEffect(block: suspend CoroutineScope.() -> Unit): Job = asyncEffect(block)
+fun BuildScope.launchEffect(block: suspend KairosCoroutineScope.() -> Unit): Job =
+ asyncEffect(block)
/**
* Launches [block] in a new coroutine, returning the result as a [Deferred].
@@ -764,17 +757,18 @@ fun BuildScope.launchEffect(block: suspend CoroutineScope.() -> Unit): Job = asy
* to a -Latest combinator. If this happens, then the coroutine will never actually be started.
*
* Shorthand for:
- * ```kotlin
- * CompletableDeferred<R>.apply {
- * effect { effectCoroutineScope.launch { complete(coroutineScope { block() }) } }
- * }
- * .await()
+ * ``` kotlin
+ * fun <R> BuildScope.asyncEffect(block: suspend KairosScope.() -> R): Deferred<R> =
+ * CompletableDeferred<R>.apply {
+ * effect { effectCoroutineScope.launch { complete(block()) } }
+ * }
+ * .await()
* ```
*/
@ExperimentalKairosApi
-fun <R> BuildScope.asyncEffect(block: suspend CoroutineScope.() -> R): Deferred<R> {
+fun <R> BuildScope.asyncEffect(block: suspend KairosCoroutineScope.() -> R): Deferred<R> {
val result = CompletableDeferred<R>()
- val job = effect { effectCoroutineScope.launch { result.complete(coroutineScope(block)) } }
+ val job = effect { launch { result.complete(block()) } }
val handle = job.invokeOnCompletion { result.cancel() }
result.invokeOnCompletion {
handle.dispose()
@@ -795,7 +789,7 @@ fun BuildScope.launchScope(block: BuildSpec<*>): Job = asyncScope(block).second
*
* By default, [builder] is only running while the returned [Events] is being
* [observed][BuildScope.observe]. If you want it to run at all times, simply add a no-op observer:
- * ```kotlin
+ * ``` kotlin
* events { ... }.apply { observe() }
* ```
*
@@ -819,7 +813,7 @@ fun <In, Out> BuildScope.coalescingEvents(
*
* By default, [builder] is only running while the returned [Events] is being
* [observed][BuildScope.observe]. If you want it to run at all times, simply add a no-op observer:
- * ```kotlin
+ * ``` kotlin
* events { ... }.apply { observe() }
* ```
*
@@ -837,7 +831,7 @@ fun <T> BuildScope.conflatedEvents(
}
/** Scope for emitting to a [BuildScope.coalescingEvents]. */
-interface CoalescingEventProducerScope<in T> {
+fun interface CoalescingEventProducerScope<in T> {
/**
* Inserts [value] into the current batch, enqueueing it for emission from this [Events] if not
* already pending.
@@ -850,7 +844,7 @@ interface CoalescingEventProducerScope<in T> {
}
/** Scope for emitting to a [BuildScope.events]. */
-interface EventProducerScope<in T> {
+fun interface EventProducerScope<in T> {
/**
* Emits a [value] to this [Events], suspending the caller until the Kairos transaction
* containing the emission has completed.
@@ -868,3 +862,11 @@ suspend fun awaitClose(block: () -> Unit): Nothing =
} finally {
block()
}
+
+/**
+ * Runs [spec] in this [BuildScope], and then re-runs it whenever [rebuildSignal] emits. Returns a
+ * [State] that holds the result of the currently-active [BuildSpec].
+ */
+@ExperimentalKairosApi
+fun <A> BuildScope.rebuildOn(rebuildSignal: Events<*>, spec: BuildSpec<A>): State<A> =
+ rebuildSignal.map { spec }.holdLatestSpec(spec)
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combinators.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combinators.kt
deleted file mode 100644
index c20864648f00..000000000000
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combinators.kt
+++ /dev/null
@@ -1,278 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.kairos
-
-import com.android.systemui.kairos.util.These
-import com.android.systemui.kairos.util.WithPrev
-import com.android.systemui.kairos.util.just
-import com.android.systemui.kairos.util.none
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.channelFlow
-import kotlinx.coroutines.flow.conflate
-
-/**
- * Returns an [Events] that emits the value sampled from the [Transactional] produced by each
- * emission of the original [Events], within the same transaction of the original emission.
- */
-@ExperimentalKairosApi
-fun <A> Events<Transactional<A>>.sampleTransactionals(): Events<A> = map { it.sample() }
-
-/** @see TransactionScope.sample */
-@ExperimentalKairosApi
-fun <A, B, C> Events<A>.sample(
- state: State<B>,
- transform: TransactionScope.(A, B) -> C,
-): Events<C> = map { transform(it, state.sample()) }
-
-/** @see TransactionScope.sample */
-@ExperimentalKairosApi
-fun <A, B, C> Events<A>.sample(
- sampleable: Transactional<B>,
- transform: TransactionScope.(A, B) -> C,
-): Events<C> = map { transform(it, sampleable.sample()) }
-
-/**
- * Like [sample], but if [state] is changing at the time it is sampled ([changes] is emitting), then
- * the new value is passed to [transform].
- *
- * Note that [sample] is both more performant, and safer to use with recursive definitions. You will
- * generally want to use it rather than this.
- *
- * @see sample
- */
-@ExperimentalKairosApi
-fun <A, B, C> Events<A>.samplePromptly(
- state: State<B>,
- transform: TransactionScope.(A, B) -> C,
-): Events<C> =
- sample(state) { a, b -> These.thiz(a to b) }
- .mergeWith(state.changes.map { These.that(it) }) { thiz, that ->
- These.both((thiz as These.This).thiz, (that as These.That).that)
- }
- .mapMaybe { these ->
- when (these) {
- // both present, transform the upstream value and the new value
- is These.Both -> just(transform(these.thiz.first, these.that))
- // no upstream present, so don't perform the sample
- is These.That -> none()
- // just the upstream, so transform the upstream and the old value
- is These.This -> just(transform(these.thiz.first, these.thiz.second))
- }
- }
-
-/**
- * Returns a cold [Flow] that, when collected, emits from this [Events]. [network] is needed to
- * transactionally connect to / disconnect from the [Events] when collection starts/stops.
- */
-@ExperimentalKairosApi
-fun <A> Events<A>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
- channelFlow { network.activateSpec { observe { trySend(it) } } }.conflate()
-
-/**
- * Returns a cold [Flow] that, when collected, emits from this [State]. [network] is needed to
- * transactionally connect to / disconnect from the [State] when collection starts/stops.
- */
-@ExperimentalKairosApi
-fun <A> State<A>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
- channelFlow { network.activateSpec { observe { trySend(it) } } }.conflate()
-
-/**
- * Returns a cold [Flow] that, when collected, applies this [BuildSpec] in a new transaction in this
- * [network], and then emits from the returned [Events].
- *
- * When collection is cancelled, so is the [BuildSpec]. This means all ongoing work is cleaned up.
- */
-@ExperimentalKairosApi
-@JvmName("eventsSpecToColdConflatedFlow")
-fun <A> BuildSpec<Events<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
- channelFlow { network.activateSpec { applySpec().observe { trySend(it) } } }.conflate()
-
-/**
- * Returns a cold [Flow] that, when collected, applies this [BuildSpec] in a new transaction in this
- * [network], and then emits from the returned [State].
- *
- * When collection is cancelled, so is the [BuildSpec]. This means all ongoing work is cleaned up.
- */
-@ExperimentalKairosApi
-@JvmName("stateSpecToColdConflatedFlow")
-fun <A> BuildSpec<State<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
- channelFlow { network.activateSpec { applySpec().observe { trySend(it) } } }.conflate()
-
-/**
- * Returns a cold [Flow] that, when collected, applies this [Transactional] in a new transaction in
- * this [network], and then emits from the returned [Events].
- */
-@ExperimentalKairosApi
-@JvmName("transactionalFlowToColdConflatedFlow")
-fun <A> Transactional<Events<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
- channelFlow { network.activateSpec { sample().observe { trySend(it) } } }.conflate()
-
-/**
- * Returns a cold [Flow] that, when collected, applies this [Transactional] in a new transaction in
- * this [network], and then emits from the returned [State].
- */
-@ExperimentalKairosApi
-@JvmName("transactionalStateToColdConflatedFlow")
-fun <A> Transactional<State<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
- channelFlow { network.activateSpec { sample().observe { trySend(it) } } }.conflate()
-
-/**
- * Returns a cold [Flow] that, when collected, applies this [Stateful] in a new transaction in this
- * [network], and then emits from the returned [Events].
- *
- * When collection is cancelled, so is the [Stateful]. This means all ongoing work is cleaned up.
- */
-@ExperimentalKairosApi
-@JvmName("statefulFlowToColdConflatedFlow")
-fun <A> Stateful<Events<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
- channelFlow { network.activateSpec { applyStateful().observe { trySend(it) } } }.conflate()
-
-/**
- * Returns a cold [Flow] that, when collected, applies this [Transactional] in a new transaction in
- * this [network], and then emits from the returned [State].
- *
- * When collection is cancelled, so is the [Stateful]. This means all ongoing work is cleaned up.
- */
-@ExperimentalKairosApi
-@JvmName("statefulStateToColdConflatedFlow")
-fun <A> Stateful<State<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
- channelFlow { network.activateSpec { applyStateful().observe { trySend(it) } } }.conflate()
-
-/** Return an [Events] that emits from the original [Events] only when [state] is `true`. */
-@ExperimentalKairosApi
-fun <A> Events<A>.filter(state: State<Boolean>): Events<A> = filter { state.sample() }
-
-private fun Iterable<Boolean>.allTrue() = all { it }
-
-private fun Iterable<Boolean>.anyTrue() = any { it }
-
-/** Returns a [State] that is `true` only when all of [states] are `true`. */
-@ExperimentalKairosApi
-fun allOf(vararg states: State<Boolean>): State<Boolean> = combine(*states) { it.allTrue() }
-
-/** Returns a [State] that is `true` when any of [states] are `true`. */
-@ExperimentalKairosApi
-fun anyOf(vararg states: State<Boolean>): State<Boolean> = combine(*states) { it.anyTrue() }
-
-/** Returns a [State] containing the inverse of the Boolean held by the original [State]. */
-@ExperimentalKairosApi fun not(state: State<Boolean>): State<Boolean> = state.mapCheapUnsafe { !it }
-
-/**
- * Represents a modal Kairos sub-network.
- *
- * When [enabled][enableMode], all network modifications are applied immediately to the Kairos
- * network. When the returned [Events] emits a [BuildMode], that mode is enabled and replaces this
- * mode, undoing all modifications in the process (any registered [observers][BuildScope.observe]
- * are unregistered, and any pending [side-effects][BuildScope.effect] are cancelled).
- *
- * Use [compiledBuildSpec] to compile and stand-up a mode graph.
- *
- * @see StatefulMode
- */
-@ExperimentalKairosApi
-fun interface BuildMode<out A> {
- /**
- * Invoked when this mode is enabled. Returns a value and an [Events] that signals a switch to a
- * new mode.
- */
- fun BuildScope.enableMode(): Pair<A, Events<BuildMode<A>>>
-}
-
-/**
- * Returns an [BuildSpec] that, when [applied][BuildScope.applySpec], stands up a modal-transition
- * graph starting with this [BuildMode], automatically switching to new modes as they are produced.
- *
- * @see BuildMode
- */
-@ExperimentalKairosApi
-val <A> BuildMode<A>.compiledBuildSpec: BuildSpec<State<A>>
- get() = buildSpec {
- var modeChangeEvents by EventsLoop<BuildMode<A>>()
- val activeMode: State<Pair<A, Events<BuildMode<A>>>> =
- modeChangeEvents
- .map { it.run { buildSpec { enableMode() } } }
- .holdLatestSpec(buildSpec { enableMode() })
- modeChangeEvents =
- activeMode
- .map { statefully { it.second.nextOnly() } }
- .applyLatestStateful()
- .switchEvents()
- activeMode.map { it.first }
- }
-
-/**
- * Represents a modal Kairos sub-network.
- *
- * When [enabled][enableMode], all state accumulation is immediately started. When the returned
- * [Events] emits a [BuildMode], that mode is enabled and replaces this mode, stopping all state
- * accumulation in the process.
- *
- * Use [compiledStateful] to compile and stand-up a mode graph.
- *
- * @see BuildMode
- */
-@ExperimentalKairosApi
-fun interface StatefulMode<out A> {
- /**
- * Invoked when this mode is enabled. Returns a value and an [Events] that signals a switch to a
- * new mode.
- */
- fun StateScope.enableMode(): Pair<A, Events<StatefulMode<A>>>
-}
-
-/**
- * Returns an [Stateful] that, when [applied][StateScope.applyStateful], stands up a
- * modal-transition graph starting with this [StatefulMode], automatically switching to new modes as
- * they are produced.
- *
- * @see BuildMode
- */
-@ExperimentalKairosApi
-val <A> StatefulMode<A>.compiledStateful: Stateful<State<A>>
- get() = statefully {
- var modeChangeEvents by EventsLoop<StatefulMode<A>>()
- val activeMode: State<Pair<A, Events<StatefulMode<A>>>> =
- modeChangeEvents
- .map { it.run { statefully { enableMode() } } }
- .holdLatestStateful(statefully { enableMode() })
- modeChangeEvents =
- activeMode
- .map { statefully { it.second.nextOnly() } }
- .applyLatestStateful()
- .switchEvents()
- activeMode.map { it.first }
- }
-
-/**
- * Runs [spec] in this [BuildScope], and then re-runs it whenever [rebuildSignal] emits. Returns a
- * [State] that holds the result of the currently-active [BuildSpec].
- */
-@ExperimentalKairosApi
-fun <A> BuildScope.rebuildOn(rebuildSignal: Events<*>, spec: BuildSpec<A>): State<A> =
- rebuildSignal.map { spec }.holdLatestSpec(spec)
-
-/**
- * Like [changes] but also includes the old value of this [State].
- *
- * Shorthand for:
- * ``` kotlin
- * stateChanges.map { WithPrev(previousValue = sample(), newValue = it) }
- * ```
- */
-@ExperimentalKairosApi
-val <A> State<A>.transitions: Events<WithPrev<A, A>>
- get() = changes.map { WithPrev(previousValue = sample(), newValue = it) }
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combine.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combine.kt
new file mode 100644
index 000000000000..b3d89c31619b
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combine.kt
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.kairos
+
+import com.android.systemui.kairos.internal.NoScope
+import com.android.systemui.kairos.internal.init
+import com.android.systemui.kairos.internal.zipStates
+
+/**
+ * Returns a [State] whose value is generated with [transform] by combining the current values of
+ * each given [State].
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.combineState
+ */
+@ExperimentalKairosApi
+@JvmName(name = "stateCombine")
+fun <A, B, C> State<A>.combine(other: State<B>, transform: KairosScope.(A, B) -> C): State<C> =
+ combine(this, other, transform)
+
+/**
+ * Returns a [State] by combining the values held inside the given [States][State] into a [List].
+ *
+ * @see State.combine
+ */
+@ExperimentalKairosApi
+fun <A> Iterable<State<A>>.combine(): State<List<A>> {
+ val operatorName = "combine"
+ val name = operatorName
+ return StateInit(
+ init(name) {
+ val states = map { it.init }
+ zipStates(
+ name,
+ operatorName,
+ states.size,
+ states = init(null) { states.map { it.connect(this) } },
+ )
+ }
+ )
+}
+
+/**
+ * Returns a [State] by combining the values held inside the given [States][State] into a [Map].
+ *
+ * @see State.combine
+ */
+@ExperimentalKairosApi
+fun <K, A> Map<K, State<A>>.combine(): State<Map<K, A>> =
+ asIterable().map { (k, state) -> state.map { v -> k to v } }.combine().map { it.toMap() }
+
+/**
+ * Returns a [State] whose value is generated with [transform] by combining the current values of
+ * each given [State].
+ *
+ * @see State.combine
+ */
+@ExperimentalKairosApi
+fun <A, B> Iterable<State<A>>.combine(transform: KairosScope.(List<A>) -> B): State<B> =
+ combine().map(transform)
+
+/**
+ * Returns a [State] by combining the values held inside the given [State]s into a [List].
+ *
+ * @see State.combine
+ */
+@ExperimentalKairosApi
+fun <A> combine(vararg states: State<A>): State<List<A>> = states.asIterable().combine()
+
+/**
+ * Returns a [State] whose value is generated with [transform] by combining the current values of
+ * each given [State].
+ *
+ * @see State.combine
+ */
+@ExperimentalKairosApi
+fun <A, B> combine(vararg states: State<A>, transform: KairosScope.(List<A>) -> B): State<B> =
+ states.asIterable().combine(transform)
+
+/**
+ * Returns a [State] whose value is generated with [transform] by combining the current values of
+ * each given [State].
+ *
+ * @see State.combine
+ */
+@ExperimentalKairosApi
+fun <A, B, Z> combine(
+ stateA: State<A>,
+ stateB: State<B>,
+ transform: KairosScope.(A, B) -> Z,
+): State<Z> {
+ val operatorName = "combine"
+ val name = operatorName
+ return StateInit(
+ init(name) {
+ zipStates(name, operatorName, stateA.init, stateB.init) { a, b ->
+ NoScope.transform(a, b)
+ }
+ }
+ )
+}
+
+/**
+ * Returns a [State] whose value is generated with [transform] by combining the current values of
+ * each given [State].
+ *
+ * @see State.combine
+ */
+@ExperimentalKairosApi
+fun <A, B, C, Z> combine(
+ stateA: State<A>,
+ stateB: State<B>,
+ stateC: State<C>,
+ transform: KairosScope.(A, B, C) -> Z,
+): State<Z> {
+ val operatorName = "combine"
+ val name = operatorName
+ return StateInit(
+ init(name) {
+ zipStates(name, operatorName, stateA.init, stateB.init, stateC.init) { a, b, c ->
+ NoScope.transform(a, b, c)
+ }
+ }
+ )
+}
+
+/**
+ * Returns a [State] whose value is generated with [transform] by combining the current values of
+ * each given [State].
+ *
+ * @see State.combine
+ */
+@ExperimentalKairosApi
+fun <A, B, C, D, Z> combine(
+ stateA: State<A>,
+ stateB: State<B>,
+ stateC: State<C>,
+ stateD: State<D>,
+ transform: KairosScope.(A, B, C, D) -> Z,
+): State<Z> {
+ val operatorName = "combine"
+ val name = operatorName
+ return StateInit(
+ init(name) {
+ zipStates(name, operatorName, stateA.init, stateB.init, stateC.init, stateD.init) {
+ a,
+ b,
+ c,
+ d ->
+ NoScope.transform(a, b, c, d)
+ }
+ }
+ )
+}
+
+/**
+ * Returns a [State] whose value is generated with [transform] by combining the current values of
+ * each given [State].
+ *
+ * @see State.combine
+ */
+@ExperimentalKairosApi
+fun <A, B, C, D, E, Z> combine(
+ stateA: State<A>,
+ stateB: State<B>,
+ stateC: State<C>,
+ stateD: State<D>,
+ stateE: State<E>,
+ transform: KairosScope.(A, B, C, D, E) -> Z,
+): State<Z> {
+ val operatorName = "combine"
+ val name = operatorName
+ return StateInit(
+ init(name) {
+ zipStates(
+ name,
+ operatorName,
+ stateA.init,
+ stateB.init,
+ stateC.init,
+ stateD.init,
+ stateE.init,
+ ) { a, b, c, d, e ->
+ NoScope.transform(a, b, c, d, e)
+ }
+ }
+ )
+}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/DeferredValue.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/DeferredValue.kt
new file mode 100644
index 000000000000..4b9bb0ee30a2
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/DeferredValue.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.kairos
+
+import com.android.systemui.kairos.internal.CompletableLazy
+
+/**
+ * A value that may not be immediately (synchronously) available, but is guaranteed to be available
+ * before this transaction is completed.
+ */
+@ExperimentalKairosApi
+class DeferredValue<out A> internal constructor(internal val unwrapped: Lazy<A>) {
+ /**
+ * Returns the value held by this [DeferredValue], or throws [IllegalStateException] if it is
+ * not yet available.
+ */
+ val value: A
+ get() = unwrapped.value
+}
+
+/** Returns an already-available [DeferredValue] containing [value]. */
+@ExperimentalKairosApi
+fun <A> deferredOf(value: A): DeferredValue<A> = DeferredValue(CompletableLazy(value))
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/EffectScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/EffectScope.kt
index 7e257f2831af..14d45d447c54 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/EffectScope.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/EffectScope.kt
@@ -16,33 +16,46 @@
package com.android.systemui.kairos
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Job
/**
- * Scope for external side-effects triggered by the Kairos network. This still occurs within the
- * context of a transaction, so general suspending calls are disallowed to prevent blocking the
- * transaction. You can use [effectCoroutineScope] to [launch][kotlinx.coroutines.launch] new
- * coroutines to perform long-running asynchronous work. This scope is alive for the duration of the
- * containing [BuildScope] that this side-effect scope is running in.
+ * Scope for external side-effects triggered by the Kairos network.
+ *
+ * This still occurs within the context of a transaction, so general suspending calls are disallowed
+ * to prevent blocking the transaction. You can [launch] new coroutines to perform long-running
+ * asynchronous work. These coroutines are kept alive for the duration of the containing
+ * [BuildScope] that this side-effect scope is running in.
*/
@ExperimentalKairosApi
-interface EffectScope : TransactionScope {
+interface EffectScope : HasNetwork, TransactionScope {
/**
- * A [CoroutineScope] whose lifecycle lives for as long as this [EffectScope] is alive. This is
- * generally until the [Job][kotlinx.coroutines.Job] returned by [BuildScope.effect] is
- * cancelled.
+ * Creates a coroutine that is a child of this [EffectScope], and returns its future result as a
+ * [Deferred].
+ *
+ * @see kotlinx.coroutines.async
*/
- @ExperimentalKairosApi val effectCoroutineScope: CoroutineScope
+ fun <R> async(
+ context: CoroutineContext = EmptyCoroutineContext,
+ start: CoroutineStart = CoroutineStart.DEFAULT,
+ block: suspend KairosCoroutineScope.() -> R,
+ ): Deferred<R>
/**
- * A [KairosNetwork] instance that can be used to transactionally query / modify the Kairos
- * network.
+ * Launches a new coroutine that is a child of this [EffectScope] without blocking the current
+ * thread and returns a reference to the coroutine as a [Job].
*
- * The lambda passed to [KairosNetwork.transact] on this instance will receive an [BuildScope]
- * that is lifetime-bound to this [EffectScope]. Once this [EffectScope] is no longer alive, any
- * modifications to the Kairos network performed via this [KairosNetwork] instance will be
- * undone (any registered [observers][BuildScope.observe] are unregistered, and any pending
- * [side-effects][BuildScope.effect] are cancelled).
+ * @see kotlinx.coroutines.launch
*/
- @ExperimentalKairosApi val kairosNetwork: KairosNetwork
+ fun launch(
+ context: CoroutineContext = EmptyCoroutineContext,
+ start: CoroutineStart = CoroutineStart.DEFAULT,
+ block: suspend KairosCoroutineScope.() -> Unit,
+ ): Job = async(context, start, block)
}
+
+@ExperimentalKairosApi interface KairosCoroutineScope : HasNetwork, CoroutineScope
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Events.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Events.kt
index e7d0096f2189..8f468c153743 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Events.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Events.kt
@@ -17,7 +17,6 @@
package com.android.systemui.kairos
import com.android.systemui.kairos.internal.CompletableLazy
-import com.android.systemui.kairos.internal.DemuxImpl
import com.android.systemui.kairos.internal.EventsImpl
import com.android.systemui.kairos.internal.Init
import com.android.systemui.kairos.internal.InitScope
@@ -27,22 +26,11 @@ import com.android.systemui.kairos.internal.NoScope
import com.android.systemui.kairos.internal.activated
import com.android.systemui.kairos.internal.cached
import com.android.systemui.kairos.internal.constInit
-import com.android.systemui.kairos.internal.demuxMap
-import com.android.systemui.kairos.internal.filterImpl
-import com.android.systemui.kairos.internal.filterJustImpl
import com.android.systemui.kairos.internal.init
import com.android.systemui.kairos.internal.mapImpl
-import com.android.systemui.kairos.internal.mergeNodes
-import com.android.systemui.kairos.internal.mergeNodesLeft
import com.android.systemui.kairos.internal.neverImpl
-import com.android.systemui.kairos.internal.switchDeferredImplSingle
-import com.android.systemui.kairos.internal.switchPromptImplSingle
import com.android.systemui.kairos.internal.util.hashString
-import com.android.systemui.kairos.util.Either
-import com.android.systemui.kairos.util.Either.Left
-import com.android.systemui.kairos.util.Either.Right
import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.just
import com.android.systemui.kairos.util.toMaybe
import java.util.concurrent.atomic.AtomicReference
import kotlin.reflect.KProperty
@@ -51,7 +39,16 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
-/** A series of values of type [A] available at discrete points in time. */
+/**
+ * A series of values of type [A] available at discrete points in time.
+ *
+ * [Events] follow these rules:
+ * 1. Within a single Kairos network transaction, an [Events] instance will only emit *once*.
+ * 2. The order that different [Events] instances emit values within a transaction is undefined, and
+ * are conceptually *simultaneous*.
+ * 3. [Events] emissions are *ephemeral* and do not last beyond the transaction they are emitted,
+ * unless explicitly [observed][BuildScope.observe] or [held][StateScope.holdState] as a [State].
+ */
@ExperimentalKairosApi
sealed class Events<out A> {
companion object {
@@ -67,7 +64,9 @@ sealed class Events<out A> {
* A forward-reference to an [Events]. Useful for recursive definitions.
*
* This reference can be used like a standard [Events], but will throw an error if its [loopback] is
- * unset before the end of the first transaction which accesses it.
+ * unset before it is [observed][BuildScope.observe].
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.eventsLoop
*/
@ExperimentalKairosApi
class EventsLoop<A> : Events<A>() {
@@ -76,7 +75,10 @@ class EventsLoop<A> : Events<A>() {
internal val init: Init<EventsImpl<A>> =
init(name = null) { deferred.value.init.connect(evalScope = this) }
- /** The [Events] this reference is referring to. */
+ /**
+ * The [Events] this reference is referring to. Must be set before this [EventsLoop] is
+ * [observed][BuildScope.observe].
+ */
var loopback: Events<A>? = null
set(value) {
value?.let {
@@ -102,6 +104,12 @@ class EventsLoop<A> : Events<A>() {
* will be queried and used.
*
* Useful for recursive definitions.
+ *
+ * ``` kotlin
+ * fun <A> Lazy<Events<A>>.defer() = deferredEvents { value }
+ * ```
+ *
+ * @see deferredEvents
*/
@ExperimentalKairosApi fun <A> Lazy<Events<A>>.defer(): Events<A> = deferInline { value }
@@ -113,6 +121,12 @@ class EventsLoop<A> : Events<A>() {
* and used.
*
* Useful for recursive definitions.
+ *
+ * ``` kotlin
+ * fun <A> DeferredValue<Events<A>>.defer() = deferredEvents { get() }
+ * ```
+ *
+ * @see deferredEvents
*/
@ExperimentalKairosApi
fun <A> DeferredValue<Events<A>>.defer(): Events<A> = deferInline { unwrapped.value }
@@ -130,25 +144,27 @@ fun <A> deferredEvents(block: KairosScope.() -> Events<A>): Events<A> = deferInl
NoScope.block()
}
-/** Returns an [Events] that emits the new value of this [State] when it changes. */
-@ExperimentalKairosApi
-val <A> State<A>.changes: Events<A>
- get() = EventsInit(init(name = null) { init.connect(evalScope = this).changes })
-
/**
- * Returns an [Events] that contains only the [just] results of applying [transform] to each value
- * of the original [Events].
+ * Returns an [Events] that contains only the
+ * [present][com.android.systemui.kairos.util.Maybe.present] results of applying [transform] to each
+ * value of the original [Events].
*
+ * @sample com.android.systemui.kairos.KairosSamples.mapMaybe
* @see mapNotNull
*/
@ExperimentalKairosApi
fun <A, B> Events<A>.mapMaybe(transform: TransactionScope.(A) -> Maybe<B>): Events<B> =
- map(transform).filterJust()
+ map(transform).filterPresent()
/**
* Returns an [Events] that contains only the non-null results of applying [transform] to each value
* of the original [Events].
*
+ * ``` kotlin
+ * fun <A> Events<A>.mapNotNull(transform: TransactionScope.(A) -> B?): Events<B> =
+ * mapMaybe { if (it == null) absent else present(it) }
+ * ```
+ *
* @see mapMaybe
*/
@ExperimentalKairosApi
@@ -156,23 +172,11 @@ fun <A, B> Events<A>.mapNotNull(transform: TransactionScope.(A) -> B?): Events<B
transform(it).toMaybe()
}
-/** Returns an [Events] containing only values of the original [Events] that are not null. */
-@ExperimentalKairosApi
-fun <A> Events<A?>.filterNotNull(): Events<A> = mapCheap { it.toMaybe() }.filterJust()
-
-/** Shorthand for `mapNotNull { it as? A }`. */
-@ExperimentalKairosApi
-inline fun <reified A> Events<*>.filterIsInstance(): Events<A> =
- mapCheap { it as? A }.filterNotNull()
-
-/** Shorthand for `mapMaybe { it }`. */
-@ExperimentalKairosApi
-fun <A> Events<Maybe<A>>.filterJust(): Events<A> =
- EventsInit(constInit(name = null, filterJustImpl { init.connect(evalScope = this) }))
-
/**
* Returns an [Events] containing the results of applying [transform] to each value of the original
* [Events].
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.mapEvents
*/
@ExperimentalKairosApi
fun <A, B> Events<A>.map(transform: TransactionScope.(A) -> B): Events<B> {
@@ -184,6 +188,7 @@ fun <A, B> Events<A>.map(transform: TransactionScope.(A) -> B): Events<B> {
* Like [map], but the emission is not cached during the transaction. Use only if [transform] is
* fast and pure.
*
+ * @sample com.android.systemui.kairos.KairosSamples.mapCheap
* @see map
*/
@ExperimentalKairosApi
@@ -196,8 +201,9 @@ fun <A, B> Events<A>.mapCheap(transform: TransactionScope.(A) -> B): Events<B> =
* Returns an [Events] that invokes [action] before each value of the original [Events] is emitted.
* Useful for logging and debugging.
*
- * ```
- * pulse.onEach { foo(it) } == pulse.map { foo(it); it }
+ * ``` kotlin
+ * fun <A> Events<A>.onEach(action: TransactionScope.(A) -> Unit): Events<A> =
+ * map { it.also { action(it) } }
* ```
*
* Note that the side effects performed in [onEach] are only performed while the resulting [Events]
@@ -207,29 +213,19 @@ fun <A, B> Events<A>.mapCheap(transform: TransactionScope.(A) -> B): Events<B> =
*/
@ExperimentalKairosApi
fun <A> Events<A>.onEach(action: TransactionScope.(A) -> Unit): Events<A> = map {
- action(it)
- it
-}
-
-/**
- * Returns an [Events] containing only values of the original [Events] that satisfy the given
- * [predicate].
- */
-@ExperimentalKairosApi
-fun <A> Events<A>.filter(predicate: TransactionScope.(A) -> Boolean): Events<A> {
- val pulse = filterImpl({ init.connect(evalScope = this) }) { predicate(it) }
- return EventsInit(constInit(name = null, pulse))
+ it.also { action(it) }
}
/**
* Splits an [Events] of pairs into a pair of [Events], where each returned [Events] emits half of
* the original.
*
- * Shorthand for:
- * ```kotlin
- * val lefts = map { it.first }
- * val rights = map { it.second }
- * return Pair(lefts, rights)
+ * ``` kotlin
+ * fun <A, B> Events<Pair<A, B>>.unzip(): Pair<Events<A>, Events<B>> {
+ * val lefts = map { it.first }
+ * val rights = map { it.second }
+ * return lefts to rights
+ * }
* ```
*/
@ExperimentalKairosApi
@@ -240,246 +236,6 @@ fun <A, B> Events<Pair<A, B>>.unzip(): Pair<Events<A>, Events<B>> {
}
/**
- * Merges the given [Events] into a single [Events] that emits events from both.
- *
- * Because [Events] can only emit one value per transaction, the provided [transformCoincidence]
- * function is used to combine coincident emissions to produce the result value to be emitted by the
- * merged [Events].
- */
-@ExperimentalKairosApi
-fun <A> Events<A>.mergeWith(
- other: Events<A>,
- name: String? = null,
- transformCoincidence: TransactionScope.(A, A) -> A = { a, _ -> a },
-): Events<A> {
- val node =
- mergeNodes(
- name = name,
- getPulse = { init.connect(evalScope = this) },
- getOther = { other.init.connect(evalScope = this) },
- ) { a, b ->
- transformCoincidence(a, b)
- }
- return EventsInit(constInit(name = null, node))
-}
-
-/**
- * Merges the given [Events] into a single [Events] that emits events from all. All coincident
- * emissions are collected into the emitted [List], preserving the input ordering.
- *
- * @see mergeWith
- * @see mergeLeft
- */
-@ExperimentalKairosApi
-fun <A> merge(vararg events: Events<A>): Events<List<A>> = events.asIterable().merge()
-
-/**
- * Merges the given [Events] into a single [Events] that emits events from all. In the case of
- * coincident emissions, the emission from the left-most [Events] is emitted.
- *
- * @see merge
- */
-@ExperimentalKairosApi
-fun <A> mergeLeft(vararg events: Events<A>): Events<A> = events.asIterable().mergeLeft()
-
-/**
- * Merges the given [Events] into a single [Events] that emits events from all.
- *
- * Because [Events] can only emit one value per transaction, the provided [transformCoincidence]
- * function is used to combine coincident emissions to produce the result value to be emitted by the
- * merged [Events].
- */
-// TODO: can be optimized to avoid creating the intermediate list
-fun <A> merge(vararg events: Events<A>, transformCoincidence: (A, A) -> A): Events<A> =
- merge(*events).map { l -> l.reduce(transformCoincidence) }
-
-/**
- * Merges the given [Events] into a single [Events] that emits events from all. All coincident
- * emissions are collected into the emitted [List], preserving the input ordering.
- *
- * @see mergeWith
- * @see mergeLeft
- */
-@ExperimentalKairosApi
-fun <A> Iterable<Events<A>>.merge(): Events<List<A>> =
- EventsInit(constInit(name = null, mergeNodes { map { it.init.connect(evalScope = this) } }))
-
-/**
- * Merges the given [Events] into a single [Events] that emits events from all. In the case of
- * coincident emissions, the emission from the left-most [Events] is emitted.
- *
- * @see merge
- */
-@ExperimentalKairosApi
-fun <A> Iterable<Events<A>>.mergeLeft(): Events<A> =
- EventsInit(constInit(name = null, mergeNodesLeft { map { it.init.connect(evalScope = this) } }))
-
-/**
- * Creates a new [Events] that emits events from all given [Events]. All simultaneous emissions are
- * collected into the emitted [List], preserving the input ordering.
- *
- * @see mergeWith
- */
-@ExperimentalKairosApi fun <A> Sequence<Events<A>>.merge(): Events<List<A>> = asIterable().merge()
-
-/**
- * Creates a new [Events] that emits events from all given [Events]. All simultaneous emissions are
- * collected into the emitted [Map], and are given the same key of the associated [Events] in the
- * input [Map].
- *
- * @see mergeWith
- */
-@ExperimentalKairosApi
-fun <K, A> Map<K, Events<A>>.merge(): Events<Map<K, A>> =
- asSequence()
- .map { (k, events) -> events.map { a -> k to a } }
- .toList()
- .merge()
- .map { it.toMap() }
-
-/**
- * Returns a [GroupedEvents] that can be used to efficiently split a single [Events] into multiple
- * downstream [Events].
- *
- * The input [Events] emits [Map] instances that specify which downstream [Events] the associated
- * value will be emitted from. These downstream [Events] can be obtained via
- * [GroupedEvents.eventsForKey].
- *
- * An example:
- * ```
- * val fooEvents: Events<Map<String, Foo>> = ...
- * val fooById: GroupedEvents<String, Foo> = fooEvents.groupByKey()
- * val fooBar: Events<Foo> = fooById["bar"]
- * ```
- *
- * This is semantically equivalent to `val fooBar = fooEvents.mapNotNull { map -> map["bar"] }` but
- * is significantly more efficient; specifically, using [mapNotNull] in this way incurs a `O(n)`
- * performance hit, where `n` is the number of different [mapNotNull] operations used to filter on a
- * specific key's presence in the emitted [Map]. [groupByKey] internally uses a [HashMap] to lookup
- * the appropriate downstream [Events], and so operates in `O(1)`.
- *
- * Note that the returned [GroupedEvents] should be cached and re-used to gain the performance
- * benefit.
- *
- * @see selector
- */
-@ExperimentalKairosApi
-fun <K, A> Events<Map<K, A>>.groupByKey(numKeys: Int? = null): GroupedEvents<K, A> =
- GroupedEvents(demuxMap({ init.connect(this) }, numKeys))
-
-/**
- * Shorthand for `map { mapOf(extractKey(it) to it) }.groupByKey()`
- *
- * @see groupByKey
- */
-@ExperimentalKairosApi
-fun <K, A> Events<A>.groupBy(
- numKeys: Int? = null,
- extractKey: TransactionScope.(A) -> K,
-): GroupedEvents<K, A> = map { mapOf(extractKey(it) to it) }.groupByKey(numKeys)
-
-/**
- * Returns two new [Events] that contain elements from this [Events] that satisfy or don't satisfy
- * [predicate].
- *
- * Using this is equivalent to `upstream.filter(predicate) to upstream.filter { !predicate(it) }`
- * but is more efficient; specifically, [partition] will only invoke [predicate] once per element.
- */
-@ExperimentalKairosApi
-fun <A> Events<A>.partition(
- predicate: TransactionScope.(A) -> Boolean
-): Pair<Events<A>, Events<A>> {
- val grouped: GroupedEvents<Boolean, A> = groupBy(numKeys = 2, extractKey = predicate)
- return Pair(grouped.eventsForKey(true), grouped.eventsForKey(false))
-}
-
-/**
- * Returns two new [Events] that contain elements from this [Events]; [Pair.first] will contain
- * [Left] values, and [Pair.second] will contain [Right] values.
- *
- * Using this is equivalent to using [filterIsInstance] in conjunction with [map] twice, once for
- * [Left]s and once for [Right]s, but is slightly more efficient; specifically, the
- * [filterIsInstance] check is only performed once per element.
- */
-@ExperimentalKairosApi
-fun <A, B> Events<Either<A, B>>.partitionEither(): Pair<Events<A>, Events<B>> {
- val (left, right) = partition { it is Left }
- return Pair(left.mapCheap { (it as Left).value }, right.mapCheap { (it as Right).value })
-}
-
-/**
- * A mapping from keys of type [K] to [Events] emitting values of type [A].
- *
- * @see groupByKey
- */
-@ExperimentalKairosApi
-class GroupedEvents<in K, out A> internal constructor(internal val impl: DemuxImpl<K, A>) {
- /**
- * Returns an [Events] that emits values of type [A] that correspond to the given [key].
- *
- * @see groupByKey
- */
- fun eventsForKey(key: K): Events<A> = EventsInit(constInit(name = null, impl.eventsForKey(key)))
-
- /**
- * Returns an [Events] that emits values of type [A] that correspond to the given [key].
- *
- * @see groupByKey
- */
- operator fun get(key: K): Events<A> = eventsForKey(key)
-}
-
-/**
- * Returns an [Events] that switches to the [Events] contained within this [State] whenever it
- * changes.
- *
- * This switch does take effect until the *next* transaction after [State] changes. For a switch
- * that takes effect immediately, see [switchEventsPromptly].
- */
-@ExperimentalKairosApi
-fun <A> State<Events<A>>.switchEvents(name: String? = null): Events<A> {
- val patches =
- mapImpl({ init.connect(this).changes }) { newEvents, _ -> newEvents.init.connect(this) }
- return EventsInit(
- constInit(
- name = null,
- switchDeferredImplSingle(
- name = name,
- getStorage = {
- init.connect(this).getCurrentWithEpoch(this).first.init.connect(this)
- },
- getPatches = { patches },
- ),
- )
- )
-}
-
-/**
- * Returns an [Events] that switches to the [Events] contained within this [State] whenever it
- * changes.
- *
- * This switch takes effect immediately within the same transaction that [State] changes. In
- * general, you should prefer [switchEvents] over this method. It is both safer and more performant.
- */
-// TODO: parameter to handle coincidental emission from both old and new
-@ExperimentalKairosApi
-fun <A> State<Events<A>>.switchEventsPromptly(): Events<A> {
- val patches =
- mapImpl({ init.connect(this).changes }) { newEvents, _ -> newEvents.init.connect(this) }
- return EventsInit(
- constInit(
- name = null,
- switchPromptImplSingle(
- getStorage = {
- init.connect(this).getCurrentWithEpoch(this).first.init.connect(this)
- },
- getPatches = { patches },
- ),
- )
- )
-}
-
-/**
* A mutable [Events] that provides the ability to [emit] values to the network, handling
* backpressure by coalescing all emissions into batches.
*
@@ -494,7 +250,7 @@ internal constructor(
private val getInitialValue: () -> Out,
internal val impl: InputNode<Out> = InputNode(),
) : Events<Out>() {
- internal val storage = AtomicReference(false to lazy { getInitialValue() })
+ private val storage = AtomicReference(false to lazy { getInitialValue() })
override fun toString(): String = "${this::class.simpleName}@$hashString"
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Filter.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Filter.kt
new file mode 100644
index 000000000000..8ca5ac8652db
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Filter.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.kairos
+
+import com.android.systemui.kairos.internal.constInit
+import com.android.systemui.kairos.internal.filterImpl
+import com.android.systemui.kairos.internal.filterPresentImpl
+import com.android.systemui.kairos.util.Maybe
+import com.android.systemui.kairos.util.toMaybe
+
+/** Return an [Events] that emits from the original [Events] only when [state] is `true`. */
+@ExperimentalKairosApi
+fun <A> Events<A>.filter(state: State<Boolean>): Events<A> = filter { state.sample() }
+
+/**
+ * Returns an [Events] containing only values of the original [Events] that are not null.
+ *
+ * ``` kotlin
+ * fun <A> Events<A?>.filterNotNull(): Events<A> = mapNotNull { it }
+ * ```
+ *
+ * @see mapNotNull
+ */
+@ExperimentalKairosApi
+fun <A> Events<A?>.filterNotNull(): Events<A> = mapCheap { it.toMaybe() }.filterPresent()
+
+/**
+ * Returns an [Events] containing only values of the original [Events] that are instances of [A].
+ *
+ * ``` kotlin
+ * inline fun <reified A> Events<*>.filterIsInstance(): Events<A> =
+ * mapNotNull { it as? A }
+ * ```
+ *
+ * @see mapNotNull
+ */
+@ExperimentalKairosApi
+inline fun <reified A> Events<*>.filterIsInstance(): Events<A> =
+ mapCheap { it as? A }.filterNotNull()
+
+/**
+ * Returns an [Events] containing only values of the original [Events] that are present.
+ *
+ * ``` kotlin
+ * fun <A> Events<Maybe<A>>.filterPresent(): Events<A> = mapMaybe { it }
+ * ```
+ *
+ * @see mapMaybe
+ */
+@ExperimentalKairosApi
+fun <A> Events<Maybe<A>>.filterPresent(): Events<A> =
+ EventsInit(constInit(name = null, filterPresentImpl { init.connect(evalScope = this) }))
+
+/**
+ * Returns an [Events] containing only values of the original [Events] that satisfy the given
+ * [predicate].
+ *
+ * ``` kotlin
+ * fun <A> Events<A>.filter(predicate: TransactionScope.(A) -> Boolean): Events<A> =
+ * mapMaybe { if (predicate(it)) present(it) else absent }
+ * ```
+ *
+ * @see mapMaybe
+ */
+@ExperimentalKairosApi
+fun <A> Events<A>.filter(predicate: TransactionScope.(A) -> Boolean): Events<A> {
+ val pulse = filterImpl({ init.connect(evalScope = this) }) { predicate(it) }
+ return EventsInit(constInit(name = null, pulse))
+}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/GroupBy.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/GroupBy.kt
new file mode 100644
index 000000000000..45da34ac9ae6
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/GroupBy.kt
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.kairos
+
+import com.android.systemui.kairos.internal.DemuxImpl
+import com.android.systemui.kairos.internal.constInit
+import com.android.systemui.kairos.internal.demuxMap
+import com.android.systemui.kairos.util.Either
+import com.android.systemui.kairos.util.These
+import com.android.systemui.kairos.util.maybeFirst
+import com.android.systemui.kairos.util.maybeSecond
+import com.android.systemui.kairos.util.orError
+
+/**
+ * Returns a [GroupedEvents] that can be used to efficiently split a single [Events] into multiple
+ * downstream [Events].
+ *
+ * The input [Events] emits [Map] instances that specify which downstream [Events] the associated
+ * value will be emitted from. These downstream [Events] can be obtained via
+ * [GroupedEvents.eventsForKey].
+ *
+ * An example:
+ * ```
+ * val fooEvents: Events<Map<String, Foo>> = ...
+ * val fooById: GroupedEvents<String, Foo> = fooEvents.groupByKey()
+ * val fooBar: Events<Foo> = fooById["bar"]
+ * ```
+ *
+ * This is semantically equivalent to `val fooBar = fooEvents.mapNotNull { map -> map["bar"] }` but
+ * is significantly more efficient; specifically, using [mapNotNull] in this way incurs a `O(n)`
+ * performance hit, where `n` is the number of different [mapNotNull] operations used to filter on a
+ * specific key's presence in the emitted [Map]. [groupByKey] internally uses a [HashMap] to lookup
+ * the appropriate downstream [Events], and so operates in `O(1)`.
+ *
+ * The optional [numKeys] argument is an optimization used to initialize the internal [HashMap].
+ *
+ * Note that the returned [GroupedEvents] should be cached and re-used to gain the performance
+ * benefit.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.groupByKey
+ * @see selector
+ */
+@ExperimentalKairosApi
+fun <K, A> Events<Map<K, A>>.groupByKey(numKeys: Int? = null): GroupedEvents<K, A> =
+ GroupedEvents(demuxMap({ init.connect(this) }, numKeys))
+
+/**
+ * Returns a [GroupedEvents] that can be used to efficiently split a single [Events] into multiple
+ * downstream [Events]. The downstream [Events] are associated with a [key][K], which is derived
+ * from each emission of the original [Events] via [extractKey].
+ *
+ * ``` kotlin
+ * fun <K, A> Events<A>.groupBy(
+ * numKeys: Int? = null,
+ * extractKey: TransactionScope.(A) -> K,
+ * ): GroupedEvents<K, A> =
+ * map { mapOf(extractKey(it) to it) }.groupByKey(numKeys)
+ * ```
+ *
+ * @see groupByKey
+ */
+@ExperimentalKairosApi
+fun <K, A> Events<A>.groupBy(
+ numKeys: Int? = null,
+ extractKey: TransactionScope.(A) -> K,
+): GroupedEvents<K, A> = map { mapOf(extractKey(it) to it) }.groupByKey(numKeys)
+
+/**
+ * A mapping from keys of type [K] to [Events] emitting values of type [A].
+ *
+ * @see groupByKey
+ */
+@ExperimentalKairosApi
+class GroupedEvents<in K, out A> internal constructor(internal val impl: DemuxImpl<K, A>) {
+ /**
+ * Returns an [Events] that emits values of type [A] that correspond to the given [key].
+ *
+ * @see groupByKey
+ */
+ fun eventsForKey(key: K): Events<A> = EventsInit(constInit(name = null, impl.eventsForKey(key)))
+
+ /**
+ * Returns an [Events] that emits values of type [A] that correspond to the given [key].
+ *
+ * @see groupByKey
+ */
+ operator fun get(key: K): Events<A> = eventsForKey(key)
+}
+
+/**
+ * Returns two new [Events] that contain elements from this [Events] that satisfy or don't satisfy
+ * [predicate].
+ *
+ * Using this is equivalent to `upstream.filter(predicate) to upstream.filter { !predicate(it) }`
+ * but is more efficient; specifically, [partition] will only invoke [predicate] once per element.
+ *
+ * ``` kotlin
+ * fun <A> Events<A>.partition(
+ * predicate: TransactionScope.(A) -> Boolean
+ * ): Pair<Events<A>, Events<A>> =
+ * map { if (predicate(it)) left(it) else right(it) }.partitionEither()
+ * ```
+ *
+ * @see partitionEither
+ */
+@ExperimentalKairosApi
+fun <A> Events<A>.partition(
+ predicate: TransactionScope.(A) -> Boolean
+): Pair<Events<A>, Events<A>> {
+ val grouped: GroupedEvents<Boolean, A> = groupBy(numKeys = 2, extractKey = predicate)
+ return Pair(grouped.eventsForKey(true), grouped.eventsForKey(false))
+}
+
+/**
+ * Returns two new [Events] that contain elements from this [Events]; [Pair.first] will contain
+ * [First] values, and [Pair.second] will contain [Second] values.
+ *
+ * Using this is equivalent to using [filterIsInstance] in conjunction with [map] twice, once for
+ * [First]s and once for [Second]s, but is slightly more efficient; specifically, the
+ * [filterIsInstance] check is only performed once per element.
+ *
+ * ``` kotlin
+ * fun <A, B> Events<Either<A, B>>.partitionEither(): Pair<Events<A>, Events<B>> =
+ * map { it.asThese() }.partitionThese()
+ * ```
+ *
+ * @see partitionThese
+ */
+@ExperimentalKairosApi
+fun <A, B> Events<Either<A, B>>.partitionEither(): Pair<Events<A>, Events<B>> {
+ val (left, right) = partition { it is Either.First }
+ return Pair(
+ left.mapCheap { (it as Either.First).value },
+ right.mapCheap { (it as Either.Second).value },
+ )
+}
+
+/**
+ * Returns two new [Events] that contain elements from this [Events]; [Pair.first] will contain
+ * [These.first] values, and [Pair.second] will contain [These.second] values. If the original
+ * emission was a [These.both], then both result [Events] will emit a value simultaneously.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.partitionThese
+ */
+@ExperimentalKairosApi
+fun <A, B> Events<These<A, B>>.partitionThese(): Pair<Events<A>, Events<B>> {
+ val grouped =
+ mapCheap {
+ when (it) {
+ is These.Both -> mapOf(true to it, false to it)
+ is These.Second -> mapOf(false to it)
+ is These.First -> mapOf(true to it)
+ }
+ }
+ .groupByKey(numKeys = 2)
+ return Pair(
+ grouped.eventsForKey(true).mapCheap {
+ it.maybeFirst().orError { "unexpected missing value" }
+ },
+ grouped.eventsForKey(false).mapCheap {
+ it.maybeSecond().orError { "unexpected missing value" }
+ },
+ )
+}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Incremental.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Incremental.kt
index c95b9e83594f..d88ae3b81349 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Incremental.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Incremental.kt
@@ -21,27 +21,31 @@ import com.android.systemui.kairos.internal.IncrementalImpl
import com.android.systemui.kairos.internal.Init
import com.android.systemui.kairos.internal.InitScope
import com.android.systemui.kairos.internal.NoScope
-import com.android.systemui.kairos.internal.awaitValues
import com.android.systemui.kairos.internal.constIncremental
import com.android.systemui.kairos.internal.constInit
import com.android.systemui.kairos.internal.init
-import com.android.systemui.kairos.internal.mapImpl
import com.android.systemui.kairos.internal.mapValuesImpl
-import com.android.systemui.kairos.internal.store.ConcurrentHashMapK
-import com.android.systemui.kairos.internal.switchDeferredImpl
-import com.android.systemui.kairos.internal.switchPromptImpl
import com.android.systemui.kairos.internal.util.hashString
import com.android.systemui.kairos.util.MapPatch
-import com.android.systemui.kairos.util.map
import com.android.systemui.kairos.util.mapPatchFromFullDiff
import kotlin.reflect.KProperty
-/** A [State] tracking a [Map] that receives incremental updates. */
+/**
+ * A [State] tracking a [Map] that receives incremental updates.
+ *
+ * [Incremental] allows one to react to the [subset of changes][updates] to the held map, without
+ * having to perform a manual diff of the map to determine what changed.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.incrementals
+ */
sealed class Incremental<K, out V> : State<Map<K, V>>() {
abstract override val init: Init<IncrementalImpl<K, V>>
}
-/** An [Incremental] that never changes. */
+/**
+ * Returns a constant [Incremental] that never changes. [changes] and [updates] are both equivalent
+ * to [emptyEvents], and [TransactionScope.sample] will always produce [value].
+ */
@ExperimentalKairosApi
fun <K, V> incrementalOf(value: Map<K, V>): Incremental<K, V> {
val operatorName = "stateOf"
@@ -57,6 +61,10 @@ fun <K, V> incrementalOf(value: Map<K, V>): Incremental<K, V> {
* [value][Lazy.value] will be queried and used.
*
* Useful for recursive definitions.
+ *
+ * ``` kotlin
+ * fun <A> Lazy<Incremental<K, V>>.defer() = deferredIncremental { value }
+ * ```
*/
@ExperimentalKairosApi
fun <K, V> Lazy<Incremental<K, V>>.defer(): Incremental<K, V> = deferInline { value }
@@ -69,6 +77,10 @@ fun <K, V> Lazy<Incremental<K, V>>.defer(): Incremental<K, V> = deferInline { va
* queried and used.
*
* Useful for recursive definitions.
+ *
+ * ``` kotlin
+ * fun <A> DeferredValue<Incremental<K, V>>.defer() = deferredIncremental { get() }
+ * ```
*/
@ExperimentalKairosApi
fun <K, V> DeferredValue<Incremental<K, V>>.defer(): Incremental<K, V> = deferInline {
@@ -119,94 +131,14 @@ fun <K, V, U> Incremental<K, V>.mapValues(
}
/**
- * Returns an [Events] that emits from a merged, incrementally-accumulated collection of [Events]
- * emitted from this, following the same "patch" rules as outlined in
- * [StateScope.foldStateMapIncrementally].
- *
- * Conceptually this is equivalent to:
- * ```kotlin
- * fun <K, V> State<Map<K, V>>.mergeEventsIncrementally(): Events<Map<K, V>> =
- * map { it.merge() }.switchEvents()
- * ```
- *
- * While the behavior is equivalent to the conceptual definition above, the implementation is
- * significantly more efficient.
- *
- * @see merge
- */
-fun <K, V> Incremental<K, Events<V>>.mergeEventsIncrementally(): Events<Map<K, V>> {
- val operatorName = "mergeEventsIncrementally"
- val name = operatorName
- val patches =
- mapImpl({ init.connect(this).patches }) { patch, _ ->
- patch.mapValues { (_, m) -> m.map { events -> events.init.connect(this) } }.asIterable()
- }
- return EventsInit(
- constInit(
- name,
- switchDeferredImpl(
- name = name,
- getStorage = {
- init
- .connect(this)
- .getCurrentWithEpoch(this)
- .first
- .mapValues { (_, events) -> events.init.connect(this) }
- .asIterable()
- },
- getPatches = { patches },
- storeFactory = ConcurrentHashMapK.Factory(),
- )
- .awaitValues(),
- )
- )
-}
-
-/**
- * Returns an [Events] that emits from a merged, incrementally-accumulated collection of [Events]
- * emitted from this, following the same "patch" rules as outlined in
- * [StateScope.foldStateMapIncrementally].
+ * A forward-reference to an [Incremental]. Useful for recursive definitions.
*
- * Conceptually this is equivalent to:
- * ```kotlin
- * fun <K, V> State<Map<K, V>>.mergeEventsIncrementallyPromptly(): Events<Map<K, V>> =
- * map { it.merge() }.switchEventsPromptly()
- * ```
- *
- * While the behavior is equivalent to the conceptual definition above, the implementation is
- * significantly more efficient.
- *
- * @see merge
+ * This reference can be used like a standard [Incremental], but will throw an error if its
+ * [loopback] is unset before it is [observed][BuildScope.observe] or
+ * [sampled][TransactionScope.sample]. Note that it is safe to invoke
+ * [TransactionScope.sampleDeferred] before [loopback] is set, provided the [DeferredValue] is not
+ * [queried][KairosScope.get].
*/
-fun <K, V> Incremental<K, Events<V>>.mergeEventsIncrementallyPromptly(): Events<Map<K, V>> {
- val operatorName = "mergeEventsIncrementally"
- val name = operatorName
- val patches =
- mapImpl({ init.connect(this).patches }) { patch, _ ->
- patch.mapValues { (_, m) -> m.map { events -> events.init.connect(this) } }.asIterable()
- }
- return EventsInit(
- constInit(
- name,
- switchPromptImpl(
- name = name,
- getStorage = {
- init
- .connect(this)
- .getCurrentWithEpoch(this)
- .first
- .mapValues { (_, events) -> events.init.connect(this) }
- .asIterable()
- },
- getPatches = { patches },
- storeFactory = ConcurrentHashMapK.Factory(),
- )
- .awaitValues(),
- )
- )
-}
-
-/** A forward-reference to an [Incremental], allowing for recursive definitions. */
@ExperimentalKairosApi
class IncrementalLoop<K, V>(private val name: String? = null) : Incremental<K, V>() {
@@ -215,7 +147,10 @@ class IncrementalLoop<K, V>(private val name: String? = null) : Incremental<K, V
override val init: Init<IncrementalImpl<K, V>> =
init(name) { deferred.value.init.connect(evalScope = this) }
- /** The [Incremental] this [IncrementalLoop] will forward to. */
+ /**
+ * The [Incremental] this reference is referring to. Must be set before this [IncrementalLoop]
+ * is [observed][BuildScope.observe].
+ */
var loopback: Incremental<K, V>? = null
set(value) {
value?.let {
@@ -237,8 +172,8 @@ class IncrementalLoop<K, V>(private val name: String? = null) : Incremental<K, V
}
/**
- * Returns an [Incremental] whose [updates] are calculated by diffing the given [State]'s
- * [transitions].
+ * Returns an [Incremental] whose [updates] are calculated by [diffing][mapPatchFromFullDiff] the
+ * given [State]'s [transitions].
*/
fun <K, V> State<Map<K, V>>.asIncremental(): Incremental<K, V> {
if (this is Incremental<K, V>) return this
@@ -264,34 +199,6 @@ fun <K, V> State<Map<K, V>>.asIncremental(): Incremental<K, V> {
)
}
-/** Returns an [Incremental] that acts like the current value of the given [State]. */
-fun <K, V> State<Incremental<K, V>>.switchIncremental(): Incremental<K, V> {
- val stateChangePatches =
- transitions.mapNotNull { (old, new) ->
- mapPatchFromFullDiff(old.sample(), new.sample()).takeIf { it.isNotEmpty() }
- }
- val innerChanges =
- map { inner ->
- merge(stateChangePatches, inner.updates) { switchPatch, upcomingPatch ->
- switchPatch + upcomingPatch
- }
- }
- .switchEventsPromptly()
- val flattened = flatten()
- return IncrementalInit(
- init("switchIncremental") {
- val upstream = flattened.init.connect(this)
- IncrementalImpl(
- "switchIncremental",
- "switchIncremental",
- upstream.changes,
- innerChanges.init.connect(this),
- upstream.store,
- )
- }
- )
-}
-
private inline fun <K, V> deferInline(
crossinline block: InitScope.() -> Incremental<K, V>
): Incremental<K, V> = IncrementalInit(init(name = null) { block().init.connect(evalScope = this) })
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosNetwork.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosNetwork.kt
index 77598b30658a..19e3fcdb7b06 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosNetwork.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosNetwork.kt
@@ -23,16 +23,15 @@ import com.android.systemui.kairos.internal.util.awaitCancellationAndThen
import com.android.systemui.kairos.internal.util.childScope
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
+import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Job
import kotlinx.coroutines.job
import kotlinx.coroutines.launch
-/**
- * Marks declarations that are still **experimental** and shouldn't be used in general production
- * code.
- */
+/** Marks APIs that are still **experimental** and shouldn't be used in general production code. */
@RequiresOptIn(
message = "This API is experimental and should not be used in general production code."
)
@@ -139,37 +138,46 @@ internal class LocalNetwork(
private val endSignal: Events<Any>,
) : KairosNetwork {
override suspend fun <R> transact(block: TransactionScope.() -> R): R =
- network.transaction("KairosNetwork.transact") { block() }.await()
+ network.transaction("KairosNetwork.transact") { block() }.awaitOrCancel()
override suspend fun activateSpec(spec: BuildSpec<*>) {
- val stopEmitter =
- CoalescingMutableEvents(
- name = "activateSpec",
- coalesce = { _, _: Unit -> },
- network = network,
- getInitialValue = {},
- )
- val job =
- network
- .transaction("KairosNetwork.activateSpec") {
- val buildScope =
- BuildScopeImpl(
- stateScope =
- StateScopeImpl(
- evalScope = this,
- endSignal = mergeLeft(stopEmitter, endSignal),
- ),
- coroutineScope = scope,
- )
- buildScope.launchScope(spec)
+ val stopEmitter = conflatedMutableEvents<Unit>()
+ network
+ .transaction("KairosNetwork.activateSpec") {
+ val buildScope =
+ BuildScopeImpl(
+ stateScope =
+ StateScopeImpl(
+ evalScope = this,
+ endSignalLazy = lazy { mergeLeft(stopEmitter, endSignal) },
+ ),
+ coroutineScope = scope,
+ )
+ buildScope.launchScope {
+ spec.applySpec()
+ launchEffect { awaitCancellationAndThen { stopEmitter.emit(Unit) } }
}
- .await()
- awaitCancellationAndThen {
- stopEmitter.emit(Unit)
- job.cancel()
- }
+ }
+ .awaitOrCancel()
+ .joinOrCancel()
}
+ private suspend fun <T> Deferred<T>.awaitOrCancel(): T =
+ try {
+ await()
+ } catch (ex: CancellationException) {
+ cancel(ex)
+ throw ex
+ }
+
+ private suspend fun Job.joinOrCancel(): Unit =
+ try {
+ join()
+ } catch (ex: CancellationException) {
+ cancel(ex)
+ throw ex
+ }
+
override fun <In, Out> coalescingMutableEvents(
coalesce: (old: Out, new: In) -> Out,
getInitialValue: () -> Out,
@@ -214,3 +222,45 @@ fun CoroutineScope.launchKairosNetwork(
scope.launch(CoroutineName("launchKairosNetwork scheduler")) { network.runInputScheduler() }
return RootKairosNetwork(network, scope, scope.coroutineContext.job)
}
+
+@ExperimentalKairosApi
+interface HasNetwork : KairosScope {
+ /**
+ * A [KairosNetwork] handle that is bound to the lifetime of a [BuildScope].
+ *
+ * It supports all of the standard functionality by which external code can interact with this
+ * Kairos network, but all [activated][KairosNetwork.activateSpec] [BuildSpec]s are bound as
+ * children to the [BuildScope], such that when the [BuildScope] is destroyed, all children are
+ * also destroyed.
+ */
+ val kairosNetwork: KairosNetwork
+}
+
+/** Returns a [MutableEvents] that can emit values into this [KairosNetwork]. */
+@ExperimentalKairosApi
+fun <T> HasNetwork.MutableEvents(): MutableEvents<T> = MutableEvents(kairosNetwork)
+
+/** Returns a [MutableState] with initial state [initialValue]. */
+@ExperimentalKairosApi
+fun <T> HasNetwork.MutableState(initialValue: T): MutableState<T> =
+ MutableState(kairosNetwork, initialValue)
+
+/** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */
+@ExperimentalKairosApi
+fun <In, Out> HasNetwork.CoalescingMutableEvents(
+ coalesce: (old: Out, new: In) -> Out,
+ initialValue: Out,
+): CoalescingMutableEvents<In, Out> = CoalescingMutableEvents(kairosNetwork, coalesce, initialValue)
+
+/** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */
+@ExperimentalKairosApi
+fun <In, Out> HasNetwork.CoalescingMutableEvents(
+ coalesce: (old: Out, new: In) -> Out,
+ getInitialValue: () -> Out,
+): CoalescingMutableEvents<In, Out> =
+ CoalescingMutableEvents(kairosNetwork, coalesce, getInitialValue)
+
+/** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */
+@ExperimentalKairosApi
+fun <T> HasNetwork.ConflatedMutableEvents(): CoalescingMutableEvents<T, T> =
+ ConflatedMutableEvents(kairosNetwork)
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosScope.kt
index ce3e9235efa8..e526f4530645 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosScope.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosScope.kt
@@ -16,42 +16,11 @@
package com.android.systemui.kairos
-import com.android.systemui.kairos.internal.CompletableLazy
-
/** Denotes [KairosScope] interfaces as [DSL markers][DslMarker]. */
@DslMarker annotation class KairosScopeMarker
/**
- * Base scope for all Kairos scopes. Used to prevent implicitly capturing other scopes from in
+ * Base scope for all Kairos scopes. Used to prevent implicitly capturing other scopes from inner
* lambdas.
*/
-@KairosScopeMarker
-@ExperimentalKairosApi
-interface KairosScope {
- /** Returns the value held by the [DeferredValue], suspending until available if necessary. */
- fun <A> DeferredValue<A>.get(): A = unwrapped.value
-}
-
-/**
- * A value that may not be immediately (synchronously) available, but is guaranteed to be available
- * before this transaction is completed.
- *
- * @see KairosScope.get
- */
-@ExperimentalKairosApi
-class DeferredValue<out A> internal constructor(internal val unwrapped: Lazy<A>)
-
-/**
- * Returns the value held by this [DeferredValue], or throws [IllegalStateException] if it is not
- * yet available.
- *
- * This API is not meant for general usage within the Kairos network. It is made available mainly
- * for debugging and logging. You should always prefer [get][KairosScope.get] if possible.
- *
- * @see KairosScope.get
- */
-@ExperimentalKairosApi fun <A> DeferredValue<A>.getUnsafe(): A = unwrapped.value
-
-/** Returns an already-available [DeferredValue] containing [value]. */
-@ExperimentalKairosApi
-fun <A> deferredOf(value: A): DeferredValue<A> = DeferredValue(CompletableLazy(value))
+@KairosScopeMarker @ExperimentalKairosApi interface KairosScope
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Merge.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Merge.kt
new file mode 100644
index 000000000000..de9dca43b5d5
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Merge.kt
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.kairos
+
+import com.android.systemui.kairos.internal.awaitValues
+import com.android.systemui.kairos.internal.constInit
+import com.android.systemui.kairos.internal.mapImpl
+import com.android.systemui.kairos.internal.mergeNodes
+import com.android.systemui.kairos.internal.mergeNodesLeft
+import com.android.systemui.kairos.internal.store.ConcurrentHashMapK
+import com.android.systemui.kairos.internal.switchDeferredImpl
+import com.android.systemui.kairos.internal.switchPromptImpl
+import com.android.systemui.kairos.util.map
+
+/**
+ * Merges the given [Events] into a single [Events] that emits events from both.
+ *
+ * Because [Events] can only emit one value per transaction, the provided [transformCoincidence]
+ * function is used to combine coincident emissions to produce the result value to be emitted by the
+ * merged [Events].
+ *
+ * ``` kotlin
+ * fun <A> Events<A>.mergeWith(
+ * other: Events<A>,
+ * transformCoincidence: TransactionScope.(A, A) -> A = { a, _ -> a },
+ * ): Events<A> =
+ * listOf(this, other).merge().map { it.reduce(transformCoincidence) }
+ * ```
+ *
+ * @see merge
+ */
+@ExperimentalKairosApi
+fun <A> Events<A>.mergeWith(
+ other: Events<A>,
+ transformCoincidence: TransactionScope.(A, A) -> A = { a, _ -> a },
+): Events<A> {
+ val node =
+ mergeNodes(
+ getPulse = { init.connect(evalScope = this) },
+ getOther = { other.init.connect(evalScope = this) },
+ ) { a, b ->
+ transformCoincidence(a, b)
+ }
+ return EventsInit(constInit(name = null, node))
+}
+
+/**
+ * Merges the given [Events] into a single [Events] that emits events from all. All coincident
+ * emissions are collected into the emitted [List], preserving the input ordering.
+ *
+ * ``` kotlin
+ * fun <A> merge(vararg events: Events<A>): Events<List<A>> = events.asIterable().merge()
+ * ```
+ *
+ * @see mergeWith
+ * @see mergeLeft
+ */
+@ExperimentalKairosApi
+fun <A> merge(vararg events: Events<A>): Events<List<A>> = events.asIterable().merge()
+
+/**
+ * Merges the given [Events] into a single [Events] that emits events from all. In the case of
+ * coincident emissions, the emission from the left-most [Events] is emitted.
+ *
+ * ``` kotlin
+ * fun <A> mergeLeft(vararg events: Events<A>): Events<A> = events.asIterable().mergeLeft()
+ * ```
+ *
+ * @see merge
+ */
+@ExperimentalKairosApi
+fun <A> mergeLeft(vararg events: Events<A>): Events<A> = events.asIterable().mergeLeft()
+
+/**
+ * Merges the given [Events] into a single [Events] that emits events from all.
+ *
+ * Because [Events] can only emit one value per transaction, the provided [transformCoincidence]
+ * function is used to combine coincident emissions to produce the result value to be emitted by the
+ * merged [Events].
+ *
+ * ``` kotlin
+ * fun <A> merge(vararg events: Events<A>, transformCoincidence: (A, A) -> A): Events<A> =
+ * merge(*events).map { l -> l.reduce(transformCoincidence) }
+ * ```
+ */
+fun <A> merge(vararg events: Events<A>, transformCoincidence: (A, A) -> A): Events<A> =
+ merge(*events).map { l -> l.reduce(transformCoincidence) }
+
+/**
+ * Merges the given [Events] into a single [Events] that emits events from all. All coincident
+ * emissions are collected into the emitted [List], preserving the input ordering.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.merge
+ * @see mergeWith
+ * @see mergeLeft
+ */
+@ExperimentalKairosApi
+fun <A> Iterable<Events<A>>.merge(): Events<List<A>> =
+ EventsInit(constInit(name = null, mergeNodes { map { it.init.connect(evalScope = this) } }))
+
+/**
+ * Merges the given [Events] into a single [Events] that emits events from all. In the case of
+ * coincident emissions, the emission from the left-most [Events] is emitted.
+ *
+ * Semantically equivalent to the following definition:
+ * ``` kotlin
+ * fun <A> Iterable<Events<A>>.mergeLeft(): Events<A> =
+ * merge().mapCheap { it.first() }
+ * ```
+ *
+ * In reality, the implementation avoids allocating the intermediate list of all coincident
+ * emissions.
+ *
+ * @see merge
+ */
+@ExperimentalKairosApi
+fun <A> Iterable<Events<A>>.mergeLeft(): Events<A> =
+ EventsInit(constInit(name = null, mergeNodesLeft { map { it.init.connect(evalScope = this) } }))
+
+/**
+ * Creates a new [Events] that emits events from all given [Events]. All simultaneous emissions are
+ * collected into the emitted [List], preserving the input ordering.
+ *
+ * ``` kotlin
+ * fun <A> Sequence<Events<A>>.merge(): Events<List<A>> = asIterable().merge()
+ * ```
+ *
+ * @see mergeWith
+ */
+@ExperimentalKairosApi fun <A> Sequence<Events<A>>.merge(): Events<List<A>> = asIterable().merge()
+
+/**
+ * Creates a new [Events] that emits events from all given [Events]. All simultaneous emissions are
+ * collected into the emitted [Map], and are given the same key of the associated [Events] in the
+ * input [Map].
+ *
+ * ``` kotlin
+ * fun <K, A> Map<K, Events<A>>.merge(): Events<Map<K, A>> =
+ * asSequence()
+ * .map { (k, events) -> events.map { a -> k to a } }
+ * .toList()
+ * .merge()
+ * .map { it.toMap() }
+ * ```
+ *
+ * @see merge
+ */
+@ExperimentalKairosApi
+fun <K, A> Map<K, Events<A>>.merge(): Events<Map<K, A>> =
+ asSequence()
+ .map { (k, events) -> events.map { a -> k to a } }
+ .toList()
+ .merge()
+ .map { it.toMap() }
+
+/**
+ * Returns an [Events] that emits from a merged, incrementally-accumulated collection of [Events]
+ * emitted from this, following the patch rules outlined in
+ * [Map.applyPatch][com.android.systemui.kairos.util.applyPatch].
+ *
+ * Conceptually this is equivalent to:
+ * ``` kotlin
+ * fun <K, V> State<Map<K, V>>.mergeEventsIncrementally(): Events<Map<K, V>> =
+ * map { it.merge() }.switchEvents()
+ * ```
+ *
+ * While the behavior is equivalent to the conceptual definition above, the implementation is
+ * significantly more efficient.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.mergeEventsIncrementally
+ * @see merge
+ */
+fun <K, V> Incremental<K, Events<V>>.mergeEventsIncrementally(): Events<Map<K, V>> {
+ val operatorName = "mergeEventsIncrementally"
+ val name = operatorName
+ val patches =
+ mapImpl({ init.connect(this).patches }) { patch, _ ->
+ patch.mapValues { (_, m) -> m.map { events -> events.init.connect(this) } }.asIterable()
+ }
+ return EventsInit(
+ constInit(
+ name,
+ switchDeferredImpl(
+ name = name,
+ getStorage = {
+ init
+ .connect(this)
+ .getCurrentWithEpoch(this)
+ .first
+ .mapValues { (_, events) -> events.init.connect(this) }
+ .asIterable()
+ },
+ getPatches = { patches },
+ storeFactory = ConcurrentHashMapK.Factory(),
+ )
+ .awaitValues(),
+ )
+ )
+}
+
+/**
+ * Returns an [Events] that emits from a merged, incrementally-accumulated collection of [Events]
+ * emitted from this, following the patch rules outlined in
+ * [Map.applyPatch][com.android.systemui.kairos.util.applyPatch].
+ *
+ * Conceptually this is equivalent to:
+ * ``` kotlin
+ * fun <K, V> State<Map<K, V>>.mergeEventsIncrementallyPromptly(): Events<Map<K, V>> =
+ * map { it.merge() }.switchEventsPromptly()
+ * ```
+ *
+ * While the behavior is equivalent to the conceptual definition above, the implementation is
+ * significantly more efficient.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.mergeEventsIncrementallyPromptly
+ * @see merge
+ */
+fun <K, V> Incremental<K, Events<V>>.mergeEventsIncrementallyPromptly(): Events<Map<K, V>> {
+ val operatorName = "mergeEventsIncrementallyPromptly"
+ val name = operatorName
+ val patches =
+ mapImpl({ init.connect(this).patches }) { patch, _ ->
+ patch.mapValues { (_, m) -> m.map { events -> events.init.connect(this) } }.asIterable()
+ }
+ return EventsInit(
+ constInit(
+ name,
+ switchPromptImpl(
+ name = name,
+ getStorage = {
+ init
+ .connect(this)
+ .getCurrentWithEpoch(this)
+ .first
+ .mapValues { (_, events) -> events.init.connect(this) }
+ .asIterable()
+ },
+ getPatches = { patches },
+ storeFactory = ConcurrentHashMapK.Factory(),
+ )
+ .awaitValues(),
+ )
+ )
+}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Modes.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Modes.kt
new file mode 100644
index 000000000000..6c070a65d5a0
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Modes.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.kairos
+
+/**
+ * A modal Kairos sub-network.
+ *
+ * When [enabled][enableMode], all network modifications are applied immediately to the Kairos
+ * network. When the returned [Events] emits a [BuildMode], that mode is enabled and replaces this
+ * mode, undoing all modifications in the process (any registered [observers][BuildScope.observe]
+ * are unregistered, and any pending [side-effects][BuildScope.effect] are cancelled).
+ *
+ * Use [compiledBuildSpec] to compile and stand-up a mode graph.
+ *
+ * @see StatefulMode
+ */
+@ExperimentalKairosApi
+fun interface BuildMode<out A> {
+ /**
+ * Invoked when this mode is enabled. Returns a value and an [Events] that signals a switch to a
+ * new mode.
+ */
+ fun BuildScope.enableMode(): Pair<A, Events<BuildMode<A>>>
+}
+
+/**
+ * Returns a [BuildSpec] that, when [applied][BuildScope.applySpec], stands up a modal-transition
+ * graph starting with this [BuildMode], automatically switching to new modes as they are produced.
+ *
+ * @see BuildMode
+ */
+@ExperimentalKairosApi
+val <A> BuildMode<A>.compiledBuildSpec: BuildSpec<State<A>>
+ get() = buildSpec {
+ var modeChangeEvents by EventsLoop<BuildMode<A>>()
+ val activeMode: State<Pair<A, Events<BuildMode<A>>>> =
+ modeChangeEvents
+ .map { it.run { buildSpec { enableMode() } } }
+ .holdLatestSpec(buildSpec { enableMode() })
+ modeChangeEvents =
+ activeMode
+ .map { statefully { it.second.nextOnly() } }
+ .applyLatestStateful()
+ .switchEvents()
+ activeMode.map { it.first }
+ }
+
+/**
+ * A modal Kairos sub-network.
+ *
+ * When [enabled][enableMode], all state accumulation is immediately started. When the returned
+ * [Events] emits a [BuildMode], that mode is enabled and replaces this mode, stopping all state
+ * accumulation in the process.
+ *
+ * Use [compiledStateful] to compile and stand-up a mode graph.
+ *
+ * @see BuildMode
+ */
+@ExperimentalKairosApi
+fun interface StatefulMode<out A> {
+ /**
+ * Invoked when this mode is enabled. Returns a value and an [Events] that signals a switch to a
+ * new mode.
+ */
+ fun StateScope.enableMode(): Pair<A, Events<StatefulMode<A>>>
+}
+
+/**
+ * Returns a [Stateful] that, when [applied][StateScope.applyStateful], stands up a modal-transition
+ * graph starting with this [StatefulMode], automatically switching to new modes as they are
+ * produced.
+ *
+ * @see StatefulMode
+ */
+@ExperimentalKairosApi
+val <A> StatefulMode<A>.compiledStateful: Stateful<State<A>>
+ get() = statefully {
+ var modeChangeEvents by EventsLoop<StatefulMode<A>>()
+ val activeMode: State<Pair<A, Events<StatefulMode<A>>>> =
+ modeChangeEvents
+ .map { it.run { statefully { enableMode() } } }
+ .holdLatestStateful(statefully { enableMode() })
+ modeChangeEvents =
+ activeMode
+ .map { statefully { it.second.nextOnly() } }
+ .applyLatestStateful()
+ .switchEvents()
+ activeMode.map { it.first }
+ }
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Selector.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Selector.kt
new file mode 100644
index 000000000000..f7decbb66de8
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Selector.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.kairos
+
+import com.android.systemui.kairos.internal.DerivedMapCheap
+import com.android.systemui.kairos.internal.StateImpl
+import com.android.systemui.kairos.internal.init
+
+/**
+ * Returns a [StateSelector] that can be used to efficiently check if the input [State] is currently
+ * holding a specific value.
+ *
+ * An example:
+ * ```
+ * val intState: State<Int> = ...
+ * val intSelector: StateSelector<Int> = intState.selector()
+ * // Tracks if intState is holding 1
+ * val isOne: State<Boolean> = intSelector.whenSelected(1)
+ * ```
+ *
+ * This is semantically equivalent to `val isOne = intState.map { i -> i == 1 }`, but is
+ * significantly more efficient; specifically, using [State.map] in this way incurs a `O(n)`
+ * performance hit, where `n` is the number of different [State.map] operations used to track a
+ * specific value. [selector] internally uses a [HashMap] to lookup the appropriate downstream
+ * [State] to update, and so operates in `O(1)`.
+ *
+ * Note that the returned [StateSelector] should be cached and re-used to gain the performance
+ * benefit.
+ *
+ * @see groupByKey
+ */
+@ExperimentalKairosApi
+fun <A> State<A>.selector(numDistinctValues: Int? = null): StateSelector<A> =
+ StateSelector(
+ this,
+ changes
+ .map { new -> mapOf(new to true, sampleDeferred().value to false) }
+ .groupByKey(numDistinctValues),
+ )
+
+/**
+ * Tracks the currently selected value of type [A] from an upstream [State].
+ *
+ * @see selector
+ */
+@ExperimentalKairosApi
+class StateSelector<in A>
+internal constructor(
+ private val upstream: State<A>,
+ private val groupedChanges: GroupedEvents<A, Boolean>,
+) {
+ /**
+ * Returns a [State] that tracks whether the upstream [State] is currently holding the given
+ * [value].
+ *
+ * @see selector
+ */
+ fun whenSelected(value: A): State<Boolean> {
+ val operatorName = "StateSelector#whenSelected"
+ val name = "$operatorName[$value]"
+ return StateInit(
+ init(name) {
+ StateImpl(
+ name,
+ operatorName,
+ groupedChanges.impl.eventsForKey(value),
+ DerivedMapCheap(upstream.init) { it == value },
+ )
+ }
+ )
+ }
+
+ operator fun get(value: A): State<Boolean> = whenSelected(value)
+}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/State.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/State.kt
index 1f0a19d5752b..22ca83c6a15a 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/State.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/State.kt
@@ -17,7 +17,6 @@
package com.android.systemui.kairos
import com.android.systemui.kairos.internal.CompletableLazy
-import com.android.systemui.kairos.internal.DerivedMapCheap
import com.android.systemui.kairos.internal.EventsImpl
import com.android.systemui.kairos.internal.Init
import com.android.systemui.kairos.internal.InitScope
@@ -37,20 +36,31 @@ import com.android.systemui.kairos.internal.mapImpl
import com.android.systemui.kairos.internal.mapStateImpl
import com.android.systemui.kairos.internal.mapStateImplCheap
import com.android.systemui.kairos.internal.util.hashString
-import com.android.systemui.kairos.internal.zipStateMap
-import com.android.systemui.kairos.internal.zipStates
+import com.android.systemui.kairos.util.WithPrev
import kotlin.reflect.KProperty
/**
- * A time-varying value with discrete changes. Essentially, a combination of a [Transactional] that
- * holds a value, and an [Events] that emits when the value changes.
+ * A time-varying value with discrete changes. Conceptually, a combination of a [Transactional] that
+ * holds a value, and an [Events] that emits when the value [changes].
+ *
+ * [States][State] follow these rules:
+ * 1. In the same transaction that [changes] emits a new value, [sample] will continue to return the
+ * previous value.
+ * 2. Unless it is [constant][stateOf], [States][State] can only be created via [StateScope]
+ * operations, or derived from other existing [States][State] via [State.map], [combine], etc.
+ * 3. [States][State] can only be [sampled][TransactionScope.sample] within a [TransactionScope].
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.states
*/
@ExperimentalKairosApi
sealed class State<out A> {
internal abstract val init: Init<StateImpl<A>>
}
-/** A [State] that never changes. */
+/**
+ * Returns a constant [State] that never changes. [changes] is equivalent to [emptyEvents] and
+ * [TransactionScope.sample] will always produce [value].
+ */
@ExperimentalKairosApi
fun <A> stateOf(value: A): State<A> {
val operatorName = "stateOf"
@@ -65,6 +75,10 @@ fun <A> stateOf(value: A): State<A> {
* will be queried and used.
*
* Useful for recursive definitions.
+ *
+ * ``` kotlin
+ * fun <A> Lazy<State<A>>.defer() = deferredState { value }
+ * ```
*/
@ExperimentalKairosApi fun <A> Lazy<State<A>>.defer(): State<A> = deferInline { value }
@@ -76,6 +90,10 @@ fun <A> stateOf(value: A): State<A> {
* and used.
*
* Useful for recursive definitions.
+ *
+ * ``` kotlin
+ * fun <A> DeferredValue<State<A>>.defer() = deferredState { get() }
+ * ```
*/
@ExperimentalKairosApi
fun <A> DeferredValue<State<A>>.defer(): State<A> = deferInline { unwrapped.value }
@@ -94,6 +112,8 @@ fun <A> deferredState(block: KairosScope.() -> State<A>): State<A> = deferInline
/**
* Returns a [State] containing the results of applying [transform] to the value held by the
* original [State].
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.mapState
*/
@ExperimentalKairosApi
fun <A, B> State<A>.map(transform: KairosScope.(A) -> B): State<B> {
@@ -110,10 +130,12 @@ fun <A, B> State<A>.map(transform: KairosScope.(A) -> B): State<B> {
* Returns a [State] that transforms the value held inside this [State] by applying it to the
* [transform].
*
- * Note that unlike [map], the result is not cached. This means that not only should [transform] be
- * fast and pure, it should be *monomorphic* (1-to-1). Failure to do this means that [changes] for
- * the returned [State] will operate unexpectedly, emitting at rates that do not reflect an
- * observable change to the returned [State].
+ * Note that unlike [State.map], the result is not cached. This means that not only should
+ * [transform] be fast and pure, it should be *monomorphic* (1-to-1). Failure to do this means that
+ * [changes] for the returned [State] will operate unexpectedly, emitting at rates that do not
+ * reflect an observable change to the returned [State].
+ *
+ * @see State.map
*/
@ExperimentalKairosApi
fun <A, B> State<A>.mapCheapUnsafe(transform: KairosScope.(A) -> B): State<B> {
@@ -125,214 +147,30 @@ fun <A, B> State<A>.mapCheapUnsafe(transform: KairosScope.(A) -> B): State<B> {
}
/**
- * Returns a [State] by combining the values held inside the given [State]s by applying them to the
- * given function [transform].
- */
-@ExperimentalKairosApi
-fun <A, B, C> State<A>.combineWith(other: State<B>, transform: KairosScope.(A, B) -> C): State<C> =
- combine(this, other, transform)
-
-/**
* Splits a [State] of pairs into a pair of [Events][State], where each returned [State] holds half
* of the original.
*
- * Shorthand for:
- * ```kotlin
- * val lefts = map { it.first }
- * val rights = map { it.second }
- * return Pair(lefts, rights)
+ * ``` kotlin
+ * fun <A, B> State<Pair<A, B>>.unzip(): Pair<State<A>, State<B>> {
+ * val first = map { it.first }
+ * val second = map { it.second }
+ * return first to second
+ * }
* ```
*/
@ExperimentalKairosApi
fun <A, B> State<Pair<A, B>>.unzip(): Pair<State<A>, State<B>> {
- val left = map { it.first }
- val right = map { it.second }
- return left to right
-}
-
-/**
- * Returns a [State] by combining the values held inside the given [States][State] into a [List].
- *
- * @see State.combineWith
- */
-@ExperimentalKairosApi
-fun <A> Iterable<State<A>>.combine(): State<List<A>> {
- val operatorName = "combine"
- val name = operatorName
- return StateInit(
- init(name) {
- val states = map { it.init }
- zipStates(
- name,
- operatorName,
- states.size,
- states = init(null) { states.map { it.connect(this) } },
- )
- }
- )
-}
-
-/**
- * Returns a [State] by combining the values held inside the given [States][State] into a [Map].
- *
- * @see State.combineWith
- */
-@ExperimentalKairosApi
-fun <K, A> Map<K, State<A>>.combine(): State<Map<K, A>> {
- val operatorName = "combine"
- val name = operatorName
- return StateInit(
- init(name) {
- zipStateMap(
- name,
- operatorName,
- size,
- states = init(null) { mapValues { it.value.init.connect(evalScope = this) } },
- )
- }
- )
-}
-
-/**
- * Returns a [State] whose value is generated with [transform] by combining the current values of
- * each given [State].
- *
- * @see State.combineWith
- */
-@ExperimentalKairosApi
-fun <A, B> Iterable<State<A>>.combine(transform: KairosScope.(List<A>) -> B): State<B> =
- combine().map(transform)
-
-/**
- * Returns a [State] by combining the values held inside the given [State]s into a [List].
- *
- * @see State.combineWith
- */
-@ExperimentalKairosApi
-fun <A> combine(vararg states: State<A>): State<List<A>> = states.asIterable().combine()
-
-/**
- * Returns a [State] whose value is generated with [transform] by combining the current values of
- * each given [State].
- *
- * @see State.combineWith
- */
-@ExperimentalKairosApi
-fun <A, B> combine(vararg states: State<A>, transform: KairosScope.(List<A>) -> B): State<B> =
- states.asIterable().combine(transform)
-
-/**
- * Returns a [State] whose value is generated with [transform] by combining the current values of
- * each given [State].
- *
- * @see State.combineWith
- */
-@ExperimentalKairosApi
-fun <A, B, Z> combine(
- stateA: State<A>,
- stateB: State<B>,
- transform: KairosScope.(A, B) -> Z,
-): State<Z> {
- val operatorName = "combine"
- val name = operatorName
- return StateInit(
- init(name) {
- zipStates(name, operatorName, stateA.init, stateB.init) { a, b ->
- NoScope.transform(a, b)
- }
- }
- )
-}
-
-/**
- * Returns a [State] whose value is generated with [transform] by combining the current values of
- * each given [State].
- *
- * @see State.combineWith
- */
-@ExperimentalKairosApi
-fun <A, B, C, Z> combine(
- stateA: State<A>,
- stateB: State<B>,
- stateC: State<C>,
- transform: KairosScope.(A, B, C) -> Z,
-): State<Z> {
- val operatorName = "combine"
- val name = operatorName
- return StateInit(
- init(name) {
- zipStates(name, operatorName, stateA.init, stateB.init, stateC.init) { a, b, c ->
- NoScope.transform(a, b, c)
- }
- }
- )
-}
-
-/**
- * Returns a [State] whose value is generated with [transform] by combining the current values of
- * each given [State].
- *
- * @see State.combineWith
- */
-@ExperimentalKairosApi
-fun <A, B, C, D, Z> combine(
- stateA: State<A>,
- stateB: State<B>,
- stateC: State<C>,
- stateD: State<D>,
- transform: KairosScope.(A, B, C, D) -> Z,
-): State<Z> {
- val operatorName = "combine"
- val name = operatorName
- return StateInit(
- init(name) {
- zipStates(name, operatorName, stateA.init, stateB.init, stateC.init, stateD.init) {
- a,
- b,
- c,
- d ->
- NoScope.transform(a, b, c, d)
- }
- }
- )
+ val first = map { it.first }
+ val second = map { it.second }
+ return first to second
}
/**
- * Returns a [State] whose value is generated with [transform] by combining the current values of
- * each given [State].
+ * Returns a [State] by applying [transform] to the value held by the original [State].
*
- * @see State.combineWith
+ * @sample com.android.systemui.kairos.KairosSamples.flatMap
*/
@ExperimentalKairosApi
-fun <A, B, C, D, E, Z> combine(
- stateA: State<A>,
- stateB: State<B>,
- stateC: State<C>,
- stateD: State<D>,
- stateE: State<E>,
- transform: KairosScope.(A, B, C, D, E) -> Z,
-): State<Z> {
- val operatorName = "combine"
- val name = operatorName
- return StateInit(
- init(name) {
- zipStates(
- name,
- operatorName,
- stateA.init,
- stateB.init,
- stateC.init,
- stateD.init,
- stateE.init,
- ) { a, b, c, d, e ->
- NoScope.transform(a, b, c, d, e)
- }
- }
- )
-}
-
-/** Returns a [State] by applying [transform] to the value held by the original [State]. */
-@ExperimentalKairosApi
fun <A, B> State<A>.flatMap(transform: KairosScope.(A) -> State<B>): State<B> {
val operatorName = "flatMap"
val name = operatorName
@@ -345,75 +183,16 @@ fun <A, B> State<A>.flatMap(transform: KairosScope.(A) -> State<B>): State<B> {
)
}
-/** Shorthand for `flatMap { it }` */
-@ExperimentalKairosApi fun <A> State<State<A>>.flatten() = flatMap { it }
-
/**
- * Returns a [StateSelector] that can be used to efficiently check if the input [State] is currently
- * holding a specific value.
+ * Returns a [State] that behaves like the current value of the original [State].
*
- * An example:
- * ```
- * val intState: State<Int> = ...
- * val intSelector: StateSelector<Int> = intState.selector()
- * // Tracks if lInt is holding 1
- * val isOne: State<Boolean> = intSelector.whenSelected(1)
+ * ``` kotlin
+ * fun <A> State<State<A>>.flatten() = flatMap { it }
* ```
*
- * This is semantically equivalent to `val isOne = intState.map { i -> i == 1 }`, but is
- * significantly more efficient; specifically, using [State.map] in this way incurs a `O(n)`
- * performance hit, where `n` is the number of different [State.map] operations used to track a
- * specific value. [selector] internally uses a [HashMap] to lookup the appropriate downstream
- * [State] to update, and so operates in `O(1)`.
- *
- * Note that the returned [StateSelector] should be cached and re-used to gain the performance
- * benefit.
- *
- * @see groupByKey
+ * @see flatMap
*/
-@ExperimentalKairosApi
-fun <A> State<A>.selector(numDistinctValues: Int? = null): StateSelector<A> =
- StateSelector(
- this,
- changes
- .map { new -> mapOf(new to true, sampleDeferred().get() to false) }
- .groupByKey(numDistinctValues),
- )
-
-/**
- * Tracks the currently selected value of type [A] from an upstream [State].
- *
- * @see selector
- */
-@ExperimentalKairosApi
-class StateSelector<in A>
-internal constructor(
- private val upstream: State<A>,
- private val groupedChanges: GroupedEvents<A, Boolean>,
-) {
- /**
- * Returns a [State] that tracks whether the upstream [State] is currently holding the given
- * [value].
- *
- * @see selector
- */
- fun whenSelected(value: A): State<Boolean> {
- val operatorName = "StateSelector#whenSelected"
- val name = "$operatorName[$value]"
- return StateInit(
- init(name) {
- StateImpl(
- name,
- operatorName,
- groupedChanges.impl.eventsForKey(value),
- DerivedMapCheap(upstream.init) { it == value },
- )
- }
- )
- }
-
- operator fun get(value: A): State<Boolean> = whenSelected(value)
-}
+@ExperimentalKairosApi fun <A> State<State<A>>.flatten() = flatMap { it }
/**
* A mutable [State] that provides the ability to manually [set its value][setValue].
@@ -441,6 +220,9 @@ class MutableState<T> internal constructor(internal val network: Network, initia
override val init: Init<StateImpl<T>>
get() = state.init
+ // TODO: not convinced this is totally safe
+ // - at least for the BuildScope smart-constructor, we can avoid the network.transaction { }
+ // call since we're already in a transaction
internal val state = run {
val changes = input.impl
val name = null
@@ -491,7 +273,17 @@ class MutableState<T> internal constructor(internal val network: Network, initia
fun setValueDeferred(value: DeferredValue<T>) = input.emit(value.unwrapped)
}
-/** A forward-reference to a [State], allowing for recursive definitions. */
+/**
+ * A forward-reference to a [State]. Useful for recursive definitions.
+ *
+ * This reference can be used like a standard [State], but will throw an error if its [loopback] is
+ * unset before it is [observed][BuildScope.observe] or [sampled][TransactionScope.sample].
+ *
+ * Note that it is safe to invoke [TransactionScope.sampleDeferred] before [loopback] is set,
+ * provided the returned [DeferredValue] is not [queried][KairosScope.get].
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.stateLoop
+ */
@ExperimentalKairosApi
class StateLoop<A> : State<A>() {
@@ -502,7 +294,10 @@ class StateLoop<A> : State<A>() {
override val init: Init<StateImpl<A>> =
init(name) { deferred.value.init.connect(evalScope = this) }
- /** The [State] this [StateLoop] will forward to. */
+ /**
+ * The [State] this reference is referring to. Must be set before this [StateLoop] is
+ * [observed][BuildScope.observe] or [sampled][TransactionScope.sample].
+ */
var loopback: State<A>? = null
set(value) {
value?.let {
@@ -528,3 +323,24 @@ internal class StateInit<A> internal constructor(override val init: Init<StateIm
private inline fun <A> deferInline(crossinline block: InitScope.() -> State<A>): State<A> =
StateInit(init(name = null) { block().init.connect(evalScope = this) })
+
+/**
+ * Like [changes] but also includes the old value of this [State].
+ *
+ * Shorthand for:
+ * ``` kotlin
+ * stateChanges.map { WithPrev(previousValue = sample(), newValue = it) }
+ * ```
+ */
+@ExperimentalKairosApi
+val <A> State<A>.transitions: Events<WithPrev<A, A>>
+ get() = changes.map { WithPrev(previousValue = sample(), newValue = it) }
+
+/**
+ * Returns an [Events] that emits the new value of this [State] when it changes.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.changes
+ */
+@ExperimentalKairosApi
+val <A> State<A>.changes: Events<A>
+ get() = EventsInit(init(name = null) { init.connect(evalScope = this).changes })
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/StateScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/StateScope.kt
index 933ff1a75a02..faeffe84e2e8 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/StateScope.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/StateScope.kt
@@ -16,13 +16,11 @@
package com.android.systemui.kairos
+import com.android.systemui.kairos.util.MapPatch
import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.Maybe.Just
import com.android.systemui.kairos.util.WithPrev
-import com.android.systemui.kairos.util.just
import com.android.systemui.kairos.util.map
import com.android.systemui.kairos.util.mapMaybeValues
-import com.android.systemui.kairos.util.none
import com.android.systemui.kairos.util.zipWith
// TODO: caching story? should each Scope have a cache of applied Stateful instances?
@@ -64,6 +62,8 @@ interface StateScope : TransactionScope {
* Note that the value contained within the [State] is not updated until *after* all [Events]
* have been processed; this keeps the value of the [State] consistent during the entire Kairos
* transaction.
+ *
+ * @see holdState
*/
fun <A> Events<A>.holdStateDeferred(initialValue: DeferredValue<A>): State<A>
@@ -71,113 +71,128 @@ interface StateScope : TransactionScope {
* Returns a [State] holding a [Map] that is updated incrementally whenever this emits a value.
*
* The value emitted is used as a "patch" for the tracked [Map]; for each key [K] in the emitted
- * map, an associated value of [Just] will insert or replace the value in the tracked [Map], and
- * an associated value of [none] will remove the key from the tracked [Map].
+ * map, an associated value of [present][Maybe.present] will insert or replace the value in the
+ * tracked [Map], and an associated value of [absent][Maybe.absent] will remove the key from the
+ * tracked [Map].
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.incrementals
+ * @see MapPatch
*/
- fun <K, V> Events<Map<K, Maybe<V>>>.foldStateMapIncrementally(
+ fun <K, V> Events<MapPatch<K, V>>.foldStateMapIncrementally(
initialValues: DeferredValue<Map<K, V>>
): Incremental<K, V>
+ /**
+ * Returns an [Events] the emits the result of applying [Statefuls][Stateful] emitted from the
+ * original [Events].
+ *
+ * Unlike [applyLatestStateful], state accumulation is not stopped with each subsequent emission
+ * of the original [Events].
+ */
+ fun <A> Events<Stateful<A>>.applyStatefuls(): Events<A>
+
+ /**
+ * Returns an [Events] containing the results of applying each [Stateful] emitted from the
+ * original [Events], and a [DeferredValue] containing the result of applying [init]
+ * immediately.
+ *
+ * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent],
+ * then the previously-active [Stateful] will be stopped with no replacement.
+ *
+ * When each [Stateful] is applied, state accumulation from the previously-active [Stateful]
+ * with the same key is stopped.
+ *
+ * The optional [numKeys] argument is an optimization used to initialize the internal storage.
+ */
+ fun <K, A, B> Events<MapPatch<K, Stateful<A>>>.applyLatestStatefulForKey(
+ init: DeferredValue<Map<K, Stateful<B>>>,
+ numKeys: Int? = null,
+ ): Pair<Events<MapPatch<K, A>>, DeferredValue<Map<K, B>>>
+
// TODO: everything below this comment can be made into extensions once we have context params
/**
* Returns an [Events] that emits from a merged, incrementally-accumulated collection of
- * [Events] emitted from this, following the same "patch" rules as outlined in
- * [foldStateMapIncrementally].
+ * [Events] emitted from this, following the patch rules outlined in
+ * [Map.applyPatch][com.android.systemui.kairos.util.applyPatch].
*
- * Conceptually this is equivalent to:
- * ```kotlin
- * fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally(
- * initialEvents: Map<K, Events<V>>,
+ * ``` kotlin
+ * fun <K, V> Events<MapPatch<K, Events<V>>>.mergeEventsIncrementally(
+ * initialEvents: DeferredValue<Map<K, Events<V>>>,
* ): Events<Map<K, V>> =
- * foldMapIncrementally(initialEvents).map { it.merge() }.switchEvents()
+ * foldMapIncrementally(initialEvents).mergeEventsIncrementally(initialEvents)
* ```
*
- * While the behavior is equivalent to the conceptual definition above, the implementation is
- * significantly more efficient.
- *
+ * @see Incremental.mergeEventsIncrementally
* @see merge
*/
- fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally(
- name: String? = null,
- initialEvents: DeferredValue<Map<K, Events<V>>>,
+ fun <K, V> Events<MapPatch<K, Events<V>>>.mergeEventsIncrementally(
+ initialEvents: DeferredValue<Map<K, Events<V>>>
): Events<Map<K, V>> = foldStateMapIncrementally(initialEvents).mergeEventsIncrementally()
/**
* Returns an [Events] that emits from a merged, incrementally-accumulated collection of
- * [Events] emitted from this, following the same "patch" rules as outlined in
- * [foldStateMapIncrementally].
+ * [Events] emitted from this, following the patch rules outlined in
+ * [Map.applyPatch][com.android.systemui.kairos.util.applyPatch].
*
- * Conceptually this is equivalent to:
- * ```kotlin
- * fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementallyPromptly(
- * initialEvents: Map<K, Events<V>>,
+ * ``` kotlin
+ * fun <K, V> Events<MapPatch<K, Events<V>>>.mergeEventsIncrementallyPromptly(
+ * initialEvents: DeferredValue<Map<K, Events<V>>>,
* ): Events<Map<K, V>> =
- * foldMapIncrementally(initialEvents).map { it.merge() }.switchEventsPromptly()
+ * foldMapIncrementally(initialEvents).mergeEventsIncrementallyPromptly(initialEvents)
* ```
*
- * While the behavior is equivalent to the conceptual definition above, the implementation is
- * significantly more efficient.
- *
+ * @see Incremental.mergeEventsIncrementallyPromptly
* @see merge
*/
- fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementallyPromptly(
- initialEvents: DeferredValue<Map<K, Events<V>>>,
- name: String? = null,
+ fun <K, V> Events<MapPatch<K, Events<V>>>.mergeEventsIncrementallyPromptly(
+ initialEvents: DeferredValue<Map<K, Events<V>>>
): Events<Map<K, V>> =
foldStateMapIncrementally(initialEvents).mergeEventsIncrementallyPromptly()
/**
* Returns an [Events] that emits from a merged, incrementally-accumulated collection of
- * [Events] emitted from this, following the same "patch" rules as outlined in
- * [foldStateMapIncrementally].
+ * [Events] emitted from this, following the patch rules outlined in
+ * [Map.applyPatch][com.android.systemui.kairos.util.applyPatch].
*
- * Conceptually this is equivalent to:
- * ```kotlin
- * fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally(
+ * ``` kotlin
+ * fun <K, V> Events<MapPatch<K, Events<V>>>.mergeEventsIncrementally(
* initialEvents: Map<K, Events<V>>,
* ): Events<Map<K, V>> =
- * foldMapIncrementally(initialEvents).map { it.merge() }.switchEvents()
+ * foldMapIncrementally(initialEvents).mergeEventsIncrementally(initialEvents)
* ```
*
- * While the behavior is equivalent to the conceptual definition above, the implementation is
- * significantly more efficient.
- *
+ * @see Incremental.mergeEventsIncrementally
* @see merge
*/
- fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally(
- name: String? = null,
- initialEvents: Map<K, Events<V>> = emptyMap(),
- ): Events<Map<K, V>> = mergeIncrementally(name, deferredOf(initialEvents))
+ fun <K, V> Events<MapPatch<K, Events<V>>>.mergeEventsIncrementally(
+ initialEvents: Map<K, Events<V>> = emptyMap()
+ ): Events<Map<K, V>> = mergeEventsIncrementally(deferredOf(initialEvents))
/**
* Returns an [Events] that emits from a merged, incrementally-accumulated collection of
- * [Events] emitted from this, following the same "patch" rules as outlined in
- * [foldStateMapIncrementally].
+ * [Events] emitted from this, following the patch rules outlined in
+ * [Map.applyPatch][com.android.systemui.kairos.util.applyPatch].
*
- * Conceptually this is equivalent to:
- * ```kotlin
- * fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementallyPromptly(
+ * ``` kotlin
+ * fun <K, V> Events<MapPatch<K, Events<V>>>.mergeEventsIncrementallyPromptly(
* initialEvents: Map<K, Events<V>>,
* ): Events<Map<K, V>> =
- * foldMapIncrementally(initialEvents).map { it.merge() }.switchEventsPromptly()
+ * foldMapIncrementally(initialEvents).mergeEventsIncrementallyPromptly(initialEvents)
* ```
*
- * While the behavior is equivalent to the conceptual definition above, the implementation is
- * significantly more efficient.
- *
+ * @see Incremental.mergeEventsIncrementallyPromptly
* @see merge
*/
- fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementallyPromptly(
- initialEvents: Map<K, Events<V>> = emptyMap(),
- name: String? = null,
- ): Events<Map<K, V>> = mergeIncrementallyPromptly(deferredOf(initialEvents), name)
+ fun <K, V> Events<MapPatch<K, Events<V>>>.mergeEventsIncrementallyPromptly(
+ initialEvents: Map<K, Events<V>> = emptyMap()
+ ): Events<Map<K, V>> = mergeEventsIncrementallyPromptly(deferredOf(initialEvents))
/** Applies the [Stateful] within this [StateScope]. */
fun <A> Stateful<A>.applyStateful(): A = this()
/**
- * Applies the [Stateful] within this [StateScope], returning the result as an [DeferredValue].
+ * Applies the [Stateful] within this [StateScope], returning the result as a [DeferredValue].
*/
fun <A> Stateful<A>.applyStatefulDeferred(): DeferredValue<A> = deferredStateScope {
applyStateful()
@@ -190,42 +205,59 @@ interface StateScope : TransactionScope {
* Note that the value contained within the [State] is not updated until *after* all [Events]
* have been processed; this keeps the value of the [State] consistent during the entire Kairos
* transaction.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.holdState
+ * @see holdStateDeferred
*/
fun <A> Events<A>.holdState(initialValue: A): State<A> =
holdStateDeferred(deferredOf(initialValue))
/**
- * Returns an [Events] the emits the result of applying [Statefuls][Stateful] emitted from the
- * original [Events].
- *
- * Unlike [applyLatestStateful], state accumulation is not stopped with each subsequent emission
- * of the original [Events].
- */
- fun <A> Events<Stateful<A>>.applyStatefuls(): Events<A>
-
- /**
* Returns an [Events] containing the results of applying [transform] to each value of the
* original [Events].
*
* [transform] can perform state accumulation via its [StateScope] receiver. Unlike
* [mapLatestStateful], accumulation is not stopped with each subsequent emission of the
* original [Events].
+ *
+ * ``` kotlin
+ * fun <A, B> Events<A>.mapStateful(transform: StateScope.(A) -> B): Events<B> =
+ * map { statefully { transform(it) } }.applyStatefuls()
+ * ```
*/
fun <A, B> Events<A>.mapStateful(transform: StateScope.(A) -> B): Events<B> =
- map { statefully { transform(it) } }.applyStatefuls()
+ mapCheap { statefully { transform(it) } }.applyStatefuls()
/**
* Returns a [State] the holds the result of applying the [Stateful] held by the original
* [State].
*
* Unlike [applyLatestStateful], state accumulation is not stopped with each state change.
+ *
+ * ``` kotlin
+ * fun <A> State<Stateful<A>>.applyStatefuls(): State<A> =
+ * changes
+ * .applyStatefuls()
+ * .holdState(initialValue = sample().applyStateful())
+ * ```
*/
fun <A> State<Stateful<A>>.applyStatefuls(): State<A> =
changes
.applyStatefuls()
- .holdStateDeferred(initialValue = deferredStateScope { sampleDeferred().get()() })
+ .holdStateDeferred(
+ initialValue = deferredStateScope { sampleDeferred().value.applyStateful() }
+ )
- /** Returns an [Events] that switches to the [Events] emitted by the original [Events]. */
+ /**
+ * Returns an [Events] that acts like the most recent [Events] to be emitted from the original
+ * [Events].
+ *
+ * ``` kotlin
+ * fun <A> Events<Events<A>>.flatten() = holdState(emptyEvents).switchEvents()
+ * ```
+ *
+ * @see switchEvents
+ */
fun <A> Events<Events<A>>.flatten() = holdState(emptyEvents).switchEvents()
/**
@@ -234,9 +266,14 @@ interface StateScope : TransactionScope {
*
* [transform] can perform state accumulation via its [StateScope] receiver. With each
* invocation of [transform], state accumulation from previous invocation is stopped.
+ *
+ * ``` kotlin
+ * fun <A, B> Events<A>.mapLatestStateful(transform: StateScope.(A) -> B): Events<B> =
+ * map { statefully { transform(it) } }.applyLatestStateful()
+ * ```
*/
fun <A, B> Events<A>.mapLatestStateful(transform: StateScope.(A) -> B): Events<B> =
- map { statefully { transform(it) } }.applyLatestStateful()
+ mapCheap { statefully { transform(it) } }.applyLatestStateful()
/**
* Returns an [Events] that switches to a new [Events] produced by [transform] every time the
@@ -244,6 +281,13 @@ interface StateScope : TransactionScope {
*
* [transform] can perform state accumulation via its [StateScope] receiver. With each
* invocation of [transform], state accumulation from previous invocation is stopped.
+ *
+ * ``` kotlin
+ * fun <A, B> Events<A>.flatMapLatestStateful(
+ * transform: StateScope.(A) -> Events<B>
+ * ): Events<B> =
+ * mapLatestStateful(transform).flatten()
+ * ```
*/
fun <A, B> Events<A>.flatMapLatestStateful(transform: StateScope.(A) -> Events<B>): Events<B> =
mapLatestStateful(transform).flatten()
@@ -254,6 +298,8 @@ interface StateScope : TransactionScope {
*
* When each [Stateful] is applied, state accumulation from the previously-active [Stateful] is
* stopped.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.applyLatestStateful
*/
fun <A> Events<Stateful<A>>.applyLatestStateful(): Events<A> = applyLatestStateful {}.first
@@ -281,19 +327,18 @@ interface StateScope : TransactionScope {
init: Stateful<A>
): Pair<Events<B>, DeferredValue<A>> {
val (events, result) =
- mapCheap { spec -> mapOf(Unit to just(spec)) }
+ mapCheap { spec -> mapOf(Unit to Maybe.present(spec)) }
.applyLatestStatefulForKey(init = mapOf(Unit to init), numKeys = 1)
val outEvents: Events<B> =
events.mapMaybe {
checkNotNull(it[Unit]) { "applyLatest: expected result, but none present in: $it" }
}
val outInit: DeferredValue<A> = deferredTransactionScope {
- val initResult: Map<Unit, A> = result.get()
+ val initResult: Map<Unit, A> = result.value
check(Unit in initResult) {
"applyLatest: expected initial result, but none present in: $initResult"
}
- @Suppress("UNCHECKED_CAST")
- initResult.getOrDefault(Unit) { null } as A
+ initResult.getValue(Unit)
}
return Pair(outEvents, outInit)
}
@@ -303,34 +348,32 @@ interface StateScope : TransactionScope {
* original [Events], and a [DeferredValue] containing the result of applying [init]
* immediately.
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
- * previously-active [Stateful] will be stopped with no replacement.
- *
* When each [Stateful] is applied, state accumulation from the previously-active [Stateful]
* with the same key is stopped.
+ *
+ * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent],
+ * then the previously-active [Stateful] will be stopped with no replacement.
+ *
+ * The optional [numKeys] argument is an optimization used to initialize the internal storage.
*/
- fun <K, A, B> Events<Map<K, Maybe<Stateful<A>>>>.applyLatestStatefulForKey(
- init: DeferredValue<Map<K, Stateful<B>>>,
+ fun <K, A, B> Events<MapPatch<K, Stateful<A>>>.applyLatestStatefulForKey(
+ init: Map<K, Stateful<B>>,
numKeys: Int? = null,
- ): Pair<Events<Map<K, Maybe<A>>>, DeferredValue<Map<K, B>>>
+ ): Pair<Events<MapPatch<K, A>>, DeferredValue<Map<K, B>>> =
+ applyLatestStatefulForKey(deferredOf(init), numKeys)
/**
- * Returns an [Events] containing the results of applying each [Stateful] emitted from the
- * original [Events], and a [DeferredValue] containing the result of applying [init]
- * immediately.
+ * Returns an [Incremental] containing the latest results of applying each [Stateful] emitted
+ * from the original [Incremental]'s [updates].
*
* When each [Stateful] is applied, state accumulation from the previously-active [Stateful]
* with the same key is stopped.
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
- * previously-active [Stateful] will be stopped with no replacement.
+ * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent],
+ * then the previously-active [Stateful] will be stopped with no replacement.
+ *
+ * The optional [numKeys] argument is an optimization used to initialize the internal storage.
*/
- fun <K, A, B> Events<Map<K, Maybe<Stateful<A>>>>.applyLatestStatefulForKey(
- init: Map<K, Stateful<B>>,
- numKeys: Int? = null,
- ): Pair<Events<Map<K, Maybe<A>>>, DeferredValue<Map<K, B>>> =
- applyLatestStatefulForKey(deferredOf(init), numKeys)
-
fun <K, V> Incremental<K, Stateful<V>>.applyLatestStatefulForKey(
numKeys: Int? = null
): Incremental<K, V> {
@@ -345,10 +388,12 @@ interface StateScope : TransactionScope {
* When each [Stateful] is applied, state accumulation from the previously-active [Stateful]
* with the same key is stopped.
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
- * previously-active [Stateful] will be stopped with no replacement.
+ * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent],
+ * then the previously-active [Stateful] will be stopped with no replacement.
+ *
+ * The optional [numKeys] argument is an optimization used to initialize the internal storage.
*/
- fun <K, A> Events<Map<K, Maybe<Stateful<A>>>>.holdLatestStatefulForKey(
+ fun <K, A> Events<MapPatch<K, Stateful<A>>>.holdLatestStatefulForKey(
init: DeferredValue<Map<K, Stateful<A>>>,
numKeys: Int? = null,
): Incremental<K, A> {
@@ -363,28 +408,33 @@ interface StateScope : TransactionScope {
* When each [Stateful] is applied, state accumulation from the previously-active [Stateful]
* with the same key is stopped.
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
- * previously-active [Stateful] will be stopped with no replacement.
+ * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent],
+ * then the previously-active [Stateful] will be stopped with no replacement.
+ *
+ * The optional [numKeys] argument is an optimization used to initialize the internal storage.
*/
- fun <K, A> Events<Map<K, Maybe<Stateful<A>>>>.holdLatestStatefulForKey(
+ fun <K, A> Events<MapPatch<K, Stateful<A>>>.holdLatestStatefulForKey(
init: Map<K, Stateful<A>> = emptyMap(),
numKeys: Int? = null,
): Incremental<K, A> = holdLatestStatefulForKey(deferredOf(init), numKeys)
/**
* Returns an [Events] containing the results of applying each [Stateful] emitted from the
- * original [Events], and a [DeferredValue] containing the result of applying [stateInit]
- * immediately.
+ * original [Events].
*
* When each [Stateful] is applied, state accumulation from the previously-active [Stateful]
* with the same key is stopped.
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
- * previously-active [Stateful] will be stopped with no replacement.
+ * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent],
+ * then the previously-active [Stateful] will be stopped with no replacement.
+ *
+ * The optional [numKeys] argument is an optimization used to initialize the internal storage.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.applyLatestStatefulForKey
*/
- fun <K, A> Events<Map<K, Maybe<Stateful<A>>>>.applyLatestStatefulForKey(
+ fun <K, A> Events<MapPatch<K, Stateful<A>>>.applyLatestStatefulForKey(
numKeys: Int? = null
- ): Events<Map<K, Maybe<A>>> =
+ ): Events<MapPatch<K, A>> =
applyLatestStatefulForKey(init = emptyMap<K, Stateful<*>>(), numKeys = numKeys).first
/**
@@ -395,18 +445,20 @@ interface StateScope : TransactionScope {
* [transform] can perform state accumulation via its [StateScope] receiver. With each
* invocation of [transform], state accumulation from previous invocation is stopped.
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
- * previously-active [StateScope] will be stopped with no replacement.
+ * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent],
+ * then the previously-active [StateScope] will be stopped with no replacement.
+ *
+ * The optional [numKeys] argument is an optimization used to initialize the internal storage.
*/
- fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestStatefulForKey(
+ fun <K, A, B> Events<MapPatch<K, A>>.mapLatestStatefulForKey(
initialValues: DeferredValue<Map<K, A>>,
numKeys: Int? = null,
transform: StateScope.(A) -> B,
- ): Pair<Events<Map<K, Maybe<B>>>, DeferredValue<Map<K, B>>> =
+ ): Pair<Events<MapPatch<K, B>>, DeferredValue<Map<K, B>>> =
map { patch -> patch.mapValues { (_, v) -> v.map { statefully { transform(it) } } } }
.applyLatestStatefulForKey(
deferredStateScope {
- initialValues.get().mapValues { (_, v) -> statefully { transform(v) } }
+ initialValues.value.mapValues { (_, v) -> statefully { transform(v) } }
},
numKeys = numKeys,
)
@@ -419,14 +471,16 @@ interface StateScope : TransactionScope {
* [transform] can perform state accumulation via its [StateScope] receiver. With each
* invocation of [transform], state accumulation from previous invocation is stopped.
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
- * previously-active [StateScope] will be stopped with no replacement.
+ * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent],
+ * then the previously-active [StateScope] will be stopped with no replacement.
+ *
+ * The optional [numKeys] argument is an optimization used to initialize the internal storage.
*/
- fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestStatefulForKey(
+ fun <K, A, B> Events<MapPatch<K, A>>.mapLatestStatefulForKey(
initialValues: Map<K, A>,
numKeys: Int? = null,
transform: StateScope.(A) -> B,
- ): Pair<Events<Map<K, Maybe<B>>>, DeferredValue<Map<K, B>>> =
+ ): Pair<Events<MapPatch<K, B>>, DeferredValue<Map<K, B>>> =
mapLatestStatefulForKey(deferredOf(initialValues), numKeys, transform)
/**
@@ -436,13 +490,24 @@ interface StateScope : TransactionScope {
* [transform] can perform state accumulation via its [StateScope] receiver. With each
* invocation of [transform], state accumulation from previous invocation is stopped.
*
- * If the [Maybe] contained within the value for an associated key is [none], then the
- * previously-active [StateScope] will be stopped with no replacement.
+ * If the [Maybe] contained within the value for an associated key is [absent][Maybe.absent],
+ * then the previously-active [StateScope] will be stopped with no replacement.
+ *
+ * The optional [numKeys] argument is an optimization used to initialize the internal storage.
+ *
+ * ``` kotlin
+ * fun <K, A, B> Events<MapPatch<K, A>>.mapLatestStatefulForKey(
+ * numKeys: Int? = null,
+ * transform: StateScope.(A) -> B,
+ * ): Pair<Events<MapPatch<K, B>>, DeferredValue<Map<K, B>>> =
+ * map { patch -> patch.mapValues { (_, mv) -> mv.map { statefully { transform(it) } } } }
+ * .applyLatestStatefulForKey(numKeys)
+ * ```
*/
- fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestStatefulForKey(
+ fun <K, A, B> Events<MapPatch<K, A>>.mapLatestStatefulForKey(
numKeys: Int? = null,
transform: StateScope.(A) -> B,
- ): Events<Map<K, Maybe<B>>> = mapLatestStatefulForKey(emptyMap(), numKeys, transform).first
+ ): Events<MapPatch<K, B>> = mapLatestStatefulForKey(emptyMap(), numKeys, transform).first
/**
* Returns an [Events] that will only emit the next event of the original [Events], and then
@@ -450,18 +515,31 @@ interface StateScope : TransactionScope {
*
* If the original [Events] is emitting an event at this exact time, then it will be the only
* even emitted from the result [Events].
+ *
+ * ``` kotlin
+ * fun <A> Events<A>.nextOnly(): Events<A> =
+ * EventsLoop<A>().apply {
+ * loopback = map { emptyEvents }.holdState(this@nextOnly).switchEvents()
+ * }
+ * ```
*/
- fun <A> Events<A>.nextOnly(name: String? = null): Events<A> =
+ fun <A> Events<A>.nextOnly(): Events<A> =
if (this === emptyEvents) {
this
} else {
- EventsLoop<A>().also {
- it.loopback =
- it.mapCheap { emptyEvents }.holdState(this@nextOnly).switchEvents(name)
+ EventsLoop<A>().apply {
+ loopback = mapCheap { emptyEvents }.holdState(this@nextOnly).switchEvents()
}
}
- /** Returns an [Events] that skips the next emission of the original [Events]. */
+ /**
+ * Returns an [Events] that skips the next emission of the original [Events].
+ *
+ * ``` kotlin
+ * fun <A> Events<A>.skipNext(): Events<A> =
+ * nextOnly().map { this@skipNext }.holdState(emptyEvents).switchEvents()
+ * ```
+ */
fun <A> Events<A>.skipNext(): Events<A> =
if (this === emptyEvents) {
this
@@ -475,6 +553,11 @@ interface StateScope : TransactionScope {
*
* If the original [Events] emits at the same time as [stop], then the returned [Events] will
* emit that value.
+ *
+ * ``` kotlin
+ * fun <A> Events<A>.takeUntil(stop: Events<*>): Events<A> =
+ * stop.map { emptyEvents }.nextOnly().holdState(this).switchEvents()
+ * ```
*/
fun <A> Events<A>.takeUntil(stop: Events<*>): Events<A> =
if (stop === emptyEvents) {
@@ -494,14 +577,19 @@ interface StateScope : TransactionScope {
val (_, init: DeferredValue<Map<Unit, A>>) =
stop
.nextOnly()
- .map { mapOf(Unit to none<Stateful<A>>()) }
+ .map { mapOf(Unit to Maybe.absent<Stateful<A>>()) }
.applyLatestStatefulForKey(init = mapOf(Unit to stateful), numKeys = 1)
- return deferredStateScope { init.get().getValue(Unit) }
+ return deferredStateScope { init.value.getValue(Unit) }
}
/**
* Returns an [Events] that emits values from the original [Events] up to and including a value
* is emitted that satisfies [predicate].
+ *
+ * ``` kotlin
+ * fun <A> Events<A>.takeUntil(predicate: TransactionScope.(A) -> Boolean): Events<A> =
+ * takeUntil(filter(predicate))
+ * ```
*/
fun <A> Events<A>.takeUntil(predicate: TransactionScope.(A) -> Boolean): Events<A> =
takeUntil(filter(predicate))
@@ -513,6 +601,18 @@ interface StateScope : TransactionScope {
* Note that the value contained within the [State] is not updated until *after* all [Events]
* have been processed; this keeps the value of the [State] consistent during the entire Kairos
* transaction.
+ *
+ * ``` kotlin
+ * fun <A, B> Events<A>.foldState(
+ * initialValue: B,
+ * transform: TransactionScope.(A, B) -> B,
+ * ): State<B> {
+ * lateinit var state: State<B>
+ * return map { a -> transform(a, state.sample()) }
+ * .holdState(initialValue)
+ * .also { state = it }
+ * }
+ * ```
*/
fun <A, B> Events<A>.foldState(
initialValue: B,
@@ -529,6 +629,18 @@ interface StateScope : TransactionScope {
* Note that the value contained within the [State] is not updated until *after* all [Events]
* have been processed; this keeps the value of the [State] consistent during the entire Kairos
* transaction.
+ *
+ * ``` kotlin
+ * fun <A, B> Events<A>.foldStateDeferred(
+ * initialValue: DeferredValue<B>,
+ * transform: TransactionScope.(A, B) -> B,
+ * ): State<B> {
+ * lateinit var state: State<B>
+ * return map { a -> transform(a, state.sample()) }
+ * .holdStateDeferred(initialValue)
+ * .also { state = it }
+ * }
+ * ```
*/
fun <A, B> Events<A>.foldStateDeferred(
initialValue: DeferredValue<B>,
@@ -551,10 +663,11 @@ interface StateScope : TransactionScope {
* have been processed; this keeps the value of the [State] consistent during the entire Kairos
* transaction.
*
- * Shorthand for:
- * ```kotlin
- * val (changes, initApplied) = applyLatestStateful(init)
- * return changes.holdStateDeferred(initApplied)
+ * ``` kotlin
+ * fun <A> Events<Stateful<A>>.holdLatestStateful(init: Stateful<A>): State<A> {
+ * val (changes, initApplied) = applyLatestStateful(init)
+ * return changes.holdStateDeferred(initApplied)
+ * }
* ```
*/
fun <A> Events<Stateful<A>>.holdLatestStateful(init: Stateful<A>): State<A> {
@@ -578,8 +691,8 @@ interface StateScope : TransactionScope {
* that the returned [Events] will not emit until the original [Events] has emitted twice.
*/
fun <A> Events<A>.pairwise(): Events<WithPrev<A, A>> =
- mapCheap { just(it) }
- .pairwise(none)
+ mapCheap { Maybe.present(it) }
+ .pairwise(Maybe.absent)
.mapMaybe { (prev, next) -> prev.zipWith(next, ::WithPrev) }
/**
@@ -599,10 +712,11 @@ interface StateScope : TransactionScope {
* Returns a [State] holding a [Map] that is updated incrementally whenever this emits a value.
*
* The value emitted is used as a "patch" for the tracked [Map]; for each key [K] in the emitted
- * map, an associated value of [Just] will insert or replace the value in the tracked [Map], and
- * an associated value of [none] will remove the key from the tracked [Map].
+ * map, an associated value of [Maybe.present] will insert or replace the value in the tracked
+ * [Map], and an associated value of [absent][Maybe.absent] will remove the key from the tracked
+ * [Map].
*/
- fun <K, V> Events<Map<K, Maybe<V>>>.foldStateMapIncrementally(
+ fun <K, V> Events<MapPatch<K, V>>.foldStateMapIncrementally(
initialValues: Map<K, V> = emptyMap()
): Incremental<K, V> = foldStateMapIncrementally(deferredOf(initialValues))
@@ -610,10 +724,11 @@ interface StateScope : TransactionScope {
* Returns an [Events] that wraps each emission of the original [Events] into an [IndexedValue],
* containing the emitted value and its index (starting from zero).
*
- * Shorthand for:
- * ```
- * val index = fold(0) { _, oldIdx -> oldIdx + 1 }
- * sample(index) { a, idx -> IndexedValue(idx, a) }
+ * ``` kotlin
+ * fun <A> Events<A>.withIndex(): Events<IndexedValue<A>> {
+ * val index = fold(0) { _, oldIdx -> oldIdx + 1 }
+ * return sample(index) { a, idx -> IndexedValue(idx, a) }
+ * }
* ```
*/
fun <A> Events<A>.withIndex(): Events<IndexedValue<A>> {
@@ -625,9 +740,11 @@ interface StateScope : TransactionScope {
* Returns an [Events] containing the results of applying [transform] to each value of the
* original [Events] and its index (starting from zero).
*
- * Shorthand for:
- * ```
- * withIndex().map { (idx, a) -> transform(idx, a) }
+ * ``` kotlin
+ * fun <A> Events<A>.mapIndexed(transform: TransactionScope.(Int, A) -> B): Events<B> {
+ * val index = foldState(0) { _, i -> i + 1 }
+ * return sample(index) { a, idx -> transform(idx, a) }
+ * }
* ```
*/
fun <A, B> Events<A>.mapIndexed(transform: TransactionScope.(Int, A) -> B): Events<B> {
@@ -635,7 +752,16 @@ interface StateScope : TransactionScope {
return sample(index) { a, idx -> transform(idx, a) }
}
- /** Returns an [Events] where all subsequent repetitions of the same value are filtered out. */
+ /**
+ * Returns an [Events] where all subsequent repetitions of the same value are filtered out.
+ *
+ * ``` kotlin
+ * fun <A> Events<A>.distinctUntilChanged(): Events<A> {
+ * val state: State<Any?> = holdState(Any())
+ * return filter { it != state.sample() }
+ * }
+ * ```
+ */
fun <A> Events<A>.distinctUntilChanged(): Events<A> {
val state: State<Any?> = holdState(Any())
return filter { it != state.sample() }
@@ -647,18 +773,35 @@ interface StateScope : TransactionScope {
*
* Note that the returned [Events] will not emit anything until [other] has emitted at least one
* value.
+ *
+ * ``` kotlin
+ * fun <A, B, C> Events<A>.sample(
+ * other: Events<B>,
+ * transform: TransactionScope.(A, B) -> C,
+ * ): Events<C> {
+ * val state = other.mapCheap { Maybe.present(it) }.holdState(Maybe.absent)
+ * return sample(state) { a, b -> b.map { transform(a, it) } }.filterPresent()
+ * }
+ * ```
*/
fun <A, B, C> Events<A>.sample(
other: Events<B>,
transform: TransactionScope.(A, B) -> C,
): Events<C> {
- val state = other.mapCheap { just(it) }.holdState(none)
- return sample(state) { a, b -> b.map { transform(a, it) } }.filterJust()
+ val state = other.mapCheap { Maybe.present(it) }.holdState(Maybe.absent)
+ return sample(state) { a, b -> b.map { transform(a, it) } }.filterPresent()
}
/**
* Returns a [State] that samples the [Transactional] held by the given [State] within the same
* transaction that the state changes.
+ *
+ * ``` kotlin
+ * fun <A> State<Transactional<A>>.sampleTransactionals(): State<A> =
+ * changes
+ * .sampleTransactionals()
+ * .holdStateDeferred(deferredTransactionScope { sample().sample() })
+ * ```
*/
fun <A> State<Transactional<A>>.sampleTransactionals(): State<A> =
changes
@@ -668,6 +811,14 @@ interface StateScope : TransactionScope {
/**
* Returns a [State] that transforms the value held inside this [State] by applying it to the
* given function [transform].
+ *
+ * Note that this is less efficient than [State.map], which should be preferred if [transform]
+ * does not need access to [TransactionScope].
+ *
+ * ``` kotlin
+ * fun <A, B> State<A>.mapTransactionally(transform: TransactionScope.(A) -> B): State<B> =
+ * map { transactionally { transform(it) } }.sampleTransactionals()
+ * ```
*/
fun <A, B> State<A>.mapTransactionally(transform: TransactionScope.(A) -> B): State<B> =
map { transactionally { transform(it) } }.sampleTransactionals()
@@ -676,7 +827,20 @@ interface StateScope : TransactionScope {
* Returns a [State] whose value is generated with [transform] by combining the current values
* of each given [State].
*
- * @see State.combineWithTransactionally
+ * Note that this is less efficient than [combine], which should be preferred if [transform]
+ * does not need access to [TransactionScope].
+ *
+ * ``` kotlin
+ * fun <A, B, Z> combineTransactionally(
+ * stateA: State<A>,
+ * stateB: State<B>,
+ * transform: TransactionScope.(A, B) -> Z,
+ * ): State<Z> =
+ * combine(stateA, stateB) { a, b -> transactionally { transform(a, b) } }
+ * .sampleTransactionals()
+ * ```
+ *
+ * @see State.combineTransactionally
*/
fun <A, B, Z> combineTransactionally(
stateA: State<A>,
@@ -690,7 +854,10 @@ interface StateScope : TransactionScope {
* Returns a [State] whose value is generated with [transform] by combining the current values
* of each given [State].
*
- * @see State.combineWithTransactionally
+ * Note that this is less efficient than [combine], which should be preferred if [transform]
+ * does not need access to [TransactionScope].
+ *
+ * @see State.combineTransactionally
*/
fun <A, B, C, Z> combineTransactionally(
stateA: State<A>,
@@ -705,7 +872,10 @@ interface StateScope : TransactionScope {
* Returns a [State] whose value is generated with [transform] by combining the current values
* of each given [State].
*
- * @see State.combineWithTransactionally
+ * Note that this is less efficient than [combine], which should be preferred if [transform]
+ * does not need access to [TransactionScope].
+ *
+ * @see State.combineTransactionally
*/
fun <A, B, C, D, Z> combineTransactionally(
stateA: State<A>,
@@ -719,7 +889,18 @@ interface StateScope : TransactionScope {
}
.sampleTransactionals()
- /** Returns a [State] by applying [transform] to the value held by the original [State]. */
+ /**
+ * Returns a [State] by applying [transform] to the value held by the original [State].
+ *
+ * Note that this is less efficient than [flatMap], which should be preferred if [transform]
+ * does not need access to [TransactionScope].
+ *
+ * ``` kotlin
+ * fun <A, B> State<A>.flatMapTransactionally(
+ * transform: TransactionScope.(A) -> State<B>
+ * ): State<B> = map { transactionally { transform(it) } }.sampleTransactionals().flatten()
+ * ```
+ */
fun <A, B> State<A>.flatMapTransactionally(
transform: TransactionScope.(A) -> State<B>
): State<B> = map { transactionally { transform(it) } }.sampleTransactionals().flatten()
@@ -728,7 +909,10 @@ interface StateScope : TransactionScope {
* Returns a [State] whose value is generated with [transform] by combining the current values
* of each given [State].
*
- * @see State.combineWithTransactionally
+ * Note that this is less efficient than [combine], which should be preferred if [transform]
+ * does not need access to [TransactionScope].
+ *
+ * @see State.combineTransactionally
*/
fun <A, Z> combineTransactionally(
vararg states: State<A>,
@@ -739,7 +923,10 @@ interface StateScope : TransactionScope {
* Returns a [State] whose value is generated with [transform] by combining the current values
* of each given [State].
*
- * @see State.combineWithTransactionally
+ * Note that this is less efficient than [combine], which should be preferred if [transform]
+ * does not need access to [TransactionScope].
+ *
+ * @see State.combineTransactionally
*/
fun <A, Z> Iterable<State<A>>.combineTransactionally(
transform: TransactionScope.(List<A>) -> Z
@@ -748,8 +935,13 @@ interface StateScope : TransactionScope {
/**
* Returns a [State] by combining the values held inside the given [State]s by applying them to
* the given function [transform].
+ *
+ * Note that this is less efficient than [combine], which should be preferred if [transform]
+ * does not need access to [TransactionScope].
*/
- fun <A, B, C> State<A>.combineWithTransactionally(
+ @Suppress("INAPPLICABLE_JVM_NAME")
+ @JvmName(name = "combineStateTransactionally")
+ fun <A, B, C> State<A>.combineTransactionally(
other: State<B>,
transform: TransactionScope.(A, B) -> C,
): State<C> = combineTransactionally(this, other, transform)
@@ -757,6 +949,15 @@ interface StateScope : TransactionScope {
/**
* Returns an [Incremental] that reflects the state of the original [Incremental], but also adds
* / removes entries based on the state of the original's values.
+ *
+ * ``` kotlin
+ * fun <K, V> Incremental<K, State<Maybe<V>>>.applyStateIncrementally(): Incremental<K, V> =
+ * mapValues { (_, v) -> v.changes }
+ * .mergeEventsIncrementallyPromptly()
+ * .foldStateMapIncrementally(
+ * deferredStateScope { sample().mapMaybeValues { (_, s) -> s.sample() } }
+ * )
+ * ```
*/
fun <K, V> Incremental<K, State<Maybe<V>>>.applyStateIncrementally(): Incremental<K, V> =
mapValues { (_, v) -> v.changes }
@@ -769,6 +970,12 @@ interface StateScope : TransactionScope {
* Returns an [Incremental] that reflects the state of the original [Incremental], but also adds
* / removes entries based on the [State] returned from applying [transform] to the original's
* entries.
+ *
+ * ``` kotlin
+ * fun <K, V, U> Incremental<K, V>.mapIncrementalState(
+ * transform: KairosScope.(Map.Entry<K, V>) -> State<Maybe<U>>
+ * ): Incremental<K, U> = mapValues { transform(it) }.applyStateIncrementally()
+ * ```
*/
fun <K, V, U> Incremental<K, V>.mapIncrementalState(
transform: KairosScope.(Map.Entry<K, V>) -> State<Maybe<U>>
@@ -778,16 +985,33 @@ interface StateScope : TransactionScope {
* Returns an [Incremental] that reflects the state of the original [Incremental], but also adds
* / removes entries based on the [State] returned from applying [transform] to the original's
* entries, such that entries are added when that state is `true`, and removed when `false`.
+ *
+ * ``` kotlin
+ * fun <K, V> Incremental<K, V>.filterIncrementally(
+ * transform: KairosScope.(Map.Entry<K, V>) -> State<Boolean>
+ * ): Incremental<K, V> = mapIncrementalState { entry ->
+ * transform(entry).map { if (it) Maybe.present(entry.value) else Maybe.absent }
+ * }
+ * ```
*/
fun <K, V> Incremental<K, V>.filterIncrementally(
transform: KairosScope.(Map.Entry<K, V>) -> State<Boolean>
): Incremental<K, V> = mapIncrementalState { entry ->
- transform(entry).map { if (it) just(entry.value) else none }
+ transform(entry).map { if (it) Maybe.present(entry.value) else Maybe.absent }
}
/**
* Returns an [Incremental] that samples the [Transactionals][Transactional] held by the
* original within the same transaction that the incremental [updates].
+ *
+ * ``` kotlin
+ * fun <K, V> Incremental<K, Transactional<V>>.sampleTransactionals(): Incremental<K, V> =
+ * updates
+ * .map { patch -> patch.mapValues { (k, mv) -> mv.map { it.sample() } } }
+ * .foldStateMapIncrementally(
+ * deferredStateScope { sample().mapValues { (k, v) -> v.sample() } }
+ * )
+ * ```
*/
fun <K, V> Incremental<K, Transactional<V>>.sampleTransactionals(): Incremental<K, V> =
updates
@@ -799,6 +1023,16 @@ interface StateScope : TransactionScope {
/**
* Returns an [Incremental] that tracks the entries of the original incremental, but values
* replaced with those obtained by applying [transform] to each original entry.
+ *
+ * Note that this is less efficient than [mapValues], which should be preferred if [transform]
+ * does not need access to [TransactionScope].
+ *
+ * ``` kotlin
+ * fun <K, V, U> Incremental<K, V>.mapValuesTransactionally(
+ * transform: TransactionScope.(Map.Entry<K, V>) -> U
+ * ): Incremental<K, U> =
+ * mapValues { transactionally { transform(it) } }.sampleTransactionals()
+ * ```
*/
fun <K, V, U> Incremental<K, V>.mapValuesTransactionally(
transform: TransactionScope.(Map.Entry<K, V>) -> U
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Switch.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Switch.kt
new file mode 100644
index 000000000000..63e27d05f73d
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Switch.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.kairos
+
+import com.android.systemui.kairos.internal.IncrementalImpl
+import com.android.systemui.kairos.internal.constInit
+import com.android.systemui.kairos.internal.init
+import com.android.systemui.kairos.internal.mapImpl
+import com.android.systemui.kairos.internal.switchDeferredImplSingle
+import com.android.systemui.kairos.internal.switchPromptImplSingle
+import com.android.systemui.kairos.util.mapPatchFromFullDiff
+
+/**
+ * Returns an [Events] that switches to the [Events] contained within this [State] whenever it
+ * changes.
+ *
+ * This switch does take effect until the *next* transaction after [State] changes. For a switch
+ * that takes effect immediately, see [switchEventsPromptly].
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.switchEvents
+ */
+@ExperimentalKairosApi
+fun <A> State<Events<A>>.switchEvents(): Events<A> {
+ val patches =
+ mapImpl({ init.connect(this).changes }) { newEvents, _ -> newEvents.init.connect(this) }
+ return EventsInit(
+ constInit(
+ name = null,
+ switchDeferredImplSingle(
+ getStorage = {
+ init.connect(this).getCurrentWithEpoch(this).first.init.connect(this)
+ },
+ getPatches = { patches },
+ ),
+ )
+ )
+}
+
+/**
+ * Returns an [Events] that switches to the [Events] contained within this [State] whenever it
+ * changes.
+ *
+ * This switch takes effect immediately within the same transaction that [State] changes. If the
+ * newly-switched-in [Events] is emitting a value within this transaction, then that value will be
+ * emitted from this switch. If not, but the previously-switched-in [Events] *is* emitting, then
+ * that value will be emitted from this switch instead. Otherwise, there will be no emission.
+ *
+ * In general, you should prefer [switchEvents] over this method. It is both safer and more
+ * performant.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.switchEventsPromptly
+ */
+// TODO: parameter to handle coincidental emission from both old and new
+@ExperimentalKairosApi
+fun <A> State<Events<A>>.switchEventsPromptly(): Events<A> {
+ val patches =
+ mapImpl({ init.connect(this).changes }) { newEvents, _ -> newEvents.init.connect(this) }
+ return EventsInit(
+ constInit(
+ name = null,
+ switchPromptImplSingle(
+ getStorage = {
+ init.connect(this).getCurrentWithEpoch(this).first.init.connect(this)
+ },
+ getPatches = { patches },
+ ),
+ )
+ )
+}
+
+/** Returns an [Incremental] that behaves like current value of this [State]. */
+fun <K, V> State<Incremental<K, V>>.switchIncremental(): Incremental<K, V> {
+ val stateChangePatches =
+ transitions.mapNotNull { (old, new) ->
+ mapPatchFromFullDiff(old.sample(), new.sample()).takeIf { it.isNotEmpty() }
+ }
+ val innerChanges =
+ map { inner ->
+ merge(stateChangePatches, inner.updates) { switchPatch, upcomingPatch ->
+ switchPatch + upcomingPatch
+ }
+ }
+ .switchEventsPromptly()
+ val flattened = flatten()
+ return IncrementalInit(
+ init("switchIncremental") {
+ val upstream = flattened.init.connect(this)
+ IncrementalImpl(
+ "switchIncremental",
+ "switchIncremental",
+ upstream.changes,
+ innerChanges.init.connect(this),
+ upstream.store,
+ )
+ }
+ )
+}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/ToColdFlow.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/ToColdFlow.kt
new file mode 100644
index 000000000000..3d2768ba9c4c
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/ToColdFlow.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.kairos
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.channelFlow
+import kotlinx.coroutines.flow.conflate
+
+/**
+ * Returns a cold [Flow] that, when collected, emits from this [Events]. [network] is needed to
+ * transactionally connect to / disconnect from the [Events] when collection starts/stops.
+ */
+@ExperimentalKairosApi
+fun <A> Events<A>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
+ channelFlow { network.activateSpec { observe { trySend(it) } } }.conflate()
+
+/**
+ * Returns a cold [Flow] that, when collected, emits from this [State]. [network] is needed to
+ * transactionally connect to / disconnect from the [State] when collection starts/stops.
+ */
+@ExperimentalKairosApi
+fun <A> State<A>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
+ channelFlow { network.activateSpec { observe { trySend(it) } } }.conflate()
+
+/**
+ * Returns a cold [Flow] that, when collected, applies this [BuildSpec] in a new transaction in this
+ * [network], and then emits from the returned [Events].
+ *
+ * When collection is cancelled, so is the [BuildSpec]. This means all ongoing work is cleaned up.
+ */
+@ExperimentalKairosApi
+@JvmName("eventsSpecToColdConflatedFlow")
+fun <A> BuildSpec<Events<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
+ channelFlow { network.activateSpec { applySpec().observe { trySend(it) } } }.conflate()
+
+/**
+ * Returns a cold [Flow] that, when collected, applies this [BuildSpec] in a new transaction in this
+ * [network], and then emits from the returned [State].
+ *
+ * When collection is cancelled, so is the [BuildSpec]. This means all ongoing work is cleaned up.
+ */
+@ExperimentalKairosApi
+@JvmName("stateSpecToColdConflatedFlow")
+fun <A> BuildSpec<State<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
+ channelFlow { network.activateSpec { applySpec().observe { trySend(it) } } }.conflate()
+
+/**
+ * Returns a cold [Flow] that, when collected, applies this [Transactional] in a new transaction in
+ * this [network], and then emits from the returned [Events].
+ */
+@ExperimentalKairosApi
+@JvmName("transactionalFlowToColdConflatedFlow")
+fun <A> Transactional<Events<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
+ channelFlow { network.activateSpec { sample().observe { trySend(it) } } }.conflate()
+
+/**
+ * Returns a cold [Flow] that, when collected, applies this [Transactional] in a new transaction in
+ * this [network], and then emits from the returned [State].
+ */
+@ExperimentalKairosApi
+@JvmName("transactionalStateToColdConflatedFlow")
+fun <A> Transactional<State<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
+ channelFlow { network.activateSpec { sample().observe { trySend(it) } } }.conflate()
+
+/**
+ * Returns a cold [Flow] that, when collected, applies this [Stateful] in a new transaction in this
+ * [network], and then emits from the returned [Events].
+ *
+ * When collection is cancelled, so is the [Stateful]. This means all ongoing work is cleaned up.
+ */
+@ExperimentalKairosApi
+@JvmName("statefulFlowToColdConflatedFlow")
+fun <A> Stateful<Events<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
+ channelFlow { network.activateSpec { applyStateful().observe { trySend(it) } } }.conflate()
+
+/**
+ * Returns a cold [Flow] that, when collected, applies this [Transactional] in a new transaction in
+ * this [network], and then emits from the returned [State].
+ *
+ * When collection is cancelled, so is the [Stateful]. This means all ongoing work is cleaned up.
+ */
+@ExperimentalKairosApi
+@JvmName("statefulStateToColdConflatedFlow")
+fun <A> Stateful<State<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
+ channelFlow { network.activateSpec { applyStateful().observe { trySend(it) } } }.conflate()
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TransactionScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TransactionScope.kt
index 225416992d52..a5ac909b7a28 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TransactionScope.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TransactionScope.kt
@@ -16,11 +16,14 @@
package com.android.systemui.kairos
+import com.android.systemui.kairos.util.Maybe
+import com.android.systemui.kairos.util.These
+
/**
* Kairos operations that are available while a transaction is active.
*
* These operations do not accumulate state, which makes [TransactionScope] weaker than
- * [StateScope], but allows them to be used in more places.
+ * [StateScope], but allows it to be used in more places.
*/
@ExperimentalKairosApi
interface TransactionScope : KairosScope {
@@ -57,7 +60,7 @@ interface TransactionScope : KairosScope {
*/
fun <A> deferredTransactionScope(block: TransactionScope.() -> A): DeferredValue<A>
- /** An [Events] that emits once, within this transaction, and then never again. */
+ /** An [Events] that emits once, within the current transaction, and then never again. */
val now: Events<Unit>
/**
@@ -66,7 +69,7 @@ interface TransactionScope : KairosScope {
*
* @see sampleDeferred
*/
- fun <A> State<A>.sample(): A = sampleDeferred().get()
+ fun <A> State<A>.sample(): A = sampleDeferred().value
/**
* Returns the current value held by this [Transactional]. Guaranteed to be consistent within
@@ -74,5 +77,55 @@ interface TransactionScope : KairosScope {
*
* @see sampleDeferred
*/
- fun <A> Transactional<A>.sample(): A = sampleDeferred().get()
+ fun <A> Transactional<A>.sample(): A = sampleDeferred().value
}
+
+/**
+ * Returns an [Events] that emits the value sampled from the [Transactional] produced by each
+ * emission of the original [Events], within the same transaction of the original emission.
+ */
+@ExperimentalKairosApi
+fun <A> Events<Transactional<A>>.sampleTransactionals(): Events<A> = map { it.sample() }
+
+/** @see TransactionScope.sample */
+@ExperimentalKairosApi
+fun <A, B, C> Events<A>.sample(
+ state: State<B>,
+ transform: TransactionScope.(A, B) -> C,
+): Events<C> = map { transform(it, state.sample()) }
+
+/** @see TransactionScope.sample */
+@ExperimentalKairosApi
+fun <A, B, C> Events<A>.sample(
+ sampleable: Transactional<B>,
+ transform: TransactionScope.(A, B) -> C,
+): Events<C> = map { transform(it, sampleable.sample()) }
+
+/**
+ * Like [sample], but if [state] is changing at the time it is sampled ([changes] is emitting), then
+ * the new value is passed to [transform].
+ *
+ * Note that [sample] is both more performant and safer to use with recursive definitions. You will
+ * generally want to use it rather than this.
+ *
+ * @see sample
+ */
+@ExperimentalKairosApi
+fun <A, B, C> Events<A>.samplePromptly(
+ state: State<B>,
+ transform: TransactionScope.(A, B) -> C,
+): Events<C> =
+ sample(state) { a, b -> These.first(a to b) }
+ .mergeWith(state.changes.map { These.second(it) }) { thiz, that ->
+ These.both((thiz as These.First).value, (that as These.Second).value)
+ }
+ .mapMaybe { these ->
+ when (these) {
+ // both present, transform the upstream value and the new value
+ is These.Both -> Maybe.present(transform(these.first.first, these.second))
+ // no upstream present, so don't perform the sample
+ is These.Second -> Maybe.absent()
+ // just the upstream, so transform the upstream and the old value
+ is These.First -> Maybe.present(transform(these.value.first, these.value.second))
+ }
+ }
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Transactional.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Transactional.kt
index 9485cd212603..cf98821fdadb 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Transactional.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Transactional.kt
@@ -29,8 +29,8 @@ import com.android.systemui.kairos.internal.util.hashString
* it is "sampled", a new result may be produced.
*
* Because Kairos operates over an "idealized" model of Time that can be passed around as a data
- * type, [Transactional]s are guaranteed to produce the same result if queried multiple times at the
- * same (conceptual) time, in order to preserve _referential transparency_.
+ * type, [Transactionals][Transactional] are guaranteed to produce the same result if queried
+ * multiple times at the same (conceptual) time, in order to preserve _referential transparency_.
*/
@ExperimentalKairosApi
class Transactional<out A> internal constructor(internal val impl: State<TransactionalImpl<A>>) {
@@ -50,6 +50,10 @@ fun <A> transactionalOf(value: A): Transactional<A> =
* queried and used.
*
* Useful for recursive definitions.
+ *
+ * ``` kotlin
+ * fun <A> DeferredValue<Transactional<A>>.defer() = deferredTransactional { get() }
+ * ```
*/
@ExperimentalKairosApi
fun <A> DeferredValue<Transactional<A>>.defer(): Transactional<A> = deferInline { unwrapped.value }
@@ -62,6 +66,10 @@ fun <A> DeferredValue<Transactional<A>>.defer(): Transactional<A> = deferInline
* [value][Lazy.value] will be queried and used.
*
* Useful for recursive definitions.
+ *
+ * ``` kotlin
+ * fun <A> Lazy<Transactional<A>>.defer() = deferredTransactional { value }
+ * ```
*/
@ExperimentalKairosApi
fun <A> Lazy<Transactional<A>>.defer(): Transactional<A> = deferInline { value }
@@ -89,7 +97,13 @@ private inline fun <A> deferInline(
/**
* Returns a [Transactional]. The passed [block] will be evaluated on demand at most once per
* transaction; any subsequent sampling within the same transaction will receive a cached value.
+ *
+ * @sample com.android.systemui.kairos.KairosSamples.sampleTransactional
*/
@ExperimentalKairosApi
fun <A> transactionally(block: TransactionScope.() -> A): Transactional<A> =
Transactional(stateOf(transactionalImpl { block() }))
+
+/** Returns a [Transactional] that, when queried, samples this [State]. */
+fun <A> State<A>.asTransactional(): Transactional<A> =
+ Transactional(map { TransactionalImpl.Const(CompletableLazy(it)) })
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/BuildScopeImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/BuildScopeImpl.kt
index b20e77a31dab..2f4c3963d2fe 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/BuildScopeImpl.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/BuildScopeImpl.kt
@@ -26,6 +26,7 @@ import com.android.systemui.kairos.EventProducerScope
import com.android.systemui.kairos.Events
import com.android.systemui.kairos.EventsInit
import com.android.systemui.kairos.GroupedEvents
+import com.android.systemui.kairos.KairosCoroutineScope
import com.android.systemui.kairos.KairosNetwork
import com.android.systemui.kairos.LocalNetwork
import com.android.systemui.kairos.MutableEvents
@@ -33,20 +34,23 @@ import com.android.systemui.kairos.TransactionScope
import com.android.systemui.kairos.groupByKey
import com.android.systemui.kairos.init
import com.android.systemui.kairos.internal.util.childScope
+import com.android.systemui.kairos.internal.util.invokeOnCancel
import com.android.systemui.kairos.internal.util.launchImmediate
import com.android.systemui.kairos.launchEffect
import com.android.systemui.kairos.mergeLeft
import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.Maybe.Just
-import com.android.systemui.kairos.util.Maybe.None
-import com.android.systemui.kairos.util.just
+import com.android.systemui.kairos.util.Maybe.Absent
+import com.android.systemui.kairos.util.Maybe.Present
import com.android.systemui.kairos.util.map
import java.util.concurrent.atomic.AtomicReference
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CompletableJob
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Deferred
import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.Job
+import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.job
@@ -60,12 +64,8 @@ internal class BuildScopeImpl(val stateScope: StateScopeImpl, val coroutineScope
LocalNetwork(network, coroutineScope, endSignal)
}
- override fun <T> events(
- name: String?,
- builder: suspend EventProducerScope<T>.() -> Unit,
- ): Events<T> =
+ override fun <T> events(builder: suspend EventProducerScope<T>.() -> Unit): Events<T> =
buildEvents(
- name,
constructEvents = { inputNode ->
val events = MutableEvents(network, inputNode)
events to
@@ -123,9 +123,9 @@ internal class BuildScopeImpl(val stateScope: StateScopeImpl, val coroutineScope
val childScope = coroutineScope.childScope()
lateinit var cancelHandle: DisposableHandle
val handle = DisposableHandle {
- subRef.getAndSet(None)?.let { output ->
- cancelHandle.dispose()
- if (output is Just) {
+ cancelHandle.dispose()
+ subRef.getAndSet(Absent)?.let { output ->
+ if (output is Present) {
@Suppress("DeferredResultUnused")
network.transaction("observeEffect cancelled") {
scheduleDeactivation(output.value)
@@ -139,14 +139,27 @@ internal class BuildScopeImpl(val stateScope: StateScopeImpl, val coroutineScope
val outputNode =
Output<A>(
context = coroutineContext,
- onDeath = { subRef.set(None) },
+ onDeath = { subRef.set(Absent) },
onEmit = { output ->
- if (subRef.get() is Just) {
+ if (subRef.get() is Present) {
// Not cancelled, safe to emit
val scope =
object : EffectScope, TransactionScope by this {
- override val effectCoroutineScope: CoroutineScope = childScope
- override val kairosNetwork: KairosNetwork = localNetwork
+ override fun <R> async(
+ context: CoroutineContext,
+ start: CoroutineStart,
+ block: suspend KairosCoroutineScope.() -> R,
+ ): Deferred<R> =
+ childScope.async(context, start) {
+ object : KairosCoroutineScope, CoroutineScope by this {
+ override val kairosNetwork: KairosNetwork
+ get() = localNetwork
+ }
+ .block()
+ }
+
+ override val kairosNetwork: KairosNetwork
+ get() = localNetwork
}
scope.block(output)
}
@@ -162,7 +175,7 @@ internal class BuildScopeImpl(val stateScope: StateScopeImpl, val coroutineScope
.activate(evalScope = stateScope.evalScope, outputNode.schedulable)
?.let { (conn, needsEval) ->
outputNode.upstream = conn
- if (!subRef.compareAndSet(null, just(outputNode))) {
+ if (!subRef.compareAndSet(null, Maybe.present(outputNode))) {
// Job's already been cancelled, schedule deactivation
scheduleDeactivation(outputNode)
} else if (needsEval) {
@@ -289,21 +302,15 @@ internal class BuildScopeImpl(val stateScope: StateScopeImpl, val coroutineScope
}
private fun mutableChildBuildScope(): BuildScopeImpl {
- val stopEmitter = newStopEmitter("mutableChildBuildScope")
val childScope = coroutineScope.childScope()
- childScope.coroutineContext.job.invokeOnCompletion { stopEmitter.emit(Unit) }
- // Ensure that once this transaction is done, the new child scope enters the completing
- // state (kept alive so long as there are child jobs).
- // TODO: need to keep the scope alive if it's used to accumulate state.
- // Otherwise, stopEmitter will emit early, due to the call to complete().
- // scheduleOutput(
- // OneShot {
- // // TODO: don't like this cast
- // (childScope.coroutineContext.job as CompletableJob).complete()
- // }
- // )
+ val stopEmitter = lazy {
+ newStopEmitter("mutableChildBuildScope").apply {
+ childScope.invokeOnCancel { emit(Unit) }
+ }
+ }
return BuildScopeImpl(
- stateScope = StateScopeImpl(evalScope = stateScope.evalScope, endSignal = stopEmitter),
+ stateScope =
+ StateScopeImpl(evalScope = stateScope.evalScope, endSignalLazy = stopEmitter),
coroutineScope = childScope,
)
}
@@ -314,6 +321,7 @@ private fun EvalScope.reenterBuildScope(
coroutineScope: CoroutineScope,
) =
BuildScopeImpl(
- stateScope = StateScopeImpl(evalScope = this, endSignal = outerScope.endSignal),
+ stateScope =
+ StateScopeImpl(evalScope = this, endSignalLazy = outerScope.stateScope.endSignalLazy),
coroutineScope,
)
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/FilterNode.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/FilterNode.kt
index 9496b06c6bd1..f86e7612655f 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/FilterNode.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/FilterNode.kt
@@ -19,16 +19,14 @@ package com.android.systemui.kairos.internal
import com.android.systemui.kairos.internal.store.Single
import com.android.systemui.kairos.internal.store.SingletonMapK
import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.Maybe.Just
-import com.android.systemui.kairos.util.just
-import com.android.systemui.kairos.util.none
+import com.android.systemui.kairos.util.Maybe.Present
-internal inline fun <A> filterJustImpl(
+internal inline fun <A> filterPresentImpl(
crossinline getPulse: EvalScope.() -> EventsImpl<Maybe<A>>
): EventsImpl<A> =
DemuxImpl(
mapImpl(getPulse) { maybeResult, _ ->
- if (maybeResult is Just) {
+ if (maybeResult is Present) {
Single(maybeResult.value)
} else {
Single<A>()
@@ -43,6 +41,7 @@ internal inline fun <A> filterImpl(
crossinline getPulse: EvalScope.() -> EventsImpl<A>,
crossinline f: EvalScope.(A) -> Boolean,
): EventsImpl<A> {
- val mapped = mapImpl(getPulse) { it, _ -> if (f(it)) just(it) else none }.cached()
- return filterJustImpl { mapped }
+ val mapped =
+ mapImpl(getPulse) { it, _ -> if (f(it)) Maybe.present(it) else Maybe.absent }.cached()
+ return filterPresentImpl { mapped }
}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/IncrementalImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/IncrementalImpl.kt
index 8a3e01af6565..9b4778ab18b1 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/IncrementalImpl.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/IncrementalImpl.kt
@@ -17,11 +17,10 @@
package com.android.systemui.kairos.internal
import com.android.systemui.kairos.internal.store.StoreEntry
+import com.android.systemui.kairos.util.MapPatch
import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.applyPatch
-import com.android.systemui.kairos.util.just
import com.android.systemui.kairos.util.map
-import com.android.systemui.kairos.util.none
+import com.android.systemui.kairos.util.toMaybe
internal class IncrementalImpl<K, out V>(
name: String?,
@@ -48,12 +47,11 @@ internal inline fun <K, V> activatedIncremental(
val store = StateSource(init)
val maybeChanges =
mapImpl(getPatches) { patch, _ ->
- val (old, _) = store.getCurrentWithEpoch(evalScope = this)
- val new = old.applyPatch(patch)
- if (new != old) just(patch to new) else none
+ val (current, _) = store.getCurrentWithEpoch(evalScope = this)
+ current.applyPatchCalm(patch).toMaybe()
}
.cached()
- val calm = filterJustImpl { maybeChanges }
+ val calm = filterPresentImpl { maybeChanges }
val changes = mapImpl({ calm }) { (_, change), _ -> change }
val patches = mapImpl({ calm }) { (patch, _), _ -> patch }
evalScope.scheduleOutput(
@@ -70,22 +68,44 @@ internal inline fun <K, V> activatedIncremental(
return IncrementalImpl(name, operatorName, changes, patches, store)
}
+private fun <K, V> Map<K, V>.applyPatchCalm(
+ patch: MapPatch<K, V>
+): Pair<MapPatch<K, V>, Map<K, V>>? {
+ val current = this
+ val filteredPatch = mutableMapOf<K, Maybe<V>>()
+ val new = current.toMutableMap()
+ for ((key, change) in patch) {
+ when (change) {
+ is Maybe.Present -> {
+ if (key !in current || current.getValue(key) != change.value) {
+ filteredPatch[key] = change
+ new[key] = change.value
+ }
+ }
+ Maybe.Absent -> {
+ if (key in current) {
+ filteredPatch[key] = change
+ new.remove(key)
+ }
+ }
+ }
+ }
+ return if (filteredPatch.isNotEmpty()) filteredPatch to new else null
+}
+
internal inline fun <K, V> EventsImpl<Map<K, Maybe<V>>>.calmUpdates(
state: StateDerived<Map<K, V>>
): Pair<EventsImpl<Map<K, Maybe<V>>>, EventsImpl<Map<K, V>>> {
val maybeUpdate =
mapImpl({ this@calmUpdates }) { patch, _ ->
val (current, _) = state.getCurrentWithEpoch(evalScope = this)
- val new = current.applyPatch(patch)
- if (new != current) {
- state.setCacheFromPush(new, epoch)
- just(patch to new)
- } else {
- none
- }
+ current
+ .applyPatchCalm(patch)
+ ?.also { (_, newMap) -> state.setCacheFromPush(newMap, epoch) }
+ .toMaybe()
}
.cached()
- val calm = filterJustImpl { maybeUpdate }
+ val calm = filterPresentImpl { maybeUpdate }
val patches = mapImpl({ calm }) { (p, _), _ -> p }
val changes = mapImpl({ calm }) { (_, s), _ -> s }
return patches to changes
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Init.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Init.kt
index 640c561a21eb..4fa107058f6e 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Init.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Init.kt
@@ -17,8 +17,6 @@
package com.android.systemui.kairos.internal
import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.just
-import com.android.systemui.kairos.util.none
import kotlinx.coroutines.ExperimentalCoroutinesApi
/** Performs actions once, when the reactive component is first connected to the network. */
@@ -44,9 +42,9 @@ internal class Init<out A>(val name: String?, private val block: InitScope.() ->
@OptIn(ExperimentalCoroutinesApi::class)
fun getUnsafe(): Maybe<A> =
if (cache.isInitialized()) {
- just(cache.value.second)
+ Maybe.present(cache.value.second)
} else {
- none
+ Maybe.absent
}
}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxDeferred.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxDeferred.kt
index cf74f755c98b..c11eb122597d 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxDeferred.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxDeferred.kt
@@ -28,14 +28,13 @@ import com.android.systemui.kairos.internal.util.hashString
import com.android.systemui.kairos.internal.util.logDuration
import com.android.systemui.kairos.internal.util.logLn
import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.Maybe.Just
-import com.android.systemui.kairos.util.Maybe.None
+import com.android.systemui.kairos.util.Maybe.Absent
+import com.android.systemui.kairos.util.Maybe.Present
import com.android.systemui.kairos.util.These
import com.android.systemui.kairos.util.flatMap
import com.android.systemui.kairos.util.getMaybe
-import com.android.systemui.kairos.util.just
-import com.android.systemui.kairos.util.maybeThat
-import com.android.systemui.kairos.util.maybeThis
+import com.android.systemui.kairos.util.maybeFirst
+import com.android.systemui.kairos.util.maybeSecond
import com.android.systemui.kairos.util.merge
import com.android.systemui.kairos.util.orError
import com.android.systemui.kairos.util.these
@@ -133,8 +132,8 @@ internal class MuxDeferredNode<W, K, V>(
val removes = mutableListOf<K>()
patch.forEach { (k, newUpstream) ->
when (newUpstream) {
- is Just -> adds.add(k to newUpstream.value)
- None -> removes.add(k)
+ is Present -> adds.add(k to newUpstream.value)
+ Absent -> removes.add(k)
}
}
@@ -282,7 +281,8 @@ internal inline fun <A> switchDeferredImplSingle(
crossinline getStorage: EvalScope.() -> EventsImpl<A>,
crossinline getPatches: EvalScope.() -> EventsImpl<EventsImpl<A>>,
): EventsImpl<A> {
- val patches = mapImpl(getPatches) { newEvents, _ -> singleOf(just(newEvents)).asIterable() }
+ val patches =
+ mapImpl(getPatches) { newEvents, _ -> singleOf(Maybe.present(newEvents)).asIterable() }
val switchDeferredImpl =
switchDeferredImpl(
name = name,
@@ -402,8 +402,8 @@ internal inline fun <A, B> mergeNodes(
): EventsImpl<These<A, B>> {
val storage =
listOf(
- mapImpl(getPulse) { it, _ -> These.thiz(it) },
- mapImpl(getOther) { it, _ -> These.that(it) },
+ mapImpl(getPulse) { it, _ -> These.first(it) },
+ mapImpl(getOther) { it, _ -> These.second(it) },
)
.asIterableWithIndex()
val switchNode =
@@ -417,9 +417,9 @@ internal inline fun <A, B> mergeNodes(
mapImpl({ switchNode }) { it, logIndent ->
val mergeResults = it.asArrayHolder()
val first =
- mergeResults.getMaybe(0).flatMap { it.getPushEvent(logIndent, this).maybeThis() }
+ mergeResults.getMaybe(0).flatMap { it.getPushEvent(logIndent, this).maybeFirst() }
val second =
- mergeResults.getMaybe(1).flatMap { it.getPushEvent(logIndent, this).maybeThat() }
+ mergeResults.getMaybe(1).flatMap { it.getPushEvent(logIndent, this).maybeSecond() }
these(first, second).orError { "unexpected missing merge result" }
}
return merged.cached()
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxPrompt.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxPrompt.kt
index 32aef5c7041b..cb2c6e51df66 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxPrompt.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxPrompt.kt
@@ -24,9 +24,8 @@ import com.android.systemui.kairos.internal.util.LogIndent
import com.android.systemui.kairos.internal.util.hashString
import com.android.systemui.kairos.internal.util.logDuration
import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.Maybe.Just
-import com.android.systemui.kairos.util.Maybe.None
-import com.android.systemui.kairos.util.just
+import com.android.systemui.kairos.util.Maybe.Absent
+import com.android.systemui.kairos.util.Maybe.Present
internal class MuxPromptNode<W, K, V>(
val name: String?,
@@ -94,8 +93,8 @@ internal class MuxPromptNode<W, K, V>(
val removes = mutableListOf<K>()
patch.forEach { (k, newUpstream) ->
when (newUpstream) {
- is Just -> adds.add(k to newUpstream.value)
- None -> removes.add(k)
+ is Present -> adds.add(k to newUpstream.value)
+ Absent -> removes.add(k)
}
}
@@ -311,7 +310,9 @@ internal inline fun <A> switchPromptImplSingle(
switchPromptImpl(
getStorage = { singleOf(getStorage()).asIterable() },
getPatches = {
- mapImpl(getPatches) { newEvents, _ -> singleOf(just(newEvents)).asIterable() }
+ mapImpl(getPatches) { newEvents, _ ->
+ singleOf(Maybe.present(newEvents)).asIterable()
+ }
},
storeFactory = SingletonMapK.Factory(),
)
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Network.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Network.kt
index fbc2b3644701..6e86dd150126 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Network.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Network.kt
@@ -21,9 +21,7 @@ import com.android.systemui.kairos.internal.util.HeteroMap
import com.android.systemui.kairos.internal.util.logDuration
import com.android.systemui.kairos.internal.util.logLn
import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.Maybe.Just
-import com.android.systemui.kairos.util.just
-import com.android.systemui.kairos.util.none
+import com.android.systemui.kairos.util.Maybe.Present
import java.util.concurrent.atomic.AtomicLong
import kotlin.coroutines.ContinuationInterceptor
import kotlin.time.measureTime
@@ -33,6 +31,7 @@ import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.isActive
import kotlinx.coroutines.job
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
@@ -148,6 +147,10 @@ internal class Network(val coroutineScope: CoroutineScope) : NetworkScope {
/** Evaluates [block] inside of a new transaction when the network is ready. */
fun <R> transaction(reason: String, block: suspend EvalScope.() -> R): Deferred<R> =
CompletableDeferred<R>(parent = coroutineScope.coroutineContext.job).also { onResult ->
+ if (!coroutineScope.isActive) {
+ onResult.cancel()
+ return@also
+ }
val job =
coroutineScope.launch {
inputScheduleChan.send(
@@ -261,25 +264,25 @@ internal class ScheduledAction<T>(
private val onResult: CompletableDeferred<T>? = null,
private val onStartTransaction: suspend EvalScope.() -> T,
) {
- private var result: Maybe<T> = none
+ private var result: Maybe<T> = Maybe.absent
suspend fun started(evalScope: EvalScope) {
- result = just(onStartTransaction(evalScope))
+ result = Maybe.present(onStartTransaction(evalScope))
}
fun fail(ex: Exception) {
- result = none
+ result = Maybe.absent
onResult?.completeExceptionally(ex)
}
fun completed() {
if (onResult != null) {
when (val result = result) {
- is Just -> onResult.complete(result.value)
+ is Present -> onResult.complete(result.value)
else -> {}
}
}
- result = none
+ result = Maybe.absent
}
}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateImpl.kt
index 46127cb2276b..da832580e7d9 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateImpl.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateImpl.kt
@@ -22,8 +22,6 @@ import com.android.systemui.kairos.internal.store.MutableMapK
import com.android.systemui.kairos.internal.store.StoreEntry
import com.android.systemui.kairos.internal.util.hashString
import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.just
-import com.android.systemui.kairos.util.none
internal open class StateImpl<out A>(
val name: String?,
@@ -73,7 +71,7 @@ internal sealed class StateDerived<A> : StateStore<A>() {
fun getCachedUnsafe(): Maybe<A> {
@Suppress("UNCHECKED_CAST")
- return if (cache == EmptyCache) none else just(cache as A)
+ return if (cache == EmptyCache) Maybe.absent else Maybe.present(cache as A)
}
protected abstract fun recalc(evalScope: EvalScope): Pair<A, Long>?
@@ -117,7 +115,8 @@ internal class StateSource<S>(init: Lazy<S>) : StateStore<S>() {
override fun toString(): String = "StateImpl(current=$_current, writeEpoch=$writeEpoch)"
- fun getStorageUnsafe(): Maybe<S> = if (_current.isInitialized()) just(_current.value) else none
+ fun getStorageUnsafe(): Maybe<S> =
+ if (_current.isInitialized()) Maybe.present(_current.value) else Maybe.absent
}
internal fun <A> constState(name: String?, operatorName: String, init: A): StateImpl<A> =
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateScopeImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateScopeImpl.kt
index bd1f94fca22f..53a704a25f13 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateScopeImpl.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateScopeImpl.kt
@@ -36,10 +36,14 @@ import com.android.systemui.kairos.switchEvents
import com.android.systemui.kairos.util.Maybe
import com.android.systemui.kairos.util.map
-internal class StateScopeImpl(val evalScope: EvalScope, override val endSignal: Events<Any>) :
+internal class StateScopeImpl(val evalScope: EvalScope, val endSignalLazy: Lazy<Events<Any>>) :
InternalStateScope, EvalScope by evalScope {
- override val endSignalOnce: Events<Any> = endSignal.nextOnlyInternal("StateScope.endSignal")
+ override val endSignal: Events<Any> by endSignalLazy
+
+ override val endSignalOnce: Events<Any> by lazy {
+ endSignal.nextOnlyInternal("StateScope.endSignal")
+ }
override fun <A> deferredStateScope(block: StateScope.() -> A): DeferredValue<A> =
DeferredValue(deferAsync { block() })
@@ -119,7 +123,7 @@ internal class StateScopeImpl(val evalScope: EvalScope, override val endSignal:
}
override fun childStateScope(newEnd: Events<Any>) =
- StateScopeImpl(evalScope, merge(newEnd, endSignal))
+ StateScopeImpl(evalScope, lazy { merge(newEnd, endSignal) })
private fun <A> Events<A>.truncateToScope(operatorName: String): Events<A> =
if (endSignalOnce === emptyEvents) {
@@ -165,4 +169,4 @@ internal class StateScopeImpl(val evalScope: EvalScope, override val endSignal:
}
private fun EvalScope.reenterStateScope(outerScope: StateScopeImpl) =
- StateScopeImpl(evalScope = this, endSignal = outerScope.endSignal)
+ StateScopeImpl(evalScope = this, endSignalLazy = outerScope.endSignalLazy)
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/HeteroMap.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/HeteroMap.kt
index 9b6940d03270..c34e67ef6926 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/HeteroMap.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/HeteroMap.kt
@@ -17,8 +17,7 @@
package com.android.systemui.kairos.internal.util
import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.Maybe.None
-import com.android.systemui.kairos.util.just
+import com.android.systemui.kairos.util.Maybe.Absent
import java.util.concurrent.ConcurrentHashMap
private object NULL
@@ -32,7 +31,7 @@ internal class HeteroMap private constructor(private val store: ConcurrentHashMa
@Suppress("UNCHECKED_CAST")
operator fun <A> get(key: Key<A>): Maybe<A> =
- store[key]?.let { just((if (it === NULL) null else it) as A) } ?: None
+ store[key]?.let { Maybe.present((if (it === NULL) null else it) as A) } ?: Absent
operator fun <A> set(key: Key<A>, value: A) {
store[key] = value ?: NULL
@@ -57,7 +56,7 @@ internal class HeteroMap private constructor(private val store: ConcurrentHashMa
@Suppress("UNCHECKED_CAST")
fun <A> remove(key: Key<A>): Maybe<A> =
- store.remove(key)?.let { just((if (it === NULL) null else it) as A) } ?: None
+ store.remove(key)?.let { Maybe.present((if (it === NULL) null else it) as A) } ?: Absent
@Suppress("UNCHECKED_CAST")
fun <A> getOrPut(key: Key<A>, defaultValue: () -> A): A =
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/Util.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/Util.kt
index 466a9f83b91f..d2a169ccc29c 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/Util.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/Util.kt
@@ -112,7 +112,7 @@ internal suspend fun awaitCancellationAndThen(block: suspend () -> Unit) {
}
}
-internal fun CoroutineScope.launchOnCancel(
+internal fun CoroutineScope.invokeOnCancel(
context: CoroutineContext = EmptyCoroutineContext,
block: () -> Unit,
): Job =
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Either.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Either.kt
index 957d46ff1ecd..9f17d5646577 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Either.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Either.kt
@@ -18,100 +18,118 @@
package com.android.systemui.kairos.util
-import com.android.systemui.kairos.util.Either.Left
-import com.android.systemui.kairos.util.Either.Right
+import com.android.systemui.kairos.util.Either.First
+import com.android.systemui.kairos.util.Either.Second
/**
- * Contains a value of two possibilities: `Left<A>` or `Right<B>`
+ * Contains a value of two possibilities: `First<A>` or `Second<B>`
*
* [Either] generalizes sealed classes the same way that [Pair] generalizes data classes; if a
* [Pair] is effectively an anonymous grouping of two instances, then an [Either] is an anonymous
* set of two options.
*/
sealed interface Either<out A, out B> {
- /** An [Either] that contains a [Left] value. */
- @JvmInline value class Left<out A>(val value: A) : Either<A, Nothing>
+ /** An [Either] that contains a [First] value. */
+ @JvmInline value class First<out A>(val value: A) : Either<A, Nothing>
- /** An [Either] that contains a [Right] value. */
- @JvmInline value class Right<out B>(val value: B) : Either<Nothing, B>
+ /** An [Either] that contains a [Second] value. */
+ @JvmInline value class Second<out B>(val value: B) : Either<Nothing, B>
+
+ companion object {
+ /** Constructs an [Either] containing the first possibility. */
+ fun <A> first(value: A): Either<A, Nothing> = First(value)
+
+ /** Constructs a [Either] containing the second possibility. */
+ fun <B> second(value: B): Either<Nothing, B> = Second(value)
+ }
}
/**
- * Returns an [Either] containing the result of applying [transform] to the [Left] value, or the
- * [Right] value unchanged.
+ * Returns an [Either] containing the result of applying [transform] to the [First] value, or the
+ * [Second] value unchanged.
*/
-inline fun <A, B, C> Either<A, C>.mapLeft(transform: (A) -> B): Either<B, C> =
+inline fun <A, B, C> Either<A, C>.mapFirst(transform: (A) -> B): Either<B, C> =
when (this) {
- is Left -> Left(transform(value))
- is Right -> this
+ is First -> First(transform(value))
+ is Second -> this
}
/**
- * Returns an [Either] containing the result of applying [transform] to the [Right] value, or the
- * [Left] value unchanged.
+ * Returns an [Either] containing the result of applying [transform] to the [Second] value, or the
+ * [First] value unchanged.
*/
-inline fun <A, B, C> Either<A, B>.mapRight(transform: (B) -> C): Either<A, C> =
+inline fun <A, B, C> Either<A, B>.mapSecond(transform: (B) -> C): Either<A, C> =
when (this) {
- is Left -> this
- is Right -> Right(transform(value))
+ is First -> this
+ is Second -> Second(transform(value))
}
-/** Returns a [Maybe] containing the [Left] value held by this [Either], if present. */
-inline fun <A> Either<A, *>.leftMaybe(): Maybe<A> =
+/** Returns a [Maybe] containing the [First] value held by this [Either], if present. */
+inline fun <A> Either<A, *>.firstMaybe(): Maybe<A> =
when (this) {
- is Left -> just(value)
- else -> none
+ is First -> Maybe.present(value)
+ else -> Maybe.absent
}
-/** Returns the [Left] value held by this [Either], or `null` if this is a [Right] value. */
-inline fun <A> Either<A, *>.leftOrNull(): A? =
+/** Returns the [First] value held by this [Either], or `null` if this is a [Second] value. */
+inline fun <A> Either<A, *>.firstOrNull(): A? =
when (this) {
- is Left -> value
+ is First -> value
else -> null
}
-/** Returns a [Maybe] containing the [Right] value held by this [Either], if present. */
-inline fun <B> Either<*, B>.rightMaybe(): Maybe<B> =
+/** Returns a [Maybe] containing the [Second] value held by this [Either], if present. */
+inline fun <B> Either<*, B>.secondMaybe(): Maybe<B> =
when (this) {
- is Right -> just(value)
- else -> none
+ is Second -> Maybe.present(value)
+ else -> Maybe.absent
}
-/** Returns the [Right] value held by this [Either], or `null` if this is a [Left] value. */
-inline fun <B> Either<*, B>.rightOrNull(): B? =
+/** Returns the [Second] value held by this [Either], or `null` if this is a [First] value. */
+inline fun <B> Either<*, B>.secondOrNull(): B? =
when (this) {
- is Right -> value
+ is Second -> value
else -> null
}
/**
- * Partitions this sequence of [Either] into two lists; [Pair.first] contains all [Left] values, and
- * [Pair.second] contains all [Right] values.
+ * Returns a [These] containing either the [First] value as [These.first], or the [Second] value as
+ * [These.second]. Will never return a [These.both].
+ */
+fun <A, B> Either<A, B>.asThese(): These<A, B> =
+ when (this) {
+ is Second -> These.second(value)
+ is First -> These.first(value)
+ }
+
+/**
+ * Partitions this sequence of [Either] into two lists; [Pair.first] contains all [First] values,
+ * and [Pair.second] contains all [Second] values.
*/
fun <A, B> Sequence<Either<A, B>>.partitionEithers(): Pair<List<A>, List<B>> {
- val lefts = mutableListOf<A>()
- val rights = mutableListOf<B>()
+ val firsts = mutableListOf<A>()
+ val seconds = mutableListOf<B>()
for (either in this) {
when (either) {
- is Left -> lefts.add(either.value)
- is Right -> rights.add(either.value)
+ is First -> firsts.add(either.value)
+ is Second -> seconds.add(either.value)
}
}
- return lefts to rights
+ return firsts to seconds
}
/**
- * Partitions this map of [Either] values into two maps; [Pair.first] contains all [Left] values,
- * and [Pair.second] contains all [Right] values.
+ * Partitions this map of [Either] values into two maps; [Pair.first] contains all [First] values,
+ * and [Pair.second] contains all [Second] values.
*/
fun <K, A, B> Map<K, Either<A, B>>.partitionEithers(): Pair<Map<K, A>, Map<K, B>> {
- val lefts = mutableMapOf<K, A>()
- val rights = mutableMapOf<K, B>()
+ val firsts = mutableMapOf<K, A>()
+ val seconds = mutableMapOf<K, B>()
for ((k, e) in this) {
when (e) {
- is Left -> lefts[k] = e.value
- is Right -> rights[k] = e.value
+ is First -> firsts[k] = e.value
+ is Second -> seconds[k] = e.value
}
}
- return lefts to rights
+ return firsts to seconds
}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/MapPatch.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/MapPatch.kt
index f368cbf8f124..8fe41bc20dfa 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/MapPatch.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/MapPatch.kt
@@ -16,9 +16,9 @@
package com.android.systemui.kairos.util
-import com.android.systemui.kairos.util.Either.Left
-import com.android.systemui.kairos.util.Either.Right
-import com.android.systemui.kairos.util.Maybe.Just
+import com.android.systemui.kairos.util.Either.First
+import com.android.systemui.kairos.util.Either.Second
+import com.android.systemui.kairos.util.Maybe.Present
/** A "patch" that can be used to batch-update a [Map], via [applyPatch]. */
typealias MapPatch<K, V> = Map<K, Maybe<V>>
@@ -27,16 +27,16 @@ typealias MapPatch<K, V> = Map<K, Maybe<V>>
* Returns a new [Map] that has [patch] applied to the original map.
*
* For each entry in [patch]:
- * * a [Just] value will be included in the new map, replacing the entry in the original map with
+ * * a [Present] value will be included in the new map, replacing the entry in the original map with
* the same key, if present.
- * * a [Maybe.None] value will be omitted from the new map, excluding the entry in the original map
- * with the same key, if present.
+ * * a [Maybe.Absent] value will be omitted from the new map, excluding the entry in the original
+ * map with the same key, if present.
*/
fun <K, V> Map<K, V>.applyPatch(patch: MapPatch<K, V>): Map<K, V> {
val (adds: List<Pair<K, V>>, removes: List<K>) =
patch
.asSequence()
- .map { (k, v) -> if (v is Just) Left(k to v.value) else Right(k) }
+ .map { (k, v) -> if (v is Present) First(k to v.value) else Second(k) }
.partitionEithers()
val removed: Map<K, V> = this - removes.toSet()
val updated: Map<K, V> = removed + adds
@@ -47,11 +47,11 @@ fun <K, V> Map<K, V>.applyPatch(patch: MapPatch<K, V>): Map<K, V> {
* Returns a [MapPatch] that, when applied, includes all of the values from the original [Map].
*
* Shorthand for:
- * ```kotlin
- * mapValues { just(it.value) }
+ * ``` kotlin
+ * mapValues { (key, value) -> Maybe.present(value) }
* ```
*/
-fun <K, V> Map<K, V>.toMapPatch(): MapPatch<K, V> = mapValues { just(it.value) }
+fun <K, V> Map<K, V>.toMapPatch(): MapPatch<K, V> = mapValues { Maybe.present(it.value) }
/**
* Returns a [MapPatch] that, when applied, includes all of the entries from [new] whose keys are
@@ -67,10 +67,10 @@ fun <K, V> mapPatchFromKeyDiff(old: Map<K, V>, new: Map<K, V>): MapPatch<K, V> {
val adds = new - old.keys
return buildMap {
for (removed in removes) {
- put(removed, none)
+ put(removed, Maybe.absent)
}
for ((newKey, newValue) in adds) {
- put(newKey, just(newValue))
+ put(newKey, Maybe.present(newValue))
}
}
}
@@ -86,13 +86,16 @@ fun <K, V> mapPatchFromKeyDiff(old: Map<K, V>, new: Map<K, V>): MapPatch<K, V> {
*/
fun <K, V> mapPatchFromFullDiff(old: Map<K, V>, new: Map<K, V>): MapPatch<K, V> {
val removes = old.keys - new.keys
- val adds = new.mapMaybeValues { (k, v) -> if (k in old && v == old[k]) none else just(v) }
+ val adds =
+ new.mapMaybeValues { (k, v) ->
+ if (k in old && v == old[k]) Maybe.absent else Maybe.present(v)
+ }
return hashMapOf<K, Maybe<V>>().apply {
for (removed in removes) {
- put(removed, none)
+ put(removed, Maybe.absent)
}
for ((newKey, newValue) in adds) {
- put(newKey, just(newValue))
+ put(newKey, Maybe.present(newValue))
}
}
}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Maybe.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Maybe.kt
index 681218399d93..4754bc443329 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Maybe.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Maybe.kt
@@ -18,8 +18,8 @@
package com.android.systemui.kairos.util
-import com.android.systemui.kairos.util.Maybe.Just
-import com.android.systemui.kairos.util.Maybe.None
+import com.android.systemui.kairos.util.Maybe.Absent
+import com.android.systemui.kairos.util.Maybe.Present
import kotlin.coroutines.Continuation
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
@@ -31,17 +31,28 @@ import kotlin.coroutines.suspendCoroutine
/** Represents a value that may or may not be present. */
sealed interface Maybe<out A> {
/** A [Maybe] value that is present. */
- @JvmInline value class Just<out A> internal constructor(val value: A) : Maybe<A>
+ @JvmInline value class Present<out A> internal constructor(val value: A) : Maybe<A>
/** A [Maybe] value that is not present. */
- data object None : Maybe<Nothing>
+ data object Absent : Maybe<Nothing>
+
+ companion object {
+ /** Returns a [Maybe] containing [value]. */
+ fun <A> present(value: A): Maybe<A> = Present(value)
+
+ /** A [Maybe] that is not present. */
+ val absent: Maybe<Nothing> = Absent
+
+ /** A [Maybe] that is not present. */
+ inline fun <A> absent(): Maybe<A> = Absent
+ }
}
/** Utilities to query [Maybe] instances from within a [maybe] block. */
@RestrictsSuspension
object MaybeScope {
suspend operator fun <A> Maybe<A>.not(): A = suspendCoroutine { k ->
- if (this is Just) k.resume(value)
+ if (this is Present) k.resume(value)
}
suspend inline fun guard(crossinline block: () -> Boolean): Unit = suspendCoroutine { k ->
@@ -53,7 +64,8 @@ object MaybeScope {
* Returns a [Maybe] value produced by evaluating [block].
*
* [block] can use its [MaybeScope] receiver to query other [Maybe] values, automatically cancelling
- * execution of [block] and producing [None] when attempting to query a [Maybe] that is not present.
+ * execution of [block] and producing [Absent] when attempting to query a [Maybe] that is not
+ * present.
*
* This can be used instead of Kotlin's built-in nullability (`?.` and `?:`) operators when dealing
* with complex combinations of nullables:
@@ -68,33 +80,30 @@ object MaybeScope {
* ```
*/
fun <A> maybe(block: suspend MaybeScope.() -> A): Maybe<A> {
- var maybeResult: Maybe<A> = None
+ var maybeResult: Maybe<A> = Absent
val k =
object : Continuation<A> {
override val context: CoroutineContext = EmptyCoroutineContext
override fun resumeWith(result: Result<A>) {
- maybeResult = result.getOrNull()?.let { just(it) } ?: None
+ maybeResult = result.getOrNull()?.let { Maybe.present(it) } ?: Absent
}
}
block.startCoroutine(MaybeScope, k)
return maybeResult
}
-/** Returns a [Just] containing this value, or [None] if `null`. */
+/** Returns a [Maybe] containing this value if it is not `null`. */
inline fun <A> (A?).toMaybe(): Maybe<A> = maybe(this)
-/** Returns a [Just] containing a non-null [value], or [None] if `null`. */
-inline fun <A> maybe(value: A?): Maybe<A> = value?.let(::just) ?: None
+/** Returns a [Maybe] containing [value] if it is not `null`. */
+inline fun <A> maybe(value: A?): Maybe<A> = value?.let { Maybe.present(it) } ?: Absent
-/** Returns a [Just] containing [value]. */
-fun <A> just(value: A): Maybe<A> = Just(value)
+/** Returns a [Maybe] that is absent. */
+fun <A> maybeOf(): Maybe<A> = Absent
-/** A [Maybe] that is not present. */
-val none: Maybe<Nothing> = None
-
-/** A [Maybe] that is not present. */
-inline fun <A> none(): Maybe<A> = None
+/** Returns a [Maybe] containing [value]. */
+fun <A> maybeOf(value: A): Maybe<A> = Present(value)
/** Returns the value present in this [Maybe], or `null` if not present. */
inline fun <A> Maybe<A>.orNull(): A? = orElse(null)
@@ -105,22 +114,22 @@ inline fun <A> Maybe<A>.orNull(): A? = orElse(null)
*/
inline fun <A, B> Maybe<A>.map(transform: (A) -> B): Maybe<B> =
when (this) {
- is Just -> just(transform(value))
- is None -> None
+ is Present -> Maybe.present(transform(value))
+ is Absent -> Absent
}
/** Returns the result of applying [transform] to the value in the original [Maybe]. */
inline fun <A, B> Maybe<A>.flatMap(transform: (A) -> Maybe<B>): Maybe<B> =
when (this) {
- is Just -> transform(value)
- is None -> None
+ is Present -> transform(value)
+ is Absent -> Absent
}
/** Returns the value present in this [Maybe], or the result of [defaultValue] if not present. */
inline fun <A> Maybe<A>.orElseGet(defaultValue: () -> A): A =
when (this) {
- is Just -> value
- is None -> defaultValue()
+ is Present -> value
+ is Absent -> defaultValue()
}
/**
@@ -132,8 +141,8 @@ inline fun <A> Maybe<A>.orError(getMessage: () -> Any): A = orElseGet { error(ge
/** Returns the value present in this [Maybe], or [defaultValue] if not present. */
inline fun <A> Maybe<A>.orElse(defaultValue: A): A =
when (this) {
- is Just -> value
- is None -> defaultValue
+ is Present -> value
+ is Absent -> defaultValue
}
/**
@@ -142,15 +151,16 @@ inline fun <A> Maybe<A>.orElse(defaultValue: A): A =
*/
inline fun <A> Maybe<A>.filter(predicate: (A) -> Boolean): Maybe<A> =
when (this) {
- is Just -> if (predicate(value)) this else None
+ is Present -> if (predicate(value)) this else Absent
else -> this
}
/** Returns a [List] containing all values that are present in this [Iterable]. */
-fun <A> Iterable<Maybe<A>>.filterJust(): List<A> = asSequence().filterJust().toList()
+fun <A> Iterable<Maybe<A>>.filterPresent(): List<A> = asSequence().filterPresent().toList()
/** Returns a [List] containing all values that are present in this [Sequence]. */
-fun <A> Sequence<Maybe<A>>.filterJust(): Sequence<A> = filterIsInstance<Just<A>>().map { it.value }
+fun <A> Sequence<Maybe<A>>.filterPresent(): Sequence<A> =
+ filterIsInstance<Present<A>>().map { it.value }
// Align
@@ -160,23 +170,25 @@ fun <A> Sequence<Maybe<A>>.filterJust(): Sequence<A> = filterIsInstance<Just<A>>
*/
inline fun <A, B, C> Maybe<A>.alignWith(other: Maybe<B>, transform: (These<A, B>) -> C): Maybe<C> =
when (this) {
- is Just -> {
+ is Present -> {
val a = value
when (other) {
- is Just -> {
+ is Present -> {
val b = other.value
- just(transform(These.both(a, b)))
+ Maybe.present(transform(These.both(a, b)))
}
- None -> just(transform(These.thiz(a)))
+
+ Absent -> Maybe.present(transform(These.first(a)))
}
}
- None ->
+ Absent ->
when (other) {
- is Just -> {
+ is Present -> {
val b = other.value
- just(transform(These.that(b)))
+ Maybe.present(transform(These.second(b)))
}
- None -> none
+
+ Absent -> Maybe.absent
}
}
@@ -190,7 +202,7 @@ infix fun <A> Maybe<A>.orElseMaybe(other: Maybe<A>): Maybe<A> = orElseGetMaybe {
*/
inline fun <A> Maybe<A>.orElseGetMaybe(other: () -> Maybe<A>): Maybe<A> =
when (this) {
- is Just -> this
+ is Present -> this
else -> other()
}
@@ -235,7 +247,7 @@ fun <A> Maybe<A>.mergeWith(other: Maybe<A>, transform: (A, A) -> A): Maybe<A> =
inline fun <A, B> Iterable<A>.mapMaybe(transform: (A) -> Maybe<B>): List<B> = buildList {
for (a in this@mapMaybe) {
val result = transform(a)
- if (result is Just) {
+ if (result is Present) {
add(result.value)
}
}
@@ -246,7 +258,7 @@ inline fun <A, B> Iterable<A>.mapMaybe(transform: (A) -> Maybe<B>): List<B> = bu
* the original sequence.
*/
fun <A, B> Sequence<A>.mapMaybe(transform: (A) -> Maybe<B>): Sequence<B> =
- map(transform).filterIsInstance<Just<B>>().map { it.value }
+ map(transform).filterIsInstance<Present<B>>().map { it.value }
/**
* Returns a map with values of only the present results of applying [transform] to each entry in
@@ -256,14 +268,14 @@ inline fun <K, A, B> Map<K, A>.mapMaybeValues(transform: (Map.Entry<K, A>) -> Ma
buildMap {
for (entry in this@mapMaybeValues) {
val result = transform(entry)
- if (result is Just) {
+ if (result is Present) {
put(entry.key, result.value)
}
}
}
/** Returns a map with all non-present values filtered out. */
-fun <K, A> Map<K, Maybe<A>>.filterJustValues(): Map<K, A> =
+fun <K, A> Map<K, Maybe<A>>.filterPresentValues(): Map<K, A> =
asSequence().mapMaybe { (key, mValue) -> mValue.map { key to it } }.toMap()
/**
@@ -277,9 +289,9 @@ fun <A, B> Maybe<Pair<A, B>>.splitPair(): Pair<Maybe<A>, Maybe<B>> =
fun <K, V> Map<K, V>.getMaybe(key: K): Maybe<V> {
val value = get(key)
if (value == null && !containsKey(key)) {
- return none
+ return Maybe.absent
} else {
@Suppress("UNCHECKED_CAST")
- return just(value as V)
+ return Maybe.present(value as V)
}
}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/These.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/These.kt
index 092dca4d2f1d..fc7b1e05b6a0 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/These.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/These.kt
@@ -16,28 +16,28 @@
package com.android.systemui.kairos.util
-import com.android.systemui.kairos.util.Maybe.Just
+import com.android.systemui.kairos.util.Maybe.Present
/** Contains at least one of two potential values. */
sealed class These<out A, out B> {
- /** Contains a single potential value. */
- class This<A, B> internal constructor(val thiz: A) : These<A, B>()
+ /** A [These] that contains a [First] value. */
+ class First<A, B> internal constructor(val value: A) : These<A, B>()
- /** Contains a single potential value. */
- class That<A, B> internal constructor(val that: B) : These<A, B>()
+ /** A [These] that contains a [Second] value. */
+ class Second<A, B> internal constructor(val value: B) : These<A, B>()
- /** Contains both potential values. */
- class Both<A, B> internal constructor(val thiz: A, val that: B) : These<A, B>()
+ /** A [These] that contains [Both] a [first] and [second] value. */
+ class Both<A, B> internal constructor(val first: A, val second: B) : These<A, B>()
companion object {
- /** Constructs a [These] containing only [thiz]. */
- fun <A> thiz(thiz: A): These<A, Nothing> = This(thiz)
+ /** Constructs a [These] containing the first possibility. */
+ fun <A> first(value: A): These<A, Nothing> = First(value)
- /** Constructs a [These] containing only [that]. */
- fun <B> that(that: B): These<Nothing, B> = That(that)
+ /** Constructs a [These] containing the second possibility. */
+ fun <B> second(value: B): These<Nothing, B> = Second(value)
- /** Constructs a [These] containing both [thiz] and [that]. */
- fun <A, B> both(thiz: A, that: B): These<A, B> = Both(thiz, that)
+ /** Constructs a [These] containing both possibilities. */
+ fun <A, B> both(first: A, second: B): These<A, B> = Both(first, second)
}
}
@@ -47,87 +47,88 @@ sealed class These<out A, out B> {
*/
inline fun <A> These<A, A>.merge(f: (A, A) -> A): A =
when (this) {
- is These.This -> thiz
- is These.That -> that
- is These.Both -> f(thiz, that)
+ is These.First -> value
+ is These.Second -> value
+ is These.Both -> f(first, second)
}
-/** Returns the [These.This] [value][These.This.thiz] present in this [These] as a [Maybe]. */
-fun <A> These<A, *>.maybeThis(): Maybe<A> =
+/** Returns the [These.First] [value][These.First.value] present in this [These] as a [Maybe]. */
+fun <A> These<A, *>.maybeFirst(): Maybe<A> =
when (this) {
- is These.Both -> just(thiz)
- is These.That -> none
- is These.This -> just(thiz)
+ is These.Both -> Maybe.present(first)
+ is These.Second -> Maybe.absent
+ is These.First -> Maybe.present(value)
}
/**
- * Returns the [These.This] [value][These.This.thiz] present in this [These], or `null` if not
+ * Returns the [These.First] [value][These.First.value] present in this [These], or `null` if not
* present.
*/
-fun <A : Any> These<A, *>.thisOrNull(): A? =
+fun <A : Any> These<A, *>.firstOrNull(): A? =
when (this) {
- is These.Both -> thiz
- is These.That -> null
- is These.This -> thiz
+ is These.Both -> first
+ is These.Second -> null
+ is These.First -> value
}
-/** Returns the [These.That] [value][These.That.that] present in this [These] as a [Maybe]. */
-fun <A> These<*, A>.maybeThat(): Maybe<A> =
+/** Returns the [These.Second] [value][These.Second.value] present in this [These] as a [Maybe]. */
+fun <A> These<*, A>.maybeSecond(): Maybe<A> =
when (this) {
- is These.Both -> just(that)
- is These.That -> just(that)
- is These.This -> none
+ is These.Both -> Maybe.present(second)
+ is These.Second -> Maybe.present(value)
+ is These.First -> Maybe.absent
}
/**
- * Returns the [These.That] [value][These.That.that] present in this [These], or `null` if not
+ * Returns the [These.Second] [value][These.Second.value] present in this [These], or `null` if not
* present.
*/
-fun <A : Any> These<*, A>.thatOrNull(): A? =
+fun <A : Any> These<*, A>.secondOrNull(): A? =
when (this) {
- is These.Both -> that
- is These.That -> that
- is These.This -> null
+ is These.Both -> second
+ is These.Second -> value
+ is These.First -> null
}
/** Returns [These.Both] values present in this [These] as a [Maybe]. */
fun <A, B> These<A, B>.maybeBoth(): Maybe<Pair<A, B>> =
when (this) {
- is These.Both -> just(thiz to that)
- else -> none
+ is These.Both -> Maybe.present(first to second)
+ else -> Maybe.absent
}
-/** Returns a [These] containing [thiz] and/or [that] if they are present. */
-fun <A, B> these(thiz: Maybe<A>, that: Maybe<B>): Maybe<These<A, B>> =
- when (thiz) {
- is Just ->
- just(
- when (that) {
- is Just -> These.both(thiz.value, that.value)
- else -> These.thiz(thiz.value)
+/** Returns a [These] containing [first] and/or [second] if they are present. */
+fun <A, B> these(first: Maybe<A>, second: Maybe<B>): Maybe<These<A, B>> =
+ when (first) {
+ is Present ->
+ Maybe.present(
+ when (second) {
+ is Present -> These.both(first.value, second.value)
+ else -> These.first(first.value)
}
)
+
else ->
- when (that) {
- is Just -> just(These.that(that.value))
- else -> none
+ when (second) {
+ is Present -> Maybe.present(These.second(second.value))
+ else -> Maybe.absent
}
}
/**
- * Returns a [These] containing [thiz] and/or [that] if they are non-null, or `null` if both are
+ * Returns a [These] containing [first] and/or [second] if they are non-null, or `null` if both are
* `null`.
*/
-fun <A : Any, B : Any> theseNull(thiz: A?, that: B?): These<A, B>? =
- thiz?.let { that?.let { These.both(thiz, that) } ?: These.thiz(thiz) }
- ?: that?.let { These.that(that) }
+fun <A : Any, B : Any> theseNotNull(first: A?, second: B?): These<A, B>? =
+ first?.let { second?.let { These.both(first, second) } ?: These.first(first) }
+ ?: second?.let { These.second(second) }
/**
- * Returns two maps, with [Pair.first] containing all [These.This] values and [Pair.second]
- * containing all [These.That] values.
+ * Returns two maps, with [Pair.first] containing all [These.First] values and [Pair.second]
+ * containing all [These.Second] values.
*
* If the value is [These.Both], then the associated key with appear in both output maps, bound to
- * [These.Both.thiz] and [These.Both.that] in each respective output.
+ * [These.Both.first] and [These.Both.second] in each respective output.
*/
fun <K, A, B> Map<K, These<A, B>>.partitionThese(): Pair<Map<K, A>, Map<K, B>> {
val a = mutableMapOf<K, A>()
@@ -135,14 +136,14 @@ fun <K, A, B> Map<K, These<A, B>>.partitionThese(): Pair<Map<K, A>, Map<K, B>> {
for ((k, t) in this) {
when (t) {
is These.Both -> {
- a[k] = t.thiz
- b[k] = t.that
+ a[k] = t.first
+ b[k] = t.second
}
- is These.That -> {
- b[k] = t.that
+ is These.Second -> {
+ b[k] = t.value
}
- is These.This -> {
- a[k] = t.thiz
+ is These.First -> {
+ a[k] = t.value
}
}
}
diff --git a/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosSamples.kt b/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosSamples.kt
new file mode 100644
index 000000000000..88a5b7a4966f
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosSamples.kt
@@ -0,0 +1,774 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.kairos
+
+import com.android.systemui.kairos.util.MapPatch
+import com.android.systemui.kairos.util.These
+import com.android.systemui.kairos.util.maybeOf
+import com.android.systemui.kairos.util.toMaybe
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestDispatcher
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class KairosSamples {
+
+ @Test fun test_mapMaybe() = runSample { mapMaybe() }
+
+ fun BuildScope.mapMaybe() {
+ val emitter = MutableEvents<String>()
+ val ints = emitter.mapMaybe { it.toIntOrNull().toMaybe() }
+
+ var observedInput: String? = null
+ emitter.observe { observedInput = it }
+
+ var observedInt: Int? = null
+ ints.observe { observedInt = it }
+
+ launchEffect {
+ // parse succeeds
+ emitter.emit("6")
+ assertEquals(observedInput, "6")
+ assertEquals(observedInt, 6)
+
+ // parse fails
+ emitter.emit("foo")
+ assertEquals(observedInput, "foo")
+ assertEquals(observedInt, 6)
+
+ // parse succeeds
+ emitter.emit("500")
+ assertEquals(observedInput, "500")
+ assertEquals(observedInt, 500)
+ }
+ }
+
+ @Test fun test_mapCheap() = runSample { mapCheap() }
+
+ fun BuildScope.mapCheap() {
+ val emitter = MutableEvents<Int>()
+
+ var invocationCount = 0
+ val squared =
+ emitter.mapCheap {
+ invocationCount++
+ it * it
+ }
+
+ var observedSquare: Int? = null
+ squared.observe { observedSquare = it }
+
+ launchEffect {
+ emitter.emit(10)
+ assertTrue(invocationCount >= 1)
+ assertEquals(observedSquare, 100)
+
+ emitter.emit(2)
+ assertTrue(invocationCount >= 2)
+ assertEquals(observedSquare, 4)
+ }
+ }
+
+ @Test fun test_mapEvents() = runSample { mapEvents() }
+
+ fun BuildScope.mapEvents() {
+ val emitter = MutableEvents<Int>()
+
+ val squared = emitter.map { it * it }
+
+ var observedSquare: Int? = null
+ squared.observe { observedSquare = it }
+
+ launchEffect {
+ emitter.emit(10)
+ assertEquals(observedSquare, 100)
+
+ emitter.emit(2)
+ assertEquals(observedSquare, 4)
+ }
+ }
+
+ @Test fun test_eventsLoop() = runSample { eventsLoop() }
+
+ fun BuildScope.eventsLoop() {
+ val emitter = MutableEvents<Unit>()
+ var newCount: Events<Int> by EventsLoop()
+ val count = newCount.holdState(0)
+ newCount = emitter.map { count.sample() + 1 }
+
+ var observedCount = 0
+ count.observe { observedCount = it }
+
+ launchEffect {
+ emitter.emit(Unit)
+ assertEquals(observedCount, expected = 1)
+
+ emitter.emit(Unit)
+ assertEquals(observedCount, expected = 2)
+ }
+ }
+
+ @Test fun test_stateLoop() = runSample { stateLoop() }
+
+ fun BuildScope.stateLoop() {
+ val emitter = MutableEvents<Unit>()
+ var count: State<Int> by StateLoop()
+ count = emitter.map { count.sample() + 1 }.holdState(0)
+
+ var observedCount = 0
+ count.observe { observedCount = it }
+
+ launchEffect {
+ emitter.emit(Unit)
+ assertEquals(observedCount, expected = 1)
+
+ emitter.emit(Unit)
+ assertEquals(observedCount, expected = 2)
+ }
+ }
+
+ @Test fun test_changes() = runSample { changes() }
+
+ fun BuildScope.changes() {
+ val emitter = MutableEvents<Int>()
+ val state = emitter.holdState(0)
+
+ var numEmissions = 0
+ emitter.observe { numEmissions++ }
+
+ var observedState = 0
+ var numChangeEmissions = 0
+ state.changes.observe {
+ observedState = it
+ numChangeEmissions++
+ }
+
+ launchEffect {
+ emitter.emit(0)
+ assertEquals(numEmissions, expected = 1)
+ assertEquals(numChangeEmissions, expected = 0)
+ assertEquals(observedState, expected = 0)
+
+ emitter.emit(5)
+ assertEquals(numEmissions, expected = 2)
+ assertEquals(numChangeEmissions, expected = 1)
+ assertEquals(observedState, expected = 5)
+
+ emitter.emit(3)
+ assertEquals(numEmissions, expected = 3)
+ assertEquals(numChangeEmissions, expected = 2)
+ assertEquals(observedState, expected = 3)
+
+ emitter.emit(3)
+ assertEquals(numEmissions, expected = 4)
+ assertEquals(numChangeEmissions, expected = 2)
+ assertEquals(observedState, expected = 3)
+
+ emitter.emit(5)
+ assertEquals(numEmissions, expected = 5)
+ assertEquals(numChangeEmissions, expected = 3)
+ assertEquals(observedState, expected = 5)
+ }
+ }
+
+ @Test fun test_partitionThese() = runSample { partitionThese() }
+
+ fun BuildScope.partitionThese() {
+ val emitter = MutableEvents<These<Int, String>>()
+ val (lefts, rights) = emitter.partitionThese()
+
+ var observedLeft: Int? = null
+ lefts.observe { observedLeft = it }
+
+ var observedRight: String? = null
+ rights.observe { observedRight = it }
+
+ launchEffect {
+ emitter.emit(These.first(10))
+ assertEquals(observedLeft, 10)
+ assertEquals(observedRight, null)
+
+ emitter.emit(These.both(2, "foo"))
+ assertEquals(observedLeft, 2)
+ assertEquals(observedRight, "foo")
+
+ emitter.emit(These.second("bar"))
+ assertEquals(observedLeft, 2)
+ assertEquals(observedRight, "bar")
+ }
+ }
+
+ @Test fun test_merge() = runSample { merge() }
+
+ fun BuildScope.merge() {
+ val emitter = MutableEvents<Int>()
+ val fizz = emitter.mapNotNull { if (it % 3 == 0) "Fizz" else null }
+ val buzz = emitter.mapNotNull { if (it % 5 == 0) "Buzz" else null }
+ val fizzbuzz = fizz.mergeWith(buzz) { _, _ -> "Fizz Buzz" }
+ val output = mergeLeft(fizzbuzz, emitter.mapCheap { it.toString() })
+
+ var observedOutput: String? = null
+ output.observe { observedOutput = it }
+
+ launchEffect {
+ emitter.emit(1)
+ assertEquals(observedOutput, "1")
+ emitter.emit(2)
+ assertEquals(observedOutput, "2")
+ emitter.emit(3)
+ assertEquals(observedOutput, "Fizz")
+ emitter.emit(4)
+ assertEquals(observedOutput, "4")
+ emitter.emit(5)
+ assertEquals(observedOutput, "Buzz")
+ emitter.emit(6)
+ assertEquals(observedOutput, "Fizz")
+ emitter.emit(15)
+ assertEquals(observedOutput, "Fizz Buzz")
+ }
+ }
+
+ @Test fun test_groupByKey() = runSample { groupByKey() }
+
+ fun BuildScope.groupByKey() {
+ val emitter = MutableEvents<Map<String, Int>>()
+ val grouped = emitter.groupByKey()
+ val groupA = grouped["A"]
+ val groupB = grouped["B"]
+
+ var numEmissions = 0
+ emitter.observe { numEmissions++ }
+
+ var observedA: Int? = null
+ groupA.observe { observedA = it }
+
+ var observedB: Int? = null
+ groupB.observe { observedB = it }
+
+ launchEffect {
+ // emit to group A
+ emitter.emit(mapOf("A" to 3))
+ assertEquals(numEmissions, 1)
+ assertEquals(observedA, 3)
+ assertEquals(observedB, null)
+
+ // emit to groups B and C, even though there are no observers of C
+ emitter.emit(mapOf("B" to 9, "C" to 100))
+ assertEquals(numEmissions, 2)
+ assertEquals(observedA, 3)
+ assertEquals(observedB, 9)
+
+ // emit to groups A and B
+ emitter.emit(mapOf("B" to 6, "A" to 14))
+ assertEquals(numEmissions, 3)
+ assertEquals(observedA, 14)
+ assertEquals(observedB, 6)
+
+ // emit to group with no listeners
+ emitter.emit(mapOf("Q" to -66))
+ assertEquals(numEmissions, 4)
+ assertEquals(observedA, 14)
+ assertEquals(observedB, 6)
+
+ // no-op emission
+ emitter.emit(emptyMap())
+ assertEquals(numEmissions, 5)
+ assertEquals(observedA, 14)
+ assertEquals(observedB, 6)
+ }
+ }
+
+ @Test fun test_switchEvents() = runSample { switchEvents() }
+
+ fun BuildScope.switchEvents() {
+ val negator = MutableEvents<Unit>()
+ val emitter = MutableEvents<Int>()
+ val negate = negator.foldState(false) { _, negate -> !negate }
+ val output =
+ negate.map { negate -> if (negate) emitter.map { it * -1 } else emitter }.switchEvents()
+
+ var observed: Int? = null
+ output.observe { observed = it }
+
+ launchEffect {
+ // emit like normal
+ emitter.emit(10)
+ assertEquals(observed, 10)
+
+ // enable negation
+ observed = null
+ negator.emit(Unit)
+ assertEquals(observed, null)
+
+ emitter.emit(99)
+ assertEquals(observed, -99)
+
+ // disable negation
+ observed = null
+ negator.emit(Unit)
+ emitter.emit(7)
+ assertEquals(observed, 7)
+ }
+ }
+
+ @Test fun test_switchEventsPromptly() = runSample { switchEventsPromptly() }
+
+ fun BuildScope.switchEventsPromptly() {
+ val emitter = MutableEvents<Int>()
+ val enabled = emitter.map { it > 10 }.holdState(false)
+ val switchedIn = enabled.map { enabled -> if (enabled) emitter else emptyEvents }
+ val deferredSwitch = switchedIn.switchEvents()
+ val promptSwitch = switchedIn.switchEventsPromptly()
+
+ var observedDeferred: Int? = null
+ deferredSwitch.observe { observedDeferred = it }
+
+ var observedPrompt: Int? = null
+ promptSwitch.observe { observedPrompt = it }
+
+ launchEffect {
+ emitter.emit(3)
+ assertEquals(observedDeferred, null)
+ assertEquals(observedPrompt, null)
+
+ emitter.emit(20)
+ assertEquals(observedDeferred, null)
+ assertEquals(observedPrompt, 20)
+
+ emitter.emit(30)
+ assertEquals(observedDeferred, 30)
+ assertEquals(observedPrompt, 30)
+
+ emitter.emit(8)
+ assertEquals(observedDeferred, 8)
+ assertEquals(observedPrompt, 8)
+
+ emitter.emit(1)
+ assertEquals(observedDeferred, 8)
+ assertEquals(observedPrompt, 8)
+ }
+ }
+
+ @Test fun test_sampleTransactional() = runSample { sampleTransactional() }
+
+ fun BuildScope.sampleTransactional() {
+ var store = 0
+ val transactional = transactionally { store++ }
+
+ effect {
+ assertEquals(store, 0)
+ assertEquals(transactional.sample(), 0)
+ assertEquals(store, 1)
+ assertEquals(transactional.sample(), 0)
+ assertEquals(store, 1)
+ }
+ }
+
+ @Test fun test_states() = runSample { states() }
+
+ fun BuildScope.states() {
+ val constantState = stateOf(10)
+ effect { assertEquals(constantState.sample(), 10) }
+
+ val mappedConstantState: State<Int> = constantState.map { it * 2 }
+ effect { assertEquals(mappedConstantState.sample(), 20) }
+
+ val emitter = MutableEvents<Int>()
+ val heldState: State<Int?> = emitter.holdState(null)
+ effect { assertEquals(heldState.sample(), null) }
+
+ var observed: Int? = null
+ var wasObserved = false
+ heldState.observe {
+ observed = it
+ wasObserved = true
+ }
+ launchEffect {
+ assertTrue(wasObserved)
+ emitter.emit(4)
+ assertEquals(observed, 4)
+ }
+
+ val combinedStates: State<Pair<Int, Int?>> =
+ combine(mappedConstantState, heldState) { a, b -> Pair(a, b) }
+
+ effect { assertEquals(combinedStates.sample(), 20 to null) }
+
+ var observedPair: Pair<Int, Int?>? = null
+ combinedStates.observe { observedPair = it }
+ launchEffect {
+ emitter.emit(12)
+ assertEquals(observedPair, 20 to 12)
+ }
+ }
+
+ @Test fun test_holdState() = runSample { holdState() }
+
+ fun BuildScope.holdState() {
+ val emitter = MutableEvents<Int>()
+ val heldState: State<Int?> = emitter.holdState(null)
+ effect { assertEquals(heldState.sample(), null) }
+
+ var observed: Int? = null
+ var wasObserved = false
+ heldState.observe {
+ observed = it
+ wasObserved = true
+ }
+ launchEffect {
+ // observation of the initial state took place immediately
+ assertTrue(wasObserved)
+
+ // state changes are also observed
+ emitter.emit(4)
+ assertEquals(observed, 4)
+
+ emitter.emit(20)
+ assertEquals(observed, 20)
+ }
+ }
+
+ @Test fun test_mapState() = runSample { mapState() }
+
+ fun BuildScope.mapState() {
+ val emitter = MutableEvents<Int>()
+ val held: State<Int> = emitter.holdState(0)
+ val squared: State<Int> = held.map { it * it }
+
+ var observed: Int? = null
+ squared.observe { observed = it }
+
+ launchEffect {
+ assertEquals(observed, 0)
+
+ emitter.emit(10)
+ assertEquals(observed, 100)
+ }
+ }
+
+ @Test fun test_combineState() = runSample { combineState() }
+
+ fun BuildScope.combineState() {
+ val emitter = MutableEvents<Int>()
+ val state = emitter.holdState(0)
+ val squared = state.map { it * it }
+ val negated = state.map { -it }
+ val combined = squared.combine(negated) { a, b -> Pair(a, b) }
+
+ val observed = mutableListOf<Pair<Int, Int>>()
+ combined.observe { observed.add(it) }
+
+ launchEffect {
+ emitter.emit(10)
+ emitter.emit(20)
+ emitter.emit(3)
+
+ assertEquals(observed, listOf(0 to 0, 100 to -10, 400 to -20, 9 to -3))
+ }
+ }
+
+ @Test fun test_flatMap() = runSample { flatMap() }
+
+ fun BuildScope.flatMap() {
+ val toggler = MutableEvents<Unit>()
+ val firstEmitter = MutableEvents<Unit>()
+ val secondEmitter = MutableEvents<Unit>()
+
+ val firstCount: State<Int> = firstEmitter.foldState(0) { _, count -> count + 1 }
+ val secondCount: State<Int> = secondEmitter.foldState(0) { _, count -> count + 1 }
+ val toggleState: State<Boolean> = toggler.foldState(true) { _, state -> !state }
+
+ val activeCount: State<Int> =
+ toggleState.flatMap { b -> if (b) firstCount else secondCount }
+
+ var observed: Int? = null
+ activeCount.observe { observed = it }
+
+ launchEffect {
+ assertEquals(observed, 0)
+
+ firstEmitter.emit(Unit)
+ assertEquals(observed, 1)
+
+ secondEmitter.emit(Unit)
+ assertEquals(observed, 1)
+
+ secondEmitter.emit(Unit)
+ assertEquals(observed, 1)
+
+ toggler.emit(Unit)
+ assertEquals(observed, 2)
+
+ toggler.emit(Unit)
+ assertEquals(observed, 1)
+ }
+ }
+
+ @Test fun test_incrementals() = runSample { incrementals() }
+
+ fun BuildScope.incrementals() {
+ val patchEmitter = MutableEvents<MapPatch<String, Int>>()
+ val incremental: Incremental<String, Int> = patchEmitter.foldStateMapIncrementally()
+ val squared = incremental.mapValues { (key, value) -> value * value }
+
+ var observedUpdate: MapPatch<String, Int>? = null
+ squared.updates.observe { observedUpdate = it }
+
+ var observedState: Map<String, Int>? = null
+ squared.observe { observedState = it }
+
+ launchEffect {
+ assertEquals(observedState, emptyMap())
+ assertEquals(observedUpdate, null)
+
+ // add entry: A => 10
+ patchEmitter.emit(mapOf("A" to maybeOf(10)))
+ assertEquals(observedState, mapOf("A" to 100))
+ assertEquals(observedUpdate, mapOf("A" to maybeOf(100)))
+
+ // update entry: A => 5
+ // add entry: B => 6
+ patchEmitter.emit(mapOf("A" to maybeOf(5), "B" to maybeOf(6)))
+ assertEquals(observedState, mapOf("A" to 25, "B" to 36))
+ assertEquals(observedUpdate, mapOf("A" to maybeOf(25), "B" to maybeOf(36)))
+
+ // remove entry: A
+ // add entry: C => 9
+ // remove non-existent entry: F
+ patchEmitter.emit(mapOf("A" to maybeOf(), "C" to maybeOf(9), "F" to maybeOf()))
+ assertEquals(observedState, mapOf("B" to 36, "C" to 81))
+ // non-existent entry is filtered from the update
+ assertEquals(observedUpdate, mapOf("A" to maybeOf(), "C" to maybeOf(81)))
+ }
+ }
+
+ @Test fun test_mergeEventsIncrementally() = runSample(block = mergeEventsIncrementally())
+
+ fun mergeEventsIncrementally(): BuildSpec<Unit> = buildSpec {
+ val patchEmitter = MutableEvents<MapPatch<String, Events<Int>>>()
+ val incremental: Incremental<String, Events<Int>> = patchEmitter.foldStateMapIncrementally()
+ val merged: Events<Map<String, Int>> = incremental.mergeEventsIncrementally()
+
+ var observed: Map<String, Int>? = null
+ merged.observe { observed = it }
+
+ launchEffect {
+ // add events entry: A
+ val emitterA = MutableEvents<Int>()
+ patchEmitter.emit(mapOf("A" to maybeOf(emitterA)))
+
+ emitterA.emit(100)
+ assertEquals(observed, mapOf("A" to 100))
+
+ // add events entry: B
+ val emitterB = MutableEvents<Int>()
+ patchEmitter.emit(mapOf("B" to maybeOf(emitterB)))
+
+ // merged emits from both A and B
+ emitterB.emit(5)
+ assertEquals(observed, mapOf("B" to 5))
+
+ emitterA.emit(20)
+ assertEquals(observed, mapOf("A" to 20))
+
+ // remove entry: A
+ patchEmitter.emit(mapOf("A" to maybeOf()))
+ emitterA.emit(0)
+ // event is not emitted now that A has been removed
+ assertEquals(observed, mapOf("A" to 20))
+
+ // but B still works
+ emitterB.emit(3)
+ assertEquals(observed, mapOf("B" to 3))
+ }
+ }
+
+ @Test
+ fun test_mergeEventsIncrementallyPromptly() =
+ runSample(block = mergeEventsIncrementallyPromptly())
+
+ fun mergeEventsIncrementallyPromptly(): BuildSpec<Unit> = buildSpec {
+ val patchEmitter = MutableEvents<MapPatch<String, Events<Int>>>()
+ val incremental: Incremental<String, Events<Int>> = patchEmitter.foldStateMapIncrementally()
+ val deferredMerge: Events<Map<String, Int>> = incremental.mergeEventsIncrementally()
+ val promptMerge: Events<Map<String, Int>> = incremental.mergeEventsIncrementallyPromptly()
+
+ var observedDeferred: Map<String, Int>? = null
+ deferredMerge.observe { observedDeferred = it }
+
+ var observedPrompt: Map<String, Int>? = null
+ promptMerge.observe { observedPrompt = it }
+
+ launchEffect {
+ val emitterA = MutableEvents<Int>()
+ patchEmitter.emit(mapOf("A" to maybeOf(emitterA)))
+
+ emitterA.emit(100)
+ assertEquals(observedDeferred, mapOf("A" to 100))
+ assertEquals(observedPrompt, mapOf("A" to 100))
+
+ val emitterB = patchEmitter.map { 5 }
+ patchEmitter.emit(mapOf("B" to maybeOf(emitterB)))
+
+ assertEquals(observedDeferred, mapOf("A" to 100))
+ assertEquals(observedPrompt, mapOf("B" to 5))
+ }
+ }
+
+ @Test fun test_applyLatestStateful() = runSample(block = applyLatestStateful())
+
+ fun applyLatestStateful(): BuildSpec<Unit> = buildSpec {
+ val reset = MutableEvents<Unit>()
+ val emitter = MutableEvents<Unit>()
+ val stateEvents: Events<State<Int>> =
+ reset
+ .map { statefully { emitter.foldState(0) { _, count -> count + 1 } } }
+ .applyLatestStateful()
+ val activeState: State<State<Int>?> = stateEvents.holdState(null)
+
+ launchEffect {
+ // nothing is active yet
+ kairosNetwork.transact { assertEquals(activeState.sample(), null) }
+
+ // activate the counter
+ reset.emit(Unit)
+ val firstState =
+ kairosNetwork.transact {
+ assertEquals(activeState.sample()?.sample(), 0)
+ activeState.sample()!!
+ }
+
+ // emit twice
+ emitter.emit(Unit)
+ emitter.emit(Unit)
+ kairosNetwork.transact { assertEquals(firstState.sample(), 2) }
+
+ // start a new counter, disabling the old one
+ reset.emit(Unit)
+ val secondState =
+ kairosNetwork.transact {
+ assertEquals(activeState.sample()?.sample(), 0)
+ activeState.sample()!!
+ }
+ kairosNetwork.transact { assertEquals(firstState.sample(), 2) }
+
+ // emit: the new counter updates, but the old one does not
+ emitter.emit(Unit)
+ kairosNetwork.transact { assertEquals(secondState.sample(), 1) }
+ kairosNetwork.transact { assertEquals(firstState.sample(), 2) }
+ }
+ }
+
+ @Test fun test_applyLatestStatefulForKey() = runSample(block = applyLatestStatefulForKey())
+
+ fun applyLatestStatefulForKey(): BuildSpec<Unit> = buildSpec {
+ val reset = MutableEvents<String>()
+ val emitter = MutableEvents<String>()
+ val stateEvents: Events<MapPatch<String, State<Int>>> =
+ reset
+ .map { key ->
+ mapOf(
+ key to
+ maybeOf(
+ statefully {
+ emitter
+ .filter { it == key }
+ .foldState(0) { _, count -> count + 1 }
+ }
+ )
+ )
+ }
+ .applyLatestStatefulForKey()
+ val activeStatesByKey: Incremental<String, State<Int>> =
+ stateEvents.foldStateMapIncrementally(emptyMap())
+
+ launchEffect {
+ // nothing is active yet
+ kairosNetwork.transact { assertEquals(activeStatesByKey.sample(), emptyMap()) }
+
+ // activate a new entry A
+ reset.emit("A")
+ val firstStateA =
+ kairosNetwork.transact {
+ val stateMap: Map<String, State<Int>> = activeStatesByKey.sample()
+ assertEquals(stateMap.keys, setOf("A"))
+ stateMap.getValue("A").also { assertEquals(it.sample(), 0) }
+ }
+
+ // emit twice to A
+ emitter.emit("A")
+ emitter.emit("A")
+ kairosNetwork.transact { assertEquals(firstStateA.sample(), 2) }
+
+ // active a new entry B
+ reset.emit("B")
+ val firstStateB =
+ kairosNetwork.transact {
+ val stateMap: Map<String, State<Int>> = activeStatesByKey.sample()
+ assertEquals(stateMap.keys, setOf("A", "B"))
+ stateMap.getValue("B").also {
+ assertEquals(it.sample(), 0)
+ assertEquals(firstStateA.sample(), 2)
+ }
+ }
+
+ // emit once to B
+ emitter.emit("B")
+ kairosNetwork.transact {
+ assertEquals(firstStateA.sample(), 2)
+ assertEquals(firstStateB.sample(), 1)
+ }
+
+ // activate a new entry for A, disabling the old entry
+ reset.emit("A")
+ val secondStateA =
+ kairosNetwork.transact {
+ val stateMap: Map<String, State<Int>> = activeStatesByKey.sample()
+ assertEquals(stateMap.keys, setOf("A", "B"))
+ stateMap.getValue("A").also {
+ assertEquals(it.sample(), 0)
+ assertEquals(firstStateB.sample(), 1)
+ }
+ }
+
+ // emit to A: the new A state updates, but the old one does not
+ emitter.emit("A")
+ kairosNetwork.transact {
+ assertEquals(firstStateA.sample(), 2)
+ assertEquals(secondStateA.sample(), 1)
+ }
+ }
+ }
+
+ private fun runSample(
+ dispatcher: TestDispatcher = UnconfinedTestDispatcher(),
+ block: BuildScope.() -> Unit,
+ ) {
+ runTest(dispatcher, timeout = 1.seconds) {
+ val kairosNetwork = backgroundScope.launchKairosNetwork()
+ backgroundScope.launch { kairosNetwork.activateSpec { block() } }
+ }
+ }
+}
+
+private fun <T> assertEquals(actual: T, expected: T) = Assert.assertEquals(expected, actual)
diff --git a/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosTests.kt b/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosTests.kt
index 150b462df655..ffe6e955e884 100644
--- a/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosTests.kt
+++ b/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosTests.kt
@@ -1,14 +1,12 @@
package com.android.systemui.kairos
import com.android.systemui.kairos.util.Either
-import com.android.systemui.kairos.util.Either.Left
-import com.android.systemui.kairos.util.Either.Right
+import com.android.systemui.kairos.util.Either.First
+import com.android.systemui.kairos.util.Either.Second
import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.Maybe.None
-import com.android.systemui.kairos.util.just
+import com.android.systemui.kairos.util.Maybe.Absent
import com.android.systemui.kairos.util.map
import com.android.systemui.kairos.util.maybe
-import com.android.systemui.kairos.util.none
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import kotlin.time.DurationUnit
@@ -142,7 +140,7 @@ class KairosTests {
// convert Eventss to States so that they can be combined
val combined =
- left.holdState("left" to 0).combineWith(right.holdState("right" to 0)) { l, r ->
+ left.holdState("left" to 0).combine(right.holdState("right" to 0)) { l, r ->
l to r
}
combined.changes // get State changes
@@ -590,7 +588,7 @@ class KairosTests {
intStopEmitter.emit(Unit) // intAH.complete()
runCurrent()
- // assertEquals(just(10), network.await())
+ // assertEquals(present(10), network.await())
}
@Test
@@ -692,12 +690,12 @@ class KairosTests {
}
runCurrent()
- emitter.emit(Left(10))
+ emitter.emit(First(10))
runCurrent()
assertEquals(20, result.value)
- emitter.emit(Right(30))
+ emitter.emit(Second(30))
runCurrent()
assertEquals(-30, result.value)
@@ -908,7 +906,7 @@ class KairosTests {
activateSpecWithResult(network) {
val bA = updater.map { it * 2 }.holdState(0)
val bB = updater.holdState(0)
- val combineD: State<Pair<Int, Int>> = bA.combineWith(bB) { a, b -> a to b }
+ val combineD: State<Pair<Int, Int>> = bA.combine(bB) { a, b -> a to b }
val sampleS = emitter.sample(combineD) { _, b -> b }
sampleS.nextDeferred()
}
@@ -1142,16 +1140,13 @@ class KairosTests {
val eRemoved =
childChangeById
.eventsForKey(childId)
- .filter { it === None }
+ .filter { it === Absent }
.onEach {
println(
"removing? (groupId=$groupId, childId=$childId)"
)
}
- .nextOnly(
- name =
- "eRemoved(groupId=$groupId, childId=$childId)"
- )
+ .nextOnly()
val addChild: Events<Maybe<State<String>>> =
now.map { mChild }
@@ -1168,13 +1163,9 @@ class KairosTests {
"removeChild (groupId=$groupId, childId=$childId)"
)
}
- .map { none() }
+ .map { Maybe.absent() }
- addChild.mergeWith(
- removeChild,
- name =
- "childUpdatesMerged(groupId=$groupId, childId=$childId)",
- ) { _, _ ->
+ addChild.mergeWith(removeChild) { _, _ ->
error("unexpected coincidence")
}
}
@@ -1182,7 +1173,7 @@ class KairosTests {
}
val mergeIncrementally: Events<Map<Int, Maybe<State<String>>>> =
map.onEach { println("merge patch: $it") }
- .mergeIncrementallyPromptly(name = "mergeIncrementally")
+ .mergeEventsIncrementallyPromptly()
mergeIncrementally
.onEach { println("foldmap patch: $it") }
.foldStateMapIncrementally()
@@ -1203,14 +1194,14 @@ class KairosTests {
val emitter2 = network.mutableEvents<Map<Int, Maybe<StateFlow<String>>>>()
println()
println("init outer 0")
- e.emit(mapOf(0 to just(emitter2.onEach { println("emitter2 emit: $it") })))
+ e.emit(mapOf(0 to Maybe.present(emitter2.onEach { println("emitter2 emit: $it") })))
runCurrent()
assertEquals(mapOf(0 to emptyMap()), state.value)
println()
println("init inner 10")
- emitter2.emit(mapOf(10 to just(MutableStateFlow("(0, 10)"))))
+ emitter2.emit(mapOf(10 to Maybe.present(MutableStateFlow("(0, 10)"))))
runCurrent()
assertEquals(mapOf(0 to mapOf(10 to "(0, 10)")), state.value)
@@ -1218,19 +1209,19 @@ class KairosTests {
// replace
println()
println("replace inner 10")
- emitter2.emit(mapOf(10 to just(MutableStateFlow("(1, 10)"))))
+ emitter2.emit(mapOf(10 to Maybe.present(MutableStateFlow("(1, 10)"))))
runCurrent()
assertEquals(mapOf(0 to mapOf(10 to "(1, 10)")), state.value)
// remove
- emitter2.emit(mapOf(10 to none()))
+ emitter2.emit(mapOf(10 to Maybe.absent()))
runCurrent()
assertEquals(mapOf(0 to emptyMap()), state.value)
// add again
- emitter2.emit(mapOf(10 to just(MutableStateFlow("(2, 10)"))))
+ emitter2.emit(mapOf(10 to Maybe.present(MutableStateFlow("(2, 10)"))))
runCurrent()
assertEquals(mapOf(0 to mapOf(10 to "(2, 10)")), state.value)
@@ -1242,9 +1233,9 @@ class KairosTests {
// batch update
emitter2.emit(
mapOf(
- 10 to none(),
- 11 to just(MutableStateFlow("(0, 11)")),
- 12 to just(MutableStateFlow("(0, 12)")),
+ 10 to Maybe.absent(),
+ 11 to Maybe.present(MutableStateFlow("(0, 11)")),
+ 12 to Maybe.present(MutableStateFlow("(0, 12)")),
)
)
runCurrent()
@@ -1278,7 +1269,7 @@ class KairosTests {
}
var outerCount = 0
- val laseventss: StateFlow<Pair<StateFlow<Int?>, StateFlow<Int?>>> =
+ val lastEvent: StateFlow<Pair<StateFlow<Int?>, StateFlow<Int?>>> =
flowOfFlows
.map { it.stateIn(backgroundScope, SharingStarted.Eagerly, null) }
.pairwise(MutableStateFlow(null))
@@ -1296,18 +1287,18 @@ class KairosTests {
assertEquals(1, outerCount)
// assertEquals(1, incCount.subscriptionCount)
- assertNull(laseventss.value.second.value)
+ assertNull(lastEvent.value.second.value)
incCount.emit(Unit)
runCurrent()
println("checking")
- assertEquals(1, laseventss.value.second.value)
+ assertEquals(1, lastEvent.value.second.value)
incCount.emit(Unit)
runCurrent()
- assertEquals(2, laseventss.value.second.value)
+ assertEquals(2, lastEvent.value.second.value)
newCount.emit(newFlow())
runCurrent()
@@ -1315,9 +1306,9 @@ class KairosTests {
runCurrent()
// verify old flow is not getting updates
- assertEquals(2, laseventss.value.first.value)
+ assertEquals(2, lastEvent.value.first.value)
// but the new one is
- assertEquals(1, laseventss.value.second.value)
+ assertEquals(1, lastEvent.value.second.value)
}
@Test
@@ -1326,7 +1317,7 @@ class KairosTests {
var observedCount: Int? = null
activateSpec(network) {
val (c, j) = asyncScope { input.foldState(0) { _, x -> x + 1 } }
- deferredBuildScopeAction { c.get().observe { observedCount = it } }
+ deferredBuildScopeAction { c.value.observe { observedCount = it } }
}
runCurrent()
assertEquals(0, observedCount)
@@ -1385,7 +1376,7 @@ class KairosTests {
activateSpec(network) {
val handle =
input.observe {
- effectCoroutineScope.launch {
+ launch {
runningCount++
awaitClose { runningCount-- }
}
@@ -1420,7 +1411,7 @@ class KairosTests {
val specJob =
activateSpec(network) {
input.takeUntil(stopper).observe {
- effectCoroutineScope.launch {
+ launch {
runningCount++
awaitClose { runningCount-- }
}