Skip to content

ports/webassembly: Add cooperative VM yield to the JS event loop. - #19427

Draft
Gadgetoid wants to merge 6 commits into
micropython:masterfrom
pimoroni:webassembly-asyncify-jspi
Draft

Gadgetoid wants to merge 6 commits into
micropython:masterfrom
pimoroni:webassembly-asyncify-jspi

Conversation

@Gadgetoid

@Gadgetoid Gadgetoid commented Jul 4, 2026 •

Copy link
Copy Markdown
Contributor

Summary

I appreciate this is an enormous tangled mess of very complicated, very deep-rooted and very scary changes.

I'm raising this for visibility, rather than as a serious proposal for upstream in its current form, but it does present some useful points.

Chiefly that JSPI is the future (A JSPI build run a Mandelbrot demo about 10x faster than its Asyncify counterpart)
, and we ought to seriously look into a cooperative WebAssembly port. The current setup functionally requires async to be usable beyond trivial things, and absolutely falls apart if you allow users to do something so brazen as while True:. It's great for what it's great for (folks have built some genuinely amazing stuff) but isn't exactly representative of MicroPython.

Additionally there are many challenges to running useful MicroPython code in the browser. This PR includes jsfetch which gives MicroPython the ability to fetch URLs. Some simple wrappers around urllib and requests make this more or less an implementation detail and allow our (Pimoroni's) Badgeware code to run unmodified on both real hardware, and in browser, even when it's fetching data from APIs. - Note: I have removed this because it was made pretty much redundant by run_sync.

I also include a no-op for @micropython.native and have been investigating doing something similar for Viper, but the latter is rather more difficult to syntax verify but not actually compile. My focus has been on parity between our devices and the web simulator, so little bits of friction like this would otherwise stop a user from just copy-pasting code between the two. - Note: I have removed this because it muddies the waters and is unrelated specifically to wasm.

Testing

I'm running this setup (shipping both Asyncify and JSPI alongside each other and auto-picking the best one for any given browser) in our prototype Badgeware simulator and it'll also be powering live examples in our documentation.

I'm now also shipping these builds with some basic playgrounds:

  1. Free-form coding with MicroPython's framebuffer blit to a canvas - https://gadgetoid.github.io/upyweb/
  2. Exploration of MicroPython driving js dom/canvas natively - https://gadgetoid.github.io/upyweb/ui.html

Trade-offs and Alternatives

This is the culmination of some months looking in to making MicroPython's WebAssembly port useful for my specific purposes. As such it's a hulking big change that - while it doesn't (afaik) break any existing uses of this port - carries a burden of understanding that I can't shoulder alone. Some input from other port users would be useful, and certainly some insight from new users of this port - I hope to put together a nice turnkey demo you can build on - would be appreciated.

Generative AI

I used generative AI tools when creating this PR, but a human has checked the
code and is responsible for the code and the description above.

@codecov

codecov Bot commented Jul 4, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.51%. Comparing base (13303f8) to head (0490b6f).
⚠️ Report is 183 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master   #19427   +/-   ##
=======================================
  Coverage   98.51%   98.51%           
=======================================
  Files         177      177           
  Lines       22927    22992   +65     
=======================================
+ Hits        22586    22651   +65     
  Misses        341      341           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented Jul 4, 2026 •

Copy link
Copy Markdown

Code size report:

Reference:  tests/basics: Update t-string test cases for unicode. [f67ab9e]
Comparison: webassembly: Add a regression test for mid-execution Ctrl-C. [merge of 0490b6f]
  mpy-cross:    +0 +0.000% 
   bare-arm:    +0 +0.000% 
minimal x86:    +0 +0.000% 
   unix x64:    +0 +0.000% standard
      stm32:    +0 +0.000% PYBV10
      esp32:    +0 +0.000% ESP32_GENERIC
     mimxrt:    +0 +0.000% TEENSY40
        rp2:    +0 +0.000% RPI_PICO_W
       samd:    +0 +0.000% ADAFRUIT_ITSYBITSY_M4_EXPRESS
  qemu rv32:    +0 +0.000% VIRT_RV32

@Gadgetoid
Gadgetoid force-pushed the webassembly-asyncify-jspi branch 4 times, most recently from 676df50 to e0f217f Compare July 5, 2026 13:54
@Gadgetoid

Gadgetoid commented Jul 5, 2026 •

Copy link
Copy Markdown
Contributor Author

This is possibly of interest to @LeaNumworks upon whose work I have no doubt been building.

Right now the Numworks simulator has A Bad Time if you're so bold as to use a loop. This is exactly the sort of behaviour this PR fixes successfully for our own simulator.

A very unhappy numworks simulator asked to do while True: pass

And since a picture - or a live demo - is worth a thousand words: https://gadgetoid.github.io/upyweb/

@Gadgetoid
Gadgetoid force-pushed the webassembly-asyncify-jspi branch 2 times, most recently from 14f7e4d to 984cdc0 Compare July 5, 2026 16:56
@LeaNumworks

Copy link
Copy Markdown
Contributor

Thanks for tagging me, I forwarded it to our team!

@dpgeorge

dpgeorge commented Jul 6, 2026

Copy link
Copy Markdown
Member

Good to know you've been getting some use out of this webassembly port!

It's on my list to look into JSPI. But the first thing there is to see how Pyodide use it and what their user-facing API is with JSPI enabled. See eg https://blog.pyodide.org/posts/jspi/

@Gadgetoid

Gadgetoid commented Jul 6, 2026 •

Copy link
Copy Markdown
Contributor Author

They use the same general shape but they have wildly different ambitions to where I was going.

I'm maintaining support for Asyncify (even if it's terribly, terribly slow) and adding JSPI on top, both supporting the ability to cooperatively yield to the browser with emscripten_sleep one blocking I/O call at a time.

Pyodide is (afaict) all-in on JSPI and offers real asyncio (implied stack switching between multiple MicroPython tasks... dark magic) and reentrant run_sync().

MicroPython Pyodide
Backends Asyncify + JSPI (portable) JSPI-only
Suspend primitive emscripten_sleep (toolchain lowers it) WebAssembly.Suspending
Entry -sJSPI, promising JSPI_EXPORTS WebAssembly.promising(...)
Concurrency single-shot (one continuation) many tasks, shadow-stack packed
"May I suspend?" mp_js_can_suspend() depth check validSuspender / can_run_sync()
Blocking I/O _jsfetch, time.sleep() run_sync(), syncified syscalls
Shadow (arg) stack dodged (never overlap tasks) hand-managed (stack_state.mjs)
Runtime state/switch none needed PyThreadState + asyncio loop
Responsiveness yield throttled ~16ms mp_js_yield none (asyncio does it)
GC vs moved stack mid-loop collect + cstack scan CPython GC + _copy buffers

As for API surface specifically:

MicroPython wasm Pyodide
Bootstrap loadMicroPython(options) loadPyodide(options)
Run code runPython(code) / runPythonAsync(code) runPython(code, {globals,locals,filename})
Top-level await via returned coroutine thenable via asyncio WebLoop
asyncio stock MicroPython module WebLoop installed by default
Block on a promise only built-ins (_jsfetch, time.sleep) run_sync(awaitable) anywhere + can_run_sync()
Async HTTP _jsfetch.request() (blocking) pyodide.http.pyfetch() (awaitable)
Responsiveness automatic ~16ms cooperative yield none (cooperate via asyncio)
JS→Py interop import js; jsffi.to_js/create_proxy/JsProxy pyodide.ffi: rich JsProxy hierarchy + lifetimes
Register JS module registerJsModule(name, obj) registerJsModule / unregisterJsModule
Interrupt mp.interrupt() (KeyboardInterrupt) setInterruptBuffer() + checkInterrupt()
Packages (mip, not exposed in JS) loadPackage / loadPackagesFromImports (micropip)
REPL replInit / replProcessCharWithAsyncify pyodide.console (Python side)
Filesystem FS FS + mountNativeFS + IDBFS/NODEFS/...

Notably there is a lot of overlap, I was solving a bunch of problems (quite unwittingly so) that Pyodide had already folded into their asyncio-first pattern.

That said I think Pyodide leans into asyncio and still doesn't solve the problem I was targeting; being able to run while True: without Python never yielding to the caller. Fixing this gives us a single, cooperative MicroPython engine that lets us simulate MicroPython-based devices on the web (and do all the previous stuff we could do with asyncio) that an unwitting user can't just lock up with two lines of code. (Interestingly even Pyodide users want to do this pyodide/pyodide#1219)

Looks like what they offer is trueasyncio running Python from multiple callers- potentially very useful for something that uses the Python language as an engine to drive scriptable behaviour across a whole, conventional web app... if this is where you want to head with MicroPython then our goals are misaligned (read: completely tangental), but it's not a bad outcome to target. Right now I don't think we have any meaningful support for multiple interpreters on any platform? Though it's been tried: https://github.com/orgs/micropython/discussions/18180 (Pyodide uses switchable greenlets so it's still one engine)

I think we want both approaches, but the road to what Pyodide is doing seems much longer and more involved than getting what we have playing nicer. But also I appreciate you don't want to commit further to an API surface that's likely to dramatically change.

@Gadgetoid

Gadgetoid commented Jul 6, 2026 •

Copy link
Copy Markdown
Contributor Author

I've walked what's presented here toward Pyodide by borrowing their can_run_sync() and run_sync() and adapting jsfetch to use it. The internal mp_js_can_suspend() I had arrived at is functionally can_run_sync() (because the sync call out to a JS Promise needs to safely suspend MicroPython) so you can:

import js
from jsffi import run_sync, can_run_sync

if can_run_sync():
    resp = run_sync(js.fetch("https://example.com/data.json"))
    data = run_sync(resp.json())          # blocking, no await needed
    print(data.title)

run_sync(js.Promise.resolve(42))

This is really useful for burying async calls into synchronous libraries- ie: where you'd normally expect a real device to do a blocking call (unless you're going significantly out of your way to work with asyncio, select, etc)

I always use run_python_async() as the entry point, and can still just do it the old async-in-everything way:

import js
import asyncio

async def main():
    resp = await js.fetch("http://127.0.0.1:8000/")
    text = await resp.text()
    print(text)

await main()

Suffice to say run_sync is one part of Pyodide where we agree.

Still kinda marvellous that any of this even works - https://gadgetoid.github.io/upyweb/ui.html

@Gadgetoid
Gadgetoid force-pushed the webassembly-asyncify-jspi branch from 0b7dab4 to e6cf9c1 Compare July 7, 2026 16:56
@Gadgetoid

Gadgetoid commented Jul 7, 2026 •

Copy link
Copy Markdown
Contributor Author

I've dropped _jsfetch (redundant) and @micropython.native noop (out of scope) from this PR since it's already a huge burden for anyone to look over and test.

I have applied some commit surgery to include an extra function to the asyncify-fast build (which unbreaks some file IO cases), since I'm keen to use this in production (it's significantly faster, but will likely encounter issues until all possible code paths are exercised and the necessary ones opted-in to asyncify's bloat). Note: I'm still having trouble with the fast build.

I think this is walking slowly toward a serious proposal. Notably half of the files touched by this PR are tests, but there are still a huge number of changes to main.c and other spicy areas.

Edit: After sinking far too much time into trying to catch all the edge cases in asyncify-fast for my purposes I'm tempted to drop it, though as just vanilla MicroPython it should be fine and may be of use to someone? Another, separate PR later perhaps.

Edit: Latest push catches a regression in Ctrl+C handling and adds a test to guard for it. Otherwise it is mostly just non-functional cleanup to bring this change more in compliance with existing practises.

@Gadgetoid
Gadgetoid force-pushed the webassembly-asyncify-jspi branch from e6cf9c1 to be119cd Compare July 7, 2026 18:11
Gadgetoid added 5 commits July 8, 2026 11:14
mp_js_hook (the Node stdin poll for the interrupt character) passed a
spurious argument to mp_hal_get_interrupt_char(), which takes none. Recent
Emscripten builds assert on the argument-count mismatch and abort the VM
mid-execution ("native function mp_hal_get_interrupt_char called with 1
args but expects 0"), which surfaces once the VM hook fires during a
longer-running script. Drop the argument.

Signed-off-by: Phil Howard <[email protected]>
Long-running or self-looping Python (a bare `while True:` game loop, a
busy computation) currently blocks the browser's main thread for as long
as it runs, because the MicroPython entry points execute to completion
synchronously. Add an optional cooperative yield so the VM periodically
hands the JS event loop a turn without the script having to `await`.

mpconfigport.h: MICROPY_ENABLE_VM_YIELD (default off) routes the existing
VM hook to mp_js_yield().
main.c: mp_js_yield() issues a throttled emscripten_sleep() from the hook,
and mp_js_sleep_ms() backs mp_hal_delay_ms(). Both suspend only when the
whole JS->MP call chain is suspend-capable (external_call_depth ==
suspendable_depth), so a synchronous re-entry from JS can't be unwound. A
MP_WEAK mp_js_yield_hook() lets an embedding run work at a safe point.
mphalport.c: mp_hal_delay_ms() sleeps via the event loop when safe.
api.js: an invoke()/settle() indirection issues the entry-point call the
right way for the build's backend (JSPI promising export, Asyncify
ccall({async:true}), or a plain synchronous ccall) and awaits the result
only when it is a Promise. The backend is announced by async_asyncify.js
and async_jspi.js, added to SRC_JS by the variant; a plain build stays
synchronous.
Makefile: make CSTD and SUPPORT_LONGJMP overridable by variants; add a
test_async target that runs the async-only fixtures (which SKIP elsewhere)
against a suspend-capable build.
variants/asyncify: reference Asyncify build with the yield enabled.
variants/jspi: experimental build using Emscripten JSPI (Wasm stack
switching) instead of Asyncify.
tests/ports/webassembly/async_yield.mjs: yield smoke test, skipped unless
the build is suspend-capable.
tools/ci.sh: build the asyncify variant and run `make test_async` on it.

The default standard and pyscript variants are unchanged and remain
synchronous.

Signed-off-by: Phil Howard <[email protected]>
A self-looping program (a bare `while True:` game loop) never returns to
the top level, which broke two assumptions of the port:

- Stopping meant tearing the whole instance down. Expose interrupt()
  (mp_sched_keyboard_interrupt) so a host can raise KeyboardInterrupt and
  stop a running script in place, then reuse the live instance.
  mp_js_sleep_ms() now slices the sleep and handles pending exceptions
  between slices, so time.sleep() is promptly interruptible too.
- Under MICROPY_GC_SPLIT_HEAP_AUTO, gc_collect() defers collection to the
  top level, which a forever-loop never reaches, so the heap grew
  unbounded to the wasm ceiling. Collect mid-execution from the VM yield
  hook instead. Asyncify scans the stack + registers; JSPI can't (switched
  stack, no register spill), so it scans the tracked C-stack range with
  bounds captured at the outermost entry. gc_alloc_threshold bounds the
  transient garbage held between collects.

main.c: interrupt / sliced sleep, and the mid-execution collector
(gc_collect_if_pending, gc_collect_cstack, MP_JS_NOTE_CSTACK_TOP, and the
MICROPY_GC_SCAN_REGISTERS / MICROPY_GC_TRACK_CSTACK selection).
api.js: expose interrupt().
variants/asyncify, variants/jspi: enable the auto split heap (and memory
growth) so a looping program is the case the mid-loop collector bounds.
tests/ports/webassembly: async_interrupt, async_restart and async_gc
fixtures for interrupt, in-place restart and bounded GC.

Signed-off-by: Phil Howard <[email protected]>
Asyncify instruments every function that could be on the stack at a
suspend. For MicroPython that is almost the whole runtime: indirect calls
(the NLR jump callbacks and the print/type-slot vtables) are
conservatively assumed to reach a suspend, so even float boxing in a hot
loop is instrumented (ASYNCIFY_ADVISE reported ~3200 functions).

Add an asyncify-fast variant that turns on ASYNCIFY_IGNORE_INDIRECT and
re-adds just the indirect-dispatch sites that genuinely sit above a
suspend (the VM call machinery + protocol slots, in asyncify_add.txt);
ASYNCIFY_ADD re-roots the analysis so their direct callers are picked up
automatically. It also needs the Wasm longjmp backend, as MicroPython's
NLR wraps execution in setjmp and the emscripten invoke_* trampolines
abort once indirect auto-detection is off.

The result runs the arithmetic / alloc / object-op hot path
uninstrumented - about 2.5x faster, and 31% smaller wasm here.

EXPERIMENTAL: the ADD list must stay complete - a suspend reachable
through an un-listed indirect call would corrupt the unwind silently.

variants/asyncify-fast: reuse the asyncify variant, add the instrumentation
flags and the Wasm exception/longjmp backend.
asyncify_add.txt: the curated dispatch-site list.
tests/ports/webassembly/asyncify_fast.mjs: force a suspend through every
dispatch path and cross-check a checksum against the full build.
tools/ci.sh: build the asyncify-fast variant and run its fixture.

Signed-off-by: Phil Howard <[email protected]>
_jsfetch shows the pattern - start an async JS operation, then suspend the
WASM stack (emscripten_sleep) until it settles - but bakes in fetch().
Generalise it: run_sync(awaitable) blocks the current flow on any JS
thenable and returns its resolved value, so Python can await JS without the
caller having to. It needs a suspend-capable build and is gated on
MICROPY_PY_JS_RUN_SYNC (off by default, on in the asyncify and jspi
variants).

run_sync refuses with RuntimeError unless mp_js_can_suspend() - the same
permission check the cooperative yield uses - so a synchronous re-entry
cannot unwind a frame it does not own. A rejection is surfaced like a
thrown JS call (JsException), and a pending KeyboardInterrupt breaks the
wait. Each in-flight await takes an integer handle into a JS-side Map
rather than a single slot, so it stays correct if more than one
continuation is ever suspended at once.

main.c: make mp_js_can_suspend() non-static so the module can gate on it.
modjsffi.c: run_sync()/can_run_sync() and their JS half (start/poll/take).
proxy_c.h: declare mp_obj_jsproxy_make_js_exception() for the reject path.
mpconfigport.h: MICROPY_PY_JS_RUN_SYNC gate, default off.
variants/asyncify, variants/jspi: enable it (fast inherits asyncify).
tests/ports/webassembly/async_run_sync.mjs: resolve, reject and type-error
paths on a suspend-capable build (skips otherwise).

Signed-off-by: Phil Howard <[email protected]>
@Gadgetoid
Gadgetoid force-pushed the webassembly-asyncify-jspi branch from be119cd to e2211f0 Compare July 8, 2026 10:39
The cooperative VM yield took over the VM hook from the plain JS_HOOK
path, which quietly dropped the node-stdin Ctrl-C polling (mp_js_hook)
during a running script, with no test to catch it. The yield now
re-polls mp_js_hook, and this test locks that in: a child spins in
Python with Ctrl-C enabled while the parent injects the interrupt byte
on its stdin, and the run must raise KeyboardInterrupt. If the hook
stops being polled the byte goes unread, the loop never stops, and the
test fails rather than the regression going unnoticed.

Runs on the asyncify variants, where the stdin hook is compiled in; it
skips on the plain synchronous build and on jspi.

Signed-off-by: Phil Howard <[email protected]>
@dpgeorge

Copy link
Copy Markdown
Member

After sinking far too much time into trying to catch all the edge cases in asyncify-fast for my purposes I'm tempted to drop it,

Dropping it sounds good to me! I'd go even further and suggest to remove all ASYNCIFY support and just go for JSPI. ASYNCIFY won't be a viable long term option, it's short term if anything.

@Gadgetoid

Copy link
Copy Markdown
Contributor Author

JSPI is currently supported in Chrome, Edge and Opera: https://caniuse.com/wf-wasm-jspi

I'm hoping that changes quickly because Asyncify is slow and unwieldy and indeed it would really simplify this PR.

It's useful as a short-term fallback, though once you start adding WebSerial code deployment (which we're considering for our simulator) you're shackled to Chrome anyway.

@ntoll

ntoll commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Hi 👋

Just adding a note of thanks for JSPI efforts and for dropping asyncify. My use of MicroPython in the browser is as a browser based runtime for doing, er, browser-y things via the FFI. I also want to emphasise how important it is that MicroPython's JSPI support has a public API that mirror's Pyodide's implementation. It means Python runtimes can be easily slotted in/out as need arises without having to change project code. Of further note is also that I believe JSPI support is coming to Firefox (it's being tracked via this issue: https://bugzilla.mozilla.org/show_bug.cgi?id=1897981). I start a new gig tomorrow so my spare cycles are going to be limited in the coming few weeks, but if you want someone to test things I'm more than happy to be that crash test dummy. 😄 👍 🚀

@Gadgetoid

Copy link
Copy Markdown
Contributor Author

if you want someone to test things I'm more than happy to be that crash test dummy

Testing- yes please. But - vitally more important - your input as another user of MicroPython WASM. I'm pretty new to this corner of the codebase and made changes to suit my needs first and foremost, then tried to shape them to be suitable for upstream second.

The points about Pyodide are particularly salient, and I agree. MicroPython reflects CPython. MicroPython WASM ought to reflect Pyodide.

Possible talking point along those lines - a Fetch API based mip?

May also be worth considering how we handle stuff like urllib and requests, which I currently vendor modified shims for- since they need a deep rewrite from using a raw socket, to just being a wrapper around Fetch.

Pyodide has experimental support for sockets when running under node - https://pyodide.org/en/stable/usage/socket.html - which, if it goes anywhere, would mean we'd have two classes of networking library:

  1. Ones which need a socket and will only work in Node
  2. Ones which can be shimmed by Fetch and might need a WASM-only counterpart (And then CORS will block your requests anyway 😆)

The one place I think we differ - for my needs in particular - is the cooperative yield to handle fully synchronous code (while True: do stuff) gracefully. That could easily be a variant/build option.

Anyway TLDR; help shaping this would be appreciated.

@ntoll

ntoll commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

👋 Apologies for my tardy response time. I started a new gig this week and I've been snowed under with on-boarding activities.

So my use for WASM MicroPython is exclusively within the browser. It turns out, if you want Pythonic things in the browser, MicroPython gives you what you need in the blink of an eye. This is in contrast to Pyodide, which brings everything but the kitchen sink, and is relatively slow to start. Both have a place - Pyodide brings the Python ecosystem to the browser, MicroPython brings "Pythonic" to browser based development. They complement each other and I intend to evolve things on from PyScript to a project I'm calling "browser friendly Python".

Blocking calls in Python in the browser and the whole sync/async dichotomy have been a pain point. In PyScript we used web workers, atomics and shared-array-buffer, to simulate blocking, but this came at a price of complexity in our code, performance trade-offs, lots of deeply technical research and obscure server configuration related to COOP/COEP headers that are often hard to control and even more difficult to make sense of. Definitely not ideal and a set of work-arounds on top of work-arounds. With the arrival of JSPI pretty much 99% of those problems go away, and it means browser friendly Python can be significantly simpler and easier to use than PyScript ever was.

This is why I'm interested in this work and willing to participate as best I can to help get this to land. FWIW @dpgeorge and I have chatted about this over the past three months or so as something affirmative to the WASM build of MicroPython. Damien also tells me there are known locations in MicroPython where the interpreter can yield to the browser's event loop so while True: pass can work without blocking the main thread.

Anyway... this is a bit brain-dumpy... but I hope you see my view of this particular mountain, and I hope we can compare notes so we arrive at a nice, simple yet effective and flexible solution for JSPI / MicroPython. 👍

@ntoll

ntoll commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Morning folks. 🌞

Over the weekend (and just recently with fresh eyes), I did a quick analysis (with the help of Claude) of the current state of play based on this PR. Rather than clutter up this PR, I've stuck it all in this gist which contains the LLM's initial draft, heavily edited and changed by me: https://gist.github.com/ntoll/88634f27175c585729e619287d008871

@dpgeorge @Gadgetoid - please take a read. It's relatively short but VERY specific as a proposal to land just JSPI, based on the initial work you (Phil) have already done.

My suggestion is this report leads to a new PR for JSPI support only.

What I expect is some discussion and questions, but the core thing you need to know is LLM estimates the PR to be around 200 LoC (i.e. relatively small). Now, knowing how Damo works, it should be relatively trivial to create this PR as a "to-throw-away" PoC. What I expect will really happen is Damo will roll his own solution 😉, so this is simply an exercise in creating a coherent, simple and easy to understand context, with some working code, from which Damien can trivially build a solution to his liking.

Please let me know what you think. Happy to answer questions. Totally understand if the answer is "nope". FWIW - JSPI is the missing link for my BFP project to land, so I have skin in this game. 🤗

@Gadgetoid

Copy link
Copy Markdown
Contributor Author

If you're willing to land that JSPI PR I'm happy to defer. My specific use case (https://try.badgewa.re) wont serve as a very good test for it.

I've been meaning to come back and prune this PR, but haven't had the time. Appreciate your insight!

@ntoll

ntoll commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

See #19594, and thanks for getting this going @Gadgetoid 🤗. Without your initial work none of this could have been done so quickly.

@dpgeorge I imagine you'll have opinions about the implementation in #19594 - please feel free to change anything. 💪

This is the final piece in the BFP jigsaw puzzle and, once landed/released, will allow me to do an alpha release of BFP in a couple of weeks once I've finished off the other 90% of work that needs completing. 😉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants