@@ -0,0 +1,375 @@
1+module Ur.Run exposing (Model, Msg, application)
2+
3+import Browser exposing (Document, UrlRequest)
4+import Browser.Navigation as Nav
5+import Dict exposing (Dict)
6+import Either exposing (Either(..))
7+import Html
8+import Json.Decode as JD
9+import Task
10+import Time
11+import Ur exposing (Agent, Path)
12+import Ur.Cmd
13+import Ur.Cmd.Internal
14+import Ur.Constructor as C
15+import Ur.Deconstructor as D
16+import Ur.Phonemic exposing (Ship)
17+import Ur.Requests exposing (..)
18+import Ur.Sub
19+import Ur.Sub.Internal
20+import Ur.Uw
21+import Url exposing (Url)
22+
23+
24+type alias SubDict msg =
25+ Dict
26+ -- (ship, agent, path)
27+ ( String, String, List String )
28+ { deconstructor : D.Deconstructor (msg -> msg) msg
29+ , number : Int
30+ }
31+
32+
33+type alias Flags =
34+ { uid : String }
35+
36+
37+type alias Model app msg =
38+ { subscriptions : SubDict msg
39+ , subscriptionIntMapping : Dict Int ( String, String, List String )
40+ , app : app
41+ , connected : Bool
42+ , eventId : Int
43+ , flags : Flags
44+ , requestsToRetry : List UrbitRequest
45+ }
46+
47+
48+type Msg msg
49+ = AppMsg msg
50+ | EventSourceMsg JD.Value
51+ | FailedRequest (List UrbitRequest)
52+ | Noop
53+ | OpenConnection
54+ | NeedsActivation
55+ | RetryRequests
56+
57+
58+application :
59+ { init : Url -> Nav.Key -> ( model, Ur.Cmd.Cmd msg )
60+ , view : model -> Document msg
61+ , update : msg -> model -> ( model, Ur.Cmd.Cmd msg )
62+ , subscriptions : model -> Sub msg
63+ , urbitSubscriptions : model -> Ur.Sub.Sub msg
64+ , onUrlRequest : UrlRequest -> msg
65+ , onUrlChange : Url -> msg
66+ , createEventSource : String -> Cmd (Msg msg)
67+ , onEventSourceMsg : (JD.Value -> Msg msg) -> Sub (Msg msg)
68+ , urbitUrl : model -> String
69+ }
70+ -> Program Flags (Model model msg) (Msg msg)
71+application inp =
72+ let
73+ { init, view, onUrlRequest, onUrlChange, onEventSourceMsg } =
74+ inp
75+ in
76+ Browser.application
77+ { init =
78+ \flags u key ->
79+ let
80+ ( app, appCmds ) =
81+ init u key
82+
83+ { subscriptions, eventId, subscriptionRequests, subscriptionIntMapping } =
84+ processUrSubs
85+ 0
86+ Dict.empty
87+ (inp.urbitSubscriptions app |> (\(Ur.Sub.Internal.Sub x) -> x))
88+
89+ ( eventId_, cmds, urReqs ) =
90+ processCmd eventId appCmds
91+
92+ url =
93+ inp.urbitUrl app ++ "/~/channel/" ++ flags.uid
94+ in
95+ ( { subscriptions = subscriptions
96+ , subscriptionIntMapping = subscriptionIntMapping
97+ , app = app
98+ , connected = False
99+ , eventId = eventId_
100+ , flags = flags
101+ , requestsToRetry = []
102+ }
103+ , [ cmds
104+ , pureCmd NeedsActivation
105+ , send
106+ { requests = urReqs ++ subscriptionRequests
107+ , url = url
108+ , error = Noop
109+ , success = Noop
110+ }
111+ ]
112+ |> Cmd.batch
113+ )
114+ , view =
115+ \model ->
116+ view model.app
117+ |> (\{ body, title } -> { title = title, body = body |> List.map (Html.map AppMsg) })
118+ , update = update inp
119+ , subscriptions =
120+ \model ->
121+ Sub.batch
122+ [ inp.subscriptions model.app |> Sub.map AppMsg
123+ , onEventSourceMsg EventSourceMsg
124+ , if List.isEmpty model.requestsToRetry then
125+ Sub.none
126+
127+ else
128+ Time.every 1000 (always RetryRequests)
129+ ]
130+ , onUrlRequest = \req -> onUrlRequest req |> AppMsg
131+ , onUrlChange = \url -> onUrlChange url |> AppMsg
132+ }
133+
134+
135+update :
136+ { r
137+ | update : msg -> app -> ( app, Ur.Cmd.Cmd msg )
138+ , createEventSource : String -> Cmd (Msg msg)
139+ , urbitUrl : app -> String
140+ , urbitSubscriptions : app -> Ur.Sub.Sub msg
141+ }
142+ -> Msg msg
143+ -> Model app msg
144+ -> ( Model app msg, Cmd (Msg msg) )
145+update inp msg model =
146+ let
147+ url =
148+ inp.urbitUrl model.app ++ "/~/channel/" ++ model.flags.uid
149+ in
150+ case msg of
151+ AppMsg msg_ ->
152+ let
153+ ( appModel, appCmds ) =
154+ inp.update msg_ model.app
155+
156+ { subscriptions, eventId, subscriptionRequests, subscriptionIntMapping } =
157+ processUrSubs
158+ model.eventId
159+ model.subscriptions
160+ (inp.urbitSubscriptions model.app |> (\(Ur.Sub.Internal.Sub x) -> x))
161+
162+ ( eventId_, cmds, urReqs ) =
163+ processCmd eventId appCmds
164+ in
165+ ( { model
166+ | app = appModel
167+ , eventId = eventId_
168+ , subscriptions = subscriptions
169+ , subscriptionIntMapping = model.subscriptionIntMapping |> Dict.union subscriptionIntMapping
170+ }
171+ , Cmd.batch
172+ [ cmds
173+ , Ur.Requests.send
174+ { url = url
175+ , error = Noop
176+ , success = Noop
177+ , requests = urReqs
178+ }
179+ , send
180+ { requests = subscriptionRequests
181+ , url = url
182+ , error = subscriptionRequests |> List.map (\( _, x ) -> x) |> FailedRequest
183+ , success = Noop
184+ }
185+ ]
186+ )
187+
188+ EventSourceMsg value ->
189+ case JD.decodeValue (JD.field "message" JD.string) value of
190+ Ok string ->
191+ case
192+ D.runBytes
193+ (D.cell D.int (D.cell D.cord D.tar)
194+ |> D.map (\a b c -> ( a, b, c ))
195+ )
196+ (Ur.Uw.decode string)
197+ of
198+ Just ( messageId, messageType, rest ) ->
199+ let
200+ ( eventId, ackReqs ) =
201+ tag model.eventId [ Ack messageId ]
202+
203+ ackCmd =
204+ send
205+ { requests = ackReqs
206+ , url = url
207+ , success = Noop
208+ , error = ackReqs |> List.map (\( _, x ) -> x) |> FailedRequest
209+ }
210+
211+ model_ =
212+ { model | eventId = eventId }
213+ in
214+ case messageType of
215+ "watch-ack" ->
216+ -- Not sure what to do. Assume things are fine.
217+ ( model_, ackCmd )
218+
219+ "poke-ack" ->
220+ -- Not sure what to do.
221+ ( model_, ackCmd )
222+
223+ "fact" ->
224+ case
225+ Dict.get messageId model.subscriptionIntMapping
226+ |> Maybe.andThen (\key -> Dict.get key model.subscriptions)
227+ of
228+ Just { deconstructor } ->
229+ case D.run (D.cell D.ignore (D.cell D.ignore deconstructor)) rest of
230+ Just subMsg ->
231+ ( model_, pureCmd (AppMsg subMsg) )
232+
233+ -- Got gargbage
234+ Nothing ->
235+ ( model_, ackCmd )
236+
237+ -- Got a fact for a subscription we do not hold
238+ Nothing ->
239+ ( model_, ackCmd )
240+
241+ _ ->
242+ ( model_, ackCmd )
243+
244+ -- got something we don't expect
245+ Nothing ->
246+ ( model, Cmd.none )
247+
248+ Err _ ->
249+ case JD.decodeValue (JD.field "error" JD.value) value of
250+ Ok _ ->
251+ ( { model | connected = False }, Cmd.none )
252+
253+ Err _ ->
254+ -- we got garbage
255+ ( model, Cmd.none )
256+
257+ NeedsActivation ->
258+ let
259+ ( eventId, reqs ) =
260+ [ Poke { ship = "~zod", agent = "hood", mark = "helm-hi", noun = C.cord "Opening airlock!" } ]
261+ |> tag model.eventId
262+ in
263+ ( { model | eventId = eventId }
264+ , send { url = url, requests = reqs, success = OpenConnection, error = NeedsActivation }
265+ )
266+
267+ Noop ->
268+ ( model, Cmd.none )
269+
270+ FailedRequest reqs ->
271+ ( { model | requestsToRetry = reqs ++ model.requestsToRetry }, Cmd.none )
272+
273+ OpenConnection ->
274+ ( { model | connected = True }, inp.createEventSource url )
275+
276+ RetryRequests ->
277+ let
278+ ( eventId, reqs ) =
279+ model.requestsToRetry |> tag model.eventId
280+ in
281+ ( { model | eventId = eventId, requestsToRetry = [] }
282+ , send
283+ { url = url
284+ , error = FailedRequest model.requestsToRetry
285+ , success = Noop
286+ , requests = reqs
287+ }
288+ )
289+
290+
291+processCmd : EventId -> Ur.Cmd.Cmd msg -> ( EventId, Cmd (Msg msg), List ( EventId, UrbitRequest ) )
292+processCmd eventId urCmds =
293+ let
294+ ( cmds, reqs ) =
295+ urCmds
296+ |> List.map
297+ (\x ->
298+ case x of
299+ Ur.Cmd.Internal.Cmd cmd ->
300+ cmd |> Cmd.map AppMsg |> Left
301+
302+ Ur.Cmd.Internal.Poke p ->
303+ Ur.Requests.Poke p |> Right
304+ )
305+ |> Either.partition
306+
307+ ( newEventId, urReqs ) =
308+ reqs |> tag eventId
309+ in
310+ ( newEventId, Cmd.batch cmds, urReqs )
311+
312+
313+pureCmd : msg -> Cmd msg
314+pureCmd msg =
315+ Task.succeed msg |> Task.perform identity
316+
317+
318+processUrSubs :
319+ EventId
320+ -> Dict ( Ship, Agent, Path ) { deconstructor : d, number : EventId }
321+ -> Dict ( Ship, Agent, Path ) d
322+ ->
323+ { subscriptions : Dict ( Ship, Agent, Path ) { deconstructor : d, number : EventId }
324+ , eventId : EventId
325+ , subscriptionRequests : List ( EventId, UrbitRequest )
326+ , subscriptionIntMapping : Dict EventId ( Ship, Agent, Path )
327+ }
328+processUrSubs eventId existingSubscriptions urbitSubs_ =
329+ let
330+ urbitSubs =
331+ urbitSubs_
332+ |> Dict.map (\_ deconstructor -> { deconstructor = deconstructor })
333+
334+ ( eventId_, newSubscriptionActions ) =
335+ Dict.diff urbitSubs existingSubscriptions
336+ |> Dict.toList
337+ |> List.map (\( address, _ ) -> ( Subscribe address, address ))
338+ |> tag eventId
339+
340+ removedSubscriptions =
341+ Dict.diff existingSubscriptions urbitSubs
342+
343+ ( eventId__, removedSubscriptionActions ) =
344+ removedSubscriptions
345+ |> Dict.toList
346+ |> List.map (\( _, { number } ) -> Unsubscribe number)
347+ |> tag eventId_
348+
349+ keyToNumber =
350+ newSubscriptionActions |> List.map (\( a, ( _, b ) ) -> ( b, a )) |> Dict.fromList
351+
352+ newSubscriptions =
353+ Dict.merge
354+ (\_ _ x -> x)
355+ (\key number { deconstructor } ->
356+ Dict.insert key
357+ { deconstructor = deconstructor
358+ , number = number
359+ }
360+ )
361+ (\_ _ x -> x)
362+ keyToNumber
363+ urbitSubs
364+ Dict.empty
365+ in
366+ { subscriptions = Dict.diff existingSubscriptions removedSubscriptions |> Dict.union newSubscriptions
367+ , subscriptionIntMapping =
368+ newSubscriptions
369+ |> Dict.toList
370+ |> List.map (\( key, { number } ) -> ( number, key ))
371+ |> Dict.fromList
372+ , eventId = eventId__
373+ , subscriptionRequests =
374+ removedSubscriptionActions ++ (newSubscriptionActions |> List.map (\( id, ( req, _ ) ) -> ( id, req )))
375+ }