Skip to content

Commit 84a6f95

Browse files
committed
Release 4.0.0-rc.14
Target FsCodec 3.0.0-rc.13 Tweaks
1 parent 832d5e9 commit 84a6f95

File tree

12 files changed

+127
-88
lines changed

12 files changed

+127
-88
lines changed

DOCUMENTATION.md

+78-39
Original file line numberDiff line numberDiff line change
@@ -315,19 +315,22 @@ highly recommended to use the following canonical skeleton layout:
315315
```fsharp
316316
module Aggregate
317317
318-
module Stream =
318+
module private Stream =
319319
let [<Literal>] Category = "category"
320320
let id = FsCodec.StreamId.gen Id.toString
321321
322-
(* Optionally, Helpers/Types *)
322+
(* Optionally (rarely) Helpers/Types *)
323323
324324
// NOTE - these types and the union case names reflect the actual storage
325325
// formats and hence need to be versioned with care
326326
[<RequiredQualifiedAccess>]
327327
module Events =
328328
329+
type Snapshotted = ... // NOTE: event body types past tense with same name as case
330+
329331
type Event =
330332
| ...
333+
| [<DataMember(Name = "Snapshotted">] Snapshotted of Snapshotted // NOTE: Snapshotted event explictly named to remind one can/should version it
331334
// optionally: `encode`, `tryDecode` (only if you're doing manual decoding)
332335
let codec = FsCodec ... Codec.Create<Event>(...)
333336
```
@@ -339,6 +342,44 @@ Some notes about the intents being satisfied here:
339342
sibling code in adjacent `module`s should not be using them directly (in
340343
general interaction should be via the `type Service`)
341344

345+
✅ DO keep `module Stream` visibility `private`, present via `module Reactions`
346+
347+
If the composition of stream names is relevant for Reactions processing, expose relevant helpers in a `module Reactions` facade.
348+
For instance, rather than having external reaction logic refer to `Aggregate.Stream.Category`, expose a facade such as:
349+
350+
```fsharp
351+
module Reactions =
352+
353+
let streamName = Stream.name
354+
let deletionNamePrefix tenantIdStr = $"%s{Stream.Category}-%s{tenantIdStr}"
355+
```
356+
357+
✅ DO use tupled arguments for the `Stream.id` function
358+
359+
All the inputs of which the `StreamId` is composed should be represented as one argument:
360+
361+
```fsharp
362+
✅ let id struct (tenantId, clientId) = FsCodec.StreamId.gen2 TenantId.toString ClientId.toString (tenantId, clientId)
363+
✅ let id = FsCodec.StreamId.gen2 TenantId.toString ClientId.toString
364+
❌ let id tenantId clientId = FsCodec.StreamId.gen2 TenantId.toString ClientId.toString (tenantId, clientId)
365+
```
366+
367+
✅ DO keep `module Stream` visibility `private`, present via `module Reactions`
368+
369+
If the composition of stream names is relevant for Reactions processing, expose relevant helpers in a `module Reactions` facade.
370+
For instance, rather than having external reaction logic refer to `Aggregate.Stream.Category`, expose a facade such as:
371+
372+
```fsharp
373+
module Reactions =
374+
375+
let streamName = Stream.name
376+
let deletionNamePrefix tenantIdStr = $"%s{Stream.Category}-%s{tenantIdStr}"
377+
let [<return: Struct>] (|For|_|) = Stream.tryDecode
378+
let [<return: Struct>] (|Decode|_|) = function
379+
| struct (For id, _) & Streams.Decode dec events -> ValueSome struct (id, events)
380+
| _ -> ValueNone
381+
```
382+
342383
```fsharp
343384
module Fold =
344385
@@ -411,17 +452,18 @@ either within the `module Aggregate`, or somewhere outside closer to the
411452

412453
```fsharp
413454
let defaultCacheDuration = System.TimeSpan.FromMinutes 20.
414-
let cacheStrategy = Equinox.CosmosStore.CachingStrategy.SlidingWindow (cache, defaultCacheDuration)
455+
let cacheStrategy cache = Equinox.CosmosStore.CachingStrategy.SlidingWindow (cache, defaultCacheDuration)
415456
416457
module EventStore =
417-
let accessStrategy = Equinox.EventStoreDb.AccessStrategy.RollingSnapshots (Fold.isOrigin, Fold.snapshot)
458+
let accessStrategy = Equinox.EventStoreDb.AccessStrategy.RollingSnapshots Fold.Snapshot.config
418459
let category (context, cache) =
419-
Equinox.EventStore.EventStoreCategory(context, Stream.Category, Events.codec, Fold.fold, Fold.initial, cacheStrategy, accessStrategy)
460+
Equinox.EventStore.EventStoreCategory(context, Stream.Category, Events.codec, Fold.fold, Fold.initial, accessStrategy, cacheStrategy cache)
420461
421462
module Cosmos =
422463
let accessStrategy = Equinox.CosmosStore.AccessStrategy.Snapshot Fold.Snapshot.config
423464
let category (context, cache) =
424-
Equinox.CosmosStore.CosmosStoreCategory(context, Stream.Category, Events.codec, Fold.fold, Fold.initial, accessStrategy, cacheStrategy)
465+
Equinox.CosmosStore.CosmosStoreCategory(context, Stream.Category, Events.codec, Fold.fold, Fold.initial, accessStrategy, cacheStrategy cache)
466+
```
425467

426468
### `MemoryStore` Storage Binding Module
427469

@@ -432,7 +474,7 @@ can use the `MemoryStore` in the context of your tests:
432474
```fsharp
433475
module MemoryStore =
434476
let category (store: Equinox.MemoryStore.VolatileStore) =
435-
Equinox.MemoryStore.MemoryStoreCategory(store, Category, Events.codec, Fold.fold, Fold.initial)
477+
Equinox.MemoryStore.MemoryStoreCategory(store, Stream.Category, Events.codec, Fold.fold, Fold.initial)
436478
```
437479

438480
Typically that binding module can live with your test helpers rather than
@@ -479,7 +521,7 @@ events on a given category of stream:
479521
tests and used to parameterize the Category's storage configuration._.
480522
Sometimes named `apply`)
481523

482-
- `interpret: (context/command etc ->) 'state -> event' list` or `decide: (context/command etc ->) 'state -> 'result*'event list`: responsible for _Deciding_ (in an [idempotent](https://en.wikipedia.org/wiki/Idempotence) manner) how the intention represented by `context/command` should be mapped with regard to the provided `state` in terms of:
524+
- `interpret: (context/command etc ->) 'state -> 'event[]` or `decide: (context/command etc ->) 'state -> 'result * 'event[]`: responsible for _Deciding_ (in an [idempotent](https://en.wikipedia.org/wiki/Idempotence) manner) how the intention represented by `context/command` should be mapped with regard to the provided `state` in terms of:
483525
a) the `'events` that should be written to the stream to record the decision
484526
b) (for the `'result` in the `decide` signature) any response to be returned to the invoker (NB returning a result likely represents a violation of the [CQS](https://en.wikipedia.org/wiki/Command%E2%80%93query_separation) and/or CQRS principles, [see Synchronous Query in the Glossary](#glossary))
485527

@@ -574,10 +616,10 @@ type Command =
574616
| Remove of itemId: int
575617
576618
let interpret command state =
577-
let has id = state |> List.exits (is id)
619+
let has id = state |> List.exists (is id)
578620
match command with
579-
| Add item -> if has item.id then [] else [Added item]
580-
| Remove id -> if has id then [Removed id] else []
621+
| Add item -> if has item.id then [||] else [| Added item |]
622+
| Remove id -> if has id then [| Removed id |] else [||]
581623
582624
(*
583625
* Optional: Snapshot/Unfold-related functions to allow establish state
@@ -710,15 +752,15 @@ follow!
710752

711753
```fsharp
712754
type Equinox.Decider(...) =
713-
StoreIntegration
755+
714756
// Run interpret function with present state, retrying with Optimistic Concurrency
715-
member _.Transact(interpret: State -> Event list): Async<unit>
757+
member _.Transact(interpret: 'state -> 'event[]): Async<unit>
716758
717759
// Run decide function with present state, retrying with Optimistic Concurrency, yielding Result on exit
718-
member _.Transact(decide: State -> Result*Event list): Async<Result>
760+
member _.Transact(decide: 'state -> 'result * 'event[]): Async<'result>
719761
720762
// Runs a Null Flow that simply yields a `projection` of `Context.State`
721-
member _.Query(projection: State -> View): Async<View>
763+
member _.Query(projection: 'state -> 'view): Async<'view>
722764
```
723765

724766
### Favorites walkthrough
@@ -789,8 +831,8 @@ type Command =
789831
| Remove of string
790832
let interpret command state =
791833
match command with
792-
| Add sku -> if state |> List.contains sku then [] else [Added sku]
793-
| Remove sku -> if state |> List.contains sku |> not then [] else [Removed sku]
834+
| Add sku -> if state |> List.contains sku then [||] else [| Added sku |]
835+
| Remove sku -> if state |> List.contains sku |> not then [||] else [| Removed sku |]
794836
```
795837

796838
Command handling should almost invariably be implemented in an
@@ -1006,13 +1048,13 @@ let fold = Array.fold evolve
10061048
type Command = Add of Todo | Update of Todo | Delete of id: int | Clear
10071049
let interpret c (state: State) =
10081050
match c with
1009-
| Add value -> [Added { value with id = state.nextId }]
1051+
| Add value -> [| Added { value with id = state.nextId } |]
10101052
| Update value ->
10111053
match state.items |> List.tryFind (function { id = id } -> id = value.id) with
1012-
| Some current when current <> value -> [Updated value]
1013-
| _ -> []
1014-
| Delete id -> if state.items |> List.exists (fun x -> x.id = id) then [Deleted id] else []
1015-
| Clear -> if state.items |> List.isEmpty then [] else [Cleared]
1054+
| Some current when current <> value -> [| Updated value |]
1055+
| _ -> [||]
1056+
| Delete id -> if state.items |> List.exists (fun x -> x.id = id) then [| Deleted id |] else [||]
1057+
| Clear -> if state.items |> List.isEmpty then [||] else [| Cleared |]
10161058
```
10171059

10181060
- Note `Add` does not adhere to the normal idempotency constraint, being
@@ -1130,7 +1172,7 @@ In this case, the Decision Process is `interpret`ing the _Command_ in the
11301172
context of a `'state`.
11311173

11321174
The function signature is:
1133-
`let interpret (context, command, args) state: Events.Event list`
1175+
`let interpret (context, command, args) state: Events.Event[]`
11341176

11351177
Note the `'state` is the last parameter; it's computed and supplied by the
11361178
Equinox Flow.
@@ -1147,12 +1189,12 @@ conflicting write have taken place since the loading of the state_
11471189

11481190
```fsharp
11491191
1150-
let interpret (context, command) state: Events.Event list =
1192+
let interpret (context, command) state: Events.Event[] =
11511193
match tryCommand context command state with
11521194
| None ->
1153-
[] // not relevant / already in effect
1195+
[||] // not relevant / already in effect
11541196
| Some eventDetails -> // accepted, mapped to event details record
1155-
[Event.HandledCommand eventDetails]
1197+
[| Events.HandledCommand eventDetails |]
11561198
11571199
type Service internal (resolve: ClientId -> Equinox.Decider<Events.Event, Fold.State>)
11581200
@@ -1180,20 +1222,20 @@ signature: you're both potentially emitting events and yielding an outcome or
11801222
projecting some of the 'state'.
11811223

11821224
In this case, the signature is: `let decide (context, command, args) state:
1183-
'result * Events.Event list`
1225+
'result * Events.Event[]`
11841226

1185-
Note that the return value is a _tuple_ of `('result,Event list):
1227+
Note that the return value is a _tuple_ of `('result, Events.Event[])`:
11861228
- the `fst` element is returned from `decider.Transact`
11871229
- the `snd` element of the tuple represents the events (if any) that should
1188-
represent the state change implied by the request.with
1230+
represent the state change implied by the request.
11891231

11901232
Note if the decision function yields events, and a conflict is detected, the
11911233
flow may result in the `decide` function being rerun with the conflicting state
11921234
until either no events are emitted, or there were on further conflicting writes
11931235
supplied by competing writers.
11941236

11951237
```fsharp
1196-
let decide (context, command) state: int * Events.Event list =
1238+
let decide (context, command) state: int * Events.Event[] =
11971239
// ... if `snd` contains event, they are written
11981240
// `fst` (an `int` in this instance) is returned as the outcome to the caller
11991241
@@ -1259,7 +1301,7 @@ let validateInterpret contextAndOrArgsAndOrCommand state =
12591301
let validateIdempotent contextAndOrArgsAndOrCommand state' =
12601302
let events' = interpret contextAndOrArgsAndOrCommand state'
12611303
match events' with
1262-
| [|] -> ()
1304+
| [||] -> ()
12631305
// TODO add clauses to validate edge cases that should still generate events on a re-run
12641306
| xs -> failwithf "Not idempotent; Generated %A in response to %A" xs contextAndOrArgsAndOrCommand
12651307
```
@@ -1317,7 +1359,7 @@ type Service internal (resolve: CartId -> Equinox.Decider<Events.Event, Fold.Sta
13171359
13181360
member _.Run(cartId, optimistic, commands: Command seq, ?prepare): Async<Fold.State> =
13191361
let decider = resolve cartId
1320-
let opt = if optimistic then Equinox.AnyCachedValue else Equinox.RequireLoad
1362+
let opt = if optimistic then Equinox.LoadOption.AnyCachedValue else Equinox.LoadOption.RequireLoad
13211363
decider.Transact(fun state -> async {
13221364
match prepare with None -> () | Some prep -> do! prep
13231365
return interpretMany Fold.fold (Seq.map interpret commands) state }, opt)
@@ -1379,7 +1421,7 @@ type Accumulator<'event, 'state>(fold: 'state -> 'event[] -> 'state, originState
13791421
type Service ... =
13801422
member _.Run(cartId, optimistic, commands: Command seq, ?prepare): Async<Fold.State> =
13811423
let decider = resolve cartId
1382-
let opt = if optimistic then Equinox.AnyCachedValue else Equinox.RequireLoad
1424+
let opt = if optimistic then Equinox.LoadOption.AnyCachedValue else Equinox.LoadOption.RequireLoad
13831425
decider.Transact(fun state -> async {
13841426
match prepare with None -> () | Some prep -> do! prep
13851427
let acc = Accumulator(Fold.fold, state)
@@ -1453,11 +1495,8 @@ Key aspects relevant to the Equinox programming model:
14531495
- In general, EventStore provides excellent caching and performance
14541496
characteristics intrinsically by virtue of its design
14551497

1456-
- Projections can be managed by either tailing streams (including the synthetic
1457-
`$all` stream) or using the Projections facility - there's no obvious reason
1458-
to wrap it, aside from being able to uniformly target CosmosDB (i.e. one
1459-
could build an `Equinox.EventStore.Projection` library and an `eqx project
1460-
stats es` with very little code).
1498+
- Projections can be managed by the `Propulsion.EventStoreDb` library; there is also
1499+
an `eqx project stats es` feature).
14611500

14621501
- In general event streams should be considered append only, with no mutations
14631502
or deletes
@@ -2339,7 +2378,7 @@ For Domain Events in an event-sourced model, their permanence and immutability i
23392378

23402379
It should be noted with regard to such requirements:
23412380
- EventStoreDB does not present any APIs for mutation of events, though deleting events is a fully supported operation (although that can be restricted). Rewrites are typically approached by doing an offline database rebuild.
2342-
- `Equinox.Cosmos` and `Equinox.CosmosStore` include support for pruning events (only) from the head of a stream. Obviously, there's nothing stopping you deleting or altering the Batch documents out of band via the underlying CosmosDB APIs directly (Note however that the semantics of document ordering within a logical partition means its strongly advised not to mutate any event Batch documents as this will cause their ordering to become incorrect relative to other events, invalidating a key tenet that Change Feed Processors rely on).
2381+
- `Equinox.CosmosStore` includes support for pruning events (only) from the head of a stream. Obviously, there's nothing stopping you deleting or altering the Batch documents out of band via the underlying CosmosDB APIs directly (Note however that the semantics of document ordering within a logical partition means its strongly advised not to mutate any event Batch documents as this will cause their ordering to become incorrect relative to other events, invalidating a key tenet that Change Feed Processors rely on).
23432382

23442383
### Growth handling strategies
23452384

samples/Infrastructure/Services.fs

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ type Store(store) =
2121
CosmosStore.CosmosStoreCategory<'event,'state,_>(store, name, codec.ToJsonElementCodec(), fold, initial, accessStrategy, caching)
2222
| Store.Config.Dynamo (store, caching, unfolds) ->
2323
let accessStrategy = if unfolds then DynamoStore.AccessStrategy.Snapshot snapshot else DynamoStore.AccessStrategy.Unoptimized
24-
DynamoStore.DynamoStoreCategory<'event,'state,_>(store, name, FsCodec.Deflate.EncodeTryDeflate codec, fold, initial, accessStrategy, caching)
24+
DynamoStore.DynamoStoreCategory<'event,'state,_>(store, name, FsCodec.Compression.EncodeTryCompress codec, fold, initial, accessStrategy, caching)
2525
| Store.Config.Es (context, caching, unfolds) ->
2626
let accessStrategy = if unfolds then EventStoreDb.AccessStrategy.RollingSnapshots snapshot else EventStoreDb.AccessStrategy.Unoptimized
2727
EventStoreDb.EventStoreCategory<'event,'state,_>(context, name, codec, fold, initial, accessStrategy, caching)

samples/Infrastructure/Store.fs

+1-1
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ module Dynamo =
211211

212212
let config (log : ILogger) (cache, unfolds) (a : Arguments) =
213213
a.Connector.LogConfiguration(log)
214-
let client = a.Connector.CreateDynamoDbClient() |> DynamoStoreClient
214+
let client = a.Connector.CreateDynamoStoreClient()
215215
let context = DynamoStoreContext(client, a.Table, maxBytes = a.TipMaxBytes, queryMaxItems = a.QueryMaxItems,
216216
?tipMaxEvents = a.TipMaxEvents, ?archiveTableName = a.ArchiveTable)
217217
context.LogConfiguration(log, "Main", a.Table, ?archiveTableName = a.ArchiveTable)

samples/Store/Domain/Domain.fsproj

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
<ItemGroup>
1818
<PackageReference Include="FSharp.Core" Version="6.0.7" ExcludeAssets="contentfiles" />
1919

20-
<PackageReference Include="FsCodec.NewtonsoftJson" Version="3.0.0-rc.10.1" />
21-
<PackageReference Include="FsCodec.SystemTextJson" Version="3.0.0-rc.10.1" />
20+
<PackageReference Include="FsCodec.NewtonsoftJson" Version="3.0.0-rc.13" />
21+
<PackageReference Include="FsCodec.SystemTextJson" Version="3.0.0-rc.13" />
2222

2323
<ProjectReference Include="..\..\..\src\Equinox\Equinox.fsproj" />
2424
</ItemGroup>

samples/Tutorial/Tutorial.fsproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828

2929
<ItemGroup>
3030
<PackageReference Include="MinVer" Version="4.2.0" PrivateAssets="All" />
31-
<PackageReference Include="FsCodec.SystemTextJson" Version="3.0.0-rc.10.1" />
31+
<PackageReference Include="FsCodec.SystemTextJson" Version="3.0.0-rc.13" />
3232
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
3333
<PackageReference Include="Serilog.Sinks.Seq" Version="5.2.0" />
3434
</ItemGroup>

samples/Tutorial/Upload.fs

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ let decide (value : UploadId) (state : Fold.State) : Choice<UploadId,UploadId> *
4747
| None -> Choice1Of2 value, [| Events.IdAssigned { value = value} |]
4848
| Some value -> Choice2Of2 value, [||]
4949

50-
type Service internal (resolve : CompanyId * PurchaseOrderId -> Equinox.Decider<Events.Event, Fold.State>) =
50+
type Service internal (resolve: struct (CompanyId * PurchaseOrderId) -> Equinox.Decider<Events.Event, Fold.State>) =
5151

5252
member _.Sync(companyId, purchaseOrderId, value) : Async<Choice<UploadId,UploadId>> =
5353
let decider = resolve (companyId, purchaseOrderId)

src/Equinox.CosmosStore/CosmosStore.fs

+2-2
Original file line numberDiff line numberDiff line change
@@ -1347,8 +1347,8 @@ type CosmosStoreCategory<'event, 'state, 'req> =
13471347
match access with
13481348
| AccessStrategy.Unoptimized -> (fun _ -> false), false, Choice1Of3 ()
13491349
| AccessStrategy.LatestKnownEvent -> (fun _ -> true), true, Choice2Of3 (fun events _ -> events |> Array.last |> Array.singleton)
1350-
| AccessStrategy.Snapshot (isOrigin, toSnapshot) -> isOrigin, true, Choice2Of3 (fun _ state -> toSnapshot state |> Array.singleton)
1351-
| AccessStrategy.MultiSnapshot (isOrigin, unfold) -> isOrigin, true, Choice2Of3 (fun _ state -> unfold state)
1350+
| AccessStrategy.Snapshot (isOrigin, toSnapshot) -> isOrigin, true, Choice2Of3 (fun _ -> toSnapshot >> Array.singleton)
1351+
| AccessStrategy.MultiSnapshot (isOrigin, unfold) -> isOrigin, true, Choice2Of3 (fun _ -> unfold)
13521352
| AccessStrategy.RollingState toSnapshot -> (fun _ -> true), true, Choice3Of3 (fun _ state -> Array.empty, toSnapshot state |> Array.singleton)
13531353
| AccessStrategy.Custom (isOrigin, transmute) -> isOrigin, true, Choice3Of3 transmute
13541354
{ inherit Equinox.Category<'event, 'state, 'req>(name,

0 commit comments

Comments
 (0)