I'm making a simple web app that looks for color words in a text, and plots statistics about them. You can test it at colors.jonreeve.com if it's not too busy. I'm using the Scotty web framework to handle the web stuff. It works OK for short texts, but longer texts, like full novels, take so long that the browser normally times out. So I'm guessing what I need here is to send the form via Jquery AJAX or something, and then have the server send JSON every so often with its status ("now loading file," "now counting colors," etc) and then when it receives a "success" signal, then redirect to some other URL?
This is my first time trying to do something like this, so forgive me if this all sounds uninformed. I also noticed that there are some similar questions out there, but I have a feeling that Scotty handles things a little differently than most setups. I noticed that there are a few functions for setting raw output, setting headers and so forth. Do I try to emit certain signals at each stage in the analysis? And how would I do that, given Haskell's handling of side-effects? I'm struggling to even think of the best approach, here.
Instead of a single long-running GET request, I would perhaps set up an endpoint accepting POST requests. The POST would return immediately with two links in the response body:
one link to a new resource representing the task result, which wouldn't be immediately available. Until then, GET requests to the result could return 409 (Conflict).
one link to a related, immediately available resource representing notifications emitted while performing the task.
Once the client has made a successful GET of the task result resource, it could DELETE it. That should delete both the task result resource and the associated notification resource.
For each POST request, you would need to spawn a background worker thread. You would also need a background thread for deleting task results that grew old (because the clients could be lazy and not invoke DELETE). These threads would communicate with MVars, TVars, channels or similar methods.
Now the question is: how to best handle the notifications emitted by the server? There are several options:
Just poll periodically the notification resource from the client. Disadvantages: potentially many HTTP requests, notifications are not received promptly.
long polling. A sequence of GET requests which are kept open until the server wants to emit some notification, or until a timeout.
server-sent events. wai-extra has support for this, but I don't know how to hook a raw wai Application back into Scotty.
websockets. Not sure how to integrate with Scotty though.
Here's the server-side skeleton of a long polling mechanism. Some preliminary imports:
{-# LANGUAGE NumDecimals #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
import Control.Concurrent (threadDelay)
import Control.Concurrent.Async (concurrently_) -- from async
import Control.Concurrent.STM -- from stm
import Control.Concurrent.STM.TMChan -- from stm-chans
import Control.Monad.IO.Class (liftIO)
import Data.Aeson (ToJSON) -- from aeson
import Data.Foldable (for_)
import Data.Text (Text)
import Web.Scotty
And here is the main code.
main :: IO ()
main =
do
chan <- atomically $ newTMChan #Text
concurrently_
( do
for_
["starting", "working on it", "finishing"]
( \msg -> do
threadDelay 10e6
atomically $ writeTMChan chan msg
)
atomically $ closeTMChan chan
)
( scotty 3000
$ get "/notifications"
$ do
mmsg <- liftIO $ atomically $ readTMChan chan
json $
case mmsg of
Nothing -> ["closed!"]
Just msg -> [msg]
)
There are two concurrent threads. One feeds messages into a closeable channel at 10 second intervals, the other runs a Scotty server, where each GET invocation hangs until a new message arrives in the channel.
Testing it from bash using curl, we should see a succession of messages:
bash$ for run in {1..4}; do curl -s localhost:3000/notifications ; done
["starting"]["working on it"]["finishing"]["closed!"]
For comparison, here's the skeleton of a solution based on server-sent events. It uses yesod instead of scotty though, because Yesod offers a way to hook as a handler the wai-extra Application that manages the events.
The Haskell code
{-# LANGUAGE NumDecimals #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
import Control.Concurrent (threadDelay)
import Control.Concurrent.Async (concurrently_) -- from async
import Control.Concurrent.STM -- from stm
import Control.Concurrent.STM.TMChan -- from stm-chans
import Control.Monad.IO.Class (liftIO)
import Data.Binary.Builder -- from binary
import Data.Foldable (for_)
import Network.Wai.EventSource -- from wai-extra
import Network.Wai.Middleware.AddHeaders -- from wai-extra
import Yesod -- from yesod
data HelloWorld = HelloWorld (TMChan ServerEvent)
mkYesod
"HelloWorld"
[parseRoutes|
/foo FooR GET
|]
instance Yesod HelloWorld
getFooR :: Handler ()
getFooR = do
HelloWorld chan <- getYesod
sendWaiApplication
. addHeaders [("Access-Control-Allow-Origin", "*")]
. eventStreamAppRaw
$ \send flush ->
let go = do
mevent <- liftIO $ atomically $ readTMChan chan
case mevent of
Nothing -> do
send CloseEvent
flush
Just event -> do
send event
flush
go
in go
main :: IO ()
main =
do
chan <- atomically $ newTMChan
concurrently_
( do
for_
[ ServerEvent
(Just (fromByteString "ev"))
(Just (fromByteString "id1"))
[fromByteString "payload1"],
ServerEvent
(Just (fromByteString "ev"))
(Just (fromByteString "id2"))
[fromByteString "payload2"],
ServerEvent
(Just (fromByteString "ev"))
(Just (fromByteString "eof"))
[fromByteString "payload3"]
]
( \msg -> do
threadDelay 10e6
atomically $ writeTMChan chan msg
)
atomically $ closeTMChan chan
)
( warp 3000 (HelloWorld chan)
)
And a small blank page to test the server-sent events. The messages appear on the browser console:
<!DOCTYPE html>
<html lang="en">
<body>
</body>
<script>
window.onload = function() {
var source = new EventSource('http://localhost:3000/foo');
source.onopen = function () { console.log('opened'); };
source.onerror = function (e) { console.error(e); };
source.addEventListener('ev', (e) => {
console.log(e);
if (e.lastEventId === 'eof') {
source.close();
}
});
}
</script>
</html>
Related
Good evening everyone. I'm not quite new to this place but finally decided to register and ask for a help. I develop a web application using Quart framework (asynchronous Flask). And now as application became bigger and more complex I decided to separate different procedures to different server instances, this is mostly because I want to keep web server clean, more abstract and free of computational load.
So I plan to use one web server with a few (if needed) identical procedure servers. All servers are based on quart framework, for now just for simplicity of development. I decided to use Crossbar.io router and autobahn to connect all servers together.
And here the problem occurred.
I followed this posts:
Running several ApplicationSessions non-blockingly using autbahn.asyncio.wamp
How can I implement an interactive websocket client with autobahn asyncio?
How I can integrate crossbar client (python3,asyncio) with tkinter
How to send Autobahn/Twisted WAMP message from outside of protocol?
Seems like I tried all possible approaches to implement autobahn websocket client in my quart application. I don't know how to make it possible so both things are working, whether Quart app works but autobahn WS client does not, or vice versa.
Simplified my quart app looks like this:
from quart import Quart, request, current_app
from config import Config
# Autobahn
import asyncio
from autobahn import wamp
from autobahn.asyncio.wamp import ApplicationSession, ApplicationRunner
import concurrent.futures
class Component(ApplicationSession):
"""
An application component registering RPC endpoints using decorators.
"""
async def onJoin(self, details):
# register all methods on this object decorated with "#wamp.register"
# as a RPC endpoint
##
results = await self.register(self)
for res in results:
if isinstance(res, wamp.protocol.Registration):
# res is an Registration instance
print("Ok, registered procedure with registration ID {}".format(res.id))
else:
# res is an Failure instance
print("Failed to register procedure: {}".format(res))
#wamp.register(u'com.mathservice.add2')
def add2(self, x, y):
return x + y
def create_app(config_class=Config):
app = Quart(__name__)
app.config.from_object(config_class)
# Blueprint registration
from app.main import bp as main_bp
app.register_blueprint(main_bp)
print ("before autobahn start")
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
runner = ApplicationRunner('ws://127.0.0.1:8080 /ws', 'realm1')
future = executor.submit(runner.run(Component))
print ("after autobahn started")
return app
from app import models
In this case application stuck in runner loop and whole application does not work (can not serve requests), it becomes possible only if I interrupt the runners(autobahn) loop by Ctrl-C.
CMD after start:
(quart-app) user#car:~/quart-app$ hypercorn --debug --error-log - --access-log - -b 0.0.0.0:8001 tengine:app
Running on 0.0.0.0:8001 over http (CTRL + C to quit)
before autobahn start
Ok, registered procedure with registration ID 4605315769796303
after pressing ctrl-C:
...
^Cafter autobahn started
2019-03-29T01:06:52 <Server sockets=[<socket.socket fd=11, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('0.0.0.0', 8001)>]> is serving
How to make it possible to work quart application with autobahn client together in non-blocking fashion? So autobahn opens and keeps websocket connection to Crossbar router and silently listen on background.
Well, after many sleepless nights I finally found a good approach to solve this conundrum.
Thanks to this post C-Python asyncio: running discord.py in a thread
So, I rewrote my code like this and was able to run my Quart app with autobahn client inside, and both are actively working in nonblocking fashion.
The whole __init__.py looks like:
from quart import Quart, request, current_app
from config import Config
def create_app(config_class=Config):
app = Quart(__name__)
app.config.from_object(config_class)
# Blueprint registration
from app.main import bp as main_bp
app.register_blueprint(main_bp)
return app
# Autobahn
import asyncio
from autobahn import wamp
from autobahn.asyncio.wamp import ApplicationSession, ApplicationRunner
import threading
class Component(ApplicationSession):
"""
An application component registering RPC endpoints using decorators.
"""
async def onJoin(self, details):
# register all methods on this object decorated with "#wamp.register"
# as a RPC endpoint
##
results = await self.register(self)
for res in results:
if isinstance(res, wamp.protocol.Registration):
# res is an Registration instance
print("Ok, registered procedure with registration ID {}".format(res.id))
else:
# res is an Failure instance
print("Failed to register procedure: {}".format(res))
def onDisconnect(self):
print('Autobahn disconnected')
#wamp.register(u'com.mathservice.add2')
def add2(self, x, y):
return x + y
async def start():
runner = ApplicationRunner('ws://127.0.0.1:8080/ws', 'realm1')
await runner.run(Component) # use client.start instead of client.run
def run_it_forever(loop):
loop.run_forever()
asyncio.get_child_watcher() # I still don't know if I need this method. It works without it.
loop = asyncio.get_event_loop()
loop.create_task(start())
print('Starting thread for Autobahn...')
thread = threading.Thread(target=run_it_forever, args=(loop,))
thread.start()
print ("Thread for Autobahn has been started...")
from app import models
With this scenario we create task with autobahn's runner.run and attach it to the current loop and then run this loop forever in new thread.
I was quite satisfied with current solution.... but then then was found out that this solution has some drawbacks, that was crucial for me, for example: reconnect if connection dropped (i.e crossbar router becomes unavailable). With this approach if connection was failed to initialize or dropped after a while it will not try to reconnect. Additionally for me it wasn't obvious how to ApplicationSession API, i.e. to register/call RPC from the code in my quart app.
Luckily I spotted another new component API that autobahn used in their documentation:
https://autobahn.readthedocs.io/en/latest/wamp/programming.html#registering-procedures
https://github.com/crossbario/autobahn-python/blob/master/examples/asyncio/wamp/component/backend.py
It has auto reconnect feature and it's easy to register functions for RPC using decorators #component.register('com.something.do'), you just need to import component before.
So here is the final view of __init__.py solution:
from quart import Quart, request, current_app
from config import Config
def create_app(config_class=Config):
...
return app
from autobahn.asyncio.component import Component, run
from autobahn.wamp.types import RegisterOptions
import asyncio
import ssl
import threading
component = Component(
transports=[
{
"type": "websocket",
"url": u"ws://localhost:8080/ws",
"endpoint": {
"type": "tcp",
"host": "localhost",
"port": 8080,
},
"options": {
"open_handshake_timeout": 100,
}
},
],
realm=u"realm1",
)
#component.on_join
def join(session, details):
print("joined {}".format(details))
async def start():
await component.start() #used component.start() instead of run([component]) as it's async function
def run_it_forever(loop):
loop.run_forever()
loop = asyncio.get_event_loop()
#asyncio.get_child_watcher() # I still don't know if I need this method. It works without it.
asyncio.get_child_watcher().attach_loop(loop)
loop.create_task(start())
print('Starting thread for Autobahn...')
thread = threading.Thread(target=run_it_forever, args=(loop,))
thread.start()
print ("Thread for Autobahn has been started...")
from app import models
I hope it will help somebody. Cheers!
I am trying to upgrade from version 0.18 to 0.19 of Elm. My project depends on elm-lang/websocket in 0.18? I cannot seem to find the equivalent package in 0.19. What am I missing?
Here is a minimal working example of an interactive form to echo input from echo.websocket.org using 2 simple input/output ports for communicating with a JavaScript WebSocket object external to the elm 0.19 module:
File: echo.elm. Compile with: elm make echo.elm --output=echo.js
port module Main exposing (main)
import Browser
import Html exposing (Html)
import Html.Attributes as HA
import Html.Events as HE
import Json.Encode as JE
-- JavaScript usage: app.ports.websocketIn.send(response);
port websocketIn : (String -> msg) -> Sub msg
-- JavaScript usage: app.ports.websocketOut.subscribe(handler);
port websocketOut : String -> Cmd msg
main = Browser.element
{ init = init
, update = update
, view = view
, subscriptions = subscriptions
}
{- MODEL -}
type alias Model =
{ responses : List String
, input : String
}
init : () -> (Model, Cmd Msg)
init _ =
( { responses = []
, input = ""
}
, Cmd.none
)
{- UPDATE -}
type Msg = Change String
| Submit String
| WebsocketIn String
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
Change input ->
( { model | input = input }
, Cmd.none
)
Submit value ->
( model
, websocketOut value
)
WebsocketIn value ->
( { model | responses = value :: model.responses }
, Cmd.none
)
{- SUBSCRIPTIONS -}
subscriptions : Model -> Sub Msg
subscriptions model =
websocketIn WebsocketIn
{- VIEW -}
li : String -> Html Msg
li string = Html.li [] [Html.text string]
view : Model -> Html Msg
view model = Html.div []
--[ Html.form [HE.onSubmit (WebsocketIn model.input)] -- Short circuit to test without ports
[ Html.form [HE.onSubmit (Submit model.input)]
[ Html.input [HA.placeholder "Enter some text.", HA.value model.input, HE.onInput Change] []
, model.responses |> List.map li |> Html.ol []
]
]
Embed the compiled echo.js into echo.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Echo</title>
<script src="echo.js"></script>
</head>
<body>
<div id="elm-node"></div>
<script>
var app = Elm.Main.init({node: document.getElementById("elm-node")});
var ws = new WebSocket("wss://echo.websocket.org");
ws.onmessage = function(message)
{
console.log(message);
app.ports.websocketIn.send(JSON.stringify({data:message.data,timeStamp:message.timeStamp}));
};
app.ports.websocketOut.subscribe(function(msg) { ws.send(msg); });
</script>
</body>
</html>
This works on Firefox 60.2.0esr on Linux but has not been tested on other platforms.
Again, this is only a minimal example to demonstrate how to use ports with WebSockets for Elm 0.19. It does not included closing the WebSocket, error handling, etc. but hopefully this example can help you get started in that direction. It is expected that WebSockets will be directly supported by Elm again soon, so this is only a temporary work-around. If you don't need to upgrade to 0.19, then consider staying with 0.18 instead.
The websocket package is currently redesigned for Elm 0.19, see this issue:
This package has not been updated for 0.19 yet. I have heard lots of folks saying they need more features from this package, so I'd rather take that into consideration in the update rather than just doing the same stuff. I recommend using ports or 0.18 if you absolutely need this right this second.
EDIT: April 15, 2020 update
The package has been archived and the Readme file updated as
follows:
The recommended way to use WebSockets with Elm for now is through
ports. You can see a minimal example in the js-integration-examples
repo [IMAGE CLIPPED]
History
We had a bare bones version of WebSockets in within Elm in versions
0.17 and 0.18, part of the introduction of subscriptions to Elm. But users found that the API was not able to cover a lot of situations
they faced in practice. How can this work with Elixir Pheonix?
Firebase? How can I use a different backoff strategy for reconnecting?
How can I hear about when the connection goes down or comes back? How
about sub-protocols?
In trying to expand the API to cover all the cases people were facing
in practice, I came to think that it may not be possible with the
current subscriptions infrastructure. (My feeling is that effect
managers may not be a great fit for web sockets because they do not
have great mechanisms for uniquely identifying resources. Do we have
one connections or two? How do we tell the difference? If it requires
function pointer equality, how can we make that reliable when an
anonymous function is used?) I did not understand this problem as well
in 2016, and I think it manifested most clearly with web sockets.
So facing the prospect of either (1) having an API that many
eventually had to leave behind for ports or (2) recommending that
people go with ports from the start, we figured that (2) was probably
the best for someone new coming to Elm. That way they would hook up to
their preferred web socket manager without the intermediate step of
learning a promising but incomplete API.
When working with Autobahn and WAMP before I have been using the Subclassing-Approach but stumbled over decorator / functions approach which I really prefer over subclassing.
However. I have a function that is being called from an external hardware (via callback) and this function needs to publish to Crossbar.io Router whenever it is being called.
This is how I've done this, keeping a reference of the Session right after the on_join -> async def joined(session, details) was called.
from autobahn.asyncio.component import Component
from autobahn.asyncio.component import run
global_session = None
comp = Component(
transports=u"ws://localhost:8080/ws",
realm=u"realm1",
)
def callback_from_hardware(msg):
if global_session is None:
return
global_session.publish(u'com.someapp.somechannel', msg)
#comp.on_join
async def joined(session, details):
global global_session
global_session = session
print("session ready")
if __name__ == "__main__":
run([comp])
This approach of keeping a reference after component has joined connection feels however a bit "odd". Is there a different approach to this? Can this done on some other way.
If not than it feels a bit more "right" with subclassing and having all the application depended code within that subclass (but however keeping everything of my app within one subclass also feels odd).
I would recommend to use asynchronous queue instead of shared session:
import asyncio
from autobahn.asyncio.component import Component
from autobahn.asyncio.component import run
queue = asyncio.queues.Queue()
comp = Component(
transports=u"ws://localhost:8080/ws",
realm=u"realm1",
)
def callback_from_hardware(msg):
queue.put_nowait((u'com.someapp.somechannel', msg,))
#comp.on_join
async def joined(session, details):
print("session ready")
while True:
topic, message, = await queue.get()
print("Publishing: topic: `%s`, message: `%s`" % (topic, message))
session.publish(topic, message)
if __name__ == "__main__":
callback_from_hardware("dassdasdasd")
run([comp])
There are multiple approaches you could take here, though the simplest IMO would be to use Crossbar's http bridge. So whenever an event callback is received from your hardware, you can just make a http POST request to Crossbar and your message will get delivered
More details about http bridge https://crossbar.io/docs/HTTP-Bridge-Publisher/
This is regarding an attempt to get a WebSocket's input and output hooked up to a Coroutine.
The following function takes a Connection then sets it up to emit a Coroutine value when a message is received.
module Main where
import Prelude
import Control.Coroutine (emit, Producer, Consumer, await)
import Control.Monad.Eff (Eff)
import Control.Monad.Eff.Console (CONSOLE, log)
import Control.Monad.Eff.Var (($=))
import Control.Monad.Reader.Trans (lift)
import Control.Monad.Rec.Class (forever)
import WebSocket (WEBSOCKET, Connection(..), newWebSocket, URL(..), runMessage, runMessageEvent)
wsProducer :: Connection → Producer String (Eff _) Unit
wsProducer (Connection s) = s.onmessage $= emit <<< runMessage <<< runMessageEvent
The Producer and Consumer will be hooked up in Main (the WebSocket connection will also be made there), that hasn't been written yet though, since the function above won't even typecheck.
How can this be made to typecheck, please? The fact it won't typecheck may mean there is a fundamental misunderstanding in the code above, if so an explanation with a code sample of a working solution would be very helpful.
Related: this answer about Halogen and WebSockets contains very similar code.
There's a couple of things wrong with that snippet. First, here's a version that works:
module Main where
import Prelude
import Control.Coroutine (Producer)
import Control.Coroutine.Aff (produce)
import Control.Monad.Aff (Aff)
import Control.Monad.Aff.AVar (AVAR)
import Control.Monad.Eff.Var (($=))
import Data.Either (Either(..))
import WebSocket (WEBSOCKET, Connection(..), runMessageEvent, runMessage)
wsProducer :: forall eff. Connection → Producer String (Aff (avar :: AVAR, ws :: WEBSOCKET | eff)) Unit
wsProducer (Connection s) =
produce \emit ->
s.onmessage $= emit <<< Left <<< runMessage <<< runMessageEvent
You're missing a use of produce, which is what brings emit into scope, and is how you make a producer.
The producer must use Aff, not Eff.
emit accepts an Either - a Left value indicates a value should be produced, and a Right indicates the producer should be closed.
Take a look at the docs for produce and hopefully the misunderstanding you mentioned will become clear!
I'm trying to set up a simple example using WebSockets in Elm, but I keep getting the run time error "input is undefined". The console does not give me any line number in my elm file or anything like that.
I was trying to use WebSockets in a large project, and I kept getting the error "a is undefined", so I decided to make this small example to try and isolate the problem.
I wrote some code that receives messages containing numbers from the websocket. It increments the numbers, and then sends the new numbers back out over the web socket. The server does the same thing, sending back the number incremented by 1 to the client.
Here is the elm code:
import Graphics.Element (Element)
import Signal
import Signal (Signal)
import Text
import Window
import WebSocket
import String
type State = Num Int
| StateErr String
input : Signal String
input =
WebSocket.connect "ws://localhost:4567/test" sendToServer
sendToServer : Signal String
sendToServer =
Signal.dropRepeats
(Signal.dropIf (\str -> str == "") "" (Signal.map formatReply state))
formatReply : State -> String
formatReply state =
case state of
Num n -> toString n
StateErr str -> ""
stepState : String -> State -> State
stepState str state =
case (String.toInt str) of
Ok n -> Num (n + 1)
Err str -> StateErr str
display : (Int,Int) -> State -> Element
display (w,h) state = Text.asText state
state : Signal State
state =
Signal.foldp stepState (Num 0) input
main : Signal Element
main =
Signal.map2 display Window.dimensions state
I tested the server side, and it's working fine, so I definitely do not think that the server is causing the issue.
When I tried the code in Firefox, I get "input is undefined". When I run it in Chrome, I get "Cannot read property 'kids' of undefined".
In Chrome, upon looking at the stack trace it seems that when the code goes to run, input is undefined. Is this a bug with the WebSocket library?
I'm very new to using Elm, so I'd appreciate any help/advice on using websockets.
I learned that the cause of my troubles, is that as of now the WebSockets library in elm is not fully implemented. I also learned that I can accomplish my goals using ports, and then implementing the websocket in javascript.
I added the following javascript to my html file:
var game = Elm.fullscreen(Elm.SlimeWarz, {rawServerInput: ""});
var socket = new WebSocket("ws://localhost:4567");
socket.onopen = function(){
console.log("Socket has been opened.");
}
socket.onmessage = function(msg){
game.ports.rawServerInput.send(msg.data);
}
game.ports.sendToServer.subscribe(sendOverWebsocket);
function sendOverWebsocket(str) {
socket.send(str);
}
Then in elm, I can send data using a ported Signal called sendtoServer
port sendToServer : Signal String
and I can view all the data I receive through the ported signal rawServerInput
port rawServerInput : Signal String
Answer
I'm going to use part of my answer to this question. I think your solution to use ports and do the websocket part in JavaScript is better than the hack I described in that answer, but you may still want to look at it. Once Elm 0.15 is released this problem should go away entirely because of a language feature and the revamp of Websocket (Http gets revamped too btw).
Context: Reason for error
The reason you get the runtime error is because of a compiler bug. The compiler only generates correct code on recursive functions, while it accepts any recursive value. Notice that your input depends on sendToServer, sendToserver depends on state and sContext: tate depends on input.
Context: Code architecture
Those kinds of cyclic signals are usually a sign of bad program architecture. You can find more on that subject here. But your architecture is not at fault here. The problem lies with the Websocket library which doesn't steer you in the right direction.