Skip to content

Commit 8daf907

Browse files
authored
Merge pull request #450 from splendo/feature/firestore-settings
Added Settings to deal with newer settings & add callback threads
2 parents ad577b3 + 5a28653 commit 8daf907

File tree

14 files changed

+625
-115
lines changed

14 files changed

+625
-115
lines changed

firebase-auth/src/jvmTest/kotlin/dev/gitlive/firebase/auth/auth.kt

+3
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@ package dev.gitlive.firebase.auth
99
actual val emulatorHost: String = "10.0.2.2"
1010

1111
actual val context: Any = Unit
12+
13+
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
14+
actual annotation class IgnoreForAndroidUnitTest

firebase-database/src/androidMain/kotlin/dev/gitlive/firebase/database/database.kt

+15-9
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* Copyright (c) 2020 GitLive Ltd. Use of this source code is governed by the Apache 2.0 license.
33
*/
44

5+
@file:JvmName("databaseAndroid")
56
package dev.gitlive.firebase.database
67

78
import com.google.android.gms.tasks.Task
@@ -16,7 +17,6 @@ import dev.gitlive.firebase.EncodeDecodeSettingsBuilder
1617
import dev.gitlive.firebase.Firebase
1718
import dev.gitlive.firebase.FirebaseApp
1819
import dev.gitlive.firebase.database.ChildEvent.Type
19-
import dev.gitlive.firebase.database.FirebaseDatabase.Companion.FirebaseDatabase
2020
import dev.gitlive.firebase.decode
2121
import dev.gitlive.firebase.reencodeTransformation
2222
import kotlinx.coroutines.CompletableDeferred
@@ -50,23 +50,23 @@ suspend fun <T> Task<T>.awaitWhileOnline(database: FirebaseDatabase): T =
5050
.first()
5151

5252
actual val Firebase.database
53-
by lazy { FirebaseDatabase(com.google.firebase.database.FirebaseDatabase.getInstance()) }
53+
by lazy { FirebaseDatabase.getInstance(com.google.firebase.database.FirebaseDatabase.getInstance()) }
5454

5555
actual fun Firebase.database(url: String) =
56-
FirebaseDatabase(com.google.firebase.database.FirebaseDatabase.getInstance(url))
56+
FirebaseDatabase.getInstance(com.google.firebase.database.FirebaseDatabase.getInstance(url))
5757

5858
actual fun Firebase.database(app: FirebaseApp) =
59-
FirebaseDatabase(com.google.firebase.database.FirebaseDatabase.getInstance(app.android))
59+
FirebaseDatabase.getInstance(com.google.firebase.database.FirebaseDatabase.getInstance(app.android))
6060

6161
actual fun Firebase.database(app: FirebaseApp, url: String) =
62-
FirebaseDatabase(com.google.firebase.database.FirebaseDatabase.getInstance(app.android, url))
62+
FirebaseDatabase.getInstance(com.google.firebase.database.FirebaseDatabase.getInstance(app.android, url))
6363

64-
actual class FirebaseDatabase private constructor(val android: com.google.firebase.database.FirebaseDatabase) {
64+
actual class FirebaseDatabase internal constructor(val android: com.google.firebase.database.FirebaseDatabase) {
6565

6666
companion object {
6767
private val instances = WeakHashMap<com.google.firebase.database.FirebaseDatabase, FirebaseDatabase>()
6868

69-
internal fun FirebaseDatabase(
69+
internal fun getInstance(
7070
android: com.google.firebase.database.FirebaseDatabase
7171
) = instances.getOrPut(android) { dev.gitlive.firebase.database.FirebaseDatabase(android) }
7272
}
@@ -79,8 +79,14 @@ actual class FirebaseDatabase private constructor(val android: com.google.fireba
7979
actual fun reference() =
8080
DatabaseReference(NativeDatabaseReference(android.reference, persistenceEnabled))
8181

82-
actual fun setPersistenceEnabled(enabled: Boolean) =
83-
android.setPersistenceEnabled(enabled).also { persistenceEnabled = enabled }
82+
actual fun setPersistenceEnabled(enabled: Boolean) {
83+
android.setPersistenceEnabled(enabled)
84+
persistenceEnabled = enabled
85+
}
86+
87+
actual fun setPersistenceCacheSizeBytes(cacheSizeInBytes: Long) {
88+
android.setPersistenceCacheSizeBytes(cacheSizeInBytes)
89+
}
8490

8591
actual fun setLoggingEnabled(enabled: Boolean) =
8692
android.setLogLevel(Logger.Level.DEBUG.takeIf { enabled } ?: Logger.Level.NONE)

firebase-database/src/commonMain/kotlin/dev/gitlive/firebase/database/database.kt

+3-1
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,12 @@ expect fun Firebase.database(app: FirebaseApp): FirebaseDatabase
3232
expect fun Firebase.database(app: FirebaseApp, url: String): FirebaseDatabase
3333

3434
expect class FirebaseDatabase {
35+
3536
fun reference(path: String): DatabaseReference
3637
fun reference(): DatabaseReference
37-
fun setPersistenceEnabled(enabled: Boolean)
3838
fun setLoggingEnabled(enabled: Boolean)
39+
fun setPersistenceEnabled(enabled: Boolean)
40+
fun setPersistenceCacheSizeBytes(cacheSizeInBytes: Long)
3941
fun useEmulator(host: String, port: Int)
4042
}
4143

firebase-database/src/iosMain/kotlin/dev/gitlive/firebase/database/database.kt

+7
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ import kotlinx.serialization.DeserializationStrategy
3737
import kotlinx.serialization.KSerializer
3838
import platform.Foundation.NSError
3939
import platform.Foundation.allObjects
40+
import platform.darwin.dispatch_queue_t
41+
import kotlin.collections.component1
42+
import kotlin.collections.component2
4043

4144
actual val Firebase.database
4245
by lazy { FirebaseDatabase(FIRDatabase.database()) }
@@ -64,6 +67,10 @@ actual class FirebaseDatabase internal constructor(val ios: FIRDatabase) {
6467
ios.persistenceEnabled = enabled
6568
}
6669

70+
actual fun setPersistenceCacheSizeBytes(cacheSizeInBytes: Long) {
71+
ios.setPersistenceCacheSizeBytes(cacheSizeInBytes.toULong())
72+
}
73+
6774
actual fun setLoggingEnabled(enabled: Boolean) =
6875
FIRDatabase.setLoggingEnabled(enabled)
6976

firebase-database/src/jsMain/kotlin/dev/gitlive/firebase/database/database.kt

+2
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,11 @@ actual fun Firebase.database(app: FirebaseApp, url: String) =
6767
rethrow { FirebaseDatabase(getDatabase(app = app.js, url = url)) }
6868

6969
actual class FirebaseDatabase internal constructor(val js: Database) {
70+
7071
actual fun reference(path: String) = rethrow { DatabaseReference(NativeDatabaseReference(ref(js, path), js)) }
7172
actual fun reference() = rethrow { DatabaseReference(NativeDatabaseReference(ref(js), js)) }
7273
actual fun setPersistenceEnabled(enabled: Boolean) {}
74+
actual fun setPersistenceCacheSizeBytes(cacheSizeInBytes: Long) {}
7375
actual fun setLoggingEnabled(enabled: Boolean) = rethrow { enableLogging(enabled) }
7476
actual fun useEmulator(host: String, port: Int) = rethrow { connectDatabaseEmulator(js, host, port) }
7577
}

firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt

+163-44
Original file line numberDiff line numberDiff line change
@@ -5,69 +5,175 @@
55
@file:JvmName("android")
66
package dev.gitlive.firebase.firestore
77

8+
import com.google.android.gms.tasks.TaskExecutors
9+
import com.google.firebase.firestore.MemoryCacheSettings
10+
import com.google.firebase.firestore.MemoryEagerGcSettings
11+
import com.google.firebase.firestore.MemoryLruGcSettings
812
import com.google.firebase.firestore.MetadataChanges
13+
import com.google.firebase.firestore.PersistentCacheSettings
914
import dev.gitlive.firebase.Firebase
1015
import dev.gitlive.firebase.FirebaseApp
16+
import kotlinx.coroutines.channels.ProducerScope
1117
import dev.gitlive.firebase.firestore.Source.*
1218
import kotlinx.coroutines.channels.awaitClose
1319
import kotlinx.coroutines.flow.Flow
1420
import kotlinx.coroutines.flow.callbackFlow
1521
import kotlinx.coroutines.runBlocking
1622
import kotlinx.coroutines.tasks.await
17-
import kotlinx.serialization.Serializable
23+
import java.util.concurrent.ConcurrentHashMap
24+
import java.util.concurrent.Executor
1825
import com.google.firebase.firestore.FieldPath as AndroidFieldPath
1926
import com.google.firebase.firestore.Filter as AndroidFilter
2027
import com.google.firebase.firestore.Query as AndroidQuery
28+
import com.google.firebase.firestore.firestoreSettings as androidFirestoreSettings
29+
import com.google.firebase.firestore.memoryCacheSettings as androidMemoryCacheSettings
30+
import com.google.firebase.firestore.memoryEagerGcSettings as androidMemoryEagerGcSettings
31+
import com.google.firebase.firestore.memoryLruGcSettings as androidMemoryLruGcSettings
32+
import com.google.firebase.firestore.persistentCacheSettings as androidPersistentCacheSettings
2133

2234
actual val Firebase.firestore get() =
2335
FirebaseFirestore(com.google.firebase.firestore.FirebaseFirestore.getInstance())
2436

2537
actual fun Firebase.firestore(app: FirebaseApp) =
2638
FirebaseFirestore(com.google.firebase.firestore.FirebaseFirestore.getInstance(app.android))
2739

28-
actual class FirebaseFirestore(val android: com.google.firebase.firestore.FirebaseFirestore) {
40+
val LocalCacheSettings.android: com.google.firebase.firestore.LocalCacheSettings get() = when (this) {
41+
is LocalCacheSettings.Persistent -> androidPersistentCacheSettings {
42+
setSizeBytes(sizeBytes)
43+
}
44+
is LocalCacheSettings.Memory -> androidMemoryCacheSettings {
45+
setGcSettings(
46+
when (garbaseCollectorSettings) {
47+
is MemoryGarbageCollectorSettings.Eager -> androidMemoryEagerGcSettings { }
48+
is MemoryGarbageCollectorSettings.LRUGC -> androidMemoryLruGcSettings {
49+
setSizeBytes(garbaseCollectorSettings.sizeBytes)
50+
}
51+
}
52+
)
53+
}
54+
}
55+
56+
// Since on iOS Callback threads are set as settings, we store the settings explicitly here as well
57+
private val callbackExecutorMap = ConcurrentHashMap<com.google.firebase.firestore.FirebaseFirestore, Executor>()
58+
59+
actual typealias NativeFirebaseFirestore = com.google.firebase.firestore.FirebaseFirestore
60+
internal actual class NativeFirebaseFirestoreWrapper actual constructor(actual val native: NativeFirebaseFirestore) {
61+
62+
actual var settings: FirebaseFirestoreSettings
63+
get() = with(native.firestoreSettings) {
64+
FirebaseFirestoreSettings(
65+
isSslEnabled,
66+
host,
67+
cacheSettings?.let { localCacheSettings ->
68+
when (localCacheSettings) {
69+
is MemoryCacheSettings -> {
70+
val garbageCollectionSettings = when (val settings = localCacheSettings.garbageCollectorSettings) {
71+
is MemoryEagerGcSettings -> MemoryGarbageCollectorSettings.Eager
72+
is MemoryLruGcSettings -> MemoryGarbageCollectorSettings.LRUGC(settings.sizeBytes)
73+
else -> throw IllegalArgumentException("Existing settings does not have valid GarbageCollectionSettings")
74+
}
75+
LocalCacheSettings.Memory(garbageCollectionSettings)
76+
}
77+
78+
is PersistentCacheSettings -> LocalCacheSettings.Persistent(localCacheSettings.sizeBytes)
79+
else -> throw IllegalArgumentException("Existing settings is not of a valid type")
80+
}
81+
} ?: kotlin.run {
82+
@Suppress("DEPRECATION")
83+
when {
84+
isPersistenceEnabled -> LocalCacheSettings.Persistent(cacheSizeBytes)
85+
cacheSizeBytes == FirebaseFirestoreSettings.CACHE_SIZE_UNLIMITED -> LocalCacheSettings.Memory(MemoryGarbageCollectorSettings.Eager)
86+
else -> LocalCacheSettings.Memory(MemoryGarbageCollectorSettings.LRUGC(cacheSizeBytes))
87+
}
88+
},
89+
callbackExecutorMap[native] ?: TaskExecutors.MAIN_THREAD
90+
)
91+
}
92+
set(value) {
93+
native.firestoreSettings = androidFirestoreSettings {
94+
isSslEnabled = value.sslEnabled
95+
host = value.host
96+
setLocalCacheSettings(value.cacheSettings.android)
97+
}
98+
callbackExecutorMap[native] = value.callbackExecutor
99+
}
29100

30-
actual fun collection(collectionPath: String) = CollectionReference(NativeCollectionReference(android.collection(collectionPath)))
101+
actual fun collection(collectionPath: String) = NativeCollectionReference(native.collection(collectionPath))
31102

32-
actual fun collectionGroup(collectionId: String) = Query(android.collectionGroup(collectionId).native)
103+
actual fun collectionGroup(collectionId: String) = native.collectionGroup(collectionId).native
33104

34-
actual fun document(documentPath: String) = DocumentReference(NativeDocumentReference(android.document(documentPath)))
105+
actual fun document(documentPath: String) = NativeDocumentReference(native.document(documentPath))
35106

36-
actual fun batch() = WriteBatch(NativeWriteBatch(android.batch()))
107+
actual fun batch() = NativeWriteBatch(native.batch())
37108

38109
actual fun setLoggingEnabled(loggingEnabled: Boolean) =
39110
com.google.firebase.firestore.FirebaseFirestore.setLoggingEnabled(loggingEnabled)
40111

41-
actual suspend fun <T> runTransaction(func: suspend Transaction.() -> T): T =
42-
android.runTransaction { runBlocking { Transaction(NativeTransaction(it)).func() } }.await()
112+
actual suspend fun <T> runTransaction(func: suspend NativeTransaction.() -> T): T =
113+
native.runTransaction { runBlocking { NativeTransaction(it).func() } }.await()
43114

44115
actual suspend fun clearPersistence() =
45-
android.clearPersistence().await().run { }
116+
native.clearPersistence().await().run { }
46117

47118
actual fun useEmulator(host: String, port: Int) {
48-
android.useEmulator(host, port)
49-
android.firestoreSettings = com.google.firebase.firestore.FirebaseFirestoreSettings.Builder()
50-
.setPersistenceEnabled(false)
51-
.build()
52-
}
53-
54-
actual fun setSettings(persistenceEnabled: Boolean?, sslEnabled: Boolean?, host: String?, cacheSizeBytes: Long?) {
55-
android.firestoreSettings = com.google.firebase.firestore.FirebaseFirestoreSettings.Builder().also { builder ->
56-
persistenceEnabled?.let { builder.setPersistenceEnabled(it) }
57-
sslEnabled?.let { builder.isSslEnabled = it }
58-
host?.let { builder.host = it }
59-
cacheSizeBytes?.let { builder.cacheSizeBytes = it }
60-
}.build()
119+
native.useEmulator(host, port)
61120
}
62121

63122
actual suspend fun disableNetwork() =
64-
android.disableNetwork().await().run { }
123+
native.disableNetwork().await().run { }
65124

66125
actual suspend fun enableNetwork() =
67-
android.enableNetwork().await().run { }
126+
native.enableNetwork().await().run { }
68127

69128
}
70129

130+
val FirebaseFirestore.android get() = native
131+
132+
actual data class FirebaseFirestoreSettings(
133+
actual val sslEnabled: Boolean,
134+
actual val host: String,
135+
actual val cacheSettings: LocalCacheSettings,
136+
val callbackExecutor: Executor,
137+
) {
138+
139+
actual companion object {
140+
actual val CACHE_SIZE_UNLIMITED: Long = -1L
141+
internal actual val DEFAULT_HOST: String = "firestore.googleapis.com"
142+
internal actual val MINIMUM_CACHE_BYTES: Long = 1 * 1024 * 1024
143+
internal actual val DEFAULT_CACHE_SIZE_BYTES: Long = 100 * 1024 * 1024
144+
}
145+
146+
actual class Builder internal constructor(
147+
actual var sslEnabled: Boolean,
148+
actual var host: String,
149+
actual var cacheSettings: LocalCacheSettings,
150+
var callbackExecutor: Executor,
151+
) {
152+
153+
actual constructor() : this(
154+
true,
155+
DEFAULT_HOST,
156+
persistentCacheSettings { },
157+
TaskExecutors.MAIN_THREAD
158+
)
159+
actual constructor(settings: FirebaseFirestoreSettings) : this(settings.sslEnabled, settings.host, settings.cacheSettings, settings.callbackExecutor)
160+
161+
actual fun build(): FirebaseFirestoreSettings = FirebaseFirestoreSettings(sslEnabled, host, cacheSettings, callbackExecutor)
162+
}
163+
}
164+
165+
actual fun firestoreSettings(
166+
settings: FirebaseFirestoreSettings?,
167+
builder: FirebaseFirestoreSettings.Builder.() -> Unit
168+
): FirebaseFirestoreSettings = FirebaseFirestoreSettings.Builder().apply {
169+
settings?.let {
170+
sslEnabled = it.sslEnabled
171+
host = it.host
172+
cacheSettings = it.cacheSettings
173+
callbackExecutor = it.callbackExecutor
174+
}
175+
}.apply(builder).build()
176+
71177
internal val SetOptions.android: com.google.firebase.firestore.SetOptions? get() = when (this) {
72178
is SetOptions.Merge -> com.google.firebase.firestore.SetOptions.merge()
73179
is SetOptions.Overwrite -> null
@@ -206,19 +312,27 @@ internal actual class NativeDocumentReference actual constructor(actual val nati
206312

207313
actual val snapshots: Flow<NativeDocumentSnapshot> get() = snapshots()
208314

209-
actual fun snapshots(includeMetadataChanges: Boolean) = callbackFlow {
210-
val metadataChanges = if(includeMetadataChanges) MetadataChanges.INCLUDE else MetadataChanges.EXCLUDE
211-
val listener = android.addSnapshotListener(metadataChanges) { snapshot, exception ->
212-
snapshot?.let { trySend(NativeDocumentSnapshot(snapshot)) }
213-
exception?.let { close(exception) }
214-
}
215-
awaitClose { listener.remove() }
315+
actual fun snapshots(includeMetadataChanges: Boolean) = addSnapshotListener(includeMetadataChanges) { snapshot, exception ->
316+
snapshot?.let { trySend(NativeDocumentSnapshot(snapshot)) }
317+
exception?.let { close(exception) }
216318
}
217319

218320
override fun equals(other: Any?): Boolean =
219321
this === other || other is NativeDocumentReference && nativeValue == other.nativeValue
220322
override fun hashCode(): Int = nativeValue.hashCode()
221323
override fun toString(): String = nativeValue.toString()
324+
325+
private fun addSnapshotListener(
326+
includeMetadataChanges: Boolean = false,
327+
listener: ProducerScope<NativeDocumentSnapshot>.(com.google.firebase.firestore.DocumentSnapshot?, com.google.firebase.firestore.FirebaseFirestoreException?) -> Unit
328+
) = callbackFlow {
329+
val executor = callbackExecutorMap[android.firestore] ?: TaskExecutors.MAIN_THREAD
330+
val metadataChanges = if(includeMetadataChanges) MetadataChanges.INCLUDE else MetadataChanges.EXCLUDE
331+
val registration = android.addSnapshotListener(executor, metadataChanges) { snapshots, exception ->
332+
listener(snapshots, exception)
333+
}
334+
awaitClose { registration.remove() }
335+
}
222336
}
223337

224338
val DocumentReference.android get() = native.android
@@ -235,21 +349,14 @@ actual open class Query internal actual constructor(nativeQuery: NativeQuery) {
235349

236350
actual fun limit(limit: Number) = Query(NativeQuery(android.limit(limit.toLong())))
237351

238-
actual val snapshots get() = callbackFlow<QuerySnapshot> {
239-
val listener = android.addSnapshotListener { snapshot, exception ->
240-
snapshot?.let { trySend(QuerySnapshot(snapshot)) }
241-
exception?.let { close(exception) }
242-
}
243-
awaitClose { listener.remove() }
352+
actual val snapshots get() = addSnapshotListener { snapshot, exception ->
353+
snapshot?.let { trySend(QuerySnapshot(snapshot)) }
354+
exception?.let { close(exception) }
244355
}
245356

246-
actual fun snapshots(includeMetadataChanges: Boolean) = callbackFlow<QuerySnapshot> {
247-
val metadataChanges = if(includeMetadataChanges) MetadataChanges.INCLUDE else MetadataChanges.EXCLUDE
248-
val listener = android.addSnapshotListener(metadataChanges) { snapshot, exception ->
249-
snapshot?.let { trySend(QuerySnapshot(snapshot)) }
250-
exception?.let { close(exception) }
251-
}
252-
awaitClose { listener.remove() }
357+
actual fun snapshots(includeMetadataChanges: Boolean) = addSnapshotListener(includeMetadataChanges) { snapshot, exception ->
358+
snapshot?.let { trySend(QuerySnapshot(snapshot)) }
359+
exception?.let { close(exception) }
253360
}
254361

255362
internal actual fun where(filter: Filter) = Query(
@@ -331,6 +438,18 @@ actual open class Query internal actual constructor(nativeQuery: NativeQuery) {
331438
internal actual fun _endBefore(vararg fieldValues: Any) = Query(android.endBefore(*fieldValues).native)
332439
internal actual fun _endAt(document: DocumentSnapshot) = Query(android.endAt(document.android).native)
333440
internal actual fun _endAt(vararg fieldValues: Any) = Query(android.endAt(*fieldValues).native)
441+
442+
private fun addSnapshotListener(
443+
includeMetadataChanges: Boolean = false,
444+
listener: ProducerScope<QuerySnapshot>.(com.google.firebase.firestore.QuerySnapshot?, com.google.firebase.firestore.FirebaseFirestoreException?) -> Unit
445+
) = callbackFlow {
446+
val executor = callbackExecutorMap[android.firestore] ?: TaskExecutors.MAIN_THREAD
447+
val metadataChanges = if(includeMetadataChanges) MetadataChanges.INCLUDE else MetadataChanges.EXCLUDE
448+
val registration = android.addSnapshotListener(executor, metadataChanges) { snapshots, exception ->
449+
listener(snapshots, exception)
450+
}
451+
awaitClose { registration.remove() }
452+
}
334453
}
335454

336455
actual typealias Direction = com.google.firebase.firestore.Query.Direction

0 commit comments

Comments
 (0)