@@ -27,13 +27,20 @@ import com.facebook.react.runtime.ReactHostImpl
2727import com.facebook.react.runtime.hermes.HermesInstance
2828import com.facebook.react.shell.MainReactPackage
2929import com.facebook.react.uimanager.ViewManager
30+ import java.io.File
31+ import java.net.HttpURLConnection
32+ import java.net.URL
3033
3134class SandboxReactNativeDelegate (
3235 private val context : Context ,
3336) {
3437 companion object {
3538 private const val TAG = " SandboxRNDelegate"
3639
40+ private const val REMOTE_BUNDLE_CONNECT_TIMEOUT_MS = 10_000
41+ private const val REMOTE_BUNDLE_READ_TIMEOUT_MS = 15_000
42+ private const val REMOTE_BUNDLE_DOWNLOAD_TIMEOUT_MS = 20_000L
43+
3744 private val sharedHosts = mutableMapOf<String , SharedReactHost >()
3845 private val registeredSubstitutionPackages = mutableListOf<ReactPackage >()
3946 private val registeredHostPackages = mutableListOf<ReactPackage >()
@@ -200,13 +207,21 @@ class SandboxReactNativeDelegate(
200207 val componentFactory = ComponentFactory ()
201208 DefaultComponentsRegistry .register(componentFactory)
202209
210+ // For a remote (http/https) bundle, disable developer support on this
211+ // ReactHost. With dev support enabled the runtime ignores jsBundleLoader
212+ // and fetches the bundle from the Metro dev server using jsMainModulePath,
213+ // turning the URL into http://localhost:8081/<url>.bundle (a 404). Local
214+ // sources ("index"/asset names) keep dev support so Fast Refresh works.
215+ val isRemoteBundle =
216+ capturedBundleSource.startsWith(" http://" ) ||
217+ capturedBundleSource.startsWith(" https://" )
203218 host =
204219 ReactHostImpl (
205220 sandboxContext,
206221 hostDelegate,
207222 componentFactory,
208- true ,
209- true ,
223+ ! isRemoteBundle ,
224+ ! isRemoteBundle ,
210225 )
211226
212227 ownsReactHost = true
@@ -293,7 +308,12 @@ class SandboxReactNativeDelegate(
293308 if (bundleSource.isEmpty()) return null
294309 return when {
295310 bundleSource.startsWith(" http://" ) || bundleSource.startsWith(" https://" ) -> {
296- JSBundleLoader .createFileLoader(bundleSource)
311+ // createFileLoader(url) treats its argument as a local path and never
312+ // downloads. Prefetch to a cache file on a worker thread instead.
313+ val cacheFile = downloadRemoteBundle(bundleSource) ? : return null
314+ // Load synchronously: on RN 0.85 bridgeless an async load executes the
315+ // bundle before TurboModule JSI bindings are installed, causing a fatal.
316+ JSBundleLoader .createFileLoader(cacheFile.absolutePath, bundleSource, true )
297317 }
298318
299319 else -> {
@@ -302,6 +322,41 @@ class SandboxReactNativeDelegate(
302322 }
303323 }
304324
325+ private fun downloadRemoteBundle (bundleSource : String ): File ? {
326+ val cacheFile = File (context.cacheDir, " sandbox-remote-${bundleSource.hashCode()} .bundle" )
327+ if (cacheFile.exists() && cacheFile.length() > 0L ) {
328+ Log .d(TAG , " Reusing cached bundle '$bundleSource ' (${cacheFile.length()} bytes)" )
329+ return cacheFile
330+ }
331+ var downloadError: Exception ? = null
332+ val worker =
333+ Thread {
334+ try {
335+ val connection = URL (bundleSource).openConnection() as HttpURLConnection
336+ connection.connectTimeout = REMOTE_BUNDLE_CONNECT_TIMEOUT_MS
337+ connection.readTimeout = REMOTE_BUNDLE_READ_TIMEOUT_MS
338+ try {
339+ connection.inputStream.use { input ->
340+ cacheFile.outputStream().use { output -> input.copyTo(output) }
341+ }
342+ } finally {
343+ connection.disconnect()
344+ }
345+ } catch (e: Exception ) {
346+ downloadError = e
347+ }
348+ }
349+ worker.start()
350+ worker.join(REMOTE_BUNDLE_DOWNLOAD_TIMEOUT_MS )
351+ if (downloadError != null || ! cacheFile.exists() || cacheFile.length() == 0L ) {
352+ cacheFile.delete()
353+ Log .e(TAG , " Failed to download remote bundle '$bundleSource '" , downloadError)
354+ return null
355+ }
356+ Log .d(TAG , " Downloaded remote bundle '$bundleSource ' (${cacheFile.length()} bytes)" )
357+ return cacheFile
358+ }
359+
305360 fun onJSIBindingsInstalled (stateHandle : Long ) {
306361 jsiStateHandle = stateHandle
307362 // Store on the shared host so views that reuse this host can
0 commit comments