FileSystem (Polyglot)

#r "nuget: fsharp.control.asyncseq, 3.2.1"
#r "nuget: system.reactive, 6.0.1-preview.1"
#r "nuget: system.reactive.linq, 6.0.1-preview.1"
#r "nuget: argu, 6.2.4"
#!import ../../lib/fsharp/Notebooks.livemd
#!import ../../lib/fsharp/Testing.livemd
#!import ../../lib/fsharp/Common.fs
#!import ../../lib/fsharp/CommonFSharp.fs
#!import ../../lib/fsharp/Async.fs
#!import ../../lib/fsharp/AsyncSeq.fs
#!import ../../lib/fsharp/Runtime.fs
#if !INTERACTIVE
open Lib
#endif
open Common
open SpiralFileSystem.Operators

watchDirectory

[<RequireQualifiedAccess>]
type FileSystemChangeType =
    | Failure
    | Changed
    | Created
    | Deleted
    | Renamed

[<RequireQualifiedAccess>]
type FileSystemChange =
    | Failure of exn: exn
    | Changed of path: string * content: string option
    | Created of path: string * content: string option
    | Deleted of path: string
    | Renamed of oldPath: string * (string * string option)


let inline watchDirectoryWithFilter filter shouldReadContent path =
    let fullPath = path |> System.IO.Path.GetFullPath
    let _locals () = $"filter: {filter} / {_locals ()}"

    let watcher =
        new System.IO.FileSystemWatcher (
            Path = fullPath,
            NotifyFilter = filter,
            EnableRaisingEvents = true,
            IncludeSubdirectories = true
        )

    let inline getEventPath (path : string) =
        path |> SpiralSm.trim |> SpiralSm.replace fullPath "" |> SpiralSm.trim_start [| '/'; '\\' |]

    let inline ticks () =
        System.DateTime.UtcNow.Ticks

    let changedStream =
        AsyncSeq.subscribeEvent
            watcher.Changed
            (fun event ->
                ticks (),
                [ FileSystemChange.Changed (getEventPath event.FullPath, None) ]
            )

    let deletedStream =
        AsyncSeq.subscribeEvent
            watcher.Deleted
            (fun event ->
                ticks (),
                [ FileSystemChange.Deleted (getEventPath event.FullPath) ]
            )

    let createdStream =
        AsyncSeq.subscribeEvent
            watcher.Created
            (fun event ->
                let path = getEventPath event.FullPath
                ticks (), [
                    FileSystemChange.Created (path, None)
                    if SpiralPlatform.is_windows () then
                        FileSystemChange.Changed (path, None)
                ])

    let renamedStream =
        AsyncSeq.subscribeEvent
            watcher.Renamed
            (fun event ->
                ticks (), [
                    FileSystemChange.Renamed (
                        getEventPath event.OldFullPath,
                        (getEventPath event.FullPath, None)
                    )
                ]
            )

    let failureStream =
        AsyncSeq.subscribeEvent
            watcher.Error
            (fun event -> ticks (), [ FileSystemChange.Failure (event.GetException ()) ])

    let stream =
        [
            changedStream
            deletedStream
            createdStream
            renamedStream
            failureStream
        ]
        |> FSharp.Control.AsyncSeq.mergeAll
        |> FSharp.Control.AsyncSeq.map (fun (t, events) ->
            events
            |> List.fold
                (fun (i, events) event ->
                    i + 1L,
                    (t + i, event) :: events)
                (0L, [])
            |> snd
            |> List.rev
        )
        |> FSharp.Control.AsyncSeq.concatSeq
        |> FSharp.Control.AsyncSeq.mapAsyncParallel (fun (t, event) -> async {
            match shouldReadContent event, event with
            | true, FileSystemChange.Changed (path, _) ->
                do! Async.Sleep 5
                let! content = fullPath </> path |> SpiralFileSystem.read_all_text_retry_async
                return t, FileSystemChange.Changed (path, content)
            | true, FileSystemChange.Created (path, _) ->
                do! Async.Sleep 5
                let! content = fullPath </> path |> SpiralFileSystem.read_all_text_retry_async
                return t, FileSystemChange.Created (path, content)
            | true, FileSystemChange.Renamed (oldPath, (newPath, _)) ->
                let! content = fullPath </> newPath |> SpiralFileSystem.read_all_text_retry_async
                return t, FileSystemChange.Renamed (oldPath, (newPath, content))
            | _ -> return t, event
        })

    let disposable =
        new_disposable (fun () ->
            trace Debug (fun () -> "FileSystem.watchWithFilter / Disposing watch stream") _locals
            watcher.EnableRaisingEvents <- false
            watcher.Dispose ()
        )

    stream, disposable

let inline watchDirectory path =
    watchDirectoryWithFilter
        (System.IO.NotifyFilters.FileName
        ||| System.IO.NotifyFilters.LastWrite
        )
        path

testEventsRaw (test)

///- --test

let inline testEventsRaw
    (watchFn : (_ -> bool) -> string -> FSharp.Control.AsyncSeq<int64 * FileSystemChange> * IDisposable)
    write
    =
    let struct (tempDir, tempDisposable) =
        "FileSystem.testEventsRaw"
        |> SpiralCrypto.hash_text
        |> SpiralFileSystem.create_temp_dir'
    let stream, disposable = watchFn (fun _ -> true) tempDir

    let events = System.Collections.Concurrent.ConcurrentBag ()

    let inline iter () =
        stream
        |> FSharp.Control.AsyncSeq.iterAsyncParallel (fun event -> async { events.Add event })

    let run = async {
        let! _ = iter () |> Async.StartChild
        do! Async.Sleep 250
        return! write tempDir
    }

    try
        run
        |> Async.runWithTimeout 60000
        |> _assertEqual (Some ())
    finally
        disposable.Dispose ()
        tempDisposable.Dispose ()

    let eventsLog =
        events
        |> Seq.toList
        |> List.sortBy fst
        |> List.fold
            (fun (prev, acc) (ticks, event) ->
                ticks, (ticks, (if prev = 0L then 0L else ticks - prev), event) :: acc
            )
            (0L, [])
        |> snd
        |> List.rev
        |> List.map (fun (diff, n, event) -> $"{n} / {diff} / {event}" |> SpiralSm.ellipsis_end 100L)
        |> SpiralSm.concat "\n"
    let _locals () = $"eventsLog: \n{eventsLog} / {_locals ()}"
    trace Debug (fun () -> "FileSystem.testEventsRaw") _locals

    events
    |> Seq.toList
    |> List.sortBy fst
    |> List.map snd
    |> List.fold
        (fun acc event ->
            match acc, event with
            | FileSystemChange.Changed (lastPath, Some lastContent) as lastEvent :: acc,
                FileSystemChange.Changed (path, Some content)
                when lastPath = path && content |> SpiralSm.starts_with lastContent
                ->
                event :: acc
            | _ -> event :: acc
        )
        []
    |> List.rev

fast (test)

///- --test

let inline write path = async {
    let n = 2

    for i = 1 to n do
        do! $"a{i}" |> SpiralFileSystem.write_all_text_async (path </> $"file{i}.txt")

    do! Async.Sleep 250

    for i = 1 to n do
        do! $"b{i}" |> SpiralFileSystem.write_all_text_async (path </> $"file{i}.txt")

    do! Async.Sleep 250

    for i = 1 to n do
        do! path </> $"file{i}.txt" |> SpiralFileSystem.move_file_async (path </> $"file_{i}.txt") |> Async.Ignore

    do! Async.Sleep 250

    for i = 1 to n do
        do! $"c{i}" |> SpiralFileSystem.write_all_text_async (path </> $"file_{i}.txt")

    do! Async.Sleep 250

    for i = 1 to n do
        do! SpiralFileSystem.delete_file_async (path </> $"file_{i}.txt") |> Async.Ignore

    do! Async.Sleep 250
}

let inline run () =
    let events = testEventsRaw watchDirectory write

    events
    |> _sequenceEqual [
        FileSystemChange.Created ("file1.txt", Some "a1")
        FileSystemChange.Changed ("file1.txt", Some "a1")
        FileSystemChange.Created ("file2.txt", Some "a2")
        FileSystemChange.Changed ("file2.txt", Some "a2")

        FileSystemChange.Changed ("file1.txt", Some "b1")
        FileSystemChange.Changed ("file2.txt", Some "b2")

        FileSystemChange.Renamed ("file1.txt", ("file_1.txt", Some "b1"))
        FileSystemChange.Renamed ("file2.txt", ("file_2.txt", Some "b2"))

        FileSystemChange.Changed ("file_1.txt", Some "c1")
        FileSystemChange.Changed ("file_2.txt", Some "c2")

        FileSystemChange.Deleted "file_1.txt"
        FileSystemChange.Deleted "file_2.txt"
    ]

run
|> retry_fn 3
|> _assertEqual (Some ())
Some ()

02:18:57 d #1 FileSystem.watchWithFilter / Disposing watch stream / filter: FileName, LastWrite
02:18:57 d #2 FileSystem.testEventsRaw / eventsLog: 
0 / 639280595364410565 / Created ("file1.txt", Some "a1")
6257 / 639280595364416822 / Changed ("file1.txt", Some "a1")
469 / 639280595364417291 / Created ("file2.txt", Some "a2")
16 / 639280595364417307 / Changed ("file2.txt", Some "a2")
7 / 639280595364417314 / Changed ("file2.txt", Some "a2")
2514437 / 639280595366931751 / Changed ("file1.txt", Some "b1")
3687 / 639280595366935438 / Changed ("file1.txt", Some "b1")
1002 / 639280595366936440 / Changed ("file2.txt", Some "b2")
68 / 639280595366936508 / Changed ("file2.txt", Some "b2")
2553567 / 639280595369490075 / Renamed ("file1.txt", ("file_1.txt", Some "b1"))
7171 / 639280595369497246 / Renamed ("file2.txt", ("file_2.txt", Some "b2"))
2567787 / 639280595372065033 / Changed ("file_1.txt", Some "c1")
1843 / 639280595372066876 / Changed ("file_1.txt", Some "c1")
3240 / 639280595372070116 / Changed ("file_2.txt", Some "c2")
112 / 639280595372070228 / Changed ("file_2.txt", Some "c2")
2526432 / 639280595374596660 / Deleted "file_1.txt"
1600 / 639280595374598260 / Deleted "file_2.txt"
[Created ("file1.txt", Some "a1"); Changed ("file1.txt", Some "a1"); Created ("file2.txt", Some "a2");
 Changed ("file2.txt", Some "a2"); Changed ("file1.txt", Some "b1"); Changed ("file2.txt", Some "b2");
 Renamed ("file1.txt", ("file_1.txt", Some "b1")); Renamed ("file2.txt", ("file_2.txt", Some "b2"));
 Changed ("file_1.txt", Some "c1"); Changed ("file_2.txt", Some "c2"); Deleted "file_1.txt"; Deleted "file_2.txt"]

Some ()

slow (test)

///- --test

let inline write path = async {
    let n = 2

    let contents =
        [ 1 .. n ]
        |> List.map (string >> String.replicate 1_000_000)

    for i = 1 to n do
        do! $"{contents.[i - 1]}a" |> SpiralFileSystem.write_all_text_async (path </> $"file{i}.txt")

    do! Async.Sleep 1500

    for i = 1 to n do
        do! $"{contents.[i - 1]}b" |> SpiralFileSystem.write_all_text_async (path </> $"file{i}.txt")

    do! Async.Sleep 1500

    for i = 1 to n do
        do! path </> $"file{i}.txt" |> SpiralFileSystem.move_file_async (path </> $"file_{i}.txt") |> Async.Ignore

    do! Async.Sleep 1500

    for i = 1 to n do
        do! $"{contents.[i - 1]}c" |> SpiralFileSystem.write_all_text_async (path </> $"file_{i}.txt")

    do! Async.Sleep 1500

    for i = 1 to n do
        do! SpiralFileSystem.delete_file_async (path </> $"file_{i}.txt") |> Async.Ignore

    do! Async.Sleep 1500
}

let inline run () =
    let events =
        testEventsRaw watchDirectory write
        |> List.map (function
            | FileSystemChange.Changed (path, Some content) ->
                FileSystemChange.Changed (path, content |> Seq.distinct |> Seq.map string |> SpiralSm.concat "" |> Some)
            | FileSystemChange.Created (path, Some content) ->
                FileSystemChange.Created (path, content |> Seq.distinct |> Seq.map string |> SpiralSm.concat "" |> Some)
            | FileSystemChange.Renamed (oldPath, (newPath, Some content)) ->
                FileSystemChange.Renamed (
                    oldPath,
                    (newPath, content |> Seq.distinct |> Seq.map string |> SpiralSm.concat "" |> Some)
                )
            | event -> event
        )

    events
    |> _sequenceEqual [
        FileSystemChange.Created ("file1.txt", Some "1a")
        FileSystemChange.Changed ("file1.txt", Some "1a")
        FileSystemChange.Created ("file2.txt", Some "2a")
        FileSystemChange.Changed ("file2.txt", Some "2a")

        FileSystemChange.Changed ("file1.txt", Some "1b")
        FileSystemChange.Changed ("file2.txt", Some "2b")

        FileSystemChange.Renamed ("file1.txt", ("file_1.txt", Some "1b"))
        FileSystemChange.Renamed ("file2.txt", ("file_2.txt", Some "2b"))

        FileSystemChange.Changed ("file_1.txt", Some "1c")
        FileSystemChange.Changed ("file_2.txt", Some "2c")

        FileSystemChange.Deleted "file_1.txt"
        FileSystemChange.Deleted "file_2.txt"
    ]

run
|> retry_fn 5
|> _assertEqual (Some ())
Some ()

02:19:05 d #3 FileSystem.watchWithFilter / Disposing watch stream / filter: FileName, LastWrite
02:19:06 d #4 FileSystem.testEventsRaw / eventsLog: 
0 / 639280595382326727 / Created
  ("file1.txt",
 ...11111111111111111111111111111111111111111111111a")
1019 / 639280595382327746 / Changed
  ("file1.txt"...11111111111111111111111111111111111111111111111a")
115 / 639280595382327861 / Changed
  ("file1.txt",...11111111111111111111111111111111111111111111111a")
20 / 639280595382327881 / Changed
  ("file1.txt",
...11111111111111111111111111111111111111111111111a")
24 / 639280595382327905 / Changed
  ("file1.txt",
...11111111111111111111111111111111111111111111111a")
11 / 639280595382327916 / Changed
  ("file1.txt",
...11111111111111111111111111111111111111111111111a")
29 / 639280595382327945 / Changed
  ("file1.txt",
...11111111111111111111111111111111111111111111111a")
11 / 639280595382327956 / Changed
  ("file1.txt",
...11111111111111111111111111111111111111111111111a")
13 / 639280595382327969 / Changed
  ("file1.txt",
...11111111111111111111111111111111111111111111111a")
19 / 639280595382327988 / Changed
  ("file1.txt",
...11111111111111111111111111111111111111111111111a")
19 / 639280595382328007 / Changed
  ("file1.txt",
...11111111111111111111111111111111111111111111111a")
51 / 639280595382328058 / Changed
  ("file1.txt",
...11111111111111111111111111111111111111111111111a")
8 / 639280595382328066 / Changed
  ("file1.txt",
 ...11111111111111111111111111111111111111111111111a")
8 / 639280595382328074 / Changed
  ("file1.txt",
 ...11111111111111111111111111111111111111111111111a")
8 / 639280595382328082 / Changed
  ("file1.txt",
 ...11111111111111111111111111111111111111111111111a")
15 / 639280595382328097 / Changed
  ("file1.txt",
...11111111111111111111111111111111111111111111111a")
8 / 639280595382328105 / Changed
  ("file1.txt",
 ...11111111111111111111111111111111111111111111111a")
71 / 639280595382328176 / Changed
  ("file1.txt",
...11111111111111111111111111111111111111111111111a")
53 / 639280595382328229 / Changed
  ("file1.txt",
...11111111111111111111111111111111111111111111111a")
20 / 639280595382328249 / Changed
  ("file1.txt",
...11111111111111111111111111111111111111111111111a")
8 / 639280595382328257 / Changed
  ("file1.txt",
 ...11111111111111111111111111111111111111111111111a")
11 / 639280595382328268 / Changed
  ("file1.txt",
...11111111111111111111111111111111111111111111111a")
7 / 639280595382328275 / Changed
  ("file1.txt",
 ...11111111111111111111111111111111111111111111111a")
8 / 639280595382328283 / Changed
  ("file1.txt",
 ...11111111111111111111111111111111111111111111111a")
7 / 639280595382328290 / Changed
  ("file1.txt",
 ...11111111111111111111111111111111111111111111111a")
8 / 639280595382328298 / Changed
  ("file1.txt",
 ...11111111111111111111111111111111111111111111111a")
7 / 639280595382328305 / Changed
  ("file1.txt",
 ...11111111111111111111111111111111111111111111111a")
20577 / 639280595382348882 / Changed
  ("file1.txt...11111111111111111111111111111111111111111111111a")
44 / 639280595382348926 / Created
  ("file2.txt",
...22222222222222222222222222222222222222222222222a")
56 / 639280595382348982 / Changed
  ("file2.txt",
...22222222222222222222222222222222222222222222222a")
137 / 639280595382349119 / Changed
  ("file2.txt",...22222222222222222222222222222222222222222222222a")
2344 / 639280595382351463 / Changed
  ("file2.txt"...22222222222222222222222222222222222222222222222a")
29 / 639280595382351492 / Changed
  ("file2.txt",
...22222222222222222222222222222222222222222222222a")
4663 / 639280595382356155 / Changed
  ("file2.txt"...22222222222222222222222222222222222222222222222a")
15033685 / 639280595397389840 / Changed
  ("file1....11111111111111111111111111111111111111111111111b")
283 / 639280595397390123 / Changed
  ("file1.txt",...11111111111111111111111111111111111111111111111b")
1269 / 639280595397391392 / Changed
  ("file1.txt"...11111111111111111111111111111111111111111111111b")
26 / 639280595397391418 / Changed
  ("file1.txt",
...11111111111111111111111111111111111111111111111b")
1351 / 639280595397392769 / Changed
  ("file1.txt"...11111111111111111111111111111111111111111111111b")
183 / 639280595397392952 / Changed
  ("file1.txt",...11111111111111111111111111111111111111111111111b")
11 / 639280595397392963 / Changed
  ("file1.txt",
...11111111111111111111111111111111111111111111111b")
197 / 639280595397393160 / Changed
  ("file1.txt",...11111111111111111111111111111111111111111111111b")
299 / 639280595397393459 / Changed
  ("file1.txt",...11111111111111111111111111111111111111111111111b")
18 / 639280595397393477 / Changed
  ("file1.txt",
...11111111111111111111111111111111111111111111111b")
72 / 639280595397393549 / Changed
  ("file1.txt",
...11111111111111111111111111111111111111111111111b")
12 / 639280595397393561 / Changed
  ("file1.txt",
...11111111111111111111111111111111111111111111111b")
69 / 639280595397393630 / Changed
  ("file1.txt",
...11111111111111111111111111111111111111111111111b")
94 / 639280595397393724 / Changed
  ("file1.txt",
...11111111111111111111111111111111111111111111111b")
15302 / 639280595397409026 / Changed
  ("file1.txt...11111111111111111111111111111111111111111111111b")
5646 / 639280595397414672 / Changed
  ("file2.txt"...22222222222222222222222222222222222222222222222b")
92 / 639280595397414764 / Changed
  ("file2.txt",
...22222222222222222222222222222222222222222222222b")
199 / 639280595397414963 / Changed
  ("file2.txt",...22222222222222222222222222222222222222222222222b")
54 / 639280595397415017 / Changed
  ("file2.txt",
...22222222222222222222222222222222222222222222222b")
140 / 639280595397415157 / Changed
  ("file2.txt",...22222222222222222222222222222222222222222222222b")
12 / 639280595397415169 / Changed
  ("file2.txt",
...22222222222222222222222222222222222222222222222b")
89 / 639280595397415258 / Changed
  ("file2.txt",
...22222222222222222222222222222222222222222222222b")
36 / 639280595397415294 / Changed
  ("file2.txt",
...22222222222222222222222222222222222222222222222b")
66 / 639280595397415360 / Changed
  ("file2.txt",
...22222222222222222222222222222222222222222222222b")
69 / 639280595397415429 / Changed
  ("file2.txt",
...22222222222222222222222222222222222222222222222b")
69 / 639280595397415498 / Changed
  ("file2.txt",
...22222222222222222222222222222222222222222222222b")
69 / 639280595397415567 / Changed
  ("file2.txt",
...22222222222222222222222222222222222222222222222b")
110 / 639280595397415677 / Changed
  ("file2.txt",...22222222222222222222222222222222222222222222222b")
131 / 639280595397415808 / Changed
  ("file2.txt",...22222222222222222222222222222222222222222222222b")
71 / 639280595397415879 / Changed
  ("file2.txt",
...22222222222222222222222222222222222222222222222b")
41 / 639280595397415920 / Changed
  ("file2.txt",
...22222222222222222222222222222222222222222222222b")
78 / 639280595397415998 / Changed
  ("file2.txt",
...22222222222222222222222222222222222222222222222b")
65 / 639280595397416063 / Changed
  ("file2.txt",
...22222222222222222222222222222222222222222222222b")
70 / 639280595397416133 / Changed
  ("file2.txt",
...22222222222222222222222222222222222222222222222b")
69 / 639280595397416202 / Changed
  ("file2.txt",
...22222222222222222222222222222222222222222222222b")
79 / 639280595397416281 / Changed
  ("file2.txt",
...22222222222222222222222222222222222222222222222b")
58 / 639280595397416339 / Changed
  ("file2.txt",
...22222222222222222222222222222222222222222222222b")
72 / 639280595397416411 / Changed
  ("file2.txt",
...22222222222222222222222222222222222222222222222b")
114 / 639280595397416525 / Changed
  ("file2.txt",...22222222222222222222222222222222222222222222222b")
79 / 639280595397416604 / Changed
  ("file2.txt",
...22222222222222222222222222222222222222222222222b")
75 / 639280595397416679 / Changed
  ("file2.txt",
...22222222222222222222222222222222222222222222222b")
51 / 639280595397416730 / Changed
  ("file2.txt",
...22222222222222222222222222222222222222222222222b")
64 / 639280595397416794 / Changed
  ("file2.txt",
...22222222222222222222222222222222222222222222222b")
84 / 639280595397416878 / Changed
  ("file2.txt",
...22222222222222222222222222222222222222222222222b")
52 / 639280595397416930 / Changed
  ("file2.txt",
...22222222222222222222222222222222222222222222222b")
25 / 639280595397416955 / Changed
  ("file2.txt",
...22222222222222222222222222222222222222222222222b")
113 / 639280595397417068 / Changed
  ("file2.txt",...22222222222222222222222222222222222222222222222b")
12 / 639280595397417080 / Changed
  ("file2.txt",
...22222222222222222222222222222222222222222222222b")
112 / 639280595397417192 / Changed
  ("file2.txt",...22222222222222222222222222222222222222222222222b")
134 / 639280595397417326 / Changed
  ("file2.txt",...22222222222222222222222222222222222222222222222b")
98 / 639280595397417424 / Changed
  ("file2.txt",
...22222222222222222222222222222222222222222222222b")
114 / 639280595397417538 / Changed
  ("file2.txt",...22222222222222222222222222222222222222222222222b")
100 / 639280595397417638 / Changed
  ("file2.txt",...22222222222222222222222222222222222222222222222b")
16 / 639280595397417654 / Changed
  ("file2.txt",
...22222222222222222222222222222222222222222222222b")
113 / 639280595397417767 / Changed
  ("file2.txt",...22222222222222222222222222222222222222222222222b")
94 / 639280595397417861 / Changed
  ("file2.txt",
...22222222222222222222222222222222222222222222222b")
11 / 639280595397417872 / Changed
  ("file2.txt",
...22222222222222222222222222222222222222222222222b")
101 / 639280595397417973 / Changed
  ("file2.txt",...22222222222222222222222222222222222222222222222b")
11 / 639280595397417984 / Changed
  ("file2.txt",
...22222222222222222222222222222222222222222222222b")
98 / 639280595397418082 / Changed
  ("file2.txt",
...22222222222222222222222222222222222222222222222b")
12 / 639280595397418094 / Changed
  ("file2.txt",
...22222222222222222222222222222222222222222222222b")
106 / 639280595397418200 / Changed
  ("file2.txt",...22222222222222222222222222222222222222222222222b")
110 / 639280595397418310 / Changed
  ("file2.txt",...22222222222222222222222222222222222222222222222b")
99 / 639280595397418409 / Changed
  ("file2.txt",
...22222222222222222222222222222222222222222222222b")
11 / 639280595397418420 / Changed
  ("file2.txt",
...22222222222222222222222222222222222222222222222b")
238 / 639280595397418658 / Changed
  ("file2.txt",...22222222222222222222222222222222222222222222222b")
14 / 639280595397418672 / Changed
  ("file2.txt",
...22222222222222222222222222222222222222222222222b")
126 / 639280595397418798 / Changed
  ("file2.txt",...22222222222222222222222222222222222222222222222b")
518 / 639280595397419316 / Changed
  ("file2.txt",...22222222222222222222222222222222222222222222222b")
9 / 639280595397419325 / Changed
  ("file2.txt",
 ...22222222222222222222222222222222222222222222222b")
8 / 639280595397419333 / Changed
  ("file2.txt",
 ...22222222222222222222222222222222222222222222222b")
8 / 639280595397419341 / Changed
  ("file2.txt",
 ...22222222222222222222222222222222222222222222222b")
12 / 639280595397419353 / Changed
  ("file2.txt",
...22222222222222222222222222222222222222222222222b")
8 / 639280595397419361 / Changed
  ("file2.txt",
 ...22222222222222222222222222222222222222222222222b")
8 / 639280595397419369 / Changed
  ("file2.txt",
 ...22222222222222222222222222222222222222222222222b")
323 / 639280595397419692 / Changed
  ("file2.txt",...22222222222222222222222222222222222222222222222b")
8 / 639280595397419700 / Changed
  ("file2.txt",
 ...22222222222222222222222222222222222222222222222b")
8 / 639280595397419708 / Changed
  ("file2.txt",
 ...22222222222222222222222222222222222222222222222b")
9 / 639280595397419717 / Changed
  ("file2.txt",
 ...22222222222222222222222222222222222222222222222b")
8 / 639280595397419725 / Changed
  ("file2.txt",
 ...22222222222222222222222222222222222222222222222b")
286 / 639280595397420011 / Changed
  ("file2.txt",...22222222222222222222222222222222222222222222222b")
8 / 639280595397420019 / Changed
  ("file2.txt",
 ...22222222222222222222222222222222222222222222222b")
8 / 639280595397420027 / Changed
  ("file2.txt",
 ...22222222222222222222222222222222222222222222222b")
26 / 639280595397420053 / Changed
  ("file2.txt",
...22222222222222222222222222222222222222222222222b")
287 / 639280595397420340 / Changed
  ("file2.txt",...22222222222222222222222222222222222222222222222b")
12 / 639280595397420352 / Changed
  ("file2.txt",
...22222222222222222222222222222222222222222222222b")
8 / 639280595397420360 / Changed
  ("file2.txt",
 ...22222222222222222222222222222222222222222222222b")
7 / 639280595397420367 / Changed
  ("file2.txt",
 ...22222222222222222222222222222222222222222222222b")
282 / 639280595397420649 / Changed
  ("file2.txt",...22222222222222222222222222222222222222222222222b")
7 / 639280595397420656 / Changed
  ("file2.txt",
 ...22222222222222222222222222222222222222222222222b")
8 / 639280595397420664 / Changed
  ("file2.txt",
 ...22222222222222222222222222222222222222222222222b")
7 / 639280595397420671 / Changed
  ("file2.txt",
 ...22222222222222222222222222222222222222222222222b")
276 / 639280595397420947 / Changed
  ("file2.txt",...22222222222222222222222222222222222222222222222b")
8 / 639280595397420955 / Changed
  ("file2.txt",
 ...22222222222222222222222222222222222222222222222b")
8 / 639280595397420963 / Changed
  ("file2.txt",
 ...22222222222222222222222222222222222222222222222b")
290 / 639280595397421253 / Changed
  ("file2.txt",...22222222222222222222222222222222222222222222222b")
8 / 639280595397421261 / Changed
  ("file2.txt",
 ...22222222222222222222222222222222222222222222222b")
8 / 639280595397421269 / Changed
  ("file2.txt",
 ...22222222222222222222222222222222222222222222222b")
8 / 639280595397421277 / Changed
  ("file2.txt",
 ...22222222222222222222222222222222222222222222222b")
13 / 639280595397421290 / Changed
  ("file2.txt",
...22222222222222222222222222222222222222222222222b")
285 / 639280595397421575 / Changed
  ("file2.txt",...22222222222222222222222222222222222222222222222b")
8 / 639280595397421583 / Changed
  ("file2.txt",
 ...22222222222222222222222222222222222222222222222b")
8 / 639280595397421591 / Changed
  ("file2.txt",
 ...22222222222222222222222222222222222222222222222b")
8 / 639280595397421599 / Changed
  ("file2.txt",
 ...22222222222222222222222222222222222222222222222b")
290 / 639280595397421889 / Changed
  ("file2.txt",...22222222222222222222222222222222222222222222222b")
8 / 639280595397421897 / Changed
  ("file2.txt",
 ...22222222222222222222222222222222222222222222222b")
8 / 639280595397421905 / Changed
  ("file2.txt",
 ...22222222222222222222222222222222222222222222222b")
7 / 639280595397421912 / Changed
  ("file2.txt",
 ...22222222222222222222222222222222222222222222222b")
284 / 639280595397422196 / Changed
  ("file2.txt",...22222222222222222222222222222222222222222222222b")
8 / 639280595397422204 / Changed
  ("file2.txt",
 ...22222222222222222222222222222222222222222222222b")
8 / 639280595397422212 / Changed
  ("file2.txt",
 ...22222222222222222222222222222222222222222222222b")
8 / 639280595397422220 / Changed
  ("file2.txt",
 ...22222222222222222222222222222222222222222222222b")
306 / 639280595397422526 / Changed
  ("file2.txt",...22222222222222222222222222222222222222222222222b")
8 / 639280595397422534 / Changed
  ("file2.txt",
 ...22222222222222222222222222222222222222222222222b")
12 / 639280595397422546 / Changed
  ("file2.txt",
...22222222222222222222222222222222222222222222222b")
8 / 639280595397422554 / Changed
  ("file2.txt",
 ...22222222222222222222222222222222222222222222222b")
8 / 639280595397422562 / Changed
  ("file2.txt",
 ...22222222222222222222222222222222222222222222222b")
297 / 639280595397422859 / Changed
  ("file2.txt",...22222222222222222222222222222222222222222222222b")
9 / 639280595397422868 / Changed
  ("file2.txt",
 ...22222222222222222222222222222222222222222222222b")
7 / 639280595397422875 / Changed
  ("file2.txt",
 ...22222222222222222222222222222222222222222222222b")
8 / 639280595397422883 / Changed
  ("file2.txt",
 ...22222222222222222222222222222222222222222222222b")
307 / 639280595397423190 / Changed
  ("file2.txt",...22222222222222222222222222222222222222222222222b")
8 / 639280595397423198 / Changed
  ("file2.txt",
 ...22222222222222222222222222222222222222222222222b")
7 / 639280595397423205 / Changed
  ("file2.txt",
 ...22222222222222222222222222222222222222222222222b")
8 / 639280595397423213 / Changed
  ("file2.txt",
 ...22222222222222222222222222222222222222222222222b")
8 / 639280595397423221 / Changed
  ("file2.txt",
 ...22222222222222222222222222222222222222222222222b")
299 / 639280595397423520 / Changed
  ("file2.txt",...22222222222222222222222222222222222222222222222b")
8 / 639280595397423528 / Changed
  ("file2.txt",
 ...22222222222222222222222222222222222222222222222b")
8 / 639280595397423536 / Changed
  ("file2.txt",
 ...22222222222222222222222222222222222222222222222b")
13 / 639280595397423549 / Changed
  ("file2.txt",
...22222222222222222222222222222222222222222222222b")
313 / 639280595397423862 / Changed
  ("file2.txt",...22222222222222222222222222222222222222222222222b")
8 / 639280595397423870 / Changed
  ("file2.txt",
 ...22222222222222222222222222222222222222222222222b")
8 / 639280595397423878 / Changed
  ("file2.txt",
 ...22222222222222222222222222222222222222222222222b")
15033169 / 639280595412457047 / Renamed
  ("file1....1111111111111111111111111111111111111111111111b"))
331 / 639280595412457378 / Renamed
  ("file2.txt",...2222222222222222222222222222222222222222222222b"))
15036371 / 639280595427493749 / Changed
  ("file_1...11111111111111111111111111111111111111111111111c")
165 / 639280595427493914 / Changed
  ("file_1.txt"...11111111111111111111111111111111111111111111111c")
12 / 639280595427493926 / Changed
  ("file_1.txt",...11111111111111111111111111111111111111111111111c")
7 / 639280595427493933 / Changed
  ("file_1.txt",
...11111111111111111111111111111111111111111111111c")
7 / 639280595427493940 / Changed
  ("file_1.txt",
...11111111111111111111111111111111111111111111111c")
7 / 639280595427493947 / Changed
  ("file_1.txt",
...11111111111111111111111111111111111111111111111c")
253 / 639280595427494200 / Changed
  ("file_1.txt"...11111111111111111111111111111111111111111111111c")
9 / 639280595427494209 / Changed
  ("file_1.txt",
...11111111111111111111111111111111111111111111111c")
6 / 639280595427494215 / Changed
  ("file_1.txt",
...11111111111111111111111111111111111111111111111c")
7 / 639280595427494222 / Changed
  ("file_1.txt",
...11111111111111111111111111111111111111111111111c")
216 / 639280595427494438 / Changed
  ("file_1.txt"...11111111111111111111111111111111111111111111111c")
87 / 639280595427494525 / Changed
  ("file_1.txt",...11111111111111111111111111111111111111111111111c")
65 / 639280595427494590 / Changed
  ("file_1.txt",...11111111111111111111111111111111111111111111111c")
100 / 639280595427494690 / Changed
  ("file_1.txt"...11111111111111111111111111111111111111111111111c")
9 / 639280595427494699 / Changed
  ("file_1.txt",
...11111111111111111111111111111111111111111111111c")
69 / 639280595427494768 / Changed
  ("file_1.txt",...11111111111111111111111111111111111111111111111c")
117 / 639280595427494885 / Changed
  ("file_1.txt"...11111111111111111111111111111111111111111111111c")
8 / 639280595427494893 / Changed
  ("file_1.txt",
...11111111111111111111111111111111111111111111111c")
101 / 639280595427494994 / Changed
  ("file_1.txt"...11111111111111111111111111111111111111111111111c")
7 / 639280595427495001 / Changed
  ("file_1.txt",
...11111111111111111111111111111111111111111111111c")
193 / 639280595427495194 / Changed
  ("file_1.txt"...11111111111111111111111111111111111111111111111c")
48 / 639280595427495242 / Changed
  ("file_1.txt",...11111111111111111111111111111111111111111111111c")
102 / 639280595427495344 / Changed
  ("file_1.txt"...11111111111111111111111111111111111111111111111c")
112 / 639280595427495456 / Changed
  ("file_1.txt"...11111111111111111111111111111111111111111111111c")
7 / 639280595427495463 / Changed
  ("file_1.txt",
...11111111111111111111111111111111111111111111111c")
64 / 639280595427495527 / Changed
  ("file_1.txt",...11111111111111111111111111111111111111111111111c")
71 / 639280595427495598 / Changed
  ("file_1.txt",...11111111111111111111111111111111111111111111111c")
61 / 639280595427495659 / Changed
  ("file_1.txt",...11111111111111111111111111111111111111111111111c")
84 / 639280595427495743 / Changed
  ("file_1.txt",...11111111111111111111111111111111111111111111111c")
531 / 639280595427496274 / Changed
  ("file_1.txt"...11111111111111111111111111111111111111111111111c")
138 / 639280595427496412 / Changed
  ("file_1.txt"...11111111111111111111111111111111111111111111111c")
9 / 639280595427496421 / Changed
  ("file_1.txt",
...11111111111111111111111111111111111111111111111c")
8 / 639280595427496429 / Changed
  ("file_1.txt",
...11111111111111111111111111111111111111111111111c")
7 / 639280595427496436 / Changed
  ("file_1.txt",
...11111111111111111111111111111111111111111111111c")
8 / 639280595427496444 / Changed
  ("file_1.txt",
...11111111111111111111111111111111111111111111111c")
71 / 639280595427496515 / Changed
  ("file_1.txt",...11111111111111111111111111111111111111111111111c")
60 / 639280595427496575 / Changed
  ("file_1.txt",...11111111111111111111111111111111111111111111111c")
77 / 639280595427496652 / Changed
  ("file_1.txt",...11111111111111111111111111111111111111111111111c")
59 / 639280595427496711 / Changed
  ("file_1.txt",...11111111111111111111111111111111111111111111111c")
117 / 639280595427496828 / Changed
  ("file_1.txt"...11111111111111111111111111111111111111111111111c")
67 / 639280595427496895 / Changed
  ("file_1.txt",...11111111111111111111111111111111111111111111111c")
90 / 639280595427496985 / Changed
  ("file_1.txt",...11111111111111111111111111111111111111111111111c")
65 / 639280595427497050 / Changed
  ("file_1.txt",...11111111111111111111111111111111111111111111111c")
61 / 639280595427497111 / Changed
  ("file_1.txt",...11111111111111111111111111111111111111111111111c")
56 / 639280595427497167 / Changed
  ("file_1.txt",...11111111111111111111111111111111111111111111111c")
56 / 639280595427497223 / Changed
  ("file_1.txt",...11111111111111111111111111111111111111111111111c")
320 / 639280595427497543 / Changed
  ("file_1.txt"...11111111111111111111111111111111111111111111111c")
12 / 639280595427497555 / Changed
  ("file_1.txt",...11111111111111111111111111111111111111111111111c")
23 / 639280595427497578 / Changed
  ("file_1.txt",...11111111111111111111111111111111111111111111111c")
9 / 639280595427497587 / Changed
  ("file_1.txt",
...11111111111111111111111111111111111111111111111c")
10 / 639280595427497597 / Changed
  ("file_1.txt",...11111111111111111111111111111111111111111111111c")
142 / 639280595427497739 / Changed
  ("file_1.txt"...11111111111111111111111111111111111111111111111c")
8 / 639280595427497747 / Changed
  ("file_1.txt",
...11111111111111111111111111111111111111111111111c")
7 / 639280595427497754 / Changed
  ("file_1.txt",
...11111111111111111111111111111111111111111111111c")
104 / 639280595427497858 / Changed
  ("file_1.txt"...11111111111111111111111111111111111111111111111c")
8831 / 639280595427506689 / Changed
  ("file_1.txt...11111111111111111111111111111111111111111111111c")
7252 / 639280595427513941 / Changed
  ("file_2.txt...22222222222222222222222222222222222222222222222c")
50 / 639280595427513991 / Changed
  ("file_2.txt",...22222222222222222222222222222222222222222222222c")
234 / 639280595427514225 / Changed
  ("file_2.txt"...22222222222222222222222222222222222222222222222c")
133 / 639280595427514358 / Changed
  ("file_2.txt"...22222222222222222222222222222222222222222222222c")
35 / 639280595427514393 / Changed
  ("file_2.txt",...22222222222222222222222222222222222222222222222c")
125 / 639280595427514518 / Changed
  ("file_2.txt"...22222222222222222222222222222222222222222222222c")
70 / 639280595427514588 / Changed
  ("file_2.txt",...22222222222222222222222222222222222222222222222c")
133 / 639280595427514721 / Changed
  ("file_2.txt"...22222222222222222222222222222222222222222222222c")
10 / 639280595427514731 / Changed
  ("file_2.txt",...22222222222222222222222222222222222222222222222c")
54 / 639280595427514785 / Changed
  ("file_2.txt",...22222222222222222222222222222222222222222222222c")
178 / 639280595427514963 / Changed
  ("file_2.txt"...22222222222222222222222222222222222222222222222c")
112 / 639280595427515075 / Changed
  ("file_2.txt"...22222222222222222222222222222222222222222222222c")
112 / 639280595427515187 / Changed
  ("file_2.txt"...22222222222222222222222222222222222222222222222c")
46 / 639280595427515233 / Changed
  ("file_2.txt",...22222222222222222222222222222222222222222222222c")
61 / 639280595427515294 / Changed
  ("file_2.txt",...22222222222222222222222222222222222222222222222c")
114 / 639280595427515408 / Changed
  ("file_2.txt"...22222222222222222222222222222222222222222222222c")
9 / 639280595427515417 / Changed
  ("file_2.txt",
...22222222222222222222222222222222222222222222222c")
120 / 639280595427515537 / Changed
  ("file_2.txt"...22222222222222222222222222222222222222222222222c")
736 / 639280595427516273 / Changed
  ("file_2.txt"...22222222222222222222222222222222222222222222222c")
55 / 639280595427516328 / Changed
  ("file_2.txt",...22222222222222222222222222222222222222222222222c")
8 / 639280595427516336 / Changed
  ("file_2.txt",
...22222222222222222222222222222222222222222222222c")
110 / 639280595427516446 / Changed
  ("file_2.txt"...22222222222222222222222222222222222222222222222c")
95 / 639280595427516541 / Changed
  ("file_2.txt",...22222222222222222222222222222222222222222222222c")
111 / 639280595427516652 / Changed
  ("file_2.txt"...22222222222222222222222222222222222222222222222c")
8 / 639280595427516660 / Changed
  ("file_2.txt",
...22222222222222222222222222222222222222222222222c")
112 / 639280595427516772 / Changed
  ("file_2.txt"...22222222222222222222222222222222222222222222222c")
68 / 639280595427516840 / Changed
  ("file_2.txt",...22222222222222222222222222222222222222222222222c")
9 / 639280595427516849 / Changed
  ("file_2.txt",
...22222222222222222222222222222222222222222222222c")
104 / 639280595427516953 / Changed
  ("file_2.txt"...22222222222222222222222222222222222222222222222c")
19 / 639280595427516972 / Changed
  ("file_2.txt",...22222222222222222222222222222222222222222222222c")
147 / 639280595427517119 / Changed
  ("file_2.txt"...22222222222222222222222222222222222222222222222c")
212 / 639280595427517331 / Changed
  ("file_2.txt"...22222222222222222222222222222222222222222222222c")
7 / 639280595427517338 / Changed
  ("file_2.txt",
...22222222222222222222222222222222222222222222222c")
7 / 639280595427517345 / Changed
  ("file_2.txt",
...22222222222222222222222222222222222222222222222c")
7 / 639280595427517352 / Changed
  ("file_2.txt",
...22222222222222222222222222222222222222222222222c")
219 / 639280595427517571 / Changed
  ("file_2.txt"...22222222222222222222222222222222222222222222222c")
10 / 639280595427517581 / Changed
  ("file_2.txt",...22222222222222222222222222222222222222222222222c")
7 / 639280595427517588 / Changed
  ("file_2.txt",
...22222222222222222222222222222222222222222222222c")
216 / 639280595427517804 / Changed
  ("file_2.txt"...22222222222222222222222222222222222222222222222c")
8 / 639280595427517812 / Changed
  ("file_2.txt",
...22222222222222222222222222222222222222222222222c")
6 / 639280595427517818 / Changed
  ("file_2.txt",
...22222222222222222222222222222222222222222222222c")
7 / 639280595427517825 / Changed
  ("file_2.txt",
...22222222222222222222222222222222222222222222222c")
214 / 639280595427518039 / Changed
  ("file_2.txt"...22222222222222222222222222222222222222222222222c")
7 / 639280595427518046 / Changed
  ("file_2.txt",
...22222222222222222222222222222222222222222222222c")
7 / 639280595427518053 / Changed
  ("file_2.txt",
...22222222222222222222222222222222222222222222222c")
218 / 639280595427518271 / Changed
  ("file_2.txt"...22222222222222222222222222222222222222222222222c")
8 / 639280595427518279 / Changed
  ("file_2.txt",
...22222222222222222222222222222222222222222222222c")
6 / 639280595427518285 / Changed
  ("file_2.txt",
...22222222222222222222222222222222222222222222222c")
8 / 639280595427518293 / Changed
  ("file_2.txt",
...22222222222222222222222222222222222222222222222c")
323 / 639280595427518616 / Changed
  ("file_2.txt"...22222222222222222222222222222222222222222222222c")
9 / 639280595427518625 / Changed
  ("file_2.txt",
...22222222222222222222222222222222222222222222222c")
7 / 639280595427518632 / Changed
  ("file_2.txt",
...22222222222222222222222222222222222222222222222c")
243 / 639280595427518875 / Changed
  ("file_2.txt"...22222222222222222222222222222222222222222222222c")
7 / 639280595427518882 / Changed
  ("file_2.txt",
...22222222222222222222222222222222222222222222222c")
220 / 639280595427519102 / Changed
  ("file_2.txt"...22222222222222222222222222222222222222222222222c")
7 / 639280595427519109 / Changed
  ("file_2.txt",
...22222222222222222222222222222222222222222222222c")
7 / 639280595427519116 / Changed
  ("file_2.txt",
...22222222222222222222222222222222222222222222222c")
213 / 639280595427519329 / Changed
  ("file_2.txt"...22222222222222222222222222222222222222222222222c")
8 / 639280595427519337 / Changed
  ("file_2.txt",
...22222222222222222222222222222222222222222222222c")
12 / 639280595427519349 / Changed
  ("file_2.txt",...22222222222222222222222222222222222222222222222c")
8 / 639280595427519357 / Changed
  ("file_2.txt",
...22222222222222222222222222222222222222222222222c")
6 / 639280595427519363 / Changed
  ("file_2.txt",
...22222222222222222222222222222222222222222222222c")
212 / 639280595427519575 / Changed
  ("file_2.txt"...22222222222222222222222222222222222222222222222c")
11 / 639280595427519586 / Changed
  ("file_2.txt",...22222222222222222222222222222222222222222222222c")
9 / 639280595427519595 / Changed
  ("file_2.txt",
...22222222222222222222222222222222222222222222222c")
223 / 639280595427519818 / Changed
  ("file_2.txt"...22222222222222222222222222222222222222222222222c")
7 / 639280595427519825 / Changed
  ("file_2.txt",
...22222222222222222222222222222222222222222222222c")
7 / 639280595427519832 / Changed
  ("file_2.txt",
...22222222222222222222222222222222222222222222222c")
209 / 639280595427520041 / Changed
  ("file_2.txt"...22222222222222222222222222222222222222222222222c")
9 / 639280595427520050 / Changed
  ("file_2.txt",
...22222222222222222222222222222222222222222222222c")
210 / 639280595427520260 / Changed
  ("file_2.txt"...22222222222222222222222222222222222222222222222c")
7 / 639280595427520267 / Changed
  ("file_2.txt",
...22222222222222222222222222222222222222222222222c")
7 / 639280595427520274 / Changed
  ("file_2.txt",
...22222222222222222222222222222222222222222222222c")
221 / 639280595427520495 / Changed
  ("file_2.txt"...22222222222222222222222222222222222222222222222c")
8 / 639280595427520503 / Changed
  ("file_2.txt",
...22222222222222222222222222222222222222222222222c")
6 / 639280595427520509 / Changed
  ("file_2.txt",
...22222222222222222222222222222222222222222222222c")
207 / 639280595427520716 / Changed
  ("file_2.txt"...22222222222222222222222222222222222222222222222c")
6 / 639280595427520722 / Changed
  ("file_2.txt",
...22222222222222222222222222222222222222222222222c")
15020956 / 639280595442541678 / Deleted "file_1.txt"
3115 / 639280595442544793 / Deleted "file_2.txt"
[Created ("file1.txt", Some "1a"); Changed ("file1.txt", Some "1a"); Created ("file2.txt", Some "2a");
 Changed ("file2.txt", Some "2a"); Changed ("file1.txt", Some "1b"); Changed ("file2.txt", Some "2b");
 Renamed ("file1.txt", ("file_1.txt", Some "1b")); Renamed ("file2.txt", ("file_2.txt", Some "2b"));
 Changed ("file_1.txt", Some "1c"); Changed ("file_2.txt", Some "2c"); Deleted "file_1.txt"; Deleted "file_2.txt"]

Some ()

testEventsSorted (test)

///- --test

let inline sortEvent event =
    match event with
    | FileSystemChange.Failure _ -> 0
    | FileSystemChange.Created _ -> 1
    | FileSystemChange.Changed _ -> 2
    | FileSystemChange.Renamed (_oldPath, _) -> 3
    | FileSystemChange.Deleted _ -> 4

let inline formatEvents events =
    events
    |> Seq.toList
    |> List.sortBy (snd >> sortEvent)
    |> List.choose (fun (ticks, event) ->
        match event with
        | FileSystemChange.Failure _ ->
            None
        | FileSystemChange.Changed (path, _) ->
            Some (ticks, System.IO.Path.GetFileName path, nameof FileSystemChangeType.Changed)
        | FileSystemChange.Created (path, _) ->
            Some (ticks, System.IO.Path.GetFileName path, nameof FileSystemChangeType.Created)
        | FileSystemChange.Deleted path ->
            Some (ticks, System.IO.Path.GetFileName path, nameof FileSystemChangeType.Deleted)
        | FileSystemChange.Renamed (_oldPath, (path, _)) ->
            Some (ticks, System.IO.Path.GetFileName path, nameof FileSystemChangeType.Renamed)
    )
    |> List.sortBy (fun (_, path, _) -> path)
    |> List.distinctBy (fun (_, path, event) -> path, event)

let inline testEventsSorted
    (watchFn : string -> FSharp.Control.AsyncSeq<int64 * FileSystemChange> * IDisposable)
    write
    =
    let struct (tempDir, tempDisposable) =
        "FileSystem.testEventsSorted"
        |> SpiralCrypto.hash_text
        |> SpiralFileSystem.create_temp_dir'
    let stream, disposable = watchFn tempDir

    let events = System.Collections.Concurrent.ConcurrentBag ()

    let inline iter () =
        stream
        |> FSharp.Control.AsyncSeq.iterAsyncParallel (fun event -> async { events.Add event })

    let run = async {
        let! _ = iter () |> Async.StartChild
        do! Async.Sleep 250
        return! write tempDir
    }

    try
        run
        |> Async.runWithTimeout 5000
        |> _assertEqual (Some ())
    finally
        disposable.Dispose ()
        tempDisposable.Dispose ()

    let events = formatEvents events

    let eventMap =
        events
        |> List.map (fun (ticks, path, event) -> path, (event, ticks))
        |> List.groupBy fst
        |> List.map (fun (path, events) ->
            let event, _ticks =
                events
                |> List.map snd
                |> List.sortByDescending snd
                |> List.head

            path, event
        )
        |> Map.ofList

    let eventList =
        events
        |> List.map (fun (_ticks, path, event) -> path, event)

    eventMap, eventList

create and delete (test)

///- --test

let inline write path = async {
    let n = 3

    for i = 1 to n do
        do! $"{i}" |> SpiralFileSystem.write_all_text_async (path </> $"file{i}.txt")

    for i = 1 to n do
        do! SpiralFileSystem.delete_file_async (path </> $"file{i}.txt") |> Async.Ignore

    do! Async.Sleep 150
}

let inline run () =
    let eventMap, eventList = testEventsSorted (watchDirectory (fun _ -> false)) write

    [
        "file1.txt", nameof FileSystemChangeType.Created
        "file1.txt", nameof FileSystemChangeType.Changed
        "file1.txt", nameof FileSystemChangeType.Deleted

        "file2.txt", nameof FileSystemChangeType.Created
        "file2.txt", nameof FileSystemChangeType.Changed
        "file2.txt", nameof FileSystemChangeType.Deleted

        "file3.txt", nameof FileSystemChangeType.Created
        "file3.txt", nameof FileSystemChangeType.Changed
        "file3.txt", nameof FileSystemChangeType.Deleted
    ]
    |> _sequenceEqual eventList

    [
        "file1.txt", nameof FileSystemChangeType.Deleted
        "file2.txt", nameof FileSystemChangeType.Deleted
        "file3.txt", nameof FileSystemChangeType.Deleted
    ]
    |> Map.ofList
    |> _sequenceEqual eventMap

run
|> retry_fn 3
|> _assertEqual (Some ())
Some ()

02:19:06 d #5 FileSystem.watchWithFilter / Disposing watch stream / filter: FileName, LastWrite
[("file1.txt", "Created"); ("file1.txt", "Changed"); ("file1.txt", "Deleted"); ("file2.txt", "Created");
 ("file2.txt", "Changed"); ("file2.txt", "Deleted"); ("file3.txt", "Created"); ("file3.txt", "Changed");
 ("file3.txt", "Deleted")]

map [("file1.txt", "Deleted"); ("file2.txt", "Deleted"); ("file3.txt", "Deleted")]

Some ()

change (test)

///- --test

let inline write path = async {
    let n = 2

    for i = 1 to n do
        do! $"{i}" |> SpiralFileSystem.write_all_text_async (path </> $"file{i}.txt")

    for i = 1 to n do
        do! "" |> SpiralFileSystem.write_all_text_async (path </> $"file{i}.txt")

    for i = 1 to n do
        do! SpiralFileSystem.delete_file_async (path </> $"file{i}.txt") |> Async.Ignore

    do! Async.Sleep 150
}

let inline run () =
    let eventMap, eventList = testEventsSorted (watchDirectory (fun _ -> false)) write

    [
        "file1.txt", nameof FileSystemChangeType.Created
        "file1.txt", nameof FileSystemChangeType.Changed
        "file1.txt", nameof FileSystemChangeType.Deleted

        "file2.txt", nameof FileSystemChangeType.Created
        "file2.txt", nameof FileSystemChangeType.Changed
        "file2.txt", nameof FileSystemChangeType.Deleted
    ]
    |> _sequenceEqual eventList

    [
        "file1.txt", nameof FileSystemChangeType.Deleted
        "file2.txt", nameof FileSystemChangeType.Deleted
    ]
    |> Map.ofList
    |> _sequenceEqual eventMap

run
|> retry_fn 3
|> _assertEqual (Some ())
Some ()

02:19:07 d #6 FileSystem.watchWithFilter / Disposing watch stream / filter: FileName, LastWrite
[("file1.txt", "Created"); ("file1.txt", "Changed"); ("file1.txt", "Deleted"); ("file2.txt", "Created");
 ("file2.txt", "Changed"); ("file2.txt", "Deleted")]

map [("file1.txt", "Deleted"); ("file2.txt", "Deleted")]

Some ()

rename (test)

///- --test

let inline write path = async {
    let n = 2

    for i = 1 to n do
        do! $"{i}" |> SpiralFileSystem.write_all_text_async (path </> $"file{i}.txt")

    for i = 1 to n do
        do! path </> $"file{i}.txt" |> SpiralFileSystem.move_file_async (path </> $"file_{i}.txt") |> Async.Ignore

    for i = 1 to n do
        do! SpiralFileSystem.delete_file_async (path </> $"file_{i}.txt") |> Async.Ignore

    do! Async.Sleep 150
}

let inline run () =
    let eventMap, eventList = testEventsSorted (watchDirectory (fun _ -> false)) write

    [
        "file1.txt", nameof FileSystemChangeType.Created
        "file1.txt", nameof FileSystemChangeType.Changed
        "file2.txt", nameof FileSystemChangeType.Created
        "file2.txt", nameof FileSystemChangeType.Changed

        "file_1.txt", nameof FileSystemChangeType.Renamed
        "file_1.txt", nameof FileSystemChangeType.Deleted

        "file_2.txt", nameof FileSystemChangeType.Renamed
        "file_2.txt", nameof FileSystemChangeType.Deleted
    ]
    |> _sequenceEqual eventList

    [
        "file1.txt", nameof FileSystemChangeType.Changed
        "file2.txt", nameof FileSystemChangeType.Changed
        "file_1.txt", nameof FileSystemChangeType.Deleted
        "file_2.txt", nameof FileSystemChangeType.Deleted
    ]
    |> Map.ofList
    |> _sequenceEqual eventMap

run
|> retry_fn 3
|> _assertEqual (Some ())
Some ()

02:19:07 d #7 FileSystem.watchWithFilter / Disposing watch stream / filter: FileName, LastWrite
[("file1.txt", "Created"); ("file1.txt", "Changed"); ("file2.txt", "Created"); ("file2.txt", "Changed");
 ("file_1.txt", "Renamed"); ("file_1.txt", "Deleted"); ("file_2.txt", "Renamed"); ("file_2.txt", "Deleted")]

map [("file1.txt", "Changed"); ("file2.txt", "Changed"); ("file_1.txt", "Deleted"); ("file_2.txt", "Deleted")]

Some ()

full (test)

///- --test

let inline write path = async {
    let n = 2

    for i = 1 to n do
        do! $"{i}" |> SpiralFileSystem.write_all_text_async (path </> $"file{i}.txt")

    for i = 1 to n do
        do! "" |> SpiralFileSystem.write_all_text_async (path </> $"file{i}.txt")

    for i = 1 to n do
        do! path </> $"file{i}.txt" |> SpiralFileSystem.move_file_async (path </> $"file_{i}.txt") |> Async.Ignore

    for i = 1 to n do
        do! $"{i}" |> SpiralFileSystem.write_all_text_async (path </> $"file_{i}.txt")

    for i = 1 to n do
        do! SpiralFileSystem.delete_file_async (path </> $"file_{i}.txt") |> Async.Ignore

    do! Async.Sleep 150
}

let inline run () =
    let eventMap, eventList = testEventsSorted (watchDirectory (fun _ -> false)) write

    [
        "file1.txt", nameof FileSystemChangeType.Created
        "file1.txt", nameof FileSystemChangeType.Changed
        "file2.txt", nameof FileSystemChangeType.Created
        "file2.txt", nameof FileSystemChangeType.Changed

        "file_1.txt", nameof FileSystemChangeType.Changed
        "file_1.txt", nameof FileSystemChangeType.Renamed
        "file_1.txt", nameof FileSystemChangeType.Deleted

        "file_2.txt", nameof FileSystemChangeType.Changed
        "file_2.txt", nameof FileSystemChangeType.Renamed
        "file_2.txt", nameof FileSystemChangeType.Deleted
    ]
    |> _sequenceEqual eventList

    [
        "file1.txt", nameof FileSystemChangeType.Changed
        "file2.txt", nameof FileSystemChangeType.Changed
        "file_1.txt", nameof FileSystemChangeType.Deleted
        "file_2.txt", nameof FileSystemChangeType.Deleted
    ]
    |> Map.ofList
    |> _sequenceEqual eventMap

run
|> retry_fn 3
|> _assertEqual (Some ())
Some ()

02:19:08 d #8 FileSystem.watchWithFilter / Disposing watch stream / filter: FileName, LastWrite
[("file1.txt", "Created"); ("file1.txt", "Changed"); ("file2.txt", "Created"); ("file2.txt", "Changed");
 ("file_1.txt", "Changed"); ("file_1.txt", "Renamed"); ("file_1.txt", "Deleted"); ("file_2.txt", "Changed");
 ("file_2.txt", "Renamed"); ("file_2.txt", "Deleted")]

map [("file1.txt", "Changed"); ("file2.txt", "Changed"); ("file_1.txt", "Deleted"); ("file_2.txt", "Deleted")]

Some ()