summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Treehugger Robot <android-test-infra-autosubmit@system.gserviceaccount.com> 2025-01-03 14:42:16 -0800
committer Android (Google) Code Review <android-gerrit@google.com> 2025-01-03 14:42:16 -0800
commit5955304a6bfe2537dd04e82b450c22dca62fe20f (patch)
tree461e9c67029b71833898ca54d6e8630b31421885
parent67c7f485a9a0369b054a15a6b060894bf82bb105 (diff)
parentcdff97f1f5b958a8d0a3977b3d62dcd12a89b49e (diff)
Merge changes from topic "kairos-rename" into main
* changes: [kairos] rename many APIs [kairos] remove most internal usage of `suspend fun`
-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.kt885
-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.kt184
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpScope.kt73
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpStateScope.kt780
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpTransactionScope.kt65
-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.kt563
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TState.kt545
-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.kt71
-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.kt312
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/DeferScope.kt77
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Demux.kt270
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/EvalScopeImpl.kt106
-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)27
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/FilterNode.kt15
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Graph.kt176
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Init.kt23
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Inputs.kt95
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/InternalScopes.kt46
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Mux.kt331
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxDeferred.kt454
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxPrompt.kt444
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Network.kt228
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/NoScope.kt30
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/NodeTypes.kt28
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Output.kt15
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/PullNodes.kt54
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Scheduler.kt90
-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)269
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateScopeImpl.kt282
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/TransactionalImpl.kt22
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/HeteroMap.kt9
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/MapUtils.kt6
-rw-r--r--packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/Util.kt61
-rw-r--r--packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosTests.kt370
49 files changed, 5322 insertions, 5513 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 ae9b8c85910f..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: suspend 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: suspend 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: suspend 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.
*/
- suspend 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.
*/
- suspend 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 209a402bd629..000000000000
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpBuildScope.kt
+++ /dev/null
@@ -1,885 +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> = suspend 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: suspend 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 */
- @ExperimentalFrpApi
- fun <R> deferredBuildScope(block: suspend FrpBuildScope.() -> R): FrpDeferredValue<R>
-
- /** TODO: Javadoc */
- @ExperimentalFrpApi fun deferredBuildScopeAction(block: suspend 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: suspend 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: suspend 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(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: suspend 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: suspend 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: suspend 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: suspend 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: suspend 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: suspend 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: suspend 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: suspend FrpBuildScope.(A) -> B,
- ): Pair<TFlow<Map<K, Maybe<B>>>, FrpDeferredValue<Map<K, B>>> =
- map { patch -> patch.mapValues { (_, v) -> v.map { frpSpec { transform(it) } } } }
- .applyLatestSpecForKey(
- deferredBuildScope {
- initialValues.get().mapValues { (_, v) -> frpSpec { 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 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: suspend FrpBuildScope.(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: suspend FrpBuildScope.(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(): TFlow<A> = tFlow { 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: suspend 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: suspend 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: suspend 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: suspend 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 suspend 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: suspend 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: suspend 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(block: suspend FrpEffectScope.() -> Unit): Job = now.observe { 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 cec76886c06d..000000000000
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpNetwork.kt
+++ /dev/null
@@ -1,184 +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 kotlin.coroutines.coroutineContext
-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: suspend 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: suspend FrpTransactionScope.() -> R): R =
- network.transaction("FrpNetwork.transact") { runInTransactionScope { block() } }.await()
-
- override suspend fun activateSpec(spec: FrpSpec<*>) {
- val job =
- network
- .transaction("FrpNetwork.activateSpec") {
- val buildScope =
- BuildScopeImpl(
- stateScope = StateScopeImpl(evalScope = this, endSignal = endSignal),
- coroutineScope = scope,
- )
- buildScope.runInBuildScope { launchScope(spec) }
- }
- .await()
- awaitCancellationAndThen { 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 ad6b2c8d04eb..000000000000
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpScope.kt
+++ /dev/null
@@ -1,73 +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 kotlin.coroutines.resume
-import kotlin.coroutines.resumeWithException
-import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.Deferred
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.suspendCancellableCoroutine
-
-/** 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
- @OptIn(ExperimentalCoroutinesApi::class)
- suspend fun <A> FrpDeferredValue<A>.get(): A = suspendCancellableCoroutine { k ->
- unwrapped.invokeOnCompletion { ex ->
- ex?.let { k.resumeWithException(ex) } ?: k.resume(unwrapped.getCompleted())
- }
- }
-}
-
-/**
- * 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: Deferred<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
-@OptIn(ExperimentalCoroutinesApi::class)
-fun <A> FrpDeferredValue<A>.getUnsafe(): A = unwrapped.getCompleted()
-
-/** Returns an already-available [FrpDeferredValue] containing [value]. */
-@ExperimentalFrpApi
-fun <A> deferredOf(value: A): FrpDeferredValue<A> = FrpDeferredValue(CompletableDeferred(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 058fc1037e58..000000000000
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpStateScope.kt
+++ /dev/null
@@ -1,780 +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> = suspend 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: suspend 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: suspend 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(
- 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>>>
- ): 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(
- initialTFlows: Map<K, TFlow<V>> = emptyMap()
- ): TFlow<Map<K, V>> = mergeIncrementally(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()
- ): TFlow<Map<K, V>> = mergeIncrementallyPromptly(deferredOf(initialTFlows))
-
- /** Applies the [FrpStateful] within this [FrpStateScope]. */
- @ExperimentalFrpApi suspend 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: suspend 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: suspend 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: suspend 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: suspend 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: suspend 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: suspend 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(): TFlow<A> =
- if (this === emptyTFlow) {
- this
- } else {
- TFlowLoop<A>().also {
- it.loopback = it.mapCheap { emptyTFlow }.hold(this@nextOnly).switch()
- }
- }
-
- /** 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: suspend 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: suspend 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: suspend 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: suspend 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: suspend 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: suspend 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: suspend 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.combineWith
- */
- @ExperimentalFrpApi
- fun <A, B, C, D, Z> combine(
- stateA: TState<A>,
- stateB: TState<B>,
- stateC: TState<C>,
- stateD: TState<D>,
- transform: suspend 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: suspend 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: suspend 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: suspend 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: suspend 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 a7ae1d9646b3..000000000000
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpTransactionScope.kt
+++ /dev/null
@@ -1,65 +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: suspend 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 suspend 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 suspend 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 362a890f44e2..000000000000
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TFlow.kt
+++ /dev/null
@@ -1,563 +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.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.CompletableDeferred
-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 = CompletableDeferred<TFlow<A>>()
-
- internal val init: Init<TFlowImpl<A>> =
- init(name = null) { deferred.await().init.connect(evalScope = this) }
-
- /** The [TFlow] this reference is referring to. */
- @ExperimentalFrpApi
- var loopback: TFlow<A>? = null
- set(value) {
- value?.let {
- check(deferred.complete(value)) { "TFlowLoop.loopback has already been set." }
- 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.await() }
-
-/** TODO */
-@ExperimentalFrpApi
-fun <A> deferTFlow(block: suspend 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: suspend 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: suspend 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: suspend 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: suspend 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: suspend 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: suspend 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>,
- transformCoincidence: suspend FrpTransactionScope.(A, A) -> A = { a, _ -> a },
-): TFlow<A> {
- val node =
- mergeNodes(
- 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: suspend 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: suspend 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(): TFlow<A> =
- TFlowInit(
- constInit(
- name = null,
- switchDeferredImplSingle(
- getStorage = {
- init.connect(this).getCurrentWithEpoch(this).first.init.connect(this)
- },
- getPatches = {
- mapImpl({ init.connect(this).changes }) { newFlow ->
- newFlow.init.connect(this)
- }
- },
- ),
- )
- )
-
-/**
- * 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> =
- TFlowInit(
- constInit(
- name = null,
- switchPromptImplSingle(
- getStorage = {
- init.connect(this).getCurrentWithEpoch(this).first.init.connect(this)
- },
- getPatches = {
- mapImpl({ init.connect(this).changes }) { newFlow ->
- newFlow.init.connect(this)
- }
- },
- ),
- )
- )
-
-/**
- * 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: suspend 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 66aa2a950fcf..000000000000
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TState.kt
+++ /dev/null
@@ -1,545 +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.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
-import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.Deferred
-import kotlinx.coroutines.async
-import kotlinx.coroutines.coroutineScope
-
-/**
- * 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.await() }
-
-/** TODO */
-@ExperimentalFrpApi
-fun <A> deferTState(block: suspend 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: suspend 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: suspend 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: suspend 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: suspend 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: suspend 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: suspend FrpScope.(A, B) -> Z,
-): TState<Z> {
- val operatorName = "combine"
- val name = operatorName
- return TStateInit(
- init(name) {
- coroutineScope {
- val dl1: Deferred<TStateImpl<A>> = async {
- stateA.init.connect(evalScope = this@init)
- }
- val dl2: Deferred<TStateImpl<B>> = async {
- stateB.init.connect(evalScope = this@init)
- }
- zipStates(name, operatorName, dl1.await(), dl2.await()) { 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: suspend FrpScope.(A, B, C) -> Z,
-): TState<Z> {
- val operatorName = "combine"
- val name = operatorName
- return TStateInit(
- init(name) {
- coroutineScope {
- val dl1: Deferred<TStateImpl<A>> = async {
- stateA.init.connect(evalScope = this@init)
- }
- val dl2: Deferred<TStateImpl<B>> = async {
- stateB.init.connect(evalScope = this@init)
- }
- val dl3: Deferred<TStateImpl<C>> = async {
- stateC.init.connect(evalScope = this@init)
- }
- zipStates(name, operatorName, dl1.await(), dl2.await(), dl3.await()) { 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: suspend FrpScope.(A, B, C, D) -> Z,
-): TState<Z> {
- val operatorName = "combine"
- val name = operatorName
- return TStateInit(
- init(name) {
- coroutineScope {
- val dl1: Deferred<TStateImpl<A>> = async {
- stateA.init.connect(evalScope = this@init)
- }
- val dl2: Deferred<TStateImpl<B>> = async {
- stateB.init.connect(evalScope = this@init)
- }
- val dl3: Deferred<TStateImpl<C>> = async {
- stateC.init.connect(evalScope = this@init)
- }
- val dl4: Deferred<TStateImpl<D>> = async {
- stateD.init.connect(evalScope = this@init)
- }
- zipStates(name, operatorName, dl1.await(), dl2.await(), dl3.await(), dl4.await()) {
- 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: suspend FrpScope.(A, B, C, D, E) -> Z,
-): TState<Z> {
- val operatorName = "combine"
- val name = operatorName
- return TStateInit(
- init(name) {
- coroutineScope {
- val dl1: Deferred<TStateImpl<A>> = async {
- stateA.init.connect(evalScope = this@init)
- }
- val dl2: Deferred<TStateImpl<B>> = async {
- stateB.init.connect(evalScope = this@init)
- }
- val dl3: Deferred<TStateImpl<C>> = async {
- stateC.init.connect(evalScope = this@init)
- }
- val dl4: Deferred<TStateImpl<D>> = async {
- stateD.init.connect(evalScope = this@init)
- }
- val dl5: Deferred<TStateImpl<E>> = async {
- stateE.init.connect(evalScope = this@init)
- }
- zipStates(
- name,
- operatorName,
- dl1.await(),
- dl2.await(),
- dl3.await(),
- dl4.await(),
- dl5.await(),
- ) { 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: suspend 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: Deferred<T>) : TState<T>() {
-
- private val input: CoalescingMutableTFlow<Deferred<T>, Deferred<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 calm: TFlowImpl<T> =
- filterImpl({ mapImpl(upstream = { changes.activated() }) { it!!.await() } }) { 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(CompletableDeferred(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 = CompletableDeferred<TState<A>>()
-
- internal val init: Init<TStateImpl<A>> =
- init(name) { deferred.await().init.connect(evalScope = this) }
-
- /** The [TState] this [TStateLoop] will forward to. */
- @ExperimentalFrpApi
- var loopback: TState<A>? = null
- set(value) {
- value?.let {
- check(deferred.complete(value)) { "TStateLoop.loopback has already been set." }
- 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: suspend 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 6b1c8c8fc3e5..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
@@ -16,57 +16,80 @@
package com.android.systemui.kairos
+import com.android.systemui.kairos.internal.CompletableLazy
import com.android.systemui.kairos.internal.InitScope
import com.android.systemui.kairos.internal.NoScope
import com.android.systemui.kairos.internal.TransactionalImpl
import com.android.systemui.kairos.internal.init
import com.android.systemui.kairos.internal.transactionalImpl
import com.android.systemui.kairos.internal.util.hashString
-import kotlinx.coroutines.CompletableDeferred
/**
* 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(CompletableDeferred(value))))
+ Transactional(stateOf(TransactionalImpl.Const(CompletableLazy(value))))
-/** TODO */
-@ExperimentalFrpApi
-fun <A> FrpDeferredValue<Transactional<A>>.defer(): Transactional<A> = deferInline {
- unwrapped.await()
-}
+/**
+ * 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: suspend FrpScope.() -> Transactional<A>): Transactional<A> =
+/**
+ * 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.runInFrpScope(block)
+ NoScope.block()
}
private inline fun <A> deferInline(
- crossinline block: suspend InitScope.() -> Transactional<A>
+ 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: suspend 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 7e6384925f38..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,25 +16,24 @@
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
-import com.android.systemui.kairos.internal.util.mapValuesParallel
+import com.android.systemui.kairos.internal.util.launchImmediate
import com.android.systemui.kairos.launchEffect
import com.android.systemui.kairos.mergeLeft
import com.android.systemui.kairos.util.Just
@@ -43,121 +42,81 @@ import com.android.systemui.kairos.util.None
import com.android.systemui.kairos.util.just
import com.android.systemui.kairos.util.map
import java.util.concurrent.atomic.AtomicReference
-import kotlin.coroutines.Continuation
import kotlin.coroutines.CoroutineContext
-import kotlin.coroutines.EmptyCoroutineContext
-import kotlin.coroutines.startCoroutine
-import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CompletableJob
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
-import kotlinx.coroutines.completeWith
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 suspend fun <R> runInBuildScope(block: suspend FrpBuildScope.() -> R): R {
- val complete = CompletableDeferred<R>(parent = coroutineContext.job)
- block.startCoroutine(
- frpScope,
- object : Continuation<R> {
- override val context: CoroutineContext
- get() = EmptyCoroutineContext
-
- override fun resumeWith(result: Result<R>) {
- complete.completeWith(result)
- }
- },
- )
- return complete.await()
- }
-
- private fun <A, T : TFlow<A>, S> buildTFlow(
- constructFlow: (InputNode<A>) -> Pair<T, S>,
- builder: suspend S.() -> Unit,
- ): TFlow<A> {
- var job: Job? = null
- val stopEmitter = newStopEmitter("buildTFlow")
- // 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 = {
- check(job == null) { "already activated" }
- job =
- reenterBuildScope(this@BuildScopeImpl, childScope).runInBuildScope {
- launchEffect {
- builder(emitter.second)
- stopEmitter.emit(Unit)
- }
- }
- },
- deactivate = {
- checkNotNull(job) { "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(builder: suspend FrpProducerScope<T>.() -> Unit): TFlow<T> =
- buildTFlow(
- constructFlow = { inputNode ->
- val flow = MutableTFlow(network, inputNode)
- flow to
- object : FrpProducerScope<T> {
+ override fun <T> events(
+ name: String?,
+ builder: suspend EventProducerScope<T>.() -> Unit,
+ ): Events<T> =
+ buildEvents(
+ name,
+ 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: suspend FrpBuildScope.() -> R): FrpDeferredValue<R> =
- FrpDeferredValue(deferAsync { runInBuildScope(block) })
+ override fun <R> deferredBuildScope(block: BuildScope.() -> R): DeferredValue<R> =
+ DeferredValue(deferAsync { block() })
- private fun deferredActionInternal(block: suspend FrpBuildScope.() -> Unit) {
- deferAction { runInBuildScope(block) }
+ override fun deferredBuildScopeAction(block: BuildScope.() -> Unit) {
+ deferAction { block() }
}
- private fun <A> TFlow<A>.observeEffectInternal(
- context: CoroutineContext,
- block: suspend 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()
@@ -172,38 +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 coroutine: suspend FrpEffectScope.() -> Unit = { block(output) }
- val complete = CompletableDeferred<Unit>(parent = coroutineContext.job)
- coroutine.startCoroutine(
- object : FrpEffectScope, FrpTransactionScope by frpScope {
- override val frpCoroutineScope: CoroutineScope = childScope
- override val frpNetwork: FrpNetwork =
- LocalFrpNetwork(network, childScope, endSignal)
- },
- completion =
- object : Continuation<Unit> {
- override val context: CoroutineContext
- get() = EmptyCoroutineContext
-
- override fun resumeWith(result: Result<Unit>) {
- complete.completeWith(result)
- }
- },
- )
- complete.await()
+ val scope =
+ object : EffectScope, TransactionScope by this@BuildScopeImpl {
+ override val effectCoroutineScope: CoroutineScope = childScope
+ override val kairosNetwork: KairosNetwork = localNetwork
+ }
+ block(scope, output)
}
},
)
- with(frpScope) { this@observeEffectInternal.takeUntil(endSignal) }
+ this@observe.takeUntil(endSignal)
.init
.connect(evalScope = stateScope.evalScope)
.activate(evalScope = stateScope.evalScope, outputNode.schedulable)
@@ -213,71 +160,110 @@ internal class BuildScopeImpl(val stateScope: StateScopeImpl, val coroutineScope
// Job's already been cancelled, schedule deactivation
scheduleDeactivation(outputNode)
} else if (needsEval) {
- outputNode.schedule(evalScope = stateScope.evalScope)
+ outputNode.schedule(0, evalScope = stateScope.evalScope)
}
} ?: run { childScope.cancel() }
}
return childScope.coroutineContext.job
}
- private fun <A, B> TFlow<A>.mapBuildInternal(
- transform: suspend 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 ->
+ 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)
- val initOut: Deferred<Map<K, B>> = deferAsync {
- init.unwrapped.await().mapValuesParallel { (k, spec) ->
- val newEnd = with(frpScope) { eventsByKey[k].skipNext() }
+ ): 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 {
+ 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) }) {
- upstreamMap ->
+ val changesNode: EventsImpl<Map<K, Maybe<A>>> =
+ mapImpl(upstream = { this@applyLatestSpecForKey.init.connect(evalScope = this) }) {
+ upstreamMap,
+ _ ->
reenterBuildScope(this@BuildScopeImpl, childScope).run {
- upstreamMap.mapValuesParallel { (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 suspend fun childBuildScope(newEnd: TFlow<Any>): BuildScopeImpl {
+ private fun childBuildScope(newEnd: Events<Any>): BuildScopeImpl {
val newCoroutineScope: CoroutineScope = coroutineScope.childScope()
return BuildScopeImpl(
stateScope = stateScope.childStateScope(newEnd),
@@ -292,7 +278,7 @@ internal class BuildScopeImpl(val stateScope: StateScopeImpl, val coroutineScope
(newCoroutineScope.coroutineContext.job as CompletableJob).complete()
}
)
- runInBuildScope { endSignal.nextOnly().observe { newCoroutineScope.cancel() } }
+ endSignalOnce.observe { newCoroutineScope.cancel() }
}
}
@@ -315,42 +301,6 @@ internal class BuildScopeImpl(val stateScope: StateScopeImpl, val coroutineScope
coroutineScope = childScope,
)
}
-
- private inner class FrpBuildScopeImpl : FrpBuildScope, FrpStateScope by stateScope.frpScope {
-
- override fun <T> tFlow(builder: suspend FrpProducerScope<T>.() -> Unit): TFlow<T> =
- tFlowInternal(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: suspend FrpBuildScope.() -> R
- ): FrpDeferredValue<R> = deferredInternal(block)
-
- override fun deferredBuildScopeAction(block: suspend FrpBuildScope.() -> Unit) =
- deferredActionInternal(block)
-
- override fun <A> TFlow<A>.observe(
- coroutineContext: CoroutineContext,
- block: suspend FrpEffectScope.(A) -> Unit,
- ): Job = observeEffectInternal(coroutineContext, block)
-
- override fun <A, B> TFlow<A>.mapBuild(transform: suspend 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/DeferScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/DeferScope.kt
index f65307c6106f..8a66f9a0d40d 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/DeferScope.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/DeferScope.kt
@@ -16,33 +16,58 @@
package com.android.systemui.kairos.internal
-import com.android.systemui.kairos.internal.util.asyncImmediate
-import com.android.systemui.kairos.internal.util.launchImmediate
-import kotlinx.coroutines.CoroutineName
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.CoroutineStart
-import kotlinx.coroutines.Deferred
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.isActive
-
-internal typealias DeferScope = CoroutineScope
-
-internal inline fun DeferScope.deferAction(
- start: CoroutineStart = CoroutineStart.UNDISPATCHED,
- crossinline block: suspend () -> Unit,
-): Job {
- check(isActive) { "Cannot perform deferral, scope already closed." }
- return launchImmediate(start, CoroutineName("deferAction")) { block() }
+internal interface DeferScope {
+ fun deferAction(block: () -> Unit)
+
+ fun <R> deferAsync(block: () -> R): Lazy<R>
}
-internal inline fun <R> DeferScope.deferAsync(
- start: CoroutineStart = CoroutineStart.UNDISPATCHED,
- crossinline block: suspend () -> R,
-): Deferred<R> {
- check(isActive) { "Cannot perform deferral, scope already closed." }
- return asyncImmediate(start, CoroutineName("deferAsync")) { block() }
+internal inline fun <A> deferScope(block: DeferScope.() -> A): A {
+ val scope =
+ object : DeferScope {
+ val deferrals = ArrayDeque<() -> Unit>() // TODO: store lazies instead?
+
+ fun drainDeferrals() {
+ while (deferrals.isNotEmpty()) {
+ deferrals.removeFirst().invoke()
+ }
+ }
+
+ override fun deferAction(block: () -> Unit) {
+ deferrals.add(block)
+ }
+
+ override fun <R> deferAsync(block: () -> R): Lazy<R> =
+ lazy(block).also { deferrals.add { it.value } }
+ }
+ return scope.block().also { scope.drainDeferrals() }
}
-internal suspend inline fun <A> deferScope(noinline block: suspend DeferScope.() -> A): A =
- coroutineScope(block)
+internal object NoValue
+
+internal class CompletableLazy<T> : Lazy<T> {
+
+ private var _value: Any?
+
+ constructor() {
+ _value = NoValue
+ }
+
+ constructor(init: T) {
+ _value = init
+ }
+
+ fun setValue(value: T) {
+ check(_value === NoValue) { "CompletableLazy value already set" }
+ _value = value
+ }
+
+ override val value: T
+ get() {
+ check(_value !== NoValue) { "CompletableLazy accessed before initialized" }
+ @Suppress("UNCHECKED_CAST")
+ return _value as T
+ }
+
+ override fun isInitialized(): Boolean = _value !== NoValue
+}
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 5f652525f036..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
@@ -21,10 +21,8 @@ import com.android.systemui.kairos.internal.store.MapHolder
import com.android.systemui.kairos.internal.store.MapK
import com.android.systemui.kairos.internal.store.MutableMapK
import com.android.systemui.kairos.internal.util.hashString
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.launch
+import com.android.systemui.kairos.internal.util.logDuration
import kotlinx.coroutines.sync.Mutex
-import kotlinx.coroutines.sync.withLock
internal class DemuxNode<W, K, A>(
private val branchNodeByKey: MutableMapK<W, K, DemuxNode<W, K, A>.BranchNode>,
@@ -34,156 +32,111 @@ internal class DemuxNode<W, K, A>(
val schedulable = Schedulable.N(this)
- inline val mutex
- get() = lifecycle.mutex
-
lateinit var upstreamConnection: NodeConnection<MapK<W, K, A>>
@Volatile private var epoch: Long = Long.MIN_VALUE
- suspend fun hasCurrentValueLocked(evalScope: EvalScope, key: K): Boolean =
- evalScope.epoch == epoch && upstreamConnection.getPushEvent(evalScope).contains(key)
+ fun hasCurrentValueLocked(logIndent: Int, evalScope: EvalScope, key: K): Boolean =
+ evalScope.epoch == epoch &&
+ upstreamConnection.getPushEvent(logIndent, evalScope).contains(key)
- suspend fun hasCurrentValue(evalScope: EvalScope, key: K): Boolean =
- mutex.withLock { hasCurrentValueLocked(evalScope, key) }
+ fun hasCurrentValue(logIndent: Int, evalScope: EvalScope, key: K): Boolean =
+ hasCurrentValueLocked(logIndent, evalScope, key)
fun getAndMaybeAddDownstream(key: K): BranchNode =
branchNodeByKey.getOrPut(key) { BranchNode(key) }
- override suspend fun schedule(evalScope: EvalScope) = coroutineScope {
- val upstreamResult = upstreamConnection.getPushEvent(evalScope)
- mutex.withLock {
+ override fun schedule(logIndent: Int, evalScope: EvalScope) =
+ logDuration(logIndent, "DemuxNode.schedule") {
+ val upstreamResult =
+ logDuration("upstream.getPushEvent") {
+ upstreamConnection.getPushEvent(currentLogIndent, evalScope)
+ }
updateEpoch(evalScope)
for ((key, _) in upstreamResult) {
- if (key !in branchNodeByKey) continue
+ if (!branchNodeByKey.contains(key)) continue
val branch = branchNodeByKey.getValue(key)
- // TODO: launchImmediate?
- launch { branch.schedule(evalScope) }
+ branch.schedule(currentLogIndent, evalScope)
}
}
- }
- override suspend fun adjustDirectUpstream(scheduler: Scheduler, oldDepth: Int, newDepth: Int) {
- coroutineScope {
- mutex.withLock {
- for ((_, branchNode) in branchNodeByKey) {
- branchNode.downstreamSet.adjustDirectUpstream(
- coroutineScope = this,
- scheduler,
- oldDepth,
- newDepth,
- )
- }
- }
+ override fun adjustDirectUpstream(scheduler: Scheduler, oldDepth: Int, newDepth: Int) {
+ for ((_, branchNode) in branchNodeByKey) {
+ branchNode.downstreamSet.adjustDirectUpstream(scheduler, oldDepth, newDepth)
}
}
- override suspend fun moveIndirectUpstreamToDirect(
+ override fun moveIndirectUpstreamToDirect(
scheduler: Scheduler,
oldIndirectDepth: Int,
oldIndirectSet: Set<MuxDeferredNode<*, *, *>>,
newDirectDepth: Int,
) {
- coroutineScope {
- mutex.withLock {
- for ((_, branchNode) in branchNodeByKey) {
- branchNode.downstreamSet.moveIndirectUpstreamToDirect(
- coroutineScope = this,
- scheduler,
- oldIndirectDepth,
- oldIndirectSet,
- newDirectDepth,
- )
- }
- }
+ for ((_, branchNode) in branchNodeByKey) {
+ branchNode.downstreamSet.moveIndirectUpstreamToDirect(
+ scheduler,
+ oldIndirectDepth,
+ oldIndirectSet,
+ newDirectDepth,
+ )
}
}
- override suspend fun adjustIndirectUpstream(
+ override fun adjustIndirectUpstream(
scheduler: Scheduler,
oldDepth: Int,
newDepth: Int,
removals: Set<MuxDeferredNode<*, *, *>>,
additions: Set<MuxDeferredNode<*, *, *>>,
) {
- coroutineScope {
- mutex.withLock {
- for ((_, branchNode) in branchNodeByKey) {
- branchNode.downstreamSet.adjustIndirectUpstream(
- coroutineScope = this,
- scheduler,
- oldDepth,
- newDepth,
- removals,
- additions,
- )
- }
- }
+ for ((_, branchNode) in branchNodeByKey) {
+ branchNode.downstreamSet.adjustIndirectUpstream(
+ scheduler,
+ oldDepth,
+ newDepth,
+ removals,
+ additions,
+ )
}
}
- override suspend fun moveDirectUpstreamToIndirect(
+ override fun moveDirectUpstreamToIndirect(
scheduler: Scheduler,
oldDirectDepth: Int,
newIndirectDepth: Int,
newIndirectSet: Set<MuxDeferredNode<*, *, *>>,
) {
- coroutineScope {
- mutex.withLock {
- for ((_, branchNode) in branchNodeByKey) {
- branchNode.downstreamSet.moveDirectUpstreamToIndirect(
- coroutineScope = this,
- scheduler,
- oldDirectDepth,
- newIndirectDepth,
- newIndirectSet,
- )
- }
- }
+ for ((_, branchNode) in branchNodeByKey) {
+ branchNode.downstreamSet.moveDirectUpstreamToIndirect(
+ scheduler,
+ oldDirectDepth,
+ newIndirectDepth,
+ newIndirectSet,
+ )
}
}
- override suspend fun removeIndirectUpstream(
+ override fun removeIndirectUpstream(
scheduler: Scheduler,
depth: Int,
indirectSet: Set<MuxDeferredNode<*, *, *>>,
) {
- coroutineScope {
- mutex.withLock {
- lifecycle.lifecycleState = DemuxLifecycleState.Dead
- for ((_, branchNode) in branchNodeByKey) {
- branchNode.downstreamSet.removeIndirectUpstream(
- coroutineScope = this,
- scheduler,
- depth,
- indirectSet,
- )
- }
- }
+ lifecycle.lifecycleState = DemuxLifecycleState.Dead
+ for ((_, branchNode) in branchNodeByKey) {
+ branchNode.downstreamSet.removeIndirectUpstream(scheduler, depth, indirectSet)
}
}
- override suspend fun removeDirectUpstream(scheduler: Scheduler, depth: Int) {
- coroutineScope {
- mutex.withLock {
- lifecycle.lifecycleState = DemuxLifecycleState.Dead
- for ((_, branchNode) in branchNodeByKey) {
- branchNode.downstreamSet.removeDirectUpstream(
- coroutineScope = this,
- scheduler,
- depth,
- )
- }
- }
+ override fun removeDirectUpstream(scheduler: Scheduler, depth: Int) {
+ lifecycle.lifecycleState = DemuxLifecycleState.Dead
+ for ((_, branchNode) in branchNodeByKey) {
+ branchNode.downstreamSet.removeDirectUpstream(scheduler, depth)
}
}
- suspend fun removeDownstreamAndDeactivateIfNeeded(key: K) {
- val deactivate =
- mutex.withLock {
- branchNodeByKey.remove(key)
- branchNodeByKey.isEmpty()
- }
+ fun removeDownstreamAndDeactivateIfNeeded(key: K) {
+ branchNodeByKey.remove(key)
+ val deactivate = branchNodeByKey.isEmpty()
if (deactivate) {
// No need for mutex here; no more concurrent changes to can occur during this phase
lifecycle.lifecycleState = DemuxLifecycleState.Inactive(spec)
@@ -195,64 +148,64 @@ internal class DemuxNode<W, K, A>(
epoch = evalScope.epoch
}
- suspend fun getPushEvent(evalScope: EvalScope, key: K): A =
- upstreamConnection.getPushEvent(evalScope).getValue(key)
+ fun getPushEvent(logIndent: Int, evalScope: EvalScope, key: K): A =
+ logDuration(logIndent, "Demux.getPushEvent($key)") {
+ upstreamConnection.getPushEvent(currentLogIndent, evalScope).getValue(key)
+ }
inner class BranchNode(val key: K) : PushNode<A> {
- private val mutex = Mutex()
-
val downstreamSet = DownstreamSet()
override val depthTracker: DepthTracker
get() = upstreamConnection.depthTracker
- override suspend fun hasCurrentValue(evalScope: EvalScope): Boolean =
- hasCurrentValue(evalScope, key)
+ override fun hasCurrentValue(logIndent: Int, evalScope: EvalScope): Boolean =
+ hasCurrentValue(logIndent, evalScope, key)
- override suspend fun getPushEvent(evalScope: EvalScope): A = getPushEvent(evalScope, key)
+ override fun getPushEvent(logIndent: Int, evalScope: EvalScope): A =
+ getPushEvent(logIndent, evalScope, key)
- override suspend fun addDownstream(downstream: Schedulable) {
- mutex.withLock { downstreamSet.add(downstream) }
+ override fun addDownstream(downstream: Schedulable) {
+ downstreamSet.add(downstream)
}
- override suspend fun removeDownstream(downstream: Schedulable) {
- mutex.withLock { downstreamSet.remove(downstream) }
+ override fun removeDownstream(downstream: Schedulable) {
+ downstreamSet.remove(downstream)
}
- override suspend fun removeDownstreamAndDeactivateIfNeeded(downstream: Schedulable) {
- val canDeactivate =
- mutex.withLock {
- downstreamSet.remove(downstream)
- downstreamSet.isEmpty()
- }
+ override fun removeDownstreamAndDeactivateIfNeeded(downstream: Schedulable) {
+ downstreamSet.remove(downstream)
+ val canDeactivate = downstreamSet.isEmpty()
if (canDeactivate) {
removeDownstreamAndDeactivateIfNeeded(key)
}
}
- override suspend fun deactivateIfNeeded() {
- if (mutex.withLock { downstreamSet.isEmpty() }) {
+ override fun deactivateIfNeeded() {
+ if (downstreamSet.isEmpty()) {
removeDownstreamAndDeactivateIfNeeded(key)
}
}
- override suspend fun scheduleDeactivationIfNeeded(evalScope: EvalScope) {
- if (mutex.withLock { downstreamSet.isEmpty() }) {
+ override fun scheduleDeactivationIfNeeded(evalScope: EvalScope) {
+ if (downstreamSet.isEmpty()) {
evalScope.scheduleDeactivation(this)
}
}
- suspend fun schedule(evalScope: EvalScope) {
- if (!coroutineScope { mutex.withLock { scheduleAll(downstreamSet, evalScope) } }) {
- evalScope.scheduleDeactivation(this)
+ fun schedule(logIndent: Int, evalScope: EvalScope) {
+ logDuration(logIndent, "DemuxBranchNode($key).schedule") {
+ if (!scheduleAll(currentLogIndent, downstreamSet, evalScope)) {
+ evalScope.scheduleDeactivation(this@BranchNode)
+ }
}
}
}
}
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> =
@@ -263,28 +216,27 @@ internal fun <W, K, A> DemuxImpl(
)
internal fun <K, A> demuxMap(
- upstream: suspend EvalScope.() -> TFlowImpl<Map<K, A>>,
+ upstream: EvalScope.() -> EventsImpl<Map<K, A>>,
numKeys: Int?,
): DemuxImpl<K, A> =
- DemuxImpl(mapImpl(upstream) { MapHolder(it) }, numKeys, ConcurrentHashMapK.Factory())
+ 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>,
) {
- suspend fun activate(
+ fun activate(
evalScope: EvalScope,
lifecycle: DemuxLifecycle<K, A>,
): Pair<DemuxNode<W, K, A>, Set<K>>? {
val demux = DemuxNode(storeFactory.create(numKeys), lifecycle, this)
- return upstream.activate(evalScope, downstream = demux.schedulable)?.let { (conn, needsEval)
- ->
+ return upstream.activate(evalScope, demux.schedulable)?.let { (conn, needsEval) ->
Pair(
demux.apply { upstreamConnection = conn },
if (needsEval) {
demux.updateEpoch(evalScope)
- conn.getPushEvent(evalScope).keys
+ conn.getPushEvent(0, evalScope).keys
} else {
emptySet()
},
@@ -294,10 +246,10 @@ 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(evalScope = this)
+ val branchNeedsEval = needsEval && branchNode.hasCurrentValue(0, evalScope = this)
ActivationResult(
connection = NodeConnection(branchNode, branchNode),
needsEval = branchNeedsEval,
@@ -311,31 +263,31 @@ internal class DemuxLifecycle<K, A>(@Volatile var lifecycleState: DemuxLifecycle
override fun toString(): String = "TFlowDmuxState[$hashString][$lifecycleState][$mutex]"
- suspend fun activate(
- evalScope: EvalScope,
- key: K,
- ): Pair<DemuxNode<*, K, A>.BranchNode, Boolean>? =
- mutex.withLock {
- when (val state = lifecycleState) {
- is DemuxLifecycleState.Dead -> null
- is DemuxLifecycleState.Active ->
- state.node.getAndMaybeAddDownstream(key) to
- state.node.hasCurrentValueLocked(evalScope, key)
- is DemuxLifecycleState.Inactive -> {
- state.spec
- .activate(evalScope, this@DemuxLifecycle)
- .also { result ->
- lifecycleState =
- if (result == null) {
- DemuxLifecycleState.Dead
- } else {
- DemuxLifecycleState.Active(result.first)
- }
- }
- ?.let { (node, needsEval) ->
- node.getAndMaybeAddDownstream(key) to (key in needsEval)
- }
- }
+ fun activate(evalScope: EvalScope, key: K): Pair<DemuxNode<*, K, A>.BranchNode, Boolean>? =
+ when (val state = lifecycleState) {
+ is DemuxLifecycleState.Dead -> {
+ null
+ }
+
+ is DemuxLifecycleState.Active -> {
+ state.node.getAndMaybeAddDownstream(key) to
+ state.node.hasCurrentValueLocked(0, evalScope, key)
+ }
+
+ is DemuxLifecycleState.Inactive -> {
+ state.spec
+ .activate(evalScope, this@DemuxLifecycle)
+ .also { result ->
+ lifecycleState =
+ if (result == null) {
+ DemuxLifecycleState.Dead
+ } else {
+ DemuxLifecycleState.Active(result.first)
+ }
+ }
+ ?.let { (node, needsEval) ->
+ node.getAndMaybeAddDownstream(key) to (key in needsEval)
+ }
}
}
}
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 afbd7120653c..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,57 +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 kotlin.coroutines.Continuation
-import kotlin.coroutines.CoroutineContext
-import kotlin.coroutines.EmptyCoroutineContext
-import kotlin.coroutines.startCoroutine
-import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.completeWith
-import kotlinx.coroutines.job
+import com.android.systemui.kairos.switchEvents
internal class EvalScopeImpl(networkScope: NetworkScope, deferScope: DeferScope) :
- EvalScope, NetworkScope by networkScope, DeferScope by deferScope {
-
- private suspend fun <A> Transactional<A>.sample(): A =
- impl.sample().sample(this@EvalScopeImpl).await()
-
- private suspend fun <A> TState<A>.sample(): A =
- init.connect(evalScope = this@EvalScopeImpl).getCurrentWithEpoch(this@EvalScopeImpl).first
-
- private val <A> Transactional<A>.deferredValue: FrpDeferredValue<A>
- get() = FrpDeferredValue(deferAsync { sample() })
+ EvalScope, NetworkScope by networkScope, DeferScope by deferScope, TransactionScope {
+
+ override fun <A> Transactional<A>.sampleDeferred(): DeferredValue<A> =
+ DeferredValue(deferAsync { impl.sample().sample(this@EvalScopeImpl).value })
+
+ override fun <A> State<A>.sampleDeferred(): DeferredValue<A> =
+ DeferredValue(
+ deferAsync {
+ init
+ .connect(evalScope = this@EvalScopeImpl)
+ .getCurrentWithEpoch(this@EvalScopeImpl)
+ .first
+ }
+ )
- private val <A> TState<A>.deferredValue: FrpDeferredValue<A>
- get() = FrpDeferredValue(deferAsync { sample() })
+ override fun <R> deferredTransactionScope(block: TransactionScope.() -> R): DeferredValue<R> =
+ DeferredValue(deferAsync { block() })
- 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) },
- CompletableDeferred(
- TFlowInit(
+ { result.mapCheap { emptyEvents }.init.connect(evalScope = this) },
+ CompletableLazy(
+ EventsInit(
constInit(
"now",
- TFlowCheap {
+ EventsImplCheap {
ActivationResult(
connection = NodeConnection(AlwaysNode, AlwaysNode),
needsEval = true,
@@ -78,42 +75,7 @@ internal class EvalScopeImpl(networkScope: NetworkScope, deferScope: DeferScope)
),
)
)
- .switch()
+ .switchEvents()
result
}
-
- private fun <R> deferredInternal(
- block: suspend FrpTransactionScope.() -> R
- ): FrpDeferredValue<R> = FrpDeferredValue(deferAsync { runInTransactionScope(block) })
-
- override suspend fun <R> runInTransactionScope(block: suspend FrpTransactionScope.() -> R): R {
- val complete = CompletableDeferred<R>(parent = coroutineContext.job)
- block.startCoroutine(
- frpScope,
- object : Continuation<R> {
- override val context: CoroutineContext
- get() = EmptyCoroutineContext
-
- override fun resumeWith(result: Result<R>) {
- complete.completeWith(result)
- }
- },
- )
- return complete.await()
- }
-
- 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: suspend 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 784a2afe0992..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,8 +17,8 @@
package com.android.systemui.kairos.internal
/* Initialized TFlow */
-internal fun interface TFlowImpl<out A> {
- suspend fun activate(evalScope: EvalScope, downstream: Schedulable): ActivationResult<A>?
+internal fun interface EventsImpl<out A> {
+ fun activate(evalScope: EvalScope, downstream: Schedulable): ActivationResult<A>?
}
internal data class ActivationResult<out A>(
@@ -26,34 +26,33 @@ 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)
}
internal typealias CheapNodeSubscribe<A> =
- suspend EvalScope.(downstream: Schedulable) -> ActivationResult<A>?
+ EvalScope.(downstream: Schedulable) -> ActivationResult<A>?
internal data class NodeConnection<out A>(
val directUpstream: PullNode<A>,
val schedulerUpstream: PushNode<*>,
)
-internal suspend fun <A> NodeConnection<A>.hasCurrentValue(evalScope: EvalScope): Boolean =
- schedulerUpstream.hasCurrentValue(evalScope)
+internal fun <A> NodeConnection<A>.hasCurrentValue(logIndent: Int, evalScope: EvalScope): Boolean =
+ schedulerUpstream.hasCurrentValue(logIndent, evalScope)
-internal suspend fun <A> NodeConnection<A>.removeDownstreamAndDeactivateIfNeeded(
- downstream: Schedulable
-) = schedulerUpstream.removeDownstreamAndDeactivateIfNeeded(downstream)
+internal fun <A> NodeConnection<A>.removeDownstreamAndDeactivateIfNeeded(downstream: Schedulable) =
+ schedulerUpstream.removeDownstreamAndDeactivateIfNeeded(downstream)
-internal suspend fun <A> NodeConnection<A>.scheduleDeactivationIfNeeded(evalScope: EvalScope) =
+internal fun <A> NodeConnection<A>.scheduleDeactivationIfNeeded(evalScope: EvalScope) =
schedulerUpstream.scheduleDeactivationIfNeeded(evalScope)
-internal suspend fun <A> NodeConnection<A>.removeDownstream(downstream: Schedulable) =
+internal fun <A> NodeConnection<A>.removeDownstream(downstream: Schedulable) =
schedulerUpstream.removeDownstream(downstream)
-internal suspend fun <A> NodeConnection<A>.getPushEvent(evalScope: EvalScope): A =
- directUpstream.getPushEvent(evalScope)
+internal fun <A> NodeConnection<A>.getPushEvent(logIndent: Int, evalScope: EvalScope): A =
+ directUpstream.getPushEvent(logIndent, evalScope)
internal val <A> NodeConnection<A>.depthTracker: DepthTracker
get() = schedulerUpstream.depthTracker
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 b60c227bcfbe..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,10 +24,10 @@ import com.android.systemui.kairos.util.just
import com.android.systemui.kairos.util.none
internal inline fun <A> filterJustImpl(
- crossinline getPulse: suspend EvalScope.() -> TFlowImpl<Maybe<A>>
-): TFlowImpl<A> =
+ crossinline getPulse: EvalScope.() -> EventsImpl<Maybe<A>>
+): EventsImpl<A> =
DemuxImpl(
- mapImpl(getPulse) { maybeResult ->
+ mapImpl(getPulse) { maybeResult, _ ->
if (maybeResult is Just) {
Single(maybeResult.value)
} else {
@@ -40,6 +40,9 @@ internal inline fun <A> filterJustImpl(
.eventsForKey(Unit)
internal inline fun <A> filterImpl(
- crossinline getPulse: suspend EvalScope.() -> TFlowImpl<A>,
- crossinline f: suspend EvalScope.(A) -> Boolean,
-): TFlowImpl<A> = filterJustImpl { mapImpl(getPulse) { if (f(it)) just(it) else none }.cached() }
+ crossinline getPulse: EvalScope.() -> EventsImpl<A>,
+ crossinline f: EvalScope.(A) -> Boolean,
+): EventsImpl<A> {
+ val mapped = mapImpl(getPulse) { it, _ -> if (f(it)) just(it) else none }.cached()
+ return filterJustImpl { mapped }
+}
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 828f13b026d3..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
@@ -18,8 +18,6 @@ package com.android.systemui.kairos.internal
import com.android.systemui.kairos.internal.util.Bag
import java.util.TreeMap
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.launch
/**
* Tracks all upstream connections for Mux nodes.
@@ -86,7 +84,7 @@ internal class DepthTracker {
@Volatile private var dirty_depthIsDirect = true
@Volatile private var dirty_isIndirectRoot = false
- fun schedule(scheduler: Scheduler, node: MuxNode<*, *, *, *>) {
+ fun schedule(scheduler: Scheduler, node: MuxNode<*, *, *>) {
if (dirty_depthIsDirect) {
scheduler.schedule(dirty_directDepth, node)
} else {
@@ -192,30 +190,27 @@ internal class DepthTracker {
return remainder
}
- suspend fun propagateChanges(scheduler: Scheduler, muxNode: MuxNode<*, *, *, *>) {
+ fun propagateChanges(scheduler: Scheduler, muxNode: MuxNode<*, *, *>) {
if (isDirty()) {
schedule(scheduler, muxNode)
}
}
fun applyChanges(
- coroutineScope: CoroutineScope,
scheduler: Scheduler,
downstreamSet: DownstreamSet,
- muxNode: MuxNode<*, *, *, *>,
+ muxNode: MuxNode<*, *, *>,
) {
when {
dirty_depthIsDirect -> {
if (snapshotIsDirect) {
downstreamSet.adjustDirectUpstream(
- coroutineScope,
scheduler,
oldDepth = snapshotDirectDepth,
newDepth = dirty_directDepth,
)
} else {
downstreamSet.moveIndirectUpstreamToDirect(
- coroutineScope,
scheduler,
oldIndirectDepth = snapshotIndirectDepth,
oldIndirectSet =
@@ -233,7 +228,6 @@ internal class DepthTracker {
dirty_hasIndirectUpstream() || dirty_isIndirectRoot -> {
if (snapshotIsDirect) {
downstreamSet.moveDirectUpstreamToIndirect(
- coroutineScope,
scheduler,
oldDirectDepth = snapshotDirectDepth,
newIndirectDepth = dirty_indirectDepth,
@@ -247,7 +241,6 @@ internal class DepthTracker {
)
} else {
downstreamSet.adjustIndirectUpstream(
- coroutineScope,
scheduler,
oldDepth = snapshotIndirectDepth,
newDepth = dirty_indirectDepth,
@@ -274,14 +267,9 @@ internal class DepthTracker {
muxNode.lifecycle.lifecycleState = MuxLifecycleState.Dead
if (snapshotIsDirect) {
- downstreamSet.removeDirectUpstream(
- coroutineScope,
- scheduler,
- depth = snapshotDirectDepth,
- )
+ downstreamSet.removeDirectUpstream(scheduler, depth = snapshotDirectDepth)
} else {
downstreamSet.removeIndirectUpstream(
- coroutineScope,
scheduler,
depth = snapshotIndirectDepth,
indirectSet =
@@ -352,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>()
@@ -374,125 +362,92 @@ internal class DownstreamSet {
}
}
- fun adjustDirectUpstream(
- coroutineScope: CoroutineScope,
- scheduler: Scheduler,
- oldDepth: Int,
- newDepth: Int,
- ) =
- coroutineScope.run {
- for (node in nodes) {
- launch { node.adjustDirectUpstream(scheduler, oldDepth, newDepth) }
- }
+ fun adjustDirectUpstream(scheduler: Scheduler, oldDepth: Int, newDepth: Int) {
+ for (node in nodes) {
+ node.adjustDirectUpstream(scheduler, oldDepth, newDepth)
}
+ }
fun moveIndirectUpstreamToDirect(
- coroutineScope: CoroutineScope,
scheduler: Scheduler,
oldIndirectDepth: Int,
oldIndirectSet: Set<MuxDeferredNode<*, *, *>>,
newDirectDepth: Int,
- ) =
- coroutineScope.run {
- for (node in nodes) {
- launch {
- node.moveIndirectUpstreamToDirect(
- scheduler,
- oldIndirectDepth,
- oldIndirectSet,
- newDirectDepth,
- )
- }
- }
- for (mover in muxMovers) {
- launch {
- mover.moveIndirectPatchNodeToDirect(scheduler, oldIndirectDepth, oldIndirectSet)
- }
- }
+ ) {
+ for (node in nodes) {
+ node.moveIndirectUpstreamToDirect(
+ scheduler,
+ oldIndirectDepth,
+ oldIndirectSet,
+ newDirectDepth,
+ )
+ }
+ for (mover in muxMovers) {
+ mover.moveIndirectPatchNodeToDirect(scheduler, oldIndirectDepth, oldIndirectSet)
}
+ }
fun adjustIndirectUpstream(
- coroutineScope: CoroutineScope,
scheduler: Scheduler,
oldDepth: Int,
newDepth: Int,
removals: Set<MuxDeferredNode<*, *, *>>,
additions: Set<MuxDeferredNode<*, *, *>>,
- ) =
- coroutineScope.run {
- for (node in nodes) {
- launch {
- node.adjustIndirectUpstream(scheduler, oldDepth, newDepth, removals, additions)
- }
- }
- for (mover in muxMovers) {
- launch {
- mover.adjustIndirectPatchNode(
- scheduler,
- oldDepth,
- newDepth,
- removals,
- additions,
- )
- }
- }
+ ) {
+ for (node in nodes) {
+ node.adjustIndirectUpstream(scheduler, oldDepth, newDepth, removals, additions)
+ }
+ for (mover in muxMovers) {
+ mover.adjustIndirectPatchNode(scheduler, oldDepth, newDepth, removals, additions)
}
+ }
fun moveDirectUpstreamToIndirect(
- coroutineScope: CoroutineScope,
scheduler: Scheduler,
oldDirectDepth: Int,
newIndirectDepth: Int,
newIndirectSet: Set<MuxDeferredNode<*, *, *>>,
- ) =
- coroutineScope.run {
- for (node in nodes) {
- launch {
- node.moveDirectUpstreamToIndirect(
- scheduler,
- oldDirectDepth,
- newIndirectDepth,
- newIndirectSet,
- )
- }
- }
- for (mover in muxMovers) {
- launch {
- mover.moveDirectPatchNodeToIndirect(scheduler, newIndirectDepth, newIndirectSet)
- }
- }
+ ) {
+ for (node in nodes) {
+ node.moveDirectUpstreamToIndirect(
+ scheduler,
+ oldDirectDepth,
+ newIndirectDepth,
+ newIndirectSet,
+ )
+ }
+ for (mover in muxMovers) {
+ mover.moveDirectPatchNodeToIndirect(scheduler, newIndirectDepth, newIndirectSet)
}
+ }
fun removeIndirectUpstream(
- coroutineScope: CoroutineScope,
scheduler: Scheduler,
depth: Int,
indirectSet: Set<MuxDeferredNode<*, *, *>>,
- ) =
- coroutineScope.run {
- for (node in nodes) {
- launch { node.removeIndirectUpstream(scheduler, depth, indirectSet) }
- }
- for (mover in muxMovers) {
- launch { mover.removeIndirectPatchNode(scheduler, depth, indirectSet) }
- }
- for (output in outputs) {
- launch { output.kill() }
- }
+ ) {
+ for (node in nodes) {
+ node.removeIndirectUpstream(scheduler, depth, indirectSet)
+ }
+ for (mover in muxMovers) {
+ mover.removeIndirectPatchNode(scheduler, depth, indirectSet)
}
+ for (output in outputs) {
+ output.kill()
+ }
+ }
- fun removeDirectUpstream(coroutineScope: CoroutineScope, scheduler: Scheduler, depth: Int) =
- coroutineScope.run {
- for (node in nodes) {
- launch { node.removeDirectUpstream(scheduler, depth) }
- }
- for (mover in muxMovers) {
- launch { mover.removeDirectPatchNode(scheduler) }
- }
- for (output in outputs) {
- launch { output.kill() }
- }
+ fun removeDirectUpstream(scheduler: Scheduler, depth: Int) {
+ for (node in nodes) {
+ node.removeDirectUpstream(scheduler, depth)
+ }
+ for (mover in muxMovers) {
+ mover.removeDirectPatchNode(scheduler)
}
+ for (output in outputs) {
+ output.kill()
+ }
+ }
fun clear() {
outputs.clear()
@@ -504,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
@@ -518,13 +473,14 @@ internal fun DownstreamSet.isEmpty() =
@Suppress("NOTHING_TO_INLINE") internal inline fun DownstreamSet.isNotEmpty() = !isEmpty()
-internal fun CoroutineScope.scheduleAll(
+internal fun scheduleAll(
+ logIndent: Int,
downstreamSet: DownstreamSet,
evalScope: EvalScope,
): Boolean {
- downstreamSet.nodes.forEach { launch { it.schedule(evalScope) } }
- downstreamSet.muxMovers.forEach { launch { it.scheduleMover(evalScope) } }
- downstreamSet.outputs.forEach { launch { it.schedule(evalScope) } }
+ downstreamSet.nodes.forEach { it.schedule(logIndent, evalScope) }
+ downstreamSet.muxMovers.forEach { it.scheduleMover(logIndent, evalScope) }
+ downstreamSet.outputs.forEach { it.schedule(logIndent, evalScope) }
downstreamSet.stateWriters.forEach { evalScope.schedule(it) }
return downstreamSet.isNotEmpty()
}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Init.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Init.kt
index 57db9a493e21..10a46775beb9 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Init.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Init.kt
@@ -19,42 +19,37 @@ package com.android.systemui.kairos.internal
import com.android.systemui.kairos.util.Maybe
import com.android.systemui.kairos.util.just
import com.android.systemui.kairos.util.none
-import java.util.concurrent.atomic.AtomicBoolean
-import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
/** Performs actions once, when the reactive component is first connected to the network. */
-internal class Init<out A>(val name: String?, private val block: suspend InitScope.() -> A) {
-
- /** Has the initialization logic been evaluated yet? */
- private val initialized = AtomicBoolean()
+internal class Init<out A>(val name: String?, private val block: InitScope.() -> A) {
/**
* Stores the result after initialization, as well as the id of the [Network] it's been
* initialized with.
*/
- private val cache = CompletableDeferred<Pair<Any, A>>()
+ private val cache = CompletableLazy<Pair<Any, A>>()
- suspend fun connect(evalScope: InitScope): A =
- if (initialized.getAndSet(true)) {
+ fun connect(evalScope: InitScope): A =
+ if (cache.isInitialized()) {
// Read from cache
- val (networkId, result) = cache.await()
+ val (networkId, result) = cache.value
check(networkId == evalScope.networkId) { "Network mismatch" }
result
} else {
// Write to cache
- block(evalScope).also { cache.complete(evalScope.networkId to it) }
+ block(evalScope).also { cache.setValue(evalScope.networkId to it) }
}
@OptIn(ExperimentalCoroutinesApi::class)
fun getUnsafe(): Maybe<A> =
- if (cache.isCompleted) {
- just(cache.getCompleted().second)
+ if (cache.isInitialized()) {
+ just(cache.value.second)
} else {
none
}
}
-internal fun <A> init(name: String?, block: suspend InitScope.() -> A) = Init(name, block)
+internal fun <A> init(name: String?, block: InitScope.() -> A) = Init(name, block)
internal fun <A> constInit(name: String?, value: A) = init(name) { value }
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 1edc8c28b2ee..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
@@ -16,104 +16,103 @@
package com.android.systemui.kairos.internal
-import com.android.systemui.kairos.internal.util.Key
+import com.android.systemui.kairos.internal.util.logDuration
import java.util.concurrent.atomic.AtomicBoolean
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.sync.Mutex
-import kotlinx.coroutines.sync.withLock
internal class InputNode<A>(
- private val activate: suspend EvalScope.() -> Unit = {},
+ private val activate: EvalScope.() -> Unit = {},
private val deactivate: () -> Unit = {},
-) : PushNode<A>, Key<A> {
+) : PushNode<A> {
private val downstreamSet = DownstreamSet()
- private val mutex = Mutex()
- private val activated = AtomicBoolean(false)
+ val activated = AtomicBoolean(false)
- @Volatile private var epoch: Long = Long.MIN_VALUE
+ private val transactionCache = TransactionCache<A>()
+ private val epoch
+ get() = transactionCache.epoch
override val depthTracker: DepthTracker = DepthTracker()
- override suspend fun hasCurrentValue(evalScope: EvalScope): Boolean = epoch == evalScope.epoch
+ override fun hasCurrentValue(logIndent: Int, evalScope: EvalScope): Boolean =
+ epoch == evalScope.epoch
- suspend fun visit(evalScope: EvalScope, value: A) {
- epoch = evalScope.epoch
- evalScope.setResult(this, value)
- coroutineScope {
- if (!mutex.withLock { scheduleAll(downstreamSet, evalScope) }) {
- evalScope.scheduleDeactivation(this@InputNode)
- }
+ fun visit(evalScope: EvalScope, value: A) {
+ transactionCache.put(evalScope, value)
+ if (!scheduleAll(0, downstreamSet, evalScope)) {
+ evalScope.scheduleDeactivation(this@InputNode)
}
}
- override suspend fun removeDownstream(downstream: Schedulable) {
- mutex.withLock { downstreamSet.remove(downstream) }
+ override fun removeDownstream(downstream: Schedulable) {
+ downstreamSet.remove(downstream)
}
- override suspend fun deactivateIfNeeded() {
- if (mutex.withLock { downstreamSet.isEmpty() && activated.getAndSet(false) }) {
+ override fun deactivateIfNeeded() {
+ if (downstreamSet.isEmpty() && activated.getAndSet(false)) {
deactivate()
}
}
- override suspend fun scheduleDeactivationIfNeeded(evalScope: EvalScope) {
- if (mutex.withLock { downstreamSet.isEmpty() }) {
+ override fun scheduleDeactivationIfNeeded(evalScope: EvalScope) {
+ if (downstreamSet.isEmpty()) {
evalScope.scheduleDeactivation(this)
}
}
- override suspend fun addDownstream(downstream: Schedulable) {
- mutex.withLock { downstreamSet.add(downstream) }
+ override fun addDownstream(downstream: Schedulable) {
+ downstreamSet.add(downstream)
}
- suspend fun addDownstreamAndActivateIfNeeded(downstream: Schedulable, evalScope: EvalScope) {
- val needsActivation =
- mutex.withLock {
- val wasEmpty = downstreamSet.isEmpty()
- downstreamSet.add(downstream)
- wasEmpty && !activated.getAndSet(true)
- }
+ fun addDownstreamAndActivateIfNeeded(downstream: Schedulable, evalScope: EvalScope) {
+ val needsActivation = run {
+ val wasEmpty = downstreamSet.isEmpty()
+ downstreamSet.add(downstream)
+ wasEmpty && !activated.getAndSet(true)
+ }
if (needsActivation) {
activate(evalScope)
}
}
- override suspend fun removeDownstreamAndDeactivateIfNeeded(downstream: Schedulable) {
- val needsDeactivation =
- mutex.withLock {
- downstreamSet.remove(downstream)
- downstreamSet.isEmpty() && activated.getAndSet(false)
- }
+ override fun removeDownstreamAndDeactivateIfNeeded(downstream: Schedulable) {
+ downstreamSet.remove(downstream)
+ val needsDeactivation = downstreamSet.isEmpty() && activated.getAndSet(false)
if (needsDeactivation) {
deactivate()
}
}
- override suspend fun getPushEvent(evalScope: EvalScope): A = evalScope.getCurrentValue(this)
+ override fun getPushEvent(logIndent: Int, evalScope: EvalScope): A =
+ logDuration(logIndent, "Input.getPushEvent", false) {
+ transactionCache.getCurrentValue(evalScope)
+ }
}
-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(connection = NodeConnection(input, input), needsEval = hasCurrentValue(input))
+ ActivationResult(
+ connection = NodeConnection(input, input),
+ needsEval = input.hasCurrentValue(0, evalScope = this),
+ )
}
internal data object AlwaysNode : PushNode<Unit> {
override val depthTracker = DepthTracker()
- override suspend fun hasCurrentValue(evalScope: EvalScope): Boolean = true
+ override fun hasCurrentValue(logIndent: Int, evalScope: EvalScope): Boolean = true
- override suspend fun removeDownstream(downstream: Schedulable) {}
+ override fun removeDownstream(downstream: Schedulable) {}
- override suspend fun deactivateIfNeeded() {}
+ override fun deactivateIfNeeded() {}
- override suspend fun scheduleDeactivationIfNeeded(evalScope: EvalScope) {}
+ override fun scheduleDeactivationIfNeeded(evalScope: EvalScope) {}
- override suspend fun addDownstream(downstream: Schedulable) {}
+ override fun addDownstream(downstream: Schedulable) {}
- override suspend fun removeDownstreamAndDeactivateIfNeeded(downstream: Schedulable) {}
+ override fun removeDownstreamAndDeactivateIfNeeded(downstream: Schedulable) {}
- override suspend fun getPushEvent(evalScope: EvalScope) = Unit
+ override fun getPushEvent(logIndent: Int, evalScope: EvalScope) =
+ logDuration(logIndent, "Always.getPushEvent", false) { Unit }
}
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 80c40ba740a5..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.internal.util.HeteroMap
-import com.android.systemui.kairos.internal.util.Key
+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
- suspend fun <R> runInTransactionScope(block: suspend FrpTransactionScope.() -> R): R
-}
-
-internal interface StateScope : EvalScope {
- override val frpScope: FrpStateScope
-
- suspend fun <R> runInStateScope(block: suspend FrpStateScope.() -> R): R
+internal interface InternalStateScope : EvalScope, StateScope {
+ val endSignal: Events<Any>
+ val endSignalOnce: Events<Any>
- val endSignal: TFlow<Any>
-
- fun childStateScope(newEnd: TFlow<Any>): StateScope
+ fun childStateScope(newEnd: Events<Any>): InternalStateScope
}
-internal interface BuildScope : StateScope {
- override val frpScope: FrpBuildScope
-
- suspend fun <R> runInBuildScope(block: suspend FrpBuildScope.() -> R): R
-}
+internal interface InternalBuildScope : InternalStateScope, BuildScope
internal interface NetworkScope : InitScope {
@@ -57,24 +44,15 @@ internal interface NetworkScope : InitScope {
val compactor: Scheduler
val scheduler: Scheduler
- val transactionStore: HeteroMap
+ val transactionStore: TransactionStore
fun scheduleOutput(output: Output<*>)
fun scheduleMuxMover(muxMover: MuxDeferredNode<*, *, *>)
- fun schedule(state: TStateSource<*>)
+ fun schedule(state: StateSource<*>)
fun scheduleDeactivation(node: PushNode<*>)
fun scheduleDeactivation(output: Output<*>)
}
-
-internal fun <A> NetworkScope.setResult(node: Key<A>, result: A) {
- transactionStore[node] = result
-}
-
-internal fun <A> NetworkScope.getCurrentValue(key: Key<A>): A =
- transactionStore.getOrError(key) { "No value for $key in transaction $epoch" }
-
-internal fun NetworkScope.hasCurrentValue(key: Key<*>): Boolean = transactionStore.contains(key)
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 a479c90cc4de..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
@@ -22,44 +22,39 @@ import com.android.systemui.kairos.internal.store.MapHolder
import com.android.systemui.kairos.internal.store.MapK
import com.android.systemui.kairos.internal.store.MutableMapK
import com.android.systemui.kairos.internal.store.asMapHolder
-import com.android.systemui.kairos.internal.util.asyncImmediate
import com.android.systemui.kairos.internal.util.hashString
-import kotlinx.coroutines.CoroutineStart
-import kotlinx.coroutines.awaitAll
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.sync.Mutex
-import kotlinx.coroutines.sync.withLock
+import com.android.systemui.kairos.internal.util.logDuration
internal typealias MuxResult<W, K, V> = MapK<W, K, PullNode<V>>
/** Base class for muxing nodes, which have a (potentially dynamic) collection of upstream nodes. */
-internal sealed class MuxNode<W, K, V, Output>(
- val lifecycle: MuxLifecycle<Output>,
+internal sealed class MuxNode<W, K, V>(
+ val lifecycle: MuxLifecycle<W, K, V>,
protected val storeFactory: MutableMapK.Factory<W, K>,
-) : PushNode<Output> {
+) : PushNode<MuxResult<W, K, V>> {
- inline val mutex
- get() = lifecycle.mutex
+ lateinit var upstreamData: MutableMapK<W, K, PullNode<V>>
+ lateinit var switchedIn: MutableMapK<W, K, BranchNode>
- @Volatile lateinit var upstreamData: MutableMapK<W, K, PullNode<V>>
- @Volatile lateinit var switchedIn: MutableMapK<W, K, BranchNode>
+ @Volatile var markedForCompaction = false
+ @Volatile var markedForEvaluation = false
val downstreamSet: DownstreamSet = DownstreamSet()
// TODO: inline DepthTracker? would need to be added to PushNode signature
final override val depthTracker = DepthTracker()
- @Volatile
- var epoch: Long = Long.MIN_VALUE
- protected set
+ val transactionCache = TransactionCache<MuxResult<W, K, V>>()
+ val epoch
+ get() = transactionCache.epoch
inline fun hasCurrentValueLocked(evalScope: EvalScope): Boolean = epoch == evalScope.epoch
- override suspend fun hasCurrentValue(evalScope: EvalScope): Boolean =
- mutex.withLock { hasCurrentValueLocked(evalScope) }
+ override fun hasCurrentValue(logIndent: Int, evalScope: EvalScope): Boolean =
+ hasCurrentValueLocked(evalScope)
- final override suspend fun addDownstream(downstream: Schedulable) {
- mutex.withLock { addDownstreamLocked(downstream) }
+ final override fun addDownstream(downstream: Schedulable) {
+ addDownstreamLocked(downstream)
}
/**
@@ -72,135 +67,121 @@ internal sealed class MuxNode<W, K, V, Output>(
downstreamSet.add(downstream)
}
- final override suspend fun removeDownstream(downstream: Schedulable) {
+ final override fun removeDownstream(downstream: Schedulable) {
// TODO: return boolean?
- mutex.withLock { downstreamSet.remove(downstream) }
+ downstreamSet.remove(downstream)
}
- final override suspend fun removeDownstreamAndDeactivateIfNeeded(downstream: Schedulable) {
- val deactivate =
- mutex.withLock {
- downstreamSet.remove(downstream)
- downstreamSet.isEmpty()
- }
+ final override fun removeDownstreamAndDeactivateIfNeeded(downstream: Schedulable) {
+ downstreamSet.remove(downstream)
+ val deactivate = downstreamSet.isEmpty()
if (deactivate) {
doDeactivate()
}
}
- final override suspend fun deactivateIfNeeded() {
- if (mutex.withLock { downstreamSet.isEmpty() }) {
+ final override fun deactivateIfNeeded() {
+ if (downstreamSet.isEmpty()) {
doDeactivate()
}
}
/** visit this node from the scheduler (push eval) */
- abstract suspend fun visit(evalScope: EvalScope)
+ abstract fun visit(logIndent: Int, evalScope: EvalScope)
/** perform deactivation logic, propagating to all upstream nodes. */
- protected abstract suspend fun doDeactivate()
+ protected abstract fun doDeactivate()
- final override suspend fun scheduleDeactivationIfNeeded(evalScope: EvalScope) {
- if (mutex.withLock { downstreamSet.isEmpty() }) {
+ final override fun scheduleDeactivationIfNeeded(evalScope: EvalScope) {
+ if (downstreamSet.isEmpty()) {
evalScope.scheduleDeactivation(this)
}
}
- suspend fun adjustDirectUpstream(scheduler: Scheduler, oldDepth: Int, newDepth: Int) {
- mutex.withLock {
- if (depthTracker.addDirectUpstream(oldDepth, newDepth)) {
- depthTracker.schedule(scheduler, this)
- }
+ fun adjustDirectUpstream(scheduler: Scheduler, oldDepth: Int, newDepth: Int) {
+
+ if (depthTracker.addDirectUpstream(oldDepth, newDepth)) {
+ depthTracker.schedule(scheduler, this)
}
}
- suspend fun moveIndirectUpstreamToDirect(
+ fun moveIndirectUpstreamToDirect(
scheduler: Scheduler,
oldIndirectDepth: Int,
oldIndirectRoots: Set<MuxDeferredNode<*, *, *>>,
newDepth: Int,
) {
- mutex.withLock {
- if (
- depthTracker.addDirectUpstream(oldDepth = null, newDepth) or
- depthTracker.removeIndirectUpstream(depth = oldIndirectDepth) or
- depthTracker.updateIndirectRoots(removals = oldIndirectRoots)
- ) {
- depthTracker.schedule(scheduler, this)
- }
+ if (
+ depthTracker.addDirectUpstream(oldDepth = null, newDepth) or
+ depthTracker.removeIndirectUpstream(depth = oldIndirectDepth) or
+ depthTracker.updateIndirectRoots(removals = oldIndirectRoots)
+ ) {
+ depthTracker.schedule(scheduler, this)
}
}
- suspend fun adjustIndirectUpstream(
+ fun adjustIndirectUpstream(
scheduler: Scheduler,
oldDepth: Int,
newDepth: Int,
removals: Set<MuxDeferredNode<*, *, *>>,
additions: Set<MuxDeferredNode<*, *, *>>,
) {
- mutex.withLock {
- if (
- depthTracker.addIndirectUpstream(oldDepth, newDepth) or
- depthTracker.updateIndirectRoots(
- additions,
- removals,
- butNot = this as? MuxDeferredNode<*, *, *>,
- )
- ) {
- depthTracker.schedule(scheduler, this)
- }
+ if (
+ depthTracker.addIndirectUpstream(oldDepth, newDepth) or
+ depthTracker.updateIndirectRoots(
+ additions,
+ removals,
+ butNot = this as? MuxDeferredNode<*, *, *>,
+ )
+ ) {
+ depthTracker.schedule(scheduler, this)
}
}
- suspend fun moveDirectUpstreamToIndirect(
+ fun moveDirectUpstreamToIndirect(
scheduler: Scheduler,
oldDepth: Int,
newDepth: Int,
newIndirectSet: Set<MuxDeferredNode<*, *, *>>,
) {
- mutex.withLock {
- if (
- depthTracker.addIndirectUpstream(oldDepth = null, newDepth) or
- depthTracker.removeDirectUpstream(oldDepth) or
- depthTracker.updateIndirectRoots(
- additions = newIndirectSet,
- butNot = this as? MuxDeferredNode<*, *, *>,
- )
- ) {
- depthTracker.schedule(scheduler, this)
- }
+ if (
+ depthTracker.addIndirectUpstream(oldDepth = null, newDepth) or
+ depthTracker.removeDirectUpstream(oldDepth) or
+ depthTracker.updateIndirectRoots(
+ additions = newIndirectSet,
+ butNot = this as? MuxDeferredNode<*, *, *>,
+ )
+ ) {
+ depthTracker.schedule(scheduler, this)
}
}
- suspend fun removeDirectUpstream(scheduler: Scheduler, depth: Int, key: K) {
- mutex.withLock {
- switchedIn.remove(key)
- if (depthTracker.removeDirectUpstream(depth)) {
- depthTracker.schedule(scheduler, this)
- }
+ fun removeDirectUpstream(scheduler: Scheduler, depth: Int, key: K) {
+ switchedIn.remove(key)
+ if (depthTracker.removeDirectUpstream(depth)) {
+ depthTracker.schedule(scheduler, this)
}
}
- suspend fun removeIndirectUpstream(
+ fun removeIndirectUpstream(
scheduler: Scheduler,
oldDepth: Int,
indirectSet: Set<MuxDeferredNode<*, *, *>>,
key: K,
) {
- mutex.withLock {
- switchedIn.remove(key)
- if (
- depthTracker.removeIndirectUpstream(oldDepth) or
- depthTracker.updateIndirectRoots(removals = indirectSet)
- ) {
- depthTracker.schedule(scheduler, this)
- }
+ switchedIn.remove(key)
+ if (
+ depthTracker.removeIndirectUpstream(oldDepth) or
+ depthTracker.updateIndirectRoots(removals = indirectSet)
+ ) {
+ depthTracker.schedule(scheduler, this)
}
}
- suspend fun visitCompact(scheduler: Scheduler) = coroutineScope {
+ fun visitCompact(scheduler: Scheduler) {
if (depthTracker.isDirty()) {
- depthTracker.applyChanges(coroutineScope = this, scheduler, downstreamSet, this@MuxNode)
+ depthTracker.applyChanges(scheduler, downstreamSet, this@MuxNode)
}
}
@@ -217,22 +198,23 @@ internal sealed class MuxNode<W, K, V, Output>(
val schedulable = Schedulable.N(this)
- @Volatile lateinit var upstream: NodeConnection<V>
+ lateinit var upstream: NodeConnection<V>
- override suspend fun schedule(evalScope: EvalScope) {
- upstreamData[key] = upstream.directUpstream
- this@MuxNode.schedule(evalScope)
+ override fun schedule(logIndent: Int, evalScope: EvalScope) {
+ logDuration(logIndent, "MuxBranchNode.schedule") {
+ if (this@MuxNode is MuxPromptNode && this@MuxNode.name != null) {
+ logLn("[${this@MuxNode}] scheduling $key")
+ }
+ upstreamData[key] = upstream.directUpstream
+ this@MuxNode.schedule(evalScope)
+ }
}
- override suspend fun adjustDirectUpstream(
- scheduler: Scheduler,
- oldDepth: Int,
- newDepth: Int,
- ) {
+ override fun adjustDirectUpstream(scheduler: Scheduler, oldDepth: Int, newDepth: Int) {
this@MuxNode.adjustDirectUpstream(scheduler, oldDepth, newDepth)
}
- override suspend fun moveIndirectUpstreamToDirect(
+ override fun moveIndirectUpstreamToDirect(
scheduler: Scheduler,
oldIndirectDepth: Int,
oldIndirectSet: Set<MuxDeferredNode<*, *, *>>,
@@ -246,7 +228,7 @@ internal sealed class MuxNode<W, K, V, Output>(
)
}
- override suspend fun adjustIndirectUpstream(
+ override fun adjustIndirectUpstream(
scheduler: Scheduler,
oldDepth: Int,
newDepth: Int,
@@ -256,7 +238,7 @@ internal sealed class MuxNode<W, K, V, Output>(
this@MuxNode.adjustIndirectUpstream(scheduler, oldDepth, newDepth, removals, additions)
}
- override suspend fun moveDirectUpstreamToIndirect(
+ override fun moveDirectUpstreamToIndirect(
scheduler: Scheduler,
oldDirectDepth: Int,
newIndirectDepth: Int,
@@ -270,11 +252,11 @@ internal sealed class MuxNode<W, K, V, Output>(
)
}
- override suspend fun removeDirectUpstream(scheduler: Scheduler, depth: Int) {
+ override fun removeDirectUpstream(scheduler: Scheduler, depth: Int) {
removeDirectUpstream(scheduler, depth, key)
}
- override suspend fun removeIndirectUpstream(
+ override fun removeIndirectUpstream(
scheduler: Scheduler,
depth: Int,
indirectSet: Set<MuxDeferredNode<*, *, *>>,
@@ -286,113 +268,110 @@ internal sealed class MuxNode<W, K, V, Output>(
}
}
-internal typealias BranchNode<W, K, V> = MuxNode<W, K, V, *>.BranchNode
+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<A>(@Volatile var lifecycleState: MuxLifecycleState<A>) : TFlowImpl<A> {
- val mutex = Mutex()
+internal class MuxLifecycle<W, K, V>(var lifecycleState: MuxLifecycleState<W, K, V>) :
+ EventsImpl<MuxResult<W, K, V>> {
- override fun toString(): String = "TFlowLifecycle[$hashString][$lifecycleState][$mutex]"
+ override fun toString(): String = "MuxLifecycle[$hashString][$lifecycleState]"
- override suspend fun activate(
+ override fun activate(
evalScope: EvalScope,
downstream: Schedulable,
- ): ActivationResult<A>? =
- mutex.withLock {
- when (val state = lifecycleState) {
- is MuxLifecycleState.Dead -> null
- is MuxLifecycleState.Active -> {
- state.node.addDownstreamLocked(downstream)
- ActivationResult(
- connection = NodeConnection(state.node, state.node),
- needsEval = state.node.hasCurrentValueLocked(evalScope),
- )
- }
- is MuxLifecycleState.Inactive -> {
- state.spec
- .activate(evalScope, this@MuxLifecycle)
- .also { node ->
- lifecycleState =
- if (node == null) {
- MuxLifecycleState.Dead
- } else {
- MuxLifecycleState.Active(node)
- }
- }
- ?.let { node ->
- node.addDownstreamLocked(downstream)
- ActivationResult(
- connection = NodeConnection(node, node),
- needsEval = false,
- )
- }
- }
+ ): ActivationResult<MuxResult<W, K, V>>? =
+ when (val state = lifecycleState) {
+ is MuxLifecycleState.Dead -> {
+ null
+ }
+ is MuxLifecycleState.Active -> {
+ state.node.addDownstreamLocked(downstream)
+ ActivationResult(
+ connection = NodeConnection(state.node, state.node),
+ needsEval = state.node.hasCurrentValueLocked(evalScope),
+ )
+ }
+ is MuxLifecycleState.Inactive -> {
+ state.spec
+ .activate(evalScope, this@MuxLifecycle)
+ .also { node ->
+ lifecycleState =
+ if (node == null) {
+ MuxLifecycleState.Dead
+ } else {
+ MuxLifecycleState.Active(node.first)
+ }
+ }
+ ?.let { (node, postActivate) ->
+ postActivate?.invoke()
+ node.addDownstreamLocked(downstream)
+ ActivationResult(connection = NodeConnection(node, node), needsEval = false)
+ }
}
}
}
-internal sealed interface MuxLifecycleState<out A> {
- class Inactive<A>(val spec: MuxActivator<A>) : MuxLifecycleState<A> {
+internal sealed interface MuxLifecycleState<out W, out K, out V> {
+ class Inactive<W, K, V>(val spec: MuxActivator<W, K, V>) : MuxLifecycleState<W, K, V> {
override fun toString(): String = "Inactive"
}
- class Active<A>(val node: MuxNode<*, *, *, A>) : MuxLifecycleState<A> {
+ class Active<W, K, V>(val node: MuxNode<W, K, V>) : MuxLifecycleState<W, K, V> {
override fun toString(): String = "Active(node=$node)"
}
- data object Dead : MuxLifecycleState<Nothing>
+ data object Dead : MuxLifecycleState<Nothing, Nothing, Nothing>
}
-internal interface MuxActivator<A> {
- suspend fun activate(evalScope: EvalScope, lifecycle: MuxLifecycle<A>): MuxNode<*, *, *, A>?
+internal interface MuxActivator<W, K, V> {
+ fun activate(
+ evalScope: EvalScope,
+ lifecycle: MuxLifecycle<W, K, V>,
+ ): Pair<MuxNode<W, K, V>, (() -> Unit)?>?
}
-internal inline fun <A> MuxLifecycle(onSubscribe: MuxActivator<A>): TFlowImpl<A> =
- MuxLifecycle(MuxLifecycleState.Inactive(onSubscribe))
+internal inline fun <W, K, V> MuxLifecycle(
+ onSubscribe: MuxActivator<W, K, V>
+): EventsImpl<MuxResult<W, K, V>> = MuxLifecycle(MuxLifecycleState.Inactive(onSubscribe))
-internal fun <K, V> TFlowImpl<MuxResult<MapHolder.W, K, V>>.awaitValues(): TFlowImpl<Map<K, V>> =
- mapImpl({ this@awaitValues }) { results ->
- results.asMapHolder().unwrapped.mapValues { it.value.getPushEvent(this) }
+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) }
}
// activation logic
-internal suspend fun <W, K, V, O> MuxNode<W, K, V, O>.initializeUpstream(
+internal fun <W, K, V> MuxNode<W, K, V>.initializeUpstream(
evalScope: EvalScope,
- getStorage: suspend EvalScope.() -> Iterable<Map.Entry<K, TFlowImpl<V>>>,
+ getStorage: EvalScope.() -> Iterable<Map.Entry<K, EventsImpl<V>>>,
storeFactory: MutableMapK.Factory<W, K>,
) {
val storage = getStorage(evalScope)
- coroutineScope {
- val initUpstream = buildList {
- storage.forEach { (key, flow) ->
- val branchNode = BranchNode(key)
- add(
- asyncImmediate(start = CoroutineStart.LAZY) {
- flow.activate(evalScope, branchNode.schedulable)?.let { (conn, needsEval) ->
- Triple(
- key,
- branchNode.apply { upstream = conn },
- if (needsEval) conn.directUpstream else null,
- )
- }
- }
- )
- }
+ val initUpstream = buildList {
+ storage.forEach { (key, events) ->
+ val branchNode = BranchNode(key)
+ add(
+ events.activate(evalScope, branchNode.schedulable)?.let { (conn, needsEval) ->
+ Triple(
+ key,
+ branchNode.apply { upstream = conn },
+ if (needsEval) conn.directUpstream else null,
+ )
+ }
+ )
}
- val results = initUpstream.awaitAll()
- switchedIn = storeFactory.create(initUpstream.size)
- upstreamData = storeFactory.create(initUpstream.size)
- for (triple in results) {
- triple?.let { (key, branch, upstream) ->
- switchedIn[key] = branch
- upstream?.let { upstreamData[key] = upstream }
- }
+ }
+ switchedIn = storeFactory.create(initUpstream.size)
+ upstreamData = storeFactory.create(initUpstream.size)
+ for (triple in initUpstream) {
+ triple?.let { (key, branch, upstream) ->
+ switchedIn[key] = branch
+ upstream?.let { upstreamData[key] = upstream }
}
}
}
-internal fun <W, K, V, O> MuxNode<W, K, V, O>.initializeDepth() {
+internal fun <W, K, V> MuxNode<W, K, V>.initializeDepth() {
switchedIn.forEach { (_, branch) ->
val conn = branch.upstream
if (conn.depthTracker.snapshotIsDirect) {
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 7f40df508fb1..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
@@ -16,15 +16,17 @@
package com.android.systemui.kairos.internal
+import com.android.systemui.kairos.internal.store.MapK
import com.android.systemui.kairos.internal.store.MutableArrayMapK
import com.android.systemui.kairos.internal.store.MutableMapK
import com.android.systemui.kairos.internal.store.SingletonMapK
import com.android.systemui.kairos.internal.store.StoreEntry
import com.android.systemui.kairos.internal.store.asArrayHolder
+import com.android.systemui.kairos.internal.store.asSingle
import com.android.systemui.kairos.internal.store.singleOf
-import com.android.systemui.kairos.internal.util.Key
import com.android.systemui.kairos.internal.util.hashString
-import com.android.systemui.kairos.internal.util.mapParallel
+import com.android.systemui.kairos.internal.util.logDuration
+import com.android.systemui.kairos.internal.util.logLn
import com.android.systemui.kairos.util.Just
import com.android.systemui.kairos.util.Maybe
import com.android.systemui.kairos.util.None
@@ -37,41 +39,49 @@ import com.android.systemui.kairos.util.maybeThis
import com.android.systemui.kairos.util.merge
import com.android.systemui.kairos.util.orError
import com.android.systemui.kairos.util.these
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.sync.withLock
internal class MuxDeferredNode<W, K, V>(
- lifecycle: MuxLifecycle<MuxResult<W, K, V>>,
- val spec: MuxActivator<MuxResult<W, K, V>>,
+ val name: String?,
+ lifecycle: MuxLifecycle<W, K, V>,
+ val spec: MuxActivator<W, K, V>,
factory: MutableMapK.Factory<W, K>,
-) : MuxNode<W, K, V, MuxResult<W, K, V>>(lifecycle, factory), Key<MuxResult<W, K, V>> {
+) : MuxNode<W, K, V>(lifecycle, factory) {
val schedulable = Schedulable.M(this)
-
- @Volatile var patches: NodeConnection<Iterable<Map.Entry<K, Maybe<TFlowImpl<V>>>>>? = null
- @Volatile var patchData: Iterable<Map.Entry<K, Maybe<TFlowImpl<V>>>>? = null
-
- override suspend fun visit(evalScope: EvalScope) {
- val scheduleDownstream = upstreamData.isNotEmpty()
- val result = upstreamData.readOnlyCopy()
- upstreamData.clear()
- val compactDownstream = depthTracker.isDirty()
- if (scheduleDownstream || compactDownstream) {
- coroutineScope {
- mutex.withLock {
- if (compactDownstream) {
+ 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" }
+ logDuration(logIndent, "MuxDeferred[$name].visit") {
+ val scheduleDownstream: Boolean
+ val result: MapK<W, K, PullNode<V>>
+ logDuration("copying upstream data", false) {
+ scheduleDownstream = upstreamData.isNotEmpty()
+ result = upstreamData.readOnlyCopy()
+ upstreamData.clear()
+ }
+ if (name != null) {
+ logLn("[${this@MuxDeferredNode}] result = $result")
+ }
+ val compactDownstream = depthTracker.isDirty()
+ if (scheduleDownstream || compactDownstream) {
+ if (compactDownstream) {
+ logDuration("compactDownstream", false) {
depthTracker.applyChanges(
- coroutineScope = this,
evalScope.scheduler,
downstreamSet,
muxNode = this@MuxDeferredNode,
)
}
- if (scheduleDownstream) {
- epoch = evalScope.epoch
- evalScope.setResult(this@MuxDeferredNode, result)
- if (!scheduleAll(downstreamSet, evalScope)) {
+ }
+ if (scheduleDownstream) {
+ logDuration("scheduleDownstream") {
+ if (name != null) {
+ logLn("[${this@MuxDeferredNode}] scheduling")
+ }
+ transactionCache.put(evalScope, result)
+ if (!scheduleAll(currentLogIndent, downstreamSet, evalScope)) {
evalScope.scheduleDeactivation(this@MuxDeferredNode)
}
}
@@ -80,26 +90,26 @@ internal class MuxDeferredNode<W, K, V>(
}
}
- override suspend fun getPushEvent(evalScope: EvalScope): MuxResult<W, K, V> =
- evalScope.getCurrentValue(key = this)
+ override fun getPushEvent(logIndent: Int, evalScope: EvalScope): MuxResult<W, K, V> =
+ logDuration(logIndent, "MuxDeferred.getPushEvent") {
+ transactionCache.getCurrentValue(evalScope).also {
+ if (name != null) {
+ logLn("[${this@MuxDeferredNode}] getPushEvent = $it")
+ }
+ }
+ }
- private suspend fun compactIfNeeded(evalScope: EvalScope) {
+ private fun compactIfNeeded(evalScope: EvalScope) {
depthTracker.propagateChanges(evalScope.compactor, this)
}
- override suspend fun doDeactivate() {
+ override fun doDeactivate() {
// Update lifecycle
- lifecycle.mutex.withLock {
- if (lifecycle.lifecycleState !is MuxLifecycleState.Active) return@doDeactivate
- lifecycle.lifecycleState = MuxLifecycleState.Inactive(spec)
- }
+ if (lifecycle.lifecycleState !is MuxLifecycleState.Active) return@doDeactivate
+ lifecycle.lifecycleState = MuxLifecycleState.Inactive(spec)
// Process branch nodes
- coroutineScope {
- switchedIn.forEach { (_, branchNode) ->
- branchNode.upstream.let {
- launch { it.removeDownstreamAndDeactivateIfNeeded(branchNode.schedulable) }
- }
- }
+ switchedIn.forEach { (_, branchNode) ->
+ branchNode.upstream.removeDownstreamAndDeactivateIfNeeded(branchNode.schedulable)
}
// Process patch node
patches?.removeDownstreamAndDeactivateIfNeeded(schedulable)
@@ -108,15 +118,18 @@ internal class MuxDeferredNode<W, K, V>(
// MOVE phase
// - concurrent moves may be occurring, but no more evals. all depth recalculations are
// deferred to the end of this phase.
- suspend fun performMove(evalScope: EvalScope) {
+ fun performMove(logIndent: Int, evalScope: EvalScope) {
+ if (name != null) {
+ logLn(logIndent, "[${this@MuxDeferredNode}] performMove (patchData = $patchData)")
+ }
+
val patch = patchData ?: return
patchData = null
- // TODO: this logic is very similar to what's in MuxPromptMoving, maybe turn into an inline
- // fun?
+ // 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) {
@@ -127,131 +140,112 @@ internal class MuxDeferredNode<W, K, V>(
val severed = mutableListOf<NodeConnection<*>>()
- coroutineScope {
- // remove and sever
- removes.forEach { k ->
- switchedIn.remove(k)?.let { branchNode: BranchNode ->
- val conn = branchNode.upstream
- severed.add(conn)
- launch { conn.removeDownstream(downstream = branchNode.schedulable) }
- depthTracker.removeDirectUpstream(conn.depthTracker.snapshotDirectDepth)
- }
+ // remove and sever
+ removes.forEach { k ->
+ switchedIn.remove(k)?.let { branchNode: BranchNode ->
+ val conn = branchNode.upstream
+ severed.add(conn)
+ conn.removeDownstream(downstream = branchNode.schedulable)
+ depthTracker.removeDirectUpstream(conn.depthTracker.snapshotDirectDepth)
}
+ }
- // add or replace
- adds
- .mapParallel { (k, newUpstream: TFlowImpl<V>) ->
- val branchNode = BranchNode(k)
- k to
- newUpstream.activate(evalScope, branchNode.schedulable)?.let { (conn, _) ->
- branchNode.apply { upstream = conn }
- }
- }
- .forEach { (k, newBranch: BranchNode?) ->
- // remove old and sever, if present
- switchedIn.remove(k)?.let { branchNode ->
- val conn = branchNode.upstream
- severed.add(conn)
- launch { conn.removeDownstream(downstream = branchNode.schedulable) }
- depthTracker.removeDirectUpstream(conn.depthTracker.snapshotDirectDepth)
- }
+ // add or replace
+ adds.forEach { (k, newUpstream: EventsImpl<V>) ->
+ // remove old and sever, if present
+ switchedIn.remove(k)?.let { branchNode ->
+ val conn = branchNode.upstream
+ severed.add(conn)
+ conn.removeDownstream(downstream = branchNode.schedulable)
+ depthTracker.removeDirectUpstream(conn.depthTracker.snapshotDirectDepth)
+ }
- // add new
- newBranch?.let {
- switchedIn[k] = newBranch
- val branchDepthTracker = newBranch.upstream.depthTracker
- if (branchDepthTracker.snapshotIsDirect) {
- depthTracker.addDirectUpstream(
- oldDepth = null,
- newDepth = branchDepthTracker.snapshotDirectDepth,
- )
- } else {
- depthTracker.addIndirectUpstream(
- oldDepth = null,
- newDepth = branchDepthTracker.snapshotIndirectDepth,
- )
- depthTracker.updateIndirectRoots(
- additions = branchDepthTracker.snapshotIndirectRoots,
- butNot = this@MuxDeferredNode,
- )
- }
- }
+ // add new
+ val newBranch = BranchNode(k)
+ newUpstream.activate(evalScope, newBranch.schedulable)?.let { (conn, _) ->
+ newBranch.upstream = conn
+ switchedIn[k] = newBranch
+ val branchDepthTracker = newBranch.upstream.depthTracker
+ if (branchDepthTracker.snapshotIsDirect) {
+ depthTracker.addDirectUpstream(
+ oldDepth = null,
+ newDepth = branchDepthTracker.snapshotDirectDepth,
+ )
+ } else {
+ depthTracker.addIndirectUpstream(
+ oldDepth = null,
+ newDepth = branchDepthTracker.snapshotIndirectDepth,
+ )
+ depthTracker.updateIndirectRoots(
+ additions = branchDepthTracker.snapshotIndirectRoots,
+ butNot = this@MuxDeferredNode,
+ )
}
+ }
}
- coroutineScope {
- for (severedNode in severed) {
- launch { severedNode.scheduleDeactivationIfNeeded(evalScope) }
- }
+ for (severedNode in severed) {
+ severedNode.scheduleDeactivationIfNeeded(evalScope)
}
compactIfNeeded(evalScope)
}
- suspend fun removeDirectPatchNode(scheduler: Scheduler) {
- mutex.withLock {
- if (
- depthTracker.removeIndirectUpstream(depth = 0) or
- depthTracker.setIsIndirectRoot(false)
- ) {
- depthTracker.schedule(scheduler, this)
- }
- patches = null
+ fun removeDirectPatchNode(scheduler: Scheduler) {
+ if (
+ depthTracker.removeIndirectUpstream(depth = 0) or depthTracker.setIsIndirectRoot(false)
+ ) {
+ depthTracker.schedule(scheduler, this)
}
+ patches = null
}
- suspend fun removeIndirectPatchNode(
+ fun removeIndirectPatchNode(
scheduler: Scheduler,
depth: Int,
indirectSet: Set<MuxDeferredNode<*, *, *>>,
) {
// indirectly connected patches forward the indirectSet
- mutex.withLock {
- if (
- depthTracker.updateIndirectRoots(removals = indirectSet) or
- depthTracker.removeIndirectUpstream(depth)
- ) {
- depthTracker.schedule(scheduler, this)
- }
- patches = null
+ if (
+ depthTracker.updateIndirectRoots(removals = indirectSet) or
+ depthTracker.removeIndirectUpstream(depth)
+ ) {
+ depthTracker.schedule(scheduler, this)
}
+ patches = null
}
- suspend fun moveIndirectPatchNodeToDirect(
+ fun moveIndirectPatchNodeToDirect(
scheduler: Scheduler,
oldIndirectDepth: Int,
oldIndirectSet: Set<MuxDeferredNode<*, *, *>>,
) {
// directly connected patches are stored as an indirect singleton set of the patchNode
- mutex.withLock {
- if (
- depthTracker.updateIndirectRoots(removals = oldIndirectSet) or
- depthTracker.removeIndirectUpstream(oldIndirectDepth) or
- depthTracker.setIsIndirectRoot(true)
- ) {
- depthTracker.schedule(scheduler, this)
- }
+ if (
+ depthTracker.updateIndirectRoots(removals = oldIndirectSet) or
+ depthTracker.removeIndirectUpstream(oldIndirectDepth) or
+ depthTracker.setIsIndirectRoot(true)
+ ) {
+ depthTracker.schedule(scheduler, this)
}
}
- suspend fun moveDirectPatchNodeToIndirect(
+ fun moveDirectPatchNodeToIndirect(
scheduler: Scheduler,
newIndirectDepth: Int,
newIndirectSet: Set<MuxDeferredNode<*, *, *>>,
) {
// indirectly connected patches forward the indirectSet
- mutex.withLock {
- if (
- depthTracker.setIsIndirectRoot(false) or
- depthTracker.updateIndirectRoots(additions = newIndirectSet, butNot = this) or
- depthTracker.addIndirectUpstream(oldDepth = null, newDepth = newIndirectDepth)
- ) {
- depthTracker.schedule(scheduler, this)
- }
+ if (
+ depthTracker.setIsIndirectRoot(false) or
+ depthTracker.updateIndirectRoots(additions = newIndirectSet, butNot = this) or
+ depthTracker.addIndirectUpstream(oldDepth = null, newDepth = newIndirectDepth)
+ ) {
+ depthTracker.schedule(scheduler, this)
}
}
- suspend fun adjustIndirectPatchNode(
+ fun adjustIndirectPatchNode(
scheduler: Scheduler,
oldDepth: Int,
newDepth: Int,
@@ -259,65 +253,73 @@ internal class MuxDeferredNode<W, K, V>(
additions: Set<MuxDeferredNode<*, *, *>>,
) {
// indirectly connected patches forward the indirectSet
- mutex.withLock {
- if (
- depthTracker.updateIndirectRoots(
- additions = additions,
- removals = removals,
- butNot = this,
- ) or depthTracker.addIndirectUpstream(oldDepth = oldDepth, newDepth = newDepth)
- ) {
- depthTracker.schedule(scheduler, this)
- }
+ if (
+ depthTracker.updateIndirectRoots(
+ additions = additions,
+ removals = removals,
+ butNot = this,
+ ) or depthTracker.addIndirectUpstream(oldDepth = oldDepth, newDepth = newDepth)
+ ) {
+ depthTracker.schedule(scheduler, this)
}
}
- suspend fun scheduleMover(evalScope: EvalScope) {
- patchData =
- checkNotNull(patches) { "mux mover scheduled with unset patches upstream node" }
- .getPushEvent(evalScope)
- evalScope.scheduleMuxMover(this)
+ fun scheduleMover(logIndent: Int, evalScope: EvalScope) {
+ logDuration(logIndent, "MuxDeferred.scheduleMover") {
+ patchData =
+ checkNotNull(patches) { "mux mover scheduled with unset patches upstream node" }
+ .getPushEvent(currentLogIndent, evalScope)
+ evalScope.scheduleMuxMover(this@MuxDeferredNode)
+ }
}
- override fun toString(): String = "${this::class.simpleName}@$hashString"
+ override fun toString(): String =
+ "${this::class.simpleName}@$hashString${name?.let { "[$it]" }.orEmpty()}"
}
internal inline fun <A> switchDeferredImplSingle(
- crossinline getStorage: suspend EvalScope.() -> TFlowImpl<A>,
- crossinline getPatches: suspend EvalScope.() -> TFlowImpl<TFlowImpl<A>>,
-): TFlowImpl<A> =
- mapImpl({
+ name: String? = null,
+ 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(
+ name = name,
getStorage = { singleOf(getStorage()).asIterable() },
- getPatches = {
- mapImpl(getPatches) { newFlow -> singleOf(just(newFlow)).asIterable() }
- },
+ getPatches = { patches },
storeFactory = SingletonMapK.Factory(),
)
- }) { map ->
- map.getValue(Unit).getPushEvent(this)
+ return mapImpl({ switchDeferredImpl }) { map, logIndent ->
+ map.asSingle().getValue(Unit).getPushEvent(logIndent, this).also {
+ if (name != null) {
+ logLn(logIndent, "[$name] extracting single mux: $it")
+ }
+ }
}
+}
internal fun <W, K, V> switchDeferredImpl(
- getStorage: suspend EvalScope.() -> Iterable<Map.Entry<K, TFlowImpl<V>>>,
- getPatches: suspend EvalScope.() -> TFlowImpl<Iterable<Map.Entry<K, Maybe<TFlowImpl<V>>>>>,
+ name: String? = null,
+ 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>> =
- MuxLifecycle(MuxDeferredActivator(getStorage, storeFactory, getPatches))
+): EventsImpl<MuxResult<W, K, V>> =
+ MuxLifecycle(MuxDeferredActivator(name, getStorage, storeFactory, getPatches))
private class MuxDeferredActivator<W, K, V>(
- private val getStorage: suspend EvalScope.() -> Iterable<Map.Entry<K, TFlowImpl<V>>>,
+ private val name: String?,
+ private val getStorage: EvalScope.() -> Iterable<Map.Entry<K, EventsImpl<V>>>,
private val storeFactory: MutableMapK.Factory<W, K>,
- private val getPatches:
- suspend EvalScope.() -> TFlowImpl<Iterable<Map.Entry<K, Maybe<TFlowImpl<V>>>>>,
-) : MuxActivator<MuxResult<W, K, V>> {
- override suspend fun activate(
+ private val getPatches: EvalScope.() -> EventsImpl<Iterable<Map.Entry<K, Maybe<EventsImpl<V>>>>>,
+) : MuxActivator<W, K, V> {
+ override fun activate(
evalScope: EvalScope,
- lifecycle: MuxLifecycle<MuxResult<W, K, V>>,
- ): MuxNode<W, *, *, MuxResult<W, K, V>>? {
+ lifecycle: MuxLifecycle<W, K, V>,
+ ): Pair<MuxNode<W, K, V>, (() -> Unit)?>? {
// Initialize mux node and switched-in connections.
val muxNode =
- MuxDeferredNode(lifecycle, this, storeFactory).apply {
+ MuxDeferredNode(name, lifecycle, this, storeFactory).apply {
initializeUpstream(evalScope, getStorage, storeFactory)
// Update depth based on all initial switched-in nodes.
initializeDepth()
@@ -327,29 +329,34 @@ private class MuxDeferredActivator<W, K, V>(
depthTracker.setIsIndirectRoot(true)
depthTracker.reset()
}
- // Setup patches connection; deferring allows for a recursive connection, where
- // muxNode is downstream of itself via patches.
- var isIndirect = true
- evalScope.deferAction {
- val (patchesConn, needsEval) =
- getPatches(evalScope).activate(evalScope, downstream = muxNode.schedulable)
- ?: run {
- isIndirect = false
- // Turns out we can't connect to patches, so update our depth and
- // propagate
- muxNode.mutex.withLock {
+
+ // Schedule for evaluation if any switched-in nodes have already emitted within
+ // this transaction.
+ if (muxNode.upstreamData.isNotEmpty()) {
+ muxNode.schedule(evalScope)
+ }
+
+ return muxNode to
+ fun() {
+ // Setup patches connection; deferring allows for a recursive connection, where
+ // muxNode is downstream of itself via patches.
+ val (patchesConn, needsEval) =
+ getPatches(evalScope).activate(evalScope, downstream = muxNode.schedulable)
+ ?: run {
+ // Turns out we can't connect to patches, so update our depth and
+ // propagate
if (muxNode.depthTracker.setIsIndirectRoot(false)) {
+ // TODO: schedules might not be necessary now that we're not
+ // parallel?
muxNode.depthTracker.schedule(evalScope.scheduler, muxNode)
}
+ return
}
- return@deferAction
- }
- muxNode.patches = patchesConn
+ muxNode.patches = patchesConn
- if (!patchesConn.schedulerUpstream.depthTracker.snapshotIsDirect) {
- // Turns out patches is indirect, so we are not a root. Update depth and
- // propagate.
- muxNode.mutex.withLock {
+ if (!patchesConn.schedulerUpstream.depthTracker.snapshotIsDirect) {
+ // Turns out patches is indirect, so we are not a root. Update depth and
+ // propagate.
if (
muxNode.depthTracker.setIsIndirectRoot(false) or
muxNode.depthTracker.addIndirectUpstream(
@@ -363,64 +370,64 @@ private class MuxDeferredActivator<W, K, V>(
muxNode.depthTracker.schedule(evalScope.scheduler, muxNode)
}
}
+ // Schedule mover to process patch emission at the end of this transaction, if
+ // needed.
+ if (needsEval) {
+ muxNode.patchData = patchesConn.getPushEvent(0, evalScope)
+ evalScope.scheduleMuxMover(muxNode)
+ }
}
- // Schedule mover to process patch emission at the end of this transaction, if
- // needed.
- if (needsEval) {
- muxNode.patchData = patchesConn.getPushEvent(evalScope)
- evalScope.scheduleMuxMover(muxNode)
- }
- }
-
- // Schedule for evaluation if any switched-in nodes have already emitted within
- // this transaction.
- if (muxNode.upstreamData.isNotEmpty()) {
- muxNode.schedule(evalScope)
- }
- return muxNode.takeUnless { muxNode.switchedIn.isEmpty() && !isIndirect }
}
}
internal inline fun <A> mergeNodes(
- crossinline getPulse: suspend EvalScope.() -> TFlowImpl<A>,
- crossinline getOther: suspend EvalScope.() -> TFlowImpl<A>,
- crossinline f: suspend EvalScope.(A, A) -> A,
-): TFlowImpl<A> {
+ crossinline getPulse: EvalScope.() -> EventsImpl<A>,
+ crossinline getOther: EvalScope.() -> EventsImpl<A>,
+ name: String? = null,
+ crossinline f: EvalScope.(A, A) -> A,
+): EventsImpl<A> {
+ val mergedThese = mergeNodes(name, getPulse, getOther)
val merged =
- mapImpl({ mergeNodes(getPulse, getOther) }) { these ->
- these.merge { thiz, that -> f(thiz, that) }
- }
+ mapImpl({ mergedThese }) { these, _ -> these.merge { thiz, that -> f(thiz, that) } }
return merged.cached()
}
-internal fun <T> Iterable<T>.asIterableWithIndex(): Iterable<StoreEntry<Int, T>> =
+internal fun <T> Iterable<T>.asIterableWithIndex(): Iterable<Map.Entry<Int, T>> =
asSequence().mapIndexed { i, t -> StoreEntry(i, t) }.asIterable()
internal inline fun <A, B> mergeNodes(
- crossinline getPulse: suspend EvalScope.() -> TFlowImpl<A>,
- crossinline getOther: suspend EvalScope.() -> TFlowImpl<B>,
-): TFlowImpl<These<A, B>> {
+ name: String? = null,
+ crossinline getPulse: EvalScope.() -> EventsImpl<A>,
+ crossinline getOther: EvalScope.() -> EventsImpl<B>,
+): EventsImpl<These<A, B>> {
val storage =
- listOf(mapImpl(getPulse) { These.thiz<A, B>(it) }, mapImpl(getOther) { These.that(it) })
+ listOf(
+ mapImpl(getPulse) { it, _ -> These.thiz<A, B>(it) },
+ mapImpl(getOther) { it, _ -> These.that(it) },
+ )
.asIterableWithIndex()
val switchNode =
switchDeferredImpl(
+ name = name,
getStorage = { storage },
getPatches = { neverImpl },
storeFactory = MutableArrayMapK.Factory(),
)
val merged =
- mapImpl({ switchNode }) { mergeResults ->
- val first = mergeResults.getMaybe(0).flatMap { it.getPushEvent(this).maybeThis() }
- val second = mergeResults.getMaybe(1).flatMap { it.getPushEvent(this).maybeThat() }
+ mapImpl({ switchNode }) { it, logIndent ->
+ val mergeResults = it.asArrayHolder()
+ val first =
+ mergeResults.getMaybe(0).flatMap { it.getPushEvent(logIndent, this).maybeThis() }
+ val second =
+ mergeResults.getMaybe(1).flatMap { it.getPushEvent(logIndent, this).maybeThat() }
these(first, second).orError { "unexpected missing merge result" }
}
return merged.cached()
}
internal inline fun <A> mergeNodes(
- crossinline getPulses: suspend EvalScope.() -> Iterable<TFlowImpl<A>>
-): TFlowImpl<List<A>> {
+ crossinline getPulses: EvalScope.() -> Iterable<EventsImpl<A>>
+): EventsImpl<List<A>> {
val switchNode =
switchDeferredImpl(
getStorage = { getPulses().asIterableWithIndex() },
@@ -428,16 +435,16 @@ internal inline fun <A> mergeNodes(
storeFactory = MutableArrayMapK.Factory(),
)
val merged =
- mapImpl({ switchNode }) {
+ mapImpl({ switchNode }) { it, logIndent ->
val mergeResults = it.asArrayHolder()
- mergeResults.map { (_, node) -> node.getPushEvent(this) }
+ mergeResults.map { (_, node) -> node.getPushEvent(logIndent, this) }
}
return merged.cached()
}
internal inline fun <A> mergeNodesLeft(
- crossinline getPulses: suspend EvalScope.() -> Iterable<TFlowImpl<A>>
-): TFlowImpl<A> {
+ crossinline getPulses: EvalScope.() -> Iterable<EventsImpl<A>>
+): EventsImpl<A> {
val switchNode =
switchDeferredImpl(
getStorage = { getPulses().asIterableWithIndex() },
@@ -445,6 +452,9 @@ internal inline fun <A> mergeNodesLeft(
storeFactory = MutableArrayMapK.Factory(),
)
val merged =
- mapImpl({ switchNode }) { mergeResults -> mergeResults.values.first().getPushEvent(this) }
+ mapImpl({ switchNode }) { it, logIndent ->
+ val mergeResults = it.asArrayHolder()
+ mergeResults.values.first().getPushEvent(logIndent, this)
+ }
return merged.cached()
}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxPrompt.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxPrompt.kt
index 839c5a64272a..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
@@ -18,207 +18,180 @@ package com.android.systemui.kairos.internal
import com.android.systemui.kairos.internal.store.MutableMapK
import com.android.systemui.kairos.internal.store.SingletonMapK
+import com.android.systemui.kairos.internal.store.asSingle
import com.android.systemui.kairos.internal.store.singleOf
-import com.android.systemui.kairos.internal.util.Key
-import com.android.systemui.kairos.internal.util.launchImmediate
-import com.android.systemui.kairos.internal.util.mapParallel
+import com.android.systemui.kairos.internal.util.LogIndent
+import com.android.systemui.kairos.internal.util.hashString
+import com.android.systemui.kairos.internal.util.logDuration
import com.android.systemui.kairos.util.Just
import com.android.systemui.kairos.util.Maybe
import com.android.systemui.kairos.util.None
import com.android.systemui.kairos.util.just
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.sync.withLock
-private typealias MuxPromptMovingResult<W, K, V> = Pair<MuxResult<W, K, V>, MuxResult<W, K, V>?>
-
-internal class MuxPromptMovingNode<W, K, V>(
- lifecycle: MuxLifecycle<MuxPromptMovingResult<W, K, V>>,
- private val spec: MuxActivator<MuxPromptMovingResult<W, K, V>>,
+internal class MuxPromptNode<W, K, V>(
+ val name: String?,
+ lifecycle: MuxLifecycle<W, K, V>,
+ private val spec: MuxActivator<W, K, V>,
factory: MutableMapK.Factory<W, K>,
-) :
- MuxNode<W, K, V, MuxPromptMovingResult<W, K, V>>(lifecycle, factory),
- Key<MuxPromptMovingResult<W, K, V>> {
-
- @Volatile var patchData: Iterable<Map.Entry<K, Maybe<TFlowImpl<V>>>>? = null
- @Volatile var patches: PatchNode? = null
+) : MuxNode<W, K, V>(lifecycle, factory) {
- @Volatile private var reEval: MuxPromptMovingResult<W, K, V>? = null
+ var patchData: Iterable<Map.Entry<K, Maybe<EventsImpl<V>>>>? = null
+ var patches: PatchNode? = null
- override suspend fun visit(evalScope: EvalScope) {
- val preSwitchNotEmpty = upstreamData.isNotEmpty()
- val preSwitchResults: MuxResult<W, K, V> = upstreamData.readOnlyCopy()
- upstreamData.clear()
+ 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<EventsImpl<V>>>>? = patchData
+ patchData = null
- val patch: Iterable<Map.Entry<K, Maybe<TFlowImpl<V>>>>? = patchData
- patchData = null
-
- val (reschedule, evalResult) =
- reEval?.let { false to it }
- ?: if (preSwitchNotEmpty || patch != null) {
- doEval(preSwitchNotEmpty, preSwitchResults, patch, evalScope)
- } else {
- false to null
+ // If there's a patch, process it.
+ patch?.let {
+ val needsReschedule = processPatch(patch, evalScope)
+ // We may need to reschedule if newly-switched-in nodes have not yet been
+ // visited within this transaction.
+ val depthIncreased = depthTracker.dirty_depthIncreased()
+ if (needsReschedule || depthIncreased) {
+ if (depthIncreased) {
+ depthTracker.schedule(evalScope.compactor, this@MuxPromptNode)
+ }
+ if (name != null) {
+ logLn(
+ "[${this@MuxPromptNode}] rescheduling (reschedule=$needsReschedule, depthIncrease=$depthIncreased)"
+ )
+ }
+ schedule(evalScope)
+ return
}
- reEval = null
-
- if (reschedule || depthTracker.dirty_depthIncreased()) {
- reEval = evalResult
- // Can't schedule downstream yet, need to compact first
- if (depthTracker.dirty_depthIncreased()) {
- depthTracker.schedule(evalScope.compactor, node = this)
}
- schedule(evalScope)
- } else {
+ val results = upstreamData.readOnlyCopy().also { upstreamData.clear() }
+
+ // If we don't need to reschedule, or there wasn't a patch at all, then we proceed
+ // with merging pre-switch and post-switch results
+ val hasResult = results.isNotEmpty()
val compactDownstream = depthTracker.isDirty()
- if (evalResult != null || compactDownstream) {
- coroutineScope {
- mutex.withLock {
- if (compactDownstream) {
- adjustDownstreamDepths(evalScope, coroutineScope = this)
- }
- if (evalResult != null) {
- epoch = evalScope.epoch
- evalScope.setResult(this@MuxPromptMovingNode, evalResult)
- if (!scheduleAll(downstreamSet, evalScope)) {
- evalScope.scheduleDeactivation(this@MuxPromptMovingNode)
- }
- }
+ if (hasResult || compactDownstream) {
+ if (compactDownstream) {
+ adjustDownstreamDepths(evalScope)
+ }
+ if (hasResult) {
+ transactionCache.put(evalScope, results)
+ if (!scheduleAll(currentLogIndent, downstreamSet, evalScope)) {
+ evalScope.scheduleDeactivation(this@MuxPromptNode)
}
}
}
}
}
- private suspend fun doEval(
- preSwitchNotEmpty: Boolean,
- preSwitchResults: MuxResult<W, K, V>,
- patch: Iterable<Map.Entry<K, Maybe<TFlowImpl<V>>>>?,
+ // side-effect: this will populate `upstreamData` with any immediately available results
+ private fun LogIndent.processPatch(
+ patch: Iterable<Map.Entry<K, Maybe<EventsImpl<V>>>>,
evalScope: EvalScope,
- ): Pair<Boolean, MuxPromptMovingResult<W, K, V>?> {
- val newlySwitchedIn: MuxResult<W, K, V>? =
- patch?.let {
- // We have a patch, process additions/updates and removals
- val adds = mutableListOf<Pair<K, TFlowImpl<V>>>()
- val removes = mutableListOf<K>()
- patch.forEach { (k, newUpstream) ->
- when (newUpstream) {
- is Just -> adds.add(k to newUpstream.value)
- None -> removes.add(k)
- }
- }
+ ): Boolean {
+ var needsReschedule = false
+ // We have a patch, process additions/updates and removals
+ val adds = mutableListOf<Pair<K, EventsImpl<V>>>()
+ val removes = mutableListOf<K>()
+ patch.forEach { (k, newUpstream) ->
+ when (newUpstream) {
+ is Just -> adds.add(k to newUpstream.value)
+ None -> removes.add(k)
+ }
+ }
- val additionsAndUpdates = mutableListOf<Pair<K, PullNode<V>>>()
- val severed = mutableListOf<NodeConnection<*>>()
-
- coroutineScope {
- // remove and sever
- removes.forEach { k ->
- switchedIn.remove(k)?.let { branchNode: BranchNode ->
- val conn: NodeConnection<V> = branchNode.upstream
- severed.add(conn)
- launchImmediate {
- conn.removeDownstream(downstream = branchNode.schedulable)
- }
- depthTracker.removeDirectUpstream(conn.depthTracker.snapshotDirectDepth)
- }
- }
+ val severed = mutableListOf<NodeConnection<*>>()
- // add or replace
- adds
- .mapParallel { (k, newUpstream: TFlowImpl<V>) ->
- val branchNode = BranchNode(k)
- k to
- newUpstream.activate(evalScope, branchNode.schedulable)?.let {
- (conn, _) ->
- branchNode.apply { upstream = conn }
- }
- }
- .forEach { (k, newBranch: BranchNode?) ->
- // remove old and sever, if present
- switchedIn.remove(k)?.let { oldBranch: BranchNode ->
- val conn: NodeConnection<V> = oldBranch.upstream
- severed.add(conn)
- launchImmediate {
- conn.removeDownstream(downstream = oldBranch.schedulable)
- }
- depthTracker.removeDirectUpstream(
- conn.depthTracker.snapshotDirectDepth
- )
- }
-
- // add new
- newBranch?.let {
- switchedIn[k] = newBranch
- additionsAndUpdates.add(k to newBranch.upstream.directUpstream)
- val branchDepthTracker = newBranch.upstream.depthTracker
- if (branchDepthTracker.snapshotIsDirect) {
- depthTracker.addDirectUpstream(
- oldDepth = null,
- newDepth = branchDepthTracker.snapshotDirectDepth,
- )
- } else {
- depthTracker.addIndirectUpstream(
- oldDepth = null,
- newDepth = branchDepthTracker.snapshotIndirectDepth,
- )
- depthTracker.updateIndirectRoots(
- additions = branchDepthTracker.snapshotIndirectRoots,
- butNot = null,
- )
- }
- }
- }
+ // remove and sever
+ removes.forEach { k ->
+ switchedIn.remove(k)?.let { branchNode: BranchNode ->
+ if (name != null) {
+ logLn("[${this@MuxPromptNode}] removing $k")
}
+ val conn: NodeConnection<V> = branchNode.upstream
+ severed.add(conn)
+ conn.removeDownstream(downstream = branchNode.schedulable)
+ depthTracker.removeDirectUpstream(conn.depthTracker.snapshotDirectDepth)
+ }
+ }
- coroutineScope {
- for (severedNode in severed) {
- launch { severedNode.scheduleDeactivationIfNeeded(evalScope) }
- }
+ // add or replace
+ adds.forEach { (k, newUpstream: EventsImpl<V>) ->
+ // remove old and sever, if present
+ switchedIn.remove(k)?.let { oldBranch: BranchNode ->
+ if (name != null) {
+ logLn("[${this@MuxPromptNode}] replacing $k")
}
+ val conn: NodeConnection<V> = oldBranch.upstream
+ severed.add(conn)
+ conn.removeDownstream(downstream = oldBranch.schedulable)
+ depthTracker.removeDirectUpstream(conn.depthTracker.snapshotDirectDepth)
+ }
- val resultStore = storeFactory.create<PullNode<V>>(additionsAndUpdates.size)
- for ((k, node) in additionsAndUpdates) {
- resultStore[k] = node
+ // add new
+ val newBranch = BranchNode(k)
+ newUpstream.activate(evalScope, newBranch.schedulable)?.let { (conn, needsEval) ->
+ newBranch.upstream = conn
+ if (name != null) {
+ logLn("[${this@MuxPromptNode}] switching in $k")
+ }
+ switchedIn[k] = newBranch
+ if (needsEval) {
+ upstreamData[k] = newBranch.upstream.directUpstream
+ } else {
+ needsReschedule = true
+ }
+ val branchDepthTracker = newBranch.upstream.depthTracker
+ if (branchDepthTracker.snapshotIsDirect) {
+ depthTracker.addDirectUpstream(
+ oldDepth = null,
+ newDepth = branchDepthTracker.snapshotDirectDepth,
+ )
+ } else {
+ depthTracker.addIndirectUpstream(
+ oldDepth = null,
+ newDepth = branchDepthTracker.snapshotIndirectDepth,
+ )
+ depthTracker.updateIndirectRoots(
+ additions = branchDepthTracker.snapshotIndirectRoots,
+ butNot = null,
+ )
}
- resultStore.takeIf { it.isNotEmpty() }?.asReadOnly()
}
+ }
- return if (preSwitchNotEmpty || newlySwitchedIn != null) {
- (newlySwitchedIn != null) to (preSwitchResults to newlySwitchedIn)
- } else {
- false to null
+ for (severedNode in severed) {
+ severedNode.scheduleDeactivationIfNeeded(evalScope)
}
+
+ return needsReschedule
}
- private fun adjustDownstreamDepths(evalScope: EvalScope, coroutineScope: CoroutineScope) {
+ private fun adjustDownstreamDepths(evalScope: EvalScope) {
if (depthTracker.dirty_depthIncreased()) {
// schedule downstream nodes on the compaction scheduler; this scheduler is drained at
// the end of this eval depth, so that all depth increases are applied before we advance
// the eval step
- depthTracker.schedule(evalScope.compactor, node = this@MuxPromptMovingNode)
+ depthTracker.schedule(evalScope.compactor, node = this@MuxPromptNode)
} else if (depthTracker.isDirty()) {
// schedule downstream nodes on the eval scheduler; this is more efficient and is only
// safe if the depth hasn't increased
depthTracker.applyChanges(
- coroutineScope,
evalScope.scheduler,
downstreamSet,
- muxNode = this@MuxPromptMovingNode,
+ muxNode = this@MuxPromptNode,
)
}
}
- override suspend fun getPushEvent(evalScope: EvalScope): MuxPromptMovingResult<W, K, V> =
- evalScope.getCurrentValue(key = this)
+ override fun getPushEvent(logIndent: Int, evalScope: EvalScope): MuxResult<W, K, V> =
+ logDuration(logIndent, "MuxPrompt.getPushEvent") {
+ transactionCache.getCurrentValue(evalScope)
+ }
- override suspend fun doDeactivate() {
+ override fun doDeactivate() {
// Update lifecycle
- lifecycle.mutex.withLock {
- if (lifecycle.lifecycleState !is MuxLifecycleState.Active) return@doDeactivate
- lifecycle.lifecycleState = MuxLifecycleState.Inactive(spec)
- }
+ if (lifecycle.lifecycleState !is MuxLifecycleState.Active) return
+ lifecycle.lifecycleState = MuxLifecycleState.Inactive(spec)
// Process branch nodes
switchedIn.forEach { (_, branchNode) ->
branchNode.upstream.removeDownstreamAndDeactivateIfNeeded(
@@ -231,57 +204,54 @@ internal class MuxPromptMovingNode<W, K, V>(
}
}
- suspend fun removeIndirectPatchNode(
+ fun removeIndirectPatchNode(
scheduler: Scheduler,
oldDepth: Int,
indirectSet: Set<MuxDeferredNode<*, *, *>>,
) {
- mutex.withLock {
- patches = null
- if (
- depthTracker.removeIndirectUpstream(oldDepth) or
- depthTracker.updateIndirectRoots(removals = indirectSet)
- ) {
- depthTracker.schedule(scheduler, this)
- }
+ patches = null
+ if (
+ depthTracker.removeIndirectUpstream(oldDepth) or
+ depthTracker.updateIndirectRoots(removals = indirectSet)
+ ) {
+ depthTracker.schedule(scheduler, this)
}
}
- suspend fun removeDirectPatchNode(scheduler: Scheduler, depth: Int) {
- mutex.withLock {
- patches = null
- if (depthTracker.removeDirectUpstream(depth)) {
- depthTracker.schedule(scheduler, this)
- }
+ fun removeDirectPatchNode(scheduler: Scheduler, depth: Int) {
+ patches = null
+ if (depthTracker.removeDirectUpstream(depth)) {
+ depthTracker.schedule(scheduler, this)
}
}
+ override fun toString(): String =
+ "${this::class.simpleName}@$hashString${name?.let { "[$it]" }.orEmpty()}"
+
inner class PatchNode : SchedulableNode {
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 suspend fun schedule(evalScope: EvalScope) {
- patchData = upstream.getPushEvent(evalScope)
- this@MuxPromptMovingNode.schedule(evalScope)
+ override fun schedule(logIndent: Int, evalScope: EvalScope) {
+ logDuration(logIndent, "MuxPromptPatchNode.schedule") {
+ patchData = upstream.getPushEvent(currentLogIndent, evalScope)
+ this@MuxPromptNode.schedule(evalScope)
+ }
}
- override suspend fun adjustDirectUpstream(
- scheduler: Scheduler,
- oldDepth: Int,
- newDepth: Int,
- ) {
- this@MuxPromptMovingNode.adjustDirectUpstream(scheduler, oldDepth, newDepth)
+ override fun adjustDirectUpstream(scheduler: Scheduler, oldDepth: Int, newDepth: Int) {
+ this@MuxPromptNode.adjustDirectUpstream(scheduler, oldDepth, newDepth)
}
- override suspend fun moveIndirectUpstreamToDirect(
+ override fun moveIndirectUpstreamToDirect(
scheduler: Scheduler,
oldIndirectDepth: Int,
oldIndirectSet: Set<MuxDeferredNode<*, *, *>>,
newDirectDepth: Int,
) {
- this@MuxPromptMovingNode.moveIndirectUpstreamToDirect(
+ this@MuxPromptNode.moveIndirectUpstreamToDirect(
scheduler,
oldIndirectDepth,
oldIndirectSet,
@@ -289,14 +259,14 @@ internal class MuxPromptMovingNode<W, K, V>(
)
}
- override suspend fun adjustIndirectUpstream(
+ override fun adjustIndirectUpstream(
scheduler: Scheduler,
oldDepth: Int,
newDepth: Int,
removals: Set<MuxDeferredNode<*, *, *>>,
additions: Set<MuxDeferredNode<*, *, *>>,
) {
- this@MuxPromptMovingNode.adjustIndirectUpstream(
+ this@MuxPromptNode.adjustIndirectUpstream(
scheduler,
oldDepth,
newDepth,
@@ -305,13 +275,13 @@ internal class MuxPromptMovingNode<W, K, V>(
)
}
- override suspend fun moveDirectUpstreamToIndirect(
+ override fun moveDirectUpstreamToIndirect(
scheduler: Scheduler,
oldDirectDepth: Int,
newIndirectDepth: Int,
newIndirectSet: Set<MuxDeferredNode<*, *, *>>,
) {
- this@MuxPromptMovingNode.moveDirectUpstreamToIndirect(
+ this@MuxPromptNode.moveDirectUpstreamToIndirect(
scheduler,
oldDirectDepth,
newIndirectDepth,
@@ -319,98 +289,70 @@ internal class MuxPromptMovingNode<W, K, V>(
)
}
- override suspend fun removeDirectUpstream(scheduler: Scheduler, depth: Int) {
- this@MuxPromptMovingNode.removeDirectPatchNode(scheduler, depth)
+ override fun removeDirectUpstream(scheduler: Scheduler, depth: Int) {
+ this@MuxPromptNode.removeDirectPatchNode(scheduler, depth)
}
- override suspend fun removeIndirectUpstream(
+ override fun removeIndirectUpstream(
scheduler: Scheduler,
depth: Int,
indirectSet: Set<MuxDeferredNode<*, *, *>>,
) {
- this@MuxPromptMovingNode.removeIndirectPatchNode(scheduler, depth, indirectSet)
+ this@MuxPromptNode.removeIndirectPatchNode(scheduler, depth, indirectSet)
}
}
}
-internal class MuxPromptEvalNode<W, K, V>(
- private val movingNode: PullNode<MuxPromptMovingResult<W, K, V>>,
- private val factory: MutableMapK.Factory<W, K>,
-) : PullNode<MuxResult<W, K, V>> {
- override suspend fun getPushEvent(evalScope: EvalScope): MuxResult<W, K, V> =
- movingNode.getPushEvent(evalScope).let { (preSwitchResults, newlySwitchedIn) ->
- newlySwitchedIn?.let {
- factory
- .create(preSwitchResults)
- .also { store ->
- newlySwitchedIn.forEach { k, pullNode -> store[k] = pullNode }
- }
- .asReadOnly()
- } ?: preSwitchResults
- }
-}
-
internal inline fun <A> switchPromptImplSingle(
- crossinline getStorage: suspend EvalScope.() -> TFlowImpl<A>,
- crossinline getPatches: suspend EvalScope.() -> TFlowImpl<TFlowImpl<A>>,
-): TFlowImpl<A> =
- mapImpl({
+ crossinline getStorage: EvalScope.() -> EventsImpl<A>,
+ crossinline getPatches: EvalScope.() -> EventsImpl<EventsImpl<A>>,
+): EventsImpl<A> {
+ val switchPromptImpl =
switchPromptImpl(
getStorage = { singleOf(getStorage()).asIterable() },
getPatches = {
- mapImpl(getPatches) { newFlow -> singleOf(just(newFlow)).asIterable() }
+ mapImpl(getPatches) { newFlow, _ -> singleOf(just(newFlow)).asIterable() }
},
storeFactory = SingletonMapK.Factory(),
)
- }) { map ->
- map.getValue(Unit).getPushEvent(this)
+ return mapImpl({ switchPromptImpl }) { map, logIndent ->
+ map.asSingle().getValue(Unit).getPushEvent(logIndent, this)
}
+}
internal fun <W, K, V> switchPromptImpl(
- getStorage: suspend EvalScope.() -> Iterable<Map.Entry<K, TFlowImpl<V>>>,
- getPatches: suspend EvalScope.() -> TFlowImpl<Iterable<Map.Entry<K, Maybe<TFlowImpl<V>>>>>,
+ name: String? = null,
+ 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>> {
- val moving = MuxLifecycle(MuxPromptActivator(getStorage, storeFactory, getPatches))
- val eval = TFlowCheap { downstream ->
- moving.activate(evalScope = this, downstream)?.let { (connection, needsEval) ->
- val evalNode = MuxPromptEvalNode(connection.directUpstream, storeFactory)
- ActivationResult(
- connection = NodeConnection(evalNode, connection.schedulerUpstream),
- needsEval = needsEval,
- )
- }
- }
- return eval.cached()
-}
+): EventsImpl<MuxResult<W, K, V>> =
+ MuxLifecycle(MuxPromptActivator(name, getStorage, storeFactory, getPatches))
private class MuxPromptActivator<W, K, V>(
- private val getStorage: suspend EvalScope.() -> Iterable<Map.Entry<K, TFlowImpl<V>>>,
+ private val name: String?,
+ private val getStorage: EvalScope.() -> Iterable<Map.Entry<K, EventsImpl<V>>>,
private val storeFactory: MutableMapK.Factory<W, K>,
- private val getPatches:
- suspend EvalScope.() -> TFlowImpl<Iterable<Map.Entry<K, Maybe<TFlowImpl<V>>>>>,
-) : MuxActivator<MuxPromptMovingResult<W, K, V>> {
- override suspend fun activate(
+ private val getPatches: EvalScope.() -> EventsImpl<Iterable<Map.Entry<K, Maybe<EventsImpl<V>>>>>,
+) : MuxActivator<W, K, V> {
+ override fun activate(
evalScope: EvalScope,
- lifecycle: MuxLifecycle<MuxPromptMovingResult<W, K, V>>,
- ): MuxNode<W, *, *, MuxPromptMovingResult<W, K, V>>? {
+ lifecycle: MuxLifecycle<W, K, V>,
+ ): Pair<MuxNode<W, K, V>, (() -> Unit)?>? {
// Initialize mux node and switched-in connections.
val movingNode =
- MuxPromptMovingNode(lifecycle, this, storeFactory).apply {
- coroutineScope {
- launch { initializeUpstream(evalScope, getStorage, storeFactory) }
- // Setup patches connection
- val patchNode = PatchNode()
- getPatches(evalScope)
- .activate(evalScope = evalScope, downstream = patchNode.schedulable)
- ?.let { (conn, needsEval) ->
- patchNode.upstream = conn
- patches = patchNode
- if (needsEval) {
- patchData = conn.getPushEvent(evalScope)
- }
+ MuxPromptNode(name, lifecycle, this, storeFactory).apply {
+ initializeUpstream(evalScope, getStorage, storeFactory)
+ // Setup patches connection
+ val patchNode = PatchNode()
+ getPatches(evalScope)
+ .activate(evalScope = evalScope, downstream = patchNode.schedulable)
+ ?.let { (conn, needsEval) ->
+ patchNode.upstream = conn
+ patches = patchNode
+ if (needsEval) {
+ patchData = conn.getPushEvent(0, evalScope)
}
- }
+ }
// Update depth based on all initial switched-in nodes.
initializeDepth()
// Update depth based on patches node.
@@ -441,6 +383,10 @@ private class MuxPromptActivator<W, K, V>(
movingNode.schedule(evalScope)
}
- return movingNode.takeUnless { it.patches == null && it.switchedIn.isEmpty() }
+ return if (movingNode.patches == null && movingNode.switchedIn.isEmpty()) {
+ null
+ } else {
+ movingNode to null
+ }
}
}
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 79d4b7a843ac..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,17 +16,17 @@
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
import com.android.systemui.kairos.util.Just
import com.android.systemui.kairos.util.Maybe
import com.android.systemui.kairos.util.just
import com.android.systemui.kairos.util.none
-import java.util.concurrent.ConcurrentHashMap
-import java.util.concurrent.ConcurrentLinkedDeque
-import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.atomic.AtomicLong
import kotlin.coroutines.ContinuationInterceptor
+import kotlin.time.measureTime
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
@@ -52,32 +52,41 @@ internal class Network(val coroutineScope: CoroutineScope) : NetworkScope {
override val network
get() = this
- override val compactor = SchedulerImpl()
- override val scheduler = SchedulerImpl()
- override val transactionStore = HeteroMap()
+ override val compactor = SchedulerImpl {
+ if (it.markedForCompaction) false
+ else {
+ it.markedForCompaction = true
+ true
+ }
+ }
+ override val scheduler = SchedulerImpl {
+ if (it.markedForEvaluation) false
+ else {
+ it.markedForEvaluation = true
+ true
+ }
+ }
+ override val transactionStore = TransactionStore()
- private val stateWrites = ConcurrentLinkedQueue<TStateSource<*>>()
- private val outputsByDispatcher =
- ConcurrentHashMap<ContinuationInterceptor, ConcurrentLinkedQueue<Output<*>>>()
- private val muxMovers = ConcurrentLinkedQueue<MuxDeferredNode<*, *, *>>()
- private val deactivations = ConcurrentLinkedDeque<PushNode<*>>()
- private val outputDeactivations = ConcurrentLinkedQueue<Output<*>>()
+ private val stateWrites = ArrayDeque<StateSource<*>>()
+ private val outputsByDispatcher = HashMap<ContinuationInterceptor, ArrayDeque<Output<*>>>()
+ private val muxMovers = ArrayDeque<MuxDeferredNode<*, *, *>>()
+ private val deactivations = ArrayDeque<PushNode<*>>()
+ private val outputDeactivations = ArrayDeque<Output<*>>()
private val transactionMutex = Mutex()
private val inputScheduleChan = Channel<ScheduledAction<*>>()
override fun scheduleOutput(output: Output<*>) {
val continuationInterceptor =
output.context[ContinuationInterceptor] ?: Dispatchers.Unconfined
- outputsByDispatcher
- .computeIfAbsent(continuationInterceptor) { ConcurrentLinkedQueue() }
- .add(output)
+ outputsByDispatcher.computeIfAbsent(continuationInterceptor) { ArrayDeque() }.add(output)
}
override fun scheduleMuxMover(muxMover: MuxDeferredNode<*, *, *>) {
muxMovers.add(muxMover)
}
- override fun schedule(state: TStateSource<*>) {
+ override fun schedule(state: StateSource<*>) {
stateWrites.add(state)
}
@@ -89,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) {
@@ -101,28 +110,37 @@ internal class Network(val coroutineScope: CoroutineScope) : NetworkScope {
actions.add(func)
}
transactionMutex.withLock {
- try {
- // Run all actions
- evalScope {
- for (action in actions) {
- launch { action.started(evalScope = this@evalScope) }
+ val e = epoch
+ val duration = measureTime {
+ logLn(0, "===starting transaction $e===")
+ try {
+ logDuration(1, "init actions") {
+ // Run all actions
+ evalScope {
+ for (action in actions) {
+ action.started(evalScope = this@evalScope)
+ }
+ }
+ }
+ // Step through the network
+ doTransaction(1)
+ } catch (e: Exception) {
+ // Signal failure
+ while (actions.isNotEmpty()) {
+ actions.removeLast().fail(e)
+ }
+ // re-throw, cancelling this coroutine
+ throw e
+ } finally {
+ logDuration(1, "signal completions") {
+ // Signal completion
+ while (actions.isNotEmpty()) {
+ actions.removeLast().completed()
+ }
}
- }
- // Step through the network
- doTransaction()
- } catch (e: Exception) {
- // Signal failure
- while (actions.isNotEmpty()) {
- actions.removeLast().fail(e)
- }
- // re-throw, cancelling this coroutine
- throw e
- } finally {
- // Signal completion
- while (actions.isNotEmpty()) {
- actions.removeLast().completed()
}
}
+ logLn(0, "===transaction $e took $duration===")
}
}
}
@@ -139,35 +157,47 @@ internal class Network(val coroutineScope: CoroutineScope) : NetworkScope {
onResult.invokeOnCompletion { job.cancel() }
}
- suspend fun <R> evalScope(block: suspend EvalScope.() -> R): R = deferScope {
+ inline fun <R> evalScope(block: EvalScope.() -> R): R = deferScope {
block(EvalScopeImpl(this@Network, this))
}
- /** Performs a transactional update of the FRP network. */
- private suspend fun doTransaction() {
+ /** Performs a transactional update of the Kairos network. */
+ private suspend fun doTransaction(logIndent: Int) {
// Traverse network, then run outputs
- do {
- scheduler.drainEval(this)
- } while (evalScope { evalOutputs(this) })
+ logDuration(logIndent, "traverse network") {
+ do {
+ val numNodes =
+ logDuration("drainEval") { scheduler.drainEval(currentLogIndent, this@Network) }
+ logLn("drained $numNodes nodes")
+ } while (logDuration("evalOutputs") { evalScope { evalOutputs(this) } })
+ }
// Update states
- evalScope { evalStateWriters(this) }
+ logDuration(logIndent, "update states") {
+ evalScope { evalStateWriters(currentLogIndent, this) }
+ }
// Invalidate caches
// Note: this needs to occur before deferred switches
- transactionStore.clear()
+ logDuration(logIndent, "clear store") { transactionStore.clear() }
epoch++
// Perform deferred switches
- evalScope { evalMuxMovers(this) }
+ logDuration(logIndent, "evalMuxMovers") {
+ evalScope { evalMuxMovers(currentLogIndent, this) }
+ }
// Compact depths
- scheduler.drainCompact()
- compactor.drainCompact()
+ logDuration(logIndent, "compact") {
+ scheduler.drainCompact(currentLogIndent)
+ compactor.drainCompact(currentLogIndent)
+ }
// Deactivate nodes with no downstream
- evalDeactivations()
+ logDuration(logIndent, "deactivations") { evalDeactivations() }
}
/** Invokes all [Output]s that have received data within this transaction. */
private suspend fun evalOutputs(evalScope: EvalScope): Boolean {
+ if (outputsByDispatcher.isEmpty()) {
+ return false
+ }
// Outputs can enqueue other outputs, so we need two loops
- if (outputsByDispatcher.isEmpty()) return false
while (outputsByDispatcher.isNotEmpty()) {
var launchedAny = false
coroutineScope {
@@ -176,57 +206,50 @@ internal class Network(val coroutineScope: CoroutineScope) : NetworkScope {
launchedAny = true
launch(key) {
while (outputs.isNotEmpty()) {
- val output = outputs.remove()
+ val output = outputs.removeFirst()
launch { output.visit(evalScope) }
}
}
}
}
}
- if (!launchedAny) outputsByDispatcher.clear()
+ if (!launchedAny) {
+ outputsByDispatcher.clear()
+ }
}
return true
}
- private suspend fun evalMuxMovers(evalScope: EvalScope) {
+ private fun evalMuxMovers(logIndent: Int, evalScope: EvalScope) {
while (muxMovers.isNotEmpty()) {
- coroutineScope {
- val toMove = muxMovers.remove()
- launch { toMove.performMove(evalScope) }
- }
+ val toMove = muxMovers.removeFirst()
+ toMove.performMove(logIndent, evalScope)
}
}
- /** Updates all [TState]es that have changed within this transaction. */
- private suspend fun evalStateWriters(evalScope: EvalScope) {
- coroutineScope {
- while (stateWrites.isNotEmpty()) {
- val latch = stateWrites.remove()
- launch { latch.updateState(evalScope) }
- }
+ /** Updates all [State]es that have changed within this transaction. */
+ private fun evalStateWriters(logIndent: Int, evalScope: EvalScope) {
+ while (stateWrites.isNotEmpty()) {
+ val latch = stateWrites.removeFirst()
+ latch.updateState(logIndent, evalScope)
}
}
- private suspend fun evalDeactivations() {
- coroutineScope {
- launch {
- while (deactivations.isNotEmpty()) {
- // traverse in reverse order
- // - deactivations are added in depth-order during the node traversal phase
- // - perform deactivations in reverse order, in case later ones propagate to
- // earlier ones
- val toDeactivate = deactivations.removeLast()
- launch { toDeactivate.deactivateIfNeeded() }
- }
- }
- while (outputDeactivations.isNotEmpty()) {
- val toDeactivate = outputDeactivations.remove()
- launch {
- toDeactivate.upstream?.removeDownstreamAndDeactivateIfNeeded(
- downstream = toDeactivate.schedulable
- )
- }
- }
+ private fun evalDeactivations() {
+ while (deactivations.isNotEmpty()) {
+ // traverse in reverse order
+ // - deactivations are added in depth-order during the node traversal phase
+ // - perform deactivations in reverse order, in case later ones propagate to
+ // earlier ones
+ val toDeactivate = deactivations.removeLast()
+ toDeactivate.deactivateIfNeeded()
+ }
+
+ while (outputDeactivations.isNotEmpty()) {
+ val toDeactivate = outputDeactivations.removeFirst()
+ toDeactivate.upstream?.removeDownstreamAndDeactivateIfNeeded(
+ downstream = toDeactivate.schedulable
+ )
}
check(deactivations.isEmpty()) { "unexpected lingering deactivations" }
check(outputDeactivations.isEmpty()) { "unexpected lingering output deactivations" }
@@ -260,4 +283,39 @@ internal class ScheduledAction<T>(
}
}
-internal typealias TransactionStore = HeteroMap
+internal class TransactionStore private constructor(private val storage: HeteroMap) {
+ constructor(capacity: Int) : this(HeteroMap(capacity))
+
+ constructor() : this(HeteroMap())
+
+ operator fun <A> get(key: HeteroMap.Key<A>): A =
+ storage.getOrError(key) { "no value for $key in this transaction" }
+
+ operator fun <A> set(key: HeteroMap.Key<A>, value: A) {
+ storage[key] = value
+ }
+
+ fun clear() = storage.clear()
+}
+
+internal class TransactionCache<A> {
+ private val key = object : HeteroMap.Key<A> {}
+ @Volatile
+ var epoch: Long = Long.MIN_VALUE
+ private set
+
+ fun getOrPut(evalScope: EvalScope, block: () -> A): A =
+ if (epoch < evalScope.epoch) {
+ epoch = evalScope.epoch
+ block().also { evalScope.transactionStore[key] = it }
+ } else {
+ evalScope.transactionStore[key]
+ }
+
+ fun put(evalScope: EvalScope, value: A) {
+ epoch = evalScope.epoch
+ evalScope.transactionStore[key] = value
+ }
+
+ fun getCurrentValue(evalScope: EvalScope): A = evalScope.transactionStore[key]
+}
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 fbd9689eb1d0..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,32 +16,6 @@
package com.android.systemui.kairos.internal
-import com.android.systemui.kairos.FrpScope
-import kotlin.coroutines.Continuation
-import kotlin.coroutines.CoroutineContext
-import kotlin.coroutines.EmptyCoroutineContext
-import kotlin.coroutines.coroutineContext
-import kotlin.coroutines.startCoroutine
-import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.completeWith
-import kotlinx.coroutines.job
+import com.android.systemui.kairos.KairosScope
-internal object NoScope {
- private object FrpScopeImpl : FrpScope
-
- suspend fun <R> runInFrpScope(block: suspend FrpScope.() -> R): R {
- val complete = CompletableDeferred<R>(coroutineContext.job)
- block.startCoroutine(
- FrpScopeImpl,
- object : Continuation<R> {
- override val context: CoroutineContext
- get() = EmptyCoroutineContext
-
- override fun resumeWith(result: Result<R>) {
- complete.completeWith(result)
- }
- },
- )
- return complete.await()
- }
-}
+internal object NoScope : KairosScope
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/NodeTypes.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/NodeTypes.kt
index 7a015d8ca1f6..39b8bfe540d2 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/NodeTypes.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/NodeTypes.kt
@@ -22,18 +22,18 @@ Muxes + Branch
*/
internal sealed interface SchedulableNode {
/** schedule this node w/ given NodeEvalScope */
- suspend fun schedule(evalScope: EvalScope)
+ fun schedule(logIndent: Int, evalScope: EvalScope)
- suspend fun adjustDirectUpstream(scheduler: Scheduler, oldDepth: Int, newDepth: Int)
+ fun adjustDirectUpstream(scheduler: Scheduler, oldDepth: Int, newDepth: Int)
- suspend fun moveIndirectUpstreamToDirect(
+ fun moveIndirectUpstreamToDirect(
scheduler: Scheduler,
oldIndirectDepth: Int,
oldIndirectSet: Set<MuxDeferredNode<*, *, *>>,
newDirectDepth: Int,
)
- suspend fun adjustIndirectUpstream(
+ fun adjustIndirectUpstream(
scheduler: Scheduler,
oldDepth: Int,
newDepth: Int,
@@ -41,20 +41,20 @@ internal sealed interface SchedulableNode {
additions: Set<MuxDeferredNode<*, *, *>>,
)
- suspend fun moveDirectUpstreamToIndirect(
+ fun moveDirectUpstreamToIndirect(
scheduler: Scheduler,
oldDirectDepth: Int,
newIndirectDepth: Int,
newIndirectSet: Set<MuxDeferredNode<*, *, *>>,
)
- suspend fun removeIndirectUpstream(
+ fun removeIndirectUpstream(
scheduler: Scheduler,
depth: Int,
indirectSet: Set<MuxDeferredNode<*, *, *>>,
)
- suspend fun removeDirectUpstream(scheduler: Scheduler, depth: Int)
+ fun removeDirectUpstream(scheduler: Scheduler, depth: Int)
}
/*
@@ -66,7 +66,7 @@ internal sealed interface PullNode<out A> {
* will read from the cache, otherwise it will perform a full evaluation, even if invoked
* multiple times within a transaction.
*/
- suspend fun getPushEvent(evalScope: EvalScope): A
+ fun getPushEvent(logIndent: Int, evalScope: EvalScope): A
}
/*
@@ -74,19 +74,19 @@ Muxes + DmuxBranch
*/
internal sealed interface PushNode<A> : PullNode<A> {
- suspend fun hasCurrentValue(evalScope: EvalScope): Boolean
+ fun hasCurrentValue(logIndent: Int, evalScope: EvalScope): Boolean
val depthTracker: DepthTracker
- suspend fun removeDownstream(downstream: Schedulable)
+ fun removeDownstream(downstream: Schedulable)
/** called during cleanup phase */
- suspend fun deactivateIfNeeded()
+ fun deactivateIfNeeded()
/** called from mux nodes after severs */
- suspend fun scheduleDeactivationIfNeeded(evalScope: EvalScope)
+ fun scheduleDeactivationIfNeeded(evalScope: EvalScope)
- suspend fun addDownstream(downstream: Schedulable)
+ fun addDownstream(downstream: Schedulable)
- suspend fun removeDownstreamAndDeactivateIfNeeded(downstream: Schedulable)
+ fun removeDownstreamAndDeactivateIfNeeded(downstream: Schedulable)
}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Output.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Output.kt
index 3373de05249c..38d8cf70b36e 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Output.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Output.kt
@@ -21,8 +21,8 @@ import kotlin.coroutines.EmptyCoroutineContext
internal class Output<A>(
val context: CoroutineContext = EmptyCoroutineContext,
- val onDeath: suspend () -> Unit = {},
- val onEmit: suspend EvalScope.(A) -> Unit,
+ val onDeath: () -> Unit = {},
+ val onEmit: EvalScope.(A) -> Unit,
) {
val schedulable = Schedulable.O(this)
@@ -33,23 +33,24 @@ internal class Output<A>(
private object NoResult
// invoked by network
- suspend fun visit(evalScope: EvalScope) {
+ fun visit(evalScope: EvalScope) {
val upstreamResult = result
check(upstreamResult !== NoResult) { "output visited with null upstream result" }
result = NoResult
@Suppress("UNCHECKED_CAST") evalScope.onEmit(upstreamResult as A)
}
- suspend fun kill() {
+ fun kill() {
onDeath()
}
- suspend fun schedule(evalScope: EvalScope) {
+ fun schedule(logIndent: Int, evalScope: EvalScope) {
result =
- checkNotNull(upstream) { "output scheduled with null upstream" }.getPushEvent(evalScope)
+ checkNotNull(upstream) { "output scheduled with null upstream" }
+ .getPushEvent(logIndent, evalScope)
evalScope.scheduleOutput(this)
}
}
-internal inline fun OneShot(crossinline onEmit: suspend EvalScope.() -> Unit): Output<Unit> =
+internal inline fun OneShot(crossinline onEmit: EvalScope.() -> Unit): Output<Unit> =
Output<Unit>(onEmit = { onEmit() }).apply { result = Unit }
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 43b621fadc67..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
@@ -16,22 +16,24 @@
package com.android.systemui.kairos.internal
-import com.android.systemui.kairos.internal.util.Key
-import kotlinx.coroutines.CoroutineStart
-import kotlinx.coroutines.Deferred
+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: suspend EvalScope.(A) -> B) :
+internal class MapNode<A, B>(val upstream: PullNode<A>, val transform: EvalScope.(A, Int) -> B) :
PullNode<B> {
- override suspend fun getPushEvent(evalScope: EvalScope): B =
- evalScope.transform(upstream.getPushEvent(evalScope))
+ override fun getPushEvent(logIndent: Int, evalScope: EvalScope): B =
+ logDuration(logIndent, "MapNode.getPushEvent") {
+ val upstream =
+ logDuration("upstream event") { upstream.getPushEvent(currentLogIndent, evalScope) }
+ logDuration("transform") { evalScope.transform(upstream, currentLogIndent) }
+ }
}
internal inline fun <A, B> mapImpl(
- crossinline upstream: suspend EvalScope.() -> TFlowImpl<A>,
- noinline transform: suspend EvalScope.(A) -> B,
-): TFlowImpl<B> = TFlowCheap { downstream ->
+ crossinline upstream: EvalScope.() -> EventsImpl<A>,
+ noinline transform: EvalScope.(A, Int) -> B,
+): EventsImpl<B> = EventsImplCheap { downstream ->
upstream().activate(evalScope = this, downstream)?.let { (connection, needsEval) ->
ActivationResult(
connection =
@@ -44,19 +46,29 @@ internal inline fun <A, B> mapImpl(
}
}
-internal class CachedNode<A>(val key: Key<Deferred<A>>, val upstream: PullNode<A>) : PullNode<A> {
- override suspend fun getPushEvent(evalScope: EvalScope): A {
- val deferred =
- evalScope.transactionStore.getOrPut(key) {
- evalScope.deferAsync(CoroutineStart.LAZY) { upstream.getPushEvent(evalScope) }
- }
- return deferred.await()
- }
+internal class CachedNode<A>(
+ private val transactionCache: TransactionCache<Lazy<A>>,
+ val upstream: PullNode<A>,
+) : PullNode<A> {
+ override fun getPushEvent(logIndent: Int, evalScope: EvalScope): A =
+ logDuration(logIndent, "CachedNode.getPushEvent") {
+ val deferred =
+ logDuration("CachedNode.getOrPut", false) {
+ transactionCache.getOrPut(evalScope) {
+ evalScope.deferAsync {
+ logDuration("CachedNode.getUpstreamEvent") {
+ upstream.getPushEvent(currentLogIndent, evalScope)
+ }
+ }
+ }
+ }
+ logDuration("await") { deferred.value }
+ }
}
-internal fun <A> TFlowImpl<A>.cached(): TFlowImpl<A> {
- val key = object : Key<Deferred<A>> {}
- return TFlowCheap {
+internal fun <A> EventsImpl<A>.cached(): EventsImpl<A> {
+ val key = TransactionCache<Lazy<A>>()
+ return EventsImplCheap { it ->
activate(this, it)?.let { (connection, needsEval) ->
ActivationResult(
connection =
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Scheduler.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Scheduler.kt
index d046420517fe..0529bcb63c07 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Scheduler.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Scheduler.kt
@@ -14,66 +14,76 @@
* limitations under the License.
*/
-@file:OptIn(ExperimentalCoroutinesApi::class)
-
package com.android.systemui.kairos.internal
-import java.util.concurrent.ConcurrentHashMap
-import java.util.concurrent.PriorityBlockingQueue
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.launch
+import com.android.systemui.kairos.internal.util.LogIndent
+import java.util.PriorityQueue
internal interface Scheduler {
- fun schedule(depth: Int, node: MuxNode<*, *, *, *>)
+ fun schedule(depth: Int, node: MuxNode<*, *, *>)
- fun scheduleIndirect(indirectDepth: Int, node: MuxNode<*, *, *, *>)
+ fun scheduleIndirect(indirectDepth: Int, node: MuxNode<*, *, *>)
}
-internal class SchedulerImpl : Scheduler {
- val enqueued = ConcurrentHashMap<MuxNode<*, *, *, *>, Any>()
- val scheduledQ =
- PriorityBlockingQueue<Pair<Int, MuxNode<*, *, *, *>>>(16, compareBy { it.first })
+internal class SchedulerImpl(private val enqueue: (MuxNode<*, *, *>) -> Boolean) : Scheduler {
+ private val scheduledQ = PriorityQueue<Pair<Int, MuxNode<*, *, *>>>(compareBy { it.first })
- override fun schedule(depth: Int, node: MuxNode<*, *, *, *>) {
- if (enqueued.putIfAbsent(node, node) == null) {
+ override fun schedule(depth: Int, node: MuxNode<*, *, *>) {
+ if (enqueue(node)) {
scheduledQ.add(Pair(depth, node))
}
}
- override fun scheduleIndirect(indirectDepth: Int, node: MuxNode<*, *, *, *>) {
+ override fun scheduleIndirect(indirectDepth: Int, node: MuxNode<*, *, *>) {
schedule(Int.MIN_VALUE + indirectDepth, node)
}
- internal suspend fun drainEval(network: Network) {
- drain { runStep ->
- runStep { muxNode -> network.evalScope { muxNode.visit(this) } }
+ internal fun drainEval(logIndent: Int, network: Network): Int =
+ drain(logIndent) { runStep ->
+ runStep { muxNode ->
+ network.evalScope {
+ muxNode.markedForEvaluation = false
+ muxNode.visit(currentLogIndent, this)
+ }
+ }
// If any visited MuxPromptNodes had their depths increased, eagerly propagate those
// depth changes now before performing further network evaluation.
- network.compactor.drainCompact()
+ val numNodes = network.compactor.drainCompact(currentLogIndent)
+ logLn("promptly compacted $numNodes nodes")
}
- }
- internal suspend fun drainCompact() {
- drain { runStep -> runStep { muxNode -> muxNode.visitCompact(scheduler = this) } }
- }
+ internal fun drainCompact(logIndent: Int): Int =
+ drain(logIndent) { runStep ->
+ runStep { muxNode ->
+ muxNode.markedForCompaction = false
+ muxNode.visitCompact(scheduler = this@SchedulerImpl)
+ }
+ }
- private suspend inline fun drain(
+ private inline fun drain(
+ logIndent: Int,
crossinline onStep:
- suspend (
- runStep: suspend (visit: suspend (MuxNode<*, *, *, *>) -> Unit) -> Unit
- ) -> Unit
- ): Unit = coroutineScope {
+ LogIndent.(
+ runStep: LogIndent.(visit: LogIndent.(MuxNode<*, *, *>) -> Unit) -> Unit
+ ) -> Unit,
+ ): Int {
+ var total = 0
while (scheduledQ.isNotEmpty()) {
val maxDepth = scheduledQ.peek()?.first ?: error("Unexpected empty scheduler")
- onStep { visit -> runStep(maxDepth, visit) }
+ LogIndent(logIndent).onStep { visit ->
+ logDuration("step $maxDepth") {
+ val subtotal = runStep(maxDepth) { visit(it) }
+ logLn("visited $subtotal nodes")
+ total += subtotal
+ }
+ }
}
+ return total
}
- private suspend inline fun runStep(
- maxDepth: Int,
- crossinline visit: suspend (MuxNode<*, *, *, *>) -> Unit,
- ) = coroutineScope {
+ private inline fun runStep(maxDepth: Int, crossinline visit: (MuxNode<*, *, *>) -> Unit): Int {
+ var total = 0
+ val toVisit = mutableListOf<MuxNode<*, *, *>>()
while (scheduledQ.peek()?.first?.let { it <= maxDepth } == true) {
val (d, node) = scheduledQ.remove()
if (
@@ -82,11 +92,15 @@ internal class SchedulerImpl : Scheduler {
) {
scheduledQ.add(node.depthTracker.dirty_directDepth to node)
} else {
- launch {
- enqueued.remove(node)
- visit(node)
- }
+ total++
+ toVisit.add(node)
}
}
+
+ for (node in toVisit) {
+ visit(node)
+ }
+
+ return total
}
}
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 916f22575b0c..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
@@ -20,29 +20,22 @@ import com.android.systemui.kairos.internal.store.ConcurrentHashMapK
import com.android.systemui.kairos.internal.store.MutableArrayMapK
import com.android.systemui.kairos.internal.store.MutableMapK
import com.android.systemui.kairos.internal.store.StoreEntry
-import com.android.systemui.kairos.internal.util.Key
import com.android.systemui.kairos.internal.util.hashString
-import com.android.systemui.kairos.internal.util.launchImmediate
import com.android.systemui.kairos.util.Maybe
import com.android.systemui.kairos.util.just
import com.android.systemui.kairos.util.none
import java.util.concurrent.atomic.AtomicLong
-import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.CoroutineStart
-import kotlinx.coroutines.Deferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.coroutineScope
-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>
- suspend fun getCurrentWithEpoch(evalScope: EvalScope): Pair<A, Long>
+ fun getCurrentWithEpoch(evalScope: EvalScope): Pair<A, Long>
}
-internal sealed class TStateDerived<A>(override val changes: TFlowImpl<A>) :
- TStateImpl<A>, Key<Deferred<Pair<A, Long>>> {
+internal sealed class StateDerived<A>(override val changes: EventsImpl<A>) : StateImpl<A> {
@Volatile
var invalidatedEpoch = Long.MIN_VALUE
@@ -52,12 +45,12 @@ internal sealed class TStateDerived<A>(override val changes: TFlowImpl<A>) :
protected var cache: Any? = EmptyCache
private set
- override suspend fun getCurrentWithEpoch(evalScope: EvalScope): Pair<A, Long> =
- evalScope.transactionStore
- .getOrPut(this) { evalScope.deferAsync(CoroutineStart.LAZY) { pull(evalScope) } }
- .await()
+ private val transactionCache = TransactionCache<Lazy<Pair<A, Long>>>()
- suspend fun pull(evalScope: EvalScope): Pair<A, Long> {
+ override fun getCurrentWithEpoch(evalScope: EvalScope): Pair<A, Long> =
+ transactionCache.getOrPut(evalScope) { evalScope.deferAsync { pull(evalScope) } }.value
+
+ fun pull(evalScope: EvalScope): Pair<A, Long> {
@Suppress("UNCHECKED_CAST")
return recalc(evalScope)?.also { (a, epoch) -> setCache(a, epoch) }
?: ((cache as A) to invalidatedEpoch)
@@ -75,65 +68,66 @@ internal sealed class TStateDerived<A>(override val changes: TFlowImpl<A>) :
return if (cache == EmptyCache) none else just(cache as A)
}
- protected abstract suspend fun recalc(evalScope: EvalScope): Pair<A, Long>?
+ protected abstract fun recalc(evalScope: EvalScope): Pair<A, Long>?
private data object EmptyCache
}
-internal class TStateSource<A>(
+internal class StateSource<A>(
override val name: String?,
override val operatorName: String,
- init: Deferred<A>,
- override val changes: TFlowImpl<A>,
-) : TStateImpl<A> {
+ init: Lazy<A>,
+ override val changes: EventsImpl<A>,
+) : StateImpl<A> {
constructor(
name: String?,
operatorName: String,
init: A,
- changes: TFlowImpl<A>,
- ) : this(name, operatorName, CompletableDeferred(init), changes)
+ changes: EventsImpl<A>,
+ ) : this(name, operatorName, CompletableLazy(init), changes)
lateinit var upstreamConnection: NodeConnection<A>
// Note: Don't need to synchronize; we will never interleave reads and writes, since all writes
// are performed at the end of a network step, after any reads would have taken place.
- @Volatile private var _current: Deferred<A> = init
+ @Volatile private var _current: Lazy<A> = init
+
@Volatile
var writeEpoch = 0L
private set
- override suspend fun getCurrentWithEpoch(evalScope: EvalScope): Pair<A, Long> =
- _current.await() to writeEpoch
+ override fun getCurrentWithEpoch(evalScope: EvalScope): Pair<A, Long> =
+ _current.value to writeEpoch
/** called by network after eval phase has completed */
- suspend fun updateState(evalScope: EvalScope) {
+ fun updateState(logIndent: Int, evalScope: EvalScope) {
// write the latch
- _current = CompletableDeferred(upstreamConnection.getPushEvent(evalScope))
+ // TODO: deferAsync?
+ _current = CompletableLazy(upstreamConnection.getPushEvent(logIndent, evalScope))
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.isCompleted) just(_current.getCompleted()) else none
+ 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: suspend EvalScope.() -> TFlowImpl<A>,
- init: Deferred<A>,
-): TStateImpl<A> {
- lateinit var state: TStateSource<A>
- val calm: TFlowImpl<A> =
+ crossinline getChanges: EvalScope.() -> EventsImpl<A>,
+ init: Lazy<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 {
@@ -149,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)
@@ -164,22 +158,28 @@ 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: suspend EvalScope.(A) -> B,
-): TStateImpl<B> =
- DerivedMapCheap(name, operatorName, this, mapImpl({ changes }) { transform(it) }, transform)
+ transform: EvalScope.(A) -> B,
+): StateImpl<B> =
+ DerivedMapCheap(
+ name,
+ operatorName,
+ this,
+ mapImpl({ changes }) { it, _ -> transform(it) },
+ transform,
+ )
internal class DerivedMapCheap<A, B>(
override val name: String?,
override val operatorName: String,
- val upstream: TStateImpl<A>,
- override val changes: TFlowImpl<B>,
- private val transform: suspend EvalScope.(A) -> B,
-) : TStateImpl<B> {
+ val upstream: StateImpl<A>,
+ override val changes: EventsImpl<B>,
+ private val transform: EvalScope.(A) -> B,
+) : StateImpl<B> {
- override suspend fun getCurrentWithEpoch(evalScope: EvalScope): Pair<B, Long> {
+ override fun getCurrentWithEpoch(evalScope: EvalScope): Pair<B, Long> {
val (a, epoch) = upstream.getCurrentWithEpoch(evalScope)
return evalScope.transform(a) to epoch
}
@@ -187,13 +187,13 @@ 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: suspend EvalScope.(A) -> B,
-): TStateImpl<B> {
- lateinit var state: TStateDerived<B>
- val mappedChanges = mapImpl({ changes }) { transform(it) }.cached().calm { state }
+ transform: EvalScope.(A) -> 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
}
@@ -201,13 +201,13 @@ internal fun <A, B> TStateImpl<A>.map(
internal class DerivedMap<A, B>(
override val name: String?,
override val operatorName: String,
- private val transform: suspend EvalScope.(A) -> B,
- val upstream: TStateImpl<A>,
- changes: TFlowImpl<B>,
-) : TStateDerived<B>(changes) {
+ private val transform: EvalScope.(A) -> B,
+ val upstream: StateImpl<A>,
+ changes: EventsImpl<B>,
+) : StateDerived<B>(changes) {
override fun toString(): String = "${this::class.simpleName}@$hashString"
- override suspend fun recalc(evalScope: EvalScope): Pair<B, Long>? {
+ override fun recalc(evalScope: EvalScope): Pair<B, Long>? {
val (a, epoch) = upstream.getCurrentWithEpoch(evalScope)
return if (epoch > invalidatedEpoch) {
evalScope.transform(a) to epoch
@@ -217,17 +217,18 @@ 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 }
+ val switchEvents =
+ mapImpl({ changes }) { newInner, _ -> newInner.getCurrentWithEpoch(this).first }
// emits the new value of the new inner state when that state is emitted, or
// falls back to the current value if a new state is *not* being emitted this
// transaction
val innerChanges =
- mapImpl({ changes }) { newInner ->
+ 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 },
@@ -240,10 +241,10 @@ 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) {
- override suspend fun recalc(evalScope: EvalScope): Pair<A, Long> {
+ 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)
return a to maxOf(epoch0, epoch1)
@@ -253,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: suspend 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>,
- transform: suspend EvalScope.(A, B) -> Z,
-): TStateImpl<Z> =
+ l1: StateImpl<A>,
+ l2: StateImpl<B>,
+ transform: EvalScope.(A, B) -> Z,
+): StateImpl<Z> =
zipStateList(null, operatorName, listOf(l1, l2)).map(name, operatorName) {
@Suppress("UNCHECKED_CAST") transform(it[0] as A, it[1] as B)
}
@@ -273,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>,
- transform: suspend EvalScope.(A, B, C) -> Z,
-): TStateImpl<Z> =
+ l1: StateImpl<A>,
+ l2: StateImpl<B>,
+ l3: StateImpl<C>,
+ transform: EvalScope.(A, B, C) -> 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)
}
@@ -285,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>,
- transform: suspend EvalScope.(A, B, C, D) -> Z,
-): TStateImpl<Z> =
+ l1: StateImpl<A>,
+ l2: StateImpl<B>,
+ l3: StateImpl<C>,
+ l4: StateImpl<D>,
+ transform: EvalScope.(A, B, C, D) -> 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)
}
@@ -298,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>,
- transform: suspend EvalScope.(A, B, C, D, E) -> Z,
-): TStateImpl<Z> =
+ l1: StateImpl<A>,
+ l2: StateImpl<B>,
+ l3: StateImpl<C>,
+ l4: StateImpl<D>,
+ l5: StateImpl<E>,
+ transform: EvalScope.(A, B, C, D, E) -> 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)
@@ -313,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,
@@ -326,18 +327,14 @@ 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,
operatorName = operatorName,
numStates = states.size,
- states =
- states
- .asSequence()
- .mapIndexed { index, tStateImpl -> StoreEntry(index, tStateImpl) }
- .asIterable(),
+ states = states.asIterableWithIndex(),
storeFactory = MutableArrayMapK.Factory(),
)
// Like mapCheap, but with caching (or like map, but without the calm changes, as they are not
@@ -347,7 +344,7 @@ internal fun <V> zipStateList(
operatorName = operatorName,
transform = { arrayStore -> arrayStore.values.toList() },
upstream = zipped,
- changes = mapImpl({ zipped.changes }) { arrayStore -> arrayStore.values.toList() },
+ changes = mapImpl({ zipped.changes }) { arrayStore, _ -> arrayStore.values.toList() },
)
}
@@ -355,35 +352,31 @@ 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>
// No need for calm; invariant ensures that changes will only emit when there's a difference
+ val switchDeferredImpl =
+ switchDeferredImpl(
+ getStorage = { stateChanges },
+ getPatches = { neverImpl },
+ storeFactory = storeFactory,
+ )
val changes =
- mapImpl({
- switchDeferredImpl(
- getStorage = { stateChanges },
- getPatches = { neverImpl },
- storeFactory = storeFactory,
- )
- }) { patch ->
+ mapImpl({ switchDeferredImpl }) { patch, logIndent ->
val store = storeFactory.create<A>(numStates)
- coroutineScope {
- states.forEach { (k, state) ->
- launchImmediate {
- store[k] =
- if (patch.contains(k)) {
- patch.getValue(k).getPushEvent(evalScope = this@mapImpl)
- } else {
- state.getCurrentWithEpoch(evalScope = this@mapImpl).first
- }
+ states.forEach { (k, state) ->
+ store[k] =
+ if (patch.contains(k)) {
+ patch.getValue(k).getPushEvent(logIndent, evalScope = this@mapImpl)
+ } else {
+ state.getCurrentWithEpoch(evalScope = this@mapImpl).first
}
- }
}
store.also { state.setCache(it, epoch) }
}
@@ -404,21 +397,17 @@ 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) {
- override suspend fun recalc(evalScope: EvalScope): Pair<MutableMapK<W, K, A>, Long> {
+) : 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)
- coroutineScope {
- for ((key, value) in upstream) {
- launchImmediate {
- val (a, epoch) = value.getCurrentWithEpoch(evalScope)
- newEpoch.accumulateAndGet(epoch, ::maxOf)
- store[key] = a
- }
- }
+ for ((key, value) in upstream) {
+ val (a, epoch) = value.getCurrentWithEpoch(evalScope)
+ newEpoch.accumulateAndGet(epoch, ::maxOf)
+ store[key] = a
}
return store to newEpoch.get()
}
@@ -430,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 94f94f510d48..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,116 +16,65 @@
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.internal.util.mapValuesParallel
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
-import kotlin.coroutines.Continuation
-import kotlin.coroutines.CoroutineContext
-import kotlin.coroutines.EmptyCoroutineContext
-import kotlin.coroutines.startCoroutine
-import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.Deferred
-import kotlinx.coroutines.completeWith
-import kotlinx.coroutines.job
-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 {
- private 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, CompletableDeferred(init))
-
- private fun <A> TFlow<A>.toTStateInternalDeferred(
- operatorName: String,
- init: Deferred<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: suspend 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(
- storage: TState<Map<K, TFlow<V>>>
- ): TFlow<Map<K, V>> {
- val name = "mergeIncrementally"
- return TFlowInit(
+ override fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally(
+ name: String?,
+ 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 { events -> events.init.connect(this) } }
+ .asIterable()
+ }
+ return EventsInit(
constInit(
name,
switchDeferredImpl(
+ name = name,
getStorage = {
storage.init
.connect(this)
.getCurrentWithEpoch(this)
.first
- .mapValuesParallel { (_, flow) -> flow.init.connect(this) }
+ .mapValues { (_, events) -> events.init.connect(this) }
.asIterable()
},
- getPatches = {
- mapImpl({ init.connect(this) }) { patch ->
- patch
- .mapValuesParallel { (_, m) ->
- m.map { flow -> flow.init.connect(this) }
- }
- .asIterable()
- }
- },
+ getPatches = { patches },
storeFactory = ConcurrentHashMapK.Factory(),
)
.awaitValues(),
@@ -133,31 +82,31 @@ 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>>>
- ): TFlow<Map<K, V>> {
- val name = "mergeIncrementallyPrompt"
- return TFlowInit(
+ override fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementallyPromptly(
+ initialEvents: DeferredValue<Map<K, Events<V>>>,
+ name: String?,
+ ): 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 { events -> events.init.connect(this) } }
+ .asIterable()
+ }
+ return EventsInit(
constInit(
name,
switchPromptImpl(
+ name = name,
getStorage = {
storage.init
.connect(this)
.getCurrentWithEpoch(this)
.first
- .mapValuesParallel { (_, flow) -> flow.init.connect(this) }
+ .mapValues { (_, events) -> events.init.connect(this) }
.asIterable()
},
- getPatches = {
- mapImpl({ init.connect(this) }) { patch ->
- patch
- .mapValuesParallel { (_, m) ->
- m.map { flow -> flow.init.connect(this) }
- }
- .asIterable()
- }
- },
+ getPatches = { patches },
storeFactory = ConcurrentHashMapK.Factory(),
)
.awaitValues(),
@@ -165,109 +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)
- val initOut: Deferred<Map<K, B>> = deferAsync {
- init.unwrapped.await().mapValuesParallel { (k, stateful) ->
- val newEnd = with(frpScope) { eventsByKey[k].skipNext() }
+ ): 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 = 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.mapValuesParallel { (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: suspend 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(
- initialTFlows: FrpDeferredValue<Map<K, TFlow<V>>>
- ): TFlow<Map<K, V>> {
- val storage: TState<Map<K, TFlow<V>>> = foldMapIncrementally(initialTFlows)
- return mergeIncrementallyInternal(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>>>
- ): TFlow<Map<K, V>> {
- val storage: TState<Map<K, TFlow<V>>> = foldMapIncrementally(initialTFlows)
- return mergeIncrementallyPromptInternal(storage)
+ 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)
-
- override fun <A> TFlow<FrpStateful<A>>.applyStatefuls(): TFlow<A> =
- observeStatefulsInternal()
- }
-
- override suspend fun <R> runInStateScope(block: suspend FrpStateScope.() -> R): R {
- val complete = CompletableDeferred<R>(parent = coroutineContext.job)
- block.startCoroutine(
- frpScope,
- object : Continuation<R> {
- override val context: CoroutineContext
- get() = EmptyCoroutineContext
+ private fun <A> Events<A>.toStateInternal(operatorName: String, init: A): State<A> =
+ toStateInternalDeferred(operatorName, CompletableLazy(init))
- override fun resumeWith(result: Result<R>) {
- complete.completeWith(result)
- }
- },
- )
- return complete.await()
+ 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 childStateScope(newEnd: TFlow<Any>) =
- StateScopeImpl(evalScope, merge(newEnd, endSignal))
}
private fun EvalScope.reenterStateScope(outerScope: StateScopeImpl) =
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/TransactionalImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/TransactionalImpl.kt
index 8647bdd5b7b1..13bd3b005871 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/TransactionalImpl.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/TransactionalImpl.kt
@@ -16,31 +16,25 @@
package com.android.systemui.kairos.internal
-import com.android.systemui.kairos.internal.util.Key
import com.android.systemui.kairos.internal.util.hashString
-import kotlinx.coroutines.CoroutineStart
-import kotlinx.coroutines.Deferred
internal sealed class TransactionalImpl<out A> {
- data class Const<out A>(val value: Deferred<A>) : TransactionalImpl<A>()
+ data class Const<out A>(val value: Lazy<A>) : TransactionalImpl<A>()
+
+ class Impl<A>(val block: EvalScope.() -> A) : TransactionalImpl<A>() {
+ val cache = TransactionCache<Lazy<A>>()
- class Impl<A>(val block: suspend EvalScope.() -> A) : TransactionalImpl<A>(), Key<Deferred<A>> {
override fun toString(): String = "${this::class.simpleName}@$hashString"
}
}
@Suppress("NOTHING_TO_INLINE")
-internal inline fun <A> transactionalImpl(
- noinline block: suspend EvalScope.() -> A
-): TransactionalImpl<A> = TransactionalImpl.Impl(block)
+internal inline fun <A> transactionalImpl(noinline block: EvalScope.() -> A): TransactionalImpl<A> =
+ TransactionalImpl.Impl(block)
-internal fun <A> TransactionalImpl<A>.sample(evalScope: EvalScope): Deferred<A> =
+internal fun <A> TransactionalImpl<A>.sample(evalScope: EvalScope): Lazy<A> =
when (this) {
is TransactionalImpl.Const -> value
is TransactionalImpl.Impl ->
- evalScope.transactionStore
- .getOrPut(this) {
- evalScope.deferAsync(start = CoroutineStart.LAZY) { evalScope.block() }
- }
- .also { it.start() }
+ cache.getOrPut(evalScope) { evalScope.deferAsync { evalScope.block() } }
}
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/HeteroMap.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/HeteroMap.kt
index 33709a97da8f..4d183481898b 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/HeteroMap.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/HeteroMap.kt
@@ -21,13 +21,14 @@ import com.android.systemui.kairos.util.None
import com.android.systemui.kairos.util.just
import java.util.concurrent.ConcurrentHashMap
-internal interface Key<A>
-
private object NULL
-internal class HeteroMap {
+internal class HeteroMap private constructor(private val store: ConcurrentHashMap<Key<*>, Any>) {
+ interface Key<A> {}
+
+ constructor() : this(ConcurrentHashMap())
- private val store = ConcurrentHashMap<Key<*>, Any>()
+ constructor(capacity: Int) : this(ConcurrentHashMap(capacity))
@Suppress("UNCHECKED_CAST")
operator fun <A> get(key: Key<A>): Maybe<A> =
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/MapUtils.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/MapUtils.kt
index ebf9a66be0ae..13f884666182 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/MapUtils.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/MapUtils.kt
@@ -16,8 +16,6 @@
package com.android.systemui.kairos.internal.util
-import kotlinx.coroutines.CoroutineStart
-import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.yield
@@ -32,7 +30,7 @@ internal suspend inline fun <K, A, B : Any, M : MutableMap<K, B>> Map<K, A>
destination.also {
coroutineScope {
mapValues {
- async {
+ asyncImmediate {
yield()
block(it)
}
@@ -53,7 +51,7 @@ internal inline fun <K, A, B : Any, M : MutableMap<K, B>> Map<K, A>.mapValuesNot
internal suspend fun <A, B> Iterable<A>.mapParallel(transform: suspend (A) -> B): List<B> =
coroutineScope {
- map { async(start = CoroutineStart.LAZY) { transform(it) } }.awaitAll()
+ map { asyncImmediate { transform(it) } }.awaitAll()
}
internal suspend fun <K, A, B, M : MutableMap<K, B>> Map<K, A>.mapValuesParallelTo(
diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/Util.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/Util.kt
index 6bb7f9f593aa..466a9f83b91f 100644
--- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/Util.kt
+++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/util/Util.kt
@@ -18,8 +18,13 @@
package com.android.systemui.kairos.internal.util
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
+import kotlin.contracts.contract
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
+import kotlin.time.DurationUnit
+import kotlin.time.measureTimedValue
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Deferred
@@ -31,6 +36,62 @@ import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.launch
import kotlinx.coroutines.newCoroutineContext
+private const val LogEnabled = false
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun logLn(indent: Int = 0, message: Any?) {
+ if (!LogEnabled) return
+ log(indent, message)
+ println()
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun log(indent: Int = 0, message: Any?) {
+ if (!LogEnabled) return
+ printIndent(indent)
+ print(message)
+}
+
+@JvmInline
+internal value class LogIndent(val currentLogIndent: Int) {
+ @OptIn(ExperimentalContracts::class)
+ inline fun <R> logDuration(prefix: String, start: Boolean = true, block: LogIndent.() -> R): R {
+ contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
+ return logDuration(currentLogIndent, prefix, start, block)
+ }
+
+ @Suppress("NOTHING_TO_INLINE")
+ inline fun logLn(message: Any?) = logLn(currentLogIndent, message)
+}
+
+@OptIn(ExperimentalContracts::class)
+internal inline fun <R> logDuration(
+ indent: Int,
+ prefix: String,
+ start: Boolean = true,
+ block: LogIndent.() -> R,
+): R {
+ contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
+ if (!LogEnabled) return LogIndent(0).block()
+ if (start) {
+ logLn(indent, prefix)
+ }
+ val (result, duration) = measureTimedValue { LogIndent(indent + 1).block() }
+
+ printIndent(indent)
+ print(prefix)
+ print(": ")
+ println(duration.toString(DurationUnit.MICROSECONDS))
+ return result
+}
+
+@Suppress("NOTHING_TO_INLINE")
+private inline fun printIndent(indent: Int) {
+ for (i in 0 until indent) {
+ print(" ")
+ }
+}
+
internal fun <A> CoroutineScope.asyncImmediate(
start: CoroutineStart = CoroutineStart.UNDISPATCHED,
context: CoroutineContext = EmptyCoroutineContext,
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()) } }