on master help sign in
Synchronous callbacks for jsaddle-wkwebview
commit675f74b7936ee980dc8b86bfc5bc467b5180ed3d authorHamish Mackenzie <Hamish.K.Mackenzie@googlemail.com> dateThu 4 May 2017, 17:01 (9 years ago) parent6d3c6a1
13 files changed+268−79
cabal.project+1 jsaddle-clib/src-ghc/Language/Javascript/JSaddle/CLib.hs+2−2 jsaddle-warp/src/Language/Javascript/JSaddle/WebSockets.hs+1−1 jsaddle-webkit2gtk/src/Language/Javascript/JSaddle/WebKitGTK.hs+30−9 jsaddle-webkitgtk/src/Language/Javascript/JSaddle/WebKitGTK.hs+2−2 jsaddle-wkwebview/cbits/WKWebView.m+31−8 jsaddle-wkwebview/src/Language/Javascript/JSaddle/WKWebView/Internal.hs+23−7 jsaddle/src-ghc/JavaScript/Array/Internal.hs−2 jsaddle/src/Language/Javascript/JSaddle/Native/Internal.hs+15−6 jsaddle/src/Language/Javascript/JSaddle/Object.hs+25−3 jsaddle/src/Language/Javascript/JSaddle/Run.hs+56−23 jsaddle/src/Language/Javascript/JSaddle/Run/Files.hs+75−13 jsaddle/src/Language/Javascript/JSaddle/Types.hs+7−3
cabal.project+1
@@ -6,6 +6,7 @@ packages:
66 jsaddle-wkwebview/
77-- jsaddle-webkitgtk/
88 jsaddle-webkit2gtk/
9+ jsaddle-clib/
910
1011package gi-atk
1112 flags: -overloaded-methods -overloaded-signals -overloaded-properties
jsaddle-clib/src-ghc/Language/Javascript/JSaddle/CLib.hs+2−2
@@ -35,7 +35,7 @@ foreign import ccall safe "wrapper"
3535jsaddleInit :: JSM () -> FunPtr (CString -> IO ()) -> IO (Ptr NativeCallbacks)
3636jsaddleInit jsm evaluateJavascriptAsyncPtr = do
3737 let evaluateJavascriptAsync = mkCallback evaluateJavascriptAsyncPtr
38− (processResult, start) <- runJavaScript (\batch ->
38+ (processResult, _, start) <- runJavaScript (\batch ->
3939 useAsCString (toStrict $ "runJSaddleBatch(" <> encode batch <> ");") $
4040 evaluateJavascriptAsync) jsm
4141 jsaddleStartPtr <- wrapStartCallback $ void $ forkIO $ start
@@ -58,7 +58,7 @@ jsaddleJs = ghcjsHelpers <> "\
5858 \runJSaddleBatch = (function() {\n\
5959 \ " <> initState <> "\n\
6060 \ return function(batch) {\n\
61− \ " <> runBatch (\a -> "jsaddle.postMessage(JSON.stringify(" <> a <> "));") <> "\
61+ \ " <> runBatch (\a -> "jsaddle.postMessage(JSON.stringify(" <> a <> "));") Nothing <> "\
6262 \ };\n\
6363 \})();\n\
6464 \jsaddle.postReady();\n"
jsaddle-warp/src/Language/Javascript/JSaddle/WebSockets.hs+1−1
@@ -48,7 +48,7 @@ jsaddleOr opts entryPoint = websocketsOr opts wsApp
4848 wsApp :: ServerApp
4949 wsApp pending_conn = do
5050 conn <- acceptRequest pending_conn
51− (processResult, start) <- runJavaScript (sendTextData conn . encode) entryPoint
51+ (processResult, _, start) <- runJavaScript (sendTextData conn . encode) entryPoint
5252 _ <- forkIO . forever $
5353 receiveDataMessage conn >>= \case
5454 (WS.Text t) ->
jsaddle-webkit2gtk/src/Language/Javascript/JSaddle/WebKitGTK.hs+30−9
@@ -67,9 +67,12 @@ import GI.Gtk
6767import GI.Gio (noCancellable)
6868import GI.JavaScriptCore (Value(..), GlobalContext(..))
6969import GI.WebKit2
70− (WebView, setSettingsEnableWriteConsoleMessagesToStdout,
71− setSettingsEnableJavascript,
72− webViewNewWithUserContentManager, userContentManagerNew,
70+ (scriptDialogPromptSetText, scriptDialogPromptGetDefaultText,
71+ scriptDialogGetMessage, scriptDialogGetDialogType,
72+ onWebViewScriptDialog, WebView,
73+ setSettingsEnableWriteConsoleMessagesToStdout,
74+ setSettingsEnableJavascript, webViewNewWithUserContentManager,
75+ userContentManagerNew,
7376 userContentManagerRegisterScriptMessageHandler,
7477 javascriptResultGetValue, javascriptResultGetGlobalContext,
7578 webViewGetUserContentManager,
@@ -78,7 +81,7 @@ import GI.WebKit2
7881 webViewRunJavascript, LoadEvent(..),
7982 UserContentManagerScriptMessageReceivedCallback, webViewLoadHtml,
8083 onWebViewLoadChanged, setSettingsEnableDeveloperExtras,
81− webViewSetSettings, webViewGetSettings)
84+ webViewSetSettings, webViewGetSettings, ScriptDialogType(..))
8285
8386import Graphics.UI.Gtk.WebKit.JavaScriptCore.JSBase
8487 (JSValueRef, JSContextRef)
@@ -87,7 +90,7 @@ import Graphics.UI.Gtk.WebKit.JavaScriptCore.JSStringRef
8790import Graphics.UI.Gtk.WebKit.JavaScriptCore.JSValueRef
8891 (jsvaluetostringcopy)
8992
90−import Language.Javascript.JSaddle (JSM, Results)
93+import Language.Javascript.JSaddle (JSM, Results, Batch)
9194import Language.Javascript.JSaddle.Run (runJavaScript)
9295import Language.Javascript.JSaddle.Run.Files (initState, runBatch, ghcjsHelpers)
9396
@@ -151,11 +154,11 @@ run main = do
151154
152155runInWebView :: JSM () -> WebView -> IO ()
153156runInWebView f webView = do
154− (processResults, start) <- runJavaScript (\batch -> postGUIAsync $
157+ (processResults, syncResults, start) <- runJavaScript (\batch -> postGUIAsync $
155158 webViewRunJavascript webView (decodeUtf8 . toStrict $ "runJSaddleBatch(" <> encode batch <> ");") noCancellable Nothing)
156159 f
157160
158− addJSaddleHandler webView processResults
161+ addJSaddleHandler webView processResults syncResults
159162 webViewRunJavascript webView (decodeUtf8 $ toStrict jsaddleJs) noCancellable . Just $
160163 \_obj _asyncResult ->
161164 void $ forkIO start
@@ -170,8 +173,8 @@ connectUserContentManagerScriptMessageReceived obj cb after = liftIO $ do
170173 cb'' <- mk_UserContentManagerScriptMessageReceivedCallback cb'
171174 connectSignalFunPtr obj "script-message-received::jsaddle" cb'' after
172175
173−addJSaddleHandler :: WebView -> (Results -> IO ()) -> IO ()
174−addJSaddleHandler webView processResult = do
176+addJSaddleHandler :: WebView -> (Results -> IO ()) -> (Results -> IO Batch) -> IO ()
177+addJSaddleHandler webView processResult syncResults = do
175178 manager <- webViewGetUserContentManager webView
176179 _ <- onUserContentManagerScriptMessageReceived manager $ \result -> do
177180 ctx <- javascriptResultGetGlobalContext result
@@ -180,6 +183,21 @@ addJSaddleHandler webView processResult = do
180183 withJSValueRef arg $ \argRef ->
181184 encodeUtf8 <$> valueToText ctxRef argRef
182185 mapM_ processResult (decode (fromStrict bs))
186+ _ <- onWebViewScriptDialog webView $ \dialog ->
187+ scriptDialogGetDialogType dialog >>= \case
188+ ScriptDialogTypePrompt ->
189+ scriptDialogGetMessage dialog >>= \case
190+ "JSaddleSync" -> do
191+ resultsText <- scriptDialogPromptGetDefaultText dialog
192+ case decode (fromStrict $ encodeUtf8 resultsText) of
193+ Just results -> do
194+ batch <- syncResults results
195+ scriptDialogPromptSetText dialog (decodeUtf8 . toStrict $ encode batch)
196+ return True
197+ Nothing -> return False
198+ _ -> return False
199+ _ -> return False
200+
183201 void $ userContentManagerRegisterScriptMessageHandler manager "jsaddle"
184202
185203jsaddleJs :: LB.ByteString
@@ -188,6 +206,9 @@ jsaddleJs = ghcjsHelpers <> mconcat
188206 , initState
189207 , "\nreturn function(batch) {\n"
190208 , runBatch (\a -> "window.webkit.messageHandlers.jsaddle.postMessage(JSON.stringify(" <> a <> "));\n")
209+ Nothing
210+ -- TODO fix https://github.com/haskell-gi/haskell-gi/issues/97 then use
211+ -- (Just (\a -> "JSON.parse(window.prompt(\"JSaddleSync\", JSON.stringify(" <> a <> ")))"))
191212 , "};\n"
192213 , "})()"
193214 ]
jsaddle-webkitgtk/src/Language/Javascript/JSaddle/WebKitGTK.hs+2−2
@@ -185,7 +185,7 @@ runInWebView f webView = do
185185 jsvalueprotect ctxRef result
186186 return result
187187
188− (processResults, start) <- runJavaScript (\batch -> postGUIAsync $
188+ (processResults, _, start) <- runJavaScript (\batch -> postGUIAsync $
189189 withJSContextRef ctx $ \ctxRef ->
190190 withJSString (decodeUtf8 . toStrict $ encode batch) $ \script -> do
191191 val <- jsvaluemakestring ctxRef script
@@ -222,7 +222,7 @@ jsaddleJs = ghcjsHelpers <> mconcat
222222 , initState
223223 , "\nreturn function(batchJSON) {\n"
224224 , " var batch = JSON.parse(batchJSON);\n"
225− , runBatch (\a -> "window.jsaddleCallback(JSON.stringify(" <> a <> "));\n")
225+ , runBatch (\a -> "window.jsaddleCallback(JSON.stringify(" <> a <> "));\n") Nothing
226226 , "};\n"
227227 , "})()\n"
228228 ]
jsaddle-wkwebview/cbits/WKWebView.m+31−8
@@ -4,26 +4,30 @@
44
55BOOL openApp(NSURL * url);
66
7−extern void jsaddleStart(HsStablePtr);
8−extern void jsaddleResult(HsStablePtr, const char * _Nonnull result);
9−
10−@interface JSaddleHandler : NSObject<WKNavigationDelegate, WKScriptMessageHandler>
7+@interface JSaddleHandler : NSObject<WKNavigationDelegate, WKScriptMessageHandler, WKUIDelegate>
118
129@property (nonatomic, assign) HsStablePtr startHandler;
1310@property (nonatomic, assign) HsStablePtr resultHandler;
11+@property (nonatomic, assign) HsStablePtr syncHandler;
12+@property (nonatomic, assign) void (^completionHandler)(NSString *result);
1413
15−- (instancetype)initHandler:(HsStablePtr)startHandler resultHandler:(HsStablePtr)resultHandler;
14+- (instancetype)initHandler:(HsStablePtr)startHandler resultHandler:(HsStablePtr)resultHandler syncHandler:(HsStablePtr)syncHandler;
1615
1716@end
1817
18+extern void jsaddleStart(HsStablePtr);
19+extern void jsaddleResult(HsStablePtr, const char * _Nonnull result);
20+extern void jsaddleSyncResult(HsStablePtr, JSaddleHandler *, const char * _Nonnull result);
21+
1922@implementation JSaddleHandler
2023
21−- (instancetype)initHandler:(HsStablePtr)startHandler resultHandler:(HsStablePtr)resultHandler
24+- (instancetype)initHandler:(HsStablePtr)startHandler resultHandler:(HsStablePtr)resultHandler syncHandler:(HsStablePtr)syncHandler
2225{
2326 self = [super init];
2427 if (self) {
2528 _startHandler = startHandler;
2629 _resultHandler = resultHandler;
30+ _syncHandler = syncHandler;
2731 }
2832 return self;
2933}
@@ -39,6 +43,18 @@ extern void jsaddleResult(HsStablePtr, const char * _Nonnull result);
3943 jsaddleResult(self.resultHandler, [s UTF8String]);
4044}
4145
46+- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt
47+ defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *result))completionHandler
48+{
49+ if([prompt isEqualToString:@"JSaddleSync"]) {
50+ _completionHandler = completionHandler;
51+ jsaddleSyncResult(self.syncHandler, self, [defaultText UTF8String]);
52+ }
53+ else {
54+ // TODO decide what if anything should go here
55+ }
56+}
57+
4258- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
4359{
4460 NSURL *url = navigationAction.request.URL;
@@ -55,10 +71,11 @@ extern void jsaddleResult(HsStablePtr, const char * _Nonnull result);
5571
5672@end
5773
58−void addJSaddleHandler(WKWebView *webView, HsStablePtr startHandler, HsStablePtr resultHandler) {
59− JSaddleHandler * handler = [[JSaddleHandler alloc] initHandler:startHandler resultHandler:resultHandler];
74+void addJSaddleHandler(WKWebView *webView, HsStablePtr startHandler, HsStablePtr resultHandler, HsStablePtr syncHandler) {
75+ JSaddleHandler * handler = [[JSaddleHandler alloc] initHandler:startHandler resultHandler:resultHandler syncHandler:syncHandler];
6076 [[[webView configuration] userContentController] addScriptMessageHandler:handler name:@"jsaddle"];
6177 webView.navigationDelegate = handler;
78+ webView.UIDelegate = handler;
6279}
6380
6481void evaluateJavaScript(WKWebView *webView, const char * _Nonnull js) {
@@ -69,6 +86,12 @@ void evaluateJavaScript(WKWebView *webView, const char * _Nonnull js) {
6986 });
7087}
7188
89+void completeSync(JSaddleHandler *handler, const char * _Nonnull s) {
90+ NSString *string = [[NSString alloc] initWithCString:s encoding:NSUTF8StringEncoding];
91+ handler.completionHandler(string);
92+ handler.completionHandler = NULL;
93+}
94+
7295void loadHTMLString(WKWebView *webView, const char * _Nonnull html) {
7396 NSString *htmlString = [NSString stringWithCString:html encoding:NSUTF8StringEncoding];
7497 dispatch_async(dispatch_get_main_queue(), ^{
jsaddle-wkwebview/src/Language/Javascript/JSaddle/WKWebView/Internal.hs+23−7
@@ -20,24 +20,27 @@ import Foreign.C.String (CString)
2020import Foreign.Ptr (Ptr, nullPtr)
2121import Foreign.StablePtr (StablePtr, newStablePtr, deRefStablePtr)
2222
23−import Language.Javascript.JSaddle (Results, JSM)
23+import Language.Javascript.JSaddle (Results, Batch, JSM)
2424import Language.Javascript.JSaddle.Run (runJavaScript)
2525import Language.Javascript.JSaddle.Run.Files (initState, runBatch, ghcjsHelpers)
2626
2727newtype WKWebView = WKWebView (Ptr WKWebView)
28+newtype JSaddleHandler = JSaddleHandler (Ptr JSaddleHandler)
2829
2930foreign export ccall jsaddleStart :: StablePtr (IO ()) -> IO ()
3031foreign export ccall jsaddleResult :: StablePtr (Results -> IO ()) -> CString -> IO ()
32+foreign export ccall jsaddleSyncResult :: StablePtr (Results -> IO Batch) -> JSaddleHandler -> CString -> IO ()
3133foreign export ccall withWebView :: WKWebView -> StablePtr (WKWebView -> IO ()) -> IO ()
32−foreign import ccall addJSaddleHandler :: WKWebView -> StablePtr (IO ()) -> StablePtr (Results -> IO ()) -> IO ()
34+foreign import ccall addJSaddleHandler :: WKWebView -> StablePtr (IO ()) -> StablePtr (Results -> IO ()) -> StablePtr (Results -> IO Batch) -> IO ()
3335foreign import ccall loadHTMLString :: WKWebView -> CString -> IO ()
3436foreign import ccall loadBundleFile :: WKWebView -> CString -> CString -> IO ()
3537foreign import ccall evaluateJavaScript :: WKWebView -> CString -> IO ()
38+foreign import ccall completeSync :: JSaddleHandler -> CString -> IO ()
3639foreign import ccall mainBundleResourcePathC :: IO CString
3740
3841-- | Run JSaddle in WKWebView
3942jsaddleMain :: JSM () -> WKWebView -> IO ()
40−jsaddleMain f webView = do
43+jsaddleMain f webView =
4144 jsaddleMain' f webView $
4245 useAsCString (toStrict indexHtml) $ loadHTMLString webView
4346
@@ -46,7 +49,7 @@ jsaddleMain f webView = do
4649jsaddleMainFile :: BS.ByteString -- ^ The file to navigate to.
4750 -> BS.ByteString -- ^ The path to allow read access to.
4851 -> JSM () -> WKWebView -> IO ()
49−jsaddleMainFile url allowing f webView = do
52+jsaddleMainFile url allowing f webView =
5053 jsaddleMain' f webView $
5154 useAsCString url $ \u ->
5255 useAsCString allowing $ \a ->
@@ -56,14 +59,15 @@ jsaddleMain' :: JSM () -> WKWebView -> IO () -> IO ()
5659jsaddleMain' f webView loadHtml = do
5760 ready <- newEmptyMVar
5861
59− (processResult, start) <- runJavaScript (\batch ->
62+ (processResult, syncResult, start) <- runJavaScript (\batch ->
6063 useAsCString (toStrict $ "runJSaddleBatch(" <> encode batch <> ");") $
6164 evaluateJavaScript webView)
6265 f
6366
6467 startHandler <- newStablePtr (putMVar ready ())
6568 resultHandler <- newStablePtr processResult
66− addJSaddleHandler webView startHandler resultHandler
69+ syncResultHandler <- newStablePtr syncResult
70+ addJSaddleHandler webView startHandler resultHandler syncResultHandler
6771 loadHtml
6872 void . forkOS $ do
6973 takeMVar ready
@@ -80,6 +84,17 @@ jsaddleResult ptrHandler s = do
8084 Nothing -> error $ "jsaddle WebSocket decode failed : " <> show result
8185 Just r -> processResult r
8286
87+jsaddleSyncResult :: StablePtr (Results -> IO Batch) -> JSaddleHandler -> CString -> IO ()
88+jsaddleSyncResult ptrHandler jsaddleHandler s = do
89+ syncProcessResult <- deRefStablePtr ptrHandler
90+ result <- packCString s
91+ case decode (fromStrict result) of
92+ Nothing -> error $ "jsaddle WebSocket decode failed : " <> show result
93+ Just r -> do -- void . forkIO $ do
94+ batch <- syncProcessResult r
95+ useAsCString (toStrict $ encode batch) $
96+ completeSync jsaddleHandler
97+
8398withWebView :: WKWebView -> StablePtr (WKWebView -> IO ()) -> IO ()
8499withWebView webView ptrF = do
85100 f <- deRefStablePtr ptrF
@@ -90,7 +105,8 @@ jsaddleJs = ghcjsHelpers <> "\
90105 \runJSaddleBatch = (function() {\n\
91106 \ " <> initState <> "\n\
92107 \ return function(batch) {\n\
93− \ " <> runBatch (\a -> "window.webkit.messageHandlers.jsaddle.postMessage(JSON.stringify(" <> a <> "));") <> "\
108+ \ " <> runBatch (\a -> "window.webkit.messageHandlers.jsaddle.postMessage(JSON.stringify(" <> a <> "));\n")
109+ (Just (\a -> "JSON.parse(window.prompt(\"JSaddleSync\", JSON.stringify(" <> a <> ")))")) <> "\
94110 \ };\n\
95111 \})();\n\
96112 \"
jsaddle/src-ghc/JavaScript/Array/Internal.hs−2
@@ -21,8 +21,6 @@ import Data.JSString.Internal.Type (JSString(..))
2121import Language.Javascript.JSaddle.Types (JSM, SomeJSArray(..), JSArray, MutableJSArray, STJSArray, Object(..), GHCJSPure(..))
2222import Language.Javascript.JSaddle.Native.Internal
2323 (newArray, getPropertyByName, getPropertyAtIndex, callAsFunction, valueToNumber)
24−import Language.Javascript.JSaddle.Run
25− (Command(..), Result(..), AsyncCommand(..), sendCommand, sendLazyCommand)
2624
2725create :: JSM MutableJSArray
2826create = SomeJSArray <$> newArray []
jsaddle/src/Language/Javascript/JSaddle/Native/Internal.hs+15−6
@@ -27,7 +27,8 @@ module Language.Javascript.JSaddle.Native.Internal (
2727 , callAsFunction
2828 , callAsConstructor
2929 , newEmptyObject
30− , newCallback
30+ , newAsyncCallback
31+ , newSyncCallback
3132 , newArray
3233 , evaluateScript
3334 , deRefVal
@@ -54,7 +55,7 @@ import Language.Javascript.JSaddle.Types
5455 JSStringReceived(..), JSStringForSend(..), JSObjectForSend(..))
5556import Language.Javascript.JSaddle.Monad (askJSM)
5657import Language.Javascript.JSaddle.Run
57− (Command(..), AsyncCommand(..), Result(..), sendCommand,
58+ (Command(..), Result(..), sendCommand,
5859 sendAsyncCommand, sendLazyCommand, wrapJSVal)
5960
6061wrapJSString :: MonadIO m => JSStringReceived -> m JSString
@@ -136,13 +137,21 @@ newEmptyObject :: JSM Object
136137newEmptyObject = Object <$> sendLazyCommand NewEmptyObject
137138{-# INLINE newEmptyObject #-}
138139
139−newCallback :: JSCallAsFunction -> JSM Object
140−newCallback f = do
141− object <- Object <$> sendLazyCommand NewCallback
140+newAsyncCallback :: JSCallAsFunction -> JSM Object
141+newAsyncCallback f = do
142+ object <- Object <$> sendLazyCommand NewAsyncCallback
142143 add <- addCallback <$> askJSM
143144 liftIO $ add object f
144145 return object
145−{-# INLINE newCallback #-}
146+{-# INLINE newAsyncCallback #-}
147+
148+newSyncCallback :: JSCallAsFunction -> JSM Object
149+newSyncCallback f = do
150+ object <- Object <$> sendLazyCommand NewSyncCallback
151+ add <- addCallback <$> askJSM
152+ liftIO $ add object f
153+ return object
154+{-# INLINE newSyncCallback #-}
146155
147156newArray :: [JSVal] -> JSM JSVal
148157newArray xs = withJSVals xs $ \xs' -> sendLazyCommand (NewArray xs')
jsaddle/src/Language/Javascript/JSaddle/Object.hs+25−3
@@ -66,6 +66,7 @@ module Language.Javascript.JSaddle.Object (
6666 -- * Calling Haskell From JavaScript
6767 , Function(..)
6868 , function
69+ , asyncFunction
6970 , freeFunction
7071 , fun
7172 , JSCallAsFunction
@@ -115,7 +116,7 @@ import Language.Javascript.JSaddle.Types
115116#else
116117import GHCJS.Marshal.Internal (ToJSVal(..))
117118import Language.Javascript.JSaddle.Native
118− (newCallback, callAsFunction, callAsConstructor)
119+ (newAsyncCallback, newSyncCallback, callAsFunction, callAsConstructor)
119120import Language.Javascript.JSaddle.Monad (askJSM, JSM)
120121import Language.Javascript.JSaddle.Types
121122 (JSString, Object(..), SomeJSArray(..),
@@ -436,8 +437,10 @@ newtype Function = Function {functionObject :: Object}
436437
437438
438439-- ^ Make a JavaScript function object that wraps a Haskell function.
440+-- Calls made to the function will be synchronous where possible
441+-- (on GHCJS it uses on `syncCallback2` with `ContinueAsync`).
439442function :: JSCallAsFunction -- ^ Haskell function to call
440− -> JSM Function -- ^ Returns a JavaScript function object that will
443+ -> JSM Function -- ^ Returns a JavaScript function object that will
441444 -- call the Haskell one when it is called
442445#ifdef ghcjs_HOST_OS
443446function f = do
@@ -449,7 +452,26 @@ foreign import javascript unsafe "$r = function () { $1(this, arguments); }"
449452 makeFunctionWithCallback :: Callback (JSVal -> JSVal -> IO ()) -> IO Object
450453#else
451454function f = do
452− object <- newCallback f
455+ object <- newSyncCallback f
456+ return $ Function object
457+#endif
458+
459+-- ^ Make a JavaScript function object that wraps a Haskell function.
460+-- Calls made to the function will be Asynchronous.
461+asyncFunction :: JSCallAsFunction -- ^ Haskell function to call
462+ -> JSM Function -- ^ Returns a JavaScript function object that will
463+ -- call the Haskell one when it is called
464+#ifdef ghcjs_HOST_OS
465+asyncFunction f = do
466+ callback <- asyncCallback2 $ \this args -> do
467+ rargs <- Array.toListIO (coerce args)
468+ f this this rargs -- TODO pass function object through
469+ Function callback <$> makeFunctionWithCallback callback
470+foreign import javascript unsafe "$r = function () { $1(this, arguments); }"
471+ makeFunctionWithCallback :: Callback (JSVal -> JSVal -> IO ()) -> IO Object
472+#else
473+asyncFunction f = do
474+ object <- newAsyncCallback f
453475 return $ Function object
454476#endif
455477
jsaddle/src/Language/Javascript/JSaddle/Run.hs+56−23
@@ -49,7 +49,7 @@ import Control.Concurrent.STM.TChan
4949import Control.Concurrent.STM.TVar
5050 (writeTVar, readTVar, readTVarIO, modifyTVar', newTVarIO)
5151import Control.Concurrent.MVar
52− (MVar, MVar, putMVar, takeMVar, newEmptyMVar)
52+ (tryTakeMVar, MVar, putMVar, takeMVar, newEmptyMVar)
5353
5454import System.IO.Unsafe (unsafeInterleaveIO)
5555import System.Mem.Weak (addFinalizer)
@@ -67,6 +67,8 @@ import Language.Javascript.JSaddle.Exception (JSException(..))
6767-- import Language.Javascript.JSaddle.Native.Internal (wrapJSVal)
6868import Control.DeepSeq (deepseq)
6969import GHC.Stats (getGCStatsEnabled, getGCStats, GCStats(..))
70+import Data.Maybe (isJust)
71+import Data.Foldable (forM_)
7072#endif
7173
7274-- | Enable (or disable) JSaddle logging
@@ -146,10 +148,12 @@ sendAsyncCommand cmd = do
146148 s <- doSendAsyncCommand <$> JSM ask
147149 liftIO $ s cmd
148150
149−runJavaScript :: (Batch -> IO ()) -> JSM () -> IO (Results -> IO (), IO ())
151+runJavaScript :: (Batch -> IO ()) -> JSM () -> IO (Results -> IO (), Results -> IO Batch, IO ())
150152runJavaScript sendBatch entryPoint = do
151153 startTime' <- getCurrentTime
152154 recvMVar <- newEmptyMVar
155+ syncSendBatch <- newEmptyMVar
156+ lastAsyncBatch <- newEmptyMVar
153157 commandChan <- newTChanIO
154158 callbacks <- newTVarIO M.empty
155159 nextRef' <- newTVarIO 0
@@ -169,17 +173,39 @@ runJavaScript sendBatch entryPoint = do
169173 , nextRef = nextRef'
170174 , doEnableLogging = atomicWriteIORef loggingEnabled
171175 }
172− let processResults = \case
176+ let processResults :: Bool -> Results -> IO ()
177+ processResults syncCallbacks = \case
173178 (ProtocolError err) -> error $ "Protocol error : " <> T.unpack err
174179 (Callback f this a) -> do
175− logInfo ("Call " <>)
176180 f'@(JSVal fNumber) <- runReaderT (unJSM $ wrapJSVal f) ctx
177181 this' <- runReaderT (unJSM $ wrapJSVal this) ctx
178182 args <- runReaderT (unJSM $ mapM wrapJSVal a) ctx
183+ logInfo (("Call " <> show fNumber <> " ") <>)
179184 (M.lookup fNumber <$> liftIO (readTVarIO callbacks)) >>= \case
180185 Nothing -> liftIO $ putStrLn "Callback called after it was freed"
181− Just cb -> void . forkIO $ runReaderT (unJSM $ cb f' this' args) ctx
186+ Just cb -> void . forkIO $ do
187+ runReaderT (unJSM $ cb f' this' args) ctx
188+ when syncCallbacks $ do
189+ doSendAsyncCommand ctx EndSyncBlock
190+ void $ doSendCommand ctx Sync
191+ Duplicate -> error "Error : Unexpected Duplicate"
182192 m -> putMVar recvMVar m
193+ asyncResults :: Results -> IO ()
194+ asyncResults = processResults False
195+ syncResults :: Results -> IO Batch
196+ syncResults results = do
197+ existing <- tryTakeMVar syncSendBatch
198+ when (isJust existing) $ error "Error : Unexpected Sync Results"
199+ tryTakeMVar lastAsyncBatch >>= \case
200+ Just lastAsync -> processResults True results >> return lastAsync
201+ Nothing -> do
202+ nextBatch <- newEmptyMVar
203+ putMVar syncSendBatch (\batch -> do
204+ existing <- tryTakeMVar nextBatch
205+ when (isJust existing) $ error "Error : Unexpected Sync Results"
206+ putMVar nextBatch batch)
207+ processResults True results
208+ takeMVar nextBatch
183209 logInfo s =
184210 readIORef loggingEnabled >>= \case
185211 True -> do
@@ -189,23 +215,30 @@ runJavaScript sendBatch entryPoint = do
189215 cbCount <- M.size <$> readTVarIO callbacks
190216 putStrLn . s $ "M " <> currentBytesUsedStr <> "CB " <> show cbCount
191217 False -> return ()
192− _ <- forkIO . forever $ readBatch commandChan >>= \case
193− (batch@(Batch cmds _), resultMVars) -> do
218+ _ <- forkIO . forM_ [1..] $ \nBatch ->
219+ readBatch nBatch commandChan >>= \case
220+ (batch@(Batch cmds _ _), resultMVars) -> do
194221 logInfo (\x -> "Sync " <> x <> show (length cmds, last cmds))
195− sendBatch batch
196− takeMVar recvMVar >>= \case
197− Success results | length results /= length resultMVars -> error "Unexpected number of jsaddle results"
198− | otherwise -> zipWithM_ putMVar resultMVars results
199− Failure results exception -> do
200− -- The exception will only be rethrown in Haskell if/when one of the
201− -- missing results (if any) is evaluated.
202− putStrLn "A JavaScript exception was thrown! (may not reach Haskell code)"
203− zipWithM_ putMVar resultMVars $ results <> repeat (ThrowJSValue exception)
204− _ -> error "Unexpected jsaddle results"
205− return (processResults, runReaderT (unJSM entryPoint) ctx)
222+ tryTakeMVar syncSendBatch >>= \case
223+ Just send -> send batch
224+ Nothing -> do
225+ putMVar lastAsyncBatch batch
226+ sendBatch batch
227+ takeMVar recvMVar >>= \r -> do
228+ _ <- tryTakeMVar lastAsyncBatch
229+ case r of
230+ Success results | length results /= length resultMVars -> error "Unexpected number of jsaddle results"
231+ | otherwise -> zipWithM_ putMVar resultMVars results
232+ Failure results exception -> do
233+ -- The exception will only be rethrown in Haskell if/when one of the
234+ -- missing results (if any) is evaluated.
235+ putStrLn "A JavaScript exception was thrown! (may not reach Haskell code)"
236+ zipWithM_ putMVar resultMVars $ results <> repeat (ThrowJSValue exception)
237+ _ -> error "Unexpected jsaddle results"
238+ return (asyncResults, syncResults, runReaderT (unJSM entryPoint) ctx)
206239 where
207− readBatch :: TChan (Either AsyncCommand (Command, MVar Result)) -> IO (Batch, [MVar Result])
208− readBatch chan = do
240+ readBatch :: Int -> TChan (Either AsyncCommand (Command, MVar Result)) -> IO (Batch, [MVar Result])
241+ readBatch nBatch chan = do
209242 first <- atomically $ readTChan chan -- We want at least one command to send
210243 loop first ([], [])
211244 where
@@ -216,17 +249,17 @@ runJavaScript sendBatch entryPoint = do
216249 let cmds = Right syncCmd:cmds'
217250 resultMVars = resultMVar:resultMVars'
218251 atomically (tryReadTChan chan) >>= \case
219− Nothing -> return (Batch (reverse cmds) False, reverse resultMVars)
252+ Nothing -> return (Batch (reverse cmds) False nBatch, reverse resultMVars)
220253 Just cmd -> loop cmd (cmds, resultMVars)
221254 loop (Left asyncCmd) (cmds', resultMVars) = do
222255 let cmds = Left asyncCmd:cmds'
223256 atomically (tryReadTChan chan) >>= \case
224− Nothing -> return (Batch (reverse cmds) False, reverse resultMVars)
257+ Nothing -> return (Batch (reverse cmds) False nBatch, reverse resultMVars)
225258 Just cmd -> loop cmd (cmds, resultMVars)
226259 -- When we have seen a SyncWithAnimationFrame command only a synchronous command should end the batch
227260 loopAnimation :: Either AsyncCommand (Command, MVar Result) -> ([Either AsyncCommand Command], [MVar Result]) -> IO (Batch, [MVar Result])
228261 loopAnimation (Right (Sync, resultMVar)) (cmds, resultMVars) =
229− return (Batch (reverse (Right Sync:cmds)) True, reverse (resultMVar:resultMVars))
262+ return (Batch (reverse (Right Sync:cmds)) True nBatch, reverse (resultMVar:resultMVars))
230263 loopAnimation (Right (syncCmd, resultMVar)) (cmds, resultMVars) =
231264 atomically (readTChan chan) >>= \cmd -> loopAnimation cmd (Right syncCmd:cmds, resultMVar:resultMVars)
232265 loopAnimation (Left asyncCmd) (cmds, resultMVars) =
jsaddle/src/Language/Javascript/JSaddle/Run/Files.hs+75−13
@@ -43,13 +43,20 @@ initState = "\
4343 \ jsaddle_values.set(3, true);\n\
4444 \ jsaddle_values.set(4, window);\n\
4545 \ var jsaddle_index = 100;\n\
46+ \ var expectedBatch = 1;\n\
4647 \"
4748
48−runBatch :: (ByteString -> ByteString) -> ByteString
49−runBatch send = "\
49+runBatch :: (ByteString -> ByteString) -> Maybe (ByteString -> ByteString) -> ByteString
50+runBatch send sendSync = "\
51+ \ var runBatch = function(firstBatch, initialSyncDepth) {\n\
5052 \ var processBatch = function(timestamp) {\n\
51− \ var results = [];\n\
52− \ try {\n\
53+ \ var batch = firstBatch;\n\
54+ \ var results = [];\n\
55+ \ try {\n\
56+ \ syncDepth = initialSyncDepth || 0;\n\
57+ \ for(;;){\n\
58+ \ if(batch[2] === expectedBatch) {\n\
59+ \ expectedBatch++;\n\
5360 \ var nCommandsLength = batch[0].length;\n\
5461 \ for (var nCommand = 0; nCommand != nCommandsLength; nCommand++) {\n\
5562 \ var cmd = batch[0][nCommand];\n\
@@ -89,7 +96,7 @@ runBatch send = "\
8996 \ var n = d.contents;\n\
9097 \ jsaddle_values.set(n, {});\n\
9198 \ break;\n\
92− \ case \"NewCallback\":\n\
99+ \ case \"NewAsyncCallback\":\n\
93100 \ (function() {\n\
94101 \ var nFunction = d.contents;\n\
95102 \ jsaddle_values.set(nFunction, function() {\n\
@@ -104,6 +111,26 @@ runBatch send = "\
104111 \ " <> send "{\"tag\": \"Callback\", \"contents\": [nFunction, nThis, args]}" <> "\n\
105112 \ })})();\n\
106113 \ break;\n\
114+ \ case \"NewSyncCallback\":\n\
115+ \ (function() {\n\
116+ \ var nFunction = d.contents;\n\
117+ \ jsaddle_values.set(nFunction, function() {\n\
118+ \ var nThis = ++jsaddle_index;\n\
119+ \ jsaddle_values.set(nThis, this);\n\
120+ \ var args = [];\n\
121+ \ for (var i = 0; i != arguments.length; i++) {\n\
122+ \ var nArg = ++jsaddle_index;\n\
123+ \ jsaddle_values.set(nArg, arguments[i]);\n\
124+ \ args[i] = nArg;\n\
125+ \ }\n" <> (
126+ case sendSync of
127+ Just s ->
128+ " runBatch(" <> s "{\"tag\": \"Callback\", \"contents\": [nFunction, nThis, args]}" <> ", 1);\n"
129+ Nothing ->
130+ " " <> send "{\"tag\": \"Callback\", \"contents\": [nFunction, nThis, args]}" <> "\n"
131+ ) <>
132+ " })})();\n\
133+ \ break;\n\
107134 \ case \"CallAsFunction\":\n\
108135 \ var n = d.contents[3];\n\
109136 \ jsaddle_values.set(n,\n\
@@ -148,6 +175,12 @@ runBatch send = "\
148175 \ var n = d.contents[0];\n\
149176 \ jsaddle_values.set(n, timestamp);\n\
150177 \ break;\n\
178+ \ case \"StartSyncBlock\":\n\
179+ \ syncDepth++;\n\
180+ \ break;\n\
181+ \ case \"EndSyncBlock\":\n\
182+ \ syncDepth--;\n\
183+ \ break;\n\
151184 \ default:\n\
152185 \ " <> send "{\"tag\": \"ProtocolError\", \"contents\": e.data}" <> "\n\
153186 \ return;\n\
@@ -210,20 +243,49 @@ runBatch send = "\
210243 \ }\n\
211244 \ }\n\
212245 \ }\n\
213− \ " <> send "{\"tag\": \"Success\", \"contents\": results}" <> "\n\
214− \ }\n\
215− \ catch (err) {\n\
216− \ var n = ++jsaddle_index;\n\
217− \ jsaddle_values.set(n, err);\n\
218− \ " <> send "{\"tag\": \"Failure\", \"contents\": [results,n]}" <> "\n\
246+ \ if(syncDepth === 0) {\n\
247+ \ " <> send "{\"tag\": \"Success\", \"contents\": results}" <> "\n\
248+ \ break;\n\
249+ \ } else {\n" <> (
250+ case sendSync of
251+ Just s ->
252+ " batch = " <> s "{\"tag\": \"Success\", \"contents\": results}" <> "\n\
253+ \ results = [];\n"
254+ Nothing ->
255+ " " <> send "{\"tag\": \"Success\", \"contents\": results}" <> "\n\
256+ \ break;\n"
257+ ) <>
258+ " }\n\
259+ \ } else {\n\
260+ \ if(syncDepth === 0) {\n\
261+ \ break;\n\
262+ \ } else {\n" <> (
263+ case sendSync of
264+ Just s ->
265+ " batch = " <> s "{\"tag\": \"Duplicate\"}" <> "\n\
266+ \ results = [];\n"
267+ Nothing ->
268+ " " <> send "{\"tag\": \"Duplicate\"}" <> "\n\
269+ \ break;\n"
270+ ) <>
271+ " }\n\
272+ \ }\n\
219273 \ }\n\
274+ \ }\n\
275+ \ catch (err) {\n\
276+ \ var n = ++jsaddle_index;\n\
277+ \ jsaddle_values.set(n, err);\n\
278+ \ " <> send "{\"tag\": \"Failure\", \"contents\": [results,n]}" <> "\n\
279+ \ }\n\
220280 \ };\n\
221− \ if(batch[1]) {\n\
281+ \ if(batch[1] && (initialSyncDepth || 0) === 0) {\n\
222282 \ window.requestAnimationFrame(processBatch);\n\
223283 \ }\n\
224284 \ else {\n\
225285 \ processBatch(window.performance ? window.performance.now() : null);\n\
226286 \ }\n\
287+ \ };\n\
288+ \ runBatch(batch);\n\
227289 \"
228290
229291-- Use this to generate this string for embedding
@@ -249,7 +311,7 @@ jsaddleJs = "\
249311 \ ws.onmessage = function(e) {\n\
250312 \ var batch = JSON.parse(e.data);\n\
251313 \\n\
252− \ " <> runBatch (\a -> "ws.send(JSON.stringify(" <> a <> "));") <> "\
314+ \ " <> runBatch (\a -> "ws.send(JSON.stringify(" <> a <> "));") Nothing <> "\
253315 \ };\n\
254316 \ };\n\
255317 \ ws.onerror = function() {\n\
jsaddle/src/Language/Javascript/JSaddle/Types.hs+7−3
@@ -315,11 +315,14 @@ data AsyncCommand = FreeRef JSValueForSend
315315 | CallAsFunction JSObjectForSend JSObjectForSend [JSValueForSend] JSValueForSend
316316 | CallAsConstructor JSObjectForSend [JSValueForSend] JSValueForSend
317317 | NewEmptyObject JSValueForSend
318− | NewCallback JSValueForSend
318+ | NewAsyncCallback JSValueForSend
319+ | NewSyncCallback JSValueForSend
319320 | NewArray [JSValueForSend] JSValueForSend
320321 | EvaluateScript JSStringForSend JSValueForSend
321322 | SyncWithAnimationFrame JSValueForSend
322− deriving (Show, Generic)
323+ | StartSyncBlock
324+ | EndSyncBlock
325+ deriving (Show, Generic)
323326
324327instance ToJSON AsyncCommand where
325328 toEncoding = genericToEncoding defaultOptions
@@ -351,7 +354,7 @@ instance FromJSON Command
351354instance NFData Command
352355
353356-- | Batch of commands that can be sent together to the JavaScript context
354−data Batch = Batch [Either AsyncCommand Command] Bool
357+data Batch = Batch [Either AsyncCommand Command] Bool Int
355358 deriving (Show, Generic)
356359
357360instance ToJSON Batch where
@@ -384,6 +387,7 @@ instance FromJSON Result
384387
385388data Results = Success [Result]
386389 | Failure [Result] JSValueReceived
390+ | Duplicate
387391 | Callback JSValueReceived JSValueReceived [JSValueReceived]
388392 | ProtocolError Text
389393 deriving (Show, Generic)