diff options
author | 2024-12-06 15:45:38 -0500 | |
---|---|---|
committer | 2025-01-03 15:58:34 -0500 | |
commit | cdff97f1f5b958a8d0a3977b3d62dcd12a89b49e (patch) | |
tree | 6d0fe5ff8d3e5708ad3d441c7cd0f9d977ee1a8a | |
parent | d6865b295361c40202506a86aeaa6dfbbbb6e5c7 (diff) |
[kairos] rename many APIs
* TState -> State
* TFlow -> Events
* FrpBuildScope -> BuildScope
* FrpStateScope -> StateScope
* FrpTransactionScope -> TransactionScope
* FrpEffectScope -> EffectScope
* FrpScope -> KairosScope
etc.
Flag: EXEMPT unused
Test: atest kairos-tests
Change-Id: I56eb686be46833539c93d8c56c5a2eec93af54b6
40 files changed, 4087 insertions, 4144 deletions
diff --git a/packages/SystemUI/utils/kairos/Android.bp b/packages/SystemUI/utils/kairos/Android.bp index 1442591eab99..e10de9978252 100644 --- a/packages/SystemUI/utils/kairos/Android.bp +++ b/packages/SystemUI/utils/kairos/Android.bp @@ -22,7 +22,7 @@ package { java_library { name: "kairos", host_supported: true, - kotlincflags: ["-opt-in=com.android.systemui.kairos.ExperimentalFrpApi"], + kotlincflags: ["-opt-in=com.android.systemui.kairos.ExperimentalKairosApi"], srcs: ["src/**/*.kt"], static_libs: [ "kotlin-stdlib", @@ -32,6 +32,7 @@ java_library { java_test { name: "kairos-test", + kotlincflags: ["-opt-in=com.android.systemui.kairos.ExperimentalKairosApi"], optimize: { enabled: false, }, diff --git a/packages/SystemUI/utils/kairos/README.md b/packages/SystemUI/utils/kairos/README.md index 85f622ca05f3..5174c45a8d82 100644 --- a/packages/SystemUI/utils/kairos/README.md +++ b/packages/SystemUI/utils/kairos/README.md @@ -22,22 +22,21 @@ you can view the semantics for `Kairos` [here](docs/semantics.md). ## Usage -First, stand up a new `FrpNetwork`. All reactive events and state is kept +First, stand up a new `KairosNetwork`. All reactive events and state is kept consistent within a single network. ``` kotlin val coroutineScope: CoroutineScope = ... -val frpNetwork = coroutineScope.newFrpNetwork() +val network = coroutineScope.launchKairosNetwork() ``` -You can use the `FrpNetwork` to stand-up a network of reactive events and state. -Events are modeled with `TFlow` (short for "transactional flow"), and state -`TState` (short for "transactional state"). +You can use the `KairosNetwork` to stand-up a network of reactive events and +state. Events are modeled with `Events`, and states with `State`. ``` kotlin -suspend fun activate(network: FrpNetwork) { +suspend fun activate(network: KairosNetwork) { network.activateSpec { - val input = network.mutableTFlow<Unit>() + val input = network.mutableEvents<Unit>() // Launch a long-running side-effect that emits to the network // every second. launchEffect { @@ -47,7 +46,7 @@ suspend fun activate(network: FrpNetwork) { } } // Accumulate state - val count: TState<Int> = input.fold { _, i -> i + 1 } + val count: State<Int> = input.foldState { _, i -> i + 1 } // Observe events to perform side-effects in reaction to them input.observe { println("Got event ${count.sample()} at time: ${System.currentTimeMillis()}") @@ -56,7 +55,7 @@ suspend fun activate(network: FrpNetwork) { } ``` -`FrpNetwork.activateSpec` will suspend indefinitely; cancelling the invocation +`KairosNetwork.activateSpec` will suspend indefinitely; cancelling the invocation will tear-down all effects and obervers running within the lambda. ## Resources diff --git a/packages/SystemUI/utils/kairos/docs/flow-to-kairos-cheatsheet.md b/packages/SystemUI/utils/kairos/docs/flow-to-kairos-cheatsheet.md index 9f7fd022f019..afe64377676d 100644 --- a/packages/SystemUI/utils/kairos/docs/flow-to-kairos-cheatsheet.md +++ b/packages/SystemUI/utils/kairos/docs/flow-to-kairos-cheatsheet.md @@ -2,117 +2,116 @@ ## Key differences -* Kairos evaluates all events (`TFlow` emissions + observers) in a transaction. +* Kairos evaluates all events (`Events` emissions + observers) in a transaction. -* Kairos splits `Flow` APIs into two distinct types: `TFlow` and `TState` +* Kairos splits `Flow` APIs into two distinct types: `Events` and `State` - * `TFlow` is roughly equivalent to `SharedFlow` w/ a replay cache that + * `Events` is roughly equivalent to `SharedFlow` w/ a replay cache that exists for the duration of the current Kairos transaction and shared with `SharingStarted.WhileSubscribed()` - * `TState` is roughly equivalent to `StateFlow` shared with + * `State` is roughly equivalent to `StateFlow` shared with `SharingStarted.Eagerly`, but the current value can only be queried within a Kairos transaction, and the value is only updated at the end of the transaction * Kairos further divides `Flow` APIs based on how they internally use state: - * **FrpTransactionScope:** APIs that internally query some state need to be + * **TransactionScope:** APIs that internally query some state need to be performed within an Kairos transaction * this scope is available from the other scopes, and from most lambdas passed to other Kairos APIs - * **FrpStateScope:** APIs that internally accumulate state in reaction to - events need to be performed within an FRP State scope (akin to a - `CoroutineScope`) + * **StateScope:** APIs that internally accumulate state in reaction to events + need to be performed within a State scope (akin to a `CoroutineScope`) - * this scope is a side-effect-free subset of FrpBuildScope, and so can be - used wherever you have an FrpBuildScope + * this scope is a side-effect-free subset of BuildScope, and so can be + used wherever you have an BuildScope - * **FrpBuildScope:** APIs that perform external side-effects (`Flow.collect`) - need to be performed within an FRP Build scope (akin to a `CoroutineScope`) + * **BuildScope:** APIs that perform external side-effects (`Flow.collect`) + need to be performed within a Build scope (akin to a `CoroutineScope`) - * this scope is available from `FrpNetwork.activateSpec { … }` + * this scope is available from `Network.activateSpec { … }` * All other APIs can be used anywhere ## emptyFlow() -Use `emptyTFlow` +Use `emptyEvents` ``` kotlin -// this TFlow emits nothing -val noEvents: TFlow<Int> = emptyTFlow +// this Events emits nothing +val noEvents: Events<Int> = emptyEvents ``` ## map { … } -Use `TFlow.map` / `TState.map` +Use `Events.map` / `State.map` ``` kotlin -val anInt: TState<Int> = … -val squared: TState<Int> = anInt.map { it * it } -val messages: TFlow<String> = … -val messageLengths: TFlow<Int> = messages.map { it.size } +val anInt: State<Int> = … +val squared: State<Int> = anInt.map { it * it } +val messages: Events<String> = … +val messageLengths: Events<Int> = messages.map { it.size } ``` ## filter { … } / mapNotNull { … } -### I have a TFlow +### I have an Events -Use `TFlow.filter` / `TFlow.mapNotNull` +Use `Events.filter` / `Events.mapNotNull` ``` kotlin -val messages: TFlow<String> = … -val nonEmpty: TFlow<String> = messages.filter { it.isNotEmpty() } +val messages: Events<String> = … +val nonEmpty: Events<String> = messages.filter { it.isNotEmpty() } ``` -### I have a TState +### I have a State -Convert the `TState` to `TFlow` using `TState.stateChanges`, then use -`TFlow.filter` / `TFlow.mapNotNull` +Convert the `State` to `Events` using `State.stateChanges`, then use +`Events.filter` / `Events.mapNotNull` -If you need to convert back to `TState`, use `TFlow.hold(initialValue)` on the -result. +If you need to convert back to `State`, use `Events.holdState(initialValue)` on +the result. ``` kotlin -tState.stateChanges.filter { … }.hold(initialValue) +state.stateChanges.filter { … }.holdState(initialValue) ``` -Note that `TFlow.hold` is only available within an `FrpStateScope` in order to -track the lifetime of the state accumulation. +Note that `Events.holdState` is only available within an `StateScope` in order +to track the lifetime of the state accumulation. ## combine(...) { … } -### I have TStates +### I have States -Use `combine(TStates)` +Use `combine(States)` ``` kotlin -val someInt: TState<Int> = … -val someString: TState<String> = … -val model: TState<MyModel> = combine(someInt, someString) { i, s -> MyModel(i, s) } +val someInt: State<Int> = … +val someString: State<String> = … +val model: State<MyModel> = combine(someInt, someString) { i, s -> MyModel(i, s) } ``` -### I have TFlows +### I have Events -Convert the TFlows to TStates using `TFlow.hold(initialValue)`, then use -`combine(TStates)` +Convert the Events to States using `Events.holdState(initialValue)`, then use +`combine(States)` If you want the behavior of Flow.combine where nothing is emitted until each -TFlow has emitted at least once, you can use filter: +Events has emitted at least once, you can use filter: ``` kotlin // null used as an example, can use a different sentinel if needed -combine(tFlowA.hold(null), tFlowB.hold(null)) { a, b -> +combine(eventsA.holdState(null), eventsB.holdState(null)) { a, b -> a?.let { b?.let { … } } } .filterNotNull() ``` -Note that `TFlow.hold` is only available within an `FrpStateScope` in order to -track the lifetime of the state accumulation. +Note that `Events.holdState` is only available within an `StateScope` in order +to track the lifetime of the state accumulation. #### Explanation @@ -126,7 +125,7 @@ has emitted at least once. This often bites developers. As a workaround, developers generally append `.onStart { emit(initialValue) }` to the `Flows` that don't immediately emit. -Kairos avoids this gotcha by forcing usage of `TState` for `combine`, thus +Kairos avoids this gotcha by forcing usage of `State` for `combine`, thus ensuring that there is always a current value to be combined for each input. ## collect { … } @@ -134,197 +133,197 @@ ensuring that there is always a current value to be combined for each input. Use `observe { … }` ``` kotlin -val job: Job = tFlow.observe { println("observed: $it") } +val job: Job = events.observe { println("observed: $it") } ``` -Note that `observe` is only available within an `FrpBuildScope` in order to -track the lifetime of the observer. `FrpBuildScope` can only come from a -top-level `FrpNetwork.transaction { … }`, or a sub-scope created by using a -`-Latest` operator. +Note that `observe` is only available within a `BuildScope` in order to track +the lifetime of the observer. `BuildScope` can only come from a top-level +`Network.transaction { … }`, or a sub-scope created by using a `-Latest` +operator. ## sample(flow) { … } -### I want to sample a TState +### I want to sample a State -Use `TState.sample()` to get the current value of a `TState`. This can be -invoked anywhere you have access to an `FrpTransactionScope`. +Use `State.sample()` to get the current value of a `State`. This can be +invoked anywhere you have access to an `TransactionScope`. ``` kotlin -// the lambda passed to map receives an FrpTransactionScope, so it can invoke +// the lambda passed to map receives an TransactionScope, so it can invoke // sample -tFlow.map { tState.sample() } +events.map { state.sample() } ``` #### Explanation -To keep all state-reads consistent, the current value of a TState can only be -queried within a Kairos transaction, modeled with `FrpTransactionScope`. Note -that both `FrpStateScope` and `FrpBuildScope` extend `FrpTransactionScope`. +To keep all state-reads consistent, the current value of a State can only be +queried within a Kairos transaction, modeled with `TransactionScope`. Note that +both `StateScope` and `BuildScope` extend `TransactionScope`. -### I want to sample a TFlow +### I want to sample an Events -Convert to a `TState` by using `TFlow.hold(initialValue)`, then use `sample`. +Convert to a `State` by using `Events.holdState(initialValue)`, then use `sample`. -Note that `hold` is only available within an `FrpStateScope` in order to track +Note that `holdState` is only available within an `StateScope` in order to track the lifetime of the state accumulation. ## stateIn(scope, sharingStarted, initialValue) -Use `TFlow.hold(initialValue)`. There is no need to supply a sharingStarted -argument; all states are accumulated eagerly. +Use `Events.holdState(initialValue)`. There is no need to supply a +sharingStarted argument; all states are accumulated eagerly. ``` kotlin -val ints: TFlow<Int> = … -val lastSeenInt: TState<Int> = ints.hold(initialValue = 0) +val ints: Events<Int> = … +val lastSeenInt: State<Int> = ints.holdState(initialValue = 0) ``` -Note that `hold` is only available within an `FrpStateScope` in order to track +Note that `holdState` is only available within an `StateScope` in order to track the lifetime of the state accumulation (akin to the scope parameter of -`Flow.stateIn`). `FrpStateScope` can only come from a top-level -`FrpNetwork.transaction { … }`, or a sub-scope created by using a `-Latest` -operator. Also note that `FrpBuildScope` extends `FrpStateScope`. +`Flow.stateIn`). `StateScope` can only come from a top-level +`Network.transaction { … }`, or a sub-scope created by using a `-Latest` +operator. Also note that `BuildScope` extends `StateScope`. ## distinctUntilChanged() -Use `distinctUntilChanged` like normal. This is only available for `TFlow`; -`TStates` are already `distinctUntilChanged`. +Use `distinctUntilChanged` like normal. This is only available for `Events`; +`States` are already `distinctUntilChanged`. ## merge(...) -### I have TFlows +### I have Eventss -Use `merge(TFlows) { … }`. The lambda argument is used to disambiguate multiple +Use `merge(Events) { … }`. The lambda argument is used to disambiguate multiple simultaneous emissions within the same transaction. #### Explanation -Under Kairos's rules, a `TFlow` may only emit up to once per transaction. This -means that if we are merging two or more `TFlows` that are emitting at the same -time (within the same transaction), the resulting merged `TFlow` must emit a +Under Kairos's rules, an `Events` may only emit up to once per transaction. This +means that if we are merging two or more `Events` that are emitting at the same +time (within the same transaction), the resulting merged `Events` must emit a single value. The lambda argument allows the developer to decide what to do in this case. -### I have TStates +### I have States -If `combine` doesn't satisfy your needs, you can use `TState.stateChanges` to -convert to a `TFlow`, and then `merge`. +If `combine` doesn't satisfy your needs, you can use `State.changes` to +convert to a `Events`, and then `merge`. ## conflatedCallbackFlow { … } -Use `tFlow { … }`. +Use `events { … }`. As a shortcut, if you already have a `conflatedCallbackFlow { … }`, you can -convert it to a TFlow via `Flow.toTFlow()`. +convert it to an Events via `Flow.toEvents()`. -Note that `tFlow` is only available within an `FrpBuildScope` in order to track -the lifetime of the input registration. +Note that `events` is only available within a `BuildScope` in order to track the +lifetime of the input registration. ## first() -### I have a TState +### I have a State -Use `TState.sample`. +Use `State.sample`. -### I have a TFlow +### I have an Events -Use `TFlow.nextOnly`, which works exactly like `Flow.first` but instead of -suspending it returns a `TFlow` that emits once. +Use `Events.nextOnly`, which works exactly like `Flow.first` but instead of +suspending it returns a `Events` that emits once. The naming is intentionally different because `first` implies that it is the first-ever value emitted from the `Flow` (which makes sense for cold `Flows`), whereas `nextOnly` indicates that only the next value relative to the current transaction (the one `nextOnly` is being invoked in) will be emitted. -Note that `nextOnly` is only available within an `FrpStateScope` in order to -track the lifetime of the state accumulation. +Note that `nextOnly` is only available within an `StateScope` in order to track +the lifetime of the state accumulation. ## flatMapLatest { … } If you want to use -Latest to cancel old side-effects, similar to what the Flow -Latest operators offer for coroutines, see `mapLatest`. -### I have a TState… +### I have a State… -#### …and want to switch TStates +#### …and want to switch States -Use `TState.flatMap` +Use `State.flatMap` ``` kotlin -val flattened = tState.flatMap { a -> getTState(a) } +val flattened = state.flatMap { a -> gestate(a) } ``` -#### …and want to switch TFlows +#### …and want to switch Events -Use `TState<TFlow<T>>.switch()` +Use `State<Events<T>>.switchEvents()` ``` kotlin -val tFlow = tState.map { a -> getTFlow(a) }.switch() +val events = state.map { a -> getEvents(a) }.switchEvents() ``` -### I have a TFlow… +### I have an Events… -#### …and want to switch TFlows +#### …and want to switch Events -Use `hold` to convert to a `TState<TFlow<T>>`, then use `switch` to switch to -the latest `TFlow`. +Use `holdState` to convert to a `State<Events<T>>`, then use `switchEvents` to +switch to the latest `Events`. ``` kotlin -val tFlow = tFlowOfFlows.hold(emptyTFlow).switch() +val events = eventsOfFlows.holdState(emptyEvents).switchEvents() ``` -#### …and want to switch TStates +#### …and want to switch States -Use `hold` to convert to a `TState<TState<T>>`, then use `flatMap` to switch to -the latest `TState`. +Use `holdState` to convert to a `State<State<T>>`, then use `flatMap` to switch +to the latest `State`. ``` kotlin -val tState = tFlowOfStates.hold(tStateOf(initialValue)).flatMap { it } +val state = eventsOfStates.holdState(stateOf(initialValue)).flatMap { it } ``` ## mapLatest { … } / collectLatest { … } -`FrpStateScope` and `FrpBuildScope` both provide `-Latest` operators that +`StateScope` and `BuildScope` both provide `-Latest` operators that automatically cancel old work when new values are emitted. ``` kotlin -val currentModel: TState<SomeModel> = … -val mapped: TState<...> = currentModel.mapLatestBuild { model -> +val currentModel: State<SomeModel> = … +val mapped: State<...> = currentModel.mapLatestBuild { model -> effect { "new model in the house: $model" } model.someState.observe { "someState: $it" } - val someData: TState<SomeInfo> = + val someData: State<SomeInfo> = getBroadcasts(model.uri) .map { extractInfo(it) } - .hold(initialInfo) + .holdState(initialInfo) … } ``` ## flowOf(...) -### I want a TState +### I want a State -Use `tStateOf(initialValue)`. +Use `stateOf(initialValue)`. -### I want a TFlow +### I want an Events Use `now.map { initialValue }` -Note that `now` is only available within an `FrpTransactionScope`. +Note that `now` is only available within an `TransactionScope`. #### Explanation -`TFlows` are not cold, and so there isn't a notion of "emit this value once +`Events` are not cold, and so there isn't a notion of "emit this value once there is a collector" like there is for `Flow`. The closest analog would be -`TState`, since the initial value is retained indefinitely until there is an +`State`, since the initial value is retained indefinitely until there is an observer. However, it is often useful to immediately emit a value within the -current transaction, usually when using a `flatMap` or `switch`. In these cases, -using `now` explicitly models that the emission will occur within the current -transaction. +current transaction, usually when using a `flatMap` or `switchEvents`. In these +cases, using `now` explicitly models that the emission will occur within the +current transaction. ``` kotlin -fun <T> FrpTransactionScope.tFlowOf(value: T): TFlow<T> = now.map { value } +fun <T> TransactionScope.eventsOf(value: T): Events<T> = now.map { value } ``` ## MutableStateFlow / MutableSharedFlow -Use `MutableTState(frpNetwork, initialValue)` and `MutableTFlow(frpNetwork)`. +Use `MutableState(frpNetwork, initialValue)` and `MutableEvents(frpNetwork)`. diff --git a/packages/SystemUI/utils/kairos/docs/semantics.md b/packages/SystemUI/utils/kairos/docs/semantics.md index d43bb4447061..c8e468050037 100644 --- a/packages/SystemUI/utils/kairos/docs/semantics.md +++ b/packages/SystemUI/utils/kairos/docs/semantics.md @@ -33,39 +33,39 @@ sealed class Time : Comparable<Time> { typealias Transactional<T> = (Time) -> T -typealias TFlow<T> = SortedMap<Time, T> +typealias Events<T> = SortedMap<Time, T> private fun <T> SortedMap<Time, T>.pairwise(): List<Pair<Pair<Time, T>, Pair<Time<T>>>> = // NOTE: pretend evaluation is lazy, so that error() doesn't immediately throw (toList() + Pair(Time.Infinity, error("no value"))).zipWithNext() -class TState<T> internal constructor( +class State<T> internal constructor( internal val current: Transactional<T>, - val stateChanges: TFlow<T>, + val stateChanges: Events<T>, ) -val emptyTFlow: TFlow<Nothing> = emptyMap() +val emptyEvents: Events<Nothing> = emptyMap() -fun <A, B> TFlow<A>.map(f: FrpTransactionScope.(A) -> B): TFlow<B> = - mapValues { (t, a) -> FrpTransactionScope(t).f(a) } +fun <A, B> Events<A>.map(f: TransactionScope.(A) -> B): Events<B> = + mapValues { (t, a) -> TransactionScope(t).f(a) } -fun <A> TFlow<A>.filter(f: FrpTransactionScope.(A) -> Boolean): TFlow<A> = - filter { (t, a) -> FrpTransactionScope(t).f(a) } +fun <A> Events<A>.filter(f: TransactionScope.(A) -> Boolean): Events<A> = + filter { (t, a) -> TransactionScope(t).f(a) } fun <A> merge( - first: TFlow<A>, - second: TFlow<A>, + first: Events<A>, + second: Events<A>, onCoincidence: Time.(A, A) -> A, -): TFlow<A> = +): Events<A> = first.toMutableMap().also { result -> second.forEach { (t, a) -> result.merge(t, a) { f, s -> - FrpTranscationScope(t).onCoincidence(f, a) + TransactionScope(t).onCoincidence(f, a) } } }.toSortedMap() -fun <A> TState<TFlow<A>>.switch(): TFlow<A> { +fun <A> State<Events<A>>.switchEvents(): Events<A> { val truncated = listOf(Pair(Time.BigBang, current.invoke(Time.BigBang))) + stateChanges.dropWhile { (time, _) -> time < time0 } val events = @@ -77,7 +77,7 @@ fun <A> TState<TFlow<A>>.switch(): TFlow<A> { return events.toSortedMap() } -fun <A> TState<TFlow<A>>.switchPromptly(): TFlow<A> { +fun <A> State<Events<A>>.switchEventsPromptly(): Events<A> { val truncated = listOf(Pair(Time.BigBang, current.invoke(Time.BigBang))) + stateChanges.dropWhile { (time, _) -> time < time0 } val events = @@ -89,24 +89,24 @@ fun <A> TState<TFlow<A>>.switchPromptly(): TFlow<A> { return events.toSortedMap() } -typealias GroupedTFlow<K, V> = TFlow<Map<K, V>> +typealias GroupedEvents<K, V> = Events<Map<K, V>> -fun <K, V> TFlow<Map<K, V>>.groupByKey(): GroupedTFlow<K, V> = this +fun <K, V> Events<Map<K, V>>.groupByKey(): GroupedEvents<K, V> = this -fun <K, V> GroupedTFlow<K, V>.eventsForKey(key: K): TFlow<V> = +fun <K, V> GroupedEvents<K, V>.eventsForKey(key: K): Events<V> = map { m -> m[k] }.filter { it != null }.map { it!! } -fun <A, B> TState<A>.map(f: (A) -> B): TState<B> = - TState( +fun <A, B> State<A>.map(f: (A) -> B): State<B> = + State( current = { t -> f(current.invoke(t)) }, stateChanges = stateChanges.map { f(it) }, ) -fun <A, B, C> TState<A>.combineWith( - other: TState<B>, +fun <A, B, C> State<A>.combineWith( + other: State<B>, f: (A, B) -> C, -): TState<C> = - TState( +): State<C> = + State( current = { t -> f(current.invoke(t), other.current.invoke(t)) }, stateChanges = run { val aChanges = @@ -129,7 +129,7 @@ fun <A, B, C> TState<A>.combineWith( }, ) -fun <A> TState<TState<A>>.flatten(): TState<A> { +fun <A> State<State<A>>.flatten(): State<A> { val changes = stateChanges .pairwise() @@ -144,55 +144,55 @@ fun <A> TState<TState<A>>.flatten(): TState<A> { inWindow } } - return TState( + return State( current = { t -> current.invoke(t).current.invoke(t) }, stateChanges = changes.toSortedMap(), ) } -open class FrpTranscationScope internal constructor( +open class TransactionScope internal constructor( internal val currentTime: Time, ) { - val now: TFlow<Unit> = + val now: Events<Unit> = sortedMapOf(currentTime to Unit) fun <A> Transactional<A>.sample(): A = invoke(currentTime) - fun <A> TState<A>.sample(): A = + fun <A> State<A>.sample(): A = current.sample() } -class FrpStateScope internal constructor( +class StateScope internal constructor( time: Time, internal val stopTime: Time, -): FrpTransactionScope(time) { +): TransactionScope(time) { - fun <A, B> TFlow<A>.fold( + fun <A, B> Events<A>.foldState( initialValue: B, - f: FrpTransactionScope.(B, A) -> B, - ): TState<B> { + f: TransactionScope.(B, A) -> B, + ): State<B> { val truncated = dropWhile { (t, _) -> t < currentTime } .takeWhile { (t, _) -> t <= stopTime } - val folded = + val foldStateed = truncated .scan(Pair(currentTime, initialValue)) { (_, b) (t, a) -> - Pair(t, FrpTransactionScope(t).f(a, b)) + Pair(t, TransactionScope(t).f(a, b)) } val lookup = { t1 -> - folded.lastOrNull { (t0, _) -> t0 < t1 }?.value ?: initialValue + foldStateed.lastOrNull { (t0, _) -> t0 < t1 }?.value ?: initialValue } - return TState(lookup, folded.toSortedMap()) + return State(lookup, foldStateed.toSortedMap()) } - fun <A> TFlow<A>.hold(initialValue: A): TState<A> = - fold(initialValue) { _, a -> a } + fun <A> Events<A>.holdState(initialValue: A): State<A> = + foldState(initialValue) { _, a -> a } - fun <K, V> TFlow<Map<K, Maybe<V>>>.foldMapIncrementally( + fun <K, V> Events<Map<K, Maybe<V>>>.foldStateMapIncrementally( initialValues: Map<K, V> - ): TState<Map<K, V>> = - fold(initialValues) { patch, map -> + ): State<Map<K, V>> = + foldState(initialValues) { patch, map -> val eithers = patch.map { (k, v) -> if (v is Just) Left(k to v.value) else Right(k) } @@ -203,18 +203,18 @@ class FrpStateScope internal constructor( updated } - fun <K : Any, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementally( - initialTFlows: Map<K, TFlow<V>>, - ): TFlow<Map<K, V>> = - foldMapIncrementally(initialTFlows).map { it.merge() }.switch() + fun <K : Any, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally( + initialEventss: Map<K, Events<V>>, + ): Events<Map<K, V>> = + foldStateMapIncrementally(initialEventss).map { it.merge() }.switchEvents() - fun <K, A, B> TFlow<Map<K, Maybe<A>>.mapLatestStatefulForKey( - transform: suspend FrpStateScope.(A) -> B, - ): TFlow<Map<K, Maybe<B>>> = + fun <K, A, B> Events<Map<K, Maybe<A>>.mapLatestStatefulForKey( + transform: suspend StateScope.(A) -> B, + ): Events<Map<K, Maybe<B>>> = pairwise().map { ((t0, patch), (t1, _)) -> patch.map { (k, ma) -> ma.map { a -> - FrpStateScope(t0, t1).transform(a) + StateScope(t0, t1).transform(a) } } } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/BuildScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/BuildScope.kt new file mode 100644 index 000000000000..3cba163dcb5f --- /dev/null +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/BuildScope.kt @@ -0,0 +1,862 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.kairos + +import com.android.systemui.kairos.util.Maybe +import com.android.systemui.kairos.util.just +import com.android.systemui.kairos.util.map +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Job +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.dropWhile +import kotlinx.coroutines.flow.scan +import kotlinx.coroutines.launch + +/** A function that modifies the KairosNetwork. */ +typealias BuildSpec<A> = BuildScope.() -> A + +/** + * Constructs a [BuildSpec]. The passed [block] will be invoked with a [BuildScope] that can be used + * to perform network-building operations, including adding new inputs and outputs to the network, + * as well as all operations available in [TransactionScope]. + */ +@ExperimentalKairosApi +@Suppress("NOTHING_TO_INLINE") +inline fun <A> buildSpec(noinline block: BuildScope.() -> A): BuildSpec<A> = block + +/** Applies the [BuildSpec] within this [BuildScope]. */ +@ExperimentalKairosApi +inline operator fun <A> BuildScope.invoke(block: BuildScope.() -> A) = run(block) + +/** Operations that add inputs and outputs to a Kairos network. */ +@ExperimentalKairosApi +interface BuildScope : StateScope { + + /** + * A [KairosNetwork] handle that is bound to this [BuildScope]. + * + * It supports all of the standard functionality by which external code can interact with this + * Kairos network, but all [activated][KairosNetwork.activateSpec] [BuildSpec]s are bound as + * children to this [BuildScope], such that when this [BuildScope] is destroyed, all children + * are also destroyed. + */ + val kairosNetwork: KairosNetwork + + /** + * Defers invoking [block] until after the current [BuildScope] code-path completes, returning a + * [DeferredValue] that can be used to reference the result. + * + * Useful for recursive definitions. + * + * @see deferredBuildScopeAction + * @see DeferredValue + */ + fun <R> deferredBuildScope(block: BuildScope.() -> R): DeferredValue<R> + + /** + * Defers invoking [block] until after the current [BuildScope] code-path completes. + * + * Useful for recursive definitions. + * + * @see deferredBuildScope + */ + fun deferredBuildScopeAction(block: BuildScope.() -> Unit) + + /** + * Returns an [Events] containing the results of applying [transform] to each value of the + * original [Events]. + * + * [transform] can perform modifications to the Kairos network via its [BuildScope] receiver. + * Unlike [mapLatestBuild], these modifications are not undone with each subsequent emission of + * the original [Events]. + * + * **NOTE:** This API does not [observe] the original [Events], meaning that unless the returned + * (or a downstream) [Events] is observed separately, [transform] will not be invoked, and no + * internal side-effects will occur. + */ + fun <A, B> Events<A>.mapBuild(transform: BuildScope.(A) -> B): Events<B> + + /** + * Invokes [block] whenever this [Events] emits a value, allowing side-effects to be safely + * performed in reaction to the emission. + * + * Specifically, [block] is deferred to the end of the transaction, and is only actually + * executed if this [BuildScope] is still active by that time. It can be deactivated due to a + * -Latest combinator, for example. + * + * Shorthand for: + * ```kotlin + * events.observe { effect { ... } } + * ``` + */ + fun <A> Events<A>.observe( + coroutineContext: CoroutineContext = EmptyCoroutineContext, + block: EffectScope.(A) -> Unit = {}, + ): Job + + /** + * Returns an [Events] containing the results of applying each [BuildSpec] emitted from the + * original [Events], and a [DeferredValue] containing the result of applying [initialSpecs] + * immediately. + * + * When each [BuildSpec] is applied, changes from the previously-active [BuildSpec] with the + * same key are undone (any registered [observers][observe] are unregistered, and any pending + * [side-effects][effect] are cancelled). + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [BuildSpec] will be undone with no replacement. + */ + fun <K, A, B> Events<Map<K, Maybe<BuildSpec<A>>>>.applyLatestSpecForKey( + initialSpecs: DeferredValue<Map<K, BuildSpec<B>>>, + numKeys: Int? = null, + ): Pair<Events<Map<K, Maybe<A>>>, DeferredValue<Map<K, B>>> + + /** + * Creates an instance of an [Events] with elements that are from [builder]. + * + * [builder] is run in its own coroutine, allowing for ongoing work that can emit to the + * provided [MutableState]. + * + * By default, [builder] is only running while the returned [Events] is being + * [observed][observe]. If you want it to run at all times, simply add a no-op observer: + * ```kotlin + * events { ... }.apply { observe() } + * ``` + */ + fun <T> events( + name: String? = null, + builder: suspend EventProducerScope<T>.() -> Unit, + ): Events<T> + + /** + * Creates an instance of an [Events] with elements that are emitted from [builder]. + * + * [builder] is run in its own coroutine, allowing for ongoing work that can emit to the + * provided [MutableState]. + * + * By default, [builder] is only running while the returned [Events] is being + * [observed][observe]. If you want it to run at all times, simply add a no-op observer: + * ```kotlin + * events { ... }.apply { observe() } + * ``` + * + * In the event of backpressure, emissions are *coalesced* into batches. When a value is + * [emitted][CoalescingEventProducerScope.emit] from [builder], it is merged into the batch via + * [coalesce]. Once the batch is consumed by the kairos network in the next transaction, the + * batch is reset back to [getInitialValue]. + */ + fun <In, Out> coalescingEvents( + getInitialValue: () -> Out, + coalesce: (old: Out, new: In) -> Out, + builder: suspend CoalescingEventProducerScope<In>.() -> Unit, + ): Events<Out> + + /** + * Creates a new [BuildScope] that is a child of this one. + * + * This new scope can be manually cancelled via the returned [Job], or will be cancelled + * automatically when its parent is cancelled. Cancellation will unregister all + * [observers][observe] and cancel all scheduled [effects][effect]. + * + * The return value from [block] can be accessed via the returned [DeferredValue]. + */ + fun <A> asyncScope(block: BuildSpec<A>): Pair<DeferredValue<A>, Job> + + // TODO: once we have context params, these can all become extensions: + + /** + * Returns an [Events] containing the results of applying the given [transform] function to each + * value of the original [Events]. + * + * Unlike [Events.map], [transform] can perform arbitrary asynchronous code. This code is run + * outside of the current Kairos transaction; when [transform] returns, the returned value is + * emitted from the result [Events] in a new transaction. + * + * Shorthand for: + * ```kotlin + * events.mapLatestBuild { a -> asyncEvent { transform(a) } }.flatten() + * ``` + */ + fun <A, B> Events<A>.mapAsyncLatest(transform: suspend (A) -> B): Events<B> = + mapLatestBuild { a -> asyncEvent { transform(a) } }.flatten() + + /** + * Invokes [block] whenever this [Events] emits a value. [block] receives an [BuildScope] that + * can be used to make further modifications to the Kairos network, and/or perform side-effects + * via [effect]. + * + * @see observe + */ + fun <A> Events<A>.observeBuild(block: BuildScope.(A) -> Unit = {}): Job = + mapBuild(block).observe() + + /** + * Returns a [StateFlow] whose [value][StateFlow.value] tracks the current + * [value of this State][State.sample], and will emit at the same rate as [State.changes]. + * + * Note that the [value][StateFlow.value] is not available until the *end* of the current + * transaction. If you need the current value before this time, then use [State.sample]. + */ + fun <A> State<A>.toStateFlow(): StateFlow<A> { + val uninitialized = Any() + var initialValue: Any? = uninitialized + val innerStateFlow = MutableStateFlow<Any?>(uninitialized) + deferredBuildScope { + initialValue = sample() + changes.observe { + innerStateFlow.value = it + initialValue = null + } + } + + @Suppress("UNCHECKED_CAST") + fun getValue(innerValue: Any?): A = + when { + innerValue !== uninitialized -> innerValue as A + initialValue !== uninitialized -> initialValue as A + else -> + error( + "Attempted to access StateFlow.value before Kairos transaction has completed." + ) + } + + return object : StateFlow<A> { + override val replayCache: List<A> + get() = innerStateFlow.replayCache.map(::getValue) + + override val value: A + get() = getValue(innerStateFlow.value) + + override suspend fun collect(collector: FlowCollector<A>): Nothing { + innerStateFlow.collect { collector.emit(getValue(it)) } + } + } + } + + /** + * Returns a [SharedFlow] configured with a replay cache of size [replay] that emits the current + * [value][State.sample] of this [State] followed by all [changes]. + */ + fun <A> State<A>.toSharedFlow(replay: Int = 0): SharedFlow<A> { + val result = MutableSharedFlow<A>(replay, extraBufferCapacity = 1) + deferredBuildScope { + result.tryEmit(sample()) + changes.observe { a -> result.tryEmit(a) } + } + return result + } + + /** + * Returns a [SharedFlow] configured with a replay cache of size [replay] that emits values + * whenever this [Events] emits. + */ + fun <A> Events<A>.toSharedFlow(replay: Int = 0): SharedFlow<A> { + val result = MutableSharedFlow<A>(replay, extraBufferCapacity = 1) + observe { a -> result.tryEmit(a) } + return result + } + + /** + * Returns a [State] that holds onto the value returned by applying the most recently emitted + * [BuildSpec] from the original [Events], or the value returned by applying [initialSpec] if + * nothing has been emitted since it was constructed. + * + * When each [BuildSpec] is applied, changes from the previously-active [BuildSpec] are undone + * (any registered [observers][observe] are unregistered, and any pending [side-effects][effect] + * are cancelled). + */ + fun <A> Events<BuildSpec<A>>.holdLatestSpec(initialSpec: BuildSpec<A>): State<A> { + val (changes: Events<A>, initApplied: DeferredValue<A>) = applyLatestSpec(initialSpec) + return changes.holdStateDeferred(initApplied) + } + + /** + * Returns a [State] containing the value returned by applying the [BuildSpec] held by the + * original [State]. + * + * When each [BuildSpec] is applied, changes from the previously-active [BuildSpec] are undone + * (any registered [observers][observe] are unregistered, and any pending [side-effects][effect] + * are cancelled). + */ + fun <A> State<BuildSpec<A>>.applyLatestSpec(): State<A> { + val (appliedChanges: Events<A>, init: DeferredValue<A>) = + changes.applyLatestSpec(buildSpec { sample().applySpec() }) + return appliedChanges.holdStateDeferred(init) + } + + /** + * Returns an [Events] containing the results of applying each [BuildSpec] emitted from the + * original [Events]. + * + * When each [BuildSpec] is applied, changes from the previously-active [BuildSpec] are undone + * (any registered [observers][observe] are unregistered, and any pending [side-effects][effect] + * are cancelled). + */ + fun <A> Events<BuildSpec<A>>.applyLatestSpec(): Events<A> = applyLatestSpec(buildSpec {}).first + + /** + * Returns an [Events] that switches to a new [Events] produced by [transform] every time the + * original [Events] emits a value. + * + * [transform] can perform modifications to the Kairos network via its [BuildScope] receiver. + * When the original [Events] emits a new value, those changes are undone (any registered + * [observers][observe] are unregistered, and any pending [effects][effect] are cancelled). + */ + fun <A, B> Events<A>.flatMapLatestBuild(transform: BuildScope.(A) -> Events<B>): Events<B> = + mapCheap { buildSpec { transform(it) } }.applyLatestSpec().flatten() + + /** + * Returns a [State] by applying [transform] to the value held by the original [State]. + * + * [transform] can perform modifications to the Kairos network via its [BuildScope] receiver. + * When the value held by the original [State] changes, those changes are undone (any registered + * [observers][observe] are unregistered, and any pending [effects][effect] are cancelled). + */ + fun <A, B> State<A>.flatMapLatestBuild(transform: BuildScope.(A) -> State<B>): State<B> = + mapLatestBuild { transform(it) }.flatten() + + /** + * Returns a [State] that transforms the value held inside this [State] by applying it to the + * [transform]. + * + * [transform] can perform modifications to the Kairos network via its [BuildScope] receiver. + * When the value held by the original [State] changes, those changes are undone (any registered + * [observers][observe] are unregistered, and any pending [effects][effect] are cancelled). + */ + fun <A, B> State<A>.mapLatestBuild(transform: BuildScope.(A) -> B): State<B> = + mapCheapUnsafe { buildSpec { transform(it) } }.applyLatestSpec() + + /** + * Returns an [Events] containing the results of applying each [BuildSpec] emitted from the + * original [Events], and a [DeferredValue] containing the result of applying [initialSpec] + * immediately. + * + * When each [BuildSpec] is applied, changes from the previously-active [BuildSpec] are undone + * (any registered [observers][observe] are unregistered, and any pending [side-effects][effect] + * are cancelled). + */ + fun <A : Any?, B> Events<BuildSpec<B>>.applyLatestSpec( + initialSpec: BuildSpec<A> + ): Pair<Events<B>, DeferredValue<A>> { + val (events, result) = + mapCheap { spec -> mapOf(Unit to just(spec)) } + .applyLatestSpecForKey(initialSpecs = mapOf(Unit to initialSpec), numKeys = 1) + val outEvents: Events<B> = + events.mapMaybe { + checkNotNull(it[Unit]) { "applyLatest: expected result, but none present in: $it" } + } + val outInit: DeferredValue<A> = deferredBuildScope { + val initResult: Map<Unit, A> = result.get() + check(Unit in initResult) { + "applyLatest: expected initial result, but none present in: $initResult" + } + @Suppress("UNCHECKED_CAST") + initResult.getOrDefault(Unit) { null } as A + } + return Pair(outEvents, outInit) + } + + /** + * Returns an [Events] containing the results of applying [transform] to each value of the + * original [Events]. + * + * [transform] can perform modifications to the Kairos network via its [BuildScope] receiver. + * With each invocation of [transform], changes from the previous invocation are undone (any + * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are + * cancelled). + */ + fun <A, B> Events<A>.mapLatestBuild(transform: BuildScope.(A) -> B): Events<B> = + mapCheap { buildSpec { transform(it) } }.applyLatestSpec() + + /** + * Returns an [Events] containing the results of applying [transform] to each value of the + * original [Events], and a [DeferredValue] containing the result of applying [transform] to + * [initialValue] immediately. + * + * [transform] can perform modifications to the Kairos network via its [BuildScope] receiver. + * With each invocation of [transform], changes from the previous invocation are undone (any + * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are + * cancelled). + */ + fun <A, B> Events<A>.mapLatestBuild( + initialValue: A, + transform: BuildScope.(A) -> B, + ): Pair<Events<B>, DeferredValue<B>> = + mapLatestBuildDeferred(deferredOf(initialValue), transform) + + /** + * Returns an [Events] containing the results of applying [transform] to each value of the + * original [Events], and a [DeferredValue] containing the result of applying [transform] to + * [initialValue] immediately. + * + * [transform] can perform modifications to the Kairos network via its [BuildScope] receiver. + * With each invocation of [transform], changes from the previous invocation are undone (any + * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are + * cancelled). + */ + fun <A, B> Events<A>.mapLatestBuildDeferred( + initialValue: DeferredValue<A>, + transform: BuildScope.(A) -> B, + ): Pair<Events<B>, DeferredValue<B>> = + mapCheap { buildSpec { transform(it) } } + .applyLatestSpec(initialSpec = buildSpec { transform(initialValue.get()) }) + + /** + * Returns an [Events] containing the results of applying each [BuildSpec] emitted from the + * original [Events], and a [DeferredValue] containing the result of applying [initialSpecs] + * immediately. + * + * When each [BuildSpec] is applied, changes from the previously-active [BuildSpec] with the + * same key are undone (any registered [observers][observe] are unregistered, and any pending + * [side-effects][effect] are cancelled). + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [BuildSpec] will be undone with no replacement. + */ + fun <K, A, B> Events<Map<K, Maybe<BuildSpec<A>>>>.applyLatestSpecForKey( + initialSpecs: Map<K, BuildSpec<B>>, + numKeys: Int? = null, + ): Pair<Events<Map<K, Maybe<A>>>, DeferredValue<Map<K, B>>> = + applyLatestSpecForKey(deferredOf(initialSpecs), numKeys) + + /** + * Returns an [Events] containing the results of applying each [BuildSpec] emitted from the + * original [Events]. + * + * When each [BuildSpec] is applied, changes from the previously-active [BuildSpec] with the + * same key are undone (any registered [observers][observe] are unregistered, and any pending + * [side-effects][effect] are cancelled). + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [BuildSpec] will be undone with no replacement. + */ + fun <K, A> Events<Map<K, Maybe<BuildSpec<A>>>>.applyLatestSpecForKey( + numKeys: Int? = null + ): Events<Map<K, Maybe<A>>> = + applyLatestSpecForKey<K, A, Nothing>(deferredOf(emptyMap()), numKeys).first + + /** + * Returns a [State] containing the latest results of applying each [BuildSpec] emitted from the + * original [Events]. + * + * When each [BuildSpec] is applied, changes from the previously-active [BuildSpec] with the + * same key are undone (any registered [observers][observe] are unregistered, and any pending + * [side-effects][effect] are cancelled). + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [BuildSpec] will be undone with no replacement. + */ + fun <K, A> Events<Map<K, Maybe<BuildSpec<A>>>>.holdLatestSpecForKey( + initialSpecs: DeferredValue<Map<K, BuildSpec<A>>>, + numKeys: Int? = null, + ): State<Map<K, A>> { + val (changes, initialValues) = applyLatestSpecForKey(initialSpecs, numKeys) + return changes.foldStateMapIncrementally(initialValues) + } + + /** + * Returns a [State] containing the latest results of applying each [BuildSpec] emitted from the + * original [Events]. + * + * When each [BuildSpec] is applied, changes from the previously-active [BuildSpec] with the + * same key are undone (any registered [observers][observe] are unregistered, and any pending + * [side-effects][effect] are cancelled). + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [BuildSpec] will be undone with no replacement. + */ + fun <K, A> Events<Map<K, Maybe<BuildSpec<A>>>>.holdLatestSpecForKey( + initialSpecs: Map<K, BuildSpec<A>> = emptyMap(), + numKeys: Int? = null, + ): State<Map<K, A>> = holdLatestSpecForKey(deferredOf(initialSpecs), numKeys) + + /** + * Returns an [Events] containing the results of applying [transform] to each value of the + * original [Events], and a [DeferredValue] containing the result of applying [transform] to + * [initialValues] immediately. + * + * [transform] can perform modifications to the Kairos network via its [BuildScope] receiver. + * With each invocation of [transform], changes from the previous invocation are undone (any + * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are + * cancelled). + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [BuildScope] will be undone with no replacement. + */ + fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestBuildForKey( + initialValues: DeferredValue<Map<K, A>>, + numKeys: Int? = null, + transform: BuildScope.(K, A) -> B, + ): Pair<Events<Map<K, Maybe<B>>>, DeferredValue<Map<K, B>>> = + map { patch -> patch.mapValues { (k, v) -> v.map { buildSpec { transform(k, it) } } } } + .applyLatestSpecForKey( + deferredBuildScope { + initialValues.get().mapValues { (k, v) -> buildSpec { transform(k, v) } } + }, + numKeys = numKeys, + ) + + /** + * Returns an [Events] containing the results of applying [transform] to each value of the + * original [Events], and a [DeferredValue] containing the result of applying [transform] to + * [initialValues] immediately. + * + * [transform] can perform modifications to the Kairos network via its [BuildScope] receiver. + * With each invocation of [transform], changes from the previous invocation are undone (any + * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are + * cancelled). + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [BuildScope] will be undone with no replacement. + */ + fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestBuildForKey( + initialValues: Map<K, A>, + numKeys: Int? = null, + transform: BuildScope.(K, A) -> B, + ): Pair<Events<Map<K, Maybe<B>>>, DeferredValue<Map<K, B>>> = + mapLatestBuildForKey(deferredOf(initialValues), numKeys, transform) + + /** + * Returns an [Events] containing the results of applying [transform] to each value of the + * original [Events]. + * + * [transform] can perform modifications to the Kairos network via its [BuildScope] receiver. + * With each invocation of [transform], changes from the previous invocation are undone (any + * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are + * cancelled). + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [BuildScope] will be undone with no replacement. + */ + fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestBuildForKey( + numKeys: Int? = null, + transform: BuildScope.(K, A) -> B, + ): Events<Map<K, Maybe<B>>> = mapLatestBuildForKey(emptyMap(), numKeys, transform).first + + /** Returns a [Deferred] containing the next value to be emitted from this [Events]. */ + fun <R> Events<R>.nextDeferred(): Deferred<R> { + lateinit var next: CompletableDeferred<R> + val job = nextOnly().observe { next.complete(it) } + next = CompletableDeferred<R>(parent = job) + return next + } + + /** Returns a [State] that reflects the [StateFlow.value] of this [StateFlow]. */ + fun <A> StateFlow<A>.toState(): State<A> { + val initial = value + return events { dropWhile { it == initial }.collect { emit(it) } }.holdState(initial) + } + + /** Returns an [Events] that emits whenever this [Flow] emits. */ + fun <A> Flow<A>.toEvents(name: String? = null): Events<A> = + events(name) { collect { emit(it) } } + + /** + * Shorthand for: + * ```kotlin + * flow.toEvents().holdState(initialValue) + * ``` + */ + fun <A> Flow<A>.toState(initialValue: A): State<A> = toEvents().holdState(initialValue) + + /** + * Shorthand for: + * ```kotlin + * flow.scan(initialValue, operation).toEvents().holdState(initialValue) + * ``` + */ + fun <A, B> Flow<A>.scanToState(initialValue: B, operation: (B, A) -> B): State<B> = + scan(initialValue, operation).toEvents().holdState(initialValue) + + /** + * Shorthand for: + * ```kotlin + * flow.scan(initialValue) { a, f -> f(a) }.toEvents().holdState(initialValue) + * ``` + */ + fun <A> Flow<(A) -> A>.scanToState(initialValue: A): State<A> = + scanToState(initialValue) { a, f -> f(a) } + + /** + * Invokes [block] whenever this [Events] emits a value. [block] receives an [BuildScope] that + * can be used to make further modifications to the Kairos network, and/or perform side-effects + * via [effect]. + * + * With each invocation of [block], changes from the previous invocation are undone (any + * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are + * cancelled). + */ + fun <A> Events<A>.observeLatestBuild(block: BuildScope.(A) -> Unit = {}): Job = + mapLatestBuild { block(it) }.observe() + + /** + * Invokes [block] whenever this [Events] emits a value, allowing side-effects to be safely + * performed in reaction to the emission. + * + * With each invocation of [block], running effects from the previous invocation are cancelled. + */ + fun <A> Events<A>.observeLatest(block: EffectScope.(A) -> Unit = {}): Job { + var innerJob: Job? = null + return observeBuild { + innerJob?.cancel() + innerJob = effect { block(it) } + } + } + + /** + * Invokes [block] with the value held by this [State], allowing side-effects to be safely + * performed in reaction to the state changing. + * + * With each invocation of [block], running effects from the previous invocation are cancelled. + */ + fun <A> State<A>.observeLatest(block: EffectScope.(A) -> Unit = {}): Job = launchScope { + var innerJob = effect { block(sample()) } + changes.observeBuild { + innerJob.cancel() + innerJob = effect { block(it) } + } + } + + /** + * Applies [block] to the value held by this [State]. [block] receives an [BuildScope] that can + * be used to make further modifications to the Kairos network, and/or perform side-effects via + * [effect]. + * + * [block] can perform modifications to the Kairos network via its [BuildScope] receiver. With + * each invocation of [block], changes from the previous invocation are undone (any registered + * [observers][observe] are unregistered, and any pending [side-effects][effect] are cancelled). + */ + fun <A> State<A>.observeLatestBuild(block: BuildScope.(A) -> Unit = {}): Job = launchScope { + var innerJob: Job = launchScope { block(sample()) } + changes.observeBuild { + innerJob.cancel() + innerJob = launchScope { block(it) } + } + } + + /** Applies the [BuildSpec] within this [BuildScope]. */ + fun <A> BuildSpec<A>.applySpec(): A = this() + + /** + * Applies the [BuildSpec] within this [BuildScope], returning the result as an [DeferredValue]. + */ + fun <A> BuildSpec<A>.applySpecDeferred(): DeferredValue<A> = deferredBuildScope { applySpec() } + + /** + * Invokes [block] on the value held in this [State]. [block] receives an [BuildScope] that can + * be used to make further modifications to the Kairos network, and/or perform side-effects via + * [effect]. + */ + fun <A> State<A>.observeBuild(block: BuildScope.(A) -> Unit = {}): Job = launchScope { + block(sample()) + changes.observeBuild(block) + } + + /** + * Invokes [block] with the current value of this [State], re-invoking whenever it changes, + * allowing side-effects to be safely performed in reaction value changing. + * + * Specifically, [block] is deferred to the end of the transaction, and is only actually + * executed if this [BuildScope] is still active by that time. It can be deactivated due to a + * -Latest combinator, for example. + * + * If the [State] is changing within the *current* transaction (i.e. [changes] is presently + * emitting) then [block] will be invoked for the first time with the new value; otherwise, it + * will be invoked with the [current][sample] value. + */ + fun <A> State<A>.observe(block: EffectScope.(A) -> Unit = {}): Job = + now.map { sample() }.mergeWith(changes) { _, new -> new }.observe { block(it) } +} + +/** + * Returns an [Events] that emits the result of [block] once it completes. [block] is evaluated + * outside of the current Kairos transaction; when it completes, the returned [Events] emits in a + * new transaction. + * + * Shorthand for: + * ``` + * events { emitter: MutableEvents<A> -> + * val a = block() + * emitter.emit(a) + * } + * ``` + */ +@ExperimentalKairosApi +fun <A> BuildScope.asyncEvent(block: suspend () -> A): Events<A> = + events { + // TODO: if block completes synchronously, it would be nice to emit within this + // transaction + emit(block()) + } + .apply { observe() } + +/** + * Performs a side-effect in a safe manner w/r/t the current Kairos transaction. + * + * Specifically, [block] is deferred to the end of the current transaction, and is only actually + * executed if this [BuildScope] is still active by that time. It can be deactivated due to a + * -Latest combinator, for example. + * + * Shorthand for: + * ```kotlin + * now.observe { block() } + * ``` + */ +@ExperimentalKairosApi +fun BuildScope.effect( + context: CoroutineContext = EmptyCoroutineContext, + block: EffectScope.() -> Unit, +): Job = now.observe(context) { block() } + +/** + * Launches [block] in a new coroutine, returning a [Job] bound to the coroutine. + * + * This coroutine is not actually started until the *end* of the current Kairos transaction. This is + * done because the current [BuildScope] might be deactivated within this transaction, perhaps due + * to a -Latest combinator. If this happens, then the coroutine will never actually be started. + * + * Shorthand for: + * ```kotlin + * effect { effectCoroutineScope.launch { block() } } + * ``` + */ +@ExperimentalKairosApi +fun BuildScope.launchEffect(block: suspend CoroutineScope.() -> Unit): Job = asyncEffect(block) + +/** + * Launches [block] in a new coroutine, returning the result as a [Deferred]. + * + * This coroutine is not actually started until the *end* of the current Kairos transaction. This is + * done because the current [BuildScope] might be deactivated within this transaction, perhaps due + * to a -Latest combinator. If this happens, then the coroutine will never actually be started. + * + * Shorthand for: + * ```kotlin + * CompletableDeferred<R>.apply { + * effect { effectCoroutineScope.launch { complete(coroutineScope { block() }) } } + * } + * .await() + * ``` + */ +@ExperimentalKairosApi +fun <R> BuildScope.asyncEffect(block: suspend CoroutineScope.() -> R): Deferred<R> { + val result = CompletableDeferred<R>() + val job = now.observe { effectCoroutineScope.launch { result.complete(coroutineScope(block)) } } + val handle = job.invokeOnCompletion { result.cancel() } + result.invokeOnCompletion { + handle.dispose() + job.cancel() + } + return result +} + +/** Like [BuildScope.asyncScope], but ignores the result of [block]. */ +@ExperimentalKairosApi +fun BuildScope.launchScope(block: BuildSpec<*>): Job = asyncScope(block).second + +/** + * Creates an instance of an [Events] with elements that are emitted from [builder]. + * + * [builder] is run in its own coroutine, allowing for ongoing work that can emit to the provided + * [MutableState]. + * + * By default, [builder] is only running while the returned [Events] is being + * [observed][BuildScope.observe]. If you want it to run at all times, simply add a no-op observer: + * ```kotlin + * events { ... }.apply { observe() } + * ``` + * + * In the event of backpressure, emissions are *coalesced* into batches. When a value is + * [emitted][CoalescingEventProducerScope.emit] from [builder], it is merged into the batch via + * [coalesce]. Once the batch is consumed by the Kairos network in the next transaction, the batch + * is reset back to [initialValue]. + */ +@ExperimentalKairosApi +fun <In, Out> BuildScope.coalescingEvents( + initialValue: Out, + coalesce: (old: Out, new: In) -> Out, + builder: suspend CoalescingEventProducerScope<In>.() -> Unit, +): Events<Out> = coalescingEvents(getInitialValue = { initialValue }, coalesce, builder) + +/** + * Creates an instance of an [Events] with elements that are emitted from [builder]. + * + * [builder] is run in its own coroutine, allowing for ongoing work that can emit to the provided + * [MutableState]. + * + * By default, [builder] is only running while the returned [Events] is being + * [observed][BuildScope.observe]. If you want it to run at all times, simply add a no-op observer: + * ```kotlin + * events { ... }.apply { observe() } + * ``` + * + * In the event of backpressure, emissions are *conflated*; any older emissions are dropped and only + * the most recent emission will be used when the Kairos network is ready. + */ +@ExperimentalKairosApi +fun <T> BuildScope.conflatedEvents( + builder: suspend CoalescingEventProducerScope<T>.() -> Unit +): Events<T> = + coalescingEvents<T, Any?>(initialValue = Any(), coalesce = { _, new -> new }, builder = builder) + .mapCheap { + @Suppress("UNCHECKED_CAST") + it as T + } + +/** Scope for emitting to a [BuildScope.coalescingEvents]. */ +interface CoalescingEventProducerScope<in T> { + /** + * Inserts [value] into the current batch, enqueueing it for emission from this [Events] if not + * already pending. + * + * Backpressure occurs when [emit] is called while the Kairos network is currently in a + * transaction; if called multiple times, then emissions will be coalesced into a single batch + * that is then processed when the network is ready. + */ + fun emit(value: T) +} + +/** Scope for emitting to a [BuildScope.events]. */ +interface EventProducerScope<in T> { + /** + * Emits a [value] to this [Events], suspending the caller until the Kairos transaction + * containing the emission has completed. + */ + suspend fun emit(value: T) +} + +/** + * Suspends forever. Upon cancellation, runs [block]. Useful for unregistering callbacks inside of + * [BuildScope.events] and [BuildScope.coalescingEvents]. + */ +suspend fun awaitClose(block: () -> Unit): Nothing = + try { + awaitCancellation() + } finally { + block() + } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combinators.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combinators.kt index d5576b3b83df..a26d5f8f122e 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combinators.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Combinators.kt @@ -25,42 +25,42 @@ import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.conflate /** - * Returns a [TFlow] that emits the value sampled from the [Transactional] produced by each emission - * of the original [TFlow], within the same transaction of the original emission. + * Returns an [Events] that emits the value sampled from the [Transactional] produced by each + * emission of the original [Events], within the same transaction of the original emission. */ -@ExperimentalFrpApi -fun <A> TFlow<Transactional<A>>.sampleTransactionals(): TFlow<A> = map { it.sample() } +@ExperimentalKairosApi +fun <A> Events<Transactional<A>>.sampleTransactionals(): Events<A> = map { it.sample() } -/** @see FrpTransactionScope.sample */ -@ExperimentalFrpApi -fun <A, B, C> TFlow<A>.sample( - state: TState<B>, - transform: FrpTransactionScope.(A, B) -> C, -): TFlow<C> = map { transform(it, state.sample()) } +/** @see TransactionScope.sample */ +@ExperimentalKairosApi +fun <A, B, C> Events<A>.sample( + state: State<B>, + transform: TransactionScope.(A, B) -> C, +): Events<C> = map { transform(it, state.sample()) } -/** @see FrpTransactionScope.sample */ -@ExperimentalFrpApi -fun <A, B, C> TFlow<A>.sample( - transactional: Transactional<B>, - transform: FrpTransactionScope.(A, B) -> C, -): TFlow<C> = map { transform(it, transactional.sample()) } +/** @see TransactionScope.sample */ +@ExperimentalKairosApi +fun <A, B, C> Events<A>.sample( + sampleable: Transactional<B>, + transform: TransactionScope.(A, B) -> C, +): Events<C> = map { transform(it, sampleable.sample()) } /** - * Like [sample], but if [state] is changing at the time it is sampled ([stateChanges] is emitting), - * then the new value is passed to [transform]. + * Like [sample], but if [state] is changing at the time it is sampled ([changes] is emitting), then + * the new value is passed to [transform]. * * Note that [sample] is both more performant, and safer to use with recursive definitions. You will * generally want to use it rather than this. * * @see sample */ -@ExperimentalFrpApi -fun <A, B, C> TFlow<A>.samplePromptly( - state: TState<B>, - transform: FrpTransactionScope.(A, B) -> C, -): TFlow<C> = +@ExperimentalKairosApi +fun <A, B, C> Events<A>.samplePromptly( + state: State<B>, + transform: TransactionScope.(A, B) -> C, +): Events<C> = sample(state) { a, b -> These.thiz<Pair<A, B>, B>(a to b) } - .mergeWith(state.stateChanges.map { These.that(it) }) { thiz, that -> + .mergeWith(state.changes.map { These.that(it) }) { thiz, that -> These.both((thiz as These.This).thiz, (that as These.That).that) } .mapMaybe { these -> @@ -75,199 +75,204 @@ fun <A, B, C> TFlow<A>.samplePromptly( } /** - * Returns a cold [Flow] that, when collected, emits from this [TFlow]. [network] is needed to - * transactionally connect to / disconnect from the [TFlow] when collection starts/stops. + * Returns a cold [Flow] that, when collected, emits from this [Events]. [network] is needed to + * transactionally connect to / disconnect from the [Events] when collection starts/stops. */ -@ExperimentalFrpApi -fun <A> TFlow<A>.toColdConflatedFlow(network: FrpNetwork): Flow<A> = +@ExperimentalKairosApi +fun <A> Events<A>.toColdConflatedFlow(network: KairosNetwork): Flow<A> = channelFlow { network.activateSpec { observe { trySend(it) } } }.conflate() /** - * Returns a cold [Flow] that, when collected, emits from this [TState]. [network] is needed to - * transactionally connect to / disconnect from the [TState] when collection starts/stops. + * Returns a cold [Flow] that, when collected, emits from this [State]. [network] is needed to + * transactionally connect to / disconnect from the [State] when collection starts/stops. */ -@ExperimentalFrpApi -fun <A> TState<A>.toColdConflatedFlow(network: FrpNetwork): Flow<A> = +@ExperimentalKairosApi +fun <A> State<A>.toColdConflatedFlow(network: KairosNetwork): Flow<A> = channelFlow { network.activateSpec { observe { trySend(it) } } }.conflate() /** - * Returns a cold [Flow] that, when collected, applies this [FrpSpec] in a new transaction in this - * [network], and then emits from the returned [TFlow]. + * Returns a cold [Flow] that, when collected, applies this [BuildSpec] in a new transaction in this + * [network], and then emits from the returned [Events]. * - * When collection is cancelled, so is the [FrpSpec]. This means all ongoing work is cleaned up. + * When collection is cancelled, so is the [BuildSpec]. This means all ongoing work is cleaned up. */ -@ExperimentalFrpApi -@JvmName("flowSpecToColdConflatedFlow") -fun <A> FrpSpec<TFlow<A>>.toColdConflatedFlow(network: FrpNetwork): Flow<A> = +@ExperimentalKairosApi +@JvmName("eventsSpecToColdConflatedFlow") +fun <A> BuildSpec<Events<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> = channelFlow { network.activateSpec { applySpec().observe { trySend(it) } } }.conflate() /** - * Returns a cold [Flow] that, when collected, applies this [FrpSpec] in a new transaction in this - * [network], and then emits from the returned [TState]. + * Returns a cold [Flow] that, when collected, applies this [BuildSpec] in a new transaction in this + * [network], and then emits from the returned [State]. * - * When collection is cancelled, so is the [FrpSpec]. This means all ongoing work is cleaned up. + * When collection is cancelled, so is the [BuildSpec]. This means all ongoing work is cleaned up. */ -@ExperimentalFrpApi +@ExperimentalKairosApi @JvmName("stateSpecToColdConflatedFlow") -fun <A> FrpSpec<TState<A>>.toColdConflatedFlow(network: FrpNetwork): Flow<A> = +fun <A> BuildSpec<State<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> = channelFlow { network.activateSpec { applySpec().observe { trySend(it) } } }.conflate() /** * Returns a cold [Flow] that, when collected, applies this [Transactional] in a new transaction in - * this [network], and then emits from the returned [TFlow]. + * this [network], and then emits from the returned [Events]. */ -@ExperimentalFrpApi +@ExperimentalKairosApi @JvmName("transactionalFlowToColdConflatedFlow") -fun <A> Transactional<TFlow<A>>.toColdConflatedFlow(network: FrpNetwork): Flow<A> = +fun <A> Transactional<Events<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> = channelFlow { network.activateSpec { sample().observe { trySend(it) } } }.conflate() /** * Returns a cold [Flow] that, when collected, applies this [Transactional] in a new transaction in - * this [network], and then emits from the returned [TState]. + * this [network], and then emits from the returned [State]. */ -@ExperimentalFrpApi +@ExperimentalKairosApi @JvmName("transactionalStateToColdConflatedFlow") -fun <A> Transactional<TState<A>>.toColdConflatedFlow(network: FrpNetwork): Flow<A> = +fun <A> Transactional<State<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> = channelFlow { network.activateSpec { sample().observe { trySend(it) } } }.conflate() /** - * Returns a cold [Flow] that, when collected, applies this [FrpStateful] in a new transaction in - * this [network], and then emits from the returned [TFlow]. + * Returns a cold [Flow] that, when collected, applies this [Stateful] in a new transaction in this + * [network], and then emits from the returned [Events]. * - * When collection is cancelled, so is the [FrpStateful]. This means all ongoing work is cleaned up. + * When collection is cancelled, so is the [Stateful]. This means all ongoing work is cleaned up. */ -@ExperimentalFrpApi +@ExperimentalKairosApi @JvmName("statefulFlowToColdConflatedFlow") -fun <A> FrpStateful<TFlow<A>>.toColdConflatedFlow(network: FrpNetwork): Flow<A> = +fun <A> Stateful<Events<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> = channelFlow { network.activateSpec { applyStateful().observe { trySend(it) } } }.conflate() /** * Returns a cold [Flow] that, when collected, applies this [Transactional] in a new transaction in - * this [network], and then emits from the returned [TState]. + * this [network], and then emits from the returned [State]. * - * When collection is cancelled, so is the [FrpStateful]. This means all ongoing work is cleaned up. + * When collection is cancelled, so is the [Stateful]. This means all ongoing work is cleaned up. */ -@ExperimentalFrpApi +@ExperimentalKairosApi @JvmName("statefulStateToColdConflatedFlow") -fun <A> FrpStateful<TState<A>>.toColdConflatedFlow(network: FrpNetwork): Flow<A> = +fun <A> Stateful<State<A>>.toColdConflatedFlow(network: KairosNetwork): Flow<A> = channelFlow { network.activateSpec { applyStateful().observe { trySend(it) } } }.conflate() -/** Return a [TFlow] that emits from the original [TFlow] only when [state] is `true`. */ -@ExperimentalFrpApi -fun <A> TFlow<A>.filter(state: TState<Boolean>): TFlow<A> = filter { state.sample() } +/** Return an [Events] that emits from the original [Events] only when [state] is `true`. */ +@ExperimentalKairosApi +fun <A> Events<A>.filter(state: State<Boolean>): Events<A> = filter { state.sample() } private fun Iterable<Boolean>.allTrue() = all { it } private fun Iterable<Boolean>.anyTrue() = any { it } -/** Returns a [TState] that is `true` only when all of [states] are `true`. */ -@ExperimentalFrpApi -fun allOf(vararg states: TState<Boolean>): TState<Boolean> = combine(*states) { it.allTrue() } +/** Returns a [State] that is `true` only when all of [states] are `true`. */ +@ExperimentalKairosApi +fun allOf(vararg states: State<Boolean>): State<Boolean> = combine(*states) { it.allTrue() } -/** Returns a [TState] that is `true` when any of [states] are `true`. */ -@ExperimentalFrpApi -fun anyOf(vararg states: TState<Boolean>): TState<Boolean> = combine(*states) { it.anyTrue() } +/** Returns a [State] that is `true` when any of [states] are `true`. */ +@ExperimentalKairosApi +fun anyOf(vararg states: State<Boolean>): State<Boolean> = combine(*states) { it.anyTrue() } -/** Returns a [TState] containing the inverse of the Boolean held by the original [TState]. */ -@ExperimentalFrpApi fun not(state: TState<Boolean>): TState<Boolean> = state.mapCheapUnsafe { !it } +/** Returns a [State] containing the inverse of the Boolean held by the original [State]. */ +@ExperimentalKairosApi fun not(state: State<Boolean>): State<Boolean> = state.mapCheapUnsafe { !it } /** - * Represents a modal FRP sub-network. + * Represents a modal Kairos sub-network. * - * When [enabled][enableMode], all network modifications are applied immediately to the FRP network. - * When the returned [TFlow] emits a [FrpBuildMode], that mode is enabled and replaces this mode, - * undoing all modifications in the process (any registered [observers][FrpBuildScope.observe] are - * unregistered, and any pending [side-effects][FrpBuildScope.effect] are cancelled). + * When [enabled][enableMode], all network modifications are applied immediately to the Kairos + * network. When the returned [Events] emits a [BuildMode], that mode is enabled and replaces this + * mode, undoing all modifications in the process (any registered [observers][BuildScope.observe] + * are unregistered, and any pending [side-effects][BuildScope.effect] are cancelled). * - * Use [compiledFrpSpec] to compile and stand-up a mode graph. + * Use [compiledBuildSpec] to compile and stand-up a mode graph. * - * @see FrpStatefulMode + * @see StatefulMode */ -@ExperimentalFrpApi -fun interface FrpBuildMode<out A> { +@ExperimentalKairosApi +fun interface BuildMode<out A> { /** - * Invoked when this mode is enabled. Returns a value and a [TFlow] that signals a switch to a + * Invoked when this mode is enabled. Returns a value and an [Events] that signals a switch to a * new mode. */ - fun FrpBuildScope.enableMode(): Pair<A, TFlow<FrpBuildMode<A>>> + fun BuildScope.enableMode(): Pair<A, Events<BuildMode<A>>> } /** - * Returns an [FrpSpec] that, when [applied][FrpBuildScope.applySpec], stands up a modal-transition - * graph starting with this [FrpBuildMode], automatically switching to new modes as they are - * produced. + * Returns an [BuildSpec] that, when [applied][BuildScope.applySpec], stands up a modal-transition + * graph starting with this [BuildMode], automatically switching to new modes as they are produced. * - * @see FrpBuildMode + * @see BuildMode */ -@ExperimentalFrpApi -val <A> FrpBuildMode<A>.compiledFrpSpec: FrpSpec<TState<A>> - get() = frpSpec { - var modeChangeEvents by TFlowLoop<FrpBuildMode<A>>() - val activeMode: TState<Pair<A, TFlow<FrpBuildMode<A>>>> = +@ExperimentalKairosApi +val <A> BuildMode<A>.compiledBuildSpec: BuildSpec<State<A>> + get() = buildSpec { + var modeChangeEvents by EventsLoop<BuildMode<A>>() + val activeMode: State<Pair<A, Events<BuildMode<A>>>> = modeChangeEvents - .map { it.run { frpSpec { enableMode() } } } - .holdLatestSpec(frpSpec { enableMode() }) + .map { it.run { buildSpec { enableMode() } } } + .holdLatestSpec(buildSpec { enableMode() }) modeChangeEvents = - activeMode.map { statefully { it.second.nextOnly() } }.applyLatestStateful().switch() + activeMode + .map { statefully { it.second.nextOnly() } } + .applyLatestStateful() + .switchEvents() activeMode.map { it.first } } /** - * Represents a modal FRP sub-network. + * Represents a modal Kairos sub-network. * * When [enabled][enableMode], all state accumulation is immediately started. When the returned - * [TFlow] emits a [FrpBuildMode], that mode is enabled and replaces this mode, stopping all state + * [Events] emits a [BuildMode], that mode is enabled and replaces this mode, stopping all state * accumulation in the process. * * Use [compiledStateful] to compile and stand-up a mode graph. * - * @see FrpBuildMode + * @see BuildMode */ -@ExperimentalFrpApi -fun interface FrpStatefulMode<out A> { +@ExperimentalKairosApi +fun interface StatefulMode<out A> { /** - * Invoked when this mode is enabled. Returns a value and a [TFlow] that signals a switch to a + * Invoked when this mode is enabled. Returns a value and an [Events] that signals a switch to a * new mode. */ - fun FrpStateScope.enableMode(): Pair<A, TFlow<FrpStatefulMode<A>>> + fun StateScope.enableMode(): Pair<A, Events<StatefulMode<A>>> } /** - * Returns an [FrpStateful] that, when [applied][FrpStateScope.applyStateful], stands up a - * modal-transition graph starting with this [FrpStatefulMode], automatically switching to new modes - * as they are produced. + * Returns an [Stateful] that, when [applied][StateScope.applyStateful], stands up a + * modal-transition graph starting with this [StatefulMode], automatically switching to new modes as + * they are produced. * - * @see FrpBuildMode + * @see BuildMode */ -@ExperimentalFrpApi -val <A> FrpStatefulMode<A>.compiledStateful: FrpStateful<TState<A>> +@ExperimentalKairosApi +val <A> StatefulMode<A>.compiledStateful: Stateful<State<A>> get() = statefully { - var modeChangeEvents by TFlowLoop<FrpStatefulMode<A>>() - val activeMode: TState<Pair<A, TFlow<FrpStatefulMode<A>>>> = + var modeChangeEvents by EventsLoop<StatefulMode<A>>() + val activeMode: State<Pair<A, Events<StatefulMode<A>>>> = modeChangeEvents .map { it.run { statefully { enableMode() } } } .holdLatestStateful(statefully { enableMode() }) modeChangeEvents = - activeMode.map { statefully { it.second.nextOnly() } }.applyLatestStateful().switch() + activeMode + .map { statefully { it.second.nextOnly() } } + .applyLatestStateful() + .switchEvents() activeMode.map { it.first } } /** - * Runs [spec] in this [FrpBuildScope], and then re-runs it whenever [rebuildSignal] emits. Returns - * a [TState] that holds the result of the currently-active [FrpSpec]. + * Runs [spec] in this [BuildScope], and then re-runs it whenever [rebuildSignal] emits. Returns a + * [State] that holds the result of the currently-active [BuildSpec]. */ -@ExperimentalFrpApi -fun <A> FrpBuildScope.rebuildOn(rebuildSignal: TFlow<*>, spec: FrpSpec<A>): TState<A> = +@ExperimentalKairosApi +fun <A> BuildScope.rebuildOn(rebuildSignal: Events<*>, spec: BuildSpec<A>): State<A> = rebuildSignal.map { spec }.holdLatestSpec(spec) /** - * Like [stateChanges] but also includes the old value of this [TState]. + * Like [changes] but also includes the old value of this [State]. * * Shorthand for: * ``` kotlin * stateChanges.map { WithPrev(previousValue = sample(), newValue = it) } * ``` */ -@ExperimentalFrpApi -val <A> TState<A>.transitions: TFlow<WithPrev<A, A>> - get() = stateChanges.map { WithPrev(previousValue = sample(), newValue = it) } +@ExperimentalKairosApi +val <A> State<A>.transitions: Events<WithPrev<A, A>> + get() = changes.map { WithPrev(previousValue = sample(), newValue = it) } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/EffectScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/EffectScope.kt new file mode 100644 index 000000000000..7e257f2831af --- /dev/null +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/EffectScope.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.kairos + +import kotlinx.coroutines.CoroutineScope + +/** + * Scope for external side-effects triggered by the Kairos network. This still occurs within the + * context of a transaction, so general suspending calls are disallowed to prevent blocking the + * transaction. You can use [effectCoroutineScope] to [launch][kotlinx.coroutines.launch] new + * coroutines to perform long-running asynchronous work. This scope is alive for the duration of the + * containing [BuildScope] that this side-effect scope is running in. + */ +@ExperimentalKairosApi +interface EffectScope : TransactionScope { + /** + * A [CoroutineScope] whose lifecycle lives for as long as this [EffectScope] is alive. This is + * generally until the [Job][kotlinx.coroutines.Job] returned by [BuildScope.effect] is + * cancelled. + */ + @ExperimentalKairosApi val effectCoroutineScope: CoroutineScope + + /** + * A [KairosNetwork] instance that can be used to transactionally query / modify the Kairos + * network. + * + * The lambda passed to [KairosNetwork.transact] on this instance will receive an [BuildScope] + * that is lifetime-bound to this [EffectScope]. Once this [EffectScope] is no longer alive, any + * modifications to the Kairos network performed via this [KairosNetwork] instance will be + * undone (any registered [observers][BuildScope.observe] are unregistered, and any pending + * [side-effects][BuildScope.effect] are cancelled). + */ + @ExperimentalKairosApi val kairosNetwork: KairosNetwork +} diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Events.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Events.kt new file mode 100644 index 000000000000..bd9b45d3be4c --- /dev/null +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Events.kt @@ -0,0 +1,577 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.kairos + +import com.android.systemui.kairos.internal.CompletableLazy +import com.android.systemui.kairos.internal.DemuxImpl +import com.android.systemui.kairos.internal.EventsImpl +import com.android.systemui.kairos.internal.Init +import com.android.systemui.kairos.internal.InitScope +import com.android.systemui.kairos.internal.InputNode +import com.android.systemui.kairos.internal.Network +import com.android.systemui.kairos.internal.NoScope +import com.android.systemui.kairos.internal.activated +import com.android.systemui.kairos.internal.cached +import com.android.systemui.kairos.internal.constInit +import com.android.systemui.kairos.internal.demuxMap +import com.android.systemui.kairos.internal.filterImpl +import com.android.systemui.kairos.internal.filterJustImpl +import com.android.systemui.kairos.internal.init +import com.android.systemui.kairos.internal.map +import com.android.systemui.kairos.internal.mapImpl +import com.android.systemui.kairos.internal.mergeNodes +import com.android.systemui.kairos.internal.mergeNodesLeft +import com.android.systemui.kairos.internal.neverImpl +import com.android.systemui.kairos.internal.switchDeferredImplSingle +import com.android.systemui.kairos.internal.switchPromptImplSingle +import com.android.systemui.kairos.internal.util.hashString +import com.android.systemui.kairos.util.Either +import com.android.systemui.kairos.util.Left +import com.android.systemui.kairos.util.Maybe +import com.android.systemui.kairos.util.Right +import com.android.systemui.kairos.util.just +import com.android.systemui.kairos.util.map +import com.android.systemui.kairos.util.toMaybe +import java.util.concurrent.atomic.AtomicReference +import kotlin.reflect.KProperty +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope + +/** A series of values of type [A] available at discrete points in time. */ +@ExperimentalKairosApi +sealed class Events<out A> { + companion object { + /** An [Events] with no values. */ + val empty: Events<Nothing> = EmptyFlow + } +} + +/** AN [Events] with no values. */ +@ExperimentalKairosApi val emptyEvents: Events<Nothing> = Events.empty + +/** + * A forward-reference to an [Events]. Useful for recursive definitions. + * + * This reference can be used like a standard [Events], but will throw an error if its [loopback] is + * unset before the end of the first transaction which accesses it. + */ +@ExperimentalKairosApi +class EventsLoop<A> : Events<A>() { + private val deferred = CompletableLazy<Events<A>>() + + internal val init: Init<EventsImpl<A>> = + init(name = null) { deferred.value.init.connect(evalScope = this) } + + /** The [Events] this reference is referring to. */ + var loopback: Events<A>? = null + set(value) { + value?.let { + check(!deferred.isInitialized()) { "TFlowLoop.loopback has already been set." } + deferred.setValue(value) + field = value + } + } + + operator fun getValue(thisRef: Any?, property: KProperty<*>): Events<A> = this + + operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Events<A>) { + loopback = value + } + + override fun toString(): String = "${this::class.simpleName}@$hashString" +} + +/** + * Returns an [Events] that acts as a deferred-reference to the [Events] produced by this [Lazy]. + * + * When the returned [Events] is accessed by the Kairos network, the [Lazy]'s [value][Lazy.value] + * will be queried and used. + * + * Useful for recursive definitions. + */ +@ExperimentalKairosApi fun <A> Lazy<Events<A>>.defer(): Events<A> = deferInline { value } + +/** + * Returns an [Events] that acts as a deferred-reference to the [Events] produced by this + * [DeferredValue]. + * + * When the returned [Events] is accessed by the Kairos network, the [DeferredValue] will be queried + * and used. + * + * Useful for recursive definitions. + */ +@ExperimentalKairosApi +fun <A> DeferredValue<Events<A>>.defer(): Events<A> = deferInline { unwrapped.value } + +/** + * Returns an [Events] that acts as a deferred-reference to the [Events] produced by [block]. + * + * When the returned [Events] is accessed by the Kairos network, [block] will be invoked and the + * returned [Events] will be used. + * + * Useful for recursive definitions. + */ +@ExperimentalKairosApi +fun <A> deferredEvents(block: KairosScope.() -> Events<A>): Events<A> = deferInline { + NoScope.block() +} + +/** Returns an [Events] that emits the new value of this [State] when it changes. */ +@ExperimentalKairosApi +val <A> State<A>.changes: Events<A> + get() = EventsInit(init(name = null) { init.connect(evalScope = this).changes }) + +/** + * Returns an [Events] that contains only the [just] results of applying [transform] to each value + * of the original [Events]. + * + * @see mapNotNull + */ +@ExperimentalKairosApi +fun <A, B> Events<A>.mapMaybe(transform: TransactionScope.(A) -> Maybe<B>): Events<B> = + map(transform).filterJust() + +/** + * Returns an [Events] that contains only the non-null results of applying [transform] to each value + * of the original [Events]. + * + * @see mapMaybe + */ +@ExperimentalKairosApi +fun <A, B> Events<A>.mapNotNull(transform: TransactionScope.(A) -> B?): Events<B> = mapMaybe { + transform(it).toMaybe() +} + +/** Returns an [Events] containing only values of the original [Events] that are not null. */ +@ExperimentalKairosApi +fun <A> Events<A?>.filterNotNull(): Events<A> = mapCheap { it.toMaybe() }.filterJust() + +/** Shorthand for `mapNotNull { it as? A }`. */ +@ExperimentalKairosApi +inline fun <reified A> Events<*>.filterIsInstance(): Events<A> = + mapCheap { it as? A }.filterNotNull() + +/** Shorthand for `mapMaybe { it }`. */ +@ExperimentalKairosApi +fun <A> Events<Maybe<A>>.filterJust(): Events<A> = + EventsInit(constInit(name = null, filterJustImpl { init.connect(evalScope = this) })) + +/** + * Returns an [Events] containing the results of applying [transform] to each value of the original + * [Events]. + */ +@ExperimentalKairosApi +fun <A, B> Events<A>.map(transform: TransactionScope.(A) -> B): Events<B> { + val mapped: EventsImpl<B> = mapImpl({ init.connect(evalScope = this) }) { a, _ -> transform(a) } + return EventsInit(constInit(name = null, mapped.cached())) +} + +/** + * Like [map], but the emission is not cached during the transaction. Use only if [transform] is + * fast and pure. + * + * @see map + */ +@ExperimentalKairosApi +fun <A, B> Events<A>.mapCheap(transform: TransactionScope.(A) -> B): Events<B> = + EventsInit( + constInit(name = null, mapImpl({ init.connect(evalScope = this) }) { a, _ -> transform(a) }) + ) + +/** + * Returns an [Events] that invokes [action] before each value of the original [Events] is emitted. + * Useful for logging and debugging. + * + * ``` + * pulse.onEach { foo(it) } == pulse.map { foo(it); it } + * ``` + * + * Note that the side effects performed in [onEach] are only performed while the resulting [Events] + * is connected to an output of the Kairos network. If your goal is to reliably perform side effects + * in response to an [Events], use the output combinators available in [BuildScope], such as + * [BuildScope.toSharedFlow] or [BuildScope.observe]. + */ +@ExperimentalKairosApi +fun <A> Events<A>.onEach(action: TransactionScope.(A) -> Unit): Events<A> = map { + action(it) + it +} + +/** + * Returns an [Events] containing only values of the original [Events] that satisfy the given + * [predicate]. + */ +@ExperimentalKairosApi +fun <A> Events<A>.filter(predicate: TransactionScope.(A) -> Boolean): Events<A> { + val pulse = filterImpl({ init.connect(evalScope = this) }) { predicate(it) } + return EventsInit(constInit(name = null, pulse)) +} + +/** + * Splits an [Events] of pairs into a pair of [Events], where each returned [Events] emits half of + * the original. + * + * Shorthand for: + * ```kotlin + * val lefts = map { it.first } + * val rights = map { it.second } + * return Pair(lefts, rights) + * ``` + */ +@ExperimentalKairosApi +fun <A, B> Events<Pair<A, B>>.unzip(): Pair<Events<A>, Events<B>> { + val lefts = map { it.first } + val rights = map { it.second } + return lefts to rights +} + +/** + * Merges the given [Events] into a single [Events] that emits events from both. + * + * Because [Events] can only emit one value per transaction, the provided [transformCoincidence] + * function is used to combine coincident emissions to produce the result value to be emitted by the + * merged [Events]. + */ +@ExperimentalKairosApi +fun <A> Events<A>.mergeWith( + other: Events<A>, + name: String? = null, + transformCoincidence: TransactionScope.(A, A) -> A = { a, _ -> a }, +): Events<A> { + val node = + mergeNodes( + name = name, + getPulse = { init.connect(evalScope = this) }, + getOther = { other.init.connect(evalScope = this) }, + ) { a, b -> + transformCoincidence(a, b) + } + return EventsInit(constInit(name = null, node)) +} + +/** + * Merges the given [Events] into a single [Events] that emits events from all. All coincident + * emissions are collected into the emitted [List], preserving the input ordering. + * + * @see mergeWith + * @see mergeLeft + */ +@ExperimentalKairosApi +fun <A> merge(vararg events: Events<A>): Events<List<A>> = events.asIterable().merge() + +/** + * Merges the given [Events] into a single [Events] that emits events from all. In the case of + * coincident emissions, the emission from the left-most [Events] is emitted. + * + * @see merge + */ +@ExperimentalKairosApi +fun <A> mergeLeft(vararg events: Events<A>): Events<A> = events.asIterable().mergeLeft() + +/** + * Merges the given [Events] into a single [Events] that emits events from all. + * + * Because [Events] can only emit one value per transaction, the provided [transformCoincidence] + * function is used to combine coincident emissions to produce the result value to be emitted by the + * merged [Events]. + */ +// TODO: can be optimized to avoid creating the intermediate list +fun <A> merge(vararg events: Events<A>, transformCoincidence: (A, A) -> A): Events<A> = + merge(*events).map { l -> l.reduce(transformCoincidence) } + +/** + * Merges the given [Events] into a single [Events] that emits events from all. All coincident + * emissions are collected into the emitted [List], preserving the input ordering. + * + * @see mergeWith + * @see mergeLeft + */ +@ExperimentalKairosApi +fun <A> Iterable<Events<A>>.merge(): Events<List<A>> = + EventsInit(constInit(name = null, mergeNodes { map { it.init.connect(evalScope = this) } })) + +/** + * Merges the given [Events] into a single [Events] that emits events from all. In the case of + * coincident emissions, the emission from the left-most [Events] is emitted. + * + * @see merge + */ +@ExperimentalKairosApi +fun <A> Iterable<Events<A>>.mergeLeft(): Events<A> = + EventsInit(constInit(name = null, mergeNodesLeft { map { it.init.connect(evalScope = this) } })) + +/** + * Creates a new [Events] that emits events from all given [Events]. All simultaneous emissions are + * collected into the emitted [List], preserving the input ordering. + * + * @see mergeWith + */ +@ExperimentalKairosApi fun <A> Sequence<Events<A>>.merge(): Events<List<A>> = asIterable().merge() + +/** + * Creates a new [Events] that emits events from all given [Events]. All simultaneous emissions are + * collected into the emitted [Map], and are given the same key of the associated [Events] in the + * input [Map]. + * + * @see mergeWith + */ +@ExperimentalKairosApi +fun <K, A> Map<K, Events<A>>.merge(): Events<Map<K, A>> = + asSequence() + .map { (k, events) -> events.map { a -> k to a } } + .toList() + .merge() + .map { it.toMap() } + +/** + * Returns a [GroupedEvents] that can be used to efficiently split a single [Events] into multiple + * downstream [Events]. + * + * The input [Events] emits [Map] instances that specify which downstream [Events] the associated + * value will be emitted from. These downstream [Events] can be obtained via + * [GroupedEvents.eventsForKey]. + * + * An example: + * ``` + * val fooEvents: Events<Map<String, Foo>> = ... + * val fooById: GroupedEvents<String, Foo> = fooEvents.groupByKey() + * val fooBar: Events<Foo> = fooById["bar"] + * ``` + * + * This is semantically equivalent to `val fooBar = fooEvents.mapNotNull { map -> map["bar"] }` but + * is significantly more efficient; specifically, using [mapNotNull] in this way incurs a `O(n)` + * performance hit, where `n` is the number of different [mapNotNull] operations used to filter on a + * specific key's presence in the emitted [Map]. [groupByKey] internally uses a [HashMap] to lookup + * the appropriate downstream [Events], and so operates in `O(1)`. + * + * Note that the returned [GroupedEvents] should be cached and re-used to gain the performance + * benefit. + * + * @see selector + */ +@ExperimentalKairosApi +fun <K, A> Events<Map<K, A>>.groupByKey(numKeys: Int? = null): GroupedEvents<K, A> = + GroupedEvents(demuxMap({ init.connect(this) }, numKeys)) + +/** + * Shorthand for `map { mapOf(extractKey(it) to it) }.groupByKey()` + * + * @see groupByKey + */ +@ExperimentalKairosApi +fun <K, A> Events<A>.groupBy( + numKeys: Int? = null, + extractKey: TransactionScope.(A) -> K, +): GroupedEvents<K, A> = map { mapOf(extractKey(it) to it) }.groupByKey(numKeys) + +/** + * Returns two new [Events] that contain elements from this [Events] that satisfy or don't satisfy + * [predicate]. + * + * Using this is equivalent to `upstream.filter(predicate) to upstream.filter { !predicate(it) }` + * but is more efficient; specifically, [partition] will only invoke [predicate] once per element. + */ +@ExperimentalKairosApi +fun <A> Events<A>.partition( + predicate: TransactionScope.(A) -> Boolean +): Pair<Events<A>, Events<A>> { + val grouped: GroupedEvents<Boolean, A> = groupBy(numKeys = 2, extractKey = predicate) + return Pair(grouped.eventsForKey(true), grouped.eventsForKey(false)) +} + +/** + * Returns two new [Events] that contain elements from this [Events]; [Pair.first] will contain + * [Left] values, and [Pair.second] will contain [Right] values. + * + * Using this is equivalent to using [filterIsInstance] in conjunction with [map] twice, once for + * [Left]s and once for [Right]s, but is slightly more efficient; specifically, the + * [filterIsInstance] check is only performed once per element. + */ +@ExperimentalKairosApi +fun <A, B> Events<Either<A, B>>.partitionEither(): Pair<Events<A>, Events<B>> { + val (left, right) = partition { it is Left } + return Pair(left.mapCheap { (it as Left).value }, right.mapCheap { (it as Right).value }) +} + +/** + * A mapping from keys of type [K] to [Events] emitting values of type [A]. + * + * @see groupByKey + */ +@ExperimentalKairosApi +class GroupedEvents<in K, out A> internal constructor(internal val impl: DemuxImpl<K, A>) { + /** + * Returns an [Events] that emits values of type [A] that correspond to the given [key]. + * + * @see groupByKey + */ + fun eventsForKey(key: K): Events<A> = EventsInit(constInit(name = null, impl.eventsForKey(key))) + + /** + * Returns an [Events] that emits values of type [A] that correspond to the given [key]. + * + * @see groupByKey + */ + operator fun get(key: K): Events<A> = eventsForKey(key) +} + +/** + * Returns an [Events] that switches to the [Events] contained within this [State] whenever it + * changes. + * + * This switch does take effect until the *next* transaction after [State] changes. For a switch + * that takes effect immediately, see [switchEventsPromptly]. + */ +@ExperimentalKairosApi +fun <A> State<Events<A>>.switchEvents(name: String? = null): Events<A> { + val patches = + mapImpl({ init.connect(this).changes }) { newFlow, _ -> newFlow.init.connect(this) } + return EventsInit( + constInit( + name = null, + switchDeferredImplSingle( + name = name, + getStorage = { + init.connect(this).getCurrentWithEpoch(this).first.init.connect(this) + }, + getPatches = { patches }, + ), + ) + ) +} + +/** + * Returns an [Events] that switches to the [Events] contained within this [State] whenever it + * changes. + * + * This switch takes effect immediately within the same transaction that [State] changes. In + * general, you should prefer [switchEvents] over this method. It is both safer and more performant. + */ +// TODO: parameter to handle coincidental emission from both old and new +@ExperimentalKairosApi +fun <A> State<Events<A>>.switchEventsPromptly(): Events<A> { + val patches = + mapImpl({ init.connect(this).changes }) { newFlow, _ -> newFlow.init.connect(this) } + return EventsInit( + constInit( + name = null, + switchPromptImplSingle( + getStorage = { + init.connect(this).getCurrentWithEpoch(this).first.init.connect(this) + }, + getPatches = { patches }, + ), + ) + ) +} + +/** + * A mutable [Events] that provides the ability to [emit] values to the network, handling + * backpressure by coalescing all emissions into batches. + * + * @see KairosNetwork.coalescingMutableEvents + */ +@ExperimentalKairosApi +class CoalescingMutableEvents<in In, Out> +internal constructor( + internal val name: String?, + internal val coalesce: (old: Lazy<Out>, new: In) -> Out, + internal val network: Network, + private val getInitialValue: () -> Out, + internal val impl: InputNode<Out> = InputNode(), +) : Events<Out>() { + internal val storage = AtomicReference(false to lazy { getInitialValue() }) + + override fun toString(): String = "${this::class.simpleName}@$hashString" + + /** + * Inserts [value] into the current batch, enqueueing it for emission from this [Events] if not + * already pending. + * + * Backpressure occurs when [emit] is called while the Kairos network is currently in a + * transaction; if called multiple times, then emissions will be coalesced into a single batch + * that is then processed when the network is ready. + */ + fun emit(value: In) { + val (scheduled, _) = + storage.getAndUpdate { (_, batch) -> true to CompletableLazy(coalesce(batch, value)) } + if (!scheduled) { + @Suppress("DeferredResultUnused") + network.transaction( + "CoalescingMutableEvents${name?.let { "($name)" }.orEmpty()}.emit" + ) { + val (_, batch) = storage.getAndSet(false to lazy { getInitialValue() }) + impl.visit(this, batch.value) + } + } + } +} + +/** + * A mutable [Events] that provides the ability to [emit] values to the network, handling + * backpressure by suspending the emitter. + * + * @see KairosNetwork.coalescingMutableEvents + */ +@ExperimentalKairosApi +class MutableEvents<T> +internal constructor(internal val network: Network, internal val impl: InputNode<T> = InputNode()) : + Events<T>() { + internal val name: String? = null + + private val storage = AtomicReference<Job?>(null) + + override fun toString(): String = "${this::class.simpleName}@$hashString" + + /** + * Emits a [value] to this [Events], suspending the caller until the Kairos transaction + * containing the emission has completed. + */ + suspend fun emit(value: T) { + coroutineScope { + var jobOrNull: Job? = null + val newEmit = + async(start = CoroutineStart.LAZY) { + jobOrNull?.join() + network.transaction("MutableEvents.emit") { impl.visit(this, value) }.await() + } + jobOrNull = storage.getAndSet(newEmit) + newEmit.await() + } + } +} + +private data object EmptyFlow : Events<Nothing>() + +internal class EventsInit<out A>(val init: Init<EventsImpl<A>>) : Events<A>() { + override fun toString(): String = "${this::class.simpleName}@$hashString" +} + +internal val <A> Events<A>.init: Init<EventsImpl<A>> + get() = + when (this) { + is EmptyFlow -> constInit("EmptyFlow", neverImpl) + is EventsInit -> init + is EventsLoop -> init + is CoalescingMutableEvents<*, A> -> constInit(name, impl.activated()) + is MutableEvents -> constInit(name, impl.activated()) + } + +private inline fun <A> deferInline(crossinline block: InitScope.() -> Events<A>): Events<A> = + EventsInit(init(name = null) { block().init.connect(evalScope = this) }) diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpBuildScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpBuildScope.kt deleted file mode 100644 index 31778dc32697..000000000000 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpBuildScope.kt +++ /dev/null @@ -1,887 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -@file:OptIn(ExperimentalCoroutinesApi::class) - -package com.android.systemui.kairos - -import com.android.systemui.kairos.util.Maybe -import com.android.systemui.kairos.util.just -import com.android.systemui.kairos.util.map -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext -import kotlin.coroutines.RestrictsSuspension -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.Job -import kotlinx.coroutines.awaitCancellation -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.FlowCollector -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.dropWhile -import kotlinx.coroutines.flow.scan -import kotlinx.coroutines.launch - -/** A function that modifies the FrpNetwork. */ -typealias FrpSpec<A> = FrpBuildScope.() -> A - -/** - * Constructs an [FrpSpec]. The passed [block] will be invoked with an [FrpBuildScope] that can be - * used to perform network-building operations, including adding new inputs and outputs to the - * network, as well as all operations available in [FrpTransactionScope]. - */ -@ExperimentalFrpApi -@Suppress("NOTHING_TO_INLINE") -inline fun <A> frpSpec(noinline block: FrpBuildScope.() -> A): FrpSpec<A> = block - -/** Applies the [FrpSpec] within this [FrpBuildScope]. */ -@ExperimentalFrpApi -inline operator fun <A> FrpBuildScope.invoke(block: FrpBuildScope.() -> A) = run(block) - -/** Operations that add inputs and outputs to an FRP network. */ -@ExperimentalFrpApi -@RestrictsSuspension -interface FrpBuildScope : FrpStateScope { - - /** TODO: Javadoc */ - val frpNetwork: FrpNetwork - - /** TODO: Javadoc */ - @ExperimentalFrpApi - fun <R> deferredBuildScope(block: FrpBuildScope.() -> R): FrpDeferredValue<R> - - /** TODO: Javadoc */ - @ExperimentalFrpApi fun deferredBuildScopeAction(block: FrpBuildScope.() -> Unit) - - /** - * Returns a [TFlow] containing the results of applying [transform] to each value of the - * original [TFlow]. - * - * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver. - * Unlike [mapLatestBuild], these modifications are not undone with each subsequent emission of - * the original [TFlow]. - * - * **NOTE:** This API does not [observe] the original [TFlow], meaning that unless the returned - * (or a downstream) [TFlow] is observed separately, [transform] will not be invoked, and no - * internal side-effects will occur. - */ - @ExperimentalFrpApi fun <A, B> TFlow<A>.mapBuild(transform: FrpBuildScope.(A) -> B): TFlow<B> - - /** - * Invokes [block] whenever this [TFlow] emits a value, allowing side-effects to be safely - * performed in reaction to the emission. - * - * Specifically, [block] is deferred to the end of the transaction, and is only actually - * executed if this [FrpBuildScope] is still active by that time. It can be deactivated due to a - * -Latest combinator, for example. - * - * Shorthand for: - * ```kotlin - * tFlow.observe { effect { ... } } - * ``` - */ - @ExperimentalFrpApi - fun <A> TFlow<A>.observe( - coroutineContext: CoroutineContext = EmptyCoroutineContext, - block: FrpEffectScope.(A) -> Unit = {}, - ): Job - - /** - * Returns a [TFlow] containing the results of applying each [FrpSpec] emitted from the original - * [TFlow], and a [FrpDeferredValue] containing the result of applying [initialSpecs] - * immediately. - * - * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] with the same - * key are undone (any registered [observers][observe] are unregistered, and any pending - * [side-effects][effect] are cancelled). - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpSpec] will be undone with no replacement. - */ - @ExperimentalFrpApi - fun <K, A, B> TFlow<Map<K, Maybe<FrpSpec<A>>>>.applyLatestSpecForKey( - initialSpecs: FrpDeferredValue<Map<K, FrpSpec<B>>>, - numKeys: Int? = null, - ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>> - - /** - * Creates an instance of a [TFlow] with elements that are from [builder]. - * - * [builder] is run in its own coroutine, allowing for ongoing work that can emit to the - * provided [MutableTFlow]. - * - * By default, [builder] is only running while the returned [TFlow] is being - * [observed][observe]. If you want it to run at all times, simply add a no-op observer: - * ```kotlin - * tFlow { ... }.apply { observe() } - * ``` - */ - @ExperimentalFrpApi - fun <T> tFlow(name: String? = null, builder: suspend FrpProducerScope<T>.() -> Unit): TFlow<T> - - /** - * Creates an instance of a [TFlow] with elements that are emitted from [builder]. - * - * [builder] is run in its own coroutine, allowing for ongoing work that can emit to the - * provided [MutableTFlow]. - * - * By default, [builder] is only running while the returned [TFlow] is being - * [observed][observe]. If you want it to run at all times, simply add a no-op observer: - * ```kotlin - * tFlow { ... }.apply { observe() } - * ``` - * - * In the event of backpressure, emissions are *coalesced* into batches. When a value is - * [emitted][FrpCoalescingProducerScope.emit] from [builder], it is merged into the batch via - * [coalesce]. Once the batch is consumed by the frp network in the next transaction, the batch - * is reset back to [getInitialValue]. - */ - @ExperimentalFrpApi - fun <In, Out> coalescingTFlow( - getInitialValue: () -> Out, - coalesce: (old: Out, new: In) -> Out, - builder: suspend FrpCoalescingProducerScope<In>.() -> Unit, - ): TFlow<Out> - - /** - * Creates a new [FrpBuildScope] that is a child of this one. - * - * This new scope can be manually cancelled via the returned [Job], or will be cancelled - * automatically when its parent is cancelled. Cancellation will unregister all - * [observers][observe] and cancel all scheduled [effects][effect]. - * - * The return value from [block] can be accessed via the returned [FrpDeferredValue]. - */ - @ExperimentalFrpApi fun <A> asyncScope(block: FrpSpec<A>): Pair<FrpDeferredValue<A>, Job> - - // TODO: once we have context params, these can all become extensions: - - /** - * Returns a [TFlow] containing the results of applying the given [transform] function to each - * value of the original [TFlow]. - * - * Unlike [TFlow.map], [transform] can perform arbitrary asynchronous code. This code is run - * outside of the current FRP transaction; when [transform] returns, the returned value is - * emitted from the result [TFlow] in a new transaction. - * - * Shorthand for: - * ```kotlin - * tflow.mapLatestBuild { a -> asyncTFlow { transform(a) } }.flatten() - * ``` - */ - @ExperimentalFrpApi - fun <A, B> TFlow<A>.mapAsyncLatest(transform: suspend (A) -> B): TFlow<B> = - mapLatestBuild { a -> asyncTFlow { transform(a) } }.flatten() - - /** - * Invokes [block] whenever this [TFlow] emits a value. [block] receives an [FrpBuildScope] that - * can be used to make further modifications to the FRP network, and/or perform side-effects via - * [effect]. - * - * @see observe - */ - @ExperimentalFrpApi - fun <A> TFlow<A>.observeBuild(block: FrpBuildScope.(A) -> Unit = {}): Job = - mapBuild(block).observe() - - /** - * Returns a [StateFlow] whose [value][StateFlow.value] tracks the current - * [value of this TState][TState.sample], and will emit at the same rate as - * [TState.stateChanges]. - * - * Note that the [value][StateFlow.value] is not available until the *end* of the current - * transaction. If you need the current value before this time, then use [TState.sample]. - */ - @ExperimentalFrpApi - fun <A> TState<A>.toStateFlow(): StateFlow<A> { - val uninitialized = Any() - var initialValue: Any? = uninitialized - val innerStateFlow = MutableStateFlow<Any?>(uninitialized) - deferredBuildScope { - initialValue = sample() - stateChanges.observe { - innerStateFlow.value = it - initialValue = null - } - } - - @Suppress("UNCHECKED_CAST") - fun getValue(innerValue: Any?): A = - when { - innerValue !== uninitialized -> innerValue as A - initialValue !== uninitialized -> initialValue as A - else -> - error( - "Attempted to access StateFlow.value before FRP transaction has completed." - ) - } - - return object : StateFlow<A> { - override val replayCache: List<A> - get() = innerStateFlow.replayCache.map(::getValue) - - override val value: A - get() = getValue(innerStateFlow.value) - - override suspend fun collect(collector: FlowCollector<A>): Nothing { - innerStateFlow.collect { collector.emit(getValue(it)) } - } - } - } - - /** - * Returns a [SharedFlow] configured with a replay cache of size [replay] that emits the current - * [value][TState.sample] of this [TState] followed by all [stateChanges]. - */ - @ExperimentalFrpApi - fun <A> TState<A>.toSharedFlow(replay: Int = 0): SharedFlow<A> { - val result = MutableSharedFlow<A>(replay, extraBufferCapacity = 1) - deferredBuildScope { - result.tryEmit(sample()) - stateChanges.observe { a -> result.tryEmit(a) } - } - return result - } - - /** - * Returns a [SharedFlow] configured with a replay cache of size [replay] that emits values - * whenever this [TFlow] emits. - */ - @ExperimentalFrpApi - fun <A> TFlow<A>.toSharedFlow(replay: Int = 0): SharedFlow<A> { - val result = MutableSharedFlow<A>(replay, extraBufferCapacity = 1) - observe { a -> result.tryEmit(a) } - return result - } - - /** - * Returns a [TState] that holds onto the value returned by applying the most recently emitted - * [FrpSpec] from the original [TFlow], or the value returned by applying [initialSpec] if - * nothing has been emitted since it was constructed. - * - * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] are undone (any - * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are - * cancelled). - */ - @ExperimentalFrpApi - fun <A> TFlow<FrpSpec<A>>.holdLatestSpec(initialSpec: FrpSpec<A>): TState<A> { - val (changes: TFlow<A>, initApplied: FrpDeferredValue<A>) = applyLatestSpec(initialSpec) - return changes.holdDeferred(initApplied) - } - - /** - * Returns a [TState] containing the value returned by applying the [FrpSpec] held by the - * original [TState]. - * - * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] are undone (any - * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are - * cancelled). - */ - @ExperimentalFrpApi - fun <A> TState<FrpSpec<A>>.applyLatestSpec(): TState<A> { - val (appliedChanges: TFlow<A>, init: FrpDeferredValue<A>) = - stateChanges.applyLatestSpec(frpSpec { sample().applySpec() }) - return appliedChanges.holdDeferred(init) - } - - /** - * Returns a [TFlow] containing the results of applying each [FrpSpec] emitted from the original - * [TFlow]. - * - * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] are undone (any - * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are - * cancelled). - */ - @ExperimentalFrpApi - fun <A> TFlow<FrpSpec<A>>.applyLatestSpec(): TFlow<A> = applyLatestSpec(frpSpec {}).first - - /** - * Returns a [TFlow] that switches to a new [TFlow] produced by [transform] every time the - * original [TFlow] emits a value. - * - * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver. - * When the original [TFlow] emits a new value, those changes are undone (any registered - * [observers][observe] are unregistered, and any pending [effects][effect] are cancelled). - */ - @ExperimentalFrpApi - fun <A, B> TFlow<A>.flatMapLatestBuild(transform: FrpBuildScope.(A) -> TFlow<B>): TFlow<B> = - mapCheap { frpSpec { transform(it) } }.applyLatestSpec().flatten() - - /** - * Returns a [TState] by applying [transform] to the value held by the original [TState]. - * - * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver. - * When the value held by the original [TState] changes, those changes are undone (any - * registered [observers][observe] are unregistered, and any pending [effects][effect] are - * cancelled). - */ - @ExperimentalFrpApi - fun <A, B> TState<A>.flatMapLatestBuild(transform: FrpBuildScope.(A) -> TState<B>): TState<B> = - mapLatestBuild { transform(it) }.flatten() - - /** - * Returns a [TState] that transforms the value held inside this [TState] by applying it to the - * [transform]. - * - * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver. - * When the value held by the original [TState] changes, those changes are undone (any - * registered [observers][observe] are unregistered, and any pending [effects][effect] are - * cancelled). - */ - @ExperimentalFrpApi - fun <A, B> TState<A>.mapLatestBuild(transform: FrpBuildScope.(A) -> B): TState<B> = - mapCheapUnsafe { frpSpec { transform(it) } }.applyLatestSpec() - - /** - * Returns a [TFlow] containing the results of applying each [FrpSpec] emitted from the original - * [TFlow], and a [FrpDeferredValue] containing the result of applying [initialSpec] - * immediately. - * - * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] are undone (any - * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are - * cancelled). - */ - @ExperimentalFrpApi - fun <A : Any?, B> TFlow<FrpSpec<B>>.applyLatestSpec( - initialSpec: FrpSpec<A> - ): Pair<TFlow<B>, FrpDeferredValue<A>> { - val (flow, result) = - mapCheap { spec -> mapOf(Unit to just(spec)) } - .applyLatestSpecForKey(initialSpecs = mapOf(Unit to initialSpec), numKeys = 1) - val outFlow: TFlow<B> = - flow.mapMaybe { - checkNotNull(it[Unit]) { "applyLatest: expected result, but none present in: $it" } - } - val outInit: FrpDeferredValue<A> = deferredBuildScope { - val initResult: Map<Unit, A> = result.get() - check(Unit in initResult) { - "applyLatest: expected initial result, but none present in: $initResult" - } - @Suppress("UNCHECKED_CAST") - initResult.getOrDefault(Unit) { null } as A - } - return Pair(outFlow, outInit) - } - - /** - * Returns a [TFlow] containing the results of applying [transform] to each value of the - * original [TFlow]. - * - * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver. - * With each invocation of [transform], changes from the previous invocation are undone (any - * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are - * cancelled). - */ - @ExperimentalFrpApi - fun <A, B> TFlow<A>.mapLatestBuild(transform: FrpBuildScope.(A) -> B): TFlow<B> = - mapCheap { frpSpec { transform(it) } }.applyLatestSpec() - - /** - * Returns a [TFlow] containing the results of applying [transform] to each value of the - * original [TFlow], and a [FrpDeferredValue] containing the result of applying [transform] to - * [initialValue] immediately. - * - * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver. - * With each invocation of [transform], changes from the previous invocation are undone (any - * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are - * cancelled). - */ - @ExperimentalFrpApi - fun <A, B> TFlow<A>.mapLatestBuild( - initialValue: A, - transform: FrpBuildScope.(A) -> B, - ): Pair<TFlow<B>, FrpDeferredValue<B>> = - mapLatestBuildDeferred(deferredOf(initialValue), transform) - - /** - * Returns a [TFlow] containing the results of applying [transform] to each value of the - * original [TFlow], and a [FrpDeferredValue] containing the result of applying [transform] to - * [initialValue] immediately. - * - * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver. - * With each invocation of [transform], changes from the previous invocation are undone (any - * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are - * cancelled). - */ - @ExperimentalFrpApi - fun <A, B> TFlow<A>.mapLatestBuildDeferred( - initialValue: FrpDeferredValue<A>, - transform: FrpBuildScope.(A) -> B, - ): Pair<TFlow<B>, FrpDeferredValue<B>> = - mapCheap { frpSpec { transform(it) } } - .applyLatestSpec(initialSpec = frpSpec { transform(initialValue.get()) }) - - /** - * Returns a [TFlow] containing the results of applying each [FrpSpec] emitted from the original - * [TFlow], and a [FrpDeferredValue] containing the result of applying [initialSpecs] - * immediately. - * - * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] with the same - * key are undone (any registered [observers][observe] are unregistered, and any pending - * [side-effects][effect] are cancelled). - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpSpec] will be undone with no replacement. - */ - @ExperimentalFrpApi - fun <K, A, B> TFlow<Map<K, Maybe<FrpSpec<A>>>>.applyLatestSpecForKey( - initialSpecs: Map<K, FrpSpec<B>>, - numKeys: Int? = null, - ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>> = - applyLatestSpecForKey(deferredOf(initialSpecs), numKeys) - - /** - * Returns a [TFlow] containing the results of applying each [FrpSpec] emitted from the original - * [TFlow]. - * - * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] with the same - * key are undone (any registered [observers][observe] are unregistered, and any pending - * [side-effects][effect] are cancelled). - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpSpec] will be undone with no replacement. - */ - @ExperimentalFrpApi - fun <K, A> TFlow<Map<K, Maybe<FrpSpec<A>>>>.applyLatestSpecForKey( - numKeys: Int? = null - ): TFlow<Map<K, Maybe<A>>> = - applyLatestSpecForKey<K, A, Nothing>(deferredOf(emptyMap()), numKeys).first - - /** - * Returns a [TState] containing the latest results of applying each [FrpSpec] emitted from the - * original [TFlow]. - * - * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] with the same - * key are undone (any registered [observers][observe] are unregistered, and any pending - * [side-effects][effect] are cancelled). - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpSpec] will be undone with no replacement. - */ - @ExperimentalFrpApi - fun <K, A> TFlow<Map<K, Maybe<FrpSpec<A>>>>.holdLatestSpecForKey( - initialSpecs: FrpDeferredValue<Map<K, FrpSpec<A>>>, - numKeys: Int? = null, - ): TState<Map<K, A>> { - val (changes, initialValues) = applyLatestSpecForKey(initialSpecs, numKeys) - return changes.foldMapIncrementally(initialValues) - } - - /** - * Returns a [TState] containing the latest results of applying each [FrpSpec] emitted from the - * original [TFlow]. - * - * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] with the same - * key are undone (any registered [observers][observe] are unregistered, and any pending - * [side-effects][effect] are cancelled). - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpSpec] will be undone with no replacement. - */ - @ExperimentalFrpApi - fun <K, A> TFlow<Map<K, Maybe<FrpSpec<A>>>>.holdLatestSpecForKey( - initialSpecs: Map<K, FrpSpec<A>> = emptyMap(), - numKeys: Int? = null, - ): TState<Map<K, A>> = holdLatestSpecForKey(deferredOf(initialSpecs), numKeys) - - /** - * Returns a [TFlow] containing the results of applying [transform] to each value of the - * original [TFlow], and a [FrpDeferredValue] containing the result of applying [transform] to - * [initialValues] immediately. - * - * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver. - * With each invocation of [transform], changes from the previous invocation are undone (any - * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are - * cancelled). - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpBuildScope] will be undone with no replacement. - */ - @ExperimentalFrpApi - fun <K, A, B> TFlow<Map<K, Maybe<A>>>.mapLatestBuildForKey( - initialValues: FrpDeferredValue<Map<K, A>>, - numKeys: Int? = null, - transform: FrpBuildScope.(K, A) -> B, - ): Pair<TFlow<Map<K, Maybe<B>>>, FrpDeferredValue<Map<K, B>>> = - map { patch -> patch.mapValues { (k, v) -> v.map { frpSpec { transform(k, it) } } } } - .applyLatestSpecForKey( - deferredBuildScope { - initialValues.get().mapValues { (k, v) -> frpSpec { transform(k, v) } } - }, - numKeys = numKeys, - ) - - /** - * Returns a [TFlow] containing the results of applying [transform] to each value of the - * original [TFlow], and a [FrpDeferredValue] containing the result of applying [transform] to - * [initialValues] immediately. - * - * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver. - * With each invocation of [transform], changes from the previous invocation are undone (any - * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are - * cancelled). - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpBuildScope] will be undone with no replacement. - */ - @ExperimentalFrpApi - fun <K, A, B> TFlow<Map<K, Maybe<A>>>.mapLatestBuildForKey( - initialValues: Map<K, A>, - numKeys: Int? = null, - transform: FrpBuildScope.(K, A) -> B, - ): Pair<TFlow<Map<K, Maybe<B>>>, FrpDeferredValue<Map<K, B>>> = - mapLatestBuildForKey(deferredOf(initialValues), numKeys, transform) - - /** - * Returns a [TFlow] containing the results of applying [transform] to each value of the - * original [TFlow]. - * - * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver. - * With each invocation of [transform], changes from the previous invocation are undone (any - * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are - * cancelled). - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpBuildScope] will be undone with no replacement. - */ - @ExperimentalFrpApi - fun <K, A, B> TFlow<Map<K, Maybe<A>>>.mapLatestBuildForKey( - numKeys: Int? = null, - transform: FrpBuildScope.(K, A) -> B, - ): TFlow<Map<K, Maybe<B>>> = mapLatestBuildForKey(emptyMap(), numKeys, transform).first - - /** Returns a [Deferred] containing the next value to be emitted from this [TFlow]. */ - @ExperimentalFrpApi - fun <R> TFlow<R>.nextDeferred(): Deferred<R> { - lateinit var next: CompletableDeferred<R> - val job = nextOnly().observe { next.complete(it) } - next = CompletableDeferred<R>(parent = job) - return next - } - - /** Returns a [TState] that reflects the [StateFlow.value] of this [StateFlow]. */ - @ExperimentalFrpApi - fun <A> StateFlow<A>.toTState(): TState<A> { - val initial = value - return tFlow { dropWhile { it == initial }.collect { emit(it) } }.hold(initial) - } - - /** Returns a [TFlow] that emits whenever this [Flow] emits. */ - @ExperimentalFrpApi - fun <A> Flow<A>.toTFlow(name: String? = null): TFlow<A> = tFlow(name) { collect { emit(it) } } - - /** - * Shorthand for: - * ```kotlin - * flow.toTFlow().hold(initialValue) - * ``` - */ - @ExperimentalFrpApi - fun <A> Flow<A>.toTState(initialValue: A): TState<A> = toTFlow().hold(initialValue) - - /** - * Shorthand for: - * ```kotlin - * flow.scan(initialValue, operation).toTFlow().hold(initialValue) - * ``` - */ - @ExperimentalFrpApi - fun <A, B> Flow<A>.scanToTState(initialValue: B, operation: (B, A) -> B): TState<B> = - scan(initialValue, operation).toTFlow().hold(initialValue) - - /** - * Shorthand for: - * ```kotlin - * flow.scan(initialValue) { a, f -> f(a) }.toTFlow().hold(initialValue) - * ``` - */ - @ExperimentalFrpApi - fun <A> Flow<(A) -> A>.scanToTState(initialValue: A): TState<A> = - scanToTState(initialValue) { a, f -> f(a) } - - /** - * Invokes [block] whenever this [TFlow] emits a value. [block] receives an [FrpBuildScope] that - * can be used to make further modifications to the FRP network, and/or perform side-effects via - * [effect]. - * - * With each invocation of [block], changes from the previous invocation are undone (any - * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are - * cancelled). - */ - @ExperimentalFrpApi - fun <A> TFlow<A>.observeLatestBuild(block: FrpBuildScope.(A) -> Unit = {}): Job = - mapLatestBuild { block(it) }.observe() - - /** - * Invokes [block] whenever this [TFlow] emits a value, allowing side-effects to be safely - * performed in reaction to the emission. - * - * With each invocation of [block], running effects from the previous invocation are cancelled. - */ - @ExperimentalFrpApi - fun <A> TFlow<A>.observeLatest(block: FrpEffectScope.(A) -> Unit = {}): Job { - var innerJob: Job? = null - return observeBuild { - innerJob?.cancel() - innerJob = effect { block(it) } - } - } - - /** - * Invokes [block] with the value held by this [TState], allowing side-effects to be safely - * performed in reaction to the state changing. - * - * With each invocation of [block], running effects from the previous invocation are cancelled. - */ - @ExperimentalFrpApi - fun <A> TState<A>.observeLatest(block: FrpEffectScope.(A) -> Unit = {}): Job = launchScope { - var innerJob = effect { block(sample()) } - stateChanges.observeBuild { - innerJob.cancel() - innerJob = effect { block(it) } - } - } - - /** - * Applies [block] to the value held by this [TState]. [block] receives an [FrpBuildScope] that - * can be used to make further modifications to the FRP network, and/or perform side-effects via - * [effect]. - * - * [block] can perform modifications to the FRP network via its [FrpBuildScope] receiver. With - * each invocation of [block], changes from the previous invocation are undone (any registered - * [observers][observe] are unregistered, and any pending [side-effects][effect] are cancelled). - */ - @ExperimentalFrpApi - fun <A> TState<A>.observeLatestBuild(block: FrpBuildScope.(A) -> Unit = {}): Job = launchScope { - var innerJob: Job = launchScope { block(sample()) } - stateChanges.observeBuild { - innerJob.cancel() - innerJob = launchScope { block(it) } - } - } - - /** Applies the [FrpSpec] within this [FrpBuildScope]. */ - @ExperimentalFrpApi fun <A> FrpSpec<A>.applySpec(): A = this() - - /** - * Applies the [FrpSpec] within this [FrpBuildScope], returning the result as an - * [FrpDeferredValue]. - */ - @ExperimentalFrpApi - fun <A> FrpSpec<A>.applySpecDeferred(): FrpDeferredValue<A> = deferredBuildScope { applySpec() } - - /** - * Invokes [block] on the value held in this [TState]. [block] receives an [FrpBuildScope] that - * can be used to make further modifications to the FRP network, and/or perform side-effects via - * [effect]. - */ - @ExperimentalFrpApi - fun <A> TState<A>.observeBuild(block: FrpBuildScope.(A) -> Unit = {}): Job = launchScope { - block(sample()) - stateChanges.observeBuild(block) - } - - /** - * Invokes [block] with the current value of this [TState], re-invoking whenever it changes, - * allowing side-effects to be safely performed in reaction value changing. - * - * Specifically, [block] is deferred to the end of the transaction, and is only actually - * executed if this [FrpBuildScope] is still active by that time. It can be deactivated due to a - * -Latest combinator, for example. - * - * If the [TState] is changing within the *current* transaction (i.e. [stateChanges] is - * presently emitting) then [block] will be invoked for the first time with the new value; - * otherwise, it will be invoked with the [current][sample] value. - */ - @ExperimentalFrpApi - fun <A> TState<A>.observe(block: FrpEffectScope.(A) -> Unit = {}): Job = - now.map { sample() }.mergeWith(stateChanges) { _, new -> new }.observe { block(it) } -} - -/** - * Returns a [TFlow] that emits the result of [block] once it completes. [block] is evaluated - * outside of the current FRP transaction; when it completes, the returned [TFlow] emits in a new - * transaction. - * - * Shorthand for: - * ``` - * tFlow { emitter: MutableTFlow<A> -> - * val a = block() - * emitter.emit(a) - * } - * ``` - */ -@ExperimentalFrpApi -fun <A> FrpBuildScope.asyncTFlow(block: suspend () -> A): TFlow<A> = - tFlow { - // TODO: if block completes synchronously, it would be nice to emit within this - // transaction - emit(block()) - } - .apply { observe() } - -/** - * Performs a side-effect in a safe manner w/r/t the current FRP transaction. - * - * Specifically, [block] is deferred to the end of the current transaction, and is only actually - * executed if this [FrpBuildScope] is still active by that time. It can be deactivated due to a - * -Latest combinator, for example. - * - * Shorthand for: - * ```kotlin - * now.observe { block() } - * ``` - */ -@ExperimentalFrpApi -fun FrpBuildScope.effect( - context: CoroutineContext = EmptyCoroutineContext, - block: FrpEffectScope.() -> Unit, -): Job = now.observe(context) { block() } - -/** - * Launches [block] in a new coroutine, returning a [Job] bound to the coroutine. - * - * This coroutine is not actually started until the *end* of the current FRP transaction. This is - * done because the current [FrpBuildScope] might be deactivated within this transaction, perhaps - * due to a -Latest combinator. If this happens, then the coroutine will never actually be started. - * - * Shorthand for: - * ```kotlin - * effect { frpCoroutineScope.launch { block() } } - * ``` - */ -@ExperimentalFrpApi -fun FrpBuildScope.launchEffect(block: suspend CoroutineScope.() -> Unit): Job = asyncEffect(block) - -/** - * Launches [block] in a new coroutine, returning the result as a [Deferred]. - * - * This coroutine is not actually started until the *end* of the current FRP transaction. This is - * done because the current [FrpBuildScope] might be deactivated within this transaction, perhaps - * due to a -Latest combinator. If this happens, then the coroutine will never actually be started. - * - * Shorthand for: - * ```kotlin - * CompletableDeferred<R>.apply { - * effect { frpCoroutineScope.launch { complete(coroutineScope { block() }) } } - * } - * .await() - * ``` - */ -@ExperimentalFrpApi -fun <R> FrpBuildScope.asyncEffect(block: suspend CoroutineScope.() -> R): Deferred<R> { - val result = CompletableDeferred<R>() - val job = now.observe { frpCoroutineScope.launch { result.complete(coroutineScope(block)) } } - val handle = job.invokeOnCompletion { result.cancel() } - result.invokeOnCompletion { - handle.dispose() - job.cancel() - } - return result -} - -/** Like [FrpBuildScope.asyncScope], but ignores the result of [block]. */ -@ExperimentalFrpApi fun FrpBuildScope.launchScope(block: FrpSpec<*>): Job = asyncScope(block).second - -/** - * Creates an instance of a [TFlow] with elements that are emitted from [builder]. - * - * [builder] is run in its own coroutine, allowing for ongoing work that can emit to the provided - * [MutableTFlow]. - * - * By default, [builder] is only running while the returned [TFlow] is being - * [observed][FrpBuildScope.observe]. If you want it to run at all times, simply add a no-op - * observer: - * ```kotlin - * tFlow { ... }.apply { observe() } - * ``` - * - * In the event of backpressure, emissions are *coalesced* into batches. When a value is - * [emitted][FrpCoalescingProducerScope.emit] from [builder], it is merged into the batch via - * [coalesce]. Once the batch is consumed by the FRP network in the next transaction, the batch is - * reset back to [initialValue]. - */ -@ExperimentalFrpApi -fun <In, Out> FrpBuildScope.coalescingTFlow( - initialValue: Out, - coalesce: (old: Out, new: In) -> Out, - builder: suspend FrpCoalescingProducerScope<In>.() -> Unit, -): TFlow<Out> = coalescingTFlow(getInitialValue = { initialValue }, coalesce, builder) - -/** - * Creates an instance of a [TFlow] with elements that are emitted from [builder]. - * - * [builder] is run in its own coroutine, allowing for ongoing work that can emit to the provided - * [MutableTFlow]. - * - * By default, [builder] is only running while the returned [TFlow] is being - * [observed][FrpBuildScope.observe]. If you want it to run at all times, simply add a no-op - * observer: - * ```kotlin - * tFlow { ... }.apply { observe() } - * ``` - * - * In the event of backpressure, emissions are *conflated*; any older emissions are dropped and only - * the most recent emission will be used when the FRP network is ready. - */ -@ExperimentalFrpApi -fun <T> FrpBuildScope.conflatedTFlow( - builder: suspend FrpCoalescingProducerScope<T>.() -> Unit -): TFlow<T> = - coalescingTFlow<T, Any?>(initialValue = Any(), coalesce = { _, new -> new }, builder = builder) - .mapCheap { - @Suppress("UNCHECKED_CAST") - it as T - } - -/** Scope for emitting to a [FrpBuildScope.coalescingTFlow]. */ -interface FrpCoalescingProducerScope<in T> { - /** - * Inserts [value] into the current batch, enqueueing it for emission from this [TFlow] if not - * already pending. - * - * Backpressure occurs when [emit] is called while the FRP network is currently in a - * transaction; if called multiple times, then emissions will be coalesced into a single batch - * that is then processed when the network is ready. - */ - fun emit(value: T) -} - -/** Scope for emitting to a [FrpBuildScope.tFlow]. */ -interface FrpProducerScope<in T> { - /** - * Emits a [value] to this [TFlow], suspending the caller until the FRP transaction containing - * the emission has completed. - */ - suspend fun emit(value: T) -} - -/** - * Suspends forever. Upon cancellation, runs [block]. Useful for unregistering callbacks inside of - * [FrpBuildScope.tFlow] and [FrpBuildScope.coalescingTFlow]. - */ -suspend fun awaitClose(block: () -> Unit): Nothing = - try { - awaitCancellation() - } finally { - block() - } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpEffectScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpEffectScope.kt deleted file mode 100644 index b39dcc131b1d..000000000000 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpEffectScope.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.kairos - -import kotlin.coroutines.RestrictsSuspension -import kotlinx.coroutines.CoroutineScope - -/** - * Scope for external side-effects triggered by the Frp network. This still occurs within the - * context of a transaction, so general suspending calls are disallowed to prevent blocking the - * transaction. You can use [frpCoroutineScope] to [launch][kotlinx.coroutines.launch] new - * coroutines to perform long-running asynchronous work. This scope is alive for the duration of the - * containing [FrpBuildScope] that this side-effect scope is running in. - */ -@RestrictsSuspension -@ExperimentalFrpApi -interface FrpEffectScope : FrpTransactionScope { - /** - * A [CoroutineScope] whose lifecycle lives for as long as this [FrpEffectScope] is alive. This - * is generally until the [Job][kotlinx.coroutines.Job] returned by [FrpBuildScope.effect] is - * cancelled. - */ - @ExperimentalFrpApi val frpCoroutineScope: CoroutineScope - - /** - * A [FrpNetwork] instance that can be used to transactionally query / modify the FRP network. - * - * The lambda passed to [FrpNetwork.transact] on this instance will receive an [FrpBuildScope] - * that is lifetime-bound to this [FrpEffectScope]. Once this [FrpEffectScope] is no longer - * alive, any modifications to the FRP network performed via this [FrpNetwork] instance will be - * undone (any registered [observers][FrpBuildScope.observe] are unregistered, and any pending - * [side-effects][FrpBuildScope.effect] are cancelled). - */ - @ExperimentalFrpApi val frpNetwork: FrpNetwork -} diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpNetwork.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpNetwork.kt deleted file mode 100644 index 0679848c6c80..000000000000 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpNetwork.kt +++ /dev/null @@ -1,197 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.kairos - -import com.android.systemui.kairos.internal.BuildScopeImpl -import com.android.systemui.kairos.internal.Network -import com.android.systemui.kairos.internal.StateScopeImpl -import com.android.systemui.kairos.internal.util.awaitCancellationAndThen -import com.android.systemui.kairos.internal.util.childScope -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext -import kotlinx.coroutines.CoroutineName -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.job -import kotlinx.coroutines.launch - -/** - * Marks declarations that are still **experimental** and shouldn't be used in general production - * code. - */ -@RequiresOptIn( - message = "This API is experimental and should not be used in general production code." -) -@Retention(AnnotationRetention.BINARY) -annotation class ExperimentalFrpApi - -/** - * External interface to an FRP network. Can be used to make transactional queries and modifications - * to the network. - */ -@ExperimentalFrpApi -interface FrpNetwork { - /** - * Runs [block] inside of a transaction, suspending until the transaction is complete. - * - * The [FrpBuildScope] receiver exposes methods that can be used to query or modify the network. - * If the network is cancelled while the caller of [transact] is suspended, then the call will - * be cancelled. - */ - @ExperimentalFrpApi suspend fun <R> transact(block: FrpTransactionScope.() -> R): R - - /** - * Activates [spec] in a transaction, suspending indefinitely. While suspended, all observers - * and long-running effects are kept alive. When cancelled, observers are unregistered and - * effects are cancelled. - */ - @ExperimentalFrpApi suspend fun activateSpec(spec: FrpSpec<*>) - - /** Returns a [CoalescingMutableTFlow] that can emit values into this [FrpNetwork]. */ - @ExperimentalFrpApi - fun <In, Out> coalescingMutableTFlow( - coalesce: (old: Out, new: In) -> Out, - getInitialValue: () -> Out, - ): CoalescingMutableTFlow<In, Out> - - /** Returns a [MutableTFlow] that can emit values into this [FrpNetwork]. */ - @ExperimentalFrpApi fun <T> mutableTFlow(): MutableTFlow<T> - - /** Returns a [MutableTState]. with initial state [initialValue]. */ - @ExperimentalFrpApi - fun <T> mutableTStateDeferred(initialValue: FrpDeferredValue<T>): MutableTState<T> -} - -/** Returns a [CoalescingMutableTFlow] that can emit values into this [FrpNetwork]. */ -@ExperimentalFrpApi -fun <In, Out> FrpNetwork.coalescingMutableTFlow( - coalesce: (old: Out, new: In) -> Out, - initialValue: Out, -): CoalescingMutableTFlow<In, Out> = - coalescingMutableTFlow(coalesce, getInitialValue = { initialValue }) - -/** Returns a [MutableTState]. with initial state [initialValue]. */ -@ExperimentalFrpApi -fun <T> FrpNetwork.mutableTState(initialValue: T): MutableTState<T> = - mutableTStateDeferred(deferredOf(initialValue)) - -/** Returns a [MutableTState]. with initial state [initialValue]. */ -@ExperimentalFrpApi -fun <T> MutableTState(network: FrpNetwork, initialValue: T): MutableTState<T> = - network.mutableTState(initialValue) - -/** Returns a [MutableTFlow] that can emit values into this [FrpNetwork]. */ -@ExperimentalFrpApi -fun <T> MutableTFlow(network: FrpNetwork): MutableTFlow<T> = network.mutableTFlow() - -/** Returns a [CoalescingMutableTFlow] that can emit values into this [FrpNetwork]. */ -@ExperimentalFrpApi -fun <In, Out> CoalescingMutableTFlow( - network: FrpNetwork, - coalesce: (old: Out, new: In) -> Out, - initialValue: Out, -): CoalescingMutableTFlow<In, Out> = network.coalescingMutableTFlow(coalesce) { initialValue } - -/** Returns a [CoalescingMutableTFlow] that can emit values into this [FrpNetwork]. */ -@ExperimentalFrpApi -fun <In, Out> CoalescingMutableTFlow( - network: FrpNetwork, - coalesce: (old: Out, new: In) -> Out, - getInitialValue: () -> Out, -): CoalescingMutableTFlow<In, Out> = network.coalescingMutableTFlow(coalesce, getInitialValue) - -/** - * Activates [spec] in a transaction and invokes [block] with the result, suspending indefinitely. - * While suspended, all observers and long-running effects are kept alive. When cancelled, observers - * are unregistered and effects are cancelled. - */ -@ExperimentalFrpApi -suspend fun <R> FrpNetwork.activateSpec(spec: FrpSpec<R>, block: suspend (R) -> Unit) { - activateSpec { - val result = spec.applySpec() - launchEffect { block(result) } - } -} - -internal class LocalFrpNetwork( - private val network: Network, - private val scope: CoroutineScope, - private val endSignal: TFlow<Any>, -) : FrpNetwork { - override suspend fun <R> transact(block: FrpTransactionScope.() -> R): R = - network.transaction("FrpNetwork.transact") { runInTransactionScope { block() } }.await() - - override suspend fun activateSpec(spec: FrpSpec<*>) { - val stopEmitter = - CoalescingMutableTFlow( - name = "activateSpec", - coalesce = { _, _: Unit -> }, - network = network, - getInitialValue = {}, - ) - val job = - network - .transaction("FrpNetwork.activateSpec") { - val buildScope = - BuildScopeImpl( - stateScope = - StateScopeImpl( - evalScope = this, - endSignal = mergeLeft(stopEmitter, endSignal), - ), - coroutineScope = scope, - ) - buildScope.runInBuildScope { launchScope(spec) } - } - .await() - awaitCancellationAndThen { - stopEmitter.emit(Unit) - job.cancel() - } - } - - override fun <In, Out> coalescingMutableTFlow( - coalesce: (old: Out, new: In) -> Out, - getInitialValue: () -> Out, - ): CoalescingMutableTFlow<In, Out> = - CoalescingMutableTFlow(null, coalesce, network, getInitialValue) - - override fun <T> mutableTFlow(): MutableTFlow<T> = MutableTFlow(network) - - override fun <T> mutableTStateDeferred(initialValue: FrpDeferredValue<T>): MutableTState<T> = - MutableTState(network, initialValue.unwrapped) -} - -/** - * Combination of an [FrpNetwork] and a [Job] that, when cancelled, will cancel the entire FRP - * network. - */ -@ExperimentalFrpApi -class RootFrpNetwork -internal constructor(private val network: Network, private val scope: CoroutineScope, job: Job) : - Job by job, FrpNetwork by LocalFrpNetwork(network, scope, emptyTFlow) - -/** Constructs a new [RootFrpNetwork] in the given [CoroutineScope]. */ -@ExperimentalFrpApi -fun CoroutineScope.newFrpNetwork( - context: CoroutineContext = EmptyCoroutineContext -): RootFrpNetwork { - val scope = childScope(context) - val network = Network(scope) - scope.launch(CoroutineName("newFrpNetwork scheduler")) { network.runInputScheduler() } - return RootFrpNetwork(network, scope, scope.coroutineContext.job) -} diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpScope.kt deleted file mode 100644 index 92cb13f77d04..000000000000 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpScope.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.kairos - -import com.android.systemui.kairos.internal.CompletableLazy -import kotlin.coroutines.RestrictsSuspension - -/** Denotes [FrpScope] interfaces as [DSL markers][DslMarker]. */ -@DslMarker annotation class FrpScopeMarker - -/** - * Base scope for all FRP scopes. Used to prevent implicitly capturing other scopes from in lambdas. - */ -@FrpScopeMarker -@RestrictsSuspension -@ExperimentalFrpApi -interface FrpScope { - /** - * Returns the value held by the [FrpDeferredValue], suspending until available if necessary. - */ - @ExperimentalFrpApi fun <A> FrpDeferredValue<A>.get(): A = unwrapped.value -} - -/** - * A value that may not be immediately (synchronously) available, but is guaranteed to be available - * before this transaction is completed. - * - * @see FrpScope.get - */ -@ExperimentalFrpApi -class FrpDeferredValue<out A> internal constructor(internal val unwrapped: Lazy<A>) - -/** - * Returns the value held by this [FrpDeferredValue], or throws [IllegalStateException] if it is not - * yet available. - * - * This API is not meant for general usage within the FRP network. It is made available mainly for - * debugging and logging. You should always prefer [get][FrpScope.get] if possible. - * - * @see FrpScope.get - */ -@ExperimentalFrpApi fun <A> FrpDeferredValue<A>.getUnsafe(): A = unwrapped.value - -/** Returns an already-available [FrpDeferredValue] containing [value]. */ -@ExperimentalFrpApi -fun <A> deferredOf(value: A): FrpDeferredValue<A> = FrpDeferredValue(CompletableLazy(value)) diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpStateScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpStateScope.kt deleted file mode 100644 index 3de246300501..000000000000 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpStateScope.kt +++ /dev/null @@ -1,799 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.kairos - -import com.android.systemui.kairos.combine as combinePure -import com.android.systemui.kairos.map as mapPure -import com.android.systemui.kairos.util.Just -import com.android.systemui.kairos.util.Left -import com.android.systemui.kairos.util.Maybe -import com.android.systemui.kairos.util.Right -import com.android.systemui.kairos.util.WithPrev -import com.android.systemui.kairos.util.just -import com.android.systemui.kairos.util.map -import com.android.systemui.kairos.util.none -import com.android.systemui.kairos.util.partitionEithers -import com.android.systemui.kairos.util.zipWith -import kotlin.coroutines.RestrictsSuspension - -typealias FrpStateful<R> = FrpStateScope.() -> R - -/** - * Returns a [FrpStateful] that, when [applied][FrpStateScope.applyStateful], invokes [block] with - * the applier's [FrpStateScope]. - */ -// TODO: caching story? should each Scope have a cache of applied FrpStateful instances? -@ExperimentalFrpApi -@Suppress("NOTHING_TO_INLINE") -inline fun <A> statefully(noinline block: FrpStateScope.() -> A): FrpStateful<A> = block - -/** - * Operations that accumulate state within the FRP network. - * - * State accumulation is an ongoing process that has a lifetime. Use `-Latest` combinators, such as - * [mapLatestStateful], to create smaller, nested lifecycles so that accumulation isn't running - * longer than needed. - */ -@ExperimentalFrpApi -@RestrictsSuspension -interface FrpStateScope : FrpTransactionScope { - - /** TODO */ - @ExperimentalFrpApi - // TODO: wish this could just be `deferred` but alas - fun <A> deferredStateScope(block: FrpStateScope.() -> A): FrpDeferredValue<A> - - /** - * Returns a [TState] that holds onto the most recently emitted value from this [TFlow], or - * [initialValue] if nothing has been emitted since it was constructed. - * - * Note that the value contained within the [TState] is not updated until *after* all [TFlow]s - * have been processed; this keeps the value of the [TState] consistent during the entire FRP - * transaction. - */ - @ExperimentalFrpApi fun <A> TFlow<A>.holdDeferred(initialValue: FrpDeferredValue<A>): TState<A> - - /** - * Returns a [TFlow] that emits from a merged, incrementally-accumulated collection of [TFlow]s - * emitted from this, following the same "patch" rules as outlined in [foldMapIncrementally]. - * - * Conceptually this is equivalent to: - * ```kotlin - * fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementally( - * initialTFlows: Map<K, TFlow<V>>, - * ): TFlow<Map<K, V>> = - * foldMapIncrementally(initialTFlows).map { it.merge() }.switch() - * ``` - * - * While the behavior is equivalent to the conceptual definition above, the implementation is - * significantly more efficient. - * - * @see merge - */ - @ExperimentalFrpApi - fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementally( - name: String? = null, - initialTFlows: FrpDeferredValue<Map<K, TFlow<V>>>, - ): TFlow<Map<K, V>> - - /** - * Returns a [TFlow] that emits from a merged, incrementally-accumulated collection of [TFlow]s - * emitted from this, following the same "patch" rules as outlined in [foldMapIncrementally]. - * - * Conceptually this is equivalent to: - * ```kotlin - * fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementallyPrompt( - * initialTFlows: Map<K, TFlow<V>>, - * ): TFlow<Map<K, V>> = - * foldMapIncrementally(initialTFlows).map { it.merge() }.switchPromptly() - * ``` - * - * While the behavior is equivalent to the conceptual definition above, the implementation is - * significantly more efficient. - * - * @see merge - */ - @ExperimentalFrpApi - fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementallyPromptly( - initialTFlows: FrpDeferredValue<Map<K, TFlow<V>>>, - name: String? = null, - ): TFlow<Map<K, V>> - - // TODO: everything below this comment can be made into extensions once we have context params - - /** - * Returns a [TFlow] that emits from a merged, incrementally-accumulated collection of [TFlow]s - * emitted from this, following the same "patch" rules as outlined in [foldMapIncrementally]. - * - * Conceptually this is equivalent to: - * ```kotlin - * fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementally( - * initialTFlows: Map<K, TFlow<V>>, - * ): TFlow<Map<K, V>> = - * foldMapIncrementally(initialTFlows).map { it.merge() }.switch() - * ``` - * - * While the behavior is equivalent to the conceptual definition above, the implementation is - * significantly more efficient. - * - * @see merge - */ - @ExperimentalFrpApi - fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementally( - name: String? = null, - initialTFlows: Map<K, TFlow<V>> = emptyMap(), - ): TFlow<Map<K, V>> = mergeIncrementally(name, deferredOf(initialTFlows)) - - /** - * Returns a [TFlow] that emits from a merged, incrementally-accumulated collection of [TFlow]s - * emitted from this, following the same "patch" rules as outlined in [foldMapIncrementally]. - * - * Conceptually this is equivalent to: - * ```kotlin - * fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementallyPrompt( - * initialTFlows: Map<K, TFlow<V>>, - * ): TFlow<Map<K, V>> = - * foldMapIncrementally(initialTFlows).map { it.merge() }.switchPromptly() - * ``` - * - * While the behavior is equivalent to the conceptual definition above, the implementation is - * significantly more efficient. - * - * @see merge - */ - @ExperimentalFrpApi - fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementallyPromptly( - initialTFlows: Map<K, TFlow<V>> = emptyMap(), - name: String? = null, - ): TFlow<Map<K, V>> = mergeIncrementallyPromptly(deferredOf(initialTFlows), name) - - /** Applies the [FrpStateful] within this [FrpStateScope]. */ - @ExperimentalFrpApi fun <A> FrpStateful<A>.applyStateful(): A = this() - - /** - * Applies the [FrpStateful] within this [FrpStateScope], returning the result as an - * [FrpDeferredValue]. - */ - @ExperimentalFrpApi - fun <A> FrpStateful<A>.applyStatefulDeferred(): FrpDeferredValue<A> = deferredStateScope { - applyStateful() - } - - /** - * Returns a [TState] that holds onto the most recently emitted value from this [TFlow], or - * [initialValue] if nothing has been emitted since it was constructed. - * - * Note that the value contained within the [TState] is not updated until *after* all [TFlow]s - * have been processed; this keeps the value of the [TState] consistent during the entire FRP - * transaction. - */ - @ExperimentalFrpApi - fun <A> TFlow<A>.hold(initialValue: A): TState<A> = holdDeferred(deferredOf(initialValue)) - - /** - * Returns a [TFlow] the emits the result of applying [FrpStatefuls][FrpStateful] emitted from - * the original [TFlow]. - * - * Unlike [applyLatestStateful], state accumulation is not stopped with each subsequent emission - * of the original [TFlow]. - */ - @ExperimentalFrpApi fun <A> TFlow<FrpStateful<A>>.applyStatefuls(): TFlow<A> - - /** - * Returns a [TFlow] containing the results of applying [transform] to each value of the - * original [TFlow]. - * - * [transform] can perform state accumulation via its [FrpStateScope] receiver. Unlike - * [mapLatestStateful], accumulation is not stopped with each subsequent emission of the - * original [TFlow]. - */ - @ExperimentalFrpApi - fun <A, B> TFlow<A>.mapStateful(transform: FrpStateScope.(A) -> B): TFlow<B> = - mapPure { statefully { transform(it) } }.applyStatefuls() - - /** - * Returns a [TState] the holds the result of applying the [FrpStateful] held by the original - * [TState]. - * - * Unlike [applyLatestStateful], state accumulation is not stopped with each state change. - */ - @ExperimentalFrpApi - fun <A> TState<FrpStateful<A>>.applyStatefuls(): TState<A> = - stateChanges - .applyStatefuls() - .holdDeferred(initialValue = deferredStateScope { sampleDeferred().get()() }) - - /** Returns a [TFlow] that switches to the [TFlow] emitted by the original [TFlow]. */ - @ExperimentalFrpApi fun <A> TFlow<TFlow<A>>.flatten() = hold(emptyTFlow).switch() - - /** - * Returns a [TFlow] containing the results of applying [transform] to each value of the - * original [TFlow]. - * - * [transform] can perform state accumulation via its [FrpStateScope] receiver. With each - * invocation of [transform], state accumulation from previous invocation is stopped. - */ - @ExperimentalFrpApi - fun <A, B> TFlow<A>.mapLatestStateful(transform: FrpStateScope.(A) -> B): TFlow<B> = - mapPure { statefully { transform(it) } }.applyLatestStateful() - - /** - * Returns a [TFlow] that switches to a new [TFlow] produced by [transform] every time the - * original [TFlow] emits a value. - * - * [transform] can perform state accumulation via its [FrpStateScope] receiver. With each - * invocation of [transform], state accumulation from previous invocation is stopped. - */ - @ExperimentalFrpApi - fun <A, B> TFlow<A>.flatMapLatestStateful(transform: FrpStateScope.(A) -> TFlow<B>): TFlow<B> = - mapLatestStateful(transform).flatten() - - /** - * Returns a [TFlow] containing the results of applying each [FrpStateful] emitted from the - * original [TFlow]. - * - * When each [FrpStateful] is applied, state accumulation from the previously-active - * [FrpStateful] is stopped. - */ - @ExperimentalFrpApi - fun <A> TFlow<FrpStateful<A>>.applyLatestStateful(): TFlow<A> = applyLatestStateful {}.first - - /** - * Returns a [TState] containing the value returned by applying the [FrpStateful] held by the - * original [TState]. - * - * When each [FrpStateful] is applied, state accumulation from the previously-active - * [FrpStateful] is stopped. - */ - @ExperimentalFrpApi - fun <A> TState<FrpStateful<A>>.applyLatestStateful(): TState<A> { - val (changes, init) = stateChanges.applyLatestStateful { sample()() } - return changes.holdDeferred(init) - } - - /** - * Returns a [TFlow] containing the results of applying each [FrpStateful] emitted from the - * original [TFlow], and a [FrpDeferredValue] containing the result of applying [init] - * immediately. - * - * When each [FrpStateful] is applied, state accumulation from the previously-active - * [FrpStateful] is stopped. - */ - @ExperimentalFrpApi - fun <A, B> TFlow<FrpStateful<B>>.applyLatestStateful( - init: FrpStateful<A> - ): Pair<TFlow<B>, FrpDeferredValue<A>> { - val (flow, result) = - mapCheap { spec -> mapOf(Unit to just(spec)) } - .applyLatestStatefulForKey(init = mapOf(Unit to init), numKeys = 1) - val outFlow: TFlow<B> = - flow.mapMaybe { - checkNotNull(it[Unit]) { "applyLatest: expected result, but none present in: $it" } - } - val outInit: FrpDeferredValue<A> = deferredTransactionScope { - val initResult: Map<Unit, A> = result.get() - check(Unit in initResult) { - "applyLatest: expected initial result, but none present in: $initResult" - } - @Suppress("UNCHECKED_CAST") - initResult.getOrDefault(Unit) { null } as A - } - return Pair(outFlow, outInit) - } - - /** - * Returns a [TFlow] containing the results of applying each [FrpStateful] emitted from the - * original [TFlow], and a [FrpDeferredValue] containing the result of applying [init] - * immediately. - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpStateful] will be stopped with no replacement. - * - * When each [FrpStateful] is applied, state accumulation from the previously-active - * [FrpStateful] with the same key is stopped. - */ - @ExperimentalFrpApi - fun <K, A, B> TFlow<Map<K, Maybe<FrpStateful<A>>>>.applyLatestStatefulForKey( - init: FrpDeferredValue<Map<K, FrpStateful<B>>>, - numKeys: Int? = null, - ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>> - - /** - * Returns a [TFlow] containing the results of applying each [FrpStateful] emitted from the - * original [TFlow], and a [FrpDeferredValue] containing the result of applying [init] - * immediately. - * - * When each [FrpStateful] is applied, state accumulation from the previously-active - * [FrpStateful] with the same key is stopped. - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpStateful] will be stopped with no replacement. - */ - @ExperimentalFrpApi - fun <K, A, B> TFlow<Map<K, Maybe<FrpStateful<A>>>>.applyLatestStatefulForKey( - init: Map<K, FrpStateful<B>>, - numKeys: Int? = null, - ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>> = - applyLatestStatefulForKey(deferredOf(init), numKeys) - - /** - * Returns a [TState] containing the latest results of applying each [FrpStateful] emitted from - * the original [TFlow]. - * - * When each [FrpStateful] is applied, state accumulation from the previously-active - * [FrpStateful] with the same key is stopped. - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpStateful] will be stopped with no replacement. - */ - @ExperimentalFrpApi - fun <K, A> TFlow<Map<K, Maybe<FrpStateful<A>>>>.holdLatestStatefulForKey( - init: FrpDeferredValue<Map<K, FrpStateful<A>>>, - numKeys: Int? = null, - ): TState<Map<K, A>> { - val (changes, initialValues) = applyLatestStatefulForKey(init, numKeys) - return changes.foldMapIncrementally(initialValues) - } - - /** - * Returns a [TState] containing the latest results of applying each [FrpStateful] emitted from - * the original [TFlow]. - * - * When each [FrpStateful] is applied, state accumulation from the previously-active - * [FrpStateful] with the same key is stopped. - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpStateful] will be stopped with no replacement. - */ - @ExperimentalFrpApi - fun <K, A> TFlow<Map<K, Maybe<FrpStateful<A>>>>.holdLatestStatefulForKey( - init: Map<K, FrpStateful<A>> = emptyMap(), - numKeys: Int? = null, - ): TState<Map<K, A>> = holdLatestStatefulForKey(deferredOf(init), numKeys) - - /** - * Returns a [TFlow] containing the results of applying each [FrpStateful] emitted from the - * original [TFlow], and a [FrpDeferredValue] containing the result of applying [init] - * immediately. - * - * When each [FrpStateful] is applied, state accumulation from the previously-active - * [FrpStateful] with the same key is stopped. - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpStateful] will be stopped with no replacement. - */ - @ExperimentalFrpApi - fun <K, A> TFlow<Map<K, Maybe<FrpStateful<A>>>>.applyLatestStatefulForKey( - numKeys: Int? = null - ): TFlow<Map<K, Maybe<A>>> = - applyLatestStatefulForKey(init = emptyMap<K, FrpStateful<*>>(), numKeys = numKeys).first - - /** - * Returns a [TFlow] containing the results of applying [transform] to each value of the - * original [TFlow], and a [FrpDeferredValue] containing the result of applying [transform] to - * [initialValues] immediately. - * - * [transform] can perform state accumulation via its [FrpStateScope] receiver. With each - * invocation of [transform], state accumulation from previous invocation is stopped. - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpStateScope] will be stopped with no replacement. - */ - @ExperimentalFrpApi - fun <K, A, B> TFlow<Map<K, Maybe<A>>>.mapLatestStatefulForKey( - initialValues: FrpDeferredValue<Map<K, A>>, - numKeys: Int? = null, - transform: FrpStateScope.(A) -> B, - ): Pair<TFlow<Map<K, Maybe<B>>>, FrpDeferredValue<Map<K, B>>> = - mapPure { patch -> patch.mapValues { (_, v) -> v.map { statefully { transform(it) } } } } - .applyLatestStatefulForKey( - deferredStateScope { - initialValues.get().mapValues { (_, v) -> statefully { transform(v) } } - }, - numKeys = numKeys, - ) - - /** - * Returns a [TFlow] containing the results of applying [transform] to each value of the - * original [TFlow], and a [FrpDeferredValue] containing the result of applying [transform] to - * [initialValues] immediately. - * - * [transform] can perform state accumulation via its [FrpStateScope] receiver. With each - * invocation of [transform], state accumulation from previous invocation is stopped. - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpStateScope] will be stopped with no replacement. - */ - @ExperimentalFrpApi - fun <K, A, B> TFlow<Map<K, Maybe<A>>>.mapLatestStatefulForKey( - initialValues: Map<K, A>, - numKeys: Int? = null, - transform: FrpStateScope.(A) -> B, - ): Pair<TFlow<Map<K, Maybe<B>>>, FrpDeferredValue<Map<K, B>>> = - mapLatestStatefulForKey(deferredOf(initialValues), numKeys, transform) - - /** - * Returns a [TFlow] containing the results of applying [transform] to each value of the - * original [TFlow]. - * - * [transform] can perform state accumulation via its [FrpStateScope] receiver. With each - * invocation of [transform], state accumulation from previous invocation is stopped. - * - * If the [Maybe] contained within the value for an associated key is [none], then the - * previously-active [FrpStateScope] will be stopped with no replacement. - */ - @ExperimentalFrpApi - fun <K, A, B> TFlow<Map<K, Maybe<A>>>.mapLatestStatefulForKey( - numKeys: Int? = null, - transform: FrpStateScope.(A) -> B, - ): TFlow<Map<K, Maybe<B>>> = mapLatestStatefulForKey(emptyMap(), numKeys, transform).first - - /** - * Returns a [TFlow] that will only emit the next event of the original [TFlow], and then will - * act as [emptyTFlow]. - * - * If the original [TFlow] is emitting an event at this exact time, then it will be the only - * even emitted from the result [TFlow]. - */ - @ExperimentalFrpApi - fun <A> TFlow<A>.nextOnly(name: String? = null): TFlow<A> = - if (this === emptyTFlow) { - this - } else { - TFlowLoop<A>().also { - it.loopback = it.mapCheap { emptyTFlow }.hold(this@nextOnly).switch(name) - } - } - - /** Returns a [TFlow] that skips the next emission of the original [TFlow]. */ - @ExperimentalFrpApi - fun <A> TFlow<A>.skipNext(): TFlow<A> = - if (this === emptyTFlow) { - this - } else { - nextOnly().mapCheap { this@skipNext }.hold(emptyTFlow).switch() - } - - /** - * Returns a [TFlow] that emits values from the original [TFlow] up until [stop] emits a value. - * - * If the original [TFlow] emits at the same time as [stop], then the returned [TFlow] will emit - * that value. - */ - @ExperimentalFrpApi - fun <A> TFlow<A>.takeUntil(stop: TFlow<*>): TFlow<A> = - if (stop === emptyTFlow) { - this - } else { - stop.mapCheap { emptyTFlow }.nextOnly().hold(this).switch() - } - - /** - * Invokes [stateful] in a new [FrpStateScope] that is a child of this one. - * - * This new scope is stopped when [stop] first emits a value, or when the parent scope is - * stopped. Stopping will end all state accumulation; any [TStates][TState] returned from this - * scope will no longer update. - */ - @ExperimentalFrpApi - fun <A> childStateScope(stop: TFlow<*>, stateful: FrpStateful<A>): FrpDeferredValue<A> { - val (_, init: FrpDeferredValue<Map<Unit, A>>) = - stop - .nextOnly() - .mapPure { mapOf(Unit to none<FrpStateful<A>>()) } - .applyLatestStatefulForKey(init = mapOf(Unit to stateful), numKeys = 1) - return deferredStateScope { init.get().getValue(Unit) } - } - - /** - * Returns a [TFlow] that emits values from the original [TFlow] up to and including a value is - * emitted that satisfies [predicate]. - */ - @ExperimentalFrpApi - fun <A> TFlow<A>.takeUntil(predicate: FrpTransactionScope.(A) -> Boolean): TFlow<A> = - takeUntil(filter(predicate)) - - /** - * Returns a [TState] that is incrementally updated when this [TFlow] emits a value, by applying - * [transform] to both the emitted value and the currently tracked state. - * - * Note that the value contained within the [TState] is not updated until *after* all [TFlow]s - * have been processed; this keeps the value of the [TState] consistent during the entire FRP - * transaction. - */ - @ExperimentalFrpApi - fun <A, B> TFlow<A>.fold( - initialValue: B, - transform: FrpTransactionScope.(A, B) -> B, - ): TState<B> { - lateinit var state: TState<B> - return mapPure { a -> transform(a, state.sample()) }.hold(initialValue).also { state = it } - } - - /** - * Returns a [TState] that is incrementally updated when this [TFlow] emits a value, by applying - * [transform] to both the emitted value and the currently tracked state. - * - * Note that the value contained within the [TState] is not updated until *after* all [TFlow]s - * have been processed; this keeps the value of the [TState] consistent during the entire FRP - * transaction. - */ - @ExperimentalFrpApi - fun <A, B> TFlow<A>.foldDeferred( - initialValue: FrpDeferredValue<B>, - transform: FrpTransactionScope.(A, B) -> B, - ): TState<B> { - lateinit var state: TState<B> - return mapPure { a -> transform(a, state.sample()) } - .holdDeferred(initialValue) - .also { state = it } - } - - /** - * Returns a [TState] that holds onto the result of applying the most recently emitted - * [FrpStateful] this [TFlow], or [init] if nothing has been emitted since it was constructed. - * - * When each [FrpStateful] is applied, state accumulation from the previously-active - * [FrpStateful] is stopped. - * - * Note that the value contained within the [TState] is not updated until *after* all [TFlow]s - * have been processed; this keeps the value of the [TState] consistent during the entire FRP - * transaction. - * - * Shorthand for: - * ```kotlin - * val (changes, initApplied) = applyLatestStateful(init) - * return changes.toTStateDeferred(initApplied) - * ``` - */ - @ExperimentalFrpApi - fun <A> TFlow<FrpStateful<A>>.holdLatestStateful(init: FrpStateful<A>): TState<A> { - val (changes, initApplied) = applyLatestStateful(init) - return changes.holdDeferred(initApplied) - } - - /** - * Returns a [TFlow] that emits the two most recent emissions from the original [TFlow]. - * [initialValue] is used as the previous value for the first emission. - * - * Shorthand for `sample(hold(init)) { new, old -> Pair(old, new) }` - */ - @ExperimentalFrpApi - fun <S, T : S> TFlow<T>.pairwise(initialValue: S): TFlow<WithPrev<S, T>> { - val previous = hold(initialValue) - return mapCheap { new -> WithPrev(previousValue = previous.sample(), newValue = new) } - } - - /** - * Returns a [TFlow] that emits the two most recent emissions from the original [TFlow]. Note - * that the returned [TFlow] will not emit until the original [TFlow] has emitted twice. - */ - @ExperimentalFrpApi - fun <A> TFlow<A>.pairwise(): TFlow<WithPrev<A, A>> = - mapCheap { just(it) } - .pairwise(none) - .mapMaybe { (prev, next) -> prev.zipWith(next, ::WithPrev) } - - /** - * Returns a [TState] that holds both the current and previous values of the original [TState]. - * [initialPreviousValue] is used as the first previous value. - * - * Shorthand for `sample(hold(init)) { new, old -> Pair(old, new) }` - */ - @ExperimentalFrpApi - fun <S, T : S> TState<T>.pairwise(initialPreviousValue: S): TState<WithPrev<S, T>> = - stateChanges - .pairwise(initialPreviousValue) - .holdDeferred(deferredTransactionScope { WithPrev(initialPreviousValue, sample()) }) - - /** - * Returns a [TState] holding a [Map] that is updated incrementally whenever this emits a value. - * - * The value emitted is used as a "patch" for the tracked [Map]; for each key [K] in the emitted - * map, an associated value of [Just] will insert or replace the value in the tracked [Map], and - * an associated value of [none] will remove the key from the tracked [Map]. - */ - @ExperimentalFrpApi - fun <K, V> TFlow<Map<K, Maybe<V>>>.foldMapIncrementally( - initialValues: FrpDeferredValue<Map<K, V>> - ): TState<Map<K, V>> = - foldDeferred(initialValues) { patch, map -> - val (adds: List<Pair<K, V>>, removes: List<K>) = - patch - .asSequence() - .map { (k, v) -> if (v is Just) Left(k to v.value) else Right(k) } - .partitionEithers() - val removed: Map<K, V> = map - removes.toSet() - val updated: Map<K, V> = removed + adds - updated - } - - /** - * Returns a [TState] holding a [Map] that is updated incrementally whenever this emits a value. - * - * The value emitted is used as a "patch" for the tracked [Map]; for each key [K] in the emitted - * map, an associated value of [Just] will insert or replace the value in the tracked [Map], and - * an associated value of [none] will remove the key from the tracked [Map]. - */ - @ExperimentalFrpApi - fun <K, V> TFlow<Map<K, Maybe<V>>>.foldMapIncrementally( - initialValues: Map<K, V> = emptyMap() - ): TState<Map<K, V>> = foldMapIncrementally(deferredOf(initialValues)) - - /** - * Returns a [TFlow] that wraps each emission of the original [TFlow] into an [IndexedValue], - * containing the emitted value and its index (starting from zero). - * - * Shorthand for: - * ``` - * val index = fold(0) { _, oldIdx -> oldIdx + 1 } - * sample(index) { a, idx -> IndexedValue(idx, a) } - * ``` - */ - @ExperimentalFrpApi - fun <A> TFlow<A>.withIndex(): TFlow<IndexedValue<A>> { - val index = fold(0) { _, old -> old + 1 } - return sample(index) { a, idx -> IndexedValue(idx, a) } - } - - /** - * Returns a [TFlow] containing the results of applying [transform] to each value of the - * original [TFlow] and its index (starting from zero). - * - * Shorthand for: - * ``` - * withIndex().map { (idx, a) -> transform(idx, a) } - * ``` - */ - @ExperimentalFrpApi - fun <A, B> TFlow<A>.mapIndexed(transform: FrpTransactionScope.(Int, A) -> B): TFlow<B> { - val index = fold(0) { _, i -> i + 1 } - return sample(index) { a, idx -> transform(idx, a) } - } - - /** Returns a [TFlow] where all subsequent repetitions of the same value are filtered out. */ - @ExperimentalFrpApi - fun <A> TFlow<A>.distinctUntilChanged(): TFlow<A> { - val state: TState<Any?> = hold(Any()) - return filter { it != state.sample() } - } - - /** - * Returns a new [TFlow] that emits at the same rate as the original [TFlow], but combines the - * emitted value with the most recent emission from [other] using [transform]. - * - * Note that the returned [TFlow] will not emit anything until [other] has emitted at least one - * value. - */ - @ExperimentalFrpApi - fun <A, B, C> TFlow<A>.sample( - other: TFlow<B>, - transform: FrpTransactionScope.(A, B) -> C, - ): TFlow<C> { - val state = other.mapCheap { just(it) }.hold(none) - return sample(state) { a, b -> b.map { transform(a, it) } }.filterJust() - } - - /** - * Returns a [TState] that samples the [Transactional] held by the given [TState] within the - * same transaction that the state changes. - */ - @ExperimentalFrpApi - fun <A> TState<Transactional<A>>.sampleTransactionals(): TState<A> = - stateChanges - .sampleTransactionals() - .holdDeferred(deferredTransactionScope { sample().sample() }) - - /** - * Returns a [TState] that transforms the value held inside this [TState] by applying it to the - * given function [transform]. - */ - @ExperimentalFrpApi - fun <A, B> TState<A>.map(transform: FrpTransactionScope.(A) -> B): TState<B> = - mapPure { transactionally { transform(it) } }.sampleTransactionals() - - /** - * Returns a [TState] whose value is generated with [transform] by combining the current values - * of each given [TState]. - * - * @see TState.combineWith - */ - @ExperimentalFrpApi - fun <A, B, Z> combine( - stateA: TState<A>, - stateB: TState<B>, - transform: FrpTransactionScope.(A, B) -> Z, - ): TState<Z> = - com.android.systemui.kairos - .combine(stateA, stateB) { a, b -> transactionally { transform(a, b) } } - .sampleTransactionals() - - /** - * Returns a [TState] whose value is generated with [transform] by combining the current values - * of each given [TState]. - * - * @see TState.combineWithTransactionally - */ - @ExperimentalFrpApi - fun <A, B, C, Z> combine( - stateA: TState<A>, - stateB: TState<B>, - stateC: TState<C>, - transform: FrpTransactionScope.(A, B, C) -> Z, - ): TState<Z> = - com.android.systemui.kairos - .combine(stateA, stateB, stateC) { a, b, c -> transactionally { transform(a, b, c) } } - .sampleTransactionals() - - /** - * Returns a [TState] whose value is generated with [transform] by combining the current values - * of each given [TState]. - * - * @see TState.combineWith - */ - @ExperimentalFrpApi - fun <A, B, C, D, Z> combine( - stateA: TState<A>, - stateB: TState<B>, - stateC: TState<C>, - stateD: TState<D>, - transform: FrpTransactionScope.(A, B, C, D) -> Z, - ): TState<Z> = - com.android.systemui.kairos - .combine(stateA, stateB, stateC, stateD) { a, b, c, d -> - transactionally { transform(a, b, c, d) } - } - .sampleTransactionals() - - /** Returns a [TState] by applying [transform] to the value held by the original [TState]. */ - @ExperimentalFrpApi - fun <A, B> TState<A>.flatMap(transform: FrpTransactionScope.(A) -> TState<B>): TState<B> = - mapPure { transactionally { transform(it) } }.sampleTransactionals().flatten() - - /** - * Returns a [TState] whose value is generated with [transform] by combining the current values - * of each given [TState]. - * - * @see TState.combineWith - */ - @ExperimentalFrpApi - fun <A, Z> combine( - vararg states: TState<A>, - transform: FrpTransactionScope.(List<A>) -> Z, - ): TState<Z> = combinePure(*states).map(transform) - - /** - * Returns a [TState] whose value is generated with [transform] by combining the current values - * of each given [TState]. - * - * @see TState.combineWith - */ - @ExperimentalFrpApi - fun <A, Z> Iterable<TState<A>>.combine( - transform: FrpTransactionScope.(List<A>) -> Z - ): TState<Z> = combinePure().map(transform) - - /** - * Returns a [TState] by combining the values held inside the given [TState]s by applying them - * to the given function [transform]. - */ - @ExperimentalFrpApi - fun <A, B, C> TState<A>.combineWith( - other: TState<B>, - transform: FrpTransactionScope.(A, B) -> C, - ): TState<C> = combine(this, other, transform) -} diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpTransactionScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpTransactionScope.kt deleted file mode 100644 index 7d48b9853e1c..000000000000 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/FrpTransactionScope.kt +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.kairos - -import kotlin.coroutines.RestrictsSuspension - -/** - * FRP operations that are available while a transaction is active. - * - * These operations do not accumulate state, which makes [FrpTransactionScope] weaker than - * [FrpStateScope], but allows them to be used in more places. - */ -@ExperimentalFrpApi -@RestrictsSuspension -interface FrpTransactionScope : FrpScope { - - /** - * Returns the current value of this [Transactional] as a [FrpDeferredValue]. - * - * @see sample - */ - @ExperimentalFrpApi fun <A> Transactional<A>.sampleDeferred(): FrpDeferredValue<A> - - /** - * Returns the current value of this [TState] as a [FrpDeferredValue]. - * - * @see sample - */ - @ExperimentalFrpApi fun <A> TState<A>.sampleDeferred(): FrpDeferredValue<A> - - /** TODO */ - @ExperimentalFrpApi - fun <A> deferredTransactionScope(block: FrpTransactionScope.() -> A): FrpDeferredValue<A> - - /** A [TFlow] that emits once, within this transaction, and then never again. */ - @ExperimentalFrpApi val now: TFlow<Unit> - - /** - * Returns the current value held by this [TState]. Guaranteed to be consistent within the same - * transaction. - */ - @ExperimentalFrpApi fun <A> TState<A>.sample(): A = sampleDeferred().get() - - /** - * Returns the current value held by this [Transactional]. Guaranteed to be consistent within - * the same transaction. - */ - @ExperimentalFrpApi fun <A> Transactional<A>.sample(): A = sampleDeferred().get() -} diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosNetwork.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosNetwork.kt new file mode 100644 index 000000000000..77598b30658a --- /dev/null +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosNetwork.kt @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.kairos + +import com.android.systemui.kairos.internal.BuildScopeImpl +import com.android.systemui.kairos.internal.Network +import com.android.systemui.kairos.internal.StateScopeImpl +import com.android.systemui.kairos.internal.util.awaitCancellationAndThen +import com.android.systemui.kairos.internal.util.childScope +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.job +import kotlinx.coroutines.launch + +/** + * Marks declarations that are still **experimental** and shouldn't be used in general production + * code. + */ +@RequiresOptIn( + message = "This API is experimental and should not be used in general production code." +) +@Retention(AnnotationRetention.BINARY) +annotation class ExperimentalKairosApi + +/** + * External interface to a Kairos network of reactive components. Can be used to make transactional + * queries and modifications to the network. + */ +@ExperimentalKairosApi +interface KairosNetwork { + /** + * Runs [block] inside of a transaction, suspending until the transaction is complete. + * + * The [BuildScope] receiver exposes methods that can be used to query or modify the network. If + * the network is cancelled while the caller of [transact] is suspended, then the call will be + * cancelled. + */ + suspend fun <R> transact(block: TransactionScope.() -> R): R + + /** + * Activates [spec] in a transaction, suspending indefinitely. While suspended, all observers + * and long-running effects are kept alive. When cancelled, observers are unregistered and + * effects are cancelled. + */ + suspend fun activateSpec(spec: BuildSpec<*>) + + /** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */ + fun <In, Out> coalescingMutableEvents( + coalesce: (old: Out, new: In) -> Out, + getInitialValue: () -> Out, + ): CoalescingMutableEvents<In, Out> + + /** Returns a [MutableState] that can emit values into this [KairosNetwork]. */ + fun <T> mutableEvents(): MutableEvents<T> + + /** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */ + fun <T> conflatedMutableEvents(): CoalescingMutableEvents<T, T> + + /** Returns a [MutableState]. with initial state [initialValue]. */ + fun <T> mutableStateDeferred(initialValue: DeferredValue<T>): MutableState<T> +} + +/** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */ +@ExperimentalKairosApi +fun <In, Out> KairosNetwork.coalescingMutableEvents( + coalesce: (old: Out, new: In) -> Out, + initialValue: Out, +): CoalescingMutableEvents<In, Out> = + coalescingMutableEvents(coalesce, getInitialValue = { initialValue }) + +/** Returns a [MutableState] with initial state [initialValue]. */ +@ExperimentalKairosApi +fun <T> KairosNetwork.mutableState(initialValue: T): MutableState<T> = + mutableStateDeferred(deferredOf(initialValue)) + +/** Returns a [MutableState] with initial state [initialValue]. */ +@ExperimentalKairosApi +fun <T> MutableState(network: KairosNetwork, initialValue: T): MutableState<T> = + network.mutableState(initialValue) + +/** Returns a [MutableEvents] that can emit values into this [KairosNetwork]. */ +@ExperimentalKairosApi +fun <T> MutableEvents(network: KairosNetwork): MutableEvents<T> = network.mutableEvents() + +/** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */ +@ExperimentalKairosApi +fun <In, Out> CoalescingMutableEvents( + network: KairosNetwork, + coalesce: (old: Out, new: In) -> Out, + initialValue: Out, +): CoalescingMutableEvents<In, Out> = network.coalescingMutableEvents(coalesce) { initialValue } + +/** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */ +@ExperimentalKairosApi +fun <In, Out> CoalescingMutableEvents( + network: KairosNetwork, + coalesce: (old: Out, new: In) -> Out, + getInitialValue: () -> Out, +): CoalescingMutableEvents<In, Out> = network.coalescingMutableEvents(coalesce, getInitialValue) + +/** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */ +@ExperimentalKairosApi +fun <T> ConflatedMutableEvents(network: KairosNetwork): CoalescingMutableEvents<T, T> = + network.conflatedMutableEvents() + +/** + * Activates [spec] in a transaction and invokes [block] with the result, suspending indefinitely. + * While suspended, all observers and long-running effects are kept alive. When cancelled, observers + * are unregistered and effects are cancelled. + */ +@ExperimentalKairosApi +suspend fun <R> KairosNetwork.activateSpec(spec: BuildSpec<R>, block: suspend (R) -> Unit) { + activateSpec { + val result = spec.applySpec() + launchEffect { block(result) } + } +} + +internal class LocalNetwork( + private val network: Network, + private val scope: CoroutineScope, + private val endSignal: Events<Any>, +) : KairosNetwork { + override suspend fun <R> transact(block: TransactionScope.() -> R): R = + network.transaction("KairosNetwork.transact") { block() }.await() + + override suspend fun activateSpec(spec: BuildSpec<*>) { + val stopEmitter = + CoalescingMutableEvents( + name = "activateSpec", + coalesce = { _, _: Unit -> }, + network = network, + getInitialValue = {}, + ) + val job = + network + .transaction("KairosNetwork.activateSpec") { + val buildScope = + BuildScopeImpl( + stateScope = + StateScopeImpl( + evalScope = this, + endSignal = mergeLeft(stopEmitter, endSignal), + ), + coroutineScope = scope, + ) + buildScope.launchScope(spec) + } + .await() + awaitCancellationAndThen { + stopEmitter.emit(Unit) + job.cancel() + } + } + + override fun <In, Out> coalescingMutableEvents( + coalesce: (old: Out, new: In) -> Out, + getInitialValue: () -> Out, + ): CoalescingMutableEvents<In, Out> = + CoalescingMutableEvents( + null, + coalesce = { old, new -> coalesce(old.value, new) }, + network, + getInitialValue, + ) + + override fun <T> conflatedMutableEvents(): CoalescingMutableEvents<T, T> = + CoalescingMutableEvents( + null, + coalesce = { _, new -> new }, + network, + { error("WTF: init value accessed for conflatedMutableEvents") }, + ) + + override fun <T> mutableEvents(): MutableEvents<T> = MutableEvents(network) + + override fun <T> mutableStateDeferred(initialValue: DeferredValue<T>): MutableState<T> = + MutableState(network, initialValue.unwrapped) +} + +/** + * Combination of an [KairosNetwork] and a [Job] that, when cancelled, will cancel the entire Kairos + * network. + */ +@ExperimentalKairosApi +class RootKairosNetwork +internal constructor(private val network: Network, private val scope: CoroutineScope, job: Job) : + Job by job, KairosNetwork by LocalNetwork(network, scope, emptyEvents) + +/** Constructs a new [RootKairosNetwork] in the given [CoroutineScope]. */ +@ExperimentalKairosApi +fun CoroutineScope.launchKairosNetwork( + context: CoroutineContext = EmptyCoroutineContext +): RootKairosNetwork { + val scope = childScope(context) + val network = Network(scope) + scope.launch(CoroutineName("launchKairosNetwork scheduler")) { network.runInputScheduler() } + return RootKairosNetwork(network, scope, scope.coroutineContext.job) +} diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosScope.kt new file mode 100644 index 000000000000..ce3e9235efa8 --- /dev/null +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/KairosScope.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.kairos + +import com.android.systemui.kairos.internal.CompletableLazy + +/** Denotes [KairosScope] interfaces as [DSL markers][DslMarker]. */ +@DslMarker annotation class KairosScopeMarker + +/** + * Base scope for all Kairos scopes. Used to prevent implicitly capturing other scopes from in + * lambdas. + */ +@KairosScopeMarker +@ExperimentalKairosApi +interface KairosScope { + /** Returns the value held by the [DeferredValue], suspending until available if necessary. */ + fun <A> DeferredValue<A>.get(): A = unwrapped.value +} + +/** + * A value that may not be immediately (synchronously) available, but is guaranteed to be available + * before this transaction is completed. + * + * @see KairosScope.get + */ +@ExperimentalKairosApi +class DeferredValue<out A> internal constructor(internal val unwrapped: Lazy<A>) + +/** + * Returns the value held by this [DeferredValue], or throws [IllegalStateException] if it is not + * yet available. + * + * This API is not meant for general usage within the Kairos network. It is made available mainly + * for debugging and logging. You should always prefer [get][KairosScope.get] if possible. + * + * @see KairosScope.get + */ +@ExperimentalKairosApi fun <A> DeferredValue<A>.getUnsafe(): A = unwrapped.value + +/** Returns an already-available [DeferredValue] containing [value]. */ +@ExperimentalKairosApi +fun <A> deferredOf(value: A): DeferredValue<A> = DeferredValue(CompletableLazy(value)) diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/State.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/State.kt new file mode 100644 index 000000000000..08b27c86c9b9 --- /dev/null +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/State.kt @@ -0,0 +1,528 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.kairos + +import com.android.systemui.kairos.internal.CompletableLazy +import com.android.systemui.kairos.internal.DerivedMapCheap +import com.android.systemui.kairos.internal.EventsImpl +import com.android.systemui.kairos.internal.Init +import com.android.systemui.kairos.internal.InitScope +import com.android.systemui.kairos.internal.Network +import com.android.systemui.kairos.internal.NoScope +import com.android.systemui.kairos.internal.Schedulable +import com.android.systemui.kairos.internal.StateImpl +import com.android.systemui.kairos.internal.StateSource +import com.android.systemui.kairos.internal.activated +import com.android.systemui.kairos.internal.cached +import com.android.systemui.kairos.internal.constInit +import com.android.systemui.kairos.internal.constState +import com.android.systemui.kairos.internal.filterImpl +import com.android.systemui.kairos.internal.flatMap +import com.android.systemui.kairos.internal.init +import com.android.systemui.kairos.internal.map +import com.android.systemui.kairos.internal.mapCheap +import com.android.systemui.kairos.internal.mapImpl +import com.android.systemui.kairos.internal.util.hashString +import com.android.systemui.kairos.internal.zipStateMap +import com.android.systemui.kairos.internal.zipStates +import kotlin.reflect.KProperty + +/** + * A time-varying value with discrete changes. Essentially, a combination of a [Transactional] that + * holds a value, and an [Events] that emits when the value changes. + */ +@ExperimentalKairosApi sealed class State<out A> + +/** A [State] that never changes. */ +@ExperimentalKairosApi +fun <A> stateOf(value: A): State<A> { + val operatorName = "stateOf" + val name = "$operatorName($value)" + return StateInit(constInit(name, constState(name, operatorName, value))) +} + +/** + * Returns a [State] that acts as a deferred-reference to the [State] produced by this [Lazy]. + * + * When the returned [State] is accessed by the Kairos network, the [Lazy]'s [value][Lazy.value] + * will be queried and used. + * + * Useful for recursive definitions. + */ +@ExperimentalKairosApi fun <A> Lazy<State<A>>.defer(): State<A> = deferInline { value } + +/** + * Returns a [State] that acts as a deferred-reference to the [State] produced by this + * [DeferredValue]. + * + * When the returned [State] is accessed by the Kairos network, the [DeferredValue] will be queried + * and used. + * + * Useful for recursive definitions. + */ +@ExperimentalKairosApi +fun <A> DeferredValue<State<A>>.defer(): State<A> = deferInline { unwrapped.value } + +/** + * Returns a [State] that acts as a deferred-reference to the [State] produced by [block]. + * + * When the returned [State] is accessed by the Kairos network, [block] will be invoked and the + * returned [State] will be used. + * + * Useful for recursive definitions. + */ +@ExperimentalKairosApi +fun <A> deferredState(block: KairosScope.() -> State<A>): State<A> = deferInline { NoScope.block() } + +/** + * Returns a [State] containing the results of applying [transform] to the value held by the + * original [State]. + */ +@ExperimentalKairosApi +fun <A, B> State<A>.map(transform: KairosScope.(A) -> B): State<B> { + val operatorName = "map" + val name = operatorName + return StateInit( + init(name) { + init.connect(evalScope = this).map(name, operatorName) { NoScope.transform(it) } + } + ) +} + +/** + * Returns a [State] that transforms the value held inside this [State] by applying it to the + * [transform]. + * + * Note that unlike [map], the result is not cached. This means that not only should [transform] be + * fast and pure, it should be *monomorphic* (1-to-1). Failure to do this means that [changes] for + * the returned [State] will operate unexpectedly, emitting at rates that do not reflect an + * observable change to the returned [State]. + */ +@ExperimentalKairosApi +fun <A, B> State<A>.mapCheapUnsafe(transform: KairosScope.(A) -> B): State<B> { + val operatorName = "map" + val name = operatorName + return StateInit( + init(name) { + init.connect(evalScope = this).mapCheap(name, operatorName) { NoScope.transform(it) } + } + ) +} + +/** + * Returns a [State] by combining the values held inside the given [State]s by applying them to the + * given function [transform]. + */ +@ExperimentalKairosApi +fun <A, B, C> State<A>.combineWith(other: State<B>, transform: KairosScope.(A, B) -> C): State<C> = + combine(this, other, transform) + +/** + * Splits a [State] of pairs into a pair of [Events][State], where each returned [State] holds half + * of the original. + * + * Shorthand for: + * ```kotlin + * val lefts = map { it.first } + * val rights = map { it.second } + * return Pair(lefts, rights) + * ``` + */ +@ExperimentalKairosApi +fun <A, B> State<Pair<A, B>>.unzip(): Pair<State<A>, State<B>> { + val left = map { it.first } + val right = map { it.second } + return left to right +} + +/** + * Returns a [State] by combining the values held inside the given [States][State] into a [List]. + * + * @see State.combineWith + */ +@ExperimentalKairosApi +fun <A> Iterable<State<A>>.combine(): State<List<A>> { + val operatorName = "combine" + val name = operatorName + return StateInit( + init(name) { + zipStates(name, operatorName, states = map { it.init.connect(evalScope = this) }) + } + ) +} + +/** + * Returns a [State] by combining the values held inside the given [States][State] into a [Map]. + * + * @see State.combineWith + */ +@ExperimentalKairosApi +fun <K, A> Map<K, State<A>>.combine(): State<Map<K, A>> { + val operatorName = "combine" + val name = operatorName + return StateInit( + init(name) { + zipStateMap( + name, + operatorName, + states = mapValues { it.value.init.connect(evalScope = this) }, + ) + } + ) +} + +/** + * Returns a [State] whose value is generated with [transform] by combining the current values of + * each given [State]. + * + * @see State.combineWith + */ +@ExperimentalKairosApi +fun <A, B> Iterable<State<A>>.combine(transform: KairosScope.(List<A>) -> B): State<B> = + combine().map(transform) + +/** + * Returns a [State] by combining the values held inside the given [State]s into a [List]. + * + * @see State.combineWith + */ +@ExperimentalKairosApi +fun <A> combine(vararg states: State<A>): State<List<A>> = states.asIterable().combine() + +/** + * Returns a [State] whose value is generated with [transform] by combining the current values of + * each given [State]. + * + * @see State.combineWith + */ +@ExperimentalKairosApi +fun <A, B> combine(vararg states: State<A>, transform: KairosScope.(List<A>) -> B): State<B> = + states.asIterable().combine(transform) + +/** + * Returns a [State] whose value is generated with [transform] by combining the current values of + * each given [State]. + * + * @see State.combineWith + */ +@ExperimentalKairosApi +fun <A, B, Z> combine( + stateA: State<A>, + stateB: State<B>, + transform: KairosScope.(A, B) -> Z, +): State<Z> { + val operatorName = "combine" + val name = operatorName + return StateInit( + init(name) { + val dl1 = stateA.init.connect(evalScope = this@init) + val dl2 = stateB.init.connect(evalScope = this@init) + zipStates(name, operatorName, dl1, dl2) { a, b -> NoScope.transform(a, b) } + } + ) +} + +/** + * Returns a [State] whose value is generated with [transform] by combining the current values of + * each given [State]. + * + * @see State.combineWith + */ +@ExperimentalKairosApi +fun <A, B, C, Z> combine( + stateA: State<A>, + stateB: State<B>, + stateC: State<C>, + transform: KairosScope.(A, B, C) -> Z, +): State<Z> { + val operatorName = "combine" + val name = operatorName + return StateInit( + init(name) { + val dl1 = stateA.init.connect(evalScope = this@init) + val dl2 = stateB.init.connect(evalScope = this@init) + val dl3 = stateC.init.connect(evalScope = this@init) + zipStates(name, operatorName, dl1, dl2, dl3) { a, b, c -> NoScope.transform(a, b, c) } + } + ) +} + +/** + * Returns a [State] whose value is generated with [transform] by combining the current values of + * each given [State]. + * + * @see State.combineWith + */ +@ExperimentalKairosApi +fun <A, B, C, D, Z> combine( + stateA: State<A>, + stateB: State<B>, + stateC: State<C>, + stateD: State<D>, + transform: KairosScope.(A, B, C, D) -> Z, +): State<Z> { + val operatorName = "combine" + val name = operatorName + return StateInit( + init(name) { + val dl1 = stateA.init.connect(evalScope = this@init) + val dl2 = stateB.init.connect(evalScope = this@init) + val dl3 = stateC.init.connect(evalScope = this@init) + val dl4 = stateD.init.connect(evalScope = this@init) + zipStates(name, operatorName, dl1, dl2, dl3, dl4) { a, b, c, d -> + NoScope.transform(a, b, c, d) + } + } + ) +} + +/** + * Returns a [State] whose value is generated with [transform] by combining the current values of + * each given [State]. + * + * @see State.combineWith + */ +@ExperimentalKairosApi +fun <A, B, C, D, E, Z> combine( + stateA: State<A>, + stateB: State<B>, + stateC: State<C>, + stateD: State<D>, + stateE: State<E>, + transform: KairosScope.(A, B, C, D, E) -> Z, +): State<Z> { + val operatorName = "combine" + val name = operatorName + return StateInit( + init(name) { + val dl1 = stateA.init.connect(evalScope = this@init) + val dl2 = stateB.init.connect(evalScope = this@init) + val dl3 = stateC.init.connect(evalScope = this@init) + val dl4 = stateD.init.connect(evalScope = this@init) + val dl5 = stateE.init.connect(evalScope = this@init) + zipStates(name, operatorName, dl1, dl2, dl3, dl4, dl5) { a, b, c, d, e -> + NoScope.transform(a, b, c, d, e) + } + } + ) +} + +/** Returns a [State] by applying [transform] to the value held by the original [State]. */ +@ExperimentalKairosApi +fun <A, B> State<A>.flatMap(transform: KairosScope.(A) -> State<B>): State<B> { + val operatorName = "flatMap" + val name = operatorName + return StateInit( + init(name) { + init.connect(this).flatMap(name, operatorName) { a -> + NoScope.transform(a).init.connect(this) + } + } + ) +} + +/** Shorthand for `flatMap { it }` */ +@ExperimentalKairosApi fun <A> State<State<A>>.flatten() = flatMap { it } + +/** + * Returns a [StateSelector] that can be used to efficiently check if the input [State] is currently + * holding a specific value. + * + * An example: + * ``` + * val intState: State<Int> = ... + * val intSelector: StateSelector<Int> = intState.selector() + * // Tracks if lInt is holding 1 + * val isOne: State<Boolean> = intSelector.whenSelected(1) + * ``` + * + * This is semantically equivalent to `val isOne = intState.map { i -> i == 1 }`, but is + * significantly more efficient; specifically, using [State.map] in this way incurs a `O(n)` + * performance hit, where `n` is the number of different [State.map] operations used to track a + * specific value. [selector] internally uses a [HashMap] to lookup the appropriate downstream + * [State] to update, and so operates in `O(1)`. + * + * Note that the returned [StateSelector] should be cached and re-used to gain the performance + * benefit. + * + * @see groupByKey + */ +@ExperimentalKairosApi +fun <A> State<A>.selector(numDistinctValues: Int? = null): StateSelector<A> = + StateSelector( + this, + changes + .map { new -> mapOf(new to true, sampleDeferred().get() to false) } + .groupByKey(numDistinctValues), + ) + +/** + * Tracks the currently selected value of type [A] from an upstream [State]. + * + * @see selector + */ +@ExperimentalKairosApi +class StateSelector<in A> +internal constructor( + private val upstream: State<A>, + private val groupedChanges: GroupedEvents<A, Boolean>, +) { + /** + * Returns a [State] that tracks whether the upstream [State] is currently holding the given + * [value]. + * + * @see selector + */ + fun whenSelected(value: A): State<Boolean> { + val operatorName = "StateSelector#whenSelected" + val name = "$operatorName[$value]" + return StateInit( + init(name) { + DerivedMapCheap( + name, + operatorName, + upstream = upstream.init.connect(evalScope = this), + changes = groupedChanges.impl.eventsForKey(value), + ) { + it == value + } + } + ) + } + + operator fun get(value: A): State<Boolean> = whenSelected(value) +} + +/** + * A mutable [State] that provides the ability to manually [set its value][setValue]. + * + * Multiple invocations of [setValue] that occur before a transaction are conflated; only the most + * recent value is used. + * + * Effectively equivalent to: + * ``` kotlin + * ConflatedMutableEvents(kairosNetwork).holdState(initialValue) + * ``` + */ +@ExperimentalKairosApi +class MutableState<T> internal constructor(internal val network: Network, initialValue: Lazy<T>) : + State<T>() { + + private val input: CoalescingMutableEvents<Lazy<T>, Lazy<T>?> = + CoalescingMutableEvents( + name = null, + coalesce = { _, new -> new }, + network = network, + getInitialValue = { null }, + ) + + internal val state = run { + val changes = input.impl + val name = null + val operatorName = "MutableState" + lateinit var state: StateSource<T> + val mapImpl = mapImpl(upstream = { changes.activated() }) { it, _ -> it!!.value } + val calm: EventsImpl<T> = + filterImpl({ mapImpl }) { new -> + new != state.getCurrentWithEpoch(evalScope = this).first + } + .cached() + state = StateSource(name, operatorName, initialValue, calm) + @Suppress("DeferredResultUnused") + network.transaction("MutableState.init") { + calm.activate(evalScope = this, downstream = Schedulable.S(state))?.let { + (connection, needsEval) -> + state.upstreamConnection = connection + if (needsEval) { + schedule(state) + } + } + } + StateInit(constInit(name, state)) + } + + /** + * Sets the value held by this [State]. + * + * Invoking will cause a [state change event][State.changes] to emit with the new value, which + * will then be applied (and thus returned by [TransactionScope.sample]) after the transaction + * is complete. + * + * Multiple invocations of [setValue] that occur before a transaction are conflated; only the + * most recent value is used. + */ + fun setValue(value: T) = input.emit(CompletableLazy(value)) + + /** + * Sets the value held by this [State]. The [DeferredValue] will not be queried until this + * [State] is explicitly [sampled][TransactionScope.sample] or [observed][BuildScope.observe]. + * + * Invoking will cause a [state change event][State.changes] to emit with the new value, which + * will then be applied (and thus returned by [TransactionScope.sample]) after the transaction + * is complete. + * + * Multiple invocations of [setValue] that occur before a transaction are conflated; only the + * most recent value is used. + */ + fun setValueDeferred(value: DeferredValue<T>) = input.emit(value.unwrapped) +} + +/** A forward-reference to a [State], allowing for recursive definitions. */ +@ExperimentalKairosApi +class StateLoop<A> : State<A>() { + + private val name: String? = null + + private val deferred = CompletableLazy<State<A>>() + + internal val init: Init<StateImpl<A>> = + init(name) { deferred.value.init.connect(evalScope = this) } + + /** The [State] this [StateLoop] will forward to. */ + var loopback: State<A>? = null + set(value) { + value?.let { + check(!deferred.isInitialized()) { "StateLoop.loopback has already been set." } + deferred.setValue(value) + field = value + } + } + + operator fun getValue(thisRef: Any?, property: KProperty<*>): State<A> = this + + operator fun setValue(thisRef: Any?, property: KProperty<*>, value: State<A>) { + loopback = value + } + + override fun toString(): String = "${this::class.simpleName}@$hashString" +} + +internal class StateInit<A> internal constructor(internal val init: Init<StateImpl<A>>) : + State<A>() { + override fun toString(): String = "${this::class.simpleName}@$hashString" +} + +internal val <A> State<A>.init: Init<StateImpl<A>> + get() = + when (this) { + is StateInit -> init + is StateLoop -> init + is MutableState -> state.init + } + +private inline fun <A> deferInline(crossinline block: InitScope.() -> State<A>): State<A> = + StateInit(init(name = null) { block().init.connect(evalScope = this) }) diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/StateScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/StateScope.kt new file mode 100644 index 000000000000..b1f48bb1ce56 --- /dev/null +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/StateScope.kt @@ -0,0 +1,760 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.kairos + +import com.android.systemui.kairos.util.Just +import com.android.systemui.kairos.util.Left +import com.android.systemui.kairos.util.Maybe +import com.android.systemui.kairos.util.Right +import com.android.systemui.kairos.util.WithPrev +import com.android.systemui.kairos.util.just +import com.android.systemui.kairos.util.map +import com.android.systemui.kairos.util.none +import com.android.systemui.kairos.util.partitionEithers +import com.android.systemui.kairos.util.zipWith + +// TODO: caching story? should each Scope have a cache of applied Stateful instances? +/** A computation that can accumulate [Events] into [State]. */ +typealias Stateful<R> = StateScope.() -> R + +/** + * Returns a [Stateful] that, when [applied][StateScope.applyStateful], invokes [block] with the + * applier's [StateScope]. + */ +@ExperimentalKairosApi +@Suppress("NOTHING_TO_INLINE") +inline fun <A> statefully(noinline block: StateScope.() -> A): Stateful<A> = block + +/** + * Operations that accumulate state within the Kairos network. + * + * State accumulation is an ongoing process that has a lifetime. Use `-Latest` combinators, such as + * [mapLatestStateful], to create smaller, nested lifecycles so that accumulation isn't running + * longer than needed. + */ +@ExperimentalKairosApi +interface StateScope : TransactionScope { + + /** + * Defers invoking [block] until after the current [StateScope] code-path completes, returning a + * [DeferredValue] that can be used to reference the result. + * + * Useful for recursive definitions. + * + * @see DeferredValue + */ + fun <A> deferredStateScope(block: StateScope.() -> A): DeferredValue<A> + + /** + * Returns a [State] that holds onto the most recently emitted value from this [Events], or + * [initialValue] if nothing has been emitted since it was constructed. + * + * Note that the value contained within the [State] is not updated until *after* all [Events] + * have been processed; this keeps the value of the [State] consistent during the entire Kairos + * transaction. + */ + fun <A> Events<A>.holdStateDeferred(initialValue: DeferredValue<A>): State<A> + + /** + * Returns an [Events] that emits from a merged, incrementally-accumulated collection of + * [Events] emitted from this, following the same "patch" rules as outlined in + * [foldStateMapIncrementally]. + * + * Conceptually this is equivalent to: + * ```kotlin + * fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally( + * initialEvents: Map<K, Events<V>>, + * ): Events<Map<K, V>> = + * foldMapIncrementally(initialEvents).map { it.merge() }.switchEvents() + * ``` + * + * While the behavior is equivalent to the conceptual definition above, the implementation is + * significantly more efficient. + * + * @see merge + */ + fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally( + name: String? = null, + initialEvents: DeferredValue<Map<K, Events<V>>>, + ): Events<Map<K, V>> + + /** + * Returns an [Events] that emits from a merged, incrementally-accumulated collection of + * [Events] emitted from this, following the same "patch" rules as outlined in + * [foldStateMapIncrementally]. + * + * Conceptually this is equivalent to: + * ```kotlin + * fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementallyPromptly( + * initialEvents: Map<K, Events<V>>, + * ): Events<Map<K, V>> = + * foldMapIncrementally(initialEvents).map { it.merge() }.switchEventsPromptly() + * ``` + * + * While the behavior is equivalent to the conceptual definition above, the implementation is + * significantly more efficient. + * + * @see merge + */ + fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementallyPromptly( + initialEvents: DeferredValue<Map<K, Events<V>>>, + name: String? = null, + ): Events<Map<K, V>> + + // TODO: everything below this comment can be made into extensions once we have context params + + /** + * Returns an [Events] that emits from a merged, incrementally-accumulated collection of + * [Events] emitted from this, following the same "patch" rules as outlined in + * [foldStateMapIncrementally]. + * + * Conceptually this is equivalent to: + * ```kotlin + * fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally( + * initialEvents: Map<K, Events<V>>, + * ): Events<Map<K, V>> = + * foldMapIncrementally(initialEvents).map { it.merge() }.switchEvents() + * ``` + * + * While the behavior is equivalent to the conceptual definition above, the implementation is + * significantly more efficient. + * + * @see merge + */ + fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally( + name: String? = null, + initialEvents: Map<K, Events<V>> = emptyMap(), + ): Events<Map<K, V>> = mergeIncrementally(name, deferredOf(initialEvents)) + + /** + * Returns an [Events] that emits from a merged, incrementally-accumulated collection of + * [Events] emitted from this, following the same "patch" rules as outlined in + * [foldStateMapIncrementally]. + * + * Conceptually this is equivalent to: + * ```kotlin + * fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementallyPromptly( + * initialEvents: Map<K, Events<V>>, + * ): Events<Map<K, V>> = + * foldMapIncrementally(initialEvents).map { it.merge() }.switchEventsPromptly() + * ``` + * + * While the behavior is equivalent to the conceptual definition above, the implementation is + * significantly more efficient. + * + * @see merge + */ + fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementallyPromptly( + initialEvents: Map<K, Events<V>> = emptyMap(), + name: String? = null, + ): Events<Map<K, V>> = mergeIncrementallyPromptly(deferredOf(initialEvents), name) + + /** Applies the [Stateful] within this [StateScope]. */ + fun <A> Stateful<A>.applyStateful(): A = this() + + /** + * Applies the [Stateful] within this [StateScope], returning the result as an [DeferredValue]. + */ + fun <A> Stateful<A>.applyStatefulDeferred(): DeferredValue<A> = deferredStateScope { + applyStateful() + } + + /** + * Returns a [State] that holds onto the most recently emitted value from this [Events], or + * [initialValue] if nothing has been emitted since it was constructed. + * + * Note that the value contained within the [State] is not updated until *after* all [Events] + * have been processed; this keeps the value of the [State] consistent during the entire Kairos + * transaction. + */ + fun <A> Events<A>.holdState(initialValue: A): State<A> = + holdStateDeferred(deferredOf(initialValue)) + + /** + * Returns an [Events] the emits the result of applying [Statefuls][Stateful] emitted from the + * original [Events]. + * + * Unlike [applyLatestStateful], state accumulation is not stopped with each subsequent emission + * of the original [Events]. + */ + fun <A> Events<Stateful<A>>.applyStatefuls(): Events<A> + + /** + * Returns an [Events] containing the results of applying [transform] to each value of the + * original [Events]. + * + * [transform] can perform state accumulation via its [StateScope] receiver. Unlike + * [mapLatestStateful], accumulation is not stopped with each subsequent emission of the + * original [Events]. + */ + fun <A, B> Events<A>.mapStateful(transform: StateScope.(A) -> B): Events<B> = + map { statefully { transform(it) } }.applyStatefuls() + + /** + * Returns a [State] the holds the result of applying the [Stateful] held by the original + * [State]. + * + * Unlike [applyLatestStateful], state accumulation is not stopped with each state change. + */ + fun <A> State<Stateful<A>>.applyStatefuls(): State<A> = + changes + .applyStatefuls() + .holdStateDeferred(initialValue = deferredStateScope { sampleDeferred().get()() }) + + /** Returns an [Events] that switches to the [Events] emitted by the original [Events]. */ + fun <A> Events<Events<A>>.flatten() = holdState(emptyEvents).switchEvents() + + /** + * Returns an [Events] containing the results of applying [transform] to each value of the + * original [Events]. + * + * [transform] can perform state accumulation via its [StateScope] receiver. With each + * invocation of [transform], state accumulation from previous invocation is stopped. + */ + fun <A, B> Events<A>.mapLatestStateful(transform: StateScope.(A) -> B): Events<B> = + map { statefully { transform(it) } }.applyLatestStateful() + + /** + * Returns an [Events] that switches to a new [Events] produced by [transform] every time the + * original [Events] emits a value. + * + * [transform] can perform state accumulation via its [StateScope] receiver. With each + * invocation of [transform], state accumulation from previous invocation is stopped. + */ + fun <A, B> Events<A>.flatMapLatestStateful(transform: StateScope.(A) -> Events<B>): Events<B> = + mapLatestStateful(transform).flatten() + + /** + * Returns an [Events] containing the results of applying each [Stateful] emitted from the + * original [Events]. + * + * When each [Stateful] is applied, state accumulation from the previously-active [Stateful] is + * stopped. + */ + fun <A> Events<Stateful<A>>.applyLatestStateful(): Events<A> = applyLatestStateful {}.first + + /** + * Returns a [State] containing the value returned by applying the [Stateful] held by the + * original [State]. + * + * When each [Stateful] is applied, state accumulation from the previously-active [Stateful] is + * stopped. + */ + fun <A> State<Stateful<A>>.applyLatestStateful(): State<A> { + val (changes, init) = changes.applyLatestStateful { sample()() } + return changes.holdStateDeferred(init) + } + + /** + * Returns an [Events] containing the results of applying each [Stateful] emitted from the + * original [Events], and a [DeferredValue] containing the result of applying [init] + * immediately. + * + * When each [Stateful] is applied, state accumulation from the previously-active [Stateful] is + * stopped. + */ + fun <A, B> Events<Stateful<B>>.applyLatestStateful( + init: Stateful<A> + ): Pair<Events<B>, DeferredValue<A>> { + val (events, result) = + mapCheap { spec -> mapOf(Unit to just(spec)) } + .applyLatestStatefulForKey(init = mapOf(Unit to init), numKeys = 1) + val outEvents: Events<B> = + events.mapMaybe { + checkNotNull(it[Unit]) { "applyLatest: expected result, but none present in: $it" } + } + val outInit: DeferredValue<A> = deferredTransactionScope { + val initResult: Map<Unit, A> = result.get() + check(Unit in initResult) { + "applyLatest: expected initial result, but none present in: $initResult" + } + @Suppress("UNCHECKED_CAST") + initResult.getOrDefault(Unit) { null } as A + } + return Pair(outEvents, outInit) + } + + /** + * Returns an [Events] containing the results of applying each [Stateful] emitted from the + * original [Events], and a [DeferredValue] containing the result of applying [init] + * immediately. + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [Stateful] will be stopped with no replacement. + * + * When each [Stateful] is applied, state accumulation from the previously-active [Stateful] + * with the same key is stopped. + */ + fun <K, A, B> Events<Map<K, Maybe<Stateful<A>>>>.applyLatestStatefulForKey( + init: DeferredValue<Map<K, Stateful<B>>>, + numKeys: Int? = null, + ): Pair<Events<Map<K, Maybe<A>>>, DeferredValue<Map<K, B>>> + + /** + * Returns an [Events] containing the results of applying each [Stateful] emitted from the + * original [Events], and a [DeferredValue] containing the result of applying [init] + * immediately. + * + * When each [Stateful] is applied, state accumulation from the previously-active [Stateful] + * with the same key is stopped. + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [Stateful] will be stopped with no replacement. + */ + fun <K, A, B> Events<Map<K, Maybe<Stateful<A>>>>.applyLatestStatefulForKey( + init: Map<K, Stateful<B>>, + numKeys: Int? = null, + ): Pair<Events<Map<K, Maybe<A>>>, DeferredValue<Map<K, B>>> = + applyLatestStatefulForKey(deferredOf(init), numKeys) + + /** + * Returns a [State] containing the latest results of applying each [Stateful] emitted from the + * original [Events]. + * + * When each [Stateful] is applied, state accumulation from the previously-active [Stateful] + * with the same key is stopped. + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [Stateful] will be stopped with no replacement. + */ + fun <K, A> Events<Map<K, Maybe<Stateful<A>>>>.holdLatestStatefulForKey( + init: DeferredValue<Map<K, Stateful<A>>>, + numKeys: Int? = null, + ): State<Map<K, A>> { + val (changes, initialValues) = applyLatestStatefulForKey(init, numKeys) + return changes.foldStateMapIncrementally(initialValues) + } + + /** + * Returns a [State] containing the latest results of applying each [Stateful] emitted from the + * original [Events]. + * + * When each [Stateful] is applied, state accumulation from the previously-active [Stateful] + * with the same key is stopped. + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [Stateful] will be stopped with no replacement. + */ + fun <K, A> Events<Map<K, Maybe<Stateful<A>>>>.holdLatestStatefulForKey( + init: Map<K, Stateful<A>> = emptyMap(), + numKeys: Int? = null, + ): State<Map<K, A>> = holdLatestStatefulForKey(deferredOf(init), numKeys) + + /** + * Returns an [Events] containing the results of applying each [Stateful] emitted from the + * original [Events], and a [DeferredValue] containing the result of applying [init] + * immediately. + * + * When each [Stateful] is applied, state accumulation from the previously-active [Stateful] + * with the same key is stopped. + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [Stateful] will be stopped with no replacement. + */ + fun <K, A> Events<Map<K, Maybe<Stateful<A>>>>.applyLatestStatefulForKey( + numKeys: Int? = null + ): Events<Map<K, Maybe<A>>> = + applyLatestStatefulForKey(init = emptyMap<K, Stateful<*>>(), numKeys = numKeys).first + + /** + * Returns an [Events] containing the results of applying [transform] to each value of the + * original [Events], and a [DeferredValue] containing the result of applying [transform] to + * [initialValues] immediately. + * + * [transform] can perform state accumulation via its [StateScope] receiver. With each + * invocation of [transform], state accumulation from previous invocation is stopped. + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [StateScope] will be stopped with no replacement. + */ + fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestStatefulForKey( + initialValues: DeferredValue<Map<K, A>>, + numKeys: Int? = null, + transform: StateScope.(A) -> B, + ): Pair<Events<Map<K, Maybe<B>>>, DeferredValue<Map<K, B>>> = + map { patch -> patch.mapValues { (_, v) -> v.map { statefully { transform(it) } } } } + .applyLatestStatefulForKey( + deferredStateScope { + initialValues.get().mapValues { (_, v) -> statefully { transform(v) } } + }, + numKeys = numKeys, + ) + + /** + * Returns an [Events] containing the results of applying [transform] to each value of the + * original [Events], and a [DeferredValue] containing the result of applying [transform] to + * [initialValues] immediately. + * + * [transform] can perform state accumulation via its [StateScope] receiver. With each + * invocation of [transform], state accumulation from previous invocation is stopped. + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [StateScope] will be stopped with no replacement. + */ + fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestStatefulForKey( + initialValues: Map<K, A>, + numKeys: Int? = null, + transform: StateScope.(A) -> B, + ): Pair<Events<Map<K, Maybe<B>>>, DeferredValue<Map<K, B>>> = + mapLatestStatefulForKey(deferredOf(initialValues), numKeys, transform) + + /** + * Returns an [Events] containing the results of applying [transform] to each value of the + * original [Events]. + * + * [transform] can perform state accumulation via its [StateScope] receiver. With each + * invocation of [transform], state accumulation from previous invocation is stopped. + * + * If the [Maybe] contained within the value for an associated key is [none], then the + * previously-active [StateScope] will be stopped with no replacement. + */ + fun <K, A, B> Events<Map<K, Maybe<A>>>.mapLatestStatefulForKey( + numKeys: Int? = null, + transform: StateScope.(A) -> B, + ): Events<Map<K, Maybe<B>>> = mapLatestStatefulForKey(emptyMap(), numKeys, transform).first + + /** + * Returns an [Events] that will only emit the next event of the original [Events], and then + * will act as [emptyEvents]. + * + * If the original [Events] is emitting an event at this exact time, then it will be the only + * even emitted from the result [Events]. + */ + fun <A> Events<A>.nextOnly(name: String? = null): Events<A> = + if (this === emptyEvents) { + this + } else { + EventsLoop<A>().also { + it.loopback = + it.mapCheap { emptyEvents }.holdState(this@nextOnly).switchEvents(name) + } + } + + /** Returns an [Events] that skips the next emission of the original [Events]. */ + fun <A> Events<A>.skipNext(): Events<A> = + if (this === emptyEvents) { + this + } else { + nextOnly().mapCheap { this@skipNext }.holdState(emptyEvents).switchEvents() + } + + /** + * Returns an [Events] that emits values from the original [Events] up until [stop] emits a + * value. + * + * If the original [Events] emits at the same time as [stop], then the returned [Events] will + * emit that value. + */ + fun <A> Events<A>.takeUntil(stop: Events<*>): Events<A> = + if (stop === emptyEvents) { + this + } else { + stop.mapCheap { emptyEvents }.nextOnly().holdState(this).switchEvents() + } + + /** + * Invokes [stateful] in a new [StateScope] that is a child of this one. + * + * This new scope is stopped when [stop] first emits a value, or when the parent scope is + * stopped. Stopping will end all state accumulation; any [States][State] returned from this + * scope will no longer update. + */ + fun <A> childStateScope(stop: Events<*>, stateful: Stateful<A>): DeferredValue<A> { + val (_, init: DeferredValue<Map<Unit, A>>) = + stop + .nextOnly() + .map { mapOf(Unit to none<Stateful<A>>()) } + .applyLatestStatefulForKey(init = mapOf(Unit to stateful), numKeys = 1) + return deferredStateScope { init.get().getValue(Unit) } + } + + /** + * Returns an [Events] that emits values from the original [Events] up to and including a value + * is emitted that satisfies [predicate]. + */ + fun <A> Events<A>.takeUntil(predicate: TransactionScope.(A) -> Boolean): Events<A> = + takeUntil(filter(predicate)) + + /** + * Returns a [State] that is incrementally updated when this [Events] emits a value, by applying + * [transform] to both the emitted value and the currently tracked state. + * + * Note that the value contained within the [State] is not updated until *after* all [Events] + * have been processed; this keeps the value of the [State] consistent during the entire Kairos + * transaction. + */ + fun <A, B> Events<A>.foldState( + initialValue: B, + transform: TransactionScope.(A, B) -> B, + ): State<B> { + lateinit var state: State<B> + return map { a -> transform(a, state.sample()) }.holdState(initialValue).also { state = it } + } + + /** + * Returns a [State] that is incrementally updated when this [Events] emits a value, by applying + * [transform] to both the emitted value and the currently tracked state. + * + * Note that the value contained within the [State] is not updated until *after* all [Events] + * have been processed; this keeps the value of the [State] consistent during the entire Kairos + * transaction. + */ + fun <A, B> Events<A>.foldStateDeferred( + initialValue: DeferredValue<B>, + transform: TransactionScope.(A, B) -> B, + ): State<B> { + lateinit var state: State<B> + return map { a -> transform(a, state.sample()) } + .holdStateDeferred(initialValue) + .also { state = it } + } + + /** + * Returns a [State] that holds onto the result of applying the most recently emitted [Stateful] + * this [Events], or [init] if nothing has been emitted since it was constructed. + * + * When each [Stateful] is applied, state accumulation from the previously-active [Stateful] is + * stopped. + * + * Note that the value contained within the [State] is not updated until *after* all [Events] + * have been processed; this keeps the value of the [State] consistent during the entire Kairos + * transaction. + * + * Shorthand for: + * ```kotlin + * val (changes, initApplied) = applyLatestStateful(init) + * return changes.toStateDeferred(initApplied) + * ``` + */ + fun <A> Events<Stateful<A>>.holdLatestStateful(init: Stateful<A>): State<A> { + val (changes, initApplied) = applyLatestStateful(init) + return changes.holdStateDeferred(initApplied) + } + + /** + * Returns an [Events] that emits the two most recent emissions from the original [Events]. + * [initialValue] is used as the previous value for the first emission. + * + * Shorthand for `sample(hold(init)) { new, old -> Pair(old, new) }` + */ + fun <S, T : S> Events<T>.pairwise(initialValue: S): Events<WithPrev<S, T>> { + val previous = holdState(initialValue) + return mapCheap { new -> WithPrev(previousValue = previous.sample(), newValue = new) } + } + + /** + * Returns an [Events] that emits the two most recent emissions from the original [Events]. Note + * that the returned [Events] will not emit until the original [Events] has emitted twice. + */ + fun <A> Events<A>.pairwise(): Events<WithPrev<A, A>> = + mapCheap { just(it) } + .pairwise(none) + .mapMaybe { (prev, next) -> prev.zipWith(next, ::WithPrev) } + + /** + * Returns a [State] that holds both the current and previous values of the original [State]. + * [initialPreviousValue] is used as the first previous value. + * + * Shorthand for `sample(hold(init)) { new, old -> Pair(old, new) }` + */ + fun <S, T : S> State<T>.pairwise(initialPreviousValue: S): State<WithPrev<S, T>> = + changes + .pairwise(initialPreviousValue) + .holdStateDeferred( + deferredTransactionScope { WithPrev(initialPreviousValue, sample()) } + ) + + /** + * Returns a [State] holding a [Map] that is updated incrementally whenever this emits a value. + * + * The value emitted is used as a "patch" for the tracked [Map]; for each key [K] in the emitted + * map, an associated value of [Just] will insert or replace the value in the tracked [Map], and + * an associated value of [none] will remove the key from the tracked [Map]. + */ + fun <K, V> Events<Map<K, Maybe<V>>>.foldStateMapIncrementally( + initialValues: DeferredValue<Map<K, V>> + ): State<Map<K, V>> = + foldStateDeferred(initialValues) { patch, map -> + val (adds: List<Pair<K, V>>, removes: List<K>) = + patch + .asSequence() + .map { (k, v) -> if (v is Just) Left(k to v.value) else Right(k) } + .partitionEithers() + val removed: Map<K, V> = map - removes.toSet() + val updated: Map<K, V> = removed + adds + updated + } + + /** + * Returns a [State] holding a [Map] that is updated incrementally whenever this emits a value. + * + * The value emitted is used as a "patch" for the tracked [Map]; for each key [K] in the emitted + * map, an associated value of [Just] will insert or replace the value in the tracked [Map], and + * an associated value of [none] will remove the key from the tracked [Map]. + */ + fun <K, V> Events<Map<K, Maybe<V>>>.foldStateMapIncrementally( + initialValues: Map<K, V> = emptyMap() + ): State<Map<K, V>> = foldStateMapIncrementally(deferredOf(initialValues)) + + /** + * Returns an [Events] that wraps each emission of the original [Events] into an [IndexedValue], + * containing the emitted value and its index (starting from zero). + * + * Shorthand for: + * ``` + * val index = fold(0) { _, oldIdx -> oldIdx + 1 } + * sample(index) { a, idx -> IndexedValue(idx, a) } + * ``` + */ + fun <A> Events<A>.withIndex(): Events<IndexedValue<A>> { + val index = foldState(0) { _, old -> old + 1 } + return sample(index) { a, idx -> IndexedValue(idx, a) } + } + + /** + * Returns an [Events] containing the results of applying [transform] to each value of the + * original [Events] and its index (starting from zero). + * + * Shorthand for: + * ``` + * withIndex().map { (idx, a) -> transform(idx, a) } + * ``` + */ + fun <A, B> Events<A>.mapIndexed(transform: TransactionScope.(Int, A) -> B): Events<B> { + val index = foldState(0) { _, i -> i + 1 } + return sample(index) { a, idx -> transform(idx, a) } + } + + /** Returns an [Events] where all subsequent repetitions of the same value are filtered out. */ + fun <A> Events<A>.distinctUntilChanged(): Events<A> { + val state: State<Any?> = holdState(Any()) + return filter { it != state.sample() } + } + + /** + * Returns a new [Events] that emits at the same rate as the original [Events], but combines the + * emitted value with the most recent emission from [other] using [transform]. + * + * Note that the returned [Events] will not emit anything until [other] has emitted at least one + * value. + */ + fun <A, B, C> Events<A>.sample( + other: Events<B>, + transform: TransactionScope.(A, B) -> C, + ): Events<C> { + val state = other.mapCheap { just(it) }.holdState(none) + return sample(state) { a, b -> b.map { transform(a, it) } }.filterJust() + } + + /** + * Returns a [State] that samples the [Transactional] held by the given [State] within the same + * transaction that the state changes. + */ + fun <A> State<Transactional<A>>.sampleTransactionals(): State<A> = + changes + .sampleTransactionals() + .holdStateDeferred(deferredTransactionScope { sample().sample() }) + + /** + * Returns a [State] that transforms the value held inside this [State] by applying it to the + * given function [transform]. + */ + fun <A, B> State<A>.mapTransactionally(transform: TransactionScope.(A) -> B): State<B> = + map { transactionally { transform(it) } }.sampleTransactionals() + + /** + * Returns a [State] whose value is generated with [transform] by combining the current values + * of each given [State]. + * + * @see State.combineWithTransactionally + */ + fun <A, B, Z> combineTransactionally( + stateA: State<A>, + stateB: State<B>, + transform: TransactionScope.(A, B) -> Z, + ): State<Z> = + combine(stateA, stateB) { a, b -> transactionally { transform(a, b) } } + .sampleTransactionals() + + /** + * Returns a [State] whose value is generated with [transform] by combining the current values + * of each given [State]. + * + * @see State.combineWithTransactionally + */ + fun <A, B, C, Z> combineTransactionally( + stateA: State<A>, + stateB: State<B>, + stateC: State<C>, + transform: TransactionScope.(A, B, C) -> Z, + ): State<Z> = + combine(stateA, stateB, stateC) { a, b, c -> transactionally { transform(a, b, c) } } + .sampleTransactionals() + + /** + * Returns a [State] whose value is generated with [transform] by combining the current values + * of each given [State]. + * + * @see State.combineWithTransactionally + */ + fun <A, B, C, D, Z> combineTransactionally( + stateA: State<A>, + stateB: State<B>, + stateC: State<C>, + stateD: State<D>, + transform: TransactionScope.(A, B, C, D) -> Z, + ): State<Z> = + combine(stateA, stateB, stateC, stateD) { a, b, c, d -> + transactionally { transform(a, b, c, d) } + } + .sampleTransactionals() + + /** Returns a [State] by applying [transform] to the value held by the original [State]. */ + fun <A, B> State<A>.flatMapTransactionally( + transform: TransactionScope.(A) -> State<B> + ): State<B> = map { transactionally { transform(it) } }.sampleTransactionals().flatten() + + /** + * Returns a [State] whose value is generated with [transform] by combining the current values + * of each given [State]. + * + * @see State.combineWithTransactionally + */ + fun <A, Z> combineTransactionally( + vararg states: State<A>, + transform: TransactionScope.(List<A>) -> Z, + ): State<Z> = combine(*states).mapTransactionally(transform) + + /** + * Returns a [State] whose value is generated with [transform] by combining the current values + * of each given [State]. + * + * @see State.combineWithTransactionally + */ + fun <A, Z> Iterable<State<A>>.combineTransactionally( + transform: TransactionScope.(List<A>) -> Z + ): State<Z> = combine().mapTransactionally(transform) + + /** + * Returns a [State] by combining the values held inside the given [State]s by applying them to + * the given function [transform]. + */ + fun <A, B, C> State<A>.combineWithTransactionally( + other: State<B>, + transform: TransactionScope.(A, B) -> C, + ): State<C> = combineTransactionally(this, other, transform) +} diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TFlow.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TFlow.kt deleted file mode 100644 index 96edc1043325..000000000000 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TFlow.kt +++ /dev/null @@ -1,566 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.kairos - -import com.android.systemui.kairos.internal.CompletableLazy -import com.android.systemui.kairos.internal.DemuxImpl -import com.android.systemui.kairos.internal.Init -import com.android.systemui.kairos.internal.InitScope -import com.android.systemui.kairos.internal.InputNode -import com.android.systemui.kairos.internal.Network -import com.android.systemui.kairos.internal.NoScope -import com.android.systemui.kairos.internal.TFlowImpl -import com.android.systemui.kairos.internal.activated -import com.android.systemui.kairos.internal.cached -import com.android.systemui.kairos.internal.constInit -import com.android.systemui.kairos.internal.demuxMap -import com.android.systemui.kairos.internal.filterImpl -import com.android.systemui.kairos.internal.filterJustImpl -import com.android.systemui.kairos.internal.init -import com.android.systemui.kairos.internal.map -import com.android.systemui.kairos.internal.mapImpl -import com.android.systemui.kairos.internal.mergeNodes -import com.android.systemui.kairos.internal.mergeNodesLeft -import com.android.systemui.kairos.internal.neverImpl -import com.android.systemui.kairos.internal.switchDeferredImplSingle -import com.android.systemui.kairos.internal.switchPromptImplSingle -import com.android.systemui.kairos.internal.util.hashString -import com.android.systemui.kairos.util.Either -import com.android.systemui.kairos.util.Left -import com.android.systemui.kairos.util.Maybe -import com.android.systemui.kairos.util.Right -import com.android.systemui.kairos.util.just -import com.android.systemui.kairos.util.map -import com.android.systemui.kairos.util.toMaybe -import java.util.concurrent.atomic.AtomicReference -import kotlin.reflect.KProperty -import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.Job -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope - -/** A series of values of type [A] available at discrete points in time. */ -@ExperimentalFrpApi -sealed class TFlow<out A> { - companion object { - /** A [TFlow] with no values. */ - val empty: TFlow<Nothing> = EmptyFlow - } -} - -/** A [TFlow] with no values. */ -@ExperimentalFrpApi val emptyTFlow: TFlow<Nothing> = TFlow.empty - -/** - * A forward-reference to a [TFlow]. Useful for recursive definitions. - * - * This reference can be used like a standard [TFlow], but will hold up evaluation of the FRP - * network until the [loopback] reference is set. - */ -@ExperimentalFrpApi -class TFlowLoop<A> : TFlow<A>() { - private val deferred = CompletableLazy<TFlow<A>>() - - internal val init: Init<TFlowImpl<A>> = - init(name = null) { deferred.value.init.connect(evalScope = this) } - - /** The [TFlow] this reference is referring to. */ - @ExperimentalFrpApi - var loopback: TFlow<A>? = null - set(value) { - value?.let { - check(!deferred.isInitialized()) { "TFlowLoop.loopback has already been set." } - deferred.setValue(value) - field = value - } - } - - operator fun getValue(thisRef: Any?, property: KProperty<*>): TFlow<A> = this - - operator fun setValue(thisRef: Any?, property: KProperty<*>, value: TFlow<A>) { - loopback = value - } - - override fun toString(): String = "${this::class.simpleName}@$hashString" -} - -/** TODO */ -@ExperimentalFrpApi fun <A> Lazy<TFlow<A>>.defer(): TFlow<A> = deferInline { value } - -/** TODO */ -@ExperimentalFrpApi -fun <A> FrpDeferredValue<TFlow<A>>.defer(): TFlow<A> = deferInline { unwrapped.value } - -/** TODO */ -@ExperimentalFrpApi -fun <A> deferTFlow(block: FrpScope.() -> TFlow<A>): TFlow<A> = deferInline { - NoScope.runInFrpScope(block) -} - -/** Returns a [TFlow] that emits the new value of this [TState] when it changes. */ -@ExperimentalFrpApi -val <A> TState<A>.stateChanges: TFlow<A> - get() = TFlowInit(init(name = null) { init.connect(evalScope = this).changes }) - -/** - * Returns a [TFlow] that contains only the [just] results of applying [transform] to each value of - * the original [TFlow]. - * - * @see mapNotNull - */ -@ExperimentalFrpApi -fun <A, B> TFlow<A>.mapMaybe(transform: FrpTransactionScope.(A) -> Maybe<B>): TFlow<B> = - map(transform).filterJust() - -/** - * Returns a [TFlow] that contains only the non-null results of applying [transform] to each value - * of the original [TFlow]. - * - * @see mapMaybe - */ -@ExperimentalFrpApi -fun <A, B> TFlow<A>.mapNotNull(transform: FrpTransactionScope.(A) -> B?): TFlow<B> = mapMaybe { - transform(it).toMaybe() -} - -/** Returns a [TFlow] containing only values of the original [TFlow] that are not null. */ -@ExperimentalFrpApi -fun <A> TFlow<A?>.filterNotNull(): TFlow<A> = mapCheap { it.toMaybe() }.filterJust() - -/** Shorthand for `mapNotNull { it as? A }`. */ -@ExperimentalFrpApi -inline fun <reified A> TFlow<*>.filterIsInstance(): TFlow<A> = mapCheap { it as? A }.filterNotNull() - -/** Shorthand for `mapMaybe { it }`. */ -@ExperimentalFrpApi -fun <A> TFlow<Maybe<A>>.filterJust(): TFlow<A> = - TFlowInit(constInit(name = null, filterJustImpl { init.connect(evalScope = this) })) - -/** - * Returns a [TFlow] containing the results of applying [transform] to each value of the original - * [TFlow]. - */ -@ExperimentalFrpApi -fun <A, B> TFlow<A>.map(transform: FrpTransactionScope.(A) -> B): TFlow<B> { - val mapped: TFlowImpl<B> = - mapImpl({ init.connect(evalScope = this) }) { a, _ -> - runInTransactionScope { transform(a) } - } - return TFlowInit(constInit(name = null, mapped.cached())) -} - -/** - * Like [map], but the emission is not cached during the transaction. Use only if [transform] is - * fast and pure. - * - * @see map - */ -@ExperimentalFrpApi -fun <A, B> TFlow<A>.mapCheap(transform: FrpTransactionScope.(A) -> B): TFlow<B> = - TFlowInit( - constInit( - name = null, - mapImpl({ init.connect(evalScope = this) }) { a, _ -> - runInTransactionScope { transform(a) } - }, - ) - ) - -/** - * Returns a [TFlow] that invokes [action] before each value of the original [TFlow] is emitted. - * Useful for logging and debugging. - * - * ``` - * pulse.onEach { foo(it) } == pulse.map { foo(it); it } - * ``` - * - * Note that the side effects performed in [onEach] are only performed while the resulting [TFlow] - * is connected to an output of the FRP network. If your goal is to reliably perform side effects in - * response to a [TFlow], use the output combinators available in [FrpBuildScope], such as - * [FrpBuildScope.toSharedFlow] or [FrpBuildScope.observe]. - */ -@ExperimentalFrpApi -fun <A> TFlow<A>.onEach(action: FrpTransactionScope.(A) -> Unit): TFlow<A> = map { - action(it) - it -} - -/** - * Returns a [TFlow] containing only values of the original [TFlow] that satisfy the given - * [predicate]. - */ -@ExperimentalFrpApi -fun <A> TFlow<A>.filter(predicate: FrpTransactionScope.(A) -> Boolean): TFlow<A> { - val pulse = - filterImpl({ init.connect(evalScope = this) }) { runInTransactionScope { predicate(it) } } - return TFlowInit(constInit(name = null, pulse)) -} - -/** - * Splits a [TFlow] of pairs into a pair of [TFlows][TFlow], where each returned [TFlow] emits half - * of the original. - * - * Shorthand for: - * ```kotlin - * val lefts = map { it.first } - * val rights = map { it.second } - * return Pair(lefts, rights) - * ``` - */ -@ExperimentalFrpApi -fun <A, B> TFlow<Pair<A, B>>.unzip(): Pair<TFlow<A>, TFlow<B>> { - val lefts = map { it.first } - val rights = map { it.second } - return lefts to rights -} - -/** - * Merges the given [TFlows][TFlow] into a single [TFlow] that emits events from both. - * - * Because [TFlow]s can only emit one value per transaction, the provided [transformCoincidence] - * function is used to combine coincident emissions to produce the result value to be emitted by the - * merged [TFlow]. - */ -@ExperimentalFrpApi -fun <A> TFlow<A>.mergeWith( - other: TFlow<A>, - name: String? = null, - transformCoincidence: FrpTransactionScope.(A, A) -> A = { a, _ -> a }, -): TFlow<A> { - val node = - mergeNodes( - name = name, - getPulse = { init.connect(evalScope = this) }, - getOther = { other.init.connect(evalScope = this) }, - ) { a, b -> - runInTransactionScope { transformCoincidence(a, b) } - } - return TFlowInit(constInit(name = null, node)) -} - -/** - * Merges the given [TFlows][TFlow] into a single [TFlow] that emits events from all. All coincident - * emissions are collected into the emitted [List], preserving the input ordering. - * - * @see mergeWith - * @see mergeLeft - */ -@ExperimentalFrpApi -fun <A> merge(vararg flows: TFlow<A>): TFlow<List<A>> = flows.asIterable().merge() - -/** - * Merges the given [TFlows][TFlow] into a single [TFlow] that emits events from all. In the case of - * coincident emissions, the emission from the left-most [TFlow] is emitted. - * - * @see merge - */ -@ExperimentalFrpApi -fun <A> mergeLeft(vararg flows: TFlow<A>): TFlow<A> = flows.asIterable().mergeLeft() - -/** - * Merges the given [TFlows][TFlow] into a single [TFlow] that emits events from all. - * - * Because [TFlow]s can only emit one value per transaction, the provided [transformCoincidence] - * function is used to combine coincident emissions to produce the result value to be emitted by the - * merged [TFlow]. - */ -// TODO: can be optimized to avoid creating the intermediate list -fun <A> merge(vararg flows: TFlow<A>, transformCoincidence: (A, A) -> A): TFlow<A> = - merge(*flows).map { l -> l.reduce(transformCoincidence) } - -/** - * Merges the given [TFlows][TFlow] into a single [TFlow] that emits events from all. All coincident - * emissions are collected into the emitted [List], preserving the input ordering. - * - * @see mergeWith - * @see mergeLeft - */ -@ExperimentalFrpApi -fun <A> Iterable<TFlow<A>>.merge(): TFlow<List<A>> = - TFlowInit(constInit(name = null, mergeNodes { map { it.init.connect(evalScope = this) } })) - -/** - * Merges the given [TFlows][TFlow] into a single [TFlow] that emits events from all. In the case of - * coincident emissions, the emission from the left-most [TFlow] is emitted. - * - * @see merge - */ -@ExperimentalFrpApi -fun <A> Iterable<TFlow<A>>.mergeLeft(): TFlow<A> = - TFlowInit(constInit(name = null, mergeNodesLeft { map { it.init.connect(evalScope = this) } })) - -/** - * Creates a new [TFlow] that emits events from all given [TFlow]s. All simultaneous emissions are - * collected into the emitted [List], preserving the input ordering. - * - * @see mergeWith - */ -@ExperimentalFrpApi fun <A> Sequence<TFlow<A>>.merge(): TFlow<List<A>> = asIterable().merge() - -/** - * Creates a new [TFlow] that emits events from all given [TFlow]s. All simultaneous emissions are - * collected into the emitted [Map], and are given the same key of the associated [TFlow] in the - * input [Map]. - * - * @see mergeWith - */ -@ExperimentalFrpApi -fun <K, A> Map<K, TFlow<A>>.merge(): TFlow<Map<K, A>> = - asSequence().map { (k, flowA) -> flowA.map { a -> k to a } }.toList().merge().map { it.toMap() } - -/** - * Returns a [GroupedTFlow] that can be used to efficiently split a single [TFlow] into multiple - * downstream [TFlow]s. - * - * The input [TFlow] emits [Map] instances that specify which downstream [TFlow] the associated - * value will be emitted from. These downstream [TFlow]s can be obtained via - * [GroupedTFlow.eventsForKey]. - * - * An example: - * ``` - * val sFoo: TFlow<Map<String, Foo>> = ... - * val fooById: GroupedTFlow<String, Foo> = sFoo.groupByKey() - * val fooBar: TFlow<Foo> = fooById["bar"] - * ``` - * - * This is semantically equivalent to `val fooBar = sFoo.mapNotNull { map -> map["bar"] }` but is - * significantly more efficient; specifically, using [mapNotNull] in this way incurs a `O(n)` - * performance hit, where `n` is the number of different [mapNotNull] operations used to filter on a - * specific key's presence in the emitted [Map]. [groupByKey] internally uses a [HashMap] to lookup - * the appropriate downstream [TFlow], and so operates in `O(1)`. - * - * Note that the result [GroupedTFlow] should be cached and re-used to gain the performance benefit. - * - * @see selector - */ -@ExperimentalFrpApi -fun <K, A> TFlow<Map<K, A>>.groupByKey(numKeys: Int? = null): GroupedTFlow<K, A> = - GroupedTFlow(demuxMap({ init.connect(this) }, numKeys)) - -/** - * Shorthand for `map { mapOf(extractKey(it) to it) }.groupByKey()` - * - * @see groupByKey - */ -@ExperimentalFrpApi -fun <K, A> TFlow<A>.groupBy( - numKeys: Int? = null, - extractKey: FrpTransactionScope.(A) -> K, -): GroupedTFlow<K, A> = map { mapOf(extractKey(it) to it) }.groupByKey(numKeys) - -/** - * Returns two new [TFlow]s that contain elements from this [TFlow] that satisfy or don't satisfy - * [predicate]. - * - * Using this is equivalent to `upstream.filter(predicate) to upstream.filter { !predicate(it) }` - * but is more efficient; specifically, [partition] will only invoke [predicate] once per element. - */ -@ExperimentalFrpApi -fun <A> TFlow<A>.partition( - predicate: FrpTransactionScope.(A) -> Boolean -): Pair<TFlow<A>, TFlow<A>> { - val grouped: GroupedTFlow<Boolean, A> = groupBy(numKeys = 2, extractKey = predicate) - return Pair(grouped.eventsForKey(true), grouped.eventsForKey(false)) -} - -/** - * Returns two new [TFlow]s that contain elements from this [TFlow]; [Pair.first] will contain - * [Left] values, and [Pair.second] will contain [Right] values. - * - * Using this is equivalent to using [filterIsInstance] in conjunction with [map] twice, once for - * [Left]s and once for [Right]s, but is slightly more efficient; specifically, the - * [filterIsInstance] check is only performed once per element. - */ -@ExperimentalFrpApi -fun <A, B> TFlow<Either<A, B>>.partitionEither(): Pair<TFlow<A>, TFlow<B>> { - val (left, right) = partition { it is Left } - return Pair(left.mapCheap { (it as Left).value }, right.mapCheap { (it as Right).value }) -} - -/** - * A mapping from keys of type [K] to [TFlow]s emitting values of type [A]. - * - * @see groupByKey - */ -@ExperimentalFrpApi -class GroupedTFlow<in K, out A> internal constructor(internal val impl: DemuxImpl<K, A>) { - /** - * Returns a [TFlow] that emits values of type [A] that correspond to the given [key]. - * - * @see groupByKey - */ - @ExperimentalFrpApi - fun eventsForKey(key: K): TFlow<A> = TFlowInit(constInit(name = null, impl.eventsForKey(key))) - - /** - * Returns a [TFlow] that emits values of type [A] that correspond to the given [key]. - * - * @see groupByKey - */ - @ExperimentalFrpApi operator fun get(key: K): TFlow<A> = eventsForKey(key) -} - -/** - * Returns a [TFlow] that switches to the [TFlow] contained within this [TState] whenever it - * changes. - * - * This switch does take effect until the *next* transaction after [TState] changes. For a switch - * that takes effect immediately, see [switchPromptly]. - */ -@ExperimentalFrpApi -fun <A> TState<TFlow<A>>.switch(name: String? = null): TFlow<A> { - val patches = - mapImpl({ init.connect(this).changes }) { newFlow, _ -> newFlow.init.connect(this) } - return TFlowInit( - constInit( - name = null, - switchDeferredImplSingle( - name = name, - getStorage = { - init.connect(this).getCurrentWithEpoch(this).first.init.connect(this) - }, - getPatches = { patches }, - ), - ) - ) -} - -/** - * Returns a [TFlow] that switches to the [TFlow] contained within this [TState] whenever it - * changes. - * - * This switch takes effect immediately within the same transaction that [TState] changes. In - * general, you should prefer [switch] over this method. It is both safer and more performant. - */ -// TODO: parameter to handle coincidental emission from both old and new -@ExperimentalFrpApi -fun <A> TState<TFlow<A>>.switchPromptly(): TFlow<A> { - val patches = - mapImpl({ init.connect(this).changes }) { newFlow, _ -> newFlow.init.connect(this) } - return TFlowInit( - constInit( - name = null, - switchPromptImplSingle( - getStorage = { - init.connect(this).getCurrentWithEpoch(this).first.init.connect(this) - }, - getPatches = { patches }, - ), - ) - ) -} - -/** - * A mutable [TFlow] that provides the ability to [emit] values to the flow, handling backpressure - * by coalescing all emissions into batches. - * - * @see FrpNetwork.coalescingMutableTFlow - */ -@ExperimentalFrpApi -class CoalescingMutableTFlow<In, Out> -internal constructor( - internal val name: String?, - internal val coalesce: (old: Out, new: In) -> Out, - internal val network: Network, - private val getInitialValue: () -> Out, - internal val impl: InputNode<Out> = InputNode(), -) : TFlow<Out>() { - internal val storage = AtomicReference(false to getInitialValue()) - - override fun toString(): String = "${this::class.simpleName}@$hashString" - - /** - * Inserts [value] into the current batch, enqueueing it for emission from this [TFlow] if not - * already pending. - * - * Backpressure occurs when [emit] is called while the FRP network is currently in a - * transaction; if called multiple times, then emissions will be coalesced into a single batch - * that is then processed when the network is ready. - */ - @ExperimentalFrpApi - fun emit(value: In) { - val (scheduled, _) = storage.getAndUpdate { (_, old) -> true to coalesce(old, value) } - if (!scheduled) { - @Suppress("DeferredResultUnused") - network.transaction("CoalescingMutableTFlow${name?.let { "($name)" }.orEmpty()}.emit") { - impl.visit(this, storage.getAndSet(false to getInitialValue()).second) - } - } - } -} - -/** - * A mutable [TFlow] that provides the ability to [emit] values to the flow, handling backpressure - * by suspending the emitter. - * - * @see FrpNetwork.coalescingMutableTFlow - */ -@ExperimentalFrpApi -class MutableTFlow<T> -internal constructor(internal val network: Network, internal val impl: InputNode<T> = InputNode()) : - TFlow<T>() { - internal val name: String? = null - - private val storage = AtomicReference<Job?>(null) - - override fun toString(): String = "${this::class.simpleName}@$hashString" - - /** - * Emits a [value] to this [TFlow], suspending the caller until the FRP transaction containing - * the emission has completed. - */ - @ExperimentalFrpApi - suspend fun emit(value: T) { - coroutineScope { - var jobOrNull: Job? = null - val newEmit = - async(start = CoroutineStart.LAZY) { - jobOrNull?.join() - network - .transaction("MutableTFlow($name).emit") { impl.visit(this, value) } - .await() - } - jobOrNull = storage.getAndSet(newEmit) - newEmit.await() - } - } - - // internal suspend fun emitInCurrentTransaction(value: T, evalScope: EvalScope) { - // if (storage.getAndSet(just(value)) is None) { - // impl.visit(evalScope) - // } - // } -} - -private data object EmptyFlow : TFlow<Nothing>() - -internal class TFlowInit<out A>(val init: Init<TFlowImpl<A>>) : TFlow<A>() { - override fun toString(): String = "${this::class.simpleName}@$hashString" -} - -internal val <A> TFlow<A>.init: Init<TFlowImpl<A>> - get() = - when (this) { - is EmptyFlow -> constInit("EmptyFlow", neverImpl) - is TFlowInit -> init - is TFlowLoop -> init - is CoalescingMutableTFlow<*, A> -> constInit(name, impl.activated()) - is MutableTFlow -> constInit(name, impl.activated()) - } - -private inline fun <A> deferInline(crossinline block: InitScope.() -> TFlow<A>): TFlow<A> = - TFlowInit(init(name = null) { block().init.connect(evalScope = this) }) diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TState.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TState.kt deleted file mode 100644 index d84a6f2ddb34..000000000000 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TState.kt +++ /dev/null @@ -1,491 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.kairos - -import com.android.systemui.kairos.internal.CompletableLazy -import com.android.systemui.kairos.internal.DerivedMapCheap -import com.android.systemui.kairos.internal.Init -import com.android.systemui.kairos.internal.InitScope -import com.android.systemui.kairos.internal.Network -import com.android.systemui.kairos.internal.NoScope -import com.android.systemui.kairos.internal.Schedulable -import com.android.systemui.kairos.internal.TFlowImpl -import com.android.systemui.kairos.internal.TStateImpl -import com.android.systemui.kairos.internal.TStateSource -import com.android.systemui.kairos.internal.activated -import com.android.systemui.kairos.internal.cached -import com.android.systemui.kairos.internal.constInit -import com.android.systemui.kairos.internal.constS -import com.android.systemui.kairos.internal.filterImpl -import com.android.systemui.kairos.internal.flatMap -import com.android.systemui.kairos.internal.init -import com.android.systemui.kairos.internal.map -import com.android.systemui.kairos.internal.mapCheap -import com.android.systemui.kairos.internal.mapImpl -import com.android.systemui.kairos.internal.util.hashString -import com.android.systemui.kairos.internal.zipStateMap -import com.android.systemui.kairos.internal.zipStates -import kotlin.reflect.KProperty - -/** - * A time-varying value with discrete changes. Essentially, a combination of a [Transactional] that - * holds a value, and a [TFlow] that emits when the value changes. - */ -@ExperimentalFrpApi sealed class TState<out A> - -/** A [TState] that never changes. */ -@ExperimentalFrpApi -fun <A> tStateOf(value: A): TState<A> { - val operatorName = "tStateOf" - val name = "$operatorName($value)" - return TStateInit(constInit(name, constS(name, operatorName, value))) -} - -/** TODO */ -@ExperimentalFrpApi fun <A> Lazy<TState<A>>.defer(): TState<A> = deferInline { value } - -/** TODO */ -@ExperimentalFrpApi -fun <A> FrpDeferredValue<TState<A>>.defer(): TState<A> = deferInline { unwrapped.value } - -/** TODO */ -@ExperimentalFrpApi -fun <A> deferTState(block: FrpScope.() -> TState<A>): TState<A> = deferInline { - NoScope.runInFrpScope(block) -} - -/** - * Returns a [TState] containing the results of applying [transform] to the value held by the - * original [TState]. - */ -@ExperimentalFrpApi -fun <A, B> TState<A>.map(transform: FrpScope.(A) -> B): TState<B> { - val operatorName = "map" - val name = operatorName - return TStateInit( - init(name) { - init.connect(evalScope = this).map(name, operatorName) { - NoScope.runInFrpScope { transform(it) } - } - } - ) -} - -/** - * Returns a [TState] that transforms the value held inside this [TState] by applying it to the - * [transform]. - * - * Note that unlike [map], the result is not cached. This means that not only should [transform] be - * fast and pure, it should be *monomorphic* (1-to-1). Failure to do this means that [stateChanges] - * for the returned [TState] will operate unexpectedly, emitting at rates that do not reflect an - * observable change to the returned [TState]. - */ -@ExperimentalFrpApi -fun <A, B> TState<A>.mapCheapUnsafe(transform: FrpScope.(A) -> B): TState<B> { - val operatorName = "map" - val name = operatorName - return TStateInit( - init(name) { - init.connect(evalScope = this).mapCheap(name, operatorName) { - NoScope.runInFrpScope { transform(it) } - } - } - ) -} - -/** - * Returns a [TState] by combining the values held inside the given [TState]s by applying them to - * the given function [transform]. - */ -@ExperimentalFrpApi -fun <A, B, C> TState<A>.combineWith(other: TState<B>, transform: FrpScope.(A, B) -> C): TState<C> = - combine(this, other, transform) - -/** - * Splits a [TState] of pairs into a pair of [TFlows][TState], where each returned [TState] holds - * half of the original. - * - * Shorthand for: - * ```kotlin - * val lefts = map { it.first } - * val rights = map { it.second } - * return Pair(lefts, rights) - * ``` - */ -@ExperimentalFrpApi -fun <A, B> TState<Pair<A, B>>.unzip(): Pair<TState<A>, TState<B>> { - val left = map { it.first } - val right = map { it.second } - return left to right -} - -/** - * Returns a [TState] by combining the values held inside the given [TStates][TState] into a [List]. - * - * @see TState.combineWith - */ -@ExperimentalFrpApi -fun <A> Iterable<TState<A>>.combine(): TState<List<A>> { - val operatorName = "combine" - val name = operatorName - return TStateInit( - init(name) { - zipStates(name, operatorName, states = map { it.init.connect(evalScope = this) }) - } - ) -} - -/** - * Returns a [TState] by combining the values held inside the given [TStates][TState] into a [Map]. - * - * @see TState.combineWith - */ -@ExperimentalFrpApi -fun <K, A> Map<K, TState<A>>.combine(): TState<Map<K, A>> { - val operatorName = "combine" - val name = operatorName - return TStateInit( - init(name) { - zipStateMap( - name, - operatorName, - states = mapValues { it.value.init.connect(evalScope = this) }, - ) - } - ) -} - -/** - * Returns a [TState] whose value is generated with [transform] by combining the current values of - * each given [TState]. - * - * @see TState.combineWith - */ -@ExperimentalFrpApi -fun <A, B> Iterable<TState<A>>.combine(transform: FrpScope.(List<A>) -> B): TState<B> = - combine().map(transform) - -/** - * Returns a [TState] by combining the values held inside the given [TState]s into a [List]. - * - * @see TState.combineWith - */ -@ExperimentalFrpApi -fun <A> combine(vararg states: TState<A>): TState<List<A>> = states.asIterable().combine() - -/** - * Returns a [TState] whose value is generated with [transform] by combining the current values of - * each given [TState]. - * - * @see TState.combineWith - */ -@ExperimentalFrpApi -fun <A, B> combine(vararg states: TState<A>, transform: FrpScope.(List<A>) -> B): TState<B> = - states.asIterable().combine(transform) - -/** - * Returns a [TState] whose value is generated with [transform] by combining the current values of - * each given [TState]. - * - * @see TState.combineWith - */ -@ExperimentalFrpApi -fun <A, B, Z> combine( - stateA: TState<A>, - stateB: TState<B>, - transform: FrpScope.(A, B) -> Z, -): TState<Z> { - val operatorName = "combine" - val name = operatorName - return TStateInit( - init(name) { - val dl1 = stateA.init.connect(evalScope = this@init) - val dl2 = stateB.init.connect(evalScope = this@init) - zipStates(name, operatorName, dl1, dl2) { a, b -> - NoScope.runInFrpScope { transform(a, b) } - } - } - ) -} - -/** - * Returns a [TState] whose value is generated with [transform] by combining the current values of - * each given [TState]. - * - * @see TState.combineWith - */ -@ExperimentalFrpApi -fun <A, B, C, Z> combine( - stateA: TState<A>, - stateB: TState<B>, - stateC: TState<C>, - transform: FrpScope.(A, B, C) -> Z, -): TState<Z> { - val operatorName = "combine" - val name = operatorName - return TStateInit( - init(name) { - val dl1 = stateA.init.connect(evalScope = this@init) - val dl2 = stateB.init.connect(evalScope = this@init) - val dl3 = stateC.init.connect(evalScope = this@init) - zipStates(name, operatorName, dl1, dl2, dl3) { a, b, c -> - NoScope.runInFrpScope { transform(a, b, c) } - } - } - ) -} - -/** - * Returns a [TState] whose value is generated with [transform] by combining the current values of - * each given [TState]. - * - * @see TState.combineWith - */ -@ExperimentalFrpApi -fun <A, B, C, D, Z> combine( - stateA: TState<A>, - stateB: TState<B>, - stateC: TState<C>, - stateD: TState<D>, - transform: FrpScope.(A, B, C, D) -> Z, -): TState<Z> { - val operatorName = "combine" - val name = operatorName - return TStateInit( - init(name) { - val dl1 = stateA.init.connect(evalScope = this@init) - val dl2 = stateB.init.connect(evalScope = this@init) - val dl3 = stateC.init.connect(evalScope = this@init) - val dl4 = stateD.init.connect(evalScope = this@init) - zipStates(name, operatorName, dl1, dl2, dl3, dl4) { a, b, c, d -> - NoScope.runInFrpScope { transform(a, b, c, d) } - } - } - ) -} - -/** - * Returns a [TState] whose value is generated with [transform] by combining the current values of - * each given [TState]. - * - * @see TState.combineWith - */ -@ExperimentalFrpApi -fun <A, B, C, D, E, Z> combine( - stateA: TState<A>, - stateB: TState<B>, - stateC: TState<C>, - stateD: TState<D>, - stateE: TState<E>, - transform: FrpScope.(A, B, C, D, E) -> Z, -): TState<Z> { - val operatorName = "combine" - val name = operatorName - return TStateInit( - init(name) { - val dl1 = stateA.init.connect(evalScope = this@init) - val dl2 = stateB.init.connect(evalScope = this@init) - val dl3 = stateC.init.connect(evalScope = this@init) - val dl4 = stateD.init.connect(evalScope = this@init) - val dl5 = stateE.init.connect(evalScope = this@init) - zipStates(name, operatorName, dl1, dl2, dl3, dl4, dl5) { a, b, c, d, e -> - NoScope.runInFrpScope { transform(a, b, c, d, e) } - } - } - ) -} - -/** Returns a [TState] by applying [transform] to the value held by the original [TState]. */ -@ExperimentalFrpApi -fun <A, B> TState<A>.flatMap(transform: FrpScope.(A) -> TState<B>): TState<B> { - val operatorName = "flatMap" - val name = operatorName - return TStateInit( - init(name) { - init.connect(this).flatMap(name, operatorName) { a -> - NoScope.runInFrpScope { transform(a) }.init.connect(this) - } - } - ) -} - -/** Shorthand for `flatMap { it }` */ -@ExperimentalFrpApi fun <A> TState<TState<A>>.flatten() = flatMap { it } - -/** - * Returns a [TStateSelector] that can be used to efficiently check if the input [TState] is - * currently holding a specific value. - * - * An example: - * ``` - * val lInt: TState<Int> = ... - * val intSelector: TStateSelector<Int> = lInt.selector() - * // Tracks if lInt is holding 1 - * val isOne: TState<Boolean> = intSelector.whenSelected(1) - * ``` - * - * This is semantically equivalent to `val isOne = lInt.map { i -> i == 1 }`, but is significantly - * more efficient; specifically, using [TState.map] in this way incurs a `O(n)` performance hit, - * where `n` is the number of different [TState.map] operations used to track a specific value. - * [selector] internally uses a [HashMap] to lookup the appropriate downstream [TState] to update, - * and so operates in `O(1)`. - * - * Note that the result [TStateSelector] should be cached and re-used to gain the performance - * benefit. - * - * @see groupByKey - */ -@ExperimentalFrpApi -fun <A> TState<A>.selector(numDistinctValues: Int? = null): TStateSelector<A> = - TStateSelector( - this, - stateChanges - .map { new -> mapOf(new to true, sampleDeferred().get() to false) } - .groupByKey(numDistinctValues), - ) - -/** - * Tracks the currently selected value of type [A] from an upstream [TState]. - * - * @see selector - */ -@ExperimentalFrpApi -class TStateSelector<in A> -internal constructor( - private val upstream: TState<A>, - private val groupedChanges: GroupedTFlow<A, Boolean>, -) { - /** - * Returns a [TState] that tracks whether the upstream [TState] is currently holding the given - * [value]. - * - * @see selector - */ - @ExperimentalFrpApi - fun whenSelected(value: A): TState<Boolean> { - val operatorName = "TStateSelector#whenSelected" - val name = "$operatorName[$value]" - return TStateInit( - init(name) { - DerivedMapCheap( - name, - operatorName, - upstream = upstream.init.connect(evalScope = this), - changes = groupedChanges.impl.eventsForKey(value), - ) { - it == value - } - } - ) - } - - @ExperimentalFrpApi operator fun get(value: A): TState<Boolean> = whenSelected(value) -} - -/** TODO */ -@ExperimentalFrpApi -class MutableTState<T> internal constructor(internal val network: Network, initialValue: Lazy<T>) : - TState<T>() { - - private val input: CoalescingMutableTFlow<Lazy<T>, Lazy<T>?> = - CoalescingMutableTFlow( - name = null, - coalesce = { _, new -> new }, - network = network, - getInitialValue = { null }, - ) - - internal val tState = run { - val changes = input.impl - val name = null - val operatorName = "MutableTState" - lateinit var state: TStateSource<T> - val mapImpl = mapImpl(upstream = { changes.activated() }) { it, _ -> it!!.value } - val calm: TFlowImpl<T> = - filterImpl({ mapImpl }) { new -> - new != state.getCurrentWithEpoch(evalScope = this).first - } - .cached() - state = TStateSource(name, operatorName, initialValue, calm) - @Suppress("DeferredResultUnused") - network.transaction("MutableTState.init") { - calm.activate(evalScope = this, downstream = Schedulable.S(state))?.let { - (connection, needsEval) -> - state.upstreamConnection = connection - if (needsEval) { - schedule(state) - } - } - } - TStateInit(constInit(name, state)) - } - - /** TODO */ - @ExperimentalFrpApi fun setValue(value: T) = input.emit(CompletableLazy(value)) - - @ExperimentalFrpApi - fun setValueDeferred(value: FrpDeferredValue<T>) = input.emit(value.unwrapped) -} - -/** A forward-reference to a [TState], allowing for recursive definitions. */ -@ExperimentalFrpApi -class TStateLoop<A> : TState<A>() { - - private val name: String? = null - - private val deferred = CompletableLazy<TState<A>>() - - internal val init: Init<TStateImpl<A>> = - init(name) { deferred.value.init.connect(evalScope = this) } - - /** The [TState] this [TStateLoop] will forward to. */ - @ExperimentalFrpApi - var loopback: TState<A>? = null - set(value) { - value?.let { - check(!deferred.isInitialized()) { "TStateLoop.loopback has already been set." } - deferred.setValue(value) - field = value - } - } - - @ExperimentalFrpApi - operator fun getValue(thisRef: Any?, property: KProperty<*>): TState<A> = this - - @ExperimentalFrpApi - operator fun setValue(thisRef: Any?, property: KProperty<*>, value: TState<A>) { - loopback = value - } - - override fun toString(): String = "${this::class.simpleName}@$hashString" -} - -internal class TStateInit<A> internal constructor(internal val init: Init<TStateImpl<A>>) : - TState<A>() { - override fun toString(): String = "${this::class.simpleName}@$hashString" -} - -internal val <A> TState<A>.init: Init<TStateImpl<A>> - get() = - when (this) { - is TStateInit -> init - is TStateLoop -> init - is MutableTState -> tState.init - } - -private inline fun <A> deferInline(crossinline block: InitScope.() -> TState<A>): TState<A> = - TStateInit(init(name = null) { block().init.connect(evalScope = this) }) diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TransactionScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TransactionScope.kt new file mode 100644 index 000000000000..225416992d52 --- /dev/null +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/TransactionScope.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.kairos + +/** + * Kairos operations that are available while a transaction is active. + * + * These operations do not accumulate state, which makes [TransactionScope] weaker than + * [StateScope], but allows them to be used in more places. + */ +@ExperimentalKairosApi +interface TransactionScope : KairosScope { + + /** + * Returns the current value of this [Transactional] as a [DeferredValue]. + * + * Compared to [sample], you may want to use this instead if you do not need to inspect the + * sampled value, but instead want to pass it to another Kairos API that accepts a + * [DeferredValue]. In this case, [sampleDeferred] is both safer and more performant. + * + * @see sample + */ + fun <A> Transactional<A>.sampleDeferred(): DeferredValue<A> + + /** + * Returns the current value of this [State] as a [DeferredValue]. + * + * Compared to [sample], you may want to use this instead if you do not need to inspect the + * sampled value, but instead want to pass it to another Kairos API that accepts a + * [DeferredValue]. In this case, [sampleDeferred] is both safer and more performant. + * + * @see sample + */ + fun <A> State<A>.sampleDeferred(): DeferredValue<A> + + /** + * Defers invoking [block] until after the current [TransactionScope] code-path completes, + * returning a [DeferredValue] that can be used to reference the result. + * + * Useful for recursive definitions. + * + * @see DeferredValue + */ + fun <A> deferredTransactionScope(block: TransactionScope.() -> A): DeferredValue<A> + + /** An [Events] that emits once, within this transaction, and then never again. */ + val now: Events<Unit> + + /** + * Returns the current value held by this [State]. Guaranteed to be consistent within the same + * transaction. + * + * @see sampleDeferred + */ + fun <A> State<A>.sample(): A = sampleDeferred().get() + + /** + * Returns the current value held by this [Transactional]. Guaranteed to be consistent within + * the same transaction. + * + * @see sampleDeferred + */ + fun <A> Transactional<A>.sample(): A = sampleDeferred().get() +} diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Transactional.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Transactional.kt index e7a5b1bbd105..9485cd212603 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Transactional.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Transactional.kt @@ -28,44 +28,68 @@ import com.android.systemui.kairos.internal.util.hashString * A time-varying value. A [Transactional] encapsulates the idea of some continuous state; each time * it is "sampled", a new result may be produced. * - * Because FRP operates over an "idealized" model of Time that can be passed around as a data type, - * [Transactional]s are guaranteed to produce the same result if queried multiple times at the same - * (conceptual) time, in order to preserve _referential transparency_. + * Because Kairos operates over an "idealized" model of Time that can be passed around as a data + * type, [Transactional]s are guaranteed to produce the same result if queried multiple times at the + * same (conceptual) time, in order to preserve _referential transparency_. */ -@ExperimentalFrpApi -class Transactional<out A> internal constructor(internal val impl: TState<TransactionalImpl<A>>) { +@ExperimentalKairosApi +class Transactional<out A> internal constructor(internal val impl: State<TransactionalImpl<A>>) { override fun toString(): String = "${this::class.simpleName}@$hashString" } /** A constant [Transactional] that produces [value] whenever it is sampled. */ -@ExperimentalFrpApi +@ExperimentalKairosApi fun <A> transactionalOf(value: A): Transactional<A> = - Transactional(tStateOf(TransactionalImpl.Const(CompletableLazy(value)))) + Transactional(stateOf(TransactionalImpl.Const(CompletableLazy(value)))) -/** TODO */ -@ExperimentalFrpApi -fun <A> FrpDeferredValue<Transactional<A>>.defer(): Transactional<A> = deferInline { - unwrapped.value -} +/** + * Returns a [Transactional] that acts as a deferred-reference to the [Transactional] produced by + * this [DeferredValue]. + * + * When the returned [Transactional] is accessed by the Kairos network, the [DeferredValue] will be + * queried and used. + * + * Useful for recursive definitions. + */ +@ExperimentalKairosApi +fun <A> DeferredValue<Transactional<A>>.defer(): Transactional<A> = deferInline { unwrapped.value } -/** TODO */ -@ExperimentalFrpApi fun <A> Lazy<Transactional<A>>.defer(): Transactional<A> = deferInline { value } +/** + * Returns a [Transactional] that acts as a deferred-reference to the [Transactional] produced by + * this [Lazy]. + * + * When the returned [Transactional] is accessed by the Kairos network, the [Lazy]'s + * [value][Lazy.value] will be queried and used. + * + * Useful for recursive definitions. + */ +@ExperimentalKairosApi +fun <A> Lazy<Transactional<A>>.defer(): Transactional<A> = deferInline { value } -/** TODO */ -@ExperimentalFrpApi -fun <A> deferTransactional(block: FrpScope.() -> Transactional<A>): Transactional<A> = deferInline { - NoScope.runInFrpScope(block) -} +/** + * Returns a [Transactional] that acts as a deferred-reference to the [Transactional] produced by + * [block]. + * + * When the returned [Transactional] is accessed by the Kairos network, [block] will be invoked and + * the returned [Transactional] will be used. + * + * Useful for recursive definitions. + */ +@ExperimentalKairosApi +fun <A> deferredTransactional(block: KairosScope.() -> Transactional<A>): Transactional<A> = + deferInline { + NoScope.block() + } private inline fun <A> deferInline( crossinline block: InitScope.() -> Transactional<A> ): Transactional<A> = - Transactional(TStateInit(init(name = null) { block().impl.init.connect(evalScope = this) })) + Transactional(StateInit(init(name = null) { block().impl.init.connect(evalScope = this) })) /** * Returns a [Transactional]. The passed [block] will be evaluated on demand at most once per * transaction; any subsequent sampling within the same transaction will receive a cached value. */ -@ExperimentalFrpApi -fun <A> transactionally(block: FrpTransactionScope.() -> A): Transactional<A> = - Transactional(tStateOf(transactionalImpl { runInTransactionScope(block) })) +@ExperimentalKairosApi +fun <A> transactionally(block: TransactionScope.() -> A): Transactional<A> = + Transactional(stateOf(transactionalImpl { block() })) diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/debug/Debug.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/debug/Debug.kt index 6f9612fab70a..d43a0bbf433e 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/debug/Debug.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/debug/Debug.kt @@ -16,18 +16,18 @@ package com.android.systemui.kairos.debug -import com.android.systemui.kairos.MutableTState -import com.android.systemui.kairos.TState -import com.android.systemui.kairos.TStateInit -import com.android.systemui.kairos.TStateLoop +import com.android.systemui.kairos.MutableState +import com.android.systemui.kairos.State +import com.android.systemui.kairos.StateInit +import com.android.systemui.kairos.StateLoop import com.android.systemui.kairos.internal.DerivedFlatten import com.android.systemui.kairos.internal.DerivedMap import com.android.systemui.kairos.internal.DerivedMapCheap import com.android.systemui.kairos.internal.DerivedZipped import com.android.systemui.kairos.internal.Init -import com.android.systemui.kairos.internal.TStateDerived -import com.android.systemui.kairos.internal.TStateImpl -import com.android.systemui.kairos.internal.TStateSource +import com.android.systemui.kairos.internal.StateDerived +import com.android.systemui.kairos.internal.StateImpl +import com.android.systemui.kairos.internal.StateSource import com.android.systemui.kairos.util.Just import com.android.systemui.kairos.util.Maybe import com.android.systemui.kairos.util.None @@ -87,12 +87,12 @@ data class Edge(val upstream: Any, val downstream: Any, val tag: Any? = null) data class Graph<T>(val nodes: Map<Any, T>, val edges: List<Edge>) -internal fun TState<*>.dump(infoMap: MutableMap<Any, InitInfo>, edges: MutableList<Edge>) { - val init: Init<TStateImpl<Any?>> = +internal fun State<*>.dump(infoMap: MutableMap<Any, InitInfo>, edges: MutableList<Edge>) { + val init: Init<StateImpl<Any?>> = when (this) { - is TStateInit -> init - is TStateLoop -> init - is MutableTState -> tState.init + is StateInit -> init + is StateLoop -> init + is MutableState -> state.init } when (val stateMaybe = init.getUnsafe()) { None -> { @@ -104,12 +104,12 @@ internal fun TState<*>.dump(infoMap: MutableMap<Any, InitInfo>, edges: MutableLi } } -internal fun TStateImpl<*>.dump(infoById: MutableMap<Any, InitInfo>, edges: MutableList<Edge>) { +internal fun StateImpl<*>.dump(infoById: MutableMap<Any, InitInfo>, edges: MutableList<Edge>) { val state = this if (state in infoById) return val stateInfo = when (state) { - is TStateDerived -> { + is StateDerived -> { val type = when (state) { is DerivedFlatten -> { @@ -151,7 +151,7 @@ internal fun TStateImpl<*>.dump(infoById: MutableMap<Any, InitInfo>, edges: Muta state.invalidatedEpoch, ) } - is TStateSource -> + is StateSource -> Source( state.name ?: state.operatorName, state.getStorageUnsafe(), @@ -174,30 +174,30 @@ internal fun TStateImpl<*>.dump(infoById: MutableMap<Any, InitInfo>, edges: Muta infoById[state] = Initialized(stateInfo) } -private fun <A> TStateImpl<A>.getUnsafe(): Maybe<A> = +private fun <A> StateImpl<A>.getUnsafe(): Maybe<A> = when (this) { - is TStateDerived -> getCachedUnsafe() - is TStateSource -> getStorageUnsafe() + is StateDerived -> getCachedUnsafe() + is StateSource -> getStorageUnsafe() is DerivedMapCheap<*, *> -> none } -private fun <A> TStateImpl<A>.getUnsafeWithEpoch(): Maybe<Pair<A, Long>> = +private fun <A> StateImpl<A>.getUnsafeWithEpoch(): Maybe<Pair<A, Long>> = when (this) { - is TStateDerived -> getCachedUnsafe().map { it to invalidatedEpoch } - is TStateSource -> getStorageUnsafe().map { it to writeEpoch } + is StateDerived -> getCachedUnsafe().map { it to invalidatedEpoch } + is StateSource -> getStorageUnsafe().map { it to writeEpoch } is DerivedMapCheap<*, *> -> none } /** - * Returns the current value held in this [TState], or [none] if the [TState] has not been + * Returns the current value held in this [State], or [none] if the [State] has not been * initialized. * * The returned [Long] is the *epoch* at which the internal cache was last updated. This can be used * to identify values which are out-of-date. */ -fun <A> TState<A>.sampleUnsafe(): Maybe<Pair<A, Long>> = +fun <A> State<A>.sampleUnsafe(): Maybe<Pair<A, Long>> = when (this) { - is MutableTState -> tState.init.getUnsafe().flatMap { it.getUnsafeWithEpoch() } - is TStateInit -> init.getUnsafe().flatMap { it.getUnsafeWithEpoch() } - is TStateLoop -> this.init.getUnsafe().flatMap { it.getUnsafeWithEpoch() } + is MutableState -> state.init.getUnsafe().flatMap { it.getUnsafeWithEpoch() } + is StateInit -> init.getUnsafe().flatMap { it.getUnsafeWithEpoch() } + is StateLoop -> this.init.getUnsafe().flatMap { it.getUnsafeWithEpoch() } } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/BuildScopeImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/BuildScopeImpl.kt index 14488a3131c7..07dc1dd2c79c 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/BuildScopeImpl.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/BuildScopeImpl.kt @@ -16,21 +16,20 @@ package com.android.systemui.kairos.internal -import com.android.systemui.kairos.CoalescingMutableTFlow -import com.android.systemui.kairos.FrpBuildScope -import com.android.systemui.kairos.FrpCoalescingProducerScope -import com.android.systemui.kairos.FrpDeferredValue -import com.android.systemui.kairos.FrpEffectScope -import com.android.systemui.kairos.FrpNetwork -import com.android.systemui.kairos.FrpProducerScope -import com.android.systemui.kairos.FrpSpec -import com.android.systemui.kairos.FrpStateScope -import com.android.systemui.kairos.FrpTransactionScope -import com.android.systemui.kairos.GroupedTFlow -import com.android.systemui.kairos.LocalFrpNetwork -import com.android.systemui.kairos.MutableTFlow -import com.android.systemui.kairos.TFlow -import com.android.systemui.kairos.TFlowInit +import com.android.systemui.kairos.BuildScope +import com.android.systemui.kairos.BuildSpec +import com.android.systemui.kairos.CoalescingEventProducerScope +import com.android.systemui.kairos.CoalescingMutableEvents +import com.android.systemui.kairos.DeferredValue +import com.android.systemui.kairos.EffectScope +import com.android.systemui.kairos.EventProducerScope +import com.android.systemui.kairos.Events +import com.android.systemui.kairos.EventsInit +import com.android.systemui.kairos.GroupedEvents +import com.android.systemui.kairos.KairosNetwork +import com.android.systemui.kairos.LocalNetwork +import com.android.systemui.kairos.MutableEvents +import com.android.systemui.kairos.TransactionScope import com.android.systemui.kairos.groupByKey import com.android.systemui.kairos.init import com.android.systemui.kairos.internal.util.childScope @@ -44,7 +43,6 @@ import com.android.systemui.kairos.util.just import com.android.systemui.kairos.util.map import java.util.concurrent.atomic.AtomicReference import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext import kotlinx.coroutines.CompletableJob import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -52,109 +50,73 @@ import kotlinx.coroutines.cancel import kotlinx.coroutines.job internal class BuildScopeImpl(val stateScope: StateScopeImpl, val coroutineScope: CoroutineScope) : - BuildScope, StateScope by stateScope { + InternalBuildScope, InternalStateScope by stateScope { private val job: Job get() = coroutineScope.coroutineContext.job - override val frpScope: FrpBuildScope = FrpBuildScopeImpl() - - override fun <R> runInBuildScope(block: FrpBuildScope.() -> R): R = frpScope.block() - - private fun <A, T : TFlow<A>, S> buildTFlow( - name: String? = null, - constructFlow: (InputNode<A>) -> Pair<T, S>, - builder: suspend S.() -> Unit, - ): TFlow<A> { - var job: Job? = null - val stopEmitter = newStopEmitter("buildTFlow[$name]") - // Create a child scope that will be kept alive beyond the end of this transaction. - val childScope = coroutineScope.childScope() - lateinit var emitter: Pair<T, S> - val inputNode = - InputNode<A>( - activate = { - // It's possible that activation occurs after all effects have been run, due - // to a MuxDeferred switch-in. For this reason, we need to activate in a new - // transaction. - check(job == null) { "[$name] already activated" } - job = - childScope.launchImmediate { - network - .transaction("buildTFlow") { - reenterBuildScope(this@BuildScopeImpl, childScope) - .runInBuildScope { - launchEffect { - builder(emitter.second) - stopEmitter.emit(Unit) - } - } - } - .await() - .join() - } - }, - deactivate = { - checkNotNull(job) { "[$name] already deactivated" }.cancel() - job = null - }, - ) - emitter = constructFlow(inputNode) - return with(frpScope) { emitter.first.takeUntil(mergeLeft(stopEmitter, endSignal)) } + override val kairosNetwork: KairosNetwork by lazy { + LocalNetwork(network, coroutineScope, endSignal) } - private fun <T> tFlowInternal( + override fun <T> events( name: String?, - builder: suspend FrpProducerScope<T>.() -> Unit, - ): TFlow<T> = - buildTFlow( + builder: suspend EventProducerScope<T>.() -> Unit, + ): Events<T> = + buildEvents( name, - constructFlow = { inputNode -> - val flow = MutableTFlow(network, inputNode) - flow to - object : FrpProducerScope<T> { + constructEvents = { inputNode -> + val events = MutableEvents(network, inputNode) + events to + object : EventProducerScope<T> { override suspend fun emit(value: T) { - flow.emit(value) + events.emit(value) } } }, builder = builder, ) - private fun <In, Out> coalescingTFlowInternal( + override fun <In, Out> coalescingEvents( getInitialValue: () -> Out, coalesce: (old: Out, new: In) -> Out, - builder: suspend FrpCoalescingProducerScope<In>.() -> Unit, - ): TFlow<Out> = - buildTFlow( - constructFlow = { inputNode -> - val flow = - CoalescingMutableTFlow(null, coalesce, network, getInitialValue, inputNode) - flow to - object : FrpCoalescingProducerScope<In> { + builder: suspend CoalescingEventProducerScope<In>.() -> Unit, + ): Events<Out> = + buildEvents( + constructEvents = { inputNode -> + val events = + CoalescingMutableEvents( + null, + coalesce = { old, new: In -> coalesce(old.value, new) }, + network, + getInitialValue, + inputNode, + ) + events to + object : CoalescingEventProducerScope<In> { override fun emit(value: In) { - flow.emit(value) + events.emit(value) } } }, builder = builder, ) - private fun <A> asyncScopeInternal(block: FrpSpec<A>): Pair<FrpDeferredValue<A>, Job> { + override fun <A> asyncScope(block: BuildSpec<A>): Pair<DeferredValue<A>, Job> { val childScope = mutableChildBuildScope() - return FrpDeferredValue(deferAsync { childScope.runInBuildScope(block) }) to childScope.job + return DeferredValue(deferAsync { block(childScope) }) to childScope.job } - private fun <R> deferredInternal(block: FrpBuildScope.() -> R): FrpDeferredValue<R> = - FrpDeferredValue(deferAsync { runInBuildScope(block) }) + override fun <R> deferredBuildScope(block: BuildScope.() -> R): DeferredValue<R> = + DeferredValue(deferAsync { block() }) - private fun deferredActionInternal(block: FrpBuildScope.() -> Unit) { - deferAction { runInBuildScope(block) } + override fun deferredBuildScopeAction(block: BuildScope.() -> Unit) { + deferAction { block() } } - private fun <A> TFlow<A>.observeEffectInternal( - context: CoroutineContext, - block: FrpEffectScope.(A) -> Unit, + override fun <A> Events<A>.observe( + coroutineContext: CoroutineContext, + block: EffectScope.(A) -> Unit, ): Job { val subRef = AtomicReference<Maybe<Output<A>>>(null) val childScope = coroutineScope.childScope() @@ -169,26 +131,26 @@ internal class BuildScopeImpl(val stateScope: StateScopeImpl, val coroutineScope } } } + val localNetwork = LocalNetwork(network, childScope, endSignal) // Defer so that we don't suspend the caller deferAction { val outputNode = Output<A>( - context = context, + context = coroutineContext, onDeath = { subRef.getAndSet(None)?.let { childScope.cancel() } }, onEmit = { output -> if (subRef.get() is Just) { // Not cancelled, safe to emit val scope = - object : FrpEffectScope, FrpTransactionScope by frpScope { - override val frpCoroutineScope: CoroutineScope = childScope - override val frpNetwork: FrpNetwork = - LocalFrpNetwork(network, childScope, endSignal) + object : EffectScope, TransactionScope by this@BuildScopeImpl { + override val effectCoroutineScope: CoroutineScope = childScope + override val kairosNetwork: KairosNetwork = localNetwork } - scope.block(output) + block(scope, output) } }, ) - with(frpScope) { this@observeEffectInternal.takeUntil(endSignal) } + this@observe.takeUntil(endSignal) .init .connect(evalScope = stateScope.evalScope) .activate(evalScope = stateScope.evalScope, outputNode.schedulable) @@ -205,63 +167,103 @@ internal class BuildScopeImpl(val stateScope: StateScopeImpl, val coroutineScope return childScope.coroutineContext.job } - private fun <A, B> TFlow<A>.mapBuildInternal(transform: FrpBuildScope.(A) -> B): TFlow<B> { + override fun <A, B> Events<A>.mapBuild(transform: BuildScope.(A) -> B): Events<B> { val childScope = coroutineScope.childScope() - return TFlowInit( + return EventsInit( constInit( "mapBuild", mapImpl({ init.connect(evalScope = this) }) { spec, _ -> reenterBuildScope(outerScope = this@BuildScopeImpl, childScope) - .runInBuildScope { transform(spec) } + .transform(spec) } .cached(), ) ) } - private fun <K, A, B> TFlow<Map<K, Maybe<FrpSpec<A>>>>.applyLatestForKeyInternal( - init: FrpDeferredValue<Map<K, FrpSpec<B>>>, + override fun <K, A, B> Events<Map<K, Maybe<BuildSpec<A>>>>.applyLatestSpecForKey( + initialSpecs: DeferredValue<Map<K, BuildSpec<B>>>, numKeys: Int?, - ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>> { - val eventsByKey: GroupedTFlow<K, Maybe<FrpSpec<A>>> = groupByKey(numKeys) + ): Pair<Events<Map<K, Maybe<A>>>, DeferredValue<Map<K, B>>> { + val eventsByKey: GroupedEvents<K, Maybe<BuildSpec<A>>> = groupByKey(numKeys) val initOut: Lazy<Map<K, B>> = deferAsync { - init.unwrapped.value.mapValues { (k, spec) -> + initialSpecs.unwrapped.value.mapValues { (k, spec) -> val newEnd = eventsByKey[k] val newScope = childBuildScope(newEnd) - newScope.runInBuildScope(spec) + newScope.spec() } } val childScope = coroutineScope.childScope() - val changesNode: TFlowImpl<Map<K, Maybe<A>>> = - mapImpl(upstream = { this@applyLatestForKeyInternal.init.connect(evalScope = this) }) { + val changesNode: EventsImpl<Map<K, Maybe<A>>> = + mapImpl(upstream = { this@applyLatestSpecForKey.init.connect(evalScope = this) }) { upstreamMap, _ -> reenterBuildScope(this@BuildScopeImpl, childScope).run { - upstreamMap.mapValues { (k: K, ma: Maybe<FrpSpec<A>>) -> + upstreamMap.mapValues { (k: K, ma: Maybe<BuildSpec<A>>) -> ma.map { spec -> - val newEnd = with(frpScope) { eventsByKey[k].skipNext() } + val newEnd = eventsByKey[k].skipNext() val newScope = childBuildScope(newEnd) - newScope.runInBuildScope(spec) + newScope.spec() } } } } - val changes: TFlow<Map<K, Maybe<A>>> = - TFlowInit(constInit("applyLatestForKey", changesNode.cached())) + val changes: Events<Map<K, Maybe<A>>> = + EventsInit(constInit("applyLatestForKey", changesNode.cached())) // Ensure effects are observed; otherwise init will stay alive longer than expected - changes.observeEffectInternal(EmptyCoroutineContext) {} - return changes to FrpDeferredValue(initOut) + changes.observe() + return changes to DeferredValue(initOut) + } + + private fun <A, T : Events<A>, S> buildEvents( + name: String? = null, + constructEvents: (InputNode<A>) -> Pair<T, S>, + builder: suspend S.() -> Unit, + ): Events<A> { + var job: Job? = null + val stopEmitter = newStopEmitter("buildEvents[$name]") + // Create a child scope that will be kept alive beyond the end of this transaction. + val childScope = coroutineScope.childScope() + lateinit var emitter: Pair<T, S> + val inputNode = + InputNode<A>( + activate = { + // It's possible that activation occurs after all effects have been run, due + // to a MuxDeferred switch-in. For this reason, we need to activate in a new + // transaction. + check(job == null) { "[$name] already activated" } + job = + childScope.launchImmediate { + network + .transaction("buildEvents") { + reenterBuildScope(this@BuildScopeImpl, childScope) + .launchEffect { + builder(emitter.second) + stopEmitter.emit(Unit) + } + } + .await() + .join() + } + }, + deactivate = { + checkNotNull(job) { "[$name] already deactivated" }.cancel() + job = null + }, + ) + emitter = constructEvents(inputNode) + return emitter.first.takeUntil(mergeLeft(stopEmitter, endSignal)) } - private fun newStopEmitter(name: String): CoalescingMutableTFlow<Unit, Unit> = - CoalescingMutableTFlow( + private fun newStopEmitter(name: String): CoalescingMutableEvents<Unit, Unit> = + CoalescingMutableEvents( name = name, coalesce = { _, _: Unit -> }, network = network, getInitialValue = {}, ) - private fun childBuildScope(newEnd: TFlow<Any>): BuildScopeImpl { + private fun childBuildScope(newEnd: Events<Any>): BuildScopeImpl { val newCoroutineScope: CoroutineScope = coroutineScope.childScope() return BuildScopeImpl( stateScope = stateScope.childStateScope(newEnd), @@ -276,7 +278,7 @@ internal class BuildScopeImpl(val stateScope: StateScopeImpl, val coroutineScope (newCoroutineScope.coroutineContext.job as CompletableJob).complete() } ) - runInBuildScope { endSignalOnce.observe { newCoroutineScope.cancel() } } + endSignalOnce.observe { newCoroutineScope.cancel() } } } @@ -299,47 +301,6 @@ internal class BuildScopeImpl(val stateScope: StateScopeImpl, val coroutineScope coroutineScope = childScope, ) } - - private inner class FrpBuildScopeImpl : FrpBuildScope, FrpStateScope by stateScope.frpScope { - - override val frpNetwork: FrpNetwork by lazy { - LocalFrpNetwork(network, coroutineScope, endSignal) - } - - override fun <T> tFlow( - name: String?, - builder: suspend FrpProducerScope<T>.() -> Unit, - ): TFlow<T> = tFlowInternal(name, builder) - - override fun <In, Out> coalescingTFlow( - getInitialValue: () -> Out, - coalesce: (old: Out, new: In) -> Out, - builder: suspend FrpCoalescingProducerScope<In>.() -> Unit, - ): TFlow<Out> = coalescingTFlowInternal(getInitialValue, coalesce, builder) - - override fun <A> asyncScope(block: FrpSpec<A>): Pair<FrpDeferredValue<A>, Job> = - asyncScopeInternal(block) - - override fun <R> deferredBuildScope(block: FrpBuildScope.() -> R): FrpDeferredValue<R> = - deferredInternal(block) - - override fun deferredBuildScopeAction(block: FrpBuildScope.() -> Unit) = - deferredActionInternal(block) - - override fun <A> TFlow<A>.observe( - coroutineContext: CoroutineContext, - block: FrpEffectScope.(A) -> Unit, - ): Job = observeEffectInternal(coroutineContext, block) - - override fun <A, B> TFlow<A>.mapBuild(transform: FrpBuildScope.(A) -> B): TFlow<B> = - mapBuildInternal(transform) - - override fun <K, A, B> TFlow<Map<K, Maybe<FrpSpec<A>>>>.applyLatestSpecForKey( - initialSpecs: FrpDeferredValue<Map<K, FrpSpec<B>>>, - numKeys: Int?, - ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>> = - applyLatestForKeyInternal(initialSpecs, numKeys) - } } private fun EvalScope.reenterBuildScope( diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Demux.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Demux.kt index b71a245c71a2..d19a47eb873e 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Demux.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Demux.kt @@ -205,7 +205,7 @@ internal class DemuxNode<W, K, A>( } internal fun <W, K, A> DemuxImpl( - upstream: TFlowImpl<MapK<W, K, A>>, + upstream: EventsImpl<MapK<W, K, A>>, numKeys: Int?, storeFactory: MutableMapK.Factory<W, K>, ): DemuxImpl<K, A> = @@ -216,14 +216,14 @@ internal fun <W, K, A> DemuxImpl( ) internal fun <K, A> demuxMap( - upstream: EvalScope.() -> TFlowImpl<Map<K, A>>, + upstream: EvalScope.() -> EventsImpl<Map<K, A>>, numKeys: Int?, ): DemuxImpl<K, A> = DemuxImpl(mapImpl(upstream) { it, _ -> MapHolder(it) }, numKeys, ConcurrentHashMapK.Factory()) internal class DemuxActivator<W, K, A>( private val numKeys: Int?, - private val upstream: TFlowImpl<MapK<W, K, A>>, + private val upstream: EventsImpl<MapK<W, K, A>>, private val storeFactory: MutableMapK.Factory<W, K>, ) { fun activate( @@ -246,7 +246,7 @@ internal class DemuxActivator<W, K, A>( } internal class DemuxImpl<in K, out A>(private val dmux: DemuxLifecycle<K, A>) { - fun eventsForKey(key: K): TFlowImpl<A> = TFlowCheap { downstream -> + fun eventsForKey(key: K): EventsImpl<A> = EventsImplCheap { downstream -> dmux.activate(evalScope = this, key)?.let { (branchNode, needsEval) -> branchNode.addDownstream(downstream) val branchNeedsEval = needsEval && branchNode.hasCurrentValue(0, evalScope = this) diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/EvalScopeImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/EvalScopeImpl.kt index 9ecfbba7d647..80a294819fac 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/EvalScopeImpl.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/EvalScopeImpl.kt @@ -16,49 +16,54 @@ package com.android.systemui.kairos.internal -import com.android.systemui.kairos.FrpDeferredValue -import com.android.systemui.kairos.FrpTransactionScope -import com.android.systemui.kairos.TFlow -import com.android.systemui.kairos.TFlowInit -import com.android.systemui.kairos.TFlowLoop -import com.android.systemui.kairos.TState -import com.android.systemui.kairos.TStateInit +import com.android.systemui.kairos.DeferredValue +import com.android.systemui.kairos.Events +import com.android.systemui.kairos.EventsInit +import com.android.systemui.kairos.EventsLoop +import com.android.systemui.kairos.State +import com.android.systemui.kairos.StateInit +import com.android.systemui.kairos.TransactionScope import com.android.systemui.kairos.Transactional -import com.android.systemui.kairos.emptyTFlow +import com.android.systemui.kairos.emptyEvents import com.android.systemui.kairos.init import com.android.systemui.kairos.mapCheap -import com.android.systemui.kairos.switch +import com.android.systemui.kairos.switchEvents internal class EvalScopeImpl(networkScope: NetworkScope, deferScope: DeferScope) : - EvalScope, NetworkScope by networkScope, DeferScope by deferScope { + EvalScope, NetworkScope by networkScope, DeferScope by deferScope, TransactionScope { - private fun <A> Transactional<A>.sample(): A = impl.sample().sample(this@EvalScopeImpl).value + override fun <A> Transactional<A>.sampleDeferred(): DeferredValue<A> = + DeferredValue(deferAsync { impl.sample().sample(this@EvalScopeImpl).value }) - private fun <A> TState<A>.sample(): A = - init.connect(evalScope = this@EvalScopeImpl).getCurrentWithEpoch(this@EvalScopeImpl).first + override fun <A> State<A>.sampleDeferred(): DeferredValue<A> = + DeferredValue( + deferAsync { + init + .connect(evalScope = this@EvalScopeImpl) + .getCurrentWithEpoch(this@EvalScopeImpl) + .first + } + ) - private val <A> Transactional<A>.deferredValue: FrpDeferredValue<A> - get() = FrpDeferredValue(deferAsync { sample() }) + override fun <R> deferredTransactionScope(block: TransactionScope.() -> R): DeferredValue<R> = + DeferredValue(deferAsync { block() }) - private val <A> TState<A>.deferredValue: FrpDeferredValue<A> - get() = FrpDeferredValue(deferAsync { sample() }) - - private val nowInternal: TFlow<Unit> by lazy { - var result by TFlowLoop<Unit>() + override val now: Events<Unit> by lazy { + var result by EventsLoop<Unit>() result = - TStateInit( + StateInit( constInit( "now", - activatedTStateSource( + activatedStateSource( "now", "now", this, - { result.mapCheap { emptyTFlow }.init.connect(evalScope = this) }, + { result.mapCheap { emptyEvents }.init.connect(evalScope = this) }, CompletableLazy( - TFlowInit( + EventsInit( constInit( "now", - TFlowCheap { + EventsImplCheap { ActivationResult( connection = NodeConnection(AlwaysNode, AlwaysNode), needsEval = true, @@ -70,27 +75,7 @@ internal class EvalScopeImpl(networkScope: NetworkScope, deferScope: DeferScope) ), ) ) - .switch() + .switchEvents() result } - - private fun <R> deferredInternal(block: FrpTransactionScope.() -> R): FrpDeferredValue<R> = - FrpDeferredValue(deferAsync { runInTransactionScope(block) }) - - override fun <R> runInTransactionScope(block: FrpTransactionScope.() -> R): R = frpScope.block() - - override val frpScope: FrpTransactionScope = FrpTransactionScopeImpl() - - inner class FrpTransactionScopeImpl : FrpTransactionScope { - override fun <A> Transactional<A>.sampleDeferred(): FrpDeferredValue<A> = deferredValue - - override fun <A> TState<A>.sampleDeferred(): FrpDeferredValue<A> = deferredValue - - override fun <R> deferredTransactionScope( - block: FrpTransactionScope.() -> R - ): FrpDeferredValue<R> = deferredInternal(block) - - override val now: TFlow<Unit> - get() = nowInternal - } } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/TFlowImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/EventsImpl.kt index 47a585abac5f..e7978b8bc5ea 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/TFlowImpl.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/EventsImpl.kt @@ -17,7 +17,7 @@ package com.android.systemui.kairos.internal /* Initialized TFlow */ -internal fun interface TFlowImpl<out A> { +internal fun interface EventsImpl<out A> { fun activate(evalScope: EvalScope, downstream: Schedulable): ActivationResult<A>? } @@ -26,8 +26,8 @@ internal data class ActivationResult<out A>( val needsEval: Boolean, ) -internal inline fun <A> TFlowCheap(crossinline cheap: CheapNodeSubscribe<A>) = - TFlowImpl { scope, ds -> +internal inline fun <A> EventsImplCheap(crossinline cheap: CheapNodeSubscribe<A>) = + EventsImpl { scope, ds -> scope.cheap(ds) } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/FilterNode.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/FilterNode.kt index 30c1a865f50a..d38bf21c5db1 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/FilterNode.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/FilterNode.kt @@ -24,8 +24,8 @@ import com.android.systemui.kairos.util.just import com.android.systemui.kairos.util.none internal inline fun <A> filterJustImpl( - crossinline getPulse: EvalScope.() -> TFlowImpl<Maybe<A>> -): TFlowImpl<A> = + crossinline getPulse: EvalScope.() -> EventsImpl<Maybe<A>> +): EventsImpl<A> = DemuxImpl( mapImpl(getPulse) { maybeResult, _ -> if (maybeResult is Just) { @@ -40,9 +40,9 @@ internal inline fun <A> filterJustImpl( .eventsForKey(Unit) internal inline fun <A> filterImpl( - crossinline getPulse: EvalScope.() -> TFlowImpl<A>, + crossinline getPulse: EvalScope.() -> EventsImpl<A>, crossinline f: EvalScope.(A) -> Boolean, -): TFlowImpl<A> { +): EventsImpl<A> { val mapped = mapImpl(getPulse) { it, _ -> if (f(it)) just(it) else none }.cached() return filterJustImpl { mapped } } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Graph.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Graph.kt index 667002bd413c..b956e44e0618 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Graph.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Graph.kt @@ -340,7 +340,7 @@ internal class DepthTracker { internal class DownstreamSet { val outputs = HashSet<Output<*>>() - val stateWriters = mutableListOf<TStateSource<*>>() + val stateWriters = mutableListOf<StateSource<*>>() val muxMovers = HashSet<MuxDeferredNode<*, *, *>>() val nodes = HashSet<SchedulableNode>() @@ -459,7 +459,7 @@ internal class DownstreamSet { // TODO: remove this indirection internal sealed interface Schedulable { - data class S constructor(val state: TStateSource<*>) : Schedulable + data class S constructor(val state: StateSource<*>) : Schedulable data class M constructor(val muxMover: MuxDeferredNode<*, *, *>) : Schedulable diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Inputs.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Inputs.kt index 1dcba4433a8d..d7cbe8087f52 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Inputs.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Inputs.kt @@ -88,7 +88,7 @@ internal class InputNode<A>( } } -internal fun <A> InputNode<A>.activated() = TFlowCheap { downstream -> +internal fun <A> InputNode<A>.activated() = EventsImplCheap { downstream -> val input = this@activated addDownstreamAndActivateIfNeeded(downstream, evalScope = this) ActivationResult( diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/InternalScopes.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/InternalScopes.kt index 62bf34810de7..cd2214370d62 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/InternalScopes.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/InternalScopes.kt @@ -16,38 +16,25 @@ package com.android.systemui.kairos.internal -import com.android.systemui.kairos.FrpBuildScope -import com.android.systemui.kairos.FrpStateScope -import com.android.systemui.kairos.FrpTransactionScope -import com.android.systemui.kairos.TFlow +import com.android.systemui.kairos.BuildScope +import com.android.systemui.kairos.Events +import com.android.systemui.kairos.StateScope +import com.android.systemui.kairos.TransactionScope internal interface InitScope { val networkId: Any } -internal interface EvalScope : NetworkScope, DeferScope { - val frpScope: FrpTransactionScope +internal interface EvalScope : NetworkScope, DeferScope, TransactionScope - fun <R> runInTransactionScope(block: FrpTransactionScope.() -> R): R -} - -internal interface StateScope : EvalScope { - override val frpScope: FrpStateScope - - fun <R> runInStateScope(block: FrpStateScope.() -> R): R - - val endSignal: TFlow<Any> +internal interface InternalStateScope : EvalScope, StateScope { + val endSignal: Events<Any> + val endSignalOnce: Events<Any> - fun childStateScope(newEnd: TFlow<Any>): StateScope - - val endSignalOnce: TFlow<Any> + fun childStateScope(newEnd: Events<Any>): InternalStateScope } -internal interface BuildScope : StateScope { - override val frpScope: FrpBuildScope - - fun <R> runInBuildScope(block: FrpBuildScope.() -> R): R -} +internal interface InternalBuildScope : InternalStateScope, BuildScope internal interface NetworkScope : InitScope { @@ -63,7 +50,7 @@ internal interface NetworkScope : InitScope { fun scheduleMuxMover(muxMover: MuxDeferredNode<*, *, *>) - fun schedule(state: TStateSource<*>) + fun schedule(state: StateSource<*>) fun scheduleDeactivation(node: PushNode<*>) diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Mux.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Mux.kt index 1cdf895ec1ed..268fec4fa486 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Mux.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Mux.kt @@ -272,9 +272,9 @@ internal typealias BranchNode<W, K, V> = MuxNode<W, K, V>.BranchNode /** Tracks lifecycle of MuxNode in the network. Essentially a mutable ref for MuxLifecycleState. */ internal class MuxLifecycle<W, K, V>(var lifecycleState: MuxLifecycleState<W, K, V>) : - TFlowImpl<MuxResult<W, K, V>> { + EventsImpl<MuxResult<W, K, V>> { - override fun toString(): String = "TFlowMuxLifecycle[$hashString][$lifecycleState]" + override fun toString(): String = "MuxLifecycle[$hashString][$lifecycleState]" override fun activate( evalScope: EvalScope, @@ -332,9 +332,9 @@ internal interface MuxActivator<W, K, V> { internal inline fun <W, K, V> MuxLifecycle( onSubscribe: MuxActivator<W, K, V> -): TFlowImpl<MuxResult<W, K, V>> = MuxLifecycle(MuxLifecycleState.Inactive(onSubscribe)) +): EventsImpl<MuxResult<W, K, V>> = MuxLifecycle(MuxLifecycleState.Inactive(onSubscribe)) -internal fun <K, V> TFlowImpl<MuxResult<MapHolder.W, K, V>>.awaitValues(): TFlowImpl<Map<K, V>> = +internal fun <K, V> EventsImpl<MuxResult<MapHolder.W, K, V>>.awaitValues(): EventsImpl<Map<K, V>> = mapImpl({ this@awaitValues }) { results, logIndent -> results.asMapHolder().unwrapped.mapValues { it.value.getPushEvent(logIndent, this) } } @@ -343,15 +343,15 @@ internal fun <K, V> TFlowImpl<MuxResult<MapHolder.W, K, V>>.awaitValues(): TFlow internal fun <W, K, V> MuxNode<W, K, V>.initializeUpstream( evalScope: EvalScope, - getStorage: EvalScope.() -> Iterable<Map.Entry<K, TFlowImpl<V>>>, + getStorage: EvalScope.() -> Iterable<Map.Entry<K, EventsImpl<V>>>, storeFactory: MutableMapK.Factory<W, K>, ) { val storage = getStorage(evalScope) val initUpstream = buildList { - storage.forEach { (key, flow) -> + storage.forEach { (key, events) -> val branchNode = BranchNode(key) add( - flow.activate(evalScope, branchNode.schedulable)?.let { (conn, needsEval) -> + events.activate(evalScope, branchNode.schedulable)?.let { (conn, needsEval) -> Triple( key, branchNode.apply { upstream = conn }, diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxDeferred.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxDeferred.kt index 5ce0248d0655..53a6ecabda6a 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxDeferred.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxDeferred.kt @@ -48,8 +48,8 @@ internal class MuxDeferredNode<W, K, V>( ) : MuxNode<W, K, V>(lifecycle, factory) { val schedulable = Schedulable.M(this) - var patches: NodeConnection<Iterable<Map.Entry<K, Maybe<TFlowImpl<V>>>>>? = null - var patchData: Iterable<Map.Entry<K, Maybe<TFlowImpl<V>>>>? = null + var patches: NodeConnection<Iterable<Map.Entry<K, Maybe<EventsImpl<V>>>>>? = null + var patchData: Iterable<Map.Entry<K, Maybe<EventsImpl<V>>>>? = null override fun visit(logIndent: Int, evalScope: EvalScope) { check(epoch < evalScope.epoch) { "node unexpectedly visited multiple times in transaction" } @@ -129,7 +129,7 @@ internal class MuxDeferredNode<W, K, V>( // TODO: this logic is very similar to what's in MuxPrompt, maybe turn into an inline fun? // We have a patch, process additions/updates and removals - val adds = mutableListOf<Pair<K, TFlowImpl<V>>>() + val adds = mutableListOf<Pair<K, EventsImpl<V>>>() val removes = mutableListOf<K>() patch.forEach { (k, newUpstream) -> when (newUpstream) { @@ -151,7 +151,7 @@ internal class MuxDeferredNode<W, K, V>( } // add or replace - adds.forEach { (k, newUpstream: TFlowImpl<V>) -> + adds.forEach { (k, newUpstream: EventsImpl<V>) -> // remove old and sever, if present switchedIn.remove(k)?.let { branchNode -> val conn = branchNode.upstream @@ -279,9 +279,9 @@ internal class MuxDeferredNode<W, K, V>( internal inline fun <A> switchDeferredImplSingle( name: String? = null, - crossinline getStorage: EvalScope.() -> TFlowImpl<A>, - crossinline getPatches: EvalScope.() -> TFlowImpl<TFlowImpl<A>>, -): TFlowImpl<A> { + crossinline getStorage: EvalScope.() -> EventsImpl<A>, + crossinline getPatches: EvalScope.() -> EventsImpl<EventsImpl<A>>, +): EventsImpl<A> { val patches = mapImpl(getPatches) { newFlow, _ -> singleOf(just(newFlow)).asIterable() } val switchDeferredImpl = switchDeferredImpl( @@ -301,17 +301,17 @@ internal inline fun <A> switchDeferredImplSingle( internal fun <W, K, V> switchDeferredImpl( name: String? = null, - getStorage: EvalScope.() -> Iterable<Map.Entry<K, TFlowImpl<V>>>, - getPatches: EvalScope.() -> TFlowImpl<Iterable<Map.Entry<K, Maybe<TFlowImpl<V>>>>>, + getStorage: EvalScope.() -> Iterable<Map.Entry<K, EventsImpl<V>>>, + getPatches: EvalScope.() -> EventsImpl<Iterable<Map.Entry<K, Maybe<EventsImpl<V>>>>>, storeFactory: MutableMapK.Factory<W, K>, -): TFlowImpl<MuxResult<W, K, V>> = +): EventsImpl<MuxResult<W, K, V>> = MuxLifecycle(MuxDeferredActivator(name, getStorage, storeFactory, getPatches)) private class MuxDeferredActivator<W, K, V>( private val name: String?, - private val getStorage: EvalScope.() -> Iterable<Map.Entry<K, TFlowImpl<V>>>, + private val getStorage: EvalScope.() -> Iterable<Map.Entry<K, EventsImpl<V>>>, private val storeFactory: MutableMapK.Factory<W, K>, - private val getPatches: EvalScope.() -> TFlowImpl<Iterable<Map.Entry<K, Maybe<TFlowImpl<V>>>>>, + private val getPatches: EvalScope.() -> EventsImpl<Iterable<Map.Entry<K, Maybe<EventsImpl<V>>>>>, ) : MuxActivator<W, K, V> { override fun activate( evalScope: EvalScope, @@ -381,11 +381,11 @@ private class MuxDeferredActivator<W, K, V>( } internal inline fun <A> mergeNodes( - crossinline getPulse: EvalScope.() -> TFlowImpl<A>, - crossinline getOther: EvalScope.() -> TFlowImpl<A>, + crossinline getPulse: EvalScope.() -> EventsImpl<A>, + crossinline getOther: EvalScope.() -> EventsImpl<A>, name: String? = null, crossinline f: EvalScope.(A, A) -> A, -): TFlowImpl<A> { +): EventsImpl<A> { val mergedThese = mergeNodes(name, getPulse, getOther) val merged = mapImpl({ mergedThese }) { these, _ -> these.merge { thiz, that -> f(thiz, that) } } @@ -397,9 +397,9 @@ internal fun <T> Iterable<T>.asIterableWithIndex(): Iterable<Map.Entry<Int, T>> internal inline fun <A, B> mergeNodes( name: String? = null, - crossinline getPulse: EvalScope.() -> TFlowImpl<A>, - crossinline getOther: EvalScope.() -> TFlowImpl<B>, -): TFlowImpl<These<A, B>> { + crossinline getPulse: EvalScope.() -> EventsImpl<A>, + crossinline getOther: EvalScope.() -> EventsImpl<B>, +): EventsImpl<These<A, B>> { val storage = listOf( mapImpl(getPulse) { it, _ -> These.thiz<A, B>(it) }, @@ -426,8 +426,8 @@ internal inline fun <A, B> mergeNodes( } internal inline fun <A> mergeNodes( - crossinline getPulses: EvalScope.() -> Iterable<TFlowImpl<A>> -): TFlowImpl<List<A>> { + crossinline getPulses: EvalScope.() -> Iterable<EventsImpl<A>> +): EventsImpl<List<A>> { val switchNode = switchDeferredImpl( getStorage = { getPulses().asIterableWithIndex() }, @@ -443,8 +443,8 @@ internal inline fun <A> mergeNodes( } internal inline fun <A> mergeNodesLeft( - crossinline getPulses: EvalScope.() -> Iterable<TFlowImpl<A>> -): TFlowImpl<A> { + crossinline getPulses: EvalScope.() -> Iterable<EventsImpl<A>> +): EventsImpl<A> { val switchNode = switchDeferredImpl( getStorage = { getPulses().asIterableWithIndex() }, diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxPrompt.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxPrompt.kt index 1c9af24a392f..785a98b105c5 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxPrompt.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/MuxPrompt.kt @@ -35,13 +35,13 @@ internal class MuxPromptNode<W, K, V>( factory: MutableMapK.Factory<W, K>, ) : MuxNode<W, K, V>(lifecycle, factory) { - var patchData: Iterable<Map.Entry<K, Maybe<TFlowImpl<V>>>>? = null + var patchData: Iterable<Map.Entry<K, Maybe<EventsImpl<V>>>>? = null var patches: PatchNode? = null override fun visit(logIndent: Int, evalScope: EvalScope) { check(epoch < evalScope.epoch) { "node unexpectedly visited multiple times in transaction" } logDuration(logIndent, "MuxPrompt.visit") { - val patch: Iterable<Map.Entry<K, Maybe<TFlowImpl<V>>>>? = patchData + val patch: Iterable<Map.Entry<K, Maybe<EventsImpl<V>>>>? = patchData patchData = null // If there's a patch, process it. @@ -85,12 +85,12 @@ internal class MuxPromptNode<W, K, V>( // side-effect: this will populate `upstreamData` with any immediately available results private fun LogIndent.processPatch( - patch: Iterable<Map.Entry<K, Maybe<TFlowImpl<V>>>>, + patch: Iterable<Map.Entry<K, Maybe<EventsImpl<V>>>>, evalScope: EvalScope, ): Boolean { var needsReschedule = false // We have a patch, process additions/updates and removals - val adds = mutableListOf<Pair<K, TFlowImpl<V>>>() + val adds = mutableListOf<Pair<K, EventsImpl<V>>>() val removes = mutableListOf<K>() patch.forEach { (k, newUpstream) -> when (newUpstream) { @@ -115,7 +115,7 @@ internal class MuxPromptNode<W, K, V>( } // add or replace - adds.forEach { (k, newUpstream: TFlowImpl<V>) -> + adds.forEach { (k, newUpstream: EventsImpl<V>) -> // remove old and sever, if present switchedIn.remove(k)?.let { oldBranch: BranchNode -> if (name != null) { @@ -232,7 +232,7 @@ internal class MuxPromptNode<W, K, V>( val schedulable = Schedulable.N(this) - lateinit var upstream: NodeConnection<Iterable<Map.Entry<K, Maybe<TFlowImpl<V>>>>> + lateinit var upstream: NodeConnection<Iterable<Map.Entry<K, Maybe<EventsImpl<V>>>>> override fun schedule(logIndent: Int, evalScope: EvalScope) { logDuration(logIndent, "MuxPromptPatchNode.schedule") { @@ -304,9 +304,9 @@ internal class MuxPromptNode<W, K, V>( } internal inline fun <A> switchPromptImplSingle( - crossinline getStorage: EvalScope.() -> TFlowImpl<A>, - crossinline getPatches: EvalScope.() -> TFlowImpl<TFlowImpl<A>>, -): TFlowImpl<A> { + crossinline getStorage: EvalScope.() -> EventsImpl<A>, + crossinline getPatches: EvalScope.() -> EventsImpl<EventsImpl<A>>, +): EventsImpl<A> { val switchPromptImpl = switchPromptImpl( getStorage = { singleOf(getStorage()).asIterable() }, @@ -322,17 +322,17 @@ internal inline fun <A> switchPromptImplSingle( internal fun <W, K, V> switchPromptImpl( name: String? = null, - getStorage: EvalScope.() -> Iterable<Map.Entry<K, TFlowImpl<V>>>, - getPatches: EvalScope.() -> TFlowImpl<Iterable<Map.Entry<K, Maybe<TFlowImpl<V>>>>>, + getStorage: EvalScope.() -> Iterable<Map.Entry<K, EventsImpl<V>>>, + getPatches: EvalScope.() -> EventsImpl<Iterable<Map.Entry<K, Maybe<EventsImpl<V>>>>>, storeFactory: MutableMapK.Factory<W, K>, -): TFlowImpl<MuxResult<W, K, V>> = +): EventsImpl<MuxResult<W, K, V>> = MuxLifecycle(MuxPromptActivator(name, getStorage, storeFactory, getPatches)) private class MuxPromptActivator<W, K, V>( private val name: String?, - private val getStorage: EvalScope.() -> Iterable<Map.Entry<K, TFlowImpl<V>>>, + private val getStorage: EvalScope.() -> Iterable<Map.Entry<K, EventsImpl<V>>>, private val storeFactory: MutableMapK.Factory<W, K>, - private val getPatches: EvalScope.() -> TFlowImpl<Iterable<Map.Entry<K, Maybe<TFlowImpl<V>>>>>, + private val getPatches: EvalScope.() -> EventsImpl<Iterable<Map.Entry<K, Maybe<EventsImpl<V>>>>>, ) : MuxActivator<W, K, V> { override fun activate( evalScope: EvalScope, diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Network.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Network.kt index b90c4c02fa7c..d13cdded7ac3 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Network.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/Network.kt @@ -16,7 +16,7 @@ package com.android.systemui.kairos.internal -import com.android.systemui.kairos.TState +import com.android.systemui.kairos.State import com.android.systemui.kairos.internal.util.HeteroMap import com.android.systemui.kairos.internal.util.logDuration import com.android.systemui.kairos.internal.util.logLn @@ -68,7 +68,7 @@ internal class Network(val coroutineScope: CoroutineScope) : NetworkScope { } override val transactionStore = TransactionStore() - private val stateWrites = ArrayDeque<TStateSource<*>>() + private val stateWrites = ArrayDeque<StateSource<*>>() private val outputsByDispatcher = HashMap<ContinuationInterceptor, ArrayDeque<Output<*>>>() private val muxMovers = ArrayDeque<MuxDeferredNode<*, *, *>>() private val deactivations = ArrayDeque<PushNode<*>>() @@ -86,7 +86,7 @@ internal class Network(val coroutineScope: CoroutineScope) : NetworkScope { muxMovers.add(muxMover) } - override fun schedule(state: TStateSource<*>) { + override fun schedule(state: StateSource<*>) { stateWrites.add(state) } @@ -98,7 +98,7 @@ internal class Network(val coroutineScope: CoroutineScope) : NetworkScope { outputDeactivations.add(output) } - /** Listens for external events and starts FRP transactions. Runs forever. */ + /** Listens for external events and starts Kairos transactions. Runs forever. */ suspend fun runInputScheduler() { val actions = mutableListOf<ScheduledAction<*>>() for (first in inputScheduleChan) { @@ -161,7 +161,7 @@ internal class Network(val coroutineScope: CoroutineScope) : NetworkScope { block(EvalScopeImpl(this@Network, this)) } - /** Performs a transactional update of the FRP network. */ + /** Performs a transactional update of the Kairos network. */ private suspend fun doTransaction(logIndent: Int) { // Traverse network, then run outputs logDuration(logIndent, "traverse network") { @@ -227,7 +227,7 @@ internal class Network(val coroutineScope: CoroutineScope) : NetworkScope { } } - /** Updates all [TState]es that have changed within this transaction. */ + /** Updates all [State]es that have changed within this transaction. */ private fun evalStateWriters(logIndent: Int, evalScope: EvalScope) { while (stateWrites.isNotEmpty()) { val latch = stateWrites.removeFirst() diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/NoScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/NoScope.kt index 14e4e1cfc143..f662f1907069 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/NoScope.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/NoScope.kt @@ -16,10 +16,6 @@ package com.android.systemui.kairos.internal -import com.android.systemui.kairos.FrpScope +import com.android.systemui.kairos.KairosScope -internal object NoScope { - private object FrpScopeImpl : FrpScope - - fun <R> runInFrpScope(block: FrpScope.() -> R): R = FrpScopeImpl.block() -} +internal object NoScope : KairosScope diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/PullNodes.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/PullNodes.kt index 5ade401da1a5..517e54f57833 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/PullNodes.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/PullNodes.kt @@ -18,7 +18,7 @@ package com.android.systemui.kairos.internal import com.android.systemui.kairos.internal.util.logDuration -internal val neverImpl: TFlowImpl<Nothing> = TFlowCheap { null } +internal val neverImpl: EventsImpl<Nothing> = EventsImplCheap { null } internal class MapNode<A, B>(val upstream: PullNode<A>, val transform: EvalScope.(A, Int) -> B) : PullNode<B> { @@ -31,9 +31,9 @@ internal class MapNode<A, B>(val upstream: PullNode<A>, val transform: EvalScope } internal inline fun <A, B> mapImpl( - crossinline upstream: EvalScope.() -> TFlowImpl<A>, + crossinline upstream: EvalScope.() -> EventsImpl<A>, noinline transform: EvalScope.(A, Int) -> B, -): TFlowImpl<B> = TFlowCheap { downstream -> +): EventsImpl<B> = EventsImplCheap { downstream -> upstream().activate(evalScope = this, downstream)?.let { (connection, needsEval) -> ActivationResult( connection = @@ -66,9 +66,9 @@ internal class CachedNode<A>( } } -internal fun <A> TFlowImpl<A>.cached(): TFlowImpl<A> { +internal fun <A> EventsImpl<A>.cached(): EventsImpl<A> { val key = TransactionCache<Lazy<A>>() - return TFlowCheap { it -> + return EventsImplCheap { it -> activate(this, it)?.let { (connection, needsEval) -> ActivationResult( connection = diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/TStateImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateImpl.kt index 9565a9c12d38..5ba645246b0f 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/TStateImpl.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateImpl.kt @@ -27,15 +27,15 @@ import com.android.systemui.kairos.util.none import java.util.concurrent.atomic.AtomicLong import kotlinx.coroutines.ExperimentalCoroutinesApi -internal sealed interface TStateImpl<out A> { +internal sealed interface StateImpl<out A> { val name: String? val operatorName: String - val changes: TFlowImpl<A> + val changes: EventsImpl<A> fun getCurrentWithEpoch(evalScope: EvalScope): Pair<A, Long> } -internal sealed class TStateDerived<A>(override val changes: TFlowImpl<A>) : TStateImpl<A> { +internal sealed class StateDerived<A>(override val changes: EventsImpl<A>) : StateImpl<A> { @Volatile var invalidatedEpoch = Long.MIN_VALUE @@ -73,17 +73,17 @@ internal sealed class TStateDerived<A>(override val changes: TFlowImpl<A>) : TSt private data object EmptyCache } -internal class TStateSource<A>( +internal class StateSource<A>( override val name: String?, override val operatorName: String, init: Lazy<A>, - override val changes: TFlowImpl<A>, -) : TStateImpl<A> { + override val changes: EventsImpl<A>, +) : StateImpl<A> { constructor( name: String?, operatorName: String, init: A, - changes: TFlowImpl<A>, + changes: EventsImpl<A>, ) : this(name, operatorName, CompletableLazy(init), changes) lateinit var upstreamConnection: NodeConnection<A> @@ -108,26 +108,26 @@ internal class TStateSource<A>( writeEpoch = evalScope.epoch } - override fun toString(): String = "TStateImpl(changes=$changes, current=$_current)" + override fun toString(): String = "StateImpl(changes=$changes, current=$_current)" @OptIn(ExperimentalCoroutinesApi::class) fun getStorageUnsafe(): Maybe<A> = if (_current.isInitialized()) just(_current.value) else none } -internal fun <A> constS(name: String?, operatorName: String, init: A): TStateImpl<A> = - TStateSource(name, operatorName, init, neverImpl) +internal fun <A> constState(name: String?, operatorName: String, init: A): StateImpl<A> = + StateSource(name, operatorName, init, neverImpl) -internal inline fun <A> activatedTStateSource( +internal inline fun <A> activatedStateSource( name: String?, operatorName: String, evalScope: EvalScope, - crossinline getChanges: EvalScope.() -> TFlowImpl<A>, + crossinline getChanges: EvalScope.() -> EventsImpl<A>, init: Lazy<A>, -): TStateImpl<A> { - lateinit var state: TStateSource<A> - val calm: TFlowImpl<A> = +): StateImpl<A> { + lateinit var state: StateSource<A> + val calm: EventsImpl<A> = filterImpl(getChanges) { new -> new != state.getCurrentWithEpoch(evalScope = this).first } - return TStateSource(name, operatorName, init, calm).also { + return StateSource(name, operatorName, init, calm).also { state = it evalScope.scheduleOutput( OneShot { @@ -143,9 +143,9 @@ internal inline fun <A> activatedTStateSource( } } -private inline fun <A> TFlowImpl<A>.calm( - crossinline getState: () -> TStateDerived<A> -): TFlowImpl<A> = +private inline fun <A> EventsImpl<A>.calm( + crossinline getState: () -> StateDerived<A> +): EventsImpl<A> = filterImpl({ this@calm }) { new -> val state = getState() val (current, _) = state.getCurrentWithEpoch(evalScope = this) @@ -158,11 +158,11 @@ private inline fun <A> TFlowImpl<A>.calm( } .cached() -internal fun <A, B> TStateImpl<A>.mapCheap( +internal fun <A, B> StateImpl<A>.mapCheap( name: String?, operatorName: String, transform: EvalScope.(A) -> B, -): TStateImpl<B> = +): StateImpl<B> = DerivedMapCheap( name, operatorName, @@ -174,10 +174,10 @@ internal fun <A, B> TStateImpl<A>.mapCheap( internal class DerivedMapCheap<A, B>( override val name: String?, override val operatorName: String, - val upstream: TStateImpl<A>, - override val changes: TFlowImpl<B>, + val upstream: StateImpl<A>, + override val changes: EventsImpl<B>, private val transform: EvalScope.(A) -> B, -) : TStateImpl<B> { +) : StateImpl<B> { override fun getCurrentWithEpoch(evalScope: EvalScope): Pair<B, Long> { val (a, epoch) = upstream.getCurrentWithEpoch(evalScope) @@ -187,12 +187,12 @@ internal class DerivedMapCheap<A, B>( override fun toString(): String = "${this::class.simpleName}@$hashString" } -internal fun <A, B> TStateImpl<A>.map( +internal fun <A, B> StateImpl<A>.map( name: String?, operatorName: String, transform: EvalScope.(A) -> B, -): TStateImpl<B> { - lateinit var state: TStateDerived<B> +): StateImpl<B> { + lateinit var state: StateDerived<B> val mappedChanges = mapImpl({ changes }) { it, _ -> transform(it) }.cached().calm { state } state = DerivedMap(name, operatorName, transform, this, mappedChanges) return state @@ -202,9 +202,9 @@ internal class DerivedMap<A, B>( override val name: String?, override val operatorName: String, private val transform: EvalScope.(A) -> B, - val upstream: TStateImpl<A>, - changes: TFlowImpl<B>, -) : TStateDerived<B>(changes) { + val upstream: StateImpl<A>, + changes: EventsImpl<B>, +) : StateDerived<B>(changes) { override fun toString(): String = "${this::class.simpleName}@$hashString" override fun recalc(evalScope: EvalScope): Pair<B, Long>? { @@ -217,7 +217,7 @@ internal class DerivedMap<A, B>( } } -internal fun <A> TStateImpl<TStateImpl<A>>.flatten(name: String?, operator: String): TStateImpl<A> { +internal fun <A> StateImpl<StateImpl<A>>.flatten(name: String?, operator: String): StateImpl<A> { // emits the current value of the new inner state, when that state is emitted val switchEvents = mapImpl({ changes }) { newInner, _ -> newInner.getCurrentWithEpoch(this).first } @@ -228,7 +228,7 @@ internal fun <A> TStateImpl<TStateImpl<A>>.flatten(name: String?, operator: Stri mapImpl({ changes }) { newInner, _ -> mergeNodes({ switchEvents }, { newInner.changes }) { _, new -> new } } - val switchedChanges: TFlowImpl<A> = + val switchedChanges: EventsImpl<A> = switchPromptImplSingle( getStorage = { this@flatten.getCurrentWithEpoch(evalScope = this).first.changes }, getPatches = { innerChanges }, @@ -241,9 +241,9 @@ internal fun <A> TStateImpl<TStateImpl<A>>.flatten(name: String?, operator: Stri internal class DerivedFlatten<A>( override val name: String?, override val operatorName: String, - val upstream: TStateImpl<TStateImpl<A>>, - changes: TFlowImpl<A>, -) : TStateDerived<A>(changes) { + val upstream: StateImpl<StateImpl<A>>, + changes: EventsImpl<A>, +) : StateDerived<A>(changes) { override fun recalc(evalScope: EvalScope): Pair<A, Long> { val (inner, epoch0) = upstream.getCurrentWithEpoch(evalScope) val (a, epoch1) = inner.getCurrentWithEpoch(evalScope) @@ -254,19 +254,19 @@ internal class DerivedFlatten<A>( } @Suppress("NOTHING_TO_INLINE") -internal inline fun <A, B> TStateImpl<A>.flatMap( +internal inline fun <A, B> StateImpl<A>.flatMap( name: String?, operatorName: String, - noinline transform: EvalScope.(A) -> TStateImpl<B>, -): TStateImpl<B> = map(null, operatorName, transform).flatten(name, operatorName) + noinline transform: EvalScope.(A) -> StateImpl<B>, +): StateImpl<B> = map(null, operatorName, transform).flatten(name, operatorName) internal fun <A, B, Z> zipStates( name: String?, operatorName: String, - l1: TStateImpl<A>, - l2: TStateImpl<B>, + l1: StateImpl<A>, + l2: StateImpl<B>, transform: EvalScope.(A, B) -> Z, -): TStateImpl<Z> = +): StateImpl<Z> = zipStateList(null, operatorName, listOf(l1, l2)).map(name, operatorName) { @Suppress("UNCHECKED_CAST") transform(it[0] as A, it[1] as B) } @@ -274,11 +274,11 @@ internal fun <A, B, Z> zipStates( internal fun <A, B, C, Z> zipStates( name: String?, operatorName: String, - l1: TStateImpl<A>, - l2: TStateImpl<B>, - l3: TStateImpl<C>, + l1: StateImpl<A>, + l2: StateImpl<B>, + l3: StateImpl<C>, transform: EvalScope.(A, B, C) -> Z, -): TStateImpl<Z> = +): StateImpl<Z> = zipStateList(null, operatorName, listOf(l1, l2, l3)).map(name, operatorName) { @Suppress("UNCHECKED_CAST") transform(it[0] as A, it[1] as B, it[2] as C) } @@ -286,12 +286,12 @@ internal fun <A, B, C, Z> zipStates( internal fun <A, B, C, D, Z> zipStates( name: String?, operatorName: String, - l1: TStateImpl<A>, - l2: TStateImpl<B>, - l3: TStateImpl<C>, - l4: TStateImpl<D>, + l1: StateImpl<A>, + l2: StateImpl<B>, + l3: StateImpl<C>, + l4: StateImpl<D>, transform: EvalScope.(A, B, C, D) -> Z, -): TStateImpl<Z> = +): StateImpl<Z> = zipStateList(null, operatorName, listOf(l1, l2, l3, l4)).map(name, operatorName) { @Suppress("UNCHECKED_CAST") transform(it[0] as A, it[1] as B, it[2] as C, it[3] as D) } @@ -299,13 +299,13 @@ internal fun <A, B, C, D, Z> zipStates( internal fun <A, B, C, D, E, Z> zipStates( name: String?, operatorName: String, - l1: TStateImpl<A>, - l2: TStateImpl<B>, - l3: TStateImpl<C>, - l4: TStateImpl<D>, - l5: TStateImpl<E>, + l1: StateImpl<A>, + l2: StateImpl<B>, + l3: StateImpl<C>, + l4: StateImpl<D>, + l5: StateImpl<E>, transform: EvalScope.(A, B, C, D, E) -> Z, -): TStateImpl<Z> = +): StateImpl<Z> = zipStateList(null, operatorName, listOf(l1, l2, l3, l4, l5)).map(name, operatorName) { @Suppress("UNCHECKED_CAST") transform(it[0] as A, it[1] as B, it[2] as C, it[3] as D, it[4] as E) @@ -314,8 +314,8 @@ internal fun <A, B, C, D, E, Z> zipStates( internal fun <K, V> zipStateMap( name: String?, operatorName: String, - states: Map<K, TStateImpl<V>>, -): TStateImpl<Map<K, V>> = + states: Map<K, StateImpl<V>>, +): StateImpl<Map<K, V>> = zipStates( name = name, operatorName = operatorName, @@ -327,8 +327,8 @@ internal fun <K, V> zipStateMap( internal fun <V> zipStateList( name: String?, operatorName: String, - states: List<TStateImpl<V>>, -): TStateImpl<List<V>> { + states: List<StateImpl<V>>, +): StateImpl<List<V>> { val zipped = zipStates( name = name, @@ -352,11 +352,11 @@ internal fun <W, K, A> zipStates( name: String?, operatorName: String, numStates: Int, - states: Iterable<Map.Entry<K, TStateImpl<A>>>, + states: Iterable<Map.Entry<K, StateImpl<A>>>, storeFactory: MutableMapK.Factory<W, K>, -): TStateImpl<MutableMapK<W, K, A>> { +): StateImpl<MutableMapK<W, K, A>> { if (numStates == 0) { - return constS(name, operatorName, storeFactory.create(0)) + return constState(name, operatorName, storeFactory.create(0)) } val stateChanges = states.asSequence().map { (k, v) -> StoreEntry(k, v.changes) }.asIterable() lateinit var state: DerivedZipped<W, K, A> @@ -397,10 +397,10 @@ internal class DerivedZipped<W, K, A>( override val name: String?, override val operatorName: String, private val upstreamSize: Int, - val upstream: Iterable<Map.Entry<K, TStateImpl<A>>>, - changes: TFlowImpl<MutableMapK<W, K, A>>, + val upstream: Iterable<Map.Entry<K, StateImpl<A>>>, + changes: EventsImpl<MutableMapK<W, K, A>>, private val storeFactory: MutableMapK.Factory<W, K>, -) : TStateDerived<MutableMapK<W, K, A>>(changes) { +) : StateDerived<MutableMapK<W, K, A>>(changes) { override fun recalc(evalScope: EvalScope): Pair<MutableMapK<W, K, A>, Long> { val newEpoch = AtomicLong() val store = storeFactory.create<A>(upstreamSize) @@ -419,10 +419,10 @@ internal class DerivedZipped<W, K, A>( internal inline fun <A> zipStates( name: String?, operatorName: String, - states: List<TStateImpl<A>>, -): TStateImpl<List<A>> = + states: List<StateImpl<A>>, +): StateImpl<List<A>> = if (states.isEmpty()) { - constS(name, operatorName, emptyList()) + constState(name, operatorName, emptyList()) } else { zipStateList(null, operatorName, states) } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateScopeImpl.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateScopeImpl.kt index 48f69036df89..b5eec85f1f5f 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateScopeImpl.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/StateScopeImpl.kt @@ -16,91 +16,52 @@ package com.android.systemui.kairos.internal -import com.android.systemui.kairos.FrpDeferredValue -import com.android.systemui.kairos.FrpStateScope -import com.android.systemui.kairos.FrpStateful -import com.android.systemui.kairos.FrpTransactionScope -import com.android.systemui.kairos.GroupedTFlow -import com.android.systemui.kairos.TFlow -import com.android.systemui.kairos.TFlowInit -import com.android.systemui.kairos.TFlowLoop -import com.android.systemui.kairos.TState -import com.android.systemui.kairos.TStateInit -import com.android.systemui.kairos.emptyTFlow +import com.android.systemui.kairos.DeferredValue +import com.android.systemui.kairos.Events +import com.android.systemui.kairos.EventsInit +import com.android.systemui.kairos.EventsLoop +import com.android.systemui.kairos.GroupedEvents +import com.android.systemui.kairos.State +import com.android.systemui.kairos.StateInit +import com.android.systemui.kairos.StateScope +import com.android.systemui.kairos.Stateful +import com.android.systemui.kairos.emptyEvents import com.android.systemui.kairos.groupByKey import com.android.systemui.kairos.init import com.android.systemui.kairos.internal.store.ConcurrentHashMapK import com.android.systemui.kairos.mapCheap import com.android.systemui.kairos.merge -import com.android.systemui.kairos.switch +import com.android.systemui.kairos.switchEvents import com.android.systemui.kairos.util.Maybe import com.android.systemui.kairos.util.map -internal class StateScopeImpl(val evalScope: EvalScope, override val endSignal: TFlow<Any>) : - StateScope, EvalScope by evalScope { +internal class StateScopeImpl(val evalScope: EvalScope, override val endSignal: Events<Any>) : + InternalStateScope, EvalScope by evalScope { - override val endSignalOnce: TFlow<Any> = endSignal.nextOnlyInternal("StateScope.endSignal") + override val endSignalOnce: Events<Any> = endSignal.nextOnlyInternal("StateScope.endSignal") - private fun <A> TFlow<A>.truncateToScope(operatorName: String): TFlow<A> = - if (endSignalOnce === emptyTFlow) { - this - } else { - endSignalOnce.mapCheap { emptyTFlow }.toTStateInternal(operatorName, this).switch() - } - - private fun <A> TFlow<A>.nextOnlyInternal(operatorName: String): TFlow<A> = - if (this === emptyTFlow) { - this - } else { - TFlowLoop<A>().apply { - loopback = - mapCheap { emptyTFlow } - .toTStateInternal(operatorName, this@nextOnlyInternal) - .switch() - } - } - - private fun <A> TFlow<A>.toTStateInternal(operatorName: String, init: A): TState<A> = - toTStateInternalDeferred(operatorName, CompletableLazy(init)) - - private fun <A> TFlow<A>.toTStateInternalDeferred( - operatorName: String, - init: Lazy<A>, - ): TState<A> { - val changes = this@toTStateInternalDeferred - val name = operatorName - val impl = - activatedTStateSource( - name, - operatorName, - evalScope, - { changes.init.connect(evalScope = this) }, - init, - ) - return TStateInit(constInit(name, impl)) - } - - private fun <R> deferredInternal(block: FrpStateScope.() -> R): FrpDeferredValue<R> = - FrpDeferredValue(deferAsync { runInStateScope(block) }) + override fun <A> deferredStateScope(block: StateScope.() -> A): DeferredValue<A> = + DeferredValue(deferAsync { block() }) - private fun <A> TFlow<A>.toTStateDeferredInternal( - initialValue: FrpDeferredValue<A> - ): TState<A> { - val operatorName = "toTStateDeferred" + override fun <A> Events<A>.holdStateDeferred(initialValue: DeferredValue<A>): State<A> { + val operatorName = "holdStateDeferred" // Ensure state is only collected until the end of this scope return truncateToScope(operatorName) - .toTStateInternalDeferred(operatorName, initialValue.unwrapped) + .toStateInternalDeferred(operatorName, initialValue.unwrapped) } - private fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementallyInternal( + override fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementally( name: String?, - storage: TState<Map<K, TFlow<V>>>, - ): TFlow<Map<K, V>> { + initialEvents: DeferredValue<Map<K, Events<V>>>, + ): Events<Map<K, V>> { + val storage: State<Map<K, Events<V>>> = foldStateMapIncrementally(initialEvents) val patches = mapImpl({ init.connect(this) }) { patch, _ -> - patch.mapValues { (_, m) -> m.map { flow -> flow.init.connect(this) } }.asIterable() + patch + .mapValues { (_, m) -> m.map { events -> events.init.connect(this) } } + .asIterable() } - return TFlowInit( + return EventsInit( constInit( name, switchDeferredImpl( @@ -110,7 +71,7 @@ internal class StateScopeImpl(val evalScope: EvalScope, override val endSignal: .connect(this) .getCurrentWithEpoch(this) .first - .mapValues { (_, flow) -> flow.init.connect(this) } + .mapValues { (_, events) -> events.init.connect(this) } .asIterable() }, getPatches = { patches }, @@ -121,15 +82,18 @@ internal class StateScopeImpl(val evalScope: EvalScope, override val endSignal: ) } - private fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementallyPromptInternal( - storage: TState<Map<K, TFlow<V>>>, + override fun <K, V> Events<Map<K, Maybe<Events<V>>>>.mergeIncrementallyPromptly( + initialEvents: DeferredValue<Map<K, Events<V>>>, name: String?, - ): TFlow<Map<K, V>> { + ): Events<Map<K, V>> { + val storage: State<Map<K, Events<V>>> = foldStateMapIncrementally(initialEvents) val patches = mapImpl({ init.connect(this) }) { patch, _ -> - patch.mapValues { (_, m) -> m.map { flow -> flow.init.connect(this) } }.asIterable() + patch + .mapValues { (_, m) -> m.map { events -> events.init.connect(this) } } + .asIterable() } - return TFlowInit( + return EventsInit( constInit( name, switchPromptImpl( @@ -139,7 +103,7 @@ internal class StateScopeImpl(val evalScope: EvalScope, override val endSignal: .connect(this) .getCurrentWithEpoch(this) .first - .mapValues { (_, flow) -> flow.init.connect(this) } + .mapValues { (_, events) -> events.init.connect(this) } .asIterable() }, getPatches = { patches }, @@ -150,96 +114,98 @@ internal class StateScopeImpl(val evalScope: EvalScope, override val endSignal: ) } - private fun <K, A, B> TFlow<Map<K, Maybe<FrpStateful<A>>>>.applyLatestStatefulForKeyInternal( - init: FrpDeferredValue<Map<K, FrpStateful<B>>>, + override fun <K, A, B> Events<Map<K, Maybe<Stateful<A>>>>.applyLatestStatefulForKey( + init: DeferredValue<Map<K, Stateful<B>>>, numKeys: Int?, - ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>> { - val eventsByKey: GroupedTFlow<K, Maybe<FrpStateful<A>>> = groupByKey(numKeys) + ): Pair<Events<Map<K, Maybe<A>>>, DeferredValue<Map<K, B>>> { + val eventsByKey: GroupedEvents<K, Maybe<Stateful<A>>> = groupByKey(numKeys) val initOut: Lazy<Map<K, B>> = deferAsync { init.unwrapped.value.mapValues { (k, stateful) -> - val newEnd = with(frpScope) { eventsByKey[k] } + val newEnd = eventsByKey[k] val newScope = childStateScope(newEnd) - newScope.runInStateScope(stateful) + newScope.stateful() } } - val changesNode: TFlowImpl<Map<K, Maybe<A>>> = - mapImpl( - upstream = { this@applyLatestStatefulForKeyInternal.init.connect(evalScope = this) } - ) { upstreamMap, _ -> - upstreamMap.mapValues { (k: K, ma: Maybe<FrpStateful<A>>) -> + val changesNode: EventsImpl<Map<K, Maybe<A>>> = + mapImpl(upstream = { this@applyLatestStatefulForKey.init.connect(evalScope = this) }) { + upstreamMap, + _ -> + upstreamMap.mapValues { (k: K, ma: Maybe<Stateful<A>>) -> reenterStateScope(this@StateScopeImpl).run { ma.map { stateful -> - val newEnd = with(frpScope) { eventsByKey[k].skipNext() } + val newEnd = eventsByKey[k].skipNext() val newScope = childStateScope(newEnd) - newScope.runInStateScope(stateful) + newScope.stateful() } } } } val operatorName = "applyLatestStatefulForKey" val name = operatorName - val changes: TFlow<Map<K, Maybe<A>>> = TFlowInit(constInit(name, changesNode.cached())) - return changes to FrpDeferredValue(initOut) + val changes: Events<Map<K, Maybe<A>>> = EventsInit(constInit(name, changesNode.cached())) + return changes to DeferredValue(initOut) } - private fun <A> TFlow<FrpStateful<A>>.observeStatefulsInternal(): TFlow<A> { - val operatorName = "observeStatefuls" + override fun <A> Events<Stateful<A>>.applyStatefuls(): Events<A> { + val operatorName = "applyStatefuls" val name = operatorName - return TFlowInit( + return EventsInit( constInit( name, - mapImpl( - upstream = { this@observeStatefulsInternal.init.connect(evalScope = this) } - ) { stateful, _ -> - reenterStateScope(outerScope = this@StateScopeImpl) - .runInStateScope(stateful) + mapImpl(upstream = { this@applyStatefuls.init.connect(evalScope = this) }) { + stateful, + _ -> + reenterStateScope(outerScope = this@StateScopeImpl).stateful() } .cached(), ) ) } - override val frpScope: FrpStateScope = FrpStateScopeImpl() - - private inner class FrpStateScopeImpl : - FrpStateScope, FrpTransactionScope by evalScope.frpScope { - - override fun <A> deferredStateScope(block: FrpStateScope.() -> A): FrpDeferredValue<A> = - deferredInternal(block) - - override fun <A> TFlow<A>.holdDeferred(initialValue: FrpDeferredValue<A>): TState<A> = - toTStateDeferredInternal(initialValue) + override fun childStateScope(newEnd: Events<Any>) = + StateScopeImpl(evalScope, merge(newEnd, endSignal)) - override fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementally( - name: String?, - initialTFlows: FrpDeferredValue<Map<K, TFlow<V>>>, - ): TFlow<Map<K, V>> { - val storage: TState<Map<K, TFlow<V>>> = foldMapIncrementally(initialTFlows) - return mergeIncrementallyInternal(name, storage) + private fun <A> Events<A>.truncateToScope(operatorName: String): Events<A> = + if (endSignalOnce === emptyEvents) { + this + } else { + endSignalOnce + .mapCheap { emptyEvents } + .toStateInternal(operatorName, this) + .switchEvents() } - override fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementallyPromptly( - initialTFlows: FrpDeferredValue<Map<K, TFlow<V>>>, - name: String?, - ): TFlow<Map<K, V>> { - val storage: TState<Map<K, TFlow<V>>> = foldMapIncrementally(initialTFlows) - return mergeIncrementallyPromptInternal(storage, name) + private fun <A> Events<A>.nextOnlyInternal(operatorName: String): Events<A> = + if (this === emptyEvents) { + this + } else { + EventsLoop<A>().apply { + loopback = + mapCheap { emptyEvents } + .toStateInternal(operatorName, this@nextOnlyInternal) + .switchEvents() + } } - override fun <K, A, B> TFlow<Map<K, Maybe<FrpStateful<A>>>>.applyLatestStatefulForKey( - init: FrpDeferredValue<Map<K, FrpStateful<B>>>, - numKeys: Int?, - ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>> = - applyLatestStatefulForKeyInternal(init, numKeys) + private fun <A> Events<A>.toStateInternal(operatorName: String, init: A): State<A> = + toStateInternalDeferred(operatorName, CompletableLazy(init)) - override fun <A> TFlow<FrpStateful<A>>.applyStatefuls(): TFlow<A> = - observeStatefulsInternal() + private fun <A> Events<A>.toStateInternalDeferred( + operatorName: String, + init: Lazy<A>, + ): State<A> { + val changes = this@toStateInternalDeferred + val name = operatorName + val impl = + activatedStateSource( + name, + operatorName, + evalScope, + { changes.init.connect(evalScope = this) }, + init, + ) + return StateInit(constInit(name, impl)) } - - override fun <R> runInStateScope(block: FrpStateScope.() -> R): R = frpScope.block() - - override fun childStateScope(newEnd: TFlow<Any>) = - StateScopeImpl(evalScope, merge(newEnd, endSignal)) } private fun EvalScope.reenterStateScope(outerScope: StateScopeImpl) = diff --git a/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosTests.kt b/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosTests.kt index f3303f697fc9..287d8cb136c4 100644 --- a/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosTests.kt +++ b/packages/SystemUI/utils/kairos/test/com/android/systemui/kairos/KairosTests.kt @@ -1,21 +1,3 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -@file:OptIn(ExperimentalCoroutinesApi::class, ExperimentalFrpApi::class) - package com.android.systemui.kairos import com.android.systemui.kairos.util.Either @@ -56,11 +38,12 @@ import org.junit.Assert.assertTrue import org.junit.Assert.fail import org.junit.Test +@OptIn(ExperimentalCoroutinesApi::class) class KairosTests { @Test fun basic() = runFrpTest { network -> - val emitter = network.mutableTFlow<Int>() + val emitter = network.mutableEvents<Int>() var result: Int? = null activateSpec(network) { emitter.observe { result = it } } runCurrent() @@ -71,8 +54,8 @@ class KairosTests { } @Test - fun basicTFlow() = runFrpTest { network -> - val emitter = network.mutableTFlow<Int>() + fun basicEvents() = runFrpTest { network -> + val emitter = network.mutableEvents<Int>() println("starting network") val result = activateSpecWithResult(network) { emitter.nextDeferred() } runCurrent() @@ -85,9 +68,9 @@ class KairosTests { } @Test - fun basicTState() = runFrpTest { network -> - val emitter = network.mutableTFlow<Int>() - val result = activateSpecWithResult(network) { emitter.hold(0).stateChanges.nextDeferred() } + fun basicState() = runFrpTest { network -> + val emitter = network.mutableEvents<Int>() + val result = activateSpecWithResult(network) { emitter.holdState(0).changes.nextDeferred() } runCurrent() emitter.emit(3) @@ -111,7 +94,7 @@ class KairosTests { fun basicTransactional() = runFrpTest { network -> var value: Int? = null var bSource = 1 - val emitter = network.mutableTFlow<Unit>() + val emitter = network.mutableEvents<Unit>() // Sampling this transactional will increment the source count. val transactional = transactionally { bSource++ } measureTime { @@ -150,24 +133,26 @@ class KairosTests { @Test fun diamondGraph() = runFrpTest { network -> - val flow = network.mutableTFlow<Int>() - val outFlow = + val flow = network.mutableEvents<Int>() + val ouevents = activateSpecWithResult(network) { - // map TFlow like we map Flow + // map Events like we map Flow val left = flow.map { "left" to it }.onEach { println("left: $it") } val right = flow.map { "right" to it }.onEach { println("right: $it") } - // convert TFlows to TStates so that they can be combined + // convert Eventss to States so that they can be combined val combined = - left.hold("left" to 0).combineWith(right.hold("right" to 0)) { l, r -> l to r } - combined.stateChanges // get TState changes + left.holdState("left" to 0).combineWith(right.holdState("right" to 0)) { l, r -> + l to r + } + combined.changes // get State changes .onEach { println("merged: $it") } .toSharedFlow() // convert back to Flow } runCurrent() val results = mutableListOf<Pair<Pair<String, Int>, Pair<String, Int>>>() - backgroundScope.launch { outFlow.toCollection(results) } + backgroundScope.launch { ouevents.toCollection(results) } runCurrent() flow.emit(1) @@ -186,19 +171,19 @@ class KairosTests { fun staticNetwork() = runFrpTest { network -> var finalSum: Int? = null - val intEmitter = network.mutableTFlow<Int>() - val sampleEmitter = network.mutableTFlow<Unit>() + val intEmitter = network.mutableEvents<Int>() + val sampleEmitter = network.mutableEvents<Unit>() activateSpecWithResult(network) { val updates = intEmitter.map { a -> { b: Int -> a + b } } val sumD = - TStateLoop<Int>().apply { + StateLoop<Int>().apply { loopback = updates .sample(this) { f, sum -> f(sum) } .onEach { println("sum update: $it") } - .hold(0) + .holdState(0) } sampleEmitter .onEach { println("sampleEmitter emitted") } @@ -228,10 +213,10 @@ class KairosTests { var wasSold = false var currentAmt: Int? = null - val coin = network.mutableTFlow<Unit>() + val coin = network.mutableEvents<Unit>() val price = 50 - val frpSpec = frpSpec { - val eSold = TFlowLoop<Unit>() + val buildSpec = buildSpec { + val eSold = EventsLoop<Unit>() val eInsert = coin.map { @@ -251,10 +236,10 @@ class KairosTests { val eUpdate = eInsert.mergeWith(eReset) { f, g -> { a -> g(f(a)) } } - val dTotal = TStateLoop<Int>() - dTotal.loopback = eUpdate.sample(dTotal) { f, total -> f(total) }.hold(price) + val dTotal = StateLoop<Int>() + dTotal.loopback = eUpdate.sample(dTotal) { f, total -> f(total) }.holdState(price) - val eAmt = dTotal.stateChanges + val eAmt = dTotal.changes val bAmt = transactionally { dTotal.sample() } eSold.loopback = coin @@ -269,7 +254,7 @@ class KairosTests { eSold.nextDeferred() } - activateSpec(network) { frpSpec.applySpec() } + activateSpec(network) { buildSpec.applySpec() } runCurrent() @@ -313,8 +298,8 @@ class KairosTests { @Test fun promptCleanup() = runFrpTest { network -> - val emitter = network.mutableTFlow<Int>() - val stopper = network.mutableTFlow<Unit>() + val emitter = network.mutableEvents<Int>() + val stopper = network.mutableEvents<Unit>() var result: Int? = null @@ -332,19 +317,19 @@ class KairosTests { } @Test - fun switchTFlow() = runFrpTest { network -> + fun switchEvents() = runFrpTest { network -> var currentSum: Int? = null - val switchHandler = network.mutableTFlow<Pair<TFlow<Int>, String>>() - val aHandler = network.mutableTFlow<Int>() - val stopHandler = network.mutableTFlow<Unit>() - val bHandler = network.mutableTFlow<Int>() + val switchHandler = network.mutableEvents<Pair<Events<Int>, String>>() + val aHandler = network.mutableEvents<Int>() + val stopHandler = network.mutableEvents<Unit>() + val bHandler = network.mutableEvents<Int>() val sumFlow = activateSpecWithResult(network) { - val switchE = TFlowLoop<TFlow<Int>>() + val switchE = EventsLoop<Events<Int>>() switchE.loopback = - switchHandler.mapStateful { (intFlow, name) -> + switchHandler.mapStateful { (inevents, name) -> println("[onEach] Switching to: $name") val nextSwitch = switchE.skipNext().onEach { println("[onEach] switched-out") } @@ -352,11 +337,11 @@ class KairosTests { stopHandler .onEach { println("[onEach] stopped") } .mergeWith(nextSwitch) { _, b -> b } - intFlow.takeUntil(stopEvent) + inevents.takeUntil(stopEvent) } - val adderE: TFlow<(Int) -> Int> = - switchE.hold(emptyTFlow).switch().map { a -> + val adderE: Events<(Int) -> Int> = + switchE.holdState(emptyEvents).switchEvents().map { a -> println("[onEach] new number $a") ({ sum: Int -> println("$a+$sum=${a + sum}") @@ -364,13 +349,13 @@ class KairosTests { }) } - val sumD = TStateLoop<Int>() + val sumD = StateLoop<Int>() sumD.loopback = adderE .sample(sumD) { f, sum -> f(sum) } .onEach { println("[onEach] writing sum: $it") } - .hold(0) - val sumE = sumD.stateChanges + .holdState(0) + val sumE = sumD.changes sumE.toSharedFlow() } @@ -494,16 +479,16 @@ class KairosTests { @Test fun switchIndirect() = runFrpTest { network -> - val emitter = network.mutableTFlow<Unit>() + val emitter = network.mutableEvents<Unit>() activateSpec(network) { - emptyTFlow.map { emitter.map { 1 } }.flatten().map { "$it" }.observe() + emptyEvents.map { emitter.map { 1 } }.flatten().map { "$it" }.observe() } runCurrent() } @Test fun switchInWithResult() = runFrpTest { network -> - val emitter = network.mutableTFlow<Unit>() + val emitter = network.mutableEvents<Unit>() val out = activateSpecWithResult(network) { emitter.map { emitter.map { 1 } }.flatten().toSharedFlow() @@ -519,11 +504,11 @@ class KairosTests { fun switchInCompleted() = runFrpTest { network -> val outputs = mutableListOf<Int>() - val switchAH = network.mutableTFlow<Unit>() - val intAH = network.mutableTFlow<Int>() - val stopEmitter = network.mutableTFlow<Unit>() + val switchAH = network.mutableEvents<Unit>() + val intAH = network.mutableEvents<Int>() + val stopEmitter = network.mutableEvents<Unit>() - val top = frpSpec { + val top = buildSpec { val intS = intAH.takeUntil(stopEmitter) val switched = switchAH.map { intS }.flatten() switched.toSharedFlow() @@ -555,13 +540,13 @@ class KairosTests { } @Test - fun switchTFlow_outerCompletesFirst() = runFrpTest { network -> + fun switchEvents_outerCompletesFirst() = runFrpTest { network -> var stepResult: Int? = null - val switchAH = network.mutableTFlow<Unit>() - val switchStopEmitter = network.mutableTFlow<Unit>() - val intStopEmitter = network.mutableTFlow<Unit>() - val intAH = network.mutableTFlow<Int>() + val switchAH = network.mutableEvents<Unit>() + val switchStopEmitter = network.mutableEvents<Unit>() + val intStopEmitter = network.mutableEvents<Unit>() + val intAH = network.mutableEvents<Int>() val flow = activateSpecWithResult(network) { val intS = intAH.takeUntil(intStopEmitter) @@ -609,8 +594,8 @@ class KairosTests { } @Test - fun mapTFlow() = runFrpTest { network -> - val emitter = network.mutableTFlow<Int>() + fun mapEvents() = runFrpTest { network -> + val emitter = network.mutableEvents<Int>() var stepResult: Int? = null val flow = @@ -644,7 +629,7 @@ class KairosTests { var pullValue = 0 val a = transactionally { pullValue } val b = transactionally { a.sample() * 2 } - val emitter = network.mutableTFlow<Unit>() + val emitter = network.mutableEvents<Unit>() val flow = activateSpecWithResult(network) { val sampleB = emitter.sample(b) { _, b -> b } @@ -668,14 +653,14 @@ class KairosTests { } @Test - fun mapTState() = runFrpTest { network -> - val emitter = network.mutableTFlow<Int>() + fun mapState() = runFrpTest { network -> + val emitter = network.mutableEvents<Int>() var stepResult: Int? = null val flow = activateSpecWithResult(network) { - val state = emitter.hold(0).map { it + 2 } + val state = emitter.holdState(0).map { it + 2 } val stateCurrent = transactionally { state.sample() } - val stateChanges = state.stateChanges + val stateChanges = state.changes val sampleState = emitter.sample(stateCurrent) { _, b -> b } val merge = stateChanges.mergeWith(sampleState) { a, b -> a + b } merge.toSharedFlow() @@ -696,14 +681,14 @@ class KairosTests { @Test fun partitionEither() = runFrpTest { network -> - val emitter = network.mutableTFlow<Either<Int, Int>>() + val emitter = network.mutableEvents<Either<Int, Int>>() val result = activateSpecWithResult(network) { val (l, r) = emitter.partitionEither() val pDiamond = l.map { it * 2 } .mergeWith(r.map { it * -1 }) { _, _ -> error("unexpected coincidence") } - pDiamond.hold(null).toStateFlow() + pDiamond.holdState(null).toStateFlow() } runCurrent() @@ -719,15 +704,16 @@ class KairosTests { } @Test - fun accumTState() = runFrpTest { network -> - val emitter = network.mutableTFlow<Int>() - val sampler = network.mutableTFlow<Unit>() + fun accumState() = runFrpTest { network -> + val emitter = network.mutableEvents<Int>() + val sampler = network.mutableEvents<Unit>() var stepResult: Int? = null val flow = activateSpecWithResult(network) { - val sumState = emitter.map { a -> { b: Int -> a + b } }.fold(0) { f, a -> f(a) } + val sumState = + emitter.map { a -> { b: Int -> a + b } }.foldState(0) { f, a -> f(a) } - sumState.stateChanges + sumState.changes .mergeWith(sampler.sample(sumState) { _, sum -> sum }) { _, _ -> error("Unexpected coincidence") } @@ -751,11 +737,11 @@ class KairosTests { } @Test - fun mergeTFlows() = runFrpTest { network -> - val first = network.mutableTFlow<Int>() - val stopFirst = network.mutableTFlow<Unit>() - val second = network.mutableTFlow<Int>() - val stopSecond = network.mutableTFlow<Unit>() + fun mergeEventss() = runFrpTest { network -> + val first = network.mutableEvents<Int>() + val stopFirst = network.mutableEvents<Unit>() + val second = network.mutableEvents<Int>() + val stopSecond = network.mutableEvents<Unit>() var stepResult: Int? = null val flow: SharedFlow<Int> @@ -832,19 +818,19 @@ class KairosTests { secondEmitDuration: ${secondEmitDuration.toString(DurationUnit.MILLISECONDS, 2)} stopFirstDuration: ${stopFirstDuration.toString(DurationUnit.MILLISECONDS, 2)} testDeadEmitFirstDuration: ${ - testDeadEmitFirstDuration.toString( - DurationUnit.MILLISECONDS, - 2, - ) - } + testDeadEmitFirstDuration.toString( + DurationUnit.MILLISECONDS, + 2, + ) + } secondEmitDuration2: ${secondEmitDuration2.toString(DurationUnit.MILLISECONDS, 2)} stopSecondDuration: ${stopSecondDuration.toString(DurationUnit.MILLISECONDS, 2)} testDeadEmitSecondDuration: ${ - testDeadEmitSecondDuration.toString( - DurationUnit.MILLISECONDS, - 2, - ) - } + testDeadEmitSecondDuration.toString( + DurationUnit.MILLISECONDS, + 2, + ) + } """ .trimIndent() ) @@ -852,10 +838,10 @@ class KairosTests { @Test fun sampleCancel() = runFrpTest { network -> - val updater = network.mutableTFlow<Int>() - val stopUpdater = network.mutableTFlow<Unit>() - val sampler = network.mutableTFlow<Unit>() - val stopSampler = network.mutableTFlow<Unit>() + val updater = network.mutableEvents<Int>() + val stopUpdater = network.mutableEvents<Unit>() + val sampler = network.mutableEvents<Unit>() + val stopSampler = network.mutableEvents<Unit>() var stepResult: Int? = null val flow = activateSpecWithResult(network) { @@ -863,7 +849,7 @@ class KairosTests { val samplerS = sampler.takeUntil(stopSamplerFirst) val stopUpdaterFirst = stopUpdater val updaterS = updater.takeUntil(stopUpdaterFirst) - val sampledS = samplerS.sample(updaterS.hold(0)) { _, b -> b } + val sampledS = samplerS.sample(updaterS.holdState(0)) { _, b -> b } sampledS.toSharedFlow() } @@ -894,35 +880,35 @@ class KairosTests { @Test fun combineStates_differentUpstreams() = runFrpTest { network -> - val a = network.mutableTFlow<Int>() - val b = network.mutableTFlow<Int>() + val a = network.mutableEvents<Int>() + val b = network.mutableEvents<Int>() var observed: Pair<Int, Int>? = null - val tState = + val state = activateSpecWithResult(network) { - val state = combine(a.hold(0), b.hold(0)) { a, b -> Pair(a, b) } - state.stateChanges.observe { observed = it } + val state = combine(a.holdState(0), b.holdState(0)) { a, b -> Pair(a, b) } + state.changes.observe { observed = it } state } - assertEquals(0 to 0, network.transact { tState.sample() }) + assertEquals(0 to 0, network.transact { state.sample() }) assertEquals(null, observed) a.emit(5) assertEquals(5 to 0, observed) - assertEquals(5 to 0, network.transact { tState.sample() }) + assertEquals(5 to 0, network.transact { state.sample() }) b.emit(3) assertEquals(5 to 3, observed) - assertEquals(5 to 3, network.transact { tState.sample() }) + assertEquals(5 to 3, network.transact { state.sample() }) } @Test fun sampleCombinedStates() = runFrpTest { network -> - val updater = network.mutableTFlow<Int>() - val emitter = network.mutableTFlow<Unit>() + val updater = network.mutableEvents<Int>() + val emitter = network.mutableEvents<Unit>() val result = activateSpecWithResult(network) { - val bA = updater.map { it * 2 }.hold(0) - val bB = updater.hold(0) - val combineD: TState<Pair<Int, Int>> = bA.combineWith(bB) { a, b -> a to b } + val bA = updater.map { it * 2 }.holdState(0) + val bB = updater.holdState(0) + val combineD: State<Pair<Int, Int>> = bA.combineWith(bB) { a, b -> a to b } val sampleS = emitter.sample(combineD) { _, b -> b } sampleS.nextDeferred() } @@ -943,13 +929,13 @@ class KairosTests { @Test fun switchMapPromptly() = runFrpTest { network -> - val emitter = network.mutableTFlow<Unit>() + val emitter = network.mutableEvents<Unit>() val result = activateSpecWithResult(network) { emitter .map { emitter.map { 1 }.map { it + 1 }.map { it * 2 } } - .hold(emptyTFlow) - .switchPromptly() + .holdState(emptyEvents) + .switchEventsPromptly() .nextDeferred() } runCurrent() @@ -963,8 +949,8 @@ class KairosTests { @Test fun switchDeeper() = runFrpTest { network -> - val emitter = network.mutableTFlow<Unit>() - val e2 = network.mutableTFlow<Unit>() + val emitter = network.mutableEvents<Unit>() + val e2 = network.mutableEvents<Unit>() val result = activateSpecWithResult(network) { val tres = @@ -989,14 +975,14 @@ class KairosTests { @Test fun recursionBasic() = runFrpTest { network -> - val add1 = network.mutableTFlow<Unit>() - val sub1 = network.mutableTFlow<Unit>() + val add1 = network.mutableEvents<Unit>() + val sub1 = network.mutableEvents<Unit>() val stepResult: StateFlow<Int> = activateSpecWithResult(network) { - val dSum = TStateLoop<Int>() + val dSum = StateLoop<Int>() val sAdd1 = add1.sample(dSum) { _, sum -> sum + 1 } val sMinus1 = sub1.sample(dSum) { _, sum -> sum - 1 } - dSum.loopback = sAdd1.mergeWith(sMinus1) { a, _ -> a }.hold(0) + dSum.loopback = sAdd1.mergeWith(sMinus1) { a, _ -> a }.holdState(0) dSum.toStateFlow() } runCurrent() @@ -1018,16 +1004,17 @@ class KairosTests { } @Test - fun recursiveTState() = runFrpTest { network -> - val e = network.mutableTFlow<Unit>() + fun recursiveState() = runFrpTest { network -> + val e = network.mutableEvents<Unit>() var changes = 0 val state = activateSpecWithResult(network) { - val s = TFlowLoop<Unit>() - val deferred = s.map { tStateOf(null) } - val e3 = e.map { tStateOf(Unit) } - val flattened = e3.mergeWith(deferred) { a, _ -> a }.hold(tStateOf(null)).flatten() - s.loopback = emptyTFlow + val s = EventsLoop<Unit>() + val deferred = s.map { stateOf(null) } + val e3 = e.map { stateOf(Unit) } + val flattened = + e3.mergeWith(deferred) { a, _ -> a }.holdState(stateOf(null)).flatten() + s.loopback = emptyEvents flattened.toStateFlow() } @@ -1037,7 +1024,7 @@ class KairosTests { @Test fun fanOut() = runFrpTest { network -> - val e = network.mutableTFlow<Map<String, Int>>() + val e = network.mutableEvents<Map<String, Int>>() val (fooFlow, barFlow) = activateSpecWithResult(network) { val selector = e.groupByKey() @@ -1073,15 +1060,15 @@ class KairosTests { @Test fun fanOutLateSubscribe() = runFrpTest { network -> - val e = network.mutableTFlow<Map<String, Int>>() + val e = network.mutableEvents<Map<String, Int>>() val barFlow = activateSpecWithResult(network) { val selector = e.groupByKey() selector .eventsForKey("foo") .map { selector.eventsForKey("bar") } - .hold(emptyTFlow) - .switchPromptly() + .holdState(emptyEvents) + .switchEventsPromptly() .toSharedFlow() } val stateFlow = barFlow.stateIn(backgroundScope, SharingStarted.Eagerly, null) @@ -1096,9 +1083,9 @@ class KairosTests { } @Test - fun inputFlowCompleted() = runFrpTest { network -> + fun inpueventsCompleted() = runFrpTest { network -> val results = mutableListOf<Int>() - val e = network.mutableTFlow<Int>() + val e = network.mutableEvents<Int>() activateSpec(network) { e.nextOnly().observe { results.add(it) } } runCurrent() @@ -1114,49 +1101,59 @@ class KairosTests { @Test fun fanOutThenMergeIncrementally() = runFrpTest { network -> - // A tflow of group updates, where a group is a tflow of child updates, where a child is a + // A events of group updates, where a group is a events of child updates, where a child is a // stateflow - val e = network.mutableTFlow<Map<Int, Maybe<TFlow<Map<Int, Maybe<StateFlow<String>>>>>>>() + val e = network.mutableEvents<Map<Int, Maybe<Events<Map<Int, Maybe<StateFlow<String>>>>>>>() println("fanOutMergeInc START") val state = activateSpecWithResult(network) { - // Convert nested Flows to nested TFlow/TState - val emitter: TFlow<Map<Int, Maybe<TFlow<Map<Int, Maybe<TState<String>>>>>>> = + // Convert nested Flows to nested Events/State + val emitter: Events<Map<Int, Maybe<Events<Map<Int, Maybe<State<String>>>>>>> = e.mapBuild { m -> m.mapValues { (_, mFlow) -> mFlow.map { it.mapBuild { m2 -> + println("m2: $m2") m2.mapValues { (_, mState) -> - mState.map { stateFlow -> stateFlow.toTState() } + mState.map { stateFlow -> stateFlow.toState() } } } } } } - // Accumulate all of our updates into a single TState - val accState: TState<Map<Int, Map<Int, String>>> = + // Accumulate all of our updates into a single State + val accState: State<Map<Int, Map<Int, String>>> = emitter .mapStateful { - changeMap: Map<Int, Maybe<TFlow<Map<Int, Maybe<TState<String>>>>>> -> + changeMap: Map<Int, Maybe<Events<Map<Int, Maybe<State<String>>>>>> -> changeMap.mapValues { (groupId, mGroupChanges) -> mGroupChanges.map { - groupChanges: TFlow<Map<Int, Maybe<TState<String>>>> -> + groupChanges: Events<Map<Int, Maybe<State<String>>>> -> // New group val childChangeById = groupChanges.groupByKey() - val map: TFlow<Map<Int, Maybe<TFlow<Maybe<TState<String>>>>>> = + val map: Events<Map<Int, Maybe<Events<Maybe<State<String>>>>>> = groupChanges.mapStateful { - gChangeMap: Map<Int, Maybe<TState<String>>> -> + gChangeMap: Map<Int, Maybe<State<String>>> -> + println("gChangeMap: $gChangeMap") gChangeMap.mapValues { (childId, mChild) -> - mChild.map { child: TState<String> -> + mChild.map { child: State<String> -> println("new child $childId in the house") // New child val eRemoved = childChangeById .eventsForKey(childId) .filter { it === None } - .nextOnly() + .onEach { + println( + "removing? (groupId=$groupId, childId=$childId)" + ) + } + .nextOnly( + name = + "eRemoved(groupId=$groupId, childId=$childId)" + ) - val addChild: TFlow<Maybe<TState<String>>> = + val addChild: Events<Maybe<State<String>>> = now.map { mChild } .onEach { println( @@ -1164,7 +1161,7 @@ class KairosTests { ) } - val removeChild: TFlow<Maybe<TState<String>>> = + val removeChild: Events<Maybe<State<String>>> = eRemoved .onEach { println( @@ -1173,23 +1170,28 @@ class KairosTests { } .map { none() } - addChild.mergeWith(removeChild) { _, _ -> + addChild.mergeWith( + removeChild, + name = + "childUpdatesMerged(groupId=$groupId, childId=$childId)", + ) { _, _ -> error("unexpected coincidence") } } } } - val mergeIncrementally: TFlow<Map<Int, Maybe<TState<String>>>> = + val mergeIncrementally: Events<Map<Int, Maybe<State<String>>>> = map.onEach { println("merge patch: $it") } - .mergeIncrementallyPromptly() + .mergeIncrementallyPromptly(name = "mergeIncrementally") mergeIncrementally - .onEach { println("patch: $it") } - .foldMapIncrementally() + .onEach { println("foldmap patch: $it") } + .foldStateMapIncrementally() .flatMap { it.combine() } } } } - .foldMapIncrementally() + .onEach { println("fold patch: $it") } + .foldStateMapIncrementally() .flatMap { it.combine() } accState.toStateFlow() @@ -1198,7 +1200,7 @@ class KairosTests { assertEquals(emptyMap(), state.value) - val emitter2 = network.mutableTFlow<Map<Int, Maybe<StateFlow<String>>>>() + val emitter2 = network.mutableEvents<Map<Int, Maybe<StateFlow<String>>>>() println() println("init outer 0") e.emit(mapOf(0 to just(emitter2.onEach { println("emitter2 emit: $it") }))) @@ -1233,6 +1235,10 @@ class KairosTests { assertEquals(mapOf(0 to mapOf(10 to "(2, 10)")), state.value) + // LogEnabled = true + + println("batch update") + // batch update emitter2.emit( mapOf( @@ -1248,13 +1254,13 @@ class KairosTests { @Test fun applyLatestNetworkChanges() = runFrpTest { network -> - val newCount = network.mutableTFlow<FrpSpec<Flow<Int>>>() + val newCount = network.mutableEvents<BuildSpec<Flow<Int>>>() val flowOfFlows: Flow<Flow<Int>> = activateSpecWithResult(network) { newCount.applyLatestSpec().toSharedFlow() } runCurrent() - val incCount = network.mutableTFlow<Unit>() - fun newFlow(): FrpSpec<SharedFlow<Int>> = frpSpec { + val incCount = network.mutableEvents<Unit>() + fun newFlow(): BuildSpec<SharedFlow<Int>> = buildSpec { launchEffect { try { println("new flow!") @@ -1263,16 +1269,16 @@ class KairosTests { println("cancelling old flow") } } - lateinit var count: TState<Int> + lateinit var count: State<Int> count = incCount .onEach { println("incrementing ${count.sample()}") } - .fold(0) { _, c -> c + 1 } - count.stateChanges.toSharedFlow() + .foldState(0) { _, c -> c + 1 } + count.changes.toSharedFlow() } var outerCount = 0 - val lastFlows: StateFlow<Pair<StateFlow<Int?>, StateFlow<Int?>>> = + val laseventss: StateFlow<Pair<StateFlow<Int?>, StateFlow<Int?>>> = flowOfFlows .map { it.stateIn(backgroundScope, SharingStarted.Eagerly, null) } .pairwise(MutableStateFlow(null)) @@ -1290,18 +1296,18 @@ class KairosTests { assertEquals(1, outerCount) // assertEquals(1, incCount.subscriptionCount) - assertNull(lastFlows.value.second.value) + assertNull(laseventss.value.second.value) incCount.emit(Unit) runCurrent() println("checking") - assertEquals(1, lastFlows.value.second.value) + assertEquals(1, laseventss.value.second.value) incCount.emit(Unit) runCurrent() - assertEquals(2, lastFlows.value.second.value) + assertEquals(2, laseventss.value.second.value) newCount.emit(newFlow()) runCurrent() @@ -1309,17 +1315,17 @@ class KairosTests { runCurrent() // verify old flow is not getting updates - assertEquals(2, lastFlows.value.first.value) + assertEquals(2, laseventss.value.first.value) // but the new one is - assertEquals(1, lastFlows.value.second.value) + assertEquals(1, laseventss.value.second.value) } @Test fun buildScope_stateAccumulation() = runFrpTest { network -> - val input = network.mutableTFlow<Unit>() + val input = network.mutableEvents<Unit>() var observedCount: Int? = null activateSpec(network) { - val (c, j) = asyncScope { input.fold(0) { _, x -> x + 1 } } + val (c, j) = asyncScope { input.foldState(0) { _, x -> x + 1 } } deferredBuildScopeAction { c.get().observe { observedCount = it } } } runCurrent() @@ -1336,7 +1342,7 @@ class KairosTests { @Test fun effect() = runFrpTest { network -> - val input = network.mutableTFlow<Unit>() + val input = network.mutableEvents<Unit>() var effectRunning = false var count = 0 activateSpec(network) { @@ -1348,7 +1354,7 @@ class KairosTests { effectRunning = false } } - merge(emptyTFlow, input.nextOnly()).observe { + merge(emptyEvents, input.nextOnly()).observe { count++ j.cancel() } @@ -1372,21 +1378,21 @@ class KairosTests { private fun runFrpTest( timeout: Duration = 3.seconds, - block: suspend TestScope.(FrpNetwork) -> Unit, + block: suspend TestScope.(KairosNetwork) -> Unit, ) { runTest(timeout = timeout) { - val network = backgroundScope.newFrpNetwork() + val network = backgroundScope.launchKairosNetwork() runCurrent() block(network) } } - private fun TestScope.activateSpec(network: FrpNetwork, spec: FrpSpec<*>) = + private fun TestScope.activateSpec(network: KairosNetwork, spec: BuildSpec<*>) = backgroundScope.launch { network.activateSpec(spec) } private suspend fun <R> TestScope.activateSpecWithResult( - network: FrpNetwork, - spec: FrpSpec<R>, + network: KairosNetwork, + spec: BuildSpec<R>, ): R = CompletableDeferred<R>() .apply { activateSpec(network) { complete(spec.applySpec()) } } |