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.JavaScriptCache and Memoize
When several callers ask for the same expensive value at once, each one starting its own load wastes work and can
overload the source. Cache and Flow.memoize run the load once and let every caller wait for that single result.
This is often called single-flight.
Memoize one computation
Flow.memoize turns a flow into a shared version of itself:
Show page setup
// Setup for the checked examples on this page.
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.JavaScript
/// Fails the docs test when an example's result differs from the value shown.
let shouldEqual expected actual =
if actual <> expected then failwithf "Expected %A but got %A" expected actual// A simulated config load: increments a counter instead of touching the filesystem, so the example
// demonstrates memoization without real I/O.
let simulateConfigLoad (reads: Ref<int>) : Flow<ClockEnvironment, string, string> =
reads |> Ref.updateAndGet ((+) 1) |> Flow.map (fun count -> $"config read {count} time(s)")
/// Loads the config twice through one memoized flow, and returns whether both loads agree and how often it was read.
let sameConfig : Flow<ClockEnvironment, string, bool * int> =
flow {
let! reads = Ref.make 0
let! loadConfig = Flow.memoize (simulateConfigLoad reads)
let! a = loadConfig
let! b = loadConfig // the same value; the load runs once
let! readCount = Ref.get reads
return a = b, readCount
}
simulateConfigLoad: Ref<int> -> Flow<ClockEnvironment,string,string>reads: Ref<int>Axial.State.Ref`1Represents a handle to a mutable reference that can be updated atomically. The type of the value stored in the reference.
intAn abbreviation for the CLI type . Basic Types
Axial.Flow`3Axial.ClockEnvironmentAn environment containing only a clock, for timed flows with no other services.
stringAn abbreviation for the CLI type . Basic Types
(|>): '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
Axial.State.RefModuleupdateAndGet: ('T -> 'T) -> Ref<'T> -> Flow<'env,'none,'T>Updates the value using the supplied function and returns the value after the update. The update function of type 'T -> 'T. The to update. A flow that returns the updated value. Ref.updateAndGet (fun x -> x + 1) myRef
(+): ^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.Flowmap: ('value -> 'next) -> Flow<'env,'error,'value> -> Flow<'env,'error,'next>Transforms the successful value of a flow. If the source fails, the is not executed. The original failure cause is preserved, including typed failures, interruption, and defects. Use map for pure value transformations after an effect has succeeded. A function of type 'value -> 'next to transform the successful value. The source flow of type to transform. A new with the transformed success value of type 'next. let flow = Flow.succeed 1 |> Flow.map (fun x -> x + 1)
count: intsameConfig: Flow<ClockEnvironment,string,(bool * int)>boolAn abbreviation for the CLI type . Basic Types
flow: FlowBuilderThe universal flow { } computation expression.
make: 'T -> Flow<'env,'none,Ref<'T>>Creates a new with the initial value. The initial value of the reference. A flow that creates and returns the reference. (Ref.make 10).RunSynchronously(())
loadConfig: Flow<ClockEnvironment,string,string>memoize: Flow<'env,'error,'value> -> Flow<'env,'none,Flow<'caller,'error,'value>>Returns a flow that runs at most once at a time and remembers its value. The first caller of the returned flow starts the computation; callers that arrive while it runs wait for the same result instead of starting another. A success is remembered for every later caller. A failure is not: the next caller starts a fresh attempt. The computation runs in the scope and environment where memoize ran, not in any caller's, so interrupting one caller never cancels the computation the others are waiting for. Closing that scope interrupts it. The computation to share. A flow that produces the memoized flow. let readConfig : Flow<unit, string, string> = Flow.delay (fun () -> Flow.ok "config") let sameConfig : Flow<unit, string, bool> = flow { let! loadConfig = Flow.memoize readConfig let! a = loadConfig let! b = loadConfig // readConfig ran once return a = b }
a: stringb: stringreadCount: intget: Ref<'T> -> Flow<'env,'none,'T>Reads the current value of the reference. The to read from. A flow that returns the current value. let readInitialValue () = flow { let! reference = Ref.make 10 return! Ref.get reference }
(=): 'T -> 'T -> boolStructural equality The first parameter. The second parameter. The result of the comparison. 5 = 5 // Evaluates to true 5 = 6 // Evaluates to false [1; 2] = [1; 2] // Evaluates to true (1, 5) = (1, 6) // Evaluates to false
sameConfig |> Flow.run (ClockEnvironment Clock.live) |> shouldEqual (Exit.Success(true, 1))
sameConfig: Flow<ClockEnvironment,string,(bool * int)>(|>): '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
Axial.Flowrun: '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.
shouldEqual: 'a -> 'a -> unitAxial.Exit`2Represents the final outcome of a workflow execution. The type of the success value. The type of the domain-specific failure value.
SuccessThe workflow completed successfully.
Cache by key
Cache.make requires 'env :> IHasClock: the runtime uses the supplied clock to schedule detached lookup fibers. Cache.makeBounded capacity evicts the least recently read key when full; Cache.makeBoundedWithExpiry capacity timeToLive also refreshes successful entries after the monotonic clock's elapsed duration. Eviction does not cancel existing waiters. Cache.makeRec passes the cache to its lookup for nested reads. Do not look up the same key recursively, directly or through a cycle: single-flight waits for the running lookup and deadlocks.
Cache.make takes a lookup function and returns a cache. Cache.get returns the value for a key, running the lookup
only when no success is cached and no lookup for that key is already running:
let loadUser (lookups: Ref<int>) (id: int) : Flow<ClockEnvironment, string, string> =
flow {
do! Flow.sleep (TimeSpan.FromMilliseconds 5.0)
do! lookups |> Ref.update ((+) 1)
return $"user {id}"
}
/// Loads a page per user id through one cache, and returns the pages and how many lookups ran.
let loadPages (userIds: int list) : Flow<ClockEnvironment, string, string list * int> =
flow {
let! lookups = Ref.make 0
let! users = Cache.make (loadUser lookups)
let! pages = userIds |> Flow.traversePar (Parallelism.bounded 8) (fun id -> users |> Cache.get id)
let! lookupCount = Ref.get lookups
return pages, lookupCount
}
loadUser: Ref<int> -> int -> Flow<ClockEnvironment,string,string>lookups: Ref<int>Axial.State.Ref`1Represents a handle to a mutable reference that can be updated atomically. The type of the value stored in the reference.
intAn abbreviation for the CLI type . Basic Types
id: intAxial.Flow`3Axial.ClockEnvironmentAn environment containing only a clock, for timed flows with no other services.
stringAn abbreviation for the CLI type . Basic Types
flow: FlowBuilderThe universal flow { } computation expression.
Axial.Flowsleep: TimeSpan -> Flow<'env,'error,unit>Suspends the flow for the specified duration, observing cancellation. The duration to sleep. A flow that completes after the specified delay, or is interrupted if cancelled first.
System.TimeSpanRepresents a time interval.
FromMilliseconds: float -> TimeSpanReturns a that represents a specified number of milliseconds. A number of milliseconds. An object that represents . is less than or greater than . -or- is . -or- is . is equal to .
(|>): '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
Axial.State.RefModuleupdate: ('T -> 'T) -> Ref<'T> -> Flow<'env,'none,unit>Updates the value of the reference using the supplied function. The update function of type 'T -> 'T. The to update. A flow that updates the value and returns unit. Ref.update (fun x -> x + 1) myRef
(+): ^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"
loadPages: int list -> Flow<ClockEnvironment,string,(string list * int)>userIds: int listlistThe 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.
make: 'T -> Flow<'env,'none,Ref<'T>>Creates a new with the initial value. The initial value of the reference. A flow that creates and returns the reference. (Ref.make 10).RunSynchronously(())
users: Cache<int,string,string>Axial.CacheModuleFunctions for creating and reading a single-flight cache.
make: ('key -> Flow<'env,'error,'value>) -> Flow<'env,'none,Cache<'key,'error,'value>>Creates a cache that computes missing values with . Requires 'env :> IHasClock because the runtime schedules detached lookup fibers using the supplied clock. Lookups capture the environment and scope where the cache is made. Closing that scope interrupts them.
pages: string listtraversePar: Parallelism -> ('value -> Flow<'env,'error,'next>) -> 'value seq -> Flow<'env,'error,'next list>Maps values to flows and runs them with bounded concurrency, returning results in input order. At most mappings run at once; as each finishes, its worker starts the next value. The first failure interrupts the mappings still running and waits for their cleanup before the flow fails, so no sibling is left running in the background. Size CPU-bound work with Parallelism.ofProcessors. When each worker needs its own connection or handle, use traverseParUsing. Scoped resources follow sequencePar: when every mapping succeeds, what each one acquired moves to the caller's scope; when any fails, all of it is released before the traversal returns. The maximum number of mappings running at once. Maps each value to a flow. The values to map. A flow containing the mapped values in the order of . let! pages = urls |> Flow.traversePar (Parallelism.bounded 8) fetchPage
Axial.ParallelismModuleCreates bounds for parallel Flow and stream operators.
bounded: int -> ParallelismCreates a positive concurrency bound. Thrown when is not positive.
get: 'key -> Cache<'key,'error,'value> -> Flow<'env,'error,'value>Returns the cached value for a key, or starts its lookup.
lookupCount: intget: Ref<'T> -> Flow<'env,'none,'T>Reads the current value of the reference. The to read from. A flow that returns the current value. let readInitialValue () = flow { let! reference = Ref.make 10 return! Ref.get reference }
loadPages [ 1; 2; 1; 1; 2 ]
|> Flow.run (ClockEnvironment Clock.live)
|> shouldEqual (Exit.Success([ "user 1"; "user 2"; "user 1"; "user 1"; "user 2" ], 2))
loadPages: int list -> Flow<ClockEnvironment,string,(string list * int)>(|>): '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
Axial.Flowrun: '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.
shouldEqual: 'a -> 'a -> unitAxial.Exit`2Represents the final outcome of a workflow execution. The type of the success value. The type of the domain-specific failure value.
SuccessThe workflow completed successfully.
Five concurrent requests for two users ran the lookup twice.
Cache.invalidate forgets one key and Cache.invalidateAll forgets every key, so the next get looks up again.
Cache.count reports how many keys are cached or loading.
Expiry, eviction, and in-flight loads
Cache.makeBoundedWithExpiry expires a successful entry after the monotonic clock's time-to-live. The cache reads
the environment's clock, so a manual clock makes expiry deterministic:
let manualClock = ManualClock(DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero))
let expiryLoads : Flow<ClockEnvironment, string, string list * int> =
flow {
let! lookups = Ref.make 0
let loadUser (id: int) : Flow<ClockEnvironment, string, string> =
flow {
do! lookups |> Ref.update ((+) 1)
return $"user {id}"
}
let! users = Cache.makeBoundedWithExpiry 2 (TimeSpan.FromSeconds 10.0) loadUser
let! first = users |> Cache.get 1
let! second = users |> Cache.get 1
manualClock.AdvanceBy(TimeSpan.FromSeconds 11.0)
let! third = users |> Cache.get 1
let! count = Ref.get lookups
return [ first; second; third ], count
}
manualClock: ManualClock``.ctor``: DateTimeOffset -> ManualClock``.ctor``: int * int * int * int * int * int * TimeSpan -> unitInitializes a new instance of the structure using the specified year, month, day, hour, minute, second, and offset. The year (1 through 9999). The month (1 through 12). The day (1 through the number of days in ). The hours (0 through 23). The minutes (0 through 59). The seconds (0 through 59). The time's offset from Coordinated Universal Time (UTC). does not represent whole minutes. is less than one or greater than 9999. -or- is less than one or greater than 12. -or- is less than one or greater than the number of days in . -or- is less than zero or greater than 23. -or- is less than 0 or greater than 59. -or- is less than 0 or greater than 59. -or- is less than -14 hours or greater than 14 hours. -or- The property is earlier than or later than .
System.TimeSpanRepresents a time interval.
Zero: TimeSpanRepresents the zero value. This field is read-only.
expiryLoads: Flow<ClockEnvironment,string,(string list * int)>Axial.Flow`3Axial.ClockEnvironmentAn environment containing only a clock, for timed flows with no other services.
stringAn abbreviation for the CLI type . Basic Types
listThe 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.
intAn abbreviation for the CLI type . Basic Types
flow: FlowBuilderThe universal flow { } computation expression.
lookups: Ref<int>Axial.State.RefModulemake: 'T -> Flow<'env,'none,Ref<'T>>Creates a new with the initial value. The initial value of the reference. A flow that creates and returns the reference. (Ref.make 10).RunSynchronously(())
loadUser: int -> Flow<ClockEnvironment,string,string>id: int(|>): '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
update: ('T -> 'T) -> Ref<'T> -> Flow<'env,'none,unit>Updates the value of the reference using the supplied function. The update function of type 'T -> 'T. The to update. A flow that updates the value and returns unit. Ref.update (fun x -> x + 1) myRef
(+): ^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"
users: Cache<int,string,string>Axial.CacheModuleFunctions for creating and reading a single-flight cache.
makeBoundedWithExpiry: int -> TimeSpan -> ('key -> Flow<'env,'error,'value>) -> Flow<'env,'none,Cache<'key,'error,'value>>Creates an LRU cache whose successful entries expire after of monotonic clock time.
FromSeconds: float -> TimeSpanReturns a that represents a specified number of seconds, where the specification is accurate to the nearest millisecond. A number of seconds, accurate to the nearest millisecond. An object that represents . is less than or greater than . -or- is . -or- is . is equal to .
first: stringget: 'key -> Cache<'key,'error,'value> -> Flow<'env,'error,'value>Returns the cached value for a key, or starts its lookup.
second: stringAdvanceBy: TimeSpan -> unitAdvances wall and monotonic time together and wakes due delays.
third: stringcount: intget: Ref<'T> -> Flow<'env,'none,'T>Reads the current value of the reference. The to read from. A flow that returns the current value. let readInitialValue () = flow { let! reference = Ref.make 10 return! Ref.get reference }
expiryLoads |> Flow.run (ClockEnvironment manualClock) |> shouldEqual (Exit.Success([ "user 1"; "user 1"; "user 1" ], 2))
expiryLoads: Flow<ClockEnvironment,string,(string list * int)>(|>): '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
Axial.Flowrun: '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 -> ClockEnvironmentmanualClock: ManualClockshouldEqual: 'a -> 'a -> unitAxial.Exit`2Represents the final outcome of a workflow execution. The type of the success value. The type of the domain-specific failure value.
SuccessThe workflow completed successfully.
The first two reads shared one lookup; after the clock passed the time-to-live, the third read started a second lookup. Bounded eviction removes the least recently read key once the capacity is exceeded.
Invalidation and eviction never cancel an in-flight lookup or its existing waiters: those waiters still receive the
result, but the key is no longer cached, so the next get starts a fresh lookup:
let invalidateInFlight : Flow<ClockEnvironment, string, string * string * int> =
flow {
let! lookups = Ref.make 0
let! loadStarted = Deferred.make<ClockEnvironment, string, unit> ()
let! finishLoad = Deferred.make<ClockEnvironment, string, unit> ()
let load (id: int) : Flow<ClockEnvironment, string, string> =
flow {
do! lookups |> Ref.update ((+) 1)
do! Deferred.succeed () loadStarted |> Flow.ignore
do! Deferred.await finishLoad
return $"user {id}"
}
let! users = Cache.make load
let! waiter = users |> Cache.get 1 |> Flow.fork
do! Deferred.await loadStarted
do! users |> Cache.invalidate 1
do! Deferred.succeed () finishLoad |> Flow.ignore
let! first = Fiber.join waiter
let! second = users |> Cache.get 1
let! count = Ref.get lookups
return first, second, count
}
invalidateInFlight: Flow<ClockEnvironment,string,(string * string * int)>Axial.Flow`3Axial.ClockEnvironmentAn environment containing only a clock, for timed flows with no other services.
stringAn abbreviation for the CLI type . Basic Types
intAn abbreviation for the CLI type . Basic Types
flow: FlowBuilderThe universal flow { } computation expression.
lookups: Ref<int>Axial.State.RefModulemake: 'T -> Flow<'env,'none,Ref<'T>>Creates a new with the initial value. The initial value of the reference. A flow that creates and returns the reference. (Ref.make 10).RunSynchronously(())
loadStarted: Deferred<string,unit>Axial.DeferredModuleFlow-native helpers for one-shot typed coordination.
make: unit -> Flow<'env,'error,Deferred<'error,'value>>Creates an empty deferred value.
unitThe type 'unit', which has only one value "()". This value is special and always uses the representation 'null'. Basic Types
finishLoad: Deferred<string,unit>load: int -> Flow<ClockEnvironment,string,string>id: int(|>): '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
update: ('T -> 'T) -> Ref<'T> -> Flow<'env,'none,unit>Updates the value of the reference using the supplied function. The update function of type 'T -> 'T. The to update. A flow that updates the value and returns unit. Ref.update (fun x -> x + 1) myRef
(+): ^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"
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
await: Deferred<'error,'value> -> Flow<'env,'error,'value>Waits for the deferred outcome, preserving success, typed failure, defect, or interruption.
users: Cache<int,string,string>Axial.CacheModuleFunctions for creating and reading a single-flight cache.
make: ('key -> Flow<'env,'error,'value>) -> Flow<'env,'none,Cache<'key,'error,'value>>Creates a cache that computes missing values with . Requires 'env :> IHasClock because the runtime schedules detached lookup fibers using the supplied clock. Lookups capture the environment and scope where the cache is made. Closing that scope interrupts them.
waiter: Fiber<string,string>get: 'key -> Cache<'key,'error,'value> -> Flow<'env,'error,'value>Returns the cached value for a key, or starts its lookup.
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.
invalidate: 'key -> Cache<'key,'error,'value> -> Flow<'env,'none,unit>Forgets a key without interrupting existing waiters.
first: stringAxial.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 }
second: stringcount: intget: Ref<'T> -> Flow<'env,'none,'T>Reads the current value of the reference. The to read from. A flow that returns the current value. let readInitialValue () = flow { let! reference = Ref.make 10 return! Ref.get reference }
invalidateInFlight |> Flow.run (ClockEnvironment Clock.live) |> shouldEqual (Exit.Success("user 1", "user 1", 2))
invalidateInFlight: Flow<ClockEnvironment,string,(string * string * int)>(|>): '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
Axial.Flowrun: '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.
shouldEqual: 'a -> 'a -> unitAxial.Exit`2Represents the final outcome of a workflow execution. The type of the success value. The type of the domain-specific failure value.
SuccessThe workflow completed successfully.
Rules
- Successes are kept; failures are not. A failed lookup is removed when it settles, so the next caller starts a fresh attempt instead of receiving a cached error.
- Interrupting a caller stops only its wait. Lookups run as detached fibers in the scope and environment where the cache (or memoized flow) was made, not in any caller's. A caller that is interrupted, for example because a user navigated away, never cancels a lookup that other callers are still waiting for.
- The owner's scope bounds the lookups. Closing the scope where the cache was made interrupts any lookup still running. Make the cache where it should live, typically in a service's layer.
- Values are kept until invalidated, evicted, or expired.
Cache.makeandFlow.memoizenever expire an entry, so invalidate keys when their source changes. A bounded cache also evicts the least recently read key when full, andCache.makeBoundedWithExpiryexpires successful entries after its time-to-live.
The cache torture test interrupts callers, fails lookups, and invalidates keys while hundreds of callers share one cache, and checks each rule above.

