Skip to content

Commit

Permalink
Merge pull request #562 from splendo/feature/552-ios-settings-crash
Browse files Browse the repository at this point in the history
Fixed a crash for settings when iOS would access the same Firebase instance twice
  • Loading branch information
nbransby authored Jul 11, 2024
2 parents 5b0c17b + c987bea commit 101869c
Show file tree
Hide file tree
Showing 8 changed files with 75 additions and 103 deletions.
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
package dev.gitlive.firebase.firestore.internal

import com.google.android.gms.tasks.TaskExecutors
import com.google.firebase.firestore.MemoryCacheSettings
import com.google.firebase.firestore.MemoryEagerGcSettings
import com.google.firebase.firestore.MemoryLruGcSettings
import com.google.firebase.firestore.PersistentCacheSettings
import com.google.firebase.firestore.firestoreSettings
import dev.gitlive.firebase.firestore.FirebaseFirestoreSettings
import dev.gitlive.firebase.firestore.LocalCacheSettings
import dev.gitlive.firebase.firestore.MemoryGarbageCollectorSettings
import dev.gitlive.firebase.firestore.NativeFirebaseFirestore
import dev.gitlive.firebase.firestore.NativeTransaction
import dev.gitlive.firebase.firestore.android
Expand All @@ -17,59 +10,6 @@ import kotlinx.coroutines.tasks.await

internal actual class NativeFirebaseFirestoreWrapper actual constructor(actual val native: NativeFirebaseFirestore) {

actual var settings: FirebaseFirestoreSettings
get() = with(native.firestoreSettings) {
FirebaseFirestoreSettings(
isSslEnabled,
host,
cacheSettings?.let { localCacheSettings ->
when (localCacheSettings) {
is MemoryCacheSettings -> {
val garbageCollectionSettings =
when (val settings = localCacheSettings.garbageCollectorSettings) {
is MemoryEagerGcSettings -> MemoryGarbageCollectorSettings.Eager
is MemoryLruGcSettings -> MemoryGarbageCollectorSettings.LRUGC(
settings.sizeBytes,
)

else -> throw IllegalArgumentException("Existing settings does not have valid GarbageCollectionSettings")
}
LocalCacheSettings.Memory(garbageCollectionSettings)
}

is PersistentCacheSettings -> LocalCacheSettings.Persistent(
localCacheSettings.sizeBytes,
)

else -> throw IllegalArgumentException("Existing settings is not of a valid type")
}
} ?: kotlin.run {
@Suppress("DEPRECATION")
when {
isPersistenceEnabled -> LocalCacheSettings.Persistent(cacheSizeBytes)
cacheSizeBytes == FirebaseFirestoreSettings.CACHE_SIZE_UNLIMITED -> LocalCacheSettings.Memory(
MemoryGarbageCollectorSettings.Eager,
)

else -> LocalCacheSettings.Memory(
MemoryGarbageCollectorSettings.LRUGC(
cacheSizeBytes,
),
)
}
},
callbackExecutorMap[native] ?: TaskExecutors.MAIN_THREAD,
)
}
set(value) {
native.firestoreSettings = firestoreSettings {
isSslEnabled = value.sslEnabled
host = value.host
setLocalCacheSettings(value.cacheSettings.android)
}
callbackExecutorMap[native] = value.callbackExecutor
}

actual fun collection(collectionPath: String) = native.collection(collectionPath)

actual fun collectionGroup(collectionId: String) = native.collectionGroup(collectionId)
Expand All @@ -82,6 +22,15 @@ internal actual class NativeFirebaseFirestoreWrapper actual constructor(actual v
actual fun setLoggingEnabled(loggingEnabled: Boolean) =
com.google.firebase.firestore.FirebaseFirestore.setLoggingEnabled(loggingEnabled)

actual fun applySettings(settings: FirebaseFirestoreSettings) {
native.firestoreSettings = firestoreSettings {
isSslEnabled = settings.sslEnabled
host = settings.host
setLocalCacheSettings(settings.cacheSettings.android)
}
callbackExecutorMap[native] = settings.callbackExecutor
}

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,12 @@ public class FirebaseFirestore internal constructor(private val wrapper: NativeF

// Important to leave this as a get property since on JS it is initialized lazily
internal val native: NativeFirebaseFirestore get() = wrapper.native

public var settings: FirebaseFirestoreSettings
get() = wrapper.settings
@Deprecated("Property can only be written.", level = DeprecationLevel.ERROR)
get() = throw NotImplementedError()
set(value) {
wrapper.settings = value
wrapper.applySettings(value)
}

public fun collection(collectionPath: String): CollectionReference = CollectionReference(wrapper.collection(collectionPath))
Expand All @@ -65,7 +67,7 @@ public class FirebaseFirestore internal constructor(private val wrapper: NativeF
wrapper.useEmulator(host, port)
}

@Deprecated("Use settings instead", replaceWith = ReplaceWith("settings = firestoreSettings{}"))
@Deprecated("Use SettingsBuilder instead", replaceWith = ReplaceWith("settings = firestoreSettings { }", "dev.gitlive.firebase.firestore.firestoreSettings"))
public fun setSettings(
persistenceEnabled: Boolean? = null,
sslEnabled: Boolean? = null,
Expand All @@ -76,18 +78,22 @@ public class FirebaseFirestore internal constructor(private val wrapper: NativeF
this.sslEnabled = sslEnabled ?: true
this.host = host ?: FirebaseFirestoreSettings.DEFAULT_HOST
this.cacheSettings = if (persistenceEnabled != false) {
LocalCacheSettings.Persistent(cacheSizeBytes ?: FirebaseFirestoreSettings.CACHE_SIZE_UNLIMITED)
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)
}
val garbageCollectionSettings =
if (cacheSize == FirebaseFirestoreSettings.CACHE_SIZE_UNLIMITED) {
MemoryGarbageCollectorSettings.Eager
} else {
MemoryGarbageCollectorSettings.LRUGC(cacheSize)
}
LocalCacheSettings.Memory(garbageCollectionSettings)
}
}
}

public suspend fun disableNetwork() {
wrapper.disableNetwork()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import dev.gitlive.firebase.firestore.NativeWriteBatch

internal expect class NativeFirebaseFirestoreWrapper internal constructor(native: NativeFirebaseFirestore) {
val native: NativeFirebaseFirestore
var settings: FirebaseFirestoreSettings

fun collection(collectionPath: String): NativeCollectionReference
fun collectionGroup(collectionId: String): NativeQuery
fun document(documentPath: String): NativeDocumentReference
fun batch(): NativeWriteBatch
fun setLoggingEnabled(loggingEnabled: Boolean)
fun applySettings(settings: FirebaseFirestoreSettings)
suspend fun clearPersistence()
suspend fun <T> runTransaction(func: suspend NativeTransaction.() -> T): T
fun useEmulator(host: String, port: Int)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,14 @@ class FirestoreSourceTest {
)

firestore = Firebase.firestore(app).apply {
useEmulator(emulatorHost, 8080)
settings = firestoreSettings(settings) {
settings = firestoreSettings {
cacheSettings = if (persistenceEnabled) {
persistentCacheSettings { }
} else {
memoryCacheSettings { }
}
}
useEmulator(emulatorHost, 8080)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package dev.gitlive.firebase.firestore

import dev.gitlive.firebase.Firebase
import dev.gitlive.firebase.FirebaseApp
import dev.gitlive.firebase.FirebaseOptions
import dev.gitlive.firebase.apps
import dev.gitlive.firebase.internal.decode
Expand Down Expand Up @@ -85,6 +86,7 @@ class FirebaseFirestoreTest {
)
}

lateinit var firebaseApp: FirebaseApp
lateinit var firestore: FirebaseFirestore

@BeforeTest
Expand All @@ -100,14 +102,15 @@ class FirebaseFirestoreTest {
gcmSenderId = "846484016111",
),
)
firebaseApp = app

firestore = Firebase.firestore(app).apply {
useEmulator(emulatorHost, 8080)
settings = firestoreSettings(settings) {
settings = firestoreSettings {
cacheSettings = memoryCacheSettings {
gcSettings = memoryEagerGcSettings { }
}
}
useEmulator(emulatorHost, 8080)
}
}

Expand Down Expand Up @@ -1031,6 +1034,12 @@ class FirebaseFirestoreTest {
fieldQuery.assertDocuments(FirestoreTest.serializer(), testOne)
}

@Test
fun testMultiple() = runTest {
Firebase.firestore(firebaseApp).disableNetwork()
Firebase.firestore(firebaseApp).enableNetwork()
}

private suspend fun setupFirestoreData(
documentOne: FirestoreTest = testOne,
documentTwo: FirestoreTest = testTwo,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,11 @@ import dev.gitlive.firebase.firestore.NativeFirebaseFirestore
import dev.gitlive.firebase.firestore.NativeTransaction
import dev.gitlive.firebase.firestore.await
import dev.gitlive.firebase.firestore.awaitResult
import dev.gitlive.firebase.firestore.firestoreSettings
import dev.gitlive.firebase.firestore.memoryCacheSettings
import kotlinx.coroutines.runBlocking

@Suppress("UNCHECKED_CAST")
internal actual class NativeFirebaseFirestoreWrapper internal actual constructor(actual val native: NativeFirebaseFirestore) {

actual var settings: FirebaseFirestoreSettings = firestoreSettings { }.also {
native.settings = it.ios
}
set(value) {
field = value
native.settings = value.ios
}

actual fun collection(collectionPath: String) = native.collectionWithPath(collectionPath)

actual fun collectionGroup(collectionId: String) = native.collectionGroupWithID(collectionId)
Expand All @@ -33,6 +23,10 @@ internal actual class NativeFirebaseFirestoreWrapper internal actual constructor
actual fun setLoggingEnabled(loggingEnabled: Boolean): Unit =
FIRFirestore.enableLogging(loggingEnabled)

actual fun applySettings(settings: FirebaseFirestoreSettings) {
native.settings = settings.ios
}

actual suspend fun <T> runTransaction(func: suspend NativeTransaction.() -> T) =
awaitResult<Any?> {
native.runTransactionWithBlock(
Expand All @@ -46,10 +40,8 @@ internal actual class NativeFirebaseFirestoreWrapper internal actual constructor

actual fun useEmulator(host: String, port: Int) {
native.useEmulatorWithHost(host, port.toLong())
settings = firestoreSettings(settings) {
this.host = "$host:$port"
cacheSettings = memoryCacheSettings { }
sslEnabled = false
native.settings = native.settings.apply {
this.sslEnabled = false
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ public actual fun Firebase.firestore(app: FirebaseApp): FirebaseFirestore =

internal actual data class NativeFirebaseFirestore(val js: JsFirestore)

public operator fun FirebaseFirestore.Companion.invoke(js: JsFirestore): FirebaseFirestore = FirebaseFirestore(NativeFirebaseFirestore(js))
public operator fun FirebaseFirestore.Companion.invoke(js: JsFirestore): FirebaseFirestore = FirebaseFirestore(
NativeFirebaseFirestore(js),
)
public val FirebaseFirestore.js: JsFirestore get() = native.js

public actual data class FirebaseFirestoreSettings(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import dev.gitlive.firebase.firestore.NativeWriteBatch
import dev.gitlive.firebase.firestore.externals.clearIndexedDbPersistence
import dev.gitlive.firebase.firestore.externals.connectFirestoreEmulator
import dev.gitlive.firebase.firestore.externals.doc
import dev.gitlive.firebase.firestore.externals.getFirestore
import dev.gitlive.firebase.firestore.externals.initializeFirestore
import dev.gitlive.firebase.firestore.externals.setLogLevel
import dev.gitlive.firebase.firestore.externals.writeBatch
Expand All @@ -21,38 +22,45 @@ import kotlinx.coroutines.await
import kotlinx.coroutines.promise

internal actual class NativeFirebaseFirestoreWrapper internal constructor(
private val createNative: NativeFirebaseFirestoreWrapper.() -> NativeFirebaseFirestore,
private val createNative: NativeFirebaseFirestoreWrapper.(FirebaseFirestoreSettings?) -> NativeFirebaseFirestore,
) {

internal actual constructor(native: NativeFirebaseFirestore) : this({ native })
internal actual constructor(native: NativeFirebaseFirestore) : this(
{ settings ->
settings?.let {
NativeFirebaseFirestore(initializeFirestore(native.js.app, settings))
} ?: native
},
)
internal constructor(app: FirebaseApp) : this(
{
{ settings ->
NativeFirebaseFirestore(
initializeFirestore(app, settings.js).also {
emulatorSettings?.run {
connectFirestoreEmulator(it, host, port)
settings?.let {
initializeFirestore(app, it.js).also {
emulatorSettings?.run {
connectFirestoreEmulator(it, host, port)
}
}
},
} ?: getFirestore(app),
)
},
)

private data class EmulatorSettings(val host: String, val port: Int)

actual var settings: FirebaseFirestoreSettings = FirebaseFirestoreSettings.Builder().build()
private var settings: FirebaseFirestoreSettings? = null
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
createNative(value) // Call initialize again to ensure native crash occurs if settings have changed
}
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`
private val lazyNative = lazy {
createNative()
createNative(settings)
}
actual val native: NativeFirebaseFirestore by lazyNative
private val js get() = native.js
Expand Down Expand Up @@ -89,6 +97,10 @@ internal actual class NativeFirebaseFirestoreWrapper internal constructor(
actual fun setLoggingEnabled(loggingEnabled: Boolean) =
rethrow { setLogLevel(if (loggingEnabled) "error" else "silent") }

actual fun applySettings(settings: FirebaseFirestoreSettings) {
this.settings = settings
}

@OptIn(DelicateCoroutinesApi::class)
actual suspend fun <T> runTransaction(func: suspend NativeTransaction.() -> T) =
rethrow {
Expand All @@ -102,8 +114,10 @@ internal actual class NativeFirebaseFirestoreWrapper internal constructor(
rethrow { clearIndexedDbPersistence(js).await() }

actual fun useEmulator(host: String, port: Int) = rethrow {
settings = firestoreSettings(settings) {
this.host = "$host:$port"
if (settings != null) {
settings = firestoreSettings(settings) {
this.host = "$host:$port"
}
}
emulatorSettings = EmulatorSettings(host, port)
}
Expand Down

0 comments on commit 101869c

Please sign in to comment.