-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathStreaming.fs
More file actions
127 lines (107 loc) · 4.64 KB
/
Streaming.fs
File metadata and controls
127 lines (107 loc) · 4.64 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
open System
open System.Collections.Concurrent
open System.Threading.Tasks
open Microsoft.AspNetCore.Builder
open Microsoft.FSharp.Core
open Falco
open Falco.Markup
open Falco.Routing
open Falco.Datastar
open Falco.Datastar.SignalPath
type User = Guid
type UserState = string
let userDisplays = ConcurrentDictionary<User, UserState>()
[<CLIMutable>]
type StreamSignals =
{ User:User
Display:UserState }
module ElementIds =
let [<Literal>] streamView = "streamView"
module SignalPath =
let userName = sp"user"
let displayType = sp"display"
module UserState =
let [<Literal>] displayBadApple = "Watching: Bad Apple"
let [<Literal>] displayUsers = "Users"
let [<Literal>] loggedOff = "Logged Off"
let handleIndex ctx = task {
let html (user:User) =
Elem.html [] [
Elem.head [ Attr.title "Streaming" ] [
Ds.cdnScript
Elem.script [ Attr.type' "module" ] []
]
Elem.body [
Ds.signal (SignalPath.userName, user)
Ds.signal (SignalPath.displayType, UserState.displayBadApple)
Ds.onSignalPatchFilter (SignalsFilter.Include SignalPath.displayType)
Ds.onSignalPatch (Ds.get "/channel")
Ds.safariStreamingFix
] [
Elem.div [ Ds.onInit (Ds.get "/stream") ] []
Text.h1 "Example: Streaming"
Elem.input [ Attr.id "streamChannel"
Attr.typeRadio
Attr.value UserState.displayBadApple
Ds.bind SignalPath.displayType ]
Elem.label [ Attr.for' "streamDisplayBadApple" ] [ Text.raw "Bad Apple" ]
Elem.input [ Attr.id "streamChannel"
Attr.typeRadio
Attr.value UserState.displayUsers
Ds.bind SignalPath.displayType ]
Elem.label [ Attr.for' "streamDisplayGuids" ] [ Text.raw "Viewers" ]
Elem.div [ Attr.id ElementIds.streamView ] []
]
]
return Response.ofHtml (html (Guid.NewGuid())) ctx
}
let handleViewChange ctx = task {
let! signals = Request.getSignals<StreamSignals> ctx
match signals with
| ValueNone -> ()
| ValueSome signals -> userDisplays.AddOrUpdate (signals.User, signals.Display, Func<User, UserState, UserState>(fun _ _ -> signals.Display)) |> ignore
}
let handleStream ctx = task {
Response.sseStartResponse ctx |> ignore
let! signals = Request.getSignals<StreamSignals> ctx
let signalUser =
match signals with
| ValueSome signals -> signals.User
| ValueNone -> failwith "no user"
// reset to displaying BadApple
// when the browser tab is hidden and re-displayed, /stream request is made again with out-dated signal values
// we could create more states for the user to restore the original view, but it wasn't the focus of the demo
userDisplays.AddOrUpdate (signalUser, UserState.displayBadApple, Func<User, UserState, UserState>(fun user userState -> UserState.displayBadApple)) |> ignore
do! Response.ssePatchSignal ctx SignalPath.displayType UserState.displayBadApple
try
while true do
let _, streamDisplay = userDisplays.TryGetValue(signalUser)
let patch =
match streamDisplay with
| UserState.displayUsers ->
Elem.pre [ Attr.id ElementIds.streamView ] [
Text.raw (
userDisplays
|> Seq.map (fun ud -> (ud.Key, ud.Value))
|> Seq.map (fun (user, display) -> ((if user = signalUser then "YOU" else user.ToString()), display ))
|> Seq.map (fun (user, display) -> $"{user}: {display}") |> String.concat Environment.NewLine)
]
| _ ->
Elem.pre [ Attr.id ElementIds.streamView ] [
Text.raw (Animation.getCurrentBadAppleFrame ())
]
do! Response.sseHtmlElements ctx patch
do! Task.Delay (TimeSpan.FromMilliseconds(50), ctx.RequestAborted)
finally
userDisplays.AddOrUpdate (signalUser, UserState.displayBadApple, Func<User, UserState, UserState>(fun _ _ -> UserState.loggedOff)) |> ignore
return ()
}
let wapp = WebApplication.Create()
wapp.UseStaticFiles() |> ignore
let endpoints =
[ get "/" (fun ctx -> handleIndex ctx)
get "/stream" (fun ctx -> handleStream ctx)
get "/channel" (fun ctx -> handleViewChange ctx) ]
wapp.UseRouting()
.UseFalco(endpoints)
.Run()