here is catered high interactive problem solved well by Phoenix
since Elixir can scale to millions of simultaneous processes managing concurrent connections; don't have to Request/Response, client can connect a channel and send/receive messages
-
Phoenix Channel converses over a
topicsending/receivingevents(messages) and keeping state(in socketstruct) -
More than a user might want same topic at a time; each user's conversation has an isolated process
-
Request/Response are stateless here, these long running conversation can be stateful.. so don't need to keep track using cookies/DB/similar
-
This only works due to true isolation & concurrency granted by base; here one crashing user wouldn't impact another
-
Channels application gotta worry of 3 things (on both client & server): managing connections; sending messages; receiving messages
users will add Annotations in Real-time; Videologue will play back video annotations for a user
first on client side, add ES6 client code to do the aforementioned 3 things; then on server side, do the same
then utilize Channel Presence, allowing users to know who is logged in
-
here Channel topics at Client would be Videos; client-side Video object construct will directly connect with Phoenix
-
update app.js to use
Videojs object instead ofPlayerobject, passing itsocketas well
- this works but gives
Unable to join {reason: "unmatched topic"}Browser console error>- with
vidChannelinitialized inVideoclient tries joining video channel
-
for each request a new
Plug.Conn; channel flow is different as after connection is made the socket will be transformed through life of connection -
first decide whether to allow connection; next create initial socket including any custom application setup
-
endpoint.exalready includessocketmount point &websocket,longpollconfiguration -
user_socket.ex gets mounted at Endpoint by default with
connect/3&id/1defined
id/1help identify socket based on state stored in socket itself; returningnillets everyone in anonymously
connect/3decides whether to make connection, receiving conn params, socket & advanced config map
UserSocketuses single connection to server handling all channels; Phoenix handle routing right message to right channel
- our
topichas identifier ofvideos:video_id; we want all users to get Annotations created on a Video regardles of who posted it
- need a VideoChannel for our application, to be included as a channel def in
UserSocketas
channel "videos:*", VideologueWeb.VideoChannel
-
creating
VideoChannel, addjoin/3to extractvideo_idfrom passedvideos:video_idtopic-subtopic in connection request and assign that on socket map -
add
vidChannel.join()..withreceive()callbacks for "ok" & "error"
current state gives following in browser console logs
transport: connected to ws://localhost:4000/socket/websocket?token=undefined&vsn=2.0.0
player.js?7e1b:26 ready {target: ej, data: null}
socket.js?1554:6 push: phoenix heartbeat (undefined, 1) {}
socket.js?1554:6 receive: ok phoenix phx_reply (1) {response: {…}, status: "ok"}
- channel module receive events via callbacks
handle_inreceives direct channel events
handle_outintercepts broadcast events
handle_inforeceives OTP messages
-
client does get a Phoenix heartbeat; lets send custom
:pingto channel to test our flow -
handle_infocallback is added toVideoChannelto keep a counter pushed to:ping -
add
vidChannel.on(event, callback)API to handle ping and console log the counter in it; viavideo.js
Controllers process a
request, Channels hold aconversation
- add
vidChannel.on(event, callback)API to handlenew_annotationinvideo.js; it to callrenderAnnotationwhich adds the user & annotation detail tomsgContainerwith safety escape from XSS
-
handle_in/3manages all incoming requests to a channel -
broadcast!/3sends event to all users on current topic; using Phoenix Pub/Sub in background
broadcast!allows sending:okand:erroras reply or:noreplyto skip sending
- add
handle_in("new_annotation", params, socket)toVideoChannelto broadcast any annotation posted
don't send all payloads as that will allow users to send arbitrary data across a topic
- for channel communication, token authentication works better.. so using
tokenatwebsocketswill not makecookiesavailable
generate a token for authenticated user and pass it to socket at frontend
-
add
window.userToken="<%= assigns[:user_token] %>"JS snippet to app.html.eex -
add
put_user_token/2withPhoenix.Token.signto Auth controller which gets used incall/2&login/2
our
socket.jsalready passes alongwindow.userTokenas Socket params
- add
Phoenix.Token.verifyatUserSocketto check upon token in socket
now to persist currently in-memory annotations, so they can be replayed
# mix phx.gen.schema Multimedia.Annotation annotations body:text at:integer user_id:references:users video_id:references:videos
* creating lib/videologue/multimedia/annotation.ex
* creating priv/repo/migrations/20210621113210_create_annotations.exs
# mix ecto.migrate
-
add
has_many :annotationsdirective to videos schema -
replace
user_id&video_idin Videologue.Multimedia.Annotations withbelongs_todirectives -
add
annotate_video/3(which Repo inserts) &list_annotations/1(which fetchs from DB) to Videologue.Multimedia -
update
handle_in/3to pulluserusingsocket.assigns.user_idmade to put user detail inbroadcast! -
add a
renderforuser.jsonto UserView for id & username
the annotations disappear on refresh
-
update
VideoChannel's joincallback to pass down list of annotations by using Phoenix's 3-tuplejoinsignature as{:ok, join_response, socket}to join channel and send a join response usingAnnotationView -
update
video.jsforvidChannel.join()...under"ok"to callrenderAnnotationfor each annotation received
vidChannel.join()
.receive("ok", ( {annotations} ) => {
console.log("joined video channel", annotations)
annotations.forEach( ann => this.renderAnnotation(msgContainer, ann) )
})
.receive("error", reason => console.log("join failed", reason))
this ensures annotations persist & gets loaded on fresh page load
now we will update
video.jsto schedule annotations to appear synced with the video progress they were postedat
- update
video.jswithvidChannel.join()...callingscheduleMessages()instead which runs every second & renders anything for which time has come
now use
data-seekin annotations added tomsgContaineras click-able entity to skip to the time in video for that comment to be made
- add a
clickevent listener tomsgContainerutilizingPlayer.seekTo
currently once client connect to server and post few annotations; if client rejoins say after a server crash it duplicate posts all annotations in
msgContainer
- track a
last_seen_idon client bumped on every new annotation; which can be checked on rejoin
//before adding last_seen_id
let vidChannel = socket.channel("videos:" + videoId)
// can send last_seen_id as param to VideoChannel & make it filter sent annotations
let vidChannel = socket.channel("videos:" + videoId, () => {
return {last_seen_id: lastSeenId}
})
-
update
vidChannel.join()..to setlastSeenIdto max response's annotation id available; so Channel knows to filter based on that and send only newer annotations on reconnect -
update
Multimedia.list_annotationswithwhere: a.id > ^since_idby assing a paramsince_idwith default value 0, which could get requiredsince_idfromVideoChannel.join/3
-
tracking which users are watching a video, cleaning up after they disconnect..
Channel Presence -
generate presence files
mix phx.gen.presencecreating presence.ex
we also need to add it to Supervision tree as
VideologueWeb.Presence
so
send(self(), :after_join)inVideoChannel.join()to allow itself let know of joining successfully
handle_info(:after_join, socket)gets invoked on successful join & will process the message above to callVideologueWeb.Presence.trackhardcoding device as
browseras planning to only target those
-
add
user-listelement inwatch/show.html.eex; usingvideo.jsappend user elements to it -
add
import {Presence} from "phoenix"on top ofvideo.jsand inonReadyadd
let userList = document.getElementById("user-list")
let presence = new Presence(vidChannel)
presence.onSync(() => {
userList.innerHTML = presence.list((id, {metas: [first, ...rest]}) => {
let count = rest.length + 1
return `<span>${id} (${count})</span>`
}).join(", ")
})
can decorate
presenceinformation by definingfetch/2and updating themetasreturned
-
add
list_users_by_ids/1toVideologue.Accounts -
add
fetch/1toVideologueWeb.Presence