Skip to content

Commit

Permalink
Merge pull request #450 from splendo/feature/firestore-settings
Browse files Browse the repository at this point in the history
Added Settings to deal with newer settings & add callback threads
  • Loading branch information
nbransby authored Apr 20, 2024
2 parents ad577b3 + 5a28653 commit 8daf907
Show file tree
Hide file tree
Showing 14 changed files with 625 additions and 115 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ package dev.gitlive.firebase.auth
actual val emulatorHost: String = "10.0.2.2"

actual val context: Any = Unit

@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
actual annotation class IgnoreForAndroidUnitTest
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* Copyright (c) 2020 GitLive Ltd. Use of this source code is governed by the Apache 2.0 license.
*/

@file:JvmName("databaseAndroid")
package dev.gitlive.firebase.database

import com.google.android.gms.tasks.Task
Expand All @@ -16,7 +17,6 @@ import dev.gitlive.firebase.EncodeDecodeSettingsBuilder
import dev.gitlive.firebase.Firebase
import dev.gitlive.firebase.FirebaseApp
import dev.gitlive.firebase.database.ChildEvent.Type
import dev.gitlive.firebase.database.FirebaseDatabase.Companion.FirebaseDatabase
import dev.gitlive.firebase.decode
import dev.gitlive.firebase.reencodeTransformation
import kotlinx.coroutines.CompletableDeferred
Expand Down Expand Up @@ -50,23 +50,23 @@ suspend fun <T> Task<T>.awaitWhileOnline(database: FirebaseDatabase): T =
.first()

actual val Firebase.database
by lazy { FirebaseDatabase(com.google.firebase.database.FirebaseDatabase.getInstance()) }
by lazy { FirebaseDatabase.getInstance(com.google.firebase.database.FirebaseDatabase.getInstance()) }

actual fun Firebase.database(url: String) =
FirebaseDatabase(com.google.firebase.database.FirebaseDatabase.getInstance(url))
FirebaseDatabase.getInstance(com.google.firebase.database.FirebaseDatabase.getInstance(url))

actual fun Firebase.database(app: FirebaseApp) =
FirebaseDatabase(com.google.firebase.database.FirebaseDatabase.getInstance(app.android))
FirebaseDatabase.getInstance(com.google.firebase.database.FirebaseDatabase.getInstance(app.android))

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

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

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

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

actual fun setPersistenceEnabled(enabled: Boolean) =
android.setPersistenceEnabled(enabled).also { persistenceEnabled = enabled }
actual fun setPersistenceEnabled(enabled: Boolean) {
android.setPersistenceEnabled(enabled)
persistenceEnabled = enabled
}

actual fun setPersistenceCacheSizeBytes(cacheSizeInBytes: Long) {
android.setPersistenceCacheSizeBytes(cacheSizeInBytes)
}

actual fun setLoggingEnabled(enabled: Boolean) =
android.setLogLevel(Logger.Level.DEBUG.takeIf { enabled } ?: Logger.Level.NONE)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,12 @@ expect fun Firebase.database(app: FirebaseApp): FirebaseDatabase
expect fun Firebase.database(app: FirebaseApp, url: String): FirebaseDatabase

expect class FirebaseDatabase {

fun reference(path: String): DatabaseReference
fun reference(): DatabaseReference
fun setPersistenceEnabled(enabled: Boolean)
fun setLoggingEnabled(enabled: Boolean)
fun setPersistenceEnabled(enabled: Boolean)
fun setPersistenceCacheSizeBytes(cacheSizeInBytes: Long)
fun useEmulator(host: String, port: Int)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.KSerializer
import platform.Foundation.NSError
import platform.Foundation.allObjects
import platform.darwin.dispatch_queue_t
import kotlin.collections.component1
import kotlin.collections.component2

actual val Firebase.database
by lazy { FirebaseDatabase(FIRDatabase.database()) }
Expand Down Expand Up @@ -64,6 +67,10 @@ actual class FirebaseDatabase internal constructor(val ios: FIRDatabase) {
ios.persistenceEnabled = enabled
}

actual fun setPersistenceCacheSizeBytes(cacheSizeInBytes: Long) {
ios.setPersistenceCacheSizeBytes(cacheSizeInBytes.toULong())
}

actual fun setLoggingEnabled(enabled: Boolean) =
FIRDatabase.setLoggingEnabled(enabled)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,11 @@ actual fun Firebase.database(app: FirebaseApp, url: String) =
rethrow { FirebaseDatabase(getDatabase(app = app.js, url = url)) }

actual class FirebaseDatabase internal constructor(val js: Database) {

actual fun reference(path: String) = rethrow { DatabaseReference(NativeDatabaseReference(ref(js, path), js)) }
actual fun reference() = rethrow { DatabaseReference(NativeDatabaseReference(ref(js), js)) }
actual fun setPersistenceEnabled(enabled: Boolean) {}
actual fun setPersistenceCacheSizeBytes(cacheSizeInBytes: Long) {}
actual fun setLoggingEnabled(enabled: Boolean) = rethrow { enableLogging(enabled) }
actual fun useEmulator(host: String, port: Int) = rethrow { connectDatabaseEmulator(js, host, port) }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,69 +5,175 @@
@file:JvmName("android")
package dev.gitlive.firebase.firestore

import com.google.android.gms.tasks.TaskExecutors
import com.google.firebase.firestore.MemoryCacheSettings
import com.google.firebase.firestore.MemoryEagerGcSettings
import com.google.firebase.firestore.MemoryLruGcSettings
import com.google.firebase.firestore.MetadataChanges
import com.google.firebase.firestore.PersistentCacheSettings
import dev.gitlive.firebase.Firebase
import dev.gitlive.firebase.FirebaseApp
import kotlinx.coroutines.channels.ProducerScope
import dev.gitlive.firebase.firestore.Source.*
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.tasks.await
import kotlinx.serialization.Serializable
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executor
import com.google.firebase.firestore.FieldPath as AndroidFieldPath
import com.google.firebase.firestore.Filter as AndroidFilter
import com.google.firebase.firestore.Query as AndroidQuery
import com.google.firebase.firestore.firestoreSettings as androidFirestoreSettings
import com.google.firebase.firestore.memoryCacheSettings as androidMemoryCacheSettings
import com.google.firebase.firestore.memoryEagerGcSettings as androidMemoryEagerGcSettings
import com.google.firebase.firestore.memoryLruGcSettings as androidMemoryLruGcSettings
import com.google.firebase.firestore.persistentCacheSettings as androidPersistentCacheSettings

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

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

actual class FirebaseFirestore(val android: com.google.firebase.firestore.FirebaseFirestore) {
val LocalCacheSettings.android: com.google.firebase.firestore.LocalCacheSettings get() = when (this) {
is LocalCacheSettings.Persistent -> androidPersistentCacheSettings {
setSizeBytes(sizeBytes)
}
is LocalCacheSettings.Memory -> androidMemoryCacheSettings {
setGcSettings(
when (garbaseCollectorSettings) {
is MemoryGarbageCollectorSettings.Eager -> androidMemoryEagerGcSettings { }
is MemoryGarbageCollectorSettings.LRUGC -> androidMemoryLruGcSettings {
setSizeBytes(garbaseCollectorSettings.sizeBytes)
}
}
)
}
}

// Since on iOS Callback threads are set as settings, we store the settings explicitly here as well
private val callbackExecutorMap = ConcurrentHashMap<com.google.firebase.firestore.FirebaseFirestore, Executor>()

actual typealias NativeFirebaseFirestore = com.google.firebase.firestore.FirebaseFirestore
internal actual class NativeFirebaseFirestoreWrapper actual constructor(actual val native: NativeFirebaseFirestore) {

actual var settings: FirebaseFirestoreSettings
get() = with(native.firestoreSettings) {
FirebaseFirestoreSettings(
isSslEnabled,
host,
cacheSettings?.let { localCacheSettings ->
when (localCacheSettings) {
is MemoryCacheSettings -> {
val garbageCollectionSettings = when (val settings = localCacheSettings.garbageCollectorSettings) {
is MemoryEagerGcSettings -> MemoryGarbageCollectorSettings.Eager
is MemoryLruGcSettings -> MemoryGarbageCollectorSettings.LRUGC(settings.sizeBytes)
else -> throw IllegalArgumentException("Existing settings does not have valid GarbageCollectionSettings")
}
LocalCacheSettings.Memory(garbageCollectionSettings)
}

is PersistentCacheSettings -> LocalCacheSettings.Persistent(localCacheSettings.sizeBytes)
else -> throw IllegalArgumentException("Existing settings is not of a valid type")
}
} ?: kotlin.run {
@Suppress("DEPRECATION")
when {
isPersistenceEnabled -> LocalCacheSettings.Persistent(cacheSizeBytes)
cacheSizeBytes == FirebaseFirestoreSettings.CACHE_SIZE_UNLIMITED -> LocalCacheSettings.Memory(MemoryGarbageCollectorSettings.Eager)
else -> LocalCacheSettings.Memory(MemoryGarbageCollectorSettings.LRUGC(cacheSizeBytes))
}
},
callbackExecutorMap[native] ?: TaskExecutors.MAIN_THREAD
)
}
set(value) {
native.firestoreSettings = androidFirestoreSettings {
isSslEnabled = value.sslEnabled
host = value.host
setLocalCacheSettings(value.cacheSettings.android)
}
callbackExecutorMap[native] = value.callbackExecutor
}

actual fun collection(collectionPath: String) = CollectionReference(NativeCollectionReference(android.collection(collectionPath)))
actual fun collection(collectionPath: String) = NativeCollectionReference(native.collection(collectionPath))

actual fun collectionGroup(collectionId: String) = Query(android.collectionGroup(collectionId).native)
actual fun collectionGroup(collectionId: String) = native.collectionGroup(collectionId).native

actual fun document(documentPath: String) = DocumentReference(NativeDocumentReference(android.document(documentPath)))
actual fun document(documentPath: String) = NativeDocumentReference(native.document(documentPath))

actual fun batch() = WriteBatch(NativeWriteBatch(android.batch()))
actual fun batch() = NativeWriteBatch(native.batch())

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

actual suspend fun <T> runTransaction(func: suspend Transaction.() -> T): T =
android.runTransaction { runBlocking { Transaction(NativeTransaction(it)).func() } }.await()
actual suspend fun <T> runTransaction(func: suspend NativeTransaction.() -> T): T =
native.runTransaction { runBlocking { NativeTransaction(it).func() } }.await()

actual suspend fun clearPersistence() =
android.clearPersistence().await().run { }
native.clearPersistence().await().run { }

actual fun useEmulator(host: String, port: Int) {
android.useEmulator(host, port)
android.firestoreSettings = com.google.firebase.firestore.FirebaseFirestoreSettings.Builder()
.setPersistenceEnabled(false)
.build()
}

actual fun setSettings(persistenceEnabled: Boolean?, sslEnabled: Boolean?, host: String?, cacheSizeBytes: Long?) {
android.firestoreSettings = com.google.firebase.firestore.FirebaseFirestoreSettings.Builder().also { builder ->
persistenceEnabled?.let { builder.setPersistenceEnabled(it) }
sslEnabled?.let { builder.isSslEnabled = it }
host?.let { builder.host = it }
cacheSizeBytes?.let { builder.cacheSizeBytes = it }
}.build()
native.useEmulator(host, port)
}

actual suspend fun disableNetwork() =
android.disableNetwork().await().run { }
native.disableNetwork().await().run { }

actual suspend fun enableNetwork() =
android.enableNetwork().await().run { }
native.enableNetwork().await().run { }

}

val FirebaseFirestore.android get() = native

actual data class FirebaseFirestoreSettings(
actual val sslEnabled: Boolean,
actual val host: String,
actual val cacheSettings: LocalCacheSettings,
val callbackExecutor: Executor,
) {

actual companion object {
actual val CACHE_SIZE_UNLIMITED: Long = -1L
internal actual val DEFAULT_HOST: String = "firestore.googleapis.com"
internal actual val MINIMUM_CACHE_BYTES: Long = 1 * 1024 * 1024
internal actual val DEFAULT_CACHE_SIZE_BYTES: Long = 100 * 1024 * 1024
}

actual class Builder internal constructor(
actual var sslEnabled: Boolean,
actual var host: String,
actual var cacheSettings: LocalCacheSettings,
var callbackExecutor: Executor,
) {

actual constructor() : this(
true,
DEFAULT_HOST,
persistentCacheSettings { },
TaskExecutors.MAIN_THREAD
)
actual constructor(settings: FirebaseFirestoreSettings) : this(settings.sslEnabled, settings.host, settings.cacheSettings, settings.callbackExecutor)

actual fun build(): FirebaseFirestoreSettings = FirebaseFirestoreSettings(sslEnabled, host, cacheSettings, callbackExecutor)
}
}

actual fun firestoreSettings(
settings: FirebaseFirestoreSettings?,
builder: FirebaseFirestoreSettings.Builder.() -> Unit
): FirebaseFirestoreSettings = FirebaseFirestoreSettings.Builder().apply {
settings?.let {
sslEnabled = it.sslEnabled
host = it.host
cacheSettings = it.cacheSettings
callbackExecutor = it.callbackExecutor
}
}.apply(builder).build()

internal val SetOptions.android: com.google.firebase.firestore.SetOptions? get() = when (this) {
is SetOptions.Merge -> com.google.firebase.firestore.SetOptions.merge()
is SetOptions.Overwrite -> null
Expand Down Expand Up @@ -206,19 +312,27 @@ internal actual class NativeDocumentReference actual constructor(actual val nati

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

actual fun snapshots(includeMetadataChanges: Boolean) = callbackFlow {
val metadataChanges = if(includeMetadataChanges) MetadataChanges.INCLUDE else MetadataChanges.EXCLUDE
val listener = android.addSnapshotListener(metadataChanges) { snapshot, exception ->
snapshot?.let { trySend(NativeDocumentSnapshot(snapshot)) }
exception?.let { close(exception) }
}
awaitClose { listener.remove() }
actual fun snapshots(includeMetadataChanges: Boolean) = addSnapshotListener(includeMetadataChanges) { snapshot, exception ->
snapshot?.let { trySend(NativeDocumentSnapshot(snapshot)) }
exception?.let { close(exception) }
}

override fun equals(other: Any?): Boolean =
this === other || other is NativeDocumentReference && nativeValue == other.nativeValue
override fun hashCode(): Int = nativeValue.hashCode()
override fun toString(): String = nativeValue.toString()

private fun addSnapshotListener(
includeMetadataChanges: Boolean = false,
listener: ProducerScope<NativeDocumentSnapshot>.(com.google.firebase.firestore.DocumentSnapshot?, com.google.firebase.firestore.FirebaseFirestoreException?) -> Unit
) = callbackFlow {
val executor = callbackExecutorMap[android.firestore] ?: TaskExecutors.MAIN_THREAD
val metadataChanges = if(includeMetadataChanges) MetadataChanges.INCLUDE else MetadataChanges.EXCLUDE
val registration = android.addSnapshotListener(executor, metadataChanges) { snapshots, exception ->
listener(snapshots, exception)
}
awaitClose { registration.remove() }
}
}

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

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

actual val snapshots get() = callbackFlow<QuerySnapshot> {
val listener = android.addSnapshotListener { snapshot, exception ->
snapshot?.let { trySend(QuerySnapshot(snapshot)) }
exception?.let { close(exception) }
}
awaitClose { listener.remove() }
actual val snapshots get() = addSnapshotListener { snapshot, exception ->
snapshot?.let { trySend(QuerySnapshot(snapshot)) }
exception?.let { close(exception) }
}

actual fun snapshots(includeMetadataChanges: Boolean) = callbackFlow<QuerySnapshot> {
val metadataChanges = if(includeMetadataChanges) MetadataChanges.INCLUDE else MetadataChanges.EXCLUDE
val listener = android.addSnapshotListener(metadataChanges) { snapshot, exception ->
snapshot?.let { trySend(QuerySnapshot(snapshot)) }
exception?.let { close(exception) }
}
awaitClose { listener.remove() }
actual fun snapshots(includeMetadataChanges: Boolean) = addSnapshotListener(includeMetadataChanges) { snapshot, exception ->
snapshot?.let { trySend(QuerySnapshot(snapshot)) }
exception?.let { close(exception) }
}

internal actual fun where(filter: Filter) = Query(
Expand Down Expand Up @@ -331,6 +438,18 @@ actual open class Query internal actual constructor(nativeQuery: NativeQuery) {
internal actual fun _endBefore(vararg fieldValues: Any) = Query(android.endBefore(*fieldValues).native)
internal actual fun _endAt(document: DocumentSnapshot) = Query(android.endAt(document.android).native)
internal actual fun _endAt(vararg fieldValues: Any) = Query(android.endAt(*fieldValues).native)

private fun addSnapshotListener(
includeMetadataChanges: Boolean = false,
listener: ProducerScope<QuerySnapshot>.(com.google.firebase.firestore.QuerySnapshot?, com.google.firebase.firestore.FirebaseFirestoreException?) -> Unit
) = callbackFlow {
val executor = callbackExecutorMap[android.firestore] ?: TaskExecutors.MAIN_THREAD
val metadataChanges = if(includeMetadataChanges) MetadataChanges.INCLUDE else MetadataChanges.EXCLUDE
val registration = android.addSnapshotListener(executor, metadataChanges) { snapshots, exception ->
listener(snapshots, exception)
}
awaitClose { registration.remove() }
}
}

actual typealias Direction = com.google.firebase.firestore.Query.Direction
Expand Down
Loading

0 comments on commit 8daf907

Please sign in to comment.