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.JavaScriptAxial.Files
Install Axial.Files; it composes IFileSystem operations into lazy streams:
dotnet add package Axial.Files
Files.walk descend root lists descendants breadth-first; descend receives each directory path and decides whether to list its children. Files.lines path reads text in batches of 2048 lines, detects UTF-8 and UTF-16 BOMs, and returns an empty stream for files with NUL bytes in the first 8192 bytes (unless they have a UTF-16 BOM). Stopping a line stream early closes its reader. Both operations work with FileSystem.test.
Files.writeAtomic path bytes writes the full contents to a sibling temporary file in the same directory, flushes it
to disk, then renames it over the destination with MoveFile(overwrite = true). Because the temporary file lives in
the same directory as the destination, the rename stays on one filesystem and is atomic: readers see either the
previous contents — or no file, when the destination did not exist yet — or the complete new contents, never a
partially written destination. A crash before the rename leaves the destination untouched and may leave a
.name.*.tmp sibling, which cleanupStaleTemps removes. Files.writeAllTextAtomic path text uses UTF-8;
Files.writeAllTextAtomicWithEncoding path text encoding selects an encoding. Failures return FileSystemError and
attempt to remove the temporary file. Power-loss durability on some filesystems also requires fsync of the parent
directory after rename, which this API does not perform.
AppendLog
AppendLog.openLog path maxBytes opens a scoped, single-writer log. AppendLog.append record log writes an 8-byte length/CRC32 header followed by the payload and flushes before acknowledging the append. On open, recovery scans frames and truncates the current file at the first incomplete or corrupt frame. AppendLog.readAll log replays valid records from <path>.1 followed by <path> in order. When the next frame would exceed maxBytes, append rotates the current file to <path>.1, replacing the previous rotated file; each file stays within the cap. This is a bounded two-file log, not unlimited history.
Concurrent appends to one log are serialised. Recovery rejects frame lengths larger than maxBytes - 8 before allocating. If a kill occurs after rotation's rename but before creation of the new current file, opening the log keeps <path>.1 and creates an empty current file.
Show page setup
open System
open System.IO
open System.Text
open Axial
open Axial.FileSystem
open Axial.Files
let shouldEqual expected actual =
if actual <> expected then failwithf "Expected %A but got %A" expected actualopen System.Text
open Axial
open Axial.FileSystem
open Axial.Files
let fileSystemEnv = { new IHasFileSystem with member _.FileSystem = FileSystem.live }
let appendAndReplay (path: string) : Flow<IHasFileSystem, FileSystemError, byte array list> =
flow {
// Open the log, append two records, and let the scope close it.
let! log = AppendLog.openLog path 1024L |> Flow.scopeResource
do! [ "first"; "second" ]
|> List.map Encoding.ASCII.GetBytes
|> List.fold (fun step record -> step |> Flow.bind (fun () -> AppendLog.append record log)) (Flow.succeed ())
// Reopen and replay the acknowledged records in order.
let! log = AppendLog.openLog path 1024L |> Flow.scopeResource
return! AppendLog.readAll log |> FlowStream.runCollect
}
SystemTextAxialFileSystemFilesfileSystemEnv: IHasFileSystemAxial.FileSystem.IHasFileSystem_: IHasFileSystemFileSystem: unit -> IFileSystemThe file-system service supplied by this environment.
Axial.FileSystem.FileSystemModulelive: IFileSystemCreates a live file-system service backed by , , and .
appendAndReplay: string -> Flow<IHasFileSystem,FileSystemError,byte array list>path: stringstringAn abbreviation for the CLI type . Basic Types
Axial.Flow`3Axial.FileSystem.FileSystemErrorDescribes a meaningful file-system failure.
byteAn abbreviation for the CLI type . Basic Types
arraySingle dimensional, zero-based arrays, written int array, string array etc. Use the values in the module to manipulate values of this type, or the notation arr.[x] to get/set array values. 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.
flow: FlowBuilderThe universal flow { } computation expression.
log: AppendLogAxial.Files.AppendLogModuleopenLog: string -> int64 -> Resource<'env,FileSystemError,AppendLog>Opens a log, preserving the rotated generation when the current file is missing, and truncates a corrupt tail. Frame lengths are bounded by maxBytes.
(|>): '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.FlowscopeResource: Resource<'env,'error,'value> -> Flow<'env,'error,'value>Acquires a described resource and registers its release with the current runtime scope. The resource is a scoped resource: it stays open until the current scope closes. Inside a forked fiber that is the fiber's own scope, which closes when the fiber settles; inside a branch of zipPar, race, sequencePar, or traversePar, the resource moves to the caller's scope if the caller receives the branch's value. Use forkResource to acquire it in the background and keep it. A flow that finishes acquiring after its scope has closed, such as a race loser, releases the resource at once and is interrupted. The acquisition and release description. A flow that succeeds with the acquired value.
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.
map: ('T -> 'U) -> 'T list -> 'U listBuilds a new collection whose elements are the results of applying the given function to each of the elements of the collection. The function to transform elements from the input list. The input list. The list of transformed elements. let inputs = [ "a"; "bbb"; "cc" ] inputs |> List.map (fun x -> x.Length) Evaluates to [ 1; 3; 2 ] This is an O(n) operation, where n is the length of the list.
System.Text.EncodingRepresents a character encoding.
ASCII: EncodingGets an encoding for the ASCII (7-bit) character set. An encoding for the ASCII (7-bit) character set.
GetBytes: string -> byte arrayWhen overridden in a derived class, encodes all the characters in the specified string into a sequence of bytes. The string containing the characters to encode. A byte array containing the results of encoding the specified set of characters. is . A fallback occurred (see Character Encoding in .NET for complete explanation) -and- is set to .
fold: ('State -> 'T -> 'State) -> 'State -> 'T list -> 'StateApplies a function to each element of the collection, threading an accumulator argument through the computation. Take the second argument, and apply the function to it and the first element of the list. Then feed this result into the function along with the second element and so on. Return the final result. If the input function is f and the elements are i0...iN then computes f (... (f s i0) i1 ...) iN. The function to update the state given the input elements. The initial state. The input list. The final state value. Making the sum of squares for the first 5 natural numbers (0, [1..5]) ||> List.fold (fun s v -> s + v * v) // evaluates 55 Shopping for fruits hungry, you tend to take more of each as the hunger grows type Fruit = Apple | Pear | Orange type BagItem = { fruit: Fruit; quantity: int } let takeMore (previous: BagItem list) fruit = let toTakeThisTime = match previous with | bagItem :: otherBagItems -> bagItem.quantity + 1 | [] -> 1 { fruit = fruit; quantity = toTakeThisTime } :: previous let inputs = [ Apple; Pear; Orange ] ([], inputs) ||> List.fold takeMore Evaluates to [{ fruit = Orange; quantity = 3 } { fruit = Pear; quantity = 2 } { fruit = Apple; quantity = 1 }] This is an O(n) operation, where n is the length of the list.
step: Flow<IHasFileSystem,FileSystemError,unit>record: byte arraybind: ('value -> Flow<'env,'error,'next>) -> Flow<'env,'error,'value> -> Flow<'env,'error,'next>Sequences a dependent flow after a successful value. This is the flatmap operation for . The continuation only runs when the source flow succeeds, and it receives the successful value. Use bind when the next effect depends on the previous result; use map when the next step is pure. On .NET, a recursive return! through bind does not keep a task per call alive. Work still pending in the source, such as a use or try/finally, lives until the source completes. A function that takes the successful value and returns a new flow. The source flow to sequence. A representing the combined workflow. let flow = Flow.succeed 1 |> Flow.bind (fun x -> Flow.succeed (x + 1))
append: byte array -> AppendLog -> Flow<'env,FileSystemError,unit>Serialises appends and flushes record contents to storage before acknowledgement. Rotation retains two generations; directory entries are not fsynced for power-loss durability.
succeed: 'value -> Flow<'env,'error,'value>Same as ok. The value to wrap in a successful flow. A flow that always succeeds with the provided value. let result = Flow.succeed 42 |> Flow.run () // result = Success 42
readAll: AppendLog -> FlowStream<'env,FileSystemError,byte array>Replays valid frames from the current file.
Axial.FlowStreamModulerunCollect: FlowStream<'a,'b,'c> -> Flow<'a,'b,'c list>Collects all emitted values into a list. stream |> FlowStream.runCollect
let path = Path.Combine(Path.GetTempPath(), "axial-docs-append-log")
for candidate in [ path; path + ".1" ] do
if File.Exists candidate then File.Delete candidate
try
appendAndReplay path
|> Flow.run fileSystemEnv
|> Exit.map (List.map (fun bytes -> System.Text.Encoding.ASCII.GetString bytes))
|> shouldEqual (Exit.Success [ "first"; "second" ])
finally
for candidate in [ path; path + ".1" ] do
if File.Exists candidate then File.Delete candidate
path: stringSystem.IO.PathPerforms operations on instances that contain file or directory path information. These operations are performed in a cross-platform manner.
Combine: string * string -> stringCombines two strings into a path. The first path to combine. The second path to combine. The combined paths. If one of the specified paths is a zero-length string, this method returns the other path. If contains an absolute path, this method returns . or contains one or more of the invalid characters defined in . or is .
GetTempPath: unit -> stringReturns the path of the current user's temporary folder. The path to the temporary folder, ending with a backslash. The caller does not have the required permissions.
candidate: string(+): ^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"
System.IO.FileProvides static methods for the creation, copying, deletion, moving, and opening of a single file, and aids in the creation of objects.
Exists: string -> boolDetermines whether the specified file exists. The file to check. if the caller has the required permissions and contains the name of an existing file; otherwise, . This method also returns if is , an invalid path, or a zero-length string. If the caller does not have sufficient permissions to read the specified file, no exception is thrown and the method returns regardless of the existence of .
Delete: string -> unitDeletes the specified file. The name of the file to be deleted. Wildcard characters are not supported. is a zero-length string, contains only white space, or contains one or more invalid characters as defined by . is . The specified path is invalid (for example, it is on an unmapped drive). The specified file is in use. -or- There is an open handle on the file, and the operating system is Windows XP or earlier. This open handle can result from enumerating directories and files. For more information, see How to: Enumerate Directories and Files. is in an invalid format. The specified path, file name, or both exceed the system-defined maximum length. The caller does not have the required permission. -or- The file is an executable file that is in use. -or- is a directory. -or- specified a read-only file.
appendAndReplay: string -> Flow<IHasFileSystem,FileSystemError,byte array 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.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
fileSystemEnv: IHasFileSystemAxial.Exitmap: ('v -> 'w) -> Exit<'v,'e> -> Exit<'w,'e>Transforms the success value of an exit outcome using the provided function. The function to transform the success value. The exit outcome to transform. A new exit outcome with the transformed success value.
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.
map: ('T -> 'U) -> 'T list -> 'U listBuilds a new collection whose elements are the results of applying the given function to each of the elements of the collection. The function to transform elements from the input list. The input list. The list of transformed elements. let inputs = [ "a"; "bbb"; "cc" ] inputs |> List.map (fun x -> x.Length) Evaluates to [ 1; 3; 2 ] This is an O(n) operation, where n is the length of the list.
bytes: byte arraySystemASCII: EncodingGets an encoding for the ASCII (7-bit) character set. An encoding for the ASCII (7-bit) character set.
TextSystem.Text.EncodingRepresents a character encoding.
GetString: byte array -> stringWhen overridden in a derived class, decodes all the bytes in the specified byte array into a string. The byte array containing the sequence of bytes to decode. A string that contains the results of decoding the specified sequence of bytes. The byte array contains invalid Unicode code points. is . A fallback occurred (see Character Encoding in .NET for complete explanation) -and- is set to .
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.
AppendLog.openLog returns a Resource, so the scope acquires and later closes the log; AppendLog.readAll
replays the records written by the earlier append.
Flush(true) makes acknowledged record contents durable. Process kills lose no acknowledged records; an unacknowledged tail can be discarded on recovery. A newly created or renamed file's directory entry is durable across power loss only when the directory is fsynced; this package does not fsync directories.
Files.watch recursive path streams FileSystemChange events for a directory or one file. Set recursive to true to include nested directories. The watcher is disposed when the stream ends, including after FlowStream.take. Watcher errors are typed FileSystemError failures. The buffer holds 1024 events; overflow fails the stream rather than silently dropping changes. Rescan the watched path after an error or overflow because filesystem notifications are not a durable log.

