[FRP] Functional reactive programming library
Flag: EXEMPT new library, unused
Test: atest kt-frp-test
Change-Id: Iaa7fe31dbd8612542730e1821384fd82f551c9f1
diff --git a/packages/SystemUI/frp/Android.bp b/packages/SystemUI/frp/Android.bp
new file mode 100644
index 0000000..c3381db
--- /dev/null
+++ b/packages/SystemUI/frp/Android.bp
@@ -0,0 +1,49 @@
+//
+// 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 {
+ default_team: "trendy_team_system_ui_please_use_a_more_specific_subteam_if_possible_",
+ default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"],
+}
+
+java_library {
+ name: "kt-frp",
+ host_supported: true,
+ kotlincflags: ["-opt-in=com.android.systemui.experimental.frp.ExperimentalFrpApi"],
+ srcs: ["src/**/*.kt"],
+ static_libs: [
+ "kotlin-stdlib",
+ "kotlinx_coroutines",
+ ],
+}
+
+java_test {
+ name: "kt-frp-test",
+ optimize: {
+ enabled: false,
+ },
+ srcs: [
+ "test/**/*.kt",
+ ],
+ static_libs: [
+ "kt-frp",
+ "junit",
+ "kotlin-stdlib",
+ "kotlin-test",
+ "kotlinx_coroutines",
+ "kotlinx_coroutines_test",
+ ],
+}
diff --git a/packages/SystemUI/frp/OWNERS b/packages/SystemUI/frp/OWNERS
new file mode 100644
index 0000000..8876ad6
--- /dev/null
+++ b/packages/SystemUI/frp/OWNERS
@@ -0,0 +1,3 @@
+steell@google.com
+nijamkin@google.com
+evanlaird@google.com
diff --git a/packages/SystemUI/frp/README.md b/packages/SystemUI/frp/README.md
new file mode 100644
index 0000000..9c5bdb0
--- /dev/null
+++ b/packages/SystemUI/frp/README.md
@@ -0,0 +1,64 @@
+# kt-frp
+
+A functional reactive programming (FRP) library for Kotlin.
+
+This library is **experimental** and should not be used for general production
+code. The APIs within are subject to change, and there may be bugs.
+
+## About FRP
+
+Functional reactive programming is a type of reactive programming system that
+follows a set of clear and composable rules, without sacrificing consistency.
+FRP exposes an API that should be familiar to those versed in Kotlin `Flow`.
+
+### Details for nerds
+
+`kt-frp` implements an applicative / monadic flavor of FRP, using a push-pull
+methodology to allow for efficient updates.
+
+"Real" functional reactive programming should be specified with denotational
+semantics ([wikipedia](https://en.wikipedia.org/wiki/Denotational_semantics)):
+you can view the semantics for `kt-frp` [here](docs/semantics.md).
+
+## Usage
+
+First, stand up a new `FrpNetwork`. All reactive events and state is kept
+consistent within a single network.
+
+``` kotlin
+val coroutineScope: CoroutineScope = ...
+val frpNetwork = coroutineScope.newFrpNetwork()
+```
+
+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").
+
+``` kotlin
+suspend fun activate(network: FrpNetwork) {
+ network.activateSpec {
+ val input = network.mutableTFlow<Unit>()
+ // Launch a long-running side-effect that emits to the network
+ // every second.
+ launchEffect {
+ while (true) {
+ input.emit(Unit)
+ delay(1.seconds)
+ }
+ }
+ // Accumulate state
+ val count: TState<Int> = input.fold { _, i -> i + 1 }
+ // Observe events to perform side-effects in reaction to them
+ input.observe {
+ println("Got event ${count.sample()} at time: ${System.currentTimeMillis()}")
+ }
+ }
+}
+```
+
+`FrpNetwork.activateSpec` will suspend indefinitely; cancelling the invocation
+will tear-down all effects and obervers running within the lambda.
+
+## Resources
+
+- [Cheatsheet for those coming from Kotlin Flow](docs/flow-to-frp-cheatsheet.md)
diff --git a/packages/SystemUI/frp/docs/flow-to-frp-cheatsheet.md b/packages/SystemUI/frp/docs/flow-to-frp-cheatsheet.md
new file mode 100644
index 0000000..e20f3e6
--- /dev/null
+++ b/packages/SystemUI/frp/docs/flow-to-frp-cheatsheet.md
@@ -0,0 +1,330 @@
+# From Flows to FRP
+
+## Key differences
+
+* FRP evaluates all events (`TFlow` emissions + observers) in a transaction.
+
+* FRP splits `Flow` APIs into two distinct types: `TFlow` and `TState`
+
+ * `TFlow` is roughly equivalent to `SharedFlow` w/ a replay cache that
+ exists for the duration of the current FRP transaction and shared with
+ `SharingStarted.WhileSubscribed()`
+
+ * `TState` is roughly equivalent to `StateFlow` shared with
+ `SharingStarted.Eagerly`, but the current value can only be queried within
+ a FRP transaction, and the value is only updated at the end of the
+ transaction
+
+* FRP further divides `Flow` APIs based on how they internally use state:
+
+ * **FrpTransactionScope:** APIs that internally query some state need to be
+ performed within an FRP transaction
+
+ * this scope is available from the other scopes, and from most lambdas
+ passed to other FRP APIs
+
+ * **FrpStateScope:** APIs that internally accumulate state in reaction to
+ events need to be performed within an FRP 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
+
+ * **FrpBuildScope:** APIs that perform external side-effects (`Flow.collect`)
+ need to be performed within an FRP Build scope (akin to a `CoroutineScope`)
+
+ * this scope is available from `FrpNetwork.activateSpec { … }`
+
+ * All other APIs can be used anywhere
+
+## emptyFlow()
+
+Use `emptyTFlow`
+
+``` kotlin
+// this TFlow emits nothing
+val noEvents: TFlow<Int> = emptyTFlow
+```
+
+## map { … }
+
+Use `TFlow.map` / `TState.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 }
+```
+
+## filter { … } / mapNotNull { … }
+
+### I have a TFlow
+
+Use `TFlow.filter` / `TFlow.mapNotNull`
+
+``` kotlin
+val messages: TFlow<String> = …
+val nonEmpty: TFlow<String> = messages.filter { it.isNotEmpty() }
+```
+
+### I have a TState
+
+Convert the `TState` to `TFlow` using `TState.stateChanges`, then use
+`TFlow.filter` / `TFlow.mapNotNull`
+
+If you need to convert back to `TState`, use `TFlow.hold(initialValue)` on the
+result.
+
+``` kotlin
+tState.stateChanges.filter { … }.hold(initialValue)
+```
+
+Note that `TFlow.hold` is only available within an `FrpStateScope` in order to
+track the lifetime of the state accumulation.
+
+## combine(...) { … }
+
+### I have TStates
+
+Use `combine(TStates)`
+
+``` kotlin
+val someInt: TState<Int> = …
+val someString: TState<String> = …
+val model: TState<MyModel> = combine(someInt, someString) { i, s -> MyModel(i, s) }
+```
+
+### I have TFlows
+
+Convert the TFlows to TStates using `TFlow.hold(initialValue)`, then use
+`combine(TStates)`
+
+If you want the behavior of Flow.combine where nothing is emitted until each
+TFlow 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 ->
+ 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.
+
+#### Explanation
+
+`Flow.combine` always tracks the last-emitted value of each `Flow` it's
+combining. This is a form of state-accumulation; internally, it collects from
+each `Flow`, tracks the latest-emitted value, and when anything changes, it
+re-runs the lambda to combine the latest values.
+
+An effect of this is that `Flow.combine` doesn't emit until each combined `Flow`
+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.
+
+FRP avoids this gotcha by forcing usage of `TState` for `combine`, thus ensuring
+that there is always a current value to be combined for each input.
+
+## collect { … }
+
+Use `observe { … }`
+
+``` kotlin
+val job: Job = tFlow.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.
+
+## sample(flow) { … }
+
+### I want to sample a TState
+
+Use `TState.sample()` to get the current value of a `TState`. This can be
+invoked anywhere you have access to an `FrpTransactionScope`.
+
+``` kotlin
+// the lambda passed to map receives an FrpTransactionScope, so it can invoke
+// sample
+tFlow.map { tState.sample() }
+```
+
+#### Explanation
+
+To keep all state-reads consistent, the current value of a TState can only be
+queried within an FRP transaction, modeled with `FrpTransactionScope`. Note that
+both `FrpStateScope` and `FrpBuildScope` extend `FrpTransactionScope`.
+
+### I want to sample a TFlow
+
+Convert to a `TState` by using `TFlow.hold(initialValue)`, then use `sample`.
+
+Note that `hold` is only available within an `FrpStateScope` 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.
+
+``` kotlin
+val ints: TFlow<Int> = …
+val lastSeenInt: TState<Int> = ints.hold(initialValue = 0)
+```
+
+Note that `hold` is only available within an `FrpStateScope` 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`.
+
+## distinctUntilChanged()
+
+Use `distinctUntilChanged` like normal. This is only available for `TFlow`;
+`TStates` are already `distinctUntilChanged`.
+
+## merge(...)
+
+### I have TFlows
+
+Use `merge(TFlows) { … }`. The lambda argument is used to disambiguate multiple
+simultaneous emissions within the same transaction.
+
+#### Explanation
+
+Under FRP'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
+single value. The lambda argument allows the developer to decide what to do in
+this case.
+
+### I have TStates
+
+If `combine` doesn't satisfy your needs, you can use `TState.stateChanges` to
+convert to a `TFlow`, and then `merge`.
+
+## conflatedCallbackFlow { … }
+
+Use `tFlow { … }`.
+
+As a shortcut, if you already have a `conflatedCallbackFlow { … }`, you can
+convert it to a TFlow via `Flow.toTFlow()`.
+
+Note that `tFlow` is only available within an `FrpBuildScope` in order to track
+the lifetime of the input registration.
+
+## first()
+
+### I have a TState
+
+Use `TState.sample`.
+
+### I have a TFlow
+
+Use `TFlow.nextOnly`, which works exactly like `Flow.first` but instead of
+suspending it returns a `TFlow` 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.
+
+## 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…
+
+#### …and want to switch TStates
+
+Use `TState.flatMap`
+
+``` kotlin
+val flattened = tState.flatMap { a -> getTState(a) }
+```
+
+#### …and want to switch TFlows
+
+Use `TState<TFlow<T>>.switch()`
+
+``` kotlin
+val tFlow = tState.map { a -> getTFlow(a) }.switch()
+```
+
+### I have a TFlow…
+
+#### …and want to switch TFlows
+
+Use `hold` to convert to a `TState<TFlow<T>>`, then use `switch` to switch to
+the latest `TFlow`.
+
+``` kotlin
+val tFlow = tFlowOfFlows.hold(emptyTFlow).switch()
+```
+
+#### …and want to switch TStates
+
+Use `hold` to convert to a `TState<TState<T>>`, then use `flatMap` to switch to
+the latest `TState`.
+
+``` kotlin
+val tState = tFlowOfStates.hold(tStateOf(initialValue)).flatMap { it }
+```
+
+## mapLatest { … } / collectLatest { … }
+
+`FrpStateScope` and `FrpBuildScope` 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 ->
+ effect { "new model in the house: $model" }
+ model.someState.observe { "someState: $it" }
+ val someData: TState<SomeInfo> =
+ getBroadcasts(model.uri)
+ .map { extractInfo(it) }
+ .hold(initialInfo)
+ …
+}
+```
+
+## flowOf(...)
+
+### I want a TState
+
+Use `tStateOf(initialValue)`.
+
+### I want a TFlow
+
+Use `now.map { initialValue }`
+
+Note that `now` is only available within an `FrpTransactionScope`.
+
+#### Explanation
+
+`TFlows` 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
+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.
+
+``` kotlin
+fun <T> FrpTransactionScope.tFlowOf(value: T): TFlow<T> = now.map { value }
+```
+
+## MutableStateFlow / MutableSharedFlow
+
+Use `MutableTState(frpNetwork, initialValue)` and `MutableTFlow(frpNetwork)`.
diff --git a/packages/SystemUI/frp/docs/semantics.md b/packages/SystemUI/frp/docs/semantics.md
new file mode 100644
index 0000000..b533190
--- /dev/null
+++ b/packages/SystemUI/frp/docs/semantics.md
@@ -0,0 +1,225 @@
+# FRP Semantics
+
+`kt-frp`'s pure API is based off of the following denotational semantics
+([wikipedia](https://en.wikipedia.org/wiki/Denotational_semantics)).
+
+The semantics model `kt-frp` types as time-varying values; by making `Time` a
+first-class value, we can define a referentially-transparent API that allows us
+to reason about the behavior of the pure FRP combinators. This is
+implementation-agnostic; we can compare the behavior of any implementation with
+expected behavior denoted by these semantics to identify bugs.
+
+The semantics are written in pseudo-Kotlin; places where we are deviating from
+real Kotlin are noted with comments.
+
+``` kotlin
+
+sealed class Time : Comparable<Time> {
+ object BigBang : Time()
+ data class At(time: BigDecimal) : Time()
+ object Infinity : Time()
+
+ override final fun compareTo(other: Time): Int =
+ when (this) {
+ BigBang -> if (other === BigBang) 0 else -1
+ is At -> when (other) {
+ BigBang -> 1
+ is At -> time.compareTo(other.time)
+ Infinity -> -1
+ }
+ Infinity -> if (other === Infinity) 0 else 1
+ }
+}
+
+typealias Transactional<T> = (Time) -> T
+
+typealias TFlow<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(
+ internal val current: Transactional<T>,
+ val stateChanges: TFlow<T>,
+)
+
+val emptyTFlow: TFlow<Nothing> = emptyMap()
+
+fun <A, B> TFlow<A>.map(f: FrpTransactionScope.(A) -> B): TFlow<B> =
+ mapValues { (t, a) -> FrpTransactionScope(t).f(a) }
+
+fun <A> TFlow<A>.filter(f: FrpTransactionScope.(A) -> Boolean): TFlow<A> =
+ filter { (t, a) -> FrpTransactionScope(t).f(a) }
+
+fun <A> merge(
+ first: TFlow<A>,
+ second: TFlow<A>,
+ onCoincidence: Time.(A, A) -> A,
+): TFlow<A> =
+ first.toMutableMap().also { result ->
+ second.forEach { (t, a) ->
+ result.merge(t, a) { f, s ->
+ FrpTranscationScope(t).onCoincidence(f, a)
+ }
+ }
+ }.toSortedMap()
+
+fun <A> TState<TFlow<A>>.switch(): TFlow<A> {
+ val truncated = listOf(Pair(Time.BigBang, current.invoke(Time.BigBang))) +
+ stateChanges.dropWhile { (time, _) -> time < time0 }
+ val events =
+ truncated
+ .pairwise()
+ .flatMap { ((t0, sa), (t2, _)) ->
+ sa.filter { (t1, a) -> t0 < t1 && t1 <= t2 }
+ }
+ return events.toSortedMap()
+}
+
+fun <A> TState<TFlow<A>>.switchPromptly(): TFlow<A> {
+ val truncated = listOf(Pair(Time.BigBang, current.invoke(Time.BigBang))) +
+ stateChanges.dropWhile { (time, _) -> time < time0 }
+ val events =
+ truncated
+ .pairwise()
+ .flatMap { ((t0, sa), (t2, _)) ->
+ sa.filter { (t1, a) -> t0 <= t1 && t1 <= t2 }
+ }
+ return events.toSortedMap()
+}
+
+typealias GroupedTFlow<K, V> = TFlow<Map<K, V>>
+
+fun <K, V> TFlow<Map<K, V>>.groupByKey(): GroupedTFlow<K, V> = this
+
+fun <K, V> GroupedTFlow<K, V>.eventsForKey(key: K): TFlow<V> =
+ map { m -> m[k] }.filter { it != null }.map { it!! }
+
+fun <A, B> TState<A>.map(f: (A) -> B): TState<B> =
+ TState(
+ current = { t -> f(current.invoke(t)) },
+ stateChanges = stateChanges.map { f(it) },
+ )
+
+fun <A, B, C> TState<A>.combineWith(
+ other: TState<B>,
+ f: (A, B) -> C,
+): TState<C> =
+ TState(
+ current = { t -> f(current.invoke(t), other.current.invoke(t)) },
+ stateChanges = run {
+ val aChanges =
+ stateChanges
+ .map { a ->
+ val b = other.current.sample()
+ Triple(a, b, f(a, b))
+ }
+ val bChanges =
+ other
+ .stateChanges
+ .map { b ->
+ val a = current.sample()
+ Triple(a, b, f(a, b))
+ }
+ merge(aChanges, bChanges) { (a, _, _), (_, b, _) ->
+ Triple(a, b, f(a, b))
+ }
+ .map { (_, _, zipped) -> zipped }
+ },
+ )
+
+fun <A> TState<TState<A>>.flatten(): TState<A> {
+ val changes =
+ stateChanges
+ .pairwise()
+ .flatMap { ((t0, oldInner), (t2, _)) ->
+ val inWindow =
+ oldInner
+ .stateChanges
+ .filter { (t1, b) -> t0 <= t1 && t1 < t2 }
+ if (inWindow.firstOrNull()?.time != t0) {
+ listOf(Pair(t0, oldInner.current.invoke(t0))) + inWindow
+ } else {
+ inWindow
+ }
+ }
+ return TState(
+ current = { t -> current.invoke(t).current.invoke(t) },
+ stateChanges = changes.toSortedMap(),
+ )
+}
+
+open class FrpTranscationScope internal constructor(
+ internal val currentTime: Time,
+) {
+ val now: TFlow<Unit> =
+ sortedMapOf(currentTime to Unit)
+
+ fun <A> Transactional<A>.sample(): A =
+ invoke(currentTime)
+
+ fun <A> TState<A>.sample(): A =
+ current.sample()
+}
+
+class FrpStateScope internal constructor(
+ time: Time,
+ internal val stopTime: Time,
+): FrpTransactionScope(time) {
+
+ fun <A, B> TFlow<A>.fold(
+ initialValue: B,
+ f: FrpTransactionScope.(B, A) -> B,
+ ): TState<B> {
+ val truncated =
+ dropWhile { (t, _) -> t < currentTime }
+ .takeWhile { (t, _) -> t <= stopTime }
+ val folded =
+ truncated
+ .scan(Pair(currentTime, initialValue)) { (_, b) (t, a) ->
+ Pair(t, FrpTransactionScope(t).f(a, b))
+ }
+ val lookup = { t1 ->
+ folded.lastOrNull { (t0, _) -> t0 < t1 }?.value ?: initialValue
+ }
+ return TState(lookup, folded.toSortedMap())
+ }
+
+ fun <A> TFlow<A>.hold(initialValue: A): TState<A> =
+ fold(initialValue) { _, a -> a }
+
+ fun <K, V> TFlow<Map<K, Maybe<V>>>.foldMapIncrementally(
+ initialValues: Map<K, V>
+ ): TState<Map<K, V>> =
+ fold(initialValues) { patch, map ->
+ val eithers = patch.map { (k, v) ->
+ if (v is Just) Left(k to v.value) else Right(k)
+ }
+ val adds = eithers.filterIsInstance<Left>().map { it.left }
+ val removes = eithers.filterIsInstance<Right>().map { it.right }
+ val removed: Map<K, V> = map - removes.toSet()
+ val updated: Map<K, V> = removed + adds
+ 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, A, B> TFlow<Map<K, Maybe<A>>.mapLatestStatefulForKey(
+ transform: suspend FrpStateScope.(A) -> B,
+ ): TFlow<Map<K, Maybe<B>>> =
+ pairwise().map { ((t0, patch), (t1, _)) ->
+ patch.map { (k, ma) ->
+ ma.map { a ->
+ FrpStateScope(t0, t1).transform(a)
+ }
+ }
+ }
+ }
+
+}
+
+```
diff --git a/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/Combinators.kt b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/Combinators.kt
new file mode 100644
index 0000000..298c071
--- /dev/null
+++ b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/Combinators.kt
@@ -0,0 +1,250 @@
+/*
+ * 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.experimental.frp
+
+import com.android.systemui.experimental.frp.util.These
+import com.android.systemui.experimental.frp.util.just
+import com.android.systemui.experimental.frp.util.none
+import kotlinx.coroutines.flow.Flow
+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.
+ */
+fun <A> TFlow<Transactional<A>>.sampleTransactionals(): TFlow<A> = map { it.sample() }
+
+/** @see FrpTransactionScope.sample */
+fun <A, B, C> TFlow<A>.sample(
+ state: TState<B>,
+ transform: suspend FrpTransactionScope.(A, B) -> C,
+): TFlow<C> = map { transform(it, state.sample()) }
+
+/** @see FrpTransactionScope.sample */
+fun <A, B, C> TFlow<A>.sample(
+ transactional: Transactional<B>,
+ transform: suspend FrpTransactionScope.(A, B) -> C,
+): TFlow<C> = map { transform(it, transactional.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].
+ *
+ * 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
+ */
+fun <A, B, C> TFlow<A>.samplePromptly(
+ state: TState<B>,
+ transform: suspend FrpTransactionScope.(A, B) -> C,
+): TFlow<C> =
+ sample(state) { a, b -> These.thiz<Pair<A, B>, B>(a to b) }
+ .mergeWith(state.stateChanges.map { These.that(it) }) { thiz, that ->
+ These.both((thiz as These.This).thiz, (that as These.That).that)
+ }
+ .mapMaybe { these ->
+ when (these) {
+ // both present, transform the upstream value and the new value
+ is These.Both -> just(transform(these.thiz.first, these.that))
+ // no upstream present, so don't perform the sample
+ is These.That -> none()
+ // just the upstream, so transform the upstream and the old value
+ is These.This -> just(transform(these.thiz.first, these.thiz.second))
+ }
+ }
+
+/**
+ * Returns a [TState] containing a map with a snapshot of the current state of each [TState] in the
+ * original map.
+ */
+fun <K, A> Map<K, TState<A>>.combineValues(): TState<Map<K, A>> =
+ asIterable()
+ .map { (k, state) -> state.map { v -> k to v } }
+ .combine()
+ .map { entries -> entries.toMap() }
+
+/**
+ * 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.
+ */
+fun <A> TFlow<A>.toColdConflatedFlow(network: FrpNetwork): 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.
+ */
+fun <A> TState<A>.toColdConflatedFlow(network: FrpNetwork): 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].
+ *
+ * When collection is cancelled, so is the [FrpSpec]. This means all ongoing work is cleaned up.
+ */
+@JvmName("flowSpecToColdConflatedFlow")
+fun <A> FrpSpec<TFlow<A>>.toColdConflatedFlow(network: FrpNetwork): 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].
+ *
+ * When collection is cancelled, so is the [FrpSpec]. This means all ongoing work is cleaned up.
+ */
+@JvmName("stateSpecToColdConflatedFlow")
+fun <A> FrpSpec<TState<A>>.toColdConflatedFlow(network: FrpNetwork): 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].
+ */
+@JvmName("transactionalFlowToColdConflatedFlow")
+fun <A> Transactional<TFlow<A>>.toColdConflatedFlow(network: FrpNetwork): 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].
+ */
+@JvmName("transactionalStateToColdConflatedFlow")
+fun <A> Transactional<TState<A>>.toColdConflatedFlow(network: FrpNetwork): 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].
+ *
+ * When collection is cancelled, so is the [FrpStateful]. This means all ongoing work is cleaned up.
+ */
+@JvmName("statefulFlowToColdConflatedFlow")
+fun <A> FrpStateful<TFlow<A>>.toColdConflatedFlow(network: FrpNetwork): 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].
+ *
+ * When collection is cancelled, so is the [FrpStateful]. This means all ongoing work is cleaned up.
+ */
+@JvmName("statefulStateToColdConflatedFlow")
+fun <A> FrpStateful<TState<A>>.toColdConflatedFlow(network: FrpNetwork): Flow<A> =
+ channelFlow { network.activateSpec { applyStateful().observe { trySend(it) } } }.conflate()
+
+/** Return a [TFlow] that emits from the original [TFlow] only when [state] is `true`. */
+fun <A> TFlow<A>.filter(state: TState<Boolean>): TFlow<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`. */
+fun allOf(vararg states: TState<Boolean>): TState<Boolean> = combine(*states) { it.allTrue() }
+
+/** Returns a [TState] that is `true` when any of [states] are `true`. */
+fun anyOf(vararg states: TState<Boolean>): TState<Boolean> = combine(*states) { it.anyTrue() }
+
+/** Returns a [TState] containing the inverse of the Boolean held by the original [TState]. */
+fun not(state: TState<Boolean>): TState<Boolean> = state.mapCheapUnsafe { !it }
+
+/**
+ * Represents a modal FRP 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).
+ *
+ * Use [compiledFrpSpec] to compile and stand-up a mode graph.
+ *
+ * @see FrpStatefulMode
+ */
+fun interface FrpBuildMode<out A> {
+ /**
+ * Invoked when this mode is enabled. Returns a value and a [TFlow] that signals a switch to a
+ * new mode.
+ */
+ suspend fun FrpBuildScope.enableMode(): Pair<A, TFlow<FrpBuildMode<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.
+ *
+ * @see FrpBuildMode
+ */
+val <A> FrpBuildMode<A>.compiledFrpSpec: FrpSpec<TState<A>>
+ get() = frpSpec {
+ var modeChangeEvents by TFlowLoop<FrpBuildMode<A>>()
+ val activeMode: TState<Pair<A, TFlow<FrpBuildMode<A>>>> =
+ modeChangeEvents
+ .map { it.run { frpSpec { enableMode() } } }
+ .holdLatestSpec(frpSpec { enableMode() })
+ modeChangeEvents =
+ activeMode.map { statefully { it.second.nextOnly() } }.applyLatestStateful().switch()
+ activeMode.map { it.first }
+ }
+
+/**
+ * Represents a modal FRP 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
+ * accumulation in the process.
+ *
+ * Use [compiledStateful] to compile and stand-up a mode graph.
+ *
+ * @see FrpBuildMode
+ */
+fun interface FrpStatefulMode<out A> {
+ /**
+ * Invoked when this mode is enabled. Returns a value and a [TFlow] that signals a switch to a
+ * new mode.
+ */
+ suspend fun FrpStateScope.enableMode(): Pair<A, TFlow<FrpStatefulMode<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.
+ *
+ * @see FrpBuildMode
+ */
+val <A> FrpStatefulMode<A>.compiledStateful: FrpStateful<TState<A>>
+ get() = statefully {
+ var modeChangeEvents by TFlowLoop<FrpStatefulMode<A>>()
+ val activeMode: TState<Pair<A, TFlow<FrpStatefulMode<A>>>> =
+ modeChangeEvents
+ .map { it.run { statefully { enableMode() } } }
+ .holdLatestStateful(statefully { enableMode() })
+ modeChangeEvents =
+ activeMode.map { statefully { it.second.nextOnly() } }.applyLatestStateful().switch()
+ 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].
+ */
+fun <A> FrpBuildScope.rebuildOn(rebuildSignal: TFlow<*>, spec: FrpSpec<A>): TState<A> =
+ rebuildSignal.map { spec }.holdLatestSpec(spec)
diff --git a/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/FrpBuildScope.kt b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/FrpBuildScope.kt
new file mode 100644
index 0000000..6e4c9eb
--- /dev/null
+++ b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/FrpBuildScope.kt
@@ -0,0 +1,864 @@
+/*
+ * 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.experimental.frp
+
+import com.android.systemui.experimental.frp.util.Maybe
+import com.android.systemui.experimental.frp.util.just
+import com.android.systemui.experimental.frp.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.launch
+
+/** A function that modifies the FrpNetwork. */
+typealias FrpSpec<A> = suspend FrpBuildScope.() -> A
+
+/**
+ * Constructs an [FrpSpec]. The passed [block] will be invoked with an [FrpBuildScope] that can be
+ * used to perform network-building operations, including adding new inputs and outputs to the
+ * network, as well as all operations available in [FrpTransactionScope].
+ */
+@ExperimentalFrpApi
+@Suppress("NOTHING_TO_INLINE")
+inline fun <A> frpSpec(noinline block: suspend FrpBuildScope.() -> A): FrpSpec<A> = block
+
+/** Applies the [FrpSpec] within this [FrpBuildScope]. */
+@ExperimentalFrpApi
+inline operator fun <A> FrpBuildScope.invoke(block: FrpBuildScope.() -> A) = run(block)
+
+/** Operations that add inputs and outputs to an FRP network. */
+@ExperimentalFrpApi
+@RestrictsSuspension
+interface FrpBuildScope : FrpStateScope {
+
+ /** TODO: Javadoc */
+ @ExperimentalFrpApi
+ fun <R> deferredBuildScope(block: suspend FrpBuildScope.() -> R): FrpDeferredValue<R>
+
+ /** TODO: Javadoc */
+ @ExperimentalFrpApi fun deferredBuildScopeAction(block: suspend FrpBuildScope.() -> Unit)
+
+ /**
+ * Returns a [TFlow] containing the results of applying [transform] to each value of the
+ * original [TFlow].
+ *
+ * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver.
+ * Unlike [mapLatestBuild], these modifications are not undone with each subsequent emission of
+ * the original [TFlow].
+ *
+ * **NOTE:** This API does not [observe] the original [TFlow], meaning that unless the returned
+ * (or a downstream) [TFlow] is observed separately, [transform] will not be invoked, and no
+ * internal side-effects will occur.
+ */
+ @ExperimentalFrpApi
+ fun <A, B> TFlow<A>.mapBuild(transform: suspend FrpBuildScope.(A) -> B): TFlow<B>
+
+ /**
+ * Invokes [block] whenever this [TFlow] emits a value, allowing side-effects to be safely
+ * performed in reaction to the emission.
+ *
+ * Specifically, [block] is deferred to the end of the transaction, and is only actually
+ * executed if this [FrpBuildScope] is still active by that time. It can be deactivated due to a
+ * -Latest combinator, for example.
+ *
+ * Shorthand for:
+ * ```kotlin
+ * tFlow.observe { effect { ... } }
+ * ```
+ */
+ @ExperimentalFrpApi
+ fun <A> TFlow<A>.observe(
+ coroutineContext: CoroutineContext = EmptyCoroutineContext,
+ block: suspend FrpEffectScope.(A) -> Unit = {},
+ ): Job
+
+ /**
+ * Returns a [TFlow] containing the results of applying each [FrpSpec] emitted from the original
+ * [TFlow], and a [FrpDeferredValue] containing the result of applying [initialSpecs]
+ * immediately.
+ *
+ * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] with the same
+ * key are undone (any registered [observers][observe] are unregistered, and any pending
+ * [side-effects][effect] are cancelled).
+ *
+ * If the [Maybe] contained within the value for an associated key is [none], then the
+ * previously-active [FrpSpec] will be undone with no replacement.
+ */
+ @ExperimentalFrpApi
+ fun <K, A, B> TFlow<Map<K, Maybe<FrpSpec<A>>>>.applyLatestSpecForKey(
+ initialSpecs: FrpDeferredValue<Map<K, FrpSpec<B>>>,
+ numKeys: Int? = null,
+ ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>>
+
+ /**
+ * Creates an instance of a [TFlow] with elements that are from [builder].
+ *
+ * [builder] is run in its own coroutine, allowing for ongoing work that can emit to the
+ * provided [MutableTFlow].
+ *
+ * By default, [builder] is only running while the returned [TFlow] is being
+ * [observed][observe]. If you want it to run at all times, simply add a no-op observer:
+ * ```kotlin
+ * tFlow { ... }.apply { observe() }
+ * ```
+ */
+ @ExperimentalFrpApi fun <T> tFlow(builder: suspend FrpProducerScope<T>.() -> Unit): TFlow<T>
+
+ /**
+ * Creates an instance of a [TFlow] with elements that are emitted from [builder].
+ *
+ * [builder] is run in its own coroutine, allowing for ongoing work that can emit to the
+ * provided [MutableTFlow].
+ *
+ * By default, [builder] is only running while the returned [TFlow] is being
+ * [observed][observe]. If you want it to run at all times, simply add a no-op observer:
+ * ```kotlin
+ * tFlow { ... }.apply { observe() }
+ * ```
+ *
+ * In the event of backpressure, emissions are *coalesced* into batches. When a value is
+ * [emitted][FrpCoalescingProducerScope.emit] from [builder], it is merged into the batch via
+ * [coalesce]. Once the batch is consumed by the frp network in the next transaction, the batch
+ * is reset back to [getInitialValue].
+ */
+ @ExperimentalFrpApi
+ fun <In, Out> coalescingTFlow(
+ getInitialValue: () -> Out,
+ coalesce: (old: Out, new: In) -> Out,
+ builder: suspend FrpCoalescingProducerScope<In>.() -> Unit,
+ ): TFlow<Out>
+
+ /**
+ * Creates a new [FrpBuildScope] that is a child of this one.
+ *
+ * This new scope can be manually cancelled via the returned [Job], or will be cancelled
+ * automatically when its parent is cancelled. Cancellation will unregister all
+ * [observers][observe] and cancel all scheduled [effects][effect].
+ *
+ * The return value from [block] can be accessed via the returned [FrpDeferredValue].
+ */
+ @ExperimentalFrpApi fun <A> asyncScope(block: FrpSpec<A>): Pair<FrpDeferredValue<A>, Job>
+
+ // TODO: once we have context params, these can all become extensions:
+
+ /**
+ * Returns a [TFlow] containing the results of applying the given [transform] function to each
+ * value of the original [TFlow].
+ *
+ * Unlike [TFlow.map], [transform] can perform arbitrary asynchronous code. This code is run
+ * outside of the current FRP transaction; when [transform] returns, the returned value is
+ * emitted from the result [TFlow] in a new transaction.
+ *
+ * Shorthand for:
+ * ```kotlin
+ * tflow.mapLatestBuild { a -> asyncTFlow { transform(a) } }.flatten()
+ * ```
+ */
+ @ExperimentalFrpApi
+ fun <A, B> TFlow<A>.mapAsyncLatest(transform: suspend (A) -> B): TFlow<B> =
+ mapLatestBuild { a -> asyncTFlow { transform(a) } }.flatten()
+
+ /**
+ * Invokes [block] whenever this [TFlow] emits a value. [block] receives an [FrpBuildScope] that
+ * can be used to make further modifications to the FRP network, and/or perform side-effects via
+ * [effect].
+ *
+ * @see observe
+ */
+ @ExperimentalFrpApi
+ fun <A> TFlow<A>.observeBuild(block: suspend FrpBuildScope.(A) -> Unit = {}): Job =
+ mapBuild(block).observe()
+
+ /**
+ * Returns a [StateFlow] whose [value][StateFlow.value] tracks the current
+ * [value of this TState][TState.sample], and will emit at the same rate as
+ * [TState.stateChanges].
+ *
+ * Note that the [value][StateFlow.value] is not available until the *end* of the current
+ * transaction. If you need the current value before this time, then use [TState.sample].
+ */
+ @ExperimentalFrpApi
+ fun <A> TState<A>.toStateFlow(): StateFlow<A> {
+ val uninitialized = Any()
+ var initialValue: Any? = uninitialized
+ val innerStateFlow = MutableStateFlow<Any?>(uninitialized)
+ deferredBuildScope {
+ initialValue = sample()
+ stateChanges.observe {
+ innerStateFlow.value = it
+ initialValue = null
+ }
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ fun getValue(innerValue: Any?): A =
+ when {
+ innerValue !== uninitialized -> innerValue as A
+ initialValue !== uninitialized -> initialValue as A
+ else ->
+ error(
+ "Attempted to access StateFlow.value before FRP transaction has completed."
+ )
+ }
+
+ return object : StateFlow<A> {
+ override val replayCache: List<A>
+ get() = innerStateFlow.replayCache.map(::getValue)
+
+ override val value: A
+ get() = getValue(innerStateFlow.value)
+
+ override suspend fun collect(collector: FlowCollector<A>): Nothing {
+ innerStateFlow.collect { collector.emit(getValue(it)) }
+ }
+ }
+ }
+
+ /**
+ * Returns a [SharedFlow] configured with a replay cache of size [replay] that emits the current
+ * [value][TState.sample] of this [TState] followed by all [stateChanges].
+ */
+ @ExperimentalFrpApi
+ fun <A> TState<A>.toSharedFlow(replay: Int = 0): SharedFlow<A> {
+ val result = MutableSharedFlow<A>(replay, extraBufferCapacity = 1)
+ deferredBuildScope {
+ result.tryEmit(sample())
+ stateChanges.observe { a -> result.tryEmit(a) }
+ }
+ return result
+ }
+
+ /**
+ * Returns a [SharedFlow] configured with a replay cache of size [replay] that emits values
+ * whenever this [TFlow] emits.
+ */
+ @ExperimentalFrpApi
+ fun <A> TFlow<A>.toSharedFlow(replay: Int = 0): SharedFlow<A> {
+ val result = MutableSharedFlow<A>(replay, extraBufferCapacity = 1)
+ observe { a -> result.tryEmit(a) }
+ return result
+ }
+
+ /**
+ * Returns a [TState] that holds onto the value returned by applying the most recently emitted
+ * [FrpSpec] from the original [TFlow], or the value returned by applying [initialSpec] if
+ * nothing has been emitted since it was constructed.
+ *
+ * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] are undone (any
+ * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are
+ * cancelled).
+ */
+ @ExperimentalFrpApi
+ fun <A> TFlow<FrpSpec<A>>.holdLatestSpec(initialSpec: FrpSpec<A>): TState<A> {
+ val (changes: TFlow<A>, initApplied: FrpDeferredValue<A>) = applyLatestSpec(initialSpec)
+ return changes.holdDeferred(initApplied)
+ }
+
+ /**
+ * Returns a [TState] containing the value returned by applying the [FrpSpec] held by the
+ * original [TState].
+ *
+ * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] are undone (any
+ * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are
+ * cancelled).
+ */
+ @ExperimentalFrpApi
+ fun <A> TState<FrpSpec<A>>.applyLatestSpec(): TState<A> {
+ val (appliedChanges: TFlow<A>, init: FrpDeferredValue<A>) =
+ stateChanges.applyLatestSpec(frpSpec { sample().applySpec() })
+ return appliedChanges.holdDeferred(init)
+ }
+
+ /**
+ * Returns a [TFlow] containing the results of applying each [FrpSpec] emitted from the original
+ * [TFlow].
+ *
+ * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] are undone (any
+ * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are
+ * cancelled).
+ */
+ @ExperimentalFrpApi
+ fun <A> TFlow<FrpSpec<A>>.applyLatestSpec(): TFlow<A> = applyLatestSpec(frpSpec {}).first
+
+ /**
+ * Returns a [TFlow] that switches to a new [TFlow] produced by [transform] every time the
+ * original [TFlow] emits a value.
+ *
+ * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver.
+ * When the original [TFlow] emits a new value, those changes are undone (any registered
+ * [observers][observe] are unregistered, and any pending [effects][effect] are cancelled).
+ */
+ @ExperimentalFrpApi
+ fun <A, B> TFlow<A>.flatMapLatestBuild(
+ transform: suspend FrpBuildScope.(A) -> TFlow<B>
+ ): TFlow<B> = mapCheap { frpSpec { transform(it) } }.applyLatestSpec().flatten()
+
+ /**
+ * Returns a [TState] by applying [transform] to the value held by the original [TState].
+ *
+ * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver.
+ * When the value held by the original [TState] changes, those changes are undone (any
+ * registered [observers][observe] are unregistered, and any pending [effects][effect] are
+ * cancelled).
+ */
+ @ExperimentalFrpApi
+ fun <A, B> TState<A>.flatMapLatestBuild(
+ transform: suspend FrpBuildScope.(A) -> TState<B>
+ ): TState<B> = mapLatestBuild { transform(it) }.flatten()
+
+ /**
+ * Returns a [TState] that transforms the value held inside this [TState] by applying it to the
+ * [transform].
+ *
+ * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver.
+ * When the value held by the original [TState] changes, those changes are undone (any
+ * registered [observers][observe] are unregistered, and any pending [effects][effect] are
+ * cancelled).
+ */
+ @ExperimentalFrpApi
+ fun <A, B> TState<A>.mapLatestBuild(transform: suspend FrpBuildScope.(A) -> B): TState<B> =
+ mapCheapUnsafe { frpSpec { transform(it) } }.applyLatestSpec()
+
+ /**
+ * Returns a [TFlow] containing the results of applying each [FrpSpec] emitted from the original
+ * [TFlow], and a [FrpDeferredValue] containing the result of applying [initialSpec]
+ * immediately.
+ *
+ * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] are undone (any
+ * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are
+ * cancelled).
+ */
+ @ExperimentalFrpApi
+ fun <A : Any?, B> TFlow<FrpSpec<B>>.applyLatestSpec(
+ initialSpec: FrpSpec<A>
+ ): Pair<TFlow<B>, FrpDeferredValue<A>> {
+ val (flow, result) =
+ mapCheap { spec -> mapOf(Unit to just(spec)) }
+ .applyLatestSpecForKey(initialSpecs = mapOf(Unit to initialSpec), numKeys = 1)
+ val outFlow: TFlow<B> =
+ flow.mapMaybe {
+ checkNotNull(it[Unit]) { "applyLatest: expected result, but none present in: $it" }
+ }
+ val outInit: FrpDeferredValue<A> = deferredBuildScope {
+ val initResult: Map<Unit, A> = result.get()
+ check(Unit in initResult) {
+ "applyLatest: expected initial result, but none present in: $initResult"
+ }
+ @Suppress("UNCHECKED_CAST")
+ initResult.getOrDefault(Unit) { null } as A
+ }
+ return Pair(outFlow, outInit)
+ }
+
+ /**
+ * Returns a [TFlow] containing the results of applying [transform] to each value of the
+ * original [TFlow].
+ *
+ * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver.
+ * With each invocation of [transform], changes from the previous invocation are undone (any
+ * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are
+ * cancelled).
+ */
+ @ExperimentalFrpApi
+ fun <A, B> TFlow<A>.mapLatestBuild(transform: suspend FrpBuildScope.(A) -> B): TFlow<B> =
+ mapCheap { frpSpec { transform(it) } }.applyLatestSpec()
+
+ /**
+ * Returns a [TFlow] containing the results of applying [transform] to each value of the
+ * original [TFlow], and a [FrpDeferredValue] containing the result of applying [transform] to
+ * [initialValue] immediately.
+ *
+ * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver.
+ * With each invocation of [transform], changes from the previous invocation are undone (any
+ * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are
+ * cancelled).
+ */
+ @ExperimentalFrpApi
+ fun <A, B> TFlow<A>.mapLatestBuild(
+ initialValue: A,
+ transform: suspend FrpBuildScope.(A) -> B,
+ ): Pair<TFlow<B>, FrpDeferredValue<B>> =
+ mapLatestBuildDeferred(deferredOf(initialValue), transform)
+
+ /**
+ * Returns a [TFlow] containing the results of applying [transform] to each value of the
+ * original [TFlow], and a [FrpDeferredValue] containing the result of applying [transform] to
+ * [initialValue] immediately.
+ *
+ * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver.
+ * With each invocation of [transform], changes from the previous invocation are undone (any
+ * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are
+ * cancelled).
+ */
+ @ExperimentalFrpApi
+ fun <A, B> TFlow<A>.mapLatestBuildDeferred(
+ initialValue: FrpDeferredValue<A>,
+ transform: suspend FrpBuildScope.(A) -> B,
+ ): Pair<TFlow<B>, FrpDeferredValue<B>> =
+ mapCheap { frpSpec { transform(it) } }
+ .applyLatestSpec(initialSpec = frpSpec { transform(initialValue.get()) })
+
+ /**
+ * Returns a [TFlow] containing the results of applying each [FrpSpec] emitted from the original
+ * [TFlow], and a [FrpDeferredValue] containing the result of applying [initialSpecs]
+ * immediately.
+ *
+ * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] with the same
+ * key are undone (any registered [observers][observe] are unregistered, and any pending
+ * [side-effects][effect] are cancelled).
+ *
+ * If the [Maybe] contained within the value for an associated key is [none], then the
+ * previously-active [FrpSpec] will be undone with no replacement.
+ */
+ @ExperimentalFrpApi
+ fun <K, A, B> TFlow<Map<K, Maybe<FrpSpec<A>>>>.applyLatestSpecForKey(
+ initialSpecs: Map<K, FrpSpec<B>>,
+ numKeys: Int? = null,
+ ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>> =
+ applyLatestSpecForKey(deferredOf(initialSpecs), numKeys)
+
+ /**
+ * Returns a [TFlow] containing the results of applying each [FrpSpec] emitted from the original
+ * [TFlow].
+ *
+ * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] with the same
+ * key are undone (any registered [observers][observe] are unregistered, and any pending
+ * [side-effects][effect] are cancelled).
+ *
+ * If the [Maybe] contained within the value for an associated key is [none], then the
+ * previously-active [FrpSpec] will be undone with no replacement.
+ */
+ @ExperimentalFrpApi
+ fun <K, A> TFlow<Map<K, Maybe<FrpSpec<A>>>>.applyLatestSpecForKey(
+ numKeys: Int? = null
+ ): TFlow<Map<K, Maybe<A>>> =
+ applyLatestSpecForKey<K, A, Nothing>(deferredOf(emptyMap()), numKeys).first
+
+ /**
+ * Returns a [TState] containing the latest results of applying each [FrpSpec] emitted from the
+ * original [TFlow].
+ *
+ * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] with the same
+ * key are undone (any registered [observers][observe] are unregistered, and any pending
+ * [side-effects][effect] are cancelled).
+ *
+ * If the [Maybe] contained within the value for an associated key is [none], then the
+ * previously-active [FrpSpec] will be undone with no replacement.
+ */
+ @ExperimentalFrpApi
+ fun <K, A> TFlow<Map<K, Maybe<FrpSpec<A>>>>.holdLatestSpecForKey(
+ initialSpecs: FrpDeferredValue<Map<K, FrpSpec<A>>>,
+ numKeys: Int? = null,
+ ): TState<Map<K, A>> {
+ val (changes, initialValues) = applyLatestSpecForKey(initialSpecs, numKeys)
+ return changes.foldMapIncrementally(initialValues)
+ }
+
+ /**
+ * Returns a [TState] containing the latest results of applying each [FrpSpec] emitted from the
+ * original [TFlow].
+ *
+ * When each [FrpSpec] is applied, changes from the previously-active [FrpSpec] with the same
+ * key are undone (any registered [observers][observe] are unregistered, and any pending
+ * [side-effects][effect] are cancelled).
+ *
+ * If the [Maybe] contained within the value for an associated key is [none], then the
+ * previously-active [FrpSpec] will be undone with no replacement.
+ */
+ @ExperimentalFrpApi
+ fun <K, A> TFlow<Map<K, Maybe<FrpSpec<A>>>>.holdLatestSpecForKey(
+ initialSpecs: Map<K, FrpSpec<A>> = emptyMap(),
+ numKeys: Int? = null,
+ ): TState<Map<K, A>> = holdLatestSpecForKey(deferredOf(initialSpecs), numKeys)
+
+ /**
+ * Returns a [TFlow] containing the results of applying [transform] to each value of the
+ * original [TFlow], and a [FrpDeferredValue] containing the result of applying [transform] to
+ * [initialValues] immediately.
+ *
+ * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver.
+ * With each invocation of [transform], changes from the previous invocation are undone (any
+ * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are
+ * cancelled).
+ *
+ * If the [Maybe] contained within the value for an associated key is [none], then the
+ * previously-active [FrpBuildScope] will be undone with no replacement.
+ */
+ @ExperimentalFrpApi
+ fun <K, A, B> TFlow<Map<K, Maybe<A>>>.mapLatestBuildForKey(
+ initialValues: FrpDeferredValue<Map<K, A>>,
+ numKeys: Int? = null,
+ transform: suspend FrpBuildScope.(A) -> B,
+ ): Pair<TFlow<Map<K, Maybe<B>>>, FrpDeferredValue<Map<K, B>>> =
+ map { patch -> patch.mapValues { (_, v) -> v.map { frpSpec { transform(it) } } } }
+ .applyLatestSpecForKey(
+ deferredBuildScope {
+ initialValues.get().mapValues { (_, v) -> frpSpec { transform(v) } }
+ },
+ numKeys = numKeys,
+ )
+
+ /**
+ * Returns a [TFlow] containing the results of applying [transform] to each value of the
+ * original [TFlow], and a [FrpDeferredValue] containing the result of applying [transform] to
+ * [initialValues] immediately.
+ *
+ * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver.
+ * With each invocation of [transform], changes from the previous invocation are undone (any
+ * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are
+ * cancelled).
+ *
+ * If the [Maybe] contained within the value for an associated key is [none], then the
+ * previously-active [FrpBuildScope] will be undone with no replacement.
+ */
+ @ExperimentalFrpApi
+ fun <K, A, B> TFlow<Map<K, Maybe<A>>>.mapLatestBuildForKey(
+ initialValues: Map<K, A>,
+ numKeys: Int? = null,
+ transform: suspend FrpBuildScope.(A) -> B,
+ ): Pair<TFlow<Map<K, Maybe<B>>>, FrpDeferredValue<Map<K, B>>> =
+ mapLatestBuildForKey(deferredOf(initialValues), numKeys, transform)
+
+ /**
+ * Returns a [TFlow] containing the results of applying [transform] to each value of the
+ * original [TFlow].
+ *
+ * [transform] can perform modifications to the FRP network via its [FrpBuildScope] receiver.
+ * With each invocation of [transform], changes from the previous invocation are undone (any
+ * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are
+ * cancelled).
+ *
+ * If the [Maybe] contained within the value for an associated key is [none], then the
+ * previously-active [FrpBuildScope] will be undone with no replacement.
+ */
+ @ExperimentalFrpApi
+ fun <K, A, B> TFlow<Map<K, Maybe<A>>>.mapLatestBuildForKey(
+ numKeys: Int? = null,
+ transform: suspend FrpBuildScope.(A) -> B,
+ ): TFlow<Map<K, Maybe<B>>> = mapLatestBuildForKey(emptyMap(), numKeys, transform).first
+
+ /** Returns a [Deferred] containing the next value to be emitted from this [TFlow]. */
+ @ExperimentalFrpApi
+ fun <R> TFlow<R>.nextDeferred(): Deferred<R> {
+ lateinit var next: CompletableDeferred<R>
+ val job = nextOnly().observe { next.complete(it) }
+ next = CompletableDeferred<R>(parent = job)
+ return next
+ }
+
+ /** Returns a [TState] that reflects the [StateFlow.value] of this [StateFlow]. */
+ @ExperimentalFrpApi
+ fun <A> StateFlow<A>.toTState(): TState<A> {
+ val initial = value
+ return tFlow { dropWhile { it == initial }.collect { emit(it) } }.hold(initial)
+ }
+
+ /** Returns a [TFlow] that emits whenever this [Flow] emits. */
+ @ExperimentalFrpApi fun <A> Flow<A>.toTFlow(): TFlow<A> = tFlow { collect { emit(it) } }
+
+ /**
+ * Shorthand for:
+ * ```kotlin
+ * flow.toTFlow().hold(initialValue)
+ * ```
+ */
+ @ExperimentalFrpApi
+ fun <A> Flow<A>.toTState(initialValue: A): TState<A> = toTFlow().hold(initialValue)
+
+ /**
+ * Invokes [block] whenever this [TFlow] emits a value. [block] receives an [FrpBuildScope] that
+ * can be used to make further modifications to the FRP network, and/or perform side-effects via
+ * [effect].
+ *
+ * With each invocation of [block], changes from the previous invocation are undone (any
+ * registered [observers][observe] are unregistered, and any pending [side-effects][effect] are
+ * cancelled).
+ */
+ @ExperimentalFrpApi
+ fun <A> TFlow<A>.observeLatestBuild(block: suspend FrpBuildScope.(A) -> Unit = {}): Job =
+ mapLatestBuild { block(it) }.observe()
+
+ /**
+ * Invokes [block] whenever this [TFlow] emits a value, allowing side-effects to be safely
+ * performed in reaction to the emission.
+ *
+ * With each invocation of [block], running effects from the previous invocation are cancelled.
+ */
+ @ExperimentalFrpApi
+ fun <A> TFlow<A>.observeLatest(block: suspend FrpEffectScope.(A) -> Unit = {}): Job {
+ var innerJob: Job? = null
+ return observeBuild {
+ innerJob?.cancel()
+ innerJob = effect { block(it) }
+ }
+ }
+
+ /**
+ * Invokes [block] with the value held by this [TState], allowing side-effects to be safely
+ * performed in reaction to the state changing.
+ *
+ * With each invocation of [block], running effects from the previous invocation are cancelled.
+ */
+ @ExperimentalFrpApi
+ fun <A> TState<A>.observeLatest(block: suspend FrpEffectScope.(A) -> Unit = {}): Job =
+ launchScope {
+ var innerJob = effect { block(sample()) }
+ stateChanges.observeBuild {
+ innerJob.cancel()
+ innerJob = effect { block(it) }
+ }
+ }
+
+ /**
+ * Applies [block] to the value held by this [TState]. [block] receives an [FrpBuildScope] that
+ * can be used to make further modifications to the FRP network, and/or perform side-effects via
+ * [effect].
+ *
+ * [block] can perform modifications to the FRP network via its [FrpBuildScope] receiver. With
+ * each invocation of [block], changes from the previous invocation are undone (any registered
+ * [observers][observe] are unregistered, and any pending [side-effects][effect] are cancelled).
+ */
+ @ExperimentalFrpApi
+ fun <A> TState<A>.observeLatestBuild(block: suspend FrpBuildScope.(A) -> Unit = {}): Job =
+ launchScope {
+ var innerJob: Job = launchScope { block(sample()) }
+ stateChanges.observeBuild {
+ innerJob.cancel()
+ innerJob = launchScope { block(it) }
+ }
+ }
+
+ /** Applies the [FrpSpec] within this [FrpBuildScope]. */
+ @ExperimentalFrpApi suspend fun <A> FrpSpec<A>.applySpec(): A = this()
+
+ /**
+ * Applies the [FrpSpec] within this [FrpBuildScope], returning the result as an
+ * [FrpDeferredValue].
+ */
+ @ExperimentalFrpApi
+ fun <A> FrpSpec<A>.applySpecDeferred(): FrpDeferredValue<A> = deferredBuildScope { applySpec() }
+
+ /**
+ * Invokes [block] on the value held in this [TState]. [block] receives an [FrpBuildScope] that
+ * can be used to make further modifications to the FRP network, and/or perform side-effects via
+ * [effect].
+ */
+ @ExperimentalFrpApi
+ fun <A> TState<A>.observeBuild(block: suspend FrpBuildScope.(A) -> Unit = {}): Job =
+ launchScope {
+ block(sample())
+ stateChanges.observeBuild(block)
+ }
+
+ /**
+ * Invokes [block] with the current value of this [TState], re-invoking whenever it changes,
+ * allowing side-effects to be safely performed in reaction value changing.
+ *
+ * Specifically, [block] is deferred to the end of the transaction, and is only actually
+ * executed if this [FrpBuildScope] is still active by that time. It can be deactivated due to a
+ * -Latest combinator, for example.
+ *
+ * If the [TState] is changing within the *current* transaction (i.e. [stateChanges] is
+ * presently emitting) then [block] will be invoked for the first time with the new value;
+ * otherwise, it will be invoked with the [current][sample] value.
+ */
+ @ExperimentalFrpApi
+ fun <A> TState<A>.observe(block: suspend FrpEffectScope.(A) -> Unit = {}): Job =
+ now.map { sample() }.mergeWith(stateChanges) { _, new -> new }.observe { block(it) }
+}
+
+/**
+ * Returns a [TFlow] that emits the result of [block] once it completes. [block] is evaluated
+ * outside of the current FRP transaction; when it completes, the returned [TFlow] emits in a new
+ * transaction.
+ *
+ * Shorthand for:
+ * ```
+ * tFlow { emitter: MutableTFlow<A> ->
+ * val a = block()
+ * emitter.emit(a)
+ * }
+ * ```
+ */
+@ExperimentalFrpApi
+fun <A> FrpBuildScope.asyncTFlow(block: suspend () -> A): TFlow<A> =
+ tFlow {
+ // TODO: if block completes synchronously, it would be nice to emit within this
+ // transaction
+ emit(block())
+ }
+ .apply { observe() }
+
+/**
+ * Performs a side-effect in a safe manner w/r/t the current FRP transaction.
+ *
+ * Specifically, [block] is deferred to the end of the current transaction, and is only actually
+ * executed if this [FrpBuildScope] is still active by that time. It can be deactivated due to a
+ * -Latest combinator, for example.
+ *
+ * Shorthand for:
+ * ```kotlin
+ * now.observe { block() }
+ * ```
+ */
+@ExperimentalFrpApi
+fun FrpBuildScope.effect(block: suspend FrpEffectScope.() -> Unit): Job = now.observe { block() }
+
+/**
+ * Launches [block] in a new coroutine, returning a [Job] bound to the coroutine.
+ *
+ * This coroutine is not actually started until the *end* of the current FRP transaction. This is
+ * done because the current [FrpBuildScope] might be deactivated within this transaction, perhaps
+ * due to a -Latest combinator. If this happens, then the coroutine will never actually be started.
+ *
+ * Shorthand for:
+ * ```kotlin
+ * effect { frpCoroutineScope.launch { block() } }
+ * ```
+ */
+@ExperimentalFrpApi
+fun FrpBuildScope.launchEffect(block: suspend CoroutineScope.() -> Unit): Job = asyncEffect(block)
+
+/**
+ * Launches [block] in a new coroutine, returning the result as a [Deferred].
+ *
+ * This coroutine is not actually started until the *end* of the current FRP transaction. This is
+ * done because the current [FrpBuildScope] might be deactivated within this transaction, perhaps
+ * due to a -Latest combinator. If this happens, then the coroutine will never actually be started.
+ *
+ * Shorthand for:
+ * ```kotlin
+ * CompletableDeferred<R>.apply {
+ * effect { frpCoroutineScope.launch { complete(coroutineScope { block() }) } }
+ * }
+ * .await()
+ * ```
+ */
+@ExperimentalFrpApi
+fun <R> FrpBuildScope.asyncEffect(block: suspend CoroutineScope.() -> R): Deferred<R> {
+ val result = CompletableDeferred<R>()
+ val job = now.observe { frpCoroutineScope.launch { result.complete(coroutineScope(block)) } }
+ val handle = job.invokeOnCompletion { result.cancel() }
+ result.invokeOnCompletion {
+ handle.dispose()
+ job.cancel()
+ }
+ return result
+}
+
+/** Like [FrpBuildScope.asyncScope], but ignores the result of [block]. */
+@ExperimentalFrpApi fun FrpBuildScope.launchScope(block: FrpSpec<*>): Job = asyncScope(block).second
+
+/**
+ * Creates an instance of a [TFlow] with elements that are emitted from [builder].
+ *
+ * [builder] is run in its own coroutine, allowing for ongoing work that can emit to the provided
+ * [MutableTFlow].
+ *
+ * By default, [builder] is only running while the returned [TFlow] is being
+ * [observed][FrpBuildScope.observe]. If you want it to run at all times, simply add a no-op
+ * observer:
+ * ```kotlin
+ * tFlow { ... }.apply { observe() }
+ * ```
+ *
+ * In the event of backpressure, emissions are *coalesced* into batches. When a value is
+ * [emitted][FrpCoalescingProducerScope.emit] from [builder], it is merged into the batch via
+ * [coalesce]. Once the batch is consumed by the FRP network in the next transaction, the batch is
+ * reset back to [initialValue].
+ */
+@ExperimentalFrpApi
+fun <In, Out> FrpBuildScope.coalescingTFlow(
+ initialValue: Out,
+ coalesce: (old: Out, new: In) -> Out,
+ builder: suspend FrpCoalescingProducerScope<In>.() -> Unit,
+): TFlow<Out> = coalescingTFlow(getInitialValue = { initialValue }, coalesce, builder)
+
+/**
+ * Creates an instance of a [TFlow] with elements that are emitted from [builder].
+ *
+ * [builder] is run in its own coroutine, allowing for ongoing work that can emit to the provided
+ * [MutableTFlow].
+ *
+ * By default, [builder] is only running while the returned [TFlow] is being
+ * [observed][FrpBuildScope.observe]. If you want it to run at all times, simply add a no-op
+ * observer:
+ * ```kotlin
+ * tFlow { ... }.apply { observe() }
+ * ```
+ *
+ * In the event of backpressure, emissions are *conflated*; any older emissions are dropped and only
+ * the most recent emission will be used when the FRP network is ready.
+ */
+@ExperimentalFrpApi
+fun <T> FrpBuildScope.conflatedTFlow(
+ builder: suspend FrpCoalescingProducerScope<T>.() -> Unit
+): TFlow<T> =
+ coalescingTFlow<T, Any?>(initialValue = Any(), coalesce = { _, new -> new }, builder = builder)
+ .mapCheap {
+ @Suppress("UNCHECKED_CAST")
+ it as T
+ }
+
+/** Scope for emitting to a [FrpBuildScope.coalescingTFlow]. */
+interface FrpCoalescingProducerScope<in T> {
+ /**
+ * Inserts [value] into the current batch, enqueueing it for emission from this [TFlow] if not
+ * already pending.
+ *
+ * Backpressure occurs when [emit] is called while the FRP network is currently in a
+ * transaction; if called multiple times, then emissions will be coalesced into a single batch
+ * that is then processed when the network is ready.
+ */
+ fun emit(value: T)
+}
+
+/** Scope for emitting to a [FrpBuildScope.tFlow]. */
+interface FrpProducerScope<in T> {
+ /**
+ * Emits a [value] to this [TFlow], suspending the caller until the FRP transaction containing
+ * the emission has completed.
+ */
+ suspend fun emit(value: T)
+}
+
+/**
+ * Suspends forever. Upon cancellation, runs [block]. Useful for unregistering callbacks inside of
+ * [FrpBuildScope.tFlow] and [FrpBuildScope.coalescingTFlow].
+ */
+suspend fun awaitClose(block: () -> Unit): Nothing =
+ try {
+ awaitCancellation()
+ } finally {
+ block()
+ }
diff --git a/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/FrpEffectScope.kt b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/FrpEffectScope.kt
new file mode 100644
index 0000000..a8ec98f
--- /dev/null
+++ b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/FrpEffectScope.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.experimental.frp
+
+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] 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] 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/frp/src/com/android/systemui/experimental/frp/FrpNetwork.kt b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/FrpNetwork.kt
new file mode 100644
index 0000000..acc76d9
--- /dev/null
+++ b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/FrpNetwork.kt
@@ -0,0 +1,195 @@
+/*
+ * 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.experimental.frp
+
+import com.android.systemui.experimental.frp.internal.BuildScopeImpl
+import com.android.systemui.experimental.frp.internal.Network
+import com.android.systemui.experimental.frp.internal.StateScopeImpl
+import com.android.systemui.experimental.frp.internal.util.awaitCancellationAndThen
+import com.android.systemui.experimental.frp.internal.util.childScope
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+import kotlin.coroutines.coroutineContext
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.job
+import kotlinx.coroutines.launch
+
+/**
+ * Marks declarations that are still **experimental** and shouldn't be used in general production
+ * code.
+ */
+@RequiresOptIn(
+ message = "This API is experimental and should not be used in general production code."
+)
+@Retention(AnnotationRetention.BINARY)
+annotation class ExperimentalFrpApi
+
+/**
+ * External interface to an FRP network. Can be used to make transactional queries and modifications
+ * to the network.
+ */
+@ExperimentalFrpApi
+interface FrpNetwork {
+ /**
+ * Runs [block] inside of a transaction, suspending until the transaction is complete.
+ *
+ * The [FrpBuildScope] receiver exposes methods that can be used to query or modify the network.
+ * If the network is cancelled while the caller of [transact] is suspended, then the call will
+ * be cancelled.
+ */
+ @ExperimentalFrpApi suspend fun <R> transact(block: suspend FrpTransactionScope.() -> R): R
+
+ /**
+ * Activates [spec] in a transaction, suspending indefinitely. While suspended, all observers
+ * and long-running effects are kept alive. When cancelled, observers are unregistered and
+ * effects are cancelled.
+ */
+ @ExperimentalFrpApi suspend fun activateSpec(spec: FrpSpec<*>)
+
+ /** Returns a [CoalescingMutableTFlow] that can emit values into this [FrpNetwork]. */
+ @ExperimentalFrpApi
+ fun <In, Out> coalescingMutableTFlow(
+ coalesce: (old: Out, new: In) -> Out,
+ getInitialValue: () -> Out,
+ ): CoalescingMutableTFlow<In, Out>
+
+ /** Returns a [MutableTFlow] that can emit values into this [FrpNetwork]. */
+ @ExperimentalFrpApi fun <T> mutableTFlow(): MutableTFlow<T>
+
+ /** Returns a [MutableTState]. with initial state [initialValue]. */
+ @ExperimentalFrpApi
+ fun <T> mutableTStateDeferred(initialValue: FrpDeferredValue<T>): MutableTState<T>
+}
+
+/** Returns a [CoalescingMutableTFlow] that can emit values into this [FrpNetwork]. */
+@ExperimentalFrpApi
+fun <In, Out> FrpNetwork.coalescingMutableTFlow(
+ coalesce: (old: Out, new: In) -> Out,
+ initialValue: Out,
+): CoalescingMutableTFlow<In, Out> =
+ coalescingMutableTFlow(coalesce, getInitialValue = { initialValue })
+
+/** Returns a [MutableTState]. with initial state [initialValue]. */
+@ExperimentalFrpApi
+fun <T> FrpNetwork.mutableTState(initialValue: T): MutableTState<T> =
+ mutableTStateDeferred(deferredOf(initialValue))
+
+/** Returns a [MutableTState]. with initial state [initialValue]. */
+@ExperimentalFrpApi
+fun <T> MutableTState(network: FrpNetwork, initialValue: T): MutableTState<T> =
+ network.mutableTState(initialValue)
+
+/** Returns a [MutableTFlow] that can emit values into this [FrpNetwork]. */
+@ExperimentalFrpApi
+fun <T> MutableTFlow(network: FrpNetwork): MutableTFlow<T> = network.mutableTFlow()
+
+/** Returns a [CoalescingMutableTFlow] that can emit values into this [FrpNetwork]. */
+@ExperimentalFrpApi
+fun <In, Out> CoalescingMutableTFlow(
+ network: FrpNetwork,
+ coalesce: (old: Out, new: In) -> Out,
+ initialValue: Out,
+): CoalescingMutableTFlow<In, Out> = network.coalescingMutableTFlow(coalesce) { initialValue }
+
+/** Returns a [CoalescingMutableTFlow] that can emit values into this [FrpNetwork]. */
+@ExperimentalFrpApi
+fun <In, Out> CoalescingMutableTFlow(
+ network: FrpNetwork,
+ coalesce: (old: Out, new: In) -> Out,
+ getInitialValue: () -> Out,
+): CoalescingMutableTFlow<In, Out> = network.coalescingMutableTFlow(coalesce, getInitialValue)
+
+/**
+ * Activates [spec] in a transaction and invokes [block] with the result, suspending indefinitely.
+ * While suspended, all observers and long-running effects are kept alive. When cancelled, observers
+ * are unregistered and effects are cancelled.
+ */
+@ExperimentalFrpApi
+suspend fun <R> FrpNetwork.activateSpec(spec: FrpSpec<R>, block: suspend (R) -> Unit) {
+ activateSpec {
+ val result = spec.applySpec()
+ launchEffect { block(result) }
+ }
+}
+
+internal class LocalFrpNetwork(
+ private val network: Network,
+ private val scope: CoroutineScope,
+ private val endSignal: TFlow<Any>,
+) : FrpNetwork {
+ override suspend fun <R> transact(block: suspend FrpTransactionScope.() -> R): R {
+ val result = CompletableDeferred<R>(coroutineContext[Job])
+ @Suppress("DeferredResultUnused")
+ network.transaction {
+ val buildScope =
+ BuildScopeImpl(
+ stateScope = StateScopeImpl(evalScope = this, endSignal = endSignal),
+ coroutineScope = scope,
+ )
+ buildScope.runInBuildScope { effect { result.complete(block()) } }
+ }
+ return result.await()
+ }
+
+ override suspend fun activateSpec(spec: FrpSpec<*>) {
+ val job =
+ network
+ .transaction {
+ val buildScope =
+ BuildScopeImpl(
+ stateScope = StateScopeImpl(evalScope = this, endSignal = endSignal),
+ coroutineScope = scope,
+ )
+ buildScope.runInBuildScope { launchScope(spec) }
+ }
+ .await()
+ awaitCancellationAndThen { job.cancel() }
+ }
+
+ override fun <In, Out> coalescingMutableTFlow(
+ coalesce: (old: Out, new: In) -> Out,
+ getInitialValue: () -> Out,
+ ): CoalescingMutableTFlow<In, Out> = CoalescingMutableTFlow(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/frp/src/com/android/systemui/experimental/frp/FrpScope.kt b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/FrpScope.kt
new file mode 100644
index 0000000..a5a7977
--- /dev/null
+++ b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/FrpScope.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.experimental.frp
+
+import kotlin.coroutines.RestrictsSuspension
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+/** Denotes [FrpScope] interfaces as [DSL markers][DslMarker]. */
+@DslMarker annotation class FrpScopeMarker
+
+/**
+ * Base scope for all FRP scopes. Used to prevent implicitly capturing other scopes from in lambdas.
+ */
+@FrpScopeMarker
+@RestrictsSuspension
+@ExperimentalFrpApi
+interface FrpScope {
+ /**
+ * Returns the value held by the [FrpDeferredValue], suspending until available if necessary.
+ */
+ @ExperimentalFrpApi
+ @OptIn(ExperimentalCoroutinesApi::class)
+ suspend fun <A> FrpDeferredValue<A>.get(): A = suspendCancellableCoroutine { k ->
+ unwrapped.invokeOnCompletion { ex ->
+ ex?.let { k.resumeWithException(ex) } ?: k.resume(unwrapped.getCompleted())
+ }
+ }
+}
+
+/**
+ * A value that may not be immediately (synchronously) available, but is guaranteed to be available
+ * before this transaction is completed.
+ *
+ * @see FrpScope.get
+ */
+@ExperimentalFrpApi
+class FrpDeferredValue<out A> internal constructor(internal val unwrapped: Deferred<A>)
+
+/**
+ * Returns the value held by this [FrpDeferredValue], or throws [IllegalStateException] if it is not
+ * yet available.
+ *
+ * This API is not meant for general usage within the FRP network. It is made available mainly for
+ * debugging and logging. You should always prefer [get][FrpScope.get] if possible.
+ *
+ * @see FrpScope.get
+ */
+@ExperimentalFrpApi
+@OptIn(ExperimentalCoroutinesApi::class)
+fun <A> FrpDeferredValue<A>.getUnsafe(): A = unwrapped.getCompleted()
+
+/** Returns an already-available [FrpDeferredValue] containing [value]. */
+@ExperimentalFrpApi
+fun <A> deferredOf(value: A): FrpDeferredValue<A> = FrpDeferredValue(CompletableDeferred(value))
diff --git a/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/FrpStateScope.kt b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/FrpStateScope.kt
new file mode 100644
index 0000000..61336f4
--- /dev/null
+++ b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/FrpStateScope.kt
@@ -0,0 +1,780 @@
+/*
+ * 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.experimental.frp
+
+import com.android.systemui.experimental.frp.combine as combinePure
+import com.android.systemui.experimental.frp.map as mapPure
+import com.android.systemui.experimental.frp.util.Just
+import com.android.systemui.experimental.frp.util.Left
+import com.android.systemui.experimental.frp.util.Maybe
+import com.android.systemui.experimental.frp.util.Right
+import com.android.systemui.experimental.frp.util.WithPrev
+import com.android.systemui.experimental.frp.util.just
+import com.android.systemui.experimental.frp.util.map
+import com.android.systemui.experimental.frp.util.none
+import com.android.systemui.experimental.frp.util.partitionEithers
+import com.android.systemui.experimental.frp.util.zipWith
+import kotlin.coroutines.RestrictsSuspension
+
+typealias FrpStateful<R> = suspend FrpStateScope.() -> R
+
+/**
+ * Returns a [FrpStateful] that, when [applied][FrpStateScope.applyStateful], invokes [block] with
+ * the applier's [FrpStateScope].
+ */
+// TODO: caching story? should each Scope have a cache of applied FrpStateful instances?
+@ExperimentalFrpApi
+@Suppress("NOTHING_TO_INLINE")
+inline fun <A> statefully(noinline block: suspend FrpStateScope.() -> A): FrpStateful<A> = block
+
+/**
+ * Operations that accumulate state within the FRP network.
+ *
+ * State accumulation is an ongoing process that has a lifetime. Use `-Latest` combinators, such as
+ * [mapLatestStateful], to create smaller, nested lifecycles so that accumulation isn't running
+ * longer than needed.
+ */
+@ExperimentalFrpApi
+@RestrictsSuspension
+interface FrpStateScope : FrpTransactionScope {
+
+ /** TODO */
+ @ExperimentalFrpApi
+ // TODO: wish this could just be `deferred` but alas
+ fun <A> deferredStateScope(block: suspend FrpStateScope.() -> A): FrpDeferredValue<A>
+
+ /**
+ * Returns a [TState] that holds onto the most recently emitted value from this [TFlow], or
+ * [initialValue] if nothing has been emitted since it was constructed.
+ *
+ * Note that the value contained within the [TState] is not updated until *after* all [TFlow]s
+ * have been processed; this keeps the value of the [TState] consistent during the entire FRP
+ * transaction.
+ */
+ @ExperimentalFrpApi fun <A> TFlow<A>.holdDeferred(initialValue: FrpDeferredValue<A>): TState<A>
+
+ /**
+ * Returns a [TFlow] that emits from a merged, incrementally-accumulated collection of [TFlow]s
+ * emitted from this, following the same "patch" rules as outlined in [foldMapIncrementally].
+ *
+ * Conceptually this is equivalent to:
+ * ```kotlin
+ * fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementally(
+ * initialTFlows: Map<K, TFlow<V>>,
+ * ): TFlow<Map<K, V>> =
+ * foldMapIncrementally(initialTFlows).map { it.merge() }.switch()
+ * ```
+ *
+ * While the behavior is equivalent to the conceptual definition above, the implementation is
+ * significantly more efficient.
+ *
+ * @see merge
+ */
+ @ExperimentalFrpApi
+ fun <K : Any, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementally(
+ initialTFlows: FrpDeferredValue<Map<K, TFlow<V>>>
+ ): TFlow<Map<K, V>>
+
+ /**
+ * Returns a [TFlow] that emits from a merged, incrementally-accumulated collection of [TFlow]s
+ * emitted from this, following the same "patch" rules as outlined in [foldMapIncrementally].
+ *
+ * Conceptually this is equivalent to:
+ * ```kotlin
+ * fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementallyPrompt(
+ * initialTFlows: Map<K, TFlow<V>>,
+ * ): TFlow<Map<K, V>> =
+ * foldMapIncrementally(initialTFlows).map { it.merge() }.switchPromptly()
+ * ```
+ *
+ * While the behavior is equivalent to the conceptual definition above, the implementation is
+ * significantly more efficient.
+ *
+ * @see merge
+ */
+ @ExperimentalFrpApi
+ fun <K : Any, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementallyPromptly(
+ initialTFlows: FrpDeferredValue<Map<K, TFlow<V>>>
+ ): TFlow<Map<K, V>>
+
+ // TODO: everything below this comment can be made into extensions once we have context params
+
+ /**
+ * Returns a [TFlow] that emits from a merged, incrementally-accumulated collection of [TFlow]s
+ * emitted from this, following the same "patch" rules as outlined in [foldMapIncrementally].
+ *
+ * Conceptually this is equivalent to:
+ * ```kotlin
+ * fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementally(
+ * initialTFlows: Map<K, TFlow<V>>,
+ * ): TFlow<Map<K, V>> =
+ * foldMapIncrementally(initialTFlows).map { it.merge() }.switch()
+ * ```
+ *
+ * While the behavior is equivalent to the conceptual definition above, the implementation is
+ * significantly more efficient.
+ *
+ * @see merge
+ */
+ @ExperimentalFrpApi
+ fun <K : Any, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementally(
+ initialTFlows: Map<K, TFlow<V>> = emptyMap()
+ ): TFlow<Map<K, V>> = mergeIncrementally(deferredOf(initialTFlows))
+
+ /**
+ * Returns a [TFlow] that emits from a merged, incrementally-accumulated collection of [TFlow]s
+ * emitted from this, following the same "patch" rules as outlined in [foldMapIncrementally].
+ *
+ * Conceptually this is equivalent to:
+ * ```kotlin
+ * fun <K, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementallyPrompt(
+ * initialTFlows: Map<K, TFlow<V>>,
+ * ): TFlow<Map<K, V>> =
+ * foldMapIncrementally(initialTFlows).map { it.merge() }.switchPromptly()
+ * ```
+ *
+ * While the behavior is equivalent to the conceptual definition above, the implementation is
+ * significantly more efficient.
+ *
+ * @see merge
+ */
+ @ExperimentalFrpApi
+ fun <K : Any, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementallyPromptly(
+ initialTFlows: Map<K, TFlow<V>> = emptyMap()
+ ): TFlow<Map<K, V>> = mergeIncrementallyPromptly(deferredOf(initialTFlows))
+
+ /** Applies the [FrpStateful] within this [FrpStateScope]. */
+ @ExperimentalFrpApi suspend fun <A> FrpStateful<A>.applyStateful(): A = this()
+
+ /**
+ * Applies the [FrpStateful] within this [FrpStateScope], returning the result as an
+ * [FrpDeferredValue].
+ */
+ @ExperimentalFrpApi
+ fun <A> FrpStateful<A>.applyStatefulDeferred(): FrpDeferredValue<A> = deferredStateScope {
+ applyStateful()
+ }
+
+ /**
+ * Returns a [TState] that holds onto the most recently emitted value from this [TFlow], or
+ * [initialValue] if nothing has been emitted since it was constructed.
+ *
+ * Note that the value contained within the [TState] is not updated until *after* all [TFlow]s
+ * have been processed; this keeps the value of the [TState] consistent during the entire FRP
+ * transaction.
+ */
+ @ExperimentalFrpApi
+ fun <A> TFlow<A>.hold(initialValue: A): TState<A> = holdDeferred(deferredOf(initialValue))
+
+ /**
+ * Returns a [TFlow] the emits the result of applying [FrpStatefuls][FrpStateful] emitted from
+ * the original [TFlow].
+ *
+ * Unlike [applyLatestStateful], state accumulation is not stopped with each subsequent emission
+ * of the original [TFlow].
+ */
+ @ExperimentalFrpApi fun <A> TFlow<FrpStateful<A>>.applyStatefuls(): TFlow<A>
+
+ /**
+ * Returns a [TFlow] containing the results of applying [transform] to each value of the
+ * original [TFlow].
+ *
+ * [transform] can perform state accumulation via its [FrpStateScope] receiver. Unlike
+ * [mapLatestStateful], accumulation is not stopped with each subsequent emission of the
+ * original [TFlow].
+ */
+ @ExperimentalFrpApi
+ fun <A, B> TFlow<A>.mapStateful(transform: suspend FrpStateScope.(A) -> B): TFlow<B> =
+ mapPure { statefully { transform(it) } }.applyStatefuls()
+
+ /**
+ * Returns a [TState] the holds the result of applying the [FrpStateful] held by the original
+ * [TState].
+ *
+ * Unlike [applyLatestStateful], state accumulation is not stopped with each state change.
+ */
+ @ExperimentalFrpApi
+ fun <A> TState<FrpStateful<A>>.applyStatefuls(): TState<A> =
+ stateChanges
+ .applyStatefuls()
+ .holdDeferred(initialValue = deferredStateScope { sampleDeferred().get()() })
+
+ /** Returns a [TFlow] that switches to the [TFlow] emitted by the original [TFlow]. */
+ @ExperimentalFrpApi fun <A> TFlow<TFlow<A>>.flatten() = hold(emptyTFlow).switch()
+
+ /**
+ * Returns a [TFlow] containing the results of applying [transform] to each value of the
+ * original [TFlow].
+ *
+ * [transform] can perform state accumulation via its [FrpStateScope] receiver. With each
+ * invocation of [transform], state accumulation from previous invocation is stopped.
+ */
+ @ExperimentalFrpApi
+ fun <A, B> TFlow<A>.mapLatestStateful(transform: suspend FrpStateScope.(A) -> B): TFlow<B> =
+ mapPure { statefully { transform(it) } }.applyLatestStateful()
+
+ /**
+ * Returns a [TFlow] that switches to a new [TFlow] produced by [transform] every time the
+ * original [TFlow] emits a value.
+ *
+ * [transform] can perform state accumulation via its [FrpStateScope] receiver. With each
+ * invocation of [transform], state accumulation from previous invocation is stopped.
+ */
+ @ExperimentalFrpApi
+ fun <A, B> TFlow<A>.flatMapLatestStateful(
+ transform: suspend FrpStateScope.(A) -> TFlow<B>
+ ): TFlow<B> = mapLatestStateful(transform).flatten()
+
+ /**
+ * Returns a [TFlow] containing the results of applying each [FrpStateful] emitted from the
+ * original [TFlow].
+ *
+ * When each [FrpStateful] is applied, state accumulation from the previously-active
+ * [FrpStateful] is stopped.
+ */
+ @ExperimentalFrpApi
+ fun <A> TFlow<FrpStateful<A>>.applyLatestStateful(): TFlow<A> = applyLatestStateful {}.first
+
+ /**
+ * Returns a [TState] containing the value returned by applying the [FrpStateful] held by the
+ * original [TState].
+ *
+ * When each [FrpStateful] is applied, state accumulation from the previously-active
+ * [FrpStateful] is stopped.
+ */
+ @ExperimentalFrpApi
+ fun <A> TState<FrpStateful<A>>.applyLatestStateful(): TState<A> {
+ val (changes, init) = stateChanges.applyLatestStateful { sample()() }
+ return changes.holdDeferred(init)
+ }
+
+ /**
+ * Returns a [TFlow] containing the results of applying each [FrpStateful] emitted from the
+ * original [TFlow], and a [FrpDeferredValue] containing the result of applying [init]
+ * immediately.
+ *
+ * When each [FrpStateful] is applied, state accumulation from the previously-active
+ * [FrpStateful] is stopped.
+ */
+ @ExperimentalFrpApi
+ fun <A, B> TFlow<FrpStateful<B>>.applyLatestStateful(
+ init: FrpStateful<A>
+ ): Pair<TFlow<B>, FrpDeferredValue<A>> {
+ val (flow, result) =
+ mapCheap { spec -> mapOf(Unit to just(spec)) }
+ .applyLatestStatefulForKey(init = mapOf(Unit to init), numKeys = 1)
+ val outFlow: TFlow<B> =
+ flow.mapMaybe {
+ checkNotNull(it[Unit]) { "applyLatest: expected result, but none present in: $it" }
+ }
+ val outInit: FrpDeferredValue<A> = deferredTransactionScope {
+ val initResult: Map<Unit, A> = result.get()
+ check(Unit in initResult) {
+ "applyLatest: expected initial result, but none present in: $initResult"
+ }
+ @Suppress("UNCHECKED_CAST")
+ initResult.getOrDefault(Unit) { null } as A
+ }
+ return Pair(outFlow, outInit)
+ }
+
+ /**
+ * Returns a [TFlow] containing the results of applying each [FrpStateful] emitted from the
+ * original [TFlow], and a [FrpDeferredValue] containing the result of applying [init]
+ * immediately.
+ *
+ * If the [Maybe] contained within the value for an associated key is [none], then the
+ * previously-active [FrpStateful] will be stopped with no replacement.
+ *
+ * When each [FrpStateful] is applied, state accumulation from the previously-active
+ * [FrpStateful] with the same key is stopped.
+ */
+ @ExperimentalFrpApi
+ fun <K, A, B> TFlow<Map<K, Maybe<FrpStateful<A>>>>.applyLatestStatefulForKey(
+ init: FrpDeferredValue<Map<K, FrpStateful<B>>>,
+ numKeys: Int? = null,
+ ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>>
+
+ /**
+ * Returns a [TFlow] containing the results of applying each [FrpStateful] emitted from the
+ * original [TFlow], and a [FrpDeferredValue] containing the result of applying [init]
+ * immediately.
+ *
+ * When each [FrpStateful] is applied, state accumulation from the previously-active
+ * [FrpStateful] with the same key is stopped.
+ *
+ * If the [Maybe] contained within the value for an associated key is [none], then the
+ * previously-active [FrpStateful] will be stopped with no replacement.
+ */
+ @ExperimentalFrpApi
+ fun <K, A, B> TFlow<Map<K, Maybe<FrpStateful<A>>>>.applyLatestStatefulForKey(
+ init: Map<K, FrpStateful<B>>,
+ numKeys: Int? = null,
+ ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>> =
+ applyLatestStatefulForKey(deferredOf(init), numKeys)
+
+ /**
+ * Returns a [TState] containing the latest results of applying each [FrpStateful] emitted from
+ * the original [TFlow].
+ *
+ * When each [FrpStateful] is applied, state accumulation from the previously-active
+ * [FrpStateful] with the same key is stopped.
+ *
+ * If the [Maybe] contained within the value for an associated key is [none], then the
+ * previously-active [FrpStateful] will be stopped with no replacement.
+ */
+ @ExperimentalFrpApi
+ fun <K, A> TFlow<Map<K, Maybe<FrpStateful<A>>>>.holdLatestStatefulForKey(
+ init: FrpDeferredValue<Map<K, FrpStateful<A>>>,
+ numKeys: Int? = null,
+ ): TState<Map<K, A>> {
+ val (changes, initialValues) = applyLatestStatefulForKey(init, numKeys)
+ return changes.foldMapIncrementally(initialValues)
+ }
+
+ /**
+ * Returns a [TState] containing the latest results of applying each [FrpStateful] emitted from
+ * the original [TFlow].
+ *
+ * When each [FrpStateful] is applied, state accumulation from the previously-active
+ * [FrpStateful] with the same key is stopped.
+ *
+ * If the [Maybe] contained within the value for an associated key is [none], then the
+ * previously-active [FrpStateful] will be stopped with no replacement.
+ */
+ @ExperimentalFrpApi
+ fun <K, A> TFlow<Map<K, Maybe<FrpStateful<A>>>>.holdLatestStatefulForKey(
+ init: Map<K, FrpStateful<A>> = emptyMap(),
+ numKeys: Int? = null,
+ ): TState<Map<K, A>> = holdLatestStatefulForKey(deferredOf(init), numKeys)
+
+ /**
+ * Returns a [TFlow] containing the results of applying each [FrpStateful] emitted from the
+ * original [TFlow], and a [FrpDeferredValue] containing the result of applying [init]
+ * immediately.
+ *
+ * When each [FrpStateful] is applied, state accumulation from the previously-active
+ * [FrpStateful] with the same key is stopped.
+ *
+ * If the [Maybe] contained within the value for an associated key is [none], then the
+ * previously-active [FrpStateful] will be stopped with no replacement.
+ */
+ @ExperimentalFrpApi
+ fun <K, A> TFlow<Map<K, Maybe<FrpStateful<A>>>>.applyLatestStatefulForKey(
+ numKeys: Int? = null
+ ): TFlow<Map<K, Maybe<A>>> =
+ applyLatestStatefulForKey(init = emptyMap<K, FrpStateful<*>>(), numKeys = numKeys).first
+
+ /**
+ * Returns a [TFlow] containing the results of applying [transform] to each value of the
+ * original [TFlow], and a [FrpDeferredValue] containing the result of applying [transform] to
+ * [initialValues] immediately.
+ *
+ * [transform] can perform state accumulation via its [FrpStateScope] receiver. With each
+ * invocation of [transform], state accumulation from previous invocation is stopped.
+ *
+ * If the [Maybe] contained within the value for an associated key is [none], then the
+ * previously-active [FrpStateScope] will be stopped with no replacement.
+ */
+ @ExperimentalFrpApi
+ fun <K, A, B> TFlow<Map<K, Maybe<A>>>.mapLatestStatefulForKey(
+ initialValues: FrpDeferredValue<Map<K, A>>,
+ numKeys: Int? = null,
+ transform: suspend FrpStateScope.(A) -> B,
+ ): Pair<TFlow<Map<K, Maybe<B>>>, FrpDeferredValue<Map<K, B>>> =
+ mapPure { patch -> patch.mapValues { (_, v) -> v.map { statefully { transform(it) } } } }
+ .applyLatestStatefulForKey(
+ deferredStateScope {
+ initialValues.get().mapValues { (_, v) -> statefully { transform(v) } }
+ },
+ numKeys = numKeys,
+ )
+
+ /**
+ * Returns a [TFlow] containing the results of applying [transform] to each value of the
+ * original [TFlow], and a [FrpDeferredValue] containing the result of applying [transform] to
+ * [initialValues] immediately.
+ *
+ * [transform] can perform state accumulation via its [FrpStateScope] receiver. With each
+ * invocation of [transform], state accumulation from previous invocation is stopped.
+ *
+ * If the [Maybe] contained within the value for an associated key is [none], then the
+ * previously-active [FrpStateScope] will be stopped with no replacement.
+ */
+ @ExperimentalFrpApi
+ fun <K, A, B> TFlow<Map<K, Maybe<A>>>.mapLatestStatefulForKey(
+ initialValues: Map<K, A>,
+ numKeys: Int? = null,
+ transform: suspend FrpStateScope.(A) -> B,
+ ): Pair<TFlow<Map<K, Maybe<B>>>, FrpDeferredValue<Map<K, B>>> =
+ mapLatestStatefulForKey(deferredOf(initialValues), numKeys, transform)
+
+ /**
+ * Returns a [TFlow] containing the results of applying [transform] to each value of the
+ * original [TFlow].
+ *
+ * [transform] can perform state accumulation via its [FrpStateScope] receiver. With each
+ * invocation of [transform], state accumulation from previous invocation is stopped.
+ *
+ * If the [Maybe] contained within the value for an associated key is [none], then the
+ * previously-active [FrpStateScope] will be stopped with no replacement.
+ */
+ @ExperimentalFrpApi
+ fun <K, A, B> TFlow<Map<K, Maybe<A>>>.mapLatestStatefulForKey(
+ numKeys: Int? = null,
+ transform: suspend FrpStateScope.(A) -> B,
+ ): TFlow<Map<K, Maybe<B>>> = mapLatestStatefulForKey(emptyMap(), numKeys, transform).first
+
+ /**
+ * Returns a [TFlow] that will only emit the next event of the original [TFlow], and then will
+ * act as [emptyTFlow].
+ *
+ * If the original [TFlow] is emitting an event at this exact time, then it will be the only
+ * even emitted from the result [TFlow].
+ */
+ @ExperimentalFrpApi
+ fun <A> TFlow<A>.nextOnly(): TFlow<A> =
+ if (this === emptyTFlow) {
+ this
+ } else {
+ TFlowLoop<A>().also {
+ it.loopback = it.mapCheap { emptyTFlow }.hold(this@nextOnly).switch()
+ }
+ }
+
+ /** Returns a [TFlow] that skips the next emission of the original [TFlow]. */
+ @ExperimentalFrpApi
+ fun <A> TFlow<A>.skipNext(): TFlow<A> =
+ if (this === emptyTFlow) {
+ this
+ } else {
+ nextOnly().mapCheap { this@skipNext }.hold(emptyTFlow).switch()
+ }
+
+ /**
+ * Returns a [TFlow] that emits values from the original [TFlow] up until [stop] emits a value.
+ *
+ * If the original [TFlow] emits at the same time as [stop], then the returned [TFlow] will emit
+ * that value.
+ */
+ @ExperimentalFrpApi
+ fun <A> TFlow<A>.takeUntil(stop: TFlow<*>): TFlow<A> =
+ if (stop === emptyTFlow) {
+ this
+ } else {
+ stop.mapCheap { emptyTFlow }.nextOnly().hold(this).switch()
+ }
+
+ /**
+ * Invokes [stateful] in a new [FrpStateScope] that is a child of this one.
+ *
+ * This new scope is stopped when [stop] first emits a value, or when the parent scope is
+ * stopped. Stopping will end all state accumulation; any [TStates][TState] returned from this
+ * scope will no longer update.
+ */
+ @ExperimentalFrpApi
+ fun <A> childStateScope(stop: TFlow<*>, stateful: FrpStateful<A>): FrpDeferredValue<A> {
+ val (_, init: FrpDeferredValue<Map<Unit, A>>) =
+ stop
+ .nextOnly()
+ .mapPure { mapOf(Unit to none<FrpStateful<A>>()) }
+ .applyLatestStatefulForKey(init = mapOf(Unit to stateful), numKeys = 1)
+ return deferredStateScope { init.get().getValue(Unit) }
+ }
+
+ /**
+ * Returns a [TFlow] that emits values from the original [TFlow] up to and including a value is
+ * emitted that satisfies [predicate].
+ */
+ @ExperimentalFrpApi
+ fun <A> TFlow<A>.takeUntil(predicate: suspend FrpTransactionScope.(A) -> Boolean): TFlow<A> =
+ takeUntil(filter(predicate))
+
+ /**
+ * Returns a [TState] that is incrementally updated when this [TFlow] emits a value, by applying
+ * [transform] to both the emitted value and the currently tracked state.
+ *
+ * Note that the value contained within the [TState] is not updated until *after* all [TFlow]s
+ * have been processed; this keeps the value of the [TState] consistent during the entire FRP
+ * transaction.
+ */
+ @ExperimentalFrpApi
+ fun <A, B> TFlow<A>.fold(
+ initialValue: B,
+ transform: suspend FrpTransactionScope.(A, B) -> B,
+ ): TState<B> {
+ lateinit var state: TState<B>
+ return mapPure { a -> transform(a, state.sample()) }.hold(initialValue).also { state = it }
+ }
+
+ /**
+ * Returns a [TState] that is incrementally updated when this [TFlow] emits a value, by applying
+ * [transform] to both the emitted value and the currently tracked state.
+ *
+ * Note that the value contained within the [TState] is not updated until *after* all [TFlow]s
+ * have been processed; this keeps the value of the [TState] consistent during the entire FRP
+ * transaction.
+ */
+ @ExperimentalFrpApi
+ fun <A, B> TFlow<A>.foldDeferred(
+ initialValue: FrpDeferredValue<B>,
+ transform: suspend FrpTransactionScope.(A, B) -> B,
+ ): TState<B> {
+ lateinit var state: TState<B>
+ return mapPure { a -> transform(a, state.sample()) }
+ .holdDeferred(initialValue)
+ .also { state = it }
+ }
+
+ /**
+ * Returns a [TState] that holds onto the result of applying the most recently emitted
+ * [FrpStateful] this [TFlow], or [init] if nothing has been emitted since it was constructed.
+ *
+ * When each [FrpStateful] is applied, state accumulation from the previously-active
+ * [FrpStateful] is stopped.
+ *
+ * Note that the value contained within the [TState] is not updated until *after* all [TFlow]s
+ * have been processed; this keeps the value of the [TState] consistent during the entire FRP
+ * transaction.
+ *
+ * Shorthand for:
+ * ```kotlin
+ * val (changes, initApplied) = applyLatestStateful(init)
+ * return changes.toTStateDeferred(initApplied)
+ * ```
+ */
+ @ExperimentalFrpApi
+ fun <A> TFlow<FrpStateful<A>>.holdLatestStateful(init: FrpStateful<A>): TState<A> {
+ val (changes, initApplied) = applyLatestStateful(init)
+ return changes.holdDeferred(initApplied)
+ }
+
+ /**
+ * Returns a [TFlow] that emits the two most recent emissions from the original [TFlow].
+ * [initialValue] is used as the previous value for the first emission.
+ *
+ * Shorthand for `sample(hold(init)) { new, old -> Pair(old, new) }`
+ */
+ @ExperimentalFrpApi
+ fun <S, T : S> TFlow<T>.pairwise(initialValue: S): TFlow<WithPrev<S, T>> {
+ val previous = hold(initialValue)
+ return mapCheap { new -> WithPrev(previousValue = previous.sample(), newValue = new) }
+ }
+
+ /**
+ * Returns a [TFlow] that emits the two most recent emissions from the original [TFlow]. Note
+ * that the returned [TFlow] will not emit until the original [TFlow] has emitted twice.
+ */
+ @ExperimentalFrpApi
+ fun <A> TFlow<A>.pairwise(): TFlow<WithPrev<A, A>> =
+ mapCheap { just(it) }
+ .pairwise(none)
+ .mapMaybe { (prev, next) -> prev.zipWith(next, ::WithPrev) }
+
+ /**
+ * Returns a [TState] that holds both the current and previous values of the original [TState].
+ * [initialPreviousValue] is used as the first previous value.
+ *
+ * Shorthand for `sample(hold(init)) { new, old -> Pair(old, new) }`
+ */
+ @ExperimentalFrpApi
+ fun <S, T : S> TState<T>.pairwise(initialPreviousValue: S): TState<WithPrev<S, T>> =
+ stateChanges
+ .pairwise(initialPreviousValue)
+ .holdDeferred(deferredTransactionScope { WithPrev(initialPreviousValue, sample()) })
+
+ /**
+ * Returns a [TState] holding a [Map] that is updated incrementally whenever this emits a value.
+ *
+ * The value emitted is used as a "patch" for the tracked [Map]; for each key [K] in the emitted
+ * map, an associated value of [Just] will insert or replace the value in the tracked [Map], and
+ * an associated value of [none] will remove the key from the tracked [Map].
+ */
+ @ExperimentalFrpApi
+ fun <K, V> TFlow<Map<K, Maybe<V>>>.foldMapIncrementally(
+ initialValues: FrpDeferredValue<Map<K, V>>
+ ): TState<Map<K, V>> =
+ foldDeferred(initialValues) { patch, map ->
+ val (adds: List<Pair<K, V>>, removes: List<K>) =
+ patch
+ .asSequence()
+ .map { (k, v) -> if (v is Just) Left(k to v.value) else Right(k) }
+ .partitionEithers()
+ val removed: Map<K, V> = map - removes.toSet()
+ val updated: Map<K, V> = removed + adds
+ updated
+ }
+
+ /**
+ * Returns a [TState] holding a [Map] that is updated incrementally whenever this emits a value.
+ *
+ * The value emitted is used as a "patch" for the tracked [Map]; for each key [K] in the emitted
+ * map, an associated value of [Just] will insert or replace the value in the tracked [Map], and
+ * an associated value of [none] will remove the key from the tracked [Map].
+ */
+ @ExperimentalFrpApi
+ fun <K, V> TFlow<Map<K, Maybe<V>>>.foldMapIncrementally(
+ initialValues: Map<K, V> = emptyMap()
+ ): TState<Map<K, V>> = foldMapIncrementally(deferredOf(initialValues))
+
+ /**
+ * Returns a [TFlow] that wraps each emission of the original [TFlow] into an [IndexedValue],
+ * containing the emitted value and its index (starting from zero).
+ *
+ * Shorthand for:
+ * ```
+ * val index = fold(0) { _, oldIdx -> oldIdx + 1 }
+ * sample(index) { a, idx -> IndexedValue(idx, a) }
+ * ```
+ */
+ @ExperimentalFrpApi
+ fun <A> TFlow<A>.withIndex(): TFlow<IndexedValue<A>> {
+ val index = fold(0) { _, old -> old + 1 }
+ return sample(index) { a, idx -> IndexedValue(idx, a) }
+ }
+
+ /**
+ * Returns a [TFlow] containing the results of applying [transform] to each value of the
+ * original [TFlow] and its index (starting from zero).
+ *
+ * Shorthand for:
+ * ```
+ * withIndex().map { (idx, a) -> transform(idx, a) }
+ * ```
+ */
+ @ExperimentalFrpApi
+ fun <A, B> TFlow<A>.mapIndexed(transform: suspend FrpTransactionScope.(Int, A) -> B): TFlow<B> {
+ val index = fold(0) { _, i -> i + 1 }
+ return sample(index) { a, idx -> transform(idx, a) }
+ }
+
+ /** Returns a [TFlow] where all subsequent repetitions of the same value are filtered out. */
+ @ExperimentalFrpApi
+ fun <A> TFlow<A>.distinctUntilChanged(): TFlow<A> {
+ val state: TState<Any?> = hold(Any())
+ return filter { it != state.sample() }
+ }
+
+ /**
+ * Returns a new [TFlow] that emits at the same rate as the original [TFlow], but combines the
+ * emitted value with the most recent emission from [other] using [transform].
+ *
+ * Note that the returned [TFlow] will not emit anything until [other] has emitted at least one
+ * value.
+ */
+ @ExperimentalFrpApi
+ fun <A, B, C> TFlow<A>.sample(
+ other: TFlow<B>,
+ transform: suspend FrpTransactionScope.(A, B) -> C,
+ ): TFlow<C> {
+ val state = other.mapCheap { just(it) }.hold(none)
+ return sample(state) { a, b -> b.map { transform(a, it) } }.filterJust()
+ }
+
+ /**
+ * Returns a [TState] that samples the [Transactional] held by the given [TState] within the
+ * same transaction that the state changes.
+ */
+ @ExperimentalFrpApi
+ fun <A> TState<Transactional<A>>.sampleTransactionals(): TState<A> =
+ stateChanges
+ .sampleTransactionals()
+ .holdDeferred(deferredTransactionScope { sample().sample() })
+
+ /**
+ * Returns a [TState] that transforms the value held inside this [TState] by applying it to the
+ * given function [transform].
+ */
+ @ExperimentalFrpApi
+ fun <A, B> TState<A>.map(transform: suspend FrpTransactionScope.(A) -> B): TState<B> =
+ mapPure { transactionally { transform(it) } }.sampleTransactionals()
+
+ /**
+ * Returns a [TState] whose value is generated with [transform] by combining the current values
+ * of each given [TState].
+ *
+ * @see TState.combineWith
+ */
+ @ExperimentalFrpApi
+ fun <A, B, Z> combine(
+ stateA: TState<A>,
+ stateB: TState<B>,
+ transform: suspend FrpTransactionScope.(A, B) -> Z,
+ ): TState<Z> =
+ com.android.systemui.experimental.frp
+ .combine(stateA, stateB) { a, b -> transactionally { transform(a, b) } }
+ .sampleTransactionals()
+
+ /**
+ * Returns a [TState] whose value is generated with [transform] by combining the current values
+ * of each given [TState].
+ *
+ * @see TState.combineWith
+ */
+ @ExperimentalFrpApi
+ fun <A, B, C, D, Z> combine(
+ stateA: TState<A>,
+ stateB: TState<B>,
+ stateC: TState<C>,
+ stateD: TState<D>,
+ transform: suspend FrpTransactionScope.(A, B, C, D) -> Z,
+ ): TState<Z> =
+ com.android.systemui.experimental.frp
+ .combine(stateA, stateB, stateC, stateD) { a, b, c, d ->
+ transactionally { transform(a, b, c, d) }
+ }
+ .sampleTransactionals()
+
+ /** Returns a [TState] by applying [transform] to the value held by the original [TState]. */
+ @ExperimentalFrpApi
+ fun <A, B> TState<A>.flatMap(
+ transform: suspend FrpTransactionScope.(A) -> TState<B>
+ ): TState<B> = mapPure { transactionally { transform(it) } }.sampleTransactionals().flatten()
+
+ /**
+ * Returns a [TState] whose value is generated with [transform] by combining the current values
+ * of each given [TState].
+ *
+ * @see TState.combineWith
+ */
+ @ExperimentalFrpApi
+ fun <A, Z> combine(
+ vararg states: TState<A>,
+ transform: suspend FrpTransactionScope.(List<A>) -> Z,
+ ): TState<Z> = combinePure(*states).map(transform)
+
+ /**
+ * Returns a [TState] whose value is generated with [transform] by combining the current values
+ * of each given [TState].
+ *
+ * @see TState.combineWith
+ */
+ @ExperimentalFrpApi
+ fun <A, Z> Iterable<TState<A>>.combine(
+ transform: suspend FrpTransactionScope.(List<A>) -> Z
+ ): TState<Z> = combinePure().map(transform)
+
+ /**
+ * Returns a [TState] by combining the values held inside the given [TState]s by applying them
+ * to the given function [transform].
+ */
+ @ExperimentalFrpApi
+ fun <A, B, C> TState<A>.combineWith(
+ other: TState<B>,
+ transform: suspend FrpTransactionScope.(A, B) -> C,
+ ): TState<C> = combine(this, other, transform)
+}
diff --git a/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/FrpTransactionScope.kt b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/FrpTransactionScope.kt
new file mode 100644
index 0000000..b0b9dbc
--- /dev/null
+++ b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/FrpTransactionScope.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.experimental.frp
+
+import kotlin.coroutines.RestrictsSuspension
+
+/**
+ * FRP operations that are available while a transaction is active.
+ *
+ * These operations do not accumulate state, which makes [FrpTransactionScope] weaker than
+ * [FrpStateScope], but allows them to be used in more places.
+ */
+@ExperimentalFrpApi
+@RestrictsSuspension
+interface FrpTransactionScope : FrpScope {
+
+ /**
+ * Returns the current value of this [Transactional] as a [FrpDeferredValue].
+ *
+ * @see sample
+ */
+ @ExperimentalFrpApi fun <A> Transactional<A>.sampleDeferred(): FrpDeferredValue<A>
+
+ /**
+ * Returns the current value of this [TState] as a [FrpDeferredValue].
+ *
+ * @see sample
+ */
+ @ExperimentalFrpApi fun <A> TState<A>.sampleDeferred(): FrpDeferredValue<A>
+
+ /** TODO */
+ @ExperimentalFrpApi
+ fun <A> deferredTransactionScope(
+ block: suspend FrpTransactionScope.() -> A
+ ): FrpDeferredValue<A>
+
+ /** A [TFlow] that emits once, within this transaction, and then never again. */
+ @ExperimentalFrpApi val now: TFlow<Unit>
+
+ /**
+ * Returns the current value held by this [TState]. Guaranteed to be consistent within the same
+ * transaction.
+ */
+ @ExperimentalFrpApi suspend fun <A> TState<A>.sample(): A = sampleDeferred().get()
+
+ /**
+ * Returns the current value held by this [Transactional]. Guaranteed to be consistent within
+ * the same transaction.
+ */
+ @ExperimentalFrpApi suspend fun <A> Transactional<A>.sample(): A = sampleDeferred().get()
+}
diff --git a/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/TFlow.kt b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/TFlow.kt
new file mode 100644
index 0000000..cca6c9a
--- /dev/null
+++ b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/TFlow.kt
@@ -0,0 +1,560 @@
+/*
+ * 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.experimental.frp
+
+import com.android.systemui.experimental.frp.internal.DemuxImpl
+import com.android.systemui.experimental.frp.internal.Init
+import com.android.systemui.experimental.frp.internal.InitScope
+import com.android.systemui.experimental.frp.internal.InputNode
+import com.android.systemui.experimental.frp.internal.Network
+import com.android.systemui.experimental.frp.internal.NoScope
+import com.android.systemui.experimental.frp.internal.TFlowImpl
+import com.android.systemui.experimental.frp.internal.activated
+import com.android.systemui.experimental.frp.internal.cached
+import com.android.systemui.experimental.frp.internal.constInit
+import com.android.systemui.experimental.frp.internal.filterNode
+import com.android.systemui.experimental.frp.internal.init
+import com.android.systemui.experimental.frp.internal.map
+import com.android.systemui.experimental.frp.internal.mapImpl
+import com.android.systemui.experimental.frp.internal.mapMaybeNode
+import com.android.systemui.experimental.frp.internal.mergeNodes
+import com.android.systemui.experimental.frp.internal.mergeNodesLeft
+import com.android.systemui.experimental.frp.internal.neverImpl
+import com.android.systemui.experimental.frp.internal.switchDeferredImplSingle
+import com.android.systemui.experimental.frp.internal.switchPromptImpl
+import com.android.systemui.experimental.frp.internal.util.hashString
+import com.android.systemui.experimental.frp.util.Either
+import com.android.systemui.experimental.frp.util.Left
+import com.android.systemui.experimental.frp.util.Maybe
+import com.android.systemui.experimental.frp.util.Right
+import com.android.systemui.experimental.frp.util.just
+import com.android.systemui.experimental.frp.util.map
+import com.android.systemui.experimental.frp.util.toMaybe
+import java.util.concurrent.atomic.AtomicReference
+import kotlin.reflect.KProperty
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.async
+import kotlinx.coroutines.coroutineScope
+
+/** A series of values of type [A] available at discrete points in time. */
+@ExperimentalFrpApi
+sealed class TFlow<out A> {
+ companion object {
+ /** A [TFlow] with no values. */
+ val empty: TFlow<Nothing> = EmptyFlow
+ }
+}
+
+/** A [TFlow] with no values. */
+@ExperimentalFrpApi val emptyTFlow: TFlow<Nothing> = TFlow.empty
+
+/**
+ * A forward-reference to a [TFlow]. Useful for recursive definitions.
+ *
+ * This reference can be used like a standard [TFlow], but will hold up evaluation of the FRP
+ * network until the [loopback] reference is set.
+ */
+@ExperimentalFrpApi
+class TFlowLoop<A> : TFlow<A>() {
+ private val deferred = CompletableDeferred<TFlow<A>>()
+
+ internal val init: Init<TFlowImpl<A>> =
+ init(name = null) { deferred.await().init.connect(evalScope = this) }
+
+ /** The [TFlow] this reference is referring to. */
+ @ExperimentalFrpApi
+ var loopback: TFlow<A>? = null
+ set(value) {
+ value?.let {
+ check(deferred.complete(value)) { "TFlowLoop.loopback has already been set." }
+ field = value
+ }
+ }
+
+ operator fun getValue(thisRef: Any?, property: KProperty<*>): TFlow<A> = this
+
+ operator fun setValue(thisRef: Any?, property: KProperty<*>, value: TFlow<A>) {
+ loopback = value
+ }
+
+ override fun toString(): String = "${this::class.simpleName}@$hashString"
+}
+
+/** TODO */
+@ExperimentalFrpApi fun <A> Lazy<TFlow<A>>.defer(): TFlow<A> = deferInline { value }
+
+/** TODO */
+@ExperimentalFrpApi
+fun <A> FrpDeferredValue<TFlow<A>>.defer(): TFlow<A> = deferInline { unwrapped.await() }
+
+/** TODO */
+@ExperimentalFrpApi
+fun <A> deferTFlow(block: suspend FrpScope.() -> TFlow<A>): TFlow<A> = deferInline {
+ NoScope.runInFrpScope(block)
+}
+
+/** Returns a [TFlow] that emits the new value of this [TState] when it changes. */
+@ExperimentalFrpApi
+val <A> TState<A>.stateChanges: TFlow<A>
+ get() = TFlowInit(init(name = null) { init.connect(evalScope = this).changes })
+
+/**
+ * Returns a [TFlow] that contains only the [just] results of applying [transform] to each value of
+ * the original [TFlow].
+ *
+ * @see mapNotNull
+ */
+@ExperimentalFrpApi
+fun <A, B> TFlow<A>.mapMaybe(transform: suspend FrpTransactionScope.(A) -> Maybe<B>): TFlow<B> {
+ val pulse =
+ mapMaybeNode({ init.connect(evalScope = this) }) { runInTransactionScope { transform(it) } }
+ return TFlowInit(constInit(name = null, pulse))
+}
+
+/**
+ * Returns a [TFlow] that contains only the non-null results of applying [transform] to each value
+ * of the original [TFlow].
+ *
+ * @see mapMaybe
+ */
+@ExperimentalFrpApi
+fun <A, B> TFlow<A>.mapNotNull(transform: suspend FrpTransactionScope.(A) -> B?): TFlow<B> =
+ mapMaybe {
+ transform(it).toMaybe()
+ }
+
+/** Returns a [TFlow] containing only values of the original [TFlow] that are not null. */
+@ExperimentalFrpApi fun <A> TFlow<A?>.filterNotNull(): TFlow<A> = mapNotNull { it }
+
+/** Shorthand for `mapNotNull { it as? A }`. */
+@ExperimentalFrpApi
+inline fun <reified A> TFlow<*>.filterIsInstance(): TFlow<A> = mapNotNull { it as? A }
+
+/** Shorthand for `mapMaybe { it }`. */
+@ExperimentalFrpApi fun <A> TFlow<Maybe<A>>.filterJust(): TFlow<A> = mapMaybe { it }
+
+/**
+ * Returns a [TFlow] containing the results of applying [transform] to each value of the original
+ * [TFlow].
+ */
+@ExperimentalFrpApi
+fun <A, B> TFlow<A>.map(transform: suspend FrpTransactionScope.(A) -> B): TFlow<B> {
+ val mapped: TFlowImpl<B> =
+ mapImpl({ init.connect(evalScope = this) }) { a -> runInTransactionScope { transform(a) } }
+ return TFlowInit(constInit(name = null, mapped.cached()))
+}
+
+/**
+ * Like [map], but the emission is not cached during the transaction. Use only if [transform] is
+ * fast and pure.
+ *
+ * @see map
+ */
+@ExperimentalFrpApi
+fun <A, B> TFlow<A>.mapCheap(transform: suspend FrpTransactionScope.(A) -> B): TFlow<B> =
+ TFlowInit(
+ constInit(
+ name = null,
+ mapImpl({ init.connect(evalScope = this) }) { a ->
+ runInTransactionScope { transform(a) }
+ },
+ )
+ )
+
+/**
+ * Returns a [TFlow] that invokes [action] before each value of the original [TFlow] is emitted.
+ * Useful for logging and debugging.
+ *
+ * ```
+ * pulse.onEach { foo(it) } == pulse.map { foo(it); it }
+ * ```
+ *
+ * Note that the side effects performed in [onEach] are only performed while the resulting [TFlow]
+ * is connected to an output of the FRP network. If your goal is to reliably perform side effects in
+ * response to a [TFlow], use the output combinators available in [FrpBuildScope], such as
+ * [FrpBuildScope.toSharedFlow] or [FrpBuildScope.observe].
+ */
+@ExperimentalFrpApi
+fun <A> TFlow<A>.onEach(action: suspend FrpTransactionScope.(A) -> Unit): TFlow<A> = map {
+ action(it)
+ it
+}
+
+/**
+ * Returns a [TFlow] containing only values of the original [TFlow] that satisfy the given
+ * [predicate].
+ */
+@ExperimentalFrpApi
+fun <A> TFlow<A>.filter(predicate: suspend FrpTransactionScope.(A) -> Boolean): TFlow<A> {
+ val pulse =
+ filterNode({ init.connect(evalScope = this) }) { runInTransactionScope { predicate(it) } }
+ return TFlowInit(constInit(name = null, pulse.cached()))
+}
+
+/**
+ * Splits a [TFlow] of pairs into a pair of [TFlows][TFlow], where each returned [TFlow] emits half
+ * of the original.
+ *
+ * Shorthand for:
+ * ```kotlin
+ * val lefts = map { it.first }
+ * val rights = map { it.second }
+ * return Pair(lefts, rights)
+ * ```
+ */
+@ExperimentalFrpApi
+fun <A, B> TFlow<Pair<A, B>>.unzip(): Pair<TFlow<A>, TFlow<B>> {
+ val lefts = map { it.first }
+ val rights = map { it.second }
+ return lefts to rights
+}
+
+/**
+ * Merges the given [TFlows][TFlow] into a single [TFlow] that emits events from both.
+ *
+ * Because [TFlow]s can only emit one value per transaction, the provided [transformCoincidence]
+ * function is used to combine coincident emissions to produce the result value to be emitted by the
+ * merged [TFlow].
+ */
+@ExperimentalFrpApi
+fun <A> TFlow<A>.mergeWith(
+ other: TFlow<A>,
+ transformCoincidence: suspend FrpTransactionScope.(A, A) -> A = { a, _ -> a },
+): TFlow<A> {
+ val node =
+ mergeNodes(
+ getPulse = { init.connect(evalScope = this) },
+ getOther = { other.init.connect(evalScope = this) },
+ ) { a, b ->
+ runInTransactionScope { transformCoincidence(a, b) }
+ }
+ return TFlowInit(constInit(name = null, node))
+}
+
+/**
+ * Merges the given [TFlows][TFlow] into a single [TFlow] that emits events from all. All coincident
+ * emissions are collected into the emitted [List], preserving the input ordering.
+ *
+ * @see mergeWith
+ * @see mergeLeft
+ */
+@ExperimentalFrpApi
+fun <A> merge(vararg flows: TFlow<A>): TFlow<List<A>> = flows.asIterable().merge()
+
+/**
+ * Merges the given [TFlows][TFlow] into a single [TFlow] that emits events from all. In the case of
+ * coincident emissions, the emission from the left-most [TFlow] is emitted.
+ *
+ * @see merge
+ */
+@ExperimentalFrpApi
+fun <A> mergeLeft(vararg flows: TFlow<A>): TFlow<A> = flows.asIterable().mergeLeft()
+
+/**
+ * Merges the given [TFlows][TFlow] into a single [TFlow] that emits events from all.
+ *
+ * Because [TFlow]s can only emit one value per transaction, the provided [transformCoincidence]
+ * function is used to combine coincident emissions to produce the result value to be emitted by the
+ * merged [TFlow].
+ */
+// TODO: can be optimized to avoid creating the intermediate list
+fun <A> merge(vararg flows: TFlow<A>, transformCoincidence: (A, A) -> A): TFlow<A> =
+ merge(*flows).map { l -> l.reduce(transformCoincidence) }
+
+/**
+ * Merges the given [TFlows][TFlow] into a single [TFlow] that emits events from all. All coincident
+ * emissions are collected into the emitted [List], preserving the input ordering.
+ *
+ * @see mergeWith
+ * @see mergeLeft
+ */
+@ExperimentalFrpApi
+fun <A> Iterable<TFlow<A>>.merge(): TFlow<List<A>> =
+ TFlowInit(constInit(name = null, mergeNodes { map { it.init.connect(evalScope = this) } }))
+
+/**
+ * Merges the given [TFlows][TFlow] into a single [TFlow] that emits events from all. In the case of
+ * coincident emissions, the emission from the left-most [TFlow] is emitted.
+ *
+ * @see merge
+ */
+@ExperimentalFrpApi
+fun <A> Iterable<TFlow<A>>.mergeLeft(): TFlow<A> =
+ TFlowInit(constInit(name = null, mergeNodesLeft { map { it.init.connect(evalScope = this) } }))
+
+/**
+ * Creates a new [TFlow] that emits events from all given [TFlow]s. All simultaneous emissions are
+ * collected into the emitted [List], preserving the input ordering.
+ *
+ * @see mergeWith
+ */
+@ExperimentalFrpApi fun <A> Sequence<TFlow<A>>.merge(): TFlow<List<A>> = asIterable().merge()
+
+/**
+ * Creates a new [TFlow] that emits events from all given [TFlow]s. All simultaneous emissions are
+ * collected into the emitted [Map], and are given the same key of the associated [TFlow] in the
+ * input [Map].
+ *
+ * @see mergeWith
+ */
+@ExperimentalFrpApi
+fun <K, A> Map<K, TFlow<A>>.merge(): TFlow<Map<K, A>> =
+ asSequence().map { (k, flowA) -> flowA.map { a -> k to a } }.toList().merge().map { it.toMap() }
+
+/**
+ * Returns a [GroupedTFlow] that can be used to efficiently split a single [TFlow] into multiple
+ * downstream [TFlow]s.
+ *
+ * The input [TFlow] emits [Map] instances that specify which downstream [TFlow] the associated
+ * value will be emitted from. These downstream [TFlow]s can be obtained via
+ * [GroupedTFlow.eventsForKey].
+ *
+ * An example:
+ * ```
+ * val sFoo: TFlow<Map<String, Foo>> = ...
+ * val fooById: GroupedTFlow<String, Foo> = sFoo.groupByKey()
+ * val fooBar: TFlow<Foo> = fooById["bar"]
+ * ```
+ *
+ * This is semantically equivalent to `val fooBar = sFoo.mapNotNull { map -> map["bar"] }` but is
+ * significantly more efficient; specifically, using [mapNotNull] in this way incurs a `O(n)`
+ * performance hit, where `n` is the number of different [mapNotNull] operations used to filter on a
+ * specific key's presence in the emitted [Map]. [groupByKey] internally uses a [HashMap] to lookup
+ * the appropriate downstream [TFlow], and so operates in `O(1)`.
+ *
+ * Note that the result [GroupedTFlow] should be cached and re-used to gain the performance benefit.
+ *
+ * @see selector
+ */
+@ExperimentalFrpApi
+fun <K, A> TFlow<Map<K, A>>.groupByKey(numKeys: Int? = null): GroupedTFlow<K, A> =
+ GroupedTFlow(DemuxImpl({ init.connect(this) }, numKeys))
+
+/**
+ * Shorthand for `map { mapOf(extractKey(it) to it) }.groupByKey()`
+ *
+ * @see groupByKey
+ */
+@ExperimentalFrpApi
+fun <K, A> TFlow<A>.groupBy(
+ numKeys: Int? = null,
+ extractKey: suspend FrpTransactionScope.(A) -> K,
+): GroupedTFlow<K, A> = map { mapOf(extractKey(it) to it) }.groupByKey(numKeys)
+
+/**
+ * Returns two new [TFlow]s that contain elements from this [TFlow] that satisfy or don't satisfy
+ * [predicate].
+ *
+ * Using this is equivalent to `upstream.filter(predicate) to upstream.filter { !predicate(it) }`
+ * but is more efficient; specifically, [partition] will only invoke [predicate] once per element.
+ */
+@ExperimentalFrpApi
+fun <A> TFlow<A>.partition(
+ predicate: suspend FrpTransactionScope.(A) -> Boolean
+): Pair<TFlow<A>, TFlow<A>> {
+ val grouped: GroupedTFlow<Boolean, A> = groupBy(numKeys = 2, extractKey = predicate)
+ return Pair(grouped.eventsForKey(true), grouped.eventsForKey(false))
+}
+
+/**
+ * Returns two new [TFlow]s that contain elements from this [TFlow]; [Pair.first] will contain
+ * [Left] values, and [Pair.second] will contain [Right] values.
+ *
+ * Using this is equivalent to using [filterIsInstance] in conjunction with [map] twice, once for
+ * [Left]s and once for [Right]s, but is slightly more efficient; specifically, the
+ * [filterIsInstance] check is only performed once per element.
+ */
+@ExperimentalFrpApi
+fun <A, B> TFlow<Either<A, B>>.partitionEither(): Pair<TFlow<A>, TFlow<B>> {
+ val (left, right) = partition { it is Left }
+ return Pair(left.mapCheap { (it as Left).value }, right.mapCheap { (it as Right).value })
+}
+
+/**
+ * A mapping from keys of type [K] to [TFlow]s emitting values of type [A].
+ *
+ * @see groupByKey
+ */
+@ExperimentalFrpApi
+class GroupedTFlow<in K, out A> internal constructor(internal val impl: DemuxImpl<K, A>) {
+ /**
+ * Returns a [TFlow] that emits values of type [A] that correspond to the given [key].
+ *
+ * @see groupByKey
+ */
+ @ExperimentalFrpApi
+ fun eventsForKey(key: K): TFlow<A> = TFlowInit(constInit(name = null, impl.eventsForKey(key)))
+
+ /**
+ * Returns a [TFlow] that emits values of type [A] that correspond to the given [key].
+ *
+ * @see groupByKey
+ */
+ @ExperimentalFrpApi operator fun get(key: K): TFlow<A> = eventsForKey(key)
+}
+
+/**
+ * Returns a [TFlow] that switches to the [TFlow] contained within this [TState] whenever it
+ * changes.
+ *
+ * This switch does take effect until the *next* transaction after [TState] changes. For a switch
+ * that takes effect immediately, see [switchPromptly].
+ */
+@ExperimentalFrpApi
+fun <A> TState<TFlow<A>>.switch(): TFlow<A> {
+ return TFlowInit(
+ constInit(
+ name = null,
+ switchDeferredImplSingle(
+ getStorage = {
+ init.connect(this).getCurrentWithEpoch(this).first.init.connect(this)
+ },
+ getPatches = {
+ mapImpl({ init.connect(this).changes }) { newFlow ->
+ newFlow.init.connect(this)
+ }
+ },
+ ),
+ )
+ )
+}
+
+/**
+ * Returns a [TFlow] that switches to the [TFlow] contained within this [TState] whenever it
+ * changes.
+ *
+ * This switch takes effect immediately within the same transaction that [TState] changes. In
+ * general, you should prefer [switch] over this method. It is both safer and more performant.
+ */
+// TODO: parameter to handle coincidental emission from both old and new
+@ExperimentalFrpApi
+fun <A> TState<TFlow<A>>.switchPromptly(): TFlow<A> {
+ val switchNode =
+ switchPromptImpl(
+ getStorage = {
+ mapOf(Unit to init.connect(this).getCurrentWithEpoch(this).first.init.connect(this))
+ },
+ getPatches = {
+ val patches = init.connect(this).changes
+ mapImpl({ patches }) { newFlow -> mapOf(Unit to just(newFlow.init.connect(this))) }
+ },
+ )
+ return TFlowInit(constInit(name = null, mapImpl({ switchNode }) { it.getValue(Unit) }))
+}
+
+/**
+ * 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 coalesce: (old: Out, new: In) -> Out,
+ internal val network: Network,
+ private val getInitialValue: () -> Out,
+ internal val impl: InputNode<Out> = InputNode(),
+) : TFlow<Out>() {
+ internal val name: String? = null
+ 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 {
+ 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 {
+ val newEmit =
+ async(start = CoroutineStart.LAZY) {
+ network.transaction { impl.visit(this, value) }.await()
+ }
+ val jobOrNull = storage.getAndSet(newEmit)
+ if (jobOrNull?.isActive != true) {
+ newEmit.await()
+ } else {
+ jobOrNull.join()
+ }
+ }
+ }
+
+ // internal suspend fun emitInCurrentTransaction(value: T, evalScope: EvalScope) {
+ // if (storage.getAndSet(just(value)) is None) {
+ // impl.visit(evalScope)
+ // }
+ // }
+}
+
+private data object EmptyFlow : TFlow<Nothing>()
+
+internal class TFlowInit<out A>(val init: Init<TFlowImpl<A>>) : TFlow<A>() {
+ override fun toString(): String = "${this::class.simpleName}@$hashString"
+}
+
+internal val <A> TFlow<A>.init: Init<TFlowImpl<A>>
+ get() =
+ when (this) {
+ is EmptyFlow -> constInit("EmptyFlow", neverImpl)
+ is TFlowInit -> init
+ is TFlowLoop -> init
+ is CoalescingMutableTFlow<*, A> -> constInit(name, impl.activated())
+ is MutableTFlow -> constInit(name, impl.activated())
+ }
+
+private inline fun <A> deferInline(crossinline block: suspend InitScope.() -> TFlow<A>): TFlow<A> =
+ TFlowInit(init(name = null) { block().init.connect(evalScope = this) })
diff --git a/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/TState.kt b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/TState.kt
new file mode 100644
index 0000000..a5ec503
--- /dev/null
+++ b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/TState.kt
@@ -0,0 +1,492 @@
+/*
+ * 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.experimental.frp
+
+import com.android.systemui.experimental.frp.internal.DerivedMapCheap
+import com.android.systemui.experimental.frp.internal.Init
+import com.android.systemui.experimental.frp.internal.InitScope
+import com.android.systemui.experimental.frp.internal.Network
+import com.android.systemui.experimental.frp.internal.NoScope
+import com.android.systemui.experimental.frp.internal.Schedulable
+import com.android.systemui.experimental.frp.internal.TFlowImpl
+import com.android.systemui.experimental.frp.internal.TStateImpl
+import com.android.systemui.experimental.frp.internal.TStateSource
+import com.android.systemui.experimental.frp.internal.activated
+import com.android.systemui.experimental.frp.internal.cached
+import com.android.systemui.experimental.frp.internal.constInit
+import com.android.systemui.experimental.frp.internal.constS
+import com.android.systemui.experimental.frp.internal.filterNode
+import com.android.systemui.experimental.frp.internal.flatMap
+import com.android.systemui.experimental.frp.internal.init
+import com.android.systemui.experimental.frp.internal.map
+import com.android.systemui.experimental.frp.internal.mapCheap
+import com.android.systemui.experimental.frp.internal.mapImpl
+import com.android.systemui.experimental.frp.internal.util.hashString
+import com.android.systemui.experimental.frp.internal.zipStates
+import kotlin.reflect.KProperty
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.async
+import kotlinx.coroutines.coroutineScope
+
+/**
+ * A time-varying value with discrete changes. Essentially, a combination of a [Transactional] that
+ * holds a value, and a [TFlow] that emits when the value changes.
+ */
+@ExperimentalFrpApi sealed class TState<out A>
+
+/** A [TState] that never changes. */
+@ExperimentalFrpApi
+fun <A> tStateOf(value: A): TState<A> {
+ val operatorName = "tStateOf"
+ val name = "$operatorName($value)"
+ return TStateInit(constInit(name, constS(name, operatorName, value)))
+}
+
+/** TODO */
+@ExperimentalFrpApi fun <A> Lazy<TState<A>>.defer(): TState<A> = deferInline { value }
+
+/** TODO */
+@ExperimentalFrpApi
+fun <A> FrpDeferredValue<TState<A>>.defer(): TState<A> = deferInline { unwrapped.await() }
+
+/** TODO */
+@ExperimentalFrpApi
+fun <A> deferTState(block: suspend FrpScope.() -> TState<A>): TState<A> = deferInline {
+ NoScope.runInFrpScope(block)
+}
+
+/**
+ * Returns a [TState] containing the results of applying [transform] to the value held by the
+ * original [TState].
+ */
+@ExperimentalFrpApi
+fun <A, B> TState<A>.map(transform: suspend FrpScope.(A) -> B): TState<B> {
+ val operatorName = "map"
+ val name = operatorName
+ return TStateInit(
+ init(name) {
+ init.connect(evalScope = this).map(name, operatorName) {
+ NoScope.runInFrpScope { transform(it) }
+ }
+ }
+ )
+}
+
+/**
+ * Returns a [TState] that transforms the value held inside this [TState] by applying it to the
+ * [transform].
+ *
+ * Note that unlike [map], the result is not cached. This means that not only should [transform] be
+ * fast and pure, it should be *monomorphic* (1-to-1). Failure to do this means that [stateChanges]
+ * for the returned [TState] will operate unexpectedly, emitting at rates that do not reflect an
+ * observable change to the returned [TState].
+ */
+@ExperimentalFrpApi
+fun <A, B> TState<A>.mapCheapUnsafe(transform: suspend FrpScope.(A) -> B): TState<B> {
+ val operatorName = "map"
+ val name = operatorName
+ return TStateInit(
+ init(name) {
+ init.connect(evalScope = this).mapCheap(name, operatorName) {
+ NoScope.runInFrpScope { transform(it) }
+ }
+ }
+ )
+}
+
+/**
+ * Returns a [TState] by combining the values held inside the given [TState]s by applying them to
+ * the given function [transform].
+ */
+@ExperimentalFrpApi
+fun <A, B, C> TState<A>.combineWith(
+ other: TState<B>,
+ transform: suspend FrpScope.(A, B) -> C,
+): TState<C> = combine(this, other, transform)
+
+/**
+ * Splits a [TState] of pairs into a pair of [TFlows][TState], where each returned [TState] holds
+ * hald 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 : Any, A> Map<K, TState<A>>.combine(): TState<Map<K, A>> {
+ val operatorName = "combine"
+ val name = operatorName
+ return TStateInit(
+ init(name) {
+ zipStates(
+ name,
+ operatorName,
+ states = mapValues { it.value.init.connect(evalScope = this) },
+ )
+ }
+ )
+}
+
+/**
+ * Returns a [TState] whose value is generated with [transform] by combining the current values of
+ * each given [TState].
+ *
+ * @see TState.combineWith
+ */
+@ExperimentalFrpApi
+fun <A, B> Iterable<TState<A>>.combine(transform: suspend FrpScope.(List<A>) -> B): TState<B> =
+ combine().map(transform)
+
+/**
+ * Returns a [TState] by combining the values held inside the given [TState]s into a [List].
+ *
+ * @see TState.combineWith
+ */
+@ExperimentalFrpApi
+fun <A> combine(vararg states: TState<A>): TState<List<A>> = states.asIterable().combine()
+
+/**
+ * Returns a [TState] whose value is generated with [transform] by combining the current values of
+ * each given [TState].
+ *
+ * @see TState.combineWith
+ */
+@ExperimentalFrpApi
+fun <A, B> combine(
+ vararg states: TState<A>,
+ transform: suspend FrpScope.(List<A>) -> B,
+): TState<B> = states.asIterable().combine(transform)
+
+/**
+ * Returns a [TState] whose value is generated with [transform] by combining the current values of
+ * each given [TState].
+ *
+ * @see TState.combineWith
+ */
+@ExperimentalFrpApi
+fun <A, B, Z> combine(
+ stateA: TState<A>,
+ stateB: TState<B>,
+ transform: suspend FrpScope.(A, B) -> Z,
+): TState<Z> {
+ val operatorName = "combine"
+ val name = operatorName
+ return TStateInit(
+ init(name) {
+ coroutineScope {
+ val dl1: Deferred<TStateImpl<A>> = async {
+ stateA.init.connect(evalScope = this@init)
+ }
+ val dl2: Deferred<TStateImpl<B>> = async {
+ stateB.init.connect(evalScope = this@init)
+ }
+ zipStates(name, operatorName, dl1.await(), dl2.await()) { a, b ->
+ NoScope.runInFrpScope { transform(a, b) }
+ }
+ }
+ }
+ )
+}
+
+/**
+ * Returns a [TState] whose value is generated with [transform] by combining the current values of
+ * each given [TState].
+ *
+ * @see TState.combineWith
+ */
+@ExperimentalFrpApi
+fun <A, B, C, Z> combine(
+ stateA: TState<A>,
+ stateB: TState<B>,
+ stateC: TState<C>,
+ transform: suspend FrpScope.(A, B, C) -> Z,
+): TState<Z> {
+ val operatorName = "combine"
+ val name = operatorName
+ return TStateInit(
+ init(name) {
+ coroutineScope {
+ val dl1: Deferred<TStateImpl<A>> = async {
+ stateA.init.connect(evalScope = this@init)
+ }
+ val dl2: Deferred<TStateImpl<B>> = async {
+ stateB.init.connect(evalScope = this@init)
+ }
+ val dl3: Deferred<TStateImpl<C>> = async {
+ stateC.init.connect(evalScope = this@init)
+ }
+ zipStates(name, operatorName, dl1.await(), dl2.await(), dl3.await()) { a, b, c ->
+ NoScope.runInFrpScope { transform(a, b, c) }
+ }
+ }
+ }
+ )
+}
+
+/**
+ * Returns a [TState] whose value is generated with [transform] by combining the current values of
+ * each given [TState].
+ *
+ * @see TState.combineWith
+ */
+@ExperimentalFrpApi
+fun <A, B, C, D, Z> combine(
+ stateA: TState<A>,
+ stateB: TState<B>,
+ stateC: TState<C>,
+ stateD: TState<D>,
+ transform: suspend FrpScope.(A, B, C, D) -> Z,
+): TState<Z> {
+ val operatorName = "combine"
+ val name = operatorName
+ return TStateInit(
+ init(name) {
+ coroutineScope {
+ val dl1: Deferred<TStateImpl<A>> = async {
+ stateA.init.connect(evalScope = this@init)
+ }
+ val dl2: Deferred<TStateImpl<B>> = async {
+ stateB.init.connect(evalScope = this@init)
+ }
+ val dl3: Deferred<TStateImpl<C>> = async {
+ stateC.init.connect(evalScope = this@init)
+ }
+ val dl4: Deferred<TStateImpl<D>> = async {
+ stateD.init.connect(evalScope = this@init)
+ }
+ zipStates(name, operatorName, dl1.await(), dl2.await(), dl3.await(), dl4.await()) {
+ a,
+ b,
+ c,
+ d ->
+ NoScope.runInFrpScope { transform(a, b, c, d) }
+ }
+ }
+ }
+ )
+}
+
+/** Returns a [TState] by applying [transform] to the value held by the original [TState]. */
+@ExperimentalFrpApi
+fun <A, B> TState<A>.flatMap(transform: suspend FrpScope.(A) -> TState<B>): TState<B> {
+ val operatorName = "flatMap"
+ val name = operatorName
+ return TStateInit(
+ init(name) {
+ init.connect(this).flatMap(name, operatorName) { a ->
+ NoScope.runInFrpScope { transform(a) }.init.connect(this)
+ }
+ }
+ )
+}
+
+/** Shorthand for `flatMap { it }` */
+@ExperimentalFrpApi fun <A> TState<TState<A>>.flatten() = flatMap { it }
+
+/**
+ * Returns a [TStateSelector] that can be used to efficiently check if the input [TState] is
+ * currently holding a specific value.
+ *
+ * An example:
+ * ```
+ * val lInt: TState<Int> = ...
+ * val intSelector: TStateSelector<Int> = lInt.selector()
+ * // Tracks if lInt is holding 1
+ * val isOne: TState<Boolean> = intSelector.whenSelected(1)
+ * ```
+ *
+ * This is semantically equivalent to `val isOne = lInt.map { i -> i == 1 }`, but is significantly
+ * more efficient; specifically, using [TState.map] in this way incurs a `O(n)` performance hit,
+ * where `n` is the number of different [TState.map] operations used to track a specific value.
+ * [selector] internally uses a [HashMap] to lookup the appropriate downstream [TState] to update,
+ * and so operates in `O(1)`.
+ *
+ * Note that the result [TStateSelector] should be cached and re-used to gain the performance
+ * benefit.
+ *
+ * @see groupByKey
+ */
+@ExperimentalFrpApi
+fun <A> TState<A>.selector(numDistinctValues: Int? = null): TStateSelector<A> =
+ TStateSelector(
+ this,
+ stateChanges
+ .map { new -> mapOf(new to true, sampleDeferred().get() to false) }
+ .groupByKey(numDistinctValues),
+ )
+
+/**
+ * Tracks the currently selected value of type [A] from an upstream [TState].
+ *
+ * @see selector
+ */
+@ExperimentalFrpApi
+class TStateSelector<A>
+internal constructor(
+ private val upstream: TState<A>,
+ private val groupedChanges: GroupedTFlow<A, Boolean>,
+) {
+ /**
+ * Returns a [TState] that tracks whether the upstream [TState] is currently holding the given
+ * [value].
+ *
+ * @see selector
+ */
+ @ExperimentalFrpApi
+ fun whenSelected(value: A): TState<Boolean> {
+ val operatorName = "TStateSelector#whenSelected"
+ val name = "$operatorName[$value]"
+ return TStateInit(
+ init(name) {
+ DerivedMapCheap(
+ name,
+ operatorName,
+ upstream = upstream.init.connect(evalScope = this),
+ changes = groupedChanges.impl.eventsForKey(value),
+ ) {
+ it == value
+ }
+ }
+ )
+ }
+
+ @ExperimentalFrpApi operator fun get(value: A): TState<Boolean> = whenSelected(value)
+}
+
+/** TODO */
+@ExperimentalFrpApi
+class MutableTState<T>
+internal constructor(internal val network: Network, initialValue: Deferred<T>) : TState<T>() {
+
+ private val input: CoalescingMutableTFlow<Deferred<T>, Deferred<T>?> =
+ CoalescingMutableTFlow(
+ coalesce = { _, new -> new },
+ network = network,
+ getInitialValue = { null },
+ )
+
+ internal val tState = run {
+ val changes = input.impl
+ val name = null
+ val operatorName = "MutableTState"
+ lateinit var state: TStateSource<T>
+ val calm: TFlowImpl<T> =
+ filterNode({ mapImpl(upstream = { changes.activated() }) { it!!.await() } }) { new ->
+ new != state.getCurrentWithEpoch(evalScope = this).first
+ }
+ .cached()
+ state = TStateSource(name, operatorName, initialValue, calm)
+ @Suppress("DeferredResultUnused")
+ network.transaction {
+ calm.activate(evalScope = this, downstream = Schedulable.S(state))?.let {
+ (connection, needsEval) ->
+ state.upstreamConnection = connection
+ if (needsEval) {
+ schedule(state)
+ }
+ }
+ }
+ TStateInit(constInit(name, state))
+ }
+
+ /** TODO */
+ @ExperimentalFrpApi fun setValue(value: T) = input.emit(CompletableDeferred(value))
+
+ @ExperimentalFrpApi
+ fun setValueDeferred(value: FrpDeferredValue<T>) = input.emit(value.unwrapped)
+}
+
+/** A forward-reference to a [TState], allowing for recursive definitions. */
+@ExperimentalFrpApi
+class TStateLoop<A> : TState<A>() {
+
+ private val name: String? = null
+
+ private val deferred = CompletableDeferred<TState<A>>()
+
+ internal val init: Init<TStateImpl<A>> =
+ init(name) { deferred.await().init.connect(evalScope = this) }
+
+ /** The [TState] this [TStateLoop] will forward to. */
+ @ExperimentalFrpApi
+ var loopback: TState<A>? = null
+ set(value) {
+ value?.let {
+ check(deferred.complete(value)) { "TStateLoop.loopback has already been set." }
+ field = value
+ }
+ }
+
+ @ExperimentalFrpApi
+ operator fun getValue(thisRef: Any?, property: KProperty<*>): TState<A> = this
+
+ @ExperimentalFrpApi
+ operator fun setValue(thisRef: Any?, property: KProperty<*>, value: TState<A>) {
+ loopback = value
+ }
+
+ override fun toString(): String = "${this::class.simpleName}@$hashString"
+}
+
+internal class TStateInit<A> internal constructor(internal val init: Init<TStateImpl<A>>) :
+ TState<A>() {
+ override fun toString(): String = "${this::class.simpleName}@$hashString"
+}
+
+internal val <A> TState<A>.init: Init<TStateImpl<A>>
+ get() =
+ when (this) {
+ is TStateInit -> init
+ is TStateLoop -> init
+ is MutableTState -> tState.init
+ }
+
+private inline fun <A> deferInline(
+ crossinline block: suspend InitScope.() -> TState<A>
+): TState<A> = TStateInit(init(name = null) { block().init.connect(evalScope = this) })
diff --git a/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/Transactional.kt b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/Transactional.kt
new file mode 100644
index 0000000..0e7b420
--- /dev/null
+++ b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/Transactional.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.experimental.frp
+
+import com.android.systemui.experimental.frp.internal.InitScope
+import com.android.systemui.experimental.frp.internal.NoScope
+import com.android.systemui.experimental.frp.internal.TransactionalImpl
+import com.android.systemui.experimental.frp.internal.init
+import com.android.systemui.experimental.frp.internal.transactionalImpl
+import com.android.systemui.experimental.frp.internal.util.hashString
+import kotlinx.coroutines.CompletableDeferred
+
+/**
+ * A time-varying value. A [Transactional] encapsulates the idea of some continuous state; each time
+ * it is "sampled", a new result may be produced.
+ *
+ * Because FRP operates over an "idealized" model of Time that can be passed around as a data type,
+ * [Transactional]s are guaranteed to produce the same result if queried multiple times at the same
+ * (conceptual) time, in order to preserve _referential transparency_.
+ */
+@ExperimentalFrpApi
+class Transactional<out A> internal constructor(internal val impl: TState<TransactionalImpl<A>>) {
+ override fun toString(): String = "${this::class.simpleName}@$hashString"
+}
+
+/** A constant [Transactional] that produces [value] whenever it is sampled. */
+@ExperimentalFrpApi
+fun <A> transactionalOf(value: A): Transactional<A> =
+ Transactional(tStateOf(TransactionalImpl.Const(CompletableDeferred(value))))
+
+/** TODO */
+@ExperimentalFrpApi
+fun <A> FrpDeferredValue<Transactional<A>>.defer(): Transactional<A> = deferInline {
+ unwrapped.await()
+}
+
+/** TODO */
+@ExperimentalFrpApi fun <A> Lazy<Transactional<A>>.defer(): Transactional<A> = deferInline { value }
+
+/** TODO */
+@ExperimentalFrpApi
+fun <A> deferTransactional(block: suspend FrpScope.() -> Transactional<A>): Transactional<A> =
+ deferInline {
+ NoScope.runInFrpScope(block)
+ }
+
+private inline fun <A> deferInline(
+ crossinline block: suspend InitScope.() -> Transactional<A>
+): Transactional<A> =
+ Transactional(TStateInit(init(name = null) { block().impl.init.connect(evalScope = this) }))
+
+/**
+ * Returns a [Transactional]. The passed [block] will be evaluated on demand at most once per
+ * transaction; any subsequent sampling within the same transaction will receive a cached value.
+ */
+@ExperimentalFrpApi
+fun <A> transactionally(block: suspend FrpTransactionScope.() -> A): Transactional<A> =
+ Transactional(tStateOf(transactionalImpl { runInTransactionScope(block) }))
diff --git a/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/debug/Debug.kt b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/debug/Debug.kt
new file mode 100644
index 0000000..8062341
--- /dev/null
+++ b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/debug/Debug.kt
@@ -0,0 +1,180 @@
+/*
+ * 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.experimental.frp.debug
+
+import com.android.systemui.experimental.frp.MutableTState
+import com.android.systemui.experimental.frp.TState
+import com.android.systemui.experimental.frp.TStateInit
+import com.android.systemui.experimental.frp.TStateLoop
+import com.android.systemui.experimental.frp.internal.DerivedFlatten
+import com.android.systemui.experimental.frp.internal.DerivedMap
+import com.android.systemui.experimental.frp.internal.DerivedMapCheap
+import com.android.systemui.experimental.frp.internal.DerivedZipped
+import com.android.systemui.experimental.frp.internal.Init
+import com.android.systemui.experimental.frp.internal.TStateDerived
+import com.android.systemui.experimental.frp.internal.TStateImpl
+import com.android.systemui.experimental.frp.internal.TStateSource
+import com.android.systemui.experimental.frp.util.Just
+import com.android.systemui.experimental.frp.util.Maybe
+import com.android.systemui.experimental.frp.util.None
+import com.android.systemui.experimental.frp.util.none
+import com.android.systemui.experimental.frp.util.orElseGet
+
+// object IdGen {
+// private val counter = AtomicLong()
+// fun getId() = counter.getAndIncrement()
+// }
+
+typealias StateGraph = Graph<ActivationInfo>
+
+sealed class StateInfo(
+ val name: String,
+ val value: Maybe<Any?>,
+ val operator: String,
+ val epoch: Long?,
+)
+
+class Source(name: String, value: Maybe<Any?>, operator: String, epoch: Long) :
+ StateInfo(name, value, operator, epoch)
+
+class Derived(
+ name: String,
+ val type: DerivedStateType,
+ value: Maybe<Any?>,
+ operator: String,
+ epoch: Long?,
+) : StateInfo(name, value, operator, epoch)
+
+sealed interface DerivedStateType
+
+data object Flatten : DerivedStateType
+
+data class Mapped(val cheap: Boolean) : DerivedStateType
+
+data object Combine : DerivedStateType
+
+sealed class InitInfo(val name: String)
+
+class Uninitialized(name: String) : InitInfo(name)
+
+class Initialized(val state: StateInfo) : InitInfo(state.name)
+
+sealed interface ActivationInfo
+
+class Inactive(val name: String) : ActivationInfo
+
+class Active(val nodeInfo: StateInfo) : ActivationInfo
+
+class Dead(val name: String) : ActivationInfo
+
+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?>> =
+ when (this) {
+ is TStateInit -> init
+ is TStateLoop -> init
+ is MutableTState -> tState.init
+ }
+ when (val stateMaybe = init.getUnsafe()) {
+ None -> {
+ infoMap[this] = Uninitialized(init.name ?: init.toString())
+ }
+ is Just -> {
+ stateMaybe.value.dump(infoMap, edges)
+ }
+ }
+}
+
+internal fun TStateImpl<*>.dump(infoById: MutableMap<Any, InitInfo>, edges: MutableList<Edge>) {
+ val state = this
+ if (state in infoById) return
+ val stateInfo =
+ when (state) {
+ is TStateDerived -> {
+ val type =
+ when (state) {
+ is DerivedFlatten -> {
+ state.upstream.dump(infoById, edges)
+ edges.add(
+ Edge(upstream = state.upstream, downstream = state, tag = "outer")
+ )
+ state.upstream
+ .getUnsafe()
+ .orElseGet { null }
+ ?.let {
+ edges.add(
+ Edge(upstream = it, downstream = state, tag = "inner")
+ )
+ it.dump(infoById, edges)
+ }
+ Flatten
+ }
+ is DerivedMap<*, *> -> {
+ state.upstream.dump(infoById, edges)
+ edges.add(Edge(upstream = state.upstream, downstream = state))
+ Mapped(cheap = false)
+ }
+ is DerivedZipped<*, *> -> {
+ state.upstream.forEach { (key, upstream) ->
+ edges.add(
+ Edge(upstream = upstream, downstream = state, tag = "key=$key")
+ )
+ upstream.dump(infoById, edges)
+ }
+ Combine
+ }
+ }
+ Derived(
+ state.name ?: state.operatorName,
+ type,
+ state.getCachedUnsafe(),
+ state.operatorName,
+ state.invalidatedEpoch,
+ )
+ }
+ is TStateSource ->
+ Source(
+ state.name ?: state.operatorName,
+ state.getStorageUnsafe(),
+ state.operatorName,
+ state.writeEpoch,
+ )
+ is DerivedMapCheap<*, *> -> {
+ state.upstream.dump(infoById, edges)
+ edges.add(Edge(upstream = state.upstream, downstream = state))
+ val type = Mapped(cheap = true)
+ Derived(
+ state.name ?: state.operatorName,
+ type,
+ state.getUnsafe(),
+ state.operatorName,
+ null,
+ )
+ }
+ }
+ infoById[state] = Initialized(stateInfo)
+}
+
+private fun <A> TStateImpl<A>.getUnsafe(): Maybe<A> =
+ when (this) {
+ is TStateDerived -> getCachedUnsafe()
+ is TStateSource -> getStorageUnsafe()
+ is DerivedMapCheap<*, *> -> none
+ }
diff --git a/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/BuildScopeImpl.kt b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/BuildScopeImpl.kt
new file mode 100644
index 0000000..127abd8
--- /dev/null
+++ b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/BuildScopeImpl.kt
@@ -0,0 +1,363 @@
+/*
+ * 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.experimental.frp.internal
+
+import com.android.systemui.experimental.frp.CoalescingMutableTFlow
+import com.android.systemui.experimental.frp.FrpBuildScope
+import com.android.systemui.experimental.frp.FrpCoalescingProducerScope
+import com.android.systemui.experimental.frp.FrpDeferredValue
+import com.android.systemui.experimental.frp.FrpEffectScope
+import com.android.systemui.experimental.frp.FrpNetwork
+import com.android.systemui.experimental.frp.FrpProducerScope
+import com.android.systemui.experimental.frp.FrpSpec
+import com.android.systemui.experimental.frp.FrpStateScope
+import com.android.systemui.experimental.frp.FrpTransactionScope
+import com.android.systemui.experimental.frp.GroupedTFlow
+import com.android.systemui.experimental.frp.LocalFrpNetwork
+import com.android.systemui.experimental.frp.MutableTFlow
+import com.android.systemui.experimental.frp.TFlow
+import com.android.systemui.experimental.frp.TFlowInit
+import com.android.systemui.experimental.frp.groupByKey
+import com.android.systemui.experimental.frp.init
+import com.android.systemui.experimental.frp.internal.util.childScope
+import com.android.systemui.experimental.frp.internal.util.launchOnCancel
+import com.android.systemui.experimental.frp.internal.util.mapValuesParallel
+import com.android.systemui.experimental.frp.launchEffect
+import com.android.systemui.experimental.frp.util.Just
+import com.android.systemui.experimental.frp.util.Maybe
+import com.android.systemui.experimental.frp.util.None
+import com.android.systemui.experimental.frp.util.just
+import com.android.systemui.experimental.frp.util.map
+import java.util.concurrent.atomic.AtomicReference
+import kotlin.coroutines.Continuation
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+import kotlin.coroutines.startCoroutine
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CompletableJob
+import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.completeWith
+import kotlinx.coroutines.job
+
+internal class BuildScopeImpl(val stateScope: StateScopeImpl, val coroutineScope: CoroutineScope) :
+ BuildScope, StateScope by stateScope {
+
+ private val job: Job
+ get() = coroutineScope.coroutineContext.job
+
+ override val frpScope: FrpBuildScope = FrpBuildScopeImpl()
+
+ override suspend fun <R> runInBuildScope(block: suspend FrpBuildScope.() -> R): R {
+ val complete = CompletableDeferred<R>(parent = coroutineContext.job)
+ block.startCoroutine(
+ frpScope,
+ object : Continuation<R> {
+ override val context: CoroutineContext
+ get() = EmptyCoroutineContext
+
+ override fun resumeWith(result: Result<R>) {
+ complete.completeWith(result)
+ }
+ },
+ )
+ return complete.await()
+ }
+
+ private fun <A, T : TFlow<A>, S> buildTFlow(
+ constructFlow: (InputNode<A>) -> Pair<T, S>,
+ builder: suspend S.() -> Unit,
+ ): TFlow<A> {
+ var job: Job? = null
+ val stopEmitter = newStopEmitter()
+ val handle = this.job.invokeOnCompletion { stopEmitter.emit(Unit) }
+ // Create a child scope that will be kept alive beyond the end of this transaction.
+ val childScope = coroutineScope.childScope()
+ lateinit var emitter: Pair<T, S>
+ val inputNode =
+ InputNode<A>(
+ activate = {
+ check(job == null) { "already activated" }
+ job =
+ reenterBuildScope(this@BuildScopeImpl, childScope).runInBuildScope {
+ launchEffect {
+ builder(emitter.second)
+ handle.dispose()
+ stopEmitter.emit(Unit)
+ }
+ }
+ },
+ deactivate = {
+ checkNotNull(job) { "already deactivated" }.cancel()
+ job = null
+ },
+ )
+ emitter = constructFlow(inputNode)
+ return with(frpScope) { emitter.first.takeUntil(stopEmitter) }
+ }
+
+ private fun <T> tFlowInternal(builder: suspend FrpProducerScope<T>.() -> Unit): TFlow<T> =
+ buildTFlow(
+ constructFlow = { inputNode ->
+ val flow = MutableTFlow(network, inputNode)
+ flow to
+ object : FrpProducerScope<T> {
+ override suspend fun emit(value: T) {
+ flow.emit(value)
+ }
+ }
+ },
+ builder = builder,
+ )
+
+ private fun <In, Out> coalescingTFlowInternal(
+ getInitialValue: () -> Out,
+ coalesce: (old: Out, new: In) -> Out,
+ builder: suspend FrpCoalescingProducerScope<In>.() -> Unit,
+ ): TFlow<Out> =
+ buildTFlow(
+ constructFlow = { inputNode ->
+ val flow = CoalescingMutableTFlow(coalesce, network, getInitialValue, inputNode)
+ flow to
+ object : FrpCoalescingProducerScope<In> {
+ override fun emit(value: In) {
+ flow.emit(value)
+ }
+ }
+ },
+ builder = builder,
+ )
+
+ private fun <A> asyncScopeInternal(block: FrpSpec<A>): Pair<FrpDeferredValue<A>, Job> {
+ val childScope = mutableChildBuildScope()
+ return FrpDeferredValue(deferAsync { childScope.runInBuildScope(block) }) to childScope.job
+ }
+
+ private fun <R> deferredInternal(block: suspend FrpBuildScope.() -> R): FrpDeferredValue<R> =
+ FrpDeferredValue(deferAsync { runInBuildScope(block) })
+
+ private fun deferredActionInternal(block: suspend FrpBuildScope.() -> Unit) {
+ deferAction { runInBuildScope(block) }
+ }
+
+ private fun <A> TFlow<A>.observeEffectInternal(
+ context: CoroutineContext,
+ block: suspend FrpEffectScope.(A) -> Unit,
+ ): Job {
+ val subRef = AtomicReference<Maybe<Output<A>>>(null)
+ val childScope = coroutineScope.childScope()
+ // When our scope is cancelled, deactivate this observer.
+ childScope.launchOnCancel(CoroutineName("TFlow.observeEffect")) {
+ subRef.getAndSet(None)?.let { output ->
+ if (output is Just) {
+ @Suppress("DeferredResultUnused")
+ network.transaction { scheduleDeactivation(output.value) }
+ }
+ }
+ }
+ // Defer so that we don't suspend the caller
+ deferAction {
+ val outputNode =
+ Output<A>(
+ context = context,
+ onDeath = { subRef.getAndSet(None)?.let { childScope.cancel() } },
+ onEmit = { output ->
+ if (subRef.get() is Just) {
+ // Not cancelled, safe to emit
+ val coroutine: suspend FrpEffectScope.() -> Unit = { block(output) }
+ val complete = CompletableDeferred<Unit>(parent = coroutineContext.job)
+ coroutine.startCoroutine(
+ object : FrpEffectScope, FrpTransactionScope by frpScope {
+ override val frpCoroutineScope: CoroutineScope = childScope
+ override val frpNetwork: FrpNetwork =
+ LocalFrpNetwork(network, childScope, endSignal)
+ },
+ completion =
+ object : Continuation<Unit> {
+ override val context: CoroutineContext
+ get() = EmptyCoroutineContext
+
+ override fun resumeWith(result: Result<Unit>) {
+ complete.completeWith(result)
+ }
+ },
+ )
+ complete.await()
+ }
+ },
+ )
+ with(frpScope) { this@observeEffectInternal.takeUntil(endSignal) }
+ .init
+ .connect(evalScope = stateScope.evalScope)
+ .activate(evalScope = stateScope.evalScope, outputNode.schedulable)
+ ?.let { (conn, needsEval) ->
+ outputNode.upstream = conn
+ if (!subRef.compareAndSet(null, just(outputNode))) {
+ // Job's already been cancelled, schedule deactivation
+ scheduleDeactivation(outputNode)
+ } else if (needsEval) {
+ outputNode.schedule(evalScope = stateScope.evalScope)
+ }
+ } ?: childScope.cancel()
+ }
+ return childScope.coroutineContext.job
+ }
+
+ private fun <A, B> TFlow<A>.mapBuildInternal(
+ transform: suspend FrpBuildScope.(A) -> B
+ ): TFlow<B> {
+ val childScope = coroutineScope.childScope()
+ return TFlowInit(
+ constInit(
+ "mapBuild",
+ mapImpl({ init.connect(evalScope = this) }) { spec ->
+ reenterBuildScope(outerScope = this@BuildScopeImpl, childScope)
+ .runInBuildScope {
+ val (result, _) = asyncScope { transform(spec) }
+ result.get()
+ }
+ }
+ .cached(),
+ )
+ )
+ }
+
+ private fun <K, A, B> TFlow<Map<K, Maybe<FrpSpec<A>>>>.applyLatestForKeyInternal(
+ init: FrpDeferredValue<Map<K, FrpSpec<B>>>,
+ numKeys: Int?,
+ ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>> {
+ val eventsByKey: GroupedTFlow<K, Maybe<FrpSpec<A>>> = groupByKey(numKeys)
+ val initOut: Deferred<Map<K, B>> = deferAsync {
+ init.unwrapped.await().mapValuesParallel { (k, spec) ->
+ val newEnd = with(frpScope) { eventsByKey[k].skipNext() }
+ val newScope = childBuildScope(newEnd)
+ newScope.runInBuildScope(spec)
+ }
+ }
+ val childScope = coroutineScope.childScope()
+ val changesNode: TFlowImpl<Map<K, Maybe<A>>> =
+ mapImpl(upstream = { this@applyLatestForKeyInternal.init.connect(evalScope = this) }) {
+ upstreamMap ->
+ reenterBuildScope(this@BuildScopeImpl, childScope).run {
+ upstreamMap.mapValuesParallel { (k: K, ma: Maybe<FrpSpec<A>>) ->
+ ma.map { spec ->
+ val newEnd = with(frpScope) { eventsByKey[k].skipNext() }
+ val newScope = childBuildScope(newEnd)
+ newScope.runInBuildScope(spec)
+ }
+ }
+ }
+ }
+ val changes: TFlow<Map<K, Maybe<A>>> =
+ TFlowInit(constInit("applyLatestForKey", changesNode.cached()))
+ // Ensure effects are observed; otherwise init will stay alive longer than expected
+ changes.observeEffectInternal(EmptyCoroutineContext) {}
+ return changes to FrpDeferredValue(initOut)
+ }
+
+ private fun newStopEmitter(): CoalescingMutableTFlow<Unit, Unit> =
+ CoalescingMutableTFlow(
+ coalesce = { _, _: Unit -> },
+ network = network,
+ getInitialValue = {},
+ )
+
+ private suspend fun childBuildScope(newEnd: TFlow<Any>): BuildScopeImpl {
+ val newCoroutineScope: CoroutineScope = coroutineScope.childScope()
+ return BuildScopeImpl(
+ stateScope = stateScope.childStateScope(newEnd),
+ coroutineScope = newCoroutineScope,
+ )
+ .apply {
+ // Ensure that once this transaction is done, the new child scope enters the
+ // completing state (kept alive so long as there are child jobs).
+ scheduleOutput(
+ OneShot {
+ // TODO: don't like this cast
+ (newCoroutineScope.coroutineContext.job as CompletableJob).complete()
+ }
+ )
+ runInBuildScope { endSignal.nextOnly().observe { newCoroutineScope.cancel() } }
+ }
+ }
+
+ private fun mutableChildBuildScope(): BuildScopeImpl {
+ val stopEmitter = newStopEmitter()
+ val childScope = coroutineScope.childScope()
+ childScope.coroutineContext.job.invokeOnCompletion { stopEmitter.emit(Unit) }
+ // Ensure that once this transaction is done, the new child scope enters the completing
+ // state (kept alive so long as there are child jobs).
+ scheduleOutput(
+ OneShot {
+ // TODO: don't like this cast
+ (childScope.coroutineContext.job as CompletableJob).complete()
+ }
+ )
+ return BuildScopeImpl(
+ stateScope = StateScopeImpl(evalScope = stateScope.evalScope, endSignal = stopEmitter),
+ coroutineScope = childScope,
+ )
+ }
+
+ private inner class FrpBuildScopeImpl : FrpBuildScope, FrpStateScope by stateScope.frpScope {
+
+ override fun <T> tFlow(builder: suspend FrpProducerScope<T>.() -> Unit): TFlow<T> =
+ tFlowInternal(builder)
+
+ override fun <In, Out> coalescingTFlow(
+ getInitialValue: () -> Out,
+ coalesce: (old: Out, new: In) -> Out,
+ builder: suspend FrpCoalescingProducerScope<In>.() -> Unit,
+ ): TFlow<Out> = coalescingTFlowInternal(getInitialValue, coalesce, builder)
+
+ override fun <A> asyncScope(block: FrpSpec<A>): Pair<FrpDeferredValue<A>, Job> =
+ asyncScopeInternal(block)
+
+ override fun <R> deferredBuildScope(
+ block: suspend FrpBuildScope.() -> R
+ ): FrpDeferredValue<R> = deferredInternal(block)
+
+ override fun deferredBuildScopeAction(block: suspend FrpBuildScope.() -> Unit) =
+ deferredActionInternal(block)
+
+ override fun <A> TFlow<A>.observe(
+ coroutineContext: CoroutineContext,
+ block: suspend FrpEffectScope.(A) -> Unit,
+ ): Job = observeEffectInternal(coroutineContext, block)
+
+ override fun <A, B> TFlow<A>.mapBuild(transform: suspend FrpBuildScope.(A) -> B): TFlow<B> =
+ mapBuildInternal(transform)
+
+ override fun <K, A, B> TFlow<Map<K, Maybe<FrpSpec<A>>>>.applyLatestSpecForKey(
+ initialSpecs: FrpDeferredValue<Map<K, FrpSpec<B>>>,
+ numKeys: Int?,
+ ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>> =
+ applyLatestForKeyInternal(initialSpecs, numKeys)
+ }
+}
+
+private fun EvalScope.reenterBuildScope(
+ outerScope: BuildScopeImpl,
+ coroutineScope: CoroutineScope,
+) =
+ BuildScopeImpl(
+ stateScope = StateScopeImpl(evalScope = this, endSignal = outerScope.endSignal),
+ coroutineScope,
+ )
diff --git a/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/DeferScope.kt b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/DeferScope.kt
new file mode 100644
index 0000000..f72ba5f
--- /dev/null
+++ b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/DeferScope.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.experimental.frp.internal
+
+import com.android.systemui.experimental.frp.internal.util.asyncImmediate
+import com.android.systemui.experimental.frp.internal.util.launchImmediate
+import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.isActive
+
+internal typealias DeferScope = CoroutineScope
+
+internal inline fun DeferScope.deferAction(
+ start: CoroutineStart = CoroutineStart.UNDISPATCHED,
+ crossinline block: suspend () -> Unit,
+): Job {
+ check(isActive) { "Cannot perform deferral, scope already closed." }
+ return launchImmediate(start, CoroutineName("deferAction")) { block() }
+}
+
+internal inline fun <R> DeferScope.deferAsync(
+ start: CoroutineStart = CoroutineStart.UNDISPATCHED,
+ crossinline block: suspend () -> R,
+): Deferred<R> {
+ check(isActive) { "Cannot perform deferral, scope already closed." }
+ return asyncImmediate(start, CoroutineName("deferAsync")) { block() }
+}
+
+internal suspend inline fun <A> deferScope(noinline block: suspend DeferScope.() -> A): A =
+ coroutineScope(block)
diff --git a/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/Demux.kt b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/Demux.kt
new file mode 100644
index 0000000..418220f
--- /dev/null
+++ b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/Demux.kt
@@ -0,0 +1,349 @@
+/*
+ * 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:Suppress("NOTHING_TO_INLINE")
+
+package com.android.systemui.experimental.frp.internal
+
+import com.android.systemui.experimental.frp.internal.util.hashString
+import com.android.systemui.experimental.frp.util.Just
+import com.android.systemui.experimental.frp.util.Maybe
+import com.android.systemui.experimental.frp.util.flatMap
+import com.android.systemui.experimental.frp.util.getMaybe
+import java.util.concurrent.ConcurrentHashMap
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.async
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+
+internal class DemuxNode<K, A>(
+ private val branchNodeByKey: ConcurrentHashMap<K, DemuxBranchNode<K, A>>,
+ val lifecycle: DemuxLifecycle<K, A>,
+ private val spec: DemuxActivator<K, A>,
+) : SchedulableNode {
+
+ val schedulable = Schedulable.N(this)
+
+ inline val mutex
+ get() = lifecycle.mutex
+
+ lateinit var upstreamConnection: NodeConnection<Map<K, A>>
+
+ fun getAndMaybeAddDownstream(key: K): DemuxBranchNode<K, A> =
+ branchNodeByKey.getOrPut(key) { DemuxBranchNode(key, this) }
+
+ override suspend fun schedule(evalScope: EvalScope) {
+ val upstreamResult = upstreamConnection.getPushEvent(evalScope)
+ if (upstreamResult is Just) {
+ coroutineScope {
+ val outerScope = this
+ mutex.withLock {
+ coroutineScope {
+ for ((key, _) in upstreamResult.value) {
+ launch {
+ branchNodeByKey[key]?.let { branch ->
+ outerScope.launch { branch.schedule(evalScope) }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ override suspend fun adjustDirectUpstream(scheduler: Scheduler, oldDepth: Int, newDepth: Int) {
+ coroutineScope {
+ mutex.withLock {
+ for ((_, branchNode) in branchNodeByKey) {
+ branchNode.downstreamSet.adjustDirectUpstream(
+ coroutineScope = this,
+ scheduler,
+ oldDepth,
+ newDepth,
+ )
+ }
+ }
+ }
+ }
+
+ override suspend fun moveIndirectUpstreamToDirect(
+ scheduler: Scheduler,
+ oldIndirectDepth: Int,
+ oldIndirectSet: Set<MuxDeferredNode<*, *>>,
+ newDirectDepth: Int,
+ ) {
+ coroutineScope {
+ mutex.withLock {
+ for ((_, branchNode) in branchNodeByKey) {
+ branchNode.downstreamSet.moveIndirectUpstreamToDirect(
+ coroutineScope = this,
+ scheduler,
+ oldIndirectDepth,
+ oldIndirectSet,
+ newDirectDepth,
+ )
+ }
+ }
+ }
+ }
+
+ override suspend fun adjustIndirectUpstream(
+ scheduler: Scheduler,
+ oldDepth: Int,
+ newDepth: Int,
+ removals: Set<MuxDeferredNode<*, *>>,
+ additions: Set<MuxDeferredNode<*, *>>,
+ ) {
+ coroutineScope {
+ mutex.withLock {
+ for ((_, branchNode) in branchNodeByKey) {
+ branchNode.downstreamSet.adjustIndirectUpstream(
+ coroutineScope = this,
+ scheduler,
+ oldDepth,
+ newDepth,
+ removals,
+ additions,
+ )
+ }
+ }
+ }
+ }
+
+ override suspend fun moveDirectUpstreamToIndirect(
+ scheduler: Scheduler,
+ oldDirectDepth: Int,
+ newIndirectDepth: Int,
+ newIndirectSet: Set<MuxDeferredNode<*, *>>,
+ ) {
+ coroutineScope {
+ mutex.withLock {
+ for ((_, branchNode) in branchNodeByKey) {
+ branchNode.downstreamSet.moveDirectUpstreamToIndirect(
+ coroutineScope = this,
+ scheduler,
+ oldDirectDepth,
+ newIndirectDepth,
+ newIndirectSet,
+ )
+ }
+ }
+ }
+ }
+
+ override suspend fun removeIndirectUpstream(
+ scheduler: Scheduler,
+ depth: Int,
+ indirectSet: Set<MuxDeferredNode<*, *>>,
+ ) {
+ coroutineScope {
+ mutex.withLock {
+ lifecycle.lifecycleState = DemuxLifecycleState.Dead
+ for ((_, branchNode) in branchNodeByKey) {
+ branchNode.downstreamSet.removeIndirectUpstream(
+ coroutineScope = this,
+ scheduler,
+ depth,
+ indirectSet,
+ )
+ }
+ }
+ }
+ }
+
+ override suspend fun removeDirectUpstream(scheduler: Scheduler, depth: Int) {
+ coroutineScope {
+ mutex.withLock {
+ lifecycle.lifecycleState = DemuxLifecycleState.Dead
+ for ((_, branchNode) in branchNodeByKey) {
+ branchNode.downstreamSet.removeDirectUpstream(
+ coroutineScope = this,
+ scheduler,
+ depth,
+ )
+ }
+ }
+ }
+ }
+
+ suspend fun removeDownstreamAndDeactivateIfNeeded(key: K) {
+ val deactivate =
+ mutex.withLock {
+ branchNodeByKey.remove(key)
+ branchNodeByKey.isEmpty()
+ }
+ if (deactivate) {
+ // No need for mutex here; no more concurrent changes to can occur during this phase
+ lifecycle.lifecycleState = DemuxLifecycleState.Inactive(spec)
+ upstreamConnection.removeDownstreamAndDeactivateIfNeeded(downstream = schedulable)
+ }
+ }
+}
+
+internal class DemuxBranchNode<K, A>(val key: K, private val demuxNode: DemuxNode<K, A>) :
+ PushNode<A> {
+
+ private val mutex = Mutex()
+
+ val downstreamSet = DownstreamSet()
+
+ override val depthTracker: DepthTracker
+ get() = demuxNode.upstreamConnection.depthTracker
+
+ override suspend fun hasCurrentValue(transactionStore: TransactionStore): Boolean =
+ demuxNode.upstreamConnection.hasCurrentValue(transactionStore)
+
+ override suspend fun getPushEvent(evalScope: EvalScope): Maybe<A> =
+ demuxNode.upstreamConnection.getPushEvent(evalScope).flatMap { it.getMaybe(key) }
+
+ override suspend fun addDownstream(downstream: Schedulable) {
+ mutex.withLock { downstreamSet.add(downstream) }
+ }
+
+ override suspend fun removeDownstream(downstream: Schedulable) {
+ mutex.withLock { downstreamSet.remove(downstream) }
+ }
+
+ override suspend fun removeDownstreamAndDeactivateIfNeeded(downstream: Schedulable) {
+ val canDeactivate =
+ mutex.withLock {
+ downstreamSet.remove(downstream)
+ downstreamSet.isEmpty()
+ }
+ if (canDeactivate) {
+ demuxNode.removeDownstreamAndDeactivateIfNeeded(key)
+ }
+ }
+
+ override suspend fun deactivateIfNeeded() {
+ if (mutex.withLock { downstreamSet.isEmpty() }) {
+ demuxNode.removeDownstreamAndDeactivateIfNeeded(key)
+ }
+ }
+
+ override suspend fun scheduleDeactivationIfNeeded(evalScope: EvalScope) {
+ if (mutex.withLock { downstreamSet.isEmpty() }) {
+ evalScope.scheduleDeactivation(this)
+ }
+ }
+
+ suspend fun schedule(evalScope: EvalScope) {
+ if (!coroutineScope { mutex.withLock { scheduleAll(downstreamSet, evalScope) } }) {
+ evalScope.scheduleDeactivation(this)
+ }
+ }
+}
+
+internal fun <K, A> DemuxImpl(
+ upstream: suspend EvalScope.() -> TFlowImpl<Map<K, A>>,
+ numKeys: Int?,
+): DemuxImpl<K, A> =
+ DemuxImpl(
+ DemuxLifecycle(
+ object : DemuxActivator<K, A> {
+ override suspend fun activate(
+ evalScope: EvalScope,
+ lifecycle: DemuxLifecycle<K, A>,
+ ): Pair<DemuxNode<K, A>, Boolean>? {
+ val dmux = DemuxNode(ConcurrentHashMap(numKeys ?: 16), lifecycle, this)
+ return upstream
+ .invoke(evalScope)
+ .activate(evalScope, downstream = dmux.schedulable)
+ ?.let { (conn, needsEval) ->
+ dmux.apply { upstreamConnection = conn } to needsEval
+ }
+ }
+ }
+ )
+ )
+
+internal class DemuxImpl<in K, out A>(private val dmux: DemuxLifecycle<K, A>) {
+ fun eventsForKey(key: K): TFlowImpl<A> = TFlowCheap { downstream ->
+ dmux.activate(evalScope = this, key)?.let { (branchNode, needsEval) ->
+ branchNode.addDownstream(downstream)
+ val branchNeedsEval = needsEval && branchNode.getPushEvent(evalScope = this) is Just
+ ActivationResult(
+ connection = NodeConnection(branchNode, branchNode),
+ needsEval = branchNeedsEval,
+ )
+ }
+ }
+}
+
+internal class DemuxLifecycle<K, A>(@Volatile var lifecycleState: DemuxLifecycleState<K, A>) {
+ val mutex = Mutex()
+
+ override fun toString(): String = "TFlowDmuxState[$hashString][$lifecycleState][$mutex]"
+
+ suspend fun activate(evalScope: EvalScope, key: K): Pair<DemuxBranchNode<K, A>, Boolean>? =
+ coroutineScope {
+ mutex
+ .withLock {
+ when (val state = lifecycleState) {
+ is DemuxLifecycleState.Dead -> null
+ is DemuxLifecycleState.Active ->
+ state.node.getAndMaybeAddDownstream(key) to
+ async {
+ state.node.upstreamConnection.hasCurrentValue(
+ evalScope.transactionStore
+ )
+ }
+ is DemuxLifecycleState.Inactive -> {
+ state.spec
+ .activate(evalScope, this@DemuxLifecycle)
+ .also { result ->
+ lifecycleState =
+ if (result == null) {
+ DemuxLifecycleState.Dead
+ } else {
+ DemuxLifecycleState.Active(result.first)
+ }
+ }
+ ?.let { (node, needsEval) ->
+ node.getAndMaybeAddDownstream(key) to
+ CompletableDeferred(needsEval)
+ }
+ }
+ }
+ }
+ ?.let { (branch, result) -> branch to result.await() }
+ }
+}
+
+internal sealed interface DemuxLifecycleState<out K, out A> {
+ class Inactive<K, A>(val spec: DemuxActivator<K, A>) : DemuxLifecycleState<K, A> {
+ override fun toString(): String = "Inactive"
+ }
+
+ class Active<K, A>(val node: DemuxNode<K, A>) : DemuxLifecycleState<K, A> {
+ override fun toString(): String = "Active(node=$node)"
+ }
+
+ data object Dead : DemuxLifecycleState<Nothing, Nothing>
+}
+
+internal interface DemuxActivator<K, A> {
+ suspend fun activate(
+ evalScope: EvalScope,
+ lifecycle: DemuxLifecycle<K, A>,
+ ): Pair<DemuxNode<K, A>, Boolean>?
+}
+
+internal inline fun <K, A> DemuxLifecycle(onSubscribe: DemuxActivator<K, A>) =
+ DemuxLifecycle(DemuxLifecycleState.Inactive(onSubscribe))
diff --git a/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/EvalScopeImpl.kt b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/EvalScopeImpl.kt
new file mode 100644
index 0000000..38bc22f
--- /dev/null
+++ b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/EvalScopeImpl.kt
@@ -0,0 +1,119 @@
+/*
+ * 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.experimental.frp.internal
+
+import com.android.systemui.experimental.frp.FrpDeferredValue
+import com.android.systemui.experimental.frp.FrpTransactionScope
+import com.android.systemui.experimental.frp.TFlow
+import com.android.systemui.experimental.frp.TFlowInit
+import com.android.systemui.experimental.frp.TFlowLoop
+import com.android.systemui.experimental.frp.TState
+import com.android.systemui.experimental.frp.TStateInit
+import com.android.systemui.experimental.frp.Transactional
+import com.android.systemui.experimental.frp.emptyTFlow
+import com.android.systemui.experimental.frp.init
+import com.android.systemui.experimental.frp.mapCheap
+import com.android.systemui.experimental.frp.switch
+import kotlin.coroutines.Continuation
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+import kotlin.coroutines.startCoroutine
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.completeWith
+import kotlinx.coroutines.job
+
+internal class EvalScopeImpl(networkScope: NetworkScope, deferScope: DeferScope) :
+ EvalScope, NetworkScope by networkScope, DeferScope by deferScope {
+
+ private suspend fun <A> Transactional<A>.sample(): A =
+ impl.sample().sample(this@EvalScopeImpl).await()
+
+ private suspend fun <A> TState<A>.sample(): A =
+ init.connect(evalScope = this@EvalScopeImpl).getCurrentWithEpoch(this@EvalScopeImpl).first
+
+ private val <A> Transactional<A>.deferredValue: FrpDeferredValue<A>
+ get() = FrpDeferredValue(deferAsync { sample() })
+
+ private val <A> TState<A>.deferredValue: FrpDeferredValue<A>
+ get() = FrpDeferredValue(deferAsync { sample() })
+
+ private val nowInternal: TFlow<Unit> by lazy {
+ var result by TFlowLoop<Unit>()
+ result =
+ TStateInit(
+ constInit(
+ "now",
+ mkState(
+ "now",
+ "now",
+ this,
+ { result.mapCheap { emptyTFlow }.init.connect(evalScope = this) },
+ CompletableDeferred(
+ TFlowInit(
+ constInit(
+ "now",
+ TFlowCheap {
+ ActivationResult(
+ connection = NodeConnection(AlwaysNode, AlwaysNode),
+ needsEval = true,
+ )
+ },
+ )
+ )
+ ),
+ ),
+ )
+ )
+ .switch()
+ result
+ }
+
+ private fun <R> deferredInternal(
+ block: suspend FrpTransactionScope.() -> R
+ ): FrpDeferredValue<R> = FrpDeferredValue(deferAsync { runInTransactionScope(block) })
+
+ override suspend fun <R> runInTransactionScope(block: suspend FrpTransactionScope.() -> R): R {
+ val complete = CompletableDeferred<R>(parent = coroutineContext.job)
+ block.startCoroutine(
+ frpScope,
+ object : Continuation<R> {
+ override val context: CoroutineContext
+ get() = EmptyCoroutineContext
+
+ override fun resumeWith(result: Result<R>) {
+ complete.completeWith(result)
+ }
+ },
+ )
+ return complete.await()
+ }
+
+ override val frpScope: FrpTransactionScope = FrpTransactionScopeImpl()
+
+ inner class FrpTransactionScopeImpl : FrpTransactionScope {
+ override fun <A> Transactional<A>.sampleDeferred(): FrpDeferredValue<A> = deferredValue
+
+ override fun <A> TState<A>.sampleDeferred(): FrpDeferredValue<A> = deferredValue
+
+ override fun <R> deferredTransactionScope(
+ block: suspend FrpTransactionScope.() -> R
+ ): FrpDeferredValue<R> = deferredInternal(block)
+
+ override val now: TFlow<Unit>
+ get() = nowInternal
+ }
+}
diff --git a/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/FilterNode.kt b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/FilterNode.kt
new file mode 100644
index 0000000..4f2a769
--- /dev/null
+++ b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/FilterNode.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.experimental.frp.internal
+
+import com.android.systemui.experimental.frp.util.Just
+import com.android.systemui.experimental.frp.util.Maybe
+import com.android.systemui.experimental.frp.util.just
+import com.android.systemui.experimental.frp.util.none
+
+internal inline fun <A, B> mapMaybeNode(
+ crossinline getPulse: suspend EvalScope.() -> TFlowImpl<A>,
+ crossinline f: suspend EvalScope.(A) -> Maybe<B>,
+): TFlowImpl<B> {
+ return DemuxImpl(
+ {
+ mapImpl(getPulse) {
+ val maybeResult = f(it)
+ if (maybeResult is Just) {
+ mapOf(Unit to maybeResult.value)
+ } else {
+ emptyMap()
+ }
+ }
+ },
+ numKeys = 1,
+ )
+ .eventsForKey(Unit)
+}
+
+internal inline fun <A> filterNode(
+ crossinline getPulse: suspend EvalScope.() -> TFlowImpl<A>,
+ crossinline f: suspend EvalScope.(A) -> Boolean,
+): TFlowImpl<A> = mapMaybeNode(getPulse) { if (f(it)) just(it) else none }
diff --git a/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/Graph.kt b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/Graph.kt
new file mode 100644
index 0000000..9425870
--- /dev/null
+++ b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/Graph.kt
@@ -0,0 +1,530 @@
+/*
+ * 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.experimental.frp.internal
+
+import com.android.systemui.experimental.frp.internal.util.Bag
+import java.util.TreeMap
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+/**
+ * Tracks all upstream connections for Mux nodes.
+ *
+ * Connections come in two flavors:
+ * 1. **DIRECT** :: The upstream node may emit events that would cause the owner of this depth
+ * tracker to also emit.
+ * 2. **INDIRECT** :: The upstream node will not emit events, but may start doing so in a future
+ * transaction (at which point its depth will change to DIRECT).
+ *
+ * DIRECT connections are the standard, active connections that propagate events through the graph.
+ * They are used to calculate the evaluation depth of a node, so that it is only visited once it is
+ * certain that all DIRECT upstream connections have already been visited (or are not emitting in
+ * the current transaction).
+ *
+ * It is *invalid* for a node to be directly upstream of itself. Doing so is an error.
+ *
+ * INDIRECT connections identify nodes that are still "alive" (should not be garbage-collected) but
+ * are presently "dormant". This only occurs when a MuxDeferredNode has nothing switched-in, but is
+ * still connected to its "patches" upstream node, implying that something *may* be switched-in at a
+ * later time.
+ *
+ * It is *invalid* for a node to be indirectly upstream of itself. These connections are
+ * automatically filtered out.
+ *
+ * When there are no connections, either DIRECT or INDIRECT, a node *dies* and all incoming/outgoing
+ * connections are freed so that it can be garbage-collected.
+ *
+ * Note that there is an edge case where a MuxDeferredNode is connected to itself via its "patches"
+ * upstream node. In this case:
+ * 1. If the node has switched-in upstream nodes, then this is perfectly valid. Downstream nodes
+ * will see a direct connection to this MuxDeferredNode.
+ * 2. Otherwise, the node would normally be considered "dormant" and downstream nodes would see an
+ * indirect connection. However, because a node cannot be indirectly upstream of itself, then the
+ * MuxDeferredNode sees no connection via its patches upstream node, and so is considered "dead".
+ * Conceptually, this makes some sense: The only way for this recursive MuxDeferredNode to become
+ * non-dormant is to switch some upstream nodes back in, but since the patches node is itself,
+ * this will never happen.
+ *
+ * This behavior underpins the recursive definition of `nextOnly`.
+ */
+internal class DepthTracker {
+
+ @Volatile var snapshotIsDirect = true
+ @Volatile private var snapshotIsIndirectRoot = false
+
+ private inline val snapshotIsIndirect: Boolean
+ get() = !snapshotIsDirect
+
+ @Volatile var snapshotIndirectDepth: Int = 0
+ @Volatile var snapshotDirectDepth: Int = 0
+
+ private val _snapshotIndirectRoots = HashSet<MuxDeferredNode<*, *>>()
+ val snapshotIndirectRoots
+ get() = _snapshotIndirectRoots.toSet()
+
+ private val indirectAdditions = HashSet<MuxDeferredNode<*, *>>()
+ private val indirectRemovals = HashSet<MuxDeferredNode<*, *>>()
+ private val dirty_directUpstreamDepths = TreeMap<Int, Int>()
+ private val dirty_indirectUpstreamDepths = TreeMap<Int, Int>()
+ private val dirty_indirectUpstreamRoots = Bag<MuxDeferredNode<*, *>>()
+ @Volatile var dirty_directDepth = 0
+ @Volatile private var dirty_indirectDepth = 0
+ @Volatile private var dirty_depthIsDirect = true
+ @Volatile private var dirty_isIndirectRoot = false
+
+ suspend fun schedule(scheduler: Scheduler, node: MuxNode<*, *, *>) {
+ if (dirty_depthIsDirect) {
+ scheduler.schedule(dirty_directDepth, node)
+ } else {
+ scheduler.scheduleIndirect(dirty_indirectDepth, node)
+ }
+ }
+
+ // only used by MuxDeferred
+ // and only when there is a direct connection to the patch node
+ fun setIsIndirectRoot(isRoot: Boolean): Boolean {
+ if (isRoot != dirty_isIndirectRoot) {
+ dirty_isIndirectRoot = isRoot
+ return !dirty_depthIsDirect
+ }
+ return false
+ }
+
+ // adds an upstream connection, and recalcs depth
+ // returns true if depth has changed
+ fun addDirectUpstream(oldDepth: Int?, newDepth: Int): Boolean {
+ if (oldDepth != null) {
+ dirty_directUpstreamDepths.compute(oldDepth) { _, count ->
+ count?.minus(1)?.takeIf { it > 0 }
+ }
+ }
+ dirty_directUpstreamDepths.compute(newDepth) { _, current -> current?.plus(1) ?: 1 }
+ return recalcDepth()
+ }
+
+ private fun recalcDepth(): Boolean {
+ val newDepth =
+ dirty_directUpstreamDepths.lastEntry()?.let { (maxDepth, _) -> maxDepth + 1 } ?: 0
+
+ val isDirect = dirty_directUpstreamDepths.isNotEmpty()
+ val isDirectChanged = dirty_depthIsDirect != isDirect
+ dirty_depthIsDirect = isDirect
+
+ return (newDepth != dirty_directDepth).also { dirty_directDepth = newDepth } or
+ isDirectChanged
+ }
+
+ private fun recalcIndirDepth(): Boolean {
+ val newDepth =
+ dirty_indirectUpstreamDepths.lastEntry()?.let { (maxDepth, _) -> maxDepth + 1 } ?: 0
+ return (!dirty_depthIsDirect && !dirty_isIndirectRoot && newDepth != dirty_indirectDepth)
+ .also { dirty_indirectDepth = newDepth }
+ }
+
+ fun removeDirectUpstream(depth: Int): Boolean {
+ dirty_directUpstreamDepths.compute(depth) { _, count -> count?.minus(1)?.takeIf { it > 0 } }
+ return recalcDepth()
+ }
+
+ fun addIndirectUpstream(oldDepth: Int?, newDepth: Int): Boolean =
+ if (oldDepth == newDepth) {
+ false
+ } else {
+ if (oldDepth != null) {
+ dirty_indirectUpstreamDepths.compute(oldDepth) { _, current ->
+ current?.minus(1)?.takeIf { it > 0 }
+ }
+ }
+ dirty_indirectUpstreamDepths.compute(newDepth) { _, current -> current?.plus(1) ?: 1 }
+ recalcIndirDepth()
+ }
+
+ fun removeIndirectUpstream(depth: Int): Boolean {
+ dirty_indirectUpstreamDepths.compute(depth) { _, current ->
+ current?.minus(1)?.takeIf { it > 0 }
+ }
+ return recalcIndirDepth()
+ }
+
+ fun updateIndirectRoots(
+ additions: Set<MuxDeferredNode<*, *>>? = null,
+ removals: Set<MuxDeferredNode<*, *>>? = null,
+ butNot: MuxDeferredNode<*, *>? = null,
+ ): Boolean {
+ val addsChanged =
+ additions
+ ?.let { dirty_indirectUpstreamRoots.addAll(additions, butNot) }
+ ?.let {
+ indirectAdditions.addAll(indirectRemovals.applyRemovalDiff(it))
+ true
+ } ?: false
+ val removalsChanged =
+ removals
+ ?.let { dirty_indirectUpstreamRoots.removeAll(removals) }
+ ?.let {
+ indirectRemovals.addAll(indirectAdditions.applyRemovalDiff(it))
+ true
+ } ?: false
+ return (!dirty_depthIsDirect && (addsChanged || removalsChanged))
+ }
+
+ private fun <T> HashSet<T>.applyRemovalDiff(changeSet: Set<T>): Set<T> {
+ val remainder = HashSet<T>()
+ for (element in changeSet) {
+ if (!add(element)) {
+ remainder.add(element)
+ }
+ }
+ return remainder
+ }
+
+ suspend fun propagateChanges(scheduler: Scheduler, muxNode: MuxNode<*, *, *>) {
+ if (isDirty()) {
+ schedule(scheduler, muxNode)
+ }
+ }
+
+ fun applyChanges(
+ coroutineScope: CoroutineScope,
+ scheduler: Scheduler,
+ downstreamSet: DownstreamSet,
+ muxNode: MuxNode<*, *, *>,
+ ) {
+ when {
+ dirty_depthIsDirect -> {
+ if (snapshotIsDirect) {
+ downstreamSet.adjustDirectUpstream(
+ coroutineScope,
+ scheduler,
+ oldDepth = snapshotDirectDepth,
+ newDepth = dirty_directDepth,
+ )
+ } else {
+ downstreamSet.moveIndirectUpstreamToDirect(
+ coroutineScope,
+ scheduler,
+ oldIndirectDepth = snapshotIndirectDepth,
+ oldIndirectSet =
+ buildSet {
+ addAll(snapshotIndirectRoots)
+ if (snapshotIsIndirectRoot) {
+ add(muxNode as MuxDeferredNode<*, *>)
+ }
+ },
+ newDirectDepth = dirty_directDepth,
+ )
+ }
+ }
+
+ dirty_hasIndirectUpstream() || dirty_isIndirectRoot -> {
+ if (snapshotIsDirect) {
+ downstreamSet.moveDirectUpstreamToIndirect(
+ coroutineScope,
+ scheduler,
+ oldDirectDepth = snapshotDirectDepth,
+ newIndirectDepth = dirty_indirectDepth,
+ newIndirectSet =
+ buildSet {
+ addAll(dirty_indirectUpstreamRoots)
+ if (dirty_isIndirectRoot) {
+ add(muxNode as MuxDeferredNode<*, *>)
+ }
+ },
+ )
+ } else {
+ downstreamSet.adjustIndirectUpstream(
+ coroutineScope,
+ scheduler,
+ oldDepth = snapshotIndirectDepth,
+ newDepth = dirty_indirectDepth,
+ removals =
+ buildSet {
+ addAll(indirectRemovals)
+ if (snapshotIsIndirectRoot && !dirty_isIndirectRoot) {
+ add(muxNode as MuxDeferredNode<*, *>)
+ }
+ },
+ additions =
+ buildSet {
+ addAll(indirectAdditions)
+ if (!snapshotIsIndirectRoot && dirty_isIndirectRoot) {
+ add(muxNode as MuxDeferredNode<*, *>)
+ }
+ },
+ )
+ }
+ }
+
+ else -> {
+ // die
+ muxNode.lifecycle.lifecycleState = MuxLifecycleState.Dead
+
+ if (snapshotIsDirect) {
+ downstreamSet.removeDirectUpstream(
+ coroutineScope,
+ scheduler,
+ depth = snapshotDirectDepth,
+ )
+ } else {
+ downstreamSet.removeIndirectUpstream(
+ coroutineScope,
+ scheduler,
+ depth = snapshotIndirectDepth,
+ indirectSet =
+ buildSet {
+ addAll(snapshotIndirectRoots)
+ if (snapshotIsIndirectRoot) {
+ add(muxNode as MuxDeferredNode<*, *>)
+ }
+ },
+ )
+ }
+ downstreamSet.clear()
+ }
+ }
+ reset()
+ }
+
+ fun dirty_hasDirectUpstream(): Boolean = dirty_directUpstreamDepths.isNotEmpty()
+
+ private fun dirty_hasIndirectUpstream(): Boolean = dirty_indirectUpstreamRoots.isNotEmpty()
+
+ override fun toString(): String =
+ "DepthTracker(" +
+ "sIsDirect=$snapshotIsDirect, " +
+ "sDirectDepth=$snapshotDirectDepth, " +
+ "sIndirectDepth=$snapshotIndirectDepth, " +
+ "sIndirectRoots=$snapshotIndirectRoots, " +
+ "dIsIndirectRoot=$dirty_isIndirectRoot, " +
+ "dDirectDepths=$dirty_directUpstreamDepths, " +
+ "dIndirectDepths=$dirty_indirectUpstreamDepths, " +
+ "dIndirectRoots=$dirty_indirectUpstreamRoots" +
+ ")"
+
+ fun reset() {
+ snapshotIsDirect = dirty_hasDirectUpstream()
+ snapshotDirectDepth = dirty_directDepth
+ snapshotIndirectDepth = dirty_indirectDepth
+ snapshotIsIndirectRoot = dirty_isIndirectRoot
+ if (indirectAdditions.isNotEmpty() || indirectRemovals.isNotEmpty()) {
+ _snapshotIndirectRoots.clear()
+ _snapshotIndirectRoots.addAll(dirty_indirectUpstreamRoots)
+ }
+ indirectAdditions.clear()
+ indirectRemovals.clear()
+ // check(!isDirty()) { "should not be dirty after a reset" }
+ }
+
+ fun isDirty(): Boolean =
+ when {
+ snapshotIsDirect -> !dirty_depthIsDirect || snapshotDirectDepth != dirty_directDepth
+ snapshotIsIndirectRoot -> dirty_depthIsDirect || !dirty_isIndirectRoot
+ else ->
+ dirty_depthIsDirect ||
+ dirty_isIndirectRoot ||
+ snapshotIndirectDepth != dirty_indirectDepth ||
+ indirectAdditions.isNotEmpty() ||
+ indirectRemovals.isNotEmpty()
+ }
+
+ fun dirty_depthIncreased(): Boolean =
+ snapshotDirectDepth < dirty_directDepth || snapshotIsIndirect && dirty_hasDirectUpstream()
+}
+
+/**
+ * Tracks downstream nodes to be scheduled when the owner of this DownstreamSet produces a value in
+ * a transaction.
+ */
+internal class DownstreamSet {
+
+ val outputs = HashSet<Output<*>>()
+ val stateWriters = mutableListOf<TStateSource<*>>()
+ val muxMovers = HashSet<MuxDeferredNode<*, *>>()
+ val nodes = HashSet<SchedulableNode>()
+
+ fun add(schedulable: Schedulable) {
+ when (schedulable) {
+ is Schedulable.S -> stateWriters.add(schedulable.state)
+ is Schedulable.M -> muxMovers.add(schedulable.muxMover)
+ is Schedulable.N -> nodes.add(schedulable.node)
+ is Schedulable.O -> outputs.add(schedulable.output)
+ }
+ }
+
+ fun remove(schedulable: Schedulable) {
+ when (schedulable) {
+ is Schedulable.S -> error("WTF: latches are never removed")
+ is Schedulable.M -> muxMovers.remove(schedulable.muxMover)
+ is Schedulable.N -> nodes.remove(schedulable.node)
+ is Schedulable.O -> outputs.remove(schedulable.output)
+ }
+ }
+
+ fun adjustDirectUpstream(
+ coroutineScope: CoroutineScope,
+ scheduler: Scheduler,
+ oldDepth: Int,
+ newDepth: Int,
+ ) =
+ coroutineScope.run {
+ for (node in nodes) {
+ launch { node.adjustDirectUpstream(scheduler, oldDepth, newDepth) }
+ }
+ }
+
+ fun moveIndirectUpstreamToDirect(
+ coroutineScope: CoroutineScope,
+ scheduler: Scheduler,
+ oldIndirectDepth: Int,
+ oldIndirectSet: Set<MuxDeferredNode<*, *>>,
+ newDirectDepth: Int,
+ ) =
+ coroutineScope.run {
+ for (node in nodes) {
+ launch {
+ node.moveIndirectUpstreamToDirect(
+ scheduler,
+ oldIndirectDepth,
+ oldIndirectSet,
+ newDirectDepth,
+ )
+ }
+ }
+ for (mover in muxMovers) {
+ launch {
+ mover.moveIndirectPatchNodeToDirect(scheduler, oldIndirectDepth, oldIndirectSet)
+ }
+ }
+ }
+
+ fun adjustIndirectUpstream(
+ coroutineScope: CoroutineScope,
+ scheduler: Scheduler,
+ oldDepth: Int,
+ newDepth: Int,
+ removals: Set<MuxDeferredNode<*, *>>,
+ additions: Set<MuxDeferredNode<*, *>>,
+ ) =
+ coroutineScope.run {
+ for (node in nodes) {
+ launch {
+ node.adjustIndirectUpstream(scheduler, oldDepth, newDepth, removals, additions)
+ }
+ }
+ for (mover in muxMovers) {
+ launch {
+ mover.adjustIndirectPatchNode(
+ scheduler,
+ oldDepth,
+ newDepth,
+ removals,
+ additions,
+ )
+ }
+ }
+ }
+
+ fun moveDirectUpstreamToIndirect(
+ coroutineScope: CoroutineScope,
+ scheduler: Scheduler,
+ oldDirectDepth: Int,
+ newIndirectDepth: Int,
+ newIndirectSet: Set<MuxDeferredNode<*, *>>,
+ ) =
+ coroutineScope.run {
+ for (node in nodes) {
+ launch {
+ node.moveDirectUpstreamToIndirect(
+ scheduler,
+ oldDirectDepth,
+ newIndirectDepth,
+ newIndirectSet,
+ )
+ }
+ }
+ for (mover in muxMovers) {
+ launch {
+ mover.moveDirectPatchNodeToIndirect(scheduler, newIndirectDepth, newIndirectSet)
+ }
+ }
+ }
+
+ fun removeIndirectUpstream(
+ coroutineScope: CoroutineScope,
+ scheduler: Scheduler,
+ depth: Int,
+ indirectSet: Set<MuxDeferredNode<*, *>>,
+ ) =
+ coroutineScope.run {
+ for (node in nodes) {
+ launch { node.removeIndirectUpstream(scheduler, depth, indirectSet) }
+ }
+ for (mover in muxMovers) {
+ launch { mover.removeIndirectPatchNode(scheduler, depth, indirectSet) }
+ }
+ for (output in outputs) {
+ launch { output.kill() }
+ }
+ }
+
+ fun removeDirectUpstream(coroutineScope: CoroutineScope, scheduler: Scheduler, depth: Int) =
+ coroutineScope.run {
+ for (node in nodes) {
+ launch { node.removeDirectUpstream(scheduler, depth) }
+ }
+ for (mover in muxMovers) {
+ launch { mover.removeDirectPatchNode(scheduler) }
+ }
+ for (output in outputs) {
+ launch { output.kill() }
+ }
+ }
+
+ fun clear() {
+ outputs.clear()
+ stateWriters.clear()
+ muxMovers.clear()
+ nodes.clear()
+ }
+}
+
+// TODO: remove this indirection
+internal sealed interface Schedulable {
+ data class S constructor(val state: TStateSource<*>) : Schedulable
+
+ data class M constructor(val muxMover: MuxDeferredNode<*, *>) : Schedulable
+
+ data class N constructor(val node: SchedulableNode) : Schedulable
+
+ data class O constructor(val output: Output<*>) : Schedulable
+}
+
+internal fun DownstreamSet.isEmpty() =
+ nodes.isEmpty() && outputs.isEmpty() && muxMovers.isEmpty() && stateWriters.isEmpty()
+
+@Suppress("NOTHING_TO_INLINE") internal inline fun DownstreamSet.isNotEmpty() = !isEmpty()
+
+internal fun CoroutineScope.scheduleAll(
+ downstreamSet: DownstreamSet,
+ evalScope: EvalScope,
+): Boolean {
+ downstreamSet.nodes.forEach { launch { it.schedule(evalScope) } }
+ downstreamSet.muxMovers.forEach { launch { it.scheduleMover(evalScope) } }
+ downstreamSet.outputs.forEach { launch { it.schedule(evalScope) } }
+ downstreamSet.stateWriters.forEach { evalScope.schedule(it) }
+ return downstreamSet.isNotEmpty()
+}
diff --git a/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/Init.kt b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/Init.kt
new file mode 100644
index 0000000..efb7a09
--- /dev/null
+++ b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/Init.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.experimental.frp.internal
+
+import com.android.systemui.experimental.frp.util.Maybe
+import com.android.systemui.experimental.frp.util.just
+import com.android.systemui.experimental.frp.util.none
+import java.util.concurrent.atomic.AtomicBoolean
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+/** Performs actions once, when the reactive component is first connected to the network. */
+internal class Init<out A>(val name: String?, private val block: suspend InitScope.() -> A) {
+
+ /** Has the initialization logic been evaluated yet? */
+ private val initialized = AtomicBoolean()
+
+ /**
+ * Stores the result after initialization, as well as the id of the [Network] it's been
+ * initialized with.
+ */
+ private val cache = CompletableDeferred<Pair<Any, A>>()
+
+ suspend fun connect(evalScope: InitScope): A =
+ if (initialized.getAndSet(true)) {
+ // Read from cache
+ val (networkId, result) = cache.await()
+ check(networkId == evalScope.networkId) { "Network mismatch" }
+ result
+ } else {
+ // Write to cache
+ block(evalScope).also { cache.complete(evalScope.networkId to it) }
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ fun getUnsafe(): Maybe<A> =
+ if (cache.isCompleted) {
+ just(cache.getCompleted().second)
+ } else {
+ none
+ }
+}
+
+internal fun <A> init(name: String?, block: suspend InitScope.() -> A) = Init(name, block)
+
+internal fun <A> constInit(name: String?, value: A) = init(name) { value }
diff --git a/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/Inputs.kt b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/Inputs.kt
new file mode 100644
index 0000000..85c87fe
--- /dev/null
+++ b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/Inputs.kt
@@ -0,0 +1,120 @@
+/*
+ * 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.experimental.frp.internal
+
+import com.android.systemui.experimental.frp.internal.util.Key
+import com.android.systemui.experimental.frp.util.Maybe
+import com.android.systemui.experimental.frp.util.just
+import java.util.concurrent.atomic.AtomicBoolean
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+
+internal class InputNode<A>(
+ private val activate: suspend EvalScope.() -> Unit = {},
+ private val deactivate: () -> Unit = {},
+) : PushNode<A>, Key<A> {
+
+ internal val downstreamSet = DownstreamSet()
+ private val mutex = Mutex()
+ private val activated = AtomicBoolean(false)
+
+ override val depthTracker: DepthTracker = DepthTracker()
+
+ override suspend fun hasCurrentValue(transactionStore: TransactionStore): Boolean =
+ transactionStore.contains(this)
+
+ suspend fun visit(evalScope: EvalScope, value: A) {
+ evalScope.setResult(this, value)
+ coroutineScope {
+ if (!mutex.withLock { scheduleAll(downstreamSet, evalScope) }) {
+ evalScope.scheduleDeactivation(this@InputNode)
+ }
+ }
+ }
+
+ override suspend fun removeDownstream(downstream: Schedulable) {
+ mutex.withLock { downstreamSet.remove(downstream) }
+ }
+
+ override suspend fun deactivateIfNeeded() {
+ if (mutex.withLock { downstreamSet.isEmpty() && activated.getAndSet(false) }) {
+ deactivate()
+ }
+ }
+
+ override suspend fun scheduleDeactivationIfNeeded(evalScope: EvalScope) {
+ if (mutex.withLock { downstreamSet.isEmpty() }) {
+ evalScope.scheduleDeactivation(this)
+ }
+ }
+
+ override suspend fun addDownstream(downstream: Schedulable) {
+ mutex.withLock { downstreamSet.add(downstream) }
+ }
+
+ suspend fun addDownstreamAndActivateIfNeeded(downstream: Schedulable, evalScope: EvalScope) {
+ val needsActivation =
+ mutex.withLock {
+ val wasEmpty = downstreamSet.isEmpty()
+ downstreamSet.add(downstream)
+ wasEmpty && !activated.getAndSet(true)
+ }
+ if (needsActivation) {
+ activate(evalScope)
+ }
+ }
+
+ override suspend fun removeDownstreamAndDeactivateIfNeeded(downstream: Schedulable) {
+ val needsDeactivation =
+ mutex.withLock {
+ downstreamSet.remove(downstream)
+ downstreamSet.isEmpty() && activated.getAndSet(false)
+ }
+ if (needsDeactivation) {
+ deactivate()
+ }
+ }
+
+ override suspend fun getPushEvent(evalScope: EvalScope): Maybe<A> =
+ evalScope.getCurrentValue(this)
+}
+
+internal fun <A> InputNode<A>.activated() = TFlowCheap { downstream ->
+ val input = this@activated
+ addDownstreamAndActivateIfNeeded(downstream, evalScope = this)
+ ActivationResult(connection = NodeConnection(input, input), needsEval = hasCurrentValue(input))
+}
+
+internal data object AlwaysNode : PushNode<Unit> {
+
+ override val depthTracker = DepthTracker()
+
+ override suspend fun hasCurrentValue(transactionStore: TransactionStore): Boolean = true
+
+ override suspend fun removeDownstream(downstream: Schedulable) {}
+
+ override suspend fun deactivateIfNeeded() {}
+
+ override suspend fun scheduleDeactivationIfNeeded(evalScope: EvalScope) {}
+
+ override suspend fun addDownstream(downstream: Schedulable) {}
+
+ override suspend fun removeDownstreamAndDeactivateIfNeeded(downstream: Schedulable) {}
+
+ override suspend fun getPushEvent(evalScope: EvalScope): Maybe<Unit> = just(Unit)
+}
diff --git a/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/InternalScopes.kt b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/InternalScopes.kt
new file mode 100644
index 0000000..b6cd906
--- /dev/null
+++ b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/InternalScopes.kt
@@ -0,0 +1,82 @@
+/*
+ * 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.experimental.frp.internal
+
+import com.android.systemui.experimental.frp.FrpBuildScope
+import com.android.systemui.experimental.frp.FrpStateScope
+import com.android.systemui.experimental.frp.FrpTransactionScope
+import com.android.systemui.experimental.frp.TFlow
+import com.android.systemui.experimental.frp.internal.util.HeteroMap
+import com.android.systemui.experimental.frp.internal.util.Key
+import com.android.systemui.experimental.frp.util.Maybe
+
+internal interface InitScope {
+ val networkId: Any
+}
+
+internal interface EvalScope : NetworkScope, DeferScope {
+ val frpScope: FrpTransactionScope
+
+ suspend fun <R> runInTransactionScope(block: suspend FrpTransactionScope.() -> R): R
+}
+
+internal interface StateScope : EvalScope {
+ override val frpScope: FrpStateScope
+
+ suspend fun <R> runInStateScope(block: suspend FrpStateScope.() -> R): R
+
+ val endSignal: TFlow<Any>
+
+ fun childStateScope(newEnd: TFlow<Any>): StateScope
+}
+
+internal interface BuildScope : StateScope {
+ override val frpScope: FrpBuildScope
+
+ suspend fun <R> runInBuildScope(block: suspend FrpBuildScope.() -> R): R
+}
+
+internal interface NetworkScope : InitScope {
+
+ val epoch: Long
+ val network: Network
+
+ val compactor: Scheduler
+ val scheduler: Scheduler
+
+ val transactionStore: HeteroMap
+
+ fun scheduleOutput(output: Output<*>)
+
+ fun scheduleMuxMover(muxMover: MuxDeferredNode<*, *>)
+
+ fun schedule(state: TStateSource<*>)
+
+ suspend fun schedule(node: MuxNode<*, *, *>)
+
+ fun scheduleDeactivation(node: PushNode<*>)
+
+ fun scheduleDeactivation(output: Output<*>)
+}
+
+internal fun <A> NetworkScope.setResult(node: Key<A>, result: A) {
+ transactionStore[node] = result
+}
+
+internal fun <A> NetworkScope.getCurrentValue(key: Key<A>): Maybe<A> = transactionStore[key]
+
+internal fun NetworkScope.hasCurrentValue(key: Key<*>): Boolean = transactionStore.contains(key)
diff --git a/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/Mux.kt b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/Mux.kt
new file mode 100644
index 0000000..e616d62
--- /dev/null
+++ b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/Mux.kt
@@ -0,0 +1,326 @@
+/*
+ * 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:Suppress("NOTHING_TO_INLINE")
+
+package com.android.systemui.experimental.frp.internal
+
+import com.android.systemui.experimental.frp.internal.util.ConcurrentNullableHashMap
+import com.android.systemui.experimental.frp.internal.util.hashString
+import com.android.systemui.experimental.frp.util.Just
+import java.util.concurrent.ConcurrentHashMap
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+
+/** Base class for muxing nodes, which have a potentially dynamic collection of upstream nodes. */
+internal sealed class MuxNode<K : Any, V, Output>(val lifecycle: MuxLifecycle<Output>) :
+ PushNode<Output> {
+
+ inline val mutex
+ get() = lifecycle.mutex
+
+ // TODO: preserve insertion order?
+ val upstreamData = ConcurrentNullableHashMap<K, V>()
+ val switchedIn = ConcurrentHashMap<K, MuxBranchNode<K, V>>()
+ val downstreamSet: DownstreamSet = DownstreamSet()
+
+ // TODO: inline DepthTracker? would need to be added to PushNode signature
+ final override val depthTracker = DepthTracker()
+
+ final override suspend fun addDownstream(downstream: Schedulable) {
+ mutex.withLock { addDownstreamLocked(downstream) }
+ }
+
+ /**
+ * Adds a downstream schedulable to this mux node, such that when this mux node emits a value,
+ * it will be scheduled for evaluation within this same transaction.
+ *
+ * Must only be called when [mutex] is acquired.
+ */
+ fun addDownstreamLocked(downstream: Schedulable) {
+ downstreamSet.add(downstream)
+ }
+
+ final override suspend fun removeDownstream(downstream: Schedulable) {
+ // TODO: return boolean?
+ mutex.withLock { downstreamSet.remove(downstream) }
+ }
+
+ final override suspend fun removeDownstreamAndDeactivateIfNeeded(downstream: Schedulable) {
+ val deactivate =
+ mutex.withLock {
+ downstreamSet.remove(downstream)
+ downstreamSet.isEmpty()
+ }
+ if (deactivate) {
+ doDeactivate()
+ }
+ }
+
+ final override suspend fun deactivateIfNeeded() {
+ if (mutex.withLock { downstreamSet.isEmpty() }) {
+ doDeactivate()
+ }
+ }
+
+ /** visit this node from the scheduler (push eval) */
+ abstract suspend fun visit(evalScope: EvalScope)
+
+ /** perform deactivation logic, propagating to all upstream nodes. */
+ protected abstract suspend fun doDeactivate()
+
+ final override suspend fun scheduleDeactivationIfNeeded(evalScope: EvalScope) {
+ if (mutex.withLock { downstreamSet.isEmpty() }) {
+ evalScope.scheduleDeactivation(this)
+ }
+ }
+
+ suspend fun adjustDirectUpstream(scheduler: Scheduler, oldDepth: Int, newDepth: Int) {
+ mutex.withLock {
+ if (depthTracker.addDirectUpstream(oldDepth, newDepth)) {
+ depthTracker.schedule(scheduler, this)
+ }
+ }
+ }
+
+ suspend fun moveIndirectUpstreamToDirect(
+ scheduler: Scheduler,
+ oldIndirectDepth: Int,
+ oldIndirectRoots: Set<MuxDeferredNode<*, *>>,
+ newDepth: Int,
+ ) {
+ mutex.withLock {
+ if (
+ depthTracker.addDirectUpstream(oldDepth = null, newDepth) or
+ depthTracker.removeIndirectUpstream(depth = oldIndirectDepth) or
+ depthTracker.updateIndirectRoots(removals = oldIndirectRoots)
+ ) {
+ depthTracker.schedule(scheduler, this)
+ }
+ }
+ }
+
+ suspend fun adjustIndirectUpstream(
+ scheduler: Scheduler,
+ oldDepth: Int,
+ newDepth: Int,
+ removals: Set<MuxDeferredNode<*, *>>,
+ additions: Set<MuxDeferredNode<*, *>>,
+ ) {
+ mutex.withLock {
+ if (
+ depthTracker.addIndirectUpstream(oldDepth, newDepth) or
+ depthTracker.updateIndirectRoots(
+ additions,
+ removals,
+ butNot = this as? MuxDeferredNode<*, *>,
+ )
+ ) {
+ depthTracker.schedule(scheduler, this)
+ }
+ }
+ }
+
+ suspend fun moveDirectUpstreamToIndirect(
+ scheduler: Scheduler,
+ oldDepth: Int,
+ newDepth: Int,
+ newIndirectSet: Set<MuxDeferredNode<*, *>>,
+ ) {
+ mutex.withLock {
+ if (
+ depthTracker.addIndirectUpstream(oldDepth = null, newDepth) or
+ depthTracker.removeDirectUpstream(oldDepth) or
+ depthTracker.updateIndirectRoots(
+ additions = newIndirectSet,
+ butNot = this as? MuxDeferredNode<*, *>,
+ )
+ ) {
+ depthTracker.schedule(scheduler, this)
+ }
+ }
+ }
+
+ suspend fun removeDirectUpstream(scheduler: Scheduler, depth: Int, key: K) {
+ mutex.withLock {
+ switchedIn.remove(key)
+ if (depthTracker.removeDirectUpstream(depth)) {
+ depthTracker.schedule(scheduler, this)
+ }
+ }
+ }
+
+ suspend fun removeIndirectUpstream(
+ scheduler: Scheduler,
+ oldDepth: Int,
+ indirectSet: Set<MuxDeferredNode<*, *>>,
+ key: K,
+ ) {
+ mutex.withLock {
+ switchedIn.remove(key)
+ if (
+ depthTracker.removeIndirectUpstream(oldDepth) or
+ depthTracker.updateIndirectRoots(removals = indirectSet)
+ ) {
+ depthTracker.schedule(scheduler, this)
+ }
+ }
+ }
+
+ suspend fun visitCompact(scheduler: Scheduler) = coroutineScope {
+ if (depthTracker.isDirty()) {
+ depthTracker.applyChanges(coroutineScope = this, scheduler, downstreamSet, this@MuxNode)
+ }
+ }
+
+ abstract fun hasCurrentValueLocked(transactionStore: TransactionStore): Boolean
+}
+
+/** An input branch of a mux node, associated with a key. */
+internal class MuxBranchNode<K : Any, V>(private val muxNode: MuxNode<K, V, *>, val key: K) :
+ SchedulableNode {
+
+ val schedulable = Schedulable.N(this)
+
+ @Volatile lateinit var upstream: NodeConnection<V>
+
+ override suspend fun schedule(evalScope: EvalScope) {
+ val upstreamResult = upstream.getPushEvent(evalScope)
+ if (upstreamResult is Just) {
+ muxNode.upstreamData[key] = upstreamResult.value
+ evalScope.schedule(muxNode)
+ }
+ }
+
+ override suspend fun adjustDirectUpstream(scheduler: Scheduler, oldDepth: Int, newDepth: Int) {
+ muxNode.adjustDirectUpstream(scheduler, oldDepth, newDepth)
+ }
+
+ override suspend fun moveIndirectUpstreamToDirect(
+ scheduler: Scheduler,
+ oldIndirectDepth: Int,
+ oldIndirectSet: Set<MuxDeferredNode<*, *>>,
+ newDirectDepth: Int,
+ ) {
+ muxNode.moveIndirectUpstreamToDirect(
+ scheduler,
+ oldIndirectDepth,
+ oldIndirectSet,
+ newDirectDepth,
+ )
+ }
+
+ override suspend fun adjustIndirectUpstream(
+ scheduler: Scheduler,
+ oldDepth: Int,
+ newDepth: Int,
+ removals: Set<MuxDeferredNode<*, *>>,
+ additions: Set<MuxDeferredNode<*, *>>,
+ ) {
+ muxNode.adjustIndirectUpstream(scheduler, oldDepth, newDepth, removals, additions)
+ }
+
+ override suspend fun moveDirectUpstreamToIndirect(
+ scheduler: Scheduler,
+ oldDirectDepth: Int,
+ newIndirectDepth: Int,
+ newIndirectSet: Set<MuxDeferredNode<*, *>>,
+ ) {
+ muxNode.moveDirectUpstreamToIndirect(
+ scheduler,
+ oldDirectDepth,
+ newIndirectDepth,
+ newIndirectSet,
+ )
+ }
+
+ override suspend fun removeDirectUpstream(scheduler: Scheduler, depth: Int) {
+ muxNode.removeDirectUpstream(scheduler, depth, key)
+ }
+
+ override suspend fun removeIndirectUpstream(
+ scheduler: Scheduler,
+ depth: Int,
+ indirectSet: Set<MuxDeferredNode<*, *>>,
+ ) {
+ muxNode.removeIndirectUpstream(scheduler, depth, indirectSet, key)
+ }
+
+ override fun toString(): String = "MuxBranchNode(key=$key, mux=$muxNode)"
+}
+
+/** Tracks lifecycle of MuxNode in the network. Essentially a mutable ref for MuxLifecycleState. */
+internal class MuxLifecycle<A>(@Volatile var lifecycleState: MuxLifecycleState<A>) : TFlowImpl<A> {
+ val mutex = Mutex()
+
+ override fun toString(): String = "TFlowLifecycle[$hashString][$lifecycleState][$mutex]"
+
+ override suspend fun activate(
+ evalScope: EvalScope,
+ downstream: Schedulable,
+ ): ActivationResult<A>? =
+ mutex.withLock {
+ when (val state = lifecycleState) {
+ is MuxLifecycleState.Dead -> null
+ is MuxLifecycleState.Active -> {
+ state.node.addDownstreamLocked(downstream)
+ ActivationResult(
+ connection = NodeConnection(state.node, state.node),
+ needsEval = state.node.hasCurrentValueLocked(evalScope.transactionStore),
+ )
+ }
+ is MuxLifecycleState.Inactive -> {
+ state.spec
+ .activate(evalScope, this@MuxLifecycle)
+ .also { node ->
+ lifecycleState =
+ if (node == null) {
+ MuxLifecycleState.Dead
+ } else {
+ MuxLifecycleState.Active(node)
+ }
+ }
+ ?.let { node ->
+ node.addDownstreamLocked(downstream)
+ ActivationResult(
+ connection = NodeConnection(node, node),
+ needsEval = false,
+ )
+ }
+ }
+ }
+ }
+}
+
+internal sealed interface MuxLifecycleState<out A> {
+ class Inactive<A>(val spec: MuxActivator<A>) : MuxLifecycleState<A> {
+ override fun toString(): String = "Inactive"
+ }
+
+ class Active<A>(val node: MuxNode<*, *, A>) : MuxLifecycleState<A> {
+ override fun toString(): String = "Active(node=$node)"
+ }
+
+ data object Dead : MuxLifecycleState<Nothing>
+}
+
+internal interface MuxActivator<A> {
+ suspend fun activate(evalScope: EvalScope, lifecycle: MuxLifecycle<A>): MuxNode<*, *, A>?
+}
+
+internal inline fun <A> MuxLifecycle(onSubscribe: MuxActivator<A>): TFlowImpl<A> =
+ MuxLifecycle(MuxLifecycleState.Inactive(onSubscribe))
diff --git a/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/MuxDeferred.kt b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/MuxDeferred.kt
new file mode 100644
index 0000000..6d43285
--- /dev/null
+++ b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/MuxDeferred.kt
@@ -0,0 +1,473 @@
+/*
+ * 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.experimental.frp.internal
+
+import com.android.systemui.experimental.frp.internal.util.Key
+import com.android.systemui.experimental.frp.internal.util.associateByIndexTo
+import com.android.systemui.experimental.frp.internal.util.hashString
+import com.android.systemui.experimental.frp.internal.util.mapParallel
+import com.android.systemui.experimental.frp.internal.util.mapValuesNotNullParallelTo
+import com.android.systemui.experimental.frp.util.Just
+import com.android.systemui.experimental.frp.util.Left
+import com.android.systemui.experimental.frp.util.Maybe
+import com.android.systemui.experimental.frp.util.None
+import com.android.systemui.experimental.frp.util.Right
+import com.android.systemui.experimental.frp.util.These
+import com.android.systemui.experimental.frp.util.flatMap
+import com.android.systemui.experimental.frp.util.getMaybe
+import com.android.systemui.experimental.frp.util.just
+import com.android.systemui.experimental.frp.util.maybeThat
+import com.android.systemui.experimental.frp.util.maybeThis
+import com.android.systemui.experimental.frp.util.merge
+import com.android.systemui.experimental.frp.util.orElseGet
+import com.android.systemui.experimental.frp.util.partitionEithers
+import com.android.systemui.experimental.frp.util.these
+import java.util.TreeMap
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.withLock
+
+internal class MuxDeferredNode<K : Any, V>(
+ lifecycle: MuxLifecycle<Map<K, V>>,
+ val spec: MuxActivator<Map<K, V>>,
+) : MuxNode<K, V, Map<K, V>>(lifecycle), Key<Map<K, V>> {
+
+ val schedulable = Schedulable.M(this)
+
+ @Volatile var patches: NodeConnection<Map<K, Maybe<TFlowImpl<V>>>>? = null
+ @Volatile var patchData: Map<K, Maybe<TFlowImpl<V>>>? = null
+
+ override fun hasCurrentValueLocked(transactionStore: TransactionStore): Boolean =
+ transactionStore.contains(this)
+
+ override suspend fun hasCurrentValue(transactionStore: TransactionStore): Boolean =
+ mutex.withLock { hasCurrentValueLocked(transactionStore) }
+
+ override suspend fun visit(evalScope: EvalScope) {
+ val result = upstreamData.toMap()
+ upstreamData.clear()
+ val scheduleDownstream = result.isNotEmpty()
+ val compactDownstream = depthTracker.isDirty()
+ if (scheduleDownstream || compactDownstream) {
+ coroutineScope {
+ mutex.withLock {
+ if (compactDownstream) {
+ depthTracker.applyChanges(
+ coroutineScope = this,
+ evalScope.scheduler,
+ downstreamSet,
+ muxNode = this@MuxDeferredNode,
+ )
+ }
+ if (scheduleDownstream) {
+ evalScope.setResult(this@MuxDeferredNode, result)
+ if (!scheduleAll(downstreamSet, evalScope)) {
+ evalScope.scheduleDeactivation(this@MuxDeferredNode)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ override suspend fun getPushEvent(evalScope: EvalScope): Maybe<Map<K, V>> =
+ evalScope.getCurrentValue(key = this)
+
+ private suspend fun compactIfNeeded(evalScope: EvalScope) {
+ depthTracker.propagateChanges(evalScope.compactor, this)
+ }
+
+ override suspend fun doDeactivate() {
+ // Update lifecycle
+ lifecycle.mutex.withLock {
+ if (lifecycle.lifecycleState !is MuxLifecycleState.Active) return@doDeactivate
+ lifecycle.lifecycleState = MuxLifecycleState.Inactive(spec)
+ }
+ // Process branch nodes
+ coroutineScope {
+ switchedIn.values.forEach { branchNode ->
+ branchNode.upstream.let {
+ launch { it.removeDownstreamAndDeactivateIfNeeded(branchNode.schedulable) }
+ }
+ }
+ }
+ // Process patch node
+ patches?.removeDownstreamAndDeactivateIfNeeded(schedulable)
+ }
+
+ // MOVE phase
+ // - concurrent moves may be occurring, but no more evals. all depth recalculations are
+ // deferred to the end of this phase.
+ suspend fun performMove(evalScope: EvalScope) {
+ val patch = patchData ?: return
+ patchData = null
+
+ // TODO: this logic is very similar to what's in MuxPromptMoving, maybe turn into an inline
+ // fun?
+
+ // We have a patch, process additions/updates and removals
+ val (adds, removes) =
+ patch
+ .asSequence()
+ .map { (k, newUpstream: Maybe<TFlowImpl<V>>) ->
+ when (newUpstream) {
+ is Just -> Left(k to newUpstream.value)
+ None -> Right(k)
+ }
+ }
+ .partitionEithers()
+
+ val severed = mutableListOf<NodeConnection<*>>()
+
+ coroutineScope {
+ // remove and sever
+ removes.forEach { k ->
+ switchedIn.remove(k)?.let { branchNode: MuxBranchNode<K, V> ->
+ val conn = branchNode.upstream
+ severed.add(conn)
+ launch { conn.removeDownstream(downstream = branchNode.schedulable) }
+ depthTracker.removeDirectUpstream(conn.depthTracker.snapshotDirectDepth)
+ }
+ }
+
+ // add or replace
+ adds
+ .mapParallel { (k, newUpstream: TFlowImpl<V>) ->
+ val branchNode = MuxBranchNode(this@MuxDeferredNode, k)
+ k to
+ newUpstream.activate(evalScope, branchNode.schedulable)?.let { (conn, _) ->
+ branchNode.apply { upstream = conn }
+ }
+ }
+ .forEach { (k, newBranch: MuxBranchNode<K, V>?) ->
+ // remove old and sever, if present
+ switchedIn.remove(k)?.let { branchNode ->
+ val conn = branchNode.upstream
+ severed.add(conn)
+ launch { conn.removeDownstream(downstream = branchNode.schedulable) }
+ depthTracker.removeDirectUpstream(conn.depthTracker.snapshotDirectDepth)
+ }
+
+ // add new
+ newBranch?.let {
+ switchedIn[k] = newBranch
+ val branchDepthTracker = newBranch.upstream.depthTracker
+ if (branchDepthTracker.snapshotIsDirect) {
+ depthTracker.addDirectUpstream(
+ oldDepth = null,
+ newDepth = branchDepthTracker.snapshotDirectDepth,
+ )
+ } else {
+ depthTracker.addIndirectUpstream(
+ oldDepth = null,
+ newDepth = branchDepthTracker.snapshotIndirectDepth,
+ )
+ depthTracker.updateIndirectRoots(
+ additions = branchDepthTracker.snapshotIndirectRoots,
+ butNot = this@MuxDeferredNode,
+ )
+ }
+ }
+ }
+ }
+
+ coroutineScope {
+ for (severedNode in severed) {
+ launch { severedNode.scheduleDeactivationIfNeeded(evalScope) }
+ }
+ }
+
+ compactIfNeeded(evalScope)
+ }
+
+ suspend fun removeDirectPatchNode(scheduler: Scheduler) {
+ mutex.withLock {
+ if (
+ depthTracker.removeIndirectUpstream(depth = 0) or
+ depthTracker.setIsIndirectRoot(false)
+ ) {
+ depthTracker.schedule(scheduler, this)
+ }
+ patches = null
+ }
+ }
+
+ suspend fun removeIndirectPatchNode(
+ scheduler: Scheduler,
+ depth: Int,
+ indirectSet: Set<MuxDeferredNode<*, *>>,
+ ) {
+ // indirectly connected patches forward the indirectSet
+ mutex.withLock {
+ if (
+ depthTracker.updateIndirectRoots(removals = indirectSet) or
+ depthTracker.removeIndirectUpstream(depth)
+ ) {
+ depthTracker.schedule(scheduler, this)
+ }
+ patches = null
+ }
+ }
+
+ suspend fun moveIndirectPatchNodeToDirect(
+ scheduler: Scheduler,
+ oldIndirectDepth: Int,
+ oldIndirectSet: Set<MuxDeferredNode<*, *>>,
+ ) {
+ // directly connected patches are stored as an indirect singleton set of the patchNode
+ mutex.withLock {
+ if (
+ depthTracker.updateIndirectRoots(removals = oldIndirectSet) or
+ depthTracker.removeIndirectUpstream(oldIndirectDepth) or
+ depthTracker.setIsIndirectRoot(true)
+ ) {
+ depthTracker.schedule(scheduler, this)
+ }
+ }
+ }
+
+ suspend fun moveDirectPatchNodeToIndirect(
+ scheduler: Scheduler,
+ newIndirectDepth: Int,
+ newIndirectSet: Set<MuxDeferredNode<*, *>>,
+ ) {
+ // indirectly connected patches forward the indirectSet
+ mutex.withLock {
+ if (
+ depthTracker.setIsIndirectRoot(false) or
+ depthTracker.updateIndirectRoots(additions = newIndirectSet, butNot = this) or
+ depthTracker.addIndirectUpstream(oldDepth = null, newDepth = newIndirectDepth)
+ ) {
+ depthTracker.schedule(scheduler, this)
+ }
+ }
+ }
+
+ suspend fun adjustIndirectPatchNode(
+ scheduler: Scheduler,
+ oldDepth: Int,
+ newDepth: Int,
+ removals: Set<MuxDeferredNode<*, *>>,
+ additions: Set<MuxDeferredNode<*, *>>,
+ ) {
+ // indirectly connected patches forward the indirectSet
+ mutex.withLock {
+ if (
+ depthTracker.updateIndirectRoots(
+ additions = additions,
+ removals = removals,
+ butNot = this,
+ ) or depthTracker.addIndirectUpstream(oldDepth = oldDepth, newDepth = newDepth)
+ ) {
+ depthTracker.schedule(scheduler, this)
+ }
+ }
+ }
+
+ suspend fun scheduleMover(evalScope: EvalScope) {
+ patchData =
+ checkNotNull(patches) { "mux mover scheduled with unset patches upstream node" }
+ .getPushEvent(evalScope)
+ .orElseGet { null }
+ evalScope.scheduleMuxMover(this)
+ }
+
+ override fun toString(): String = "${this::class.simpleName}@$hashString"
+}
+
+internal inline fun <A> switchDeferredImplSingle(
+ crossinline getStorage: suspend EvalScope.() -> TFlowImpl<A>,
+ crossinline getPatches: suspend EvalScope.() -> TFlowImpl<TFlowImpl<A>>,
+): TFlowImpl<A> =
+ mapImpl({
+ switchDeferredImpl(
+ getStorage = { mapOf(Unit to getStorage()) },
+ getPatches = { mapImpl(getPatches) { newFlow -> mapOf(Unit to just(newFlow)) } },
+ )
+ }) { map ->
+ map.getValue(Unit)
+ }
+
+internal fun <K : Any, A> switchDeferredImpl(
+ getStorage: suspend EvalScope.() -> Map<K, TFlowImpl<A>>,
+ getPatches: suspend EvalScope.() -> TFlowImpl<Map<K, Maybe<TFlowImpl<A>>>>,
+): TFlowImpl<Map<K, A>> =
+ MuxLifecycle(
+ object : MuxActivator<Map<K, A>> {
+ override suspend fun activate(
+ evalScope: EvalScope,
+ lifecycle: MuxLifecycle<Map<K, A>>,
+ ): MuxNode<*, *, Map<K, A>>? {
+ val storage: Map<K, TFlowImpl<A>> = getStorage(evalScope)
+ // Initialize mux node and switched-in connections.
+ val muxNode =
+ MuxDeferredNode(lifecycle, this).apply {
+ storage.mapValuesNotNullParallelTo(switchedIn) { (key, flow) ->
+ val branchNode = MuxBranchNode(this@apply, key)
+ flow.activate(evalScope, branchNode.schedulable)?.let {
+ (conn, needsEval) ->
+ branchNode
+ .apply { upstream = conn }
+ .also {
+ if (needsEval) {
+ val result = conn.getPushEvent(evalScope)
+ if (result is Just) {
+ upstreamData[key] = result.value
+ }
+ }
+ }
+ }
+ }
+ }
+ // Update depth based on all initial switched-in nodes.
+ muxNode.switchedIn.values.forEach { branch ->
+ val conn = branch.upstream
+ if (conn.depthTracker.snapshotIsDirect) {
+ muxNode.depthTracker.addDirectUpstream(
+ oldDepth = null,
+ newDepth = conn.depthTracker.snapshotDirectDepth,
+ )
+ } else {
+ muxNode.depthTracker.addIndirectUpstream(
+ oldDepth = null,
+ newDepth = conn.depthTracker.snapshotIndirectDepth,
+ )
+ muxNode.depthTracker.updateIndirectRoots(
+ additions = conn.depthTracker.snapshotIndirectRoots,
+ butNot = muxNode,
+ )
+ }
+ }
+ // We don't have our patches connection established yet, so for now pretend we have
+ // a direct connection to patches. We will update downstream nodes later if this
+ // turns out to be a lie.
+ muxNode.depthTracker.setIsIndirectRoot(true)
+ muxNode.depthTracker.reset()
+
+ // Setup patches connection; deferring allows for a recursive connection, where
+ // muxNode is downstream of itself via patches.
+ var isIndirect = true
+ evalScope.deferAction {
+ val (patchesConn, needsEval) =
+ getPatches(evalScope).activate(evalScope, downstream = muxNode.schedulable)
+ ?: run {
+ isIndirect = false
+ // Turns out we can't connect to patches, so update our depth and
+ // propagate
+ muxNode.mutex.withLock {
+ if (muxNode.depthTracker.setIsIndirectRoot(false)) {
+ muxNode.depthTracker.schedule(evalScope.scheduler, muxNode)
+ }
+ }
+ return@deferAction
+ }
+ muxNode.patches = patchesConn
+
+ if (!patchesConn.schedulerUpstream.depthTracker.snapshotIsDirect) {
+ // Turns out patches is indirect, so we are not a root. Update depth and
+ // propagate.
+ muxNode.mutex.withLock {
+ if (
+ muxNode.depthTracker.setIsIndirectRoot(false) or
+ muxNode.depthTracker.addIndirectUpstream(
+ oldDepth = null,
+ newDepth = patchesConn.depthTracker.snapshotIndirectDepth,
+ ) or
+ muxNode.depthTracker.updateIndirectRoots(
+ additions = patchesConn.depthTracker.snapshotIndirectRoots
+ )
+ ) {
+ muxNode.depthTracker.schedule(evalScope.scheduler, muxNode)
+ }
+ }
+ }
+ // Schedule mover to process patch emission at the end of this transaction, if
+ // needed.
+ if (needsEval) {
+ val result = patchesConn.getPushEvent(evalScope)
+ if (result is Just) {
+ muxNode.patchData = result.value
+ evalScope.scheduleMuxMover(muxNode)
+ }
+ }
+ }
+
+ // Schedule for evaluation if any switched-in nodes have already emitted within
+ // this transaction.
+ if (muxNode.upstreamData.isNotEmpty()) {
+ evalScope.schedule(muxNode)
+ }
+ return muxNode.takeUnless { muxNode.switchedIn.isEmpty() && !isIndirect }
+ }
+ }
+ )
+
+internal inline fun <A> mergeNodes(
+ crossinline getPulse: suspend EvalScope.() -> TFlowImpl<A>,
+ crossinline getOther: suspend EvalScope.() -> TFlowImpl<A>,
+ crossinline f: suspend EvalScope.(A, A) -> A,
+): TFlowImpl<A> {
+ val merged =
+ mapImpl({ mergeNodes(getPulse, getOther) }) { these ->
+ these.merge { thiz, that -> f(thiz, that) }
+ }
+ return merged.cached()
+}
+
+internal inline fun <A, B> mergeNodes(
+ crossinline getPulse: suspend EvalScope.() -> TFlowImpl<A>,
+ crossinline getOther: suspend EvalScope.() -> TFlowImpl<B>,
+): TFlowImpl<These<A, B>> {
+ val storage =
+ mapOf(
+ 0 to mapImpl(getPulse) { These.thiz<A, B>(it) },
+ 1 to mapImpl(getOther) { These.that(it) },
+ )
+ val switchNode = switchDeferredImpl(getStorage = { storage }, getPatches = { neverImpl })
+ val merged =
+ mapImpl({ switchNode }) { mergeResults ->
+ val first = mergeResults.getMaybe(0).flatMap { it.maybeThis() }
+ val second = mergeResults.getMaybe(1).flatMap { it.maybeThat() }
+ these(first, second).orElseGet { error("unexpected missing merge result") }
+ }
+ return merged.cached()
+}
+
+internal inline fun <A> mergeNodes(
+ crossinline getPulses: suspend EvalScope.() -> Iterable<TFlowImpl<A>>
+): TFlowImpl<List<A>> {
+ val switchNode =
+ switchDeferredImpl(
+ getStorage = { getPulses().associateByIndexTo(TreeMap()) },
+ getPatches = { neverImpl },
+ )
+ val merged = mapImpl({ switchNode }) { mergeResults -> mergeResults.values.toList() }
+ return merged.cached()
+}
+
+internal inline fun <A> mergeNodesLeft(
+ crossinline getPulses: suspend EvalScope.() -> Iterable<TFlowImpl<A>>
+): TFlowImpl<A> {
+ val switchNode =
+ switchDeferredImpl(
+ getStorage = { getPulses().associateByIndexTo(TreeMap()) },
+ getPatches = { neverImpl },
+ )
+ val merged =
+ mapImpl({ switchNode }) { mergeResults: Map<Int, A> -> mergeResults.values.first() }
+ return merged.cached()
+}
diff --git a/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/MuxPrompt.kt b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/MuxPrompt.kt
new file mode 100644
index 0000000..ea0c150
--- /dev/null
+++ b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/MuxPrompt.kt
@@ -0,0 +1,472 @@
+/*
+ * 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.experimental.frp.internal
+
+import com.android.systemui.experimental.frp.internal.util.Key
+import com.android.systemui.experimental.frp.internal.util.launchImmediate
+import com.android.systemui.experimental.frp.internal.util.mapParallel
+import com.android.systemui.experimental.frp.internal.util.mapValuesNotNullParallelTo
+import com.android.systemui.experimental.frp.util.Just
+import com.android.systemui.experimental.frp.util.Left
+import com.android.systemui.experimental.frp.util.Maybe
+import com.android.systemui.experimental.frp.util.None
+import com.android.systemui.experimental.frp.util.Right
+import com.android.systemui.experimental.frp.util.filterJust
+import com.android.systemui.experimental.frp.util.map
+import com.android.systemui.experimental.frp.util.partitionEithers
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.withLock
+
+internal class MuxPromptMovingNode<K : Any, V>(
+ lifecycle: MuxLifecycle<Pair<Map<K, V>, Map<K, PullNode<V>>?>>,
+ private val spec: MuxActivator<Pair<Map<K, V>, Map<K, PullNode<V>>?>>,
+) :
+ MuxNode<K, V, Pair<Map<K, V>, Map<K, PullNode<V>>?>>(lifecycle),
+ Key<Pair<Map<K, V>, Map<K, PullNode<V>>?>> {
+
+ @Volatile var patchData: Map<K, Maybe<TFlowImpl<V>>>? = null
+ @Volatile var patches: MuxPromptPatchNode<K, V>? = null
+
+ @Volatile private var reEval: Pair<Map<K, V>, Map<K, PullNode<V>>?>? = null
+
+ override fun hasCurrentValueLocked(transactionStore: TransactionStore): Boolean =
+ transactionStore.contains(this)
+
+ override suspend fun hasCurrentValue(transactionStore: TransactionStore): Boolean =
+ mutex.withLock { hasCurrentValueLocked(transactionStore) }
+
+ override suspend fun visit(evalScope: EvalScope) {
+ val preSwitchResults: Map<K, V> = upstreamData.toMap()
+ upstreamData.clear()
+
+ val patch: Map<K, Maybe<TFlowImpl<V>>>? = patchData
+ patchData = null
+
+ val (reschedule, evalResult) =
+ reEval?.let { false to it }
+ ?: if (preSwitchResults.isNotEmpty() || patch?.isNotEmpty() == true) {
+ doEval(preSwitchResults, patch, evalScope)
+ } else {
+ false to null
+ }
+ reEval = null
+
+ if (reschedule || depthTracker.dirty_depthIncreased()) {
+ reEval = evalResult
+ // Can't schedule downstream yet, need to compact first
+ if (depthTracker.dirty_depthIncreased()) {
+ depthTracker.schedule(evalScope.compactor, node = this)
+ }
+ evalScope.schedule(this)
+ } else {
+ val compactDownstream = depthTracker.isDirty()
+ if (evalResult != null || compactDownstream) {
+ coroutineScope {
+ mutex.withLock {
+ if (compactDownstream) {
+ adjustDownstreamDepths(evalScope, coroutineScope = this)
+ }
+ if (evalResult != null) {
+ evalScope.setResult(this@MuxPromptMovingNode, evalResult)
+ if (!scheduleAll(downstreamSet, evalScope)) {
+ evalScope.scheduleDeactivation(this@MuxPromptMovingNode)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private suspend fun doEval(
+ preSwitchResults: Map<K, V>,
+ patch: Map<K, Maybe<TFlowImpl<V>>>?,
+ evalScope: EvalScope,
+ ): Pair<Boolean, Pair<Map<K, V>, Map<K, PullNode<V>>?>?> {
+ val newlySwitchedIn: Map<K, PullNode<V>>? =
+ patch?.let {
+ // We have a patch, process additions/updates and removals
+ val (adds, removes) =
+ patch
+ .asSequence()
+ .map { (k, newUpstream: Maybe<TFlowImpl<V>>) ->
+ when (newUpstream) {
+ is Just -> Left(k to newUpstream.value)
+ None -> Right(k)
+ }
+ }
+ .partitionEithers()
+
+ val additionsAndUpdates = mutableMapOf<K, PullNode<V>>()
+ val severed = mutableListOf<NodeConnection<*>>()
+
+ coroutineScope {
+ // remove and sever
+ removes.forEach { k ->
+ switchedIn.remove(k)?.let { branchNode: MuxBranchNode<K, V> ->
+ val conn: NodeConnection<V> = branchNode.upstream
+ severed.add(conn)
+ launchImmediate {
+ conn.removeDownstream(downstream = branchNode.schedulable)
+ }
+ depthTracker.removeDirectUpstream(conn.depthTracker.snapshotDirectDepth)
+ }
+ }
+
+ // add or replace
+ adds
+ .mapParallel { (k, newUpstream: TFlowImpl<V>) ->
+ val branchNode = MuxBranchNode(this@MuxPromptMovingNode, k)
+ k to
+ newUpstream.activate(evalScope, branchNode.schedulable)?.let {
+ (conn, _) ->
+ branchNode.apply { upstream = conn }
+ }
+ }
+ .forEach { (k, newBranch: MuxBranchNode<K, V>?) ->
+ // remove old and sever, if present
+ switchedIn.remove(k)?.let { oldBranch: MuxBranchNode<K, V> ->
+ val conn: NodeConnection<V> = oldBranch.upstream
+ severed.add(conn)
+ launchImmediate {
+ conn.removeDownstream(downstream = oldBranch.schedulable)
+ }
+ depthTracker.removeDirectUpstream(
+ conn.depthTracker.snapshotDirectDepth
+ )
+ }
+
+ // add new
+ newBranch?.let {
+ switchedIn[k] = newBranch
+ additionsAndUpdates[k] = newBranch.upstream.directUpstream
+ val branchDepthTracker = newBranch.upstream.depthTracker
+ if (branchDepthTracker.snapshotIsDirect) {
+ depthTracker.addDirectUpstream(
+ oldDepth = null,
+ newDepth = branchDepthTracker.snapshotDirectDepth,
+ )
+ } else {
+ depthTracker.addIndirectUpstream(
+ oldDepth = null,
+ newDepth = branchDepthTracker.snapshotIndirectDepth,
+ )
+ depthTracker.updateIndirectRoots(
+ additions = branchDepthTracker.snapshotIndirectRoots,
+ butNot = null,
+ )
+ }
+ }
+ }
+ }
+
+ coroutineScope {
+ for (severedNode in severed) {
+ launch { severedNode.scheduleDeactivationIfNeeded(evalScope) }
+ }
+ }
+
+ additionsAndUpdates.takeIf { it.isNotEmpty() }
+ }
+
+ return if (preSwitchResults.isNotEmpty() || newlySwitchedIn != null) {
+ (newlySwitchedIn != null) to (preSwitchResults to newlySwitchedIn)
+ } else {
+ false to null
+ }
+ }
+
+ private suspend fun adjustDownstreamDepths(
+ evalScope: EvalScope,
+ coroutineScope: CoroutineScope,
+ ) {
+ if (depthTracker.dirty_depthIncreased()) {
+ // schedule downstream nodes on the compaction scheduler; this scheduler is drained at
+ // the end of this eval depth, so that all depth increases are applied before we advance
+ // the eval step
+ depthTracker.schedule(evalScope.compactor, node = this@MuxPromptMovingNode)
+ } else if (depthTracker.isDirty()) {
+ // schedule downstream nodes on the eval scheduler; this is more efficient and is only
+ // safe if the depth hasn't increased
+ depthTracker.applyChanges(
+ coroutineScope,
+ evalScope.scheduler,
+ downstreamSet,
+ muxNode = this@MuxPromptMovingNode,
+ )
+ }
+ }
+
+ override suspend fun getPushEvent(
+ evalScope: EvalScope
+ ): Maybe<Pair<Map<K, V>, Map<K, PullNode<V>>?>> = evalScope.getCurrentValue(key = this)
+
+ override suspend fun doDeactivate() {
+ // Update lifecycle
+ lifecycle.mutex.withLock {
+ if (lifecycle.lifecycleState !is MuxLifecycleState.Active) return@doDeactivate
+ lifecycle.lifecycleState = MuxLifecycleState.Inactive(spec)
+ }
+ // Process branch nodes
+ switchedIn.values.forEach { branchNode ->
+ branchNode.upstream.removeDownstreamAndDeactivateIfNeeded(
+ downstream = branchNode.schedulable
+ )
+ }
+ // Process patch node
+ patches?.let { patches ->
+ patches.upstream.removeDownstreamAndDeactivateIfNeeded(downstream = patches.schedulable)
+ }
+ }
+
+ suspend fun removeIndirectPatchNode(
+ scheduler: Scheduler,
+ oldDepth: Int,
+ indirectSet: Set<MuxDeferredNode<*, *>>,
+ ) {
+ mutex.withLock {
+ patches = null
+ if (
+ depthTracker.removeIndirectUpstream(oldDepth) or
+ depthTracker.updateIndirectRoots(removals = indirectSet)
+ ) {
+ depthTracker.schedule(scheduler, this)
+ }
+ }
+ }
+
+ suspend fun removeDirectPatchNode(scheduler: Scheduler, depth: Int) {
+ mutex.withLock {
+ patches = null
+ if (depthTracker.removeDirectUpstream(depth)) {
+ depthTracker.schedule(scheduler, this)
+ }
+ }
+ }
+}
+
+internal class MuxPromptEvalNode<K, V>(
+ private val movingNode: PullNode<Pair<Map<K, V>, Map<K, PullNode<V>>?>>
+) : PullNode<Map<K, V>> {
+ override suspend fun getPushEvent(evalScope: EvalScope): Maybe<Map<K, V>> =
+ movingNode.getPushEvent(evalScope).map { (preSwitchResults, newlySwitchedIn) ->
+ coroutineScope {
+ newlySwitchedIn
+ ?.map { (k, v) -> async { v.getPushEvent(evalScope).map { k to it } } }
+ ?.awaitAll()
+ ?.asSequence()
+ ?.filterJust()
+ ?.toMap(preSwitchResults.toMutableMap()) ?: preSwitchResults
+ }
+ }
+}
+
+// TODO: inner class?
+internal class MuxPromptPatchNode<K : Any, V>(private val muxNode: MuxPromptMovingNode<K, V>) :
+ SchedulableNode {
+
+ val schedulable = Schedulable.N(this)
+
+ lateinit var upstream: NodeConnection<Map<K, Maybe<TFlowImpl<V>>>>
+
+ override suspend fun schedule(evalScope: EvalScope) {
+ val upstreamResult = upstream.getPushEvent(evalScope)
+ if (upstreamResult is Just) {
+ muxNode.patchData = upstreamResult.value
+ evalScope.schedule(muxNode)
+ }
+ }
+
+ override suspend fun adjustDirectUpstream(scheduler: Scheduler, oldDepth: Int, newDepth: Int) {
+ muxNode.adjustDirectUpstream(scheduler, oldDepth, newDepth)
+ }
+
+ override suspend fun moveIndirectUpstreamToDirect(
+ scheduler: Scheduler,
+ oldIndirectDepth: Int,
+ oldIndirectSet: Set<MuxDeferredNode<*, *>>,
+ newDirectDepth: Int,
+ ) {
+ muxNode.moveIndirectUpstreamToDirect(
+ scheduler,
+ oldIndirectDepth,
+ oldIndirectSet,
+ newDirectDepth,
+ )
+ }
+
+ override suspend fun adjustIndirectUpstream(
+ scheduler: Scheduler,
+ oldDepth: Int,
+ newDepth: Int,
+ removals: Set<MuxDeferredNode<*, *>>,
+ additions: Set<MuxDeferredNode<*, *>>,
+ ) {
+ muxNode.adjustIndirectUpstream(scheduler, oldDepth, newDepth, removals, additions)
+ }
+
+ override suspend fun moveDirectUpstreamToIndirect(
+ scheduler: Scheduler,
+ oldDirectDepth: Int,
+ newIndirectDepth: Int,
+ newIndirectSet: Set<MuxDeferredNode<*, *>>,
+ ) {
+ muxNode.moveDirectUpstreamToIndirect(
+ scheduler,
+ oldDirectDepth,
+ newIndirectDepth,
+ newIndirectSet,
+ )
+ }
+
+ override suspend fun removeDirectUpstream(scheduler: Scheduler, depth: Int) {
+ muxNode.removeDirectPatchNode(scheduler, depth)
+ }
+
+ override suspend fun removeIndirectUpstream(
+ scheduler: Scheduler,
+ depth: Int,
+ indirectSet: Set<MuxDeferredNode<*, *>>,
+ ) {
+ muxNode.removeIndirectPatchNode(scheduler, depth, indirectSet)
+ }
+}
+
+internal fun <K : Any, A> switchPromptImpl(
+ getStorage: suspend EvalScope.() -> Map<K, TFlowImpl<A>>,
+ getPatches: suspend EvalScope.() -> TFlowImpl<Map<K, Maybe<TFlowImpl<A>>>>,
+): TFlowImpl<Map<K, A>> {
+ val moving =
+ MuxLifecycle(
+ object : MuxActivator<Pair<Map<K, A>, Map<K, PullNode<A>>?>> {
+ override suspend fun activate(
+ evalScope: EvalScope,
+ lifecycle: MuxLifecycle<Pair<Map<K, A>, Map<K, PullNode<A>>?>>,
+ ): MuxNode<*, *, Pair<Map<K, A>, Map<K, PullNode<A>>?>>? {
+ val storage: Map<K, TFlowImpl<A>> = getStorage(evalScope)
+ // Initialize mux node and switched-in connections.
+ val movingNode =
+ MuxPromptMovingNode(lifecycle, this).apply {
+ coroutineScope {
+ launch {
+ storage.mapValuesNotNullParallelTo(switchedIn) { (key, flow) ->
+ val branchNode = MuxBranchNode(this@apply, key)
+ flow
+ .activate(
+ evalScope = evalScope,
+ downstream = branchNode.schedulable,
+ )
+ ?.let { (conn, needsEval) ->
+ branchNode
+ .apply { upstream = conn }
+ .also {
+ if (needsEval) {
+ val result =
+ conn.getPushEvent(evalScope)
+ if (result is Just) {
+ upstreamData[key] = result.value
+ }
+ }
+ }
+ }
+ }
+ }
+ // Setup patches connection
+ val patchNode = MuxPromptPatchNode(this@apply)
+ getPatches(evalScope)
+ .activate(
+ evalScope = evalScope,
+ downstream = patchNode.schedulable,
+ )
+ ?.let { (conn, needsEval) ->
+ patchNode.upstream = conn
+ patches = patchNode
+
+ if (needsEval) {
+ val result = conn.getPushEvent(evalScope)
+ if (result is Just) {
+ patchData = result.value
+ }
+ }
+ }
+ }
+ }
+ // Update depth based on all initial switched-in nodes.
+ movingNode.switchedIn.values.forEach { branch ->
+ val conn = branch.upstream
+ if (conn.depthTracker.snapshotIsDirect) {
+ movingNode.depthTracker.addDirectUpstream(
+ oldDepth = null,
+ newDepth = conn.depthTracker.snapshotDirectDepth,
+ )
+ } else {
+ movingNode.depthTracker.addIndirectUpstream(
+ oldDepth = null,
+ newDepth = conn.depthTracker.snapshotIndirectDepth,
+ )
+ movingNode.depthTracker.updateIndirectRoots(
+ additions = conn.depthTracker.snapshotIndirectRoots,
+ butNot = null,
+ )
+ }
+ }
+ // Update depth based on patches node.
+ movingNode.patches?.upstream?.let { conn ->
+ if (conn.depthTracker.snapshotIsDirect) {
+ movingNode.depthTracker.addDirectUpstream(
+ oldDepth = null,
+ newDepth = conn.depthTracker.snapshotDirectDepth,
+ )
+ } else {
+ movingNode.depthTracker.addIndirectUpstream(
+ oldDepth = null,
+ newDepth = conn.depthTracker.snapshotIndirectDepth,
+ )
+ movingNode.depthTracker.updateIndirectRoots(
+ additions = conn.depthTracker.snapshotIndirectRoots,
+ butNot = null,
+ )
+ }
+ }
+ movingNode.depthTracker.reset()
+
+ // Schedule for evaluation if any switched-in nodes or the patches node have
+ // already emitted within this transaction.
+ if (movingNode.patchData != null || movingNode.upstreamData.isNotEmpty()) {
+ evalScope.schedule(movingNode)
+ }
+
+ return movingNode.takeUnless { it.patches == null && it.switchedIn.isEmpty() }
+ }
+ }
+ )
+
+ val eval = TFlowCheap { downstream ->
+ moving.activate(evalScope = this, downstream)?.let { (connection, needsEval) ->
+ val evalNode = MuxPromptEvalNode(connection.directUpstream)
+ ActivationResult(
+ connection = NodeConnection(evalNode, connection.schedulerUpstream),
+ needsEval = needsEval,
+ )
+ }
+ }
+ return eval.cached()
+}
diff --git a/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/Network.kt b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/Network.kt
new file mode 100644
index 0000000..b5ffe75
--- /dev/null
+++ b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/Network.kt
@@ -0,0 +1,252 @@
+/*
+ * 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.experimental.frp.internal
+
+import com.android.systemui.experimental.frp.TState
+import com.android.systemui.experimental.frp.internal.util.HeteroMap
+import com.android.systemui.experimental.frp.util.Just
+import com.android.systemui.experimental.frp.util.Maybe
+import com.android.systemui.experimental.frp.util.just
+import com.android.systemui.experimental.frp.util.none
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.ConcurrentLinkedDeque
+import java.util.concurrent.ConcurrentLinkedQueue
+import java.util.concurrent.atomic.AtomicLong
+import kotlin.coroutines.ContinuationInterceptor
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.job
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.yield
+
+private val nextNetworkId = AtomicLong()
+
+internal class Network(val coroutineScope: CoroutineScope) : NetworkScope {
+
+ override val networkId: Any = nextNetworkId.getAndIncrement()
+
+ @Volatile
+ override var epoch: Long = 0L
+ private set
+
+ override val network
+ get() = this
+
+ override val compactor = SchedulerImpl()
+ override val scheduler = SchedulerImpl()
+ override val transactionStore = HeteroMap()
+
+ private val stateWrites = ConcurrentLinkedQueue<TStateSource<*>>()
+ private val outputsByDispatcher =
+ ConcurrentHashMap<ContinuationInterceptor, ConcurrentLinkedQueue<Output<*>>>()
+ private val muxMovers = ConcurrentLinkedQueue<MuxDeferredNode<*, *>>()
+ private val deactivations = ConcurrentLinkedDeque<PushNode<*>>()
+ private val outputDeactivations = ConcurrentLinkedQueue<Output<*>>()
+ private val transactionMutex = Mutex()
+ private val inputScheduleChan = Channel<ScheduledAction<*>>()
+
+ override fun scheduleOutput(output: Output<*>) {
+ val continuationInterceptor =
+ output.context[ContinuationInterceptor] ?: Dispatchers.Unconfined
+ outputsByDispatcher
+ .computeIfAbsent(continuationInterceptor) { ConcurrentLinkedQueue() }
+ .add(output)
+ }
+
+ override fun scheduleMuxMover(muxMover: MuxDeferredNode<*, *>) {
+ muxMovers.add(muxMover)
+ }
+
+ override fun schedule(state: TStateSource<*>) {
+ stateWrites.add(state)
+ }
+
+ // TODO: weird that we have this *and* scheduler exposed
+ override suspend fun schedule(node: MuxNode<*, *, *>) {
+ scheduler.schedule(node.depthTracker.dirty_directDepth, node)
+ }
+
+ override fun scheduleDeactivation(node: PushNode<*>) {
+ deactivations.add(node)
+ }
+
+ override fun scheduleDeactivation(output: Output<*>) {
+ outputDeactivations.add(output)
+ }
+
+ /** Listens for external events and starts FRP transactions. Runs forever. */
+ suspend fun runInputScheduler() = coroutineScope {
+ launch { scheduler.activate() }
+ launch { compactor.activate() }
+ val actions = mutableListOf<ScheduledAction<*>>()
+ for (first in inputScheduleChan) {
+ // Drain and conflate all transaction requests into a single transaction
+ actions.add(first)
+ while (true) {
+ yield()
+ val func = inputScheduleChan.tryReceive().getOrNull() ?: break
+ actions.add(func)
+ }
+ transactionMutex.withLock {
+ // Run all actions
+ evalScope {
+ for (action in actions) {
+ launch { action.started(evalScope = this@evalScope) }
+ }
+ }
+ // Step through the network
+ doTransaction()
+ // Signal completion
+ while (actions.isNotEmpty()) {
+ actions.removeLast().completed()
+ }
+ }
+ }
+ }
+
+ /** Evaluates [block] inside of a new transaction when the network is ready. */
+ fun <R> transaction(block: suspend EvalScope.() -> R): Deferred<R> =
+ CompletableDeferred<R>(parent = coroutineScope.coroutineContext.job).also { onResult ->
+ val job =
+ coroutineScope.launch {
+ inputScheduleChan.send(
+ ScheduledAction(onStartTransaction = block, onResult = onResult)
+ )
+ }
+ onResult.invokeOnCompletion { job.cancel() }
+ }
+
+ suspend fun <R> evalScope(block: suspend EvalScope.() -> R): R = deferScope {
+ block(EvalScopeImpl(this@Network, this))
+ }
+
+ /** Performs a transactional update of the FRP network. */
+ private suspend fun doTransaction() {
+ // Traverse network, then run outputs
+ do {
+ scheduler.drainEval(this)
+ } while (evalScope { evalOutputs(this) })
+ // Update states
+ evalScope { evalStateWriters(this) }
+ transactionStore.clear()
+ // Perform deferred switches
+ evalScope { evalMuxMovers(this) }
+ // Compact depths
+ scheduler.drainCompact()
+ compactor.drainCompact()
+ // Deactivate nodes with no downstream
+ evalDeactivations()
+ epoch++
+ }
+
+ /** Invokes all [Output]s that have received data within this transaction. */
+ private suspend fun evalOutputs(evalScope: EvalScope): Boolean {
+ // Outputs can enqueue other outputs, so we need two loops
+ if (outputsByDispatcher.isEmpty()) return false
+ while (outputsByDispatcher.isNotEmpty()) {
+ var launchedAny = false
+ coroutineScope {
+ for ((key, outputs) in outputsByDispatcher) {
+ if (outputs.isNotEmpty()) {
+ launchedAny = true
+ launch(key) {
+ while (outputs.isNotEmpty()) {
+ val output = outputs.remove()
+ launch { output.visit(evalScope) }
+ }
+ }
+ }
+ }
+ }
+ if (!launchedAny) outputsByDispatcher.clear()
+ }
+ return true
+ }
+
+ private suspend fun evalMuxMovers(evalScope: EvalScope) {
+ while (muxMovers.isNotEmpty()) {
+ coroutineScope {
+ val toMove = muxMovers.remove()
+ launch { toMove.performMove(evalScope) }
+ }
+ }
+ }
+
+ /** Updates all [TState]es that have changed within this transaction. */
+ private suspend fun evalStateWriters(evalScope: EvalScope) {
+ coroutineScope {
+ while (stateWrites.isNotEmpty()) {
+ val latch = stateWrites.remove()
+ launch { latch.updateState(evalScope) }
+ }
+ }
+ }
+
+ private suspend fun evalDeactivations() {
+ coroutineScope {
+ launch {
+ while (deactivations.isNotEmpty()) {
+ // traverse in reverse order
+ // - deactivations are added in depth-order during the node traversal phase
+ // - perform deactivations in reverse order, in case later ones propagate to
+ // earlier ones
+ val toDeactivate = deactivations.removeLast()
+ launch { toDeactivate.deactivateIfNeeded() }
+ }
+ }
+ while (outputDeactivations.isNotEmpty()) {
+ val toDeactivate = outputDeactivations.remove()
+ launch {
+ toDeactivate.upstream?.removeDownstreamAndDeactivateIfNeeded(
+ downstream = toDeactivate.schedulable
+ )
+ }
+ }
+ }
+ check(deactivations.isEmpty()) { "unexpected lingering deactivations" }
+ check(outputDeactivations.isEmpty()) { "unexpected lingering output deactivations" }
+ }
+}
+
+internal class ScheduledAction<T>(
+ private val onResult: CompletableDeferred<T>? = null,
+ private val onStartTransaction: suspend EvalScope.() -> T,
+) {
+ private var result: Maybe<T> = none
+
+ suspend fun started(evalScope: EvalScope) {
+ result = just(onStartTransaction(evalScope))
+ }
+
+ fun completed() {
+ if (onResult != null) {
+ when (val result = result) {
+ is Just -> onResult.complete(result.value)
+ else -> {}
+ }
+ }
+ result = none
+ }
+}
+
+internal typealias TransactionStore = HeteroMap
diff --git a/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/NoScope.kt b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/NoScope.kt
new file mode 100644
index 0000000..6375918
--- /dev/null
+++ b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/NoScope.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.experimental.frp.internal
+
+import com.android.systemui.experimental.frp.FrpScope
+import kotlin.coroutines.Continuation
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+import kotlin.coroutines.coroutineContext
+import kotlin.coroutines.startCoroutine
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.completeWith
+import kotlinx.coroutines.job
+
+internal object NoScope {
+ private object FrpScopeImpl : FrpScope
+
+ suspend fun <R> runInFrpScope(block: suspend FrpScope.() -> R): R {
+ val complete = CompletableDeferred<R>(coroutineContext.job)
+ block.startCoroutine(
+ FrpScopeImpl,
+ object : Continuation<R> {
+ override val context: CoroutineContext
+ get() = EmptyCoroutineContext
+
+ override fun resumeWith(result: Result<R>) {
+ complete.completeWith(result)
+ }
+ },
+ )
+ return complete.await()
+ }
+}
diff --git a/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/NodeTypes.kt b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/NodeTypes.kt
new file mode 100644
index 0000000..e7f76a0
--- /dev/null
+++ b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/NodeTypes.kt
@@ -0,0 +1,94 @@
+/*
+ * 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.experimental.frp.internal
+
+import com.android.systemui.experimental.frp.util.Maybe
+
+/*
+Dmux
+Muxes + Branch
+*/
+internal sealed interface SchedulableNode {
+ /** schedule this node w/ given NodeEvalScope */
+ suspend fun schedule(evalScope: EvalScope)
+
+ suspend fun adjustDirectUpstream(scheduler: Scheduler, oldDepth: Int, newDepth: Int)
+
+ suspend fun moveIndirectUpstreamToDirect(
+ scheduler: Scheduler,
+ oldIndirectDepth: Int,
+ oldIndirectSet: Set<MuxDeferredNode<*, *>>,
+ newDirectDepth: Int,
+ )
+
+ suspend fun adjustIndirectUpstream(
+ scheduler: Scheduler,
+ oldDepth: Int,
+ newDepth: Int,
+ removals: Set<MuxDeferredNode<*, *>>,
+ additions: Set<MuxDeferredNode<*, *>>,
+ )
+
+ suspend fun moveDirectUpstreamToIndirect(
+ scheduler: Scheduler,
+ oldDirectDepth: Int,
+ newIndirectDepth: Int,
+ newIndirectSet: Set<MuxDeferredNode<*, *>>,
+ )
+
+ suspend fun removeIndirectUpstream(
+ scheduler: Scheduler,
+ depth: Int,
+ indirectSet: Set<MuxDeferredNode<*, *>>,
+ )
+
+ suspend fun removeDirectUpstream(scheduler: Scheduler, depth: Int)
+}
+
+/*
+All but Dmux
+ */
+internal sealed interface PullNode<out A> {
+ /**
+ * query the result of this node within the current transaction. if the node is cached, this
+ * will read from the cache, otherwise it will perform a full evaluation, even if invoked
+ * multiple times within a transaction.
+ */
+ suspend fun getPushEvent(evalScope: EvalScope): Maybe<A>
+}
+
+/*
+Muxes + DmuxBranch
+ */
+internal sealed interface PushNode<A> : PullNode<A> {
+
+ suspend fun hasCurrentValue(transactionStore: TransactionStore): Boolean
+
+ val depthTracker: DepthTracker
+
+ suspend fun removeDownstream(downstream: Schedulable)
+
+ /** called during cleanup phase */
+ suspend fun deactivateIfNeeded()
+
+ /** called from mux nodes after severs */
+ suspend fun scheduleDeactivationIfNeeded(evalScope: EvalScope)
+
+ suspend fun addDownstream(downstream: Schedulable)
+
+ suspend fun removeDownstreamAndDeactivateIfNeeded(downstream: Schedulable)
+}
diff --git a/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/Output.kt b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/Output.kt
new file mode 100644
index 0000000..e60dcca
--- /dev/null
+++ b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/Output.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.experimental.frp.internal
+
+import com.android.systemui.experimental.frp.util.Just
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+
+internal class Output<A>(
+ val context: CoroutineContext = EmptyCoroutineContext,
+ val onDeath: suspend () -> Unit = {},
+ val onEmit: suspend EvalScope.(A) -> Unit,
+) {
+
+ val schedulable = Schedulable.O(this)
+
+ @Volatile var upstream: NodeConnection<A>? = null
+ @Volatile var result: Any? = NoResult
+
+ private object NoResult
+
+ // invoked by network
+ suspend fun visit(evalScope: EvalScope) {
+ val upstreamResult = result
+ check(upstreamResult !== NoResult) { "output visited with null upstream result" }
+ result = null
+ @Suppress("UNCHECKED_CAST") evalScope.onEmit(upstreamResult as A)
+ }
+
+ suspend fun kill() {
+ onDeath()
+ }
+
+ suspend fun schedule(evalScope: EvalScope) {
+ val upstreamResult =
+ checkNotNull(upstream) { "output scheduled with null upstream" }.getPushEvent(evalScope)
+ if (upstreamResult is Just) {
+ result = upstreamResult.value
+ evalScope.scheduleOutput(this)
+ }
+ }
+}
+
+internal inline fun OneShot(crossinline onEmit: suspend EvalScope.() -> Unit): Output<Unit> =
+ Output<Unit>(onEmit = { onEmit() }).apply { result = Unit }
diff --git a/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/PullNodes.kt b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/PullNodes.kt
new file mode 100644
index 0000000..b4656e0
--- /dev/null
+++ b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/PullNodes.kt
@@ -0,0 +1,74 @@
+/*
+ * 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.experimental.frp.internal
+
+import com.android.systemui.experimental.frp.internal.util.Key
+import com.android.systemui.experimental.frp.util.Maybe
+import com.android.systemui.experimental.frp.util.map
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Deferred
+
+internal val neverImpl: TFlowImpl<Nothing> = TFlowCheap { null }
+
+internal class MapNode<A, B>(val upstream: PullNode<A>, val transform: suspend EvalScope.(A) -> B) :
+ PullNode<B> {
+ override suspend fun getPushEvent(evalScope: EvalScope): Maybe<B> =
+ upstream.getPushEvent(evalScope).map { evalScope.transform(it) }
+}
+
+internal inline fun <A, B> mapImpl(
+ crossinline upstream: suspend EvalScope.() -> TFlowImpl<A>,
+ noinline transform: suspend EvalScope.(A) -> B,
+): TFlowImpl<B> = TFlowCheap { downstream ->
+ upstream().activate(evalScope = this, downstream)?.let { (connection, needsEval) ->
+ ActivationResult(
+ connection =
+ NodeConnection(
+ directUpstream = MapNode(connection.directUpstream, transform),
+ schedulerUpstream = connection.schedulerUpstream,
+ ),
+ needsEval = needsEval,
+ )
+ }
+}
+
+internal class CachedNode<A>(val key: Key<Deferred<Maybe<A>>>, val upstream: PullNode<A>) :
+ PullNode<A> {
+ override suspend fun getPushEvent(evalScope: EvalScope): Maybe<A> {
+ val deferred =
+ evalScope.transactionStore.getOrPut(key) {
+ evalScope.deferAsync(CoroutineStart.LAZY) { upstream.getPushEvent(evalScope) }
+ }
+ return deferred.await()
+ }
+}
+
+internal fun <A> TFlowImpl<A>.cached(): TFlowImpl<A> {
+ val key = object : Key<Deferred<Maybe<A>>> {}
+ return TFlowCheap {
+ activate(this, it)?.let { (connection, needsEval) ->
+ ActivationResult(
+ connection =
+ NodeConnection(
+ directUpstream = CachedNode(key, connection.directUpstream),
+ schedulerUpstream = connection.schedulerUpstream,
+ ),
+ needsEval = needsEval,
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/Scheduler.kt b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/Scheduler.kt
new file mode 100644
index 0000000..4fef865
--- /dev/null
+++ b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/Scheduler.kt
@@ -0,0 +1,106 @@
+/*
+ * 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.experimental.frp.internal
+
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.PriorityBlockingQueue
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+
+internal interface Scheduler {
+ suspend fun schedule(depth: Int, node: MuxNode<*, *, *>)
+
+ suspend fun scheduleIndirect(indirectDepth: Int, node: MuxNode<*, *, *>)
+}
+
+internal class SchedulerImpl : Scheduler {
+ val enqueued = ConcurrentHashMap<MuxNode<*, *, *>, Any>()
+ val scheduledQ = PriorityBlockingQueue<Pair<Int, MuxNode<*, *, *>>>(16, compareBy { it.first })
+ val chan = Channel<Pair<Int, MuxNode<*, *, *>>>(Channel.UNLIMITED)
+
+ override suspend fun schedule(depth: Int, node: MuxNode<*, *, *>) {
+ if (enqueued.putIfAbsent(node, node) == null) {
+ chan.send(Pair(depth, node))
+ }
+ }
+
+ override suspend fun scheduleIndirect(indirectDepth: Int, node: MuxNode<*, *, *>) {
+ schedule(Int.MIN_VALUE + indirectDepth, node)
+ }
+
+ suspend fun activate() {
+ for (nodeSchedule in chan) {
+ scheduledQ.add(nodeSchedule)
+ drainChan()
+ }
+ }
+
+ internal suspend fun drainEval(network: Network) {
+ drain { runStep ->
+ runStep { muxNode -> network.evalScope { muxNode.visit(this) } }
+ // If any visited MuxPromptNodes had their depths increased, eagerly propagate those
+ // depth
+ // changes now before performing further network evaluation.
+ network.compactor.drainCompact()
+ }
+ }
+
+ internal suspend fun drainCompact() {
+ drain { runStep -> runStep { muxNode -> muxNode.visitCompact(scheduler = this) } }
+ }
+
+ private suspend inline fun drain(
+ crossinline onStep:
+ suspend (runStep: suspend (visit: suspend (MuxNode<*, *, *>) -> Unit) -> Unit) -> Unit
+ ): Unit = coroutineScope {
+ while (!chan.isEmpty || scheduledQ.isNotEmpty()) {
+ drainChan()
+ val maxDepth = scheduledQ.peek()?.first ?: error("Unexpected empty scheduler")
+ onStep { visit -> runStep(maxDepth, visit) }
+ }
+ }
+
+ private suspend fun drainChan() {
+ while (!chan.isEmpty) {
+ scheduledQ.add(chan.receive())
+ }
+ }
+
+ private suspend inline fun runStep(
+ maxDepth: Int,
+ crossinline visit: suspend (MuxNode<*, *, *>) -> Unit,
+ ) = coroutineScope {
+ while (scheduledQ.peek()?.first?.let { it <= maxDepth } == true) {
+ val (d, node) = scheduledQ.remove()
+ if (
+ node.depthTracker.dirty_hasDirectUpstream() &&
+ d < node.depthTracker.dirty_directDepth
+ ) {
+ scheduledQ.add(node.depthTracker.dirty_directDepth to node)
+ } else {
+ launch {
+ enqueued.remove(node)
+ visit(node)
+ }
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/StateScopeImpl.kt b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/StateScopeImpl.kt
new file mode 100644
index 0000000..c1d1076
--- /dev/null
+++ b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/StateScopeImpl.kt
@@ -0,0 +1,257 @@
+/*
+ * 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.experimental.frp.internal
+
+import com.android.systemui.experimental.frp.FrpDeferredValue
+import com.android.systemui.experimental.frp.FrpStateScope
+import com.android.systemui.experimental.frp.FrpStateful
+import com.android.systemui.experimental.frp.FrpTransactionScope
+import com.android.systemui.experimental.frp.GroupedTFlow
+import com.android.systemui.experimental.frp.TFlow
+import com.android.systemui.experimental.frp.TFlowInit
+import com.android.systemui.experimental.frp.TFlowLoop
+import com.android.systemui.experimental.frp.TState
+import com.android.systemui.experimental.frp.TStateInit
+import com.android.systemui.experimental.frp.emptyTFlow
+import com.android.systemui.experimental.frp.groupByKey
+import com.android.systemui.experimental.frp.init
+import com.android.systemui.experimental.frp.internal.util.mapValuesParallel
+import com.android.systemui.experimental.frp.mapCheap
+import com.android.systemui.experimental.frp.merge
+import com.android.systemui.experimental.frp.switch
+import com.android.systemui.experimental.frp.util.Maybe
+import com.android.systemui.experimental.frp.util.map
+import kotlin.coroutines.Continuation
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+import kotlin.coroutines.startCoroutine
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.completeWith
+import kotlinx.coroutines.job
+
+internal class StateScopeImpl(val evalScope: EvalScope, override val endSignal: TFlow<Any>) :
+ StateScope, EvalScope by evalScope {
+
+ private val endSignalOnce: TFlow<Any> = endSignal.nextOnlyInternal("StateScope.endSignal")
+
+ private fun <A> TFlow<A>.truncateToScope(operatorName: String): TFlow<A> =
+ if (endSignalOnce === emptyTFlow) {
+ this
+ } else {
+ endSignalOnce.mapCheap { emptyTFlow }.toTStateInternal(operatorName, this).switch()
+ }
+
+ private fun <A> TFlow<A>.nextOnlyInternal(operatorName: String): TFlow<A> =
+ if (this === emptyTFlow) {
+ this
+ } else {
+ TFlowLoop<A>().apply {
+ loopback =
+ mapCheap { emptyTFlow }
+ .toTStateInternal(operatorName, this@nextOnlyInternal)
+ .switch()
+ }
+ }
+
+ private fun <A> TFlow<A>.toTStateInternal(operatorName: String, init: A): TState<A> =
+ toTStateInternalDeferred(operatorName, CompletableDeferred(init))
+
+ private fun <A> TFlow<A>.toTStateInternalDeferred(
+ operatorName: String,
+ init: Deferred<A>,
+ ): TState<A> {
+ val changes = this@toTStateInternalDeferred
+ val name = operatorName
+ val impl =
+ mkState(name, operatorName, evalScope, { changes.init.connect(evalScope = this) }, init)
+ return TStateInit(constInit(name, impl))
+ }
+
+ private fun <R> deferredInternal(block: suspend FrpStateScope.() -> R): FrpDeferredValue<R> =
+ FrpDeferredValue(deferAsync { runInStateScope(block) })
+
+ private fun <A> TFlow<A>.toTStateDeferredInternal(
+ initialValue: FrpDeferredValue<A>
+ ): TState<A> {
+ val operatorName = "toTStateDeferred"
+ // Ensure state is only collected until the end of this scope
+ return truncateToScope(operatorName)
+ .toTStateInternalDeferred(operatorName, initialValue.unwrapped)
+ }
+
+ private fun <K : Any, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementallyInternal(
+ storage: TState<Map<K, TFlow<V>>>
+ ): TFlow<Map<K, V>> {
+ val name = "mergeIncrementally"
+ return TFlowInit(
+ constInit(
+ name,
+ switchDeferredImpl(
+ getStorage = {
+ storage.init
+ .connect(this)
+ .getCurrentWithEpoch(this)
+ .first
+ .mapValuesParallel { (_, flow) -> flow.init.connect(this) }
+ },
+ getPatches = {
+ mapImpl({ init.connect(this) }) { patch ->
+ patch.mapValuesParallel { (_, m) ->
+ m.map { flow -> flow.init.connect(this) }
+ }
+ }
+ },
+ ),
+ )
+ )
+ }
+
+ private fun <K : Any, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementallyPromptInternal(
+ storage: TState<Map<K, TFlow<V>>>
+ ): TFlow<Map<K, V>> {
+ val name = "mergeIncrementallyPrompt"
+ return TFlowInit(
+ constInit(
+ name,
+ switchPromptImpl(
+ getStorage = {
+ storage.init
+ .connect(this)
+ .getCurrentWithEpoch(this)
+ .first
+ .mapValuesParallel { (_, flow) -> flow.init.connect(this) }
+ },
+ getPatches = {
+ mapImpl({ init.connect(this) }) { patch ->
+ patch.mapValuesParallel { (_, m) ->
+ m.map { flow -> flow.init.connect(this) }
+ }
+ }
+ },
+ ),
+ )
+ )
+ }
+
+ private fun <K, A, B> TFlow<Map<K, Maybe<FrpStateful<A>>>>.applyLatestStatefulForKeyInternal(
+ init: FrpDeferredValue<Map<K, FrpStateful<B>>>,
+ numKeys: Int?,
+ ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>> {
+ val eventsByKey: GroupedTFlow<K, Maybe<FrpStateful<A>>> = groupByKey(numKeys)
+ val initOut: Deferred<Map<K, B>> = deferAsync {
+ init.unwrapped.await().mapValuesParallel { (k, stateful) ->
+ val newEnd = with(frpScope) { eventsByKey[k].skipNext() }
+ val newScope = childStateScope(newEnd)
+ newScope.runInStateScope(stateful)
+ }
+ }
+ val changesNode: TFlowImpl<Map<K, Maybe<A>>> =
+ mapImpl(
+ upstream = { this@applyLatestStatefulForKeyInternal.init.connect(evalScope = this) }
+ ) { upstreamMap ->
+ upstreamMap.mapValuesParallel { (k: K, ma: Maybe<FrpStateful<A>>) ->
+ reenterStateScope(this@StateScopeImpl).run {
+ ma.map { stateful ->
+ val newEnd = with(frpScope) { eventsByKey[k].skipNext() }
+ val newScope = childStateScope(newEnd)
+ newScope.runInStateScope(stateful)
+ }
+ }
+ }
+ }
+ val operatorName = "applyLatestStatefulForKey"
+ val name = operatorName
+ val changes: TFlow<Map<K, Maybe<A>>> = TFlowInit(constInit(name, changesNode.cached()))
+ return changes to FrpDeferredValue(initOut)
+ }
+
+ private fun <A> TFlow<FrpStateful<A>>.observeStatefulsInternal(): TFlow<A> {
+ val operatorName = "observeStatefuls"
+ val name = operatorName
+ return TFlowInit(
+ constInit(
+ name,
+ mapImpl(
+ upstream = { this@observeStatefulsInternal.init.connect(evalScope = this) }
+ ) { stateful ->
+ reenterStateScope(outerScope = this@StateScopeImpl)
+ .runInStateScope(stateful)
+ }
+ .cached(),
+ )
+ )
+ }
+
+ override val frpScope: FrpStateScope = FrpStateScopeImpl()
+
+ private inner class FrpStateScopeImpl :
+ FrpStateScope, FrpTransactionScope by evalScope.frpScope {
+
+ override fun <A> deferredStateScope(
+ block: suspend FrpStateScope.() -> A
+ ): FrpDeferredValue<A> = deferredInternal(block)
+
+ override fun <A> TFlow<A>.holdDeferred(initialValue: FrpDeferredValue<A>): TState<A> =
+ toTStateDeferredInternal(initialValue)
+
+ override fun <K : Any, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementally(
+ initialTFlows: FrpDeferredValue<Map<K, TFlow<V>>>
+ ): TFlow<Map<K, V>> {
+ val storage: TState<Map<K, TFlow<V>>> = foldMapIncrementally(initialTFlows)
+ return mergeIncrementallyInternal(storage)
+ }
+
+ override fun <K : Any, V> TFlow<Map<K, Maybe<TFlow<V>>>>.mergeIncrementallyPromptly(
+ initialTFlows: FrpDeferredValue<Map<K, TFlow<V>>>
+ ): TFlow<Map<K, V>> {
+ val storage: TState<Map<K, TFlow<V>>> = foldMapIncrementally(initialTFlows)
+ return mergeIncrementallyPromptInternal(storage)
+ }
+
+ override fun <K, A, B> TFlow<Map<K, Maybe<FrpStateful<A>>>>.applyLatestStatefulForKey(
+ init: FrpDeferredValue<Map<K, FrpStateful<B>>>,
+ numKeys: Int?,
+ ): Pair<TFlow<Map<K, Maybe<A>>>, FrpDeferredValue<Map<K, B>>> =
+ applyLatestStatefulForKeyInternal(init, numKeys)
+
+ override fun <A> TFlow<FrpStateful<A>>.applyStatefuls(): TFlow<A> =
+ observeStatefulsInternal()
+ }
+
+ override suspend fun <R> runInStateScope(block: suspend FrpStateScope.() -> R): R {
+ val complete = CompletableDeferred<R>(parent = coroutineContext.job)
+ block.startCoroutine(
+ frpScope,
+ object : Continuation<R> {
+ override val context: CoroutineContext
+ get() = EmptyCoroutineContext
+
+ override fun resumeWith(result: Result<R>) {
+ complete.completeWith(result)
+ }
+ },
+ )
+ return complete.await()
+ }
+
+ override fun childStateScope(newEnd: TFlow<Any>) =
+ StateScopeImpl(evalScope, merge(newEnd, endSignal))
+}
+
+private fun EvalScope.reenterStateScope(outerScope: StateScopeImpl) =
+ StateScopeImpl(evalScope = this, endSignal = outerScope.endSignal)
diff --git a/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/TFlowImpl.kt b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/TFlowImpl.kt
new file mode 100644
index 0000000..7997864
--- /dev/null
+++ b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/TFlowImpl.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.experimental.frp.internal
+
+import com.android.systemui.experimental.frp.util.Maybe
+
+/* Initialized TFlow */
+internal fun interface TFlowImpl<out A> {
+ suspend fun activate(evalScope: EvalScope, downstream: Schedulable): ActivationResult<A>?
+}
+
+internal data class ActivationResult<out A>(
+ val connection: NodeConnection<A>,
+ val needsEval: Boolean,
+)
+
+internal inline fun <A> TFlowCheap(crossinline cheap: CheapNodeSubscribe<A>) =
+ TFlowImpl { scope, ds ->
+ scope.cheap(ds)
+ }
+
+internal typealias CheapNodeSubscribe<A> =
+ suspend EvalScope.(downstream: Schedulable) -> ActivationResult<A>?
+
+internal data class NodeConnection<out A>(
+ val directUpstream: PullNode<A>,
+ val schedulerUpstream: PushNode<*>,
+)
+
+internal suspend fun <A> NodeConnection<A>.hasCurrentValue(
+ transactionStore: TransactionStore
+): Boolean = schedulerUpstream.hasCurrentValue(transactionStore)
+
+internal suspend fun <A> NodeConnection<A>.removeDownstreamAndDeactivateIfNeeded(
+ downstream: Schedulable
+) = schedulerUpstream.removeDownstreamAndDeactivateIfNeeded(downstream)
+
+internal suspend fun <A> NodeConnection<A>.scheduleDeactivationIfNeeded(evalScope: EvalScope) =
+ schedulerUpstream.scheduleDeactivationIfNeeded(evalScope)
+
+internal suspend fun <A> NodeConnection<A>.removeDownstream(downstream: Schedulable) =
+ schedulerUpstream.removeDownstream(downstream)
+
+internal suspend fun <A> NodeConnection<A>.getPushEvent(evalScope: EvalScope): Maybe<A> =
+ directUpstream.getPushEvent(evalScope)
+
+internal val <A> NodeConnection<A>.depthTracker: DepthTracker
+ get() = schedulerUpstream.depthTracker
diff --git a/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/TStateImpl.kt b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/TStateImpl.kt
new file mode 100644
index 0000000..d8b6dac
--- /dev/null
+++ b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/TStateImpl.kt
@@ -0,0 +1,377 @@
+/*
+ * 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.experimental.frp.internal
+
+import com.android.systemui.experimental.frp.internal.util.Key
+import com.android.systemui.experimental.frp.internal.util.associateByIndex
+import com.android.systemui.experimental.frp.internal.util.hashString
+import com.android.systemui.experimental.frp.internal.util.mapValuesParallel
+import com.android.systemui.experimental.frp.util.Just
+import com.android.systemui.experimental.frp.util.Maybe
+import com.android.systemui.experimental.frp.util.just
+import com.android.systemui.experimental.frp.util.none
+import java.util.concurrent.atomic.AtomicLong
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+internal sealed interface TStateImpl<out A> {
+ val name: String?
+ val operatorName: String
+ val changes: TFlowImpl<A>
+
+ suspend fun getCurrentWithEpoch(evalScope: EvalScope): Pair<A, Long>
+}
+
+internal sealed class TStateDerived<A>(override val changes: TFlowImpl<A>) :
+ TStateImpl<A>, Key<Deferred<Pair<A, Long>>> {
+
+ @Volatile
+ var invalidatedEpoch = Long.MIN_VALUE
+ private set
+
+ @Volatile
+ protected var cache: Any? = EmptyCache
+ private set
+
+ override suspend fun getCurrentWithEpoch(evalScope: EvalScope): Pair<A, Long> =
+ evalScope.transactionStore
+ .getOrPut(this) { evalScope.deferAsync(CoroutineStart.LAZY) { pull(evalScope) } }
+ .await()
+
+ suspend fun pull(evalScope: EvalScope): Pair<A, Long> {
+ @Suppress("UNCHECKED_CAST")
+ return recalc(evalScope)?.also { (a, epoch) -> setCache(a, epoch) }
+ ?: ((cache as A) to invalidatedEpoch)
+ }
+
+ fun setCache(value: A, epoch: Long) {
+ if (epoch > invalidatedEpoch) {
+ cache = value
+ invalidatedEpoch = epoch
+ }
+ }
+
+ fun getCachedUnsafe(): Maybe<A> {
+ @Suppress("UNCHECKED_CAST")
+ return if (cache == EmptyCache) none else just(cache as A)
+ }
+
+ protected abstract suspend fun recalc(evalScope: EvalScope): Pair<A, Long>?
+
+ private data object EmptyCache
+}
+
+internal class TStateSource<A>(
+ override val name: String?,
+ override val operatorName: String,
+ init: Deferred<A>,
+ override val changes: TFlowImpl<A>,
+) : TStateImpl<A> {
+ constructor(
+ name: String?,
+ operatorName: String,
+ init: A,
+ changes: TFlowImpl<A>,
+ ) : this(name, operatorName, CompletableDeferred(init), changes)
+
+ lateinit var upstreamConnection: NodeConnection<A>
+
+ // Note: Don't need to synchronize; we will never interleave reads and writes, since all writes
+ // are performed at the end of a network step, after any reads would have taken place.
+
+ @Volatile private var _current: Deferred<A> = init
+ @Volatile
+ var writeEpoch = 0L
+ private set
+
+ override suspend fun getCurrentWithEpoch(evalScope: EvalScope): Pair<A, Long> =
+ _current.await() to writeEpoch
+
+ /** called by network after eval phase has completed */
+ suspend fun updateState(evalScope: EvalScope) {
+ // write the latch
+ val eventResult = upstreamConnection.getPushEvent(evalScope)
+ if (eventResult is Just) {
+ _current = CompletableDeferred(eventResult.value)
+ writeEpoch = evalScope.epoch
+ }
+ }
+
+ override fun toString(): String = "TStateImpl(changes=$changes, current=$_current)"
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ fun getStorageUnsafe(): Maybe<A> =
+ if (_current.isCompleted) just(_current.getCompleted()) else none
+}
+
+internal fun <A> constS(name: String?, operatorName: String, init: A): TStateImpl<A> =
+ TStateSource(name, operatorName, init, neverImpl)
+
+internal inline fun <A> mkState(
+ name: String?,
+ operatorName: String,
+ evalScope: EvalScope,
+ crossinline getChanges: suspend EvalScope.() -> TFlowImpl<A>,
+ init: Deferred<A>,
+): TStateImpl<A> {
+ lateinit var state: TStateSource<A>
+ val calm: TFlowImpl<A> =
+ filterNode(getChanges) { new -> new != state.getCurrentWithEpoch(evalScope = this).first }
+ .cached()
+ return TStateSource(name, operatorName, init, calm).also {
+ state = it
+ evalScope.scheduleOutput(
+ OneShot {
+ calm.activate(evalScope = this, downstream = Schedulable.S(state))?.let {
+ (connection, needsEval) ->
+ state.upstreamConnection = connection
+ if (needsEval) {
+ schedule(state)
+ }
+ }
+ }
+ )
+ }
+}
+
+private inline fun <A> TFlowImpl<A>.calm(
+ crossinline getState: () -> TStateDerived<A>
+): TFlowImpl<A> =
+ filterNode({ this@calm }) { new ->
+ val state = getState()
+ val (current, _) = state.getCurrentWithEpoch(evalScope = this)
+ if (new != current) {
+ state.setCache(new, epoch)
+ true
+ } else {
+ false
+ }
+ }
+ .cached()
+
+internal fun <A, B> TStateImpl<A>.mapCheap(
+ name: String?,
+ operatorName: String,
+ transform: suspend EvalScope.(A) -> B,
+): TStateImpl<B> =
+ DerivedMapCheap(name, operatorName, this, mapImpl({ changes }) { transform(it) }, transform)
+
+internal class DerivedMapCheap<A, B>(
+ override val name: String?,
+ override val operatorName: String,
+ val upstream: TStateImpl<A>,
+ override val changes: TFlowImpl<B>,
+ private val transform: suspend EvalScope.(A) -> B,
+) : TStateImpl<B> {
+
+ override suspend fun getCurrentWithEpoch(evalScope: EvalScope): Pair<B, Long> {
+ val (a, epoch) = upstream.getCurrentWithEpoch(evalScope)
+ return evalScope.transform(a) to epoch
+ }
+
+ override fun toString(): String = "${this::class.simpleName}@$hashString"
+}
+
+internal fun <A, B> TStateImpl<A>.map(
+ name: String?,
+ operatorName: String,
+ transform: suspend EvalScope.(A) -> B,
+): TStateImpl<B> {
+ lateinit var state: TStateDerived<B>
+ val mappedChanges = mapImpl({ changes }) { transform(it) }.cached().calm { state }
+ state = DerivedMap(name, operatorName, transform, this, mappedChanges)
+ return state
+}
+
+internal class DerivedMap<A, B>(
+ override val name: String?,
+ override val operatorName: String,
+ private val transform: suspend EvalScope.(A) -> B,
+ val upstream: TStateImpl<A>,
+ changes: TFlowImpl<B>,
+) : TStateDerived<B>(changes) {
+ override fun toString(): String = "${this::class.simpleName}@$hashString"
+
+ override suspend fun recalc(evalScope: EvalScope): Pair<B, Long>? {
+ val (a, epoch) = upstream.getCurrentWithEpoch(evalScope)
+ return if (epoch > invalidatedEpoch) {
+ evalScope.transform(a) to epoch
+ } else {
+ null
+ }
+ }
+}
+
+internal fun <A> TStateImpl<TStateImpl<A>>.flatten(name: String?, operator: String): TStateImpl<A> {
+ // emits the current value of the new inner state, when that state is emitted
+ val switchEvents = mapImpl({ changes }) { newInner -> newInner.getCurrentWithEpoch(this).first }
+ // emits the new value of the new inner state when that state is emitted, or
+ // falls back to the current value if a new state is *not* being emitted this
+ // transaction
+ val innerChanges =
+ mapImpl({ changes }) { newInner ->
+ mergeNodes({ switchEvents }, { newInner.changes }) { _, new -> new }
+ }
+ val switchedChanges: TFlowImpl<A> =
+ mapImpl({
+ switchPromptImpl(
+ getStorage = {
+ mapOf(Unit to this@flatten.getCurrentWithEpoch(evalScope = this).first.changes)
+ },
+ getPatches = { mapImpl({ innerChanges }) { new -> mapOf(Unit to just(new)) } },
+ )
+ }) { map ->
+ map.getValue(Unit)
+ }
+ lateinit var state: DerivedFlatten<A>
+ state = DerivedFlatten(name, operator, this, switchedChanges.calm { state })
+ return state
+}
+
+internal class DerivedFlatten<A>(
+ override val name: String?,
+ override val operatorName: String,
+ val upstream: TStateImpl<TStateImpl<A>>,
+ changes: TFlowImpl<A>,
+) : TStateDerived<A>(changes) {
+ override suspend fun recalc(evalScope: EvalScope): Pair<A, Long> {
+ val (inner, epoch0) = upstream.getCurrentWithEpoch(evalScope)
+ val (a, epoch1) = inner.getCurrentWithEpoch(evalScope)
+ return a to maxOf(epoch0, epoch1)
+ }
+
+ override fun toString(): String = "${this::class.simpleName}@$hashString"
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <A, B> TStateImpl<A>.flatMap(
+ name: String?,
+ operatorName: String,
+ noinline transform: suspend EvalScope.(A) -> TStateImpl<B>,
+): TStateImpl<B> = map(null, operatorName, transform).flatten(name, operatorName)
+
+internal fun <A, B, Z> zipStates(
+ name: String?,
+ operatorName: String,
+ l1: TStateImpl<A>,
+ l2: TStateImpl<B>,
+ transform: suspend EvalScope.(A, B) -> Z,
+): TStateImpl<Z> =
+ zipStates(null, operatorName, mapOf(0 to l1, 1 to l2)).map(name, operatorName) {
+ val a = it.getValue(0)
+ val b = it.getValue(1)
+ @Suppress("UNCHECKED_CAST") transform(a as A, b as B)
+ }
+
+internal fun <A, B, C, Z> zipStates(
+ name: String?,
+ operatorName: String,
+ l1: TStateImpl<A>,
+ l2: TStateImpl<B>,
+ l3: TStateImpl<C>,
+ transform: suspend EvalScope.(A, B, C) -> Z,
+): TStateImpl<Z> =
+ zipStates(null, operatorName, mapOf(0 to l1, 1 to l2, 2 to l3)).map(name, operatorName) {
+ val a = it.getValue(0)
+ val b = it.getValue(1)
+ val c = it.getValue(2)
+ @Suppress("UNCHECKED_CAST") transform(a as A, b as B, c as C)
+ }
+
+internal fun <A, B, C, D, Z> zipStates(
+ name: String?,
+ operatorName: String,
+ l1: TStateImpl<A>,
+ l2: TStateImpl<B>,
+ l3: TStateImpl<C>,
+ l4: TStateImpl<D>,
+ transform: suspend EvalScope.(A, B, C, D) -> Z,
+): TStateImpl<Z> =
+ zipStates(null, operatorName, mapOf(0 to l1, 1 to l2, 2 to l3, 3 to l4)).map(
+ name,
+ operatorName,
+ ) {
+ val a = it.getValue(0)
+ val b = it.getValue(1)
+ val c = it.getValue(2)
+ val d = it.getValue(3)
+ @Suppress("UNCHECKED_CAST") transform(a as A, b as B, c as C, d as D)
+ }
+
+internal fun <K : Any, A> zipStates(
+ name: String?,
+ operatorName: String,
+ states: Map<K, TStateImpl<A>>,
+): TStateImpl<Map<K, A>> {
+ if (states.isEmpty()) return constS(name, operatorName, emptyMap())
+ val stateChanges: Map<K, TFlowImpl<A>> = states.mapValues { it.value.changes }
+ lateinit var state: DerivedZipped<K, A>
+ // No need for calm; invariant ensures that changes will only emit when there's a difference
+ val changes: TFlowImpl<Map<K, A>> =
+ mapImpl({
+ switchDeferredImpl(getStorage = { stateChanges }, getPatches = { neverImpl })
+ }) { patch ->
+ states
+ .mapValues { (k, v) ->
+ if (k in patch) {
+ patch.getValue(k)
+ } else {
+ v.getCurrentWithEpoch(evalScope = this).first
+ }
+ }
+ .also { state.setCache(it, epoch) }
+ }
+ state = DerivedZipped(name, operatorName, states, changes)
+ return state
+}
+
+internal class DerivedZipped<K : Any, A>(
+ override val name: String?,
+ override val operatorName: String,
+ val upstream: Map<K, TStateImpl<A>>,
+ changes: TFlowImpl<Map<K, A>>,
+) : TStateDerived<Map<K, A>>(changes) {
+ override suspend fun recalc(evalScope: EvalScope): Pair<Map<K, A>, Long> {
+ val newEpoch = AtomicLong()
+ return upstream.mapValuesParallel {
+ val (a, epoch) = it.value.getCurrentWithEpoch(evalScope)
+ newEpoch.accumulateAndGet(epoch, ::maxOf)
+ a
+ } to newEpoch.get()
+ }
+
+ override fun toString(): String = "${this::class.simpleName}@$hashString"
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <A> zipStates(
+ name: String?,
+ operatorName: String,
+ states: List<TStateImpl<A>>,
+): TStateImpl<List<A>> =
+ if (states.isEmpty()) {
+ constS(name, operatorName, emptyList())
+ } else {
+ zipStates(null, operatorName, states.asIterable().associateByIndex()).mapCheap(
+ name,
+ operatorName,
+ ) {
+ it.values.toList()
+ }
+ }
diff --git a/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/TransactionalImpl.kt b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/TransactionalImpl.kt
new file mode 100644
index 0000000..c3f80a1
--- /dev/null
+++ b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/TransactionalImpl.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.experimental.frp.internal
+
+import com.android.systemui.experimental.frp.internal.util.Key
+import com.android.systemui.experimental.frp.internal.util.hashString
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Deferred
+
+internal sealed class TransactionalImpl<out A> {
+ data class Const<out A>(val value: Deferred<A>) : TransactionalImpl<A>()
+
+ class Impl<A>(val block: suspend EvalScope.() -> A) : TransactionalImpl<A>(), Key<Deferred<A>> {
+ override fun toString(): String = "${this::class.simpleName}@$hashString"
+ }
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <A> transactionalImpl(
+ noinline block: suspend EvalScope.() -> A
+): TransactionalImpl<A> = TransactionalImpl.Impl(block)
+
+internal fun <A> TransactionalImpl<A>.sample(evalScope: EvalScope): Deferred<A> =
+ when (this) {
+ is TransactionalImpl.Const -> value
+ is TransactionalImpl.Impl ->
+ evalScope.transactionStore
+ .getOrPut(this) {
+ evalScope.deferAsync(start = CoroutineStart.LAZY) { evalScope.block() }
+ }
+ .also { it.start() }
+ }
diff --git a/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/util/Bag.kt b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/util/Bag.kt
new file mode 100644
index 0000000..cc5538e
--- /dev/null
+++ b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/util/Bag.kt
@@ -0,0 +1,79 @@
+/*
+ * 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.experimental.frp.internal.util
+
+internal class Bag<T> private constructor(private val intMap: MutableMap<T, Int>) :
+ Set<T> by intMap.keys {
+
+ constructor() : this(hashMapOf())
+
+ override fun toString(): String = intMap.toString()
+
+ fun add(element: T): Boolean {
+ val entry = intMap[element]
+ return if (entry != null) {
+ intMap[element] = entry + 1
+ false
+ } else {
+ intMap[element] = 1
+ true
+ }
+ }
+
+ fun remove(element: T): Boolean {
+ val entry = intMap[element]
+ return when {
+ entry == null -> {
+ false
+ }
+ entry <= 1 -> {
+ intMap.remove(element)
+ true
+ }
+ else -> {
+ intMap[element] = entry - 1
+ false
+ }
+ }
+ }
+
+ fun addAll(elements: Iterable<T>, butNot: T? = null): Set<T>? {
+ val newlyAdded = hashSetOf<T>()
+ for (value in elements) {
+ if (value != butNot) {
+ if (add(value)) {
+ newlyAdded.add(value)
+ }
+ }
+ }
+ return newlyAdded.ifEmpty { null }
+ }
+
+ fun clear() {
+ intMap.clear()
+ }
+
+ fun removeAll(elements: Collection<T>): Set<T>? {
+ val result = hashSetOf<T>()
+ for (element in elements) {
+ if (remove(element)) {
+ result.add(element)
+ }
+ }
+ return result.ifEmpty { null }
+ }
+}
diff --git a/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/util/ConcurrentNullableHashMap.kt b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/util/ConcurrentNullableHashMap.kt
new file mode 100644
index 0000000..449aa19
--- /dev/null
+++ b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/util/ConcurrentNullableHashMap.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.experimental.frp.internal.util
+
+import java.util.concurrent.ConcurrentHashMap
+
+internal class ConcurrentNullableHashMap<K : Any, V>
+private constructor(private val inner: ConcurrentHashMap<K, Any>) {
+ constructor() : this(ConcurrentHashMap())
+
+ @Suppress("UNCHECKED_CAST")
+ operator fun get(key: K): V? = inner[key]?.takeIf { it !== NullValue } as V?
+
+ @Suppress("UNCHECKED_CAST")
+ fun put(key: K, value: V?): V? =
+ inner.put(key, value ?: NullValue)?.takeIf { it !== NullValue } as V?
+
+ operator fun set(key: K, value: V?) {
+ put(key, value)
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ fun toMap(): Map<K, V> = inner.mapValues { (_, v) -> v.takeIf { it !== NullValue } as V }
+
+ fun clear() {
+ inner.clear()
+ }
+
+ fun isNotEmpty(): Boolean = inner.isNotEmpty()
+}
+
+private object NullValue
diff --git a/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/util/HeteroMap.kt b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/util/HeteroMap.kt
new file mode 100644
index 0000000..14a567c
--- /dev/null
+++ b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/util/HeteroMap.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.experimental.frp.internal.util
+
+import com.android.systemui.experimental.frp.util.Maybe
+import com.android.systemui.experimental.frp.util.None
+import com.android.systemui.experimental.frp.util.just
+import java.util.concurrent.ConcurrentHashMap
+
+internal interface Key<A>
+
+private object NULL
+
+internal class HeteroMap {
+
+ private val store = ConcurrentHashMap<Key<*>, Any>()
+
+ @Suppress("UNCHECKED_CAST")
+ operator fun <A> get(key: Key<A>): Maybe<A> =
+ store[key]?.let { just((if (it === NULL) null else it) as A) } ?: None
+
+ operator fun <A> set(key: Key<A>, value: A) {
+ store[key] = value ?: NULL
+ }
+
+ operator fun contains(key: Key<*>): Boolean = store.containsKey(key)
+
+ fun clear() {
+ store.clear()
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ fun <A> remove(key: Key<A>): Maybe<A> =
+ store.remove(key)?.let { just((if (it === NULL) null else it) as A) } ?: None
+
+ @Suppress("UNCHECKED_CAST")
+ fun <A> getOrPut(key: Key<A>, defaultValue: () -> A): A =
+ store.compute(key) { _, value -> value ?: defaultValue() ?: NULL } as A
+}
diff --git a/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/util/MapUtils.kt b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/util/MapUtils.kt
new file mode 100644
index 0000000..6f19a76
--- /dev/null
+++ b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/util/MapUtils.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.experimental.frp.internal.util
+
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.yield
+
+// TODO: It's possible that this is less efficient than having each coroutine directly insert into a
+// ConcurrentHashMap, but then we would lose ordering
+internal suspend inline fun <K, A, B : Any, M : MutableMap<K, B>> Map<K, A>
+ .mapValuesNotNullParallelTo(
+ destination: M,
+ crossinline block: suspend (Map.Entry<K, A>) -> B?,
+): M =
+ destination.also {
+ coroutineScope {
+ mapValues {
+ async {
+ yield()
+ block(it)
+ }
+ }
+ }
+ .mapValuesNotNullTo(it) { (_, deferred) -> deferred.await() }
+ }
+
+internal inline fun <K, A, B : Any, M : MutableMap<K, B>> Map<K, A>.mapValuesNotNullTo(
+ destination: M,
+ block: (Map.Entry<K, A>) -> B?,
+): M =
+ destination.also {
+ for (entry in this@mapValuesNotNullTo) {
+ block(entry)?.let { destination.put(entry.key, it) }
+ }
+ }
+
+internal suspend fun <A, B> Iterable<A>.mapParallel(transform: suspend (A) -> B): List<B> =
+ coroutineScope {
+ map { async(start = CoroutineStart.LAZY) { transform(it) } }.awaitAll()
+ }
+
+internal suspend fun <K, A, B, M : MutableMap<K, B>> Map<K, A>.mapValuesParallelTo(
+ destination: M,
+ transform: suspend (Map.Entry<K, A>) -> B,
+): Map<K, B> = entries.mapParallel { it.key to transform(it) }.toMap(destination)
+
+internal suspend fun <K, A, B> Map<K, A>.mapValuesParallel(
+ transform: suspend (Map.Entry<K, A>) -> B
+): Map<K, B> = mapValuesParallelTo(mutableMapOf(), transform)
diff --git a/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/util/Util.kt b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/util/Util.kt
new file mode 100644
index 0000000..0a47429
--- /dev/null
+++ b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/internal/util/Util.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.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.experimental.frp.internal.util
+
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.newCoroutineContext
+
+internal fun <A> CoroutineScope.asyncImmediate(
+ start: CoroutineStart = CoroutineStart.UNDISPATCHED,
+ context: CoroutineContext = EmptyCoroutineContext,
+ block: suspend CoroutineScope.() -> A,
+): Deferred<A> = async(start = start, context = Dispatchers.Unconfined + context, block = block)
+
+internal fun CoroutineScope.launchImmediate(
+ start: CoroutineStart = CoroutineStart.UNDISPATCHED,
+ context: CoroutineContext = EmptyCoroutineContext,
+ block: suspend CoroutineScope.() -> Unit,
+): Job = launch(start = start, context = Dispatchers.Unconfined + context, block = block)
+
+internal suspend fun awaitCancellationAndThen(block: suspend () -> Unit) {
+ try {
+ awaitCancellation()
+ } finally {
+ block()
+ }
+}
+
+internal fun CoroutineScope.launchOnCancel(
+ context: CoroutineContext = EmptyCoroutineContext,
+ block: () -> Unit,
+): Job =
+ launch(context = context, start = CoroutineStart.UNDISPATCHED) {
+ awaitCancellationAndThen(block)
+ }
+
+internal fun CoroutineScope.childScope(
+ context: CoroutineContext = EmptyCoroutineContext
+): CoroutineScope {
+ val newContext = newCoroutineContext(context)
+ val newJob = Job(parent = newContext[Job])
+ return CoroutineScope(newContext + newJob)
+}
+
+internal fun <A> Iterable<A>.associateByIndex(): Map<Int, A> = buildMap {
+ forEachIndexed { index, a -> put(index, a) }
+}
+
+internal fun <A, M : MutableMap<Int, A>> Iterable<A>.associateByIndexTo(destination: M): M =
+ destination.apply { forEachIndexed { index, a -> put(index, a) } }
+
+internal val Any.hashString: String
+ get() = Integer.toHexString(System.identityHashCode(this))
diff --git a/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/util/Either.kt b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/util/Either.kt
new file mode 100644
index 0000000..dca8364
--- /dev/null
+++ b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/util/Either.kt
@@ -0,0 +1,114 @@
+/*
+ * 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:Suppress("NOTHING_TO_INLINE")
+
+package com.android.systemui.experimental.frp.util
+
+/**
+ * Contains a value of two possibilities: `Left<A>` or `Right<B>`
+ *
+ * [Either] generalizes sealed classes the same way that [Pair] generalizes data classes; if a
+ * [Pair] is effectively an anonymous grouping of two instances, then an [Either] is an anonymous
+ * set of two options.
+ */
+sealed class Either<out A, out B>
+
+/** An [Either] that contains a [Left] value. */
+data class Left<out A>(val value: A) : Either<A, Nothing>()
+
+/** An [Either] that contains a [Right] value. */
+data class Right<out B>(val value: B) : Either<Nothing, B>()
+
+/**
+ * Returns an [Either] containing the result of applying [transform] to the [Left] value, or the
+ * [Right] value unchanged.
+ */
+inline fun <A, B, C> Either<A, C>.mapLeft(transform: (A) -> B): Either<B, C> =
+ when (this) {
+ is Left -> Left(transform(value))
+ is Right -> this
+ }
+
+/**
+ * Returns an [Either] containing the result of applying [transform] to the [Right] value, or the
+ * [Left] value unchanged.
+ */
+inline fun <A, B, C> Either<A, B>.mapRight(transform: (B) -> C): Either<A, C> =
+ when (this) {
+ is Left -> this
+ is Right -> Right(transform(value))
+ }
+
+/** Returns a [Maybe] containing the [Left] value held by this [Either], if present. */
+inline fun <A> Either<A, *>.leftMaybe(): Maybe<A> =
+ when (this) {
+ is Left -> just(value)
+ else -> None
+ }
+
+/** Returns the [Left] value held by this [Either], or `null` if this is a [Right] value. */
+inline fun <A> Either<A, *>.leftOrNull(): A? =
+ when (this) {
+ is Left -> value
+ else -> null
+ }
+
+/** Returns a [Maybe] containing the [Right] value held by this [Either], if present. */
+inline fun <B> Either<*, B>.rightMaybe(): Maybe<B> =
+ when (this) {
+ is Right -> just(value)
+ else -> None
+ }
+
+/** Returns the [Right] value held by this [Either], or `null` if this is a [Left] value. */
+inline fun <B> Either<*, B>.rightOrNull(): B? =
+ when (this) {
+ is Right -> value
+ else -> null
+ }
+
+/**
+ * Partitions this sequence of [Either] into two lists; [Pair.first] contains all [Left] values, and
+ * [Pair.second] contains all [Right] values.
+ */
+fun <A, B> Sequence<Either<A, B>>.partitionEithers(): Pair<List<A>, List<B>> {
+ val lefts = mutableListOf<A>()
+ val rights = mutableListOf<B>()
+ for (either in this) {
+ when (either) {
+ is Left -> lefts.add(either.value)
+ is Right -> rights.add(either.value)
+ }
+ }
+ return lefts to rights
+}
+
+/**
+ * Partitions this map of [Either] values into two maps; [Pair.first] contains all [Left] values,
+ * and [Pair.second] contains all [Right] values.
+ */
+fun <K, A, B> Map<K, Either<A, B>>.partitionEithers(): Pair<Map<K, A>, Map<K, B>> {
+ val lefts = mutableMapOf<K, A>()
+ val rights = mutableMapOf<K, B>()
+ for ((k, e) in this) {
+ when (e) {
+ is Left -> lefts[k] = e.value
+ is Right -> rights[k] = e.value
+ }
+ }
+ return lefts to rights
+}
diff --git a/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/util/Maybe.kt b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/util/Maybe.kt
new file mode 100644
index 0000000..59c680e
--- /dev/null
+++ b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/util/Maybe.kt
@@ -0,0 +1,271 @@
+/*
+ * 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:Suppress("NOTHING_TO_INLINE", "SuspendCoroutine")
+
+package com.android.systemui.experimental.frp.util
+
+import kotlin.coroutines.Continuation
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+import kotlin.coroutines.RestrictsSuspension
+import kotlin.coroutines.resume
+import kotlin.coroutines.startCoroutine
+import kotlin.coroutines.suspendCoroutine
+
+/** Represents a value that may or may not be present. */
+sealed class Maybe<out A>
+
+/** A [Maybe] value that is present. */
+data class Just<out A> internal constructor(val value: A) : Maybe<A>()
+
+/** A [Maybe] value that is not present. */
+data object None : Maybe<Nothing>()
+
+/** Utilities to query [Maybe] instances from within a [maybe] block. */
+@RestrictsSuspension
+object MaybeScope {
+ suspend operator fun <A> Maybe<A>.not(): A = suspendCoroutine { k ->
+ if (this is Just) k.resume(value)
+ }
+
+ suspend inline fun guard(crossinline block: () -> Boolean): Unit = suspendCoroutine { k ->
+ if (block()) k.resume(Unit)
+ }
+}
+
+/**
+ * Returns a [Maybe] value produced by evaluating [block].
+ *
+ * [block] can use its [MaybeScope] receiver to query other [Maybe] values, automatically cancelling
+ * execution of [block] and producing [None] when attempting to query a [Maybe] that is not present.
+ *
+ * This can be used instead of Kotlin's built-in nullability (`?.` and `?:`) operators when dealing
+ * with complex combinations of nullables:
+ * ``` kotlin
+ * val aMaybe: Maybe<Any> = ...
+ * val bMaybe: Maybe<Any> = ...
+ * val result: String = maybe {
+ * val a = !aMaybe
+ * val b = !bMaybe
+ * "Got: $a and $b"
+ * }
+ * ```
+ */
+fun <A> maybe(block: suspend MaybeScope.() -> A): Maybe<A> {
+ var maybeResult: Maybe<A> = None
+ val k =
+ object : Continuation<A> {
+ override val context: CoroutineContext = EmptyCoroutineContext
+
+ override fun resumeWith(result: Result<A>) {
+ maybeResult = result.getOrNull()?.let { just(it) } ?: None
+ }
+ }
+ block.startCoroutine(MaybeScope, k)
+ return maybeResult
+}
+
+/** Returns a [Just] containing this value, or [None] if `null`. */
+inline fun <A> (A?).toMaybe(): Maybe<A> = maybe(this)
+
+/** Returns a [Just] containing a non-null [value], or [None] if `null`. */
+inline fun <A> maybe(value: A?): Maybe<A> = value?.let(::just) ?: None
+
+/** Returns a [Just] containing [value]. */
+fun <A> just(value: A): Maybe<A> = Just(value)
+
+/** A [Maybe] that is not present. */
+val none: Maybe<Nothing> = None
+
+/** A [Maybe] that is not present. */
+inline fun <A> none(): Maybe<A> = None
+
+/** Returns the value present in this [Maybe], or `null` if not present. */
+inline fun <A> Maybe<A>.orNull(): A? = orElse(null)
+
+/**
+ * Returns a [Maybe] holding the result of applying [transform] to the value in the original
+ * [Maybe].
+ */
+inline fun <A, B> Maybe<A>.map(transform: (A) -> B): Maybe<B> =
+ when (this) {
+ is Just -> just(transform(value))
+ is None -> None
+ }
+
+/** Returns the result of applying [transform] to the value in the original [Maybe]. */
+inline fun <A, B> Maybe<A>.flatMap(transform: (A) -> Maybe<B>): Maybe<B> =
+ when (this) {
+ is Just -> transform(value)
+ is None -> None
+ }
+
+/** Returns the value present in this [Maybe], or the result of [defaultValue] if not present. */
+inline fun <A> Maybe<A>.orElseGet(defaultValue: () -> A): A =
+ when (this) {
+ is Just -> value
+ is None -> defaultValue()
+ }
+
+/**
+ * Returns the value present in this [Maybe], or invokes [error] with the message returned from
+ * [getMessage].
+ */
+inline fun <A> Maybe<A>.orError(getMessage: () -> Any): A = orElseGet { error(getMessage()) }
+
+/** Returns the value present in this [Maybe], or [defaultValue] if not present. */
+inline fun <A> Maybe<A>.orElse(defaultValue: A): A =
+ when (this) {
+ is Just -> value
+ is None -> defaultValue
+ }
+
+/**
+ * Returns a [Maybe] that contains the present in the original [Maybe], only if it satisfies
+ * [predicate].
+ */
+inline fun <A> Maybe<A>.filter(predicate: (A) -> Boolean): Maybe<A> =
+ when (this) {
+ is Just -> if (predicate(value)) this else None
+ else -> this
+ }
+
+/** Returns a [List] containing all values that are present in this [Iterable]. */
+fun <A> Iterable<Maybe<A>>.filterJust(): List<A> = asSequence().filterJust().toList()
+
+/** Returns a [List] containing all values that are present in this [Sequence]. */
+fun <A> Sequence<Maybe<A>>.filterJust(): Sequence<A> = filterIsInstance<Just<A>>().map { it.value }
+
+// Align
+
+/**
+ * Returns a [Maybe] containing the result of applying the values present in the original [Maybe]
+ * and other, applied to [transform] as a [These].
+ */
+inline fun <A, B, C> Maybe<A>.alignWith(other: Maybe<B>, transform: (These<A, B>) -> C): Maybe<C> =
+ when (this) {
+ is Just -> {
+ val a = value
+ when (other) {
+ is Just -> {
+ val b = other.value
+ just(transform(These.both(a, b)))
+ }
+ None -> just(transform(These.thiz(a)))
+ }
+ }
+ None ->
+ when (other) {
+ is Just -> {
+ val b = other.value
+ just(transform(These.that(b)))
+ }
+ None -> none
+ }
+ }
+
+// Alt
+
+/** Returns a [Maybe] containing the value present in the original [Maybe], or [other]. */
+infix fun <A> Maybe<A>.orElseMaybe(other: Maybe<A>): Maybe<A> = orElseGetMaybe { other }
+
+/**
+ * Returns a [Maybe] containing the value present in the original [Maybe], or the result of [other].
+ */
+inline fun <A> Maybe<A>.orElseGetMaybe(other: () -> Maybe<A>): Maybe<A> =
+ when (this) {
+ is Just -> this
+ else -> other()
+ }
+
+// Apply
+
+/**
+ * Returns a [Maybe] containing the value present in [argMaybe] applied to the function present in
+ * the original [Maybe].
+ */
+fun <A, B> Maybe<(A) -> B>.apply(argMaybe: Maybe<A>): Maybe<B> = flatMap { f ->
+ argMaybe.map { a -> f(a) }
+}
+
+/**
+ * Returns a [Maybe] containing the result of applying [transform] to the values present in the
+ * original [Maybe] and [other].
+ */
+inline fun <A, B, C> Maybe<A>.zipWith(other: Maybe<B>, transform: (A, B) -> C) = flatMap { a ->
+ other.map { b -> transform(a, b) }
+}
+
+// Bind
+
+/**
+ * Returns a [Maybe] containing the value present in the [Maybe] present in the original [Maybe].
+ */
+fun <A> Maybe<Maybe<A>>.flatten(): Maybe<A> = flatMap { it }
+
+// Semigroup
+
+/**
+ * Returns a [Maybe] containing the result of applying the values present in the original [Maybe]
+ * and other, applied to [transform].
+ */
+fun <A> Maybe<A>.mergeWith(other: Maybe<A>, transform: (A, A) -> A): Maybe<A> =
+ alignWith(other) { it.merge(transform) }
+
+/**
+ * Returns a list containing only the present results of applying [transform] to each element in the
+ * original iterable.
+ */
+fun <A, B> Iterable<A>.mapMaybe(transform: (A) -> Maybe<B>): List<B> =
+ asSequence().mapMaybe(transform).toList()
+
+/**
+ * Returns a sequence containing only the present results of applying [transform] to each element in
+ * the original sequence.
+ */
+fun <A, B> Sequence<A>.mapMaybe(transform: (A) -> Maybe<B>): Sequence<B> =
+ map(transform).filterIsInstance<Just<B>>().map { it.value }
+
+/**
+ * Returns a map with values of only the present results of applying [transform] to each entry in
+ * the original map.
+ */
+inline fun <K, A, B> Map<K, A>.mapMaybeValues(
+ crossinline p: (Map.Entry<K, A>) -> Maybe<B>
+): Map<K, B> = asSequence().mapMaybe { entry -> p(entry).map { entry.key to it } }.toMap()
+
+/** Returns a map with all non-present values filtered out. */
+fun <K, A> Map<K, Maybe<A>>.filterJustValues(): Map<K, A> =
+ asSequence().mapMaybe { (key, mValue) -> mValue.map { key to it } }.toMap()
+
+/**
+ * Returns a pair of [Maybes][Maybe] that contain the [Pair.first] and [Pair.second] values present
+ * in the original [Maybe].
+ */
+fun <A, B> Maybe<Pair<A, B>>.splitPair(): Pair<Maybe<A>, Maybe<B>> =
+ map { it.first } to map { it.second }
+
+/** Returns the value associated with [key] in this map as a [Maybe]. */
+fun <K, V> Map<K, V>.getMaybe(key: K): Maybe<V> {
+ val value = get(key)
+ if (value == null && !containsKey(key)) {
+ return none
+ } else {
+ @Suppress("UNCHECKED_CAST")
+ return just(value as V)
+ }
+}
diff --git a/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/util/These.kt b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/util/These.kt
new file mode 100644
index 0000000..5404c07
--- /dev/null
+++ b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/util/These.kt
@@ -0,0 +1,148 @@
+/*
+ * 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.experimental.frp.util
+
+/** Contains at least one of two potential values. */
+sealed class These<A, B> {
+ /** Contains a single potential value. */
+ class This<A, B> internal constructor(val thiz: A) : These<A, B>()
+
+ /** Contains a single potential value. */
+ class That<A, B> internal constructor(val that: B) : These<A, B>()
+
+ /** Contains both potential values. */
+ class Both<A, B> internal constructor(val thiz: A, val that: B) : These<A, B>()
+
+ companion object {
+ /** Constructs a [These] containing only [thiz]. */
+ fun <A, B> thiz(thiz: A): These<A, B> = This(thiz)
+
+ /** Constructs a [These] containing only [that]. */
+ fun <A, B> that(that: B): These<A, B> = That(that)
+
+ /** Constructs a [These] containing both [thiz] and [that]. */
+ fun <A, B> both(thiz: A, that: B): These<A, B> = Both(thiz, that)
+ }
+}
+
+/**
+ * Returns a single value from this [These]; either the single value held within, or the result of
+ * applying [f] to both values.
+ */
+inline fun <A> These<A, A>.merge(f: (A, A) -> A): A =
+ when (this) {
+ is These.This -> thiz
+ is These.That -> that
+ is These.Both -> f(thiz, that)
+ }
+
+/** Returns the [These.This] [value][These.This.thiz] present in this [These] as a [Maybe]. */
+fun <A> These<A, *>.maybeThis(): Maybe<A> =
+ when (this) {
+ is These.Both -> just(thiz)
+ is These.That -> None
+ is These.This -> just(thiz)
+ }
+
+/**
+ * Returns the [These.This] [value][These.This.thiz] present in this [These], or `null` if not
+ * present.
+ */
+fun <A : Any> These<A, *>.thisOrNull(): A? =
+ when (this) {
+ is These.Both -> thiz
+ is These.That -> null
+ is These.This -> thiz
+ }
+
+/** Returns the [These.That] [value][These.That.that] present in this [These] as a [Maybe]. */
+fun <A> These<*, A>.maybeThat(): Maybe<A> =
+ when (this) {
+ is These.Both -> just(that)
+ is These.That -> just(that)
+ is These.This -> None
+ }
+
+/**
+ * Returns the [These.That] [value][These.That.that] present in this [These], or `null` if not
+ * present.
+ */
+fun <A : Any> These<*, A>.thatOrNull(): A? =
+ when (this) {
+ is These.Both -> that
+ is These.That -> that
+ is These.This -> null
+ }
+
+/** Returns [These.Both] values present in this [These] as a [Maybe]. */
+fun <A, B> These<A, B>.maybeBoth(): Maybe<Pair<A, B>> =
+ when (this) {
+ is These.Both -> just(thiz to that)
+ else -> None
+ }
+
+/** Returns a [These] containing [thiz] and/or [that] if they are present. */
+fun <A, B> these(thiz: Maybe<A>, that: Maybe<B>): Maybe<These<A, B>> =
+ when (thiz) {
+ is Just ->
+ just(
+ when (that) {
+ is Just -> These.both(thiz.value, that.value)
+ else -> These.thiz(thiz.value)
+ }
+ )
+ else ->
+ when (that) {
+ is Just -> just(These.that(that.value))
+ else -> none
+ }
+ }
+
+/**
+ * Returns a [These] containing [thiz] and/or [that] if they are non-null, or `null` if both are
+ * `null`.
+ */
+fun <A : Any, B : Any> theseNull(thiz: A?, that: B?): These<A, B>? =
+ thiz?.let { that?.let { These.both(thiz, that) } ?: These.thiz(thiz) }
+ ?: that?.let { These.that(that) }
+
+/**
+ * Returns two maps, with [Pair.first] containing all [These.This] values and [Pair.second]
+ * containing all [These.That] values.
+ *
+ * If the value is [These.Both], then the associated key with appear in both output maps, bound to
+ * [These.Both.thiz] and [These.Both.that] in each respective output.
+ */
+fun <K, A, B> Map<K, These<A, B>>.partitionThese(): Pair<Map<K, A>, Map<K, B>> {
+ val a = mutableMapOf<K, A>()
+ val b = mutableMapOf<K, B>()
+ for ((k, t) in this) {
+ when (t) {
+ is These.Both -> {
+ a[k] = t.thiz
+ b[k] = t.that
+ }
+ is These.That -> {
+ b[k] = t.that
+ }
+ is These.This -> {
+ a[k] = t.thiz
+ }
+ }
+ }
+ return a to b
+}
diff --git a/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/util/WithPrev.kt b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/util/WithPrev.kt
new file mode 100644
index 0000000..e52a6e1
--- /dev/null
+++ b/packages/SystemUI/frp/src/com/android/systemui/experimental/frp/util/WithPrev.kt
@@ -0,0 +1,20 @@
+/*
+ * 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.experimental.frp.util
+
+/** Holds a [newValue] emitted from a `TFlow`, along with the [previousValue] emitted value. */
+data class WithPrev<out S, out T : S>(val previousValue: S, val newValue: T)
diff --git a/packages/SystemUI/frp/test/com/android/systemui/experimental/frp/FrpTests.kt b/packages/SystemUI/frp/test/com/android/systemui/experimental/frp/FrpTests.kt
new file mode 100644
index 0000000..a58f499
--- /dev/null
+++ b/packages/SystemUI/frp/test/com/android/systemui/experimental/frp/FrpTests.kt
@@ -0,0 +1,1370 @@
+/*
+ * 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.experimental.frp
+
+import com.android.systemui.experimental.frp.util.Either
+import com.android.systemui.experimental.frp.util.Left
+import com.android.systemui.experimental.frp.util.Maybe
+import com.android.systemui.experimental.frp.util.None
+import com.android.systemui.experimental.frp.util.Right
+import com.android.systemui.experimental.frp.util.just
+import com.android.systemui.experimental.frp.util.map
+import com.android.systemui.experimental.frp.util.maybe
+import com.android.systemui.experimental.frp.util.none
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.seconds
+import kotlin.time.DurationUnit
+import kotlin.time.measureTime
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.toCollection
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class FrpTests {
+
+ @Test
+ fun basic() = runFrpTest { network ->
+ val emitter = network.mutableTFlow<Int>()
+ var result: Int? = null
+ activateSpec(network) { emitter.observe { result = it } }
+ runCurrent()
+ emitter.emit(3)
+ runCurrent()
+ assertEquals(3, result)
+ runCurrent()
+ }
+
+ @Test
+ fun basicTFlow() = runFrpTest { network ->
+ val emitter = network.mutableTFlow<Int>()
+ println("starting network")
+ val result = activateSpecWithResult(network) { emitter.nextDeferred() }
+ runCurrent()
+ println("emitting")
+ emitter.emit(3)
+ runCurrent()
+ println("awaiting")
+ assertEquals(3, result.await())
+ runCurrent()
+ }
+
+ @Test
+ fun basicTState() = runFrpTest { network ->
+ val emitter = network.mutableTFlow<Int>()
+ val result = activateSpecWithResult(network) { emitter.hold(0).stateChanges.nextDeferred() }
+ runCurrent()
+
+ emitter.emit(3)
+ runCurrent()
+
+ assertEquals(3, result.await())
+ }
+
+ @Test
+ fun basicEvent() = runFrpTest { network ->
+ val emitter = MutableSharedFlow<Int>()
+ val result = activateSpecWithResult(network) { async { emitter.first() } }
+ runCurrent()
+ emitter.emit(1)
+ runCurrent()
+ assertTrue("Result eventual has not completed.", result.isCompleted)
+ assertEquals(1, result.await())
+ }
+
+ @Test
+ fun basicTransactional() = runFrpTest { network ->
+ var value: Int? = null
+ var bSource = 1
+ val emitter = network.mutableTFlow<Unit>()
+ // Sampling this transactional will increment the source count.
+ val transactional = transactionally { bSource++ }
+ measureTime {
+ activateSpecWithResult(network) {
+ // Two different flows that sample the same transactional.
+ (0 until 2).map {
+ val sampled = emitter.sample(transactional) { _, v -> v }
+ sampled.toSharedFlow()
+ }
+ }
+ .forEach { backgroundScope.launch { it.collect { value = it } } }
+ runCurrent()
+ }
+ .also { println("setup: ${it.toString(DurationUnit.MILLISECONDS, 2)}") }
+
+ measureTime {
+ emitter.emit(Unit)
+ runCurrent()
+ }
+ .also { println("emit 1: ${it.toString(DurationUnit.MILLISECONDS, 2)}") }
+
+ // Even though the transactional would be sampled twice, the first result is cached.
+ assertEquals(2, bSource)
+ assertEquals(1, value)
+
+ measureTime {
+ bSource = 10
+ emitter.emit(Unit)
+ runCurrent()
+ }
+ .also { println("emit 2: ${it.toString(DurationUnit.MILLISECONDS, 2)}") }
+
+ assertEquals(11, bSource)
+ assertEquals(10, value)
+ }
+
+ @Test
+ fun diamondGraph() = runFrpTest { network ->
+ val flow = network.mutableTFlow<Int>()
+ val outFlow =
+ activateSpecWithResult(network) {
+ // map TFlow 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
+ val combined =
+ left.hold("left" to 0).combineWith(right.hold("right" to 0)) { l, r -> l to r }
+ combined.stateChanges // get TState 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) }
+ runCurrent()
+
+ flow.emit(1)
+ runCurrent()
+
+ flow.emit(2)
+ runCurrent()
+
+ assertEquals(
+ listOf(("left" to 1) to ("right" to 1), ("left" to 2) to ("right" to 2)),
+ results,
+ )
+ }
+
+ @Test
+ fun staticNetwork() = runFrpTest { network ->
+ var finalSum: Int? = null
+
+ val intEmitter = network.mutableTFlow<Int>()
+ val sampleEmitter = network.mutableTFlow<Unit>()
+
+ activateSpecWithResult(network) {
+ val updates = intEmitter.map { a -> { b: Int -> a + b } }
+
+ val sumD =
+ TStateLoop<Int>().apply {
+ loopback =
+ updates
+ .sample(this) { f, sum -> f(sum) }
+ .onEach { println("sum update: $it") }
+ .hold(0)
+ }
+ sampleEmitter
+ .onEach { println("sampleEmitter emitted") }
+ .sample(sumD) { _, sum -> sum }
+ .onEach { println("sampled: $it") }
+ .nextDeferred()
+ }
+ .let { launch { finalSum = it.await() } }
+
+ runCurrent()
+
+ (1..5).forEach { i ->
+ println("emitting: $i")
+ intEmitter.emit(i)
+ runCurrent()
+ }
+ runCurrent()
+
+ sampleEmitter.emit(Unit)
+ runCurrent()
+
+ assertEquals(15, finalSum)
+ }
+
+ @Test
+ fun recursiveDefinition() = runFrpTest { network ->
+ var wasSold = false
+ var currentAmt: Int? = null
+
+ val coin = network.mutableTFlow<Unit>()
+ val price = 50
+ val frpSpec = frpSpec {
+ val eSold = TFlowLoop<Unit>()
+
+ val eInsert =
+ coin.map {
+ { runningTotal: Int ->
+ println("TEST: $runningTotal - 10 = ${runningTotal - 10}")
+ runningTotal - 10
+ }
+ }
+
+ val eReset =
+ eSold.map {
+ { _: Int ->
+ println("TEST: Resetting")
+ price
+ }
+ }
+
+ 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 eAmt = dTotal.stateChanges
+ val bAmt = transactionally { dTotal.sample() }
+ eSold.loopback =
+ coin
+ .sample(bAmt) { coin, total -> coin to total }
+ .mapMaybe { (_, total) -> maybe { guard { total <= 10 } } }
+
+ val amts = eAmt.filter { amt -> amt >= 0 }
+
+ amts.observe { currentAmt = it }
+ eSold.observe { wasSold = true }
+
+ eSold.nextDeferred()
+ }
+
+ activateSpec(network) { frpSpec.applySpec() }
+
+ runCurrent()
+
+ println()
+ println()
+ coin.emit(Unit)
+ runCurrent()
+
+ assertEquals(40, currentAmt)
+
+ println()
+ println()
+ coin.emit(Unit)
+ runCurrent()
+
+ assertEquals(30, currentAmt)
+
+ println()
+ println()
+ coin.emit(Unit)
+ runCurrent()
+
+ assertEquals(20, currentAmt)
+
+ println()
+ println()
+ coin.emit(Unit)
+ runCurrent()
+
+ assertEquals(10, currentAmt)
+ assertEquals(false, wasSold)
+
+ println()
+ println()
+ coin.emit(Unit)
+ runCurrent()
+
+ assertEquals(true, wasSold)
+ assertEquals(50, currentAmt)
+ }
+
+ @Test
+ fun promptCleanup() = runFrpTest { network ->
+ val emitter = network.mutableTFlow<Int>()
+ val stopper = network.mutableTFlow<Unit>()
+
+ var result: Int? = null
+
+ val flow = activateSpecWithResult(network) { emitter.takeUntil(stopper).toSharedFlow() }
+ backgroundScope.launch { flow.collect { result = it } }
+ runCurrent()
+
+ emitter.emit(2)
+ runCurrent()
+
+ assertEquals(2, result)
+
+ stopper.emit(Unit)
+ runCurrent()
+ }
+
+ @Test
+ fun switchTFlow() = 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 sumFlow =
+ activateSpecWithResult(network) {
+ val switchE = TFlowLoop<TFlow<Int>>()
+ switchE.loopback =
+ switchHandler.mapStateful { (intFlow, name) ->
+ println("[onEach] Switching to: $name")
+ val nextSwitch =
+ switchE.skipNext().onEach { println("[onEach] switched-out") }
+ val stopEvent =
+ stopHandler
+ .onEach { println("[onEach] stopped") }
+ .mergeWith(nextSwitch) { _, b -> b }
+ intFlow.takeUntil(stopEvent)
+ }
+
+ val adderE: TFlow<(Int) -> Int> =
+ switchE.hold(emptyTFlow).switch().map { a ->
+ println("[onEach] new number $a")
+ ({ sum: Int ->
+ println("$a+$sum=${a + sum}")
+ sum + a
+ })
+ }
+
+ val sumD = TStateLoop<Int>()
+ sumD.loopback =
+ adderE
+ .sample(sumD) { f, sum -> f(sum) }
+ .onEach { println("[onEach] writing sum: $it") }
+ .hold(0)
+ val sumE = sumD.stateChanges
+
+ sumE.toSharedFlow()
+ }
+
+ runCurrent()
+
+ backgroundScope.launch { sumFlow.collect { currentSum = it } }
+
+ runCurrent()
+
+ switchHandler.emit(aHandler to "A")
+ runCurrent()
+
+ aHandler.emit(1)
+ runCurrent()
+
+ assertEquals(1, currentSum)
+
+ aHandler.emit(2)
+ runCurrent()
+
+ assertEquals(3, currentSum)
+
+ aHandler.emit(3)
+ runCurrent()
+
+ assertEquals(6, currentSum)
+
+ aHandler.emit(4)
+ runCurrent()
+
+ assertEquals(10, currentSum)
+
+ aHandler.emit(5)
+ runCurrent()
+
+ assertEquals(15, currentSum)
+
+ switchHandler.emit(bHandler to "B")
+ runCurrent()
+
+ aHandler.emit(6)
+ runCurrent()
+
+ assertEquals(15, currentSum)
+
+ bHandler.emit(6)
+ runCurrent()
+
+ assertEquals(21, currentSum)
+
+ bHandler.emit(7)
+ runCurrent()
+
+ assertEquals(28, currentSum)
+
+ bHandler.emit(8)
+ runCurrent()
+
+ assertEquals(36, currentSum)
+
+ bHandler.emit(9)
+ runCurrent()
+
+ assertEquals(45, currentSum)
+
+ bHandler.emit(10)
+ runCurrent()
+
+ assertEquals(55, currentSum)
+
+ println()
+ println("Stopping: B")
+ stopHandler.emit(Unit) // bHandler.complete()
+ runCurrent()
+
+ bHandler.emit(20)
+ runCurrent()
+
+ assertEquals(55, currentSum)
+
+ println()
+ println("Switching to: A2")
+ switchHandler.emit(aHandler to "A2")
+ runCurrent()
+
+ println("aHandler.emit(11)")
+ aHandler.emit(11)
+ runCurrent()
+
+ assertEquals(66, currentSum)
+
+ aHandler.emit(12)
+ runCurrent()
+
+ assertEquals(78, currentSum)
+
+ aHandler.emit(13)
+ runCurrent()
+
+ assertEquals(91, currentSum)
+
+ aHandler.emit(14)
+ runCurrent()
+
+ assertEquals(105, currentSum)
+
+ aHandler.emit(15)
+ runCurrent()
+
+ assertEquals(120, currentSum)
+
+ stopHandler.emit(Unit)
+ runCurrent()
+
+ aHandler.emit(100)
+ runCurrent()
+
+ assertEquals(120, currentSum)
+ }
+
+ @Test
+ fun switchIndirect() = runFrpTest { network ->
+ val emitter = network.mutableTFlow<Unit>()
+ activateSpec(network) {
+ emptyTFlow.map { emitter.map { 1 } }.flatten().map { "$it" }.observe()
+ }
+ runCurrent()
+ }
+
+ @Test
+ fun switchInWithResult() = runFrpTest { network ->
+ val emitter = network.mutableTFlow<Unit>()
+ val out =
+ activateSpecWithResult(network) {
+ emitter.map { emitter.map { 1 } }.flatten().toSharedFlow()
+ }
+ val result = out.stateIn(backgroundScope, SharingStarted.Eagerly, null)
+ runCurrent()
+ emitter.emit(Unit)
+ runCurrent()
+ assertEquals(null, result.value)
+ }
+
+ @Test
+ fun switchInCompleted() = runFrpTest { network ->
+ val outputs = mutableListOf<Int>()
+
+ val switchAH = network.mutableTFlow<Unit>()
+ val intAH = network.mutableTFlow<Int>()
+ val stopEmitter = network.mutableTFlow<Unit>()
+
+ val top = frpSpec {
+ val intS = intAH.takeUntil(stopEmitter)
+ val switched = switchAH.map { intS }.flatten()
+ switched.toSharedFlow()
+ }
+ val flow = activateSpecWithResult(network) { top.applySpec() }
+ backgroundScope.launch { flow.collect { outputs.add(it) } }
+ runCurrent()
+
+ switchAH.emit(Unit)
+ runCurrent()
+
+ stopEmitter.emit(Unit)
+ runCurrent()
+
+ // assertEquals(0, intAH.subscriptionCount.value)
+ intAH.emit(10)
+ runCurrent()
+
+ assertEquals(true, outputs.isEmpty())
+
+ switchAH.emit(Unit)
+ runCurrent()
+
+ // assertEquals(0, intAH.subscriptionCount.value)
+ intAH.emit(10)
+ runCurrent()
+
+ assertEquals(true, outputs.isEmpty())
+ }
+
+ @Test
+ fun switchTFlow_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 flow =
+ activateSpecWithResult(network) {
+ val intS = intAH.takeUntil(intStopEmitter)
+ val switchS = switchAH.takeUntil(switchStopEmitter)
+
+ val switched = switchS.map { intS }.flatten()
+ switched.toSharedFlow()
+ }
+ backgroundScope.launch { flow.collect { stepResult = it } }
+ runCurrent()
+
+ // assertEquals(0, intAH.subscriptionCount.value)
+ intAH.emit(100)
+ runCurrent()
+
+ assertEquals(null, stepResult)
+
+ switchAH.emit(Unit)
+ runCurrent()
+
+ // assertEquals(1, intAH.subscriptionCount.value)
+
+ intAH.emit(5)
+ runCurrent()
+
+ assertEquals(5, stepResult)
+
+ println("stop outer")
+ switchStopEmitter.emit(Unit) // switchAH.complete()
+ runCurrent()
+
+ // assertEquals(1, intAH.subscriptionCount.value)
+ // assertEquals(0, switchAH.subscriptionCount.value)
+
+ intAH.emit(10)
+ runCurrent()
+
+ assertEquals(10, stepResult)
+
+ println("stop inner")
+ intStopEmitter.emit(Unit) // intAH.complete()
+ runCurrent()
+
+ // assertEquals(just(10), network.await())
+ }
+
+ @Test
+ fun mapTFlow() = runFrpTest { network ->
+ val emitter = network.mutableTFlow<Int>()
+ var stepResult: Int? = null
+
+ val flow =
+ activateSpecWithResult(network) {
+ val mappedS = emitter.map { it * it }
+ mappedS.toSharedFlow()
+ }
+
+ backgroundScope.launch { flow.collect { stepResult = it } }
+ runCurrent()
+
+ emitter.emit(1)
+ runCurrent()
+
+ assertEquals(1, stepResult)
+
+ emitter.emit(2)
+ runCurrent()
+
+ assertEquals(4, stepResult)
+
+ emitter.emit(10)
+ runCurrent()
+
+ assertEquals(100, stepResult)
+ }
+
+ @Test
+ fun mapTransactional() = runFrpTest { network ->
+ var doubledResult: Int? = null
+ var pullValue = 0
+ val a = transactionally { pullValue }
+ val b = transactionally { a.sample() * 2 }
+ val emitter = network.mutableTFlow<Unit>()
+ val flow =
+ activateSpecWithResult(network) {
+ val sampleB = emitter.sample(b) { _, b -> b }
+ sampleB.toSharedFlow()
+ }
+
+ backgroundScope.launch { flow.collect { doubledResult = it } }
+
+ runCurrent()
+
+ emitter.emit(Unit)
+ runCurrent()
+
+ assertEquals(0, doubledResult)
+
+ pullValue = 5
+ emitter.emit(Unit)
+ runCurrent()
+
+ assertEquals(10, doubledResult)
+ }
+
+ @Test
+ fun mapTState() = runFrpTest { network ->
+ val emitter = network.mutableTFlow<Int>()
+ var stepResult: Int? = null
+ val flow =
+ activateSpecWithResult(network) {
+ val state = emitter.hold(0).map { it + 2 }
+ val stateCurrent = transactionally { state.sample() }
+ val stateChanges = state.stateChanges
+ val sampleState = emitter.sample(stateCurrent) { _, b -> b }
+ val merge = stateChanges.mergeWith(sampleState) { a, b -> a + b }
+ merge.toSharedFlow()
+ }
+ backgroundScope.launch { flow.collect { stepResult = it } }
+ runCurrent()
+
+ emitter.emit(1)
+ runCurrent()
+
+ assertEquals(5, stepResult)
+
+ emitter.emit(10)
+ runCurrent()
+
+ assertEquals(15, stepResult)
+ }
+
+ @Test
+ fun partitionEither() = runFrpTest { network ->
+ val emitter = network.mutableTFlow<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()
+ }
+ runCurrent()
+
+ emitter.emit(Left(10))
+ runCurrent()
+
+ assertEquals(20, result.value)
+
+ emitter.emit(Right(30))
+ runCurrent()
+
+ assertEquals(-30, result.value)
+ }
+
+ @Test
+ fun accumTState() = runFrpTest { network ->
+ val emitter = network.mutableTFlow<Int>()
+ val sampler = network.mutableTFlow<Unit>()
+ var stepResult: Int? = null
+ val flow =
+ activateSpecWithResult(network) {
+ val sumState = emitter.map { a -> { b: Int -> a + b } }.fold(0) { f, a -> f(a) }
+
+ sumState.stateChanges
+ .mergeWith(sampler.sample(sumState) { _, sum -> sum }) { _, _ ->
+ error("Unexpected coincidence")
+ }
+ .toSharedFlow()
+ }
+
+ backgroundScope.launch { flow.collect { stepResult = it } }
+ runCurrent()
+
+ emitter.emit(5)
+ runCurrent()
+ assertEquals(5, stepResult)
+
+ emitter.emit(10)
+ runCurrent()
+ assertEquals(15, stepResult)
+
+ sampler.emit(Unit)
+ runCurrent()
+ assertEquals(15, stepResult)
+ }
+
+ @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>()
+ var stepResult: Int? = null
+
+ val flow: SharedFlow<Int>
+ val setupDuration = measureTime {
+ flow =
+ activateSpecWithResult(network) {
+ val firstS = first.takeUntil(stopFirst)
+ val secondS = second.takeUntil(stopSecond)
+ val mergedS =
+ firstS.mergeWith(secondS) { _, _ -> error("Unexpected coincidence") }
+ mergedS.toSharedFlow()
+ // mergedS.last("onComplete")
+ }
+ backgroundScope.launch { flow.collect { stepResult = it } }
+ runCurrent()
+ }
+
+ // assertEquals(1, first.subscriptionCount.value)
+ // assertEquals(1, second.subscriptionCount.value)
+
+ val firstEmitDuration = measureTime {
+ first.emit(1)
+ runCurrent()
+ }
+
+ assertEquals(1, stepResult)
+
+ val secondEmitDuration = measureTime {
+ second.emit(2)
+ runCurrent()
+ }
+
+ assertEquals(2, stepResult)
+
+ val stopFirstDuration = measureTime {
+ stopFirst.emit(Unit)
+ runCurrent()
+ }
+
+ // assertEquals(0, first.subscriptionCount.value)
+ val testDeadEmitFirstDuration = measureTime {
+ first.emit(10)
+ runCurrent()
+ }
+
+ assertEquals(2, stepResult)
+
+ // assertEquals(1, second.subscriptionCount.value)
+
+ val secondEmitDuration2 = measureTime {
+ second.emit(3)
+ runCurrent()
+ }
+
+ assertEquals(3, stepResult)
+
+ val stopSecondDuration = measureTime {
+ stopSecond.emit(Unit)
+ runCurrent()
+ }
+
+ // assertEquals(0, second.subscriptionCount.value)
+ val testDeadEmitSecondDuration = measureTime {
+ second.emit(10)
+ runCurrent()
+ }
+
+ assertEquals(3, stepResult)
+
+ println(
+ """
+ setupDuration: ${setupDuration.toString(DurationUnit.MILLISECONDS, 2)}
+ firstEmitDuration: ${firstEmitDuration.toString(DurationUnit.MILLISECONDS, 2)}
+ secondEmitDuration: ${secondEmitDuration.toString(DurationUnit.MILLISECONDS, 2)}
+ stopFirstDuration: ${stopFirstDuration.toString(DurationUnit.MILLISECONDS, 2)}
+ testDeadEmitFirstDuration: ${
+ testDeadEmitFirstDuration.toString(
+ DurationUnit.MILLISECONDS,
+ 2,
+ )
+ }
+ secondEmitDuration2: ${secondEmitDuration2.toString(DurationUnit.MILLISECONDS, 2)}
+ stopSecondDuration: ${stopSecondDuration.toString(DurationUnit.MILLISECONDS, 2)}
+ testDeadEmitSecondDuration: ${
+ testDeadEmitSecondDuration.toString(
+ DurationUnit.MILLISECONDS,
+ 2,
+ )
+ }
+ """
+ .trimIndent()
+ )
+ }
+
+ @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>()
+ var stepResult: Int? = null
+ val flow =
+ activateSpecWithResult(network) {
+ val stopSamplerFirst = stopSampler
+ val samplerS = sampler.takeUntil(stopSamplerFirst)
+ val stopUpdaterFirst = stopUpdater
+ val updaterS = updater.takeUntil(stopUpdaterFirst)
+ val sampledS = samplerS.sample(updaterS.hold(0)) { _, b -> b }
+ sampledS.toSharedFlow()
+ }
+
+ backgroundScope.launch { flow.collect { stepResult = it } }
+ runCurrent()
+
+ updater.emit(1)
+ runCurrent()
+
+ sampler.emit(Unit)
+ runCurrent()
+
+ assertEquals(1, stepResult)
+
+ stopSampler.emit(Unit)
+ runCurrent()
+
+ // assertEquals(0, updater.subscriptionCount.value)
+ // assertEquals(0, sampler.subscriptionCount.value)
+ updater.emit(10)
+ runCurrent()
+
+ sampler.emit(Unit)
+ runCurrent()
+
+ assertEquals(1, stepResult)
+ }
+
+ @Test
+ fun combineStates_differentUpstreams() = runFrpTest { network ->
+ val a = network.mutableTFlow<Int>()
+ val b = network.mutableTFlow<Int>()
+ var observed: Pair<Int, Int>? = null
+ val tState =
+ activateSpecWithResult(network) {
+ val state = combine(a.hold(0), b.hold(0)) { a, b -> Pair(a, b) }
+ state.stateChanges.observe { observed = it }
+ state
+ }
+ assertEquals(0 to 0, network.transact { tState.sample() })
+ assertEquals(null, observed)
+ a.emit(5)
+ assertEquals(5 to 0, observed)
+ assertEquals(5 to 0, network.transact { tState.sample() })
+ b.emit(3)
+ assertEquals(5 to 3, observed)
+ assertEquals(5 to 3, network.transact { tState.sample() })
+ }
+
+ @Test
+ fun sampleCombinedStates() = runFrpTest { network ->
+ val updater = network.mutableTFlow<Int>()
+ val emitter = network.mutableTFlow<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 sampleS = emitter.sample(combineD) { _, b -> b }
+ sampleS.nextDeferred()
+ }
+ println("launching")
+ runCurrent()
+
+ println("emitting update")
+ updater.emit(10)
+ runCurrent()
+
+ println("emitting sampler")
+ emitter.emit(Unit)
+ runCurrent()
+
+ println("asserting")
+ assertEquals(20 to 10, result.await())
+ }
+
+ @Test
+ fun switchMapPromptly() = runFrpTest { network ->
+ val emitter = network.mutableTFlow<Unit>()
+ val result =
+ activateSpecWithResult(network) {
+ emitter
+ .map { emitter.map { 1 }.map { it + 1 }.map { it * 2 } }
+ .hold(emptyTFlow)
+ .switchPromptly()
+ .nextDeferred()
+ }
+ runCurrent()
+
+ emitter.emit(Unit)
+ runCurrent()
+
+ assertTrue("Not complete", result.isCompleted)
+ assertEquals(4, result.await())
+ }
+
+ @Test
+ fun switchDeeper() = runFrpTest { network ->
+ val emitter = network.mutableTFlow<Unit>()
+ val e2 = network.mutableTFlow<Unit>()
+ val result =
+ activateSpecWithResult(network) {
+ val tres =
+ merge(e2.map { 1 }, e2.map { 2 }, transformCoincidence = { a, b -> a + b })
+ tres.observeBuild()
+ val switch = emitter.map { tres }.flatten()
+ merge(switch, e2.map { null }, transformCoincidence = { a, _ -> a })
+ .filterNotNull()
+ .nextDeferred()
+ }
+ runCurrent()
+
+ emitter.emit(Unit)
+ runCurrent()
+
+ e2.emit(Unit)
+ runCurrent()
+
+ assertTrue("Not complete", result.isCompleted)
+ assertEquals(3, result.await())
+ }
+
+ @Test
+ fun recursionBasic() = runFrpTest { network ->
+ val add1 = network.mutableTFlow<Unit>()
+ val sub1 = network.mutableTFlow<Unit>()
+ val stepResult: StateFlow<Int> =
+ activateSpecWithResult(network) {
+ val dSum = TStateLoop<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.toStateFlow()
+ }
+ runCurrent()
+
+ add1.emit(Unit)
+ runCurrent()
+
+ assertEquals(1, stepResult.value)
+
+ add1.emit(Unit)
+ runCurrent()
+
+ assertEquals(2, stepResult.value)
+
+ sub1.emit(Unit)
+ runCurrent()
+
+ assertEquals(1, stepResult.value)
+ }
+
+ @Test
+ fun recursiveTState() = runFrpTest { network ->
+ val e = network.mutableTFlow<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
+ flattened.toStateFlow()
+ }
+
+ backgroundScope.launch { state.collect { changes++ } }
+ runCurrent()
+ }
+
+ @Test
+ fun fanOut() = runFrpTest { network ->
+ val e = network.mutableTFlow<Map<String, Int>>()
+ val (fooFlow, barFlow) =
+ activateSpecWithResult(network) {
+ val selector = e.groupByKey()
+ val foos = selector.eventsForKey("foo")
+ val bars = selector.eventsForKey("bar")
+ foos.toSharedFlow() to bars.toSharedFlow()
+ }
+ val stateFlow = fooFlow.stateIn(backgroundScope, SharingStarted.Eagerly, null)
+ backgroundScope.launch { barFlow.collect { error("unexpected bar") } }
+ runCurrent()
+
+ assertEquals(null, stateFlow.value)
+
+ e.emit(mapOf("foo" to 1))
+ runCurrent()
+
+ assertEquals(1, stateFlow.value)
+ }
+
+ @Test
+ fun fanOutLateSubscribe() = runFrpTest { network ->
+ val e = network.mutableTFlow<Map<String, Int>>()
+ val barFlow =
+ activateSpecWithResult(network) {
+ val selector = e.groupByKey()
+ selector
+ .eventsForKey("foo")
+ .map { selector.eventsForKey("bar") }
+ .hold(emptyTFlow)
+ .switchPromptly()
+ .toSharedFlow()
+ }
+ val stateFlow = barFlow.stateIn(backgroundScope, SharingStarted.Eagerly, null)
+ runCurrent()
+
+ assertEquals(null, stateFlow.value)
+
+ e.emit(mapOf("foo" to 0, "bar" to 1))
+ runCurrent()
+
+ assertEquals(1, stateFlow.value)
+ }
+
+ @Test
+ fun inputFlowCompleted() = runFrpTest { network ->
+ val results = mutableListOf<Int>()
+ val e = network.mutableTFlow<Int>()
+ activateSpec(network) { e.nextOnly().observe { results.add(it) } }
+ runCurrent()
+
+ e.emit(10)
+ runCurrent()
+
+ assertEquals(listOf(10), results)
+
+ e.emit(20)
+ runCurrent()
+ assertEquals(listOf(10), results)
+ }
+
+ @Test
+ fun fanOutThenMergeIncrementally() = runFrpTest { network ->
+ // A tflow of group updates, where a group is a tflow of child updates, where a child is a
+ // stateflow
+ val e = network.mutableTFlow<Map<Int, Maybe<TFlow<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>>>>>>> =
+ e.mapBuild { m ->
+ m.mapValues { (_, mFlow) ->
+ mFlow.map {
+ it.mapBuild { m2 ->
+ m2.mapValues { (_, mState) ->
+ mState.map { stateFlow -> stateFlow.toTState() }
+ }
+ }
+ }
+ }
+ }
+ // Accumulate all of our updates into a single TState
+ val accState: TState<Map<Int, Map<Int, String>>> =
+ emitter
+ .mapStateful {
+ changeMap: Map<Int, Maybe<TFlow<Map<Int, Maybe<TState<String>>>>>> ->
+ changeMap.mapValues { (groupId, mGroupChanges) ->
+ mGroupChanges.map {
+ groupChanges: TFlow<Map<Int, Maybe<TState<String>>>> ->
+ // New group
+ val childChangeById = groupChanges.groupByKey()
+ val map: TFlow<Map<Int, Maybe<TFlow<Maybe<TState<String>>>>>> =
+ groupChanges.mapStateful {
+ gChangeMap: Map<Int, Maybe<TState<String>>> ->
+ gChangeMap.mapValues { (childId, mChild) ->
+ mChild.map { child: TState<String> ->
+ println("new child $childId in the house")
+ // New child
+ val eRemoved =
+ childChangeById
+ .eventsForKey(childId)
+ .filter { it === None }
+ .nextOnly()
+
+ val addChild: TFlow<Maybe<TState<String>>> =
+ now.map { mChild }
+ .onEach {
+ println(
+ "addChild (groupId=$groupId, childId=$childId) ${child.sample()}"
+ )
+ }
+
+ val removeChild: TFlow<Maybe<TState<String>>> =
+ eRemoved
+ .onEach {
+ println(
+ "removeChild (groupId=$groupId, childId=$childId)"
+ )
+ }
+ .map { none() }
+
+ addChild.mergeWith(removeChild) { _, _ ->
+ error("unexpected coincidence")
+ }
+ }
+ }
+ }
+ val mergeIncrementally: TFlow<Map<Int, Maybe<TState<String>>>> =
+ map.onEach { println("merge patch: $it") }
+ .mergeIncrementallyPromptly()
+ mergeIncrementally
+ .onEach { println("patch: $it") }
+ .foldMapIncrementally()
+ .flatMap { it.combineValues() }
+ }
+ }
+ }
+ .foldMapIncrementally()
+ .flatMap { it.combineValues() }
+
+ accState.toStateFlow()
+ }
+ runCurrent()
+
+ assertEquals(emptyMap(), state.value)
+
+ val emitter2 = network.mutableTFlow<Map<Int, Maybe<StateFlow<String>>>>()
+ println()
+ println("init outer 0")
+ e.emit(mapOf(0 to just(emitter2.onEach { println("emitter2 emit: $it") })))
+ runCurrent()
+
+ assertEquals(mapOf(0 to emptyMap()), state.value)
+
+ println()
+ println("init inner 10")
+ emitter2.emit(mapOf(10 to just(MutableStateFlow("(0, 10)"))))
+ runCurrent()
+
+ assertEquals(mapOf(0 to mapOf(10 to "(0, 10)")), state.value)
+
+ // replace
+ println()
+ println("replace inner 10")
+ emitter2.emit(mapOf(10 to just(MutableStateFlow("(1, 10)"))))
+ runCurrent()
+
+ assertEquals(mapOf(0 to mapOf(10 to "(1, 10)")), state.value)
+
+ // remove
+ emitter2.emit(mapOf(10 to none()))
+ runCurrent()
+
+ assertEquals(mapOf(0 to emptyMap()), state.value)
+
+ // add again
+ emitter2.emit(mapOf(10 to just(MutableStateFlow("(2, 10)"))))
+ runCurrent()
+
+ assertEquals(mapOf(0 to mapOf(10 to "(2, 10)")), state.value)
+
+ // batch update
+ emitter2.emit(
+ mapOf(
+ 10 to none(),
+ 11 to just(MutableStateFlow("(0, 11)")),
+ 12 to just(MutableStateFlow("(0, 12)")),
+ )
+ )
+ runCurrent()
+
+ assertEquals(mapOf(0 to mapOf(11 to "(0, 11)", 12 to "(0, 12)")), state.value)
+ }
+
+ @Test
+ fun applyLatestNetworkChanges() = runFrpTest { network ->
+ val newCount = network.mutableTFlow<FrpSpec<Flow<Int>>>()
+ val flowOfFlows: Flow<Flow<Int>> =
+ activateSpecWithResult(network) { newCount.applyLatestSpec().toSharedFlow() }
+ runCurrent()
+
+ val incCount = network.mutableTFlow<Unit>()
+ fun newFlow(): FrpSpec<SharedFlow<Int>> = frpSpec {
+ launchEffect {
+ try {
+ println("new flow!")
+ awaitCancellation()
+ } finally {
+ println("cancelling old flow")
+ }
+ }
+ lateinit var count: TState<Int>
+ count =
+ incCount
+ .onEach { println("incrementing ${count.sample()}") }
+ .fold(0) { _, c -> c + 1 }
+ count.stateChanges.toSharedFlow()
+ }
+
+ var outerCount = 0
+ val lastFlows: StateFlow<Pair<StateFlow<Int?>, StateFlow<Int?>>> =
+ flowOfFlows
+ .map { it.stateIn(backgroundScope, SharingStarted.Eagerly, null) }
+ .pairwise(MutableStateFlow(null))
+ .onEach { outerCount++ }
+ .stateIn(
+ backgroundScope,
+ SharingStarted.Eagerly,
+ MutableStateFlow(null) to MutableStateFlow(null),
+ )
+
+ runCurrent()
+
+ newCount.emit(newFlow())
+ runCurrent()
+
+ assertEquals(1, outerCount)
+ // assertEquals(1, incCount.subscriptionCount)
+ assertNull(lastFlows.value.second.value)
+
+ incCount.emit(Unit)
+ runCurrent()
+
+ println("checking")
+ assertEquals(1, lastFlows.value.second.value)
+
+ incCount.emit(Unit)
+ runCurrent()
+
+ assertEquals(2, lastFlows.value.second.value)
+
+ newCount.emit(newFlow())
+ runCurrent()
+ incCount.emit(Unit)
+ runCurrent()
+
+ // verify old flow is not getting updates
+ assertEquals(2, lastFlows.value.first.value)
+ // but the new one is
+ assertEquals(1, lastFlows.value.second.value)
+ }
+
+ @Test
+ fun effect() = runFrpTest { network ->
+ val input = network.mutableTFlow<Unit>()
+ var effectRunning = false
+ var count = 0
+ activateSpec(network) {
+ val j = launchEffect {
+ effectRunning = true
+ try {
+ awaitCancellation()
+ } finally {
+ effectRunning = false
+ }
+ }
+ merge(emptyTFlow, input.nextOnly()).observe {
+ count++
+ j.cancel()
+ }
+ }
+ runCurrent()
+ assertEquals(true, effectRunning)
+ assertEquals(0, count)
+
+ println("1")
+ input.emit(Unit)
+ assertEquals(false, effectRunning)
+ assertEquals(1, count)
+
+ println("2")
+ input.emit(Unit)
+ assertEquals(1, count)
+ println("3")
+ input.emit(Unit)
+ assertEquals(1, count)
+ }
+
+ private fun runFrpTest(
+ timeout: Duration = 3.seconds,
+ block: suspend TestScope.(FrpNetwork) -> Unit,
+ ) {
+ runTest(timeout = timeout) {
+ val network = backgroundScope.newFrpNetwork()
+ runCurrent()
+ block(network)
+ }
+ }
+
+ private fun TestScope.activateSpec(network: FrpNetwork, spec: FrpSpec<*>) =
+ backgroundScope.launch { network.activateSpec(spec) }
+
+ private suspend fun <R> TestScope.activateSpecWithResult(
+ network: FrpNetwork,
+ spec: FrpSpec<R>,
+ ): R =
+ CompletableDeferred<R>()
+ .apply { activateSpec(network) { complete(spec.applySpec()) } }
+ .await()
+}
+
+private fun <T> assertEquals(expected: T, actual: T) =
+ org.junit.Assert.assertEquals(expected, actual)
+
+private fun <A> Flow<A>.pairwise(init: A): Flow<Pair<A, A>> = flow {
+ var prev = init
+ collect {
+ emit(prev to it)
+ prev = it
+ }
+}