More coroutine tweaks

This commit is contained in:
arkon 2021-01-10 11:01:10 -05:00
parent 2ffbee3db2
commit c9cf9cfff0
13 changed files with 113 additions and 125 deletions

View File

@ -20,9 +20,7 @@ import org.acra.sender.HttpSender
import org.conscrypt.Conscrypt import org.conscrypt.Conscrypt
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.InjektScope
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import uy.kohesive.injekt.registry.default.DefaultRegistrar
import java.security.Security import java.security.Security
@AcraCore( @AcraCore(
@ -53,7 +51,6 @@ open class App : Application(), LifecycleObserver {
Security.insertProviderAt(Conscrypt.newProvider(), 1) Security.insertProviderAt(Conscrypt.newProvider(), 1)
} }
Injekt = InjektScope(DefaultRegistrar())
Injekt.importModule(AppModule(this)) Injekt.importModule(AppModule(this))
setupAcra() setupAcra()

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi package eu.kanade.tachiyomi
import android.app.Application import android.app.Application
import android.os.Handler
import com.google.gson.Gson import com.google.gson.Gson
import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
@ -11,8 +12,6 @@ import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import uy.kohesive.injekt.api.InjektModule import uy.kohesive.injekt.api.InjektModule
import uy.kohesive.injekt.api.InjektRegistrar import uy.kohesive.injekt.api.InjektRegistrar
@ -48,15 +47,16 @@ class AppModule(val app: Application) : InjektModule {
addSingletonFactory { Json { ignoreUnknownKeys = true } } addSingletonFactory { Json { ignoreUnknownKeys = true } }
// Asynchronously init expensive components for a faster cold start // Asynchronously init expensive components for a faster cold start
Handler().post {
get<PreferencesHelper>()
GlobalScope.launch { get<PreferencesHelper>() } get<NetworkHelper>()
GlobalScope.launch { get<NetworkHelper>() } get<SourceManager>()
GlobalScope.launch { get<SourceManager>() } get<DatabaseHelper>()
GlobalScope.launch { get<DatabaseHelper>() } get<DownloadManager>()
}
GlobalScope.launch { get<DownloadManager>() }
} }
} }

View File

@ -10,7 +10,6 @@ import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.util.chapter.NoChaptersException import eu.kanade.tachiyomi.util.chapter.NoChaptersException
import eu.kanade.tachiyomi.util.lang.await
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
@ -91,7 +90,7 @@ abstract class AbstractBackupRestore<T : AbstractBackupManager>(protected val co
if (service != null && service.isLogged) { if (service != null && service.isLogged) {
try { try {
val updatedTrack = service.refresh(track) val updatedTrack = service.refresh(track)
db.insertTrack(updatedTrack).await() db.insertTrack(updatedTrack).executeAsBlocking()
} catch (e: Exception) { } catch (e: Exception) {
errors.add(Date() to "${manga.title} - ${e.message}") errors.add(Date() to "${manga.title} - ${e.message}")
} }

View File

@ -15,7 +15,6 @@ import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.toSManga import eu.kanade.tachiyomi.source.model.toSManga
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
import eu.kanade.tachiyomi.util.lang.await
import eu.kanade.tachiyomi.util.lang.runAsObservable import eu.kanade.tachiyomi.util.lang.runAsObservable
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
@ -255,7 +254,7 @@ open class GlobalSearchPresenter(
val networkManga = source.getMangaDetails(manga.toMangaInfo()) val networkManga = source.getMangaDetails(manga.toMangaInfo())
manga.copyFrom(networkManga.toSManga()) manga.copyFrom(networkManga.toSManga())
manga.initialized = true manga.initialized = true
db.insertManga(manga).await() db.insertManga(manga).executeAsBlocking()
return manga return manga
} }

View File

@ -199,7 +199,7 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
return return
} }
launchIO { lifecycleScope.launchIO {
try { try {
val pendingUpdates = ExtensionGithubApi().checkForUpdates(this@MainActivity) val pendingUpdates = ExtensionGithubApi().checkForUpdates(this@MainActivity)
preferences.extensionUpdatesCount().set(pendingUpdates.size) preferences.extensionUpdatesCount().set(pendingUpdates.size)

View File

@ -24,7 +24,6 @@ import eu.kanade.tachiyomi.ui.manga.chapter.ChapterItem
import eu.kanade.tachiyomi.util.chapter.ChapterSettingsHelper import eu.kanade.tachiyomi.util.chapter.ChapterSettingsHelper
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import eu.kanade.tachiyomi.util.isLocal import eu.kanade.tachiyomi.util.isLocal
import eu.kanade.tachiyomi.util.lang.await
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.withUIContext import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.prepUpdateCover import eu.kanade.tachiyomi.util.prepUpdateCover
@ -168,7 +167,7 @@ class MangaPresenter(
manga.prepUpdateCover(coverCache, sManga, manualFetch) manga.prepUpdateCover(coverCache, sManga, manualFetch)
manga.copyFrom(sManga) manga.copyFrom(sManga)
manga.initialized = true manga.initialized = true
db.insertManga(manga).await() db.insertManga(manga).executeAsBlocking()
withUIContext { view?.onFetchMangaInfoDone() } withUIContext { view?.onFetchMangaInfoDone() }
} catch (e: Throwable) { } catch (e: Throwable) {
@ -350,7 +349,7 @@ class MangaPresenter(
hasRequested = true hasRequested = true
if (fetchChaptersJob?.isActive == true) return if (fetchChaptersJob?.isActive == true) return
fetchChaptersJob = launchIO { fetchChaptersJob = presenterScope.launchIO {
try { try {
val chapters = source.getChapterList(manga.toMangaInfo()) val chapters = source.getChapterList(manga.toMangaInfo())
.map { it.toSChapter() } .map { it.toSChapter() }
@ -464,7 +463,7 @@ class MangaPresenter(
} }
launchIO { launchIO {
db.updateChaptersProgress(chapters).await() db.updateChaptersProgress(chapters).executeAsBlocking()
if (preferences.removeAfterMarkedAsRead()) { if (preferences.removeAfterMarkedAsRead()) {
deleteChapters(chapters) deleteChapters(chapters)
@ -489,7 +488,7 @@ class MangaPresenter(
selectedChapters selectedChapters
.forEach { .forEach {
it.bookmark = bookmarked it.bookmark = bookmarked
db.updateChapterProgress(it).await() db.updateChapterProgress(it).executeAsBlocking()
} }
} }
} }

View File

@ -8,7 +8,6 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.lang.await
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.withUIContext import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
@ -67,7 +66,7 @@ class TrackPresenter(
.map { .map {
async { async {
val track = it.service.refresh(it.track!!) val track = it.service.refresh(it.track!!)
db.insertTrack(track).await() db.insertTrack(track).executeAsBlocking()
} }
} }
.awaitAll() .awaitAll()
@ -98,7 +97,7 @@ class TrackPresenter(
launchIO { launchIO {
try { try {
service.bind(item) service.bind(item)
db.insertTrack(item).await() db.insertTrack(item).executeAsBlocking()
} catch (e: Throwable) { } catch (e: Throwable) {
withUIContext { context.toast(e.message) } withUIContext { context.toast(e.message) }
} }
@ -116,7 +115,7 @@ class TrackPresenter(
launchIO { launchIO {
try { try {
service.update(track) service.update(track)
db.insertTrack(track).await() db.insertTrack(track).executeAsBlocking()
withUIContext { view?.onRefreshDone() } withUIContext { view?.onRefreshDone() }
} catch (e: Throwable) { } catch (e: Throwable) {
withUIContext { view?.onRefreshError(e) } withUIContext { view?.onRefreshError(e) }

View File

@ -21,7 +21,6 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
import eu.kanade.tachiyomi.util.isLocal import eu.kanade.tachiyomi.util.isLocal
import eu.kanade.tachiyomi.util.lang.await
import eu.kanade.tachiyomi.util.lang.byteSize import eu.kanade.tachiyomi.util.lang.byteSize
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.takeBytes import eu.kanade.tachiyomi.util.lang.takeBytes
@ -664,7 +663,7 @@ class ReaderPresenter(
val trackManager = Injekt.get<TrackManager>() val trackManager = Injekt.get<TrackManager>()
launchIO { launchIO {
db.getTracks(manga).await() db.getTracks(manga).executeAsBlocking()
.mapNotNull { track -> .mapNotNull { track ->
val service = trackManager.getService(track.sync_id) val service = trackManager.getService(track.sync_id)
if (service != null && service.isLogged && chapterRead > track.last_chapter_read) { if (service != null && service.isLogged && chapterRead > track.last_chapter_read) {
@ -675,7 +674,7 @@ class ReaderPresenter(
async { async {
runCatching { runCatching {
service.update(track) service.update(track)
db.insertTrack(track).await() db.insertTrack(track).executeAsBlocking()
} }
} }
} else { } else {
@ -683,8 +682,8 @@ class ReaderPresenter(
} }
} }
.awaitAll() .awaitAll()
.filter { it.isFailure } .mapNotNull { it.exceptionOrNull() }
.forEach { it.exceptionOrNull()?.let { e -> Timber.w(e) } } .forEach { Timber.w(it) }
} }
} }

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.setting.track package eu.kanade.tachiyomi.ui.setting.track
import android.net.Uri import android.net.Uri
import androidx.lifecycle.lifecycleScope
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchIO
class AnilistLoginActivity : BaseOAuthLoginActivity() { class AnilistLoginActivity : BaseOAuthLoginActivity() {
@ -9,7 +10,7 @@ class AnilistLoginActivity : BaseOAuthLoginActivity() {
val regex = "(?:access_token=)(.*?)(?:&)".toRegex() val regex = "(?:access_token=)(.*?)(?:&)".toRegex()
val matchResult = regex.find(data?.fragment.toString()) val matchResult = regex.find(data?.fragment.toString())
if (matchResult?.groups?.get(1) != null) { if (matchResult?.groups?.get(1) != null) {
launchIO { lifecycleScope.launchIO {
trackManager.aniList.login(matchResult.groups[1]!!.value) trackManager.aniList.login(matchResult.groups[1]!!.value)
returnToSettings() returnToSettings()
} }

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.setting.track package eu.kanade.tachiyomi.ui.setting.track
import android.net.Uri import android.net.Uri
import androidx.lifecycle.lifecycleScope
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchIO
class BangumiLoginActivity : BaseOAuthLoginActivity() { class BangumiLoginActivity : BaseOAuthLoginActivity() {
@ -8,7 +9,7 @@ class BangumiLoginActivity : BaseOAuthLoginActivity() {
override fun handleResult(data: Uri?) { override fun handleResult(data: Uri?) {
val code = data?.getQueryParameter("code") val code = data?.getQueryParameter("code")
if (code != null) { if (code != null) {
launchIO { lifecycleScope.launchIO {
trackManager.bangumi.login(code) trackManager.bangumi.login(code)
returnToSettings() returnToSettings()
} }

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.setting.track package eu.kanade.tachiyomi.ui.setting.track
import android.net.Uri import android.net.Uri
import androidx.lifecycle.lifecycleScope
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchIO
class MyAnimeListLoginActivity : BaseOAuthLoginActivity() { class MyAnimeListLoginActivity : BaseOAuthLoginActivity() {
@ -8,7 +9,7 @@ class MyAnimeListLoginActivity : BaseOAuthLoginActivity() {
override fun handleResult(data: Uri?) { override fun handleResult(data: Uri?) {
val code = data?.getQueryParameter("code") val code = data?.getQueryParameter("code")
if (code != null) { if (code != null) {
launchIO { lifecycleScope.launchIO {
trackManager.myAnimeList.login(code) trackManager.myAnimeList.login(code)
returnToSettings() returnToSettings()
} }

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.setting.track package eu.kanade.tachiyomi.ui.setting.track
import android.net.Uri import android.net.Uri
import androidx.lifecycle.lifecycleScope
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchIO
class ShikimoriLoginActivity : BaseOAuthLoginActivity() { class ShikimoriLoginActivity : BaseOAuthLoginActivity() {
@ -8,7 +9,7 @@ class ShikimoriLoginActivity : BaseOAuthLoginActivity() {
override fun handleResult(data: Uri?) { override fun handleResult(data: Uri?) {
val code = data?.getQueryParameter("code") val code = data?.getQueryParameter("code")
if (code != null) { if (code != null) {
launchIO { lifecycleScope.launchIO {
trackManager.shikimori.login(code) trackManager.shikimori.login(code)
returnToSettings() returnToSettings()
} }

View File

@ -1,7 +1,5 @@
package eu.kanade.tachiyomi.util.lang package eu.kanade.tachiyomi.util.lang
import com.pushtorefresh.storio.operations.PreparedOperation
import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetObject
import kotlinx.coroutines.CancellableContinuation import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.CoroutineStart
@ -10,11 +8,8 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import rx.Completable
import rx.CompletableSubscriber
import rx.Emitter import rx.Emitter
import rx.Observable import rx.Observable
import rx.Observer import rx.Observer
@ -53,49 +48,46 @@ suspend fun <T> Single<T>.await(subscribeOn: Scheduler? = null): T {
} }
} }
suspend fun <T> PreparedOperation<T>.await(): T = asRxSingle().await() // suspend fun Completable.awaitSuspending(subscribeOn: Scheduler? = null) {
suspend fun <T> PreparedGetObject<T>.await(): T? = asRxSingle().await() // return suspendCancellableCoroutine { continuation ->
// val self = if (subscribeOn != null) subscribeOn(subscribeOn) else this
suspend fun Completable.awaitSuspending(subscribeOn: Scheduler? = null) { // lateinit var sub: Subscription
return suspendCancellableCoroutine { continuation -> // sub = self.subscribe(
val self = if (subscribeOn != null) subscribeOn(subscribeOn) else this // {
lateinit var sub: Subscription // continuation.resume(Unit) {
sub = self.subscribe( // sub.unsubscribe()
{ // }
continuation.resume(Unit) { // },
sub.unsubscribe() // {
} // if (!continuation.isCancelled) {
}, // continuation.resumeWithException(it)
{ // }
if (!continuation.isCancelled) { // }
continuation.resumeWithException(it) // )
} //
} // continuation.invokeOnCancellation {
) // sub.unsubscribe()
// }
continuation.invokeOnCancellation { // }
sub.unsubscribe() // }
} //
} // suspend fun Completable.awaitCompleted(): Unit = suspendCancellableCoroutine { cont ->
} // subscribe(
// object : CompletableSubscriber {
suspend fun Completable.awaitCompleted(): Unit = suspendCancellableCoroutine { cont -> // override fun onSubscribe(s: Subscription) {
subscribe( // cont.unsubscribeOnCancellation(s)
object : CompletableSubscriber { // }
override fun onSubscribe(s: Subscription) { //
cont.unsubscribeOnCancellation(s) // override fun onCompleted() {
} // cont.resume(Unit)
// }
override fun onCompleted() { //
cont.resume(Unit) // override fun onError(e: Throwable) {
} // cont.resumeWithException(e)
// }
override fun onError(e: Throwable) { // }
cont.resumeWithException(e) // )
} // }
}
)
}
suspend fun <T> Single<T>.await(): T = suspendCancellableCoroutine { cont -> suspend fun <T> Single<T>.await(): T = suspendCancellableCoroutine { cont ->
cont.unsubscribeOnCancellation( cont.unsubscribeOnCancellation(
@ -113,27 +105,27 @@ suspend fun <T> Single<T>.await(): T = suspendCancellableCoroutine { cont ->
) )
} }
suspend fun <T> Observable<T>.awaitFirst(): T = first().awaitOne() // suspend fun <T> Observable<T>.awaitFirst(): T = first().awaitOne()
//
suspend fun <T> Observable<T>.awaitFirstOrDefault(default: T): T = // suspend fun <T> Observable<T>.awaitFirstOrDefault(default: T): T =
firstOrDefault(default).awaitOne() // firstOrDefault(default).awaitOne()
//
suspend fun <T> Observable<T>.awaitFirstOrNull(): T? = firstOrDefault(null).awaitOne() // suspend fun <T> Observable<T>.awaitFirstOrNull(): T? = firstOrDefault(null).awaitOne()
//
suspend fun <T> Observable<T>.awaitFirstOrElse(defaultValue: () -> T): T = switchIfEmpty( // suspend fun <T> Observable<T>.awaitFirstOrElse(defaultValue: () -> T): T = switchIfEmpty(
Observable.fromCallable( // Observable.fromCallable(
defaultValue // defaultValue
) // )
).first().awaitOne() // ).first().awaitOne()
//
suspend fun <T> Observable<T>.awaitLast(): T = last().awaitOne() // suspend fun <T> Observable<T>.awaitLast(): T = last().awaitOne()
suspend fun <T> Observable<T>.awaitSingle(): T = single().awaitOne() suspend fun <T> Observable<T>.awaitSingle(): T = single().awaitOne()
suspend fun <T> Observable<T>.awaitSingleOrDefault(default: T): T = // suspend fun <T> Observable<T>.awaitSingleOrDefault(default: T): T =
singleOrDefault(default).awaitOne() // singleOrDefault(default).awaitOne()
//
suspend fun <T> Observable<T>.awaitSingleOrNull(): T? = singleOrDefault(null).awaitOne() // suspend fun <T> Observable<T>.awaitSingleOrNull(): T? = singleOrDefault(null).awaitOne()
private suspend fun <T> Observable<T>.awaitOne(): T = suspendCancellableCoroutine { cont -> private suspend fun <T> Observable<T>.awaitOne(): T = suspendCancellableCoroutine { cont ->
cont.unsubscribeOnCancellation( cont.unsubscribeOnCancellation(
@ -192,31 +184,31 @@ fun <T : Any> Observable<T>.asFlow(): Flow<T> = callbackFlow {
awaitClose { subscription.unsubscribe() } awaitClose { subscription.unsubscribe() }
} }
fun <T : Any> Flow<T>.asObservable(backpressureMode: Emitter.BackpressureMode = Emitter.BackpressureMode.NONE): Observable<T> { // fun <T : Any> Flow<T>.asObservable(backpressureMode: Emitter.BackpressureMode = Emitter.BackpressureMode.NONE): Observable<T> {
return Observable.create( // return Observable.create(
{ emitter -> // { emitter ->
/* // /*
* ATOMIC is used here to provide stable behaviour of subscribe+dispose pair even if // * ATOMIC is used here to provide stable behaviour of subscribe+dispose pair even if
* asObservable is already invoked from unconfined // * asObservable is already invoked from unconfined
*/ // */
val job = GlobalScope.launch(Dispatchers.Unconfined, start = CoroutineStart.ATOMIC) { // val job = GlobalScope.launch(Dispatchers.Unconfined, start = CoroutineStart.ATOMIC) {
try { // try {
collect { emitter.onNext(it) } // collect { emitter.onNext(it) }
emitter.onCompleted() // emitter.onCompleted()
} catch (e: Throwable) { // } catch (e: Throwable) {
// Ignore `CancellationException` as error, since it indicates "normal cancellation" // // Ignore `CancellationException` as error, since it indicates "normal cancellation"
if (e !is CancellationException) { // if (e !is CancellationException) {
emitter.onError(e) // emitter.onError(e)
} else { // } else {
emitter.onCompleted() // emitter.onCompleted()
} // }
} // }
} // }
emitter.setCancellation { job.cancel() } // emitter.setCancellation { job.cancel() }
}, // },
backpressureMode // backpressureMode
) // )
} // }
fun <T> runAsObservable( fun <T> runAsObservable(
block: suspend () -> T, block: suspend () -> T,