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.JavaScriptSubscriptionRef
SubscriptionRef<'a> is a reference whose changes can be streamed. SubscriptionRef.changes starts with the value that
is current when the stream starts, then delivers every later update. Nothing falls between the two: an update made
after that first value always reaches the stream, and no update arrives twice.
Use it when consumers join at different times and each needs the current state before the updates, such as a display that opens after a sensor feed started. A hub subscriber sees only values published after it subscribes, so it would wait for the next reading with nothing to show.
> open Axial.PlatformService;;
> (flow {
- let! (temperature: Axial.State.SubscriptionRef<float>) = Axial.State.SubscriptionRef.make 20.0
- do! temperature |> Axial.State.SubscriptionRef.set 21.5
- let! shown = Deferred.make<ClockEnvironment, Never, unit> ()
- let! display =
- temperature
- |> Axial.State.SubscriptionRef.changes QueueStrategy.Unbounded
- |> FlowStream.tapFlow (fun _ -> Deferred.succeed () shown |> Flow.ignore)
- |> FlowStream.take 3
- |> FlowStream.runCollect
- |> Flow.fork
- do! Deferred.await shown
- do! temperature |> Axial.State.SubscriptionRef.set 22.0
- do! temperature |> Axial.State.SubscriptionRef.update (fun value -> value + 0.5)
- return! Fiber.join display
- } : Flow<ClockEnvironment, Never, float list>)
- |> Flow.run (ClockEnvironment Clock.live);;val it: Exit<float list,Never> = Success [21.5; 22.0; 22.5]AxialPlatformServiceflow: FlowBuilderThe universal flow { } computation expression.
temperature: SubscriptionRef<float>Axial.State.SubscriptionRef`1A mutable reference whose changes can be streamed: each stream starts with the current value and then receives every later update, with no gap and no duplicate between the two. Use it for state that consumers join at different times, such as the latest reading shown by a display that opens after the feed started. A plain gives a late subscriber only later values; a SubscriptionRef also gives it the value current when it joined. The type of the stored value.
StatefloatAn abbreviation for the CLI type . Basic Types
make: 'a -> Flow<'env,'error,SubscriptionRef<'a>>Creates a reference holding . open Axial.State let temperature : Flow<unit, Never, SubscriptionRef<float>> = SubscriptionRef.make 20.0
Axial.State.SubscriptionRefModuleCreates, reads, updates, and streams values. Updates are serialized with the stream subscriptions: a changes stream reads the current value and subscribes while no update is in progress, so it never misses an update made after that value, and never sees one twice. An update is delivered to every changes stream before the next update starts. A stream using a full QueueStrategy.BackPressure buffer therefore makes updates wait for it, as a hub publisher does.
(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
set: 'a -> SubscriptionRef<'a> -> Flow<'env,'error,unit>Replaces the value and delivers it to every changes stream. temperature |> SubscriptionRef.set 21.5
shown: Deferred<Never,unit>Axial.DeferredModuleFlow-native helpers for one-shot typed coordination.
make: unit -> Flow<'env,'error,Deferred<'error,'value>>Creates an empty deferred value.
Axial.ClockEnvironmentAn environment containing only a clock, for timed flows with no other services.
Axial.NeverRepresents an error channel that cannot occur.
unitThe type 'unit', which has only one value "()". This value is special and always uses the representation 'null'. Basic Types
display: Fiber<Never,float list>changes: QueueStrategy -> SubscriptionRef<'a> -> FlowStream<'env,'error,'a>Streams the current value followed by every later update, buffered for this stream by . The stream subscribes when it starts and unsubscribes when it ends. It does not end on its own; stop it with FlowStream.take, by interrupting its consumer, or by closing the consumer's scope. Use QueueStrategy.Sliding 1 for a consumer that only needs the latest value, and a lossless strategy for one that must see every change. temperature |> SubscriptionRef.changes (QueueStrategy.Sliding 1) |> FlowStream.runForEach (printfn "%.1f")
Axial.QueueStrategyWhat a queue, or a hub subscription, does with a value offered while it is full. Lossless delivery needs either back-pressure or unbounded memory. BackPressure keeps every value by making the producer wait; Unbounded keeps every value by growing without limit; Dropping and Sliding never make the producer wait and lose values instead. A non-positive capacity fails the flow that uses the strategy with a defect.
UnboundedLossless and never suspends the producer; memory grows with the backlog.
Axial.FlowStreamModuletapFlow: ('a -> Flow<'b,'c,unit>) -> FlowStream<'b,'c,'a> -> FlowStream<'b,'c,'a>Runs an effect for each value before emitting the original value. stream |> FlowStream.tapFlow logValue
succeed: 'value -> Deferred<'error,'value> -> Flow<'env,'workflowError,bool>Attempts to complete the deferred value successfully.
Axial.Flowignore: Flow<'env,'error,'value> -> Flow<'env,'error,unit>Maps the successful value of a synchronous flow to unit. The source flow. A flow that succeeds with unit instead of the original value. let flow = Flow.succeed 42 |> Flow.ignore
take: int -> FlowStream<'a,'b,'c> -> FlowStream<'a,'b,'c>Emits at most values. stream |> FlowStream.take 10
runCollect: FlowStream<'a,'b,'c> -> Flow<'a,'b,'c list>Collects all emitted values into a list. stream |> FlowStream.runCollect
fork: Flow<'env,'error,'value> -> Flow<'env,'none,Fiber<'error,'value>>Starts a flow in a new fiber without waiting for it to complete. Forking turns a cold flow description into hot child work and returns a handle that can later be joined or interrupted. Prefer zipPar or race when the caller only needs a simple parallel composition. Wait for the handle with Fiber.join or Fiber.await, and stop it with Fiber.interrupt. The fiber runs in its own scope and releases its scoped resources (anything registered with a scope, such as scopeResource, scopeFinalizer, or a hub subscription) when it settles, whether or not anyone joins it. A value the fiber returns therefore outlives the scoped resources the fiber acquired: a resource returned through Fiber.join has already been released. Use forkResource to acquire a resource in the background and keep it, or zipPar and sequencePar to acquire several in parallel. The flow to fork. A flow that produces a handle.
await: Deferred<'error,'value> -> Flow<'env,'error,'value>Waits for the deferred outcome, preserving success, typed failure, defect, or interruption.
update: ('a -> 'a) -> SubscriptionRef<'a> -> Flow<'env,'error,unit>Applies a pure function to the value and delivers the result to every changes stream.
value: float(+): ^T1 -> ^T2 -> ^T3Overloaded addition operator The first parameter. The second parameter. The result of the operation. 2 + 2 // Evaluates to 4 "Hello " + "World" // Evaluates to "Hello World"
Axial.FiberModuleOperations on a running , the handle returned by Flow.fork. Every operation returns a flow; nothing waits or interrupts until that flow runs. Reading a fiber's outcome through join, await, or interrupt marks it observed, so a defect it died with is not also reported as unobserved.
join: Fiber<'error,'value> -> Flow<'env,'error,'value>Waits for a fiber and returns its value, failing the same way the fiber failed. Joining preserves the child's error channel: a Cause.Fail becomes the same typed error, and interruption and defects remain interruption and defects. Use await to inspect the outcome instead. The fiber to join. A flow that completes with the fiber's value. let loadProfile : Flow<unit, string, string> = Flow.ok "profile" let loadOrders : Flow<unit, string, int list> = Flow.ok [ 1; 2 ] let page = flow { let! fiber = Flow.fork loadProfile let! orders = loadOrders let! profile = Fiber.join fiber return profile, orders }
Axial.Flow`3listThe type of immutable singly-linked lists. See the module for further operations related to lists. Use the constructors [] and :: (infix) to create values of this type, or the notation [1; 2; 3]. Use the values in the List module to manipulate values of this type, or pattern match against the values directly. See also F# Language Guide - Lists.
run: 'env -> Flow<'env,'error,'value> -> Exit<'value,'error>Runs the workflow and blocks until the final exit is available. The environment used by the workflow. The workflow to run. The final workflow exit. let exit = workflow |> Flow.run environment
``.ctor``: IClock -> ClockEnvironmentAxial.PlatformService.ClockHelpers for the clock service.
live: IClockCreates a live clock backed by and a monotonic timer.
The display joined after the value had changed from 20.0 to 21.5, so it started from 21.5. The writer waited until
the display had shown that reading, then made two updates, and the display saw both.
Choosing how each stream buffers
changes takes a QueueStrategy for that stream alone. A display that only needs the newest value uses
QueueStrategy.Sliding 1: while it is busy, older updates are replaced and the writer never waits. A recorder that must
see every change uses QueueStrategy.Unbounded, or QueueStrategy.BackPressure n when a full buffer should make
updates wait for it.
Reading and updating
SubscriptionRef.get reads the current value. set, update, and modify change it and deliver the new value to every
running changes stream before the next update starts. modify takes a function returning (result, newValue), in the
same order as Ref.modify.
A changes stream subscribes when it starts and unsubscribes when it ends. It does not end on its own: stop it with
FlowStream.take or FlowStream.takeWhile, by interrupting its consumer, or by closing the consumer's scope.
The refs torture test checks that a late view sees no gap and no duplicate while updates race with it.

