| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Sorry, something went wrong.
Moves the embedded runtime from nodejs-mobile v18.20.4 to the digidem fork's v24.19.0-0. The fork versions releases `<node>-<mobile-rev>` and renamed its assets, so the download script changes shape as well as version. Two things the upgrade forces rather than merely allows: `--no-experimental-fetch` is gone from iOS argv — Node removed the flag in 23, and passing an unknown flag aborts before any JS runs. The iOS build now serves WebAssembly through a polyfill inside nodejs-mobile itself and has a working `fetch`, so the whole iOS-only shim stack goes with it: `index.ios.js`, the polywasm/undici installers, the SIMD-wasm alias and the loader-entry redirect. Both platforms now bundle from one entry, differing only in the `__loadAddon` banner. better-sqlite3 11 doesn't compile against V8 13.6 (Node 24 dropped the `ObjectTemplate::SetAccessor` overload it uses), and as a raw V8 addon it needs an ABI-matched prebuild per Node version, so the tree is pinned to one 12.10.0 through `overrides` and consumes the new ABI 137 prebuilds. Also picked up along the way: - TMPDIR now points at a real directory. An Android app process has none and there is no `/tmp`, which is where `os.tmpdir()` otherwise lands; reading it needed the credentials fix that arrived with this release. - V8's on-disk code cache is enabled via `NODE_COMPILE_CACHE`. Env var rather than `module.enableCompileCache()` so it covers `loader.mjs` and the Sentry chunk, which compile before any of our JS could call the runtime API. The backend flushes it at `ready` instead of leaving it to node's exit hook — the low-memory killer and iOS's suspended-app kill both skip that hook, so the cache would rarely be written at all. - The 24.x NodeMobile.xcframework has no x86_64 simulator slice, so the x64-simulator prebuild leg and its `lipo` pass are gone. - `readNodeJsMobileVersions()` matched `NODE_MODULE_VERSION (.+)`, which on the Node 24 header hits the `NODE_EMBEDDER_MODULE_VERSION` passthrough first and yields a garbage ABI in prebuild URLs. - Node 24's `v8config.h` #errors below C++20; the NDK defaults to gnu++17. - Sentry events carry the mobile revision as a `nodejs_mobile` tag; `contexts.runtime` only has the upstream Node version.
`lite` drops ICU, the inspector, `node:sqlite` and TypeScript type-stripping, and on iOS also V8's compiled tiers — dead weight there, since it runs jitless and serves WebAssembly through the bundled polyfill. None of it is a loss. The v18 build we came from reports 0 ICU symbols and no `icudt` data, i.e. it was already `--with-intl=none`, so `full` would *add* an `Intl` this backend has never had; the two `Intl` references that survive bundling are a Sentry helper that returns early unless `process.versions.icu` is set, and mapbox style-spec expression evaluators whose exposure is unchanged from v18. We use the `better-sqlite3` addon rather than `node:sqlite`, ship plain JS, and every `node:inspector` reference in the bundle is an `await import()` inside a try/catch or an opt-in Sentry integration. Addon prebuilds are flavour-neutral: both flavours ship byte-identical headers and export the same V8 symbol set, and every symbol our better-sqlite3 prebuild imports resolves against the lite `libnode`. Sizes against the v18 build this replaces, rather than against full: android arm64 61 -> 65 MB android armv7a 57 -> 59 MB android x86_64 63 -> 64 MB ios device slice 53 -> 44 MB
|
All alerts resolved. Learn more about Socket for GitHub. This PR previously contained dependency changes with security issues that have been resolved, removed, or ignored. |
Sorry, something went wrong.
12.10.0 built against Node 24 crashes on real hardware. The backend reaches ready and aborts ~86ms later, on every boot: # node[2591]: node::RemoveEnvironmentCleanupHook(...) at ../src/api/hooks.cc:142 # Assertion failed: (env) != nullptr Fatal signal 6 (SIGABRT) Everything downstream in the e2e run is fallout — RPC_TIMEOUT, socket connect timeouts, `device-id` never rendering. `libbetter-sqlite3__12.10.0.so` was the only shipped addon importing `Add`/`RemoveEnvironmentCleanupHook`, the raw V8 embedder API; the other six are N-API and import none of it. 13.0.0 migrated to N-API. The shipped binary now has zero V8 symbols and zero cleanup-hook references, so the crash class is gone by construction rather than patched. It also stops being ABI-locked: one artifact serves every Node version, which is why `usesNapi` flips and the prebuild URL loses its `-node-<abi>` infix. 13.x dropped `bindings` for its own `lib/binding.js`, which knows only linux/darwin/win32 prebuild paths before falling back to node-gyp build dirs — none of which exist on device — so the addon-loader gains a pattern for it. Because every pattern here matches upstream source text, a miss would silently ship a resolver that only fails on a device; the plugin now fails the build instead when that specific resolver goes unrewritten. Verified: 11/11 Android lifecycle tests and 8/8 iOS XCTest, 12 clean boots, zero occurrences of the crash signature.
Removing this alias alongside the polywasm/fetch installers was wrong. Those installers are genuinely redundant now — the runtime supplies `WebAssembly` and `fetch` — but the alias targets a different thing: the npm `undici@6` this backend bundles, not Node's built-in undici. nodejs-mobile 24's bootstrap sets `UNDICI_NO_WASM_SIMD=1` to steer undici off the SIMD build of llhttp, but that env var is an undici 7.x feature, so the bundled 6.23.0 copy ignores it and calls `WebAssembly.compile(llhttp_simd-wasm)` unconditionally. polywasm compiles function bodies lazily, so that compile succeeds and then throws `Unsupported instruction: 0xFD` on the first parser callback — past the try/catch undici wraps the compile in. `@comapeo/core`'s maps plugin and `secret-stream-http` (via `@comapeo/map-server`) both import `fetch` from that bundled copy, so this covers online map styles and peer blob/SMP fetches. Verified: the SIMD payload is present in both bundles without the alias, and absent from the iOS bundle with it. The e2e suite could not have caught this — its map-server tests use the built-in fallback map only, with no project, no uploaded SMP and no network. Also from review: don't set TMPDIR/NODE_COMPILE_CACHE to a directory we failed to create (and check `isDirectory`, since `mkdirs()` returns false when the directory already exists); correct the JNI comment, which claimed TMPDIR must be set before startup — only NODE_COMPILE_CACHE must; drop stale `lipo` and non-NAPI references.
…bundle The redirect that keeps undici's SIMD llhttp away from polywasm matches an upstream import specifier, so an undici reshuffle turns it into a silent no-op and the SIMD bytes come back. Nothing downstream notices: the failure needs a real network fetch on a jitless device, and the e2e map-server tests deliberately use the built-in fallback map with no project, no uploaded SMP and no network. That is why removing the redirect went unnoticed in the first place. Assert the outcome rather than the mechanism — after writing the iOS bundle, fail if it still contains the SIMD payload. The marker is the first slice of that module's base64 that differs from its non-SIMD sibling, so it identifies the specific wasm build without depending on module names surviving minification. No undici installed means nothing to assert, which is the right answer if the dependency ever goes away (see #232). Verified in both directions: the build passes as-is, and fails with an actionable message when the redirect is made not to match.
| // regenerable data wants. TMPDIR and HOME are already set by iOS. | ||
| if let compileCacheDir = AppLifecycleDelegate.resolveCompileCacheDir() { | ||
| setenv("NODE_COMPILE_CACHE", compileCacheDir, 1) | ||
| setenv("NODE_COMPILE_CACHE_PORTABLE", "1", 1) |
There was a problem hiding this comment.
as with android, is the portable option necessary?
Sorry, something went wrong.
There was a problem hiding this comment.
Dropped here too — same reasoning as the Android one, see that thread for the measurement.
Sorry, something went wrong.
- Drop NODE_COMPILE_CACHE_PORTABLE on both platforms. It keys cache entries by path relative to the cache dir, which only helps when the modules and the cache move together. Ours never do: the cache is in cacheDir/Library Caches and the JS is in filesDir/the app bundle, so the relative path still contains the varying component. Verified by moving the module dir between runs — the entry hash changes either way, so the setting bought nothing. - Report a failure to create either directory to Sentry via `logCapture`, the existing helper for notable non-exception events, rather than a local log line. A device that can never write there silently pays the cold-compile cost on every launch, so the rate is worth seeing. - Trim the addon-loader comments to what a maintainer of the current code needs: no better-sqlite3 version history, and no restating between the file header and the pattern it describes.
| Back | FazBrowse Home | New Git URL |
Moves the embedded runtime from nodejs-mobile v18.20.4 to the digidem fork's
v24.19.0-0 — Node 24.19.0, V8 13.6, ABI 137 — on the lite flavour.
What the upgrade forces
--no-experimental-fetch is gone from the iOS argv. Node removed the flag in
23, and an unknown flag aborts before any JS runs, so this is not optional.
It was there because nodejs-mobile's jitless iOS V8 had no WebAssembly and
undici calls WebAssembly.compile at module init. The 24.x build serves
WebAssembly from a polyfill inside the runtime itself — I confirmed polywasm
is in the shipped bytes on both platforms — so fetch works natively and the
whole iOS-only shim stack goes with it: index.ios.js, the polywasm and undici
installers, the SIMD-wasm alias, and the loader-entry redirect. Both platforms
now bundle from one entry and differ only in the __loadAddon banner.
better-sqlite3 is pinned to 13.0.3 through overrides. 11.10.0 cannot be
compiled against V8 13.6 at all — it uses the ObjectTemplate::SetAccessor
overload that was removed. 12.10.0 compiles, but aborts on real devices: the
backend reaches ready and dies ~86ms later in
node::RemoveEnvironmentCleanupHook (Assertion failed: (env) != nullptr),
which the BrowserStack run caught and no local test did. It was the only shipped
addon importing the raw V8 embedder API.
13.0.0 migrated to N-API, so the shipped binary has zero V8 symbols and zero
cleanup-hook references — the crash class is gone by construction. It also stops
being ABI-locked, so one artifact serves every Node version; usesNapi flips and
the prebuild URL loses its -node-<abi> infix. 13.x dropped bindings for its
own lib/binding.js resolver, which only knows linux/darwin/win32 paths, so the
addon-loader gains a pattern for it plus a build-time error if that specific
resolver ever goes unrewritten. The other six addons were already N-API and carry
over untouched.
Why lite
The v18 libnode.so reports zero ICU symbols and no icudt data — it was
already --with-intl=none. So full would add an Intl this backend has
never had, and lite is the parity choice rather than a reduction. Checked
against the actual bundle: the surviving Intl.DateTimeFormat reference is a
Sentry helper that returns early unless process.versions.icu is set; the
Intl.Collator/Intl.NumberFormat ones are mapbox style-spec expression
evaluators whose exposure is identical to v18 today. Every node:inspector
reference is an await import() inside a try/catch or an opt-in Sentry
integration, there is no node:sqlite, and we ship plain JS.
Prebuilds are flavour-neutral: lite and full ship byte-identical headers and
export the same 1538 V8 symbols, and all 76 symbols the better-sqlite3 prebuild
imports resolve against lite's libnode.
Sizes, against the v18 build being replaced rather than against full:
Environment node inherits
The 24.x release is the first where an embedder-set TMPDIR is actually
readable on Android — an app process is forked from the zygote without
exec(), so upstream's SafeGetenv() privilege heuristic used to decline the
whole group. An Android app process has no TMPDIR and there is no /tmp,
which is exactly where os.tmpdir() falls back to, so anything writing there
failed with ENOENT. It now points at a real directory under cacheDir, set
through a new JNI setenv bridge because node reads it while creating the
Environment — assigning process.env from JS is too late.
V8's on-disk code cache is enabled the same way, via NODE_COMPILE_CACHE
(plus NODE_COMPILE_CACHE_PORTABLE). Env var rather than
module.enableCompileCache() deliberately: the env var is read at Environment
creation, so it covers loader.mjs itself and the Sentry chunk, which are the
two largest compiles on the boot path and both run before any of our JS could
call the runtime API.
The backend flushes it explicitly once boot reaches ready, instead of leaving
it to node's exit handler. That hook is the one moment mobile denies us — the
low-memory killer takes :ComapeoCore outright, and iOS usually kills the app
while suspended without ever reaching applicationWillTerminate — so a cache
that only landed on a clean exit would rarely land at all. By ready
everything on the boot path, which is the set worth caching, has compiled. It
measures ~3.7 MB across 14 files, written once per install; later boots are
cache hits and rewrite nothing.
Also in here
header defines that macro twice and the first is the
NODE_EMBEDDER_MODULE_VERSION passthrough, so the old pattern would have put
a garbage ABI into every prebuild URL.
directions: Node 24's v8config.h emits "C++20 or later required." under
the NDK's default gnu++17, and compiles clean with the flag.
prebuild target and the lipo pass that merged the two simulator binaries
are gone. Intel Macs can't run the simulator against this runtime either way.
contexts.runtime only ever had the upstream Node version.
Verified on device
Android emulator (Pixel_7a_API_29, arm64): 11/11 app-level tests
(ServiceLifecycleTest, ShutdownPathTest, which spawn :ComapeoCore and boot
Node) plus 26 module tests. iOS simulator (iPhone 16): 8/8 XCTest including
testFullServiceLifecycle. Both logs show the runtime identifying itself as
v24.19.0, both sockets listening, and exitCode=0 on shutdown.
The new environment plumbing is visible working rather than merely wired up:
And it pays for itself. Across 12 consecutive boots on the emulator, measured
from the setEnv line (i.e. after asset extraction, so this is module compile
time rather than first-run noise) to the first JS line:
Time to first JS halves. That is also the argument for flushing at ready
rather than at exit: the cache was written on boot 1 and paid off from boot 2,
across separate process spawns. A low-end device should see a larger absolute
saving than this M2 emulator.
One thing this turned up, filed separately as #231 and not caused by this
PR: shutdown: control close failed ERR_STREAM_WRITE_AFTER_END on every
shutdown, both platforms. It is not cosmetic — the stopping control frame is
never delivered at all (13 started, 12 ready, 0 stopping across the
emulator runs), so the graceful-shutdown signal the design added has never
fired. broadcast() queues the frame, streamx defers the socket write to the
next tick, and close() ends the socket synchronously in that same tick.
It reproduces identically on Node 18.20.4, 20, 22 and 24 against this repo's own
framed-stream/streamx versions, those versions are unchanged from main,
and this branch touches none of server-helper.js, message-port.js or
simple-rpc.js. It dates from #47.
Depends on
All landed:
Android harness, and three emulator fixes (snapshots removed, google_apis
below API 30, RAM halved) released as v2.3.0
assets for 12.10.0 are published, and its own CI smoke-tested them against
nodejs-mobile 24.19.0-0 on Android API 24, API 30 and an iOS simulator
Follow-ups, not in scope here
measurement behind that decision was taken on Node 18; node has since
reworked its source-map cache, so it's worth re-measuring on device.
lever for capping V8's old space on devices where exceeding the cgroup limit
kills the whole app. Relevant to the :ComapeoCore LMK work, but it changes
memory behaviour and belongs in its own change.
the better-sqlite3 addon — the one dependency that costs us a per-Node-
version prebuild.