Embedded Sky.Live — a Live app inside a Task program

A Sky.Live app normally owns its process. When it starts it:

That is correct for main = App.run app. It is wrong when the Live app is one part of a larger program, for example a worker loop that also serves a small status UI, or a program that runs a Live app next to a Sky.Http.Server.

Embedded mode (v0.27) runs the app as a guest. Turn it on with App.withEmbedded (Std.App) or Live.withEmbedded (Std.Live). In embedded mode the app:

The host program owns shutdown.

The pattern

Spawn the app, then do the other work in the same main:

module Main exposing (main)

import Sky.Core.Prelude exposing (..)
import Sky.Core.Error as Error exposing (Error)
import Sky.Core.Http as Http
import Sky.Core.String as String
import Sky.Core.Task as Task exposing (Step(..))
import Sky.Core.Time as Time
import Std.App as App exposing (Config(..), webDefaults)
import Std.Cmd as Cmd
import Std.Log as Log
import Std.Sub as Sub
import Std.Ui as Ui exposing (Element)


type alias Model =
    { ticks : Int }


type Msg
    = Tick


init : a -> ( Model, Cmd Msg )
init _ =
    ( { ticks = 0 }, Cmd.none )


update : Msg -> Model -> ( Model, Cmd Msg )
update _ model =
    ( { model | ticks = model.ticks + 1 }, Cmd.none )


view : Model -> Element Msg
view model =
    Ui.text ("ticks: " ++ String.fromInt model.ticks)


statusApp =
    App.app { init = init, update = update, view = view, subscriptions = \_ -> Sub.none }
        |> App.withNotFound ()
        |> App.withConfig (WebConfig { webDefaults | port = 8090 })
        |> App.withEmbedded


-- The host's own work: here, a loop that runs forever.
work : Int -> Task Error (Step Int ())
work n =
    Time.sleep 1000
        |> Task.andThen (\_ -> Log.println ("worker tick " ++ String.fromInt n))
        |> Task.map (\_ -> Loop (n + 1))


main : Task Error ()
main =
    Task.spawn
        (App.run statusApp
            |> Task.onError (\e -> Log.println ("status UI did not start: " ++ Error.toString e))
        )
        |> Task.andThen (\_ -> Task.loop work 0)

App.run is rewritten to the target's runner wherever it appears, so it works inside Task.spawn. Task.spawn returns at once; the app serves on its own goroutine. Handle the app's Err inside the spawned Task (Task.onError), as above, or run it without Task.spawn when the rest of main should wait for it.

Starting and stopping an app: App.serve

App.run with withEmbedded runs the app until the process ends; nothing can stop it. App.serve starts the same app and hands back a handle:

App.serve : App HasFallback () page model msg key -> Task Error App.Running
App.address : App.Running -> String
App.stop : App.Running -> Task Error ()
module Main exposing (main)

import Sky.Core.Prelude exposing (..)
import Sky.Core.Error exposing (Error)
import Sky.Core.Http as Http
import Sky.Core.String as String
import Sky.Core.Task as Task
import Std.App as App exposing (Config(..), webDefaults)
import Std.Cmd as Cmd
import Std.Log as Log
import Std.Sub as Sub
import Std.Ui as Ui exposing (Element)


type alias Model =
    { count : Int }


type Msg
    = Increment


init : a -> ( Model, Cmd Msg )
init _ =
    ( { count = 0 }, Cmd.none )


update : Msg -> Model -> ( Model, Cmd Msg )
update _ model =
    ( { model | count = model.count + 1 }, Cmd.none )


view : Model -> Element Msg
view model =
    Ui.text ("count: " ++ String.fromInt model.count)


statusApp =
    App.app { init = init, update = update, view = view, subscriptions = \_ -> Sub.none }
        |> App.withNotFound ()
        |> App.withConfig (WebConfig { webDefaults | port = 0 })


main : Task Error ()
main =
    App.serve statusApp
        |> Task.andThen
            (\running ->
                Http.get ("http://" ++ App.address running ++ "/")
                    |> Task.andThen (\resp -> Log.println ("status UI answered " ++ String.fromInt resp.status))
                    |> Task.andThen (\_ -> App.stop running)
            )

App.serve always runs Sky.Live, whatever --target a build selects. A dispatched Std.App build for a target that does not run Sky.Live (terminal:*, web:app, mobile:*, tablet:<os>, desktop:<os>) refuses an entry whose sources call App.serve, and names the file.

Several apps in one process

Two apps served in one process on different ports keep their own state, and stopping one does not touch the other (v0.27, runtime-go/rt/live_serve.go). Per app:

One per process, by design:

A browser keeps cookies per host, not per port, so two apps on localhost:8001 and localhost:8002 receive the same sky_sid cookie. Each app keeps its own session under that id (the other app's state is never visible), but when one app rotates the id at a sign-in, the other app sees a new id and starts a fresh session. Serve the apps on different host names, or use the header session transport (architecture).

Shutdown

Because the embedded app installs no signal handler, a signal does what the host's own setup says:

Caveats