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