summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Steve Elliott <steell@google.com> 2024-12-06 15:45:38 -0500
committer Steve Elliott <steell@google.com> 2025-01-03 15:58:34 -0500
commitcdff97f1f5b958a8d0a3977b3d62dcd12a89b49e (patch)
tree6d0fe5ff8d3e5708ad3d441c7cd0f9d977ee1a8a
parentd6865b295361c40202506a86aeaa6dfbbbb6e5c7 (diff)
[kairos] rename many APIs
* TState -> State * TFlow -> Events * FrpBuildScope -> BuildScope * FrpStateScope -> StateScope * FrpTransactionScope -> TransactionScope * FrpEffectScope -> EffectScope * FrpScope -> KairosScope etc. Flag: EXEMPT unused Test: atest kairos-tests Change-Id: I56eb686be46833539c93d8c56c5a2eec93af54b6
-rw-r--r--packages/SystemUI/utils/kairos/Android.bp3
-rw-r--r--packages/SystemUI/utils/kairos/README.md17
-rw-r--r--packages/SystemUI/utils/kairos/docs/flow-to-kairos-cheatsheet.md245
-rw-r--r--packages/SystemUI/utils/kairos/docs/semantics.md100
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/BuildScope.kt862
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combinators.kt237
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/EffectScope.kt48
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Events.kt577
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpBuildScope.kt887
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpEffectScope.kt49
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpNetwork.kt197
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpScope.kt60
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpStateScope.kt799
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpTransactionScope.kt63
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosNetwork.kt216
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosScope.kt57
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/State.kt528
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/StateScope.kt760
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TFlow.kt566
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TState.kt491
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TransactionScope.kt78
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Transactional.kt70
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/debug/Debug.kt52
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/BuildScopeImpl.kt279
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Demux.kt8
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/EvalScopeImpl.kt77
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/EventsImpl.kt (renamed from packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/TFlowImpl.kt)6
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/FilterNode.kt8
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Graph.kt4
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Inputs.kt2
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/InternalScopes.kt35
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Mux.kt14
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxDeferred.kt44
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxPrompt.kt28
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Network.kt12
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/NoScope.kt8
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/PullNodes.kt10
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateImpl.kt (renamed from packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/TStateImpl.kt)138
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateScopeImpl.kt226
-rw-r--r--packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosTests.kt370
40 files changed, 4087 insertions, 4144 deletions
diff --git a/packages/SystemUI/utils/kairos/Android.bp b/packages/SystemUI/utils/kairos/Android.bp
index 1442591eab99..e10de9978252 100644
--- a/packages/SystemUI/utils/kairos/Android.bp
+++ b/packages/SystemUI/utils/kairos/Android.bp
@@ -22,7 +22,7 @@ package {
java_library {
name: "kairos",
host_supported: true,
- kotlincflags: ["-opt-in=com.android.systemui.kairos.ExperimentalFrpApi"],
+ kotlincflags: ["-opt-in=com.android.systemui.kairos.ExperimentalKairosApi"],
srcs: ["src/**/*.kt"],
static_libs: [
"kotlin-stdlib",
@@ -32,6 +32,7 @@ java_library {
java_test {
name: "kairos-test",
+ kotlincflags: ["-opt-in=com.android.systemui.kairos.ExperimentalKairosApi"],
optimize: {
enabled: false,
},
diff --git a/packages/SystemUI/utils/kairos/README.md b/packages/SystemUI/utils/kairos/README.md
index 85f622ca05f3..5174c45a8d82 100644
--- a/packages/SystemUI/utils/kairos/README.md
+++ b/packages/SystemUI/utils/kairos/README.md
@@ -22,22 +22,21 @@ you can view the semantics for `Kairos` [here](docs/semantics.md).
## Usage
-First, stand up a new `FrpNetwork`. All reactive events and state is kept
+First, stand up a new `KairosNetwork`. All reactive events and state is kept
consistent within a single network.
``` kotlin
val coroutineScope: CoroutineScope = ...
-val frpNetwork = coroutineScope.newFrpNetwork()
+val network = coroutineScope.launchKairosNetwork()
```
-You can use the `FrpNetwork` to stand-up a network of reactive events and state.
-Events are modeled with `TFlow` (short for "transactional flow"), and state
-`TState` (short for "transactional state").
+You can use the `KairosNetwork` to stand-up a network of reactive events and
+state. Events are modeled with `Events`, and states with `State`.
``` kotlin
-suspend fun activate(network: FrpNetwork) {
+suspend fun activate(network: KairosNetwork) {
network.activateSpec {
- val input = network.mutableTFlow<Unit>()
+ val input = network.mutableEvents<Unit>()
// Launch a long-running side-effect that emits to the network
// every second.
launchEffect {
@@ -47,7 +46,7 @@ suspend fun activate(network: FrpNetwork) {
}
}
// Accumulate state
- val count: TState<Int> = input.fold { _, i -> i + 1 }
+ val count: State<Int> = input.foldState { _, i -> i + 1 }
// Observe events to perform side-effects in reaction to them
input.observe {
println("Got event ${count.sample()} at time: ${System.currentTimeMillis()}")
@@ -56,7 +55,7 @@ suspend fun activate(network: FrpNetwork) {
}
```
-`FrpNetwork.activateSpec` will suspend indefinitely; cancelling the invocation
+`KairosNetwork.activateSpec` will suspend indefinitely; cancelling the invocation
will tear-down all effects and obervers running within the lambda.
## Resources
diff --git a/packages/SystemUI/utils/kairos/docs/flow-to-kairos-cheatsheet.md b/packages/SystemUI/utils/kairos/docs/flow-to-kairos-cheatsheet.md
index 9f7fd022f019..afe64377676d 100644
--- a/packages/SystemUI/utils/kairos/docs/flow-to-kairos-cheatsheet.md
+++ b/packages/SystemUI/utils/kairos/docs/flow-to-kairos-cheatsheet.md
@@ -2,117 +2,116 @@
## Key differences
-* Kairos evaluates all events (`TFlow` emissions + observers) in a transaction.
+* Kairos evaluates all events (`Events` emissions + observers) in a transaction.
-* Kairos splits `Flow` APIs into two distinct types: `TFlow` and `TState`
+* Kairos splits `Flow` APIs into two distinct types: `Events` and `State`
- * `TFlow` is roughly equivalent to `SharedFlow` w/ a replay cache that
+ * `Events` is roughly equivalent to `SharedFlow` w/ a replay cache that
exists for the duration of the current Kairos transaction and shared with
`SharingStarted.WhileSubscribed()`
- * `TState` is roughly equivalent to `StateFlow` shared with
+ * `State` is roughly equivalent to `StateFlow` shared with
`SharingStarted.Eagerly`, but the current value can only be queried within
a Kairos transaction, and the value is only updated at the end of the
transaction
* Kairos further divides `Flow` APIs based on how they internally use state:
- * **FrpTransactionScope:** APIs that internally query some state need to be
+ * **TransactionScope:** APIs that internally query some state need to be
performed within an Kairos transaction
* this scope is available from the other scopes, and from most lambdas
passed to other Kairos APIs
- * **FrpStateScope:** APIs that internally accumulate state in reaction to
- events need to be performed within an FRP State scope (akin to a
- `CoroutineScope`)
+ * **StateScope:** APIs that internally accumulate state in reaction to events
+ need to be performed within a State scope (akin to a `CoroutineScope`)
- * this scope is a side-effect-free subset of FrpBuildScope, and so can be
- used wherever you have an FrpBuildScope
+ * this scope is a side-effect-free subset of BuildScope, and so can be
+ used wherever you have an BuildScope
- * **FrpBuildScope:** APIs that perform external side-effects (`Flow.collect`)
- need to be performed within an FRP Build scope (akin to a `CoroutineScope`)
+ * **BuildScope:** APIs that perform external side-effects (`Flow.collect`)
+ need to be performed within a Build scope (akin to a `CoroutineScope`)
- * this scope is available from `FrpNetwork.activateSpec { … }`
+ * this scope is available from `Network.activateSpec { … }`
* All other APIs can be used anywhere
## emptyFlow()
-Use `emptyTFlow`
+Use `emptyEvents`
``` kotlin
-// this TFlow emits nothing
-val noEvents: TFlow<Int> = emptyTFlow
+// this Events emits nothing
+val noEvents: Events<Int> = emptyEvents
```
## map { … }
-Use `TFlow.map` / `TState.map`
+Use `Events.map` / `State.map`
``` kotlin
-val anInt: TState<Int> = …
-val squared: TState<Int> = anInt.map { it * it }
-val messages: TFlow<String> = …
-val messageLengths: TFlow<Int> = messages.map { it.size }
+val anInt: State<Int> = …
+val squared: State<Int> = anInt.map { it * it }
+val messages: Events<String> = …
+val messageLengths: Events<Int> = messages.map { it.size }
```
## filter { … } / mapNotNull { … }
-### I have a TFlow
+### I have an Events
-Use `TFlow.filter` / `TFlow.mapNotNull`
+Use `Events.filter` / `Events.mapNotNull`
``` kotlin
-val messages: TFlow<String> = …
-val nonEmpty: TFlow<String> = messages.filter { it.isNotEmpty() }
+val messages: Events<String> = …
+val nonEmpty: Events<String> = messages.filter { it.isNotEmpty() }
```
-### I have a TState
+### I have a State
-Convert the `TState` to `TFlow` using `TState.stateChanges`, then use
-`TFlow.filter` / `TFlow.mapNotNull`
+Convert the `State` to `Events` using `State.stateChanges`, then use
+`Events.filter` / `Events.mapNotNull`
-If you need to convert back to `TState`, use `TFlow.hold(initialValue)` on the
-result.
+If you need to convert back to `State`, use `Events.holdState(initialValue)` on
+the result.
``` kotlin
-tState.stateChanges.filter { … }.hold(initialValue)
+state.stateChanges.filter { … }.holdState(initialValue)
```
-Note that `TFlow.hold` is only available within an `FrpStateScope` in order to
-track the lifetime of the state accumulation.
+Note that `Events.holdState` is only available within an `StateScope` in order
+to track the lifetime of the state accumulation.
## combine(...) { … }
-### I have TStates
+### I have States
-Use `combine(TStates)`
+Use `combine(States)`
``` kotlin
-val someInt: TState<Int> = …
-val someString: TState<String> = …
-val model: TState<MyModel> = combine(someInt, someString) { i, s -> MyModel(i, s) }
+val someInt: State<Int> = …
+val someString: State<String> = …
+val model: State<MyModel> = combine(someInt, someString) { i, s -> MyModel(i, s) }
```
-### I have TFlows
+### I have Events
-Convert the TFlows to TStates using `TFlow.hold(initialValue)`, then use
-`combine(TStates)`
+Convert the Events to States using `Events.holdState(initialValue)`, then use
+`combine(States)`
If you want the behavior of Flow.combine where nothing is emitted until each
-TFlow has emitted at least once, you can use filter:
+Events has emitted at least once, you can use filter:
``` kotlin
// null used as an example, can use a different sentinel if needed
-combine(tFlowA.hold(null), tFlowB.hold(null)) { a, b ->
+combine(eventsA.holdState(null), eventsB.holdState(null)) { a, b ->
a?.let { b?.let { … } }
}
.filterNotNull()
```
-Note that `TFlow.hold` is only available within an `FrpStateScope` in order to
-track the lifetime of the state accumulation.
+Note that `Events.holdState` is only available within an `StateScope` in order
+to track the lifetime of the state accumulation.
#### Explanation
@@ -126,7 +125,7 @@ has emitted at least once. This often bites developers. As a workaround,
developers generally append `.onStart { emit(initialValue) }` to the `Flows`
that don't immediately emit.
-Kairos avoids this gotcha by forcing usage of `TState` for `combine`, thus
+Kairos avoids this gotcha by forcing usage of `State` for `combine`, thus
ensuring that there is always a current value to be combined for each input.
## collect { … }
@@ -134,197 +133,197 @@ ensuring that there is always a current value to be combined for each input.
Use `observe { … }`
``` kotlin
-val job: Job = tFlow.observe { println("observed: $it") }
+val job: Job = events.observe { println("observed: $it") }
```
-Note that `observe` is only available within an `FrpBuildScope` in order to
-track the lifetime of the observer. `FrpBuildScope` can only come from a
-top-level `FrpNetwork.transaction { … }`, or a sub-scope created by using a
-`-Latest` operator.
+Note that `observe` is only available within a `BuildScope` in order to track
+the lifetime of the observer. `BuildScope` can only come from a top-level
+`Network.transaction { … }`, or a sub-scope created by using a `-Latest`
+operator.
## sample(flow) { … }
-### I want to sample a TState
+### I want to sample a State
-Use `TState.sample()` to get the current value of a `TState`. This can be
-invoked anywhere you have access to an `FrpTransactionScope`.
+Use `State.sample()` to get the current value of a `State`. This can be
+invoked anywhere you have access to an `TransactionScope`.
``` kotlin
-// the lambda passed to map receives an FrpTransactionScope, so it can invoke
+// the lambda passed to map receives an TransactionScope, so it can invoke
// sample
-tFlow.map { tState.sample() }
+events.map { state.sample() }
```
#### Explanation
-To keep all state-reads consistent, the current value of a TState can only be
-queried within a Kairos transaction, modeled with `FrpTransactionScope`. Note
-that both `FrpStateScope` and `FrpBuildScope` extend `FrpTransactionScope`.
+To keep all state-reads consistent, the current value of a State can only be
+queried within a Kairos transaction, modeled with `TransactionScope`. Note that
+both `StateScope` and `BuildScope` extend `TransactionScope`.
-### I want to sample a TFlow
+### I want to sample an Events
-Convert to a `TState` by using `TFlow.hold(initialValue)`, then use `sample`.
+Convert to a `State` by using `Events.holdState(initialValue)`, then use `sample`.
-Note that `hold` is only available within an `FrpStateScope` in order to track
+Note that `holdState` is only available within an `StateScope` in order to track
the lifetime of the state accumulation.
## stateIn(scope, sharingStarted, initialValue)
-Use `TFlow.hold(initialValue)`. There is no need to supply a sharingStarted
-argument; all states are accumulated eagerly.
+Use `Events.holdState(initialValue)`. There is no need to supply a
+sharingStarted argument; all states are accumulated eagerly.
``` kotlin
-val ints: TFlow<Int> = …
-val lastSeenInt: TState<Int> = ints.hold(initialValue = 0)
+val ints: Events<Int> = …
+val lastSeenInt: State<Int> = ints.holdState(initialValue = 0)
```
-Note that `hold` is only available within an `FrpStateScope` in order to track
+Note that `holdState` is only available within an `StateScope` in order to track
the lifetime of the state accumulation (akin to the scope parameter of
-`Flow.stateIn`). `FrpStateScope` can only come from a top-level
-`FrpNetwork.transaction { … }`, or a sub-scope created by using a `-Latest`
-operator. Also note that `FrpBuildScope` extends `FrpStateScope`.
+`Flow.stateIn`). `StateScope` can only come from a top-level
+`Network.transaction { … }`, or a sub-scope created by using a `-Latest`
+operator. Also note that `BuildScope` extends `StateScope`.
## distinctUntilChanged()
-Use `distinctUntilChanged` like normal. This is only available for `TFlow`;
-`TStates` are already `distinctUntilChanged`.
+Use `distinctUntilChanged` like normal. This is only available for `Events`;
+`States` are already `distinctUntilChanged`.
## merge(...)
-### I have TFlows
+### I have Eventss
-Use `merge(TFlows) { … }`. The lambda argument is used to disambiguate multiple
+Use `merge(Events) { … }`. The lambda argument is used to disambiguate multiple
simultaneous emissions within the same transaction.
#### Explanation
-Under Kairos's rules, a `TFlow` may only emit up to once per transaction. This
-means that if we are merging two or more `TFlows` that are emitting at the same
-time (within the same transaction), the resulting merged `TFlow` must emit a
+Under Kairos's rules, an `Events` may only emit up to once per transaction. This
+means that if we are merging two or more `Events` that are emitting at the same
+time (within the same transaction), the resulting merged `Events` must emit a
single value. The lambda argument allows the developer to decide what to do in
this case.
-### I have TStates
+### I have States
-If `combine` doesn't satisfy your needs, you can use `TState.stateChanges` to
-convert to a `TFlow`, and then `merge`.
+If `combine` doesn't satisfy your needs, you can use `State.changes` to
+convert to a `Events`, and then `merge`.
## conflatedCallbackFlow { … }
-Use `tFlow { … }`.
+Use `events { … }`.
As a shortcut, if you already have a `conflatedCallbackFlow { … }`, you can
-convert it to a TFlow via `Flow.toTFlow()`.
+convert it to an Events via `Flow.toEvents()`.
-Note that `tFlow` is only available within an `FrpBuildScope` in order to track
-the lifetime of the input registration.
+Note that `events` is only available within a `BuildScope` in order to track the
+lifetime of the input registration.
## first()
-### I have a TState
+### I have a State
-Use `TState.sample`.
+Use `State.sample`.
-### I have a TFlow
+### I have an Events
-Use `TFlow.nextOnly`, which works exactly like `Flow.first` but instead of
-suspending it returns a `TFlow` that emits once.
+Use `Events.nextOnly`, which works exactly like `Flow.first` but instead of
+suspending it returns a `Events` that emits once.
The naming is intentionally different because `first` implies that it is the
first-ever value emitted from the `Flow` (which makes sense for cold `Flows`),
whereas `nextOnly` indicates that only the next value relative to the current
transaction (the one `nextOnly` is being invoked in) will be emitted.
-Note that `nextOnly` is only available within an `FrpStateScope` in order to
-track the lifetime of the state accumulation.
+Note that `nextOnly` is only available within an `StateScope` in order to track
+the lifetime of the state accumulation.
## flatMapLatest { … }
If you want to use -Latest to cancel old side-effects, similar to what the Flow
-Latest operators offer for coroutines, see `mapLatest`.
-### I have a TState…
+### I have a State…
-#### …and want to switch TStates
+#### …and want to switch States
-Use `TState.flatMap`
+Use `State.flatMap`
``` kotlin
-val flattened = tState.flatMap { a -> getTState(a) }
+val flattened = state.flatMap { a -> gestate(a) }
```
-#### …and want to switch TFlows
+#### …and want to switch Events
-Use `TState<TFlow<T>>.switch()`
+Use `State<Events<T>>.switchEvents()`
``` kotlin
-val tFlow = tState.map { a -> getTFlow(a) }.switch()
+val events = state.map { a -> getEvents(a) }.switchEvents()
```
-### I have a TFlow…
+### I have an Events…
-#### …and want to switch TFlows
+#### …and want to switch Events
-Use `hold` to convert to a `TState<TFlow<T>>`, then use `switch` to switch to
-the latest `TFlow`.
+Use `holdState` to convert to a `State<Events<T>>`, then use `switchEvents` to
+switch to the latest `Events`.
``` kotlin
-val tFlow = tFlowOfFlows.hold(emptyTFlow).switch()
+val events = eventsOfFlows.holdState(emptyEvents).switchEvents()
```
-#### …and want to switch TStates
+#### …and want to switch States
-Use `hold` to convert to a `TState<TState<T>>`, then use `flatMap` to switch to
-the latest `TState`.
+Use `holdState` to convert to a `State<State<T>>`, then use `flatMap` to switch
+to the latest `State`.
``` kotlin
-val tState = tFlowOfStates.hold(tStateOf(initialValue)).flatMap { it }
+val state = eventsOfStates.holdState(stateOf(initialValue)).flatMap { it }
```
## mapLatest { … } / collectLatest { … }
-`FrpStateScope` and `FrpBuildScope` both provide `-Latest` operators that
+`StateScope` and `BuildScope` both provide `-Latest` operators that
automatically cancel old work when new values are emitted.
``` kotlin
-val currentModel: TState<SomeModel> = …
-val mapped: TState<...> = currentModel.mapLatestBuild { model ->
+val currentModel: State<SomeModel> = …
+val mapped: State<...> = currentModel.mapLatestBuild { model ->
effect { "new model in the house: $model" }
model.someState.observe { "someState: $it" }
- val someData: TState<SomeInfo> =
+ val someData: State<SomeInfo> =
getBroadcasts(model.uri)
.map { extractInfo(it) }
- .hold(initialInfo)
+ .holdState(initialInfo)
}
```
## flowOf(...)
-### I want a TState
+### I want a State
-Use `tStateOf(initialValue)`.
+Use `stateOf(initialValue)`.
-### I want a TFlow
+### I want an Events
Use `now.map { initialValue }`
-Note that `now` is only available within an `FrpTransactionScope`.
+Note that `now` is only available within an `TransactionScope`.
#### Explanation
-`TFlows` are not cold, and so there isn't a notion of "emit this value once
+`Events` are not cold, and so there isn't a notion of "emit this value once
there is a collector" like there is for `Flow`. The closest analog would be
-`TState`, since the initial value is retained indefinitely until there is an
+`State`, since the initial value is retained indefinitely until there is an
observer. However, it is often useful to immediately emit a value within the
-current transaction, usually when using a `flatMap` or `switch`. In these cases,
-using `now` explicitly models that the emission will occur within the current
-transaction.
+current transaction, usually when using a `flatMap` or `switchEvents`. In these
+cases, using `now` explicitly models that the emission will occur within the
+current transaction.
``` kotlin
-fun <T> FrpTransactionScope.tFlowOf(value: T): TFlow<T> = now.map { value }
+fun <T> TransactionScope.eventsOf(value: T): Events<T> = now.map { value }
```
## MutableStateFlow / MutableSharedFlow
-Use `MutableTState(frpNetwork, initialValue)` and `MutableTFlow(frpNetwork)`.
+Use `MutableState(frpNetwork, initialValue)` and `MutableEvents(frpNetwork)`.
diff --git a/packages/SystemUI/utils/kairos/docs/semantics.md b/packages/SystemUI/utils/kairos/docs/semantics.md
index d43bb4447061..c8e468050037 100644
--- a/packages/SystemUI/utils/kairos/docs/semantics.md
+++ b/packages/SystemUI/utils/kairos/docs/semantics.md
@@ -33,39 +33,39 @@ sealed class Time : Comparable<Time> {
typealias Transactional<T> = (Time) -> T
-typealias TFlow<T> = SortedMap<Time, T>
+typealias Events<T> = SortedMap<Time, T>
private fun <T> SortedMap<Time, T>.pairwise(): List<Pair<Pair<Time, T>, Pair<Time<T>>>> =
// NOTE: pretend evaluation is lazy, so that error() doesn't immediately throw
(toList() + Pair(Time.Infinity, error("no value"))).zipWithNext()
-class TState<T> internal constructor(
+class State<T> internal constructor(
internal val current: Transactional<T>,
- val stateChanges: TFlow<T>,
+ val stateChanges: Events<T>,
)
-val emptyTFlow: TFlow<Nothing> = emptyMap()
+val emptyEvents: Events<Nothing> = emptyMap()
-fun <A, B> TFlow<A>.map(f: FrpTransactionScope.(A) -> B): TFlow<B> =
- mapValues { (t, a) -> FrpTransactionScope(t).f(a) }
+fun <A, B> Events<A>.map(f: TransactionScope.(A) -> B): Events<B> =
+ mapValues { (t, a) -> TransactionScope(t).f(a) }
-fun <A> TFlow<A>.filter(f: FrpTransactionScope.(A) -> Boolean): TFlow<A> =
- filter { (t, a) -> FrpTransactionScope(t).f(a) }
+fun <A> Events<A>.filter(f: TransactionScope.(A) -> Boolean): Events<A> =
+ filter { (t, a) -> TransactionScope(t).f(a) }
fun <A> merge(
- first: TFlow<A>,
- second: TFlow<A>,
+ first: Events<A>,
+ second: Events<A>,
onCoincidence: Time.(A, A) -> A,
-): TFlow<A> =
+): Events<A> =
first.toMutableMap().also { result ->
second.forEach { (t, a) ->
result.merge(t, a) { f, s ->
- FrpTranscationScope(t).onCoincidence(f, a)
+ TransactionScope(t).onCoincidence(f, a)
}
}
}.toSortedMap()
-fun <A> TState<TFlow<A>>.switch(): TFlow<A> {
+fun <A> State<Events<A>>.switchEvents(): Events<A> {
val truncated = listOf(Pair(Time.BigBang, current.invoke(Time.BigBang))) +
stateChanges.dropWhile { (time, _) -> time < time0 }
val events =
@@ -77,7 +77,7 @@ fun <A> TState<TFlow<A>>.switch(): TFlow<A> {
return events.toSortedMap()
}
-fun <A> TState<TFlow<A>>.switchPromptly(): TFlow<A> {
+fun <A> State<Events<A>>.switchEventsPromptly(): Events<A> {
val truncated = listOf(Pair(Time.BigBang, current.invoke(Time.BigBang))) +
stateChanges.dropWhile { (time, _) -> time < time0 }
val events =
@@ -89,24 +89,24 @@ fun <A> TState<TFlow<A>>.switchPromptly(): TFlow<A> {
return events.toSortedMap()
}
-typealias GroupedTFlow<K, V> = TFlow<Map<K, V>>
+typealias GroupedEvents<K, V> = Events<Map<K, V>>
-fun <K, V> TFlow<Map<K, V>>.groupByKey(): GroupedTFlow<K, V> = this
+fun <K, V> Events<Map<K, V>>.groupByKey(): GroupedEvents<K, V> = this
-fun <K, V> GroupedTFlow<K, V>.eventsForKey(key: K): TFlow<V> =
+fun <K, V> GroupedEvents<K, V>.eventsForKey(key: K): Events<V> =
map { m -> m[k] }.filter { it != null }.map { it!! }
-fun <A, B> TState<A>.map(f: (A) -> B): TState<B> =
- TState(
+fun <A, B> State<A>.map(f: (A) -> B): State<B> =
+ State(
current = { t -> f(current.invoke(t)) },
stateChanges = stateChanges.map { f(it) },
)
-fun <A, B, C> TState<A>.combineWith(
- other: TState<B>,
+fun <A, B, C> State<A>.combineWith(
+ other: State<B>,
f: (A, B) -> C,
-): TState<C> =
- TState(
+): State<C> =
+ State(
current = { t -> f(current.invoke(t), other.current.invoke(t)) },
stateChanges = run {
val aChanges =
@@ -129,7 +129,7 @@ fun <A, B, C> TState<A>.combineWith(
},
)
-fun <A> TState<TState<A>>.flatten(): TState<A> {
+fun <A> State<State<A>>.flatten(): State<A> {
val changes =
stateChanges
.pairwise()
@@ -144,55 +144,55 @@ fun <A> TState<TState<A>>.flatten(): TState<A> {
inWindow
}
}
- return TState(
+ return State(
current = { t -> current.invoke(t).current.invoke(t) },
stateChanges = changes.toSortedMap(),
)
}
-open class FrpTranscationScope internal constructor(
+open class TransactionScope internal constructor(
internal val currentTime: Time,
) {
- val now: TFlow<Unit> =
+ val now: Events<Unit> =
sortedMapOf(currentTime to Unit)
fun <A> Transactional<A>.sample(): A =
invoke(currentTime)
- fun <A> TState<A>.sample(): A =
+ fun <A> State<A>.sample(): A =
current.sample()
}
-class FrpStateScope internal constructor(
+class StateScope internal constructor(
time: Time,
internal val stopTime: Time,
-): FrpTransactionScope(time) {
+): TransactionScope(time) {
- fun <A, B> TFlow<A>.fold(
+ fun <A, B> Events<A>.foldState(
initialValue: B,
- f: FrpTransactionScope.(B, A) -> B,
- ): TState<B> {
+ f: TransactionScope.(B, A) -> B,
+ ): State<B> {
val truncated =
dropWhile { (t, _) -> t < currentTime }
.takeWhile { (t, _) -> t <= stopTime }
- val folded =
+ val foldStateed =
truncated
.scan(Pair(currentTime, initialValue)) { (_, b) (t, a) ->
- Pair(t, FrpTransactionScope(t).f(a, b))
+ Pair(t, TransactionScope(t).f(a, b))
}
val lookup = { t1 ->
- folded.lastOrNull { (t0, _) -> t0 < t1 }?.value ?: initialValue
+ foldStateed.lastOrNull { (t0, _) -> t0 < t1 }?.value ?: initialValue
}
- return TState(lookup, folded.toSortedMap())
+ return State(lookup, foldStateed.toSortedMap())
}
- fun <A> TFlow<A>.hold(initialValue: A): TState<A> =
- fold(initialValue) { _, a -> a }
+ fun <A> Events<A>.holdState(initialValue: A): State<A> =
+ foldState(initialValue) { _, a -> a }
- fun <K, V> TFlow<Map<K, Maybe<V>>>.foldMapIncrementally(
+ fun <K, V> Events<Map<K, Maybe<V>>>.foldStateMapIncrementally(
initialValues: Map<K, V>
- ): TState<Map<K, V>> =
- fold(initialValues) { patch, map ->
+ ): State<Map<K, V>> =
+ foldState(initialValues) { patch, map ->
val eithers = patch.map { (k, v) ->
if (v is Just) Left(k to v.value) else Right(k)
}
@@ -203,18 +203,18 @@ class FrpStateScope internal constructor(
updated
}
- fun <K : Any, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementally(
- initialTFlows: Map<K, TFlow<V>>,
- ): TFlow<Map<K, V>> =
- foldMapIncrementally(initialTFlows).map { it.merge() }.switch()
+ fun <K : Any, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally(
+ initialEventss: Map<K, Events<V>>,
+ ): Events<Map<K, V>> =
+ foldStateMapIncrementally(initialEventss).map { it.merge() }.switchEvents()
- fun <K, A, B> TFlow<Map<K, Maybe<A>>.mapLatestStatefulForKey(
- transform: suspend FrpStateScope.(A) -> B,
- ): TFlow<Map<K, Maybe<B>>> =
+ fun <K, A, B> Events<Map<K, Maybe<A>>.mapLatestStatefulForKey(
+ transform: suspend StateScope.(A) -> B,
+ ): Events<Map<K, Maybe<B>>> =
pairwise().map { ((t0, patch), (t1, _)) ->
patch.map { (k, ma) ->
ma.map { a ->
- FrpStateScope(t0, t1).transform(a)
+ StateScope(t0, t1).transform(a)
}
}
}
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
new file mode 100644
index 000000000000..3cba163dcb5f
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/BuildScope.kt
@@ -0,0 +1,862 @@
+/*
+ * 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.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.Job
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.FlowCollector
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+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. */
+typealias BuildSpec<A> = BuildScope.() -> A
+
+/**
+ * Constructs a [BuildSpec]. The passed [block] will be invoked with a [BuildScope] that can be used
+ * to perform network-building operations, including adding new inputs and outputs to the network,
+ * as well as all operations available in [TransactionScope].
+ */
+@ExperimentalKairosApi
+@Suppress("NOTHING_TO_INLINE")
+inline fun <A> buildSpec(noinline block: BuildScope.() -> A): BuildSpec<A> = block
+
+/** Applies the [BuildSpec] within this [BuildScope]. */
+@ExperimentalKairosApi
+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
+
+ /**
+ * Defers invoking [block] until after the current [BuildScope] code-path completes, returning a
+ * [DeferredValue] that can be used to reference the result.
+ *
+ * Useful for recursive definitions.
+ *
+ * @see deferredBuildScopeAction
+ * @see DeferredValue
+ */
+ fun <R> deferredBuildScope(block: BuildScope.() -> R): DeferredValue<R>
+
+ /**
+ * Defers invoking [block] until after the current [BuildScope] code-path completes.
+ *
+ * Useful for recursive definitions.
+ *
+ * @see deferredBuildScope
+ */
+ fun deferredBuildScopeAction(block: BuildScope.() -> Unit)
+
+ /**
+ * Returns an [Events] containing the results of applying [transform] to each value of the
+ * original [Events].
+ *
+ * [transform] can perform modifications to the Kairos network via its [BuildScope] receiver.
+ * Unlike [mapLatestBuild], these modifications are not undone with each subsequent emission of
+ * the original [Events].
+ *
+ * **NOTE:** This API does not [observe] the original [Events], meaning that unless the returned
+ * (or a downstream) [Events] is observed separately, [transform] will not be invoked, and no
+ * internal side-effects will occur.
+ */
+ fun <A, B> Events<A>.mapBuild(transform: BuildScope.(A) -> B): Events<B>
+
+ /**
+ * Invokes [block] whenever this [Events] emits a value, allowing side-effects to be safely
+ * performed in reaction to the emission.
+ *
+ * Specifically, [block] is deferred to the end of the transaction, and is only actually
+ * 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 { ... } }
+ * ```
+ */
+ fun <A> Events<A>.observe(
+ coroutineContext: CoroutineContext = EmptyCoroutineContext,
+ block: EffectScope.(A) -> Unit = {},
+ ): Job
+
+ /**
+ * Returns an [Events] containing the results of applying each [BuildSpec] emitted from the
+ * original [Events], and a [DeferredValue] containing the result of applying [initialSpecs]
+ * immediately.
+ *
+ * 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] contained within the value for an associated key is [none], then the
+ * previously-active [BuildSpec] will be undone with no replacement.
+ */
+ fun <K, A, B> Events<Map<K, Maybe<BuildSpec<A>>>>.applyLatestSpecForKey(
+ initialSpecs: DeferredValue<Map<K, BuildSpec<B>>>,
+ numKeys: Int? = null,
+ ): Pair<Events<Map<K, Maybe<A>>>, DeferredValue<Map<K, B>>>
+
+ /**
+ * Creates an instance of an [Events] with elements that are from [builder].
+ *
+ * [builder] is run in its own coroutine, allowing for ongoing work that can emit to the
+ * provided [MutableState].
+ *
+ * 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() }
+ * ```
+ */
+ fun <T> events(
+ name: String? = null,
+ 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].
+ *
+ * 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() }
+ * ```
+ *
+ * In the event of backpressure, emissions are *coalesced* into batches. When a value is
+ * [emitted][CoalescingEventProducerScope.emit] from [builder], it is merged into the batch via
+ * [coalesce]. Once the batch is consumed by the kairos network in the next transaction, the
+ * batch is reset back to [getInitialValue].
+ */
+ fun <In, Out> coalescingEvents(
+ getInitialValue: () -> Out,
+ coalesce: (old: Out, new: In) -> Out,
+ builder: suspend CoalescingEventProducerScope<In>.() -> Unit,
+ ): Events<Out>
+
+ /**
+ * Creates a new [BuildScope] that is a child of this one.
+ *
+ * This new scope can be manually cancelled via the returned [Job], or will be cancelled
+ * automatically when its parent is cancelled. Cancellation will unregister all
+ * [observers][observe] and cancel all scheduled [effects][effect].
+ *
+ * The return value from [block] can be accessed via the returned [DeferredValue].
+ */
+ fun <A> asyncScope(block: BuildSpec<A>): Pair<DeferredValue<A>, Job>
+
+ // TODO: once we have context params, these can all become extensions:
+
+ /**
+ * Returns an [Events] containing the results of applying the given [transform] function to each
+ * value of the original [Events].
+ *
+ * Unlike [Events.map], [transform] can perform arbitrary asynchronous code. This code is run
+ * 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()
+ * ```
+ */
+ fun <A, B> Events<A>.mapAsyncLatest(transform: suspend (A) -> B): Events<B> =
+ mapLatestBuild { a -> asyncEvent { transform(a) } }.flatten()
+
+ /**
+ * Invokes [block] whenever this [Events] emits a value. [block] receives an [BuildScope] that
+ * can be used to make further modifications to the Kairos network, and/or perform side-effects
+ * via [effect].
+ *
+ * @see observe
+ */
+ fun <A> Events<A>.observeBuild(block: BuildScope.(A) -> Unit = {}): Job =
+ mapBuild(block).observe()
+
+ /**
+ * 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."
+ )
+ }
+
+ return object : StateFlow<A> {
+ override val replayCache: List<A>
+ get() = innerStateFlow.replayCache.map(::getValue)
+
+ override val value: A
+ get() = getValue(innerStateFlow.value)
+
+ override suspend fun collect(collector: FlowCollector<A>): Nothing {
+ innerStateFlow.collect { collector.emit(getValue(it)) }
+ }
+ }
+ }
+
+ /**
+ * Returns a [SharedFlow] configured with a replay cache of size [replay] that emits the current
+ * [value][State.sample] of this [State] followed by all [changes].
+ */
+ fun <A> State<A>.toSharedFlow(replay: Int = 0): SharedFlow<A> {
+ val result = MutableSharedFlow<A>(replay, extraBufferCapacity = 1)
+ deferredBuildScope {
+ result.tryEmit(sample())
+ changes.observe { a -> result.tryEmit(a) }
+ }
+ return result
+ }
+
+ /**
+ * Returns a [SharedFlow] configured with a replay cache of size [replay] that emits values
+ * whenever this [Events] emits.
+ */
+ fun <A> Events<A>.toSharedFlow(replay: Int = 0): SharedFlow<A> {
+ val result = MutableSharedFlow<A>(replay, extraBufferCapacity = 1)
+ observe { a -> result.tryEmit(a) }
+ return result
+ }
+
+ /**
+ * Returns a [State] that holds onto the value returned by applying the most recently emitted
+ * [BuildSpec] from the original [Events], or the value returned by applying [initialSpec] if
+ * nothing has been emitted since it was constructed.
+ *
+ * When each [BuildSpec] is applied, changes from the previously-active [BuildSpec] are undone
+ * (any registered [observers][observe] are unregistered, and any pending [side-effects][effect]
+ * are cancelled).
+ */
+ fun <A> Events<BuildSpec<A>>.holdLatestSpec(initialSpec: BuildSpec<A>): State<A> {
+ val (changes: Events<A>, initApplied: DeferredValue<A>) = applyLatestSpec(initialSpec)
+ return changes.holdStateDeferred(initApplied)
+ }
+
+ /**
+ * Returns a [State] containing the value returned by applying the [BuildSpec] held by the
+ * original [State].
+ *
+ * When each [BuildSpec] is applied, changes from the previously-active [BuildSpec] are undone
+ * (any registered [observers][observe] are unregistered, and any pending [side-effects][effect]
+ * are cancelled).
+ */
+ fun <A> State<BuildSpec<A>>.applyLatestSpec(): State<A> {
+ val (appliedChanges: Events<A>, init: DeferredValue<A>) =
+ changes.applyLatestSpec(buildSpec { sample().applySpec() })
+ return appliedChanges.holdStateDeferred(init)
+ }
+
+ /**
+ * Returns an [Events] containing the results of applying each [BuildSpec] emitted from the
+ * original [Events].
+ *
+ * When each [BuildSpec] is applied, changes from the previously-active [BuildSpec] are undone
+ * (any registered [observers][observe] are unregistered, and any pending [side-effects][effect]
+ * are cancelled).
+ */
+ fun <A> Events<BuildSpec<A>>.applyLatestSpec(): Events<A> = applyLatestSpec(buildSpec {}).first
+
+ /**
+ * Returns an [Events] that switches to a new [Events] produced by [transform] every time the
+ * original [Events] emits a value.
+ *
+ * [transform] can perform modifications to the Kairos network via its [BuildScope] receiver.
+ * When the original [Events] emits a new value, those changes are undone (any registered
+ * [observers][observe] are unregistered, and any pending [effects][effect] are cancelled).
+ */
+ fun <A, B> Events<A>.flatMapLatestBuild(transform: BuildScope.(A) -> Events<B>): Events<B> =
+ mapCheap { buildSpec { transform(it) } }.applyLatestSpec().flatten()
+
+ /**
+ * Returns a [State] by applying [transform] to the value held by the original [State].
+ *
+ * [transform] can perform modifications to the Kairos network via its [BuildScope] receiver.
+ * When the value held by the original [State] changes, those changes are undone (any registered
+ * [observers][observe] are unregistered, and any pending [effects][effect] are cancelled).
+ */
+ fun <A, B> State<A>.flatMapLatestBuild(transform: BuildScope.(A) -> State<B>): State<B> =
+ mapLatestBuild { transform(it) }.flatten()
+
+ /**
+ * Returns a [State] that transforms the value held inside this [State] by applying it to the
+ * [transform].
+ *
+ * [transform] can perform modifications to the Kairos network via its [BuildScope] receiver.
+ * When the value held by the original [State] changes, those changes are undone (any registered
+ * [observers][observe] are unregistered, and any pending [effects][effect] are cancelled).
+ */
+ fun <A, B> State<A>.mapLatestBuild(transform: BuildScope.(A) -> B): State<B> =
+ mapCheapUnsafe { buildSpec { transform(it) } }.applyLatestSpec()
+
+ /**
+ * Returns an [Events] containing the results of applying each [BuildSpec] emitted from the
+ * original [Events], and a [DeferredValue] containing the result of applying [initialSpec]
+ * immediately.
+ *
+ * When each [BuildSpec] is applied, changes from the previously-active [BuildSpec] are undone
+ * (any registered [observers][observe] are unregistered, and any pending [side-effects][effect]
+ * are cancelled).
+ */
+ fun <A : Any?, B> Events<BuildSpec<B>>.applyLatestSpec(
+ initialSpec: BuildSpec<A>
+ ): Pair<Events<B>, DeferredValue<A>> {
+ val (events, result) =
+ mapCheap { spec -> mapOf(Unit to just(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()
+ check(Unit in initResult) {
+ "applyLatest: expected initial result, but none present in: $initResult"
+ }
+ @Suppress("UNCHECKED_CAST")
+ initResult.getOrDefault(Unit) { null } as A
+ }
+ return Pair(outEvents, outInit)
+ }
+
+ /**
+ * Returns an [Events] containing the results of applying [transform] to each value of the
+ * original [Events].
+ *
+ * [transform] can perform modifications to the Kairos network via its [BuildScope] receiver.
+ * With each invocation of [transform], changes from the previous invocation are undone (any
+ * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are
+ * cancelled).
+ */
+ fun <A, B> Events<A>.mapLatestBuild(transform: BuildScope.(A) -> B): Events<B> =
+ mapCheap { buildSpec { transform(it) } }.applyLatestSpec()
+
+ /**
+ * Returns an [Events] containing the results of applying [transform] to each value of the
+ * original [Events], and a [DeferredValue] containing the result of applying [transform] to
+ * [initialValue] immediately.
+ *
+ * [transform] can perform modifications to the Kairos network via its [BuildScope] receiver.
+ * With each invocation of [transform], changes from the previous invocation are undone (any
+ * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are
+ * cancelled).
+ */
+ fun <A, B> Events<A>.mapLatestBuild(
+ initialValue: A,
+ transform: BuildScope.(A) -> B,
+ ): Pair<Events<B>, DeferredValue<B>> =
+ mapLatestBuildDeferred(deferredOf(initialValue), transform)
+
+ /**
+ * Returns an [Events] containing the results of applying [transform] to each value of the
+ * original [Events], and a [DeferredValue] containing the result of applying [transform] to
+ * [initialValue] immediately.
+ *
+ * [transform] can perform modifications to the Kairos network via its [BuildScope] receiver.
+ * With each invocation of [transform], changes from the previous invocation are undone (any
+ * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are
+ * cancelled).
+ */
+ fun <A, B> Events<A>.mapLatestBuildDeferred(
+ initialValue: DeferredValue<A>,
+ transform: BuildScope.(A) -> B,
+ ): Pair<Events<B>, DeferredValue<B>> =
+ mapCheap { buildSpec { transform(it) } }
+ .applyLatestSpec(initialSpec = buildSpec { transform(initialValue.get()) })
+
+ /**
+ * Returns an [Events] containing the results of applying each [BuildSpec] emitted from the
+ * original [Events], and a [DeferredValue] containing the result of applying [initialSpecs]
+ * immediately.
+ *
+ * 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] contained within the value for an associated key is [none], then the
+ * previously-active [BuildSpec] will be undone with no replacement.
+ */
+ fun <K, A, B> Events<Map<K, Maybe<BuildSpec<A>>>>.applyLatestSpecForKey(
+ initialSpecs: Map<K, BuildSpec<B>>,
+ numKeys: Int? = null,
+ ): Pair<Events<Map<K, Maybe<A>>>, DeferredValue<Map<K, B>>> =
+ applyLatestSpecForKey(deferredOf(initialSpecs), numKeys)
+
+ /**
+ * Returns an [Events] containing the results of applying each [BuildSpec] emitted from the
+ * original [Events].
+ *
+ * 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] contained within the value for an associated key is [none], then the
+ * previously-active [BuildSpec] will be undone with no replacement.
+ */
+ fun <K, A> Events<Map<K, Maybe<BuildSpec<A>>>>.applyLatestSpecForKey(
+ numKeys: Int? = null
+ ): Events<Map<K, Maybe<A>>> =
+ applyLatestSpecForKey<K, A, Nothing>(deferredOf(emptyMap()), numKeys).first
+
+ /**
+ * Returns a [State] containing the latest results of applying each [BuildSpec] emitted from the
+ * original [Events].
+ *
+ * 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] contained within the value for an associated key is [none], then the
+ * previously-active [BuildSpec] will be undone with no replacement.
+ */
+ fun <K, A> Events<Map<K, Maybe<BuildSpec<A>>>>.holdLatestSpecForKey(
+ initialSpecs: DeferredValue<Map<K, BuildSpec<A>>>,
+ numKeys: Int? = null,
+ ): State<Map<K, A>> {
+ val (changes, initialValues) = applyLatestSpecForKey(initialSpecs, numKeys)
+ return changes.foldStateMapIncrementally(initialValues)
+ }
+
+ /**
+ * Returns a [State] containing the latest results of applying each [BuildSpec] emitted from the
+ * original [Events].
+ *
+ * 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] contained within the value for an associated key is [none], then the
+ * previously-active [BuildSpec] will be undone with no replacement.
+ */
+ fun <K, A> Events<Map<K, Maybe<BuildSpec<A>>>>.holdLatestSpecForKey(
+ initialSpecs: Map<K, BuildSpec<A>> = emptyMap(),
+ numKeys: Int? = null,
+ ): State<Map<K, A>> = holdLatestSpecForKey(deferredOf(initialSpecs), numKeys)
+
+ /**
+ * Returns an [Events] containing the results of applying [transform] to each value of the
+ * original [Events], and a [DeferredValue] containing the result of applying [transform] to
+ * [initialValues] immediately.
+ *
+ * [transform] can perform modifications to the Kairos network via its [BuildScope] receiver.
+ * With each invocation of [transform], changes from the previous invocation 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
+ * previously-active [BuildScope] will be undone with no replacement.
+ */
+ fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestBuildForKey(
+ initialValues: DeferredValue<Map<K, A>>,
+ numKeys: Int? = null,
+ transform: BuildScope.(K, A) -> B,
+ ): Pair<Events<Map<K, Maybe<B>>>, DeferredValue<Map<K, B>>> =
+ map { patch -> patch.mapValues { (k, v) -> v.map { buildSpec { transform(k, it) } } } }
+ .applyLatestSpecForKey(
+ deferredBuildScope {
+ initialValues.get().mapValues { (k, v) -> buildSpec { transform(k, v) } }
+ },
+ numKeys = numKeys,
+ )
+
+ /**
+ * Returns an [Events] containing the results of applying [transform] to each value of the
+ * original [Events], and a [DeferredValue] containing the result of applying [transform] to
+ * [initialValues] immediately.
+ *
+ * [transform] can perform modifications to the Kairos network via its [BuildScope] receiver.
+ * With each invocation of [transform], changes from the previous invocation 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
+ * previously-active [BuildScope] will be undone with no replacement.
+ */
+ fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestBuildForKey(
+ initialValues: Map<K, A>,
+ numKeys: Int? = null,
+ transform: BuildScope.(K, A) -> B,
+ ): Pair<Events<Map<K, Maybe<B>>>, DeferredValue<Map<K, B>>> =
+ mapLatestBuildForKey(deferredOf(initialValues), numKeys, transform)
+
+ /**
+ * Returns an [Events] containing the results of applying [transform] to each value of the
+ * original [Events].
+ *
+ * [transform] can perform modifications to the Kairos network via its [BuildScope] receiver.
+ * With each invocation of [transform], changes from the previous invocation 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
+ * previously-active [BuildScope] will be undone with no replacement.
+ */
+ fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestBuildForKey(
+ numKeys: Int? = null,
+ transform: BuildScope.(K, A) -> B,
+ ): Events<Map<K, Maybe<B>>> = mapLatestBuildForKey(emptyMap(), numKeys, transform).first
+
+ /** Returns a [Deferred] containing the next value to be emitted from this [Events]. */
+ fun <R> Events<R>.nextDeferred(): Deferred<R> {
+ lateinit var next: CompletableDeferred<R>
+ val job = nextOnly().observe { next.complete(it) }
+ next = CompletableDeferred<R>(parent = job)
+ return next
+ }
+
+ /** Returns a [State] that reflects the [StateFlow.value] of this [StateFlow]. */
+ fun <A> StateFlow<A>.toState(): State<A> {
+ val initial = value
+ return events { dropWhile { it == initial }.collect { emit(it) } }.holdState(initial)
+ }
+
+ /** Returns an [Events] that emits whenever this [Flow] emits. */
+ fun <A> Flow<A>.toEvents(name: String? = null): Events<A> =
+ events(name) { collect { emit(it) } }
+
+ /**
+ * Shorthand for:
+ * ```kotlin
+ * flow.toEvents().holdState(initialValue)
+ * ```
+ */
+ fun <A> Flow<A>.toState(initialValue: A): State<A> = toEvents().holdState(initialValue)
+
+ /**
+ * Shorthand for:
+ * ```kotlin
+ * flow.scan(initialValue, operation).toEvents().holdState(initialValue)
+ * ```
+ */
+ fun <A, B> Flow<A>.scanToState(initialValue: B, operation: (B, A) -> B): State<B> =
+ scan(initialValue, operation).toEvents().holdState(initialValue)
+
+ /**
+ * Shorthand for:
+ * ```kotlin
+ * flow.scan(initialValue) { a, f -> f(a) }.toEvents().holdState(initialValue)
+ * ```
+ */
+ fun <A> Flow<(A) -> A>.scanToState(initialValue: A): State<A> =
+ scanToState(initialValue) { a, f -> f(a) }
+
+ /**
+ * Invokes [block] whenever this [Events] emits a value. [block] receives an [BuildScope] that
+ * can be used to make further modifications to the Kairos network, and/or perform side-effects
+ * via [effect].
+ *
+ * With each invocation of [block], changes from the previous invocation are undone (any
+ * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are
+ * cancelled).
+ */
+ fun <A> Events<A>.observeLatestBuild(block: BuildScope.(A) -> Unit = {}): Job =
+ mapLatestBuild { block(it) }.observe()
+
+ /**
+ * Invokes [block] whenever this [Events] emits a value, allowing side-effects to be safely
+ * performed in reaction to the emission.
+ *
+ * With each invocation of [block], running effects from the previous invocation are cancelled.
+ */
+ fun <A> Events<A>.observeLatest(block: EffectScope.(A) -> Unit = {}): Job {
+ var innerJob: Job? = null
+ return observeBuild {
+ innerJob?.cancel()
+ innerJob = effect { block(it) }
+ }
+ }
+
+ /**
+ * Invokes [block] with the value held by this [State], allowing side-effects to be safely
+ * performed in reaction to the state changing.
+ *
+ * With each invocation of [block], running effects from the previous invocation are cancelled.
+ */
+ fun <A> State<A>.observeLatest(block: EffectScope.(A) -> Unit = {}): Job = launchScope {
+ var innerJob = effect { block(sample()) }
+ changes.observeBuild {
+ innerJob.cancel()
+ innerJob = effect { block(it) }
+ }
+ }
+
+ /**
+ * Applies [block] to the value held by 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].
+ *
+ * [block] can perform modifications to the Kairos network via its [BuildScope] receiver. With
+ * each invocation of [block], changes from the previous invocation are undone (any registered
+ * [observers][observe] are unregistered, and any pending [side-effects][effect] are cancelled).
+ */
+ fun <A> State<A>.observeLatestBuild(block: BuildScope.(A) -> Unit = {}): Job = launchScope {
+ var innerJob: Job = launchScope { block(sample()) }
+ changes.observeBuild {
+ innerJob.cancel()
+ innerJob = launchScope { block(it) }
+ }
+ }
+
+ /** Applies the [BuildSpec] within this [BuildScope]. */
+ fun <A> BuildSpec<A>.applySpec(): A = this()
+
+ /**
+ * Applies the [BuildSpec] within this [BuildScope], returning the result as an [DeferredValue].
+ */
+ fun <A> BuildSpec<A>.applySpecDeferred(): DeferredValue<A> = deferredBuildScope { applySpec() }
+
+ /**
+ * 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].
+ */
+ fun <A> State<A>.observeBuild(block: BuildScope.(A) -> Unit = {}): Job = launchScope {
+ block(sample())
+ changes.observeBuild(block)
+ }
+
+ /**
+ * Invokes [block] with the current value of this [State], re-invoking whenever it changes,
+ * allowing side-effects to be safely performed in reaction value changing.
+ *
+ * Specifically, [block] is deferred to the end of the transaction, and is only actually
+ * executed if this [BuildScope] is still active by that time. It can be deactivated due to a
+ * -Latest combinator, for example.
+ *
+ * If the [State] is changing within the *current* transaction (i.e. [changes] is presently
+ * emitting) then [block] will be invoked for the first time with the new value; otherwise, it
+ * will be invoked with the [current][sample] value.
+ */
+ fun <A> State<A>.observe(block: EffectScope.(A) -> Unit = {}): Job =
+ now.map { sample() }.mergeWith(changes) { _, new -> new }.observe { block(it) }
+}
+
+/**
+ * Returns an [Events] that emits the result of [block] once it completes. [block] is evaluated
+ * 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)
+ * }
+ * ```
+ */
+@ExperimentalKairosApi
+fun <A> BuildScope.asyncEvent(block: suspend () -> A): Events<A> =
+ events {
+ // TODO: if block completes synchronously, it would be nice to emit within this
+ // transaction
+ emit(block())
+ }
+ .apply { observe() }
+
+/**
+ * Performs a side-effect in a safe manner w/r/t the current Kairos transaction.
+ *
+ * Specifically, [block] is deferred to the end of the current transaction, and is only actually
+ * executed if this [BuildScope] is still active by that time. It can be deactivated due to a
+ * -Latest combinator, for example.
+ *
+ * Shorthand for:
+ * ```kotlin
+ * now.observe { block() }
+ * ```
+ */
+@ExperimentalKairosApi
+fun BuildScope.effect(
+ context: CoroutineContext = EmptyCoroutineContext,
+ block: EffectScope.() -> Unit,
+): Job = now.observe(context) { block() }
+
+/**
+ * Launches [block] in a new coroutine, returning a [Job] bound to the coroutine.
+ *
+ * This coroutine is not actually started until the *end* of the current Kairos transaction. This is
+ * 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() } }
+ * ```
+ */
+@ExperimentalKairosApi
+fun BuildScope.launchEffect(block: suspend CoroutineScope.() -> Unit): Job = asyncEffect(block)
+
+/**
+ * Launches [block] in a new coroutine, returning the result as a [Deferred].
+ *
+ * This coroutine is not actually started until the *end* of the current Kairos transaction. This is
+ * 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
+ * CompletableDeferred<R>.apply {
+ * effect { effectCoroutineScope.launch { complete(coroutineScope { block() }) } }
+ * }
+ * .await()
+ * ```
+ */
+@ExperimentalKairosApi
+fun <R> BuildScope.asyncEffect(block: suspend CoroutineScope.() -> R): Deferred<R> {
+ val result = CompletableDeferred<R>()
+ val job = now.observe { effectCoroutineScope.launch { result.complete(coroutineScope(block)) } }
+ val handle = job.invokeOnCompletion { result.cancel() }
+ result.invokeOnCompletion {
+ handle.dispose()
+ job.cancel()
+ }
+ return result
+}
+
+/** Like [BuildScope.asyncScope], but ignores the result of [block]. */
+@ExperimentalKairosApi
+fun BuildScope.launchScope(block: BuildSpec<*>): Job = asyncScope(block).second
+
+/**
+ * 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].
+ *
+ * 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
+ * events { ... }.apply { observe() }
+ * ```
+ *
+ * In the event of backpressure, emissions are *coalesced* into batches. When a value is
+ * [emitted][CoalescingEventProducerScope.emit] from [builder], it is merged into the batch via
+ * [coalesce]. Once the batch is consumed by the Kairos network in the next transaction, the batch
+ * is reset back to [initialValue].
+ */
+@ExperimentalKairosApi
+fun <In, Out> BuildScope.coalescingEvents(
+ initialValue: Out,
+ coalesce: (old: Out, new: In) -> Out,
+ builder: suspend CoalescingEventProducerScope<In>.() -> Unit,
+): Events<Out> = coalescingEvents(getInitialValue = { initialValue }, coalesce, 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].
+ *
+ * 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
+ * events { ... }.apply { observe() }
+ * ```
+ *
+ * In the event of backpressure, emissions are *conflated*; any older emissions are dropped and only
+ * the most recent emission will be used when the Kairos network is ready.
+ */
+@ExperimentalKairosApi
+fun <T> BuildScope.conflatedEvents(
+ builder: suspend CoalescingEventProducerScope<T>.() -> Unit
+): Events<T> =
+ coalescingEvents<T, Any?>(initialValue = Any(), coalesce = { _, new -> new }, builder = builder)
+ .mapCheap {
+ @Suppress("UNCHECKED_CAST")
+ it as T
+ }
+
+/** Scope for emitting to a [BuildScope.coalescingEvents]. */
+interface CoalescingEventProducerScope<in T> {
+ /**
+ * Inserts [value] into the current batch, enqueueing it for emission from this [Events] if not
+ * already pending.
+ *
+ * Backpressure occurs when [emit] is called while the Kairos network is currently in a
+ * transaction; if called multiple times, then emissions will be coalesced into a single batch
+ * that is then processed when the network is ready.
+ */
+ fun emit(value: T)
+}
+
+/** Scope for emitting to a [BuildScope.events]. */
+interface EventProducerScope<in T> {
+ /**
+ * Emits a [value] to this [Events], suspending the caller until the Kairos transaction
+ * containing the emission has completed.
+ */
+ suspend fun emit(value: T)
+}
+
+/**
+ * Suspends forever. Upon cancellation, runs [block]. Useful for unregistering callbacks inside of
+ * [BuildScope.events] and [BuildScope.coalescingEvents].
+ */
+suspend fun awaitClose(block: () -> Unit): Nothing =
+ try {
+ awaitCancellation()
+ } finally {
+ block()
+ }
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
index d5576b3b83df..a26d5f8f122e 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combinators.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combinators.kt
@@ -25,42 +25,42 @@ import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.conflate
/**
- * Returns a [TFlow] that emits the value sampled from the [Transactional] produced by each emission
- * of the original [TFlow], within the same transaction of the original emission.
+ * 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.
*/
-@ExperimentalFrpApi
-fun <A> TFlow<Transactional<A>>.sampleTransactionals(): TFlow<A> = map { it.sample() }
+@ExperimentalKairosApi
+fun <A> Events<Transactional<A>>.sampleTransactionals(): Events<A> = map { it.sample() }
-/** @see FrpTransactionScope.sample */
-@ExperimentalFrpApi
-fun <A, B, C> TFlow<A>.sample(
- state: TState<B>,
- transform: FrpTransactionScope.(A, B) -> C,
-): TFlow<C> = map { transform(it, state.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 FrpTransactionScope.sample */
-@ExperimentalFrpApi
-fun <A, B, C> TFlow<A>.sample(
- transactional: Transactional<B>,
- transform: FrpTransactionScope.(A, B) -> C,
-): TFlow<C> = map { transform(it, transactional.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 ([stateChanges] is emitting),
- * then the new value is passed to [transform].
+ * 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
*/
-@ExperimentalFrpApi
-fun <A, B, C> TFlow<A>.samplePromptly(
- state: TState<B>,
- transform: FrpTransactionScope.(A, B) -> C,
-): TFlow<C> =
+@ExperimentalKairosApi
+fun <A, B, C> Events<A>.samplePromptly(
+ state: State<B>,
+ transform: TransactionScope.(A, B) -> C,
+): Events<C> =
sample(state) { a, b -> These.thiz<Pair<A, B>, B>(a to b) }
- .mergeWith(state.stateChanges.map { These.that(it) }) { thiz, that ->
+ .mergeWith(state.changes.map { These.that(it) }) { thiz, that ->
These.both((thiz as These.This).thiz, (that as These.That).that)
}
.mapMaybe { these ->
@@ -75,199 +75,204 @@ fun <A, B, C> TFlow<A>.samplePromptly(
}
/**
- * Returns a cold [Flow] that, when collected, emits from this [TFlow]. [network] is needed to
- * transactionally connect to / disconnect from the [TFlow] when collection starts/stops.
+ * 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.
*/
-@ExperimentalFrpApi
-fun <A> TFlow<A>.toColdConflatedFlow(network: FrpNetwork): Flow<A> =
+@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 [TState]. [network] is needed to
- * transactionally connect to / disconnect from the [TState] when collection starts/stops.
+ * 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.
*/
-@ExperimentalFrpApi
-fun <A> TState<A>.toColdConflatedFlow(network: FrpNetwork): Flow<A> =
+@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 [FrpSpec] in a new transaction in this
- * [network], and then emits from the returned [TFlow].
+ * 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 [FrpSpec]. This means all ongoing work is cleaned up.
+ * When collection is cancelled, so is the [BuildSpec]. This means all ongoing work is cleaned up.
*/
-@ExperimentalFrpApi
-@JvmName("flowSpecToColdConflatedFlow")
-fun <A> FrpSpec<TFlow<A>>.toColdConflatedFlow(network: FrpNetwork): Flow<A> =
+@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 [FrpSpec] in a new transaction in this
- * [network], and then emits from the returned [TState].
+ * 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 [FrpSpec]. This means all ongoing work is cleaned up.
+ * When collection is cancelled, so is the [BuildSpec]. This means all ongoing work is cleaned up.
*/
-@ExperimentalFrpApi
+@ExperimentalKairosApi
@JvmName("stateSpecToColdConflatedFlow")
-fun <A> FrpSpec<TState<A>>.toColdConflatedFlow(network: FrpNetwork): Flow<A> =
+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 [TFlow].
+ * this [network], and then emits from the returned [Events].
*/
-@ExperimentalFrpApi
+@ExperimentalKairosApi
@JvmName("transactionalFlowToColdConflatedFlow")
-fun <A> Transactional<TFlow<A>>.toColdConflatedFlow(network: FrpNetwork): Flow<A> =
+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 [TState].
+ * this [network], and then emits from the returned [State].
*/
-@ExperimentalFrpApi
+@ExperimentalKairosApi
@JvmName("transactionalStateToColdConflatedFlow")
-fun <A> Transactional<TState<A>>.toColdConflatedFlow(network: FrpNetwork): Flow<A> =
+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 [FrpStateful] in a new transaction in
- * this [network], and then emits from the returned [TFlow].
+ * 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 [FrpStateful]. This means all ongoing work is cleaned up.
+ * When collection is cancelled, so is the [Stateful]. This means all ongoing work is cleaned up.
*/
-@ExperimentalFrpApi
+@ExperimentalKairosApi
@JvmName("statefulFlowToColdConflatedFlow")
-fun <A> FrpStateful<TFlow<A>>.toColdConflatedFlow(network: FrpNetwork): Flow<A> =
+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 [TState].
+ * this [network], and then emits from the returned [State].
*
- * When collection is cancelled, so is the [FrpStateful]. This means all ongoing work is cleaned up.
+ * When collection is cancelled, so is the [Stateful]. This means all ongoing work is cleaned up.
*/
-@ExperimentalFrpApi
+@ExperimentalKairosApi
@JvmName("statefulStateToColdConflatedFlow")
-fun <A> FrpStateful<TState<A>>.toColdConflatedFlow(network: FrpNetwork): Flow<A> =
+fun <A> Stateful<State<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> =
channelFlow { network.activateSpec { applyStateful().observe { trySend(it) } } }.conflate()
-/** Return a [TFlow] that emits from the original [TFlow] only when [state] is `true`. */
-@ExperimentalFrpApi
-fun <A> TFlow<A>.filter(state: TState<Boolean>): TFlow<A> = filter { state.sample() }
+/** 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 [TState] that is `true` only when all of [states] are `true`. */
-@ExperimentalFrpApi
-fun allOf(vararg states: TState<Boolean>): TState<Boolean> = combine(*states) { it.allTrue() }
+/** 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 [TState] that is `true` when any of [states] are `true`. */
-@ExperimentalFrpApi
-fun anyOf(vararg states: TState<Boolean>): TState<Boolean> = combine(*states) { it.anyTrue() }
+/** 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 [TState] containing the inverse of the Boolean held by the original [TState]. */
-@ExperimentalFrpApi fun not(state: TState<Boolean>): TState<Boolean> = state.mapCheapUnsafe { !it }
+/** 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 FRP sub-network.
+ * Represents a modal Kairos sub-network.
*
- * When [enabled][enableMode], all network modifications are applied immediately to the FRP network.
- * When the returned [TFlow] emits a [FrpBuildMode], that mode is enabled and replaces this mode,
- * undoing all modifications in the process (any registered [observers][FrpBuildScope.observe] are
- * unregistered, and any pending [side-effects][FrpBuildScope.effect] are cancelled).
+ * 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 [compiledFrpSpec] to compile and stand-up a mode graph.
+ * Use [compiledBuildSpec] to compile and stand-up a mode graph.
*
- * @see FrpStatefulMode
+ * @see StatefulMode
*/
-@ExperimentalFrpApi
-fun interface FrpBuildMode<out A> {
+@ExperimentalKairosApi
+fun interface BuildMode<out A> {
/**
- * Invoked when this mode is enabled. Returns a value and a [TFlow] that signals a switch to a
+ * Invoked when this mode is enabled. Returns a value and an [Events] that signals a switch to a
* new mode.
*/
- fun FrpBuildScope.enableMode(): Pair<A, TFlow<FrpBuildMode<A>>>
+ fun BuildScope.enableMode(): Pair<A, Events<BuildMode<A>>>
}
/**
- * Returns an [FrpSpec] that, when [applied][FrpBuildScope.applySpec], stands up a modal-transition
- * graph starting with this [FrpBuildMode], automatically switching to new modes as they are
- * produced.
+ * 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 FrpBuildMode
+ * @see BuildMode
*/
-@ExperimentalFrpApi
-val <A> FrpBuildMode<A>.compiledFrpSpec: FrpSpec<TState<A>>
- get() = frpSpec {
- var modeChangeEvents by TFlowLoop<FrpBuildMode<A>>()
- val activeMode: TState<Pair<A, TFlow<FrpBuildMode<A>>>> =
+@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 { frpSpec { enableMode() } } }
- .holdLatestSpec(frpSpec { enableMode() })
+ .map { it.run { buildSpec { enableMode() } } }
+ .holdLatestSpec(buildSpec { enableMode() })
modeChangeEvents =
- activeMode.map { statefully { it.second.nextOnly() } }.applyLatestStateful().switch()
+ activeMode
+ .map { statefully { it.second.nextOnly() } }
+ .applyLatestStateful()
+ .switchEvents()
activeMode.map { it.first }
}
/**
- * Represents a modal FRP sub-network.
+ * Represents a modal Kairos sub-network.
*
* When [enabled][enableMode], all state accumulation is immediately started. When the returned
- * [TFlow] emits a [FrpBuildMode], that mode is enabled and replaces this mode, stopping all state
+ * [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 FrpBuildMode
+ * @see BuildMode
*/
-@ExperimentalFrpApi
-fun interface FrpStatefulMode<out A> {
+@ExperimentalKairosApi
+fun interface StatefulMode<out A> {
/**
- * Invoked when this mode is enabled. Returns a value and a [TFlow] that signals a switch to a
+ * Invoked when this mode is enabled. Returns a value and an [Events] that signals a switch to a
* new mode.
*/
- fun FrpStateScope.enableMode(): Pair<A, TFlow<FrpStatefulMode<A>>>
+ fun StateScope.enableMode(): Pair<A, Events<StatefulMode<A>>>
}
/**
- * Returns an [FrpStateful] that, when [applied][FrpStateScope.applyStateful], stands up a
- * modal-transition graph starting with this [FrpStatefulMode], automatically switching to new modes
- * as they are produced.
+ * 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 FrpBuildMode
+ * @see BuildMode
*/
-@ExperimentalFrpApi
-val <A> FrpStatefulMode<A>.compiledStateful: FrpStateful<TState<A>>
+@ExperimentalKairosApi
+val <A> StatefulMode<A>.compiledStateful: Stateful<State<A>>
get() = statefully {
- var modeChangeEvents by TFlowLoop<FrpStatefulMode<A>>()
- val activeMode: TState<Pair<A, TFlow<FrpStatefulMode<A>>>> =
+ 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().switch()
+ activeMode
+ .map { statefully { it.second.nextOnly() } }
+ .applyLatestStateful()
+ .switchEvents()
activeMode.map { it.first }
}
/**
- * Runs [spec] in this [FrpBuildScope], and then re-runs it whenever [rebuildSignal] emits. Returns
- * a [TState] that holds the result of the currently-active [FrpSpec].
+ * 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].
*/
-@ExperimentalFrpApi
-fun <A> FrpBuildScope.rebuildOn(rebuildSignal: TFlow<*>, spec: FrpSpec<A>): TState<A> =
+@ExperimentalKairosApi
+fun <A> BuildScope.rebuildOn(rebuildSignal: Events<*>, spec: BuildSpec<A>): State<A> =
rebuildSignal.map { spec }.holdLatestSpec(spec)
/**
- * Like [stateChanges] but also includes the old value of this [TState].
+ * Like [changes] but also includes the old value of this [State].
*
* Shorthand for:
* ``` kotlin
* stateChanges.map { WithPrev(previousValue = sample(), newValue = it) }
* ```
*/
-@ExperimentalFrpApi
-val <A> TState<A>.transitions: TFlow<WithPrev<A, A>>
- get() = 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/EffectScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/EffectScope.kt
new file mode 100644
index 000000000000..7e257f2831af
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/EffectScope.kt
@@ -0,0 +1,48 @@
+/*
+ * 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 kotlinx.coroutines.CoroutineScope
+
+/**
+ * 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.
+ */
+@ExperimentalKairosApi
+interface EffectScope : 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.
+ */
+ @ExperimentalKairosApi val effectCoroutineScope: CoroutineScope
+
+ /**
+ * A [KairosNetwork] instance that can be used to transactionally query / modify the Kairos
+ * network.
+ *
+ * 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).
+ */
+ @ExperimentalKairosApi val kairosNetwork: KairosNetwork
+}
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
new file mode 100644
index 000000000000..bd9b45d3be4c
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Events.kt
@@ -0,0 +1,577 @@
+/*
+ * 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.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
+import com.android.systemui.kairos.internal.InputNode
+import com.android.systemui.kairos.internal.Network
+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.map
+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.Left
+import com.android.systemui.kairos.util.Maybe
+import com.android.systemui.kairos.util.Right
+import com.android.systemui.kairos.util.just
+import com.android.systemui.kairos.util.map
+import com.android.systemui.kairos.util.toMaybe
+import java.util.concurrent.atomic.AtomicReference
+import kotlin.reflect.KProperty
+import kotlinx.coroutines.CoroutineStart
+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. */
+@ExperimentalKairosApi
+sealed class Events<out A> {
+ companion object {
+ /** An [Events] with no values. */
+ val empty: Events<Nothing> = EmptyFlow
+ }
+}
+
+/** AN [Events] with no values. */
+@ExperimentalKairosApi val emptyEvents: Events<Nothing> = Events.empty
+
+/**
+ * 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.
+ */
+@ExperimentalKairosApi
+class EventsLoop<A> : Events<A>() {
+ private val deferred = CompletableLazy<Events<A>>()
+
+ internal val init: Init<EventsImpl<A>> =
+ init(name = null) { deferred.value.init.connect(evalScope = this) }
+
+ /** The [Events] this reference is referring to. */
+ var loopback: Events<A>? = null
+ set(value) {
+ value?.let {
+ check(!deferred.isInitialized()) { "TFlowLoop.loopback has already been set." }
+ deferred.setValue(value)
+ field = value
+ }
+ }
+
+ operator fun getValue(thisRef: Any?, property: KProperty<*>): Events<A> = this
+
+ operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Events<A>) {
+ loopback = value
+ }
+
+ override fun toString(): String = "${this::class.simpleName}@$hashString"
+}
+
+/**
+ * Returns an [Events] that acts as a deferred-reference to the [Events] produced by this [Lazy].
+ *
+ * When the returned [Events] is accessed by the Kairos network, the [Lazy]'s [value][Lazy.value]
+ * will be queried and used.
+ *
+ * Useful for recursive definitions.
+ */
+@ExperimentalKairosApi fun <A> Lazy<Events<A>>.defer(): Events<A> = deferInline { value }
+
+/**
+ * Returns an [Events] that acts as a deferred-reference to the [Events] produced by this
+ * [DeferredValue].
+ *
+ * When the returned [Events] is accessed by the Kairos network, the [DeferredValue] will be queried
+ * and used.
+ *
+ * Useful for recursive definitions.
+ */
+@ExperimentalKairosApi
+fun <A> DeferredValue<Events<A>>.defer(): Events<A> = deferInline { unwrapped.value }
+
+/**
+ * Returns an [Events] that acts as a deferred-reference to the [Events] produced by [block].
+ *
+ * When the returned [Events] is accessed by the Kairos network, [block] will be invoked and the
+ * returned [Events] will be used.
+ *
+ * Useful for recursive definitions.
+ */
+@ExperimentalKairosApi
+fun <A> deferredEvents(block: KairosScope.() -> Events<A>): Events<A> = deferInline {
+ 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].
+ *
+ * @see mapNotNull
+ */
+@ExperimentalKairosApi
+fun <A, B> Events<A>.mapMaybe(transform: TransactionScope.(A) -> Maybe<B>): Events<B> =
+ map(transform).filterJust()
+
+/**
+ * Returns an [Events] that contains only the non-null results of applying [transform] to each value
+ * of the original [Events].
+ *
+ * @see mapMaybe
+ */
+@ExperimentalKairosApi
+fun <A, B> Events<A>.mapNotNull(transform: TransactionScope.(A) -> B?): Events<B> = mapMaybe {
+ 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].
+ */
+@ExperimentalKairosApi
+fun <A, B> Events<A>.map(transform: TransactionScope.(A) -> B): Events<B> {
+ val mapped: EventsImpl<B> = mapImpl({ init.connect(evalScope = this) }) { a, _ -> transform(a) }
+ return EventsInit(constInit(name = null, mapped.cached()))
+}
+
+/**
+ * Like [map], but the emission is not cached during the transaction. Use only if [transform] is
+ * fast and pure.
+ *
+ * @see map
+ */
+@ExperimentalKairosApi
+fun <A, B> Events<A>.mapCheap(transform: TransactionScope.(A) -> B): Events<B> =
+ EventsInit(
+ constInit(name = null, mapImpl({ init.connect(evalScope = this) }) { a, _ -> transform(a) })
+ )
+
+/**
+ * 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 }
+ * ```
+ *
+ * Note that the side effects performed in [onEach] are only performed while the resulting [Events]
+ * is connected to an output of the Kairos network. If your goal is to reliably perform side effects
+ * in response to an [Events], use the output combinators available in [BuildScope], such as
+ * [BuildScope.toSharedFlow] or [BuildScope.observe].
+ */
+@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))
+}
+
+/**
+ * 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)
+ * ```
+ */
+@ExperimentalKairosApi
+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
+}
+
+/**
+ * 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 }) { newFlow, _ -> newFlow.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 }) { newFlow, _ -> newFlow.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.
+ *
+ * @see KairosNetwork.coalescingMutableEvents
+ */
+@ExperimentalKairosApi
+class CoalescingMutableEvents<in In, Out>
+internal constructor(
+ internal val name: String?,
+ internal val coalesce: (old: Lazy<Out>, new: In) -> Out,
+ internal val network: Network,
+ private val getInitialValue: () -> Out,
+ internal val impl: InputNode<Out> = InputNode(),
+) : Events<Out>() {
+ internal val storage = AtomicReference(false to lazy { getInitialValue() })
+
+ override fun toString(): String = "${this::class.simpleName}@$hashString"
+
+ /**
+ * Inserts [value] into the current batch, enqueueing it for emission from this [Events] if not
+ * already pending.
+ *
+ * Backpressure occurs when [emit] is called while the Kairos network is currently in a
+ * transaction; if called multiple times, then emissions will be coalesced into a single batch
+ * that is then processed when the network is ready.
+ */
+ fun emit(value: In) {
+ val (scheduled, _) =
+ storage.getAndUpdate { (_, batch) -> true to CompletableLazy(coalesce(batch, value)) }
+ if (!scheduled) {
+ @Suppress("DeferredResultUnused")
+ network.transaction(
+ "CoalescingMutableEvents${name?.let { "($name)" }.orEmpty()}.emit"
+ ) {
+ val (_, batch) = storage.getAndSet(false to lazy { getInitialValue() })
+ impl.visit(this, batch.value)
+ }
+ }
+ }
+}
+
+/**
+ * A mutable [Events] that provides the ability to [emit] values to the network, handling
+ * backpressure by suspending the emitter.
+ *
+ * @see KairosNetwork.coalescingMutableEvents
+ */
+@ExperimentalKairosApi
+class MutableEvents<T>
+internal constructor(internal val network: Network, internal val impl: InputNode<T> = InputNode()) :
+ Events<T>() {
+ internal val name: String? = null
+
+ private val storage = AtomicReference<Job?>(null)
+
+ override fun toString(): String = "${this::class.simpleName}@$hashString"
+
+ /**
+ * Emits a [value] to this [Events], suspending the caller until the Kairos transaction
+ * containing the emission has completed.
+ */
+ suspend fun emit(value: T) {
+ coroutineScope {
+ var jobOrNull: Job? = null
+ val newEmit =
+ async(start = CoroutineStart.LAZY) {
+ jobOrNull?.join()
+ network.transaction("MutableEvents.emit") { impl.visit(this, value) }.await()
+ }
+ jobOrNull = storage.getAndSet(newEmit)
+ newEmit.await()
+ }
+ }
+}
+
+private data object EmptyFlow : Events<Nothing>()
+
+internal class EventsInit<out A>(val init: Init<EventsImpl<A>>) : Events<A>() {
+ override fun toString(): String = "${this::class.simpleName}@$hashString"
+}
+
+internal val <A> Events<A>.init: Init<EventsImpl<A>>
+ get() =
+ when (this) {
+ is EmptyFlow -> constInit("EmptyFlow", neverImpl)
+ is EventsInit -> init
+ is EventsLoop -> init
+ is CoalescingMutableEvents<*, A> -> constInit(name, impl.activated())
+ is MutableEvents -> constInit(name, impl.activated())
+ }
+
+private inline fun <A> deferInline(crossinline block: InitScope.() -> Events<A>): Events<A> =
+ EventsInit(init(name = null) { block().init.connect(evalScope = this) })
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpBuildScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpBuildScope.kt
deleted file mode 100644
index 31778dc32697..000000000000
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpBuildScope.kt
+++ /dev/null
@@ -1,887 +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.
- */
-
-@file:OptIn(ExperimentalCoroutinesApi::class)
-
-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 kotlin.coroutines.RestrictsSuspension
-import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Deferred
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-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
-import kotlinx.coroutines.flow.MutableStateFlow
-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 FrpNetwork. */
-typealias FrpSpec<A> = FrpBuildScope.() -> A
-
-/**
- * Constructs an [FrpSpec]. The passed [block] will be invoked with an [FrpBuildScope] that can be
- * used to perform network-building operations, including adding new inputs and outputs to the
- * network, as well as all operations available in [FrpTransactionScope].
- */
-@ExperimentalFrpApi
-@Suppress("NOTHING_TO_INLINE")
-inline fun <A> frpSpec(noinline block: FrpBuildScope.() -> A): FrpSpec<A> = block
-
-/** Applies the [FrpSpec] within this [FrpBuildScope]. */
-@ExperimentalFrpApi
-inline operator fun <A> FrpBuildScope.invoke(block: FrpBuildScope.() -> A) = run(block)
-
-/** Operations that add inputs and outputs to an FRP network. */
-@ExperimentalFrpApi
-@RestrictsSuspension
-interface FrpBuildScope : FrpStateScope {
-
- /** TODO: Javadoc */
- val frpNetwork: FrpNetwork
-
- /** TODO: Javadoc */
- @ExperimentalFrpApi
- fun <R> deferredBuildScope(block: FrpBuildScope.() -> R): FrpDeferredValue<R>
-
- /** TODO: Javadoc */
- @ExperimentalFrpApi fun deferredBuildScopeAction(block: FrpBuildScope.() -> Unit)
-
- /**
- * Returns a [TFlow] containing the results of applying [transform] to each value of the
- * original [TFlow].
- *
- * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver.
- * Unlike [mapLatestBuild], these modifications are not undone with each subsequent emission of
- * the original [TFlow].
- *
- * **NOTE:** This API does not [observe] the original [TFlow], meaning that unless the returned
- * (or a downstream) [TFlow] is observed separately, [transform] will not be invoked, and no
- * internal side-effects will occur.
- */
- @ExperimentalFrpApi fun <A, B> TFlow<A>.mapBuild(transform: FrpBuildScope.(A) -> B): TFlow<B>
-
- /**
- * Invokes [block] whenever this [TFlow] emits a value, allowing side-effects to be safely
- * performed in reaction to the emission.
- *
- * Specifically, [block] is deferred to the end of the transaction, and is only actually
- * executed if this [FrpBuildScope] is still active by that time. It can be deactivated due to a
- * -Latest combinator, for example.
- *
- * Shorthand for:
- * ```kotlin
- * tFlow.observe { effect { ... } }
- * ```
- */
- @ExperimentalFrpApi
- fun <A> TFlow<A>.observe(
- coroutineContext: CoroutineContext = EmptyCoroutineContext,
- block: FrpEffectScope.(A) -> Unit = {},
- ): Job
-
- /**
- * Returns a [TFlow] containing the results of applying each [FrpSpec] emitted from the original
- * [TFlow], and a [FrpDeferredValue] containing the result of applying [initialSpecs]
- * immediately.
- *
- * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] with the 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
- * previously-active [FrpSpec] will be undone with no replacement.
- */
- @ExperimentalFrpApi
- fun <K, A, B> TFlow<Map<K, Maybe<FrpSpec<A>>>>.applyLatestSpecForKey(
- initialSpecs: FrpDeferredValue<Map<K, FrpSpec<B>>>,
- numKeys: Int? = null,
- ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>>
-
- /**
- * Creates an instance of a [TFlow] with elements that are from [builder].
- *
- * [builder] is run in its own coroutine, allowing for ongoing work that can emit to the
- * provided [MutableTFlow].
- *
- * By default, [builder] is only running while the returned [TFlow] is being
- * [observed][observe]. If you want it to run at all times, simply add a no-op observer:
- * ```kotlin
- * tFlow { ... }.apply { observe() }
- * ```
- */
- @ExperimentalFrpApi
- fun <T> tFlow(name: String? = null, builder: suspend FrpProducerScope<T>.() -> Unit): TFlow<T>
-
- /**
- * Creates an instance of a [TFlow] with elements that are emitted from [builder].
- *
- * [builder] is run in its own coroutine, allowing for ongoing work that can emit to the
- * provided [MutableTFlow].
- *
- * By default, [builder] is only running while the returned [TFlow] is being
- * [observed][observe]. If you want it to run at all times, simply add a no-op observer:
- * ```kotlin
- * tFlow { ... }.apply { observe() }
- * ```
- *
- * In the event of backpressure, emissions are *coalesced* into batches. When a value is
- * [emitted][FrpCoalescingProducerScope.emit] from [builder], it is merged into the batch via
- * [coalesce]. Once the batch is consumed by the frp network in the next transaction, the batch
- * is reset back to [getInitialValue].
- */
- @ExperimentalFrpApi
- fun <In, Out> coalescingTFlow(
- getInitialValue: () -> Out,
- coalesce: (old: Out, new: In) -> Out,
- builder: suspend FrpCoalescingProducerScope<In>.() -> Unit,
- ): TFlow<Out>
-
- /**
- * Creates a new [FrpBuildScope] that is a child of this one.
- *
- * This new scope can be manually cancelled via the returned [Job], or will be cancelled
- * automatically when its parent is cancelled. Cancellation will unregister all
- * [observers][observe] and cancel all scheduled [effects][effect].
- *
- * The return value from [block] can be accessed via the returned [FrpDeferredValue].
- */
- @ExperimentalFrpApi fun <A> asyncScope(block: FrpSpec<A>): Pair<FrpDeferredValue<A>, Job>
-
- // TODO: once we have context params, these can all become extensions:
-
- /**
- * Returns a [TFlow] containing the results of applying the given [transform] function to each
- * value of the original [TFlow].
- *
- * Unlike [TFlow.map], [transform] can perform arbitrary asynchronous code. This code is run
- * outside of the current FRP transaction; when [transform] returns, the returned value is
- * emitted from the result [TFlow] in a new transaction.
- *
- * Shorthand for:
- * ```kotlin
- * tflow.mapLatestBuild { a -> asyncTFlow { transform(a) } }.flatten()
- * ```
- */
- @ExperimentalFrpApi
- fun <A, B> TFlow<A>.mapAsyncLatest(transform: suspend (A) -> B): TFlow<B> =
- mapLatestBuild { a -> asyncTFlow { transform(a) } }.flatten()
-
- /**
- * Invokes [block] whenever this [TFlow] emits a value. [block] receives an [FrpBuildScope] that
- * can be used to make further modifications to the FRP network, and/or perform side-effects via
- * [effect].
- *
- * @see observe
- */
- @ExperimentalFrpApi
- fun <A> TFlow<A>.observeBuild(block: FrpBuildScope.(A) -> Unit = {}): Job =
- mapBuild(block).observe()
-
- /**
- * Returns a [StateFlow] whose [value][StateFlow.value] tracks the current
- * [value of this TState][TState.sample], and will emit at the same rate as
- * [TState.stateChanges].
- *
- * 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 [TState.sample].
- */
- @ExperimentalFrpApi
- fun <A> TState<A>.toStateFlow(): StateFlow<A> {
- val uninitialized = Any()
- var initialValue: Any? = uninitialized
- val innerStateFlow = MutableStateFlow<Any?>(uninitialized)
- deferredBuildScope {
- initialValue = sample()
- stateChanges.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 FRP transaction has completed."
- )
- }
-
- return object : StateFlow<A> {
- override val replayCache: List<A>
- get() = innerStateFlow.replayCache.map(::getValue)
-
- override val value: A
- get() = getValue(innerStateFlow.value)
-
- override suspend fun collect(collector: FlowCollector<A>): Nothing {
- innerStateFlow.collect { collector.emit(getValue(it)) }
- }
- }
- }
-
- /**
- * Returns a [SharedFlow] configured with a replay cache of size [replay] that emits the current
- * [value][TState.sample] of this [TState] followed by all [stateChanges].
- */
- @ExperimentalFrpApi
- fun <A> TState<A>.toSharedFlow(replay: Int = 0): SharedFlow<A> {
- val result = MutableSharedFlow<A>(replay, extraBufferCapacity = 1)
- deferredBuildScope {
- result.tryEmit(sample())
- stateChanges.observe { a -> result.tryEmit(a) }
- }
- return result
- }
-
- /**
- * Returns a [SharedFlow] configured with a replay cache of size [replay] that emits values
- * whenever this [TFlow] emits.
- */
- @ExperimentalFrpApi
- fun <A> TFlow<A>.toSharedFlow(replay: Int = 0): SharedFlow<A> {
- val result = MutableSharedFlow<A>(replay, extraBufferCapacity = 1)
- observe { a -> result.tryEmit(a) }
- return result
- }
-
- /**
- * Returns a [TState] that holds onto the value returned by applying the most recently emitted
- * [FrpSpec] from the original [TFlow], or the value returned by applying [initialSpec] if
- * nothing has been emitted since it was constructed.
- *
- * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] are undone (any
- * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are
- * cancelled).
- */
- @ExperimentalFrpApi
- fun <A> TFlow<FrpSpec<A>>.holdLatestSpec(initialSpec: FrpSpec<A>): TState<A> {
- val (changes: TFlow<A>, initApplied: FrpDeferredValue<A>) = applyLatestSpec(initialSpec)
- return changes.holdDeferred(initApplied)
- }
-
- /**
- * Returns a [TState] containing the value returned by applying the [FrpSpec] held by the
- * original [TState].
- *
- * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] are undone (any
- * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are
- * cancelled).
- */
- @ExperimentalFrpApi
- fun <A> TState<FrpSpec<A>>.applyLatestSpec(): TState<A> {
- val (appliedChanges: TFlow<A>, init: FrpDeferredValue<A>) =
- stateChanges.applyLatestSpec(frpSpec { sample().applySpec() })
- return appliedChanges.holdDeferred(init)
- }
-
- /**
- * Returns a [TFlow] containing the results of applying each [FrpSpec] emitted from the original
- * [TFlow].
- *
- * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] are undone (any
- * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are
- * cancelled).
- */
- @ExperimentalFrpApi
- fun <A> TFlow<FrpSpec<A>>.applyLatestSpec(): TFlow<A> = applyLatestSpec(frpSpec {}).first
-
- /**
- * Returns a [TFlow] that switches to a new [TFlow] produced by [transform] every time the
- * original [TFlow] emits a value.
- *
- * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver.
- * When the original [TFlow] emits a new value, those changes are undone (any registered
- * [observers][observe] are unregistered, and any pending [effects][effect] are cancelled).
- */
- @ExperimentalFrpApi
- fun <A, B> TFlow<A>.flatMapLatestBuild(transform: FrpBuildScope.(A) -> TFlow<B>): TFlow<B> =
- mapCheap { frpSpec { transform(it) } }.applyLatestSpec().flatten()
-
- /**
- * Returns a [TState] by applying [transform] to the value held by the original [TState].
- *
- * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver.
- * When the value held by the original [TState] changes, those changes are undone (any
- * registered [observers][observe] are unregistered, and any pending [effects][effect] are
- * cancelled).
- */
- @ExperimentalFrpApi
- fun <A, B> TState<A>.flatMapLatestBuild(transform: FrpBuildScope.(A) -> TState<B>): TState<B> =
- mapLatestBuild { transform(it) }.flatten()
-
- /**
- * Returns a [TState] that transforms the value held inside this [TState] by applying it to the
- * [transform].
- *
- * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver.
- * When the value held by the original [TState] changes, those changes are undone (any
- * registered [observers][observe] are unregistered, and any pending [effects][effect] are
- * cancelled).
- */
- @ExperimentalFrpApi
- fun <A, B> TState<A>.mapLatestBuild(transform: FrpBuildScope.(A) -> B): TState<B> =
- mapCheapUnsafe { frpSpec { transform(it) } }.applyLatestSpec()
-
- /**
- * Returns a [TFlow] containing the results of applying each [FrpSpec] emitted from the original
- * [TFlow], and a [FrpDeferredValue] containing the result of applying [initialSpec]
- * immediately.
- *
- * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] are undone (any
- * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are
- * cancelled).
- */
- @ExperimentalFrpApi
- fun <A : Any?, B> TFlow<FrpSpec<B>>.applyLatestSpec(
- initialSpec: FrpSpec<A>
- ): Pair<TFlow<B>, FrpDeferredValue<A>> {
- val (flow, result) =
- mapCheap { spec -> mapOf(Unit to just(spec)) }
- .applyLatestSpecForKey(initialSpecs = mapOf(Unit to initialSpec), numKeys = 1)
- val outFlow: TFlow<B> =
- flow.mapMaybe {
- checkNotNull(it[Unit]) { "applyLatest: expected result, but none present in: $it" }
- }
- val outInit: FrpDeferredValue<A> = deferredBuildScope {
- val initResult: Map<Unit, A> = result.get()
- check(Unit in initResult) {
- "applyLatest: expected initial result, but none present in: $initResult"
- }
- @Suppress("UNCHECKED_CAST")
- initResult.getOrDefault(Unit) { null } as A
- }
- return Pair(outFlow, outInit)
- }
-
- /**
- * Returns a [TFlow] containing the results of applying [transform] to each value of the
- * original [TFlow].
- *
- * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver.
- * With each invocation of [transform], changes from the previous invocation are undone (any
- * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are
- * cancelled).
- */
- @ExperimentalFrpApi
- fun <A, B> TFlow<A>.mapLatestBuild(transform: FrpBuildScope.(A) -> B): TFlow<B> =
- mapCheap { frpSpec { transform(it) } }.applyLatestSpec()
-
- /**
- * Returns a [TFlow] containing the results of applying [transform] to each value of the
- * original [TFlow], and a [FrpDeferredValue] containing the result of applying [transform] to
- * [initialValue] immediately.
- *
- * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver.
- * With each invocation of [transform], changes from the previous invocation are undone (any
- * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are
- * cancelled).
- */
- @ExperimentalFrpApi
- fun <A, B> TFlow<A>.mapLatestBuild(
- initialValue: A,
- transform: FrpBuildScope.(A) -> B,
- ): Pair<TFlow<B>, FrpDeferredValue<B>> =
- mapLatestBuildDeferred(deferredOf(initialValue), transform)
-
- /**
- * Returns a [TFlow] containing the results of applying [transform] to each value of the
- * original [TFlow], and a [FrpDeferredValue] containing the result of applying [transform] to
- * [initialValue] immediately.
- *
- * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver.
- * With each invocation of [transform], changes from the previous invocation are undone (any
- * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are
- * cancelled).
- */
- @ExperimentalFrpApi
- fun <A, B> TFlow<A>.mapLatestBuildDeferred(
- initialValue: FrpDeferredValue<A>,
- transform: FrpBuildScope.(A) -> B,
- ): Pair<TFlow<B>, FrpDeferredValue<B>> =
- mapCheap { frpSpec { transform(it) } }
- .applyLatestSpec(initialSpec = frpSpec { transform(initialValue.get()) })
-
- /**
- * Returns a [TFlow] containing the results of applying each [FrpSpec] emitted from the original
- * [TFlow], and a [FrpDeferredValue] containing the result of applying [initialSpecs]
- * immediately.
- *
- * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] with the 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
- * previously-active [FrpSpec] will be undone with no replacement.
- */
- @ExperimentalFrpApi
- fun <K, A, B> TFlow<Map<K, Maybe<FrpSpec<A>>>>.applyLatestSpecForKey(
- initialSpecs: Map<K, FrpSpec<B>>,
- numKeys: Int? = null,
- ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>> =
- applyLatestSpecForKey(deferredOf(initialSpecs), numKeys)
-
- /**
- * Returns a [TFlow] containing the results of applying each [FrpSpec] emitted from the original
- * [TFlow].
- *
- * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] with the 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
- * previously-active [FrpSpec] will be undone with no replacement.
- */
- @ExperimentalFrpApi
- fun <K, A> TFlow<Map<K, Maybe<FrpSpec<A>>>>.applyLatestSpecForKey(
- numKeys: Int? = null
- ): TFlow<Map<K, Maybe<A>>> =
- applyLatestSpecForKey<K, A, Nothing>(deferredOf(emptyMap()), numKeys).first
-
- /**
- * Returns a [TState] containing the latest results of applying each [FrpSpec] emitted from the
- * original [TFlow].
- *
- * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] with the 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
- * previously-active [FrpSpec] will be undone with no replacement.
- */
- @ExperimentalFrpApi
- fun <K, A> TFlow<Map<K, Maybe<FrpSpec<A>>>>.holdLatestSpecForKey(
- initialSpecs: FrpDeferredValue<Map<K, FrpSpec<A>>>,
- numKeys: Int? = null,
- ): TState<Map<K, A>> {
- val (changes, initialValues) = applyLatestSpecForKey(initialSpecs, numKeys)
- return changes.foldMapIncrementally(initialValues)
- }
-
- /**
- * Returns a [TState] containing the latest results of applying each [FrpSpec] emitted from the
- * original [TFlow].
- *
- * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] with the 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
- * previously-active [FrpSpec] will be undone with no replacement.
- */
- @ExperimentalFrpApi
- fun <K, A> TFlow<Map<K, Maybe<FrpSpec<A>>>>.holdLatestSpecForKey(
- initialSpecs: Map<K, FrpSpec<A>> = emptyMap(),
- numKeys: Int? = null,
- ): TState<Map<K, A>> = holdLatestSpecForKey(deferredOf(initialSpecs), numKeys)
-
- /**
- * Returns a [TFlow] containing the results of applying [transform] to each value of the
- * original [TFlow], and a [FrpDeferredValue] containing the result of applying [transform] to
- * [initialValues] immediately.
- *
- * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver.
- * With each invocation of [transform], changes from the previous invocation 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
- * previously-active [FrpBuildScope] will be undone with no replacement.
- */
- @ExperimentalFrpApi
- fun <K, A, B> TFlow<Map<K, Maybe<A>>>.mapLatestBuildForKey(
- initialValues: FrpDeferredValue<Map<K, A>>,
- numKeys: Int? = null,
- transform: FrpBuildScope.(K, A) -> B,
- ): Pair<TFlow<Map<K, Maybe<B>>>, FrpDeferredValue<Map<K, B>>> =
- map { patch -> patch.mapValues { (k, v) -> v.map { frpSpec { transform(k, it) } } } }
- .applyLatestSpecForKey(
- deferredBuildScope {
- initialValues.get().mapValues { (k, v) -> frpSpec { transform(k, v) } }
- },
- numKeys = numKeys,
- )
-
- /**
- * Returns a [TFlow] containing the results of applying [transform] to each value of the
- * original [TFlow], and a [FrpDeferredValue] containing the result of applying [transform] to
- * [initialValues] immediately.
- *
- * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver.
- * With each invocation of [transform], changes from the previous invocation 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
- * previously-active [FrpBuildScope] will be undone with no replacement.
- */
- @ExperimentalFrpApi
- fun <K, A, B> TFlow<Map<K, Maybe<A>>>.mapLatestBuildForKey(
- initialValues: Map<K, A>,
- numKeys: Int? = null,
- transform: FrpBuildScope.(K, A) -> B,
- ): Pair<TFlow<Map<K, Maybe<B>>>, FrpDeferredValue<Map<K, B>>> =
- mapLatestBuildForKey(deferredOf(initialValues), numKeys, transform)
-
- /**
- * Returns a [TFlow] containing the results of applying [transform] to each value of the
- * original [TFlow].
- *
- * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver.
- * With each invocation of [transform], changes from the previous invocation 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
- * previously-active [FrpBuildScope] will be undone with no replacement.
- */
- @ExperimentalFrpApi
- fun <K, A, B> TFlow<Map<K, Maybe<A>>>.mapLatestBuildForKey(
- numKeys: Int? = null,
- transform: FrpBuildScope.(K, A) -> B,
- ): TFlow<Map<K, Maybe<B>>> = mapLatestBuildForKey(emptyMap(), numKeys, transform).first
-
- /** Returns a [Deferred] containing the next value to be emitted from this [TFlow]. */
- @ExperimentalFrpApi
- fun <R> TFlow<R>.nextDeferred(): Deferred<R> {
- lateinit var next: CompletableDeferred<R>
- val job = nextOnly().observe { next.complete(it) }
- next = CompletableDeferred<R>(parent = job)
- return next
- }
-
- /** Returns a [TState] that reflects the [StateFlow.value] of this [StateFlow]. */
- @ExperimentalFrpApi
- fun <A> StateFlow<A>.toTState(): TState<A> {
- val initial = value
- return tFlow { dropWhile { it == initial }.collect { emit(it) } }.hold(initial)
- }
-
- /** Returns a [TFlow] that emits whenever this [Flow] emits. */
- @ExperimentalFrpApi
- fun <A> Flow<A>.toTFlow(name: String? = null): TFlow<A> = tFlow(name) { collect { emit(it) } }
-
- /**
- * Shorthand for:
- * ```kotlin
- * flow.toTFlow().hold(initialValue)
- * ```
- */
- @ExperimentalFrpApi
- fun <A> Flow<A>.toTState(initialValue: A): TState<A> = toTFlow().hold(initialValue)
-
- /**
- * Shorthand for:
- * ```kotlin
- * flow.scan(initialValue, operation).toTFlow().hold(initialValue)
- * ```
- */
- @ExperimentalFrpApi
- fun <A, B> Flow<A>.scanToTState(initialValue: B, operation: (B, A) -> B): TState<B> =
- scan(initialValue, operation).toTFlow().hold(initialValue)
-
- /**
- * Shorthand for:
- * ```kotlin
- * flow.scan(initialValue) { a, f -> f(a) }.toTFlow().hold(initialValue)
- * ```
- */
- @ExperimentalFrpApi
- fun <A> Flow<(A) -> A>.scanToTState(initialValue: A): TState<A> =
- scanToTState(initialValue) { a, f -> f(a) }
-
- /**
- * Invokes [block] whenever this [TFlow] emits a value. [block] receives an [FrpBuildScope] that
- * can be used to make further modifications to the FRP network, and/or perform side-effects via
- * [effect].
- *
- * With each invocation of [block], changes from the previous invocation are undone (any
- * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are
- * cancelled).
- */
- @ExperimentalFrpApi
- fun <A> TFlow<A>.observeLatestBuild(block: FrpBuildScope.(A) -> Unit = {}): Job =
- mapLatestBuild { block(it) }.observe()
-
- /**
- * Invokes [block] whenever this [TFlow] emits a value, allowing side-effects to be safely
- * performed in reaction to the emission.
- *
- * With each invocation of [block], running effects from the previous invocation are cancelled.
- */
- @ExperimentalFrpApi
- fun <A> TFlow<A>.observeLatest(block: FrpEffectScope.(A) -> Unit = {}): Job {
- var innerJob: Job? = null
- return observeBuild {
- innerJob?.cancel()
- innerJob = effect { block(it) }
- }
- }
-
- /**
- * Invokes [block] with the value held by this [TState], allowing side-effects to be safely
- * performed in reaction to the state changing.
- *
- * With each invocation of [block], running effects from the previous invocation are cancelled.
- */
- @ExperimentalFrpApi
- fun <A> TState<A>.observeLatest(block: FrpEffectScope.(A) -> Unit = {}): Job = launchScope {
- var innerJob = effect { block(sample()) }
- stateChanges.observeBuild {
- innerJob.cancel()
- innerJob = effect { block(it) }
- }
- }
-
- /**
- * Applies [block] to the value held by this [TState]. [block] receives an [FrpBuildScope] that
- * can be used to make further modifications to the FRP network, and/or perform side-effects via
- * [effect].
- *
- * [block] can perform modifications to the FRP network via its [FrpBuildScope] receiver. With
- * each invocation of [block], changes from the previous invocation are undone (any registered
- * [observers][observe] are unregistered, and any pending [side-effects][effect] are cancelled).
- */
- @ExperimentalFrpApi
- fun <A> TState<A>.observeLatestBuild(block: FrpBuildScope.(A) -> Unit = {}): Job = launchScope {
- var innerJob: Job = launchScope { block(sample()) }
- stateChanges.observeBuild {
- innerJob.cancel()
- innerJob = launchScope { block(it) }
- }
- }
-
- /** Applies the [FrpSpec] within this [FrpBuildScope]. */
- @ExperimentalFrpApi fun <A> FrpSpec<A>.applySpec(): A = this()
-
- /**
- * Applies the [FrpSpec] within this [FrpBuildScope], returning the result as an
- * [FrpDeferredValue].
- */
- @ExperimentalFrpApi
- fun <A> FrpSpec<A>.applySpecDeferred(): FrpDeferredValue<A> = deferredBuildScope { applySpec() }
-
- /**
- * Invokes [block] on the value held in this [TState]. [block] receives an [FrpBuildScope] that
- * can be used to make further modifications to the FRP network, and/or perform side-effects via
- * [effect].
- */
- @ExperimentalFrpApi
- fun <A> TState<A>.observeBuild(block: FrpBuildScope.(A) -> Unit = {}): Job = launchScope {
- block(sample())
- stateChanges.observeBuild(block)
- }
-
- /**
- * Invokes [block] with the current value of this [TState], re-invoking whenever it changes,
- * allowing side-effects to be safely performed in reaction value changing.
- *
- * Specifically, [block] is deferred to the end of the transaction, and is only actually
- * executed if this [FrpBuildScope] is still active by that time. It can be deactivated due to a
- * -Latest combinator, for example.
- *
- * If the [TState] is changing within the *current* transaction (i.e. [stateChanges] is
- * presently emitting) then [block] will be invoked for the first time with the new value;
- * otherwise, it will be invoked with the [current][sample] value.
- */
- @ExperimentalFrpApi
- fun <A> TState<A>.observe(block: FrpEffectScope.(A) -> Unit = {}): Job =
- now.map { sample() }.mergeWith(stateChanges) { _, new -> new }.observe { block(it) }
-}
-
-/**
- * Returns a [TFlow] that emits the result of [block] once it completes. [block] is evaluated
- * outside of the current FRP transaction; when it completes, the returned [TFlow] emits in a new
- * transaction.
- *
- * Shorthand for:
- * ```
- * tFlow { emitter: MutableTFlow<A> ->
- * val a = block()
- * emitter.emit(a)
- * }
- * ```
- */
-@ExperimentalFrpApi
-fun <A> FrpBuildScope.asyncTFlow(block: suspend () -> A): TFlow<A> =
- tFlow {
- // TODO: if block completes synchronously, it would be nice to emit within this
- // transaction
- emit(block())
- }
- .apply { observe() }
-
-/**
- * Performs a side-effect in a safe manner w/r/t the current FRP transaction.
- *
- * Specifically, [block] is deferred to the end of the current transaction, and is only actually
- * executed if this [FrpBuildScope] is still active by that time. It can be deactivated due to a
- * -Latest combinator, for example.
- *
- * Shorthand for:
- * ```kotlin
- * now.observe { block() }
- * ```
- */
-@ExperimentalFrpApi
-fun FrpBuildScope.effect(
- context: CoroutineContext = EmptyCoroutineContext,
- block: FrpEffectScope.() -> Unit,
-): Job = now.observe(context) { block() }
-
-/**
- * Launches [block] in a new coroutine, returning a [Job] bound to the coroutine.
- *
- * This coroutine is not actually started until the *end* of the current FRP transaction. This is
- * done because the current [FrpBuildScope] 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 { frpCoroutineScope.launch { block() } }
- * ```
- */
-@ExperimentalFrpApi
-fun FrpBuildScope.launchEffect(block: suspend CoroutineScope.() -> Unit): Job = asyncEffect(block)
-
-/**
- * Launches [block] in a new coroutine, returning the result as a [Deferred].
- *
- * This coroutine is not actually started until the *end* of the current FRP transaction. This is
- * done because the current [FrpBuildScope] 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
- * CompletableDeferred<R>.apply {
- * effect { frpCoroutineScope.launch { complete(coroutineScope { block() }) } }
- * }
- * .await()
- * ```
- */
-@ExperimentalFrpApi
-fun <R> FrpBuildScope.asyncEffect(block: suspend CoroutineScope.() -> R): Deferred<R> {
- val result = CompletableDeferred<R>()
- val job = now.observe { frpCoroutineScope.launch { result.complete(coroutineScope(block)) } }
- val handle = job.invokeOnCompletion { result.cancel() }
- result.invokeOnCompletion {
- handle.dispose()
- job.cancel()
- }
- return result
-}
-
-/** Like [FrpBuildScope.asyncScope], but ignores the result of [block]. */
-@ExperimentalFrpApi fun FrpBuildScope.launchScope(block: FrpSpec<*>): Job = asyncScope(block).second
-
-/**
- * Creates an instance of a [TFlow] with elements that are emitted from [builder].
- *
- * [builder] is run in its own coroutine, allowing for ongoing work that can emit to the provided
- * [MutableTFlow].
- *
- * By default, [builder] is only running while the returned [TFlow] is being
- * [observed][FrpBuildScope.observe]. If you want it to run at all times, simply add a no-op
- * observer:
- * ```kotlin
- * tFlow { ... }.apply { observe() }
- * ```
- *
- * In the event of backpressure, emissions are *coalesced* into batches. When a value is
- * [emitted][FrpCoalescingProducerScope.emit] from [builder], it is merged into the batch via
- * [coalesce]. Once the batch is consumed by the FRP network in the next transaction, the batch is
- * reset back to [initialValue].
- */
-@ExperimentalFrpApi
-fun <In, Out> FrpBuildScope.coalescingTFlow(
- initialValue: Out,
- coalesce: (old: Out, new: In) -> Out,
- builder: suspend FrpCoalescingProducerScope<In>.() -> Unit,
-): TFlow<Out> = coalescingTFlow(getInitialValue = { initialValue }, coalesce, builder)
-
-/**
- * Creates an instance of a [TFlow] with elements that are emitted from [builder].
- *
- * [builder] is run in its own coroutine, allowing for ongoing work that can emit to the provided
- * [MutableTFlow].
- *
- * By default, [builder] is only running while the returned [TFlow] is being
- * [observed][FrpBuildScope.observe]. If you want it to run at all times, simply add a no-op
- * observer:
- * ```kotlin
- * tFlow { ... }.apply { observe() }
- * ```
- *
- * In the event of backpressure, emissions are *conflated*; any older emissions are dropped and only
- * the most recent emission will be used when the FRP network is ready.
- */
-@ExperimentalFrpApi
-fun <T> FrpBuildScope.conflatedTFlow(
- builder: suspend FrpCoalescingProducerScope<T>.() -> Unit
-): TFlow<T> =
- coalescingTFlow<T, Any?>(initialValue = Any(), coalesce = { _, new -> new }, builder = builder)
- .mapCheap {
- @Suppress("UNCHECKED_CAST")
- it as T
- }
-
-/** Scope for emitting to a [FrpBuildScope.coalescingTFlow]. */
-interface FrpCoalescingProducerScope<in T> {
- /**
- * Inserts [value] into the current batch, enqueueing it for emission from this [TFlow] if not
- * already pending.
- *
- * Backpressure occurs when [emit] is called while the FRP network is currently in a
- * transaction; if called multiple times, then emissions will be coalesced into a single batch
- * that is then processed when the network is ready.
- */
- fun emit(value: T)
-}
-
-/** Scope for emitting to a [FrpBuildScope.tFlow]. */
-interface FrpProducerScope<in T> {
- /**
- * Emits a [value] to this [TFlow], suspending the caller until the FRP transaction containing
- * the emission has completed.
- */
- suspend fun emit(value: T)
-}
-
-/**
- * Suspends forever. Upon cancellation, runs [block]. Useful for unregistering callbacks inside of
- * [FrpBuildScope.tFlow] and [FrpBuildScope.coalescingTFlow].
- */
-suspend fun awaitClose(block: () -> Unit): Nothing =
- try {
- awaitCancellation()
- } finally {
- block()
- }
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpEffectScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpEffectScope.kt
deleted file mode 100644
index b39dcc131b1d..000000000000
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpEffectScope.kt
+++ /dev/null
@@ -1,49 +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 kotlin.coroutines.RestrictsSuspension
-import kotlinx.coroutines.CoroutineScope
-
-/**
- * Scope for external side-effects triggered by the Frp network. This still occurs within the
- * context of a transaction, so general suspending calls are disallowed to prevent blocking the
- * transaction. You can use [frpCoroutineScope] to [launch][kotlinx.coroutines.launch] new
- * coroutines to perform long-running asynchronous work. This scope is alive for the duration of the
- * containing [FrpBuildScope] that this side-effect scope is running in.
- */
-@RestrictsSuspension
-@ExperimentalFrpApi
-interface FrpEffectScope : FrpTransactionScope {
- /**
- * A [CoroutineScope] whose lifecycle lives for as long as this [FrpEffectScope] is alive. This
- * is generally until the [Job][kotlinx.coroutines.Job] returned by [FrpBuildScope.effect] is
- * cancelled.
- */
- @ExperimentalFrpApi val frpCoroutineScope: CoroutineScope
-
- /**
- * A [FrpNetwork] instance that can be used to transactionally query / modify the FRP network.
- *
- * The lambda passed to [FrpNetwork.transact] on this instance will receive an [FrpBuildScope]
- * that is lifetime-bound to this [FrpEffectScope]. Once this [FrpEffectScope] is no longer
- * alive, any modifications to the FRP network performed via this [FrpNetwork] instance will be
- * undone (any registered [observers][FrpBuildScope.observe] are unregistered, and any pending
- * [side-effects][FrpBuildScope.effect] are cancelled).
- */
- @ExperimentalFrpApi val frpNetwork: FrpNetwork
-}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpNetwork.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpNetwork.kt
deleted file mode 100644
index 0679848c6c80..000000000000
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpNetwork.kt
+++ /dev/null
@@ -1,197 +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.internal.BuildScopeImpl
-import com.android.systemui.kairos.internal.Network
-import com.android.systemui.kairos.internal.StateScopeImpl
-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.CoroutineName
-import kotlinx.coroutines.CoroutineScope
-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.
- */
-@RequiresOptIn(
- message = "This API is experimental and should not be used in general production code."
-)
-@Retention(AnnotationRetention.BINARY)
-annotation class ExperimentalFrpApi
-
-/**
- * External interface to an FRP network. Can be used to make transactional queries and modifications
- * to the network.
- */
-@ExperimentalFrpApi
-interface FrpNetwork {
- /**
- * Runs [block] inside of a transaction, suspending until the transaction is complete.
- *
- * The [FrpBuildScope] receiver exposes methods that can be used to query or modify the network.
- * If the network is cancelled while the caller of [transact] is suspended, then the call will
- * be cancelled.
- */
- @ExperimentalFrpApi suspend fun <R> transact(block: FrpTransactionScope.() -> R): R
-
- /**
- * Activates [spec] in a transaction, suspending indefinitely. While suspended, all observers
- * and long-running effects are kept alive. When cancelled, observers are unregistered and
- * effects are cancelled.
- */
- @ExperimentalFrpApi suspend fun activateSpec(spec: FrpSpec<*>)
-
- /** Returns a [CoalescingMutableTFlow] that can emit values into this [FrpNetwork]. */
- @ExperimentalFrpApi
- fun <In, Out> coalescingMutableTFlow(
- coalesce: (old: Out, new: In) -> Out,
- getInitialValue: () -> Out,
- ): CoalescingMutableTFlow<In, Out>
-
- /** Returns a [MutableTFlow] that can emit values into this [FrpNetwork]. */
- @ExperimentalFrpApi fun <T> mutableTFlow(): MutableTFlow<T>
-
- /** Returns a [MutableTState]. with initial state [initialValue]. */
- @ExperimentalFrpApi
- fun <T> mutableTStateDeferred(initialValue: FrpDeferredValue<T>): MutableTState<T>
-}
-
-/** Returns a [CoalescingMutableTFlow] that can emit values into this [FrpNetwork]. */
-@ExperimentalFrpApi
-fun <In, Out> FrpNetwork.coalescingMutableTFlow(
- coalesce: (old: Out, new: In) -> Out,
- initialValue: Out,
-): CoalescingMutableTFlow<In, Out> =
- coalescingMutableTFlow(coalesce, getInitialValue = { initialValue })
-
-/** Returns a [MutableTState]. with initial state [initialValue]. */
-@ExperimentalFrpApi
-fun <T> FrpNetwork.mutableTState(initialValue: T): MutableTState<T> =
- mutableTStateDeferred(deferredOf(initialValue))
-
-/** Returns a [MutableTState]. with initial state [initialValue]. */
-@ExperimentalFrpApi
-fun <T> MutableTState(network: FrpNetwork, initialValue: T): MutableTState<T> =
- network.mutableTState(initialValue)
-
-/** Returns a [MutableTFlow] that can emit values into this [FrpNetwork]. */
-@ExperimentalFrpApi
-fun <T> MutableTFlow(network: FrpNetwork): MutableTFlow<T> = network.mutableTFlow()
-
-/** Returns a [CoalescingMutableTFlow] that can emit values into this [FrpNetwork]. */
-@ExperimentalFrpApi
-fun <In, Out> CoalescingMutableTFlow(
- network: FrpNetwork,
- coalesce: (old: Out, new: In) -> Out,
- initialValue: Out,
-): CoalescingMutableTFlow<In, Out> = network.coalescingMutableTFlow(coalesce) { initialValue }
-
-/** Returns a [CoalescingMutableTFlow] that can emit values into this [FrpNetwork]. */
-@ExperimentalFrpApi
-fun <In, Out> CoalescingMutableTFlow(
- network: FrpNetwork,
- coalesce: (old: Out, new: In) -> Out,
- getInitialValue: () -> Out,
-): CoalescingMutableTFlow<In, Out> = network.coalescingMutableTFlow(coalesce, getInitialValue)
-
-/**
- * Activates [spec] in a transaction and invokes [block] with the result, suspending indefinitely.
- * While suspended, all observers and long-running effects are kept alive. When cancelled, observers
- * are unregistered and effects are cancelled.
- */
-@ExperimentalFrpApi
-suspend fun <R> FrpNetwork.activateSpec(spec: FrpSpec<R>, block: suspend (R) -> Unit) {
- activateSpec {
- val result = spec.applySpec()
- launchEffect { block(result) }
- }
-}
-
-internal class LocalFrpNetwork(
- private val network: Network,
- private val scope: CoroutineScope,
- private val endSignal: TFlow<Any>,
-) : FrpNetwork {
- override suspend fun <R> transact(block: FrpTransactionScope.() -> R): R =
- network.transaction("FrpNetwork.transact") { runInTransactionScope { block() } }.await()
-
- override suspend fun activateSpec(spec: FrpSpec<*>) {
- val stopEmitter =
- CoalescingMutableTFlow(
- name = "activateSpec",
- coalesce = { _, _: Unit -> },
- network = network,
- getInitialValue = {},
- )
- val job =
- network
- .transaction("FrpNetwork.activateSpec") {
- val buildScope =
- BuildScopeImpl(
- stateScope =
- StateScopeImpl(
- evalScope = this,
- endSignal = mergeLeft(stopEmitter, endSignal),
- ),
- coroutineScope = scope,
- )
- buildScope.runInBuildScope { launchScope(spec) }
- }
- .await()
- awaitCancellationAndThen {
- stopEmitter.emit(Unit)
- job.cancel()
- }
- }
-
- override fun <In, Out> coalescingMutableTFlow(
- coalesce: (old: Out, new: In) -> Out,
- getInitialValue: () -> Out,
- ): CoalescingMutableTFlow<In, Out> =
- CoalescingMutableTFlow(null, coalesce, network, getInitialValue)
-
- override fun <T> mutableTFlow(): MutableTFlow<T> = MutableTFlow(network)
-
- override fun <T> mutableTStateDeferred(initialValue: FrpDeferredValue<T>): MutableTState<T> =
- MutableTState(network, initialValue.unwrapped)
-}
-
-/**
- * Combination of an [FrpNetwork] and a [Job] that, when cancelled, will cancel the entire FRP
- * network.
- */
-@ExperimentalFrpApi
-class RootFrpNetwork
-internal constructor(private val network: Network, private val scope: CoroutineScope, job: Job) :
- Job by job, FrpNetwork by LocalFrpNetwork(network, scope, emptyTFlow)
-
-/** Constructs a new [RootFrpNetwork] in the given [CoroutineScope]. */
-@ExperimentalFrpApi
-fun CoroutineScope.newFrpNetwork(
- context: CoroutineContext = EmptyCoroutineContext
-): RootFrpNetwork {
- val scope = childScope(context)
- val network = Network(scope)
- scope.launch(CoroutineName("newFrpNetwork scheduler")) { network.runInputScheduler() }
- return RootFrpNetwork(network, scope, scope.coroutineContext.job)
-}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpScope.kt
deleted file mode 100644
index 92cb13f77d04..000000000000
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpScope.kt
+++ /dev/null
@@ -1,60 +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.internal.CompletableLazy
-import kotlin.coroutines.RestrictsSuspension
-
-/** Denotes [FrpScope] interfaces as [DSL markers][DslMarker]. */
-@DslMarker annotation class FrpScopeMarker
-
-/**
- * Base scope for all FRP scopes. Used to prevent implicitly capturing other scopes from in lambdas.
- */
-@FrpScopeMarker
-@RestrictsSuspension
-@ExperimentalFrpApi
-interface FrpScope {
- /**
- * Returns the value held by the [FrpDeferredValue], suspending until available if necessary.
- */
- @ExperimentalFrpApi fun <A> FrpDeferredValue<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 FrpScope.get
- */
-@ExperimentalFrpApi
-class FrpDeferredValue<out A> internal constructor(internal val unwrapped: Lazy<A>)
-
-/**
- * Returns the value held by this [FrpDeferredValue], or throws [IllegalStateException] if it is not
- * yet available.
- *
- * This API is not meant for general usage within the FRP network. It is made available mainly for
- * debugging and logging. You should always prefer [get][FrpScope.get] if possible.
- *
- * @see FrpScope.get
- */
-@ExperimentalFrpApi fun <A> FrpDeferredValue<A>.getUnsafe(): A = unwrapped.value
-
-/** Returns an already-available [FrpDeferredValue] containing [value]. */
-@ExperimentalFrpApi
-fun <A> deferredOf(value: A): FrpDeferredValue<A> = FrpDeferredValue(CompletableLazy(value))
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpStateScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpStateScope.kt
deleted file mode 100644
index 3de246300501..000000000000
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpStateScope.kt
+++ /dev/null
@@ -1,799 +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.combine as combinePure
-import com.android.systemui.kairos.map as mapPure
-import com.android.systemui.kairos.util.Just
-import com.android.systemui.kairos.util.Left
-import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.Right
-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.none
-import com.android.systemui.kairos.util.partitionEithers
-import com.android.systemui.kairos.util.zipWith
-import kotlin.coroutines.RestrictsSuspension
-
-typealias FrpStateful<R> = FrpStateScope.() -> R
-
-/**
- * Returns a [FrpStateful] that, when [applied][FrpStateScope.applyStateful], invokes [block] with
- * the applier's [FrpStateScope].
- */
-// TODO: caching story? should each Scope have a cache of applied FrpStateful instances?
-@ExperimentalFrpApi
-@Suppress("NOTHING_TO_INLINE")
-inline fun <A> statefully(noinline block: FrpStateScope.() -> A): FrpStateful<A> = block
-
-/**
- * Operations that accumulate state within the FRP network.
- *
- * State accumulation is an ongoing process that has a lifetime. Use `-Latest` combinators, such as
- * [mapLatestStateful], to create smaller, nested lifecycles so that accumulation isn't running
- * longer than needed.
- */
-@ExperimentalFrpApi
-@RestrictsSuspension
-interface FrpStateScope : FrpTransactionScope {
-
- /** TODO */
- @ExperimentalFrpApi
- // TODO: wish this could just be `deferred` but alas
- fun <A> deferredStateScope(block: FrpStateScope.() -> A): FrpDeferredValue<A>
-
- /**
- * Returns a [TState] that holds onto the most recently emitted value from this [TFlow], or
- * [initialValue] if nothing has been emitted since it was constructed.
- *
- * Note that the value contained within the [TState] is not updated until *after* all [TFlow]s
- * have been processed; this keeps the value of the [TState] consistent during the entire FRP
- * transaction.
- */
- @ExperimentalFrpApi fun <A> TFlow<A>.holdDeferred(initialValue: FrpDeferredValue<A>): TState<A>
-
- /**
- * Returns a [TFlow] that emits from a merged, incrementally-accumulated collection of [TFlow]s
- * emitted from this, following the same "patch" rules as outlined in [foldMapIncrementally].
- *
- * Conceptually this is equivalent to:
- * ```kotlin
- * fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementally(
- * initialTFlows: Map<K, TFlow<V>>,
- * ): TFlow<Map<K, V>> =
- * foldMapIncrementally(initialTFlows).map { it.merge() }.switch()
- * ```
- *
- * While the behavior is equivalent to the conceptual definition above, the implementation is
- * significantly more efficient.
- *
- * @see merge
- */
- @ExperimentalFrpApi
- fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementally(
- name: String? = null,
- initialTFlows: FrpDeferredValue<Map<K, TFlow<V>>>,
- ): TFlow<Map<K, V>>
-
- /**
- * Returns a [TFlow] that emits from a merged, incrementally-accumulated collection of [TFlow]s
- * emitted from this, following the same "patch" rules as outlined in [foldMapIncrementally].
- *
- * Conceptually this is equivalent to:
- * ```kotlin
- * fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementallyPrompt(
- * initialTFlows: Map<K, TFlow<V>>,
- * ): TFlow<Map<K, V>> =
- * foldMapIncrementally(initialTFlows).map { it.merge() }.switchPromptly()
- * ```
- *
- * While the behavior is equivalent to the conceptual definition above, the implementation is
- * significantly more efficient.
- *
- * @see merge
- */
- @ExperimentalFrpApi
- fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementallyPromptly(
- initialTFlows: FrpDeferredValue<Map<K, TFlow<V>>>,
- name: String? = null,
- ): TFlow<Map<K, V>>
-
- // TODO: everything below this comment can be made into extensions once we have context params
-
- /**
- * Returns a [TFlow] that emits from a merged, incrementally-accumulated collection of [TFlow]s
- * emitted from this, following the same "patch" rules as outlined in [foldMapIncrementally].
- *
- * Conceptually this is equivalent to:
- * ```kotlin
- * fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementally(
- * initialTFlows: Map<K, TFlow<V>>,
- * ): TFlow<Map<K, V>> =
- * foldMapIncrementally(initialTFlows).map { it.merge() }.switch()
- * ```
- *
- * While the behavior is equivalent to the conceptual definition above, the implementation is
- * significantly more efficient.
- *
- * @see merge
- */
- @ExperimentalFrpApi
- fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementally(
- name: String? = null,
- initialTFlows: Map<K, TFlow<V>> = emptyMap(),
- ): TFlow<Map<K, V>> = mergeIncrementally(name, deferredOf(initialTFlows))
-
- /**
- * Returns a [TFlow] that emits from a merged, incrementally-accumulated collection of [TFlow]s
- * emitted from this, following the same "patch" rules as outlined in [foldMapIncrementally].
- *
- * Conceptually this is equivalent to:
- * ```kotlin
- * fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementallyPrompt(
- * initialTFlows: Map<K, TFlow<V>>,
- * ): TFlow<Map<K, V>> =
- * foldMapIncrementally(initialTFlows).map { it.merge() }.switchPromptly()
- * ```
- *
- * While the behavior is equivalent to the conceptual definition above, the implementation is
- * significantly more efficient.
- *
- * @see merge
- */
- @ExperimentalFrpApi
- fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementallyPromptly(
- initialTFlows: Map<K, TFlow<V>> = emptyMap(),
- name: String? = null,
- ): TFlow<Map<K, V>> = mergeIncrementallyPromptly(deferredOf(initialTFlows), name)
-
- /** Applies the [FrpStateful] within this [FrpStateScope]. */
- @ExperimentalFrpApi fun <A> FrpStateful<A>.applyStateful(): A = this()
-
- /**
- * Applies the [FrpStateful] within this [FrpStateScope], returning the result as an
- * [FrpDeferredValue].
- */
- @ExperimentalFrpApi
- fun <A> FrpStateful<A>.applyStatefulDeferred(): FrpDeferredValue<A> = deferredStateScope {
- applyStateful()
- }
-
- /**
- * Returns a [TState] that holds onto the most recently emitted value from this [TFlow], or
- * [initialValue] if nothing has been emitted since it was constructed.
- *
- * Note that the value contained within the [TState] is not updated until *after* all [TFlow]s
- * have been processed; this keeps the value of the [TState] consistent during the entire FRP
- * transaction.
- */
- @ExperimentalFrpApi
- fun <A> TFlow<A>.hold(initialValue: A): TState<A> = holdDeferred(deferredOf(initialValue))
-
- /**
- * Returns a [TFlow] the emits the result of applying [FrpStatefuls][FrpStateful] emitted from
- * the original [TFlow].
- *
- * Unlike [applyLatestStateful], state accumulation is not stopped with each subsequent emission
- * of the original [TFlow].
- */
- @ExperimentalFrpApi fun <A> TFlow<FrpStateful<A>>.applyStatefuls(): TFlow<A>
-
- /**
- * Returns a [TFlow] containing the results of applying [transform] to each value of the
- * original [TFlow].
- *
- * [transform] can perform state accumulation via its [FrpStateScope] receiver. Unlike
- * [mapLatestStateful], accumulation is not stopped with each subsequent emission of the
- * original [TFlow].
- */
- @ExperimentalFrpApi
- fun <A, B> TFlow<A>.mapStateful(transform: FrpStateScope.(A) -> B): TFlow<B> =
- mapPure { statefully { transform(it) } }.applyStatefuls()
-
- /**
- * Returns a [TState] the holds the result of applying the [FrpStateful] held by the original
- * [TState].
- *
- * Unlike [applyLatestStateful], state accumulation is not stopped with each state change.
- */
- @ExperimentalFrpApi
- fun <A> TState<FrpStateful<A>>.applyStatefuls(): TState<A> =
- stateChanges
- .applyStatefuls()
- .holdDeferred(initialValue = deferredStateScope { sampleDeferred().get()() })
-
- /** Returns a [TFlow] that switches to the [TFlow] emitted by the original [TFlow]. */
- @ExperimentalFrpApi fun <A> TFlow<TFlow<A>>.flatten() = hold(emptyTFlow).switch()
-
- /**
- * Returns a [TFlow] containing the results of applying [transform] to each value of the
- * original [TFlow].
- *
- * [transform] can perform state accumulation via its [FrpStateScope] receiver. With each
- * invocation of [transform], state accumulation from previous invocation is stopped.
- */
- @ExperimentalFrpApi
- fun <A, B> TFlow<A>.mapLatestStateful(transform: FrpStateScope.(A) -> B): TFlow<B> =
- mapPure { statefully { transform(it) } }.applyLatestStateful()
-
- /**
- * Returns a [TFlow] that switches to a new [TFlow] produced by [transform] every time the
- * original [TFlow] emits a value.
- *
- * [transform] can perform state accumulation via its [FrpStateScope] receiver. With each
- * invocation of [transform], state accumulation from previous invocation is stopped.
- */
- @ExperimentalFrpApi
- fun <A, B> TFlow<A>.flatMapLatestStateful(transform: FrpStateScope.(A) -> TFlow<B>): TFlow<B> =
- mapLatestStateful(transform).flatten()
-
- /**
- * Returns a [TFlow] containing the results of applying each [FrpStateful] emitted from the
- * original [TFlow].
- *
- * When each [FrpStateful] is applied, state accumulation from the previously-active
- * [FrpStateful] is stopped.
- */
- @ExperimentalFrpApi
- fun <A> TFlow<FrpStateful<A>>.applyLatestStateful(): TFlow<A> = applyLatestStateful {}.first
-
- /**
- * Returns a [TState] containing the value returned by applying the [FrpStateful] held by the
- * original [TState].
- *
- * When each [FrpStateful] is applied, state accumulation from the previously-active
- * [FrpStateful] is stopped.
- */
- @ExperimentalFrpApi
- fun <A> TState<FrpStateful<A>>.applyLatestStateful(): TState<A> {
- val (changes, init) = stateChanges.applyLatestStateful { sample()() }
- return changes.holdDeferred(init)
- }
-
- /**
- * Returns a [TFlow] containing the results of applying each [FrpStateful] emitted from the
- * original [TFlow], and a [FrpDeferredValue] containing the result of applying [init]
- * immediately.
- *
- * When each [FrpStateful] is applied, state accumulation from the previously-active
- * [FrpStateful] is stopped.
- */
- @ExperimentalFrpApi
- fun <A, B> TFlow<FrpStateful<B>>.applyLatestStateful(
- init: FrpStateful<A>
- ): Pair<TFlow<B>, FrpDeferredValue<A>> {
- val (flow, result) =
- mapCheap { spec -> mapOf(Unit to just(spec)) }
- .applyLatestStatefulForKey(init = mapOf(Unit to init), numKeys = 1)
- val outFlow: TFlow<B> =
- flow.mapMaybe {
- checkNotNull(it[Unit]) { "applyLatest: expected result, but none present in: $it" }
- }
- val outInit: FrpDeferredValue<A> = deferredTransactionScope {
- val initResult: Map<Unit, A> = result.get()
- check(Unit in initResult) {
- "applyLatest: expected initial result, but none present in: $initResult"
- }
- @Suppress("UNCHECKED_CAST")
- initResult.getOrDefault(Unit) { null } as A
- }
- return Pair(outFlow, outInit)
- }
-
- /**
- * Returns a [TFlow] containing the results of applying each [FrpStateful] emitted from the
- * original [TFlow], and a [FrpDeferredValue] containing the result of applying [init]
- * immediately.
- *
- * If the [Maybe] contained within the value for an associated key is [none], then the
- * previously-active [FrpStateful] will be stopped with no replacement.
- *
- * When each [FrpStateful] is applied, state accumulation from the previously-active
- * [FrpStateful] with the same key is stopped.
- */
- @ExperimentalFrpApi
- fun <K, A, B> TFlow<Map<K, Maybe<FrpStateful<A>>>>.applyLatestStatefulForKey(
- init: FrpDeferredValue<Map<K, FrpStateful<B>>>,
- numKeys: Int? = null,
- ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>>
-
- /**
- * Returns a [TFlow] containing the results of applying each [FrpStateful] emitted from the
- * original [TFlow], and a [FrpDeferredValue] containing the result of applying [init]
- * immediately.
- *
- * When each [FrpStateful] is applied, state accumulation from the previously-active
- * [FrpStateful] with the same key is stopped.
- *
- * If the [Maybe] contained within the value for an associated key is [none], then the
- * previously-active [FrpStateful] will be stopped with no replacement.
- */
- @ExperimentalFrpApi
- fun <K, A, B> TFlow<Map<K, Maybe<FrpStateful<A>>>>.applyLatestStatefulForKey(
- init: Map<K, FrpStateful<B>>,
- numKeys: Int? = null,
- ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>> =
- applyLatestStatefulForKey(deferredOf(init), numKeys)
-
- /**
- * Returns a [TState] containing the latest results of applying each [FrpStateful] emitted from
- * the original [TFlow].
- *
- * When each [FrpStateful] is applied, state accumulation from the previously-active
- * [FrpStateful] with the same key is stopped.
- *
- * If the [Maybe] contained within the value for an associated key is [none], then the
- * previously-active [FrpStateful] will be stopped with no replacement.
- */
- @ExperimentalFrpApi
- fun <K, A> TFlow<Map<K, Maybe<FrpStateful<A>>>>.holdLatestStatefulForKey(
- init: FrpDeferredValue<Map<K, FrpStateful<A>>>,
- numKeys: Int? = null,
- ): TState<Map<K, A>> {
- val (changes, initialValues) = applyLatestStatefulForKey(init, numKeys)
- return changes.foldMapIncrementally(initialValues)
- }
-
- /**
- * Returns a [TState] containing the latest results of applying each [FrpStateful] emitted from
- * the original [TFlow].
- *
- * When each [FrpStateful] is applied, state accumulation from the previously-active
- * [FrpStateful] with the same key is stopped.
- *
- * If the [Maybe] contained within the value for an associated key is [none], then the
- * previously-active [FrpStateful] will be stopped with no replacement.
- */
- @ExperimentalFrpApi
- fun <K, A> TFlow<Map<K, Maybe<FrpStateful<A>>>>.holdLatestStatefulForKey(
- init: Map<K, FrpStateful<A>> = emptyMap(),
- numKeys: Int? = null,
- ): TState<Map<K, A>> = holdLatestStatefulForKey(deferredOf(init), numKeys)
-
- /**
- * Returns a [TFlow] containing the results of applying each [FrpStateful] emitted from the
- * original [TFlow], and a [FrpDeferredValue] containing the result of applying [init]
- * immediately.
- *
- * When each [FrpStateful] is applied, state accumulation from the previously-active
- * [FrpStateful] with the same key is stopped.
- *
- * If the [Maybe] contained within the value for an associated key is [none], then the
- * previously-active [FrpStateful] will be stopped with no replacement.
- */
- @ExperimentalFrpApi
- fun <K, A> TFlow<Map<K, Maybe<FrpStateful<A>>>>.applyLatestStatefulForKey(
- numKeys: Int? = null
- ): TFlow<Map<K, Maybe<A>>> =
- applyLatestStatefulForKey(init = emptyMap<K, FrpStateful<*>>(), numKeys = numKeys).first
-
- /**
- * Returns a [TFlow] containing the results of applying [transform] to each value of the
- * original [TFlow], and a [FrpDeferredValue] containing the result of applying [transform] to
- * [initialValues] immediately.
- *
- * [transform] can perform state accumulation via its [FrpStateScope] 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 [FrpStateScope] will be stopped with no replacement.
- */
- @ExperimentalFrpApi
- fun <K, A, B> TFlow<Map<K, Maybe<A>>>.mapLatestStatefulForKey(
- initialValues: FrpDeferredValue<Map<K, A>>,
- numKeys: Int? = null,
- transform: FrpStateScope.(A) -> B,
- ): Pair<TFlow<Map<K, Maybe<B>>>, FrpDeferredValue<Map<K, B>>> =
- mapPure { patch -> patch.mapValues { (_, v) -> v.map { statefully { transform(it) } } } }
- .applyLatestStatefulForKey(
- deferredStateScope {
- initialValues.get().mapValues { (_, v) -> statefully { transform(v) } }
- },
- numKeys = numKeys,
- )
-
- /**
- * Returns a [TFlow] containing the results of applying [transform] to each value of the
- * original [TFlow], and a [FrpDeferredValue] containing the result of applying [transform] to
- * [initialValues] immediately.
- *
- * [transform] can perform state accumulation via its [FrpStateScope] 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 [FrpStateScope] will be stopped with no replacement.
- */
- @ExperimentalFrpApi
- fun <K, A, B> TFlow<Map<K, Maybe<A>>>.mapLatestStatefulForKey(
- initialValues: Map<K, A>,
- numKeys: Int? = null,
- transform: FrpStateScope.(A) -> B,
- ): Pair<TFlow<Map<K, Maybe<B>>>, FrpDeferredValue<Map<K, B>>> =
- mapLatestStatefulForKey(deferredOf(initialValues), numKeys, transform)
-
- /**
- * Returns a [TFlow] containing the results of applying [transform] to each value of the
- * original [TFlow].
- *
- * [transform] can perform state accumulation via its [FrpStateScope] 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 [FrpStateScope] will be stopped with no replacement.
- */
- @ExperimentalFrpApi
- fun <K, A, B> TFlow<Map<K, Maybe<A>>>.mapLatestStatefulForKey(
- numKeys: Int? = null,
- transform: FrpStateScope.(A) -> B,
- ): TFlow<Map<K, Maybe<B>>> = mapLatestStatefulForKey(emptyMap(), numKeys, transform).first
-
- /**
- * Returns a [TFlow] that will only emit the next event of the original [TFlow], and then will
- * act as [emptyTFlow].
- *
- * If the original [TFlow] is emitting an event at this exact time, then it will be the only
- * even emitted from the result [TFlow].
- */
- @ExperimentalFrpApi
- fun <A> TFlow<A>.nextOnly(name: String? = null): TFlow<A> =
- if (this === emptyTFlow) {
- this
- } else {
- TFlowLoop<A>().also {
- it.loopback = it.mapCheap { emptyTFlow }.hold(this@nextOnly).switch(name)
- }
- }
-
- /** Returns a [TFlow] that skips the next emission of the original [TFlow]. */
- @ExperimentalFrpApi
- fun <A> TFlow<A>.skipNext(): TFlow<A> =
- if (this === emptyTFlow) {
- this
- } else {
- nextOnly().mapCheap { this@skipNext }.hold(emptyTFlow).switch()
- }
-
- /**
- * Returns a [TFlow] that emits values from the original [TFlow] up until [stop] emits a value.
- *
- * If the original [TFlow] emits at the same time as [stop], then the returned [TFlow] will emit
- * that value.
- */
- @ExperimentalFrpApi
- fun <A> TFlow<A>.takeUntil(stop: TFlow<*>): TFlow<A> =
- if (stop === emptyTFlow) {
- this
- } else {
- stop.mapCheap { emptyTFlow }.nextOnly().hold(this).switch()
- }
-
- /**
- * Invokes [stateful] in a new [FrpStateScope] that is a child of this one.
- *
- * This new scope is stopped when [stop] first emits a value, or when the parent scope is
- * stopped. Stopping will end all state accumulation; any [TStates][TState] returned from this
- * scope will no longer update.
- */
- @ExperimentalFrpApi
- fun <A> childStateScope(stop: TFlow<*>, stateful: FrpStateful<A>): FrpDeferredValue<A> {
- val (_, init: FrpDeferredValue<Map<Unit, A>>) =
- stop
- .nextOnly()
- .mapPure { mapOf(Unit to none<FrpStateful<A>>()) }
- .applyLatestStatefulForKey(init = mapOf(Unit to stateful), numKeys = 1)
- return deferredStateScope { init.get().getValue(Unit) }
- }
-
- /**
- * Returns a [TFlow] that emits values from the original [TFlow] up to and including a value is
- * emitted that satisfies [predicate].
- */
- @ExperimentalFrpApi
- fun <A> TFlow<A>.takeUntil(predicate: FrpTransactionScope.(A) -> Boolean): TFlow<A> =
- takeUntil(filter(predicate))
-
- /**
- * Returns a [TState] that is incrementally updated when this [TFlow] emits a value, by applying
- * [transform] to both the emitted value and the currently tracked state.
- *
- * Note that the value contained within the [TState] is not updated until *after* all [TFlow]s
- * have been processed; this keeps the value of the [TState] consistent during the entire FRP
- * transaction.
- */
- @ExperimentalFrpApi
- fun <A, B> TFlow<A>.fold(
- initialValue: B,
- transform: FrpTransactionScope.(A, B) -> B,
- ): TState<B> {
- lateinit var state: TState<B>
- return mapPure { a -> transform(a, state.sample()) }.hold(initialValue).also { state = it }
- }
-
- /**
- * Returns a [TState] that is incrementally updated when this [TFlow] emits a value, by applying
- * [transform] to both the emitted value and the currently tracked state.
- *
- * Note that the value contained within the [TState] is not updated until *after* all [TFlow]s
- * have been processed; this keeps the value of the [TState] consistent during the entire FRP
- * transaction.
- */
- @ExperimentalFrpApi
- fun <A, B> TFlow<A>.foldDeferred(
- initialValue: FrpDeferredValue<B>,
- transform: FrpTransactionScope.(A, B) -> B,
- ): TState<B> {
- lateinit var state: TState<B>
- return mapPure { a -> transform(a, state.sample()) }
- .holdDeferred(initialValue)
- .also { state = it }
- }
-
- /**
- * Returns a [TState] that holds onto the result of applying the most recently emitted
- * [FrpStateful] this [TFlow], or [init] if nothing has been emitted since it was constructed.
- *
- * When each [FrpStateful] is applied, state accumulation from the previously-active
- * [FrpStateful] is stopped.
- *
- * Note that the value contained within the [TState] is not updated until *after* all [TFlow]s
- * have been processed; this keeps the value of the [TState] consistent during the entire FRP
- * transaction.
- *
- * Shorthand for:
- * ```kotlin
- * val (changes, initApplied) = applyLatestStateful(init)
- * return changes.toTStateDeferred(initApplied)
- * ```
- */
- @ExperimentalFrpApi
- fun <A> TFlow<FrpStateful<A>>.holdLatestStateful(init: FrpStateful<A>): TState<A> {
- val (changes, initApplied) = applyLatestStateful(init)
- return changes.holdDeferred(initApplied)
- }
-
- /**
- * Returns a [TFlow] that emits the two most recent emissions from the original [TFlow].
- * [initialValue] is used as the previous value for the first emission.
- *
- * Shorthand for `sample(hold(init)) { new, old -> Pair(old, new) }`
- */
- @ExperimentalFrpApi
- fun <S, T : S> TFlow<T>.pairwise(initialValue: S): TFlow<WithPrev<S, T>> {
- val previous = hold(initialValue)
- return mapCheap { new -> WithPrev(previousValue = previous.sample(), newValue = new) }
- }
-
- /**
- * Returns a [TFlow] that emits the two most recent emissions from the original [TFlow]. Note
- * that the returned [TFlow] will not emit until the original [TFlow] has emitted twice.
- */
- @ExperimentalFrpApi
- fun <A> TFlow<A>.pairwise(): TFlow<WithPrev<A, A>> =
- mapCheap { just(it) }
- .pairwise(none)
- .mapMaybe { (prev, next) -> prev.zipWith(next, ::WithPrev) }
-
- /**
- * Returns a [TState] that holds both the current and previous values of the original [TState].
- * [initialPreviousValue] is used as the first previous value.
- *
- * Shorthand for `sample(hold(init)) { new, old -> Pair(old, new) }`
- */
- @ExperimentalFrpApi
- fun <S, T : S> TState<T>.pairwise(initialPreviousValue: S): TState<WithPrev<S, T>> =
- stateChanges
- .pairwise(initialPreviousValue)
- .holdDeferred(deferredTransactionScope { WithPrev(initialPreviousValue, sample()) })
-
- /**
- * Returns a [TState] 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].
- */
- @ExperimentalFrpApi
- fun <K, V> TFlow<Map<K, Maybe<V>>>.foldMapIncrementally(
- initialValues: FrpDeferredValue<Map<K, V>>
- ): TState<Map<K, V>> =
- foldDeferred(initialValues) { patch, map ->
- 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) }
- .partitionEithers()
- val removed: Map<K, V> = map - removes.toSet()
- val updated: Map<K, V> = removed + adds
- updated
- }
-
- /**
- * Returns a [TState] 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].
- */
- @ExperimentalFrpApi
- fun <K, V> TFlow<Map<K, Maybe<V>>>.foldMapIncrementally(
- initialValues: Map<K, V> = emptyMap()
- ): TState<Map<K, V>> = foldMapIncrementally(deferredOf(initialValues))
-
- /**
- * Returns a [TFlow] that wraps each emission of the original [TFlow] 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) }
- * ```
- */
- @ExperimentalFrpApi
- fun <A> TFlow<A>.withIndex(): TFlow<IndexedValue<A>> {
- val index = fold(0) { _, old -> old + 1 }
- return sample(index) { a, idx -> IndexedValue(idx, a) }
- }
-
- /**
- * Returns a [TFlow] containing the results of applying [transform] to each value of the
- * original [TFlow] and its index (starting from zero).
- *
- * Shorthand for:
- * ```
- * withIndex().map { (idx, a) -> transform(idx, a) }
- * ```
- */
- @ExperimentalFrpApi
- fun <A, B> TFlow<A>.mapIndexed(transform: FrpTransactionScope.(Int, A) -> B): TFlow<B> {
- val index = fold(0) { _, i -> i + 1 }
- return sample(index) { a, idx -> transform(idx, a) }
- }
-
- /** Returns a [TFlow] where all subsequent repetitions of the same value are filtered out. */
- @ExperimentalFrpApi
- fun <A> TFlow<A>.distinctUntilChanged(): TFlow<A> {
- val state: TState<Any?> = hold(Any())
- return filter { it != state.sample() }
- }
-
- /**
- * Returns a new [TFlow] that emits at the same rate as the original [TFlow], but combines the
- * emitted value with the most recent emission from [other] using [transform].
- *
- * Note that the returned [TFlow] will not emit anything until [other] has emitted at least one
- * value.
- */
- @ExperimentalFrpApi
- fun <A, B, C> TFlow<A>.sample(
- other: TFlow<B>,
- transform: FrpTransactionScope.(A, B) -> C,
- ): TFlow<C> {
- val state = other.mapCheap { just(it) }.hold(none)
- return sample(state) { a, b -> b.map { transform(a, it) } }.filterJust()
- }
-
- /**
- * Returns a [TState] that samples the [Transactional] held by the given [TState] within the
- * same transaction that the state changes.
- */
- @ExperimentalFrpApi
- fun <A> TState<Transactional<A>>.sampleTransactionals(): TState<A> =
- stateChanges
- .sampleTransactionals()
- .holdDeferred(deferredTransactionScope { sample().sample() })
-
- /**
- * Returns a [TState] that transforms the value held inside this [TState] by applying it to the
- * given function [transform].
- */
- @ExperimentalFrpApi
- fun <A, B> TState<A>.map(transform: FrpTransactionScope.(A) -> B): TState<B> =
- mapPure { transactionally { transform(it) } }.sampleTransactionals()
-
- /**
- * Returns a [TState] whose value is generated with [transform] by combining the current values
- * of each given [TState].
- *
- * @see TState.combineWith
- */
- @ExperimentalFrpApi
- fun <A, B, Z> combine(
- stateA: TState<A>,
- stateB: TState<B>,
- transform: FrpTransactionScope.(A, B) -> Z,
- ): TState<Z> =
- com.android.systemui.kairos
- .combine(stateA, stateB) { a, b -> transactionally { transform(a, b) } }
- .sampleTransactionals()
-
- /**
- * Returns a [TState] whose value is generated with [transform] by combining the current values
- * of each given [TState].
- *
- * @see TState.combineWithTransactionally
- */
- @ExperimentalFrpApi
- fun <A, B, C, Z> combine(
- stateA: TState<A>,
- stateB: TState<B>,
- stateC: TState<C>,
- transform: FrpTransactionScope.(A, B, C) -> Z,
- ): TState<Z> =
- com.android.systemui.kairos
- .combine(stateA, stateB, stateC) { a, b, c -> transactionally { transform(a, b, c) } }
- .sampleTransactionals()
-
- /**
- * Returns a [TState] whose value is generated with [transform] by combining the current values
- * of each given [TState].
- *
- * @see TState.combineWith
- */
- @ExperimentalFrpApi
- fun <A, B, C, D, Z> combine(
- stateA: TState<A>,
- stateB: TState<B>,
- stateC: TState<C>,
- stateD: TState<D>,
- transform: FrpTransactionScope.(A, B, C, D) -> Z,
- ): TState<Z> =
- com.android.systemui.kairos
- .combine(stateA, stateB, stateC, stateD) { a, b, c, d ->
- transactionally { transform(a, b, c, d) }
- }
- .sampleTransactionals()
-
- /** Returns a [TState] by applying [transform] to the value held by the original [TState]. */
- @ExperimentalFrpApi
- fun <A, B> TState<A>.flatMap(transform: FrpTransactionScope.(A) -> TState<B>): TState<B> =
- mapPure { transactionally { transform(it) } }.sampleTransactionals().flatten()
-
- /**
- * Returns a [TState] whose value is generated with [transform] by combining the current values
- * of each given [TState].
- *
- * @see TState.combineWith
- */
- @ExperimentalFrpApi
- fun <A, Z> combine(
- vararg states: TState<A>,
- transform: FrpTransactionScope.(List<A>) -> Z,
- ): TState<Z> = combinePure(*states).map(transform)
-
- /**
- * Returns a [TState] whose value is generated with [transform] by combining the current values
- * of each given [TState].
- *
- * @see TState.combineWith
- */
- @ExperimentalFrpApi
- fun <A, Z> Iterable<TState<A>>.combine(
- transform: FrpTransactionScope.(List<A>) -> Z
- ): TState<Z> = combinePure().map(transform)
-
- /**
- * Returns a [TState] by combining the values held inside the given [TState]s by applying them
- * to the given function [transform].
- */
- @ExperimentalFrpApi
- fun <A, B, C> TState<A>.combineWith(
- other: TState<B>,
- transform: FrpTransactionScope.(A, B) -> C,
- ): TState<C> = combine(this, other, transform)
-}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpTransactionScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpTransactionScope.kt
deleted file mode 100644
index 7d48b9853e1c..000000000000
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpTransactionScope.kt
+++ /dev/null
@@ -1,63 +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 kotlin.coroutines.RestrictsSuspension
-
-/**
- * FRP operations that are available while a transaction is active.
- *
- * These operations do not accumulate state, which makes [FrpTransactionScope] weaker than
- * [FrpStateScope], but allows them to be used in more places.
- */
-@ExperimentalFrpApi
-@RestrictsSuspension
-interface FrpTransactionScope : FrpScope {
-
- /**
- * Returns the current value of this [Transactional] as a [FrpDeferredValue].
- *
- * @see sample
- */
- @ExperimentalFrpApi fun <A> Transactional<A>.sampleDeferred(): FrpDeferredValue<A>
-
- /**
- * Returns the current value of this [TState] as a [FrpDeferredValue].
- *
- * @see sample
- */
- @ExperimentalFrpApi fun <A> TState<A>.sampleDeferred(): FrpDeferredValue<A>
-
- /** TODO */
- @ExperimentalFrpApi
- fun <A> deferredTransactionScope(block: FrpTransactionScope.() -> A): FrpDeferredValue<A>
-
- /** A [TFlow] that emits once, within this transaction, and then never again. */
- @ExperimentalFrpApi val now: TFlow<Unit>
-
- /**
- * Returns the current value held by this [TState]. Guaranteed to be consistent within the same
- * transaction.
- */
- @ExperimentalFrpApi fun <A> TState<A>.sample(): A = sampleDeferred().get()
-
- /**
- * Returns the current value held by this [Transactional]. Guaranteed to be consistent within
- * the same transaction.
- */
- @ExperimentalFrpApi fun <A> Transactional<A>.sample(): A = sampleDeferred().get()
-}
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
new file mode 100644
index 000000000000..77598b30658a
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosNetwork.kt
@@ -0,0 +1,216 @@
+/*
+ * 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.internal.BuildScopeImpl
+import com.android.systemui.kairos.internal.Network
+import com.android.systemui.kairos.internal.StateScopeImpl
+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.CoroutineName
+import kotlinx.coroutines.CoroutineScope
+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.
+ */
+@RequiresOptIn(
+ message = "This API is experimental and should not be used in general production code."
+)
+@Retention(AnnotationRetention.BINARY)
+annotation class ExperimentalKairosApi
+
+/**
+ * External interface to a Kairos network of reactive components. Can be used to make transactional
+ * queries and modifications to the network.
+ */
+@ExperimentalKairosApi
+interface KairosNetwork {
+ /**
+ * Runs [block] inside of a transaction, suspending until the transaction is complete.
+ *
+ * The [BuildScope] receiver exposes methods that can be used to query or modify the network. If
+ * the network is cancelled while the caller of [transact] is suspended, then the call will be
+ * cancelled.
+ */
+ suspend fun <R> transact(block: TransactionScope.() -> R): R
+
+ /**
+ * Activates [spec] in a transaction, suspending indefinitely. While suspended, all observers
+ * and long-running effects are kept alive. When cancelled, observers are unregistered and
+ * effects are cancelled.
+ */
+ suspend fun activateSpec(spec: BuildSpec<*>)
+
+ /** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */
+ fun <In, Out> coalescingMutableEvents(
+ coalesce: (old: Out, new: In) -> Out,
+ getInitialValue: () -> Out,
+ ): CoalescingMutableEvents<In, Out>
+
+ /** Returns a [MutableState] that can emit values into this [KairosNetwork]. */
+ fun <T> mutableEvents(): MutableEvents<T>
+
+ /** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */
+ fun <T> conflatedMutableEvents(): CoalescingMutableEvents<T, T>
+
+ /** Returns a [MutableState]. with initial state [initialValue]. */
+ fun <T> mutableStateDeferred(initialValue: DeferredValue<T>): MutableState<T>
+}
+
+/** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */
+@ExperimentalKairosApi
+fun <In, Out> KairosNetwork.coalescingMutableEvents(
+ coalesce: (old: Out, new: In) -> Out,
+ initialValue: Out,
+): CoalescingMutableEvents<In, Out> =
+ coalescingMutableEvents(coalesce, getInitialValue = { initialValue })
+
+/** Returns a [MutableState] with initial state [initialValue]. */
+@ExperimentalKairosApi
+fun <T> KairosNetwork.mutableState(initialValue: T): MutableState<T> =
+ mutableStateDeferred(deferredOf(initialValue))
+
+/** Returns a [MutableState] with initial state [initialValue]. */
+@ExperimentalKairosApi
+fun <T> MutableState(network: KairosNetwork, initialValue: T): MutableState<T> =
+ network.mutableState(initialValue)
+
+/** Returns a [MutableEvents] that can emit values into this [KairosNetwork]. */
+@ExperimentalKairosApi
+fun <T> MutableEvents(network: KairosNetwork): MutableEvents<T> = network.mutableEvents()
+
+/** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */
+@ExperimentalKairosApi
+fun <In, Out> CoalescingMutableEvents(
+ network: KairosNetwork,
+ coalesce: (old: Out, new: In) -> Out,
+ initialValue: Out,
+): CoalescingMutableEvents<In, Out> = network.coalescingMutableEvents(coalesce) { initialValue }
+
+/** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */
+@ExperimentalKairosApi
+fun <In, Out> CoalescingMutableEvents(
+ network: KairosNetwork,
+ coalesce: (old: Out, new: In) -> Out,
+ getInitialValue: () -> Out,
+): CoalescingMutableEvents<In, Out> = network.coalescingMutableEvents(coalesce, getInitialValue)
+
+/** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */
+@ExperimentalKairosApi
+fun <T> ConflatedMutableEvents(network: KairosNetwork): CoalescingMutableEvents<T, T> =
+ network.conflatedMutableEvents()
+
+/**
+ * Activates [spec] in a transaction and invokes [block] with the result, suspending indefinitely.
+ * While suspended, all observers and long-running effects are kept alive. When cancelled, observers
+ * are unregistered and effects are cancelled.
+ */
+@ExperimentalKairosApi
+suspend fun <R> KairosNetwork.activateSpec(spec: BuildSpec<R>, block: suspend (R) -> Unit) {
+ activateSpec {
+ val result = spec.applySpec()
+ launchEffect { block(result) }
+ }
+}
+
+internal class LocalNetwork(
+ private val network: Network,
+ private val scope: CoroutineScope,
+ private val endSignal: Events<Any>,
+) : KairosNetwork {
+ override suspend fun <R> transact(block: TransactionScope.() -> R): R =
+ network.transaction("KairosNetwork.transact") { block() }.await()
+
+ 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)
+ }
+ .await()
+ awaitCancellationAndThen {
+ stopEmitter.emit(Unit)
+ job.cancel()
+ }
+ }
+
+ override fun <In, Out> coalescingMutableEvents(
+ coalesce: (old: Out, new: In) -> Out,
+ getInitialValue: () -> Out,
+ ): CoalescingMutableEvents<In, Out> =
+ CoalescingMutableEvents(
+ null,
+ coalesce = { old, new -> coalesce(old.value, new) },
+ network,
+ getInitialValue,
+ )
+
+ override fun <T> conflatedMutableEvents(): CoalescingMutableEvents<T, T> =
+ CoalescingMutableEvents(
+ null,
+ coalesce = { _, new -> new },
+ network,
+ { error("WTF: init value accessed for conflatedMutableEvents") },
+ )
+
+ override fun <T> mutableEvents(): MutableEvents<T> = MutableEvents(network)
+
+ override fun <T> mutableStateDeferred(initialValue: DeferredValue<T>): MutableState<T> =
+ MutableState(network, initialValue.unwrapped)
+}
+
+/**
+ * Combination of an [KairosNetwork] and a [Job] that, when cancelled, will cancel the entire Kairos
+ * network.
+ */
+@ExperimentalKairosApi
+class RootKairosNetwork
+internal constructor(private val network: Network, private val scope: CoroutineScope, job: Job) :
+ Job by job, KairosNetwork by LocalNetwork(network, scope, emptyEvents)
+
+/** Constructs a new [RootKairosNetwork] in the given [CoroutineScope]. */
+@ExperimentalKairosApi
+fun CoroutineScope.launchKairosNetwork(
+ context: CoroutineContext = EmptyCoroutineContext
+): RootKairosNetwork {
+ val scope = childScope(context)
+ val network = Network(scope)
+ scope.launch(CoroutineName("launchKairosNetwork scheduler")) { network.runInputScheduler() }
+ return RootKairosNetwork(network, scope, scope.coroutineContext.job)
+}
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
new file mode 100644
index 000000000000..ce3e9235efa8
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosScope.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.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
+ * 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))
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
new file mode 100644
index 000000000000..08b27c86c9b9
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/State.kt
@@ -0,0 +1,528 @@
+/*
+ * 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.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
+import com.android.systemui.kairos.internal.Network
+import com.android.systemui.kairos.internal.NoScope
+import com.android.systemui.kairos.internal.Schedulable
+import com.android.systemui.kairos.internal.StateImpl
+import com.android.systemui.kairos.internal.StateSource
+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.constState
+import com.android.systemui.kairos.internal.filterImpl
+import com.android.systemui.kairos.internal.flatMap
+import com.android.systemui.kairos.internal.init
+import com.android.systemui.kairos.internal.map
+import com.android.systemui.kairos.internal.mapCheap
+import com.android.systemui.kairos.internal.mapImpl
+import com.android.systemui.kairos.internal.util.hashString
+import com.android.systemui.kairos.internal.zipStateMap
+import com.android.systemui.kairos.internal.zipStates
+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.
+ */
+@ExperimentalKairosApi sealed class State<out A>
+
+/** A [State] that never changes. */
+@ExperimentalKairosApi
+fun <A> stateOf(value: A): State<A> {
+ val operatorName = "stateOf"
+ val name = "$operatorName($value)"
+ return StateInit(constInit(name, constState(name, operatorName, value)))
+}
+
+/**
+ * Returns a [State] that acts as a deferred-reference to the [State] produced by this [Lazy].
+ *
+ * When the returned [State] is accessed by the Kairos network, the [Lazy]'s [value][Lazy.value]
+ * will be queried and used.
+ *
+ * Useful for recursive definitions.
+ */
+@ExperimentalKairosApi fun <A> Lazy<State<A>>.defer(): State<A> = deferInline { value }
+
+/**
+ * Returns a [State] that acts as a deferred-reference to the [State] produced by this
+ * [DeferredValue].
+ *
+ * When the returned [State] is accessed by the Kairos network, the [DeferredValue] will be queried
+ * and used.
+ *
+ * Useful for recursive definitions.
+ */
+@ExperimentalKairosApi
+fun <A> DeferredValue<State<A>>.defer(): State<A> = deferInline { unwrapped.value }
+
+/**
+ * Returns a [State] that acts as a deferred-reference to the [State] produced by [block].
+ *
+ * When the returned [State] is accessed by the Kairos network, [block] will be invoked and the
+ * returned [State] will be used.
+ *
+ * Useful for recursive definitions.
+ */
+@ExperimentalKairosApi
+fun <A> deferredState(block: KairosScope.() -> State<A>): State<A> = deferInline { NoScope.block() }
+
+/**
+ * Returns a [State] containing the results of applying [transform] to the value held by the
+ * original [State].
+ */
+@ExperimentalKairosApi
+fun <A, B> State<A>.map(transform: KairosScope.(A) -> B): State<B> {
+ val operatorName = "map"
+ val name = operatorName
+ return StateInit(
+ init(name) {
+ init.connect(evalScope = this).map(name, operatorName) { NoScope.transform(it) }
+ }
+ )
+}
+
+/**
+ * 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].
+ */
+@ExperimentalKairosApi
+fun <A, B> State<A>.mapCheapUnsafe(transform: KairosScope.(A) -> B): State<B> {
+ val operatorName = "map"
+ val name = operatorName
+ return StateInit(
+ init(name) {
+ init.connect(evalScope = this).mapCheap(name, operatorName) { NoScope.transform(it) }
+ }
+ )
+}
+
+/**
+ * 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)
+ * ```
+ */
+@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) {
+ zipStates(name, operatorName, states = map { it.init.connect(evalScope = 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,
+ states = 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) {
+ val dl1 = stateA.init.connect(evalScope = this@init)
+ val dl2 = stateB.init.connect(evalScope = this@init)
+ zipStates(name, operatorName, dl1, dl2) { 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) {
+ val dl1 = stateA.init.connect(evalScope = this@init)
+ val dl2 = stateB.init.connect(evalScope = this@init)
+ val dl3 = stateC.init.connect(evalScope = this@init)
+ zipStates(name, operatorName, dl1, dl2, dl3) { 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) {
+ val dl1 = stateA.init.connect(evalScope = this@init)
+ val dl2 = stateB.init.connect(evalScope = this@init)
+ val dl3 = stateC.init.connect(evalScope = this@init)
+ val dl4 = stateD.init.connect(evalScope = this@init)
+ zipStates(name, operatorName, dl1, dl2, dl3, dl4) { 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.combineWith
+ */
+@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) {
+ val dl1 = stateA.init.connect(evalScope = this@init)
+ val dl2 = stateB.init.connect(evalScope = this@init)
+ val dl3 = stateC.init.connect(evalScope = this@init)
+ val dl4 = stateD.init.connect(evalScope = this@init)
+ val dl5 = stateE.init.connect(evalScope = this@init)
+ zipStates(name, operatorName, dl1, dl2, dl3, dl4, dl5) { 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
+ return StateInit(
+ init(name) {
+ init.connect(this).flatMap(name, operatorName) { a ->
+ NoScope.transform(a).init.connect(this)
+ }
+ }
+ )
+}
+
+/** 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.
+ *
+ * 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)
+ * ```
+ *
+ * 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().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) {
+ DerivedMapCheap(
+ name,
+ operatorName,
+ upstream = upstream.init.connect(evalScope = this),
+ changes = groupedChanges.impl.eventsForKey(value),
+ ) {
+ it == value
+ }
+ }
+ )
+ }
+
+ operator fun get(value: A): State<Boolean> = whenSelected(value)
+}
+
+/**
+ * A mutable [State] that provides the ability to manually [set its value][setValue].
+ *
+ * Multiple invocations of [setValue] that occur before a transaction are conflated; only the most
+ * recent value is used.
+ *
+ * Effectively equivalent to:
+ * ``` kotlin
+ * ConflatedMutableEvents(kairosNetwork).holdState(initialValue)
+ * ```
+ */
+@ExperimentalKairosApi
+class MutableState<T> internal constructor(internal val network: Network, initialValue: Lazy<T>) :
+ State<T>() {
+
+ private val input: CoalescingMutableEvents<Lazy<T>, Lazy<T>?> =
+ CoalescingMutableEvents(
+ name = null,
+ coalesce = { _, new -> new },
+ network = network,
+ getInitialValue = { null },
+ )
+
+ internal val state = run {
+ val changes = input.impl
+ val name = null
+ val operatorName = "MutableState"
+ lateinit var state: StateSource<T>
+ val mapImpl = mapImpl(upstream = { changes.activated() }) { it, _ -> it!!.value }
+ val calm: EventsImpl<T> =
+ filterImpl({ mapImpl }) { new ->
+ new != state.getCurrentWithEpoch(evalScope = this).first
+ }
+ .cached()
+ state = StateSource(name, operatorName, initialValue, calm)
+ @Suppress("DeferredResultUnused")
+ network.transaction("MutableState.init") {
+ calm.activate(evalScope = this, downstream = Schedulable.S(state))?.let {
+ (connection, needsEval) ->
+ state.upstreamConnection = connection
+ if (needsEval) {
+ schedule(state)
+ }
+ }
+ }
+ StateInit(constInit(name, state))
+ }
+
+ /**
+ * Sets the value held by this [State].
+ *
+ * Invoking will cause a [state change event][State.changes] to emit with the new value, which
+ * will then be applied (and thus returned by [TransactionScope.sample]) after the transaction
+ * is complete.
+ *
+ * Multiple invocations of [setValue] that occur before a transaction are conflated; only the
+ * most recent value is used.
+ */
+ fun setValue(value: T) = input.emit(CompletableLazy(value))
+
+ /**
+ * Sets the value held by this [State]. The [DeferredValue] will not be queried until this
+ * [State] is explicitly [sampled][TransactionScope.sample] or [observed][BuildScope.observe].
+ *
+ * Invoking will cause a [state change event][State.changes] to emit with the new value, which
+ * will then be applied (and thus returned by [TransactionScope.sample]) after the transaction
+ * is complete.
+ *
+ * Multiple invocations of [setValue] that occur before a transaction are conflated; only the
+ * most recent value is used.
+ */
+ fun setValueDeferred(value: DeferredValue<T>) = input.emit(value.unwrapped)
+}
+
+/** A forward-reference to a [State], allowing for recursive definitions. */
+@ExperimentalKairosApi
+class StateLoop<A> : State<A>() {
+
+ private val name: String? = null
+
+ private val deferred = CompletableLazy<State<A>>()
+
+ internal val init: Init<StateImpl<A>> =
+ init(name) { deferred.value.init.connect(evalScope = this) }
+
+ /** The [State] this [StateLoop] will forward to. */
+ var loopback: State<A>? = null
+ set(value) {
+ value?.let {
+ check(!deferred.isInitialized()) { "StateLoop.loopback has already been set." }
+ deferred.setValue(value)
+ field = value
+ }
+ }
+
+ operator fun getValue(thisRef: Any?, property: KProperty<*>): State<A> = this
+
+ operator fun setValue(thisRef: Any?, property: KProperty<*>, value: State<A>) {
+ loopback = value
+ }
+
+ override fun toString(): String = "${this::class.simpleName}@$hashString"
+}
+
+internal class StateInit<A> internal constructor(internal val init: Init<StateImpl<A>>) :
+ State<A>() {
+ override fun toString(): String = "${this::class.simpleName}@$hashString"
+}
+
+internal val <A> State<A>.init: Init<StateImpl<A>>
+ get() =
+ when (this) {
+ is StateInit -> init
+ is StateLoop -> init
+ is MutableState -> state.init
+ }
+
+private inline fun <A> deferInline(crossinline block: InitScope.() -> State<A>): State<A> =
+ StateInit(init(name = null) { block().init.connect(evalScope = this) })
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
new file mode 100644
index 000000000000..b1f48bb1ce56
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/StateScope.kt
@@ -0,0 +1,760 @@
+/*
+ * 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.Just
+import com.android.systemui.kairos.util.Left
+import com.android.systemui.kairos.util.Maybe
+import com.android.systemui.kairos.util.Right
+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.none
+import com.android.systemui.kairos.util.partitionEithers
+import com.android.systemui.kairos.util.zipWith
+
+// TODO: caching story? should each Scope have a cache of applied Stateful instances?
+/** A computation that can accumulate [Events] into [State]. */
+typealias Stateful<R> = StateScope.() -> R
+
+/**
+ * Returns a [Stateful] that, when [applied][StateScope.applyStateful], invokes [block] with the
+ * applier's [StateScope].
+ */
+@ExperimentalKairosApi
+@Suppress("NOTHING_TO_INLINE")
+inline fun <A> statefully(noinline block: StateScope.() -> A): Stateful<A> = block
+
+/**
+ * Operations that accumulate state within the Kairos network.
+ *
+ * State accumulation is an ongoing process that has a lifetime. Use `-Latest` combinators, such as
+ * [mapLatestStateful], to create smaller, nested lifecycles so that accumulation isn't running
+ * longer than needed.
+ */
+@ExperimentalKairosApi
+interface StateScope : TransactionScope {
+
+ /**
+ * Defers invoking [block] until after the current [StateScope] code-path completes, returning a
+ * [DeferredValue] that can be used to reference the result.
+ *
+ * Useful for recursive definitions.
+ *
+ * @see DeferredValue
+ */
+ fun <A> deferredStateScope(block: StateScope.() -> A): DeferredValue<A>
+
+ /**
+ * Returns a [State] that holds onto the most recently emitted value from this [Events], or
+ * [initialValue] if nothing has been emitted since it was constructed.
+ *
+ * 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.
+ */
+ fun <A> Events<A>.holdStateDeferred(initialValue: DeferredValue<A>): State<A>
+
+ /**
+ * 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].
+ *
+ * Conceptually this is equivalent to:
+ * ```kotlin
+ * fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally(
+ * initialEvents: Map<K, Events<V>>,
+ * ): Events<Map<K, V>> =
+ * foldMapIncrementally(initialEvents).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> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally(
+ name: String? = null,
+ initialEvents: DeferredValue<Map<K, Events<V>>>,
+ ): Events<Map<K, V>>
+
+ /**
+ * 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].
+ *
+ * Conceptually this is equivalent to:
+ * ```kotlin
+ * fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementallyPromptly(
+ * initialEvents: Map<K, Events<V>>,
+ * ): Events<Map<K, V>> =
+ * foldMapIncrementally(initialEvents).map { it.merge() }.switchEventsPromptly()
+ * ```
+ *
+ * While the behavior is equivalent to the conceptual definition above, the implementation is
+ * significantly more efficient.
+ *
+ * @see merge
+ */
+ fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementallyPromptly(
+ initialEvents: DeferredValue<Map<K, Events<V>>>,
+ name: String? = null,
+ ): Events<Map<K, V>>
+
+ // 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].
+ *
+ * Conceptually this is equivalent to:
+ * ```kotlin
+ * fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally(
+ * initialEvents: Map<K, Events<V>>,
+ * ): Events<Map<K, V>> =
+ * foldMapIncrementally(initialEvents).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> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally(
+ name: String? = null,
+ initialEvents: Map<K, Events<V>> = emptyMap(),
+ ): Events<Map<K, V>> = mergeIncrementally(name, 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].
+ *
+ * Conceptually this is equivalent to:
+ * ```kotlin
+ * fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementallyPromptly(
+ * initialEvents: Map<K, Events<V>>,
+ * ): Events<Map<K, V>> =
+ * foldMapIncrementally(initialEvents).map { it.merge() }.switchEventsPromptly()
+ * ```
+ *
+ * While the behavior is equivalent to the conceptual definition above, the implementation is
+ * significantly more efficient.
+ *
+ * @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)
+
+ /** 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].
+ */
+ fun <A> Stateful<A>.applyStatefulDeferred(): DeferredValue<A> = deferredStateScope {
+ applyStateful()
+ }
+
+ /**
+ * Returns a [State] that holds onto the most recently emitted value from this [Events], or
+ * [initialValue] if nothing has been emitted since it was constructed.
+ *
+ * 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.
+ */
+ 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].
+ */
+ fun <A, B> Events<A>.mapStateful(transform: StateScope.(A) -> B): Events<B> =
+ map { 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.
+ */
+ fun <A> State<Stateful<A>>.applyStatefuls(): State<A> =
+ changes
+ .applyStatefuls()
+ .holdStateDeferred(initialValue = deferredStateScope { sampleDeferred().get()() })
+
+ /** Returns an [Events] that switches to the [Events] emitted by the original [Events]. */
+ fun <A> Events<Events<A>>.flatten() = holdState(emptyEvents).switchEvents()
+
+ /**
+ * 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. With each
+ * invocation of [transform], state accumulation from previous invocation is stopped.
+ */
+ fun <A, B> Events<A>.mapLatestStateful(transform: StateScope.(A) -> B): Events<B> =
+ map { statefully { transform(it) } }.applyLatestStateful()
+
+ /**
+ * Returns an [Events] that switches to a new [Events] produced by [transform] every time the
+ * original [Events] emits a value.
+ *
+ * [transform] can perform state accumulation via its [StateScope] receiver. With each
+ * invocation of [transform], state accumulation from previous invocation is stopped.
+ */
+ fun <A, B> Events<A>.flatMapLatestStateful(transform: StateScope.(A) -> Events<B>): Events<B> =
+ mapLatestStateful(transform).flatten()
+
+ /**
+ * Returns an [Events] containing the results of applying each [Stateful] emitted from the
+ * original [Events].
+ *
+ * When each [Stateful] is applied, state accumulation from the previously-active [Stateful] is
+ * stopped.
+ */
+ fun <A> Events<Stateful<A>>.applyLatestStateful(): Events<A> = applyLatestStateful {}.first
+
+ /**
+ * Returns a [State] containing the value returned by applying the [Stateful] held by the
+ * original [State].
+ *
+ * When each [Stateful] is applied, state accumulation from the previously-active [Stateful] is
+ * stopped.
+ */
+ fun <A> State<Stateful<A>>.applyLatestStateful(): State<A> {
+ val (changes, init) = changes.applyLatestStateful { sample()() }
+ return changes.holdStateDeferred(init)
+ }
+
+ /**
+ * 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.
+ *
+ * When each [Stateful] is applied, state accumulation from the previously-active [Stateful] is
+ * stopped.
+ */
+ fun <A, B> Events<Stateful<B>>.applyLatestStateful(
+ init: Stateful<A>
+ ): Pair<Events<B>, DeferredValue<A>> {
+ val (events, result) =
+ mapCheap { spec -> mapOf(Unit to just(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()
+ check(Unit in initResult) {
+ "applyLatest: expected initial result, but none present in: $initResult"
+ }
+ @Suppress("UNCHECKED_CAST")
+ initResult.getOrDefault(Unit) { null } as A
+ }
+ return Pair(outEvents, outInit)
+ }
+
+ /**
+ * 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 [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.
+ */
+ fun <K, A, B> Events<Map<K, Maybe<Stateful<A>>>>.applyLatestStatefulForKey(
+ init: DeferredValue<Map<K, Stateful<B>>>,
+ numKeys: Int? = null,
+ ): Pair<Events<Map<K, Maybe<A>>>, DeferredValue<Map<K, B>>>
+
+ /**
+ * 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.
+ *
+ * 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.
+ */
+ 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)
+
+ /**
+ * Returns a [State] containing the latest results of applying each [Stateful] emitted from the
+ * 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.
+ */
+ fun <K, A> Events<Map<K, Maybe<Stateful<A>>>>.holdLatestStatefulForKey(
+ init: DeferredValue<Map<K, Stateful<A>>>,
+ numKeys: Int? = null,
+ ): State<Map<K, A>> {
+ val (changes, initialValues) = applyLatestStatefulForKey(init, numKeys)
+ return changes.foldStateMapIncrementally(initialValues)
+ }
+
+ /**
+ * Returns a [State] containing the latest results of applying each [Stateful] emitted from the
+ * 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.
+ */
+ fun <K, A> Events<Map<K, Maybe<Stateful<A>>>>.holdLatestStatefulForKey(
+ init: Map<K, Stateful<A>> = emptyMap(),
+ numKeys: Int? = null,
+ ): State<Map<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 [init]
+ * immediately.
+ *
+ * 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.
+ */
+ fun <K, A> Events<Map<K, Maybe<Stateful<A>>>>.applyLatestStatefulForKey(
+ numKeys: Int? = null
+ ): Events<Map<K, Maybe<A>>> =
+ applyLatestStatefulForKey(init = emptyMap<K, Stateful<*>>(), numKeys = numKeys).first
+
+ /**
+ * Returns an [Events] containing the results of applying [transform] to each value of the
+ * original [Events], and a [DeferredValue] containing the result of applying [transform] to
+ * [initialValues] immediately.
+ *
+ * [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.
+ */
+ fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestStatefulForKey(
+ initialValues: DeferredValue<Map<K, A>>,
+ numKeys: Int? = null,
+ transform: StateScope.(A) -> B,
+ ): Pair<Events<Map<K, Maybe<B>>>, DeferredValue<Map<K, B>>> =
+ map { patch -> patch.mapValues { (_, v) -> v.map { statefully { transform(it) } } } }
+ .applyLatestStatefulForKey(
+ deferredStateScope {
+ initialValues.get().mapValues { (_, v) -> statefully { transform(v) } }
+ },
+ numKeys = numKeys,
+ )
+
+ /**
+ * Returns an [Events] containing the results of applying [transform] to each value of the
+ * original [Events], and a [DeferredValue] containing the result of applying [transform] to
+ * [initialValues] immediately.
+ *
+ * [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.
+ */
+ fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestStatefulForKey(
+ initialValues: Map<K, A>,
+ numKeys: Int? = null,
+ transform: StateScope.(A) -> B,
+ ): Pair<Events<Map<K, Maybe<B>>>, DeferredValue<Map<K, B>>> =
+ mapLatestStatefulForKey(deferredOf(initialValues), numKeys, transform)
+
+ /**
+ * 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. 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.
+ */
+ fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestStatefulForKey(
+ numKeys: Int? = null,
+ transform: StateScope.(A) -> B,
+ ): Events<Map<K, Maybe<B>>> = mapLatestStatefulForKey(emptyMap(), numKeys, transform).first
+
+ /**
+ * Returns an [Events] that will only emit the next event of the original [Events], and then
+ * will act as [emptyEvents].
+ *
+ * If the original [Events] is emitting an event at this exact time, then it will be the only
+ * even emitted from the result [Events].
+ */
+ fun <A> Events<A>.nextOnly(name: String? = null): Events<A> =
+ if (this === emptyEvents) {
+ this
+ } else {
+ EventsLoop<A>().also {
+ it.loopback =
+ it.mapCheap { emptyEvents }.holdState(this@nextOnly).switchEvents(name)
+ }
+ }
+
+ /** Returns an [Events] that skips the next emission of the original [Events]. */
+ fun <A> Events<A>.skipNext(): Events<A> =
+ if (this === emptyEvents) {
+ this
+ } else {
+ nextOnly().mapCheap { this@skipNext }.holdState(emptyEvents).switchEvents()
+ }
+
+ /**
+ * Returns an [Events] that emits values from the original [Events] up until [stop] emits a
+ * value.
+ *
+ * If the original [Events] emits at the same time as [stop], then the returned [Events] will
+ * emit that value.
+ */
+ fun <A> Events<A>.takeUntil(stop: Events<*>): Events<A> =
+ if (stop === emptyEvents) {
+ this
+ } else {
+ stop.mapCheap { emptyEvents }.nextOnly().holdState(this).switchEvents()
+ }
+
+ /**
+ * Invokes [stateful] in a new [StateScope] that is a child of this one.
+ *
+ * This new scope is stopped when [stop] first emits a value, or when the parent scope is
+ * stopped. Stopping will end all state accumulation; any [States][State] returned from this
+ * scope will no longer update.
+ */
+ fun <A> childStateScope(stop: Events<*>, stateful: Stateful<A>): DeferredValue<A> {
+ val (_, init: DeferredValue<Map<Unit, A>>) =
+ stop
+ .nextOnly()
+ .map { mapOf(Unit to none<Stateful<A>>()) }
+ .applyLatestStatefulForKey(init = mapOf(Unit to stateful), numKeys = 1)
+ return deferredStateScope { init.get().getValue(Unit) }
+ }
+
+ /**
+ * Returns an [Events] that emits values from the original [Events] up to and including a value
+ * is emitted that satisfies [predicate].
+ */
+ fun <A> Events<A>.takeUntil(predicate: TransactionScope.(A) -> Boolean): Events<A> =
+ takeUntil(filter(predicate))
+
+ /**
+ * Returns a [State] that is incrementally updated when this [Events] emits a value, by applying
+ * [transform] to both the emitted value and the currently tracked state.
+ *
+ * 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.
+ */
+ 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 }
+ }
+
+ /**
+ * Returns a [State] that is incrementally updated when this [Events] emits a value, by applying
+ * [transform] to both the emitted value and the currently tracked state.
+ *
+ * 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.
+ */
+ 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 }
+ }
+
+ /**
+ * Returns a [State] that holds onto the result of applying the most recently emitted [Stateful]
+ * this [Events], or [init] if nothing has been emitted since it was constructed.
+ *
+ * When each [Stateful] is applied, state accumulation from the previously-active [Stateful] is
+ * stopped.
+ *
+ * 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.
+ *
+ * Shorthand for:
+ * ```kotlin
+ * val (changes, initApplied) = applyLatestStateful(init)
+ * return changes.toStateDeferred(initApplied)
+ * ```
+ */
+ fun <A> Events<Stateful<A>>.holdLatestStateful(init: Stateful<A>): State<A> {
+ val (changes, initApplied) = applyLatestStateful(init)
+ return changes.holdStateDeferred(initApplied)
+ }
+
+ /**
+ * Returns an [Events] that emits the two most recent emissions from the original [Events].
+ * [initialValue] is used as the previous value for the first emission.
+ *
+ * Shorthand for `sample(hold(init)) { new, old -> Pair(old, new) }`
+ */
+ fun <S, T : S> Events<T>.pairwise(initialValue: S): Events<WithPrev<S, T>> {
+ val previous = holdState(initialValue)
+ return mapCheap { new -> WithPrev(previousValue = previous.sample(), newValue = new) }
+ }
+
+ /**
+ * Returns an [Events] that emits the two most recent emissions from the original [Events]. Note
+ * 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)
+ .mapMaybe { (prev, next) -> prev.zipWith(next, ::WithPrev) }
+
+ /**
+ * Returns a [State] that holds both the current and previous values of the original [State].
+ * [initialPreviousValue] is used as the first previous value.
+ *
+ * Shorthand for `sample(hold(init)) { new, old -> Pair(old, new) }`
+ */
+ fun <S, T : S> State<T>.pairwise(initialPreviousValue: S): State<WithPrev<S, T>> =
+ changes
+ .pairwise(initialPreviousValue)
+ .holdStateDeferred(
+ deferredTransactionScope { WithPrev(initialPreviousValue, sample()) }
+ )
+
+ /**
+ * 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].
+ */
+ fun <K, V> Events<Map<K, Maybe<V>>>.foldStateMapIncrementally(
+ initialValues: DeferredValue<Map<K, V>>
+ ): State<Map<K, V>> =
+ foldStateDeferred(initialValues) { patch, map ->
+ 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) }
+ .partitionEithers()
+ val removed: Map<K, V> = map - removes.toSet()
+ val updated: Map<K, V> = removed + adds
+ updated
+ }
+
+ /**
+ * 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].
+ */
+ fun <K, V> Events<Map<K, Maybe<V>>>.foldStateMapIncrementally(
+ initialValues: Map<K, V> = emptyMap()
+ ): State<Map<K, V>> = foldStateMapIncrementally(deferredOf(initialValues))
+
+ /**
+ * 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) }
+ * ```
+ */
+ fun <A> Events<A>.withIndex(): Events<IndexedValue<A>> {
+ val index = foldState(0) { _, old -> old + 1 }
+ return sample(index) { a, idx -> IndexedValue(idx, a) }
+ }
+
+ /**
+ * 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) }
+ * ```
+ */
+ fun <A, B> 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) }
+ }
+
+ /** Returns an [Events] where all subsequent repetitions of the same value are filtered out. */
+ fun <A> Events<A>.distinctUntilChanged(): Events<A> {
+ val state: State<Any?> = holdState(Any())
+ return filter { it != state.sample() }
+ }
+
+ /**
+ * Returns a new [Events] that emits at the same rate as the original [Events], but combines the
+ * emitted value with the most recent emission from [other] using [transform].
+ *
+ * Note that the returned [Events] will not emit anything until [other] has emitted at least one
+ * value.
+ */
+ 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()
+ }
+
+ /**
+ * Returns a [State] that samples the [Transactional] held by the given [State] within the same
+ * transaction that the state changes.
+ */
+ fun <A> State<Transactional<A>>.sampleTransactionals(): State<A> =
+ changes
+ .sampleTransactionals()
+ .holdStateDeferred(deferredTransactionScope { sample().sample() })
+
+ /**
+ * Returns a [State] that transforms the value held inside this [State] by applying it to the
+ * given function [transform].
+ */
+ fun <A, B> State<A>.mapTransactionally(transform: TransactionScope.(A) -> B): State<B> =
+ map { transactionally { transform(it) } }.sampleTransactionals()
+
+ /**
+ * Returns a [State] whose value is generated with [transform] by combining the current values
+ * of each given [State].
+ *
+ * @see State.combineWithTransactionally
+ */
+ 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()
+
+ /**
+ * Returns a [State] whose value is generated with [transform] by combining the current values
+ * of each given [State].
+ *
+ * @see State.combineWithTransactionally
+ */
+ fun <A, B, C, Z> combineTransactionally(
+ stateA: State<A>,
+ stateB: State<B>,
+ stateC: State<C>,
+ transform: TransactionScope.(A, B, C) -> Z,
+ ): State<Z> =
+ combine(stateA, stateB, stateC) { a, b, c -> transactionally { transform(a, b, c) } }
+ .sampleTransactionals()
+
+ /**
+ * Returns a [State] whose value is generated with [transform] by combining the current values
+ * of each given [State].
+ *
+ * @see State.combineWithTransactionally
+ */
+ fun <A, B, C, D, Z> combineTransactionally(
+ stateA: State<A>,
+ stateB: State<B>,
+ stateC: State<C>,
+ stateD: State<D>,
+ transform: TransactionScope.(A, B, C, D) -> Z,
+ ): State<Z> =
+ combine(stateA, stateB, stateC, stateD) { a, b, c, d ->
+ transactionally { transform(a, b, c, d) }
+ }
+ .sampleTransactionals()
+
+ /** Returns a [State] by applying [transform] to the value held by the original [State]. */
+ fun <A, B> State<A>.flatMapTransactionally(
+ transform: TransactionScope.(A) -> State<B>
+ ): State<B> = map { transactionally { transform(it) } }.sampleTransactionals().flatten()
+
+ /**
+ * Returns a [State] whose value is generated with [transform] by combining the current values
+ * of each given [State].
+ *
+ * @see State.combineWithTransactionally
+ */
+ fun <A, Z> combineTransactionally(
+ vararg states: State<A>,
+ transform: TransactionScope.(List<A>) -> Z,
+ ): State<Z> = combine(*states).mapTransactionally(transform)
+
+ /**
+ * Returns a [State] whose value is generated with [transform] by combining the current values
+ * of each given [State].
+ *
+ * @see State.combineWithTransactionally
+ */
+ fun <A, Z> Iterable<State<A>>.combineTransactionally(
+ transform: TransactionScope.(List<A>) -> Z
+ ): State<Z> = combine().mapTransactionally(transform)
+
+ /**
+ * Returns a [State] by combining the values held inside the given [State]s by applying them to
+ * the given function [transform].
+ */
+ fun <A, B, C> State<A>.combineWithTransactionally(
+ other: State<B>,
+ transform: TransactionScope.(A, B) -> C,
+ ): State<C> = combineTransactionally(this, other, transform)
+}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TFlow.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TFlow.kt
deleted file mode 100644
index 96edc1043325..000000000000
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TFlow.kt
+++ /dev/null
@@ -1,566 +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.internal.CompletableLazy
-import com.android.systemui.kairos.internal.DemuxImpl
-import com.android.systemui.kairos.internal.Init
-import com.android.systemui.kairos.internal.InitScope
-import com.android.systemui.kairos.internal.InputNode
-import com.android.systemui.kairos.internal.Network
-import com.android.systemui.kairos.internal.NoScope
-import com.android.systemui.kairos.internal.TFlowImpl
-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.map
-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.Left
-import com.android.systemui.kairos.util.Maybe
-import com.android.systemui.kairos.util.Right
-import com.android.systemui.kairos.util.just
-import com.android.systemui.kairos.util.map
-import com.android.systemui.kairos.util.toMaybe
-import java.util.concurrent.atomic.AtomicReference
-import kotlin.reflect.KProperty
-import kotlinx.coroutines.CoroutineStart
-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. */
-@ExperimentalFrpApi
-sealed class TFlow<out A> {
- companion object {
- /** A [TFlow] with no values. */
- val empty: TFlow<Nothing> = EmptyFlow
- }
-}
-
-/** A [TFlow] with no values. */
-@ExperimentalFrpApi val emptyTFlow: TFlow<Nothing> = TFlow.empty
-
-/**
- * A forward-reference to a [TFlow]. Useful for recursive definitions.
- *
- * This reference can be used like a standard [TFlow], but will hold up evaluation of the FRP
- * network until the [loopback] reference is set.
- */
-@ExperimentalFrpApi
-class TFlowLoop<A> : TFlow<A>() {
- private val deferred = CompletableLazy<TFlow<A>>()
-
- internal val init: Init<TFlowImpl<A>> =
- init(name = null) { deferred.value.init.connect(evalScope = this) }
-
- /** The [TFlow] this reference is referring to. */
- @ExperimentalFrpApi
- var loopback: TFlow<A>? = null
- set(value) {
- value?.let {
- check(!deferred.isInitialized()) { "TFlowLoop.loopback has already been set." }
- deferred.setValue(value)
- field = value
- }
- }
-
- operator fun getValue(thisRef: Any?, property: KProperty<*>): TFlow<A> = this
-
- operator fun setValue(thisRef: Any?, property: KProperty<*>, value: TFlow<A>) {
- loopback = value
- }
-
- override fun toString(): String = "${this::class.simpleName}@$hashString"
-}
-
-/** TODO */
-@ExperimentalFrpApi fun <A> Lazy<TFlow<A>>.defer(): TFlow<A> = deferInline { value }
-
-/** TODO */
-@ExperimentalFrpApi
-fun <A> FrpDeferredValue<TFlow<A>>.defer(): TFlow<A> = deferInline { unwrapped.value }
-
-/** TODO */
-@ExperimentalFrpApi
-fun <A> deferTFlow(block: FrpScope.() -> TFlow<A>): TFlow<A> = deferInline {
- NoScope.runInFrpScope(block)
-}
-
-/** Returns a [TFlow] that emits the new value of this [TState] when it changes. */
-@ExperimentalFrpApi
-val <A> TState<A>.stateChanges: TFlow<A>
- get() = TFlowInit(init(name = null) { init.connect(evalScope = this).changes })
-
-/**
- * Returns a [TFlow] that contains only the [just] results of applying [transform] to each value of
- * the original [TFlow].
- *
- * @see mapNotNull
- */
-@ExperimentalFrpApi
-fun <A, B> TFlow<A>.mapMaybe(transform: FrpTransactionScope.(A) -> Maybe<B>): TFlow<B> =
- map(transform).filterJust()
-
-/**
- * Returns a [TFlow] that contains only the non-null results of applying [transform] to each value
- * of the original [TFlow].
- *
- * @see mapMaybe
- */
-@ExperimentalFrpApi
-fun <A, B> TFlow<A>.mapNotNull(transform: FrpTransactionScope.(A) -> B?): TFlow<B> = mapMaybe {
- transform(it).toMaybe()
-}
-
-/** Returns a [TFlow] containing only values of the original [TFlow] that are not null. */
-@ExperimentalFrpApi
-fun <A> TFlow<A?>.filterNotNull(): TFlow<A> = mapCheap { it.toMaybe() }.filterJust()
-
-/** Shorthand for `mapNotNull { it as? A }`. */
-@ExperimentalFrpApi
-inline fun <reified A> TFlow<*>.filterIsInstance(): TFlow<A> = mapCheap { it as? A }.filterNotNull()
-
-/** Shorthand for `mapMaybe { it }`. */
-@ExperimentalFrpApi
-fun <A> TFlow<Maybe<A>>.filterJust(): TFlow<A> =
- TFlowInit(constInit(name = null, filterJustImpl { init.connect(evalScope = this) }))
-
-/**
- * Returns a [TFlow] containing the results of applying [transform] to each value of the original
- * [TFlow].
- */
-@ExperimentalFrpApi
-fun <A, B> TFlow<A>.map(transform: FrpTransactionScope.(A) -> B): TFlow<B> {
- val mapped: TFlowImpl<B> =
- mapImpl({ init.connect(evalScope = this) }) { a, _ ->
- runInTransactionScope { transform(a) }
- }
- return TFlowInit(constInit(name = null, mapped.cached()))
-}
-
-/**
- * Like [map], but the emission is not cached during the transaction. Use only if [transform] is
- * fast and pure.
- *
- * @see map
- */
-@ExperimentalFrpApi
-fun <A, B> TFlow<A>.mapCheap(transform: FrpTransactionScope.(A) -> B): TFlow<B> =
- TFlowInit(
- constInit(
- name = null,
- mapImpl({ init.connect(evalScope = this) }) { a, _ ->
- runInTransactionScope { transform(a) }
- },
- )
- )
-
-/**
- * Returns a [TFlow] that invokes [action] before each value of the original [TFlow] is emitted.
- * Useful for logging and debugging.
- *
- * ```
- * pulse.onEach { foo(it) } == pulse.map { foo(it); it }
- * ```
- *
- * Note that the side effects performed in [onEach] are only performed while the resulting [TFlow]
- * is connected to an output of the FRP network. If your goal is to reliably perform side effects in
- * response to a [TFlow], use the output combinators available in [FrpBuildScope], such as
- * [FrpBuildScope.toSharedFlow] or [FrpBuildScope.observe].
- */
-@ExperimentalFrpApi
-fun <A> TFlow<A>.onEach(action: FrpTransactionScope.(A) -> Unit): TFlow<A> = map {
- action(it)
- it
-}
-
-/**
- * Returns a [TFlow] containing only values of the original [TFlow] that satisfy the given
- * [predicate].
- */
-@ExperimentalFrpApi
-fun <A> TFlow<A>.filter(predicate: FrpTransactionScope.(A) -> Boolean): TFlow<A> {
- val pulse =
- filterImpl({ init.connect(evalScope = this) }) { runInTransactionScope { predicate(it) } }
- return TFlowInit(constInit(name = null, pulse))
-}
-
-/**
- * Splits a [TFlow] of pairs into a pair of [TFlows][TFlow], where each returned [TFlow] emits half
- * of the original.
- *
- * Shorthand for:
- * ```kotlin
- * val lefts = map { it.first }
- * val rights = map { it.second }
- * return Pair(lefts, rights)
- * ```
- */
-@ExperimentalFrpApi
-fun <A, B> TFlow<Pair<A, B>>.unzip(): Pair<TFlow<A>, TFlow<B>> {
- val lefts = map { it.first }
- val rights = map { it.second }
- return lefts to rights
-}
-
-/**
- * Merges the given [TFlows][TFlow] into a single [TFlow] that emits events from both.
- *
- * Because [TFlow]s 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 [TFlow].
- */
-@ExperimentalFrpApi
-fun <A> TFlow<A>.mergeWith(
- other: TFlow<A>,
- name: String? = null,
- transformCoincidence: FrpTransactionScope.(A, A) -> A = { a, _ -> a },
-): TFlow<A> {
- val node =
- mergeNodes(
- name = name,
- getPulse = { init.connect(evalScope = this) },
- getOther = { other.init.connect(evalScope = this) },
- ) { a, b ->
- runInTransactionScope { transformCoincidence(a, b) }
- }
- return TFlowInit(constInit(name = null, node))
-}
-
-/**
- * Merges the given [TFlows][TFlow] into a single [TFlow] that emits events from all. All coincident
- * emissions are collected into the emitted [List], preserving the input ordering.
- *
- * @see mergeWith
- * @see mergeLeft
- */
-@ExperimentalFrpApi
-fun <A> merge(vararg flows: TFlow<A>): TFlow<List<A>> = flows.asIterable().merge()
-
-/**
- * Merges the given [TFlows][TFlow] into a single [TFlow] that emits events from all. In the case of
- * coincident emissions, the emission from the left-most [TFlow] is emitted.
- *
- * @see merge
- */
-@ExperimentalFrpApi
-fun <A> mergeLeft(vararg flows: TFlow<A>): TFlow<A> = flows.asIterable().mergeLeft()
-
-/**
- * Merges the given [TFlows][TFlow] into a single [TFlow] that emits events from all.
- *
- * Because [TFlow]s 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 [TFlow].
- */
-// TODO: can be optimized to avoid creating the intermediate list
-fun <A> merge(vararg flows: TFlow<A>, transformCoincidence: (A, A) -> A): TFlow<A> =
- merge(*flows).map { l -> l.reduce(transformCoincidence) }
-
-/**
- * Merges the given [TFlows][TFlow] into a single [TFlow] that emits events from all. All coincident
- * emissions are collected into the emitted [List], preserving the input ordering.
- *
- * @see mergeWith
- * @see mergeLeft
- */
-@ExperimentalFrpApi
-fun <A> Iterable<TFlow<A>>.merge(): TFlow<List<A>> =
- TFlowInit(constInit(name = null, mergeNodes { map { it.init.connect(evalScope = this) } }))
-
-/**
- * Merges the given [TFlows][TFlow] into a single [TFlow] that emits events from all. In the case of
- * coincident emissions, the emission from the left-most [TFlow] is emitted.
- *
- * @see merge
- */
-@ExperimentalFrpApi
-fun <A> Iterable<TFlow<A>>.mergeLeft(): TFlow<A> =
- TFlowInit(constInit(name = null, mergeNodesLeft { map { it.init.connect(evalScope = this) } }))
-
-/**
- * Creates a new [TFlow] that emits events from all given [TFlow]s. All simultaneous emissions are
- * collected into the emitted [List], preserving the input ordering.
- *
- * @see mergeWith
- */
-@ExperimentalFrpApi fun <A> Sequence<TFlow<A>>.merge(): TFlow<List<A>> = asIterable().merge()
-
-/**
- * Creates a new [TFlow] that emits events from all given [TFlow]s. All simultaneous emissions are
- * collected into the emitted [Map], and are given the same key of the associated [TFlow] in the
- * input [Map].
- *
- * @see mergeWith
- */
-@ExperimentalFrpApi
-fun <K, A> Map<K, TFlow<A>>.merge(): TFlow<Map<K, A>> =
- asSequence().map { (k, flowA) -> flowA.map { a -> k to a } }.toList().merge().map { it.toMap() }
-
-/**
- * Returns a [GroupedTFlow] that can be used to efficiently split a single [TFlow] into multiple
- * downstream [TFlow]s.
- *
- * The input [TFlow] emits [Map] instances that specify which downstream [TFlow] the associated
- * value will be emitted from. These downstream [TFlow]s can be obtained via
- * [GroupedTFlow.eventsForKey].
- *
- * An example:
- * ```
- * val sFoo: TFlow<Map<String, Foo>> = ...
- * val fooById: GroupedTFlow<String, Foo> = sFoo.groupByKey()
- * val fooBar: TFlow<Foo> = fooById["bar"]
- * ```
- *
- * This is semantically equivalent to `val fooBar = sFoo.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 [TFlow], and so operates in `O(1)`.
- *
- * Note that the result [GroupedTFlow] should be cached and re-used to gain the performance benefit.
- *
- * @see selector
- */
-@ExperimentalFrpApi
-fun <K, A> TFlow<Map<K, A>>.groupByKey(numKeys: Int? = null): GroupedTFlow<K, A> =
- GroupedTFlow(demuxMap({ init.connect(this) }, numKeys))
-
-/**
- * Shorthand for `map { mapOf(extractKey(it) to it) }.groupByKey()`
- *
- * @see groupByKey
- */
-@ExperimentalFrpApi
-fun <K, A> TFlow<A>.groupBy(
- numKeys: Int? = null,
- extractKey: FrpTransactionScope.(A) -> K,
-): GroupedTFlow<K, A> = map { mapOf(extractKey(it) to it) }.groupByKey(numKeys)
-
-/**
- * Returns two new [TFlow]s that contain elements from this [TFlow] 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.
- */
-@ExperimentalFrpApi
-fun <A> TFlow<A>.partition(
- predicate: FrpTransactionScope.(A) -> Boolean
-): Pair<TFlow<A>, TFlow<A>> {
- val grouped: GroupedTFlow<Boolean, A> = groupBy(numKeys = 2, extractKey = predicate)
- return Pair(grouped.eventsForKey(true), grouped.eventsForKey(false))
-}
-
-/**
- * Returns two new [TFlow]s that contain elements from this [TFlow]; [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.
- */
-@ExperimentalFrpApi
-fun <A, B> TFlow<Either<A, B>>.partitionEither(): Pair<TFlow<A>, TFlow<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 [TFlow]s emitting values of type [A].
- *
- * @see groupByKey
- */
-@ExperimentalFrpApi
-class GroupedTFlow<in K, out A> internal constructor(internal val impl: DemuxImpl<K, A>) {
- /**
- * Returns a [TFlow] that emits values of type [A] that correspond to the given [key].
- *
- * @see groupByKey
- */
- @ExperimentalFrpApi
- fun eventsForKey(key: K): TFlow<A> = TFlowInit(constInit(name = null, impl.eventsForKey(key)))
-
- /**
- * Returns a [TFlow] that emits values of type [A] that correspond to the given [key].
- *
- * @see groupByKey
- */
- @ExperimentalFrpApi operator fun get(key: K): TFlow<A> = eventsForKey(key)
-}
-
-/**
- * Returns a [TFlow] that switches to the [TFlow] contained within this [TState] whenever it
- * changes.
- *
- * This switch does take effect until the *next* transaction after [TState] changes. For a switch
- * that takes effect immediately, see [switchPromptly].
- */
-@ExperimentalFrpApi
-fun <A> TState<TFlow<A>>.switch(name: String? = null): TFlow<A> {
- val patches =
- mapImpl({ init.connect(this).changes }) { newFlow, _ -> newFlow.init.connect(this) }
- return TFlowInit(
- constInit(
- name = null,
- switchDeferredImplSingle(
- name = name,
- getStorage = {
- init.connect(this).getCurrentWithEpoch(this).first.init.connect(this)
- },
- getPatches = { patches },
- ),
- )
- )
-}
-
-/**
- * Returns a [TFlow] that switches to the [TFlow] contained within this [TState] whenever it
- * changes.
- *
- * This switch takes effect immediately within the same transaction that [TState] changes. In
- * general, you should prefer [switch] over this method. It is both safer and more performant.
- */
-// TODO: parameter to handle coincidental emission from both old and new
-@ExperimentalFrpApi
-fun <A> TState<TFlow<A>>.switchPromptly(): TFlow<A> {
- val patches =
- mapImpl({ init.connect(this).changes }) { newFlow, _ -> newFlow.init.connect(this) }
- return TFlowInit(
- constInit(
- name = null,
- switchPromptImplSingle(
- getStorage = {
- init.connect(this).getCurrentWithEpoch(this).first.init.connect(this)
- },
- getPatches = { patches },
- ),
- )
- )
-}
-
-/**
- * A mutable [TFlow] that provides the ability to [emit] values to the flow, handling backpressure
- * by coalescing all emissions into batches.
- *
- * @see FrpNetwork.coalescingMutableTFlow
- */
-@ExperimentalFrpApi
-class CoalescingMutableTFlow<In, Out>
-internal constructor(
- internal val name: String?,
- internal val coalesce: (old: Out, new: In) -> Out,
- internal val network: Network,
- private val getInitialValue: () -> Out,
- internal val impl: InputNode<Out> = InputNode(),
-) : TFlow<Out>() {
- internal val storage = AtomicReference(false to getInitialValue())
-
- override fun toString(): String = "${this::class.simpleName}@$hashString"
-
- /**
- * Inserts [value] into the current batch, enqueueing it for emission from this [TFlow] if not
- * already pending.
- *
- * Backpressure occurs when [emit] is called while the FRP network is currently in a
- * transaction; if called multiple times, then emissions will be coalesced into a single batch
- * that is then processed when the network is ready.
- */
- @ExperimentalFrpApi
- fun emit(value: In) {
- val (scheduled, _) = storage.getAndUpdate { (_, old) -> true to coalesce(old, value) }
- if (!scheduled) {
- @Suppress("DeferredResultUnused")
- network.transaction("CoalescingMutableTFlow${name?.let { "($name)" }.orEmpty()}.emit") {
- impl.visit(this, storage.getAndSet(false to getInitialValue()).second)
- }
- }
- }
-}
-
-/**
- * A mutable [TFlow] that provides the ability to [emit] values to the flow, handling backpressure
- * by suspending the emitter.
- *
- * @see FrpNetwork.coalescingMutableTFlow
- */
-@ExperimentalFrpApi
-class MutableTFlow<T>
-internal constructor(internal val network: Network, internal val impl: InputNode<T> = InputNode()) :
- TFlow<T>() {
- internal val name: String? = null
-
- private val storage = AtomicReference<Job?>(null)
-
- override fun toString(): String = "${this::class.simpleName}@$hashString"
-
- /**
- * Emits a [value] to this [TFlow], suspending the caller until the FRP transaction containing
- * the emission has completed.
- */
- @ExperimentalFrpApi
- suspend fun emit(value: T) {
- coroutineScope {
- var jobOrNull: Job? = null
- val newEmit =
- async(start = CoroutineStart.LAZY) {
- jobOrNull?.join()
- network
- .transaction("MutableTFlow($name).emit") { impl.visit(this, value) }
- .await()
- }
- jobOrNull = storage.getAndSet(newEmit)
- newEmit.await()
- }
- }
-
- // internal suspend fun emitInCurrentTransaction(value: T, evalScope: EvalScope) {
- // if (storage.getAndSet(just(value)) is None) {
- // impl.visit(evalScope)
- // }
- // }
-}
-
-private data object EmptyFlow : TFlow<Nothing>()
-
-internal class TFlowInit<out A>(val init: Init<TFlowImpl<A>>) : TFlow<A>() {
- override fun toString(): String = "${this::class.simpleName}@$hashString"
-}
-
-internal val <A> TFlow<A>.init: Init<TFlowImpl<A>>
- get() =
- when (this) {
- is EmptyFlow -> constInit("EmptyFlow", neverImpl)
- is TFlowInit -> init
- is TFlowLoop -> init
- is CoalescingMutableTFlow<*, A> -> constInit(name, impl.activated())
- is MutableTFlow -> constInit(name, impl.activated())
- }
-
-private inline fun <A> deferInline(crossinline block: InitScope.() -> TFlow<A>): TFlow<A> =
- TFlowInit(init(name = null) { block().init.connect(evalScope = this) })
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TState.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TState.kt
deleted file mode 100644
index d84a6f2ddb34..000000000000
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TState.kt
+++ /dev/null
@@ -1,491 +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.internal.CompletableLazy
-import com.android.systemui.kairos.internal.DerivedMapCheap
-import com.android.systemui.kairos.internal.Init
-import com.android.systemui.kairos.internal.InitScope
-import com.android.systemui.kairos.internal.Network
-import com.android.systemui.kairos.internal.NoScope
-import com.android.systemui.kairos.internal.Schedulable
-import com.android.systemui.kairos.internal.TFlowImpl
-import com.android.systemui.kairos.internal.TStateImpl
-import com.android.systemui.kairos.internal.TStateSource
-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.constS
-import com.android.systemui.kairos.internal.filterImpl
-import com.android.systemui.kairos.internal.flatMap
-import com.android.systemui.kairos.internal.init
-import com.android.systemui.kairos.internal.map
-import com.android.systemui.kairos.internal.mapCheap
-import com.android.systemui.kairos.internal.mapImpl
-import com.android.systemui.kairos.internal.util.hashString
-import com.android.systemui.kairos.internal.zipStateMap
-import com.android.systemui.kairos.internal.zipStates
-import kotlin.reflect.KProperty
-
-/**
- * A time-varying value with discrete changes. Essentially, a combination of a [Transactional] that
- * holds a value, and a [TFlow] that emits when the value changes.
- */
-@ExperimentalFrpApi sealed class TState<out A>
-
-/** A [TState] that never changes. */
-@ExperimentalFrpApi
-fun <A> tStateOf(value: A): TState<A> {
- val operatorName = "tStateOf"
- val name = "$operatorName($value)"
- return TStateInit(constInit(name, constS(name, operatorName, value)))
-}
-
-/** TODO */
-@ExperimentalFrpApi fun <A> Lazy<TState<A>>.defer(): TState<A> = deferInline { value }
-
-/** TODO */
-@ExperimentalFrpApi
-fun <A> FrpDeferredValue<TState<A>>.defer(): TState<A> = deferInline { unwrapped.value }
-
-/** TODO */
-@ExperimentalFrpApi
-fun <A> deferTState(block: FrpScope.() -> TState<A>): TState<A> = deferInline {
- NoScope.runInFrpScope(block)
-}
-
-/**
- * Returns a [TState] containing the results of applying [transform] to the value held by the
- * original [TState].
- */
-@ExperimentalFrpApi
-fun <A, B> TState<A>.map(transform: FrpScope.(A) -> B): TState<B> {
- val operatorName = "map"
- val name = operatorName
- return TStateInit(
- init(name) {
- init.connect(evalScope = this).map(name, operatorName) {
- NoScope.runInFrpScope { transform(it) }
- }
- }
- )
-}
-
-/**
- * Returns a [TState] that transforms the value held inside this [TState] 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 [stateChanges]
- * for the returned [TState] will operate unexpectedly, emitting at rates that do not reflect an
- * observable change to the returned [TState].
- */
-@ExperimentalFrpApi
-fun <A, B> TState<A>.mapCheapUnsafe(transform: FrpScope.(A) -> B): TState<B> {
- val operatorName = "map"
- val name = operatorName
- return TStateInit(
- init(name) {
- init.connect(evalScope = this).mapCheap(name, operatorName) {
- NoScope.runInFrpScope { transform(it) }
- }
- }
- )
-}
-
-/**
- * Returns a [TState] by combining the values held inside the given [TState]s by applying them to
- * the given function [transform].
- */
-@ExperimentalFrpApi
-fun <A, B, C> TState<A>.combineWith(other: TState<B>, transform: FrpScope.(A, B) -> C): TState<C> =
- combine(this, other, transform)
-
-/**
- * Splits a [TState] of pairs into a pair of [TFlows][TState], where each returned [TState] holds
- * half of the original.
- *
- * Shorthand for:
- * ```kotlin
- * val lefts = map { it.first }
- * val rights = map { it.second }
- * return Pair(lefts, rights)
- * ```
- */
-@ExperimentalFrpApi
-fun <A, B> TState<Pair<A, B>>.unzip(): Pair<TState<A>, TState<B>> {
- val left = map { it.first }
- val right = map { it.second }
- return left to right
-}
-
-/**
- * Returns a [TState] by combining the values held inside the given [TStates][TState] into a [List].
- *
- * @see TState.combineWith
- */
-@ExperimentalFrpApi
-fun <A> Iterable<TState<A>>.combine(): TState<List<A>> {
- val operatorName = "combine"
- val name = operatorName
- return TStateInit(
- init(name) {
- zipStates(name, operatorName, states = map { it.init.connect(evalScope = this) })
- }
- )
-}
-
-/**
- * Returns a [TState] by combining the values held inside the given [TStates][TState] into a [Map].
- *
- * @see TState.combineWith
- */
-@ExperimentalFrpApi
-fun <K, A> Map<K, TState<A>>.combine(): TState<Map<K, A>> {
- val operatorName = "combine"
- val name = operatorName
- return TStateInit(
- init(name) {
- zipStateMap(
- name,
- operatorName,
- states = mapValues { it.value.init.connect(evalScope = this) },
- )
- }
- )
-}
-
-/**
- * Returns a [TState] whose value is generated with [transform] by combining the current values of
- * each given [TState].
- *
- * @see TState.combineWith
- */
-@ExperimentalFrpApi
-fun <A, B> Iterable<TState<A>>.combine(transform: FrpScope.(List<A>) -> B): TState<B> =
- combine().map(transform)
-
-/**
- * Returns a [TState] by combining the values held inside the given [TState]s into a [List].
- *
- * @see TState.combineWith
- */
-@ExperimentalFrpApi
-fun <A> combine(vararg states: TState<A>): TState<List<A>> = states.asIterable().combine()
-
-/**
- * Returns a [TState] whose value is generated with [transform] by combining the current values of
- * each given [TState].
- *
- * @see TState.combineWith
- */
-@ExperimentalFrpApi
-fun <A, B> combine(vararg states: TState<A>, transform: FrpScope.(List<A>) -> B): TState<B> =
- states.asIterable().combine(transform)
-
-/**
- * Returns a [TState] whose value is generated with [transform] by combining the current values of
- * each given [TState].
- *
- * @see TState.combineWith
- */
-@ExperimentalFrpApi
-fun <A, B, Z> combine(
- stateA: TState<A>,
- stateB: TState<B>,
- transform: FrpScope.(A, B) -> Z,
-): TState<Z> {
- val operatorName = "combine"
- val name = operatorName
- return TStateInit(
- init(name) {
- val dl1 = stateA.init.connect(evalScope = this@init)
- val dl2 = stateB.init.connect(evalScope = this@init)
- zipStates(name, operatorName, dl1, dl2) { a, b ->
- NoScope.runInFrpScope { transform(a, b) }
- }
- }
- )
-}
-
-/**
- * Returns a [TState] whose value is generated with [transform] by combining the current values of
- * each given [TState].
- *
- * @see TState.combineWith
- */
-@ExperimentalFrpApi
-fun <A, B, C, Z> combine(
- stateA: TState<A>,
- stateB: TState<B>,
- stateC: TState<C>,
- transform: FrpScope.(A, B, C) -> Z,
-): TState<Z> {
- val operatorName = "combine"
- val name = operatorName
- return TStateInit(
- init(name) {
- val dl1 = stateA.init.connect(evalScope = this@init)
- val dl2 = stateB.init.connect(evalScope = this@init)
- val dl3 = stateC.init.connect(evalScope = this@init)
- zipStates(name, operatorName, dl1, dl2, dl3) { a, b, c ->
- NoScope.runInFrpScope { transform(a, b, c) }
- }
- }
- )
-}
-
-/**
- * Returns a [TState] whose value is generated with [transform] by combining the current values of
- * each given [TState].
- *
- * @see TState.combineWith
- */
-@ExperimentalFrpApi
-fun <A, B, C, D, Z> combine(
- stateA: TState<A>,
- stateB: TState<B>,
- stateC: TState<C>,
- stateD: TState<D>,
- transform: FrpScope.(A, B, C, D) -> Z,
-): TState<Z> {
- val operatorName = "combine"
- val name = operatorName
- return TStateInit(
- init(name) {
- val dl1 = stateA.init.connect(evalScope = this@init)
- val dl2 = stateB.init.connect(evalScope = this@init)
- val dl3 = stateC.init.connect(evalScope = this@init)
- val dl4 = stateD.init.connect(evalScope = this@init)
- zipStates(name, operatorName, dl1, dl2, dl3, dl4) { a, b, c, d ->
- NoScope.runInFrpScope { transform(a, b, c, d) }
- }
- }
- )
-}
-
-/**
- * Returns a [TState] whose value is generated with [transform] by combining the current values of
- * each given [TState].
- *
- * @see TState.combineWith
- */
-@ExperimentalFrpApi
-fun <A, B, C, D, E, Z> combine(
- stateA: TState<A>,
- stateB: TState<B>,
- stateC: TState<C>,
- stateD: TState<D>,
- stateE: TState<E>,
- transform: FrpScope.(A, B, C, D, E) -> Z,
-): TState<Z> {
- val operatorName = "combine"
- val name = operatorName
- return TStateInit(
- init(name) {
- val dl1 = stateA.init.connect(evalScope = this@init)
- val dl2 = stateB.init.connect(evalScope = this@init)
- val dl3 = stateC.init.connect(evalScope = this@init)
- val dl4 = stateD.init.connect(evalScope = this@init)
- val dl5 = stateE.init.connect(evalScope = this@init)
- zipStates(name, operatorName, dl1, dl2, dl3, dl4, dl5) { a, b, c, d, e ->
- NoScope.runInFrpScope { transform(a, b, c, d, e) }
- }
- }
- )
-}
-
-/** Returns a [TState] by applying [transform] to the value held by the original [TState]. */
-@ExperimentalFrpApi
-fun <A, B> TState<A>.flatMap(transform: FrpScope.(A) -> TState<B>): TState<B> {
- val operatorName = "flatMap"
- val name = operatorName
- return TStateInit(
- init(name) {
- init.connect(this).flatMap(name, operatorName) { a ->
- NoScope.runInFrpScope { transform(a) }.init.connect(this)
- }
- }
- )
-}
-
-/** Shorthand for `flatMap { it }` */
-@ExperimentalFrpApi fun <A> TState<TState<A>>.flatten() = flatMap { it }
-
-/**
- * Returns a [TStateSelector] that can be used to efficiently check if the input [TState] is
- * currently holding a specific value.
- *
- * An example:
- * ```
- * val lInt: TState<Int> = ...
- * val intSelector: TStateSelector<Int> = lInt.selector()
- * // Tracks if lInt is holding 1
- * val isOne: TState<Boolean> = intSelector.whenSelected(1)
- * ```
- *
- * This is semantically equivalent to `val isOne = lInt.map { i -> i == 1 }`, but is significantly
- * more efficient; specifically, using [TState.map] in this way incurs a `O(n)` performance hit,
- * where `n` is the number of different [TState.map] operations used to track a specific value.
- * [selector] internally uses a [HashMap] to lookup the appropriate downstream [TState] to update,
- * and so operates in `O(1)`.
- *
- * Note that the result [TStateSelector] should be cached and re-used to gain the performance
- * benefit.
- *
- * @see groupByKey
- */
-@ExperimentalFrpApi
-fun <A> TState<A>.selector(numDistinctValues: Int? = null): TStateSelector<A> =
- TStateSelector(
- this,
- stateChanges
- .map { new -> mapOf(new to true, sampleDeferred().get() to false) }
- .groupByKey(numDistinctValues),
- )
-
-/**
- * Tracks the currently selected value of type [A] from an upstream [TState].
- *
- * @see selector
- */
-@ExperimentalFrpApi
-class TStateSelector<in A>
-internal constructor(
- private val upstream: TState<A>,
- private val groupedChanges: GroupedTFlow<A, Boolean>,
-) {
- /**
- * Returns a [TState] that tracks whether the upstream [TState] is currently holding the given
- * [value].
- *
- * @see selector
- */
- @ExperimentalFrpApi
- fun whenSelected(value: A): TState<Boolean> {
- val operatorName = "TStateSelector#whenSelected"
- val name = "$operatorName[$value]"
- return TStateInit(
- init(name) {
- DerivedMapCheap(
- name,
- operatorName,
- upstream = upstream.init.connect(evalScope = this),
- changes = groupedChanges.impl.eventsForKey(value),
- ) {
- it == value
- }
- }
- )
- }
-
- @ExperimentalFrpApi operator fun get(value: A): TState<Boolean> = whenSelected(value)
-}
-
-/** TODO */
-@ExperimentalFrpApi
-class MutableTState<T> internal constructor(internal val network: Network, initialValue: Lazy<T>) :
- TState<T>() {
-
- private val input: CoalescingMutableTFlow<Lazy<T>, Lazy<T>?> =
- CoalescingMutableTFlow(
- name = null,
- coalesce = { _, new -> new },
- network = network,
- getInitialValue = { null },
- )
-
- internal val tState = run {
- val changes = input.impl
- val name = null
- val operatorName = "MutableTState"
- lateinit var state: TStateSource<T>
- val mapImpl = mapImpl(upstream = { changes.activated() }) { it, _ -> it!!.value }
- val calm: TFlowImpl<T> =
- filterImpl({ mapImpl }) { new ->
- new != state.getCurrentWithEpoch(evalScope = this).first
- }
- .cached()
- state = TStateSource(name, operatorName, initialValue, calm)
- @Suppress("DeferredResultUnused")
- network.transaction("MutableTState.init") {
- calm.activate(evalScope = this, downstream = Schedulable.S(state))?.let {
- (connection, needsEval) ->
- state.upstreamConnection = connection
- if (needsEval) {
- schedule(state)
- }
- }
- }
- TStateInit(constInit(name, state))
- }
-
- /** TODO */
- @ExperimentalFrpApi fun setValue(value: T) = input.emit(CompletableLazy(value))
-
- @ExperimentalFrpApi
- fun setValueDeferred(value: FrpDeferredValue<T>) = input.emit(value.unwrapped)
-}
-
-/** A forward-reference to a [TState], allowing for recursive definitions. */
-@ExperimentalFrpApi
-class TStateLoop<A> : TState<A>() {
-
- private val name: String? = null
-
- private val deferred = CompletableLazy<TState<A>>()
-
- internal val init: Init<TStateImpl<A>> =
- init(name) { deferred.value.init.connect(evalScope = this) }
-
- /** The [TState] this [TStateLoop] will forward to. */
- @ExperimentalFrpApi
- var loopback: TState<A>? = null
- set(value) {
- value?.let {
- check(!deferred.isInitialized()) { "TStateLoop.loopback has already been set." }
- deferred.setValue(value)
- field = value
- }
- }
-
- @ExperimentalFrpApi
- operator fun getValue(thisRef: Any?, property: KProperty<*>): TState<A> = this
-
- @ExperimentalFrpApi
- operator fun setValue(thisRef: Any?, property: KProperty<*>, value: TState<A>) {
- loopback = value
- }
-
- override fun toString(): String = "${this::class.simpleName}@$hashString"
-}
-
-internal class TStateInit<A> internal constructor(internal val init: Init<TStateImpl<A>>) :
- TState<A>() {
- override fun toString(): String = "${this::class.simpleName}@$hashString"
-}
-
-internal val <A> TState<A>.init: Init<TStateImpl<A>>
- get() =
- when (this) {
- is TStateInit -> init
- is TStateLoop -> init
- is MutableTState -> tState.init
- }
-
-private inline fun <A> deferInline(crossinline block: InitScope.() -> TState<A>): TState<A> =
- TStateInit(init(name = null) { block().init.connect(evalScope = this) })
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
new file mode 100644
index 000000000000..225416992d52
--- /dev/null
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TransactionScope.kt
@@ -0,0 +1,78 @@
+/*
+ * 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
+
+/**
+ * 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.
+ */
+@ExperimentalKairosApi
+interface TransactionScope : KairosScope {
+
+ /**
+ * Returns the current value of this [Transactional] as a [DeferredValue].
+ *
+ * Compared to [sample], you may want to use this instead if you do not need to inspect the
+ * sampled value, but instead want to pass it to another Kairos API that accepts a
+ * [DeferredValue]. In this case, [sampleDeferred] is both safer and more performant.
+ *
+ * @see sample
+ */
+ fun <A> Transactional<A>.sampleDeferred(): DeferredValue<A>
+
+ /**
+ * Returns the current value of this [State] as a [DeferredValue].
+ *
+ * Compared to [sample], you may want to use this instead if you do not need to inspect the
+ * sampled value, but instead want to pass it to another Kairos API that accepts a
+ * [DeferredValue]. In this case, [sampleDeferred] is both safer and more performant.
+ *
+ * @see sample
+ */
+ fun <A> State<A>.sampleDeferred(): DeferredValue<A>
+
+ /**
+ * Defers invoking [block] until after the current [TransactionScope] code-path completes,
+ * returning a [DeferredValue] that can be used to reference the result.
+ *
+ * Useful for recursive definitions.
+ *
+ * @see DeferredValue
+ */
+ fun <A> deferredTransactionScope(block: TransactionScope.() -> A): DeferredValue<A>
+
+ /** An [Events] that emits once, within this transaction, and then never again. */
+ val now: Events<Unit>
+
+ /**
+ * Returns the current value held by this [State]. Guaranteed to be consistent within the same
+ * transaction.
+ *
+ * @see sampleDeferred
+ */
+ fun <A> State<A>.sample(): A = sampleDeferred().get()
+
+ /**
+ * Returns the current value held by this [Transactional]. Guaranteed to be consistent within
+ * the same transaction.
+ *
+ * @see sampleDeferred
+ */
+ fun <A> Transactional<A>.sample(): A = sampleDeferred().get()
+}
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 e7a5b1bbd105..9485cd212603 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
@@ -28,44 +28,68 @@ import com.android.systemui.kairos.internal.util.hashString
* A time-varying value. A [Transactional] encapsulates the idea of some continuous state; each time
* it is "sampled", a new result may be produced.
*
- * Because FRP 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_.
+ * 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_.
*/
-@ExperimentalFrpApi
-class Transactional<out A> internal constructor(internal val impl: TState<TransactionalImpl<A>>) {
+@ExperimentalKairosApi
+class Transactional<out A> internal constructor(internal val impl: State<TransactionalImpl<A>>) {
override fun toString(): String = "${this::class.simpleName}@$hashString"
}
/** A constant [Transactional] that produces [value] whenever it is sampled. */
-@ExperimentalFrpApi
+@ExperimentalKairosApi
fun <A> transactionalOf(value: A): Transactional<A> =
- Transactional(tStateOf(TransactionalImpl.Const(CompletableLazy(value))))
+ Transactional(stateOf(TransactionalImpl.Const(CompletableLazy(value))))
-/** TODO */
-@ExperimentalFrpApi
-fun <A> FrpDeferredValue<Transactional<A>>.defer(): Transactional<A> = deferInline {
- unwrapped.value
-}
+/**
+ * Returns a [Transactional] that acts as a deferred-reference to the [Transactional] produced by
+ * this [DeferredValue].
+ *
+ * When the returned [Transactional] is accessed by the Kairos network, the [DeferredValue] will be
+ * queried and used.
+ *
+ * Useful for recursive definitions.
+ */
+@ExperimentalKairosApi
+fun <A> DeferredValue<Transactional<A>>.defer(): Transactional<A> = deferInline { unwrapped.value }
-/** TODO */
-@ExperimentalFrpApi fun <A> Lazy<Transactional<A>>.defer(): Transactional<A> = deferInline { value }
+/**
+ * Returns a [Transactional] that acts as a deferred-reference to the [Transactional] produced by
+ * this [Lazy].
+ *
+ * When the returned [Transactional] is accessed by the Kairos network, the [Lazy]'s
+ * [value][Lazy.value] will be queried and used.
+ *
+ * Useful for recursive definitions.
+ */
+@ExperimentalKairosApi
+fun <A> Lazy<Transactional<A>>.defer(): Transactional<A> = deferInline { value }
-/** TODO */
-@ExperimentalFrpApi
-fun <A> deferTransactional(block: FrpScope.() -> Transactional<A>): Transactional<A> = deferInline {
- NoScope.runInFrpScope(block)
-}
+/**
+ * Returns a [Transactional] that acts as a deferred-reference to the [Transactional] produced by
+ * [block].
+ *
+ * When the returned [Transactional] is accessed by the Kairos network, [block] will be invoked and
+ * the returned [Transactional] will be used.
+ *
+ * Useful for recursive definitions.
+ */
+@ExperimentalKairosApi
+fun <A> deferredTransactional(block: KairosScope.() -> Transactional<A>): Transactional<A> =
+ deferInline {
+ NoScope.block()
+ }
private inline fun <A> deferInline(
crossinline block: InitScope.() -> Transactional<A>
): Transactional<A> =
- Transactional(TStateInit(init(name = null) { block().impl.init.connect(evalScope = this) }))
+ Transactional(StateInit(init(name = null) { block().impl.init.connect(evalScope = this) }))
/**
* 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.
*/
-@ExperimentalFrpApi
-fun <A> transactionally(block: FrpTransactionScope.() -> A): Transactional<A> =
- Transactional(tStateOf(transactionalImpl { runInTransactionScope(block) }))
+@ExperimentalKairosApi
+fun <A> transactionally(block: TransactionScope.() -> A): Transactional<A> =
+ Transactional(stateOf(transactionalImpl { block() }))
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/debug/Debug.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/debug/Debug.kt
index 6f9612fab70a..d43a0bbf433e 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/debug/Debug.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/debug/Debug.kt
@@ -16,18 +16,18 @@
package com.android.systemui.kairos.debug
-import com.android.systemui.kairos.MutableTState
-import com.android.systemui.kairos.TState
-import com.android.systemui.kairos.TStateInit
-import com.android.systemui.kairos.TStateLoop
+import com.android.systemui.kairos.MutableState
+import com.android.systemui.kairos.State
+import com.android.systemui.kairos.StateInit
+import com.android.systemui.kairos.StateLoop
import com.android.systemui.kairos.internal.DerivedFlatten
import com.android.systemui.kairos.internal.DerivedMap
import com.android.systemui.kairos.internal.DerivedMapCheap
import com.android.systemui.kairos.internal.DerivedZipped
import com.android.systemui.kairos.internal.Init
-import com.android.systemui.kairos.internal.TStateDerived
-import com.android.systemui.kairos.internal.TStateImpl
-import com.android.systemui.kairos.internal.TStateSource
+import com.android.systemui.kairos.internal.StateDerived
+import com.android.systemui.kairos.internal.StateImpl
+import com.android.systemui.kairos.internal.StateSource
import com.android.systemui.kairos.util.Just
import com.android.systemui.kairos.util.Maybe
import com.android.systemui.kairos.util.None
@@ -87,12 +87,12 @@ data class Edge(val upstream: Any, val downstream: Any, val tag: Any? = null)
data class Graph<T>(val nodes: Map<Any, T>, val edges: List<Edge>)
-internal fun TState<*>.dump(infoMap: MutableMap<Any, InitInfo>, edges: MutableList<Edge>) {
- val init: Init<TStateImpl<Any?>> =
+internal fun State<*>.dump(infoMap: MutableMap<Any, InitInfo>, edges: MutableList<Edge>) {
+ val init: Init<StateImpl<Any?>> =
when (this) {
- is TStateInit -> init
- is TStateLoop -> init
- is MutableTState -> tState.init
+ is StateInit -> init
+ is StateLoop -> init
+ is MutableState -> state.init
}
when (val stateMaybe = init.getUnsafe()) {
None -> {
@@ -104,12 +104,12 @@ internal fun TState<*>.dump(infoMap: MutableMap<Any, InitInfo>, edges: MutableLi
}
}
-internal fun TStateImpl<*>.dump(infoById: MutableMap<Any, InitInfo>, edges: MutableList<Edge>) {
+internal fun StateImpl<*>.dump(infoById: MutableMap<Any, InitInfo>, edges: MutableList<Edge>) {
val state = this
if (state in infoById) return
val stateInfo =
when (state) {
- is TStateDerived -> {
+ is StateDerived -> {
val type =
when (state) {
is DerivedFlatten -> {
@@ -151,7 +151,7 @@ internal fun TStateImpl<*>.dump(infoById: MutableMap<Any, InitInfo>, edges: Muta
state.invalidatedEpoch,
)
}
- is TStateSource ->
+ is StateSource ->
Source(
state.name ?: state.operatorName,
state.getStorageUnsafe(),
@@ -174,30 +174,30 @@ internal fun TStateImpl<*>.dump(infoById: MutableMap<Any, InitInfo>, edges: Muta
infoById[state] = Initialized(stateInfo)
}
-private fun <A> TStateImpl<A>.getUnsafe(): Maybe<A> =
+private fun <A> StateImpl<A>.getUnsafe(): Maybe<A> =
when (this) {
- is TStateDerived -> getCachedUnsafe()
- is TStateSource -> getStorageUnsafe()
+ is StateDerived -> getCachedUnsafe()
+ is StateSource -> getStorageUnsafe()
is DerivedMapCheap<*, *> -> none
}
-private fun <A> TStateImpl<A>.getUnsafeWithEpoch(): Maybe<Pair<A, Long>> =
+private fun <A> StateImpl<A>.getUnsafeWithEpoch(): Maybe<Pair<A, Long>> =
when (this) {
- is TStateDerived -> getCachedUnsafe().map { it to invalidatedEpoch }
- is TStateSource -> getStorageUnsafe().map { it to writeEpoch }
+ is StateDerived -> getCachedUnsafe().map { it to invalidatedEpoch }
+ is StateSource -> getStorageUnsafe().map { it to writeEpoch }
is DerivedMapCheap<*, *> -> none
}
/**
- * Returns the current value held in this [TState], or [none] if the [TState] has not been
+ * Returns the current value held in this [State], or [none] if the [State] has not been
* initialized.
*
* The returned [Long] is the *epoch* at which the internal cache was last updated. This can be used
* to identify values which are out-of-date.
*/
-fun <A> TState<A>.sampleUnsafe(): Maybe<Pair<A, Long>> =
+fun <A> State<A>.sampleUnsafe(): Maybe<Pair<A, Long>> =
when (this) {
- is MutableTState -> tState.init.getUnsafe().flatMap { it.getUnsafeWithEpoch() }
- is TStateInit -> init.getUnsafe().flatMap { it.getUnsafeWithEpoch() }
- is TStateLoop -> this.init.getUnsafe().flatMap { it.getUnsafeWithEpoch() }
+ is MutableState -> state.init.getUnsafe().flatMap { it.getUnsafeWithEpoch() }
+ is StateInit -> init.getUnsafe().flatMap { it.getUnsafeWithEpoch() }
+ is StateLoop -> this.init.getUnsafe().flatMap { it.getUnsafeWithEpoch() }
}
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 14488a3131c7..07dc1dd2c79c 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
@@ -16,21 +16,20 @@
package com.android.systemui.kairos.internal
-import com.android.systemui.kairos.CoalescingMutableTFlow
-import com.android.systemui.kairos.FrpBuildScope
-import com.android.systemui.kairos.FrpCoalescingProducerScope
-import com.android.systemui.kairos.FrpDeferredValue
-import com.android.systemui.kairos.FrpEffectScope
-import com.android.systemui.kairos.FrpNetwork
-import com.android.systemui.kairos.FrpProducerScope
-import com.android.systemui.kairos.FrpSpec
-import com.android.systemui.kairos.FrpStateScope
-import com.android.systemui.kairos.FrpTransactionScope
-import com.android.systemui.kairos.GroupedTFlow
-import com.android.systemui.kairos.LocalFrpNetwork
-import com.android.systemui.kairos.MutableTFlow
-import com.android.systemui.kairos.TFlow
-import com.android.systemui.kairos.TFlowInit
+import com.android.systemui.kairos.BuildScope
+import com.android.systemui.kairos.BuildSpec
+import com.android.systemui.kairos.CoalescingEventProducerScope
+import com.android.systemui.kairos.CoalescingMutableEvents
+import com.android.systemui.kairos.DeferredValue
+import com.android.systemui.kairos.EffectScope
+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.KairosNetwork
+import com.android.systemui.kairos.LocalNetwork
+import com.android.systemui.kairos.MutableEvents
+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
@@ -44,7 +43,6 @@ import com.android.systemui.kairos.util.just
import com.android.systemui.kairos.util.map
import java.util.concurrent.atomic.AtomicReference
import kotlin.coroutines.CoroutineContext
-import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.CompletableJob
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
@@ -52,109 +50,73 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.job
internal class BuildScopeImpl(val stateScope: StateScopeImpl, val coroutineScope: CoroutineScope) :
- BuildScope, StateScope by stateScope {
+ InternalBuildScope, InternalStateScope by stateScope {
private val job: Job
get() = coroutineScope.coroutineContext.job
- override val frpScope: FrpBuildScope = FrpBuildScopeImpl()
-
- override fun <R> runInBuildScope(block: FrpBuildScope.() -> R): R = frpScope.block()
-
- private fun <A, T : TFlow<A>, S> buildTFlow(
- name: String? = null,
- constructFlow: (InputNode<A>) -> Pair<T, S>,
- builder: suspend S.() -> Unit,
- ): TFlow<A> {
- var job: Job? = null
- val stopEmitter = newStopEmitter("buildTFlow[$name]")
- // Create a child scope that will be kept alive beyond the end of this transaction.
- val childScope = coroutineScope.childScope()
- lateinit var emitter: Pair<T, S>
- val inputNode =
- InputNode<A>(
- activate = {
- // It's possible that activation occurs after all effects have been run, due
- // to a MuxDeferred switch-in. For this reason, we need to activate in a new
- // transaction.
- check(job == null) { "[$name] already activated" }
- job =
- childScope.launchImmediate {
- network
- .transaction("buildTFlow") {
- reenterBuildScope(this@BuildScopeImpl, childScope)
- .runInBuildScope {
- launchEffect {
- builder(emitter.second)
- stopEmitter.emit(Unit)
- }
- }
- }
- .await()
- .join()
- }
- },
- deactivate = {
- checkNotNull(job) { "[$name] already deactivated" }.cancel()
- job = null
- },
- )
- emitter = constructFlow(inputNode)
- return with(frpScope) { emitter.first.takeUntil(mergeLeft(stopEmitter, endSignal)) }
+ override val kairosNetwork: KairosNetwork by lazy {
+ LocalNetwork(network, coroutineScope, endSignal)
}
- private fun <T> tFlowInternal(
+ override fun <T> events(
name: String?,
- builder: suspend FrpProducerScope<T>.() -> Unit,
- ): TFlow<T> =
- buildTFlow(
+ builder: suspend EventProducerScope<T>.() -> Unit,
+ ): Events<T> =
+ buildEvents(
name,
- constructFlow = { inputNode ->
- val flow = MutableTFlow(network, inputNode)
- flow to
- object : FrpProducerScope<T> {
+ constructEvents = { inputNode ->
+ val events = MutableEvents(network, inputNode)
+ events to
+ object : EventProducerScope<T> {
override suspend fun emit(value: T) {
- flow.emit(value)
+ events.emit(value)
}
}
},
builder = builder,
)
- private fun <In, Out> coalescingTFlowInternal(
+ override fun <In, Out> coalescingEvents(
getInitialValue: () -> Out,
coalesce: (old: Out, new: In) -> Out,
- builder: suspend FrpCoalescingProducerScope<In>.() -> Unit,
- ): TFlow<Out> =
- buildTFlow(
- constructFlow = { inputNode ->
- val flow =
- CoalescingMutableTFlow(null, coalesce, network, getInitialValue, inputNode)
- flow to
- object : FrpCoalescingProducerScope<In> {
+ builder: suspend CoalescingEventProducerScope<In>.() -> Unit,
+ ): Events<Out> =
+ buildEvents(
+ constructEvents = { inputNode ->
+ val events =
+ CoalescingMutableEvents(
+ null,
+ coalesce = { old, new: In -> coalesce(old.value, new) },
+ network,
+ getInitialValue,
+ inputNode,
+ )
+ events to
+ object : CoalescingEventProducerScope<In> {
override fun emit(value: In) {
- flow.emit(value)
+ events.emit(value)
}
}
},
builder = builder,
)
- private fun <A> asyncScopeInternal(block: FrpSpec<A>): Pair<FrpDeferredValue<A>, Job> {
+ override fun <A> asyncScope(block: BuildSpec<A>): Pair<DeferredValue<A>, Job> {
val childScope = mutableChildBuildScope()
- return FrpDeferredValue(deferAsync { childScope.runInBuildScope(block) }) to childScope.job
+ return DeferredValue(deferAsync { block(childScope) }) to childScope.job
}
- private fun <R> deferredInternal(block: FrpBuildScope.() -> R): FrpDeferredValue<R> =
- FrpDeferredValue(deferAsync { runInBuildScope(block) })
+ override fun <R> deferredBuildScope(block: BuildScope.() -> R): DeferredValue<R> =
+ DeferredValue(deferAsync { block() })
- private fun deferredActionInternal(block: FrpBuildScope.() -> Unit) {
- deferAction { runInBuildScope(block) }
+ override fun deferredBuildScopeAction(block: BuildScope.() -> Unit) {
+ deferAction { block() }
}
- private fun <A> TFlow<A>.observeEffectInternal(
- context: CoroutineContext,
- block: FrpEffectScope.(A) -> Unit,
+ override fun <A> Events<A>.observe(
+ coroutineContext: CoroutineContext,
+ block: EffectScope.(A) -> Unit,
): Job {
val subRef = AtomicReference<Maybe<Output<A>>>(null)
val childScope = coroutineScope.childScope()
@@ -169,26 +131,26 @@ internal class BuildScopeImpl(val stateScope: StateScopeImpl, val coroutineScope
}
}
}
+ val localNetwork = LocalNetwork(network, childScope, endSignal)
// Defer so that we don't suspend the caller
deferAction {
val outputNode =
Output<A>(
- context = context,
+ context = coroutineContext,
onDeath = { subRef.getAndSet(None)?.let { childScope.cancel() } },
onEmit = { output ->
if (subRef.get() is Just) {
// Not cancelled, safe to emit
val scope =
- object : FrpEffectScope, FrpTransactionScope by frpScope {
- override val frpCoroutineScope: CoroutineScope = childScope
- override val frpNetwork: FrpNetwork =
- LocalFrpNetwork(network, childScope, endSignal)
+ object : EffectScope, TransactionScope by this@BuildScopeImpl {
+ override val effectCoroutineScope: CoroutineScope = childScope
+ override val kairosNetwork: KairosNetwork = localNetwork
}
- scope.block(output)
+ block(scope, output)
}
},
)
- with(frpScope) { this@observeEffectInternal.takeUntil(endSignal) }
+ this@observe.takeUntil(endSignal)
.init
.connect(evalScope = stateScope.evalScope)
.activate(evalScope = stateScope.evalScope, outputNode.schedulable)
@@ -205,63 +167,103 @@ internal class BuildScopeImpl(val stateScope: StateScopeImpl, val coroutineScope
return childScope.coroutineContext.job
}
- private fun <A, B> TFlow<A>.mapBuildInternal(transform: FrpBuildScope.(A) -> B): TFlow<B> {
+ override fun <A, B> Events<A>.mapBuild(transform: BuildScope.(A) -> B): Events<B> {
val childScope = coroutineScope.childScope()
- return TFlowInit(
+ return EventsInit(
constInit(
"mapBuild",
mapImpl({ init.connect(evalScope = this) }) { spec, _ ->
reenterBuildScope(outerScope = this@BuildScopeImpl, childScope)
- .runInBuildScope { transform(spec) }
+ .transform(spec)
}
.cached(),
)
)
}
- private fun <K, A, B> TFlow<Map<K, Maybe<FrpSpec<A>>>>.applyLatestForKeyInternal(
- init: FrpDeferredValue<Map<K, FrpSpec<B>>>,
+ override fun <K, A, B> Events<Map<K, Maybe<BuildSpec<A>>>>.applyLatestSpecForKey(
+ initialSpecs: DeferredValue<Map<K, BuildSpec<B>>>,
numKeys: Int?,
- ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>> {
- val eventsByKey: GroupedTFlow<K, Maybe<FrpSpec<A>>> = groupByKey(numKeys)
+ ): Pair<Events<Map<K, Maybe<A>>>, DeferredValue<Map<K, B>>> {
+ val eventsByKey: GroupedEvents<K, Maybe<BuildSpec<A>>> = groupByKey(numKeys)
val initOut: Lazy<Map<K, B>> = deferAsync {
- init.unwrapped.value.mapValues { (k, spec) ->
+ initialSpecs.unwrapped.value.mapValues { (k, spec) ->
val newEnd = eventsByKey[k]
val newScope = childBuildScope(newEnd)
- newScope.runInBuildScope(spec)
+ newScope.spec()
}
}
val childScope = coroutineScope.childScope()
- val changesNode: TFlowImpl<Map<K, Maybe<A>>> =
- mapImpl(upstream = { this@applyLatestForKeyInternal.init.connect(evalScope = this) }) {
+ val changesNode: EventsImpl<Map<K, Maybe<A>>> =
+ mapImpl(upstream = { this@applyLatestSpecForKey.init.connect(evalScope = this) }) {
upstreamMap,
_ ->
reenterBuildScope(this@BuildScopeImpl, childScope).run {
- upstreamMap.mapValues { (k: K, ma: Maybe<FrpSpec<A>>) ->
+ upstreamMap.mapValues { (k: K, ma: Maybe<BuildSpec<A>>) ->
ma.map { spec ->
- val newEnd = with(frpScope) { eventsByKey[k].skipNext() }
+ val newEnd = eventsByKey[k].skipNext()
val newScope = childBuildScope(newEnd)
- newScope.runInBuildScope(spec)
+ newScope.spec()
}
}
}
}
- val changes: TFlow<Map<K, Maybe<A>>> =
- TFlowInit(constInit("applyLatestForKey", changesNode.cached()))
+ val changes: Events<Map<K, Maybe<A>>> =
+ EventsInit(constInit("applyLatestForKey", changesNode.cached()))
// Ensure effects are observed; otherwise init will stay alive longer than expected
- changes.observeEffectInternal(EmptyCoroutineContext) {}
- return changes to FrpDeferredValue(initOut)
+ changes.observe()
+ return changes to DeferredValue(initOut)
+ }
+
+ private fun <A, T : Events<A>, S> buildEvents(
+ name: String? = null,
+ constructEvents: (InputNode<A>) -> Pair<T, S>,
+ builder: suspend S.() -> Unit,
+ ): Events<A> {
+ var job: Job? = null
+ val stopEmitter = newStopEmitter("buildEvents[$name]")
+ // Create a child scope that will be kept alive beyond the end of this transaction.
+ val childScope = coroutineScope.childScope()
+ lateinit var emitter: Pair<T, S>
+ val inputNode =
+ InputNode<A>(
+ activate = {
+ // It's possible that activation occurs after all effects have been run, due
+ // to a MuxDeferred switch-in. For this reason, we need to activate in a new
+ // transaction.
+ check(job == null) { "[$name] already activated" }
+ job =
+ childScope.launchImmediate {
+ network
+ .transaction("buildEvents") {
+ reenterBuildScope(this@BuildScopeImpl, childScope)
+ .launchEffect {
+ builder(emitter.second)
+ stopEmitter.emit(Unit)
+ }
+ }
+ .await()
+ .join()
+ }
+ },
+ deactivate = {
+ checkNotNull(job) { "[$name] already deactivated" }.cancel()
+ job = null
+ },
+ )
+ emitter = constructEvents(inputNode)
+ return emitter.first.takeUntil(mergeLeft(stopEmitter, endSignal))
}
- private fun newStopEmitter(name: String): CoalescingMutableTFlow<Unit, Unit> =
- CoalescingMutableTFlow(
+ private fun newStopEmitter(name: String): CoalescingMutableEvents<Unit, Unit> =
+ CoalescingMutableEvents(
name = name,
coalesce = { _, _: Unit -> },
network = network,
getInitialValue = {},
)
- private fun childBuildScope(newEnd: TFlow<Any>): BuildScopeImpl {
+ private fun childBuildScope(newEnd: Events<Any>): BuildScopeImpl {
val newCoroutineScope: CoroutineScope = coroutineScope.childScope()
return BuildScopeImpl(
stateScope = stateScope.childStateScope(newEnd),
@@ -276,7 +278,7 @@ internal class BuildScopeImpl(val stateScope: StateScopeImpl, val coroutineScope
(newCoroutineScope.coroutineContext.job as CompletableJob).complete()
}
)
- runInBuildScope { endSignalOnce.observe { newCoroutineScope.cancel() } }
+ endSignalOnce.observe { newCoroutineScope.cancel() }
}
}
@@ -299,47 +301,6 @@ internal class BuildScopeImpl(val stateScope: StateScopeImpl, val coroutineScope
coroutineScope = childScope,
)
}
-
- private inner class FrpBuildScopeImpl : FrpBuildScope, FrpStateScope by stateScope.frpScope {
-
- override val frpNetwork: FrpNetwork by lazy {
- LocalFrpNetwork(network, coroutineScope, endSignal)
- }
-
- override fun <T> tFlow(
- name: String?,
- builder: suspend FrpProducerScope<T>.() -> Unit,
- ): TFlow<T> = tFlowInternal(name, builder)
-
- override fun <In, Out> coalescingTFlow(
- getInitialValue: () -> Out,
- coalesce: (old: Out, new: In) -> Out,
- builder: suspend FrpCoalescingProducerScope<In>.() -> Unit,
- ): TFlow<Out> = coalescingTFlowInternal(getInitialValue, coalesce, builder)
-
- override fun <A> asyncScope(block: FrpSpec<A>): Pair<FrpDeferredValue<A>, Job> =
- asyncScopeInternal(block)
-
- override fun <R> deferredBuildScope(block: FrpBuildScope.() -> R): FrpDeferredValue<R> =
- deferredInternal(block)
-
- override fun deferredBuildScopeAction(block: FrpBuildScope.() -> Unit) =
- deferredActionInternal(block)
-
- override fun <A> TFlow<A>.observe(
- coroutineContext: CoroutineContext,
- block: FrpEffectScope.(A) -> Unit,
- ): Job = observeEffectInternal(coroutineContext, block)
-
- override fun <A, B> TFlow<A>.mapBuild(transform: FrpBuildScope.(A) -> B): TFlow<B> =
- mapBuildInternal(transform)
-
- override fun <K, A, B> TFlow<Map<K, Maybe<FrpSpec<A>>>>.applyLatestSpecForKey(
- initialSpecs: FrpDeferredValue<Map<K, FrpSpec<B>>>,
- numKeys: Int?,
- ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>> =
- applyLatestForKeyInternal(initialSpecs, numKeys)
- }
}
private fun EvalScope.reenterBuildScope(
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Demux.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Demux.kt
index b71a245c71a2..d19a47eb873e 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Demux.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Demux.kt
@@ -205,7 +205,7 @@ internal class DemuxNode<W, K, A>(
}
internal fun <W, K, A> DemuxImpl(
- upstream: TFlowImpl<MapK<W, K, A>>,
+ upstream: EventsImpl<MapK<W, K, A>>,
numKeys: Int?,
storeFactory: MutableMapK.Factory<W, K>,
): DemuxImpl<K, A> =
@@ -216,14 +216,14 @@ internal fun <W, K, A> DemuxImpl(
)
internal fun <K, A> demuxMap(
- upstream: EvalScope.() -> TFlowImpl<Map<K, A>>,
+ upstream: EvalScope.() -> EventsImpl<Map<K, A>>,
numKeys: Int?,
): DemuxImpl<K, A> =
DemuxImpl(mapImpl(upstream) { it, _ -> MapHolder(it) }, numKeys, ConcurrentHashMapK.Factory())
internal class DemuxActivator<W, K, A>(
private val numKeys: Int?,
- private val upstream: TFlowImpl<MapK<W, K, A>>,
+ private val upstream: EventsImpl<MapK<W, K, A>>,
private val storeFactory: MutableMapK.Factory<W, K>,
) {
fun activate(
@@ -246,7 +246,7 @@ internal class DemuxActivator<W, K, A>(
}
internal class DemuxImpl<in K, out A>(private val dmux: DemuxLifecycle<K, A>) {
- fun eventsForKey(key: K): TFlowImpl<A> = TFlowCheap { downstream ->
+ fun eventsForKey(key: K): EventsImpl<A> = EventsImplCheap { downstream ->
dmux.activate(evalScope = this, key)?.let { (branchNode, needsEval) ->
branchNode.addDownstream(downstream)
val branchNeedsEval = needsEval && branchNode.hasCurrentValue(0, evalScope = this)
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/EvalScopeImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/EvalScopeImpl.kt
index 9ecfbba7d647..80a294819fac 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/EvalScopeImpl.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/EvalScopeImpl.kt
@@ -16,49 +16,54 @@
package com.android.systemui.kairos.internal
-import com.android.systemui.kairos.FrpDeferredValue
-import com.android.systemui.kairos.FrpTransactionScope
-import com.android.systemui.kairos.TFlow
-import com.android.systemui.kairos.TFlowInit
-import com.android.systemui.kairos.TFlowLoop
-import com.android.systemui.kairos.TState
-import com.android.systemui.kairos.TStateInit
+import com.android.systemui.kairos.DeferredValue
+import com.android.systemui.kairos.Events
+import com.android.systemui.kairos.EventsInit
+import com.android.systemui.kairos.EventsLoop
+import com.android.systemui.kairos.State
+import com.android.systemui.kairos.StateInit
+import com.android.systemui.kairos.TransactionScope
import com.android.systemui.kairos.Transactional
-import com.android.systemui.kairos.emptyTFlow
+import com.android.systemui.kairos.emptyEvents
import com.android.systemui.kairos.init
import com.android.systemui.kairos.mapCheap
-import com.android.systemui.kairos.switch
+import com.android.systemui.kairos.switchEvents
internal class EvalScopeImpl(networkScope: NetworkScope, deferScope: DeferScope) :
- EvalScope, NetworkScope by networkScope, DeferScope by deferScope {
+ EvalScope, NetworkScope by networkScope, DeferScope by deferScope, TransactionScope {
- private fun <A> Transactional<A>.sample(): A = impl.sample().sample(this@EvalScopeImpl).value
+ override fun <A> Transactional<A>.sampleDeferred(): DeferredValue<A> =
+ DeferredValue(deferAsync { impl.sample().sample(this@EvalScopeImpl).value })
- private fun <A> TState<A>.sample(): A =
- init.connect(evalScope = this@EvalScopeImpl).getCurrentWithEpoch(this@EvalScopeImpl).first
+ override fun <A> State<A>.sampleDeferred(): DeferredValue<A> =
+ DeferredValue(
+ deferAsync {
+ init
+ .connect(evalScope = this@EvalScopeImpl)
+ .getCurrentWithEpoch(this@EvalScopeImpl)
+ .first
+ }
+ )
- private val <A> Transactional<A>.deferredValue: FrpDeferredValue<A>
- get() = FrpDeferredValue(deferAsync { sample() })
+ override fun <R> deferredTransactionScope(block: TransactionScope.() -> R): DeferredValue<R> =
+ DeferredValue(deferAsync { block() })
- private val <A> TState<A>.deferredValue: FrpDeferredValue<A>
- get() = FrpDeferredValue(deferAsync { sample() })
-
- private val nowInternal: TFlow<Unit> by lazy {
- var result by TFlowLoop<Unit>()
+ override val now: Events<Unit> by lazy {
+ var result by EventsLoop<Unit>()
result =
- TStateInit(
+ StateInit(
constInit(
"now",
- activatedTStateSource(
+ activatedStateSource(
"now",
"now",
this,
- { result.mapCheap { emptyTFlow }.init.connect(evalScope = this) },
+ { result.mapCheap { emptyEvents }.init.connect(evalScope = this) },
CompletableLazy(
- TFlowInit(
+ EventsInit(
constInit(
"now",
- TFlowCheap {
+ EventsImplCheap {
ActivationResult(
connection = NodeConnection(AlwaysNode, AlwaysNode),
needsEval = true,
@@ -70,27 +75,7 @@ internal class EvalScopeImpl(networkScope: NetworkScope, deferScope: DeferScope)
),
)
)
- .switch()
+ .switchEvents()
result
}
-
- private fun <R> deferredInternal(block: FrpTransactionScope.() -> R): FrpDeferredValue<R> =
- FrpDeferredValue(deferAsync { runInTransactionScope(block) })
-
- override fun <R> runInTransactionScope(block: FrpTransactionScope.() -> R): R = frpScope.block()
-
- override val frpScope: FrpTransactionScope = FrpTransactionScopeImpl()
-
- inner class FrpTransactionScopeImpl : FrpTransactionScope {
- override fun <A> Transactional<A>.sampleDeferred(): FrpDeferredValue<A> = deferredValue
-
- override fun <A> TState<A>.sampleDeferred(): FrpDeferredValue<A> = deferredValue
-
- override fun <R> deferredTransactionScope(
- block: FrpTransactionScope.() -> R
- ): FrpDeferredValue<R> = deferredInternal(block)
-
- override val now: TFlow<Unit>
- get() = nowInternal
- }
}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/TFlowImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/EventsImpl.kt
index 47a585abac5f..e7978b8bc5ea 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/TFlowImpl.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/EventsImpl.kt
@@ -17,7 +17,7 @@
package com.android.systemui.kairos.internal
/* Initialized TFlow */
-internal fun interface TFlowImpl<out A> {
+internal fun interface EventsImpl<out A> {
fun activate(evalScope: EvalScope, downstream: Schedulable): ActivationResult<A>?
}
@@ -26,8 +26,8 @@ internal data class ActivationResult<out A>(
val needsEval: Boolean,
)
-internal inline fun <A> TFlowCheap(crossinline cheap: CheapNodeSubscribe<A>) =
- TFlowImpl { scope, ds ->
+internal inline fun <A> EventsImplCheap(crossinline cheap: CheapNodeSubscribe<A>) =
+ EventsImpl { scope, ds ->
scope.cheap(ds)
}
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 30c1a865f50a..d38bf21c5db1 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
@@ -24,8 +24,8 @@ import com.android.systemui.kairos.util.just
import com.android.systemui.kairos.util.none
internal inline fun <A> filterJustImpl(
- crossinline getPulse: EvalScope.() -> TFlowImpl<Maybe<A>>
-): TFlowImpl<A> =
+ crossinline getPulse: EvalScope.() -> EventsImpl<Maybe<A>>
+): EventsImpl<A> =
DemuxImpl(
mapImpl(getPulse) { maybeResult, _ ->
if (maybeResult is Just) {
@@ -40,9 +40,9 @@ internal inline fun <A> filterJustImpl(
.eventsForKey(Unit)
internal inline fun <A> filterImpl(
- crossinline getPulse: EvalScope.() -> TFlowImpl<A>,
+ crossinline getPulse: EvalScope.() -> EventsImpl<A>,
crossinline f: EvalScope.(A) -> Boolean,
-): TFlowImpl<A> {
+): EventsImpl<A> {
val mapped = mapImpl(getPulse) { it, _ -> if (f(it)) just(it) else none }.cached()
return filterJustImpl { mapped }
}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Graph.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Graph.kt
index 667002bd413c..b956e44e0618 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Graph.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Graph.kt
@@ -340,7 +340,7 @@ internal class DepthTracker {
internal class DownstreamSet {
val outputs = HashSet<Output<*>>()
- val stateWriters = mutableListOf<TStateSource<*>>()
+ val stateWriters = mutableListOf<StateSource<*>>()
val muxMovers = HashSet<MuxDeferredNode<*, *, *>>()
val nodes = HashSet<SchedulableNode>()
@@ -459,7 +459,7 @@ internal class DownstreamSet {
// TODO: remove this indirection
internal sealed interface Schedulable {
- data class S constructor(val state: TStateSource<*>) : Schedulable
+ data class S constructor(val state: StateSource<*>) : Schedulable
data class M constructor(val muxMover: MuxDeferredNode<*, *, *>) : Schedulable
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Inputs.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Inputs.kt
index 1dcba4433a8d..d7cbe8087f52 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Inputs.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Inputs.kt
@@ -88,7 +88,7 @@ internal class InputNode<A>(
}
}
-internal fun <A> InputNode<A>.activated() = TFlowCheap { downstream ->
+internal fun <A> InputNode<A>.activated() = EventsImplCheap { downstream ->
val input = this@activated
addDownstreamAndActivateIfNeeded(downstream, evalScope = this)
ActivationResult(
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/InternalScopes.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/InternalScopes.kt
index 62bf34810de7..cd2214370d62 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/InternalScopes.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/InternalScopes.kt
@@ -16,38 +16,25 @@
package com.android.systemui.kairos.internal
-import com.android.systemui.kairos.FrpBuildScope
-import com.android.systemui.kairos.FrpStateScope
-import com.android.systemui.kairos.FrpTransactionScope
-import com.android.systemui.kairos.TFlow
+import com.android.systemui.kairos.BuildScope
+import com.android.systemui.kairos.Events
+import com.android.systemui.kairos.StateScope
+import com.android.systemui.kairos.TransactionScope
internal interface InitScope {
val networkId: Any
}
-internal interface EvalScope : NetworkScope, DeferScope {
- val frpScope: FrpTransactionScope
+internal interface EvalScope : NetworkScope, DeferScope, TransactionScope
- fun <R> runInTransactionScope(block: FrpTransactionScope.() -> R): R
-}
-
-internal interface StateScope : EvalScope {
- override val frpScope: FrpStateScope
-
- fun <R> runInStateScope(block: FrpStateScope.() -> R): R
-
- val endSignal: TFlow<Any>
+internal interface InternalStateScope : EvalScope, StateScope {
+ val endSignal: Events<Any>
+ val endSignalOnce: Events<Any>
- fun childStateScope(newEnd: TFlow<Any>): StateScope
-
- val endSignalOnce: TFlow<Any>
+ fun childStateScope(newEnd: Events<Any>): InternalStateScope
}
-internal interface BuildScope : StateScope {
- override val frpScope: FrpBuildScope
-
- fun <R> runInBuildScope(block: FrpBuildScope.() -> R): R
-}
+internal interface InternalBuildScope : InternalStateScope, BuildScope
internal interface NetworkScope : InitScope {
@@ -63,7 +50,7 @@ internal interface NetworkScope : InitScope {
fun scheduleMuxMover(muxMover: MuxDeferredNode<*, *, *>)
- fun schedule(state: TStateSource<*>)
+ fun schedule(state: StateSource<*>)
fun scheduleDeactivation(node: PushNode<*>)
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Mux.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Mux.kt
index 1cdf895ec1ed..268fec4fa486 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Mux.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Mux.kt
@@ -272,9 +272,9 @@ internal typealias BranchNode<W, K, V> = MuxNode<W, K, V>.BranchNode
/** Tracks lifecycle of MuxNode in the network. Essentially a mutable ref for MuxLifecycleState. */
internal class MuxLifecycle<W, K, V>(var lifecycleState: MuxLifecycleState<W, K, V>) :
- TFlowImpl<MuxResult<W, K, V>> {
+ EventsImpl<MuxResult<W, K, V>> {
- override fun toString(): String = "TFlowMuxLifecycle[$hashString][$lifecycleState]"
+ override fun toString(): String = "MuxLifecycle[$hashString][$lifecycleState]"
override fun activate(
evalScope: EvalScope,
@@ -332,9 +332,9 @@ internal interface MuxActivator<W, K, V> {
internal inline fun <W, K, V> MuxLifecycle(
onSubscribe: MuxActivator<W, K, V>
-): TFlowImpl<MuxResult<W, K, V>> = MuxLifecycle(MuxLifecycleState.Inactive(onSubscribe))
+): EventsImpl<MuxResult<W, K, V>> = MuxLifecycle(MuxLifecycleState.Inactive(onSubscribe))
-internal fun <K, V> TFlowImpl<MuxResult<MapHolder.W, K, V>>.awaitValues(): TFlowImpl<Map<K, V>> =
+internal fun <K, V> EventsImpl<MuxResult<MapHolder.W, K, V>>.awaitValues(): EventsImpl<Map<K, V>> =
mapImpl({ this@awaitValues }) { results, logIndent ->
results.asMapHolder().unwrapped.mapValues { it.value.getPushEvent(logIndent, this) }
}
@@ -343,15 +343,15 @@ internal fun <K, V> TFlowImpl<MuxResult<MapHolder.W, K, V>>.awaitValues(): TFlow
internal fun <W, K, V> MuxNode<W, K, V>.initializeUpstream(
evalScope: EvalScope,
- getStorage: EvalScope.() -> Iterable<Map.Entry<K, TFlowImpl<V>>>,
+ getStorage: EvalScope.() -> Iterable<Map.Entry<K, EventsImpl<V>>>,
storeFactory: MutableMapK.Factory<W, K>,
) {
val storage = getStorage(evalScope)
val initUpstream = buildList {
- storage.forEach { (key, flow) ->
+ storage.forEach { (key, events) ->
val branchNode = BranchNode(key)
add(
- flow.activate(evalScope, branchNode.schedulable)?.let { (conn, needsEval) ->
+ events.activate(evalScope, branchNode.schedulable)?.let { (conn, needsEval) ->
Triple(
key,
branchNode.apply { upstream = conn },
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 5ce0248d0655..53a6ecabda6a 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
@@ -48,8 +48,8 @@ internal class MuxDeferredNode<W, K, V>(
) : MuxNode<W, K, V>(lifecycle, factory) {
val schedulable = Schedulable.M(this)
- var patches: NodeConnection<Iterable<Map.Entry<K, Maybe<TFlowImpl<V>>>>>? = null
- var patchData: Iterable<Map.Entry<K, Maybe<TFlowImpl<V>>>>? = null
+ var patches: NodeConnection<Iterable<Map.Entry<K, Maybe<EventsImpl<V>>>>>? = null
+ var patchData: Iterable<Map.Entry<K, Maybe<EventsImpl<V>>>>? = null
override fun visit(logIndent: Int, evalScope: EvalScope) {
check(epoch < evalScope.epoch) { "node unexpectedly visited multiple times in transaction" }
@@ -129,7 +129,7 @@ internal class MuxDeferredNode<W, K, V>(
// TODO: this logic is very similar to what's in MuxPrompt, maybe turn into an inline fun?
// We have a patch, process additions/updates and removals
- val adds = mutableListOf<Pair<K, TFlowImpl<V>>>()
+ val adds = mutableListOf<Pair<K, EventsImpl<V>>>()
val removes = mutableListOf<K>()
patch.forEach { (k, newUpstream) ->
when (newUpstream) {
@@ -151,7 +151,7 @@ internal class MuxDeferredNode<W, K, V>(
}
// add or replace
- adds.forEach { (k, newUpstream: TFlowImpl<V>) ->
+ adds.forEach { (k, newUpstream: EventsImpl<V>) ->
// remove old and sever, if present
switchedIn.remove(k)?.let { branchNode ->
val conn = branchNode.upstream
@@ -279,9 +279,9 @@ internal class MuxDeferredNode<W, K, V>(
internal inline fun <A> switchDeferredImplSingle(
name: String? = null,
- crossinline getStorage: EvalScope.() -> TFlowImpl<A>,
- crossinline getPatches: EvalScope.() -> TFlowImpl<TFlowImpl<A>>,
-): TFlowImpl<A> {
+ crossinline getStorage: EvalScope.() -> EventsImpl<A>,
+ crossinline getPatches: EvalScope.() -> EventsImpl<EventsImpl<A>>,
+): EventsImpl<A> {
val patches = mapImpl(getPatches) { newFlow, _ -> singleOf(just(newFlow)).asIterable() }
val switchDeferredImpl =
switchDeferredImpl(
@@ -301,17 +301,17 @@ internal inline fun <A> switchDeferredImplSingle(
internal fun <W, K, V> switchDeferredImpl(
name: String? = null,
- getStorage: EvalScope.() -> Iterable<Map.Entry<K, TFlowImpl<V>>>,
- getPatches: EvalScope.() -> TFlowImpl<Iterable<Map.Entry<K, Maybe<TFlowImpl<V>>>>>,
+ getStorage: EvalScope.() -> Iterable<Map.Entry<K, EventsImpl<V>>>,
+ getPatches: EvalScope.() -> EventsImpl<Iterable<Map.Entry<K, Maybe<EventsImpl<V>>>>>,
storeFactory: MutableMapK.Factory<W, K>,
-): TFlowImpl<MuxResult<W, K, V>> =
+): EventsImpl<MuxResult<W, K, V>> =
MuxLifecycle(MuxDeferredActivator(name, getStorage, storeFactory, getPatches))
private class MuxDeferredActivator<W, K, V>(
private val name: String?,
- private val getStorage: EvalScope.() -> Iterable<Map.Entry<K, TFlowImpl<V>>>,
+ private val getStorage: EvalScope.() -> Iterable<Map.Entry<K, EventsImpl<V>>>,
private val storeFactory: MutableMapK.Factory<W, K>,
- private val getPatches: EvalScope.() -> TFlowImpl<Iterable<Map.Entry<K, Maybe<TFlowImpl<V>>>>>,
+ private val getPatches: EvalScope.() -> EventsImpl<Iterable<Map.Entry<K, Maybe<EventsImpl<V>>>>>,
) : MuxActivator<W, K, V> {
override fun activate(
evalScope: EvalScope,
@@ -381,11 +381,11 @@ private class MuxDeferredActivator<W, K, V>(
}
internal inline fun <A> mergeNodes(
- crossinline getPulse: EvalScope.() -> TFlowImpl<A>,
- crossinline getOther: EvalScope.() -> TFlowImpl<A>,
+ crossinline getPulse: EvalScope.() -> EventsImpl<A>,
+ crossinline getOther: EvalScope.() -> EventsImpl<A>,
name: String? = null,
crossinline f: EvalScope.(A, A) -> A,
-): TFlowImpl<A> {
+): EventsImpl<A> {
val mergedThese = mergeNodes(name, getPulse, getOther)
val merged =
mapImpl({ mergedThese }) { these, _ -> these.merge { thiz, that -> f(thiz, that) } }
@@ -397,9 +397,9 @@ internal fun <T> Iterable<T>.asIterableWithIndex(): Iterable<Map.Entry<Int, T>>
internal inline fun <A, B> mergeNodes(
name: String? = null,
- crossinline getPulse: EvalScope.() -> TFlowImpl<A>,
- crossinline getOther: EvalScope.() -> TFlowImpl<B>,
-): TFlowImpl<These<A, B>> {
+ crossinline getPulse: EvalScope.() -> EventsImpl<A>,
+ crossinline getOther: EvalScope.() -> EventsImpl<B>,
+): EventsImpl<These<A, B>> {
val storage =
listOf(
mapImpl(getPulse) { it, _ -> These.thiz<A, B>(it) },
@@ -426,8 +426,8 @@ internal inline fun <A, B> mergeNodes(
}
internal inline fun <A> mergeNodes(
- crossinline getPulses: EvalScope.() -> Iterable<TFlowImpl<A>>
-): TFlowImpl<List<A>> {
+ crossinline getPulses: EvalScope.() -> Iterable<EventsImpl<A>>
+): EventsImpl<List<A>> {
val switchNode =
switchDeferredImpl(
getStorage = { getPulses().asIterableWithIndex() },
@@ -443,8 +443,8 @@ internal inline fun <A> mergeNodes(
}
internal inline fun <A> mergeNodesLeft(
- crossinline getPulses: EvalScope.() -> Iterable<TFlowImpl<A>>
-): TFlowImpl<A> {
+ crossinline getPulses: EvalScope.() -> Iterable<EventsImpl<A>>
+): EventsImpl<A> {
val switchNode =
switchDeferredImpl(
getStorage = { getPulses().asIterableWithIndex() },
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 1c9af24a392f..785a98b105c5 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
@@ -35,13 +35,13 @@ internal class MuxPromptNode<W, K, V>(
factory: MutableMapK.Factory<W, K>,
) : MuxNode<W, K, V>(lifecycle, factory) {
- var patchData: Iterable<Map.Entry<K, Maybe<TFlowImpl<V>>>>? = null
+ var patchData: Iterable<Map.Entry<K, Maybe<EventsImpl<V>>>>? = null
var patches: PatchNode? = null
override fun visit(logIndent: Int, evalScope: EvalScope) {
check(epoch < evalScope.epoch) { "node unexpectedly visited multiple times in transaction" }
logDuration(logIndent, "MuxPrompt.visit") {
- val patch: Iterable<Map.Entry<K, Maybe<TFlowImpl<V>>>>? = patchData
+ val patch: Iterable<Map.Entry<K, Maybe<EventsImpl<V>>>>? = patchData
patchData = null
// If there's a patch, process it.
@@ -85,12 +85,12 @@ internal class MuxPromptNode<W, K, V>(
// side-effect: this will populate `upstreamData` with any immediately available results
private fun LogIndent.processPatch(
- patch: Iterable<Map.Entry<K, Maybe<TFlowImpl<V>>>>,
+ patch: Iterable<Map.Entry<K, Maybe<EventsImpl<V>>>>,
evalScope: EvalScope,
): Boolean {
var needsReschedule = false
// We have a patch, process additions/updates and removals
- val adds = mutableListOf<Pair<K, TFlowImpl<V>>>()
+ val adds = mutableListOf<Pair<K, EventsImpl<V>>>()
val removes = mutableListOf<K>()
patch.forEach { (k, newUpstream) ->
when (newUpstream) {
@@ -115,7 +115,7 @@ internal class MuxPromptNode<W, K, V>(
}
// add or replace
- adds.forEach { (k, newUpstream: TFlowImpl<V>) ->
+ adds.forEach { (k, newUpstream: EventsImpl<V>) ->
// remove old and sever, if present
switchedIn.remove(k)?.let { oldBranch: BranchNode ->
if (name != null) {
@@ -232,7 +232,7 @@ internal class MuxPromptNode<W, K, V>(
val schedulable = Schedulable.N(this)
- lateinit var upstream: NodeConnection<Iterable<Map.Entry<K, Maybe<TFlowImpl<V>>>>>
+ lateinit var upstream: NodeConnection<Iterable<Map.Entry<K, Maybe<EventsImpl<V>>>>>
override fun schedule(logIndent: Int, evalScope: EvalScope) {
logDuration(logIndent, "MuxPromptPatchNode.schedule") {
@@ -304,9 +304,9 @@ internal class MuxPromptNode<W, K, V>(
}
internal inline fun <A> switchPromptImplSingle(
- crossinline getStorage: EvalScope.() -> TFlowImpl<A>,
- crossinline getPatches: EvalScope.() -> TFlowImpl<TFlowImpl<A>>,
-): TFlowImpl<A> {
+ crossinline getStorage: EvalScope.() -> EventsImpl<A>,
+ crossinline getPatches: EvalScope.() -> EventsImpl<EventsImpl<A>>,
+): EventsImpl<A> {
val switchPromptImpl =
switchPromptImpl(
getStorage = { singleOf(getStorage()).asIterable() },
@@ -322,17 +322,17 @@ internal inline fun <A> switchPromptImplSingle(
internal fun <W, K, V> switchPromptImpl(
name: String? = null,
- getStorage: EvalScope.() -> Iterable<Map.Entry<K, TFlowImpl<V>>>,
- getPatches: EvalScope.() -> TFlowImpl<Iterable<Map.Entry<K, Maybe<TFlowImpl<V>>>>>,
+ getStorage: EvalScope.() -> Iterable<Map.Entry<K, EventsImpl<V>>>,
+ getPatches: EvalScope.() -> EventsImpl<Iterable<Map.Entry<K, Maybe<EventsImpl<V>>>>>,
storeFactory: MutableMapK.Factory<W, K>,
-): TFlowImpl<MuxResult<W, K, V>> =
+): EventsImpl<MuxResult<W, K, V>> =
MuxLifecycle(MuxPromptActivator(name, getStorage, storeFactory, getPatches))
private class MuxPromptActivator<W, K, V>(
private val name: String?,
- private val getStorage: EvalScope.() -> Iterable<Map.Entry<K, TFlowImpl<V>>>,
+ private val getStorage: EvalScope.() -> Iterable<Map.Entry<K, EventsImpl<V>>>,
private val storeFactory: MutableMapK.Factory<W, K>,
- private val getPatches: EvalScope.() -> TFlowImpl<Iterable<Map.Entry<K, Maybe<TFlowImpl<V>>>>>,
+ private val getPatches: EvalScope.() -> EventsImpl<Iterable<Map.Entry<K, Maybe<EventsImpl<V>>>>>,
) : MuxActivator<W, K, V> {
override fun activate(
evalScope: EvalScope,
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 b90c4c02fa7c..d13cdded7ac3 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
@@ -16,7 +16,7 @@
package com.android.systemui.kairos.internal
-import com.android.systemui.kairos.TState
+import com.android.systemui.kairos.State
import com.android.systemui.kairos.internal.util.HeteroMap
import com.android.systemui.kairos.internal.util.logDuration
import com.android.systemui.kairos.internal.util.logLn
@@ -68,7 +68,7 @@ internal class Network(val coroutineScope: CoroutineScope) : NetworkScope {
}
override val transactionStore = TransactionStore()
- private val stateWrites = ArrayDeque<TStateSource<*>>()
+ private val stateWrites = ArrayDeque<StateSource<*>>()
private val outputsByDispatcher = HashMap<ContinuationInterceptor, ArrayDeque<Output<*>>>()
private val muxMovers = ArrayDeque<MuxDeferredNode<*, *, *>>()
private val deactivations = ArrayDeque<PushNode<*>>()
@@ -86,7 +86,7 @@ internal class Network(val coroutineScope: CoroutineScope) : NetworkScope {
muxMovers.add(muxMover)
}
- override fun schedule(state: TStateSource<*>) {
+ override fun schedule(state: StateSource<*>) {
stateWrites.add(state)
}
@@ -98,7 +98,7 @@ internal class Network(val coroutineScope: CoroutineScope) : NetworkScope {
outputDeactivations.add(output)
}
- /** Listens for external events and starts FRP transactions. Runs forever. */
+ /** Listens for external events and starts Kairos transactions. Runs forever. */
suspend fun runInputScheduler() {
val actions = mutableListOf<ScheduledAction<*>>()
for (first in inputScheduleChan) {
@@ -161,7 +161,7 @@ internal class Network(val coroutineScope: CoroutineScope) : NetworkScope {
block(EvalScopeImpl(this@Network, this))
}
- /** Performs a transactional update of the FRP network. */
+ /** Performs a transactional update of the Kairos network. */
private suspend fun doTransaction(logIndent: Int) {
// Traverse network, then run outputs
logDuration(logIndent, "traverse network") {
@@ -227,7 +227,7 @@ internal class Network(val coroutineScope: CoroutineScope) : NetworkScope {
}
}
- /** Updates all [TState]es that have changed within this transaction. */
+ /** Updates all [State]es that have changed within this transaction. */
private fun evalStateWriters(logIndent: Int, evalScope: EvalScope) {
while (stateWrites.isNotEmpty()) {
val latch = stateWrites.removeFirst()
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/NoScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/NoScope.kt
index 14e4e1cfc143..f662f1907069 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/NoScope.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/NoScope.kt
@@ -16,10 +16,6 @@
package com.android.systemui.kairos.internal
-import com.android.systemui.kairos.FrpScope
+import com.android.systemui.kairos.KairosScope
-internal object NoScope {
- private object FrpScopeImpl : FrpScope
-
- fun <R> runInFrpScope(block: FrpScope.() -> R): R = FrpScopeImpl.block()
-}
+internal object NoScope : KairosScope
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/PullNodes.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/PullNodes.kt
index 5ade401da1a5..517e54f57833 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/PullNodes.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/PullNodes.kt
@@ -18,7 +18,7 @@ package com.android.systemui.kairos.internal
import com.android.systemui.kairos.internal.util.logDuration
-internal val neverImpl: TFlowImpl<Nothing> = TFlowCheap { null }
+internal val neverImpl: EventsImpl<Nothing> = EventsImplCheap { null }
internal class MapNode<A, B>(val upstream: PullNode<A>, val transform: EvalScope.(A, Int) -> B) :
PullNode<B> {
@@ -31,9 +31,9 @@ internal class MapNode<A, B>(val upstream: PullNode<A>, val transform: EvalScope
}
internal inline fun <A, B> mapImpl(
- crossinline upstream: EvalScope.() -> TFlowImpl<A>,
+ crossinline upstream: EvalScope.() -> EventsImpl<A>,
noinline transform: EvalScope.(A, Int) -> B,
-): TFlowImpl<B> = TFlowCheap { downstream ->
+): EventsImpl<B> = EventsImplCheap { downstream ->
upstream().activate(evalScope = this, downstream)?.let { (connection, needsEval) ->
ActivationResult(
connection =
@@ -66,9 +66,9 @@ internal class CachedNode<A>(
}
}
-internal fun <A> TFlowImpl<A>.cached(): TFlowImpl<A> {
+internal fun <A> EventsImpl<A>.cached(): EventsImpl<A> {
val key = TransactionCache<Lazy<A>>()
- return TFlowCheap { it ->
+ return EventsImplCheap { it ->
activate(this, it)?.let { (connection, needsEval) ->
ActivationResult(
connection =
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/TStateImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateImpl.kt
index 9565a9c12d38..5ba645246b0f 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/TStateImpl.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateImpl.kt
@@ -27,15 +27,15 @@ import com.android.systemui.kairos.util.none
import java.util.concurrent.atomic.AtomicLong
import kotlinx.coroutines.ExperimentalCoroutinesApi
-internal sealed interface TStateImpl<out A> {
+internal sealed interface StateImpl<out A> {
val name: String?
val operatorName: String
- val changes: TFlowImpl<A>
+ val changes: EventsImpl<A>
fun getCurrentWithEpoch(evalScope: EvalScope): Pair<A, Long>
}
-internal sealed class TStateDerived<A>(override val changes: TFlowImpl<A>) : TStateImpl<A> {
+internal sealed class StateDerived<A>(override val changes: EventsImpl<A>) : StateImpl<A> {
@Volatile
var invalidatedEpoch = Long.MIN_VALUE
@@ -73,17 +73,17 @@ internal sealed class TStateDerived<A>(override val changes: TFlowImpl<A>) : TSt
private data object EmptyCache
}
-internal class TStateSource<A>(
+internal class StateSource<A>(
override val name: String?,
override val operatorName: String,
init: Lazy<A>,
- override val changes: TFlowImpl<A>,
-) : TStateImpl<A> {
+ override val changes: EventsImpl<A>,
+) : StateImpl<A> {
constructor(
name: String?,
operatorName: String,
init: A,
- changes: TFlowImpl<A>,
+ changes: EventsImpl<A>,
) : this(name, operatorName, CompletableLazy(init), changes)
lateinit var upstreamConnection: NodeConnection<A>
@@ -108,26 +108,26 @@ internal class TStateSource<A>(
writeEpoch = evalScope.epoch
}
- override fun toString(): String = "TStateImpl(changes=$changes, current=$_current)"
+ override fun toString(): String = "StateImpl(changes=$changes, current=$_current)"
@OptIn(ExperimentalCoroutinesApi::class)
fun getStorageUnsafe(): Maybe<A> = if (_current.isInitialized()) just(_current.value) else none
}
-internal fun <A> constS(name: String?, operatorName: String, init: A): TStateImpl<A> =
- TStateSource(name, operatorName, init, neverImpl)
+internal fun <A> constState(name: String?, operatorName: String, init: A): StateImpl<A> =
+ StateSource(name, operatorName, init, neverImpl)
-internal inline fun <A> activatedTStateSource(
+internal inline fun <A> activatedStateSource(
name: String?,
operatorName: String,
evalScope: EvalScope,
- crossinline getChanges: EvalScope.() -> TFlowImpl<A>,
+ crossinline getChanges: EvalScope.() -> EventsImpl<A>,
init: Lazy<A>,
-): TStateImpl<A> {
- lateinit var state: TStateSource<A>
- val calm: TFlowImpl<A> =
+): StateImpl<A> {
+ lateinit var state: StateSource<A>
+ val calm: EventsImpl<A> =
filterImpl(getChanges) { new -> new != state.getCurrentWithEpoch(evalScope = this).first }
- return TStateSource(name, operatorName, init, calm).also {
+ return StateSource(name, operatorName, init, calm).also {
state = it
evalScope.scheduleOutput(
OneShot {
@@ -143,9 +143,9 @@ internal inline fun <A> activatedTStateSource(
}
}
-private inline fun <A> TFlowImpl<A>.calm(
- crossinline getState: () -> TStateDerived<A>
-): TFlowImpl<A> =
+private inline fun <A> EventsImpl<A>.calm(
+ crossinline getState: () -> StateDerived<A>
+): EventsImpl<A> =
filterImpl({ this@calm }) { new ->
val state = getState()
val (current, _) = state.getCurrentWithEpoch(evalScope = this)
@@ -158,11 +158,11 @@ private inline fun <A> TFlowImpl<A>.calm(
}
.cached()
-internal fun <A, B> TStateImpl<A>.mapCheap(
+internal fun <A, B> StateImpl<A>.mapCheap(
name: String?,
operatorName: String,
transform: EvalScope.(A) -> B,
-): TStateImpl<B> =
+): StateImpl<B> =
DerivedMapCheap(
name,
operatorName,
@@ -174,10 +174,10 @@ internal fun <A, B> TStateImpl<A>.mapCheap(
internal class DerivedMapCheap<A, B>(
override val name: String?,
override val operatorName: String,
- val upstream: TStateImpl<A>,
- override val changes: TFlowImpl<B>,
+ val upstream: StateImpl<A>,
+ override val changes: EventsImpl<B>,
private val transform: EvalScope.(A) -> B,
-) : TStateImpl<B> {
+) : StateImpl<B> {
override fun getCurrentWithEpoch(evalScope: EvalScope): Pair<B, Long> {
val (a, epoch) = upstream.getCurrentWithEpoch(evalScope)
@@ -187,12 +187,12 @@ internal class DerivedMapCheap<A, B>(
override fun toString(): String = "${this::class.simpleName}@$hashString"
}
-internal fun <A, B> TStateImpl<A>.map(
+internal fun <A, B> StateImpl<A>.map(
name: String?,
operatorName: String,
transform: EvalScope.(A) -> B,
-): TStateImpl<B> {
- lateinit var state: TStateDerived<B>
+): StateImpl<B> {
+ lateinit var state: StateDerived<B>
val mappedChanges = mapImpl({ changes }) { it, _ -> transform(it) }.cached().calm { state }
state = DerivedMap(name, operatorName, transform, this, mappedChanges)
return state
@@ -202,9 +202,9 @@ internal class DerivedMap<A, B>(
override val name: String?,
override val operatorName: String,
private val transform: EvalScope.(A) -> B,
- val upstream: TStateImpl<A>,
- changes: TFlowImpl<B>,
-) : TStateDerived<B>(changes) {
+ val upstream: StateImpl<A>,
+ changes: EventsImpl<B>,
+) : StateDerived<B>(changes) {
override fun toString(): String = "${this::class.simpleName}@$hashString"
override fun recalc(evalScope: EvalScope): Pair<B, Long>? {
@@ -217,7 +217,7 @@ internal class DerivedMap<A, B>(
}
}
-internal fun <A> TStateImpl<TStateImpl<A>>.flatten(name: String?, operator: String): TStateImpl<A> {
+internal fun <A> StateImpl<StateImpl<A>>.flatten(name: String?, operator: String): StateImpl<A> {
// emits the current value of the new inner state, when that state is emitted
val switchEvents =
mapImpl({ changes }) { newInner, _ -> newInner.getCurrentWithEpoch(this).first }
@@ -228,7 +228,7 @@ internal fun <A> TStateImpl<TStateImpl<A>>.flatten(name: String?, operator: Stri
mapImpl({ changes }) { newInner, _ ->
mergeNodes({ switchEvents }, { newInner.changes }) { _, new -> new }
}
- val switchedChanges: TFlowImpl<A> =
+ val switchedChanges: EventsImpl<A> =
switchPromptImplSingle(
getStorage = { this@flatten.getCurrentWithEpoch(evalScope = this).first.changes },
getPatches = { innerChanges },
@@ -241,9 +241,9 @@ internal fun <A> TStateImpl<TStateImpl<A>>.flatten(name: String?, operator: Stri
internal class DerivedFlatten<A>(
override val name: String?,
override val operatorName: String,
- val upstream: TStateImpl<TStateImpl<A>>,
- changes: TFlowImpl<A>,
-) : TStateDerived<A>(changes) {
+ val upstream: StateImpl<StateImpl<A>>,
+ changes: EventsImpl<A>,
+) : StateDerived<A>(changes) {
override fun recalc(evalScope: EvalScope): Pair<A, Long> {
val (inner, epoch0) = upstream.getCurrentWithEpoch(evalScope)
val (a, epoch1) = inner.getCurrentWithEpoch(evalScope)
@@ -254,19 +254,19 @@ internal class DerivedFlatten<A>(
}
@Suppress("NOTHING_TO_INLINE")
-internal inline fun <A, B> TStateImpl<A>.flatMap(
+internal inline fun <A, B> StateImpl<A>.flatMap(
name: String?,
operatorName: String,
- noinline transform: EvalScope.(A) -> TStateImpl<B>,
-): TStateImpl<B> = map(null, operatorName, transform).flatten(name, operatorName)
+ noinline transform: EvalScope.(A) -> StateImpl<B>,
+): StateImpl<B> = map(null, operatorName, transform).flatten(name, operatorName)
internal fun <A, B, Z> zipStates(
name: String?,
operatorName: String,
- l1: TStateImpl<A>,
- l2: TStateImpl<B>,
+ l1: StateImpl<A>,
+ l2: StateImpl<B>,
transform: EvalScope.(A, B) -> Z,
-): TStateImpl<Z> =
+): StateImpl<Z> =
zipStateList(null, operatorName, listOf(l1, l2)).map(name, operatorName) {
@Suppress("UNCHECKED_CAST") transform(it[0] as A, it[1] as B)
}
@@ -274,11 +274,11 @@ internal fun <A, B, Z> zipStates(
internal fun <A, B, C, Z> zipStates(
name: String?,
operatorName: String,
- l1: TStateImpl<A>,
- l2: TStateImpl<B>,
- l3: TStateImpl<C>,
+ l1: StateImpl<A>,
+ l2: StateImpl<B>,
+ l3: StateImpl<C>,
transform: EvalScope.(A, B, C) -> Z,
-): TStateImpl<Z> =
+): StateImpl<Z> =
zipStateList(null, operatorName, listOf(l1, l2, l3)).map(name, operatorName) {
@Suppress("UNCHECKED_CAST") transform(it[0] as A, it[1] as B, it[2] as C)
}
@@ -286,12 +286,12 @@ internal fun <A, B, C, Z> zipStates(
internal fun <A, B, C, D, Z> zipStates(
name: String?,
operatorName: String,
- l1: TStateImpl<A>,
- l2: TStateImpl<B>,
- l3: TStateImpl<C>,
- l4: TStateImpl<D>,
+ l1: StateImpl<A>,
+ l2: StateImpl<B>,
+ l3: StateImpl<C>,
+ l4: StateImpl<D>,
transform: EvalScope.(A, B, C, D) -> Z,
-): TStateImpl<Z> =
+): StateImpl<Z> =
zipStateList(null, operatorName, listOf(l1, l2, l3, l4)).map(name, operatorName) {
@Suppress("UNCHECKED_CAST") transform(it[0] as A, it[1] as B, it[2] as C, it[3] as D)
}
@@ -299,13 +299,13 @@ internal fun <A, B, C, D, Z> zipStates(
internal fun <A, B, C, D, E, Z> zipStates(
name: String?,
operatorName: String,
- l1: TStateImpl<A>,
- l2: TStateImpl<B>,
- l3: TStateImpl<C>,
- l4: TStateImpl<D>,
- l5: TStateImpl<E>,
+ l1: StateImpl<A>,
+ l2: StateImpl<B>,
+ l3: StateImpl<C>,
+ l4: StateImpl<D>,
+ l5: StateImpl<E>,
transform: EvalScope.(A, B, C, D, E) -> Z,
-): TStateImpl<Z> =
+): StateImpl<Z> =
zipStateList(null, operatorName, listOf(l1, l2, l3, l4, l5)).map(name, operatorName) {
@Suppress("UNCHECKED_CAST")
transform(it[0] as A, it[1] as B, it[2] as C, it[3] as D, it[4] as E)
@@ -314,8 +314,8 @@ internal fun <A, B, C, D, E, Z> zipStates(
internal fun <K, V> zipStateMap(
name: String?,
operatorName: String,
- states: Map<K, TStateImpl<V>>,
-): TStateImpl<Map<K, V>> =
+ states: Map<K, StateImpl<V>>,
+): StateImpl<Map<K, V>> =
zipStates(
name = name,
operatorName = operatorName,
@@ -327,8 +327,8 @@ internal fun <K, V> zipStateMap(
internal fun <V> zipStateList(
name: String?,
operatorName: String,
- states: List<TStateImpl<V>>,
-): TStateImpl<List<V>> {
+ states: List<StateImpl<V>>,
+): StateImpl<List<V>> {
val zipped =
zipStates(
name = name,
@@ -352,11 +352,11 @@ internal fun <W, K, A> zipStates(
name: String?,
operatorName: String,
numStates: Int,
- states: Iterable<Map.Entry<K, TStateImpl<A>>>,
+ states: Iterable<Map.Entry<K, StateImpl<A>>>,
storeFactory: MutableMapK.Factory<W, K>,
-): TStateImpl<MutableMapK<W, K, A>> {
+): StateImpl<MutableMapK<W, K, A>> {
if (numStates == 0) {
- return constS(name, operatorName, storeFactory.create(0))
+ return constState(name, operatorName, storeFactory.create(0))
}
val stateChanges = states.asSequence().map { (k, v) -> StoreEntry(k, v.changes) }.asIterable()
lateinit var state: DerivedZipped<W, K, A>
@@ -397,10 +397,10 @@ internal class DerivedZipped<W, K, A>(
override val name: String?,
override val operatorName: String,
private val upstreamSize: Int,
- val upstream: Iterable<Map.Entry<K, TStateImpl<A>>>,
- changes: TFlowImpl<MutableMapK<W, K, A>>,
+ val upstream: Iterable<Map.Entry<K, StateImpl<A>>>,
+ changes: EventsImpl<MutableMapK<W, K, A>>,
private val storeFactory: MutableMapK.Factory<W, K>,
-) : TStateDerived<MutableMapK<W, K, A>>(changes) {
+) : StateDerived<MutableMapK<W, K, A>>(changes) {
override fun recalc(evalScope: EvalScope): Pair<MutableMapK<W, K, A>, Long> {
val newEpoch = AtomicLong()
val store = storeFactory.create<A>(upstreamSize)
@@ -419,10 +419,10 @@ internal class DerivedZipped<W, K, A>(
internal inline fun <A> zipStates(
name: String?,
operatorName: String,
- states: List<TStateImpl<A>>,
-): TStateImpl<List<A>> =
+ states: List<StateImpl<A>>,
+): StateImpl<List<A>> =
if (states.isEmpty()) {
- constS(name, operatorName, emptyList())
+ constState(name, operatorName, emptyList())
} else {
zipStateList(null, operatorName, states)
}
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 48f69036df89..b5eec85f1f5f 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
@@ -16,91 +16,52 @@
package com.android.systemui.kairos.internal
-import com.android.systemui.kairos.FrpDeferredValue
-import com.android.systemui.kairos.FrpStateScope
-import com.android.systemui.kairos.FrpStateful
-import com.android.systemui.kairos.FrpTransactionScope
-import com.android.systemui.kairos.GroupedTFlow
-import com.android.systemui.kairos.TFlow
-import com.android.systemui.kairos.TFlowInit
-import com.android.systemui.kairos.TFlowLoop
-import com.android.systemui.kairos.TState
-import com.android.systemui.kairos.TStateInit
-import com.android.systemui.kairos.emptyTFlow
+import com.android.systemui.kairos.DeferredValue
+import com.android.systemui.kairos.Events
+import com.android.systemui.kairos.EventsInit
+import com.android.systemui.kairos.EventsLoop
+import com.android.systemui.kairos.GroupedEvents
+import com.android.systemui.kairos.State
+import com.android.systemui.kairos.StateInit
+import com.android.systemui.kairos.StateScope
+import com.android.systemui.kairos.Stateful
+import com.android.systemui.kairos.emptyEvents
import com.android.systemui.kairos.groupByKey
import com.android.systemui.kairos.init
import com.android.systemui.kairos.internal.store.ConcurrentHashMapK
import com.android.systemui.kairos.mapCheap
import com.android.systemui.kairos.merge
-import com.android.systemui.kairos.switch
+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: TFlow<Any>) :
- StateScope, EvalScope by evalScope {
+internal class StateScopeImpl(val evalScope: EvalScope, override val endSignal: Events<Any>) :
+ InternalStateScope, EvalScope by evalScope {
- override val endSignalOnce: TFlow<Any> = endSignal.nextOnlyInternal("StateScope.endSignal")
+ override val endSignalOnce: Events<Any> = endSignal.nextOnlyInternal("StateScope.endSignal")
- private fun <A> TFlow<A>.truncateToScope(operatorName: String): TFlow<A> =
- if (endSignalOnce === emptyTFlow) {
- this
- } else {
- endSignalOnce.mapCheap { emptyTFlow }.toTStateInternal(operatorName, this).switch()
- }
-
- private fun <A> TFlow<A>.nextOnlyInternal(operatorName: String): TFlow<A> =
- if (this === emptyTFlow) {
- this
- } else {
- TFlowLoop<A>().apply {
- loopback =
- mapCheap { emptyTFlow }
- .toTStateInternal(operatorName, this@nextOnlyInternal)
- .switch()
- }
- }
-
- private fun <A> TFlow<A>.toTStateInternal(operatorName: String, init: A): TState<A> =
- toTStateInternalDeferred(operatorName, CompletableLazy(init))
-
- private fun <A> TFlow<A>.toTStateInternalDeferred(
- operatorName: String,
- init: Lazy<A>,
- ): TState<A> {
- val changes = this@toTStateInternalDeferred
- val name = operatorName
- val impl =
- activatedTStateSource(
- name,
- operatorName,
- evalScope,
- { changes.init.connect(evalScope = this) },
- init,
- )
- return TStateInit(constInit(name, impl))
- }
-
- private fun <R> deferredInternal(block: FrpStateScope.() -> R): FrpDeferredValue<R> =
- FrpDeferredValue(deferAsync { runInStateScope(block) })
+ override fun <A> deferredStateScope(block: StateScope.() -> A): DeferredValue<A> =
+ DeferredValue(deferAsync { block() })
- private fun <A> TFlow<A>.toTStateDeferredInternal(
- initialValue: FrpDeferredValue<A>
- ): TState<A> {
- val operatorName = "toTStateDeferred"
+ override fun <A> Events<A>.holdStateDeferred(initialValue: DeferredValue<A>): State<A> {
+ val operatorName = "holdStateDeferred"
// Ensure state is only collected until the end of this scope
return truncateToScope(operatorName)
- .toTStateInternalDeferred(operatorName, initialValue.unwrapped)
+ .toStateInternalDeferred(operatorName, initialValue.unwrapped)
}
- private fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementallyInternal(
+ override fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally(
name: String?,
- storage: TState<Map<K, TFlow<V>>>,
- ): TFlow<Map<K, V>> {
+ initialEvents: DeferredValue<Map<K, Events<V>>>,
+ ): Events<Map<K, V>> {
+ val storage: State<Map<K, Events<V>>> = foldStateMapIncrementally(initialEvents)
val patches =
mapImpl({ init.connect(this) }) { patch, _ ->
- patch.mapValues { (_, m) -> m.map { flow -> flow.init.connect(this) } }.asIterable()
+ patch
+ .mapValues { (_, m) -> m.map { events -> events.init.connect(this) } }
+ .asIterable()
}
- return TFlowInit(
+ return EventsInit(
constInit(
name,
switchDeferredImpl(
@@ -110,7 +71,7 @@ internal class StateScopeImpl(val evalScope: EvalScope, override val endSignal:
.connect(this)
.getCurrentWithEpoch(this)
.first
- .mapValues { (_, flow) -> flow.init.connect(this) }
+ .mapValues { (_, events) -> events.init.connect(this) }
.asIterable()
},
getPatches = { patches },
@@ -121,15 +82,18 @@ internal class StateScopeImpl(val evalScope: EvalScope, override val endSignal:
)
}
- private fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementallyPromptInternal(
- storage: TState<Map<K, TFlow<V>>>,
+ override fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementallyPromptly(
+ initialEvents: DeferredValue<Map<K, Events<V>>>,
name: String?,
- ): TFlow<Map<K, V>> {
+ ): Events<Map<K, V>> {
+ val storage: State<Map<K, Events<V>>> = foldStateMapIncrementally(initialEvents)
val patches =
mapImpl({ init.connect(this) }) { patch, _ ->
- patch.mapValues { (_, m) -> m.map { flow -> flow.init.connect(this) } }.asIterable()
+ patch
+ .mapValues { (_, m) -> m.map { events -> events.init.connect(this) } }
+ .asIterable()
}
- return TFlowInit(
+ return EventsInit(
constInit(
name,
switchPromptImpl(
@@ -139,7 +103,7 @@ internal class StateScopeImpl(val evalScope: EvalScope, override val endSignal:
.connect(this)
.getCurrentWithEpoch(this)
.first
- .mapValues { (_, flow) -> flow.init.connect(this) }
+ .mapValues { (_, events) -> events.init.connect(this) }
.asIterable()
},
getPatches = { patches },
@@ -150,96 +114,98 @@ internal class StateScopeImpl(val evalScope: EvalScope, override val endSignal:
)
}
- private fun <K, A, B> TFlow<Map<K, Maybe<FrpStateful<A>>>>.applyLatestStatefulForKeyInternal(
- init: FrpDeferredValue<Map<K, FrpStateful<B>>>,
+ override fun <K, A, B> Events<Map<K, Maybe<Stateful<A>>>>.applyLatestStatefulForKey(
+ init: DeferredValue<Map<K, Stateful<B>>>,
numKeys: Int?,
- ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>> {
- val eventsByKey: GroupedTFlow<K, Maybe<FrpStateful<A>>> = groupByKey(numKeys)
+ ): Pair<Events<Map<K, Maybe<A>>>, DeferredValue<Map<K, B>>> {
+ val eventsByKey: GroupedEvents<K, Maybe<Stateful<A>>> = groupByKey(numKeys)
val initOut: Lazy<Map<K, B>> = deferAsync {
init.unwrapped.value.mapValues { (k, stateful) ->
- val newEnd = with(frpScope) { eventsByKey[k] }
+ val newEnd = eventsByKey[k]
val newScope = childStateScope(newEnd)
- newScope.runInStateScope(stateful)
+ newScope.stateful()
}
}
- val changesNode: TFlowImpl<Map<K, Maybe<A>>> =
- mapImpl(
- upstream = { this@applyLatestStatefulForKeyInternal.init.connect(evalScope = this) }
- ) { upstreamMap, _ ->
- upstreamMap.mapValues { (k: K, ma: Maybe<FrpStateful<A>>) ->
+ val changesNode: EventsImpl<Map<K, Maybe<A>>> =
+ mapImpl(upstream = { this@applyLatestStatefulForKey.init.connect(evalScope = this) }) {
+ upstreamMap,
+ _ ->
+ upstreamMap.mapValues { (k: K, ma: Maybe<Stateful<A>>) ->
reenterStateScope(this@StateScopeImpl).run {
ma.map { stateful ->
- val newEnd = with(frpScope) { eventsByKey[k].skipNext() }
+ val newEnd = eventsByKey[k].skipNext()
val newScope = childStateScope(newEnd)
- newScope.runInStateScope(stateful)
+ newScope.stateful()
}
}
}
}
val operatorName = "applyLatestStatefulForKey"
val name = operatorName
- val changes: TFlow<Map<K, Maybe<A>>> = TFlowInit(constInit(name, changesNode.cached()))
- return changes to FrpDeferredValue(initOut)
+ val changes: Events<Map<K, Maybe<A>>> = EventsInit(constInit(name, changesNode.cached()))
+ return changes to DeferredValue(initOut)
}
- private fun <A> TFlow<FrpStateful<A>>.observeStatefulsInternal(): TFlow<A> {
- val operatorName = "observeStatefuls"
+ override fun <A> Events<Stateful<A>>.applyStatefuls(): Events<A> {
+ val operatorName = "applyStatefuls"
val name = operatorName
- return TFlowInit(
+ return EventsInit(
constInit(
name,
- mapImpl(
- upstream = { this@observeStatefulsInternal.init.connect(evalScope = this) }
- ) { stateful, _ ->
- reenterStateScope(outerScope = this@StateScopeImpl)
- .runInStateScope(stateful)
+ mapImpl(upstream = { this@applyStatefuls.init.connect(evalScope = this) }) {
+ stateful,
+ _ ->
+ reenterStateScope(outerScope = this@StateScopeImpl).stateful()
}
.cached(),
)
)
}
- override val frpScope: FrpStateScope = FrpStateScopeImpl()
-
- private inner class FrpStateScopeImpl :
- FrpStateScope, FrpTransactionScope by evalScope.frpScope {
-
- override fun <A> deferredStateScope(block: FrpStateScope.() -> A): FrpDeferredValue<A> =
- deferredInternal(block)
-
- override fun <A> TFlow<A>.holdDeferred(initialValue: FrpDeferredValue<A>): TState<A> =
- toTStateDeferredInternal(initialValue)
+ override fun childStateScope(newEnd: Events<Any>) =
+ StateScopeImpl(evalScope, merge(newEnd, endSignal))
- override fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementally(
- name: String?,
- initialTFlows: FrpDeferredValue<Map<K, TFlow<V>>>,
- ): TFlow<Map<K, V>> {
- val storage: TState<Map<K, TFlow<V>>> = foldMapIncrementally(initialTFlows)
- return mergeIncrementallyInternal(name, storage)
+ private fun <A> Events<A>.truncateToScope(operatorName: String): Events<A> =
+ if (endSignalOnce === emptyEvents) {
+ this
+ } else {
+ endSignalOnce
+ .mapCheap { emptyEvents }
+ .toStateInternal(operatorName, this)
+ .switchEvents()
}
- override fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementallyPromptly(
- initialTFlows: FrpDeferredValue<Map<K, TFlow<V>>>,
- name: String?,
- ): TFlow<Map<K, V>> {
- val storage: TState<Map<K, TFlow<V>>> = foldMapIncrementally(initialTFlows)
- return mergeIncrementallyPromptInternal(storage, name)
+ private fun <A> Events<A>.nextOnlyInternal(operatorName: String): Events<A> =
+ if (this === emptyEvents) {
+ this
+ } else {
+ EventsLoop<A>().apply {
+ loopback =
+ mapCheap { emptyEvents }
+ .toStateInternal(operatorName, this@nextOnlyInternal)
+ .switchEvents()
+ }
}
- override fun <K, A, B> TFlow<Map<K, Maybe<FrpStateful<A>>>>.applyLatestStatefulForKey(
- init: FrpDeferredValue<Map<K, FrpStateful<B>>>,
- numKeys: Int?,
- ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>> =
- applyLatestStatefulForKeyInternal(init, numKeys)
+ private fun <A> Events<A>.toStateInternal(operatorName: String, init: A): State<A> =
+ toStateInternalDeferred(operatorName, CompletableLazy(init))
- override fun <A> TFlow<FrpStateful<A>>.applyStatefuls(): TFlow<A> =
- observeStatefulsInternal()
+ private fun <A> Events<A>.toStateInternalDeferred(
+ operatorName: String,
+ init: Lazy<A>,
+ ): State<A> {
+ val changes = this@toStateInternalDeferred
+ val name = operatorName
+ val impl =
+ activatedStateSource(
+ name,
+ operatorName,
+ evalScope,
+ { changes.init.connect(evalScope = this) },
+ init,
+ )
+ return StateInit(constInit(name, impl))
}
-
- override fun <R> runInStateScope(block: FrpStateScope.() -> R): R = frpScope.block()
-
- override fun childStateScope(newEnd: TFlow<Any>) =
- StateScopeImpl(evalScope, merge(newEnd, endSignal))
}
private fun EvalScope.reenterStateScope(outerScope: StateScopeImpl) =
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 f3303f697fc9..287d8cb136c4 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,21 +1,3 @@
-/*
- * 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.
- */
-
-@file:OptIn(ExperimentalCoroutinesApi::class, ExperimentalFrpApi::class)
-
package com.android.systemui.kairos
import com.android.systemui.kairos.util.Either
@@ -56,11 +38,12 @@ import org.junit.Assert.assertTrue
import org.junit.Assert.fail
import org.junit.Test
+@OptIn(ExperimentalCoroutinesApi::class)
class KairosTests {
@Test
fun basic() = runFrpTest { network ->
- val emitter = network.mutableTFlow<Int>()
+ val emitter = network.mutableEvents<Int>()
var result: Int? = null
activateSpec(network) { emitter.observe { result = it } }
runCurrent()
@@ -71,8 +54,8 @@ class KairosTests {
}
@Test
- fun basicTFlow() = runFrpTest { network ->
- val emitter = network.mutableTFlow<Int>()
+ fun basicEvents() = runFrpTest { network ->
+ val emitter = network.mutableEvents<Int>()
println("starting network")
val result = activateSpecWithResult(network) { emitter.nextDeferred() }
runCurrent()
@@ -85,9 +68,9 @@ class KairosTests {
}
@Test
- fun basicTState() = runFrpTest { network ->
- val emitter = network.mutableTFlow<Int>()
- val result = activateSpecWithResult(network) { emitter.hold(0).stateChanges.nextDeferred() }
+ fun basicState() = runFrpTest { network ->
+ val emitter = network.mutableEvents<Int>()
+ val result = activateSpecWithResult(network) { emitter.holdState(0).changes.nextDeferred() }
runCurrent()
emitter.emit(3)
@@ -111,7 +94,7 @@ class KairosTests {
fun basicTransactional() = runFrpTest { network ->
var value: Int? = null
var bSource = 1
- val emitter = network.mutableTFlow<Unit>()
+ val emitter = network.mutableEvents<Unit>()
// Sampling this transactional will increment the source count.
val transactional = transactionally { bSource++ }
measureTime {
@@ -150,24 +133,26 @@ class KairosTests {
@Test
fun diamondGraph() = runFrpTest { network ->
- val flow = network.mutableTFlow<Int>()
- val outFlow =
+ val flow = network.mutableEvents<Int>()
+ val ouevents =
activateSpecWithResult(network) {
- // map TFlow like we map Flow
+ // map Events like we map Flow
val left = flow.map { "left" to it }.onEach { println("left: $it") }
val right = flow.map { "right" to it }.onEach { println("right: $it") }
- // convert TFlows to TStates so that they can be combined
+ // convert Eventss to States so that they can be combined
val combined =
- left.hold("left" to 0).combineWith(right.hold("right" to 0)) { l, r -> l to r }
- combined.stateChanges // get TState changes
+ left.holdState("left" to 0).combineWith(right.holdState("right" to 0)) { l, r ->
+ l to r
+ }
+ combined.changes // get State changes
.onEach { println("merged: $it") }
.toSharedFlow() // convert back to Flow
}
runCurrent()
val results = mutableListOf<Pair<Pair<String, Int>, Pair<String, Int>>>()
- backgroundScope.launch { outFlow.toCollection(results) }
+ backgroundScope.launch { ouevents.toCollection(results) }
runCurrent()
flow.emit(1)
@@ -186,19 +171,19 @@ class KairosTests {
fun staticNetwork() = runFrpTest { network ->
var finalSum: Int? = null
- val intEmitter = network.mutableTFlow<Int>()
- val sampleEmitter = network.mutableTFlow<Unit>()
+ val intEmitter = network.mutableEvents<Int>()
+ val sampleEmitter = network.mutableEvents<Unit>()
activateSpecWithResult(network) {
val updates = intEmitter.map { a -> { b: Int -> a + b } }
val sumD =
- TStateLoop<Int>().apply {
+ StateLoop<Int>().apply {
loopback =
updates
.sample(this) { f, sum -> f(sum) }
.onEach { println("sum update: $it") }
- .hold(0)
+ .holdState(0)
}
sampleEmitter
.onEach { println("sampleEmitter emitted") }
@@ -228,10 +213,10 @@ class KairosTests {
var wasSold = false
var currentAmt: Int? = null
- val coin = network.mutableTFlow<Unit>()
+ val coin = network.mutableEvents<Unit>()
val price = 50
- val frpSpec = frpSpec {
- val eSold = TFlowLoop<Unit>()
+ val buildSpec = buildSpec {
+ val eSold = EventsLoop<Unit>()
val eInsert =
coin.map {
@@ -251,10 +236,10 @@ class KairosTests {
val eUpdate = eInsert.mergeWith(eReset) { f, g -> { a -> g(f(a)) } }
- val dTotal = TStateLoop<Int>()
- dTotal.loopback = eUpdate.sample(dTotal) { f, total -> f(total) }.hold(price)
+ val dTotal = StateLoop<Int>()
+ dTotal.loopback = eUpdate.sample(dTotal) { f, total -> f(total) }.holdState(price)
- val eAmt = dTotal.stateChanges
+ val eAmt = dTotal.changes
val bAmt = transactionally { dTotal.sample() }
eSold.loopback =
coin
@@ -269,7 +254,7 @@ class KairosTests {
eSold.nextDeferred()
}
- activateSpec(network) { frpSpec.applySpec() }
+ activateSpec(network) { buildSpec.applySpec() }
runCurrent()
@@ -313,8 +298,8 @@ class KairosTests {
@Test
fun promptCleanup() = runFrpTest { network ->
- val emitter = network.mutableTFlow<Int>()
- val stopper = network.mutableTFlow<Unit>()
+ val emitter = network.mutableEvents<Int>()
+ val stopper = network.mutableEvents<Unit>()
var result: Int? = null
@@ -332,19 +317,19 @@ class KairosTests {
}
@Test
- fun switchTFlow() = runFrpTest { network ->
+ fun switchEvents() = runFrpTest { network ->
var currentSum: Int? = null
- val switchHandler = network.mutableTFlow<Pair<TFlow<Int>, String>>()
- val aHandler = network.mutableTFlow<Int>()
- val stopHandler = network.mutableTFlow<Unit>()
- val bHandler = network.mutableTFlow<Int>()
+ val switchHandler = network.mutableEvents<Pair<Events<Int>, String>>()
+ val aHandler = network.mutableEvents<Int>()
+ val stopHandler = network.mutableEvents<Unit>()
+ val bHandler = network.mutableEvents<Int>()
val sumFlow =
activateSpecWithResult(network) {
- val switchE = TFlowLoop<TFlow<Int>>()
+ val switchE = EventsLoop<Events<Int>>()
switchE.loopback =
- switchHandler.mapStateful { (intFlow, name) ->
+ switchHandler.mapStateful { (inevents, name) ->
println("[onEach] Switching to: $name")
val nextSwitch =
switchE.skipNext().onEach { println("[onEach] switched-out") }
@@ -352,11 +337,11 @@ class KairosTests {
stopHandler
.onEach { println("[onEach] stopped") }
.mergeWith(nextSwitch) { _, b -> b }
- intFlow.takeUntil(stopEvent)
+ inevents.takeUntil(stopEvent)
}
- val adderE: TFlow<(Int) -> Int> =
- switchE.hold(emptyTFlow).switch().map { a ->
+ val adderE: Events<(Int) -> Int> =
+ switchE.holdState(emptyEvents).switchEvents().map { a ->
println("[onEach] new number $a")
({ sum: Int ->
println("$a+$sum=${a + sum}")
@@ -364,13 +349,13 @@ class KairosTests {
})
}
- val sumD = TStateLoop<Int>()
+ val sumD = StateLoop<Int>()
sumD.loopback =
adderE
.sample(sumD) { f, sum -> f(sum) }
.onEach { println("[onEach] writing sum: $it") }
- .hold(0)
- val sumE = sumD.stateChanges
+ .holdState(0)
+ val sumE = sumD.changes
sumE.toSharedFlow()
}
@@ -494,16 +479,16 @@ class KairosTests {
@Test
fun switchIndirect() = runFrpTest { network ->
- val emitter = network.mutableTFlow<Unit>()
+ val emitter = network.mutableEvents<Unit>()
activateSpec(network) {
- emptyTFlow.map { emitter.map { 1 } }.flatten().map { "$it" }.observe()
+ emptyEvents.map { emitter.map { 1 } }.flatten().map { "$it" }.observe()
}
runCurrent()
}
@Test
fun switchInWithResult() = runFrpTest { network ->
- val emitter = network.mutableTFlow<Unit>()
+ val emitter = network.mutableEvents<Unit>()
val out =
activateSpecWithResult(network) {
emitter.map { emitter.map { 1 } }.flatten().toSharedFlow()
@@ -519,11 +504,11 @@ class KairosTests {
fun switchInCompleted() = runFrpTest { network ->
val outputs = mutableListOf<Int>()
- val switchAH = network.mutableTFlow<Unit>()
- val intAH = network.mutableTFlow<Int>()
- val stopEmitter = network.mutableTFlow<Unit>()
+ val switchAH = network.mutableEvents<Unit>()
+ val intAH = network.mutableEvents<Int>()
+ val stopEmitter = network.mutableEvents<Unit>()
- val top = frpSpec {
+ val top = buildSpec {
val intS = intAH.takeUntil(stopEmitter)
val switched = switchAH.map { intS }.flatten()
switched.toSharedFlow()
@@ -555,13 +540,13 @@ class KairosTests {
}
@Test
- fun switchTFlow_outerCompletesFirst() = runFrpTest { network ->
+ fun switchEvents_outerCompletesFirst() = runFrpTest { network ->
var stepResult: Int? = null
- val switchAH = network.mutableTFlow<Unit>()
- val switchStopEmitter = network.mutableTFlow<Unit>()
- val intStopEmitter = network.mutableTFlow<Unit>()
- val intAH = network.mutableTFlow<Int>()
+ val switchAH = network.mutableEvents<Unit>()
+ val switchStopEmitter = network.mutableEvents<Unit>()
+ val intStopEmitter = network.mutableEvents<Unit>()
+ val intAH = network.mutableEvents<Int>()
val flow =
activateSpecWithResult(network) {
val intS = intAH.takeUntil(intStopEmitter)
@@ -609,8 +594,8 @@ class KairosTests {
}
@Test
- fun mapTFlow() = runFrpTest { network ->
- val emitter = network.mutableTFlow<Int>()
+ fun mapEvents() = runFrpTest { network ->
+ val emitter = network.mutableEvents<Int>()
var stepResult: Int? = null
val flow =
@@ -644,7 +629,7 @@ class KairosTests {
var pullValue = 0
val a = transactionally { pullValue }
val b = transactionally { a.sample() * 2 }
- val emitter = network.mutableTFlow<Unit>()
+ val emitter = network.mutableEvents<Unit>()
val flow =
activateSpecWithResult(network) {
val sampleB = emitter.sample(b) { _, b -> b }
@@ -668,14 +653,14 @@ class KairosTests {
}
@Test
- fun mapTState() = runFrpTest { network ->
- val emitter = network.mutableTFlow<Int>()
+ fun mapState() = runFrpTest { network ->
+ val emitter = network.mutableEvents<Int>()
var stepResult: Int? = null
val flow =
activateSpecWithResult(network) {
- val state = emitter.hold(0).map { it + 2 }
+ val state = emitter.holdState(0).map { it + 2 }
val stateCurrent = transactionally { state.sample() }
- val stateChanges = state.stateChanges
+ val stateChanges = state.changes
val sampleState = emitter.sample(stateCurrent) { _, b -> b }
val merge = stateChanges.mergeWith(sampleState) { a, b -> a + b }
merge.toSharedFlow()
@@ -696,14 +681,14 @@ class KairosTests {
@Test
fun partitionEither() = runFrpTest { network ->
- val emitter = network.mutableTFlow<Either<Int, Int>>()
+ val emitter = network.mutableEvents<Either<Int, Int>>()
val result =
activateSpecWithResult(network) {
val (l, r) = emitter.partitionEither()
val pDiamond =
l.map { it * 2 }
.mergeWith(r.map { it * -1 }) { _, _ -> error("unexpected coincidence") }
- pDiamond.hold(null).toStateFlow()
+ pDiamond.holdState(null).toStateFlow()
}
runCurrent()
@@ -719,15 +704,16 @@ class KairosTests {
}
@Test
- fun accumTState() = runFrpTest { network ->
- val emitter = network.mutableTFlow<Int>()
- val sampler = network.mutableTFlow<Unit>()
+ fun accumState() = runFrpTest { network ->
+ val emitter = network.mutableEvents<Int>()
+ val sampler = network.mutableEvents<Unit>()
var stepResult: Int? = null
val flow =
activateSpecWithResult(network) {
- val sumState = emitter.map { a -> { b: Int -> a + b } }.fold(0) { f, a -> f(a) }
+ val sumState =
+ emitter.map { a -> { b: Int -> a + b } }.foldState(0) { f, a -> f(a) }
- sumState.stateChanges
+ sumState.changes
.mergeWith(sampler.sample(sumState) { _, sum -> sum }) { _, _ ->
error("Unexpected coincidence")
}
@@ -751,11 +737,11 @@ class KairosTests {
}
@Test
- fun mergeTFlows() = runFrpTest { network ->
- val first = network.mutableTFlow<Int>()
- val stopFirst = network.mutableTFlow<Unit>()
- val second = network.mutableTFlow<Int>()
- val stopSecond = network.mutableTFlow<Unit>()
+ fun mergeEventss() = runFrpTest { network ->
+ val first = network.mutableEvents<Int>()
+ val stopFirst = network.mutableEvents<Unit>()
+ val second = network.mutableEvents<Int>()
+ val stopSecond = network.mutableEvents<Unit>()
var stepResult: Int? = null
val flow: SharedFlow<Int>
@@ -832,19 +818,19 @@ class KairosTests {
secondEmitDuration: ${secondEmitDuration.toString(DurationUnit.MILLISECONDS, 2)}
stopFirstDuration: ${stopFirstDuration.toString(DurationUnit.MILLISECONDS, 2)}
testDeadEmitFirstDuration: ${
- testDeadEmitFirstDuration.toString(
- DurationUnit.MILLISECONDS,
- 2,
- )
- }
+ testDeadEmitFirstDuration.toString(
+ DurationUnit.MILLISECONDS,
+ 2,
+ )
+ }
secondEmitDuration2: ${secondEmitDuration2.toString(DurationUnit.MILLISECONDS, 2)}
stopSecondDuration: ${stopSecondDuration.toString(DurationUnit.MILLISECONDS, 2)}
testDeadEmitSecondDuration: ${
- testDeadEmitSecondDuration.toString(
- DurationUnit.MILLISECONDS,
- 2,
- )
- }
+ testDeadEmitSecondDuration.toString(
+ DurationUnit.MILLISECONDS,
+ 2,
+ )
+ }
"""
.trimIndent()
)
@@ -852,10 +838,10 @@ class KairosTests {
@Test
fun sampleCancel() = runFrpTest { network ->
- val updater = network.mutableTFlow<Int>()
- val stopUpdater = network.mutableTFlow<Unit>()
- val sampler = network.mutableTFlow<Unit>()
- val stopSampler = network.mutableTFlow<Unit>()
+ val updater = network.mutableEvents<Int>()
+ val stopUpdater = network.mutableEvents<Unit>()
+ val sampler = network.mutableEvents<Unit>()
+ val stopSampler = network.mutableEvents<Unit>()
var stepResult: Int? = null
val flow =
activateSpecWithResult(network) {
@@ -863,7 +849,7 @@ class KairosTests {
val samplerS = sampler.takeUntil(stopSamplerFirst)
val stopUpdaterFirst = stopUpdater
val updaterS = updater.takeUntil(stopUpdaterFirst)
- val sampledS = samplerS.sample(updaterS.hold(0)) { _, b -> b }
+ val sampledS = samplerS.sample(updaterS.holdState(0)) { _, b -> b }
sampledS.toSharedFlow()
}
@@ -894,35 +880,35 @@ class KairosTests {
@Test
fun combineStates_differentUpstreams() = runFrpTest { network ->
- val a = network.mutableTFlow<Int>()
- val b = network.mutableTFlow<Int>()
+ val a = network.mutableEvents<Int>()
+ val b = network.mutableEvents<Int>()
var observed: Pair<Int, Int>? = null
- val tState =
+ val state =
activateSpecWithResult(network) {
- val state = combine(a.hold(0), b.hold(0)) { a, b -> Pair(a, b) }
- state.stateChanges.observe { observed = it }
+ val state = combine(a.holdState(0), b.holdState(0)) { a, b -> Pair(a, b) }
+ state.changes.observe { observed = it }
state
}
- assertEquals(0 to 0, network.transact { tState.sample() })
+ assertEquals(0 to 0, network.transact { state.sample() })
assertEquals(null, observed)
a.emit(5)
assertEquals(5 to 0, observed)
- assertEquals(5 to 0, network.transact { tState.sample() })
+ assertEquals(5 to 0, network.transact { state.sample() })
b.emit(3)
assertEquals(5 to 3, observed)
- assertEquals(5 to 3, network.transact { tState.sample() })
+ assertEquals(5 to 3, network.transact { state.sample() })
}
@Test
fun sampleCombinedStates() = runFrpTest { network ->
- val updater = network.mutableTFlow<Int>()
- val emitter = network.mutableTFlow<Unit>()
+ val updater = network.mutableEvents<Int>()
+ val emitter = network.mutableEvents<Unit>()
val result =
activateSpecWithResult(network) {
- val bA = updater.map { it * 2 }.hold(0)
- val bB = updater.hold(0)
- val combineD: TState<Pair<Int, Int>> = bA.combineWith(bB) { a, b -> a to b }
+ 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 sampleS = emitter.sample(combineD) { _, b -> b }
sampleS.nextDeferred()
}
@@ -943,13 +929,13 @@ class KairosTests {
@Test
fun switchMapPromptly() = runFrpTest { network ->
- val emitter = network.mutableTFlow<Unit>()
+ val emitter = network.mutableEvents<Unit>()
val result =
activateSpecWithResult(network) {
emitter
.map { emitter.map { 1 }.map { it + 1 }.map { it * 2 } }
- .hold(emptyTFlow)
- .switchPromptly()
+ .holdState(emptyEvents)
+ .switchEventsPromptly()
.nextDeferred()
}
runCurrent()
@@ -963,8 +949,8 @@ class KairosTests {
@Test
fun switchDeeper() = runFrpTest { network ->
- val emitter = network.mutableTFlow<Unit>()
- val e2 = network.mutableTFlow<Unit>()
+ val emitter = network.mutableEvents<Unit>()
+ val e2 = network.mutableEvents<Unit>()
val result =
activateSpecWithResult(network) {
val tres =
@@ -989,14 +975,14 @@ class KairosTests {
@Test
fun recursionBasic() = runFrpTest { network ->
- val add1 = network.mutableTFlow<Unit>()
- val sub1 = network.mutableTFlow<Unit>()
+ val add1 = network.mutableEvents<Unit>()
+ val sub1 = network.mutableEvents<Unit>()
val stepResult: StateFlow<Int> =
activateSpecWithResult(network) {
- val dSum = TStateLoop<Int>()
+ val dSum = StateLoop<Int>()
val sAdd1 = add1.sample(dSum) { _, sum -> sum + 1 }
val sMinus1 = sub1.sample(dSum) { _, sum -> sum - 1 }
- dSum.loopback = sAdd1.mergeWith(sMinus1) { a, _ -> a }.hold(0)
+ dSum.loopback = sAdd1.mergeWith(sMinus1) { a, _ -> a }.holdState(0)
dSum.toStateFlow()
}
runCurrent()
@@ -1018,16 +1004,17 @@ class KairosTests {
}
@Test
- fun recursiveTState() = runFrpTest { network ->
- val e = network.mutableTFlow<Unit>()
+ fun recursiveState() = runFrpTest { network ->
+ val e = network.mutableEvents<Unit>()
var changes = 0
val state =
activateSpecWithResult(network) {
- val s = TFlowLoop<Unit>()
- val deferred = s.map { tStateOf(null) }
- val e3 = e.map { tStateOf(Unit) }
- val flattened = e3.mergeWith(deferred) { a, _ -> a }.hold(tStateOf(null)).flatten()
- s.loopback = emptyTFlow
+ val s = EventsLoop<Unit>()
+ val deferred = s.map { stateOf(null) }
+ val e3 = e.map { stateOf(Unit) }
+ val flattened =
+ e3.mergeWith(deferred) { a, _ -> a }.holdState(stateOf(null)).flatten()
+ s.loopback = emptyEvents
flattened.toStateFlow()
}
@@ -1037,7 +1024,7 @@ class KairosTests {
@Test
fun fanOut() = runFrpTest { network ->
- val e = network.mutableTFlow<Map<String, Int>>()
+ val e = network.mutableEvents<Map<String, Int>>()
val (fooFlow, barFlow) =
activateSpecWithResult(network) {
val selector = e.groupByKey()
@@ -1073,15 +1060,15 @@ class KairosTests {
@Test
fun fanOutLateSubscribe() = runFrpTest { network ->
- val e = network.mutableTFlow<Map<String, Int>>()
+ val e = network.mutableEvents<Map<String, Int>>()
val barFlow =
activateSpecWithResult(network) {
val selector = e.groupByKey()
selector
.eventsForKey("foo")
.map { selector.eventsForKey("bar") }
- .hold(emptyTFlow)
- .switchPromptly()
+ .holdState(emptyEvents)
+ .switchEventsPromptly()
.toSharedFlow()
}
val stateFlow = barFlow.stateIn(backgroundScope, SharingStarted.Eagerly, null)
@@ -1096,9 +1083,9 @@ class KairosTests {
}
@Test
- fun inputFlowCompleted() = runFrpTest { network ->
+ fun inpueventsCompleted() = runFrpTest { network ->
val results = mutableListOf<Int>()
- val e = network.mutableTFlow<Int>()
+ val e = network.mutableEvents<Int>()
activateSpec(network) { e.nextOnly().observe { results.add(it) } }
runCurrent()
@@ -1114,49 +1101,59 @@ class KairosTests {
@Test
fun fanOutThenMergeIncrementally() = runFrpTest { network ->
- // A tflow of group updates, where a group is a tflow of child updates, where a child is a
+ // A events of group updates, where a group is a events of child updates, where a child is a
// stateflow
- val e = network.mutableTFlow<Map<Int, Maybe<TFlow<Map<Int, Maybe<StateFlow<String>>>>>>>()
+ val e = network.mutableEvents<Map<Int, Maybe<Events<Map<Int, Maybe<StateFlow<String>>>>>>>()
println("fanOutMergeInc START")
val state =
activateSpecWithResult(network) {
- // Convert nested Flows to nested TFlow/TState
- val emitter: TFlow<Map<Int, Maybe<TFlow<Map<Int, Maybe<TState<String>>>>>>> =
+ // Convert nested Flows to nested Events/State
+ val emitter: Events<Map<Int, Maybe<Events<Map<Int, Maybe<State<String>>>>>>> =
e.mapBuild { m ->
m.mapValues { (_, mFlow) ->
mFlow.map {
it.mapBuild { m2 ->
+ println("m2: $m2")
m2.mapValues { (_, mState) ->
- mState.map { stateFlow -> stateFlow.toTState() }
+ mState.map { stateFlow -> stateFlow.toState() }
}
}
}
}
}
- // Accumulate all of our updates into a single TState
- val accState: TState<Map<Int, Map<Int, String>>> =
+ // Accumulate all of our updates into a single State
+ val accState: State<Map<Int, Map<Int, String>>> =
emitter
.mapStateful {
- changeMap: Map<Int, Maybe<TFlow<Map<Int, Maybe<TState<String>>>>>> ->
+ changeMap: Map<Int, Maybe<Events<Map<Int, Maybe<State<String>>>>>> ->
changeMap.mapValues { (groupId, mGroupChanges) ->
mGroupChanges.map {
- groupChanges: TFlow<Map<Int, Maybe<TState<String>>>> ->
+ groupChanges: Events<Map<Int, Maybe<State<String>>>> ->
// New group
val childChangeById = groupChanges.groupByKey()
- val map: TFlow<Map<Int, Maybe<TFlow<Maybe<TState<String>>>>>> =
+ val map: Events<Map<Int, Maybe<Events<Maybe<State<String>>>>>> =
groupChanges.mapStateful {
- gChangeMap: Map<Int, Maybe<TState<String>>> ->
+ gChangeMap: Map<Int, Maybe<State<String>>> ->
+ println("gChangeMap: $gChangeMap")
gChangeMap.mapValues { (childId, mChild) ->
- mChild.map { child: TState<String> ->
+ mChild.map { child: State<String> ->
println("new child $childId in the house")
// New child
val eRemoved =
childChangeById
.eventsForKey(childId)
.filter { it === None }
- .nextOnly()
+ .onEach {
+ println(
+ "removing? (groupId=$groupId, childId=$childId)"
+ )
+ }
+ .nextOnly(
+ name =
+ "eRemoved(groupId=$groupId, childId=$childId)"
+ )
- val addChild: TFlow<Maybe<TState<String>>> =
+ val addChild: Events<Maybe<State<String>>> =
now.map { mChild }
.onEach {
println(
@@ -1164,7 +1161,7 @@ class KairosTests {
)
}
- val removeChild: TFlow<Maybe<TState<String>>> =
+ val removeChild: Events<Maybe<State<String>>> =
eRemoved
.onEach {
println(
@@ -1173,23 +1170,28 @@ class KairosTests {
}
.map { none() }
- addChild.mergeWith(removeChild) { _, _ ->
+ addChild.mergeWith(
+ removeChild,
+ name =
+ "childUpdatesMerged(groupId=$groupId, childId=$childId)",
+ ) { _, _ ->
error("unexpected coincidence")
}
}
}
}
- val mergeIncrementally: TFlow<Map<Int, Maybe<TState<String>>>> =
+ val mergeIncrementally: Events<Map<Int, Maybe<State<String>>>> =
map.onEach { println("merge patch: $it") }
- .mergeIncrementallyPromptly()
+ .mergeIncrementallyPromptly(name = "mergeIncrementally")
mergeIncrementally
- .onEach { println("patch: $it") }
- .foldMapIncrementally()
+ .onEach { println("foldmap patch: $it") }
+ .foldStateMapIncrementally()
.flatMap { it.combine() }
}
}
}
- .foldMapIncrementally()
+ .onEach { println("fold patch: $it") }
+ .foldStateMapIncrementally()
.flatMap { it.combine() }
accState.toStateFlow()
@@ -1198,7 +1200,7 @@ class KairosTests {
assertEquals(emptyMap(), state.value)
- val emitter2 = network.mutableTFlow<Map<Int, Maybe<StateFlow<String>>>>()
+ 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") })))
@@ -1233,6 +1235,10 @@ class KairosTests {
assertEquals(mapOf(0 to mapOf(10 to "(2, 10)")), state.value)
+ // LogEnabled = true
+
+ println("batch update")
+
// batch update
emitter2.emit(
mapOf(
@@ -1248,13 +1254,13 @@ class KairosTests {
@Test
fun applyLatestNetworkChanges() = runFrpTest { network ->
- val newCount = network.mutableTFlow<FrpSpec<Flow<Int>>>()
+ val newCount = network.mutableEvents<BuildSpec<Flow<Int>>>()
val flowOfFlows: Flow<Flow<Int>> =
activateSpecWithResult(network) { newCount.applyLatestSpec().toSharedFlow() }
runCurrent()
- val incCount = network.mutableTFlow<Unit>()
- fun newFlow(): FrpSpec<SharedFlow<Int>> = frpSpec {
+ val incCount = network.mutableEvents<Unit>()
+ fun newFlow(): BuildSpec<SharedFlow<Int>> = buildSpec {
launchEffect {
try {
println("new flow!")
@@ -1263,16 +1269,16 @@ class KairosTests {
println("cancelling old flow")
}
}
- lateinit var count: TState<Int>
+ lateinit var count: State<Int>
count =
incCount
.onEach { println("incrementing ${count.sample()}") }
- .fold(0) { _, c -> c + 1 }
- count.stateChanges.toSharedFlow()
+ .foldState(0) { _, c -> c + 1 }
+ count.changes.toSharedFlow()
}
var outerCount = 0
- val lastFlows: StateFlow<Pair<StateFlow<Int?>, StateFlow<Int?>>> =
+ val laseventss: StateFlow<Pair<StateFlow<Int?>, StateFlow<Int?>>> =
flowOfFlows
.map { it.stateIn(backgroundScope, SharingStarted.Eagerly, null) }
.pairwise(MutableStateFlow(null))
@@ -1290,18 +1296,18 @@ class KairosTests {
assertEquals(1, outerCount)
// assertEquals(1, incCount.subscriptionCount)
- assertNull(lastFlows.value.second.value)
+ assertNull(laseventss.value.second.value)
incCount.emit(Unit)
runCurrent()
println("checking")
- assertEquals(1, lastFlows.value.second.value)
+ assertEquals(1, laseventss.value.second.value)
incCount.emit(Unit)
runCurrent()
- assertEquals(2, lastFlows.value.second.value)
+ assertEquals(2, laseventss.value.second.value)
newCount.emit(newFlow())
runCurrent()
@@ -1309,17 +1315,17 @@ class KairosTests {
runCurrent()
// verify old flow is not getting updates
- assertEquals(2, lastFlows.value.first.value)
+ assertEquals(2, laseventss.value.first.value)
// but the new one is
- assertEquals(1, lastFlows.value.second.value)
+ assertEquals(1, laseventss.value.second.value)
}
@Test
fun buildScope_stateAccumulation() = runFrpTest { network ->
- val input = network.mutableTFlow<Unit>()
+ val input = network.mutableEvents<Unit>()
var observedCount: Int? = null
activateSpec(network) {
- val (c, j) = asyncScope { input.fold(0) { _, x -> x + 1 } }
+ val (c, j) = asyncScope { input.foldState(0) { _, x -> x + 1 } }
deferredBuildScopeAction { c.get().observe { observedCount = it } }
}
runCurrent()
@@ -1336,7 +1342,7 @@ class KairosTests {
@Test
fun effect() = runFrpTest { network ->
- val input = network.mutableTFlow<Unit>()
+ val input = network.mutableEvents<Unit>()
var effectRunning = false
var count = 0
activateSpec(network) {
@@ -1348,7 +1354,7 @@ class KairosTests {
effectRunning = false
}
}
- merge(emptyTFlow, input.nextOnly()).observe {
+ merge(emptyEvents, input.nextOnly()).observe {
count++
j.cancel()
}
@@ -1372,21 +1378,21 @@ class KairosTests {
private fun runFrpTest(
timeout: Duration = 3.seconds,
- block: suspend TestScope.(FrpNetwork) -> Unit,
+ block: suspend TestScope.(KairosNetwork) -> Unit,
) {
runTest(timeout = timeout) {
- val network = backgroundScope.newFrpNetwork()
+ val network = backgroundScope.launchKairosNetwork()
runCurrent()
block(network)
}
}
- private fun TestScope.activateSpec(network: FrpNetwork, spec: FrpSpec<*>) =
+ private fun TestScope.activateSpec(network: KairosNetwork, spec: BuildSpec<*>) =
backgroundScope.launch { network.activateSpec(spec) }
private suspend fun <R> TestScope.activateSpecWithResult(
- network: FrpNetwork,
- spec: FrpSpec<R>,
+ network: KairosNetwork,
+ spec: BuildSpec<R>,
): R =
CompletableDeferred<R>()
.apply { activateSpec(network) { complete(spec.applySpec()) } }