Stream: jco

Topic: fetch-failing-in-browser-shim


view this post on Zulip Notification Bot (Dec 23 2024 at 07:52):

A message was moved here from #jco > Basic hello-wasi-http-js sample from ground up by Victor Adossi.

view this post on Zulip Victor Adossi (Dec 23 2024 at 08:20):

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.

view this post on Zulip Victor Adossi (Dec 23 2024 at 08:47):

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!

Example of using jco with browser-powered fetch(). Contribute to vados-cosmonic/jco-browser-fetch-example development by creating an account on GitHub.
JavaScript toolchain for working with WebAssembly Components - bytecodealliance/jco

view this post on Zulip Victor Adossi (Dec 23 2024 at 08:50):

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):

https://github.com/wasmCloud/wasmCloud/blob/main/examples/typescript/components/http-password-checker/rollup.config.mjs

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!)

wasmCloud is an open source Cloud Native Computing Foundation (CNCF) project that enables teams to build, manage, and scale polyglot apps across any cloud, K8s, or edge. - wasmCloud/wasmCloud

view this post on Zulip Thomas Wang (Dec 24 2024 at 01:09):

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

Example of using jco with browser-powered fetch(). Contribute to vados-cosmonic/jco-browser-fetch-example development by creating an account on GitHub.
Example of using jco with browser-powered fetch(). Contribute to vados-cosmonic/jco-browser-fetch-example development by creating an account on GitHub.
Example of using jco with browser-powered fetch(). Contribute to vados-cosmonic/jco-browser-fetch-example development by creating an account on GitHub.

view this post on Zulip Thomas Wang (Dec 24 2024 at 01:14):

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?

JavaScript toolchain for working with WebAssembly Components - bytecodealliance/jco
JavaScript toolchain for working with WebAssembly Components - bytecodealliance/jco

view this post on Zulip Thomas Wang (Dec 24 2024 at 03:08):

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

Example of using jco with browser-powered fetch(). Contribute to vados-cosmonic/jco-browser-fetch-example development by creating an account on GitHub.
Example of using jco with browser-powered fetch(). Contribute to cytommi/jco-browser-fetch-example development by creating an account on GitHub.

view this post on Zulip Victor Adossi (Dec 24 2024 at 04:14):

Hey sorry for the last minute breakage -- the trailing comma is fixed now, thanks for pointing it out!

view this post on Zulip Victor Adossi (Dec 24 2024 at 04:24):

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:

https://github.com/vados-cosmonic/jco-browser-fetch-example/commit/48982ab306ba544b3e7fa444526e0b8b96aebabf

And the import before use should look like this:

const { ping } = await import("./component.js");
Signed-off-by: Victor Adossi <vadossi@cosmonic.com>

view this post on Zulip Thomas Wang (Dec 24 2024 at 08:42):

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?

view this post on Zulip Thomas Wang (Dec 24 2024 at 19:10):

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 and outgoingHandler 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:

view this post on Zulip Victor Adossi (Dec 25 2024 at 02:12):

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 in dist/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 and outgoingHandler 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

The StarlingMonkey JS runtime. Contribute to bytecodealliance/StarlingMonkey development by creating an account on GitHub.
The StarlingMonkey JS runtime. Contribute to bytecodealliance/StarlingMonkey development by creating an account on GitHub.

view this post on Zulip Victor Adossi (Dec 25 2024 at 03:55):

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:

Adds options to jco transpile and jco types: .addOption(new Option('--async-mode [mode]', 'use async imports and exports').choices(['sync', 'jspi', 'asyncify&#39...
This example adds a component that uses browser-provided fetch() (and related shims) which returns the response for a GET request to a given URL. This example should showcase how to use a transpile...

view this post on Zulip Thomas Wang (Dec 27 2024 at 19:25):

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!

  1. Is it accurate to say that the wasi preview2-shim serves as the wasi implementation that gets called from StarlingMonkey's HostAPI? (so js .fetch() -> starlingmonkey fetch embedding -> starlingmonkey hostapi.cpp -> wasi (implemented as wasi preview2-shim)). Here's my mental model (link here):
    Screenshot 2024-12-27 at 11.18.11 AM.png

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!

Adds options to jco transpile and jco types: .addOption(new Option('--async-mode [mode]', 'use async imports and exports').choices(['sync', 'jspi', 'asyncify&#39...
JavaScript toolchain for working with WebAssembly Components - bytecodealliance/jco
JavaScript toolchain for working with WebAssembly Components - cytommi/jco
The StarlingMonkey JS runtime. Contribute to bytecodealliance/StarlingMonkey development by creating an account on GitHub.
The StarlingMonkey JS runtime. Contribute to bytecodealliance/StarlingMonkey development by creating an account on GitHub.
Excalidraw is a virtual collaborative whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them.

view this post on Zulip Victor Adossi (Dec 30 2024 at 07:57):

Regarding (1)

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 ReadableStreams 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!)

Regarding (2)

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.

A language binding generator for WebAssembly interface types - bytecodealliance/wit-bindgen
The StarlingMonkey JS runtime. Contribute to bytecodealliance/StarlingMonkey development by creating an account on GitHub.
The StarlingMonkey JS runtime. Contribute to bytecodealliance/StarlingMonkey development by creating an account on GitHub.
The StarlingMonkey JS runtime. Contribute to bytecodealliance/StarlingMonkey development by creating an account on GitHub.
The StarlingMonkey JS runtime. Contribute to bytecodealliance/StarlingMonkey development by creating an account on GitHub.
The StarlingMonkey JS runtime. Contribute to bytecodealliance/StarlingMonkey development by creating an account on GitHub.
The StarlingMonkey JS runtime. Contribute to bytecodealliance/StarlingMonkey development by creating an account on GitHub.
The StarlingMonkey JS runtime. Contribute to bytecodealliance/StarlingMonkey development by creating an account on GitHub.
The StarlingMonkey JS runtime. Contribute to bytecodealliance/StarlingMonkey development by creating an account on GitHub.
Contribute to WebAssembly/wasi-http development by creating an account on GitHub.

Last updated: Jan 24 2025 at 00:11 UTC