ECO-394: externref support in the WebAssembly bindings; add js-umami framework test - #112
ECO-394: externref support in the WebAssembly bindings; add js-umami framework test#112Arshia001 wants to merge 25 commits into
Conversation
ECO-355 phase DB: backend apps that need a real database declare it in a
top-level 'database' block in routes.json; the harness then starts an
ephemeral PostgreSQL before the app server and injects the declared env
vars ({dbUrl}/{dbHost}/{dbPort}/{dbUser}/{dbPassword}/{dbName} placeholders,
plus {port} expanded at spawn time), tearing everything down afterwards.
Postgres comes from the embedded-postgres npm package (real zonky.io
binaries run as plain child processes), installed on demand into
.framework-test/db-tools with the shared pnpm store. No Docker: framework
tests run on macOS CI runners (no Docker daemon) and locally via make.
The WASIX framework runner forwards only an env allowlist into the guest;
the harness now lists the app-specific names in FRAMEWORK_TEST_EXTRA_ENV
and the runner forwards those too.
Also update the ECO-355 plan: SQLite spike resolved as no-go; former
Tier B apps fold into the external-DB phase (triage table included).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First DB-backed framework-test app, exercising the new embedded-database provisioning. Green on Node, QuickJS native, and QuickJS WASIX. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Native and WASIX must expose the exact same functionality, and WASIX has no dynamic linking — so process.dlopen now throws a catchable ERR_DLOPEN_FAILED before ever calling dlopen(), on every target. Failing early is load-bearing: legacy NAPI_MODULE-style prebuilds (bufferutil, utf-8-validate, most node-gyp-build prebuilds) call the unexported napi_module_register symbol from a static constructor *during* dlopen, which previously killed the whole process with an uncatchable dynamic linker error. With the early throw, optional native accelerators fall back to their pure-JS implementations exactly as they do under WASIX. The now-unreachable dlopen machinery (library cache, initializer lookup, open/close helpers) is removed. N-API support itself is unaffected: native napi tests are statically linked, and safe mode / WASIX load addons through the wasmer napi extension. Verified: process.dlopen throws catchably on both the QuickJS and V8 native binaries; bufferutil resolves to its JS fallback; js-hedgedoc passes Node/native/WASIX and js-totaljs-cms native unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… artifacts Three harness extensions driven by js-rssmonster (second DB-backed app): - MySQL provider (kind: "mysql") via mysql-memory-server: uses a matching system mysqld when available, otherwise downloads official binaries once and caches them. The harness user is created with a password through the init SQL since the package's own user is passwordless and localhost-only. - database.setup: routes.json can list shell commands (sequelize migrations/seeds) that run after the database is up and before the app server starts. They always run on the host toolchain: package .bin launchers prefer the sibling node_modules/.bin/node over PATH — on Edge stages that is the injected Edge runner — so the harness points that shim at host Node for the duration and restores the stage runner afterwards. - Never delete git-tracked GENERATED_FRAMEWORK_PATHS entries: vendored apps commit prebuilt final artifacts (js-hedgedoc's public/build, js-rssmonster's dist), and the node stage used to remove them from the working tree — which is how js-hedgedoc's assets silently went missing from its original commit (restored in the examples submodule alongside js-rssmonster). Bump wasmer-examples: js-rssmonster (RSSMonster + MySQL, green on Node/native/WASIX) and the js-hedgedoc asset restore. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Native and WASIX must expose the same functionality, and process.platform
differing ('linux' vs 'wasi') made packages that switch on it behave
differently per target — playwright-core's registry throws at require time
on unknown platforms, which broke Uptime Kuma's boot only on WASIX. WASIX
emulates Linux syscall semantics, so report 'linux'.
Consequence: node tests guarded by common.isLinux now run on the WASIX
suite; skip test-pipe-abstract-socket-http (abstract sockets are a Linux
kernel feature WASIX does not implement). Full wasix quickjs suite green
locally: 1671 passed, 0 failed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Readiness used to accept any HTTP response, so apps that expose a temporary server during boot-time migrations (Uptime Kuma's migration page 404s API routes) were validated too early. The readiness probe now polls the first route until it answers with one of its expected statuses, and a top-level routes.json serverReadyTimeoutMs can extend the window for slow-migrating apps. Document the readiness rule, the database block, and serverReadyTimeoutMs in plans/framework-integration-tests.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Green on Node, QuickJS native, and QuickJS WASIX. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The MySQL provider now creates its user with mysql_native_password and pins the server range to 8.0.x: apps on the legacy 'mysql' 2.x driver (Firekylin's think-model-mysql -> think-mysql -> mysql chain) cannot authenticate with MySQL 8's default caching_sha2_password, and 8.4+ disables the native password plugin by default. mysql2-based apps (RSSMonster, Uptime Kuma) are unaffected — all four DB apps re-validated on native and WASIX. Bump wasmer-examples: js-firekylin (Firekylin 1.7.3 + MySQL), green on Node and QuickJS native. Skipped on the WASIX framework target: ThinkJS always serves through cluster.fork(), and the cluster IPC channel is not functional under WASIX (worker process.send fails with EPIPE — extra fds are not passed through wasmer's process spawn; the worker can never receive its listen handle, so the master respawns it forever). Tracked as a WASIX runtime capability gap in the ECO-355 plan. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The IPC read path was stubbed to ENOSYS under __wasi__, killing every child_process.fork / cluster.fork message channel on first readiness (stream error -> fd closed -> sends fail EPIPE). With the fix, fork IPC message channels work end-to-end under WASIX: worker online handshake, process.send in both directions, and cluster's listen negotiation all function. Cluster serving still needs connection handle-passing (or a reuseport-style strategy) and js-firekylin stays skipped on WASIX. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The cluster IPC message channel works after the libuv-wasix fix; the remaining gap is connection handle-passing for cluster serving. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
uv__sock_reuseport failed UV_ENOTSUP under __wasi__ before the runtime was consulted, even though the whole path below works (wasix-libc maps SO_REUSEPORT to sock_set_opt_flag; wasmer applies it to the host socket before bind). Verified under WASIX: same-port listeners in one process and across forked processes, EADDRINUSE still enforced without the flag, 40 connections balanced 18/22 across two worker processes, and Node's test-dgram-reuseport.js passes. This provides the primitive for the reuseport-based cluster scheduling strategy that would let cluster-served apps (js-firekylin) run on WASIX. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WASIX cannot pass listen handles between processes (no SCM_RIGHTS), which
made both of Node cluster's scheduling strategies unusable there: round
robin passes every accepted connection to a worker, and the shared-handle
mode passes the listen handle itself. SO_REUSEPORT, however, works end to
end under WASIX (wasix-libc -> sock_set_opt_flag -> wasmer applies it to
the host socket), with genuine kernel-level connection balancing across
forked guest processes.
Add a third scheduling handle: under WASIX, queryServer for TCP listens
answers with a ReusePortHandle that binds nothing in the primary and
replies { reusePort: true }; the worker then creates its own listen handle
with UV_TCP_REUSEPORT. All cluster bookkeeping (listening events, worker
registry, close tracking) works unchanged. UDP, fd, and pipe listens keep
their existing paths, as does everything on native targets.
Because process.platform reports 'linux' under WASIX, the branch uses a
new isWasix constant on the process_methods binding.
Verified under WASIX: two cluster workers serve HTTP on one port with
requests balanced 6/6, cluster 'listening' events fire in the primary, and
js-firekylin (ThinkJS, cluster-served) passes the framework test — so it
is removed from the WASIX skip list. Regressions green: js-firekylin
native (round robin unchanged), js-hedgedoc/js-rssmonster WASIX,
js-svelte native.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Project policy keeps the Node lib/ tree byte-identical to upstream, so the reuseport scheduling strategy moves from lib/internal/cluster into src/edge_cluster_wasix.cc. lib/ is restored to its pre-change state. The strategy is now installed from EdgeRuntime before the main builtin executes: in WASIX cluster workers (NODE_UNIQUE_ID still present at that point), an embedded script replaces the worker-side cluster._getServer — an exported, replaceable property — so TCP port listens bind their own UV_TCP_REUSEPORT handle instead of asking the primary for one, and report the 'listening' act for the primary's bookkeeping. No primary-side changes are needed at all: the primary never learns a handle key, so its registry and cleanup paths are untouched. UDP, fd, and pipe listens keep the upstream path, as does everything on native targets (compile-time gate). Known limitation inherited from the child-only shape: _getServerData/ _setServerData is not round-tripped through the primary (e.g. TLS session ticket keys stay per-worker). The isWasix constant on process_methods is removed again — the native implementation is compile-time gated and nothing else consumes it. Verified under WASIX: cluster workers balance raw TCP connections 13/11 (the earlier 12/0 observation was HTTP agent keep-alive correctly reusing one connection), js-firekylin passes the framework test, and regressions are green (firekylin native round-robin, uptime-kuma WASIX, svelte native). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With fork IPC (libuv-wasix plain read) and the cluster reuseport strategy in place, 11 of the 17 tests in WASIX_SKIP_CLUSTER_FORK_TESTS pass and are removed from the skip list, including TCP cluster serving (test-http-server-drop-connections-in-cluster, test-tls-ticket-cluster) and the child_process fork/messaging tests (test-diagnostics-channel-process, the domain and http fork harnesses). Two entries were misfiled and move to their real groups: test-http-client-with-create-connection fails on a unix-socket listen (unix-socket group) and test-crypto-secure-heap fails on OpenSSL secure heap (crypto group). What remains cluster-specific is UDP cluster listens, which still go through shared-handle passing, plus the known_issues negative test whose error-swallowing path (exit 0 on non-success worker messages) engages now that fork IPC delivers messages — the upstream known issue is not observable under WASIX. Full wasix quickjs suite locally: 1681 passed, 0 failed (baseline before: 1671 with the old skip list). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
UDP cluster listens went through shared-handle passing and were the last cluster capability broken under WASIX. The worker-side override now also covers udp4/udp6 port listens via dgram._createSocketHandle with UV_UDP_REUSEPORT; the kernel distributes datagrams between the workers by source hash (flows pin to a worker) instead of shared-socket delivery. Two contract details surfaced by the upstream tests: - _getServer callbacks must stay asynchronous (an IPC round trip upstream); the override now defers cb via process.nextTick, which test-dgram-cluster-close-during-bind's close-during-bind window depends on. - dgram passes the raw bind() arguments through: options.port can be null, undefined, or the bind callback function (socket.bind(cb)); all of those mean an ephemeral-port listen per the bind([port][, address][, callback]) signature. WASIX_SKIP_CLUSTER_FORK_TESTS is now empty: every cluster/fork test in the wasix lanes passes, including the known_issues negative test (back to failing-as-expected: with reuseport the port-0 rebind scenario behaves deterministically again). Verified: cluster UDP echo distributes 11/5 across two workers; full wasix quickjs suite 1686 passed / 0 failed; js-firekylin green on WASIX and native. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The test spawns an external cksum binary in the guest; the first exec cold-downloads and LLVM-compiles wasmer/coreutils, which exceeds the per-test timeout on CI runners with an empty wasmer cache. It passes locally with a warm cache, so this is an environment cost, not a cluster/fork or subprocess capability gap. Filed under the subprocess-shell group. (CI wasix suite was otherwise green: 1685 passed / 1 failed.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Same class as test-http-chunk-problem: the test execs ab through a shell,
and on CI runners with an empty wasmer cache the first external exec
cold-downloads and compiles wasmer/bash + wasmer/coreutils, exceeding the
per-test timeout. Locally the test self-skips gracefully ('problem
spawning ab') because the warm-cached shell starts fast enough. These two
are the only external-binary tests among the recent cluster/fork unskips;
the remaining nine are node-child-only and passed CI twice.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ipping test-http-chunk-problem (spawns cat) and test-http-full-response (execs ab through a shell) rely on guest binaries that ARE available (wasmer/bash, wasmer/coreutils); their CI timeouts came from the first exec cold- downloading and compiling those packages, not from a capability gap. Give them the scaled timeout (WASIX_SLOW_TESTS, 12x) and keep the coverage rather than skipping. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…x + arch) Regression exposed by 'Report process.platform as linux under WASIX': the app's start command is `serve` (Vercel static server), which statically imports clipboardy -> arch@2.2.0. arch's getconf-execSync path is gated on process.platform === 'linux' and only reached because process.arch is 'unknown' under WASIX (the x64/ia32 fast-returns don't fire). WASIX cannot spawn /bin/sh (EACCES), so `serve` crashes at import. Under the old 'wasi' platform arch returned 'x86' without shelling out, so this passed. Skip on the WASIX edge stage only (matches js-astro-ssr-standalone); Node baseline and QuickJS native keep full coverage. process.platform='linux' stays — it is load-bearing for Uptime Kuma (playwright). Proper long-term fix is to serve static-site apps via the harness's internal static server on edge stages so `serve` is never invoked. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ace (ECO-394) - externref registry: JS values rooted in a state-held array, id in foreign host info - real Table.get/set/grow/new incl. funcref wrap via wasm_ref_as_func - ToNumber coercion for i32/f32/f64 (glue predicates return booleans) - multi-value export results return a JS array - Memory.buffer cached + detached on growth at JS<->wasm boundaries Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… legacy behavior)
Umami's isValidTimezone calls Intl.DateTimeFormat(undefined, {timeZone}) as a
plain function; the stub threw, rejecting every timezone.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sm query compiler) The ECO-355/ECO-394 acceptance app: every Prisma query runs through the wasm-bindgen query compiler and its __wbindgen_externrefs table. Routes cover login (bcrypt + Prisma) and the full /api/send analytics ingest. Green locally on native and WASIX with a dev wasmer carrying the WARP-70 Part A C API surface and the wasm_c_api_v0 bridge fixes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| #include "edge_cluster_wasix.h" | ||
|
|
||
| #if defined(__wasi__) | ||
|
|
||
| #include <cstdlib> | ||
|
|
There was a problem hiding this comment.
Remove all changes to cluster from this PR, so it's focused solely on the Wasm externref fixes
| @@ -0,0 +1,22 @@ | |||
| #ifndef EDGE_CLUSTER_WASIX_H_ | |||
There was a problem hiding this comment.
Remove all changes to cluster from this PR, so it's focused solely on the Wasm externref fixes
| return true; | ||
| } | ||
|
|
||
| // ECMA-402 legacy behavior: Intl.DateTimeFormat and Intl.NumberFormat may be |
There was a problem hiding this comment.
Remove all changes to Intl from this PR, so it's focused solely on the Wasm externref fixes
| std::string g_process_title = "node"; | ||
| uint32_t g_process_debug_port = 9229; | ||
| std::mutex g_process_umask_mutex; | ||
| std::mutex g_process_dlopen_mutex; |
There was a problem hiding this comment.
This is a no go.
Remove all changes to the native dlopen from this PR, so it's focused solely on the Wasm externref fixes
| // Under WASIX, cluster workers get the reuseport scheduling strategy | ||
| // installed before the main builtin runs pre-execution (which consumes | ||
| // NODE_UNIQUE_ID). No-op elsewhere. | ||
| EdgeMaybeInstallWasixClusterReusePort(env); |
There was a problem hiding this comment.
Remove all changes to cluster from this PR, so it's focused solely on the Wasm externref fixes
syrusakbary
left a comment
There was a problem hiding this comment.
This PR should focus ONLY on externref support for now, so it's easier to review.
Remove all other changes unrelated do that
Summary
Wires
externrefsupport throughsrc/webassembly/edge_wasm.ccon top of the new wasmer C-API reference surface (WARP-70 Part A), unblocking wasm-bindgen modules — most importantly Prisma 7's wasm query compiler — under edgejs+quickjs on both native and WASIX. Adds js-umami (Umami v3.2.0, Postgres) as the acceptance framework test: every Prisma query it makes runs through the wasm-bindgen query compiler and its__wbindgen_externrefstable.edge_wasm.cc changes
wasm_foreign_new+wasm_ref_set_host_info). Only JSnullmaps to the null reference —undefined/true/falseare real externrefs (wasm-bindgen sentinel slots).Table.get/set/grow/new, including funcrefTable.getwrapping viawasm_ref_as_func; JS-created tables are now backed bywasm_table_new(thelocal_onlybookkeeping is gone, so they can be imported too).JsToWasmVal: wasm-bindgen predicate imports return booleans for i32-typed results; strictnapi_get_value_int32rejected them.r[0]/r[2]; returning only the first value silently produced__wbg_ptr = 0→ "null pointer passed to rust").Memory.bufferdetach-on-grow: the buffer is cached per memory object and detached at JS↔wasm boundaries when the backing storage moves/grows, matching JS-API semantics that wasm-bindgen's view caching relies on. (Previously a fresh external ArrayBuffer per access left cached views silently stale — and dangling — aftermemory.grow.)Intl fix
Intl.DateTimeFormat/Intl.NumberFormatare now callable withoutnew(ECMA-402 legacy behavior). Umami'sisValidTimezonecallsIntl.DateTimeFormat(undefined, { timeZone })as a plain function; the previous throw made every timezone "invalid" and 400'd all timezone-parameterized endpoints.js-umami framework test
Vendors the final
next buildartifact (output: standalone) rather than source — umami's real build needs a liveDATABASE_URLand network at build time (seeEDGEJS-NOTES.mdin the app dir). Routes cover the Next server, static assets,POST /api/auth/login(bcrypt + Prisma via the wasm compiler), and the fullPOST /api/sendanalytics ingest (bot check, GeoIP, session + event insert).Verification
make framework-test-quickjs-native js-umamiandWASMER_BIN=<dev wasmer> make framework-test-quickjs-wasix js-umamiare green locally (6/6 routes on every stage, no regressions vs Node).Dependencies / merge order
wasm_c_api_v0bridge fixes).EDGE_QUICKJS_WASMER_VERSION(cmake/EdgeWasmer.cmake) is bumped — edgejs pins released wasmer dists, and the WASIX stage additionally needs the released runner. All local verification above used a dev wasmer built from that branch.eco-355-db-backed-apps: DB-backed framework-test provisioning, native-addon ban, WASIX cluster/platform work) — this branch is stacked on it; merging ECO-355: DB-backed framework tests — embedded Postgres provisioning + js-hedgedoc; disable native addon loading #111 first will shrink this diff to the ECO-394 work.🤖 Generated with Claude Code