-
Notifications
You must be signed in to change notification settings - Fork 26
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[RFC] Soft real-time items #51
Comments
I had some thoughts today about what the end result of an implementation could look like for a developer wanting to use WebSockets in Sugar. RouterIn the router, it should be fairly simple and should remain consistent with the other available macros. defmodule My.Router do
use Sugar.Router
get "/", My.Controllers.Main, :index
socket "/", My.Controllers.Main, :notifications, name: :notifications
socket "/chat", My.Controllers.Chat, :index, name: :chat
end Routes should be able to be sent to standard controllers, allowing a developer to blend functionality as he/she sees fit. ControllerInside of a controller, defmodule My.Controllers.Main do
use Sugar.Controller
def index(conn, []) do
# GET handler for index
render conn
end
socket :notifications do
def handle("notify", socket, data) do
# Do something on "notify" events
socket
end
end
end
defmodule My.Controllers.Chat do
use Sugar.Controller
socket :index do
def handle("message", socket, data) do
socket
|> broadcast(data["message"])
end
def handle("message:" <> user, socket, data) do
socket
|> send(user, data["message"])
end
end
I've not seen implementations like this before. Typically, there is a clear separation between socket handlers and standard actions (i.e. different modules), so there will need to be some testing to see if this is a possibility. EventsWith the defmodule My.Queries.User do
import Ecto.Query
alias Sugar.Events
after_insert :notify
def create(data) do
# Build query
My.Repos.Main.insert(query)
end
def notify(changeset) do
Events.broadcast(:notifications, "new:user", changeset)
end
end This is a rough idea, so I'd love to see any feedback on this direction. |
I think the 'handle' functions should be at the same level than the regular actions inside the controller. Actually in that scenario the 'socket' macro in the controller is not needed (and the first rule of macros is...). Pattern matching can be used to determine the right 'handle' function for a given 'socket' route. Something like 'def handle(: notifications, "notify", socket, data)'. That would keep controllers from getting messy when they grow as they would always be one level deep functions. Also allow simpler testability. To free a controller from some code one can always put it on other module and 'use' it from the controller. About 'Events' , I can't avoid feeling uncomfortable sending data to a client straight from models, or outside of a controller at all. I have no constructive proposal for this right now, but then again, it's up to the developer to do it that way. So I guess it's ok. Thanks for asking anyway. I'm glad sugar keeps growing! |
Thanks for the feedback, @rubencaro! I definitely agree that macros should only be used when necessary, and in this case, I'm thinking there will be some work needed at compile time. For things like There's always a good possibility that things can be simplified, especially since my first passes in my head aren't always the best plan of attack. I always want to keep things as simple as possible. The example with the If you have any other criticisms, please don't hesitate to lay them out. |
I'd personally lean toward keeping as much socket-specific stuff in the router (and outside the controller) as possible; it seems more appropriate (and more natural and more intuitive and less confusing) as a routing concern in general (including whatever compile-time stuff is necessary), reserving the controller(s) for performing actions upon sockets without needing to worry nearly as much about the semantics of how/why those actions were called. In other words, I'd probably prefer something structured like so (though probably not exactly like so; my experience with WebSockets is admittedly not very developed compared to that of more typical HTTP requests, so my imagination of this might be atypical relative to what a more experienced WebSockets guru would expect): defmodule My.Router do
use Sugar.Router
get "/", My.Controllers.Main, :index
# magic happens here
socket "/" do
# magic also happens here
handle "notify", My.Controllers.Main
end
socket "/chat" do
# Not very DRY in this example, but the point is clarity and intuition
handle "message", My.Controllers.Chat
handle "message:*", My.Controllers.Chat
end
end
defmodule My.Controllers.Main do
use Sugar.Controller
def index(conn, _args) do
conn |> do_something
end
# socket handler defaults to "handle"; uses pattern matching to figure out
# what to handle
def handle("notify", socket, data) do
socket |> do_something_with(data["notification"]) # or somesuch
end
end
defmodule My.Controllers.Chat do
use Sugar.Controller
# same deal here; default to "handle/3", use pattern matching
def handle("message", socket, data) do
socket |> broadcast(data["message"])
end
def handle("message:" <> user, socket, data) do
socket |> send(user, data["message"])
end
end Basically, if we're going to do before-compile magic in order to get our sockets/handlers setup, I think it should be in the router (where we've already piled on a bunch of macros and where the addition of these particular macros would make the most sense) rather than the controller (which is currently pretty magic-free (other than the automagical view rendering, perhaps) and probably shouldn't be burdened with what really amounts to routing concerns). I don't know how feasible this will be in practice, but I reckon it should be similar to the way |
Putting a little more thought into my own suggestion, I think something like defmodule My.Router do
use Sugar.Router
# ...
socket "/chat", My.Controllers.Chat
end would probably also be sufficiently terse without sacrificing clarity if it's documented that the router will direct to the specified controller's Of course, if more explicitness is required, we could expand this to accept an optional atom representing a function name, like so: defmodule My.Router do
use Sugar.Router
# ...
socket "/chat", My.Controllers.Chat, :chat
end
defmodule My.Controllers.Chat do
use Sugar.Controller
# ...
def chat("message:" <> user, socket, data) do
socket |> send(user, data["message"])
end
end Of course, coming up with ideas is the easy part. No idea how well this will translate to reality :) |
+1 for all that! |
👍 I'm going to start poking around, probably within the week, to see what's possible. I'm hoping we can do most of this without being limited too much by getting Cowboy/Plug to play nicely when it comes to WebSockets. |
Started in on this tonight and got a demo going with Plug and a WebSocket handler. Documenting here: Cowboy WebSocket handler: defmodule WsHandler do
@behaviour :cowboy_websocket_handler
## Init
def init(_transport, _req, _opts) do
{:upgrade, :protocol, :cowboy_websocket}
end
def websocket_init(_transport, req, _opts) do
{:ok, req, :undefined_state}
end
## Handle
def websocket_handle({:text, msg}, req, state) do
{:reply, {:text, "responding to " <> msg}, req, state, :hibernate}
end
def websocket_handle(_any, req, state) do
{:reply, {:text, "whut?"}, req, state, :hibernate}
end
## Info
def websocket_info({:timeout, _ref, msg}, req, state) do
{:reply, {:text, msg}, req, state}
end
def websocket_info(_info, req, state) do
{:ok, req, state, :hibernate}
end
## Terminate
def websocket_terminate(_Reason, _req, _state) do
:ok
end
def terminate(_reason, _req, _state) do
:ok
end
end Standard Plug: defmodule Router do
use Plug.Router
plug :match
plug :dispatch
get "/" do
conn
|> send_resp(200, "index")
end
end Application callback: defmodule WebSocket do
use Application
def start(_type, _args) do
import Supervisor.Spec, warn: false
children = []
plug = Router
opts = []
dispatch = build_dispatch(plug, opts)
Plug.Adapters.Cowboy.http plug, opts, [dispatch: dispatch]
opts = [strategy: :one_for_one, name: WebSocket.Supervisor]
Supervisor.start_link(children, opts)
end
defp build_dispatch(plug, opts) do
opts = plug.init(opts)
[{:_, [ {"/ws", WsHandler, []},
{:_, Plug.Adapters.Cowboy.Handler, {plug, opts}} ]}]
end
end Biggest point to note there is the use of At compile time, we'll need to build a |
For anyone interested in the development, you can follow along here: https://github.com/slogsdon/plug-web-socket |
Alright, I have a good, extremely simple base ready for review: https://github.com/slogsdon/plug-web-socket. If things go well, I'll start on the Event layer or the Tear this up as much as possible. I won't feel bad. Thanks! Walkthrough
The There's nothing needed in a controller except plain functions. Currently, the handling of At the moment, the By default, WebSocket messages are to be a |
I went ahead with a basic Events layer: defmodule WebSocket.Events do
use GenEvents
def start_link(ref) do
case GenEvent.start_link(name: ref) do
{:ok, pid} ->
GenEvent.add_handler(ref, __MODULE__, [])
{:ok, pid}
{:error, {:already_started, pid}} ->
{:ok, pid}
otherwise ->
otherwise
end
end
def join(ref, pid) do
GenEvent.notify(ref, {:add_client, pid})
end
def leave(ref, pid) do
GenEvent.notify(ref, {:remove_client, pid})
end
def broadcast(ref, event, originator) do
GenEvent.notify(ref, {:send, event, originator})
end
def broadcast!(ref, event) do
broadcast(ref, event, nil)
end
def stop(ref) do
GenEvent.stop(ref)
end
# ...
end |
Ok, last update. Worked on the macro, but it's has some room for improvement. defmodule WebSocket.Router do
use Plug.Router
use WebSocket.Macro
socket "/topic", WebSocket.TopicController, :handle
socket "/echo", WebSocket.EchoController, :echo
# ...
end Edited: because I'm a dummy. |
Need something like this to keep up with newer frameworks and web application development trends. The technologies below all allow the client to receive unsolicited updates from the server, while WebSockets allow the client to also send data back to the server on the same connection.
Technologies involved
Items needed
Sugar.Events
Sugar.Router.socket/3
Notification/event layer
This would allow notifications/events to be published by producers (data layer, api endpoints, etc.) and subscribed by consumers (clients). Would most likely be built upon
GenEvent
or something similar.Interaction layer
This would interface the notification layer with the clients by means of the above technologies, e.g. WebSockets. A behaviour would most likely be created to allow for the differing technologies to talk to the clients via the current Plug adapter (web server) in a consistent manner.
API additions
I already have some macros thought out from working on another project the would remove cruft from a WebSockets handler, which should be easy to expand to include the necessary bits for long polling and SSE.
The text was updated successfully, but these errors were encountered: