Repository F# setup
open System
open System.IO
open System.Threading
open System.Threading.Tasks
open Axial
open Axial.Layers
open Axial.Console
open Axial.FileSystem
open Axial.Hosting
open Axial.Hosting.Browser
open Axial.Hosting.Node
open Axial.PlatformService
open Axial.State
open Axial.Telemetry
open Axial.Telemetry.JavaScriptElmish programs
The Axial.ElmishFlows package exposes the Axial.ElmishFlows namespace and connects an Elmish 5 program to Axial
workflows. Create one runtime per program, passing an environment and a Post function that delivers callbacks on the
host's dispatch thread. When already on that thread, Post should run inline; otherwise it should post to it. Do not
call Elmish's dispatch directly from workers. Connect OnDefect to the host's crash reporting path; typed failures
are messages, but defects are not.
Install
dotnet add package Axial.ElmishFlows
Axial.ElmishFlows brings Elmish 5 with it. Reference Elmish directly when your program uses Program.mkProgram and
dispatch. Open Axial.ElmishFlows for the Axial API, and Axial.PlatformService when your environment needs
Clock.live.
A minimal runtime supplies an environment, a Post function, a defect handler, and a history capacity:
open Axial
open Axial.ElmishFlows
open Axial.PlatformService
// In an Elmish host, `Post` is usually the program's dispatch, so callbacks run on the host's dispatch thread.
// Here the code is already on that thread, so it runs the callback inline.
let runtime =
ElmishRuntime.start
{ Post = (fun callback -> callback ())
OnDefect = (fun defect -> failwith defect.Rendered)
HistoryCapacity = 32 }
(ClockEnvironment Clock.live)When Post is not already on the dispatch thread it must post to it (for example Application.Post in WPF or
dispatching back to a UI loop); never call Elmish's dispatch directly from a worker.
First-use lifecycle
ElmishRuntime requires an environment implementing IHasClock. Keep the runtime's clock environment stable, and
supply a command's current dependencies with Flow.localEnv. For example, a git client can open another repository
without recreating its Elmish runtime:
open Axial
open Axial.ElmishFlows
open Axial.PlatformService
type RepositoryEnvironment = { Clock: IClock; Repository: string } with
interface IHasClock with
member this.Clock = this.Clock
let runtime =
ElmishRuntime.start
{ Post = (fun callback -> callback ())
OnDefect = (fun defect -> failwith defect.Rendered)
HistoryCapacity = 32 }
(ClockEnvironment Clock.live)
let currentEnv = { Clock = Clock.live; Repository = "/repos/new" }
let repositoryName : Flow<RepositoryEnvironment, string, string> =
Flow.envWith (fun env -> env.Repository)
let command =
Cmd.OfFlow.either "repository" runtime
(repositoryName |> Flow.localEnv (fun _ -> currentEnv))
id
(fun _ -> "unavailable")Flow.localEnv selects the environment when the command runs; capture the selected environment for each command when
constructing it so later repository switches do not change already-issued requests.
Provisioning with startLayer
When a runtime needs resources that must live for the whole program — a Cache.make* cache, queues, hubs, or a
Layer.fromFlow resource — build them with a layer and start the runtime with ElmishRuntime.startLayer. The layer
is provisioned inside the runtime's root scope, so Stop/StopAndWait releases what the layer acquired. A typed
provisioning failure returns Error and leaves nothing running.
open Axial
open Axial.Layers
open Axial.ElmishFlows
open Axial.PlatformService
type CacheEnvironment = { Clock: IClock; Cache: Cache<int, Never, string> } with
interface IHasClock with
member this.Clock = this.Clock
let layer : Layer<ClockEnvironment, string, CacheEnvironment> =
Layer.fromFlow (flow {
let! cache =
Cache.make (fun key -> Flow.succeed $"value-{key}" : Flow<ClockEnvironment, Never, string>)
let! clock = Flow.envWith (fun env -> env.Clock)
return { Clock = clock; Cache = cache }
})
let runtime =
match
ElmishRuntime.startLayer
{ Post = (fun callback -> callback ())
OnDefect = (fun defect -> failwith defect.Rendered)
HistoryCapacity = 32 }
layer
(ClockEnvironment Clock.live)
|> Async.RunSynchronously
with
| Ok runtime -> runtime
| Error error -> failwith errorCommand parameter order
Axial's commands keep Elmish's "work first" idea, but the runtime owns the command's root scope, so it precedes the flow. Pass the flow positionally, not piped:
let command =
Cmd.OfFlow.either "increment" runtime
(Flow.succeed 2)
id
(fun (_: string) -> 0)The order is name runtime workflow onSuccess onError for either; perform drops onError, attempt drops
onSuccess, and Cmd.OfFlow.start name runtime workflow is fire-and-forget: it runs a
Flow<'env, Never, unit> as a named fiber in the root scope, dispatches nothing, and reports defects to OnDefect.
Operation reference
The exhaustive operation inventory lives in the API reference:
- Cmd:
Cmd.OfFlow.either/perform/attempt/startand the latest-winsCmd.OfFlow.Latestvariants. - Sub:
Sub.OfFlowStream.subscribefor running a stream under a subscription id. - Inbox:
Inbox.create/offer/post/dropped/streamfor synchronous UI input and search-as-you-type. - ElmishRuntime:
start,startLayer,test, and theStop/StopAndWaitshutdown methods. - TestRuntime:
run,settle,drain,dispatched, anddefectsfor tests with a manual clock.

