Skip to content

Commit f821c25

Browse files
committed
feat(runtime): __NS_DEV__.seedModuleBodies for batch prewarm seeding from the boot archive
The JS bootstrap downloads the dev server's /__ns_dev__/boot-archive (NDJSON of {url, body} entries) and seeds them directly into the one-shot prewarm cache consumed by HttpFetchText during V8's synchronous module walk — replacing hundreds of serial kickstart fetches with one payload. Mechanism only: the server computes the closure and bodies; the runtime stores them behind the same gates as kickstart prefetch. Returns { ok, seeded, bytes } so callers can fall back to kickstartPrefetch. [skip ci]
1 parent d49581c commit f821c25

3 files changed

Lines changed: 111 additions & 5 deletions

File tree

test-app/app/src/main/assets/app/tests/testNsDevBoundary.mjs

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
//
33
// Pins the mechanism-only native contract: the runtime exposes exactly the
44
// `__NS_DEV__` namespace (configureRuntime, invalidateModules,
5-
// kickstartPrefetch, getLoadedModuleUrls, setDevBootComplete,
6-
// terminateAllWorkers, and the debug-only canonicalizeHttpUrlKey), and
7-
// nothing else. HMR *policy* — `import.meta.hot`, hot-data/accept/dispose
8-
// registries, dev-session state, boot orchestration — lives in the JS dev
9-
// client (@nativescript/vite), not in the runtime.
5+
// kickstartPrefetch, seedModuleBodies, getLoadedModuleUrls,
6+
// setDevBootComplete, terminateAllWorkers, and the debug-only
7+
// canonicalizeHttpUrlKey), and nothing else. HMR *policy* —
8+
// `import.meta.hot`, hot-data/accept/dispose registries, dev-session state,
9+
// boot orchestration — lives in the JS dev client (@nativescript/vite), not
10+
// in the runtime.
1011

1112
describe("__NS_DEV__ dev-loader boundary", function () {
1213
it("exposes the __NS_DEV__ namespace with the core primitives", function () {
@@ -15,13 +16,31 @@ describe("__NS_DEV__ dev-loader boundary", function () {
1516
expect(typeof dev.configureRuntime).toBe("function");
1617
expect(typeof dev.invalidateModules).toBe("function");
1718
expect(typeof dev.kickstartPrefetch).toBe("function");
19+
expect(typeof dev.seedModuleBodies).toBe("function");
1820
expect(typeof dev.getLoadedModuleUrls).toBe("function");
1921
expect(typeof dev.setDevBootComplete).toBe("function");
2022
// Main isolate: worker termination is installed here (and ONLY here —
2123
// worker isolates must not receive it).
2224
expect(typeof dev.terminateAllWorkers).toBe("function");
2325
});
2426

27+
it("seedModuleBodies rejects invalid input without seeding", function () {
28+
const dev = globalThis.__NS_DEV__;
29+
const noArg = dev.seedModuleBodies();
30+
expect(noArg.ok).toBe(false);
31+
expect(noArg.seeded).toBe(0);
32+
const badEntries = dev.seedModuleBodies([
33+
null,
34+
{ body: "export {};" }, // no url
35+
{ url: "not-a-url", body: "export {};" }, // non-http scheme
36+
{ url: "http://127.0.0.1:5173/ns/m/src/app.css", body: "body{}" }, // non-JS shape
37+
{ url: "http://127.0.0.1:5173/ns/m/src/main", body: "" }, // empty body
38+
]);
39+
expect(badEntries.ok).toBe(false);
40+
expect(badEntries.seeded).toBe(0);
41+
expect(badEntries.bytes).toBe(0);
42+
});
43+
2544
it("keeps the dev surface confined to __NS_DEV__ (no flat __ns* globals)", function () {
2645
// The contract is the single namespace object: no dev primitive is
2746
// reachable as a flat global, so tooling can feature-detect exactly

test-app/runtime/src/main/cpp/HMRSupport.cpp

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1402,6 +1402,91 @@ void KickstartHmrPrefetchCallback(const v8::FunctionCallbackInfo<v8::Value>& inf
14021402
buildResult(ok, fetched, elapsedMs);
14031403
}
14041404

1405+
// `__NS_DEV__.seedModuleBodies(entries)` — batch prewarm-cache seeding.
1406+
//
1407+
// The JS bootstrap downloads `/__ns_dev__/boot-archive` (NDJSON of
1408+
// {url, body} lines) and hands the parsed entries here. Each entry lands in
1409+
// the one-shot prewarm cache (`g_prefetchCache`, consumed by `HttpFetchText`
1410+
// during V8's synchronous module walk), behind the same gates as a kickstart
1411+
// fetch. Mechanism only: the dev server computed the closure and produced
1412+
// the bodies; the runtime just stores them.
1413+
//
1414+
// Accepts Array<{ url, body }>. Returns { ok, seeded, bytes }; callers fall
1415+
// back to `kickstartPrefetch(urls)` when nothing was seeded.
1416+
void SeedModuleBodiesCallback(const v8::FunctionCallbackInfo<v8::Value>& info) {
1417+
v8::Isolate* isolate = info.GetIsolate();
1418+
v8::HandleScope scope(isolate);
1419+
v8::Local<v8::Context> ctx = isolate->GetCurrentContext();
1420+
1421+
auto buildResult = [&](bool ok, size_t seeded, size_t bytes) {
1422+
v8::Local<v8::Object> result = v8::Object::New(isolate);
1423+
(void)result->Set(ctx, ToV8String(isolate, "ok"), v8::Boolean::New(isolate, ok));
1424+
(void)result->Set(ctx, ToV8String(isolate, "seeded"),
1425+
v8::Integer::NewFromUnsigned(isolate, (uint32_t)seeded));
1426+
(void)result->Set(ctx, ToV8String(isolate, "bytes"),
1427+
v8::Number::New(isolate, (double)bytes));
1428+
info.GetReturnValue().Set(result);
1429+
};
1430+
1431+
if (info.Length() < 1 || !info[0]->IsArray()) {
1432+
if (tns::IsScriptLoadingLogEnabled()) {
1433+
DEBUG_WRITE("[__NS_DEV__.seedModuleBodies] expected Array<{url, body}>");
1434+
}
1435+
buildResult(false, 0, 0);
1436+
return;
1437+
}
1438+
1439+
v8::Local<v8::Array> arr = info[0].As<v8::Array>();
1440+
const uint32_t len = arr->Length();
1441+
v8::Local<v8::String> urlKey = ToV8String(isolate, "url");
1442+
v8::Local<v8::String> bodyKey = ToV8String(isolate, "body");
1443+
1444+
size_t seeded = 0;
1445+
size_t bytes = 0;
1446+
for (uint32_t i = 0; i < len; i++) {
1447+
v8::Local<v8::Value> elemVal;
1448+
if (!arr->Get(ctx, i).ToLocal(&elemVal) || !elemVal->IsObject()) continue;
1449+
v8::Local<v8::Object> elem = elemVal.As<v8::Object>();
1450+
1451+
v8::Local<v8::Value> urlVal;
1452+
if (!elem->Get(ctx, urlKey).ToLocal(&urlVal) || !urlVal->IsString()) continue;
1453+
v8::String::Utf8Value urlU8(isolate, urlVal);
1454+
if (!*urlU8) continue;
1455+
std::string url(*urlU8);
1456+
if (url.empty()) continue;
1457+
1458+
// Same gates a kickstart fetch passes before it may populate the
1459+
// prewarm cache (scheme, JS-source shape, remote-URL allowlist).
1460+
if (!StartsWith(url, "http://") && !StartsWith(url, "https://")) continue;
1461+
if (!LooksLikeJsSourceUrl(url)) continue;
1462+
if (!IsRemoteUrlAllowed(url)) continue;
1463+
1464+
v8::Local<v8::Value> bodyVal;
1465+
if (!elem->Get(ctx, bodyKey).ToLocal(&bodyVal) || !bodyVal->IsString()) continue;
1466+
v8::String::Utf8Value bodyU8(isolate, bodyVal);
1467+
if (!*bodyU8) continue;
1468+
std::string body(*bodyU8);
1469+
if (body.empty()) continue;
1470+
1471+
const size_t bodySize = body.size();
1472+
// Overwrite unconditionally — the archive body is the authoritative
1473+
// fresh copy, mirroring the kickstart's overwrite semantics.
1474+
{
1475+
std::lock_guard<std::mutex> lock(g_prefetchMutex);
1476+
g_prefetchCache[url] = std::move(body);
1477+
}
1478+
++seeded;
1479+
bytes += bodySize;
1480+
}
1481+
1482+
if (tns::IsScriptLoadingLogEnabled()) {
1483+
DEBUG_WRITE("[__NS_DEV__.seedModuleBodies] seeded=%lu bytes=%lu of %u entries",
1484+
(unsigned long)seeded, (unsigned long)bytes, len);
1485+
}
1486+
1487+
buildResult(seeded > 0, seeded, bytes);
1488+
}
1489+
14051490
void GetLoadedModuleUrlsCallback(const v8::FunctionCallbackInfo<v8::Value>& info) {
14061491
v8::Isolate* isolate = info.GetIsolate();
14071492
v8::HandleScope scope(isolate);
@@ -1468,6 +1553,7 @@ void InitializeHmrDevGlobals(v8::Isolate* isolate, v8::Local<v8::Context> contex
14681553
InstallDevFunction(isolate, context, dev, "configureRuntime", ConfigureDevRuntimeCallback);
14691554
InstallDevFunction(isolate, context, dev, "invalidateModules", InvalidateModulesCallback);
14701555
InstallDevFunction(isolate, context, dev, "kickstartPrefetch", KickstartHmrPrefetchCallback);
1556+
InstallDevFunction(isolate, context, dev, "seedModuleBodies", SeedModuleBodiesCallback);
14711557
InstallDevFunction(isolate, context, dev, "getLoadedModuleUrls", GetLoadedModuleUrlsCallback);
14721558
InstallDevFunction(isolate, context, dev, "setDevBootComplete", SetDevBootCompleteCallback);
14731559

test-app/runtime/src/main/cpp/HMRSupport.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ void MirrorGlobalOnGlobalThis(v8::Isolate* isolate, v8::Local<v8::Context> conte
179179
// - configureRuntime(config) (import map + volatile patterns)
180180
// - invalidateModules(urls) (registry + cache eviction)
181181
// - kickstartPrefetch(urls, opts?) (parallel HTTP prewarm, list mode)
182+
// - seedModuleBodies(entries) (batch prewarm seeding from the boot archive)
182183
// - getLoadedModuleUrls() (registry introspection)
183184
// - setDevBootComplete(value?) (boot-complete signal)
184185
// - terminateAllWorkers() (main isolate only; see CallbackHandlers.h)

0 commit comments

Comments
 (0)