• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.kairos
18 
19 import com.android.systemui.kairos.internal.CompletableLazy
20 import com.android.systemui.kairos.internal.EventsImpl
21 import com.android.systemui.kairos.internal.Init
22 import com.android.systemui.kairos.internal.InitScope
23 import com.android.systemui.kairos.internal.InputNode
24 import com.android.systemui.kairos.internal.Network
25 import com.android.systemui.kairos.internal.NoScope
26 import com.android.systemui.kairos.internal.activated
27 import com.android.systemui.kairos.internal.cached
28 import com.android.systemui.kairos.internal.constInit
29 import com.android.systemui.kairos.internal.init
30 import com.android.systemui.kairos.internal.mapImpl
31 import com.android.systemui.kairos.internal.neverImpl
32 import com.android.systemui.kairos.internal.util.hashString
33 import com.android.systemui.kairos.util.Maybe
34 import com.android.systemui.kairos.util.toMaybe
35 import java.util.concurrent.atomic.AtomicReference
36 import kotlin.reflect.KProperty
37 import kotlinx.coroutines.CoroutineStart
38 import kotlinx.coroutines.Job
39 import kotlinx.coroutines.async
40 import kotlinx.coroutines.coroutineScope
41 
42 /**
43  * A series of values of type [A] available at discrete points in time.
44  *
45  * [Events] follow these rules:
46  * 1. Within a single Kairos network transaction, an [Events] instance will only emit *once*.
47  * 2. The order that different [Events] instances emit values within a transaction is undefined, and
48  *    are conceptually *simultaneous*.
49  * 3. [Events] emissions are *ephemeral* and do not last beyond the transaction they are emitted,
50  *    unless explicitly [observed][BuildScope.observe] or [held][StateScope.holdState] as a [State].
51  */
52 @ExperimentalKairosApi
53 sealed class Events<out A> {
54     companion object {
55         /** An [Events] with no values. */
56         val empty: Events<Nothing> = EmptyEvents
57     }
58 }
59 
60 /** An [Events] with no values. */
61 @ExperimentalKairosApi val emptyEvents: Events<Nothing> = Events.empty
62 
63 /**
64  * A forward-reference to an [Events]. Useful for recursive definitions.
65  *
66  * This reference can be used like a standard [Events], but will throw an error if its [loopback] is
67  * unset before it is [observed][BuildScope.observe].
68  *
69  * @sample com.android.systemui.kairos.KairosSamples.eventsLoop
70  */
71 @ExperimentalKairosApi
72 class EventsLoop<A> : Events<A>() {
73     private val deferred = CompletableLazy<Events<A>>()
74 
75     internal val init: Init<EventsImpl<A>> =
<lambda>null76         init(name = null) { deferred.value.init.connect(evalScope = this) }
77 
78     /**
79      * The [Events] this reference is referring to. Must be set before this [EventsLoop] is
80      * [observed][BuildScope.observe].
81      */
82     var loopback: Events<A>? = null
83         set(value) {
<lambda>null84             value?.let {
85                 check(!deferred.isInitialized()) { "EventsLoop.loopback has already been set." }
86                 deferred.setValue(value)
87                 field = value
88             }
89         }
90 
getValuenull91     operator fun getValue(thisRef: Any?, property: KProperty<*>): Events<A> = this
92 
93     operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Events<A>) {
94         loopback = value
95     }
96 
toStringnull97     override fun toString(): String = "${this::class.simpleName}@$hashString"
98 }
99 
100 /**
101  * Returns an [Events] that acts as a deferred-reference to the [Events] produced by this [Lazy].
102  *
103  * When the returned [Events] is accessed by the Kairos network, the [Lazy]'s [value][Lazy.value]
104  * will be queried and used.
105  *
106  * Useful for recursive definitions.
107  *
108  * ```
109  *   fun <A> Lazy<Events<A>>.defer() = deferredEvents { value }
110  * ```
111  *
112  * @see deferredEvents
113  */
114 @ExperimentalKairosApi fun <A> Lazy<Events<A>>.defer(): Events<A> = deferInline { value }
115 
116 /**
117  * Returns an [Events] that acts as a deferred-reference to the [Events] produced by this
118  * [DeferredValue].
119  *
120  * When the returned [Events] is accessed by the Kairos network, the [DeferredValue] will be queried
121  * and used.
122  *
123  * Useful for recursive definitions.
124  *
125  * ```
126  *   fun <A> DeferredValue<Events<A>>.defer() = deferredEvents { get() }
127  * ```
128  *
129  * @see deferredEvents
130  */
131 @ExperimentalKairosApi
<lambda>null132 fun <A> DeferredValue<Events<A>>.defer(): Events<A> = deferInline { unwrapped.value }
133 
134 /**
135  * Returns an [Events] that acts as a deferred-reference to the [Events] produced by [block].
136  *
137  * When the returned [Events] is accessed by the Kairos network, [block] will be invoked and the
138  * returned [Events] will be used.
139  *
140  * Useful for recursive definitions.
141  */
142 @ExperimentalKairosApi
<lambda>null143 fun <A> deferredEvents(block: KairosScope.() -> Events<A>): Events<A> = deferInline {
144     NoScope.block()
145 }
146 
147 /**
148  * Returns an [Events] that contains only the
149  * [present][com.android.systemui.kairos.util.Maybe.present] results of applying [transform] to each
150  * value of the original [Events].
151  *
152  * @sample com.android.systemui.kairos.KairosSamples.mapMaybe
153  * @see mapNotNull
154  */
155 @ExperimentalKairosApi
mapMaybenull156 fun <A, B> Events<A>.mapMaybe(transform: TransactionScope.(A) -> Maybe<B>): Events<B> =
157     map(transform).filterPresent()
158 
159 /**
160  * Returns an [Events] that contains only the non-null results of applying [transform] to each value
161  * of the original [Events].
162  *
163  * ```
164  *  fun <A> Events<A>.mapNotNull(transform: TransactionScope.(A) -> B?): Events<B> =
165  *      mapMaybe { if (it == null) absent else present(it) }
166  * ```
167  *
168  * @see mapMaybe
169  */
170 @ExperimentalKairosApi
171 fun <A, B> Events<A>.mapNotNull(transform: TransactionScope.(A) -> B?): Events<B> = mapMaybe {
172     transform(it).toMaybe()
173 }
174 
175 /**
176  * Returns an [Events] containing the results of applying [transform] to each value of the original
177  * [Events].
178  *
179  * @sample com.android.systemui.kairos.KairosSamples.mapEvents
180  */
181 @ExperimentalKairosApi
mapnull182 fun <A, B> Events<A>.map(transform: TransactionScope.(A) -> B): Events<B> {
183     val mapped: EventsImpl<B> = mapImpl({ init.connect(evalScope = this) }) { a, _ -> transform(a) }
184     return EventsInit(constInit(name = null, mapped.cached()))
185 }
186 
187 /**
188  * Like [map], but the emission is not cached during the transaction. Use only if [transform] is
189  * fast and pure.
190  *
191  * @sample com.android.systemui.kairos.KairosSamples.mapCheap
192  * @see map
193  */
194 @ExperimentalKairosApi
mapCheapnull195 fun <A, B> Events<A>.mapCheap(transform: TransactionScope.(A) -> B): Events<B> =
196     EventsInit(
197         constInit(name = null, mapImpl({ init.connect(evalScope = this) }) { a, _ -> transform(a) })
198     )
199 
200 /**
201  * Returns an [Events] that invokes [action] before each value of the original [Events] is emitted.
202  * Useful for logging and debugging.
203  *
204  * ```
205  *   fun <A> Events<A>.onEach(action: TransactionScope.(A) -> Unit): Events<A> =
206  *       map { it.also { action(it) } }
207  * ```
208  *
209  * Note that the side effects performed in [onEach] are only performed while the resulting [Events]
210  * is connected to an output of the Kairos network. If your goal is to reliably perform side effects
211  * in response to an [Events], use the output combinators available in [BuildScope], such as
212  * [BuildScope.toSharedFlow] or [BuildScope.observe].
213  */
214 @ExperimentalKairosApi
<lambda>null215 fun <A> Events<A>.onEach(action: TransactionScope.(A) -> Unit): Events<A> = map {
216     it.also { action(it) }
217 }
218 
219 /**
220  * Splits an [Events] of pairs into a pair of [Events], where each returned [Events] emits half of
221  * the original.
222  *
223  * ```
224  *   fun <A, B> Events<Pair<A, B>>.unzip(): Pair<Events<A>, Events<B>> {
225  *       val lefts = map { it.first }
226  *       val rights = map { it.second }
227  *       return lefts to rights
228  *   }
229  * ```
230  */
231 @ExperimentalKairosApi
unzipnull232 fun <A, B> Events<Pair<A, B>>.unzip(): Pair<Events<A>, Events<B>> {
233     val lefts = map { it.first }
234     val rights = map { it.second }
235     return lefts to rights
236 }
237 
238 /**
239  * A mutable [Events] that provides the ability to [emit] values to the network, handling
240  * backpressure by coalescing all emissions into batches.
241  *
242  * @see KairosNetwork.coalescingMutableEvents
243  */
244 @ExperimentalKairosApi
245 class CoalescingMutableEvents<in In, Out>
246 internal constructor(
247     internal val name: String?,
248     internal val coalesce: (old: Lazy<Out>, new: In) -> Out,
249     internal val network: Network,
250     private val getInitialValue: () -> Out,
251     internal val impl: InputNode<Out> = InputNode(),
252 ) : Events<Out>() {
<lambda>null253     private val storage = AtomicReference(false to lazy { getInitialValue() })
254 
toStringnull255     override fun toString(): String = "${this::class.simpleName}@$hashString"
256 
257     /**
258      * Inserts [value] into the current batch, enqueueing it for emission from this [Events] if not
259      * already pending.
260      *
261      * Backpressure occurs when [emit] is called while the Kairos network is currently in a
262      * transaction; if called multiple times, then emissions will be coalesced into a single batch
263      * that is then processed when the network is ready.
264      */
265     fun emit(value: In) {
266         val (scheduled, _) =
267             storage.getAndUpdate { (_, batch) -> true to CompletableLazy(coalesce(batch, value)) }
268         if (!scheduled) {
269             @Suppress("DeferredResultUnused")
270             network.transaction(
271                 "CoalescingMutableEvents${name?.let { "($name)" }.orEmpty()}.emit"
272             ) {
273                 val (_, batch) = storage.getAndSet(false to lazy { getInitialValue() })
274                 impl.visit(this, batch.value)
275             }
276         }
277     }
278 }
279 
280 /**
281  * A mutable [Events] that provides the ability to [emit] values to the network, handling
282  * backpressure by suspending the emitter.
283  *
284  * @see KairosNetwork.coalescingMutableEvents
285  */
286 @ExperimentalKairosApi
287 class MutableEvents<T>
288 internal constructor(internal val network: Network, internal val impl: InputNode<T> = InputNode()) :
289     Events<T>() {
290     internal val name: String? = null
291 
292     private val storage = AtomicReference<Job?>(null)
293 
toStringnull294     override fun toString(): String = "${this::class.simpleName}@$hashString"
295 
296     /**
297      * Emits a [value] to this [Events], suspending the caller until the Kairos transaction
298      * containing the emission has completed.
299      */
300     suspend fun emit(value: T) {
301         coroutineScope {
302             var jobOrNull: Job? = null
303             val newEmit =
304                 async(start = CoroutineStart.LAZY) {
305                     jobOrNull?.join()
306                     network.transaction("MutableEvents.emit") { impl.visit(this, value) }.await()
307                 }
308             jobOrNull = storage.getAndSet(newEmit)
309             newEmit.await()
310         }
311     }
312 }
313 
314 private data object EmptyEvents : Events<Nothing>()
315 
316 internal class EventsInit<out A>(val init: Init<EventsImpl<A>>) : Events<A>() {
toStringnull317     override fun toString(): String = "${this::class.simpleName}@$hashString"
318 }
319 
320 internal val <A> Events<A>.init: Init<EventsImpl<A>>
321     get() =
322         when (this) {
323             is EmptyEvents -> constInit("EmptyEvents", neverImpl)
324             is EventsInit -> init
325             is EventsLoop -> init
326             is CoalescingMutableEvents<*, A> -> constInit(name, impl.activated())
327             is MutableEvents -> constInit(name, impl.activated())
328         }
329 
deferInlinenull330 private inline fun <A> deferInline(crossinline block: InitScope.() -> Events<A>): Events<A> =
331     EventsInit(init(name = null) { block().init.connect(evalScope = this) })
332