From c80c5f2c032e778b095508292c1a78bfa2ae745d Mon Sep 17 00:00:00 2001 From: Gijs van Veen Date: Wed, 20 Dec 2023 17:25:13 +0100 Subject: [PATCH 1/8] Added Settings to deal with newer settings & add callback threads --- .../kotlin/dev/gitlive/firebase/auth/auth.kt | 3 + .../dev/gitlive/firebase/database/database.kt | 31 +- .../dev/gitlive/firebase/database/database.kt | 14 +- .../dev/gitlive/firebase/database/database.kt | 18 +- .../dev/gitlive/firebase/database/database.kt | 13 +- firebase-firestore/build.gradle.kts | 4 - .../gitlive/firebase/firestore/firestore.kt | 126 +++-- .../gitlive/firebase/firestore/firestore.kt | 31 +- .../gitlive/firebase/firestore/firestore.kt | 2 +- .../gitlive/firebase/firestore/firestore.kt | 48 +- .../gitlive/firebase/firestore/firestore.kt | 40 +- .../gitlive/firebase/firestore/Geopoint.kt | 19 + .../gitlive/firebase/firestore/Timestamp.kt | 35 ++ .../gitlive/firebase/firestore/firestore.kt | 521 ++++++++++++++++++ .../kotlin/dev/gitlive/firebase/TestUtils.kt | 1 - .../kotlin/dev/gitlive/firebase/TestUtils.kt | 17 - 16 files changed, 838 insertions(+), 85 deletions(-) create mode 100644 firebase-firestore/src/jvmMain/kotlin/dev/gitlive/firebase/firestore/Geopoint.kt create mode 100644 firebase-firestore/src/jvmMain/kotlin/dev/gitlive/firebase/firestore/Timestamp.kt create mode 100644 firebase-firestore/src/jvmMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt delete mode 100644 test-utils/src/jvmMain/kotlin/dev/gitlive/firebase/TestUtils.kt diff --git a/firebase-auth/src/jvmTest/kotlin/dev/gitlive/firebase/auth/auth.kt b/firebase-auth/src/jvmTest/kotlin/dev/gitlive/firebase/auth/auth.kt index 55c7b7cea..d83d421fb 100644 --- a/firebase-auth/src/jvmTest/kotlin/dev/gitlive/firebase/auth/auth.kt +++ b/firebase-auth/src/jvmTest/kotlin/dev/gitlive/firebase/auth/auth.kt @@ -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 diff --git a/firebase-database/src/androidMain/kotlin/dev/gitlive/firebase/database/database.kt b/firebase-database/src/androidMain/kotlin/dev/gitlive/firebase/database/database.kt index 0ce0e0d12..2451e40d0 100644 --- a/firebase-database/src/androidMain/kotlin/dev/gitlive/firebase/database/database.kt +++ b/firebase-database/src/androidMain/kotlin/dev/gitlive/firebase/database/database.kt @@ -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 @@ -9,7 +10,6 @@ import com.google.firebase.database.* 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.encode import kotlinx.coroutines.CompletableDeferred @@ -37,27 +37,37 @@ suspend fun Task.awaitWhileOnline(): T = 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() - internal fun FirebaseDatabase( + internal fun getInstance( android: com.google.firebase.database.FirebaseDatabase ) = instances.getOrPut(android) { dev.gitlive.firebase.database.FirebaseDatabase(android) } } + actual data class Settings( + actual val persistenceEnabled: Boolean = false, + actual val persistenceCacheSizeBytes: Long? = null, + ) { + + actual companion object { + actual fun createSettings(persistenceEnabled: Boolean, persistenceCacheSizeBytes: Long?) = Settings(persistenceEnabled, persistenceCacheSizeBytes) + } + } + private var persistenceEnabled = true actual fun reference(path: String) = @@ -66,8 +76,11 @@ actual class FirebaseDatabase private constructor(val android: com.google.fireba actual fun reference() = DatabaseReference(android.reference, persistenceEnabled) - actual fun setPersistenceEnabled(enabled: Boolean) = - android.setPersistenceEnabled(enabled).also { persistenceEnabled = enabled } + actual fun setSettings(settings: Settings) { + android.setPersistenceEnabled(settings.persistenceEnabled) + persistenceEnabled = settings.persistenceEnabled + settings.persistenceCacheSizeBytes?.let { android.setPersistenceCacheSizeBytes(it) } + } actual fun setLoggingEnabled(enabled: Boolean) = android.setLogLevel(Logger.Level.DEBUG.takeIf { enabled } ?: Logger.Level.NONE) diff --git a/firebase-database/src/commonMain/kotlin/dev/gitlive/firebase/database/database.kt b/firebase-database/src/commonMain/kotlin/dev/gitlive/firebase/database/database.kt index d5bbf6aee..ca4cd2a51 100644 --- a/firebase-database/src/commonMain/kotlin/dev/gitlive/firebase/database/database.kt +++ b/firebase-database/src/commonMain/kotlin/dev/gitlive/firebase/database/database.kt @@ -25,13 +25,25 @@ expect fun Firebase.database(app: FirebaseApp): FirebaseDatabase expect fun Firebase.database(app: FirebaseApp, url: String): FirebaseDatabase expect class FirebaseDatabase { + + class Settings { + val persistenceEnabled: Boolean + val persistenceCacheSizeBytes: Long? + + companion object { + fun createSettings(persistenceEnabled: Boolean = false, persistenceCacheSizeBytes: Long? = null): Settings + } + } + fun reference(path: String): DatabaseReference fun reference(): DatabaseReference - fun setPersistenceEnabled(enabled: Boolean) + fun setSettings(settings: Settings) fun setLoggingEnabled(enabled: Boolean) fun useEmulator(host: String, port: Int) } +fun FirebaseDatabase.setPersistenceEnabled(enabled: Boolean) = setSettings(FirebaseDatabase.Settings.createSettings(persistenceEnabled = enabled)) + data class ChildEvent internal constructor( val snapshot: DataSnapshot, val type: Type, diff --git a/firebase-database/src/iosMain/kotlin/dev/gitlive/firebase/database/database.kt b/firebase-database/src/iosMain/kotlin/dev/gitlive/firebase/database/database.kt index 128d7b6c2..a717eaceb 100644 --- a/firebase-database/src/iosMain/kotlin/dev/gitlive/firebase/database/database.kt +++ b/firebase-database/src/iosMain/kotlin/dev/gitlive/firebase/database/database.kt @@ -25,6 +25,7 @@ import kotlinx.serialization.KSerializer import kotlinx.serialization.SerializationStrategy import platform.Foundation.NSError import platform.Foundation.allObjects +import platform.darwin.dispatch_queue_t import kotlin.collections.component1 import kotlin.collections.component2 @@ -44,14 +45,27 @@ actual fun Firebase.database(app: FirebaseApp, url: String): FirebaseDatabase = actual class FirebaseDatabase internal constructor(val ios: FIRDatabase) { + actual data class Settings( + actual val persistenceEnabled: Boolean = false, + actual val persistenceCacheSizeBytes: Long? = null, + val callbackQueue: dispatch_queue_t = null + ) { + + actual companion object { + actual fun createSettings(persistenceEnabled: Boolean, persistenceCacheSizeBytes: Long?) = Settings(persistenceEnabled, persistenceCacheSizeBytes) + } + } + actual fun reference(path: String) = DatabaseReference(ios.referenceWithPath(path), ios.persistenceEnabled) actual fun reference() = DatabaseReference(ios.reference(), ios.persistenceEnabled) - actual fun setPersistenceEnabled(enabled: Boolean) { - ios.persistenceEnabled = enabled + actual fun setSettings(settings: Settings) { + ios.persistenceEnabled = settings.persistenceEnabled + settings.persistenceCacheSizeBytes?.let { ios.setPersistenceCacheSizeBytes(it.toULong()) } + settings.callbackQueue?.let { ios.callbackQueue = it } } actual fun setLoggingEnabled(enabled: Boolean) = diff --git a/firebase-database/src/jsMain/kotlin/dev/gitlive/firebase/database/database.kt b/firebase-database/src/jsMain/kotlin/dev/gitlive/firebase/database/database.kt index d4d47b6be..4139a553e 100644 --- a/firebase-database/src/jsMain/kotlin/dev/gitlive/firebase/database/database.kt +++ b/firebase-database/src/jsMain/kotlin/dev/gitlive/firebase/database/database.kt @@ -44,9 +44,20 @@ 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 data class Settings( + actual val persistenceEnabled: Boolean = false, + actual val persistenceCacheSizeBytes: Long? = null, + ) { + + actual companion object { + actual fun createSettings(persistenceEnabled: Boolean, persistenceCacheSizeBytes: Long?) = Settings(persistenceEnabled, persistenceCacheSizeBytes) + } + } + actual fun reference(path: String) = rethrow { DatabaseReference(ref(js, path), js) } actual fun reference() = rethrow { DatabaseReference(ref(js), js) } - actual fun setPersistenceEnabled(enabled: Boolean) {} + actual fun setSettings(settings: Settings) {} actual fun setLoggingEnabled(enabled: Boolean) = rethrow { enableLogging(enabled) } actual fun useEmulator(host: String, port: Int) = rethrow { connectDatabaseEmulator(js, host, port) } } diff --git a/firebase-firestore/build.gradle.kts b/firebase-firestore/build.gradle.kts index 5679694a4..921ccd57f 100644 --- a/firebase-firestore/build.gradle.kts +++ b/firebase-firestore/build.gradle.kts @@ -171,10 +171,6 @@ kotlin { api("com.google.firebase:firebase-firestore") } } - - getByName("jvmMain") { - kotlin.srcDir("src/androidMain/kotlin") - } } } diff --git a/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt index dcc6a2ebc..1ba855b90 100644 --- a/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt +++ b/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt @@ -5,9 +5,12 @@ @file:JvmName("android") package dev.gitlive.firebase.firestore -import com.google.android.gms.tasks.Task +import com.google.android.gms.tasks.TaskExecutors import com.google.firebase.firestore.* +import com.google.firebase.firestore.util.Executors import dev.gitlive.firebase.* +import dev.gitlive.firebase.firestore.FirebaseFirestoreException +import kotlinx.coroutines.channels.ProducerScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow @@ -16,6 +19,8 @@ import kotlinx.coroutines.tasks.await import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.Serializable import kotlinx.serialization.SerializationStrategy +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.Executor actual val Firebase.firestore get() = FirebaseFirestore(com.google.firebase.firestore.FirebaseFirestore.getInstance()) @@ -37,8 +42,40 @@ private fun performUpdate( update: (com.google.firebase.firestore.FieldPath, Any?, Array) -> R ) = performUpdate(fieldsAndValues, { it.android }, { encode(it, true) }, update) +val LocalCacheSettings.android: com.google.firebase.firestore.LocalCacheSettings get() = when (this) { + is LocalCacheSettings.Persistent -> persistentCacheSettings { + sizeBytes?.let { setSizeBytes(it) } + } + is LocalCacheSettings.Memory -> memoryCacheSettings { + setGcSettings( + when (garbaseCollectorSettings) { + is LocalCacheSettings.Memory.GarbageCollectorSettings.Eager -> memoryEagerGcSettings { } + is LocalCacheSettings.Memory.GarbageCollectorSettings.LRUGC -> memoryLruGcSettings { + garbaseCollectorSettings.sizeBytes?.let { + setSizeBytes(it) + } + } + } + ) + } +} + +// Since on iOS Callback threads are set as settings, we store the settings explicitly here as well +private val callbackExecutorMap = ConcurrentHashMap() + actual class FirebaseFirestore(val android: com.google.firebase.firestore.FirebaseFirestore) { + actual data class Settings( + actual val sslEnabled: Boolean? = null, + actual val host: String? = null, + actual val cacheSettings: LocalCacheSettings? = null, + val callbackExecutor: Executor = TaskExecutors.MAIN_THREAD, + ) { + actual companion object { + actual fun create(sslEnabled: Boolean?, host: String?, cacheSettings: LocalCacheSettings?) = Settings(sslEnabled, host, cacheSettings) + } + } + actual fun collection(collectionPath: String) = CollectionReference(android.collection(collectionPath)) actual fun collectionGroup(collectionId: String) = Query(android.collectionGroup(collectionId)) @@ -58,19 +95,33 @@ 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() + android.firestoreSettings = firestoreSettings { } } - 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 fun setSettings(settings: Settings) { + android.firestoreSettings = firestoreSettings { + settings.sslEnabled?.let { isSslEnabled = it } + settings.host?.let { host = it } + settings.cacheSettings?.let { setLocalCacheSettings(it.android) } } + callbackExecutorMap[android] = settings.callbackExecutor + } + + @Suppress("DEPRECATION") + actual fun updateSettings(settings: Settings) { + android.firestoreSettings = firestoreSettings { + isSslEnabled = settings.sslEnabled ?: android.firestoreSettings.isSslEnabled + host = settings.host ?: android.firestoreSettings.host + val cacheSettings = settings.cacheSettings?.android ?: android.firestoreSettings.cacheSettings + cacheSettings?.let { + setLocalCacheSettings(it) + } ?: kotlin.run { + isPersistenceEnabled = android.firestoreSettings.isPersistenceEnabled + setCacheSizeBytes(android.firestoreSettings.cacheSizeBytes) + } + } + callbackExecutorMap[android] = settings.callbackExecutor + } actual suspend fun disableNetwork() = android.disableNetwork().await().run { } @@ -269,18 +320,26 @@ actual class DocumentReference actual constructor(internal actual val nativeValu actual val snapshots: Flow 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(DocumentSnapshot(snapshot)) } - exception?.let { close(exception) } - } - awaitClose { listener.remove() } + actual fun snapshots(includeMetadataChanges: Boolean) = addSnapshotListener(includeMetadataChanges) { snapshot, exception -> + snapshot?.let { trySend(DocumentSnapshot(snapshot)) } + exception?.let { close(exception) } } override fun equals(other: Any?): Boolean = this === other || other is DocumentReference && nativeValue == other.nativeValue override fun hashCode(): Int = nativeValue.hashCode() override fun toString(): String = nativeValue.toString() + + private fun addSnapshotListener( + includeMetadataChanges: Boolean = false, + listener: ProducerScope.(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() } + } } actual open class Query(open val android: com.google.firebase.firestore.Query) { @@ -289,21 +348,14 @@ actual open class Query(open val android: com.google.firebase.firestore.Query) { actual fun limit(limit: Number) = Query(android.limit(limit.toLong())) - actual val snapshots get() = callbackFlow { - 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 { - 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(field: String, equalTo: Any?) = Query(android.whereEqualTo(field, equalTo)) @@ -352,6 +404,18 @@ actual open class Query(open val android: com.google.firebase.firestore.Query) { internal actual fun _endBefore(vararg fieldValues: Any) = Query(android.endBefore(*fieldValues)) internal actual fun _endAt(document: DocumentSnapshot) = Query(android.endAt(document.android)) internal actual fun _endAt(vararg fieldValues: Any) = Query(android.endAt(*fieldValues)) + + private fun addSnapshotListener( + includeMetadataChanges: Boolean = false, + listener: ProducerScope.(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 diff --git a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt index 8dc81fedc..5398029ef 100644 --- a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt +++ b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt @@ -18,7 +18,29 @@ expect val Firebase.firestore: FirebaseFirestore /** Returns the [FirebaseFirestore] instance of a given [FirebaseApp]. */ expect fun Firebase.firestore(app: FirebaseApp): FirebaseFirestore +sealed class LocalCacheSettings { + data class Persistent(val sizeBytes: Long? = null) : LocalCacheSettings() + data class Memory(val garbaseCollectorSettings: GarbageCollectorSettings) : LocalCacheSettings() { + sealed class GarbageCollectorSettings { + data object Eager : GarbageCollectorSettings() + data class LRUGC(val sizeBytes: Long? = null) : GarbageCollectorSettings() + } + } +} + expect class FirebaseFirestore { + + class Settings { + + companion object { + fun create(sslEnabled: Boolean? = null, host: String? = null, cacheSettings: LocalCacheSettings? = null): Settings + } + + val sslEnabled: Boolean? + val host: String? + val cacheSettings: LocalCacheSettings? + } + fun collection(collectionPath: String): CollectionReference fun collectionGroup(collectionId: String): Query fun document(documentPath: String): DocumentReference @@ -27,11 +49,18 @@ expect class FirebaseFirestore { suspend fun clearPersistence() suspend fun runTransaction(func: suspend Transaction.() -> T): T fun useEmulator(host: String, port: Int) - fun setSettings(persistenceEnabled: Boolean? = null, sslEnabled: Boolean? = null, host: String? = null, cacheSizeBytes: Long? = null) + fun setSettings(settings: Settings) + fun updateSettings(settings: Settings) suspend fun disableNetwork() suspend fun enableNetwork() } +fun FirebaseFirestore.setSettings( + sslEnabled: Boolean? = null, + host: String? = null, + cacheSettings: LocalCacheSettings? = null +) = FirebaseFirestore.Settings.create(sslEnabled, host, cacheSettings) + expect class Transaction { fun set(documentRef: DocumentReference, data: Any, encodeDefaults: Boolean = true, merge: Boolean = false): Transaction diff --git a/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt index 8b2956682..ada32568d 100644 --- a/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt +++ b/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt @@ -69,7 +69,7 @@ class FirebaseFirestoreTest { firestore = Firebase.firestore(app).apply { useEmulator(emulatorHost, 8080) - setSettings(persistenceEnabled = false) + setSettings(FirebaseFirestore.Settings.create(cacheSettings = LocalCacheSettings.Memory(LocalCacheSettings.Memory.GarbageCollectorSettings.Eager))) } } diff --git a/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt index e7baf2583..12fa0936f 100644 --- a/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt +++ b/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt @@ -17,6 +17,9 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.SerializationStrategy import platform.Foundation.NSError import platform.Foundation.NSNull +import platform.Foundation.NSNumber +import platform.Foundation.numberWithLong +import platform.darwin.dispatch_queue_t actual val Firebase.firestore get() = FirebaseFirestore(FIRFirestore.firestore()) @@ -25,9 +28,30 @@ actual fun Firebase.firestore(app: FirebaseApp): FirebaseFirestore = FirebaseFir FIRFirestore.firestoreForApp(app.ios as objcnames.classes.FIRApp) ) +val LocalCacheSettings.ios: FIRLocalCacheSettingsProtocol get() = when (this) { + is LocalCacheSettings.Persistent -> sizeBytes?.let { FIRPersistentCacheSettings(NSNumber.numberWithLong(it)) } ?: FIRPersistentCacheSettings() + is LocalCacheSettings.Memory -> FIRMemoryCacheSettings( + when (garbaseCollectorSettings) { + is LocalCacheSettings.Memory.GarbageCollectorSettings.Eager -> FIRMemoryEagerGCSettings() + is LocalCacheSettings.Memory.GarbageCollectorSettings.LRUGC -> garbaseCollectorSettings.sizeBytes?.let { FIRMemoryLRUGCSettings(NSNumber.numberWithLong(it)) } ?: FIRMemoryLRUGCSettings() + } + ) +} + @Suppress("UNCHECKED_CAST") actual class FirebaseFirestore(val ios: FIRFirestore) { + actual data class Settings( + actual val sslEnabled: Boolean? = null, + actual val host: String? = null, + actual val cacheSettings: LocalCacheSettings? = null, + val dispatchQueue: dispatch_queue_t = null + ) { + actual companion object { + actual fun create(sslEnabled: Boolean?, host: String?, cacheSettings: LocalCacheSettings?) = Settings(sslEnabled, host, cacheSettings) + } + } + actual fun collection(collectionPath: String) = CollectionReference(ios.collectionWithPath(collectionPath)) actual fun collectionGroup(collectionId: String) = Query(ios.collectionGroupWithID(collectionId)) @@ -46,20 +70,26 @@ actual class FirebaseFirestore(val ios: FIRFirestore) { await { ios.clearPersistenceWithCompletion(it) } actual fun useEmulator(host: String, port: Int) { + ios.useEmulatorWithHost(host, port.toLong()) ios.settings = ios.settings.apply { - this.host = "$host:$port" - persistenceEnabled = false + cacheSettings = FIRMemoryCacheSettings() sslEnabled = false } } - actual fun setSettings(persistenceEnabled: Boolean?, sslEnabled: Boolean?, host: String?, cacheSizeBytes: Long?) { - ios.settings = FIRFirestoreSettings().also { settings -> - persistenceEnabled?.let { settings.persistenceEnabled = it } - sslEnabled?.let { settings.sslEnabled = it } - host?.let { settings.host = it } - cacheSizeBytes?.let { settings.cacheSizeBytes = it } - } + actual fun setSettings(settings: Settings) { + ios.settings = FIRFirestoreSettings().applySettings(settings) + } + + actual fun updateSettings(settings: Settings) { + ios.settings = ios.settings.applySettings(settings) + } + + private fun FIRFirestoreSettings.applySettings(settings: Settings): FIRFirestoreSettings = apply { + settings.cacheSettings?.let { cacheSettings = it.ios } + settings.sslEnabled?.let { sslEnabled = it } + settings.host?.let { host = it } + settings.dispatchQueue?.let { dispatchQueue = it } } actual suspend fun disableNetwork() { diff --git a/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt index 46833d684..b419a5ab5 100644 --- a/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt +++ b/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt @@ -61,6 +61,18 @@ private fun performUpdate( actual class FirebaseFirestore(jsFirestore: Firestore) { + actual data class Settings( + actual val sslEnabled: Boolean? = null, + actual val host: String? = null, + actual val cacheSettings: LocalCacheSettings? = null + ) { + actual companion object { + actual fun create(sslEnabled: Boolean?, host: String?, cacheSettings: LocalCacheSettings?) = Settings(sslEnabled, host, cacheSettings) + } + } + + private var lastSettings = Settings() + var js: Firestore = jsFirestore private set @@ -83,17 +95,29 @@ actual class FirebaseFirestore(jsFirestore: Firestore) { actual fun useEmulator(host: String, port: Int) = rethrow { connectFirestoreEmulator(js, host, port) } - actual fun setSettings(persistenceEnabled: Boolean?, sslEnabled: Boolean?, host: String?, cacheSizeBytes: Long?) { - if(persistenceEnabled == true) enableIndexedDbPersistence(js) - - val settings = json().apply { - sslEnabled?.let { set("ssl", it) } - host?.let { set("host", it) } - cacheSizeBytes?.let { set("cacheSizeBytes", it) } + actual fun setSettings(settings: Settings) { + lastSettings = settings + if(settings.cacheSettings is LocalCacheSettings.Persistent) enableIndexedDbPersistence(js) + + val jsSettings = json().apply { + settings.sslEnabled?.let { set("ssl", it) } + settings.host?.let { set("host", it) } + when (val cacheSettings = settings.cacheSettings) { + is LocalCacheSettings.Persistent -> cacheSettings.sizeBytes + is LocalCacheSettings.Memory -> when (val garbageCollectorSettings = cacheSettings.garbaseCollectorSettings) { + is LocalCacheSettings.Memory.GarbageCollectorSettings.Eager -> null + is LocalCacheSettings.Memory.GarbageCollectorSettings.LRUGC -> garbageCollectorSettings.sizeBytes + } + null -> null + }?.let { set("cacheSizeBytes", it) } } - js = initializeFirestore(js.app, settings) + js = initializeFirestore(js.app, jsSettings) } + actual fun updateSettings(settings: Settings) = setSettings( + Settings(settings.sslEnabled ?: lastSettings.sslEnabled, settings.host ?: lastSettings.host, settings.cacheSettings ?: lastSettings.cacheSettings) + ) + actual suspend fun disableNetwork() { rethrow { disableNetwork(js).await() } } diff --git a/firebase-firestore/src/jvmMain/kotlin/dev/gitlive/firebase/firestore/Geopoint.kt b/firebase-firestore/src/jvmMain/kotlin/dev/gitlive/firebase/firestore/Geopoint.kt new file mode 100644 index 000000000..7523619f5 --- /dev/null +++ b/firebase-firestore/src/jvmMain/kotlin/dev/gitlive/firebase/firestore/Geopoint.kt @@ -0,0 +1,19 @@ +package dev.gitlive.firebase.firestore + +import kotlinx.serialization.Serializable + +/** A class representing a platform specific Firebase GeoPoint. */ +actual typealias NativeGeoPoint = com.google.firebase.firestore.GeoPoint + +/** A class representing a Firebase GeoPoint. */ +@Serializable(with = GeoPointSerializer::class) +actual class GeoPoint internal actual constructor(internal actual val nativeValue: NativeGeoPoint) { + actual constructor(latitude: Double, longitude: Double) : this(NativeGeoPoint(latitude, longitude)) + actual val latitude: Double = nativeValue.latitude + actual val longitude: Double = nativeValue.longitude + + override fun equals(other: Any?): Boolean = + this === other || other is GeoPoint && nativeValue == other.nativeValue + override fun hashCode(): Int = nativeValue.hashCode() + override fun toString(): String = nativeValue.toString() +} diff --git a/firebase-firestore/src/jvmMain/kotlin/dev/gitlive/firebase/firestore/Timestamp.kt b/firebase-firestore/src/jvmMain/kotlin/dev/gitlive/firebase/firestore/Timestamp.kt new file mode 100644 index 000000000..cc9a2ddb9 --- /dev/null +++ b/firebase-firestore/src/jvmMain/kotlin/dev/gitlive/firebase/firestore/Timestamp.kt @@ -0,0 +1,35 @@ +@file:JvmName("androidTimestamp") +package dev.gitlive.firebase.firestore + +import kotlinx.serialization.Serializable + +/** A class representing a platform specific Firebase Timestamp. */ +actual typealias NativeTimestamp = com.google.firebase.Timestamp + +/** A base class that could be used to combine [Timestamp] and [Timestamp.ServerTimestamp] in the same field. */ +@Serializable(with = BaseTimestampSerializer::class) +actual sealed class BaseTimestamp + +/** A class representing a Firebase Timestamp. */ +@Serializable(with = TimestampSerializer::class) +actual class Timestamp internal actual constructor( + internal actual val nativeValue: NativeTimestamp +): BaseTimestamp() { + actual constructor(seconds: Long, nanoseconds: Int) : this(NativeTimestamp(seconds, nanoseconds)) + + actual val seconds: Long = nativeValue.seconds + actual val nanoseconds: Int = nativeValue.nanoseconds + + override fun equals(other: Any?): Boolean = + this === other || other is Timestamp && nativeValue == other.nativeValue + override fun hashCode(): Int = nativeValue.hashCode() + override fun toString(): String = nativeValue.toString() + + actual companion object { + actual fun now(): Timestamp = Timestamp(NativeTimestamp.now()) + } + + /** A server time timestamp. */ + @Serializable(with = ServerTimestampSerializer::class) + actual object ServerTimestamp: BaseTimestamp() +} diff --git a/firebase-firestore/src/jvmMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/jvmMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt new file mode 100644 index 000000000..7ad3937bc --- /dev/null +++ b/firebase-firestore/src/jvmMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt @@ -0,0 +1,521 @@ +/* + * Copyright (c) 2020 GitLive Ltd. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:JvmName("JVM") +package dev.gitlive.firebase.firestore + +import com.google.android.gms.tasks.Task +import com.google.android.gms.tasks.TaskExecutors +import com.google.firebase.firestore.* +import dev.gitlive.firebase.* +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.DeserializationStrategy +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationStrategy +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.Executor + +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)) + +/** Helper method to perform an update operation. */ +@JvmName("performUpdateFields") +private fun performUpdate( + fieldsAndValues: Array>, + update: (String, Any?, Array) -> R +) = performUpdate(fieldsAndValues, { it }, { encode(it, true) }, update) + +/** Helper method to perform an update operation. */ +@JvmName("performUpdateFieldPaths") +private fun performUpdate( + fieldsAndValues: Array>, + update: (com.google.firebase.firestore.FieldPath, Any?, Array) -> R +) = performUpdate(fieldsAndValues, { it.android }, { encode(it, true) }, update) + +// Since on iOS Callback threads are set as settings, we store the settings explicitly here as well +private val callbackExecutorMap = ConcurrentHashMap() + +actual data class FirebaseFirestore(val android: com.google.firebase.firestore.FirebaseFirestore) { + + actual data class Settings( + actual val sslEnabled: Boolean? = null, + actual val host: String? = null, + actual val cacheSettings: LocalCacheSettings? = null, + val callbackExecutor: Executor = TaskExecutors.MAIN_THREAD, + ) { + actual companion object { + actual fun create(sslEnabled: Boolean?, host: String?, cacheSettings: LocalCacheSettings?) = Settings(sslEnabled, host, cacheSettings) + } + } + + private var lastSettings = Settings() + + actual fun collection(collectionPath: String) = CollectionReference(android.collection(collectionPath)) + + actual fun document(documentPath: String) = DocumentReference(android.document(documentPath)) + + actual fun collectionGroup(collectionId: String) = Query(android.collectionGroup(collectionId)) + + actual fun batch() = WriteBatch(android.batch()) + + actual fun setLoggingEnabled(loggingEnabled: Boolean) = + com.google.firebase.firestore.FirebaseFirestore.setLoggingEnabled(loggingEnabled) + + actual suspend fun runTransaction(func: suspend Transaction.() -> T): T = + android.runTransaction { runBlocking { Transaction(it).func() } }.await() + + actual suspend fun clearPersistence() = + android.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(settings: Settings) { + lastSettings = settings + android.firestoreSettings = com.google.firebase.firestore.FirebaseFirestoreSettings.Builder().also { builder -> + if (settings.cacheSettings is LocalCacheSettings.Persistent) { + builder.isPersistenceEnabled = true + } + settings.sslEnabled?.let { builder.isSslEnabled = it } + settings.host?.let { builder.host = it } + when (val cacheSettings = settings.cacheSettings) { + is LocalCacheSettings.Persistent -> cacheSettings.sizeBytes + is LocalCacheSettings.Memory -> when (val garbageCollectorSettings = cacheSettings.garbaseCollectorSettings) { + is LocalCacheSettings.Memory.GarbageCollectorSettings.Eager -> null + is LocalCacheSettings.Memory.GarbageCollectorSettings.LRUGC -> garbageCollectorSettings.sizeBytes + } + null -> null + }?.let { builder.cacheSizeBytes = it } + }.build() + callbackExecutorMap[android] = settings.callbackExecutor + } + + actual fun updateSettings(settings: Settings) = setSettings( + Settings(settings.sslEnabled ?: lastSettings.sslEnabled, settings.host ?: lastSettings.host, settings.cacheSettings ?: lastSettings.cacheSettings) + ) + + actual suspend fun disableNetwork() = + android.disableNetwork().await().run { } + + actual suspend fun enableNetwork() = + android.enableNetwork().await().run { } + +} + +actual class WriteBatch(val android: com.google.firebase.firestore.WriteBatch) { + + actual inline fun set(documentRef: DocumentReference, data: T, encodeDefaults: Boolean, merge: Boolean) = when(merge) { + true -> android.set(documentRef.android, encode(data, encodeDefaults)!!, SetOptions.merge()) + false -> android.set(documentRef.android, encode(data, encodeDefaults)!!) + }.let { this } + + actual inline fun set(documentRef: DocumentReference, data: T, encodeDefaults: Boolean, vararg mergeFields: String) = + android.set(documentRef.android, encode(data, encodeDefaults)!!, SetOptions.mergeFields(*mergeFields)) + .let { this } + + actual inline fun set(documentRef: DocumentReference, data: T, encodeDefaults: Boolean, vararg mergeFieldPaths: FieldPath) = + android.set(documentRef.android, encode(data, encodeDefaults)!!, SetOptions.mergeFieldPaths(mergeFieldPaths.map { it.android })) + .let { this } + + actual fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, merge: Boolean) = when(merge) { + true -> android.set(documentRef.android, encode(strategy, data, encodeDefaults)!!, SetOptions.merge()) + false -> android.set(documentRef.android, encode(strategy, data, encodeDefaults)!!) + }.let { this } + + actual fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, vararg mergeFields: String) = + android.set(documentRef.android, encode(strategy, data, encodeDefaults)!!, SetOptions.mergeFields(*mergeFields)) + .let { this } + + actual fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, vararg mergeFieldPaths: FieldPath) = + android.set(documentRef.android, encode(strategy, data, encodeDefaults)!!, SetOptions.mergeFieldPaths(mergeFieldPaths.map { it.android })) + .let { this } + + @Suppress("UNCHECKED_CAST") + actual inline fun update(documentRef: DocumentReference, data: T, encodeDefaults: Boolean) = + android.update(documentRef.android, encode(data, encodeDefaults) as Map).let { this } + + @Suppress("UNCHECKED_CAST") + actual fun update(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean) = + android.update(documentRef.android, encode(strategy, data, encodeDefaults) as Map).let { this } + + @JvmName("updateFields") + actual fun update(documentRef: DocumentReference, vararg fieldsAndValues: Pair) = + performUpdate(fieldsAndValues) { field, value, moreFieldsAndValues -> + android.update(documentRef.android, field, value, *moreFieldsAndValues) + }.let { this } + + @JvmName("updateFieldPaths") + actual fun update(documentRef: DocumentReference, vararg fieldsAndValues: Pair) = + performUpdate(fieldsAndValues) { field, value, moreFieldsAndValues -> + android.update(documentRef.android, field, value, *moreFieldsAndValues) + }.let { this } + + actual fun delete(documentRef: DocumentReference) = + android.delete(documentRef.android).let { this } + + actual suspend fun commit() = android.commit().await().run { Unit } + +} + +actual class Transaction(val android: com.google.firebase.firestore.Transaction) { + + actual fun set(documentRef: DocumentReference, data: Any, encodeDefaults: Boolean, merge: Boolean) = when(merge) { + true -> android.set(documentRef.android, encode(data, encodeDefaults)!!, SetOptions.merge()) + false -> android.set(documentRef.android, encode(data, encodeDefaults)!!) + }.let { this } + + actual fun set(documentRef: DocumentReference, data: Any, encodeDefaults: Boolean, vararg mergeFields: String) = + android.set(documentRef.android, encode(data, encodeDefaults)!!, SetOptions.mergeFields(*mergeFields)) + .let { this } + + actual fun set(documentRef: DocumentReference, data: Any, encodeDefaults: Boolean, vararg mergeFieldPaths: FieldPath) = + android.set(documentRef.android, encode(data, encodeDefaults)!!, SetOptions.mergeFieldPaths(mergeFieldPaths.map { it.android })) + .let { this } + + actual fun set( + documentRef: DocumentReference, + strategy: SerializationStrategy, + data: T, + encodeDefaults: Boolean, + merge: Boolean + ) = when(merge) { + true -> android.set(documentRef.android, encode(strategy, data, encodeDefaults)!!, SetOptions.merge()) + false -> android.set(documentRef.android, encode(strategy, data, encodeDefaults)!!) + }.let { this } + + actual fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, vararg mergeFields: String) = + android.set(documentRef.android, encode(strategy, data, encodeDefaults)!!, SetOptions.mergeFields(*mergeFields)) + .let { this } + + actual fun set(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, vararg mergeFieldPaths: FieldPath) = + android.set(documentRef.android, encode(strategy, data, encodeDefaults)!!, SetOptions.mergeFieldPaths(mergeFieldPaths.map { it.android })) + .let { this } + + @Suppress("UNCHECKED_CAST") + actual fun update(documentRef: DocumentReference, data: Any, encodeDefaults: Boolean) = + android.update(documentRef.android, encode(data, encodeDefaults) as Map).let { this } + + @Suppress("UNCHECKED_CAST") + actual fun update(documentRef: DocumentReference, strategy: SerializationStrategy, data: T, encodeDefaults: Boolean) = + android.update(documentRef.android, encode(strategy, data, encodeDefaults) as Map).let { this } + + @JvmName("updateFields") + actual fun update(documentRef: DocumentReference, vararg fieldsAndValues: Pair) = + performUpdate(fieldsAndValues) { field, value, moreFieldsAndValues -> + android.update(documentRef.android, field, value, *moreFieldsAndValues) + }.let { this } + + @JvmName("updateFieldPaths") + actual fun update(documentRef: DocumentReference, vararg fieldsAndValues: Pair) = + performUpdate(fieldsAndValues) { field, value, moreFieldsAndValues -> + android.update(documentRef.android, field, value, *moreFieldsAndValues) + }.let { this } + + actual fun delete(documentRef: DocumentReference) = + android.delete(documentRef.android).let { this } + + actual suspend fun get(documentRef: DocumentReference) = + DocumentSnapshot(android.get(documentRef.android)) +} + +/** A class representing a platform specific Firebase DocumentReference. */ +actual typealias NativeDocumentReference = com.google.firebase.firestore.DocumentReference + +@Serializable(with = DocumentReferenceSerializer::class) +actual class DocumentReference actual constructor(internal actual val nativeValue: NativeDocumentReference) { + val android: NativeDocumentReference by ::nativeValue + actual val id: String + get() = android.id + + actual val path: String + get() = android.path + + actual val parent: CollectionReference + get() = CollectionReference(android.parent) + + actual fun collection(collectionPath: String) = CollectionReference(android.collection(collectionPath)) + + actual suspend inline fun set(data: T, encodeDefaults: Boolean, merge: Boolean) = when(merge) { + true -> android.set(encode(data, encodeDefaults)!!, SetOptions.merge()) + false -> android.set(encode(data, encodeDefaults)!!) + }.await().run { Unit } + + actual suspend inline fun set(data: T, encodeDefaults: Boolean, vararg mergeFields: String) = + android.set(encode(data, encodeDefaults)!!, SetOptions.mergeFields(*mergeFields)) + .await().run { Unit } + + actual suspend inline fun set(data: T, encodeDefaults: Boolean, vararg mergeFieldPaths: FieldPath) = + android.set(encode(data, encodeDefaults)!!, SetOptions.mergeFieldPaths(mergeFieldPaths.map { it.android })) + .await().run { Unit } + + actual suspend fun set(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, merge: Boolean) = when(merge) { + true -> android.set(encode(strategy, data, encodeDefaults)!!, SetOptions.merge()) + false -> android.set(encode(strategy, data, encodeDefaults)!!) + }.await().run { Unit } + + actual suspend fun set(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, vararg mergeFields: String) = + android.set(encode(strategy, data, encodeDefaults)!!, SetOptions.mergeFields(*mergeFields)) + .await().run { Unit } + + actual suspend fun set(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean, vararg mergeFieldPaths: FieldPath) = + android.set(encode(strategy, data, encodeDefaults)!!, SetOptions.mergeFieldPaths(mergeFieldPaths.map { it.android })) + .await().run { Unit } + + @Suppress("UNCHECKED_CAST") + actual suspend inline fun update(data: T, encodeDefaults: Boolean) = + android.update(encode(data, encodeDefaults) as Map).await().run { Unit } + + @Suppress("UNCHECKED_CAST") + actual suspend fun update(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean) = + android.update(encode(strategy, data, encodeDefaults) as Map).await().run { Unit } + + @JvmName("updateFields") + actual suspend fun update(vararg fieldsAndValues: Pair) = + performUpdate(fieldsAndValues) { field, value, moreFieldsAndValues -> + android.update(field, value, *moreFieldsAndValues) + }?.await() + .run { Unit } + + @JvmName("updateFieldPaths") + actual suspend fun update(vararg fieldsAndValues: Pair) = + performUpdate(fieldsAndValues) { field, value, moreFieldsAndValues -> + android.update(field, value, *moreFieldsAndValues) + }?.await() + .run { Unit } + + actual suspend fun delete() = + android.delete().await().run { Unit } + + actual suspend fun get() = + DocumentSnapshot(android.get().await()) + + actual val snapshots: Flow get() = snapshots() + + actual fun snapshots(includeMetadataChanges: Boolean) = addSnapshotListener(includeMetadataChanges) { snapshot, exception -> + snapshot?.let { trySend(DocumentSnapshot(snapshot)) } + exception?.let { close(exception) } + } + override fun equals(other: Any?): Boolean = + this === other || other is DocumentReference && nativeValue == other.nativeValue + override fun hashCode(): Int = nativeValue.hashCode() + override fun toString(): String = nativeValue.toString() + + private fun addSnapshotListener( + includeMetadataChanges: Boolean = false, + listener: ProducerScope.(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() } + } +} + +actual open class Query(open val android: com.google.firebase.firestore.Query) { + + actual suspend fun get() = QuerySnapshot(android.get().await()) + + actual fun limit(limit: Number) = Query(android.limit(limit.toLong())) + + actual val snapshots get() = addSnapshotListener { snapshot, exception -> + snapshot?.let { trySend(QuerySnapshot(snapshot)) } + exception?.let { close(exception) } + } + + actual fun snapshots(includeMetadataChanges: Boolean) = addSnapshotListener(includeMetadataChanges) { snapshot, exception -> + snapshot?.let { trySend(QuerySnapshot(snapshot)) } + exception?.let { close(exception) } + } + + internal actual fun _where(field: String, equalTo: Any?) = Query(android.whereEqualTo(field, equalTo)) + internal actual fun _where(path: FieldPath, equalTo: Any?) = Query(android.whereEqualTo(path.android, equalTo)) + + internal actual fun _where(field: String, equalTo: DocumentReference) = Query(android.whereEqualTo(field, equalTo.android)) + internal actual fun _where(path: FieldPath, equalTo: DocumentReference) = Query(android.whereEqualTo(path.android, equalTo.android)) + + internal actual fun _where(field: String, lessThan: Any?, greaterThan: Any?, arrayContains: Any?) = Query( + (lessThan?.let { android.whereLessThan(field, it) } ?: android).let { android2 -> + (greaterThan?.let { android2.whereGreaterThan(field, it) } ?: android2).let { android3 -> + arrayContains?.let { android3.whereArrayContains(field, it) } ?: android3 + } + } + ) + + internal actual fun _where(path: FieldPath, lessThan: Any?, greaterThan: Any?, arrayContains: Any?) = Query( + (lessThan?.let { android.whereLessThan(path.android, it) } ?: android).let { android2 -> + (greaterThan?.let { android2.whereGreaterThan(path.android, it) } ?: android2).let { android3 -> + arrayContains?.let { android3.whereArrayContains(path.android, it) } ?: android3 + } + } + ) + + internal actual fun _where(field: String, inArray: List?, arrayContainsAny: List?) = Query( + (inArray?.let { android.whereIn(field, it) } ?: android).let { android2 -> + arrayContainsAny?.let { android2.whereArrayContainsAny(field, it) } ?: android2 + } + ) + + internal actual fun _where(path: FieldPath, inArray: List?, arrayContainsAny: List?) = Query( + (inArray?.let { android.whereIn(path.android, it) } ?: android).let { android2 -> + arrayContainsAny?.let { android2.whereArrayContainsAny(path.android, it) } ?: android2 + } + ) + + internal actual fun _orderBy(field: String, direction: Direction) = Query(android.orderBy(field, direction)) + internal actual fun _orderBy(field: FieldPath, direction: Direction) = Query(android.orderBy(field.android, direction)) + + internal actual fun _startAfter(document: DocumentSnapshot) = Query(android.startAfter(document.android)) + internal actual fun _startAfter(vararg fieldValues: Any) = Query(android.startAfter(*fieldValues)) + internal actual fun _startAt(document: DocumentSnapshot) = Query(android.startAt(document.android)) + internal actual fun _startAt(vararg fieldValues: Any) = Query(android.startAt(*fieldValues)) + + internal actual fun _endBefore(document: DocumentSnapshot) = Query(android.endBefore(document.android)) + internal actual fun _endBefore(vararg fieldValues: Any) = Query(android.endBefore(*fieldValues)) + internal actual fun _endAt(document: DocumentSnapshot) = Query(android.endAt(document.android)) + internal actual fun _endAt(vararg fieldValues: Any) = Query(android.endAt(*fieldValues)) + + private fun addSnapshotListener( + includeMetadataChanges: Boolean = false, + listener: ProducerScope.(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 +actual typealias ChangeType = com.google.firebase.firestore.DocumentChange.Type + +actual class CollectionReference(override val android: com.google.firebase.firestore.CollectionReference) : Query(android) { + + actual val path: String + get() = android.path + + actual val document: DocumentReference + get() = DocumentReference(android.document()) + + actual val parent: DocumentReference? + get() = android.parent?.let{DocumentReference(it)} + + actual fun document(documentPath: String) = DocumentReference(android.document(documentPath)) + + actual suspend inline fun add(data: T, encodeDefaults: Boolean) = + DocumentReference(android.add(encode(data, encodeDefaults)!!).await()) + + actual suspend fun add(data: T, strategy: SerializationStrategy, encodeDefaults: Boolean) = + DocumentReference(android.add(encode(strategy, data, encodeDefaults)!!).await()) + actual suspend fun add(strategy: SerializationStrategy, data: T, encodeDefaults: Boolean) = + DocumentReference(android.add(encode(strategy, data, encodeDefaults)!!).await()) +} + +actual typealias FirebaseFirestoreException = com.google.firebase.firestore.FirebaseFirestoreException + +actual val FirebaseFirestoreException.code: FirestoreExceptionCode get() = code + +actual typealias FirestoreExceptionCode = com.google.firebase.firestore.FirebaseFirestoreException.Code + +actual class QuerySnapshot(val android: com.google.firebase.firestore.QuerySnapshot) { + actual val documents + get() = android.documents.map { DocumentSnapshot(it) } + actual val documentChanges + get() = android.documentChanges.map { DocumentChange(it) } + actual val metadata: SnapshotMetadata get() = SnapshotMetadata(android.metadata) +} + +actual class DocumentChange(val android: com.google.firebase.firestore.DocumentChange) { + actual val document: DocumentSnapshot + get() = DocumentSnapshot(android.document) + actual val newIndex: Int + get() = android.newIndex + actual val oldIndex: Int + get() = android.oldIndex + actual val type: ChangeType + get() = android.type +} + +@Suppress("UNCHECKED_CAST") +actual class DocumentSnapshot(val android: com.google.firebase.firestore.DocumentSnapshot) { + + actual val id get() = android.id + actual val reference get() = DocumentReference(android.reference) + + actual inline fun data(serverTimestampBehavior: ServerTimestampBehavior): T = + decode(value = android.getData(serverTimestampBehavior.toAndroid())) + + actual fun data(strategy: DeserializationStrategy, serverTimestampBehavior: ServerTimestampBehavior): T = + decode(strategy, android.getData(serverTimestampBehavior.toAndroid())) + + actual inline fun get(field: String, serverTimestampBehavior: ServerTimestampBehavior): T = + decode(value = android.get(field, serverTimestampBehavior.toAndroid())) + + actual fun get(field: String, strategy: DeserializationStrategy, serverTimestampBehavior: ServerTimestampBehavior): T = + decode(strategy, android.get(field, serverTimestampBehavior.toAndroid())) + + actual fun contains(field: String) = android.contains(field) + + actual val exists get() = android.exists() + + actual val metadata: SnapshotMetadata get() = SnapshotMetadata(android.metadata) + + fun ServerTimestampBehavior.toAndroid(): com.google.firebase.firestore.DocumentSnapshot.ServerTimestampBehavior = when (this) { + ServerTimestampBehavior.ESTIMATE -> com.google.firebase.firestore.DocumentSnapshot.ServerTimestampBehavior.ESTIMATE + ServerTimestampBehavior.NONE -> com.google.firebase.firestore.DocumentSnapshot.ServerTimestampBehavior.NONE + ServerTimestampBehavior.PREVIOUS -> com.google.firebase.firestore.DocumentSnapshot.ServerTimestampBehavior.PREVIOUS + } +} + +actual class SnapshotMetadata(val android: com.google.firebase.firestore.SnapshotMetadata) { + actual val hasPendingWrites: Boolean get() = android.hasPendingWrites() + actual val isFromCache: Boolean get() = android.isFromCache() +} + +actual class FieldPath private constructor(val android: com.google.firebase.firestore.FieldPath) { + actual constructor(vararg fieldNames: String) : this(com.google.firebase.firestore.FieldPath.of(*fieldNames)) + actual val documentId: FieldPath get() = FieldPath(com.google.firebase.firestore.FieldPath.documentId()) + + override fun equals(other: Any?): Boolean = other is FieldPath && android == other.android + override fun hashCode(): Int = android.hashCode() + override fun toString(): String = android.toString() +} + +/** Represents a platform specific Firebase FieldValue. */ +private typealias NativeFieldValue = com.google.firebase.firestore.FieldValue + +/** Represents a Firebase FieldValue. */ +@Serializable(with = FieldValueSerializer::class) +actual class FieldValue internal actual constructor(internal actual val nativeValue: Any) { + init { + require(nativeValue is NativeFieldValue) + } + override fun equals(other: Any?): Boolean = + this === other || other is FieldValue && nativeValue == other.nativeValue + override fun hashCode(): Int = nativeValue.hashCode() + override fun toString(): String = nativeValue.toString() + + actual companion object { + actual val serverTimestamp: FieldValue get() = FieldValue(NativeFieldValue.serverTimestamp()) + actual val delete: FieldValue get() = FieldValue(NativeFieldValue.delete()) + actual fun increment(value: Int): FieldValue = FieldValue(NativeFieldValue.increment(value.toDouble())) + actual fun arrayUnion(vararg elements: Any): FieldValue = FieldValue(NativeFieldValue.arrayUnion(*elements)) + actual fun arrayRemove(vararg elements: Any): FieldValue = FieldValue(NativeFieldValue.arrayRemove(*elements)) + } +} diff --git a/test-utils/src/commonMain/kotlin/dev/gitlive/firebase/TestUtils.kt b/test-utils/src/commonMain/kotlin/dev/gitlive/firebase/TestUtils.kt index bf9fcdc19..52ff28a51 100644 --- a/test-utils/src/commonMain/kotlin/dev/gitlive/firebase/TestUtils.kt +++ b/test-utils/src/commonMain/kotlin/dev/gitlive/firebase/TestUtils.kt @@ -1,4 +1,3 @@ -@file:JvmName("TestUtilsJVM") /* * Copyright (c) 2020 GitLive Ltd. Use of this source code is governed by the Apache 2.0 license. */ diff --git a/test-utils/src/jvmMain/kotlin/dev/gitlive/firebase/TestUtils.kt b/test-utils/src/jvmMain/kotlin/dev/gitlive/firebase/TestUtils.kt deleted file mode 100644 index 67528d98f..000000000 --- a/test-utils/src/jvmMain/kotlin/dev/gitlive/firebase/TestUtils.kt +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright (c) 2020 GitLive Ltd. Use of this source code is governed by the Apache 2.0 license. - */package dev.gitlive.firebase - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.runBlocking - -package dev.gitlive.firebase - -actual fun runTest(test: suspend CoroutineScope.() -> Unit) = kotlinx.coroutines.test.runTest { test() } -actual fun runBlockingTest(action: suspend CoroutineScope.() -> Unit) = runBlocking(block = action) - -actual fun nativeMapOf(vararg pairs: Pair): Any = mapOf(*pairs) -actual fun nativeListOf(vararg elements: Any): Any = listOf(*elements) -actual fun nativeAssertEquals(expected: Any?, actual: Any?) { - kotlin.test.assertEquals(expected, actual) -} From 93d88f4d2a2fc069734872387e7e2c9610e9f43c Mon Sep 17 00:00:00 2001 From: Gijs van Veen Date: Mon, 15 Apr 2024 12:48:16 +0200 Subject: [PATCH 2/8] Use API aligned with Android specifications --- .../dev/gitlive/firebase/database/database.kt | 21 +-- .../dev/gitlive/firebase/database/database.kt | 14 +- .../dev/gitlive/firebase/database/database.kt | 25 ++-- .../dev/gitlive/firebase/database/database.kt | 13 +- .../gitlive/firebase/firestore/firestore.kt | 121 +++++++++++------ .../firebase/firestore/LocalCacheSettings.kt | 90 +++++++++++++ .../gitlive/firebase/firestore/firestore.kt | 73 ++++++----- .../gitlive/firebase/firestore/firestore.kt | 8 +- .../gitlive/firebase/firestore/firestore.kt | 78 +++++++---- .../firebase/firestore/externals/firestore.kt | 25 +++- .../gitlive/firebase/firestore/firestore.kt | 123 ++++++++++++------ 11 files changed, 393 insertions(+), 198 deletions(-) create mode 100644 firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/LocalCacheSettings.kt diff --git a/firebase-database/src/androidMain/kotlin/dev/gitlive/firebase/database/database.kt b/firebase-database/src/androidMain/kotlin/dev/gitlive/firebase/database/database.kt index 8bcead222..3980e67f0 100644 --- a/firebase-database/src/androidMain/kotlin/dev/gitlive/firebase/database/database.kt +++ b/firebase-database/src/androidMain/kotlin/dev/gitlive/firebase/database/database.kt @@ -71,16 +71,6 @@ actual class FirebaseDatabase internal constructor(val android: com.google.fireb ) = instances.getOrPut(android) { dev.gitlive.firebase.database.FirebaseDatabase(android) } } - actual data class Settings( - actual val persistenceEnabled: Boolean = false, - actual val persistenceCacheSizeBytes: Long? = null, - ) { - - actual companion object { - actual fun createSettings(persistenceEnabled: Boolean, persistenceCacheSizeBytes: Long?) = Settings(persistenceEnabled, persistenceCacheSizeBytes) - } - } - private var persistenceEnabled = true actual fun reference(path: String) = @@ -89,10 +79,13 @@ actual class FirebaseDatabase internal constructor(val android: com.google.fireb actual fun reference() = DatabaseReference(NativeDatabaseReference(android.reference, persistenceEnabled)) - actual fun setSettings(settings: Settings) { - android.setPersistenceEnabled(settings.persistenceEnabled) - persistenceEnabled = settings.persistenceEnabled - settings.persistenceCacheSizeBytes?.let { android.setPersistenceCacheSizeBytes(it) } + actual fun setPersistenceEnabled(enabled: Boolean) { + android.setPersistenceEnabled(enabled) + persistenceEnabled = enabled + } + + actual fun setPersistenceCacheSizeBytes(cacheSizeInBytes: Long) { + android.setPersistenceCacheSizeBytes(cacheSizeInBytes) } actual fun setLoggingEnabled(enabled: Boolean) = diff --git a/firebase-database/src/commonMain/kotlin/dev/gitlive/firebase/database/database.kt b/firebase-database/src/commonMain/kotlin/dev/gitlive/firebase/database/database.kt index 572ec3a80..7bbd44935 100644 --- a/firebase-database/src/commonMain/kotlin/dev/gitlive/firebase/database/database.kt +++ b/firebase-database/src/commonMain/kotlin/dev/gitlive/firebase/database/database.kt @@ -33,24 +33,14 @@ expect fun Firebase.database(app: FirebaseApp, url: String): FirebaseDatabase expect class FirebaseDatabase { - class Settings { - val persistenceEnabled: Boolean - val persistenceCacheSizeBytes: Long? - - companion object { - fun createSettings(persistenceEnabled: Boolean = false, persistenceCacheSizeBytes: Long? = null): Settings - } - } - fun reference(path: String): DatabaseReference fun reference(): DatabaseReference - fun setSettings(settings: Settings) fun setLoggingEnabled(enabled: Boolean) + fun setPersistenceEnabled(enabled: Boolean) + fun setPersistenceCacheSizeBytes(cacheSizeInBytes: Long) fun useEmulator(host: String, port: Int) } -fun FirebaseDatabase.setPersistenceEnabled(enabled: Boolean) = setSettings(FirebaseDatabase.Settings.createSettings(persistenceEnabled = enabled)) - data class ChildEvent internal constructor( val snapshot: DataSnapshot, val type: Type, diff --git a/firebase-database/src/iosMain/kotlin/dev/gitlive/firebase/database/database.kt b/firebase-database/src/iosMain/kotlin/dev/gitlive/firebase/database/database.kt index 843a744dd..f59d83155 100644 --- a/firebase-database/src/iosMain/kotlin/dev/gitlive/firebase/database/database.kt +++ b/firebase-database/src/iosMain/kotlin/dev/gitlive/firebase/database/database.kt @@ -57,27 +57,22 @@ actual fun Firebase.database(app: FirebaseApp, url: String): FirebaseDatabase = actual class FirebaseDatabase internal constructor(val ios: FIRDatabase) { - actual data class Settings( - actual val persistenceEnabled: Boolean = false, - actual val persistenceCacheSizeBytes: Long? = null, - val callbackQueue: dispatch_queue_t = null - ) { - - actual companion object { - actual fun createSettings(persistenceEnabled: Boolean, persistenceCacheSizeBytes: Long?) = Settings(persistenceEnabled, persistenceCacheSizeBytes) - } - } - actual fun reference(path: String) = DatabaseReference(NativeDatabaseReference(ios.referenceWithPath(path), ios.persistenceEnabled)) actual fun reference() = DatabaseReference(NativeDatabaseReference(ios.reference(), ios.persistenceEnabled)) - actual fun setSettings(settings: Settings) { - ios.persistenceEnabled = settings.persistenceEnabled - settings.persistenceCacheSizeBytes?.let { ios.setPersistenceCacheSizeBytes(it.toULong()) } - settings.callbackQueue?.let { ios.callbackQueue = it } + actual fun setPersistenceEnabled(enabled: Boolean) { + ios.persistenceEnabled = enabled + } + + actual fun setPersistenceCacheSizeBytes(cacheSizeInBytes: Long) { + ios.setPersistenceCacheSizeBytes(cacheSizeInBytes.toULong()) + } + + fun setCallbackQueue(callbackQueue: dispatch_queue_t) { + ios.callbackQueue = callbackQueue } actual fun setLoggingEnabled(enabled: Boolean) = diff --git a/firebase-database/src/jsMain/kotlin/dev/gitlive/firebase/database/database.kt b/firebase-database/src/jsMain/kotlin/dev/gitlive/firebase/database/database.kt index 837908e52..dfb187d8e 100644 --- a/firebase-database/src/jsMain/kotlin/dev/gitlive/firebase/database/database.kt +++ b/firebase-database/src/jsMain/kotlin/dev/gitlive/firebase/database/database.kt @@ -68,19 +68,10 @@ actual fun Firebase.database(app: FirebaseApp, url: String) = actual class FirebaseDatabase internal constructor(val js: Database) { - actual data class Settings( - actual val persistenceEnabled: Boolean = false, - actual val persistenceCacheSizeBytes: Long? = null, - ) { - - actual companion object { - actual fun createSettings(persistenceEnabled: Boolean, persistenceCacheSizeBytes: Long?) = Settings(persistenceEnabled, persistenceCacheSizeBytes) - } - } - actual fun reference(path: String) = rethrow { DatabaseReference(NativeDatabaseReference(ref(js, path), js)) } actual fun reference() = rethrow { DatabaseReference(NativeDatabaseReference(ref(js), js)) } - actual fun setSettings(settings: Settings) {} + 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) } } diff --git a/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt index 6d7eecba3..17008ec75 100644 --- a/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt +++ b/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt @@ -6,6 +6,7 @@ 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 @@ -21,6 +22,7 @@ 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 @@ -35,16 +37,14 @@ actual fun Firebase.firestore(app: FirebaseApp) = val LocalCacheSettings.android: com.google.firebase.firestore.LocalCacheSettings get() = when (this) { is LocalCacheSettings.Persistent -> persistentCacheSettings { - sizeBytes?.let { setSizeBytes(it) } + setSizeBytes(sizeBytes) } is LocalCacheSettings.Memory -> memoryCacheSettings { setGcSettings( when (garbaseCollectorSettings) { - is LocalCacheSettings.Memory.GarbageCollectorSettings.Eager -> memoryEagerGcSettings { } - is LocalCacheSettings.Memory.GarbageCollectorSettings.LRUGC -> memoryLruGcSettings { - garbaseCollectorSettings.sizeBytes?.let { - setSizeBytes(it) - } + is GarbageCollectorSettings.Eager -> memoryEagerGcSettings { } + is GarbageCollectorSettings.LRUGC -> memoryLruGcSettings { + setSizeBytes(garbaseCollectorSettings.sizeBytes) } } ) @@ -56,16 +56,42 @@ private val callbackExecutorMap = ConcurrentHashMap + 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 { + 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))) @@ -89,31 +115,6 @@ actual class FirebaseFirestore(val android: com.google.firebase.firestore.Fireba android.firestoreSettings = firestoreSettings { } } - actual fun setSettings(settings: Settings) { - android.firestoreSettings = firestoreSettings { - settings.sslEnabled?.let { isSslEnabled = it } - settings.host?.let { host = it } - settings.cacheSettings?.let { setLocalCacheSettings(it.android) } - } - callbackExecutorMap[android] = settings.callbackExecutor - } - - @Suppress("DEPRECATION") - actual fun updateSettings(settings: Settings) { - android.firestoreSettings = firestoreSettings { - isSslEnabled = settings.sslEnabled ?: android.firestoreSettings.isSslEnabled - host = settings.host ?: android.firestoreSettings.host - val cacheSettings = settings.cacheSettings?.android ?: android.firestoreSettings.cacheSettings - cacheSettings?.let { - setLocalCacheSettings(it) - } ?: kotlin.run { - isPersistenceEnabled = android.firestoreSettings.isPersistenceEnabled - cacheSizeBytes = android.firestoreSettings.cacheSizeBytes - } - } - callbackExecutorMap[android] = settings.callbackExecutor - } - actual suspend fun disableNetwork() = android.disableNetwork().await().run { } @@ -122,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 diff --git a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/LocalCacheSettings.kt b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/LocalCacheSettings.kt new file mode 100644 index 000000000..1ce50e619 --- /dev/null +++ b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/LocalCacheSettings.kt @@ -0,0 +1,90 @@ +package dev.gitlive.firebase.firestore + +sealed class LocalCacheSettings { + + internal companion object { + // Firestore cache defaults to 100MB + const val DEFAULT_CACHE_SIZE = 100L*1024L*1024L + } + + data class Persistent(val sizeBytes: Long) : LocalCacheSettings() { + + companion object { + fun newBuilder(): Builder = BuilderImpl() + } + + interface Builder { + var sizeBytes: Long + fun build(): Persistent + } + + private class BuilderImpl( + override var sizeBytes: Long = DEFAULT_CACHE_SIZE + ) : Builder { + override fun build(): Persistent = Persistent(sizeBytes) + } + } + data class Memory(val garbaseCollectorSettings: GarbageCollectorSettings) : LocalCacheSettings() { + + companion object { + fun newBuilder(): Builder = BuilderImpl() + } + + interface Builder { + + var gcSettings: GarbageCollectorSettings + + fun build(): Memory + } + + private class BuilderImpl( + override var gcSettings: GarbageCollectorSettings = GarbageCollectorSettings.Eager + ) : Builder { + override fun build(): Memory = Memory(gcSettings) + } + } +} + +sealed class GarbageCollectorSettings { + data object Eager : GarbageCollectorSettings() { + + fun newBuilder(): Builder = BuilderImpl() + + interface Builder { + fun build(): Eager + } + + private class BuilderImpl : Builder { + override fun build(): Eager = Eager + } + } + data class LRUGC(val sizeBytes: Long) : GarbageCollectorSettings() { + + companion object { + fun newBuilder(): Builder = BuilderImpl() + } + + interface Builder { + var sizeBytes: Long + fun build(): LRUGC + } + + private class BuilderImpl( + override var sizeBytes: Long = LocalCacheSettings.DEFAULT_CACHE_SIZE + ) : Builder { + override fun build(): LRUGC = LRUGC(sizeBytes) + } + } +} + +fun memoryCacheSettings(builder: LocalCacheSettings.Memory.Builder.() -> Unit): LocalCacheSettings.Memory = + LocalCacheSettings.Memory.newBuilder().apply(builder).build() + +fun memoryEagerGcSettings(builder: GarbageCollectorSettings.Eager.Builder.() -> Unit) = + GarbageCollectorSettings.Eager.newBuilder().apply(builder).build() + +fun memoryLruGcSettings(builder: GarbageCollectorSettings.LRUGC.Builder.() -> Unit) = + GarbageCollectorSettings.LRUGC.newBuilder().apply(builder).build() + +fun persistentCacheSettings(builder: LocalCacheSettings.Persistent.Builder.() -> Unit) = + LocalCacheSettings.Persistent.newBuilder().apply(builder).build() \ No newline at end of file diff --git a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt index c962a0d21..c82370b94 100644 --- a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt +++ b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt @@ -18,28 +18,9 @@ expect val Firebase.firestore: FirebaseFirestore /** Returns the [FirebaseFirestore] instance of a given [FirebaseApp]. */ expect fun Firebase.firestore(app: FirebaseApp): FirebaseFirestore -sealed class LocalCacheSettings { - data class Persistent(val sizeBytes: Long? = null) : LocalCacheSettings() - data class Memory(val garbaseCollectorSettings: GarbageCollectorSettings) : LocalCacheSettings() { - sealed class GarbageCollectorSettings { - data object Eager : GarbageCollectorSettings() - data class LRUGC(val sizeBytes: Long? = null) : GarbageCollectorSettings() - } - } -} - expect class FirebaseFirestore { - class Settings { - - companion object { - fun create(sslEnabled: Boolean? = null, host: String? = null, cacheSettings: LocalCacheSettings? = null): Settings - } - - val sslEnabled: Boolean? - val host: String? - val cacheSettings: LocalCacheSettings? - } + var settings: FirestoreSettings fun collection(collectionPath: String): CollectionReference fun collectionGroup(collectionId: String): Query @@ -49,35 +30,55 @@ expect class FirebaseFirestore { suspend fun clearPersistence() suspend fun runTransaction(func: suspend Transaction.() -> T): T fun useEmulator(host: String, port: Int) - fun setSettings(settings: Settings) - fun updateSettings(settings: Settings) suspend fun disableNetwork() suspend fun enableNetwork() } +val FirestoreSettings.Companion.CACHE_SIZE_UNLIMITED get() = -1L +val FirestoreSettings.Companion.DEFAULT_HOST get() = "firestore.googleapis.com" + +expect class FirestoreSettings { + + companion object {} + + interface Builder { + var sslEnabled: Boolean + var host: String + var cacheSettings: LocalCacheSettings + + fun build(): FirestoreSettings + } + + val sslEnabled: Boolean + val host: String + val cacheSettings: LocalCacheSettings +} + +expect fun firestoreSettings(settings: FirestoreSettings? = null, builder: FirestoreSettings.Builder.() -> Unit): FirestoreSettings + @Deprecated("Use dev.gitlive.firebase.firestore instead", replaceWith = ReplaceWith("setSettings(FirebaseFirestore.Settings.create())")) fun FirebaseFirestore.setSettings( persistenceEnabled: Boolean? = null, sslEnabled: Boolean? = null, host: String? = null, cacheSizeBytes: Long? = null, -) = setSettings( - FirebaseFirestore.Settings.create( - sslEnabled, - host, - persistenceEnabled?.let { persistence -> - if (persistence) { - LocalCacheSettings.Persistent(cacheSizeBytes) +) { + settings = firestoreSettings { + this.sslEnabled = sslEnabled ?: true + this.host = host ?: FirestoreSettings.DEFAULT_HOST + this.cacheSettings = if (persistenceEnabled != false) { + LocalCacheSettings.Persistent(cacheSizeBytes ?: FirestoreSettings.CACHE_SIZE_UNLIMITED) + } else { + val cacheSize = cacheSizeBytes ?: FirestoreSettings.CACHE_SIZE_UNLIMITED + val garbageCollectionSettings = if (cacheSize == FirestoreSettings.CACHE_SIZE_UNLIMITED) { + GarbageCollectorSettings.Eager } else { - LocalCacheSettings.Memory( - cacheSizeBytes?.let { - LocalCacheSettings.Memory.GarbageCollectorSettings.LRUGC(it) - } ?: LocalCacheSettings.Memory.GarbageCollectorSettings.Eager - ) + GarbageCollectorSettings.LRUGC(cacheSize) } + LocalCacheSettings.Memory(garbageCollectionSettings) } - ) -) + } +} @PublishedApi internal sealed class SetOptions { diff --git a/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt index e08bc95a4..a968b1646 100644 --- a/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt +++ b/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt @@ -100,7 +100,13 @@ class FirebaseFirestoreTest { firestore = Firebase.firestore(app).apply { useEmulator(emulatorHost, 8080) - setSettings(FirebaseFirestore.Settings.create(cacheSettings = LocalCacheSettings.Memory(LocalCacheSettings.Memory.GarbageCollectorSettings.Eager))) + settings = firestoreSettings(settings) { + cacheSettings = memoryCacheSettings { + gcSettings = memoryLruGcSettings { + sizeBytes = 50L*1024L*1024L + } + } + } } } diff --git a/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt index 700f7d7f5..a504cc81d 100644 --- a/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt +++ b/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt @@ -20,6 +20,7 @@ import platform.Foundation.NSError import platform.Foundation.NSNull import platform.Foundation.NSNumber import platform.Foundation.numberWithLong +import platform.darwin.dispatch_get_main_queue import platform.darwin.dispatch_queue_t actual val Firebase.firestore get() = @@ -30,11 +31,11 @@ actual fun Firebase.firestore(app: FirebaseApp): FirebaseFirestore = FirebaseFir ) val LocalCacheSettings.ios: FIRLocalCacheSettingsProtocol get() = when (this) { - is LocalCacheSettings.Persistent -> sizeBytes?.let { FIRPersistentCacheSettings(NSNumber.numberWithLong(it)) } ?: FIRPersistentCacheSettings() + is LocalCacheSettings.Persistent -> FIRPersistentCacheSettings(NSNumber.numberWithLong(sizeBytes)) is LocalCacheSettings.Memory -> FIRMemoryCacheSettings( when (garbaseCollectorSettings) { - is LocalCacheSettings.Memory.GarbageCollectorSettings.Eager -> FIRMemoryEagerGCSettings() - is LocalCacheSettings.Memory.GarbageCollectorSettings.LRUGC -> garbaseCollectorSettings.sizeBytes?.let { FIRMemoryLRUGCSettings(NSNumber.numberWithLong(it)) } ?: FIRMemoryLRUGCSettings() + is GarbageCollectorSettings.Eager -> FIRMemoryEagerGCSettings() + is GarbageCollectorSettings.LRUGC -> FIRMemoryLRUGCSettings(NSNumber.numberWithLong(garbaseCollectorSettings.sizeBytes)) } ) } @@ -42,16 +43,11 @@ val LocalCacheSettings.ios: FIRLocalCacheSettingsProtocol get() = when (this) { @Suppress("UNCHECKED_CAST") actual class FirebaseFirestore(val ios: FIRFirestore) { - actual data class Settings( - actual val sslEnabled: Boolean? = null, - actual val host: String? = null, - actual val cacheSettings: LocalCacheSettings? = null, - val dispatchQueue: dispatch_queue_t = null - ) { - actual companion object { - actual fun create(sslEnabled: Boolean?, host: String?, cacheSettings: LocalCacheSettings?) = Settings(sslEnabled, host, cacheSettings) + actual var settings: FirestoreSettings = FirestoreSettings.BuilderImpl().build() + set(value) { + field = value + ios.settings = value.ios } - } actual fun collection(collectionPath: String) = CollectionReference(NativeCollectionReference(ios.collectionWithPath(collectionPath))) @@ -78,30 +74,62 @@ actual class FirebaseFirestore(val ios: FIRFirestore) { } } - actual fun setSettings(settings: Settings) { - ios.settings = FIRFirestoreSettings().applySettings(settings) + actual suspend fun disableNetwork() { + await { ios.disableNetworkWithCompletion(it) } } - actual fun updateSettings(settings: Settings) { - ios.settings = ios.settings.applySettings(settings) + actual suspend fun enableNetwork() { + await { ios.enableNetworkWithCompletion(it) } } +} + +actual data class FirestoreSettings( + actual val sslEnabled: Boolean, + actual val host: String, + actual val cacheSettings: LocalCacheSettings, + val dispatchQueue: dispatch_queue_t, +) { + + actual companion object {} - private fun FIRFirestoreSettings.applySettings(settings: Settings): FIRFirestoreSettings = apply { - settings.cacheSettings?.let { cacheSettings = it.ios } - settings.sslEnabled?.let { sslEnabled = it } - settings.host?.let { host = it } - settings.dispatchQueue?.let { dispatchQueue = it } + actual interface Builder { + actual var sslEnabled: Boolean + actual var host: String + actual var cacheSettings: LocalCacheSettings + var dispatchQueue: dispatch_queue_t + + actual fun build(): FirestoreSettings } - actual suspend fun disableNetwork() { - await { ios.disableNetworkWithCompletion(it) } + internal class BuilderImpl : Builder { + override var sslEnabled: Boolean = true + override var host: String = DEFAULT_HOST + override var cacheSettings: LocalCacheSettings = LocalCacheSettings.Persistent(CACHE_SIZE_UNLIMITED) + override var dispatchQueue: dispatch_queue_t = dispatch_get_main_queue() + + override fun build() = FirestoreSettings(sslEnabled, host, cacheSettings, dispatchQueue) } - actual suspend fun enableNetwork() { - await { ios.enableNetworkWithCompletion(it) } + val ios: FIRFirestoreSettings get() = FIRFirestoreSettings().apply { + cacheSettings = this@FirestoreSettings.cacheSettings.ios + sslEnabled = this@FirestoreSettings.sslEnabled + host = this@FirestoreSettings.host + dispatchQueue = this@FirestoreSettings.dispatchQueue } } +actual fun firestoreSettings( + settings: FirestoreSettings?, + builder: FirestoreSettings.Builder.() -> Unit +): FirestoreSettings = FirestoreSettings.BuilderImpl().apply { + settings?.let { + sslEnabled = it.sslEnabled + host = it.host + cacheSettings = it.cacheSettings + dispatchQueue = it.dispatchQueue + } +}.apply(builder).build() + @Suppress("UNCHECKED_CAST") @PublishedApi internal actual class NativeWriteBatch(val ios: FIRWriteBatch) { diff --git a/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/externals/firestore.kt b/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/externals/firestore.kt index 920b95eaf..47d89c046 100644 --- a/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/externals/firestore.kt +++ b/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/externals/firestore.kt @@ -291,11 +291,32 @@ external interface FirestoreLocalCache { val kind: String } +external interface MemoryLocalCache : FirestoreLocalCache +external interface PersistentLocalCache : FirestoreLocalCache + +external interface MemoryCacheSettings { + val garbageCollector: MemoryGarbageCollector +} + +external interface MemoryGarbageCollector { + val kind: String +} + +external interface MemoryLruGarbageCollector : MemoryGarbageCollector +external interface MemoryEagerGarbageCollector : MemoryGarbageCollector + +external interface PersistentCacheSettings { + val cacheSizeBytes: Int + val tabManager: PersistentTabManager +} + external interface PersistentTabManager { val kind: String } -external fun memoryLocalCache(): FirestoreLocalCache -external fun persistentLocalCache(settings: dynamic = definedExternally): FirestoreLocalCache +external fun memoryLocalCache(settings: MemoryCacheSettings): MemoryLocalCache +external fun memoryEagerGarbageCollector(): MemoryEagerGarbageCollector +external fun memoryLruGarbageCollector(settings: dynamic = definedExternally): MemoryLruGarbageCollector +external fun persistentLocalCache(settings: PersistentCacheSettings): PersistentLocalCache external fun persistentSingleTabManager(settings: dynamic = definedExternally): PersistentTabManager external fun persistentMultipleTabManager(): PersistentTabManager diff --git a/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt index 060277928..95e1949da 100644 --- a/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt +++ b/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt @@ -7,7 +7,10 @@ package dev.gitlive.firebase.firestore import dev.gitlive.firebase.Firebase import dev.gitlive.firebase.FirebaseApp import dev.gitlive.firebase.FirebaseException +import dev.gitlive.firebase.externals.getApp import dev.gitlive.firebase.firestore.externals.Firestore +import dev.gitlive.firebase.firestore.externals.MemoryCacheSettings +import dev.gitlive.firebase.firestore.externals.PersistentCacheSettings import dev.gitlive.firebase.firestore.externals.QueryConstraint import dev.gitlive.firebase.firestore.externals.addDoc import dev.gitlive.firebase.firestore.externals.and @@ -15,15 +18,16 @@ import dev.gitlive.firebase.firestore.externals.clearIndexedDbPersistence import dev.gitlive.firebase.firestore.externals.connectFirestoreEmulator import dev.gitlive.firebase.firestore.externals.deleteDoc import dev.gitlive.firebase.firestore.externals.doc -import dev.gitlive.firebase.firestore.externals.documentId as jsDocumentId -import dev.gitlive.firebase.firestore.externals.enableIndexedDbPersistence import dev.gitlive.firebase.firestore.externals.getDoc import dev.gitlive.firebase.firestore.externals.getDocs -import dev.gitlive.firebase.firestore.externals.getFirestore import dev.gitlive.firebase.firestore.externals.initializeFirestore +import dev.gitlive.firebase.firestore.externals.memoryEagerGarbageCollector +import dev.gitlive.firebase.firestore.externals.memoryLocalCache +import dev.gitlive.firebase.firestore.externals.memoryLruGarbageCollector import dev.gitlive.firebase.firestore.externals.onSnapshot import dev.gitlive.firebase.firestore.externals.or import dev.gitlive.firebase.firestore.externals.orderBy +import dev.gitlive.firebase.firestore.externals.persistentLocalCache import dev.gitlive.firebase.firestore.externals.query import dev.gitlive.firebase.firestore.externals.refEqual import dev.gitlive.firebase.firestore.externals.setDoc @@ -35,9 +39,9 @@ import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.promise -import kotlinx.serialization.Serializable import kotlin.js.Json import kotlin.js.json +import dev.gitlive.firebase.externals.FirebaseApp as JsFirebaseApp import dev.gitlive.firebase.firestore.externals.CollectionReference as JsCollectionReference import dev.gitlive.firebase.firestore.externals.DocumentChange as JsDocumentChange import dev.gitlive.firebase.firestore.externals.DocumentReference as JsDocumentReference @@ -51,6 +55,7 @@ import dev.gitlive.firebase.firestore.externals.WriteBatch as JsWriteBatch import dev.gitlive.firebase.firestore.externals.collection as jsCollection import dev.gitlive.firebase.firestore.externals.collectionGroup as jsCollectionGroup import dev.gitlive.firebase.firestore.externals.disableNetwork as jsDisableNetwork +import dev.gitlive.firebase.firestore.externals.documentId as jsDocumentId import dev.gitlive.firebase.firestore.externals.enableNetwork as jsEnableNetwork import dev.gitlive.firebase.firestore.externals.endAt as jsEndAt import dev.gitlive.firebase.firestore.externals.endBefore as jsEndBefore @@ -62,27 +67,25 @@ import dev.gitlive.firebase.firestore.externals.updateDoc as jsUpdate import dev.gitlive.firebase.firestore.externals.where as jsWhere actual val Firebase.firestore get() = - rethrow { FirebaseFirestore(getFirestore()) } + rethrow { FirebaseFirestore(getApp()) } actual fun Firebase.firestore(app: FirebaseApp) = - rethrow { FirebaseFirestore(getFirestore(app.js)) } + rethrow { FirebaseFirestore(app.js) } -actual class FirebaseFirestore(jsFirestore: Firestore) { +actual class FirebaseFirestore(app: JsFirebaseApp) { - actual data class Settings( - actual val sslEnabled: Boolean? = null, - actual val host: String? = null, - actual val cacheSettings: LocalCacheSettings? = null - ) { - actual companion object { - actual fun create(sslEnabled: Boolean?, host: String?, cacheSettings: LocalCacheSettings?) = Settings(sslEnabled, host, cacheSettings) - } - } + private data class EmulatorSettings(val host: String, val port: Int) - private var lastSettings = Settings() + actual var settings: FirestoreSettings = FirestoreSettings.BuilderImpl().build() + private var emulatorSettings: EmulatorSettings? = null - var js: Firestore = jsFirestore - private set + val js: Firestore by lazy { + initializeFirestore(app, settings.js).also { + emulatorSettings?.run { + connectFirestoreEmulator(it, host, port) + } + } + } actual fun collection(collectionPath: String) = rethrow { CollectionReference(NativeCollectionReference(jsCollection(js, collectionPath))) } @@ -101,31 +104,10 @@ actual class FirebaseFirestore(jsFirestore: Firestore) { actual suspend fun clearPersistence() = rethrow { clearIndexedDbPersistence(js).await() } - actual fun useEmulator(host: String, port: Int) = rethrow { connectFirestoreEmulator(js, host, port) } - - actual fun setSettings(settings: Settings) { - lastSettings = settings - if(settings.cacheSettings is LocalCacheSettings.Persistent) enableIndexedDbPersistence(js) - - val jsSettings = json().apply { - settings.sslEnabled?.let { set("ssl", it) } - settings.host?.let { set("host", it) } - when (val cacheSettings = settings.cacheSettings) { - is LocalCacheSettings.Persistent -> cacheSettings.sizeBytes - is LocalCacheSettings.Memory -> when (val garbageCollectorSettings = cacheSettings.garbaseCollectorSettings) { - is LocalCacheSettings.Memory.GarbageCollectorSettings.Eager -> null - is LocalCacheSettings.Memory.GarbageCollectorSettings.LRUGC -> garbageCollectorSettings.sizeBytes - } - null -> null - }?.let { set("cacheSizeBytes", it) } - } - js = initializeFirestore(js.app, jsSettings) + actual fun useEmulator(host: String, port: Int) = rethrow { + emulatorSettings = EmulatorSettings(host, port) } - actual fun updateSettings(settings: Settings) = setSettings( - Settings(settings.sslEnabled ?: lastSettings.sslEnabled, settings.host ?: lastSettings.host, settings.cacheSettings ?: lastSettings.cacheSettings) - ) - actual suspend fun disableNetwork() { rethrow { jsDisableNetwork(js).await() } } @@ -135,6 +117,63 @@ actual class FirebaseFirestore(jsFirestore: Firestore) { } } +actual data class FirestoreSettings( + actual val sslEnabled: Boolean, + actual val host: String, + actual val cacheSettings: LocalCacheSettings, +) { + + actual companion object {} + + actual interface Builder { + actual var sslEnabled: Boolean + actual var host: String + actual var cacheSettings: LocalCacheSettings + + actual fun build(): FirestoreSettings + } + + internal class BuilderImpl : Builder { + override var sslEnabled: Boolean = true + override var host: String = DEFAULT_HOST + override var cacheSettings: LocalCacheSettings = LocalCacheSettings.Persistent(CACHE_SIZE_UNLIMITED) + + override fun build() = FirestoreSettings(sslEnabled, host, cacheSettings) + } + + @Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") + val js: Json get() = json().apply { + set("ssl", sslEnabled) + set("host", host) + set("localCache", + when (cacheSettings) { + is LocalCacheSettings.Persistent -> persistentLocalCache( + json( + "cacheSizeBytes" to cacheSettings.sizeBytes + ).asDynamic() as PersistentCacheSettings + ) + is LocalCacheSettings.Memory -> { + val garbageCollecorSettings = when (val garbageCollectorSettings = cacheSettings.garbaseCollectorSettings) { + is GarbageCollectorSettings.Eager -> memoryEagerGarbageCollector() + is GarbageCollectorSettings.LRUGC -> memoryLruGarbageCollector(json("cacheSizeBytes" to garbageCollectorSettings.sizeBytes)) + } + memoryLocalCache(json("garbageCollector" to garbageCollecorSettings).asDynamic() as MemoryCacheSettings) + } + }) + } +} + +actual fun firestoreSettings( + settings: FirestoreSettings?, + builder: FirestoreSettings.Builder.() -> Unit +): FirestoreSettings = FirestoreSettings.BuilderImpl().apply { + settings?.let { + sslEnabled = it.sslEnabled + host = it.host + cacheSettings = it.cacheSettings + } +}.apply(builder).build() + internal val SetOptions.js: Json get() = when (this) { is SetOptions.Merge -> json("merge" to true) is SetOptions.Overwrite -> json("merge" to false) From 473e9ba655cea79907e6d8b77498aff28d42aaf3 Mon Sep 17 00:00:00 2001 From: Gijs van Veen Date: Mon, 15 Apr 2024 12:59:04 +0200 Subject: [PATCH 3/8] Make iOS emulator track settings correctly --- .../kotlin/dev/gitlive/firebase/firestore/firestore.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt index a504cc81d..08e382cc3 100644 --- a/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt +++ b/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt @@ -68,8 +68,9 @@ actual class FirebaseFirestore(val ios: FIRFirestore) { actual fun useEmulator(host: String, port: Int) { ios.useEmulatorWithHost(host, port.toLong()) - ios.settings = ios.settings.apply { - cacheSettings = FIRMemoryCacheSettings() + settings = firestoreSettings(settings) { + this.host = "$host:$port" + cacheSettings = memoryCacheSettings { } sslEnabled = false } } From d7b3719db7b37c56e20c361ab1f0037b7a1d507d Mon Sep 17 00:00:00 2001 From: Gijs van Veen Date: Mon, 15 Apr 2024 13:53:39 +0200 Subject: [PATCH 4/8] Minor cleanup/platform consistency fixes --- .../kotlin/dev/gitlive/firebase/firestore/firestore.kt | 2 +- .../kotlin/dev/gitlive/firebase/firestore/firestore.kt | 4 +--- .../jsMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt | 3 +++ 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt index 17008ec75..807e8965a 100644 --- a/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt +++ b/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt @@ -75,6 +75,7 @@ actual class FirebaseFirestore(val android: com.google.firebase.firestore.Fireba 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) @@ -112,7 +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 = firestoreSettings { } } actual suspend fun disableNetwork() = diff --git a/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt index a968b1646..4928a36cb 100644 --- a/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt +++ b/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt @@ -102,9 +102,7 @@ class FirebaseFirestoreTest { useEmulator(emulatorHost, 8080) settings = firestoreSettings(settings) { cacheSettings = memoryCacheSettings { - gcSettings = memoryLruGcSettings { - sizeBytes = 50L*1024L*1024L - } + gcSettings = memoryEagerGcSettings { } } } } diff --git a/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt index 95e1949da..a559863d5 100644 --- a/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt +++ b/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt @@ -105,6 +105,9 @@ actual class FirebaseFirestore(app: JsFirebaseApp) { rethrow { clearIndexedDbPersistence(js).await() } actual fun useEmulator(host: String, port: Int) = rethrow { + settings = firestoreSettings(settings) { + this.host = "$host:$port" + } emulatorSettings = EmulatorSettings(host, port) } From 96df88183556c7399c6a49c2ea8422227627b1a5 Mon Sep 17 00:00:00 2001 From: Gijs van Veen Date: Wed, 17 Apr 2024 21:19:54 +0200 Subject: [PATCH 5/8] PR remarks --- .../dev/gitlive/firebase/database/database.kt | 4 - .../gitlive/firebase/firestore/firestore.kt | 118 ++++++++------- .../firebase/firestore/LocalCacheSettings.kt | 81 ++++------ .../gitlive/firebase/firestore/firestore.kt | 141 +++++++++++++----- .../gitlive/firebase/firestore/firestore.kt | 14 ++ .../gitlive/firebase/firestore/firestore.kt | 99 ++++++------ .../gitlive/firebase/firestore/firestore.kt | 98 +++++++----- 7 files changed, 329 insertions(+), 226 deletions(-) diff --git a/firebase-database/src/iosMain/kotlin/dev/gitlive/firebase/database/database.kt b/firebase-database/src/iosMain/kotlin/dev/gitlive/firebase/database/database.kt index f59d83155..92de88a7c 100644 --- a/firebase-database/src/iosMain/kotlin/dev/gitlive/firebase/database/database.kt +++ b/firebase-database/src/iosMain/kotlin/dev/gitlive/firebase/database/database.kt @@ -71,10 +71,6 @@ actual class FirebaseDatabase internal constructor(val ios: FIRDatabase) { ios.setPersistenceCacheSizeBytes(cacheSizeInBytes.toULong()) } - fun setCallbackQueue(callbackQueue: dispatch_queue_t) { - ios.callbackQueue = callbackQueue - } - actual fun setLoggingEnabled(enabled: Boolean) = FIRDatabase.setLoggingEnabled(enabled) diff --git a/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt index 807e8965a..204d53909 100644 --- a/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt +++ b/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt @@ -6,13 +6,11 @@ package dev.gitlive.firebase.firestore import com.google.android.gms.tasks.TaskExecutors -import com.google.firebase.firestore.FirebaseFirestoreSettings +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.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 com.google.firebase.firestore.PersistentCacheSettings import dev.gitlive.firebase.Firebase import dev.gitlive.firebase.FirebaseApp import kotlinx.coroutines.channels.ProducerScope @@ -21,13 +19,16 @@ 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 +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()) @@ -36,14 +37,14 @@ 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 { + is LocalCacheSettings.Persistent -> androidPersistentCacheSettings { setSizeBytes(sizeBytes) } - is LocalCacheSettings.Memory -> memoryCacheSettings { + is LocalCacheSettings.Memory -> androidMemoryCacheSettings { setGcSettings( when (garbaseCollectorSettings) { - is GarbageCollectorSettings.Eager -> memoryEagerGcSettings { } - is GarbageCollectorSettings.LRUGC -> memoryLruGcSettings { + is MemoryGarbageCollectorSettings.Eager -> androidMemoryEagerGcSettings { } + is MemoryGarbageCollectorSettings.LRUGC -> androidMemoryLruGcSettings { setSizeBytes(garbaseCollectorSettings.sizeBytes) } } @@ -54,107 +55,116 @@ val LocalCacheSettings.android: com.google.firebase.firestore.LocalCacheSettings // Since on iOS Callback threads are set as settings, we store the settings explicitly here as well private val callbackExecutorMap = ConcurrentHashMap() -actual class FirebaseFirestore(val android: com.google.firebase.firestore.FirebaseFirestore) { +actual typealias NativeFirebaseFirestore = com.google.firebase.firestore.FirebaseFirestore +actual internal class NativeFirebaseFirestoreWrapper actual constructor(actual val native: NativeFirebaseFirestore) { - actual var settings: FirestoreSettings - get() = with(android.firestoreSettings) { - FirestoreSettings( + actual var settings: FirebaseFirestoreSettings + get() = with(native.firestoreSettings) { + FirebaseFirestoreSettings( isSslEnabled, host, cacheSettings?.let { localCacheSettings -> when (localCacheSettings) { - is com.google.firebase.firestore.MemoryCacheSettings -> { + is 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) + is MemoryEagerGcSettings -> MemoryGarbageCollectorSettings.Eager + is MemoryLruGcSettings -> MemoryGarbageCollectorSettings.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) + + 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 == FirestoreSettings.CACHE_SIZE_UNLIMITED -> LocalCacheSettings.Memory(GarbageCollectorSettings.Eager) - else -> LocalCacheSettings.Memory(GarbageCollectorSettings.LRUGC(cacheSizeBytes)) + cacheSizeBytes == FirebaseFirestoreSettings.CACHE_SIZE_UNLIMITED -> LocalCacheSettings.Memory(MemoryGarbageCollectorSettings.Eager) + else -> LocalCacheSettings.Memory(MemoryGarbageCollectorSettings.LRUGC(cacheSizeBytes)) } }, - callbackExecutorMap[android] ?: TaskExecutors.MAIN_THREAD + callbackExecutorMap[native] ?: TaskExecutors.MAIN_THREAD ) } set(value) { - android.firestoreSettings = firestoreSettings { + native.firestoreSettings = androidFirestoreSettings { isSslEnabled = value.sslEnabled host = value.host setLocalCacheSettings(value.cacheSettings.android) } - callbackExecutorMap[android] = value.callbackExecutor + 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 runTransaction(func: suspend Transaction.() -> T): T = - android.runTransaction { runBlocking { Transaction(NativeTransaction(it)).func() } }.await() + actual suspend fun 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) + 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 { } } -actual data class FirestoreSettings( +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 interface Builder { - actual var sslEnabled: Boolean - actual var host: String - actual var cacheSettings: LocalCacheSettings - var callbackExecutor: Executor - - actual fun build(): FirestoreSettings + 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 } - 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 + 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) - override fun build() = FirestoreSettings(sslEnabled, host, cacheSettings, callbackExecutor) + actual fun build(): FirebaseFirestoreSettings = FirebaseFirestoreSettings(sslEnabled, host, cacheSettings, callbackExecutor) } } actual fun firestoreSettings( - settings: FirestoreSettings?, - builder: FirestoreSettings.Builder.() -> Unit -): FirestoreSettings = FirestoreSettings.BuilderImpl().apply { + settings: FirebaseFirestoreSettings?, + builder: FirebaseFirestoreSettings.Builder.() -> Unit +): FirebaseFirestoreSettings = FirebaseFirestoreSettings.Builder().apply { settings?.let { sslEnabled = it.sslEnabled host = it.host diff --git a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/LocalCacheSettings.kt b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/LocalCacheSettings.kt index 1ce50e619..a701b8a69 100644 --- a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/LocalCacheSettings.kt +++ b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/LocalCacheSettings.kt @@ -1,90 +1,69 @@ package dev.gitlive.firebase.firestore -sealed class LocalCacheSettings { +sealed interface LocalCacheSettings { - internal companion object { - // Firestore cache defaults to 100MB - const val DEFAULT_CACHE_SIZE = 100L*1024L*1024L - } - - data class Persistent(val sizeBytes: Long) : LocalCacheSettings() { + data class Persistent internal constructor(val sizeBytes: Long) : LocalCacheSettings { companion object { - fun newBuilder(): Builder = BuilderImpl() + fun newBuilder(): Builder = Builder() } - interface Builder { - var sizeBytes: Long - fun build(): Persistent - } - - private class BuilderImpl( - override var sizeBytes: Long = DEFAULT_CACHE_SIZE - ) : Builder { - override fun build(): Persistent = Persistent(sizeBytes) + class Builder internal constructor() { + var sizeBytes: Long = FirebaseFirestoreSettings.DEFAULT_CACHE_SIZE_BYTES + fun build(): Persistent = Persistent(sizeBytes) } } - data class Memory(val garbaseCollectorSettings: GarbageCollectorSettings) : LocalCacheSettings() { + data class Memory internal constructor(val garbaseCollectorSettings: MemoryGarbageCollectorSettings) : LocalCacheSettings { companion object { - fun newBuilder(): Builder = BuilderImpl() + fun newBuilder(): Builder = Builder() } - interface Builder { - - var gcSettings: GarbageCollectorSettings + class Builder internal constructor() { - fun build(): Memory - } + var gcSettings: MemoryGarbageCollectorSettings = MemoryGarbageCollectorSettings.Eager.newBuilder().build() - private class BuilderImpl( - override var gcSettings: GarbageCollectorSettings = GarbageCollectorSettings.Eager - ) : Builder { - override fun build(): Memory = Memory(gcSettings) + fun build(): Memory = Memory(gcSettings) } } } -sealed class GarbageCollectorSettings { - data object Eager : GarbageCollectorSettings() { +typealias PersistentCacheSettings = LocalCacheSettings.Persistent +typealias MemoryCacheSettings = LocalCacheSettings.Memory - fun newBuilder(): Builder = BuilderImpl() +sealed interface MemoryGarbageCollectorSettings { + data object Eager : MemoryGarbageCollectorSettings { - interface Builder { - fun build(): Eager - } + fun newBuilder(): Builder = Builder() - private class BuilderImpl : Builder { - override fun build(): Eager = Eager + class Builder internal constructor() { + fun build(): Eager = Eager } } - data class LRUGC(val sizeBytes: Long) : GarbageCollectorSettings() { + data class LRUGC internal constructor(val sizeBytes: Long) : MemoryGarbageCollectorSettings { companion object { - fun newBuilder(): Builder = BuilderImpl() + fun newBuilder(): Builder = Builder() } - interface Builder { - var sizeBytes: Long - fun build(): LRUGC - } - - private class BuilderImpl( - override var sizeBytes: Long = LocalCacheSettings.DEFAULT_CACHE_SIZE - ) : Builder { - override fun build(): LRUGC = LRUGC(sizeBytes) + class Builder internal constructor() { + var sizeBytes: Long = FirebaseFirestoreSettings.DEFAULT_CACHE_SIZE_BYTES + fun build(): LRUGC = LRUGC(sizeBytes) } } } +typealias MemoryEagerGcSettings = MemoryGarbageCollectorSettings.Eager +typealias MemoryLruGcSettings = MemoryGarbageCollectorSettings.LRUGC + fun memoryCacheSettings(builder: LocalCacheSettings.Memory.Builder.() -> Unit): LocalCacheSettings.Memory = LocalCacheSettings.Memory.newBuilder().apply(builder).build() -fun memoryEagerGcSettings(builder: GarbageCollectorSettings.Eager.Builder.() -> Unit) = - GarbageCollectorSettings.Eager.newBuilder().apply(builder).build() +fun memoryEagerGcSettings(builder: MemoryGarbageCollectorSettings.Eager.Builder.() -> Unit) = + MemoryGarbageCollectorSettings.Eager.newBuilder().apply(builder).build() -fun memoryLruGcSettings(builder: GarbageCollectorSettings.LRUGC.Builder.() -> Unit) = - GarbageCollectorSettings.LRUGC.newBuilder().apply(builder).build() +fun memoryLruGcSettings(builder: MemoryGarbageCollectorSettings.LRUGC.Builder.() -> Unit) = + MemoryGarbageCollectorSettings.LRUGC.newBuilder().apply(builder).build() fun persistentCacheSettings(builder: LocalCacheSettings.Persistent.Builder.() -> Unit) = LocalCacheSettings.Persistent.newBuilder().apply(builder).build() \ No newline at end of file diff --git a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt index c82370b94..37787082f 100644 --- a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt +++ b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt @@ -6,6 +6,7 @@ package dev.gitlive.firebase.firestore import dev.gitlive.firebase.* import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.map import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.Serializable @@ -18,35 +19,123 @@ expect val Firebase.firestore: FirebaseFirestore /** Returns the [FirebaseFirestore] instance of a given [FirebaseApp]. */ expect fun Firebase.firestore(app: FirebaseApp): FirebaseFirestore -expect class FirebaseFirestore { +expect class NativeFirebaseFirestore - var settings: FirestoreSettings +internal expect class NativeFirebaseFirestoreWrapper internal constructor(native: NativeFirebaseFirestore) { + val native: NativeFirebaseFirestore + var settings: FirebaseFirestoreSettings - fun collection(collectionPath: String): CollectionReference - fun collectionGroup(collectionId: String): Query - fun document(documentPath: String): DocumentReference - fun batch(): WriteBatch + fun collection(collectionPath: String): NativeCollectionReference + fun collectionGroup(collectionId: String): NativeQuery + fun document(documentPath: String): NativeDocumentReference + fun batch(): NativeWriteBatch fun setLoggingEnabled(loggingEnabled: Boolean) suspend fun clearPersistence() - suspend fun runTransaction(func: suspend Transaction.() -> T): T + suspend fun runTransaction(func: suspend NativeTransaction.() -> T): T fun useEmulator(host: String, port: Int) suspend fun disableNetwork() suspend fun enableNetwork() } -val FirestoreSettings.Companion.CACHE_SIZE_UNLIMITED get() = -1L -val FirestoreSettings.Companion.DEFAULT_HOST get() = "firestore.googleapis.com" +class FirebaseFirestore internal constructor(private val wrapper: NativeFirebaseFirestoreWrapper) { + + constructor(native: NativeFirebaseFirestore) : this(NativeFirebaseFirestoreWrapper(native)) + + val native = wrapper.native + var settings: FirebaseFirestoreSettings + get() = wrapper.settings + set(value) { + if (isConfigured.compareAndSet(expect = false, update = false) || value == wrapper.settings) { + wrapper.settings = value + } else { + throw IllegalStateException("FirebaseFirestore has already been started and its settings can no longer be changed. You can only call setFirestoreSettings() before calling any other methods on a FirebaseFirestore object.") + } + } + + // Should probably use atomic from atomicfu + private val isConfigured = MutableStateFlow(false) + + fun collection(collectionPath: String): CollectionReference = ensureConfigured { + CollectionReference(wrapper.collection(collectionPath)) + } + fun collectionGroup(collectionId: String): Query = ensureConfigured { + Query(wrapper.collectionGroup(collectionId)) + } + fun document(documentPath: String): DocumentReference = ensureConfigured { + DocumentReference(wrapper.document(documentPath)) + } + fun batch(): WriteBatch = ensureConfigured { + WriteBatch(wrapper.batch()) + } + fun setLoggingEnabled(loggingEnabled: Boolean) = ensureConfigured { + wrapper.setLoggingEnabled(loggingEnabled) + } + suspend fun clearPersistence() = ensureConfiguredSuspended { + wrapper.clearPersistence() + } + suspend fun runTransaction(func: suspend Transaction.() -> T): T = ensureConfiguredSuspended { + wrapper.runTransaction { func(Transaction(this)) } + } + fun useEmulator(host: String, port: Int) = wrapper.useEmulator(host, port) + @Deprecated("Use settings instead", replaceWith = ReplaceWith("settings = firestoreSettings{}")) + fun setSettings( + persistenceEnabled: Boolean? = null, + sslEnabled: Boolean? = null, + host: String? = null, + cacheSizeBytes: Long? = null, + ) { + settings = firestoreSettings { + this.sslEnabled = sslEnabled ?: true + this.host = host ?: FirebaseFirestoreSettings.DEFAULT_HOST + this.cacheSettings = if (persistenceEnabled != false) { + LocalCacheSettings.Persistent(cacheSizeBytes ?: FirebaseFirestoreSettings.CACHE_SIZE_UNLIMITED) + } else { + val cacheSize = cacheSizeBytes ?: FirebaseFirestoreSettings.CACHE_SIZE_UNLIMITED + val garbageCollectionSettings = if (cacheSize == FirebaseFirestoreSettings.CACHE_SIZE_UNLIMITED) { + MemoryGarbageCollectorSettings.Eager + } else { + MemoryGarbageCollectorSettings.LRUGC(cacheSize) + } + LocalCacheSettings.Memory(garbageCollectionSettings) + } + } + } + suspend fun disableNetwork() = ensureConfiguredSuspended { + wrapper.disableNetwork() + } + suspend fun enableNetwork() = ensureConfiguredSuspended { + wrapper.enableNetwork() + } + + private fun ensureConfigured(action: () -> T): T { + isConfigured.compareAndSet(expect = false, update = true) + return action() + } + + private suspend fun ensureConfiguredSuspended(action: suspend () -> T): T { + isConfigured.compareAndSet(expect = false, update = true) + return action() + } +} + +expect class FirebaseFirestoreSettings { -expect class FirestoreSettings { + companion object { + val CACHE_SIZE_UNLIMITED: Long + internal val DEFAULT_HOST: String + internal val MINIMUM_CACHE_BYTES: Long + internal val DEFAULT_CACHE_SIZE_BYTES: Long + } - companion object {} + class Builder constructor() { + + constructor(settings: FirebaseFirestoreSettings) - interface Builder { var sslEnabled: Boolean var host: String var cacheSettings: LocalCacheSettings - fun build(): FirestoreSettings + fun build(): FirebaseFirestoreSettings } val sslEnabled: Boolean @@ -54,31 +143,7 @@ expect class FirestoreSettings { val cacheSettings: LocalCacheSettings } -expect fun firestoreSettings(settings: FirestoreSettings? = null, builder: FirestoreSettings.Builder.() -> Unit): FirestoreSettings - -@Deprecated("Use dev.gitlive.firebase.firestore instead", replaceWith = ReplaceWith("setSettings(FirebaseFirestore.Settings.create())")) -fun FirebaseFirestore.setSettings( - persistenceEnabled: Boolean? = null, - sslEnabled: Boolean? = null, - host: String? = null, - cacheSizeBytes: Long? = null, -) { - settings = firestoreSettings { - this.sslEnabled = sslEnabled ?: true - this.host = host ?: FirestoreSettings.DEFAULT_HOST - this.cacheSettings = if (persistenceEnabled != false) { - LocalCacheSettings.Persistent(cacheSizeBytes ?: FirestoreSettings.CACHE_SIZE_UNLIMITED) - } else { - val cacheSize = cacheSizeBytes ?: FirestoreSettings.CACHE_SIZE_UNLIMITED - val garbageCollectionSettings = if (cacheSize == FirestoreSettings.CACHE_SIZE_UNLIMITED) { - GarbageCollectorSettings.Eager - } else { - GarbageCollectorSettings.LRUGC(cacheSize) - } - LocalCacheSettings.Memory(garbageCollectionSettings) - } - } -} +expect fun firestoreSettings(settings: FirebaseFirestoreSettings? = null, builder: FirebaseFirestoreSettings.Builder.() -> Unit): FirebaseFirestoreSettings @PublishedApi internal sealed class SetOptions { diff --git a/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt index 4928a36cb..643cea2e4 100644 --- a/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt +++ b/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt @@ -28,6 +28,7 @@ import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import kotlin.test.assertNotEquals import kotlin.test.assertNotNull import kotlin.test.assertNull @@ -719,6 +720,19 @@ class FirebaseFirestoreTest { assertEquals(updatedData.documentReference.path, updatedSavedData.documentReference.path) } + @Test + fun testUpdateSettings() = runTest { + firestore.settings = firestoreSettings(firestore.settings) { + cacheSettings = persistentCacheSettings { } + } + firestore.enableNetwork() + assertFailsWith { + firestore.settings = firestoreSettings(firestore.settings) { + cacheSettings = memoryCacheSettings { } + } + } + } + @Serializable data class TestDataWithDocumentReference( val uid: String, diff --git a/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt index 08e382cc3..7f3eae78c 100644 --- a/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt +++ b/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt @@ -9,13 +9,9 @@ import cocoapods.FirebaseFirestoreInternal.FIRDocumentChangeType.* import dev.gitlive.firebase.* import kotlinx.cinterop.* import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.Deferred import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.runBlocking -import kotlinx.serialization.DeserializationStrategy -import kotlinx.serialization.Serializable -import kotlinx.serialization.SerializationStrategy import platform.Foundation.NSError import platform.Foundation.NSNull import platform.Foundation.NSNumber @@ -34,40 +30,44 @@ val LocalCacheSettings.ios: FIRLocalCacheSettingsProtocol get() = when (this) { is LocalCacheSettings.Persistent -> FIRPersistentCacheSettings(NSNumber.numberWithLong(sizeBytes)) is LocalCacheSettings.Memory -> FIRMemoryCacheSettings( when (garbaseCollectorSettings) { - is GarbageCollectorSettings.Eager -> FIRMemoryEagerGCSettings() - is GarbageCollectorSettings.LRUGC -> FIRMemoryLRUGCSettings(NSNumber.numberWithLong(garbaseCollectorSettings.sizeBytes)) + is MemoryGarbageCollectorSettings.Eager -> FIRMemoryEagerGCSettings() + is MemoryGarbageCollectorSettings.LRUGC -> FIRMemoryLRUGCSettings(NSNumber.numberWithLong(garbaseCollectorSettings.sizeBytes)) } ) } +actual typealias NativeFirebaseFirestore = FIRFirestore + @Suppress("UNCHECKED_CAST") -actual class FirebaseFirestore(val ios: FIRFirestore) { +actual internal class NativeFirebaseFirestoreWrapper internal actual constructor(actual val native: NativeFirebaseFirestore) { - actual var settings: FirestoreSettings = FirestoreSettings.BuilderImpl().build() + actual var settings: FirebaseFirestoreSettings = firestoreSettings { }.also { + native.settings = it.ios + } set(value) { field = value - ios.settings = value.ios + native.settings = value.ios } - actual fun collection(collectionPath: String) = CollectionReference(NativeCollectionReference(ios.collectionWithPath(collectionPath))) + actual fun collection(collectionPath: String) = NativeCollectionReference(native.collectionWithPath(collectionPath)) - actual fun collectionGroup(collectionId: String) = Query(ios.collectionGroupWithID(collectionId).native) + actual fun collectionGroup(collectionId: String) = native.collectionGroupWithID(collectionId).native - actual fun document(documentPath: String) = DocumentReference(NativeDocumentReference(ios.documentWithPath(documentPath))) + actual fun document(documentPath: String) = NativeDocumentReference(native.documentWithPath(documentPath)) - actual fun batch() = WriteBatch(NativeWriteBatch(ios.batch())) + actual fun batch() = NativeWriteBatch(native.batch()) actual fun setLoggingEnabled(loggingEnabled: Boolean): Unit = FIRFirestore.enableLogging(loggingEnabled) - actual suspend fun runTransaction(func: suspend Transaction.() -> T) = - awaitResult { ios.runTransactionWithBlock({ transaction, _ -> runBlocking { Transaction(NativeTransaction(transaction!!)).func() } }, it) } as T + actual suspend fun runTransaction(func: suspend NativeTransaction.() -> T) = + awaitResult { native.runTransactionWithBlock({ transaction, _ -> runBlocking { NativeTransaction(transaction!!).func() } }, it) } as T actual suspend fun clearPersistence() = - await { ios.clearPersistenceWithCompletion(it) } + await { native.clearPersistenceWithCompletion(it) } actual fun useEmulator(host: String, port: Int) { - ios.useEmulatorWithHost(host, port.toLong()) + native.useEmulatorWithHost(host, port.toLong()) settings = firestoreSettings(settings) { this.host = "$host:$port" cacheSettings = memoryCacheSettings { } @@ -76,53 +76,66 @@ actual class FirebaseFirestore(val ios: FIRFirestore) { } actual suspend fun disableNetwork() { - await { ios.disableNetworkWithCompletion(it) } + await { native.disableNetworkWithCompletion(it) } } actual suspend fun enableNetwork() { - await { ios.enableNetworkWithCompletion(it) } + await { native.enableNetworkWithCompletion(it) } } } -actual data class FirestoreSettings( +val FirebaseFirestore.ios get() = native + +actual data class FirebaseFirestoreSettings( actual val sslEnabled: Boolean, actual val host: String, actual val cacheSettings: LocalCacheSettings, val dispatchQueue: dispatch_queue_t, ) { - actual companion object {} - - actual interface Builder { - actual var sslEnabled: Boolean - actual var host: String - actual var cacheSettings: LocalCacheSettings - var dispatchQueue: dispatch_queue_t - - actual fun build(): FirestoreSettings + 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 } - internal class BuilderImpl : Builder { - override var sslEnabled: Boolean = true - override var host: String = DEFAULT_HOST - override var cacheSettings: LocalCacheSettings = LocalCacheSettings.Persistent(CACHE_SIZE_UNLIMITED) - override var dispatchQueue: dispatch_queue_t = dispatch_get_main_queue() - - override fun build() = FirestoreSettings(sslEnabled, host, cacheSettings, dispatchQueue) + actual class Builder( + actual var sslEnabled: Boolean, + actual var host: String, + actual var cacheSettings: LocalCacheSettings, + var dispatchQueue: dispatch_queue_t, + ) { + + actual constructor() : this( + true, + DEFAULT_HOST, + persistentCacheSettings { }, + dispatch_get_main_queue(), + ) + + actual constructor(settings: FirebaseFirestoreSettings) : this( + settings.sslEnabled, + settings.host, + settings.cacheSettings, + settings.dispatchQueue, + ) + + actual fun build(): FirebaseFirestoreSettings = FirebaseFirestoreSettings(sslEnabled, host, cacheSettings, dispatchQueue) } val ios: FIRFirestoreSettings get() = FIRFirestoreSettings().apply { - cacheSettings = this@FirestoreSettings.cacheSettings.ios - sslEnabled = this@FirestoreSettings.sslEnabled - host = this@FirestoreSettings.host - dispatchQueue = this@FirestoreSettings.dispatchQueue + cacheSettings = this@FirebaseFirestoreSettings.cacheSettings.ios + sslEnabled = this@FirebaseFirestoreSettings.sslEnabled + host = this@FirebaseFirestoreSettings.host + dispatchQueue = this@FirebaseFirestoreSettings.dispatchQueue } } actual fun firestoreSettings( - settings: FirestoreSettings?, - builder: FirestoreSettings.Builder.() -> Unit -): FirestoreSettings = FirestoreSettings.BuilderImpl().apply { + settings: FirebaseFirestoreSettings?, + builder: FirebaseFirestoreSettings.Builder.() -> Unit +): FirebaseFirestoreSettings = FirebaseFirestoreSettings.Builder().apply { settings?.let { sslEnabled = it.sslEnabled host = it.host diff --git a/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt index a559863d5..56b4e3d8b 100644 --- a/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt +++ b/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt @@ -41,7 +41,9 @@ import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.promise import kotlin.js.Json import kotlin.js.json +import kotlin.math.acos import dev.gitlive.firebase.externals.FirebaseApp as JsFirebaseApp +import dev.gitlive.firebase.firestore.externals.Firestore as JsFirestore import dev.gitlive.firebase.firestore.externals.CollectionReference as JsCollectionReference import dev.gitlive.firebase.firestore.externals.DocumentChange as JsDocumentChange import dev.gitlive.firebase.firestore.externals.DocumentReference as JsDocumentReference @@ -67,39 +69,55 @@ import dev.gitlive.firebase.firestore.externals.updateDoc as jsUpdate import dev.gitlive.firebase.firestore.externals.where as jsWhere actual val Firebase.firestore get() = - rethrow { FirebaseFirestore(getApp()) } + rethrow { FirebaseFirestore(NativeFirebaseFirestoreWrapper(getApp())) } actual fun Firebase.firestore(app: FirebaseApp) = - rethrow { FirebaseFirestore(app.js) } - -actual class FirebaseFirestore(app: JsFirebaseApp) { + rethrow { FirebaseFirestore(NativeFirebaseFirestoreWrapper(app.js)) } + +actual data class NativeFirebaseFirestore(val js: JsFirestore) + +actual internal class NativeFirebaseFirestoreWrapper internal constructor( + private val createNative: NativeFirebaseFirestoreWrapper.() -> NativeFirebaseFirestore +){ + + internal actual constructor(native: NativeFirebaseFirestore) : this({ native }) + internal constructor(app: JsFirebaseApp) : this( + { + NativeFirebaseFirestore( + initializeFirestore(app, settings.js).also { + emulatorSettings?.run { + connectFirestoreEmulator(it, host, port) + } + } + ) + } + ) private data class EmulatorSettings(val host: String, val port: Int) - actual var settings: FirestoreSettings = FirestoreSettings.BuilderImpl().build() + actual var settings: FirebaseFirestoreSettings = FirebaseFirestoreSettings.Builder().build() private var emulatorSettings: EmulatorSettings? = null - val js: Firestore by lazy { - initializeFirestore(app, settings.js).also { - emulatorSettings?.run { - connectFirestoreEmulator(it, host, port) - } - } + // initializeFirestore must be called before any call, including before `getFirestore()` + // To allow settings to be updated, we defer creating the wrapper until the first call to `native` + actual val native: NativeFirebaseFirestore by lazy { + createNative() } + private val js get() = native.js - actual fun collection(collectionPath: String) = rethrow { CollectionReference(NativeCollectionReference(jsCollection(js, collectionPath))) } + actual fun collection(collectionPath: String) = rethrow { NativeCollectionReference(jsCollection(js, collectionPath)) } - actual fun collectionGroup(collectionId: String) = rethrow { Query(jsCollectionGroup(js, collectionId)) } + actual fun collectionGroup(collectionId: String) = rethrow { NativeQuery(jsCollectionGroup(js, collectionId)) } - actual fun document(documentPath: String) = rethrow { DocumentReference(NativeDocumentReference(doc(js, documentPath))) } + actual fun document(documentPath: String) = rethrow { NativeDocumentReference(doc(js, documentPath)) } - actual fun batch() = rethrow { WriteBatch(NativeWriteBatch(writeBatch(js))) } + actual fun batch() = rethrow { NativeWriteBatch(writeBatch(js)) } actual fun setLoggingEnabled(loggingEnabled: Boolean) = rethrow { setLogLevel( if(loggingEnabled) "error" else "silent") } - actual suspend fun runTransaction(func: suspend Transaction.() -> T) = - rethrow { jsRunTransaction(js, { GlobalScope.promise { Transaction(NativeTransaction(it)).func() } } ).await() } + actual suspend fun runTransaction(func: suspend NativeTransaction.() -> T) = + rethrow { jsRunTransaction(js, { GlobalScope.promise { NativeTransaction(it).func() } } ).await() } actual suspend fun clearPersistence() = rethrow { clearIndexedDbPersistence(js).await() } @@ -120,28 +138,36 @@ actual class FirebaseFirestore(app: JsFirebaseApp) { } } -actual data class FirestoreSettings( +val FirebaseFirestore.js: JsFirestore get() = native.js + +actual data class FirebaseFirestoreSettings( actual val sslEnabled: Boolean, actual val host: String, actual val cacheSettings: LocalCacheSettings, ) { - actual companion object {} - - actual interface Builder { - actual var sslEnabled: Boolean - actual var host: String - actual var cacheSettings: LocalCacheSettings - - actual fun build(): FirestoreSettings + 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 + // According to documentation, default JS Firestore cache size is 40MB, not 100MB + internal actual val DEFAULT_CACHE_SIZE_BYTES: Long = 40 * 1024 * 1024 } - internal class BuilderImpl : Builder { - override var sslEnabled: Boolean = true - override var host: String = DEFAULT_HOST - override var cacheSettings: LocalCacheSettings = LocalCacheSettings.Persistent(CACHE_SIZE_UNLIMITED) + actual class Builder internal constructor( + actual var sslEnabled: Boolean, + actual var host: String, + actual var cacheSettings: LocalCacheSettings, + ) { + + actual constructor() : this( + true, + DEFAULT_HOST, + persistentCacheSettings { }, + ) + actual constructor(settings: FirebaseFirestoreSettings) : this(settings.sslEnabled, settings.host, settings.cacheSettings) - override fun build() = FirestoreSettings(sslEnabled, host, cacheSettings) + actual fun build(): FirebaseFirestoreSettings = FirebaseFirestoreSettings(sslEnabled, host, cacheSettings) } @Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") @@ -157,8 +183,8 @@ actual data class FirestoreSettings( ) is LocalCacheSettings.Memory -> { val garbageCollecorSettings = when (val garbageCollectorSettings = cacheSettings.garbaseCollectorSettings) { - is GarbageCollectorSettings.Eager -> memoryEagerGarbageCollector() - is GarbageCollectorSettings.LRUGC -> memoryLruGarbageCollector(json("cacheSizeBytes" to garbageCollectorSettings.sizeBytes)) + is MemoryGarbageCollectorSettings.Eager -> memoryEagerGarbageCollector() + is MemoryGarbageCollectorSettings.LRUGC -> memoryLruGarbageCollector(json("cacheSizeBytes" to garbageCollectorSettings.sizeBytes)) } memoryLocalCache(json("garbageCollector" to garbageCollecorSettings).asDynamic() as MemoryCacheSettings) } @@ -167,9 +193,9 @@ actual data class FirestoreSettings( } actual fun firestoreSettings( - settings: FirestoreSettings?, - builder: FirestoreSettings.Builder.() -> Unit -): FirestoreSettings = FirestoreSettings.BuilderImpl().apply { + settings: FirebaseFirestoreSettings?, + builder: FirebaseFirestoreSettings.Builder.() -> Unit +): FirebaseFirestoreSettings = FirebaseFirestoreSettings.Builder().apply { settings?.let { sslEnabled = it.sslEnabled host = it.host From f6b41cacefaf2e41a60d0c0a36fb4135abbd7e2e Mon Sep 17 00:00:00 2001 From: Gijs van Veen Date: Thu, 18 Apr 2024 08:20:06 +0200 Subject: [PATCH 6/8] Removed IllegalStateException --- .../gitlive/firebase/firestore/firestore.kt | 50 ++++--------------- .../gitlive/firebase/firestore/firestore.kt | 14 ------ 2 files changed, 9 insertions(+), 55 deletions(-) diff --git a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt index 37787082f..ff849f8c7 100644 --- a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt +++ b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt @@ -6,7 +6,6 @@ package dev.gitlive.firebase.firestore import dev.gitlive.firebase.* import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.map import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.Serializable @@ -52,30 +51,13 @@ class FirebaseFirestore internal constructor(private val wrapper: NativeFirebase } } - // Should probably use atomic from atomicfu - private val isConfigured = MutableStateFlow(false) - - fun collection(collectionPath: String): CollectionReference = ensureConfigured { - CollectionReference(wrapper.collection(collectionPath)) - } - fun collectionGroup(collectionId: String): Query = ensureConfigured { - Query(wrapper.collectionGroup(collectionId)) - } - fun document(documentPath: String): DocumentReference = ensureConfigured { - DocumentReference(wrapper.document(documentPath)) - } - fun batch(): WriteBatch = ensureConfigured { - WriteBatch(wrapper.batch()) - } - fun setLoggingEnabled(loggingEnabled: Boolean) = ensureConfigured { - wrapper.setLoggingEnabled(loggingEnabled) - } - suspend fun clearPersistence() = ensureConfiguredSuspended { - wrapper.clearPersistence() - } - suspend fun runTransaction(func: suspend Transaction.() -> T): T = ensureConfiguredSuspended { - wrapper.runTransaction { func(Transaction(this)) } - } + fun collection(collectionPath: String): CollectionReference = CollectionReference(wrapper.collection(collectionPath)) + fun collectionGroup(collectionId: String): Query = Query(wrapper.collectionGroup(collectionId)) + fun document(documentPath: String): DocumentReference = DocumentReference(wrapper.document(documentPath)) + fun batch(): WriteBatch = WriteBatch(wrapper.batch()) + fun setLoggingEnabled(loggingEnabled: Boolean) = wrapper.setLoggingEnabled(loggingEnabled) + suspend fun clearPersistence() = wrapper.clearPersistence() + suspend fun runTransaction(func: suspend Transaction.() -> T): T = wrapper.runTransaction { func(Transaction(this)) } fun useEmulator(host: String, port: Int) = wrapper.useEmulator(host, port) @Deprecated("Use settings instead", replaceWith = ReplaceWith("settings = firestoreSettings{}")) fun setSettings( @@ -100,22 +82,8 @@ class FirebaseFirestore internal constructor(private val wrapper: NativeFirebase } } } - suspend fun disableNetwork() = ensureConfiguredSuspended { - wrapper.disableNetwork() - } - suspend fun enableNetwork() = ensureConfiguredSuspended { - wrapper.enableNetwork() - } - - private fun ensureConfigured(action: () -> T): T { - isConfigured.compareAndSet(expect = false, update = true) - return action() - } - - private suspend fun ensureConfiguredSuspended(action: suspend () -> T): T { - isConfigured.compareAndSet(expect = false, update = true) - return action() - } + suspend fun disableNetwork() = wrapper.disableNetwork() + suspend fun enableNetwork() = wrapper.enableNetwork() } expect class FirebaseFirestoreSettings { diff --git a/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt index 643cea2e4..4928a36cb 100644 --- a/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt +++ b/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/firestore.kt @@ -28,7 +28,6 @@ import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertFailsWith import kotlin.test.assertNotEquals import kotlin.test.assertNotNull import kotlin.test.assertNull @@ -720,19 +719,6 @@ class FirebaseFirestoreTest { assertEquals(updatedData.documentReference.path, updatedSavedData.documentReference.path) } - @Test - fun testUpdateSettings() = runTest { - firestore.settings = firestoreSettings(firestore.settings) { - cacheSettings = persistentCacheSettings { } - } - firestore.enableNetwork() - assertFailsWith { - firestore.settings = firestoreSettings(firestore.settings) { - cacheSettings = memoryCacheSettings { } - } - } - } - @Serializable data class TestDataWithDocumentReference( val uid: String, From a535e95a9b0a00bd06dfd6d82af402ba2eda2900 Mon Sep 17 00:00:00 2001 From: Gijs van Veen Date: Thu, 18 Apr 2024 20:24:23 +0200 Subject: [PATCH 7/8] Fixed missing revert --- .../kotlin/dev/gitlive/firebase/firestore/firestore.kt | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt index ff849f8c7..043e36d08 100644 --- a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt +++ b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt @@ -44,11 +44,7 @@ class FirebaseFirestore internal constructor(private val wrapper: NativeFirebase var settings: FirebaseFirestoreSettings get() = wrapper.settings set(value) { - if (isConfigured.compareAndSet(expect = false, update = false) || value == wrapper.settings) { - wrapper.settings = value - } else { - throw IllegalStateException("FirebaseFirestore has already been started and its settings can no longer be changed. You can only call setFirestoreSettings() before calling any other methods on a FirebaseFirestore object.") - } + wrapper.settings = value } fun collection(collectionPath: String): CollectionReference = CollectionReference(wrapper.collection(collectionPath)) From 5a286536b78b645c79f53f5a4a6da2b3beae4e1d Mon Sep 17 00:00:00 2001 From: Gijs van Veen Date: Fri, 19 Apr 2024 09:51:31 +0200 Subject: [PATCH 8/8] Fixed failing test --- .../dev/gitlive/firebase/firestore/firestore.kt | 2 +- .../dev/gitlive/firebase/firestore/firestore.kt | 3 ++- .../firebase/firestore/FirestoreSourceTest.kt | 8 +++++++- .../dev/gitlive/firebase/firestore/firestore.kt | 2 +- .../dev/gitlive/firebase/firestore/firestore.kt | 12 ++++++++++-- 5 files changed, 21 insertions(+), 6 deletions(-) diff --git a/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt index 35aca8603..eb73173e5 100644 --- a/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt +++ b/firebase-firestore/src/androidMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt @@ -57,7 +57,7 @@ val LocalCacheSettings.android: com.google.firebase.firestore.LocalCacheSettings private val callbackExecutorMap = ConcurrentHashMap() actual typealias NativeFirebaseFirestore = com.google.firebase.firestore.FirebaseFirestore -actual internal class NativeFirebaseFirestoreWrapper actual constructor(actual val native: NativeFirebaseFirestore) { +internal actual class NativeFirebaseFirestoreWrapper actual constructor(actual val native: NativeFirebaseFirestore) { actual var settings: FirebaseFirestoreSettings get() = with(native.firestoreSettings) { diff --git a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt index 840d649b4..6afe7dd7a 100644 --- a/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt +++ b/firebase-firestore/src/commonMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt @@ -40,7 +40,8 @@ class FirebaseFirestore internal constructor(private val wrapper: NativeFirebase constructor(native: NativeFirebaseFirestore) : this(NativeFirebaseFirestoreWrapper(native)) - val native = wrapper.native + // Important to leave this as a get property since on JS it is initialized lazily + val native get() = wrapper.native var settings: FirebaseFirestoreSettings get() = wrapper.settings set(value) { diff --git a/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/FirestoreSourceTest.kt b/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/FirestoreSourceTest.kt index 439d459e7..8d849900b 100644 --- a/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/FirestoreSourceTest.kt +++ b/firebase-firestore/src/commonTest/kotlin/dev/gitlive/firebase/firestore/FirestoreSourceTest.kt @@ -39,7 +39,13 @@ class FirestoreSourceTest { firestore = Firebase.firestore(app).apply { useEmulator(emulatorHost, 8080) - setSettings(persistenceEnabled = persistenceEnabled) + settings = firestoreSettings(settings) { + cacheSettings = if (persistenceEnabled) { + persistentCacheSettings { } + } else { + memoryCacheSettings { } + } + } } } diff --git a/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt index e094e6f51..266f77e35 100644 --- a/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt +++ b/firebase-firestore/src/iosMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt @@ -39,7 +39,7 @@ val LocalCacheSettings.ios: FIRLocalCacheSettingsProtocol get() = when (this) { actual typealias NativeFirebaseFirestore = FIRFirestore @Suppress("UNCHECKED_CAST") -actual internal class NativeFirebaseFirestoreWrapper internal actual constructor(actual val native: NativeFirebaseFirestore) { +internal actual class NativeFirebaseFirestoreWrapper internal actual constructor(actual val native: NativeFirebaseFirestore) { actual var settings: FirebaseFirestoreSettings = firestoreSettings { }.also { native.settings = it.ios diff --git a/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt b/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt index 19982b7e3..4600481aa 100644 --- a/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt +++ b/firebase-firestore/src/jsMain/kotlin/dev/gitlive/firebase/firestore/firestore.kt @@ -78,7 +78,7 @@ actual fun Firebase.firestore(app: FirebaseApp) = actual data class NativeFirebaseFirestore(val js: JsFirestore) -actual internal class NativeFirebaseFirestoreWrapper internal constructor( +internal actual class NativeFirebaseFirestoreWrapper internal constructor( private val createNative: NativeFirebaseFirestoreWrapper.() -> NativeFirebaseFirestore ){ @@ -98,13 +98,21 @@ actual internal class NativeFirebaseFirestoreWrapper internal constructor( private data class EmulatorSettings(val host: String, val port: Int) actual var settings: FirebaseFirestoreSettings = FirebaseFirestoreSettings.Builder().build() + set(value) { + if (lazyNative.isInitialized()) { + throw IllegalStateException("FirebaseFirestore has already been started and its settings can no longer be changed. You can only call setFirestoreSettings() before calling any other methods on a FirebaseFirestore object.") + } else { + field = value + } + } private var emulatorSettings: EmulatorSettings? = null // initializeFirestore must be called before any call, including before `getFirestore()` // To allow settings to be updated, we defer creating the wrapper until the first call to `native` - actual val native: NativeFirebaseFirestore by lazy { + private val lazyNative = lazy { createNative() } + actual val native: NativeFirebaseFirestore by lazyNative private val js get() = native.js actual fun collection(collectionPath: String) = rethrow { NativeCollectionReference(jsCollection(js, collectionPath)) }