elm-ssr 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,170 @@
1
+ module ElmSsr.Runtime exposing
2
+ ( Config, Ports
3
+ , State, Msg
4
+ , program
5
+ )
6
+
7
+ {-| The engine that renders routes.
8
+
9
+ Every route is a stateless page: the runtime decodes the request, runs the
10
+ matched route's loader (pumping its effects through the Worker), and renders the
11
+ resulting document. There is no document-level update loop — all interactivity
12
+ lives in islands ([`ElmSsr.Island`](./Island.elm)).
13
+
14
+ Application authors never call this directly; the generated `Main` wires the
15
+ ports and the file-based router to [`program`](#program).
16
+
17
+ @docs Config, Ports
18
+ @docs State, Msg
19
+ @docs program
20
+
21
+ -}
22
+
23
+ import ElmSsr.Action as Action exposing (Action)
24
+ import ElmSsr.Document as Document exposing (Document)
25
+ import ElmSsr.Document.Encode as Encode
26
+ import ElmSsr.Loader as Loader exposing (Loader)
27
+ import ElmSsr.Page as Page
28
+ import ElmSsr.Route as Route exposing (Request)
29
+ import Json.Decode as Decode
30
+ import Json.Encode as Json
31
+ import Platform
32
+
33
+
34
+ {-| The ports the generated `Main` connects to the JS runtime. -}
35
+ type alias Ports =
36
+ { effectRequest : Json.Value -> Cmd Msg
37
+ , effectResult : (Decode.Value -> Msg) -> Sub Msg
38
+ , rendered : Json.Value -> Cmd Msg
39
+ , start : (Decode.Value -> Msg) -> Sub Msg
40
+ }
41
+
42
+
43
+ {-| The file-based router plus the ports. -}
44
+ type alias Config =
45
+ { router : Request -> Loader (Document Never)
46
+ , action : Request -> Action (Document Never)
47
+ , ports : Ports
48
+ }
49
+
50
+
51
+ {-| Runtime state for one request. -}
52
+ type State
53
+ = AwaitingStart Request
54
+ | LoadingPage (Decode.Value -> Loader (Document Never))
55
+ | PerformingAction (Decode.Value -> Action (Document Never))
56
+ | Rendered
57
+ | Aborted Int String
58
+
59
+
60
+ {-| Runtime messages. -}
61
+ type Msg
62
+ = StartRequested
63
+ | EffectResolved Decode.Value
64
+
65
+
66
+ {-| Build the `Program` for an app's routes. -}
67
+ program : Config -> Program Decode.Value State Msg
68
+ program config =
69
+ Platform.worker
70
+ { init = init
71
+ , update = update config
72
+ , subscriptions = subscriptions config
73
+ }
74
+
75
+
76
+ init : Decode.Value -> ( State, Cmd Msg )
77
+ init flags =
78
+ case Decode.decodeValue Route.decoder flags of
79
+ Ok request ->
80
+ ( AwaitingStart request, Cmd.none )
81
+
82
+ Err decodeError ->
83
+ ( Aborted 400 ("Invalid request flags: " ++ Decode.errorToString decodeError), Cmd.none )
84
+
85
+
86
+ update : Config -> Msg -> State -> ( State, Cmd Msg )
87
+ update config msg state =
88
+ case msg of
89
+ StartRequested ->
90
+ case state of
91
+ AwaitingStart request ->
92
+ if Route.method request == "GET" || Route.method request == "HEAD" then
93
+ advance config (config.router request)
94
+
95
+ else
96
+ advanceAction config (config.action request)
97
+
98
+ Aborted status message ->
99
+ ( state, renderError config status message )
100
+
101
+ _ ->
102
+ ( state, Cmd.none )
103
+
104
+ EffectResolved value ->
105
+ case state of
106
+ LoadingPage continue ->
107
+ advance config (continue value)
108
+
109
+ PerformingAction continue ->
110
+ advanceAction config (continue value)
111
+
112
+ _ ->
113
+ ( state, Cmd.none )
114
+
115
+
116
+ advance : Config -> Loader (Document Never) -> ( State, Cmd Msg )
117
+ advance config loader =
118
+ case Loader.step loader of
119
+ Loader.Resolved document ->
120
+ ( Rendered, render config document )
121
+
122
+ Loader.Errored status message ->
123
+ ( Aborted status message, renderError config status message )
124
+
125
+ Loader.Await effect continue ->
126
+ ( LoadingPage continue, config.ports.effectRequest (Loader.encodeEffect effect) )
127
+
128
+
129
+ advanceAction : Config -> Action (Document Never) -> ( State, Cmd Msg )
130
+ advanceAction config action =
131
+ case Action.step action of
132
+ Action.Resolved document ->
133
+ ( Rendered, render config document )
134
+
135
+ Action.Errored status message ->
136
+ ( Aborted status message, renderError config status message )
137
+
138
+ Action.Moved url ->
139
+ ( Rendered, config.ports.rendered (Action.encodeStep (always Json.null) (Action.Moved url)) )
140
+
141
+ Action.SentJson value ->
142
+ ( Rendered, config.ports.rendered (Action.encodeStep (always Json.null) (Action.SentJson value)) )
143
+
144
+ Action.Await effect continue ->
145
+ ( PerformingAction continue, config.ports.effectRequest (Loader.encodeEffect effect) )
146
+
147
+
148
+ render : Config -> Document Never -> Cmd Msg
149
+ render config document =
150
+ config.ports.rendered (Action.encodeStep Encode.encode (Action.Resolved (Document.map never document)))
151
+
152
+
153
+ renderError : Config -> Int -> String -> Cmd Msg
154
+ renderError config status message =
155
+ config.ports.rendered
156
+ (Json.object
157
+ [ ( "kind", Json.string "errored" )
158
+ , ( "status", Json.int status )
159
+ , ( "message", Json.string message )
160
+ , ( "value", Encode.encode (Document.map never (Page.error status message)) )
161
+ ]
162
+ )
163
+
164
+
165
+ subscriptions : Config -> State -> Sub Msg
166
+ subscriptions config _ =
167
+ Sub.batch
168
+ [ config.ports.start (\_ -> StartRequested)
169
+ , config.ports.effectResult EffectResolved
170
+ ]