Skip to content

Commit 2818ad8

Browse files
committed
fix(http-loader): survive the Network permission prompt on physical install, true off-main fetches, and unified volatile-URL registry identity
For HMR dev sessions with physical devices.
1 parent b292578 commit 2818ad8

5 files changed

Lines changed: 389 additions & 230 deletions

File tree

NativeScript/runtime/DevFlags.mm

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,24 @@ bool IsScriptLoadingLogEnabled() {
1616

1717
// HTTP module loader flags
1818

19-
// Reads `httpModulePrefetch` from app config (default: DISABLED).
19+
// Reads `httpModulePrefetch` from app config (default: ENABLED in debug
20+
// builds, disabled in release — release never uses the HTTP module
21+
// loader anyway).
2022
//
21-
// Apps that want to opt in for testing can set:
23+
// Why default-on for debug: the speculative prefetcher is what gives the
24+
// cold boot K-way fetch parallelism (see HMRSupport.mm). On the iOS
25+
// Simulator the serial fallback is tolerable (loopback fetches are
26+
// sub-millisecond), but on a PHYSICAL device fetching over Wi-Fi the
27+
// serial path multiplies real network round-trips by thousands of boot
28+
// modules — enough to blow past the ~20s launch watchdog, which then
29+
// kills the app before boot completes (works from Xcode only because
30+
// lldb disables the watchdog).
31+
//
32+
// Apps can still force it either way:
2233
//
2334
// // nativescript.config.ts
2435
// export default {
25-
// httpModulePrefetch: true,
36+
// httpModulePrefetch: false, // explicit opt-out (or true to force on)
2637
// } as NativeScriptConfig;
2738
//
2839
// Returning false here short-circuits both the cache lookup and the prefetch
@@ -31,21 +42,26 @@ bool IsHttpModulePrefetchEnabled() {
3142
static std::once_flag s_initFlag;
3243
static bool s_enabled = false;
3344
std::call_once(s_initFlag, []() {
45+
const char* source = "build-default";
3446
@autoreleasepool {
3547
id value = Runtime::GetAppConfigValue("httpModulePrefetch");
3648
if (value && [value respondsToSelector:@selector(boolValue)]) {
3749
s_enabled = [value boolValue];
50+
source = "config";
51+
} else {
52+
s_enabled = RuntimeConfig.IsDebug;
3853
}
3954
}
4055
// Startup banner. Gated on the logScriptLoading flag so it stays silent
4156
// by default — flip the flag in nativescript.config.ts when diagnosing
4257
// why prefetch is or isn't engaging.
4358
//
44-
// [http-loader] prefetch=disabled ← expected default
45-
// [http-loader] prefetch=enabled ← only if config opt-in
59+
// [http-loader] prefetch=enabled source=build-default ← expected debug default
60+
// [http-loader] prefetch=disabled source=build-default ← expected release default
61+
// [http-loader] prefetch=... source=config ← explicit config override
4662
if (IsScriptLoadingLogEnabled()) {
47-
Log(@"[http-loader] prefetch=%s shared-session=on hmr-kickstart=on",
48-
s_enabled ? "enabled" : "disabled");
63+
Log(@"[http-loader] prefetch=%s source=%s shared-session=on hmr-kickstart=on",
64+
s_enabled ? "enabled" : "disabled", source);
4965
}
5066
});
5167
return s_enabled;

NativeScript/runtime/HMRSupport.h

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -168,9 +168,9 @@ void EvictHttpModulePrefetchCacheUrls(const std::vector<std::string>& urls);
168168
// thread until the BFS has fully drained or `timeoutSeconds` elapses.
169169
//
170170
// Designed to be invoked from JS (via `__nsKickstartHmrPrefetch`)
171-
// immediately before the Angular HMR client re-imports the entry —
172-
// by the time V8 walks the dep tree, every reachable body is already
173-
// in `g_prefetchCache` and the walk runs at memory speed instead of
171+
// immediately before the HMR client re-imports the entry — by the
172+
// time V8 walks the dep tree, every reachable body is already in
173+
// `g_prefetchCache` and the walk runs at memory speed instead of
174174
// network speed (turning a ~3s 200-fetch refresh into ~250ms).
175175
//
176176
// Returns `true` when the BFS drained cleanly. On timeout or seed
@@ -190,14 +190,14 @@ bool KickstartHmrPrefetchSync(const std::string& seedUrl,
190190
// variant above, this one fetches ONLY the explicit URL list it was
191191
// given (no body scanning, no BFS recursion).
192192
//
193-
// This is the right shape for HMR: the dev server's
194-
// `collectAngularEvictionUrls` already computed the inverse-dep
195-
// closure of the changed file; re-discovering it via in-process
196-
// scanning would just duplicate that work and re-fetch modules V8
197-
// has already compiled. By feeding the precomputed list directly we
198-
// turn N sequential `LoadHttpModuleForUrl` calls (the importer chain
199-
// during V8's ResolveModuleCallback walk) into a single parallel
200-
// wave that completes before V8 starts walking.
193+
// This is the right shape for HMR: the dev server already computed
194+
// the inverse-dep closure of the changed file (the update payload's
195+
// eviction set); re-discovering it via in-process scanning would just
196+
// duplicate that work and re-fetch modules V8 has already compiled.
197+
// By feeding the precomputed list directly we turn N sequential
198+
// `LoadHttpModuleForUrl` calls (the importer chain during V8's
199+
// ResolveModuleCallback walk) into a single parallel wave that
200+
// completes before V8 starts walking.
201201
//
202202
// Same semantics as `KickstartHmrPrefetchSync` for everything else:
203203
// blocks the calling thread until the wave drains or `timeoutSeconds`
@@ -240,10 +240,10 @@ void InitializeHotEventDispatcher(v8::Isolate* isolate, v8::Local<v8::Context> c
240240

241241
// Drain and execute `import.meta.hot.dispose(cb)` callbacks for the given module
242242
// keys. If `keys` is empty, drains every registered callback across every module
243-
// (the right behaviour for whole-app HMR reboots like Angular's
244-
// `__reboot_ng_modules__`, where the entire JS realm's side effects are being
245-
// thrown away). Each callback is invoked with that module's `hot.data` object so
246-
// users can persist state across the reload (matches Vite spec).
243+
// (the right behaviour for whole-app HMR reboots, where the entire JS realm's
244+
// side effects are being thrown away). Each callback is invoked with that
245+
// module's `hot.data` object so users can persist state across the reload
246+
// (matches Vite spec).
247247
//
248248
// Callbacks are removed from the registry after execution so a second drain in
249249
// the same cycle is a clean no-op. Per-callback failures are logged (when
@@ -254,10 +254,10 @@ void InitializeHotEventDispatcher(v8::Isolate* isolate, v8::Local<v8::Context> c
254254
int RunHotDisposeCallbacks(v8::Isolate* isolate, v8::Local<v8::Context> context,
255255
const std::vector<std::string>& keys);
256256

257-
// Initialize the global `__nsRunHmrDispose([keys?])` function so the HMR client
258-
// (e.g. @nativescript/vite's Angular HMR client) can drain dispose callbacks
259-
// from JS. Mirrors the `InitializeHotEventDispatcher` pattern. Should be called
260-
// once per main isolate during runtime init, gated on dev mode.
257+
// Initialize the global `__nsRunHmrDispose([keys?])` function so HMR clients
258+
// (e.g. @nativescript/vite's) can drain dispose callbacks from JS. Mirrors the
259+
// `InitializeHotEventDispatcher` pattern. Should be called once per main
260+
// isolate during runtime init, gated on dev mode.
261261
//
262262
// JS signature: `__nsRunHmrDispose(keys?: string[]) => number`
263263
// - `keys` omitted / null / undefined / empty array → drain everything.
@@ -276,10 +276,10 @@ int RunHotPruneCallbacks(v8::Isolate* isolate, v8::Local<v8::Context> context,
276276
const std::vector<std::string>& keys);
277277

278278
// Initialize the global `__nsRunHmrPrune([keys?])` function. Symmetric with
279-
// `__nsRunHmrDispose` but for `prune` callbacks. The Angular HMR client does
280-
// NOT call this today (its wholesale `__reboot_ng_modules__` model has no
281-
// per-module prune step), but the runner is plumbed end-to-end so future
282-
// per-module HMR clients have the entry point ready.
279+
// `__nsRunHmrDispose` but for `prune` callbacks. Clients with per-module
280+
// update models call this when modules leave the import graph;
281+
// wholesale-reboot clients have no prune step. Plumbed end-to-end so the
282+
// entry point is ready for either model.
283283
//
284284
// JS signature: `__nsRunHmrPrune(keys?: string[]) => number`
285285
void InitializeHotPruneRunner(v8::Isolate* isolate, v8::Local<v8::Context> context);
@@ -305,8 +305,9 @@ bool IsAnyModuleDeclined(const std::vector<std::string>& keys);
305305

306306
// Initialize the global `__nsHasDeclinedModule([keys?])` function. Returns
307307
// `true` if any of the listed keys is declined (or if the declined set is
308-
// non-empty AND no keys were passed). The Angular HMR client calls this with
309-
// `evictPaths` before reboot; on `true` it falls back to `__nsReloadDevApp()`.
308+
// non-empty AND no keys were passed). HMR clients call this with the update's
309+
// eviction set (the wire payload's `evictPaths`) before applying; on `true`
310+
// they fall back to `__nsReloadDevApp()`.
310311
//
311312
// JS signature: `__nsHasDeclinedModule(keys?: string[]) => boolean`
312313
void InitializeHotDeclinedHelper(v8::Isolate* isolate, v8::Local<v8::Context> context);

0 commit comments

Comments
 (0)