packages feed

jsaddle-wasm 0.0.1.0 → 0.1.0.0

raw patch · 6 files changed

+70/−49 lines, 6 filesdep ~ghc-experimentalPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: ghc-experimental

API changes (from Hackage documentation)

Files

CHANGELOG.md view
@@ -1,5 +1,10 @@ # Revision history for jsaddle-wasm +## 0.1.0.0 -- 2025-03-06++ * Add support for synchronous callbacks (when using `run`, but not `runWorker`) using the new synchronous Wasm JSFFI exports feature.+   Correspondingly, jsaddle-wasm requires a GHC with support for that feature; please use older versions of this library if you can not yet upgrade.+ ## 0.0.1.0 -- 2024-10-26   * Added `runWorker` and `jsaddleScript` for running JSaddle and WASM logic in different execution environments. See the README for details on how to use this.
README.md view
@@ -3,7 +3,7 @@ [![Hackage](https://img.shields.io/hackage/v/jsaddle-wasm)](https://hackage.haskell.org/package/jsaddle-wasm) [![Haddocks](https://img.shields.io/badge/documentation-Haddocks-purple)](https://hackage.haskell.org/package/jsaddle-wasm/docs/Language-Javascript-JSaddle-Wasm.html) -Run [JSaddle][] `JSM` actions with the [GHC WASM backend][].+Run [JSaddle][] `JSM` actions with the [GHC Wasm backend][].  This can for example be used to compile and run [Miso][] or [Reflex][] apps in the browser. @@ -12,14 +12,18 @@  ## Examples - - Several Miso examples: https://github.com/tweag/ghc-wasm-miso-examples+ - Miso examples: https://github.com/tweag/ghc-wasm-miso-examples - - Reflex TodoMVC example: https://github.com/tweag/ghc-wasm-miso-examples/pull/18+ - Reflex examples: https://github.com/tweag/ghc-wasm-reflex-examples + - Ormolu Live: https://github.com/tweag/ormolu/tree/master/ormolu-live+   (uses the web worker approach described below)+ ## How to use -Install a WASM-enabled GHC with support for the WASM JSFFI from [ghc-wasm-meta][] (GHC 9.10 or newer).+Install a Wasm-enabled GHC with support for the Wasm JSFFI (including [synchronous JSFFI exports][sync-jsffi-exports][^missing-sync-jsffi]) from [ghc-wasm-meta][] (GHC 9.10 or newer). + Assuming you built your application as an `app :: JSM ()`:  ```haskell@@ -31,14 +35,14 @@ main = JSaddle.Wasm.run app ``` -Build the WASM binary with the following GHC options:+Build the Wasm binary with the following GHC options: ```cabal ghc-options: -no-hs-main -optl-mexec-model=reactor "-optl-Wl,--export=hs_start" ```  Now, run the post-linker script as described in the [GHC User's Guide][ghc-users-guide-js-api]; we will call the resulting JavaScript file `ghc_wasm_jsffi.js`. -Then, following the [GHC User's Guide][ghc-users-guide-js-api], you can run the WASM binary in the browser via e.g. [browser_wasi_shim][]:+Then, following the [GHC User's Guide][ghc-users-guide-js-api], you can run the Wasm binary in the browser via e.g. [browser_wasi_shim][]: ```javascript import { WASI, OpenFile, File, ConsoleStdout } from "@bjorn3/browser_wasi_shim"; import ghc_wasm_jsffi from "./ghc_wasm_jsffi.js";@@ -64,11 +68,11 @@  ### Separating execution environments -It is also possible to run the WASM worker in a different execution environment (e.g. a web worker) than the JSaddle JavaScript code that dispatches the JSaddle command messages.+It is also possible to run the Wasm worker in a different execution environment (e.g. a web worker) than the JSaddle JavaScript code that dispatches the JSaddle command messages. -An advantage of this approach is that computationally expensive operations in WASM do not block the UI thread. A disadvantage is that there is some overhead for copying the data back and forth, and everything relying on synchronous callbacks (e.g. `stopPropagation`/`preventDefault`) definitely no longer works.+An advantage of this approach is that computationally expensive operations in Wasm do not block the UI thread. A disadvantage is that there is some overhead for copying the data back and forth, and everything relying on synchronous callbacks (e.g. `stopPropagation`/`preventDefault`) definitely no longer works. - - Instead of the `run` function above, you need to use `runWorker`:+ - Instead of the `run` function above, you need to use `runWorker` (again assuming `app :: JSM ()`):     ```haskell    import Language.Javascript.JSaddle.Wasm qualified as JSaddle.Wasm@@ -76,12 +80,12 @@    foreign export javascript "hs_runWorker" runWorker :: JSVal -> IO ()     runWorker :: JSVal -> IO ()-   runWorker = JSaddle.Wasm.runWorker Ormolu.Live.app+   runWorker = JSaddle.Wasm.runWorker app    ```     The argument to `runWorker` here can be any message port in the sense of the [Channel Messaging API][]. In particular, it must provide a `postMessage` function and a `message` event. -   For example, in a web worker, you can initialize the WASM module as above, and then run+   For example, in a web worker, you can initialize the Wasm module as above, and then run    ```javascript    await instance.exports.hs_runWorker(globalThis);    ```@@ -101,15 +105,14 @@  ## Potential future work - - Take a closer look at synchronous callbacks. We have no special handling currently, but basic things like `stopPropagation`/`preventDefault` already seem to work fine with `run` (but not with `runWorker`, as expected).  - Testing (e.g. via Selenium).  - Add logging/stats.  - Performance/benchmarking (not clear that this is actually a bottleneck for most applications).     - Optimize existing command-based implementation.        - Reuse buffers        - Use a serialization format more efficient than JSON.-    - Patch `jsaddle` to not go through commands, by using the WASM JS FFI.-    - Implement `ghcjs-dom` API directly via the WASM JS FFI.+    - Patch `jsaddle` to not go through commands, by using the Wasm JS FFI.+    - Implement `ghcjs-dom` API directly via the Wasm JS FFI.        This would involve creating a `ghcjs-dom-wasm` package by adapting the FFI import syntax from `ghcjs-dom-jsffi`/`ghcjs-dom-javascript` appropriately. @@ -117,14 +120,17 @@  ## Related projects - - [WebGHC/jsaddle-wasm](https://github.com/WebGHC/jsaddle-wasm) for the analogue for [WebGHC][] instead of the [GHC WASM backend][].+ - [WebGHC/jsaddle-wasm](https://github.com/WebGHC/jsaddle-wasm) for the analogue for [WebGHC][] instead of the [GHC Wasm backend][]. +[^missing-sync-jsffi]: Otherwise, you will see errors involving `unknown type name 'HsFUN'`.+ [JSaddle]: https://github.com/ghcjs/jsaddle-[GHC WASM backend]: https://www.tweag.io/blog/2022-11-22-wasm-backend-merged-in-ghc+[GHC Wasm backend]: https://www.tweag.io/blog/2022-11-22-wasm-backend-merged-in-ghc [Miso]: https://github.com/dmjio/miso [Reflex]: https://github.com/reflex-frp/reflex-[ghc-wasm-meta]: https://gitlab.haskell.org/ghc/ghc-wasm-meta+[ghc-wasm-meta]: https://gitlab.haskell.org/haskell-wasm/ghc-wasm-meta [browser_wasi_shim]: https://github.com/bjorn3/browser_wasi_shim [ghc-users-guide-js-api]: https://ghc.gitlab.haskell.org/ghc/doc/users_guide/wasm.html#the-javascript-api [WebGHC]: https://webghc.github.io [Channel Messaging API]: https://developer.mozilla.org/en-US/docs/Web/API/Channel_Messaging_API+[sync-jsffi-exports]: https://gitlab.haskell.org/ghc/ghc/-/merge_requests/13994
jsaddle-wasm.cabal view
@@ -1,8 +1,8 @@ cabal-version: 3.0 name: jsaddle-wasm-version: 0.0.1.0-synopsis: Run JSaddle JSM with the GHC WASM backend-description: Run JSaddle @JSM@ with the GHC WASM backend.+version: 0.1.0.0+synopsis: Run JSaddle JSM with the GHC Wasm backend+description: Run JSaddle @JSM@ with the GHC Wasm backend. category: Web, Javascript homepage: https://github.com/amesgen/jsaddle-wasm bug-reports: https://github.com/amesgen/jsaddle-wasm/issues@@ -58,7 +58,7 @@     build-depends:       aeson >=2 && <2.3,       bytestring >=0.11 && <0.13,-      ghc-experimental ^>=0.1,+      ghc-experimental ^>=0.1 || >=9.1000 && <9.1300,       stm ^>=2.5,      hs-source-dirs: src-wasm
src-native/Language/Javascript/JSaddle/Wasm/Internal.hs view
@@ -9,10 +9,10 @@  run :: JSM () -> IO () run _ =-  fail "Language.Javascript.JSaddle.Wasm.run: only works on WASM backend"+  fail "Language.Javascript.JSaddle.Wasm.run: only works on Wasm backend"  runWorker :: JSM () -> JSVal -> IO () runWorker _ _ =-  fail "Language.Javascript.JSaddle.Wasm.runWorker: only works on WASM backend"+  fail "Language.Javascript.JSaddle.Wasm.runWorker: only works on Wasm backend"  data JSVal
src-wasm/Language/Javascript/JSaddle/Wasm/Internal.hs view
@@ -21,9 +21,9 @@ import Language.Javascript.JSaddle.Types (Batch, JSM)  -- Note: It is also possible to implement this succinctly on top of 'runWorker'--- and 'jsaddleScript', but then e.g. @stopPropagation@/@preventDefault@--- definitely don't work, whereas they work (at least in simple cases) with the--- implementation below.+-- and 'jsaddleScript' (using MessageChannel), but then e.g.+-- @stopPropagation@/@preventDefault@ don't work, whereas the implementation+-- below has special support via @processResultSync@. run :: JSM () -> IO () run entryPoint = do   -- TODO rather use a bounded (even size 1) queue?@@ -35,8 +35,8 @@   readBatchCallback <-     mkPushCallback $ atomically $ readTQueue outgoingMsgQueue -  let jsaddleRunner :: JSVal -> IO ()-      jsaddleRunner processResultCallback = do+  let jsaddleRunner :: JSVal -> JSVal -> IO ()+      jsaddleRunner processResultCallback processResultSyncCallback = do         s <-           byteStringToJSString . BLC8.toStrict . BLC8.unlines $             [ JSaddle.Files.ghcjsHelpers,@@ -46,14 +46,11 @@               "    const batch = JSON.parse(await readBatch());",               JSaddle.Files.runBatch                 (\r -> "processResult(JSON.stringify(" <> r <> "));")-                -- TODO Think more about how synchronous dispatching works here-                -- (see processSyncResult). For some reason, it already /seems/-                -- to work fine, at least in simple cases.-                Nothing,+                (Just \r -> "JSON.parse(processResultSync(JSON.stringify(" <> r <> ")))"),               "  }",               "})();"             ]-        evaluate =<< js_eval s processResultCallback readBatchCallback+        evaluate =<< js_eval s processResultCallback processResultSyncCallback readBatchCallback    runHelper entryPoint sendOutgoingMessage jsaddleRunner @@ -62,28 +59,39 @@   runHelper     entryPoint     (evaluate <=< js_postMessage worker)-    (evaluate <=< js_onMessage worker)+    (\processResult _processResultSync -> evaluate =<< js_onMessage worker processResult)  runHelper ::   JSM () ->   -- | How to send an outgoing message.   (JSString -> IO ()) ->-  -- | Start receiving incoming messages. For every message, invoke the given-  -- 'JSVal' callback.-  (JSVal -> IO ()) ->+  -- | Start receiving incoming messages. For every message, invoke one of the+  -- two given 'JSVal' callbacks (for sync/async processing, respectively).+  (JSVal -> JSVal -> IO ()) ->   IO () runHelper entryPoint sendOutgoingMessage onIncomingMessage = do-  (processResult, _processSyncResult, start) <-+  (processResult, processSyncResult, start) <-     runJavaScript sendBatch entryPoint -  processResultCallback <- mkPullCallback \s -> do-    bs <- jsStringToByteString s-    case A.eitherDecodeStrict bs of-      Left e -> fail $ "jsaddle: received invalid JSON: " <> show e-      Right r -> processResult r+  let receiveBatch :: JSString -> IO ()+      receiveBatch s = do+        bs <- jsStringToByteString s+        case A.eitherDecodeStrict bs of+          Left e -> fail $ "jsaddle: received invalid JSON: " <> show e+          Right r -> processResult r -  onIncomingMessage processResultCallback+      processBatchSync :: JSString -> IO JSString+      processBatchSync s = do+        bs <- jsStringToByteString s+        case A.eitherDecodeStrict bs of+          Left e -> fail $ "jsaddle: received invalid JSON: " <> show e+          Right r -> byteStringToJSString . BLC8.toStrict . A.encode =<< processSyncResult r +  processResultCallback <- mkPullCallback receiveBatch+  processResultSyncCallback <- mkSyncCallback processBatchSync++  onIncomingMessage processResultCallback processResultSyncCallback+   start   where     sendBatch :: Batch -> IO ()@@ -95,10 +103,12 @@  foreign import javascript "wrapper" mkPullCallback :: (JSString -> IO ()) -> IO JSVal +foreign import javascript "wrapper sync" mkSyncCallback :: (JSString -> IO JSString) -> IO JSVal+ foreign import javascript "wrapper" mkPushCallback :: IO JSString -> IO JSVal -foreign import javascript safe "new Function('processResult','readBatch',`(()=>{${$1}})()`)($2, $3)"-  js_eval :: JSString -> JSVal -> JSVal -> IO ()+foreign import javascript safe "new Function('processResult','processResultSync','readBatch',`(()=>{${$1}})()`)($2, $3, $4)"+  js_eval :: JSString -> JSVal -> JSVal -> JSVal -> IO ()  -- Worker 
src/Language/Javascript/JSaddle/Wasm.hs view
@@ -1,7 +1,7 @@ -- | See the [README](https://github.com/amesgen/jsaddle-wasm) for more details. ----- While this package also compiles on non-WASM GHCs for convenience, running--- this function will immediately fail.+-- While this package also compiles on non-Wasm GHCs for convenience, running+-- any function from this module will immediately fail in that case. module Language.Javascript.JSaddle.Wasm   ( run,     runWorker,@@ -17,7 +17,7 @@ import Language.Javascript.JSaddle.Wasm.Internal qualified as Internal import Language.Javascript.JSaddle.Wasm.JS (jsaddleScript) --- | Run a 'JSM' action via the WASM JavaScript FFI.+-- | Run a 'JSM' action via the Wasm JavaScript FFI. run :: JSM () -> IO () run = Internal.run