A message was moved here from #jco > Basic hello-wasi-http-js sample from ground up by Victor Adossi.
Hey @Thomas Wang just moved your issue over to a something new so it's easier for people to find, repro and hopefully help with!
Have you tried without bundling? I'm wondering if esbuild
isn't taking in dependencies it should be.
I actually just got your example working, will push up a repo shortly.
Hey @Thomas Wang check out this repo for my working example -- I'm not sure exactly what's getting caught up in your Webpack setup, but hopefully it's helpful to you:
https://github.com/vados-cosmonic/jco-browser-fetch-example
PRs welcome to that repo -- I'll try and add it as an official jco
example sometime!
Oh also, as far as bundling goes, we do have an example over in wasmCloud (a project I'm a maintainer of & whose stewarding corporate entity I currently work for):
Just to be clear -- you do not need to use anything related to wasmCloud there or even glance at the other parts of the example -- I link it here only because it's a good example of a working Rollup config with external NodeJS deps pulled in (we use it to pull in valibot
).
There may be other good examples though (maybe I should also commit a similar example to jco
!)
Thank you for providing the examples! I tried out your example but it seems like the importmap
in demo.html
is misconfigured (due to a trailing comma in the json).
Here's the error I see:
Screenshot 2024-12-23 at 5.01.46 PM.png
Due to failed loading of the importmap, I believe the example just directly imports components.js
and runs it without any of the component sandboxing.
After fixing the trailing comma (see my forked branch with the one liner diff), a different error arises:
Screenshot 2024-12-23 at 5.06.06 PM.png
I'm most confused by the implementation of the wasi:http shim in the @bytecodealliance/preview2-shim package.
To me it looks incomplete, where both incomingHandler
and outgoingHandler
are left empty. Could this be the root cause?
Thomas Wang said:
After fixing the trailing comma (see my forked branch with the one liner diff), a different error arises:
Screenshot 2024-12-23 at 5.06.06 PM.png
I realized that I didn't pass in a string to invoke ping()
:man_facepalming: . After fixing the two obvious bugs (diff here), I'm getting the same error I faced in my bundled solution:
Screenshot 2024-12-23 at 7.05.24 PM.png
Here's the branch on my fork where I attempt to run the bundle. You can serve the bundle on my branch with pnpm bundle:go
Hey sorry for the last minute breakage -- the trailing comma is fixed now, thanks for pointing it out!
AH I just figured out the problem -- the importmap
entry for component.js
was the problem.
So the problem you're running to in your own code is with the bundling of imports for component.js
-- the imports in component.js
are not being resolved properly -- they need to be mapped.
In the browser we have essentially reproduced this (accidentally, on my part) by putting component.js
in the import map -- the sibling entries cannot be used to one of the imports themselves, so you need to do the import of the module later.
Here's the commit that fixes things:
And the import before use should look like this:
const { ping } = await import("./component.js");
By removing the importmap entry for "./component.js": "./dist/transpiled/component.js"
, doesn't the code effectively mean that we're directly importing the component implementation instead of the componentized output in dist/transpiled
?
Thomas Wang said:
I'm most confused by the implementation of the wasi:http shim in the @bytecodealliance/preview2-shim package.
To me it looks incomplete, where both
incomingHandler
andoutgoingHandler
are left empty. Could this be the root cause?
What do you think of this? Does it look like an incomplete implementation in lib/browser/http.js
to you? Thanks for all the help! :pray:
Thomas Wang said:
By removing the importmap entry for
"./component.js": "./dist/transpiled/component.js"
, doesn't the code effectively mean that we're directly importing the component implementation instead of the componentized output indist/transpiled
?
Nope! The code inside dist/transpiled
is actually using the imports that we're filling in on the browser side! If you take a look at dist/transpiled/component.js
, it has a bunch of generated code but also imports from other modules. There's a difference between the component itself's implementation (which will have stuff like how to handle/encode, lift and lower, convert basic types, etc) and the platform implementation that's expected to be filled out (and adhere to the semantics of wasi:*
imports)
If you look into that folder you'll see component.js
and the interfaces
folder, but those are all Typescript definition files -- i.e. *.d.ts
files -- they don't include implementation, it's up to the code on the browser side (the "platform") to provide that.
Thomas Wang said:
Thomas Wang said:
I'm most confused by the implementation of the wasi:http shim in the @bytecodealliance/preview2-shim package.
To me it looks incomplete, where both
incomingHandler
andoutgoingHandler
are left empty. Could this be the root cause?What do you think of this? Does it look like an incomplete implementation in
lib/browser/http.js
to you? Thanks for all the help! :pray:
This is fine in your case because it's not what is actually called -- fetch()
is.
I think the context that's missing here is that fetch()
is implemented in terms of WASI at a lower level -- StarlingMonkey, the JS runtime underneath. You can check out the implementation here.
To use outgoingHandler
explicitly, you'd need to import it, which 99% of people will not do from JS contexts.
That said, outgoingHandler
could/should definitely be written in terms of built in fetch
in the future though -- as jco transpile
can be used on components from other languages that do try to use wasi:http/outgoing-handler
Ahh @Thomas Wang I finally got what you were pointing at -- and I'm getting the same error as you (with my import pointed at the transpiled component) -- your note about the unfinished impl made me realize I was running off some unreleased code that had the impls which is why I was ending at a different spot...
I've made a PR to jco
that should explain this (and be a gate for the async PR) -- this is an example we should check on every release once the updated code lands.
Thanks for bringing this up :bow:
Thank you so much for following up on this! I've got a few followup questions hereon for my own curiosity if you don't mind!
js .fetch()
-> starlingmonkey fetch embedding
-> starlingmonkey hostapi.cpp
-> wasi (implemented as wasi preview2-shim)
). Here's my mental model (link here): 2.Is it impossible to implement HTTP handlers synchronously? I noticed your PR is blocked by the one adding JSPI and Asyncify. I’m struggling to understand what’s missing in the current HTTP handler implementation in the WASI-browser shim that's causing the issue—especially since Node's WASI shim works. Why are asyncified exports/imports necessary for supporting wasi-http?
Thank you!
Yes, I think that understanding is right, that diagram is looks excellent to me. I think the only thing you might be missing is the C calls generated by wit-bindgen
in all this (explained below).
To supply some evidence, you can see in the StarlingMonkey fetch
impl -- that it deals in WASI terms. As you have already noticed, when components are run in the browser, they're actually disassembled into modules with WASI imports and then the host obviously has to provide implementations for those imports in the normal WASM module way.
Just a note, but StarlingMonkey can't implement the platform-specific stuff, because in this context it is crammed into a WebAssembly component (module, at the end of the day) and run in a WASI-supporting host context, it isn't the host. A JS WebAssembly Component is roughly StarlingMonkey compiled to WASM + the source JS , with some initialization steps done. It's theoretically impossible for StarlingMonkey to sort of short circuit/stub/avoid expensive calls to the host, but I'm assuming that's not what we're talking about here.
The host is the browser (or V8 + NodeJS in the Node case).
The Host API helps turn commands like fetch()
into WASI-compliant host calls, with the help of the host API -- For example in the supporting builtins/web/fetch/request-response.cpp
's work creating outgoing request bodies.
In request-response.cpp
we write_all
to the outgoing body, then eventually finish
the outgoing body (the lexical order is a little weird because of how ReadableStream
s work, if you look at the comment above), all of those get turned into host_api
methods like write_all
and HttpOutgoingBody::close
, which do some engine stuff, and ultimately/most-importantly WASI stuff that is actually completely imported from C, via bindings generated from wit-bindgen
(ex. wasi_io_streams_method_output_stream_blocking_flush(...)
.
Unfortunately I think I may have not mentioned wit-bindgen
until now, but basically it is a language-specific imports (and exports) into code for a given programming language. The generated bindings are in the host API repo.
So all this is to say that you're correct -- when JS runs in StarlingMonkey (inside a component) it translates some builtins (like fetch()
) to WASI underneath (inside the StarlingMonkey engine), and that code is translated to WASI calls on the host.
To be more concrete, like we've been talking about, the implementation of OutgoingRequest::send()
, is actually trying to go out to wasi:http/outgoing-handler.handle
. This is implemented in NodeJS but as you found unimplemented in browsers on main
.
@Guy Bedford / @Till Schneidereit / @Calvin Prewitt would love some correction here on anything I missed above :)
This is why I was incorrectly thinking it was supposed to just work -- I was thinking that HttpOutgoingRequest::send
at the StarlingMonkey level actually translated to send()
at the shim level, but I was wrong about this. In fact it's silly, because outgoing-request
has no send()
!, it was just a utility addition. The thing we needed is outgoing handler which is not on main
(yet!)
It is possible, but far from ideal -- this is because of the web platform use of asynchronous primitives to avoid blocking the main thread (etc), but WebAssembly (and core modules especially!) do not expect async host functions (this is what JSPI is for and emscriptens Asyncify is for) This may be why we didn't have the browser impl yet but I'm not 100% sure.
You could imagine one way to get around this is to use something like a web worker -- or some other non-main-thread execution context, but that's obviously not quite as ideal as getting decent same-event-loop support.
Last updated: Jan 24 2025 at 00:11 UTC