Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,20 @@ import com.facebook.react.runtime.ReactHostImpl
import com.facebook.react.runtime.hermes.HermesInstance
import com.facebook.react.shell.MainReactPackage
import com.facebook.react.uimanager.ViewManager
import java.io.File
import java.net.HttpURLConnection
import java.net.URL

class SandboxReactNativeDelegate(
private val context: Context,
) {
companion object {
private const val TAG = "SandboxRNDelegate"

private const val REMOTE_BUNDLE_CONNECT_TIMEOUT_MS = 10_000
private const val REMOTE_BUNDLE_READ_TIMEOUT_MS = 15_000
private const val REMOTE_BUNDLE_DOWNLOAD_TIMEOUT_MS = 20_000L

private val sharedHosts = mutableMapOf<String, SharedReactHost>()
private val registeredSubstitutionPackages = mutableListOf<ReactPackage>()
private val registeredHostPackages = mutableListOf<ReactPackage>()
Expand Down Expand Up @@ -166,13 +173,21 @@ class SandboxReactNativeDelegate(
val componentFactory = ComponentFactory()
DefaultComponentsRegistry.register(componentFactory)

// For a remote (http/https) bundle, disable developer support on this
// ReactHost. With dev support enabled the runtime ignores jsBundleLoader
// and fetches the bundle from the Metro dev server using jsMainModulePath,
// turning the URL into http://localhost:8081/<url>.bundle (a 404). Local
// sources ("index"/asset names) keep dev support so Fast Refresh works.
val isRemoteBundle =
capturedBundleSource.startsWith("http://") ||
capturedBundleSource.startsWith("https://")
host =
ReactHostImpl(
sandboxContext,
hostDelegate,
componentFactory,
true,
true,
!isRemoteBundle,
!isRemoteBundle,
)

ownsReactHost = true
Expand Down Expand Up @@ -259,7 +274,12 @@ class SandboxReactNativeDelegate(
if (bundleSource.isEmpty()) return null
return when {
bundleSource.startsWith("http://") || bundleSource.startsWith("https://") -> {
JSBundleLoader.createFileLoader(bundleSource)
// createFileLoader(url) treats its argument as a local path and never
// downloads. Prefetch to a cache file on a worker thread instead.
val cacheFile = downloadRemoteBundle(bundleSource) ?: return null
// Load synchronously: on RN 0.85 bridgeless an async load executes the
// bundle before TurboModule JSI bindings are installed, causing a fatal.
JSBundleLoader.createFileLoader(cacheFile.absolutePath, bundleSource, true)
}

else -> {
Expand All @@ -268,6 +288,41 @@ class SandboxReactNativeDelegate(
}
}

private fun downloadRemoteBundle(bundleSource: String): File? {
val cacheFile = File(context.cacheDir, "sandbox-remote-${bundleSource.hashCode()}.bundle")
if (cacheFile.exists() && cacheFile.length() > 0L) {
Log.d(TAG, "Reusing cached bundle '$bundleSource' (${cacheFile.length()} bytes)")
return cacheFile
}
var downloadError: Exception? = null
val worker =
Thread {
try {
val connection = URL(bundleSource).openConnection() as HttpURLConnection
connection.connectTimeout = REMOTE_BUNDLE_CONNECT_TIMEOUT_MS
connection.readTimeout = REMOTE_BUNDLE_READ_TIMEOUT_MS
try {
connection.inputStream.use { input ->
cacheFile.outputStream().use { output -> input.copyTo(output) }
}
} finally {
connection.disconnect()
}
} catch (e: Exception) {
downloadError = e
}
}
worker.start()
worker.join(REMOTE_BUNDLE_DOWNLOAD_TIMEOUT_MS)
if (downloadError != null || !cacheFile.exists() || cacheFile.length() == 0L) {
cacheFile.delete()
Log.e(TAG, "Failed to download remote bundle '$bundleSource'", downloadError)
return null
}
Log.d(TAG, "Downloaded remote bundle '$bundleSource' (${cacheFile.length()} bytes)")
return cacheFile
}

fun onJSIBindingsInstalled(stateHandle: Long) {
jsiStateHandle = stateHandle
}
Expand Down
Loading