From 6a12a711a1a5931d0c357016b79920a846bddf28 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Fri, 4 Mar 2022 16:01:19 +0000 Subject: [PATCH 1/5] Rename TransactAsync -> Transact --- CHANGELOG.md | 4 ++++ DOCUMENTATION.md | 13 +++++++------ samples/Store/Domain/Cart.fs | 8 ++++---- samples/Store/Domain/SavedForLater.fs | 2 +- src/Equinox/Decider.fs | 6 +++--- 5 files changed, 19 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 384f2c9cd..e5e68fea3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,13 @@ The `Unreleased` section name is replaced by the expected version of next releas ## [Unreleased] ### Added + +- `Equinox`: `Decider.Transact(interpret : 'state -> Async<'event list>)` [#308](https://github.com/jet/equinox/pull/308) + ### Changed - `eqx`/`Equinox.Tool`: Flip `-P` option to opt _in_ to pretty printing [#313](https://github.com/jet/equinox/pull/313) +- `Equinox`: rename `Decider.TransactAsync` to `Transact` [#308](https://github.com/jet/equinox/pull/308) - `CosmosStore`: Require `Microsoft.Azure.Cosmos` v `3.0.25` [#310](https://github.com/jet/equinox/pull/310) - `CosmosStore`: Switch to natively using `JsonElement` event bodies [#305](https://github.com/jet/equinox/pull/305) :pray: [@ylibrach](https://github.com/ylibrach) - `CosmosStore`: Switch to natively using `System.Text.Json` for serialization of all `Microsoft.Azure.Cosmos` round-trips [#305](https://github.com/jet/equinox/pull/305) :pray: [@ylibrach](https://github.com/ylibrach) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 5d0eb35cb..f744f138d 100755 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -1326,7 +1326,7 @@ type Service internal (resolve : CartId -> Equinox.Decider = let decider = resolve (cartId,if optimistic then Some Equinox.AllowStale else None) - decider.TransactAsync(fun state -> async { + decider.Transact(fun state -> async { match prepare with None -> () | Some prep -> do! prep return interpretMany Fold.fold (Seq.map interpret commands) state }) ``` @@ -1368,7 +1368,7 @@ type Accumulator<'event, 'state>(fold : 'state -> 'event seq -> 'state, originSt interpret x.State |> accumulated.AddRange /// Invoke an Async decision function, gathering the events (if any) that /// it decides are necessary into the `Accumulated` sequence - member x.TransactAsync(interpret : 'state -> Async<'event list>) : Async = async { + member x.Transact(interpret : 'state -> Async<'event list>) : Async = async { let! events = interpret x.State accumulated.AddRange events } /// Invoke a decision function, while also propagating a result yielded as @@ -1379,21 +1379,22 @@ type Accumulator<'event, 'state>(fold : 'state -> 'event seq -> 'state, originSt result /// Invoke a decision function, while also propagating a result yielded as /// the fst of an (result, events) pair - member x.TransactAsync(decide : 'state -> Async<'result * 'event list>) : Async<'result> = async { + member x.Transact(decide : 'state -> Async<'result * 'event list>) : Async<'result> = async { let! result, newEvents = decide x.State accumulated.AddRange newEvents return result } type Service ... = member _.Run(cartId, optimistic, commands : Command seq, ?prepare) : Async = - let decider = resolve (cartId,if optimistic then Some Equinox.AllowStale else None) - decider.TransactAsync(fun state -> async { + let decider = resolve cartId + let opt = if optimistic then Some Equinox.AllowStale else Equinox.RequireLoad + decider.Transact(fun state -> async { match prepare with None -> () | Some prep -> do! prep let acc = Accumulator(Fold.fold, state) for cmd in commands do acc.Transact(interpret cmd) return acc.State, acc.Accumulated - }) + }, opt) ``` # Equinox Architectural Overview diff --git a/samples/Store/Domain/Cart.fs b/samples/Store/Domain/Cart.fs index 284ea1ef3..4628b77c3 100644 --- a/samples/Store/Domain/Cart.fs +++ b/samples/Store/Domain/Cart.fs @@ -112,7 +112,7 @@ type Accumulator<'event, 'state>(fold : 'state -> 'event seq -> 'state, originSt member x.Transact(interpret : 'state -> 'event list) : unit = interpret x.State |> accumulated.AddRange /// Invoke an Async decision function, gathering the events (if any) that it decides are necessary into the `Accumulated` sequence - member x.TransactAsync(interpret : 'state -> Async<'event list>) : Async = async { + member x.Transact(interpret : 'state -> Async<'event list>) : Async = async { let! events = interpret x.State accumulated.AddRange events } /// Invoke a decision function, while also propagating a result yielded as the fst of an (result, events) pair @@ -121,7 +121,7 @@ type Accumulator<'event, 'state>(fold : 'state -> 'event seq -> 'state, originSt accumulated.AddRange newEvents result /// Invoke a decision function, while also propagating a result yielded as the fst of an (result, events) pair - member x.TransactAsync(decide : 'state -> Async<'result * 'event list>) : Async<'result> = async { + member x.Transact(decide : 'state -> Async<'result * 'event list>) : Async<'result> = async { let! result, newEvents = decide x.State accumulated.AddRange newEvents return result } @@ -138,13 +138,13 @@ type Service internal (resolve : CartId * Equinox.ResolveOption option -> Equino member _.Run(cartId, optimistic, commands : Command seq, ?prepare) : Async = let decider = resolve (cartId,if optimistic then Some Equinox.AllowStale else None) - decider.TransactAsync(fun state -> async { + decider.Transact(fun state -> async { match prepare with None -> () | Some prep -> do! prep #if ACCUMULATOR let acc = Accumulator(Fold.fold, state) for cmd in commands do acc.Transact(interpret cmd) - return acc.State, acc.Accumulated }) + return acc.State, acc.Accumulated } #else return interpretMany Fold.fold (Seq.map interpret commands) state }) #endif diff --git a/samples/Store/Domain/SavedForLater.fs b/samples/Store/Domain/SavedForLater.fs index 92be8c047..dd708ef92 100644 --- a/samples/Store/Domain/SavedForLater.fs +++ b/samples/Store/Domain/SavedForLater.fs @@ -119,7 +119,7 @@ type Service internal (resolve : ClientId -> Equinox.Deciderbool) -> Async)) : Async = let decider = resolve clientId - decider.TransactAsync(fun (state : Fold.State) -> async { + decider.Transact(fun (state : Fold.State) -> async { let contents = seq { for item in state -> item.skuId } |> set let! cmd = resolveCommand contents.Contains let _, events = decide maxSavedItems cmd state diff --git a/src/Equinox/Decider.fs b/src/Equinox/Decider.fs index 2295e0b5f..0918e0387 100755 --- a/src/Equinox/Decider.fs +++ b/src/Equinox/Decider.fs @@ -45,14 +45,14 @@ type Decider<'event, 'state> /// 1a. (if events yielded) Attempt to sync the yielded events events to the stream /// 1b. Tries up to maxAttempts times in the case of a conflict, throwing MaxResyncsExhaustedException to signal failure. /// 2. Yield result - member _.TransactAsync(decide : 'state -> Async<'result * 'event list>) : Async<'result> = + member _.Transact(decide : 'state -> Async<'result * 'event list>) : Async<'result> = transact (fun context -> decide context.State) (fun result _context -> result) /// 0. Invoke the supplied _Async_ decide function with the present state (including extended context), holding the 'result /// 1a. (if events yielded) Attempt to sync the yielded events events to the stream /// 1b. Tries up to maxAttempts times in the case of a conflict, throwing MaxResyncsExhaustedException to signal failure. - /// 2. Uses mapResult to render the final outcome from the 'result and/or the final ISyncContext - /// 3. Yields the outcome + /// 2. Uses mapResult to render the final 'view from the 'result and/or the final ISyncContext + /// 3. Yields the 'view member _.TransactEx(decide : ISyncContext<'state> -> Async<'result * 'event list>, mapResult : 'result -> ISyncContext<'state> -> 'view) : Async<'view> = transact decide mapResult From ffdb52eabf944fb4d3abc3b71971fe49b006debd Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Fri, 4 Mar 2022 16:18:49 +0000 Subject: [PATCH 2/5] Fix changelog --- CHANGELOG.md | 4 ++-- samples/Store/Domain/Cart.fs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5e68fea3..33646993d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,12 +10,12 @@ The `Unreleased` section name is replaced by the expected version of next releas ### Added -- `Equinox`: `Decider.Transact(interpret : 'state -> Async<'event list>)` [#308](https://github.com/jet/equinox/pull/308) +- `Equinox`: `Decider.Transact(interpret : 'state -> Async<'event list>)` [#314](https://github.com/jet/equinox/pull/314) ### Changed - `eqx`/`Equinox.Tool`: Flip `-P` option to opt _in_ to pretty printing [#313](https://github.com/jet/equinox/pull/313) -- `Equinox`: rename `Decider.TransactAsync` to `Transact` [#308](https://github.com/jet/equinox/pull/308) +- `Equinox`: rename `Decider.TransactAsync` to `Transact` [#314](https://github.com/jet/equinox/pull/314) - `CosmosStore`: Require `Microsoft.Azure.Cosmos` v `3.0.25` [#310](https://github.com/jet/equinox/pull/310) - `CosmosStore`: Switch to natively using `JsonElement` event bodies [#305](https://github.com/jet/equinox/pull/305) :pray: [@ylibrach](https://github.com/ylibrach) - `CosmosStore`: Switch to natively using `System.Text.Json` for serialization of all `Microsoft.Azure.Cosmos` round-trips [#305](https://github.com/jet/equinox/pull/305) :pray: [@ylibrach](https://github.com/ylibrach) diff --git a/samples/Store/Domain/Cart.fs b/samples/Store/Domain/Cart.fs index 4628b77c3..4fc7da256 100644 --- a/samples/Store/Domain/Cart.fs +++ b/samples/Store/Domain/Cart.fs @@ -144,7 +144,7 @@ type Service internal (resolve : CartId * Equinox.ResolveOption option -> Equino let acc = Accumulator(Fold.fold, state) for cmd in commands do acc.Transact(interpret cmd) - return acc.State, acc.Accumulated } + return acc.State, acc.Accumulated }) #else return interpretMany Fold.fold (Seq.map interpret commands) state }) #endif From b69c871c738cd85c24d92c4e09ebf3e13b11ea60 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Fri, 4 Mar 2022 16:58:44 +0000 Subject: [PATCH 3/5] Fixes --- DOCUMENTATION.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index f744f138d..d5ade1395 100755 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -1386,15 +1386,14 @@ type Accumulator<'event, 'state>(fold : 'state -> 'event seq -> 'state, originSt type Service ... = member _.Run(cartId, optimistic, commands : Command seq, ?prepare) : Async = - let decider = resolve cartId - let opt = if optimistic then Some Equinox.AllowStale else Equinox.RequireLoad + let decider = resolve (cartId,if optimistic then Some Equinox.AllowStale else None) decider.Transact(fun state -> async { match prepare with None -> () | Some prep -> do! prep let acc = Accumulator(Fold.fold, state) for cmd in commands do acc.Transact(interpret cmd) return acc.State, acc.Accumulated - }, opt) + }) ``` # Equinox Architectural Overview From 703ccb353b02fe59bc73b4825f05dfbcb2096efd Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Fri, 4 Mar 2022 17:33:47 +0000 Subject: [PATCH 4/5] Remove misleading statement This is an anti-pattern, if anything --- DOCUMENTATION.md | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index d5ade1395..ae7040ebb 100755 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -697,7 +697,7 @@ Equinox’s Command Handling consists of < 200 lines including interfaces and comments in https://github.com/jet/equinox/tree/master/src/Equinox - the elements you'll touch in a normal application are: -- [`module Flow`](https://github.com/jet/equinox/blob/master/src/Equinox/Flow.fs#L34) - +- [`module Flow`](https://github.com/jet/equinox/blob/master/src/Equinox/Core.fs#L34) - internal implementation of Optimistic Concurrency Control / retry loop used by `Decider`. It's recommended to at least scan this file as it defines the Transaction semantics that are central to Equinox and the overall `Decider` concept. @@ -854,13 +854,6 @@ let create resolve = Service(resolve) ``` -The `Decider`-related functions in a given Aggregate establish the access -patterns used across when Service methods access streams (see below). Typically -these are relatively straightforward calls forwarding to a `Equinox.Decider` -equivalent (see [`src/Equinox/Decider.fs`](src/Equinox/Decider.fs)), which in -turn use the Optimistic Concurrency retry-loop in -[`src/Equinox/Flow.fs`](src/Equinox/Flow.fs). - `Read` above will do a roundtrip to the Store in order to fetch the most recent state (in `AllowStale` mode, the store roundtrip can be optimized out by reading through the cache). This Synchronous Read can be used to From 89d54d12e232fb7bd494df601c026f609af49926 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Fri, 4 Mar 2022 17:36:26 +0000 Subject: [PATCH 5/5] Rename Flow.fs -> Core.fs --- README.md | 4 ++-- src/Equinox/{Flow.fs => Core.fs} | 0 src/Equinox/Equinox.fsproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename src/Equinox/{Flow.fs => Core.fs} (100%) diff --git a/README.md b/README.md index 531aaa925..b9890ff37 100644 --- a/README.md +++ b/README.md @@ -745,7 +745,7 @@ Ouch, not looking forward to reading all that logic :frown: ? [Have a read, it's > I'm having some trouble understanding how Equinox+ESDB handles "expected version". Most of the examples use `Equinox.Decider.Transact` which is storage agnostic and doesn't offer any obvious concurrency checking. In `Equinox.EventStore.Context`, there's a `Sync` and `TrySync` that take a `Token` which holds a `streamVersion`. Should I be be using that instead of `Transact`? -The bulk of the implementation is in [`Equinox/Flow.fs`](https://github.com/jet/equinox/blob/master/src/Equinox/Flow.fs) +The bulk of the implementation is in [`Equinox/Decider.fs`](https://github.com/jet/equinox/blob/master/src/Equinox/Decider.fs) There are [sequence diagrams in Documentation MD](https://github.com/jet/equinox/blob/master/DOCUMENTATION.md#code-diagrams-for-equinoxeventstore--equinoxsqlstreamstore) but I'll summarize here: @@ -763,7 +763,7 @@ b. for CosmosDB, the `expectedVersion` can actually be an `expectedEtag` - this (The second usage did not necessitate an interface change - i.e. the Token mechanism was introduced to handle the first case, and just happened to fit the second case) -> Alternatively, I'm seeing in `proReactor` that there's a decide that does version checking. Is this recommended? [code](https://github.com/jet/dotnet-templates/blob/3329510601450ab77bcc40df7a407c5f0e3c8464/propulsion-reactor/TodoSummary.fs#L30-L52) +> Alternatively, I'm seeing in `proReactor` that there's a `decide` that does version checking. Is this recommended? [code](https://github.com/jet/dotnet-templates/blob/3329510601450ab77bcc40df7a407c5f0e3c8464/propulsion-reactor/TodoSummary.fs#L30-L52) If you need to know the version in your actual handler, QueryEx and other such APIs alongside Transact expose it (e.g. if you want to include a version to accompany a directly rendered piece of data). (Note that doing this - including a version in a rendering of something should not be a goto strategy - i.e. having APIs that pass around expectedVersion is not a good idea in general) diff --git a/src/Equinox/Flow.fs b/src/Equinox/Core.fs similarity index 100% rename from src/Equinox/Flow.fs rename to src/Equinox/Core.fs diff --git a/src/Equinox/Equinox.fsproj b/src/Equinox/Equinox.fsproj index 3b65b78f4..d524656ea 100644 --- a/src/Equinox/Equinox.fsproj +++ b/src/Equinox/Equinox.fsproj @@ -9,7 +9,7 @@ - +