-
Notifications
You must be signed in to change notification settings - Fork 194
/
StoreReadResponse.kt
176 lines (154 loc) · 5.31 KB
/
StoreReadResponse.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
/*
* Copyright 2019 Google LLC
*
* 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
*
* https://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 org.mobilenativefoundation.store.store5
/**
* Holder for responses from Store.
*
* Instead of using regular error channels (a.k.a. throwing exceptions), Store uses this holder
* class to represent each response. This allows the flow to keep running even if an error happens
* so that if there is an observable single source of truth, application can keep observing it.
*/
sealed class StoreReadResponse<out Output> {
/**
* Represents the source of the Response.
*/
abstract val origin: StoreReadResponseOrigin
object Initial : StoreReadResponse<Nothing>() {
override val origin: StoreReadResponseOrigin = StoreReadResponseOrigin.Initial
}
/**
* Loading event dispatched by [Store] to signal the [Fetcher] is in progress.
*/
data class Loading(override val origin: StoreReadResponseOrigin) : StoreReadResponse<Nothing>()
/**
* Data dispatched by [Store]
*/
data class Data<Output>(val value: Output, override val origin: StoreReadResponseOrigin) :
StoreReadResponse<Output>()
/**
* No new data event dispatched by Store to signal the [Fetcher] returned no data (i.e., the
* returned [kotlinx.coroutines.flow.Flow], when collected, was empty).
*/
data class NoNewData(override val origin: StoreReadResponseOrigin) : StoreReadResponse<Nothing>()
/**
* Error dispatched by a pipeline
*/
sealed class Error : StoreReadResponse<Nothing>() {
data class Exception(
val error: Throwable,
override val origin: StoreReadResponseOrigin
) : Error()
data class Message(
val message: String,
override val origin: StoreReadResponseOrigin
) : Error()
data class Custom<E : Any>(
val error: E,
override val origin: StoreReadResponseOrigin
) : Error()
}
/**
* Returns the available data or throws [NullPointerException] if there is no data.
*/
fun requireData(): Output {
return when (this) {
is Data -> value
is Error -> this.doThrow()
else -> throw NullPointerException("there is no data in $this")
}
}
/**
* If this [StoreReadResponse] is of type [StoreReadResponse.Error], throws the exception
* Otherwise, does nothing.
*/
fun throwIfError() {
if (this is Error) {
this.doThrow()
}
}
/**
* If this [StoreReadResponse] is of type [StoreReadResponse.Error], returns the available error
* from it. Otherwise, returns `null`.
*/
fun errorMessageOrNull(): String? {
return when (this) {
is Error.Message -> message
is Error.Exception -> error.message ?: "exception: ${error::class}"
else -> null
}
}
/**
* If there is data available, returns it; otherwise returns null.
*/
fun dataOrNull(): Output? = when (this) {
is Data -> value
else -> null
}
private fun errorOrNull(): Throwable? {
if (this is Error.Exception) {
return error
}
return null
}
/**
* @returns Error if there is one, else null.
*/
@Suppress("UNCHECKED_CAST")
fun <E : Any> errorOrNull(): E? {
if (this is Error.Custom<*>) {
return (this as? Error.Custom<E>)?.error
}
return errorOrNull() as? E
}
@Suppress("UNCHECKED_CAST")
internal fun <T> swapType(): StoreReadResponse<T> = when (this) {
is Error -> this
is Loading -> this
is NoNewData -> this
is Data -> throw RuntimeException("cannot swap type for StoreResponse.Data")
is Initial -> this
}
}
/**
* Represents the origin for a [StoreReadResponse].
*/
sealed class StoreReadResponseOrigin {
/**
* [StoreReadResponse] is sent from the cache
*/
object Cache : StoreReadResponseOrigin()
/**
* [StoreReadResponse] is sent from the persister
*/
object SourceOfTruth : StoreReadResponseOrigin()
/**
* [StoreReadResponse] is sent from a fetcher
* @property name Unique name to enable differentiation when [org.mobilenativefoundation.store.store5.Fetcher.fallback] exists
*/
data class Fetcher(val name: String? = null) : StoreReadResponseOrigin()
object Initial : StoreReadResponseOrigin()
}
fun StoreReadResponse.Error.doThrow(): Nothing = when (this) {
is StoreReadResponse.Error.Exception -> throw error
is StoreReadResponse.Error.Message -> throw RuntimeException(message)
is StoreReadResponse.Error.Custom<*> -> {
if (error is Throwable) {
throw error
} else {
throw RuntimeException("Non-throwable custom error: $error")
}
}
}