NativeScript / NativeScript/android
ESM loader: remaining cross-platform decisions and deferred follow-ups from #1965
Nessuno ha ancora preso questa issue.
- Lingua principale
- C++
- Stelle
- 563
- Fork
- 144
- Merge medio
- 10h 46m
- PR unite (30g)
- 14
Descrizione
#1965 landed the full loader-overhaul parity with ios#383 plus several hardening rounds. Everything that could be fixed unilaterally was; this issue tracks what deliberately remains: contract questions that need one answer on both platforms, and Android-side follow-ups that were out of the PR's scope. The iOS-side defects the same reviews surfaced are tracked in NativeScript/ios#443 (fixes landed as NativeScript/ios#444).
Cross-platform decisions (one answer, both runtimes)
- require(esm) facade evaluates under sync-strict (shared bug):
createPumpingRequire()of an already-settled TLA module with a default export throws the async-graph refusal namingns:require-facade:<path>—IsGraphAsync()is transitive and the facade requests the TLA target. Violates the contract twice (pumping lifts the TLA refusal; exports-cascade step 3) and the error recommends the API that just failed. Fix shape to agree on: directEvaluate()+ assert-fulfilled for the facade, or a synthetic-module bypass on the evaluation options. - Import-map keys carrying queries: Android strips query/fragment before the map consult (query-bearing keys can never match); iOS matches them for static imports. The HTML import-maps spec matches the specifier as written. Pick a side and align.
-
import.meta.urlshape: Android exposes the full device path; iOS base-strips tofile:///app/.... Portable code matchingfile:///app/breaks on Android today. Couples withfile://specifier candidate handling and iOS'sdirnameinconsistency. - Transport identity: Android sends
User-Agent: NativeScript-HTTP-ESMand inherits the processCookieHandler; iOS sends no UA and disables cookies. Cookies especially need a joint call. - Transport timeouts: Android 15s connect/15s read vs iOS 5s/10s. The docs now state the per-attempt/per-read semantics honestly, but the values should converge (a stalled edge can outlive the graph deadline).
-
HasPendingAsyncModuleGraphWorkis process-wide on both platforms: a worker's graph load holds the main boot backstop open. Make it per-isolate. - Instantiate-failure registry semantics: iOS evicts the root, Android leaves-and-self-heals; HTML/Node module-map semantics say keep-and-rethrow. Neither does that — pick one.
- Import attributes are silently ignored (
import x from './d.json' with { type: 'json' }loads as JS on both); Node throws. Document or implement, jointly. - Import-map validation error-text shape: parse-detail suffix conditionality and multi-offense selection differ slightly between the platforms' JSON parsers. Normalize or document.
-
createRequire's hand-rolledfile:parser disagrees withnode:url(%2Fhonored,file://FILE:rejected vs Node); shared. Rebase it on theURLprimordial (the comment claiming that's unavailable is wrong on both sides). - JSON modules are built textually (
"export default " + jsonText), so a"__proto__"key becomes a prototype setter instead of an own property. Shared deviation from JSON-module semantics. -
require-factoryhooks:__requireOverrideis Android-only with no producers in-tree;__pauseOnNextRequireis iOS-only (inspector-produced). Converge or delete. - The 1s local-entry yield: normative in both docs, but its original purpose was iOS's pre-
main()runway. Decide whether to shrink/remove (doc change on both). - Waiter-invariant race fixture: the "already-evaluating with no continuation attached" dynamic-import window is documented but unenforced on both platforms — needs a spec that pins it.
- Pump mechanism shape on iOS (surfaced by porting android#1965's timer specs to iOS): a pumping require started from inside iOS's shared JS-timer
CFRunLoopTimercallback can never observe another timer fire — CFRunLoopTimer does not re-enter. Android's pump drains the ordered lane directly and is immune; whether iOS should adopt the same drain shape instead of spinning the runloop is a joint pump-contract call. - Frozen-at-mint pumping options: neither suite pins that the options object is validated once and immune to later mutation. Add the spec on both.
Android follow-ups
Reclassified 2026-08-20: the five checked items immediately below turned out to be features present in the merged ios#383 code that the port had missed — implemented in #2021.
-
ESM code cache — landed in #2021:
CompileFileEsModuleconsumes/produces cache blobs (.mcachekeying avoids colliding with the classic.cachefor the same file). -
Fetch concurrency cap — landed in #2021: 16 concurrent fetches process-wide (iOS:
HTTPMaximumConnectionsPerHost = 16), queued jobs drained by finishing threads, spawn failures delivered as transport errors. -
Canonical-key transport signatures — landed in #2021: the transport takes keys from callers (iOS's shape); also fixed collapsed-scheme cache-bust marks never matching.
-
HTTP worker entries — landed in #2021:
new Worker("http://…")works end-to-end (security gate, vocabulary copy, settle gate, inspector URL), spec included. -
ns:modulebinding beside the loader state — landed in #2021 (ios5ca0c43c), including theParseImportMap/InstallParsedImportMapsplit that removes the double parse. -
Lock the process-global interop caches — done in 7c9c3db3, though not as scoped here. The three caches this item names were already covered by #2013:
Consoletimers andArgConverter's type-long cache became per-runtimeRuntimeState::For<T>(isolate)(theIsolate*-keyed process-global maps are gone entirely),MetadataNode's name/tree-node caches gots_nodeCacheMutex,MetadataReadergot its ownStateMutex, andJEnv::s_classCache/s_missingClassesplusMethodCache::s_mthod_ctor_signature_cacheeach got ashared_mutex— all on the lock-the-find-and-the-emplace-but-never-across-the-build discipline this item asks for. Auditing the runtime for what genuinely remained in that class turned up four more, all fixed in 7c9c3db3:File::Buffer— one process-wide 1MB scratch buffer thatFile::ReadTextfilled and returned a pointer into. Main and worker runtimes read modules concurrently from their own threads, so one thread'sfreadoverwrote bytes another was still copying out: silent module-source corruption rather than a crash, which is why this one never appeared in a tombstone. It also never saved the allocation it appears to save — every caller goes through thestd::stringoverload, which copied out of it on the next line — so reads now go straight into the returned string and the borrowing overload (no callers) is deleted.JType::EnsureInstance— published*instancebeforeInithad filledclazz/ctor/valueMethodId, so a second thread could find the pointer non-null and call through uninitialised JNI ids, on the hot boxing path. Nowstd::call_onceperType.CallbackHandlers::Init— runs once per runtime fromPrepareV8Runtime, so every worker start rewrote the process-global class and method-id statics (and re-ranMethodCache::Init) while the isolates already running were reading them. Process-global half now undercall_once;MetadataNode::Init(isolate)stays per-isolate.MetadataNode::IsJavascriptKeyword— filled a function-localstatic set<string>behind anempty()check, racing reads from every runtime's thread.
Audited and deliberately left alone:
JsV8InspectorClient::Domainsis unguarded but main-isolate only (workers useWorkerInspectorClient, which never registers domain dispatchers);SimpleProfiler::s_framesis racy and stores a pointer into a reallocating vector, butSimpleProfiler::Inithas no callers (dead code);PageResource::s_cachedPageResourcesis write-only, nothing reads it. -
Runtime::ReadFileText'sm_fileWriteMutexis broken three ways (surfaced while removingFile::Buffer, which this mutex superficially looked like it guarded — it never did). Its real purpose is LiveSync:NativeScriptSyncServiceSocketImpl.createOrOverrideFile()takesruntime.lock()so the runtime does not compile a half-written hot-synced module. But (1)m_fileWriteMutexis a non-static member (Runtime.h), so LiveSync locking one runtime excludes nothing on a worker's thread — precisely the case that matters; (2) the lock is taken afterprepareFile()deletes the file andnew FileOutputStreamtruncates it, so it covers onlyfos.writeand not the window a reader actually hits; (3) thefinallycallsruntime.unlock()even whenlock()was never reached (a throw frommkdirs()/new FileOutputStream), which is UB and on bionic can release a lock the JS thread holds insideReadFileText. Fix direction: make the write atomic — temp file plus same-directoryATOMIC_MOVE— instead of locking readers.rename(2)within a directory is atomic, so a reader gets the whole old or the whole new file; that also closes the delete/truncate window no reader-side lock can reach, and drops a mutex from every debug module read.m_fileWriteMutexandRuntime::Lock/Unlockthen delete cleanly, keepingcom.tns.Runtime.lock()/unlock()as deprecated no-ops since they arepublicand external tooling may call them. Implemented in #2022 — unmerged: LiveSync has no automated coverage in the runtime test app, so it needs a manual hot-sync check (ns run android, edit a file, confirm reload; also a newly-created file and a deleted one, sinceprepareFile()is gone) before merging. -
NS_DEBUGis unreachable on a real device (env var); the docs call it "the only way to trace boot". Adebug.nativescript.*system property (pattern already used inRuntime::GetAndroidVersion) would make it real. -
Remaining K14 leftovers: three resolver/import empty-return-without-exception windows in teardown/OOM paths;
catch (std::exception e)by value inModuleInternal.cpp;IsolateData::RUNTIMEis never cleared (theTryGetRuntimeguards are defense-in-depth only and one teardown comment overstates the invariant). -
NS_TIMERS_NESTING_CLAMPis documented opt-in and defined by no build file — confirm that's intended (the HTML 4ms nesting clamp is currently off everywhere, including the test suite). -
Verify
@nativescript/core'ssetTimeoutrides__ns__setTimeout: the pumping-require timer story assumes it (the test app's own polyfill is a JavaHandlerpost that no pump can dispatch, by construction). Confirm against core before advertising the behavior. -
Dev-server smoke test against
@nativescript/vite(the same gate as iOS): validates the canonicalization change, the vocabulary purge, the MIME gate, and the ESM-entry routing — and, if the sync-fetch anomaly guard stays cold, authorizes deleting it. -
Accepted-and-documented, revisit if it bites: the boot backstop blocks the launching thread (up to 120s, dev-time HTTP entries only) by explicit decision —
runtime.run()returning entry-settled is load-bearing forNativeScriptActivity;require.resolve/require.cache/require.mainremain absent (feature-detectable).
Guida per i contributori
Apri la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Direzione di ricerca
Inizia separando le decisioni relative al contratto multipiattaforma dalle attività successive per Android, quindi leggi Runtime.h, Runtime::ReadFileText e ModuleInternal.cpp per il lavoro nativo esplicitamente indicato. Usa l’app di test del runtime Android e il controllo ns run android LiveSync elencato come punti di ingresso per la validazione; ogni elemento selezionato è completato solo quando il comportamento indicato è implementato o documentato e i controlli della piattaforma pertinenti hanno esito positivo.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Valutazione
- Stack tecnologico
- android, cpp
- Ambito
- mobile-dev
- Tipo di issue
- Refactoring
- Difficoltà
- 5/5
- Tempo stimato
- Più di una settimana
- Stato di attività
- Tranquilla
- Chiarezza
- Da chiarire
- Idoneità per principianti
- 20/100