diff --git a/README.md b/README.md index c914328..f37ff73 100644 --- a/README.md +++ b/README.md @@ -69,3 +69,19 @@ mainthread.Call(func() { ``` However, be careful with `mainthread.CallNonBlock` when dealing with local variables. + +## WebAssembly (`GOOS=js GOARCH=wasm`) + +A parallel implementation ships under the `js && wasm` build tag. Go programs +compiled for the browser run as a single JavaScript event-loop thread, so +there is no separate main thread to queue onto — and blocking the event loop +deadlocks `requestAnimationFrame`. The WASM build therefore: + +- runs `Run`, `Call`, `CallErr`, and `CallVal` inline on the calling goroutine, +- spawns a goroutine for `CallNonBlock` so the caller still returns without + waiting, and +- exports `CallQueueCap` only for source compatibility; it is unused. + +Downstream libraries that need to differ for WASM (for example, to avoid using +finalizers that post to the queue) can gate on the same `js && wasm` build +tag. diff --git a/mainthread.go b/mainthread.go index e445ce0..fb7a124 100644 --- a/mainthread.go +++ b/mainthread.go @@ -1,3 +1,5 @@ +//go:build !js + package mainthread import ( diff --git a/mainthread_wasm.go b/mainthread_wasm.go new file mode 100644 index 0000000..06fb98b --- /dev/null +++ b/mainthread_wasm.go @@ -0,0 +1,34 @@ +//go:build js && wasm + +package mainthread + +// Under WASM the entire program runs on the JS event-loop thread. Queuing onto +// a separate main thread would deadlock, so the blocking helpers (Run, Call, +// CallErr, CallVal) run the callback inline. CallNonBlock uses a goroutine to +// preserve its "return immediately" semantic. + +// CallQueueCap exists for source compatibility with the desktop build; the +// WASM shim does not queue anything, so the value is unused. +var CallQueueCap = 16 + +// Run invokes the supplied function synchronously and returns when it exits. +// On WASM there is no separate main thread to pump, so Run is effectively a +// direct call. +func Run(run func()) { run() } + +// Call runs f inline on the current goroutine. Equivalent to the desktop +// behavior of queueing onto the main thread and blocking until done, because +// under WASM the calling goroutine is already on the main (and only) thread. +func Call(f func()) { f() } + +// CallNonBlock runs f on a new goroutine so the caller is not blocked. This +// matches the desktop semantic ("queue and return") as closely as possible +// under a single-threaded runtime; f will execute cooperatively once the +// caller yields to the scheduler. +func CallNonBlock(f func()) { go f() } + +// CallErr runs f inline and returns its error. +func CallErr(f func() error) error { return f() } + +// CallVal runs f inline and returns its value. +func CallVal[T any](f func() T) T { return f() }