diff --git a/CHANGELOG.md b/CHANGELOG.md index 384f2c9cd..33646993d 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>)` [#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` [#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/DOCUMENTATION.md b/DOCUMENTATION.md index 5d0eb35cb..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 @@ -1326,7 +1319,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 +1361,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,7 +1372,7 @@ 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 } @@ -1387,7 +1380,7 @@ 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,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 let acc = Accumulator(Fold.fold, state) for cmd in commands do 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/samples/Store/Domain/Cart.fs b/samples/Store/Domain/Cart.fs index 284ea1ef3..4fc7da256 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,7 +138,7 @@ 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) 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/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/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 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 @@ - +