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.JavaScriptQueue
Queue<'a> passes values from producer fibers to a consumer. It is FIFO, suspends instead of blocking a thread, and
keeps Axial's interruption guarantees: interrupting a suspended take never loses an element, and interrupting a
suspended Queue.offer never enqueues its value.
Use a queue when there is exactly one logical consumer. Each value goes to exactly one taker, so several fibers taking from one queue share its values between them. When zero or many consumers must each see every value, use a hub.
A queue has two sides. The Queue module creates queues and offers to them. The Dequeue module takes from them,
inspects them, and shuts them down; its functions accept a Queue, and a hub subscription is a Dequeue too, so a
consumer is written once for both.
Choosing a strategy
A QueueStrategy decides what Queue.offer does when the queue is full.
| Strategy | Shorthand | Full queue | offer returns |
|---|---|---|---|
QueueStrategy.BackPressure n |
Queue.bounded n |
suspends the producer until a value is taken | true |
QueueStrategy.Dropping n |
Queue.dropping n |
discards the new value | false |
QueueStrategy.Sliding n |
Queue.sliding n |
evicts the oldest value | true |
QueueStrategy.Unbounded |
Queue.unbounded () |
never full; memory grows with the backlog | true |
BackPressure is the lossless choice: a slow consumer slows its producers down. Sliding suits a latest-value feed
such as a display, where old readings are worth less than new ones.
Queue.tryOffer is a synchronous, immediate attempt for a callback that cannot wait. It returns
QueueTryOfferResult.Accepted, Full, Dropped, Evicted, or Shutdown. Full means a back-pressure queue did not
store the value. Dropped means a dropping queue discarded it. Evicted means a sliding queue stored the new value
but removed its oldest value. Only Accepted stores the value without loss. A controller ingress adapter can treat
Full or Shutdown as an explicit fault or retry condition; Queue.tryOffer does not choose that policy.
> (flow {
- let! (display: Queue<int>) = Queue.make (QueueStrategy.Sliding 3)
- do! display |> Queue.offerAll [ 1..6 ] |> Flow.ignore
- return! Dequeue.takeAll display
- } : Flow<unit, Never, int list>)
- |> Flow.run ();;val it: Exit<int list,Never> = Success [4; 5; 6]flow: FlowBuilderThe universal flow { } computation expression.
display: Queue<int>Axial.Queue`1An asynchronous FIFO queue for handing values between fibers, with an overflow strategy chosen at creation. Create one with Queue.make or a shorthand such as Queue.bounded, offer with Queue.offer, and take with the Dequeue functions. A queue is meant for one logical consumer; use Hub when zero or many consumers each need every value. The type of the queued values.
intAn abbreviation for the CLI type . Basic Types
Axial.QueueModuleCreates queues and offers values to them. Take values, inspect, and shut a queue down with the Dequeue functions.
make: QueueStrategy -> Flow<'env,'error,Queue<'a>>Creates a queue with the given overflow strategy. A non-positive capacity fails the returned flow with a defect. Queue.make (QueueStrategy.Sliding 1)
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.
SlidingA full buffer evicts its oldest value to make room for the new one.
(|>): '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
offerAll: 'a seq -> Queue<'a> -> Flow<'env,'error,bool>Adds values in order according to the queue's strategy. true when every value was accepted. A dropping queue accepts values while it has room and returns false if it discarded any. A bounded queue suspends as needed; values from other producers may be interleaved between the batch's values, but the batch's own order is kept. If the flow is interrupted part way, the values already accepted stay in the queue. flow { let! (samples: Queue<float>) = Queue.unbounded () do! samples |> Queue.offerAll [ 1.0; 2.0; 3.0 ] |> Flow.ignore return! samples |> Dequeue.takeUpTo 500 }
(..): ^T -> ^T -> ^T seqThe standard overloaded range operator, e.g. [n..m] for lists, seq {n..m} for sequences The start value of the range. The end value of the range. The sequence spanning the range. [1..4] // Evaluates to [1; 2; 3; 4] [1.5..4.4] // Evaluates to [1.5; 2.5; 3.5] ['a'..'d'] // Evaluates to ['a'; 'b'; 'c'; 'd'] [|1..4|] // Evaluates to an array [|1; 2; 3; 4|] { 1..4 } // Evaluates to a sequence [1; 2; 3; 4])
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
Axial.DequeueModuleTakes values from a , inspects it, and shuts it down. Every function here also accepts a .
takeAll: Dequeue<'a> -> Flow<'env,'error,'a list>Removes every available value in FIFO order, without suspending.
Axial.Flow`3unitThe type 'unit', which has only one value "()". This value is special and always uses the representation 'null'. Basic Types
Axial.NeverRepresents an error channel that cannot occur.
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.
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
Producing and consuming
Dequeue.take suspends while the queue is empty. Dequeue.poll, Dequeue.takeUpTo, and Dequeue.takeAll never
suspend.
FlowStream.fromDequeue turns a queue into a stream for its consumer. The stream suspends while the queue is empty and
ends normally once the queue is shut down and drained.
> open Axial.PlatformService;;
> (flow {
- let! (readings: Queue<int>) = Queue.bounded 2
- let! consumer =
- readings
- |> FlowStream.fromDequeue
- |> FlowStream.chunkBySize 2
- |> FlowStream.runCollect
- |> Flow.fork
- do! readings |> Queue.offerAll [ 1..5 ] |> Flow.ignore
- do! Dequeue.shutdown readings
- return! Fiber.join consumer
- } : Flow<ClockEnvironment, Never, int list list>)
- |> Flow.run (ClockEnvironment Clock.live);;val it: Exit<int list list,Never> = Success [[1; 2]; [3; 4]; [5]]AxialPlatformServiceflow: FlowBuilderThe universal flow { } computation expression.
readings: Queue<int>Axial.Queue`1An asynchronous FIFO queue for handing values between fibers, with an overflow strategy chosen at creation. Create one with Queue.make or a shorthand such as Queue.bounded, offer with Queue.offer, and take with the Dequeue functions. A queue is meant for one logical consumer; use Hub when zero or many consumers each need every value. The type of the queued values.
intAn abbreviation for the CLI type . Basic Types
Axial.QueueModuleCreates queues and offers values to them. Take values, inspect, and shut a queue down with the Dequeue functions.
bounded: int -> Flow<'env,'error,Queue<'a>>Creates a lossless queue whose offer suspends while it holds values. Shorthand for Queue.make (QueueStrategy.BackPressure capacity). Queue.bounded 64
consumer: Fiber<Never,int list list>(|>): '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.FlowStreamModulefromDequeue: Dequeue<'value> -> FlowStream<'env,'error,'value>Creates a stream that takes values from a queue or hub subscription until it is shut down and drained. Shutdown is the normal end of the stream, not a failure: after Dequeue.shutdown or Hub.shutdown the stream emits the remaining backlog and then completes. Each pull suspends while the queue is empty. Interrupting a pull leaves any value it would have received in the queue. flow { let! (jobs: Queue<string>) = Queue.bounded 8 do! jobs |> Queue.offerAll [ "a"; "b" ] |> Flow.ignore do! Dequeue.shutdown jobs return! jobs |> FlowStream.fromDequeue |> FlowStream.runCollect }
chunkBySize: int -> FlowStream<'a,'b,'c> -> FlowStream<'a,'b,'c list>Groups consecutive values into non-empty lists of at most elements. The operator pulls and retains at most upstream values for each emitted list.
runCollect: FlowStream<'a,'b,'c> -> Flow<'a,'b,'c list>Collects all emitted values into a list. stream |> FlowStream.runCollect
Axial.Flowfork: 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.
offerAll: 'a seq -> Queue<'a> -> Flow<'env,'error,bool>Adds values in order according to the queue's strategy. true when every value was accepted. A dropping queue accepts values while it has room and returns false if it discarded any. A bounded queue suspends as needed; values from other producers may be interleaved between the batch's values, but the batch's own order is kept. If the flow is interrupted part way, the values already accepted stay in the queue. flow { let! (samples: Queue<float>) = Queue.unbounded () do! samples |> Queue.offerAll [ 1.0; 2.0; 3.0 ] |> Flow.ignore return! samples |> Dequeue.takeUpTo 500 }
(..): ^T -> ^T -> ^T seqThe standard overloaded range operator, e.g. [n..m] for lists, seq {n..m} for sequences The start value of the range. The end value of the range. The sequence spanning the range. [1..4] // Evaluates to [1; 2; 3; 4] [1.5..4.4] // Evaluates to [1.5; 2.5; 3.5] ['a'..'d'] // Evaluates to ['a'; 'b'; 'c'; 'd'] [|1..4|] // Evaluates to an array [|1; 2; 3; 4|] { 1..4 } // Evaluates to a sequence [1; 2; 3; 4])
ignore: 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
Axial.DequeueModuleTakes values from a , inspects it, and shuts it down. Every function here also accepts a .
shutdown: Dequeue<'a> -> Flow<'env,'error,unit>Shuts the queue down. Calling it again has no effect. Either side may shut a queue down. On a hub subscription this unsubscribes: the hub stops delivering to it, and a publisher waiting on it moves on.
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`3Axial.ClockEnvironmentAn environment containing only a clock, for timed flows with no other services.
Axial.NeverRepresents an error channel that cannot occur.
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.
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 producer offered five values into a queue that holds two, so it suspended until the consumer made room.
Feeding a queue from streams
FlowStream.runIntoQueue queue offers every value of a stream, waiting while a bounded queue is full. It does not shut
the queue down, so several producers can feed one queue. FlowStream.mergePar runs several streams concurrently and
emits their values as they arrive, which fits device readers feeding one control loop.
> open Axial.PlatformService;;
> (flow {
- let! (inputs: Queue<int>) = Queue.bounded 4
- let! loop = inputs |> FlowStream.fromDequeue |> FlowStream.runCollect |> Flow.fork
- do!
- [ FlowStream.fromSeq [ 1; 2; 3 ]; FlowStream.fromSeq [ 10; 20 ] ]
- |> FlowStream.mergePar
- |> FlowStream.runIntoQueue inputs
- do! Dequeue.shutdown inputs
- let! received = Fiber.join loop
- return List.sort received
- } : Flow<ClockEnvironment, Never, int list>)
- |> Flow.run (ClockEnvironment Clock.live);;val it: Exit<int list,Never> = Success [1; 2; 3; 10; 20]AxialPlatformServiceflow: FlowBuilderThe universal flow { } computation expression.
inputs: Queue<int>Axial.Queue`1An asynchronous FIFO queue for handing values between fibers, with an overflow strategy chosen at creation. Create one with Queue.make or a shorthand such as Queue.bounded, offer with Queue.offer, and take with the Dequeue functions. A queue is meant for one logical consumer; use Hub when zero or many consumers each need every value. The type of the queued values.
intAn abbreviation for the CLI type . Basic Types
Axial.QueueModuleCreates queues and offers values to them. Take values, inspect, and shut a queue down with the Dequeue functions.
bounded: int -> Flow<'env,'error,Queue<'a>>Creates a lossless queue whose offer suspends while it holds values. Shorthand for Queue.make (QueueStrategy.BackPressure capacity). Queue.bounded 64
loop: Fiber<Never,int list>(|>): '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.FlowStreamModulefromDequeue: Dequeue<'value> -> FlowStream<'env,'error,'value>Creates a stream that takes values from a queue or hub subscription until it is shut down and drained. Shutdown is the normal end of the stream, not a failure: after Dequeue.shutdown or Hub.shutdown the stream emits the remaining backlog and then completes. Each pull suspends while the queue is empty. Interrupting a pull leaves any value it would have received in the queue. flow { let! (jobs: Queue<string>) = Queue.bounded 8 do! jobs |> Queue.offerAll [ "a"; "b" ] |> Flow.ignore do! Dequeue.shutdown jobs return! jobs |> FlowStream.fromDequeue |> FlowStream.runCollect }
runCollect: FlowStream<'a,'b,'c> -> Flow<'a,'b,'c list>Collects all emitted values into a list. stream |> FlowStream.runCollect
Axial.Flowfork: 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.
fromSeq: 'value seq -> FlowStream<'env,'error,'value>Creates a stream from a synchronous sequence of values. The sequence of values to be emitted by the stream. A that yields each value from the sequence. FlowStream.fromSeq [1..10] |> FlowStream.runCollect |> Flow.run ()
mergePar: FlowStream<'env,'error,'value> list -> FlowStream<'env,'error,'value>Runs several streams concurrently and emits their values as they arrive. Each stream keeps its own order; values from different streams interleave in arrival order. The merged stream ends when every stream has ended. The first failure is delivered after the values already buffered, and the other streams are interrupted. When the consumer stops early, every stream is interrupted. [ readerA; readerB; readerC ] |> FlowStream.mergePar |> FlowStream.runIntoQueue controlInputs
runIntoQueue: Queue<'value> -> FlowStream<'env,'error,'value> -> Flow<'env,'error,unit>Runs a stream and offers every value to a queue, waiting while a bounded queue is full. The queue is not shut down when the stream ends, so several streams can feed one queue; shut it down with Dequeue.shutdown once every producer has finished. If the queue is shut down first, the flow is interrupted. deviceReadings |> FlowStream.runIntoQueue controlInputs
Axial.DequeueModuleTakes values from a , inspects it, and shuts it down. Every function here also accepts a .
shutdown: Dequeue<'a> -> Flow<'env,'error,unit>Shuts the queue down. Calling it again has no effect. Either side may shut a queue down. On a hub subscription this unsubscribes: the hub stops delivering to it, and a publisher waiting on it moves on.
received: int listAxial.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 }
Microsoft.FSharp.Collections.ListModuleContains operations for working with values of type . Operations for collections such as lists, arrays, sets, maps and sequences. See also F# Collection Types in the F# Language Guide.
sort: 'T list -> 'T listSorts the given list using . This is a stable sort, i.e. the original order of equal elements is preserved. This is an O(n log n) operation, where n is the length of the list. The input list. The sorted list. let input = [8; 4; 3; 1; 6; 1] List.sort input Evaluates to [1; 1; 3; 4; 6; 8].
Axial.Flow`3Axial.ClockEnvironmentAn environment containing only a clock, for timed flows with no other services.
Axial.NeverRepresents an error channel that cannot occur.
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.
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.
Values from different streams interleave in arrival order, and each stream keeps its own order. The merged stream ends when every stream has ended; the first failure ends it after the values already buffered and interrupts the other streams.
FlowStream.buffer strategy runs a stream ahead of its consumer in its own fiber, buffered by a QueueStrategy. With
QueueStrategy.Sliding 1, a slow consumer only ever sees the latest value. The end of the stream and its failure travel
outside the buffer, so a lossy strategy never drops them.
Taking in batches
A consumer that writes to a database wants to wait for work, then take everything that has piled up, with one write
per batch rather than per value. Dequeue.takeBetween min max suspends until at least min values are available,
then takes up to max.
> (flow {
- let! (samples: Queue<int>) = Queue.unbounded ()
- do! samples |> Queue.offerAll [ 1..7 ] |> Flow.ignore
- let! first = samples |> Dequeue.takeBetween 1 5
- let! second = samples |> Dequeue.takeBetween 1 5
- return [ first; second ]
- } : Flow<unit, Never, int list list>)
- |> Flow.run ();;val it: Exit<int list list,Never> = Success [[1; 2; 3; 4; 5]; [6; 7]]flow: FlowBuilderThe universal flow { } computation expression.
samples: Queue<int>Axial.Queue`1An asynchronous FIFO queue for handing values between fibers, with an overflow strategy chosen at creation. Create one with Queue.make or a shorthand such as Queue.bounded, offer with Queue.offer, and take with the Dequeue functions. A queue is meant for one logical consumer; use Hub when zero or many consumers each need every value. The type of the queued values.
intAn abbreviation for the CLI type . Basic Types
Axial.QueueModuleCreates queues and offers values to them. Take values, inspect, and shut a queue down with the Dequeue functions.
unbounded: unit -> Flow<'env,'error,Queue<'a>>Creates a queue that never rejects or suspends an offer. Memory grows with the backlog; prefer a bounded queue unless producers are otherwise limited.
(|>): '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
offerAll: 'a seq -> Queue<'a> -> Flow<'env,'error,bool>Adds values in order according to the queue's strategy. true when every value was accepted. A dropping queue accepts values while it has room and returns false if it discarded any. A bounded queue suspends as needed; values from other producers may be interleaved between the batch's values, but the batch's own order is kept. If the flow is interrupted part way, the values already accepted stay in the queue. flow { let! (samples: Queue<float>) = Queue.unbounded () do! samples |> Queue.offerAll [ 1.0; 2.0; 3.0 ] |> Flow.ignore return! samples |> Dequeue.takeUpTo 500 }
(..): ^T -> ^T -> ^T seqThe standard overloaded range operator, e.g. [n..m] for lists, seq {n..m} for sequences The start value of the range. The end value of the range. The sequence spanning the range. [1..4] // Evaluates to [1; 2; 3; 4] [1.5..4.4] // Evaluates to [1.5; 2.5; 3.5] ['a'..'d'] // Evaluates to ['a'; 'b'; 'c'; 'd'] [|1..4|] // Evaluates to an array [|1; 2; 3; 4|] { 1..4 } // Evaluates to a sequence [1; 2; 3; 4])
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
first: int listAxial.DequeueModuleTakes values from a , inspects it, and shuts it down. Every function here also accepts a .
takeBetween: int -> int -> Dequeue<'a> -> Flow<'env,'error,'a list>Removes between and values in FIFO order, suspending until at least are available. This is the batching consumer's take: it waits for work, then takes everything that has accumulated up to a batch limit. It takes nothing until min values are available and then takes the whole batch at once, so an interruption while it waits leaves every value in the queue. Takers are served in the order they started waiting, a single take and a takeBetween alike. After shutdown it returns the remaining values even if there are fewer than min, and is interrupted once nothing remains. min must be at least 1, no greater than max, and no greater than a bounded queue's capacity; otherwise the flow fails with a defect. // Wait for at least one sample, then write up to 500 in one batch. samples |> Dequeue.takeBetween 1 500
second: int listAxial.Flow`3unitThe type 'unit', which has only one value "()". This value is special and always uses the representation 'null'. Basic Types
Axial.NeverRepresents an error channel that cannot occur.
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.
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
takeBetween takes nothing until its minimum is available and then takes the whole batch at once, so interrupting it
while it waits leaves every value in the queue. After shutdown it returns whatever remains, even if that is fewer than
min.
Shutdown
Dequeue.shutdown ends a queue without discarding its backlog:
- fibers suspended in a take or an offer are interrupted;
- later offers are interrupted;
- later takes return the remaining values first, then are interrupted;
poll,takeUpTo, andtakeAllreturn nothing once the queue is empty;- calling
shutdownagain has no effect.
Draining after shutdown is what lets a consumer finish its work when an application stops. Fork the consumer with
Flow.forkGraceful (Dequeue.shutdown queue) grace, and closing its scope shuts the queue down and waits for the consumer
to drain it; see stopping a consumer gracefully. Dequeue.isShutdown
reports the state, and Dequeue.awaitShutdown suspends until it happens.
Monitoring a queue
Dequeue.stats reads a queue's size, capacity, waiting takers and offerers, and how many values it has accepted,
dropped, and evicted since it was created, all in one consistent snapshot. The counters only grow, so the difference
between two snapshots is a rate. Dequeue.capacity returns the capacity alone, or None for an unbounded queue.
> (flow {
- let! (display: Queue<int>) = Queue.sliding 2
- do! display |> Queue.offerAll [ 1..5 ] |> Flow.ignore
- let! stats = Dequeue.stats display
- return $"size {stats.Size}, accepted {stats.Accepted}, evicted {stats.Evicted}"
- } : Flow<unit, Never, string>)
- |> Flow.run ();;val it: Exit<string,Never> = Success "size 2, accepted 5, evicted 3"flow: FlowBuilderThe universal flow { } computation expression.
display: Queue<int>Axial.Queue`1An asynchronous FIFO queue for handing values between fibers, with an overflow strategy chosen at creation. Create one with Queue.make or a shorthand such as Queue.bounded, offer with Queue.offer, and take with the Dequeue functions. A queue is meant for one logical consumer; use Hub when zero or many consumers each need every value. The type of the queued values.
intAn abbreviation for the CLI type . Basic Types
Axial.QueueModuleCreates queues and offers values to them. Take values, inspect, and shut a queue down with the Dequeue functions.
sliding: int -> Flow<'env,'error,Queue<'a>>Creates a queue whose offer evicts the oldest value when full and returns true. Shorthand for Queue.make (QueueStrategy.Sliding capacity).
(|>): '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
offerAll: 'a seq -> Queue<'a> -> Flow<'env,'error,bool>Adds values in order according to the queue's strategy. true when every value was accepted. A dropping queue accepts values while it has room and returns false if it discarded any. A bounded queue suspends as needed; values from other producers may be interleaved between the batch's values, but the batch's own order is kept. If the flow is interrupted part way, the values already accepted stay in the queue. flow { let! (samples: Queue<float>) = Queue.unbounded () do! samples |> Queue.offerAll [ 1.0; 2.0; 3.0 ] |> Flow.ignore return! samples |> Dequeue.takeUpTo 500 }
(..): ^T -> ^T -> ^T seqThe standard overloaded range operator, e.g. [n..m] for lists, seq {n..m} for sequences The start value of the range. The end value of the range. The sequence spanning the range. [1..4] // Evaluates to [1; 2; 3; 4] [1.5..4.4] // Evaluates to [1.5; 2.5; 3.5] ['a'..'d'] // Evaluates to ['a'; 'b'; 'c'; 'd'] [|1..4|] // Evaluates to an array [|1; 2; 3; 4|] { 1..4 } // Evaluates to a sequence [1; 2; 3; 4])
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
stats: QueueStatsAxial.DequeueModuleTakes values from a , inspects it, and shuts it down. Every function here also accepts a .
stats: Dequeue<'a> -> Flow<'env,'error,QueueStats>Reads the queue's backlog and counters in one consistent snapshot. flow { let! stats = Dequeue.stats historian if stats.Capacity |> Option.exists (fun limit -> stats.Size * 10 >= limit * 8) then do! raiseAlarm "historian buffer 80% full" }
Axial.Flow`3unitThe type 'unit', which has only one value "()". This value is special and always uses the representation 'null'. Basic Types
Axial.NeverRepresents an error channel that cannot occur.
stringAn abbreviation for the CLI type . Basic Types
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
To export these figures as metrics, register the queue with QueueMetrics.observe; see
watching queues and hub subscriptions.
Tying a queue to a scope
Queue.makeScoped creates a queue that is shut down when the current scope closes. Use it inside Flow.scoped, a forked
fiber, or an application root, so the queue's lifetime follows the work that owns it.
> (flow {
- let! (jobs: Queue<string>) =
- flow {
- let! (jobs: Queue<string>) = Queue.makeScoped (QueueStrategy.BackPressure 8)
- do! jobs |> Queue.offerAll [ "a"; "b" ] |> Flow.ignore
- return jobs
- }
- |> Flow.scoped
- return! jobs |> FlowStream.fromDequeue |> FlowStream.runCollect
- } : Flow<unit, Never, string list>)
- |> Flow.run ();;val it: Exit<string list,Never> = Success ["a"; "b"]flow: FlowBuilderThe universal flow { } computation expression.
jobs: Queue<string>Axial.Queue`1An asynchronous FIFO queue for handing values between fibers, with an overflow strategy chosen at creation. Create one with Queue.make or a shorthand such as Queue.bounded, offer with Queue.offer, and take with the Dequeue functions. A queue is meant for one logical consumer; use Hub when zero or many consumers each need every value. The type of the queued values.
stringAn abbreviation for the CLI type . Basic Types
Axial.QueueModuleCreates queues and offers values to them. Take values, inspect, and shut a queue down with the Dequeue functions.
makeScoped: QueueStrategy -> Flow<'env,'error,Queue<'a>>Creates a queue that is shut down when the current scope closes. Use it inside Flow.scoped, a forked fiber, or an application root. Closing the scope interrupts fibers suspended on the queue and ends any FlowStream.fromDequeue consumer after it drains the backlog. flow { let! (jobs: Queue<string>) = Queue.makeScoped (QueueStrategy.BackPressure 64) do! jobs |> Queue.offer "first job" |> Flow.ignore } |> Flow.scoped
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.
BackPressureLossless. A full buffer suspends the producer until a value is taken.
(|>): '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
offerAll: 'a seq -> Queue<'a> -> Flow<'env,'error,bool>Adds values in order according to the queue's strategy. true when every value was accepted. A dropping queue accepts values while it has room and returns false if it discarded any. A bounded queue suspends as needed; values from other producers may be interleaved between the batch's values, but the batch's own order is kept. If the flow is interrupted part way, the values already accepted stay in the queue. flow { let! (samples: Queue<float>) = Queue.unbounded () do! samples |> Queue.offerAll [ 1.0; 2.0; 3.0 ] |> Flow.ignore return! samples |> Dequeue.takeUpTo 500 }
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
scoped: Flow<'env,'error,'value> -> Flow<'env,'error,'value>Runs a flow in a child scope and closes that scope before returning. Resources and fibers acquired inside the flow are released after success, typed failure, defect, or interruption without waiting for the surrounding application scope to close.
Axial.FlowStreamModulefromDequeue: Dequeue<'value> -> FlowStream<'env,'error,'value>Creates a stream that takes values from a queue or hub subscription until it is shut down and drained. Shutdown is the normal end of the stream, not a failure: after Dequeue.shutdown or Hub.shutdown the stream emits the remaining backlog and then completes. Each pull suspends while the queue is empty. Interrupting a pull leaves any value it would have received in the queue. flow { let! (jobs: Queue<string>) = Queue.bounded 8 do! jobs |> Queue.offerAll [ "a"; "b" ] |> Flow.ignore do! Dequeue.shutdown jobs return! jobs |> FlowStream.fromDequeue |> FlowStream.runCollect }
runCollect: FlowStream<'a,'b,'c> -> Flow<'a,'b,'c list>Collects all emitted values into a list. stream |> FlowStream.runCollect
Axial.Flow`3unitThe type 'unit', which has only one value "()". This value is special and always uses the representation 'null'. Basic Types
Axial.NeverRepresents an error channel that cannot occur.
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.
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
Closing the scope shut the queue down, so the stream ended once it had delivered the values that were already queued. Without the shutdown, the consumer would have waited for more values forever.
Fairness and interruption
Suspended takers are served in the order they began waiting, and so are suspended offerers of a bounded queue.
Values leave in FIFO order even when takers are interrupted, which the queue torture test checks under
constant interruption. A taker interrupted at the same moment a value is handed to it gives the value back to the front
of the queue. Takers are served one at a time, the next only after the previous one has resumed with its value or given
it back, so nothing later can have been taken ahead of a value that comes back. An offer interrupted before its value
was accepted is withdrawn. An offer accepted in the same moment as its interruption has already taken effect and reports
true.

