From aeb607c17c0ff5b7a46e2772166f4f476f910de1 Mon Sep 17 00:00:00 2001 From: amirrezapanahi Date: Fri, 24 Jan 2025 15:01:21 +0000 Subject: [PATCH 1/5] [feat]: Add new helper client types for optimistic update to support frontend development. what changed?: add new helper client types for optimistic update why?: aid in front end development effect?: n/a --- src/SAFE.Client/SAFE.fs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/SAFE.Client/SAFE.fs b/src/SAFE.Client/SAFE.fs index 6fdae2e..e96c346 100644 --- a/src/SAFE.Client/SAFE.fs +++ b/src/SAFE.Client/SAFE.fs @@ -185,4 +185,24 @@ module RemoteData = /// `Loaded x -> Loading x`; /// `NotStarted -> Loading None`; /// `Loading x -> Loading x`; - let startLoading (remote: RemoteData<'T>) = remote.StartLoading \ No newline at end of file + let startLoading (remote: RemoteData<'T>) = remote.StartLoading + +///A type which represents optimistic updates. +type Optimistic<'a> = { + Prev: 'a option + Curr: 'a option +}with + + ///Assign the current value as the now previous value and the passed in value as the new current value + member this.Update value = + { + Curr = Some value + Prev = this.Curr + } + + ///Rollback to the previous value in the case of a failure + member this.Rollback = + { + Curr = this.Prev + Prev = None + } From b4fd825e891e4927dfd2197d550d1a74bd378d2c Mon Sep 17 00:00:00 2001 From: amirrezapanahi Date: Sat, 25 Jan 2025 10:55:19 +0000 Subject: [PATCH 2/5] [feat]: Updated XML docs; changed Curr property to Value for clarity. what changed?: more xml docs and change Curr property to Value for more familiar usage why?: doing x.Curr makes less sense than doing x.Value effect?: n/a --- src/SAFE.Client/SAFE.fs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/SAFE.Client/SAFE.fs b/src/SAFE.Client/SAFE.fs index e96c346..5b31337 100644 --- a/src/SAFE.Client/SAFE.fs +++ b/src/SAFE.Client/SAFE.fs @@ -188,21 +188,23 @@ module RemoteData = let startLoading (remote: RemoteData<'T>) = remote.StartLoading ///A type which represents optimistic updates. -type Optimistic<'a> = { - Prev: 'a option - Curr: 'a option +type Optimistic<'T> = { + /// The previous value, if any + Prev: 'T option + /// The current value, if any + Value: 'T option }with - ///Assign the current value as the now previous value and the passed in value as the new current value + /// Updates the current value, shifting the existing current value to previous. member this.Update value = { - Curr = Some value - Prev = this.Curr + Value = Some value + Prev = this.Value } - ///Rollback to the previous value in the case of a failure - member this.Rollback = + /// Rolls back to the previous value, discarding the current one. + member this.Rollback () = { - Curr = this.Prev + Value = this.Prev Prev = None } From d4386c92b4e2d6ea2bafa709aa8503ea3a063bbe Mon Sep 17 00:00:00 2001 From: amirrezapanahi Date: Fri, 14 Feb 2025 13:07:37 +0000 Subject: [PATCH 3/5] [feat]: Added tests and utility functions for Optimistic helper type based on PR feedback. what changed?: added tests for Optimistic helper type; Also added utility functions for Optimistic helper type why?: from pr feedback effect?: better tested library --- src/SAFE.Client/SAFE.fs | 42 +++++++++++++++++++ test/SAFE.Client.Tests/Program.fs | 69 ++++++++++++++++++++++++++++++- 2 files changed, 110 insertions(+), 1 deletion(-) diff --git a/src/SAFE.Client/SAFE.fs b/src/SAFE.Client/SAFE.fs index 5b31337..9baad7f 100644 --- a/src/SAFE.Client/SAFE.fs +++ b/src/SAFE.Client/SAFE.fs @@ -208,3 +208,45 @@ type Optimistic<'T> = { Value = this.Prev Prev = None } + + /// Maps the underlying optimistic value, when it exists, into another shape. + member this.Map (f: 'T -> 'U) = + { + Value = Option.map f this.Value + Prev = Option.map f this.Prev + } + + /// Binds both current and previous values using the provided function + member this.Bind (f: 'T -> Optimistic<'U>) = + match this.Value with + | Some v -> f v // Just use the result directly + | None -> { Value = None; Prev = None } + + /// Returns the current value as an option + member this.AsOption = this.Value + +/// Module containing functions for working with Optimistic type +module Optimistic = + /// Creates a new Optimistic value with no history + let create value = { Value = Some value; Prev = None } + + /// Creates an empty Optimistic value + let empty = { Value = None; Prev = None } + + /// Updates the current value, shifting existing value to previous + let update value (optimistic: Optimistic<'T>) = optimistic.Update value + + /// Rolls back to the previous value + let rollback (optimistic: Optimistic<'T>) = optimistic.Rollback() + + /// Maps both current and previous values + let map f (optimistic: Optimistic<'T>) = optimistic.Map f + + /// Binds both current and previous values + let bind f (optimistic: Optimistic<'T>) = optimistic.Bind f + + /// Returns the current value as an option + let asOption (optimistic: Optimistic<'T>) = optimistic.AsOption + + /// Returns the previous value as an option + let asPrevOption optimistic = optimistic.Prev diff --git a/test/SAFE.Client.Tests/Program.fs b/test/SAFE.Client.Tests/Program.fs index 3d04034..54b47ce 100644 --- a/test/SAFE.Client.Tests/Program.fs +++ b/test/SAFE.Client.Tests/Program.fs @@ -125,5 +125,72 @@ let remoteData = | RemoteDataCase.Loaded -> Loading (Some true)) ] +let optimistic = + testList "Optimistic" [ + testList "create" [ + testCase "creates new value with no history" <| fun _ -> + let opt = Optimistic.create 42 + Expect.equal opt.Value (Some 42) "Current value should be set" + Expect.equal opt.Prev None "Previous value should be None" + ] + + testList "empty" [ + testCase "creates empty optimistic value" <| fun _ -> + let opt = Optimistic.empty + Expect.equal opt.Value None "Current value should be None" + Expect.equal opt.Prev None "Previous value should be None" + ] + + testList "update" [ + testCase "updates value and shifts previous" <| fun _ -> + let opt = Optimistic.create 42 + let updated = opt.Update 84 + Expect.equal updated.Value (Some 84) "Current value should be updated" + Expect.equal updated.Prev (Some 42) "Previous value should be old current" + ] + + testList "rollback" [ + testCase "rolls back to previous value" <| fun _ -> + let opt = Optimistic.create 42 |> Optimistic.update 84 + let rolled = opt.Rollback() + Expect.equal rolled.Value (Some 42) "Current value should be previous" + Expect.equal rolled.Prev None "Previous value should be cleared" + ] + + testList "map" [ + testCase "maps both values" <| fun _ -> + let opt = { Value = Some 42; Prev = Some 21 } + let mapped = opt.Map string + Expect.equal mapped.Value (Some "42") "Current value should be mapped" + Expect.equal mapped.Prev (Some "21") "Previous value should be mapped" + ] + + testList "bind" [ + testCase "binds value with history" <| fun _ -> + let opt = { Value = Some 42; Prev = Some 21 } + let bound = opt.Bind (fun x -> { Value = Some (string x); Prev = None}) + Expect.equal bound.Value (Some "42") "Current value should be bound" + Expect.equal bound.Prev (None) "Previous value should be bound" + ] + + testList "asOption" [ + testCase "returns current value as option" <| fun _ -> + let opt = Optimistic.create 42 + Expect.equal (Optimistic.asOption opt) (Some 42) "Should return current value" + ] + + testList "asPrevOption" [ + testCase "returns previous value as option" <| fun _ -> + let opt = Optimistic.create 42 |> Optimistic.update 84 + Expect.equal (Optimistic.asPrevOption opt) (Some 42) "Should return previous value" + ] + ] + +let allTests = + testList "All Tests" [ + remoteData + optimistic + ] + [] -let main _ = Mocha.runTests remoteData \ No newline at end of file +let main _ = Mocha.runTests allTests From 56b5a3ba4dfc9ad4928e4d00e02abc6ebe4c7c89 Mon Sep 17 00:00:00 2001 From: amirrezapanahi Date: Fri, 14 Feb 2025 17:45:04 +0000 Subject: [PATCH 4/5] [fix]: Opted for DU over record type to prevent illegal states. what changed?: opt in for a DU instead of a record type for Optimistic helper type why?: make illegal states unrepresentable effect?: n/a --- src/SAFE.Client/SAFE.fs | 79 ++++++++++------------- test/SAFE.Client.Tests/Program.fs | 101 +++++++++++++++++++++--------- 2 files changed, 102 insertions(+), 78 deletions(-) diff --git a/src/SAFE.Client/SAFE.fs b/src/SAFE.Client/SAFE.fs index 9baad7f..bb2e85b 100644 --- a/src/SAFE.Client/SAFE.fs +++ b/src/SAFE.Client/SAFE.fs @@ -188,50 +188,44 @@ module RemoteData = let startLoading (remote: RemoteData<'T>) = remote.StartLoading ///A type which represents optimistic updates. -type Optimistic<'T> = { - /// The previous value, if any - Prev: 'T option - /// The current value, if any - Value: 'T option -}with - - /// Updates the current value, shifting the existing current value to previous. - member this.Update value = - { - Value = Some value - Prev = this.Value - } - - /// Rolls back to the previous value, discarding the current one. - member this.Rollback () = - { - Value = this.Prev - Prev = None - } - - /// Maps the underlying optimistic value, when it exists, into another shape. - member this.Map (f: 'T -> 'U) = - { - Value = Option.map f this.Value - Prev = Option.map f this.Prev - } - - /// Binds both current and previous values using the provided function - member this.Bind (f: 'T -> Optimistic<'U>) = - match this.Value with - | Some v -> f v // Just use the result directly - | None -> { Value = None; Prev = None } - - /// Returns the current value as an option - member this.AsOption = this.Value +type Optimistic<'T> = + | NonExistant + | Exists of value:'T * prev:'T option + with + /// Retrieves the current value + member this.Value = + match this with + | NonExistant -> None + | Exists (v, pv) -> Some v + + /// Updates the current value, shifting the existing current value to previous. + member this.Update (value: 'T) = + match this with + | NonExistant -> NonExistant + | Exists (v, pv) -> Exists (value, Some v) + + /// Rolls back to the previous value, discarding the current one. + member this.Rollback () = + match this with + | NonExistant -> NonExistant + | Exists (_, Some pv) -> Exists (pv , None) + | Exists (_, None) -> NonExistant + + /// Maps the underlying optimistic value, when it exists, into another shape. + member this.Map (f: 'T -> 'U) = + match this with + | NonExistant -> NonExistant + | Exists (v, pv) -> Exists (f v, pv |> Option.map f) /// Module containing functions for working with Optimistic type module Optimistic = /// Creates a new Optimistic value with no history - let create value = { Value = Some value; Prev = None } + let create value = + Exists (value, None) /// Creates an empty Optimistic value - let empty = { Value = None; Prev = None } + let empty = + NonExistant /// Updates the current value, shifting existing value to previous let update value (optimistic: Optimistic<'T>) = optimistic.Update value @@ -241,12 +235,3 @@ module Optimistic = /// Maps both current and previous values let map f (optimistic: Optimistic<'T>) = optimistic.Map f - - /// Binds both current and previous values - let bind f (optimistic: Optimistic<'T>) = optimistic.Bind f - - /// Returns the current value as an option - let asOption (optimistic: Optimistic<'T>) = optimistic.AsOption - - /// Returns the previous value as an option - let asPrevOption optimistic = optimistic.Prev diff --git a/test/SAFE.Client.Tests/Program.fs b/test/SAFE.Client.Tests/Program.fs index 54b47ce..651f63c 100644 --- a/test/SAFE.Client.Tests/Program.fs +++ b/test/SAFE.Client.Tests/Program.fs @@ -124,65 +124,104 @@ let remoteData = | RemoteDataCase.LoadingPopulated -> Loading (Some true) | RemoteDataCase.Loaded -> Loading (Some true)) ] - let optimistic = testList "Optimistic" [ testList "create" [ testCase "creates new value with no history" <| fun _ -> let opt = Optimistic.create 42 - Expect.equal opt.Value (Some 42) "Current value should be set" - Expect.equal opt.Prev None "Previous value should be None" + match opt with + | Exists (value, prev) -> + Expect.equal value 42 "Current value should be set" + Expect.equal prev None "Previous value should be None" + | NonExistant -> + failtest "Should not be NonExistant" ] testList "empty" [ testCase "creates empty optimistic value" <| fun _ -> let opt = Optimistic.empty - Expect.equal opt.Value None "Current value should be None" - Expect.equal opt.Prev None "Previous value should be None" + Expect.equal opt NonExistant "Should be NonExistant" + ] + + testList "Value property" [ + testCase "returns Some for existing value" <| fun _ -> + let opt = Optimistic.create 42 + Expect.equal opt.Value (Some 42) "Should return Some with current value" + + testCase "returns None for NonExistant" <| fun _ -> + let opt = Optimistic.empty + Expect.equal opt.Value None "Should return None for NonExistant" ] testList "update" [ testCase "updates value and shifts previous" <| fun _ -> let opt = Optimistic.create 42 let updated = opt.Update 84 - Expect.equal updated.Value (Some 84) "Current value should be updated" - Expect.equal updated.Prev (Some 42) "Previous value should be old current" + match updated with + | Exists (value, prev) -> + Expect.equal value 84 "Current value should be updated" + Expect.equal prev (Some 42) "Previous value should be old current" + | NonExistant -> + failtest "Should not be NonExistant" + + testCase "update on NonExistant remains NonExistant" <| fun _ -> + let opt = Optimistic.empty + let updated = opt.Update 42 + Expect.equal updated NonExistant "Should remain NonExistant" ] testList "rollback" [ testCase "rolls back to previous value" <| fun _ -> - let opt = Optimistic.create 42 |> Optimistic.update 84 + let opt = Optimistic.create 42 |> fun o -> o.Update 84 + let rolled = opt.Rollback() + match rolled with + | Exists (value, prev) -> + Expect.equal value 42 "Current value should be previous" + Expect.equal prev None "Previous value should be None" + | NonExistant -> + failtest "Should not be NonExistant" + + testCase "rollback on NonExistant remains NonExistant" <| fun _ -> + let opt = Optimistic.empty let rolled = opt.Rollback() - Expect.equal rolled.Value (Some 42) "Current value should be previous" - Expect.equal rolled.Prev None "Previous value should be cleared" + Expect.equal rolled NonExistant "Should remain NonExistant" ] testList "map" [ - testCase "maps both values" <| fun _ -> - let opt = { Value = Some 42; Prev = Some 21 } + testCase "maps both current and previous values" <| fun _ -> + let opt = Optimistic.create 42 |> fun o -> o.Update 84 let mapped = opt.Map string - Expect.equal mapped.Value (Some "42") "Current value should be mapped" - Expect.equal mapped.Prev (Some "21") "Previous value should be mapped" - ] - - testList "bind" [ - testCase "binds value with history" <| fun _ -> - let opt = { Value = Some 42; Prev = Some 21 } - let bound = opt.Bind (fun x -> { Value = Some (string x); Prev = None}) - Expect.equal bound.Value (Some "42") "Current value should be bound" - Expect.equal bound.Prev (None) "Previous value should be bound" + match mapped with + | Exists (value, prev) -> + Expect.equal value "84" "Current value should be mapped" + Expect.equal prev (Some "42") "Previous value should be mapped" + | NonExistant -> + failtest "Should not be NonExistant" + + testCase "map on NonExistant remains NonExistant" <| fun _ -> + let opt = Optimistic.empty + let mapped = opt.Map string + Expect.equal mapped NonExistant "Should remain NonExistant" ] - testList "asOption" [ - testCase "returns current value as option" <| fun _ -> + testList "module functions" [ + testCase "update function matches member" <| fun _ -> let opt = Optimistic.create 42 - Expect.equal (Optimistic.asOption opt) (Some 42) "Should return current value" - ] - - testList "asPrevOption" [ - testCase "returns previous value as option" <| fun _ -> - let opt = Optimistic.create 42 |> Optimistic.update 84 - Expect.equal (Optimistic.asPrevOption opt) (Some 42) "Should return previous value" + let memberUpdate = opt.Update 84 + let moduleUpdate = Optimistic.update 84 opt + Expect.equal moduleUpdate memberUpdate "Module update should match member update" + + testCase "rollback function matches member" <| fun _ -> + let opt = Optimistic.create 42 |> fun o -> o.Update 84 + let memberRollback = opt.Rollback() + let moduleRollback = Optimistic.rollback opt + Expect.equal moduleRollback memberRollback "Module rollback should match member rollback" + + testCase "map function matches member" <| fun _ -> + let opt = Optimistic.create 42 + let memberMap = opt.Map string + let moduleMap = Optimistic.map string opt + Expect.equal moduleMap memberMap "Module map should match member map" ] ] From ad9db93db739eb1ee404adfa6ab945b2d8509973 Mon Sep 17 00:00:00 2001 From: Amir <78684494+arpxspace@users.noreply.github.com> Date: Fri, 28 Feb 2025 13:07:11 +0000 Subject: [PATCH 5/5] Update src/SAFE.Client/SAFE.fs Co-authored-by: Matt Gallagher <46973220+mattgallagher92@users.noreply.github.com> --- src/SAFE.Client/SAFE.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SAFE.Client/SAFE.fs b/src/SAFE.Client/SAFE.fs index bb2e85b..80d54f1 100644 --- a/src/SAFE.Client/SAFE.fs +++ b/src/SAFE.Client/SAFE.fs @@ -189,7 +189,7 @@ module RemoteData = ///A type which represents optimistic updates. type Optimistic<'T> = - | NonExistant + | NonExistent | Exists of value:'T * prev:'T option with /// Retrieves the current value