@@ -1,22 +1,28 @@
1−module Ur.Run exposing (Model, Msg, application)
1+module Ur.Run exposing (element, document, application, Program, Model, Msg)
2+
3+{-| This module contains functions to run your application with Urbit integration.
4+
5+@docs element, document, application, Program, Model, Msg
6+
7+-}
28
39import Browser exposing (Document, UrlRequest)
410import Browser.Navigation as Nav
511import Dict exposing (Dict)
612import Either exposing (Either(..))
7−import Html
13+import Html exposing (Html)
814import Json.Decode as JD
915import Task
1016import Time
11−import Ur exposing (Agent, Path)
1217import Ur.Cmd
1318import Ur.Cmd.Internal
1419import Ur.Constructor as C
1520import Ur.Deconstructor as D
16−import Ur.Phonemic exposing (Ship)
21+import Ur.NounDiff exposing (Patch, deconstructPatch)
1722import Ur.Requests exposing (..)
1823import Ur.Sub
1924import Ur.Sub.Internal
25+import Ur.Types exposing (..)
2026import Ur.Uw
2127import Url exposing (Url)
2228
@@ -25,8 +31,9 @@ type alias SubDict msg =
2531 Dict
2632 -- (ship, agent, path)
2733 ( String, String, List String )
28− { deconstructor : D.Deconstructor (msg -> msg) msg
34+ { deconstructor : D.Deconstructor msg
2935 , number : Int
36+ , sink : Bool
3037 }
3138
3239
@@ -34,6 +41,8 @@ type alias Flags =
3441 { uid : String }
3542
3643
44+{-| An Urbit wrapper around your application model.
45+-}
3746type alias Model app msg =
3847 { subscriptions : SubDict msg
3948 , subscriptionIntMapping : Dict Int ( String, String, List String )
@@ -42,9 +51,12 @@ type alias Model app msg =
4251 , eventId : Int
4352 , flags : Flags
4453 , requestsToRetry : List UrbitRequest
54+ , sinks : Dict Int Noun
4555 }
4656
4757
58+{-| An Urbit wrapper around your application messages.
59+-}
4860type Msg msg
4961 = AppMsg msg
5062 | EventSourceMsg JD.Value
@@ -55,6 +67,61 @@ type Msg msg
5567 | RetryRequests
5668
5769
70+{-| A wrapper around your application `Program`.
71+-}
72+type alias Program model msg =
73+ Platform.Program Flags (Model model msg) (Msg msg)
74+
75+
76+{-| The same as `Browser.element` but with urbit stuff added around it.
77+-}
78+element :
79+ { init : ( model, Ur.Cmd.Cmd msg )
80+ , view : model -> Html msg
81+ , update : msg -> model -> ( model, Ur.Cmd.Cmd msg )
82+ , subscriptions : model -> Sub msg
83+ , urbitSubscriptions : model -> Ur.Sub.Sub msg
84+ , createEventSource : String -> Cmd (Msg msg)
85+ , onEventSourceMsg : (JD.Value -> Msg msg) -> Sub (Msg msg)
86+ , urbitUrl : model -> String
87+ }
88+ -> Program model msg
89+element inp =
90+ Browser.element
91+ { init = init inp inp.init
92+ , view = \model -> inp.view model.app |> Html.map AppMsg
93+ , update = update inp
94+ , subscriptions = subscriptions inp
95+ }
96+
97+
98+{-| The same as `Browser.element` but with urbit stuff added around it.
99+-}
100+document :
101+ { init : ( model, Ur.Cmd.Cmd msg )
102+ , view : model -> Document msg
103+ , update : msg -> model -> ( model, Ur.Cmd.Cmd msg )
104+ , subscriptions : model -> Sub msg
105+ , urbitSubscriptions : model -> Ur.Sub.Sub msg
106+ , createEventSource : String -> Cmd (Msg msg)
107+ , onEventSourceMsg : (JD.Value -> Msg msg) -> Sub (Msg msg)
108+ , urbitUrl : model -> String
109+ }
110+ -> Program model msg
111+document inp =
112+ Browser.document
113+ { init = init inp inp.init
114+ , view =
115+ \model ->
116+ inp.view model.app
117+ |> (\{ body, title } -> { title = title, body = body |> List.map (Html.map AppMsg) })
118+ , update = update inp
119+ , subscriptions = subscriptions inp
120+ }
121+
122+
123+{-| The same as `Browser.element` but with urbit stuff added around it.
124+-}
58125application :
59126 { init : Url -> Nav.Key -> ( model, Ur.Cmd.Cmd msg )
60127 , view : model -> Document msg
@@ -67,66 +134,20 @@ application :
67134 , onEventSourceMsg : (JD.Value -> Msg msg) -> Sub (Msg msg)
68135 , urbitUrl : model -> String
69136 }
70− -> Program Flags (Model model msg) (Msg msg)
137+ -> Program model msg
71138application inp =
72139 let
73− { init, view, onUrlRequest, onUrlChange, onEventSourceMsg } =
140+ { view, onUrlRequest, onUrlChange } =
74141 inp
75142 in
76143 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− )
144+ { init = \flags url key -> init inp (inp.init url key) flags
114145 , view =
115146 \model ->
116147 view model.app
117148 |> (\{ body, title } -> { title = title, body = body |> List.map (Html.map AppMsg) })
118149 , 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− ]
150+ , subscriptions = subscriptions inp
130151 , onUrlRequest = \req -> onUrlRequest req |> AppMsg
131152 , onUrlChange = \url -> onUrlChange url |> AppMsg
132153 }
@@ -153,20 +174,20 @@ update inp msg model =
153174 ( appModel, appCmds ) =
154175 inp.update msg_ model.app
155176
156− { subscriptions, eventId, subscriptionRequests, subscriptionIntMapping } =
177+ subsResult =
157178 processUrSubs
158179 model.eventId
159180 model.subscriptions
160− (inp.urbitSubscriptions model.app |> (\(Ur.Sub.Internal.Sub x) -> x))
181+ (inp.urbitSubscriptions appModel |> (\(Ur.Sub.Internal.Sub x) -> x))
161182
162183 ( eventId_, cmds, urReqs ) =
163− processCmd eventId appCmds
184+ processCmd subsResult.eventId appCmds
164185 in
165186 ( { model
166187 | app = appModel
167188 , eventId = eventId_
168− , subscriptions = subscriptions
169− , subscriptionIntMapping = model.subscriptionIntMapping |> Dict.union subscriptionIntMapping
189+ , subscriptions = subsResult.subscriptions
190+ , subscriptionIntMapping = model.subscriptionIntMapping |> Dict.union subsResult.subscriptionIntMapping
170191 }
171192 , Cmd.batch
172193 [ cmds
@@ -177,9 +198,9 @@ update inp msg model =
177198 , requests = urReqs
178199 }
179200 , send
180− { requests = subscriptionRequests
201+ { requests = subsResult.subscriptionRequests
181202 , url = url
182− , error = subscriptionRequests |> List.map (\( _, x ) -> x) |> FailedRequest
203+ , error = subsResult.subscriptionRequests |> List.map (\( _, x ) -> x) |> FailedRequest
183204 , success = Noop
184205 }
185206 ]
@@ -190,12 +211,10 @@ update inp msg model =
190211 Ok string ->
191212 case
192213 D.runBytes
193− (D.cell D.int (D.cell D.cord D.tar)
194− |> D.map (\a b c -> ( a, b, c ))
195− )
214+ (D.cell D.int (D.cell D.cord D.tar))
196215 (Ur.Uw.decode string)
197216 of
198− Just ( messageId, messageType, rest ) ->
217+ Just ( messageId, ( messageType, rest ) ) ->
199218 let
200219 ( eventId, ackReqs ) =
201220 tag model.eventId [ Ack messageId ]
@@ -225,14 +244,51 @@ update inp msg model =
225244 Dict.get messageId model.subscriptionIntMapping
226245 |> Maybe.andThen (\key -> Dict.get key model.subscriptions)
227246 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 )
247+ Just { deconstructor, sink } ->
248+ if sink then
249+ case D.run (D.cell D.ignore (D.cell D.ignore deconstructSink) |> D.map (\( (), ( (), s ) ) -> s)) rest of
250+ Just (Flush noun) ->
251+ ( { model | sinks = Dict.insert messageId noun model.sinks }
252+ , case D.run deconstructor noun of
253+ Just subMsg ->
254+ pureCmd (AppMsg subMsg)
255+
256+ -- Got garbage
257+ Nothing ->
258+ Cmd.none
259+ )
260+
261+ Just (Drain patch) ->
262+ case Dict.get messageId model.sinks of
263+ Just oldNoun ->
264+ let
265+ newNoun =
266+ Ur.NounDiff.apply patch oldNoun
267+ in
268+ ( { model | sinks = Dict.insert messageId newNoun model.sinks }
269+ , case D.run deconstructor newNoun of
270+ Just subMsg ->
271+ pureCmd (AppMsg subMsg)
272+
273+ Nothing ->
274+ Cmd.none
275+ )
276+
277+ Nothing ->
278+ ( model, Cmd.none )
279+
280+ -- Got garbage
281+ Nothing ->
282+ ( model, Cmd.none )
283+
284+ else
285+ case D.run (D.cell D.ignore (D.cell D.ignore deconstructor) |> D.map (\( (), ( (), m ) ) -> m)) rest of
286+ Just subMsg ->
287+ ( model_, pureCmd (AppMsg subMsg) )
288+
289+ -- Got gargbage
290+ Nothing ->
291+ ( model_, ackCmd )
236292
237293 -- Got a fact for a subscription we do not hold
238294 Nothing ->
@@ -257,11 +313,11 @@ update inp msg model =
257313 NeedsActivation ->
258314 let
259315 ( eventId, reqs ) =
260− [ Poke { ship = "~zod", agent = "hood", mark = "helm-hi", noun = C.cord "Opening airlock!" } ]
316+ [ Poke { ship = "", agent = "hood", mark = "helm-hi", noun = C.cord "Opening airlock!" } ]
261317 |> tag model.eventId
262318 in
263319 ( { model | eventId = eventId }
264− , send { url = url, requests = reqs, success = OpenConnection, error = NeedsActivation }
320+ , send { url = url, requests = reqs, success = OpenConnection, error = Noop }
265321 )
266322
267323 Noop ->
@@ -288,6 +344,19 @@ update inp msg model =
288344 )
289345
290346
347+type Sink
348+ = Flush Noun
349+ | Drain Patch
350+
351+
352+deconstructSink : D.Deconstructor Sink
353+deconstructSink =
354+ D.oneOf
355+ [ D.cell (D.const D.cord "flush") D.tar |> D.map (\( (), n ) -> Flush n)
356+ , D.cell (D.const D.cord "drain") (D.lazy deconstructPatch) |> D.map (\( (), patch ) -> Drain patch)
357+ ]
358+
359+
291360processCmd : EventId -> Ur.Cmd.Cmd msg -> ( EventId, Cmd (Msg msg), List ( EventId, UrbitRequest ) )
292361processCmd eventId urCmds =
293362 let
@@ -317,10 +386,10 @@ pureCmd msg =
317386
318387processUrSubs :
319388 EventId
320− -> Dict ( Ship, Agent, Path ) { deconstructor : d, number : EventId }
321− -> Dict ( Ship, Agent, Path ) d
389+ -> Dict ( Ship, Agent, Path ) { deconstructor : d, number : EventId, sink : Bool }
390+ -> Dict ( Ship, Agent, Path ) { deconstructor : d, sink : Bool }
322391 ->
323− { subscriptions : Dict ( Ship, Agent, Path ) { deconstructor : d, number : EventId }
392+ { subscriptions : Dict ( Ship, Agent, Path ) { deconstructor : d, number : EventId, sink : Bool }
324393 , eventId : EventId
325394 , subscriptionRequests : List ( EventId, UrbitRequest )
326395 , subscriptionIntMapping : Dict EventId ( Ship, Agent, Path )
@@ -329,7 +398,7 @@ processUrSubs eventId existingSubscriptions urbitSubs_ =
329398 let
330399 urbitSubs =
331400 urbitSubs_
332− |> Dict.map (\_ deconstructor -> { deconstructor = deconstructor })
401+ |> Dict.map (\_ { deconstructor, sink } -> { deconstructor = deconstructor, sink = sink })
333402
334403 ( eventId_, newSubscriptionActions ) =
335404 Dict.diff urbitSubs existingSubscriptions
@@ -352,10 +421,11 @@ processUrSubs eventId existingSubscriptions urbitSubs_ =
352421 newSubscriptions =
353422 Dict.merge
354423 (\_ _ x -> x)
355− (\key number { deconstructor } ->
424+ (\key number { deconstructor, sink } ->
356425 Dict.insert key
357426 { deconstructor = deconstructor
358427 , number = number
428+ , sink = sink
359429 }
360430 )
361431 (\_ _ x -> x)
@@ -373,3 +443,68 @@ processUrSubs eventId existingSubscriptions urbitSubs_ =
373443 , subscriptionRequests =
374444 removedSubscriptionActions ++ (newSubscriptionActions |> List.map (\( id, ( req, _ ) ) -> ( id, req )))
375445 }
446+
447+
448+init :
449+ { a | urbitSubscriptions : model -> Ur.Sub.Sub msg, urbitUrl : model -> String }
450+ -> ( model, Ur.Cmd.Cmd msg )
451+ -> Flags
452+ ->
453+ ( Model model msg
454+ , Cmd (Msg msg)
455+ )
456+init inp ( app, appCmds ) flags =
457+ let
458+ subsResult =
459+ processUrSubs
460+ 0
461+ Dict.empty
462+ (inp.urbitSubscriptions app |> (\(Ur.Sub.Internal.Sub x) -> x))
463+
464+ ( eventId_, cmds, urReqs ) =
465+ processCmd subsResult.eventId appCmds
466+
467+ url =
468+ inp.urbitUrl app ++ "/~/channel/" ++ flags.uid
469+ in
470+ ( { subscriptions = subsResult.subscriptions
471+ , sinks = Dict.empty
472+ , subscriptionIntMapping = subsResult.subscriptionIntMapping
473+ , app = app
474+ , connected = False
475+ , eventId = eventId_
476+ , flags = flags
477+ , requestsToRetry = []
478+ }
479+ , [ cmds
480+ , pureCmd NeedsActivation
481+ , send
482+ { requests = urReqs ++ subsResult.subscriptionRequests
483+ , url = url
484+ , error = Noop
485+ , success = Noop
486+ }
487+ ]
488+ |> Cmd.batch
489+ )
490+
491+
492+subscriptions :
493+ { a | subscriptions : b -> Sub msg, onEventSourceMsg : (JD.Value -> Msg c) -> Sub (Msg msg) }
494+ -> { d | app : b, requestsToRetry : List e, connected : Bool }
495+ -> Sub (Msg msg)
496+subscriptions inp model =
497+ Sub.batch
498+ [ inp.subscriptions model.app |> Sub.map AppMsg
499+ , inp.onEventSourceMsg EventSourceMsg
500+ , if List.isEmpty model.requestsToRetry then
501+ Sub.none
502+
503+ else
504+ Time.every 1000 (always RetryRequests)
505+ , if not model.connected then
506+ Time.every 10000 (always NeedsActivation)
507+
508+ else
509+ Sub.none
510+ ]