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.BuildScopeImpl
20 import com.android.systemui.kairos.internal.Network
21 import com.android.systemui.kairos.internal.StateScopeImpl
22 import com.android.systemui.kairos.internal.util.awaitCancellationAndThen
23 import com.android.systemui.kairos.internal.util.childScope
24 import kotlin.coroutines.CoroutineContext
25 import kotlin.coroutines.EmptyCoroutineContext
26 import kotlinx.coroutines.CancellationException
27 import kotlinx.coroutines.CoroutineName
28 import kotlinx.coroutines.CoroutineScope
29 import kotlinx.coroutines.Deferred
30 import kotlinx.coroutines.Job
31 import kotlinx.coroutines.job
32 import kotlinx.coroutines.launch
33
34 /** Marks APIs that are still **experimental** and shouldn't be used in general production code. */
35 @RequiresOptIn(
36 message = "This API is experimental and should not be used in general production code."
37 )
38 @Retention(AnnotationRetention.BINARY)
39 annotation class ExperimentalKairosApi
40
41 /**
42 * External interface to a Kairos network of reactive components. Can be used to make transactional
43 * queries and modifications to the network.
44 */
45 @ExperimentalKairosApi
46 interface KairosNetwork {
47 /**
48 * Runs [block] inside of a transaction, suspending until the transaction is complete.
49 *
50 * The [BuildScope] receiver exposes methods that can be used to query or modify the network. If
51 * the network is cancelled while the caller of [transact] is suspended, then the call will be
52 * cancelled.
53 */
54 suspend fun <R> transact(block: TransactionScope.() -> R): R
55
56 /**
57 * Activates [spec] in a transaction, suspending indefinitely. While suspended, all observers
58 * and long-running effects are kept alive. When cancelled, observers are unregistered and
59 * effects are cancelled.
60 */
61 suspend fun activateSpec(spec: BuildSpec<*>)
62
63 /** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */
64 fun <In, Out> coalescingMutableEvents(
65 coalesce: (old: Out, new: In) -> Out,
66 getInitialValue: () -> Out,
67 ): CoalescingMutableEvents<In, Out>
68
69 /** Returns a [MutableState] that can emit values into this [KairosNetwork]. */
70 fun <T> mutableEvents(): MutableEvents<T>
71
72 /** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */
73 fun <T> conflatedMutableEvents(): CoalescingMutableEvents<T, T>
74
75 /** Returns a [MutableState]. with initial state [initialValue]. */
76 fun <T> mutableStateDeferred(initialValue: DeferredValue<T>): MutableState<T>
77 }
78
79 /** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */
80 @ExperimentalKairosApi
coalescingMutableEventsnull81 fun <In, Out> KairosNetwork.coalescingMutableEvents(
82 coalesce: (old: Out, new: In) -> Out,
83 initialValue: Out,
84 ): CoalescingMutableEvents<In, Out> =
85 coalescingMutableEvents(coalesce, getInitialValue = { initialValue })
86
87 /** Returns a [MutableState] with initial state [initialValue]. */
88 @ExperimentalKairosApi
mutableStatenull89 fun <T> KairosNetwork.mutableState(initialValue: T): MutableState<T> =
90 mutableStateDeferred(deferredOf(initialValue))
91
92 /** Returns a [MutableState] with initial state [initialValue]. */
93 @ExperimentalKairosApi
94 fun <T> MutableState(network: KairosNetwork, initialValue: T): MutableState<T> =
95 network.mutableState(initialValue)
96
97 /** Returns a [MutableEvents] that can emit values into this [KairosNetwork]. */
98 @ExperimentalKairosApi
99 fun <T> MutableEvents(network: KairosNetwork): MutableEvents<T> = network.mutableEvents()
100
101 /** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */
102 @ExperimentalKairosApi
103 fun <In, Out> CoalescingMutableEvents(
104 network: KairosNetwork,
105 coalesce: (old: Out, new: In) -> Out,
106 initialValue: Out,
107 ): CoalescingMutableEvents<In, Out> = network.coalescingMutableEvents(coalesce) { initialValue }
108
109 /** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */
110 @ExperimentalKairosApi
CoalescingMutableEventsnull111 fun <In, Out> CoalescingMutableEvents(
112 network: KairosNetwork,
113 coalesce: (old: Out, new: In) -> Out,
114 getInitialValue: () -> Out,
115 ): CoalescingMutableEvents<In, Out> = network.coalescingMutableEvents(coalesce, getInitialValue)
116
117 /** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */
118 @ExperimentalKairosApi
119 fun <T> ConflatedMutableEvents(network: KairosNetwork): CoalescingMutableEvents<T, T> =
120 network.conflatedMutableEvents()
121
122 /**
123 * Activates [spec] in a transaction and invokes [block] with the result, suspending indefinitely.
124 * While suspended, all observers and long-running effects are kept alive. When cancelled, observers
125 * are unregistered and effects are cancelled.
126 */
127 @ExperimentalKairosApi
128 suspend fun <R> KairosNetwork.activateSpec(spec: BuildSpec<R>, block: suspend (R) -> Unit) {
129 activateSpec {
130 val result = spec.applySpec()
131 launchEffect { block(result) }
132 }
133 }
134
135 internal class LocalNetwork(
136 private val network: Network,
137 private val scope: CoroutineScope,
138 private val endSignal: Events<Any>,
139 ) : KairosNetwork {
transactnull140 override suspend fun <R> transact(block: TransactionScope.() -> R): R =
141 network.transaction("KairosNetwork.transact") { block() }.awaitOrCancel()
142
activateSpecnull143 override suspend fun activateSpec(spec: BuildSpec<*>) {
144 val stopEmitter = conflatedMutableEvents<Unit>()
145 network
146 .transaction("KairosNetwork.activateSpec") {
147 val buildScope =
148 BuildScopeImpl(
149 stateScope =
150 StateScopeImpl(
151 evalScope = this,
152 endSignalLazy = lazy { mergeLeft(stopEmitter, endSignal) },
153 ),
154 coroutineScope = scope,
155 )
156 buildScope.launchScope {
157 spec.applySpec()
158 launchEffect { awaitCancellationAndThen { stopEmitter.emit(Unit) } }
159 }
160 }
161 .awaitOrCancel()
162 .joinOrCancel()
163 }
164
awaitOrCancelnull165 private suspend fun <T> Deferred<T>.awaitOrCancel(): T =
166 try {
167 await()
168 } catch (ex: CancellationException) {
169 cancel(ex)
170 throw ex
171 }
172
joinOrCancelnull173 private suspend fun Job.joinOrCancel(): Unit =
174 try {
175 join()
176 } catch (ex: CancellationException) {
177 cancel(ex)
178 throw ex
179 }
180
coalescingMutableEventsnull181 override fun <In, Out> coalescingMutableEvents(
182 coalesce: (old: Out, new: In) -> Out,
183 getInitialValue: () -> Out,
184 ): CoalescingMutableEvents<In, Out> =
185 CoalescingMutableEvents(
186 null,
187 coalesce = { old, new -> coalesce(old.value, new) },
188 network,
189 getInitialValue,
190 )
191
conflatedMutableEventsnull192 override fun <T> conflatedMutableEvents(): CoalescingMutableEvents<T, T> =
193 CoalescingMutableEvents(
194 null,
195 coalesce = { _, new -> new },
196 network,
<lambda>null197 { error("WTF: init value accessed for conflatedMutableEvents") },
198 )
199
mutableEventsnull200 override fun <T> mutableEvents(): MutableEvents<T> = MutableEvents(network)
201
202 override fun <T> mutableStateDeferred(initialValue: DeferredValue<T>): MutableState<T> =
203 MutableState(network, initialValue.unwrapped)
204 }
205
206 /**
207 * Combination of an [KairosNetwork] and a [Job] that, when cancelled, will cancel the entire Kairos
208 * network.
209 */
210 @ExperimentalKairosApi
211 class RootKairosNetwork
212 internal constructor(private val network: Network, private val scope: CoroutineScope, job: Job) :
213 Job by job, KairosNetwork by LocalNetwork(network, scope, emptyEvents)
214
215 /** Constructs a new [RootKairosNetwork] in the given [CoroutineScope]. */
216 @ExperimentalKairosApi
217 fun CoroutineScope.launchKairosNetwork(
218 context: CoroutineContext = EmptyCoroutineContext
219 ): RootKairosNetwork {
220 val scope = childScope(context)
221 val network = Network(scope)
222 scope.launch(CoroutineName("launchKairosNetwork scheduler")) { network.runInputScheduler() }
223 return RootKairosNetwork(network, scope, scope.coroutineContext.job)
224 }
225
226 @ExperimentalKairosApi
227 interface HasNetwork : KairosScope {
228 /**
229 * A [KairosNetwork] handle that is bound to the lifetime of a [BuildScope].
230 *
231 * It supports all of the standard functionality by which external code can interact with this
232 * Kairos network, but all [activated][KairosNetwork.activateSpec] [BuildSpec]s are bound as
233 * children to the [BuildScope], such that when the [BuildScope] is destroyed, all children are
234 * also destroyed.
235 */
236 val kairosNetwork: KairosNetwork
237 }
238
239 /** Returns a [MutableEvents] that can emit values into this [KairosNetwork]. */
240 @ExperimentalKairosApi
MutableEventsnull241 fun <T> HasNetwork.MutableEvents(): MutableEvents<T> = MutableEvents(kairosNetwork)
242
243 /** Returns a [MutableState] with initial state [initialValue]. */
244 @ExperimentalKairosApi
245 fun <T> HasNetwork.MutableState(initialValue: T): MutableState<T> =
246 MutableState(kairosNetwork, initialValue)
247
248 /** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */
249 @ExperimentalKairosApi
250 fun <In, Out> HasNetwork.CoalescingMutableEvents(
251 coalesce: (old: Out, new: In) -> Out,
252 initialValue: Out,
253 ): CoalescingMutableEvents<In, Out> = CoalescingMutableEvents(kairosNetwork, coalesce, initialValue)
254
255 /** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */
256 @ExperimentalKairosApi
257 fun <In, Out> HasNetwork.CoalescingMutableEvents(
258 coalesce: (old: Out, new: In) -> Out,
259 getInitialValue: () -> Out,
260 ): CoalescingMutableEvents<In, Out> =
261 CoalescingMutableEvents(kairosNetwork, coalesce, getInitialValue)
262
263 /** Returns a [CoalescingMutableEvents] that can emit values into this [KairosNetwork]. */
264 @ExperimentalKairosApi
265 fun <T> HasNetwork.ConflatedMutableEvents(): CoalescingMutableEvents<T, T> =
266 ConflatedMutableEvents(kairosNetwork)
267