Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Settings to deal with newer settings & add callback threads #450

Merged
merged 15 commits into from
Apr 20, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,14 @@ actual class FirebaseDatabase internal constructor(val ios: FIRDatabase) {
ios.persistenceEnabled = enabled
}

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

fun setCallbackQueue(callbackQueue: dispatch_queue_t) {
ios.callbackQueue = callbackQueue
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets not start adding coverage for platform specific APIs, let users use .ios


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,15 +5,26 @@
@file:JvmName("android")
package dev.gitlive.firebase.firestore

import com.google.android.gms.tasks.TaskExecutors
import com.google.firebase.firestore.FirebaseFirestoreSettings
import com.google.firebase.firestore.MetadataChanges
import com.google.firebase.firestore.firestoreSettings
import com.google.firebase.firestore.memoryCacheSettings
import com.google.firebase.firestore.memoryEagerGcSettings
import com.google.firebase.firestore.memoryLruGcSettings
import com.google.firebase.firestore.persistentCacheSettings
import dev.gitlive.firebase.Firebase
import dev.gitlive.firebase.FirebaseApp
import kotlinx.coroutines.channels.ProducerScope
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.lang.IllegalArgumentException
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
Expand All @@ -24,8 +35,65 @@ actual val Firebase.firestore get() =
actual fun Firebase.firestore(app: FirebaseApp) =
FirebaseFirestore(com.google.firebase.firestore.FirebaseFirestore.getInstance(app.android))

val LocalCacheSettings.android: com.google.firebase.firestore.LocalCacheSettings get() = when (this) {
is LocalCacheSettings.Persistent -> persistentCacheSettings {
setSizeBytes(sizeBytes)
}
is LocalCacheSettings.Memory -> memoryCacheSettings {
setGcSettings(
when (garbaseCollectorSettings) {
is GarbageCollectorSettings.Eager -> memoryEagerGcSettings { }
is GarbageCollectorSettings.LRUGC -> memoryLruGcSettings {
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 class FirebaseFirestore(val android: com.google.firebase.firestore.FirebaseFirestore) {

actual var settings: FirestoreSettings
get() = with(android.firestoreSettings) {
FirestoreSettings(
isSslEnabled,
host,
cacheSettings?.let { localCacheSettings ->
when (localCacheSettings) {
is com.google.firebase.firestore.MemoryCacheSettings -> {
val garbageCollectionSettings = when (val settings = localCacheSettings.garbageCollectorSettings) {
is com.google.firebase.firestore.MemoryEagerGcSettings -> GarbageCollectorSettings.Eager
is com.google.firebase.firestore.MemoryLruGcSettings -> GarbageCollectorSettings.LRUGC(settings.sizeBytes)
else -> throw IllegalArgumentException("Existing settings does not have valid GarbageCollectionSettings")
}
LocalCacheSettings.Memory(garbageCollectionSettings)
}
is com.google.firebase.firestore.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 == FirestoreSettings.CACHE_SIZE_UNLIMITED -> LocalCacheSettings.Memory(GarbageCollectorSettings.Eager)
else -> LocalCacheSettings.Memory(GarbageCollectorSettings.LRUGC(cacheSizeBytes))
}
},
callbackExecutorMap[android] ?: TaskExecutors.MAIN_THREAD
)
}
set(value) {
android.firestoreSettings = firestoreSettings {
isSslEnabled = value.sslEnabled
host = value.host
setLocalCacheSettings(value.cacheSettings.android)
}
callbackExecutorMap[android] = value.callbackExecutor
}

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

actual fun collectionGroup(collectionId: String) = Query(android.collectionGroup(collectionId).native)
Expand All @@ -45,18 +113,6 @@ actual class FirebaseFirestore(val android: com.google.firebase.firestore.Fireba

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()
}

actual suspend fun disableNetwork() =
Expand All @@ -67,6 +123,46 @@ actual class FirebaseFirestore(val android: com.google.firebase.firestore.Fireba

}

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

actual companion object {}

actual interface Builder {
actual var sslEnabled: Boolean
actual var host: String
actual var cacheSettings: LocalCacheSettings
var callbackExecutor: Executor

actual fun build(): FirestoreSettings
}

internal class BuilderImpl : Builder {
override var sslEnabled: Boolean = true
override var host: String = FirestoreSettings.DEFAULT_HOST
override var cacheSettings: LocalCacheSettings = LocalCacheSettings.Persistent(CACHE_SIZE_UNLIMITED)
override var callbackExecutor: Executor = TaskExecutors.MAIN_THREAD

override fun build() = FirestoreSettings(sslEnabled, host, cacheSettings, callbackExecutor)
}
}

actual fun firestoreSettings(
settings: FirestoreSettings?,
builder: FirestoreSettings.Builder.() -> Unit
): FirestoreSettings = FirestoreSettings.BuilderImpl().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 @@ -205,19 +301,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 @@ -234,21 +338,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 @@ -330,6 +427,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
Loading